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 = [ dependencies = [
"dacite>=1.9.2", "dacite>=1.9.2",
"jinja2>=3.1.6", "jinja2>=3.1.6",
"markdown>=3.8.2",
"marko>=2.1.4", "marko>=2.1.4",
"pygments>=2.19.1", "pygments>=2.19.1",
"pygments-ashen>=0.1.3", "pygments-ashen>=0.1.3",
"pygments-kakoune>=0.1.0", "pygments-kakoune>=0.1.0",
"pymdown-extensions>=10.16",
"python-dateutil>=2.9.0.post0", "python-dateutil>=2.9.0.post0",
"python-frontmatter>=1.1.0", "python-frontmatter>=1.1.0",
"rich>=14.0.0", "rich>=14.0.0",

View file

@ -1,3 +1,4 @@
from markdown import Markdown
from rich import print from rich import print
from typing import Any, override from typing import Any, override
from pathlib import Path from pathlib import Path
@ -8,6 +9,15 @@ from marko.parser import Parser
from zona.config import ZonaConfig from zona.config import ZonaConfig
from zona.layout import Layout 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 import highlight
from pygments.lexers import get_lexer_by_name, TextLexer from pygments.lexers import get_lexer_by_name, TextLexer
from pygments.formatters import HtmlFormatter from pygments.formatters import HtmlFormatter
@ -19,7 +29,7 @@ from zona.log import get_logger
logger = get_logger() logger = get_logger()
class ZonaRenderer(HTMLRenderer): class ZonaLinkProcessor(Treeprocessor):
def __init__( def __init__(
self, self,
config: ZonaConfig | None, config: ZonaConfig | None,
@ -40,73 +50,36 @@ class ZonaRenderer(HTMLRenderer):
self.config: ZonaConfig | None = config self.config: ZonaConfig | None = config
@override @override
def render_link(self, element: Link): def run(self, root: etree.Element):
href = element.dest for element in root.iter("a"):
assert isinstance(href, str) href = element.get("href")
if self.resolve: if not href:
cur = Path(href) continue
_href = href if self.resolve:
if href.startswith("/"): cur = Path(href)
# resolve relative to content root _href = href
resolved = ( if href.startswith("/"):
self.layout.content / cur.relative_to("/") # resolve relative to content root
).resolve() resolved = (
else: self.layout.content / cur.relative_to("/")
# treat as relative link and try to resolve ).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}"
)
else: else:
logger.debug( # treat as relative link and try to resolve
f"Warning: resolved path {resolved} not found in item map" resolved = (self.source.parent / cur).resolve()
) # only substitute if link points to an actual file
body: Any = self.render_children(element) if resolved.exists():
return f'<a href="{href}" target="_blank">{body}</a>' item = self.item_map.get(resolved)
if item:
# TODO: image compression/dithering? href = util.normalize_url(item.url)
@override element.set("href", href)
def render_image(self, element: Image): logger.debug(
assert self.config f"Link in file {self.source}: {_href} resolved to {href}"
if not self.config.markdown.image_labels: )
return super().render_image(element) else:
# get label text from children logger.debug(
text = self.render_children(element) f"Warning: resolved path {resolved} not found in item map"
title = element.title or "" )
caption = f"<small>{text}</small>" if text else "" element.set("target", "_blank")
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>"
)
def get_formatter(config: ZonaConfig): def get_formatter(config: ZonaConfig):
@ -125,26 +98,42 @@ def md_to_html(
layout: Layout | None = None, layout: Layout | None = None,
item_map: dict[Path, Item] | None = None, item_map: dict[Path, Item] | None = None,
) -> str: ) -> str:
if resolve_links and ( if config:
source is None or layout is None or item_map is None md = Markdown(
): extensions=[
raise TypeError( CodeHiliteExtension(
"md_to_html() missing source and ctx when resolve_links is true" 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() else:
ast = parser.parse(content) md = Markdown()
renderer = ZonaRenderer( if resolve_links:
config, if source is None or layout is None or item_map is None:
resolve_links, raise TypeError(
source, "md_to_html() missing source and ctx when resolve_links is true"
layout=layout, )
item_map=item_map, md.treeprocessors.register(
) item=ZonaLinkProcessor(
return renderer.render(ast) config, resolve_links, source, layout, item_map
),
name="zona_links",
priority=15,
)
return md.convert(content)
def get_style_defs(config: ZonaConfig) -> str: def get_style_defs(config: ZonaConfig) -> str:
formatter = get_formatter(config) formatter = get_formatter(config)
defs = formatter.get_style_defs("pre.code-block code") defs = formatter.get_style_defs(".codehilite")
assert isinstance(defs, str) assert isinstance(defs, str)
return defs 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" }, { 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]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "3.0.0" 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" }, { 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]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.1" version = "8.4.1"
@ -398,10 +420,12 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "dacite" }, { name = "dacite" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "markdown" },
{ name = "marko" }, { name = "marko" },
{ name = "pygments" }, { name = "pygments" },
{ name = "pygments-ashen" }, { name = "pygments-ashen" },
{ name = "pygments-kakoune" }, { name = "pygments-kakoune" },
{ name = "pymdown-extensions" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "python-frontmatter" }, { name = "python-frontmatter" },
{ name = "rich" }, { name = "rich" },
@ -421,10 +445,12 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "dacite", specifier = ">=1.9.2" }, { name = "dacite", specifier = ">=1.9.2" },
{ name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", specifier = ">=3.1.6" },
{ name = "markdown", specifier = ">=3.8.2" },
{ name = "marko", specifier = ">=2.1.4" }, { name = "marko", specifier = ">=2.1.4" },
{ name = "pygments", specifier = ">=2.19.1" }, { name = "pygments", specifier = ">=2.19.1" },
{ name = "pygments-ashen", specifier = ">=0.1.3" }, { name = "pygments-ashen", specifier = ">=0.1.3" },
{ name = "pygments-kakoune", specifier = ">=0.1.0" }, { name = "pygments-kakoune", specifier = ">=0.1.0" },
{ name = "pymdown-extensions", specifier = ">=10.16" },
{ name = "python-dateutil", specifier = ">=2.9.0.post0" }, { name = "python-dateutil", specifier = ">=2.9.0.post0" },
{ name = "python-frontmatter", specifier = ">=1.1.0" }, { name = "python-frontmatter", specifier = ">=1.1.0" },
{ name = "rich", specifier = ">=14.0.0" }, { name = "rich", specifier = ">=14.0.0" },