migrate to python-markdown
This commit is contained in:
parent
9f38b16d0c
commit
9b4e18d607
3 changed files with 100 additions and 83 deletions
|
@ -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",
|
||||
|
|
|
@ -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
26
uv.lock
generated
|
@ -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" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue