Add shared/sx/html_to_sx.py (HTMLParser-based HTML→sx converter) and update lexical_to_sx.py so HTML cards, markdown cards, and captions all produce native sx expressions instead of opaque HTML strings. - ~kg-html now wraps native sx children (editor can identify the block) - New ~kg-md component for markdown card blocks - Captions are sx expressions, not escaped HTML strings - kg_cards.sx: replace (raw! caption) with direct caption rendering - sx-editor.js: htmlToSx() via DOMParser, serializeInline for captions, _childrenSx for ~kg-html/~kg-md, new kg-md edit UI - Migration script (blog/scripts/migrate_sx_html.py) to re-convert stored sx_content from lexical source Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Re-convert sx_content from lexical JSON to eliminate ~kg-html wrappers and
|
|
raw caption strings.
|
|
|
|
The updated lexical_to_sx converter now produces native sx expressions instead
|
|
of (1) wrapping HTML/markdown cards in (~kg-html :html "...") and (2) storing
|
|
captions as escaped HTML strings. This script re-runs the conversion on all
|
|
posts that already have sx_content, overwriting the old output.
|
|
|
|
Usage:
|
|
cd blog && python3 scripts/migrate_sx_html.py [--dry-run]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
|
|
from sqlalchemy import select, and_
|
|
|
|
|
|
async def migrate(dry_run: bool = False) -> int:
|
|
from shared.db.session import get_session
|
|
from models.ghost_content import Post
|
|
from bp.blog.ghost.lexical_to_sx import lexical_to_sx
|
|
|
|
converted = 0
|
|
skipped = 0
|
|
errors = 0
|
|
|
|
async with get_session() as sess:
|
|
# All posts with lexical content (whether or not sx_content exists)
|
|
stmt = select(Post).where(
|
|
and_(
|
|
Post.lexical.isnot(None),
|
|
Post.lexical != "",
|
|
)
|
|
)
|
|
result = await sess.execute(stmt)
|
|
posts = result.scalars().all()
|
|
|
|
print(f"Found {len(posts)} posts with lexical content")
|
|
|
|
for post in posts:
|
|
try:
|
|
new_sx = lexical_to_sx(post.lexical)
|
|
if post.sx_content == new_sx:
|
|
skipped += 1
|
|
continue
|
|
|
|
if dry_run:
|
|
old_has_kg = "~kg-html" in (post.sx_content or "")
|
|
old_has_raw = "raw! caption" in (post.sx_content or "")
|
|
markers = []
|
|
if old_has_kg:
|
|
markers.append("~kg-html")
|
|
if old_has_raw:
|
|
markers.append("raw-caption")
|
|
tag = f" [{', '.join(markers)}]" if markers else ""
|
|
print(f" [DRY RUN] {post.slug}: {len(new_sx)} chars{tag}")
|
|
else:
|
|
post.sx_content = new_sx
|
|
print(f" Converted: {post.slug} ({len(new_sx)} chars)")
|
|
converted += 1
|
|
except Exception as e:
|
|
print(f" ERROR: {post.slug}: {e}", file=sys.stderr)
|
|
errors += 1
|
|
|
|
if not dry_run and converted:
|
|
await sess.commit()
|
|
|
|
print(f"\nDone: {converted} converted, {skipped} unchanged, {errors} errors")
|
|
return converted
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Re-convert sx_content to eliminate ~kg-html and raw captions"
|
|
)
|
|
parser.add_argument("--dry-run", action="store_true",
|
|
help="Preview changes without writing to database")
|
|
args = parser.parse_args()
|
|
|
|
asyncio.run(migrate(dry_run=args.dry_run))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|