Coverage for src/lektor_ng/markdown/__init__.py: 97%
72 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 13:49 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 13:49 +0000
1import sys
2import warnings
3from collections.abc import Hashable
4from importlib import metadata
5from typing import TYPE_CHECKING, Any
6from weakref import ref as weakref
8from markupsafe import Markup
10from lektor_ng.markdown.controller import (
11 ControllerCache,
12 FieldOptions,
13 MarkdownController,
14 Meta,
15 RenderResult,
16)
17from lektor_ng.sourceobj import SourceObject
18from lektor_ng.utils import DeprecatedWarning, deprecated
20if TYPE_CHECKING: # pragma: no cover
21 from lektor.environment import Environment
23controller_class: type[MarkdownController]
26MISTUNE_VERSION = metadata.version("mistune")
27if MISTUNE_VERSION.startswith("0."):
28 from lektor_ng.markdown.mistune0 import MarkdownController0 as controller_class
29elif MISTUNE_VERSION.startswith("2."):
30 from lektor_ng.markdown.mistune2 import MarkdownController2 as controller_class
31else: # pragma: no cover
32 raise ImportError("Unsupported version of mistune")
35get_controller = ControllerCache(controller_class)
38class Markdown:
39 def __init__(self, source: str, record: SourceObject | None, field_options: FieldOptions) -> None:
40 self.source = source
41 self.__record = weakref(record) if record is not None else None
42 self.__field_options = field_options
43 self.__cache: dict[Hashable, RenderResult] = {}
45 def __bool__(self) -> bool:
46 return bool(self.source)
48 __nonzero__ = __bool__
50 @property
51 def record(self) -> SourceObject:
52 ref = self.__record
53 if ref is None:
54 return None
55 record = ref()
56 if record is None:
57 raise RuntimeError("Record has gone away")
58 return record
60 def __render(self) -> RenderResult:
61 # When the markdown instance is attached to a cached object we
62 # can end up in the situation where, e.g., the base_url has
63 # changed from the time we were put into the cache to the time
64 # where we got referenced by something elsewhere. Since this
65 # affects the processing of relative links, in that case we
66 # need to re-process our markdown.
67 controller = get_controller()
68 key = controller.get_cache_key()
69 result = self.__cache.get(key) if key is not None else None
70 if result is None:
71 result = controller.render(self.source, self.record, self.__field_options)
72 if key is not None:
73 self.__cache[key] = result
74 return result
76 @property
77 def meta(self) -> Meta:
78 return self.__render().meta
80 @property
81 def html(self) -> Markup:
82 return Markup(self.__render().html)
84 def __getitem__(self, name: str) -> Any:
85 return self.meta[name]
87 def __str__(self) -> str:
88 return self.__render().html
90 def __html__(self) -> Markup:
91 return self.html
94# Deprecated methods and attributes follow
95#
96# It is hoped that these are sufficient that most plugins that extend
97# the markdown processing will continue to work (so long as mistune is
98# pinned to 0.x).
99_deprecated_moved_to_submodule = {"escape", "ImprovedRenderer", "MarkdownConfig"}
102def __getattr__(name):
103 """Access to ``escape``, ``ImprovedRenderer``, and ``MarkdownConfig``.
105 These are imported from from either our .mistune0 or .mistune2 modules,
106 as appropriate for the installed version of mistune.
107 """
108 if name not in _deprecated_moved_to_submodule:
109 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
110 message = DeprecatedWarning(f"{__name__}.{name}", version="3.4.0")
111 warnings.warn(message, stacklevel=2)
112 mistune_module = sys.modules[controller_class.__module__]
113 return getattr(mistune_module, name)
116def __dir__():
117 return sorted(set(globals().keys()) | _deprecated_moved_to_submodule)
120@deprecated(version="3.4.0")
121def make_markdown(env: "Environment") -> Any: # (Environment) -> mistune.Markdown
122 return get_controller(env).make_parser()
125@deprecated(version="3.4.0")
126def markdown_to_html(text: str, record: SourceObject, field_options: FieldOptions) -> RenderResult:
127 return get_controller().render(text, record, field_options)