1from __future__ import annotations
2
3from collections.abc import Iterable, MutableMapping
4from collections.abc import MutableMapping as MutableMappingABC
5from pathlib import Path
6from typing import TYPE_CHECKING, Any, Callable, TypedDict, cast
7
8if TYPE_CHECKING:
9 from typing_extensions import NotRequired
10
11
12EnvType = MutableMapping[str, Any] # note: could use TypeAlias in python 3.10
13"""Type for the environment sandbox used in parsing and rendering,
14which stores mutable variables for use by plugins and rules.
15"""
16
17
18class OptionsType(TypedDict):
19 """Options for parsing."""
20
21 maxNesting: int
22 """Internal protection, recursion limit."""
23 html: bool
24 """Enable HTML tags in source."""
25 linkify: bool
26 """Enable autoconversion of URL-like texts to links."""
27 typographer: bool
28 """Enable smartquotes and replacements."""
29 quotes: str
30 """Quote characters."""
31 xhtmlOut: bool
32 """Use '/' to close single tags (<br />)."""
33 breaks: bool
34 """Convert newlines in paragraphs into <br>."""
35 langPrefix: str
36 """CSS language prefix for fenced blocks."""
37 highlight: Callable[[str, str, str], str] | None
38 """Highlighter function: (content, lang, attrs) -> str."""
39 store_labels: NotRequired[bool]
40 """Store link label in link/image token's metadata (under Token.meta['label']).
41
42 This is a Python only option, and is intended for the use of round-trip parsing.
43 """
44
45
46class PresetType(TypedDict):
47 """Preset configuration for markdown-it."""
48
49 options: OptionsType
50 """Options for parsing."""
51 components: MutableMapping[str, MutableMapping[str, list[str]]]
52 """Components for parsing and rendering."""
53
54
55class OptionsDict(MutableMappingABC): # type: ignore
56 """A dictionary, with attribute access to core markdownit configuration options."""
57
58 # Note: ideally we would probably just remove attribute access entirely,
59 # but we keep it for backwards compatibility.
60
61 def __init__(self, options: OptionsType) -> None:
62 self._options = cast(OptionsType, dict(options))
63
64 def __getitem__(self, key: str) -> Any:
65 return self._options[key] # type: ignore[literal-required]
66
67 def __setitem__(self, key: str, value: Any) -> None:
68 self._options[key] = value # type: ignore[literal-required]
69
70 def __delitem__(self, key: str) -> None:
71 del self._options[key] # type: ignore
72
73 def __iter__(self) -> Iterable[str]: # type: ignore
74 return iter(self._options)
75
76 def __len__(self) -> int:
77 return len(self._options)
78
79 def __repr__(self) -> str:
80 return repr(self._options)
81
82 def __str__(self) -> str:
83 return str(self._options)
84
85 @property
86 def maxNesting(self) -> int:
87 """Internal protection, recursion limit."""
88 return self._options["maxNesting"]
89
90 @maxNesting.setter
91 def maxNesting(self, value: int) -> None:
92 self._options["maxNesting"] = value
93
94 @property
95 def html(self) -> bool:
96 """Enable HTML tags in source."""
97 return self._options["html"]
98
99 @html.setter
100 def html(self, value: bool) -> None:
101 self._options["html"] = value
102
103 @property
104 def linkify(self) -> bool:
105 """Enable autoconversion of URL-like texts to links."""
106 return self._options["linkify"]
107
108 @linkify.setter
109 def linkify(self, value: bool) -> None:
110 self._options["linkify"] = value
111
112 @property
113 def typographer(self) -> bool:
114 """Enable smartquotes and replacements."""
115 return self._options["typographer"]
116
117 @typographer.setter
118 def typographer(self, value: bool) -> None:
119 self._options["typographer"] = value
120
121 @property
122 def quotes(self) -> str:
123 """Quote characters."""
124 return self._options["quotes"]
125
126 @quotes.setter
127 def quotes(self, value: str) -> None:
128 self._options["quotes"] = value
129
130 @property
131 def xhtmlOut(self) -> bool:
132 """Use '/' to close single tags (<br />)."""
133 return self._options["xhtmlOut"]
134
135 @xhtmlOut.setter
136 def xhtmlOut(self, value: bool) -> None:
137 self._options["xhtmlOut"] = value
138
139 @property
140 def breaks(self) -> bool:
141 """Convert newlines in paragraphs into <br>."""
142 return self._options["breaks"]
143
144 @breaks.setter
145 def breaks(self, value: bool) -> None:
146 self._options["breaks"] = value
147
148 @property
149 def langPrefix(self) -> str:
150 """CSS language prefix for fenced blocks."""
151 return self._options["langPrefix"]
152
153 @langPrefix.setter
154 def langPrefix(self, value: str) -> None:
155 self._options["langPrefix"] = value
156
157 @property
158 def highlight(self) -> Callable[[str, str, str], str] | None:
159 """Highlighter function: (content, langName, langAttrs) -> escaped HTML."""
160 return self._options["highlight"]
161
162 @highlight.setter
163 def highlight(self, value: Callable[[str, str, str], str] | None) -> None:
164 self._options["highlight"] = value
165
166
167def read_fixture_file(path: str | Path) -> list[list[Any]]:
168 text = Path(path).read_text(encoding="utf-8")
169 tests = []
170 section = 0
171 last_pos = 0
172 lines = text.splitlines(keepends=True)
173 for i in range(len(lines)):
174 if lines[i].rstrip() == ".":
175 if section == 0:
176 tests.append([i, lines[i - 1].strip()])
177 section = 1
178 elif section == 1:
179 tests[-1].append("".join(lines[last_pos + 1 : i]))
180 section = 2
181 elif section == 2:
182 tests[-1].append("".join(lines[last_pos + 1 : i]))
183 section = 0
184
185 last_pos = i
186 return tests