mathspec 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mathspec/__init__.py +59 -0
- mathspec/__main__.py +87 -0
- mathspec/_expression_parser.py +436 -0
- mathspec/_expression_resolver.py +739 -0
- mathspec/_sealed.py +62 -0
- mathspec/_where_parser.py +288 -0
- mathspec/_where_resolver.py +745 -0
- mathspec/_yaml.py +141 -0
- mathspec/advice.py +79 -0
- mathspec/boundedness.py +168 -0
- mathspec/degree.py +209 -0
- mathspec/dimensions.py +377 -0
- mathspec/errors.py +105 -0
- mathspec/exclusivity.py +587 -0
- mathspec/expansion.py +124 -0
- mathspec/lowering.py +302 -0
- mathspec/model.py +905 -0
- mathspec/operators.py +235 -0
- mathspec/piecewise.py +505 -0
- mathspec/program.py +1514 -0
- mathspec/py.typed +0 -0
- mathspec/resolution.py +475 -0
- mathspec/separability.py +185 -0
- mathspec/sos.py +159 -0
- mathspec/typesetting/README.md +110 -0
- mathspec/typesetting/__init__.py +223 -0
- mathspec/typesetting/format.py +273 -0
- mathspec/typesetting/latex.py +139 -0
- mathspec/typesetting/legend.py +286 -0
- mathspec/typesetting/markdown.py +105 -0
- mathspec/typesetting/symbols.py +310 -0
- mathspec/typesetting/typst.py +147 -0
- mathspec/typesetting/walk.py +941 -0
- mathspec/validation.py +338 -0
- mathspec-0.1.0.dist-info/METADATA +247 -0
- mathspec-0.1.0.dist-info/RECORD +38 -0
- mathspec-0.1.0.dist-info/WHEEL +4 -0
- mathspec-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""The legend: the glossary of symbols, and a note for each notation the equations use.
|
|
6
|
+
|
|
7
|
+
What the equations use is read off the program before anything prints
|
|
8
|
+
(:func:`notice`), so the legend explains every symbol the walk will print and
|
|
9
|
+
nothing the walk decides is asked of it twice.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import TYPE_CHECKING, Literal
|
|
16
|
+
|
|
17
|
+
from mathspec.program import (
|
|
18
|
+
CountComparison,
|
|
19
|
+
DimensionComparison,
|
|
20
|
+
DimensionPosition,
|
|
21
|
+
ExpressionComparison,
|
|
22
|
+
PulledBackPredicate,
|
|
23
|
+
Translate,
|
|
24
|
+
TranslatedPredicate,
|
|
25
|
+
WindowSum,
|
|
26
|
+
walk_regions,
|
|
27
|
+
)
|
|
28
|
+
from mathspec.typesetting.format import Entry
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Iterable
|
|
32
|
+
|
|
33
|
+
from mathspec.program import Expression, Mask, Program, RelationDeclaration
|
|
34
|
+
from mathspec.typesetting.format import Format, OperatorName
|
|
35
|
+
from mathspec.typesetting.symbols import Symbols
|
|
36
|
+
|
|
37
|
+
#: What a translation does with the row the shift vacates. Three policies get
|
|
38
|
+
#: three spellings because they are three different equations at the boundary.
|
|
39
|
+
TranslationPolicy = Literal['plain', 'wrap', 'edge']
|
|
40
|
+
|
|
41
|
+
#: The positional forms an equation can print, each of which the legend explains once.
|
|
42
|
+
PositionForm = Literal['plain', 'grouped', 'from_end']
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def policy_of(node: Translate | WindowSum) -> TranslationPolicy:
|
|
46
|
+
"""Which translation symbol *node* prints with: cyclic, filled at the edge, or plain."""
|
|
47
|
+
if node.wrap:
|
|
48
|
+
return 'wrap'
|
|
49
|
+
if isinstance(node, Translate) and node.fill is not None:
|
|
50
|
+
return 'edge'
|
|
51
|
+
return 'plain'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Noticed:
|
|
56
|
+
"""What the equations use that the legend has to explain.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
policies: Each translation policy some ``shift`` or ``sum_back`` prints.
|
|
60
|
+
grouped: Whether some translation is counted inside a relation's group.
|
|
61
|
+
positions: Each form a ``position()`` prints in.
|
|
62
|
+
numeric_coordinates: Each dimension whose index is compared against a
|
|
63
|
+
number, where "position 3" and "the coordinate 3" are both
|
|
64
|
+
readings of a line.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
policies: frozenset[TranslationPolicy]
|
|
68
|
+
grouped: bool
|
|
69
|
+
positions: frozenset[PositionForm]
|
|
70
|
+
numeric_coordinates: frozenset[str]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def notice(program: Program) -> Noticed:
|
|
74
|
+
"""What printing *program* uses, read off every tree the walk prints."""
|
|
75
|
+
policies: set[TranslationPolicy] = set()
|
|
76
|
+
grouped = False
|
|
77
|
+
positions: set[PositionForm] = set()
|
|
78
|
+
numeric: set[str] = set()
|
|
79
|
+
|
|
80
|
+
def expressions(*roots: Expression) -> None:
|
|
81
|
+
nonlocal grouped
|
|
82
|
+
for node, regions in walk_regions(*roots):
|
|
83
|
+
if isinstance(node, WindowSum) or (isinstance(node, Translate) and node.offset != 0):
|
|
84
|
+
policies.add(policy_of(node))
|
|
85
|
+
grouped = grouped or node.partition is not None
|
|
86
|
+
for region in regions:
|
|
87
|
+
masks(region)
|
|
88
|
+
|
|
89
|
+
seen: set[int] = set()
|
|
90
|
+
|
|
91
|
+
def masks(mask: Mask) -> None:
|
|
92
|
+
if id(mask) in seen:
|
|
93
|
+
return
|
|
94
|
+
seen.add(id(mask))
|
|
95
|
+
for atom in mask.atoms:
|
|
96
|
+
if isinstance(atom, DimensionComparison) and isinstance(atom.value, int | float):
|
|
97
|
+
numeric.add(atom.name)
|
|
98
|
+
elif isinstance(atom, DimensionPosition):
|
|
99
|
+
positions.add('grouped' if atom.partition is not None else 'plain')
|
|
100
|
+
if atom.position < 0:
|
|
101
|
+
positions.add('from_end')
|
|
102
|
+
elif isinstance(atom, ExpressionComparison):
|
|
103
|
+
expressions(atom.left, atom.right)
|
|
104
|
+
elif isinstance(atom, CountComparison):
|
|
105
|
+
masks(atom.predicate)
|
|
106
|
+
elif isinstance(atom, TranslatedPredicate | PulledBackPredicate):
|
|
107
|
+
masks(atom.operand)
|
|
108
|
+
|
|
109
|
+
expressions(*program.roots)
|
|
110
|
+
for entry in program.expressions.values():
|
|
111
|
+
expressions(entry.expression)
|
|
112
|
+
for curve in program.piecewise.values():
|
|
113
|
+
expressions(*(link.expression for link in curve.links))
|
|
114
|
+
for declaration in (*program.constraints.values(), *program.variables.values()):
|
|
115
|
+
if declaration.where is not None:
|
|
116
|
+
masks(declaration.where)
|
|
117
|
+
for assumption in program.assumptions.values():
|
|
118
|
+
masks(assumption.predicate)
|
|
119
|
+
if assumption.where is not None:
|
|
120
|
+
masks(assumption.where)
|
|
121
|
+
return Noticed(frozenset(policies), grouped, frozenset(positions), frozenset(numeric))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class Legend:
|
|
126
|
+
"""The glossary and the notes, spelled with one program's symbols in one format."""
|
|
127
|
+
|
|
128
|
+
program: Program
|
|
129
|
+
symbols: Symbols
|
|
130
|
+
format: Format
|
|
131
|
+
|
|
132
|
+
def _op(self, name: OperatorName) -> str:
|
|
133
|
+
return self.format.operators[name]
|
|
134
|
+
|
|
135
|
+
def glossaries(self, noticed: Noticed, defined: Iterable[str]) -> list[tuple[str, list[Entry]]]:
|
|
136
|
+
"""The sets, parameters, variables and definitions, each with its symbol, its dims and its description.
|
|
137
|
+
|
|
138
|
+
*defined* names the expressions that print under their own symbol, so
|
|
139
|
+
a legend row stands exactly where a symbol does.
|
|
140
|
+
"""
|
|
141
|
+
fmt, program = self.format, self.program
|
|
142
|
+
sets = [
|
|
143
|
+
self._entry(
|
|
144
|
+
self.symbols.set[d],
|
|
145
|
+
f'index {fmt.math(self.symbols.index[d])} {fmt.dash} {fmt.mono(d)}{self._coords(d, noticed)}',
|
|
146
|
+
block.description,
|
|
147
|
+
)
|
|
148
|
+
for d, block in program.dimensions.items()
|
|
149
|
+
]
|
|
150
|
+
parameters = [
|
|
151
|
+
self._entry(self.symbols.name[p], f'{fmt.mono(p)}{self._over(list(block.dims))}', block.description)
|
|
152
|
+
for p, block in program.parameters.items()
|
|
153
|
+
]
|
|
154
|
+
variables = [
|
|
155
|
+
self._entry(self.symbols.name[v], f'{fmt.mono(v)}{self._over(list(block.dims))}', block.description)
|
|
156
|
+
for v, block in program.variables.items()
|
|
157
|
+
]
|
|
158
|
+
shown = set(defined)
|
|
159
|
+
definitions = [
|
|
160
|
+
self._entry(self.symbols.name[e], f'{fmt.mono(e)}{self._over(list(block.dims))}', block.description)
|
|
161
|
+
for e, block in program.expressions.items()
|
|
162
|
+
if e in shown
|
|
163
|
+
]
|
|
164
|
+
groups = (('Sets', sets), ('Parameters', parameters), ('Variables', variables), ('Definitions', definitions))
|
|
165
|
+
return [(title, entries) for title, entries in groups if entries]
|
|
166
|
+
|
|
167
|
+
def _entry(self, symbol: str, what: str, description: str | None) -> Entry:
|
|
168
|
+
meaning = f'{what} {self.format.dash} {self.format.escape(description)}' if description else what
|
|
169
|
+
return Entry(symbol, meaning)
|
|
170
|
+
|
|
171
|
+
def _over(self, dims: list[str]) -> str:
|
|
172
|
+
if not dims:
|
|
173
|
+
return ' (scalar)'
|
|
174
|
+
product = self.format.joined([self.symbols.set[d] for d in dims], self._op('times'))
|
|
175
|
+
return f' over {self.format.math(product)}'
|
|
176
|
+
|
|
177
|
+
def _signature(self, name: str, lk: RelationDeclaration) -> str:
|
|
178
|
+
"""A relation in the legend: a function from its key sets to its value sets, or a relation inside the product."""
|
|
179
|
+
|
|
180
|
+
def product(roles: Iterable[str]) -> str:
|
|
181
|
+
return self.format.joined([self.symbols.set[lk.dim(r)] for r in roles], self._op('times'))
|
|
182
|
+
|
|
183
|
+
if lk.values:
|
|
184
|
+
return f'{self.format.upright(name)}: {product(lk.key)} {self._op("maps_to")} {product(lk.values)}'
|
|
185
|
+
return f'{self.format.upright(name)} {self._op("subset_of")} {product(lk.roles)}'
|
|
186
|
+
|
|
187
|
+
def _coords(self, dim: str, noticed: Noticed) -> str:
|
|
188
|
+
"""The dimension's carried structure: each relation with a column over it, as the map or relation it is.
|
|
189
|
+
|
|
190
|
+
The dtype is named only where an equation compared the index against a
|
|
191
|
+
number, the one place "position 3" and "the coordinate 3" are both
|
|
192
|
+
readings of a line.
|
|
193
|
+
"""
|
|
194
|
+
carried = self.program.relations_of(dim)
|
|
195
|
+
clauses = []
|
|
196
|
+
if dim in noticed.numeric_coordinates:
|
|
197
|
+
clauses.append(f' ({self.format.mono(self.program.dimensions[dim].dtype)} coordinates)')
|
|
198
|
+
if carried:
|
|
199
|
+
maps = self.format.joined([self._signature(c, lk) for c, lk in carried.items()], '')
|
|
200
|
+
clauses.append(f' with {self.format.math(maps)}')
|
|
201
|
+
return ''.join(clauses)
|
|
202
|
+
|
|
203
|
+
def convention_notes(self) -> list[str]:
|
|
204
|
+
"""What the two faces mean, with the model's own symbols.
|
|
205
|
+
|
|
206
|
+
Only where the model has both, and quoting only derived symbols: a
|
|
207
|
+
table is the author's to write, so a symbol it supplies is not one this
|
|
208
|
+
note governs.
|
|
209
|
+
"""
|
|
210
|
+
derived = [
|
|
211
|
+
next((n for n in names if n not in self.symbols.overridden), None)
|
|
212
|
+
for names in (self.program.parameters, self.program.variables)
|
|
213
|
+
]
|
|
214
|
+
if not all(derived):
|
|
215
|
+
return []
|
|
216
|
+
given, chosen = (self.format.math(self.symbols.name[n]) for n in derived if n is not None)
|
|
217
|
+
return [
|
|
218
|
+
f'Upright is what the model is given {self.format.dash} a parameter such as {given}, a coordinate '
|
|
219
|
+
f'map, a label {self.format.dash} and italic is what the solver chooses, such as {chosen}. '
|
|
220
|
+
f'An index is italic too, being what a quantifier chooses, and a set is script.'
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
def translation_notes(self, noticed: Noticed) -> list[str]:
|
|
224
|
+
"""A sentence for each translation symbol the model printed; plain ``t-k`` needs none."""
|
|
225
|
+
notes = []
|
|
226
|
+
if 'wrap' in noticed.policies:
|
|
227
|
+
cyclic = self.format.math(f't {self._op("cyclic_minus")} k')
|
|
228
|
+
notes.append(
|
|
229
|
+
f'{cyclic} denotes cyclic translation: index {self.format.math("t-k")} taken modulo the size of '
|
|
230
|
+
f'the dimension ({self.format.mono("roll")}). Plain {self.format.math("t-k")} '
|
|
231
|
+
f'({self.format.mono("shift")}) has no wraparound {self.format.dash} terms translated past '
|
|
232
|
+
f'the edge are simply absent.'
|
|
233
|
+
)
|
|
234
|
+
if 'edge' in noticed.policies:
|
|
235
|
+
filled = self.format.math(f't {self.format.subscript(self._op("edge_minus"), ["v"])} k')
|
|
236
|
+
notes.append(
|
|
237
|
+
f'{filled} denotes translation with {self.format.math("v")} standing where index '
|
|
238
|
+
f'{self.format.math("t-k")} leaves the dimension ({self.format.mono("shift(edge=v)")}), so the row '
|
|
239
|
+
f'at that boundary is built and carries {self.format.math("v")} rather than being dropped.'
|
|
240
|
+
)
|
|
241
|
+
if noticed.grouped:
|
|
242
|
+
applied = self.format.apply(self.format.upright('relation'), 't')
|
|
243
|
+
counted = self.format.math(f't {self.format.superscript(self._op("cyclic_minus"), applied)} k')
|
|
244
|
+
note = (
|
|
245
|
+
f'{counted} denotes a translation counted inside the group a relation puts {self.format.math("t")} '
|
|
246
|
+
f'in ({self.format.mono("shift(by=relation)")}), so a term never crosses out of its own group.'
|
|
247
|
+
)
|
|
248
|
+
if 'edge' in noticed.policies:
|
|
249
|
+
both = self.format.superscript(self.format.subscript(self._op('edge_minus'), ['v']), applied)
|
|
250
|
+
note += (
|
|
251
|
+
f' The two modifiers take different slots {self.format.dash} the group above, the fill '
|
|
252
|
+
f'below {self.format.dash} so {self.format.math(f"t {both} k")} is both at once.'
|
|
253
|
+
)
|
|
254
|
+
notes.append(note)
|
|
255
|
+
return notes
|
|
256
|
+
|
|
257
|
+
def position_notes(self, noticed: Noticed) -> list[str]:
|
|
258
|
+
"""A sentence for each positional symbol the model printed; the first says which of ``pos(t)`` and ``t`` is the position."""
|
|
259
|
+
notes = []
|
|
260
|
+
if noticed.positions:
|
|
261
|
+
index = self.format.math('t')
|
|
262
|
+
place = self.format.math(self.format.apply(self._op('position'), 't'))
|
|
263
|
+
dash = self.format.dash
|
|
264
|
+
notes.append(
|
|
265
|
+
f"{place} denotes where index {index} sits along its dimension's own order {dash} the order "
|
|
266
|
+
f'{self.format.mono("shift")} steps along, not the order labels sort in {dash} counted from '
|
|
267
|
+
f'{self.format.math("0")}. The index itself stays the coordinate, so {index} compares against '
|
|
268
|
+
f'labels and {place} against positions.'
|
|
269
|
+
)
|
|
270
|
+
if 'grouped' in noticed.positions:
|
|
271
|
+
applied = self.format.apply(self.format.upright('relation'), 't')
|
|
272
|
+
grouped = self.format.math(self.format.apply(self.format.subscript(self._op('position'), [applied]), 't'))
|
|
273
|
+
group = self.format.math(self.format.subscript(self.format.script('T'), [applied]))
|
|
274
|
+
notes.append(
|
|
275
|
+
f'{grouped} counts within the group a relation puts {self.format.math("t")} in: the subscript names '
|
|
276
|
+
f'the map, {group} is the group it lands in, and that group has a first position of its own.'
|
|
277
|
+
)
|
|
278
|
+
if 'from_end' in noticed.positions:
|
|
279
|
+
size = self.format.cardinality(self.format.script('T'))
|
|
280
|
+
last = self.format.math(f'{size} {self._op("minus")} 1')
|
|
281
|
+
notes.append(
|
|
282
|
+
f'{self.format.math(size)} denotes the size of the set being counted along, and a position '
|
|
283
|
+
f'counted from the end prints against it {self.format.dash} {last} is the last position, one '
|
|
284
|
+
f'less than the size because the first is {self.format.math("0")}.'
|
|
285
|
+
)
|
|
286
|
+
return notes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: mathspec Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
r"""GitHub-flavoured Markdown. GitHub renders math with MathJax, so the math is :class:`LatexFormat`'s and only the document layer differs.
|
|
6
|
+
|
|
7
|
+
Both delimiters are the verbatim pair — ``$`…`$`` inline and a ``math`` fence
|
|
8
|
+
for a block — because GitHub runs Markdown's escape pass *inside* a ``$…$``
|
|
9
|
+
span, stripping the backslash from every escape TeX needs: ``\mathrm{gen\_bus}``
|
|
10
|
+
reached MathJax as ``\mathrm{gen_bus}``, a subscript, and ``\{0, 1\}`` as a
|
|
11
|
+
group with no braces. The verbatim pair hands the span over untouched, so the
|
|
12
|
+
math this prints is the math :mod:`~mathspec.typesetting.latex` prints, and
|
|
13
|
+
stays what every other MathJax and KaTeX reads too.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from typing import TYPE_CHECKING, ClassVar, override
|
|
20
|
+
|
|
21
|
+
from mathspec.typesetting.format import escaped, paragraphs
|
|
22
|
+
from mathspec.typesetting.latex import LatexFormat
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from mathspec.typesetting.format import Entry, Line
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: What Markdown reads as markup inside a paragraph, each escaped by a leading
|
|
29
|
+
#: backslash — CommonMark lets any ASCII punctuation be. ``$`` is GitHub's
|
|
30
|
+
#: inline math, ``~`` its strikethrough; the pipe is a table cell's to escape.
|
|
31
|
+
_SPECIALS = frozenset('\\`*_[]<>~$#')
|
|
32
|
+
|
|
33
|
+
#: A list, quote or heading marker is markup only at the start of a line.
|
|
34
|
+
_LEADING_MARKER = re.compile(r'(^|\n)([-+>]|\d+[.)])(?= )')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _escape(text: str) -> str:
|
|
38
|
+
escaped = ''.join(f'\\{c}' if c in _SPECIALS else c for c in text)
|
|
39
|
+
return _LEADING_MARKER.sub(r'\1\\\2', escaped)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cell(text: str) -> str:
|
|
43
|
+
"""*text* as one table cell: a pipe would end it and a newline would end the row."""
|
|
44
|
+
return text.replace('|', r'\|').replace('\n', ' ')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MarkdownFormat(LatexFormat):
|
|
48
|
+
"""See :class:`mathspec.typesetting.format.Format`. Math is LaTeX's; prose is not."""
|
|
49
|
+
|
|
50
|
+
#: The character, not TeX's ligature: no Markdown renderer this output
|
|
51
|
+
#: is aimed at substitutes one, so `---` reaches the reader as three
|
|
52
|
+
#: hyphens in the middle of a legend row.
|
|
53
|
+
dash: ClassVar[str] = '\N{EM DASH}'
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
def mono(self, text: str) -> str:
|
|
57
|
+
"""A backtick span — this one lands in prose, not in math."""
|
|
58
|
+
return f'`{text}`'
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
def escape(self, prose: str) -> str:
|
|
62
|
+
"""Prose with every special escaped, as the other two notations do, and a backtick span kept as the code span it is."""
|
|
63
|
+
return escaped(prose, _escape, self.mono)
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
def math(self, expression: str) -> str:
|
|
67
|
+
r"""Bare math in prose, in the verbatim inline pair rather than ``$…$``."""
|
|
68
|
+
return f'$`{expression}`$'
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
def equations(self, lines: list[Line], *, numbered: bool) -> str:
|
|
72
|
+
"""One fenced block per equation, with the name *outside* the math.
|
|
73
|
+
|
|
74
|
+
A label is the name the file gives the line rather than a symbol, so it
|
|
75
|
+
sets as the code span prose has and math does not, and ``aligned`` has
|
|
76
|
+
nothing to line up across one-equation blocks. ``numbered`` is ignored:
|
|
77
|
+
``aligned`` cannot carry numbers.
|
|
78
|
+
"""
|
|
79
|
+
del numbered
|
|
80
|
+
blocks = []
|
|
81
|
+
for line in lines:
|
|
82
|
+
block = f'```math\n{self.equation(line)}\n```'
|
|
83
|
+
if line.label:
|
|
84
|
+
block = f'**{self.mono(line.label)}**\n\n{block}'
|
|
85
|
+
blocks.append(block)
|
|
86
|
+
return '\n\n'.join(blocks)
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
def glossary(self, entries: list[Entry]) -> str:
|
|
90
|
+
rows = '\n'.join(f'| {_cell(self.math(e.symbol))} | {_cell(e.meaning)} |' for e in entries)
|
|
91
|
+
return f'| Symbol | Meaning |\n|---|---|\n{rows}'
|
|
92
|
+
|
|
93
|
+
@override
|
|
94
|
+
def section(self, title: str, body: str) -> str:
|
|
95
|
+
return f'#### {title}\n\n{body}'
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def note(self, text: str) -> str:
|
|
99
|
+
return text
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
def document(self, blocks: list[str], *, standalone: bool) -> str:
|
|
103
|
+
"""No preamble: ``standalone`` adds the heading a fragment is pasted under."""
|
|
104
|
+
body = paragraphs(blocks)
|
|
105
|
+
return f'## The math\n\n{body}' if standalone else body
|