#!/usr/bin/env python
# /// script
# requires-python = ">=3.10"
# dependencies = ["mistune>=3.0"]
# ///
"""Convert a Markdown file into a Mews page (Mews Profile 0.1).
Parsing is CommonMark (mistune) plus pipe tables; uv installs mistune
from the script header on the first run. The converter maps the tree onto
what mews-0.1.dtd permits: ATX and setext headings, paragraphs, emphasis,
strong, code, links, images (with titles), hard breaks, fenced and
indented code blocks, block quotes, ordered and unordered lists (with
start), and pipe tables. Raw HTML is escaped as text and table alignment
markers are dropped, because Mews has neither. Tables are recognized only
at the top level, matching the DTD, which allows them nowhere else.
Run with: uv run md2mews.py page.md -o page.html --lang en
"""
import argparse
import html
import sys
from pathlib import Path
import mistune
VERSION = "0.1"
DOCTYPE = (
''
)
VARIANTS = ("mews-warm", "mews-cool", "mews-green", "mews-mono")
MARKDOWN = mistune.create_markdown(renderer="ast", plugins=["table"])
def esc(text):
return html.escape(text)
def plain(nodes):
"""Text content of inline nodes, used for image alt text."""
parts = []
for node in nodes:
if node["type"] in ("text", "codespan", "inline_html"):
parts.append(node.get("raw", ""))
else:
parts.append(plain(node.get("children", [])))
return "".join(parts)
def inline_nodes(nodes):
out = []
for node in nodes:
kind, attrs = node["type"], node.get("attrs", {})
if kind == "text":
out.append(esc(node["raw"]))
elif kind == "codespan":
out.append(f"{esc(node['raw'])}")
elif kind == "emphasis":
out.append(f"{inline_nodes(node['children'])}")
elif kind == "strong":
out.append(f"{inline_nodes(node['children'])}")
elif kind == "link":
title = f' title="{esc(attrs["title"])}"' if attrs.get("title") else ""
out.append(f'{inline_nodes(node["children"])}')
elif kind == "image":
title = f' title="{esc(attrs["title"])}"' if attrs.get("title") else ""
alt = esc(plain(node["children"]))
out.append(f'')
elif kind == "linebreak":
out.append("
")
elif kind == "softbreak":
out.append(" ")
elif kind == "inline_html":
out.append(esc(node["raw"]))
else:
sys.exit(f"error: unsupported inline markup: {kind}")
return "".join(out)
def render_block(node, level, no_table):
"""Render one block node into indented lines.
no_table is set inside block quotes and list items, where the Mews DTD
does not allow a table.
"""
pad = " " * level
kind, attrs = node["type"], node.get("attrs", {})
if kind in ("paragraph", "block_text"):
return [f"{pad}
{inline_nodes(node['children'])}
" if kind == "paragraph" else f"{pad}{inline_nodes(node['children'])}"] if kind == "heading": tag = f"h{attrs['level']}" return [f"{pad}<{tag}>{inline_nodes(node['children'])}{tag}>"] if kind == "block_quote": return [f"{pad}", *render_blocks(node["children"], level + 1, True), f"{pad}"] if kind == "block_code": return [f"{pad}
{esc(node['raw'].rstrip(chr(10)))}"]
if kind == "thematic_break":
return [f"{pad}{esc(node['raw'].strip())}
"] if kind == "list": tag = "ol" if attrs["ordered"] else "ul" start = f' start="{attrs["start"]}"' if attrs.get("start") not in (None, 1) else "" out = [f"{pad}<{tag}{start}>"] for item in node["children"]: out.extend(render_item(item, level + 1)) out.append(f"{pad}{tag}>") return out if kind == "table": if no_table: sys.exit("error: a table inside a quote or list item cannot be a Mews page") return render_table(node, pad) sys.exit(f"error: unsupported Markdown block: {kind}") def render_item(node, level): pad = " " * level children = node["children"] if len(children) == 1 and children[0]["type"] == "block_text": return [f"{pad}