Skip to content

Commit 409fa6c

Browse files
committed
transforms: Enhance the marker DSL to allow setting properties
This allows `.path[key]=val` annotations. For now Alectryon only supports one property, `[lang]`, which specifies which lexer to use to highlight a particular fragment of code (though technically, since +- polarities get translated into the "enabled" property, specifying `[enabled]=…` would also work to enable the display of a particular piece of code).
1 parent 5f42d75 commit 409fa6c

File tree

6 files changed

+81
-53
lines changed

6 files changed

+81
-53
lines changed

alectryon/core.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import Any, Iterator, List, Tuple, Union, NamedTuple
2222

2323
from collections import namedtuple, defaultdict
24+
from contextlib import contextmanager
2425
from shlex import quote
2526
from shutil import which
2627
from subprocess import Popen, PIPE, check_output
@@ -62,14 +63,14 @@ class Enriched():
6263
def __new__(cls, *args, **kwargs):
6364
if len(args) < len(getattr(super(), "_fields", ())):
6465
# Don't repeat fields given by position (it breaks pickle & deepcopy)
65-
kwargs = {"ids": [], "markers": [], "flags": {}, **kwargs}
66+
kwargs = {"ids": [], "markers": [], "props": {}, **kwargs}
6667
return super().__new__(cls, *args, **kwargs)
6768

6869
def _enrich(nt):
6970
# LATER: Use dataclass + multiple inheritance; change `ids` and `markers` to
7071
# mutable `id` and `marker` fields.
7172
name = "Rich" + nt.__name__
72-
fields = nt._fields + ("ids", "markers", "flags")
73+
fields = nt._fields + ("ids", "markers", "props")
7374
# Using ``type`` this way ensures compatibility with pickling
7475
return type(name, (Enriched, namedtuple(name, fields)),
7576
{"__slots__": ()})
@@ -96,15 +97,28 @@ def __call__(self, prefix):
9697
self.counters[prefix] += 1
9798
return self.stem + prefix + b16(self.counters[prefix])
9899

99-
class Backend: # pylint: disable=no-member
100+
@contextmanager
101+
def nullctx():
102+
yield
103+
104+
class Backend:
105+
def __init__(self, highlighter):
106+
self.highlighter = highlighter
107+
100108
def gen_sentence(self, s): raise NotImplementedError()
101109
def gen_hyp(self, hyp): raise NotImplementedError()
102110
def gen_goal(self, goal): raise NotImplementedError()
103111
def gen_message(self, message): raise NotImplementedError()
104112
def highlight(self, s): raise NotImplementedError()
105113
def gen_names(self, names): raise NotImplementedError()
114+
def gen_code(self, code): raise NotImplementedError()
106115
def gen_txt(self, s): raise NotImplementedError()
107116

117+
def highlight_enriched(self, obj):
118+
lang = obj.props.get("lang")
119+
with self.highlighter.override(lang=lang) if lang else nullctx():
120+
return self.highlight(obj.contents)
121+
108122
def _gen_any(self, obj):
109123
if isinstance(obj, (Text, RichSentence)):
110124
self.gen_sentence(obj)
@@ -115,7 +129,7 @@ def _gen_any(self, obj):
115129
elif isinstance(obj, RichMessage):
116130
self.gen_message(obj)
117131
elif isinstance(obj, RichCode):
118-
self.highlight(obj.contents)
132+
self.gen_code(obj)
119133
elif isinstance(obj, Names):
120134
self.gen_names(obj)
121135
elif isinstance(obj, str):

alectryon/docutils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ def _find_mref_target(cls, path, ios, last_io):
485485
# Unfold to ensure visibility (but only if search succeeded)
486486
if sentence.annots.unfold is None:
487487
sentence.annots.unfold = True
488-
goal.flags.setdefault("unfold", True)
488+
goal.props.setdefault("unfold", True)
489489
if "type" in path:
490490
return hyp.type
491491
if "body" in path:
@@ -581,7 +581,7 @@ def replace_one_io(cls, node, fmt, generator):
581581

582582
@classmethod
583583
def replace_one_quote(cls, node, fmt, generator):
584-
target = transforms.strip_ids_and_flags(deepcopy(node.details["target"]))
584+
target = transforms.strip_ids_and_props(deepcopy(node.details["target"]), {"enabled"})
585585
cls.replace_one(node, fmt, node.details["path"], generator.gen_part,
586586
target, inline=node.details["inline"])
587587

alectryon/html.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020

21-
from contextlib import contextmanager
2221
from functools import wraps
2322
from os import path
2423
import pickle
@@ -27,7 +26,7 @@
2726
from dominate.util import text as txt
2827

2928
from . import transforms, GENERATOR
30-
from .core import b16, Gensym, Backend, Text, RichSentence, Goals, Messages, Asset
29+
from .core import b16, nullctx, Gensym, Backend, Text, RichSentence, Goals, Messages, Asset
3130

3231
_SELF_PATH = path.dirname(path.realpath(__file__))
3332

@@ -90,13 +89,9 @@ def _fn(self, *args, **kwargs):
9089
return _fn
9190
return _deduplicate
9291

93-
@contextmanager
94-
def nullctx():
95-
yield
96-
9792
class HtmlGenerator(Backend):
9893
def __init__(self, highlighter, gensym_stem="", minify=False):
99-
self.highlighter = highlighter
94+
super().__init__(highlighter)
10095
self.gensym = None if minify else Gensym(gensym_stem + "-" if gensym_stem else "")
10196
self.minify, self.backrefs = minify, ({} if minify else None)
10297

@@ -110,9 +105,9 @@ def gen_clickable(toggle, cls, *contents):
110105
def highlight(self, s):
111106
return self.highlighter(s)
112107

113-
def gen_code(self, dom, code, **kwargs):
114-
with dom(self.highlight(code.contents), **kwargs):
115-
self.gen_mrefs(code)
108+
def gen_code(self, code):
109+
self.highlight_enriched(code)
110+
self.gen_mrefs(code)
116111

117112
@staticmethod
118113
def gen_names(names):
@@ -126,10 +121,12 @@ def gen_hyp(self, hyp):
126121
if hyp.body:
127122
with tags.span(cls="hyp-body"):
128123
tags.b(":= ")
129-
self.gen_code(tags.span, hyp.body)
124+
with tags.span():
125+
self.gen_code(hyp.body)
130126
with tags.span(cls="hyp-type"):
131127
tags.b(": ")
132-
self.gen_code(tags.span, hyp.type)
128+
with tags.span():
129+
self.gen_code(hyp.type)
133130
self.gen_mrefs(hyp)
134131

135132
@deduplicate(".goal-hyps")
@@ -141,7 +138,8 @@ def gen_hyps(self, hyps):
141138

142139
@deduplicate(".goal-conclusion")
143140
def gen_ccl(self, conclusion):
144-
self.gen_code(tags.div, conclusion, cls="goal-conclusion")
141+
with tags.div(cls="goal-conclusion"):
142+
self.gen_code(conclusion)
145143

146144
@deduplicate(".alectryon-goal")
147145
def gen_goal(self, goal, toggle=None): # pylint: disable=arguments-differ
@@ -176,7 +174,7 @@ def gen_extra_goals(self, goals):
176174
with tags.div(cls='alectryon-extra-goals'):
177175
for goal in goals:
178176
toggle = goal.hypotheses and \
179-
self.gen_checkbox(goal.flags.get("unfold"),
177+
self.gen_checkbox(goal.props.get("unfold"),
180178
"alectryon-extra-goal-toggle")
181179
self.gen_goal(goal, toggle=toggle)
182180

@@ -190,13 +188,12 @@ def gen_goals(self, goals):
190188

191189
def gen_input(self, fr, toggle):
192190
cls = "alectryon-input" + (" alectryon-failed" if fr.annots.fails else "")
193-
with self.gen_clickable(toggle, cls, self.highlight(fr.input.contents)):
191+
with self.gen_clickable(toggle, cls, self.highlight_enriched(fr.input)):
194192
self.gen_mrefs(fr)
195193
self.gen_mrefs(fr.input)
196194

197195
def gen_message(self, message):
198-
self.highlight(message.contents)
199-
self.gen_mrefs(message)
196+
self.gen_code(message)
200197

201198
@deduplicate(".alectryon-output")
202199
def gen_output(self, fr):

alectryon/latex.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,11 @@ def __getattribute__(self, macro_name):
182182
macros = Macros()
183183

184184
class LatexGenerator(Backend):
185-
def __init__(self, highlighter):
186-
self.highlighter = highlighter
187-
188185
def highlight(self, s):
189186
return [Raw(self.highlighter(s, prefix="", suffix=""), verbatim=True)]
190187

191188
def gen_code(self, code):
192-
with Concat(*self.highlight(code.contents)) as block:
189+
with Concat(*self.highlight_enriched(code)) as block:
193190
self.gen_mrefs(code)
194191
return block
195192

alectryon/pygments.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,6 @@ def highlight_latex(code, lang,
193193

194194
HIGHLIGHTERS = {"html": highlight_html, "latex": highlight_latex}
195195

196-
def make_highlighter(fmt, lang, style=None):
197-
highlighter = HIGHLIGHTERS[fmt]
198-
return lambda s, *args, **kwargs: \
199-
highlighter(s, *args, **{"style": style, "lang": lang, **kwargs}) # type: ignore
200-
201196
@contextmanager
202197
def munged_dict(d, updates):
203198
saved = d.copy()
@@ -207,6 +202,23 @@ def munged_dict(d, updates):
207202
finally:
208203
d.update(saved)
209204

205+
class Highlighter: # LATER: dataclass
206+
def __init__(self, fmt, lang, style=None):
207+
self.kwargs = {"lang": lang, "style": style}
208+
self.highlighter = HIGHLIGHTERS[fmt]
209+
210+
def __call__(self, code, **kwargs):
211+
return self.highlighter(code, **{**self.kwargs, **kwargs})
212+
213+
@contextmanager
214+
def override(self, **kwargs):
215+
assert set(kwargs.keys()) <= {"lang", "style"}
216+
with munged_dict(self.kwargs, kwargs):
217+
yield
218+
219+
def make_highlighter(fmt, lang, style=None):
220+
return Highlighter(fmt, lang, style)
221+
210222
class WarnOnErrorTokenFilter(Filter):
211223
"""Print a warning when the lexer generates an error token."""
212224

alectryon/transforms.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@
3232
RichHypothesis, RichGoal, RichMessage, RichCode, \
3333
Goals, Messages, RichSentence
3434

35-
PathAnnot = namedtuple("PathAnnot", "enabled path")
35+
PathAnnot = namedtuple("PathAnnot", "path key val")
3636

3737
class IOAnnots:
3838
def __init__(self):
3939
self.filters = None
4040
self.unfold = None
4141
self.fails = None
42-
self.paths = []
42+
self.props = []
4343

4444
NO = re.compile("no-")
4545
RE = re.compile("(?P<io>[-a-z]+)")
@@ -77,8 +77,8 @@ def update(self, annot):
7777
raise ValueError("Unknown flag `{}`.".format(flag))
7878
self.filters[flag] = not negated
7979

80-
def update_paths(self, polarity, path):
81-
self.paths.append(PathAnnot(polarity != "-", path))
80+
def update_props(self, path, key, val):
81+
self.props.append(PathAnnot(path, key, val))
8282

8383
@property
8484
def hidden(self):
@@ -93,8 +93,8 @@ def __getitem__(self, key):
9393
return self.filters[key] if self.filters else True
9494

9595
def __repr__(self):
96-
return "IOAnnots(unfold={}, fails={}, filters={}, paths={})".format(
97-
self.unfold, self.fails, self.filters, self.paths)
96+
return "IOAnnots(unfold={}, fails={}, filters={}, props={})".format(
97+
self.unfold, self.fails, self.filters, self.props)
9898

9999
def _enrich_goal(g):
100100
return RichGoal(g.name,
@@ -115,7 +115,11 @@ def enrich_sentences(fragments):
115115
yield fr
116116

117117
ISOLATED = r"(?:\s|\A){}(?=\s|\Z)"
118-
POLARIZED_PATH_SEGMENT = r"(?P<polarity>[-+]?)(?P<path>(?:{})+)".format(
118+
POLARIZED_PATH_SEGMENT = r"""
119+
(?P<polarity>[-+]?)
120+
(?P<path>(?:{})+)
121+
(?:\[(?P<key>[a-z]+)\]=(?P<value>[A-Za-z0-9_]+))?
122+
""".format(
119123
markers.MARKER_PATH_SEGMENT.pattern)
120124

121125
ONE_IO_FLAG = r"(?:{}|{})".format(
@@ -151,11 +155,13 @@ def _parse_path(path):
151155

152156
def _update_io_flags(annots, flags_str, regex):
153157
for mannot in regex.finditer(flags_str):
154-
io, path, polarity = mannot.group("io", "path", "polarity")
158+
io, path, polarity, key, val = mannot.group("io", "path", "polarity", "key", "value")
155159
if io:
156160
annots.update(io)
157161
else:
158-
annots.update_paths(polarity, _parse_path(path))
162+
if not key:
163+
key, val = "enabled", polarity != "-"
164+
annots.update_props(_parse_path(path), key, val)
159165

160166
def read_io_flags(annots, flags_str):
161167
_update_io_flags(annots, flags_str, ONE_IO_FLAG_RE)
@@ -218,7 +224,7 @@ def _find_marked(sentence, path):
218224
else:
219225
yield sentence
220226

221-
def _find_flagged(sentence):
227+
def _find_hidden_by_annots(sentence):
222228
annots = sentence.annots
223229
if not annots["in"]:
224230
yield sentence.input
@@ -233,31 +239,32 @@ def _find_flagged(sentence):
233239
yield m
234240

235241
def process_io_annots(fragments):
236-
"""Convert IO annotations to "enabled" flags."""
242+
"""Convert IO annotations to pre-object properties."""
237243
for fr in fragments:
238244
if isinstance(fr, RichSentence):
239-
for (enabled, path) in fr.annots.paths:
245+
for (path, key, val) in fr.annots.props:
240246
for obj in _find_marked(fr, path):
241-
obj.flags["enabled"] = enabled
242-
for obj in _find_flagged(fr):
243-
obj.flags["enabled"] = False
247+
obj.props[key] = val
248+
249+
for obj in _find_hidden_by_annots(fr):
250+
obj.props["enabled"] = False
244251

245252
for g in fragment_goals(fr):
246253
if not any(_enabled(h) for h in g.hypotheses) and not _enabled(g.conclusion):
247-
g.flags["enabled"] = False
254+
g.props["enabled"] = False
248255

249256
any_output = any(_enabled(o) for os in fr.outputs for o in _output_objects(os))
250257

251258
if not _enabled(fr.input) and not any_output:
252-
fr.flags["enabled"] = False
259+
fr.props["enabled"] = False
253260

254261
if not fr.annots.unfold and not _enabled(fr.input) and any_output:
255262
MSG = "Cannot show output of {!r} without .in or .unfold."
256263
yield ValueError(MSG.format(fr.input.contents))
257264
yield fr
258265

259266
def _enabled(o):
260-
return o.flags.get("enabled", True)
267+
return o.props.get("enabled", True)
261268

262269
def _commit_enabled(objs):
263270
objs[:] = [o for o in objs if _enabled(o)]
@@ -322,12 +329,13 @@ def _sub_objects(obj):
322329
return ()
323330
assert False
324331

325-
def strip_ids_and_flags(obj):
332+
def strip_ids_and_props(obj, props):
326333
if isinstance(obj, Enriched):
327334
obj.ids.clear() # type: ignore
328-
obj.flags.clear() # type: ignore
335+
for p in props:
336+
obj.props.pop(p, None) # type: ignore
329337
for obj_ in _sub_objects(obj):
330-
strip_ids_and_flags(obj_)
338+
strip_ids_and_props(obj_, props)
331339
return obj
332340

333341
LEADING_BLANKS_RE = re.compile(r'\A([ \t]*(?:\n|\Z))?(.*?)([ \t]*)\Z',

0 commit comments

Comments
 (0)