migrate to python-markdown

This commit is contained in:
Daniel Fichtinger 2025-07-05 00:01:24 -04:00
parent 9f38b16d0c
commit 9b4e18d607
3 changed files with 100 additions and 83 deletions

View file

@ -10,10 +10,12 @@ requires-python = ">=3.12"
dependencies = [
"dacite>=1.9.2",
"jinja2>=3.1.6",
"markdown>=3.8.2",
"marko>=2.1.4",
"pygments>=2.19.1",
"pygments-ashen>=0.1.3",
"pygments-kakoune>=0.1.0",
"pymdown-extensions>=10.16",
"python-dateutil>=2.9.0.post0",
"python-frontmatter>=1.1.0",
"rich>=14.0.0",

View file

@ -1,3 +1,4 @@
from markdown import Markdown
from rich import print
from typing import Any, override
from pathlib import Path
@ -8,6 +9,15 @@ from marko.parser import Parser
from zona.config import ZonaConfig
from zona.layout import Layout
from markdown.treeprocessors import Treeprocessor
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.extra import ExtraExtension
from markdown.extensions.smarty import SmartyExtension
from markdown.extensions.sane_lists import SaneListExtension
from pymdownx.inlinehilite import InlineHiliteExtension
import xml.etree.ElementTree as etree
from pygments import highlight
from pygments.lexers import get_lexer_by_name, TextLexer
from pygments.formatters import HtmlFormatter
@ -19,7 +29,7 @@ from zona.log import get_logger
logger = get_logger()
class ZonaRenderer(HTMLRenderer):
class ZonaLinkProcessor(Treeprocessor):
def __init__(
self,
config: ZonaConfig | None,
@ -40,73 +50,36 @@ class ZonaRenderer(HTMLRenderer):
self.config: ZonaConfig | None = config
@override
def render_link(self, element: Link):
href = element.dest
assert isinstance(href, str)
if self.resolve:
cur = Path(href)
_href = href
if href.startswith("/"):
# resolve relative to content root
resolved = (
self.layout.content / cur.relative_to("/")
).resolve()
else:
# treat as relative link and try to resolve
resolved = (self.source.parent / cur).resolve()
# only substitute if link points to an actual file
if resolved.exists():
item = self.item_map.get(resolved)
if item:
href = util.normalize_url(item.url)
logger.debug(
f"Link in file {self.source}: {_href} resolved to {href}"
)
def run(self, root: etree.Element):
for element in root.iter("a"):
href = element.get("href")
if not href:
continue
if self.resolve:
cur = Path(href)
_href = href
if href.startswith("/"):
# resolve relative to content root
resolved = (
self.layout.content / cur.relative_to("/")
).resolve()
else:
logger.debug(
f"Warning: resolved path {resolved} not found in item map"
)
body: Any = self.render_children(element)
return f'<a href="{href}" target="_blank">{body}</a>'
# TODO: image compression/dithering?
@override
def render_image(self, element: Image):
assert self.config
if not self.config.markdown.image_labels:
return super().render_image(element)
# get label text from children
text = self.render_children(element)
title = element.title or ""
caption = f"<small>{text}</small>" if text else ""
return (
f'<div class="image-container">\n'
# TODO: convert to plaintext and add as alt attribute
f'<img src="{element.dest}" title="{title}">\n'
f"{caption}</div>"
)
@override
def render_fenced_code(self, element: FencedCode):
assert self.config
config = self.config.markdown.syntax_highlighting
if not config.enabled:
return super().render_fenced_code(element)
code = "".join(child.children for child in element.children) # type: ignore
lang = element.lang or "text"
try:
lexer = get_lexer_by_name(lang, stripall=False)
except Exception:
lexer = TextLexer(stripall=False) # type: ignore
formatter = get_formatter(self.config)
highlighted = highlight(code, lexer, formatter) # type: ignore
return (
f'<pre class="code-block language-{lang}">'
f"<code>{highlighted}</code></pre>"
)
# treat as relative link and try to resolve
resolved = (self.source.parent / cur).resolve()
# only substitute if link points to an actual file
if resolved.exists():
item = self.item_map.get(resolved)
if item:
href = util.normalize_url(item.url)
element.set("href", href)
logger.debug(
f"Link in file {self.source}: {_href} resolved to {href}"
)
else:
logger.debug(
f"Warning: resolved path {resolved} not found in item map"
)
element.set("target", "_blank")
def get_formatter(config: ZonaConfig):
@ -125,26 +98,42 @@ def md_to_html(
layout: Layout | None = None,
item_map: dict[Path, Item] | None = None,
) -> str:
if resolve_links and (
source is None or layout is None or item_map is None
):
raise TypeError(
"md_to_html() missing source and ctx when resolve_links is true"
if config:
md = Markdown(
extensions=[
CodeHiliteExtension(
linenums=False,
noclasses=False,
pygments_style=config.markdown.syntax_highlighting.theme,
),
ExtraExtension(),
SmartyExtension(),
"pymdownx.tilde",
"pymdownx.caret",
"pymdownx.smartsymbols",
InlineHiliteExtension(css_class="codehilite"),
SaneListExtension(),
]
)
parser = Parser()
ast = parser.parse(content)
renderer = ZonaRenderer(
config,
resolve_links,
source,
layout=layout,
item_map=item_map,
)
return renderer.render(ast)
else:
md = Markdown()
if resolve_links:
if source is None or layout is None or item_map is None:
raise TypeError(
"md_to_html() missing source and ctx when resolve_links is true"
)
md.treeprocessors.register(
item=ZonaLinkProcessor(
config, resolve_links, source, layout, item_map
),
name="zona_links",
priority=15,
)
return md.convert(content)
def get_style_defs(config: ZonaConfig) -> str:
formatter = get_formatter(config)
defs = formatter.get_style_defs("pre.code-block code")
defs = formatter.get_style_defs(".codehilite")
assert isinstance(defs, str)
return defs

26
uv.lock generated
View file

@ -65,6 +65,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markdown"
version = "3.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@ -200,6 +209,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/e8/ccd661508989a0469ee296909f2d6cae8ac24304f9db9afe706f0ef5b8d5/pygments_kakoune-0.1.0-py3-none-any.whl", hash = "sha256:573a933b61c7c7993f52fd02a04b7a936558da2b5a6690728732f2c0a67221a9", size = 3606, upload-time = "2025-06-30T20:27:30.457Z" },
]
[[package]]
name = "pymdown-extensions"
version = "10.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
@ -398,10 +420,12 @@ source = { editable = "." }
dependencies = [
{ name = "dacite" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "marko" },
{ name = "pygments" },
{ name = "pygments-ashen" },
{ name = "pygments-kakoune" },
{ name = "pymdown-extensions" },
{ name = "python-dateutil" },
{ name = "python-frontmatter" },
{ name = "rich" },
@ -421,10 +445,12 @@ dev = [
requires-dist = [
{ name = "dacite", specifier = ">=1.9.2" },
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "markdown", specifier = ">=3.8.2" },
{ name = "marko", specifier = ">=2.1.4" },
{ name = "pygments", specifier = ">=2.19.1" },
{ name = "pygments-ashen", specifier = ">=0.1.3" },
{ name = "pygments-kakoune", specifier = ">=0.1.0" },
{ name = "pymdown-extensions", specifier = ">=10.16" },
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
{ name = "python-frontmatter", specifier = ">=1.1.0" },
{ name = "rich", specifier = ">=14.0.0" },