sphinxnotes.render 1.0a42__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.
- sphinxnotes/render/__init__.py +93 -0
- sphinxnotes/render/ctx.py +76 -0
- sphinxnotes/render/ctxnodes.py +254 -0
- sphinxnotes/render/data.py +546 -0
- sphinxnotes/render/extractx.py +184 -0
- sphinxnotes/render/markup.py +122 -0
- sphinxnotes/render/meta.py +35 -0
- sphinxnotes/render/pipeline.py +300 -0
- sphinxnotes/render/py.typed +0 -0
- sphinxnotes/render/render.py +60 -0
- sphinxnotes/render/sources.py +129 -0
- sphinxnotes/render/template.py +146 -0
- sphinxnotes/render/utils/__init__.py +211 -0
- sphinxnotes/render/utils/ctxproxy.py +144 -0
- sphinxnotes/render/utils/freestyle.py +93 -0
- sphinxnotes_render-1.0a42.dist-info/METADATA +78 -0
- sphinxnotes_render-1.0a42.dist-info/RECORD +20 -0
- sphinxnotes_render-1.0a42.dist-info/WHEEL +5 -0
- sphinxnotes_render-1.0a42.dist-info/licenses/LICENSE +29 -0
- sphinxnotes_render-1.0a42.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sphinxnotes.render.data
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Context data type definitions.
|
|
6
|
+
|
|
7
|
+
:copyright: Copyright 2025~2026 by the Shengyu Zhang.
|
|
8
|
+
:license: BSD, see LICENSE for details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, asdict, field as dataclass_field
|
|
15
|
+
from ast import literal_eval
|
|
16
|
+
|
|
17
|
+
from .utils import Unpicklable
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from typing import Any, Callable, Generator, Self, Literal
|
|
21
|
+
|
|
22
|
+
# ===================================
|
|
23
|
+
# Basic types: Value, Form, Flag, ...
|
|
24
|
+
# ===================================
|
|
25
|
+
|
|
26
|
+
type PlainValue = bool | int | float | str | object
|
|
27
|
+
type Value = None | PlainValue | list[PlainValue]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ValueWrapper:
|
|
32
|
+
v: Value
|
|
33
|
+
|
|
34
|
+
# TODO: __post_init__ to assert type
|
|
35
|
+
|
|
36
|
+
def as_plain(self) -> PlainValue | None:
|
|
37
|
+
if self.v is None:
|
|
38
|
+
return None
|
|
39
|
+
if isinstance(self.v, list):
|
|
40
|
+
return self.v[0] if len(self.v) else None
|
|
41
|
+
return self.v
|
|
42
|
+
|
|
43
|
+
def as_list(self) -> list[PlainValue]:
|
|
44
|
+
if self.v is None:
|
|
45
|
+
return []
|
|
46
|
+
elif isinstance(self.v, list):
|
|
47
|
+
return [x for x in self.v]
|
|
48
|
+
else:
|
|
49
|
+
return [self.v]
|
|
50
|
+
|
|
51
|
+
def as_str(self) -> str | None:
|
|
52
|
+
v = self.as_plain()
|
|
53
|
+
return self._strify(v) if v is not None else None
|
|
54
|
+
|
|
55
|
+
def as_str_list(self) -> list[str]:
|
|
56
|
+
return [self._strify(x) for x in self.as_list()]
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _strify(v: PlainValue) -> str:
|
|
60
|
+
return REGISTRY.strifys[type(v)](v)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Form:
|
|
65
|
+
"""Defines how to split the string and the container type."""
|
|
66
|
+
|
|
67
|
+
ctype: type
|
|
68
|
+
sep: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Flag:
|
|
73
|
+
name: str
|
|
74
|
+
default: bool
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
type ByOptionStore = Literal['assign', 'append']
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ByOption:
|
|
82
|
+
name: str
|
|
83
|
+
etype: type
|
|
84
|
+
default: Value
|
|
85
|
+
store: ByOptionStore
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ========
|
|
89
|
+
# Registry
|
|
90
|
+
# ========
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _bool_conv(v: str | None) -> bool:
|
|
94
|
+
v = v.lower().strip() if v is not None else None
|
|
95
|
+
if v in ('true', 'yes', '1', 'on', 'y', ''):
|
|
96
|
+
return True
|
|
97
|
+
if v in ('false', 'no', '0', 'off', 'n', None):
|
|
98
|
+
return False
|
|
99
|
+
# Same to :meth:`directives.flag`.
|
|
100
|
+
raise ValueError(f'No argument is allowed; "{v}" supplied')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _str_conv(v: str) -> str:
|
|
104
|
+
try:
|
|
105
|
+
vv = literal_eval(v)
|
|
106
|
+
except (ValueError, SyntaxError):
|
|
107
|
+
return v
|
|
108
|
+
return vv if isinstance(vv, str) else v
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Registry:
|
|
112
|
+
"""Stores supported element types and element forms (containers)."""
|
|
113
|
+
|
|
114
|
+
etypes: dict[str, type]
|
|
115
|
+
ctypes: set[type] = {list, tuple, set}
|
|
116
|
+
convs: dict[type, Callable[[str], PlainValue]]
|
|
117
|
+
strifys: dict[type, Callable[[PlainValue], str]]
|
|
118
|
+
forms: dict[str, Form]
|
|
119
|
+
flags: dict[str, Flag]
|
|
120
|
+
byopts: dict[str, ByOption]
|
|
121
|
+
|
|
122
|
+
_sep_by_option: ByOption
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self.etypes = {}
|
|
126
|
+
self.convs = {}
|
|
127
|
+
self.strifys = {}
|
|
128
|
+
self.forms = {}
|
|
129
|
+
self.flags = {}
|
|
130
|
+
self.byopts = {}
|
|
131
|
+
|
|
132
|
+
# Add builtin types.
|
|
133
|
+
self.add_type('bool', bool, _bool_conv, str, aliases=['flag'])
|
|
134
|
+
self.add_type('int', int, int, strify=str, aliases=['integer'])
|
|
135
|
+
self.add_type('float', float, float, str, aliases=['number', 'num'])
|
|
136
|
+
self.add_type('str', str, _str_conv, str, aliases=['string'])
|
|
137
|
+
|
|
138
|
+
# Add builtin forms.
|
|
139
|
+
self.add_form('list', list, ',')
|
|
140
|
+
self.add_form('lines', list, '\n')
|
|
141
|
+
self.add_form('words', list, ' ')
|
|
142
|
+
self.add_form('set', set, ' ')
|
|
143
|
+
|
|
144
|
+
# Add builtin flags.
|
|
145
|
+
self.add_flag('required', False, aliases=['require', 'req'])
|
|
146
|
+
|
|
147
|
+
# Add builtin by-option.
|
|
148
|
+
self.add_by_option('sep', str, aliases=['separate'])
|
|
149
|
+
# NOTE: the "sep" by-option is a special builtin flag, extract it for
|
|
150
|
+
# later usage.
|
|
151
|
+
self._sep_by_option = self.byopts['sep']
|
|
152
|
+
|
|
153
|
+
# from pprint import pprint
|
|
154
|
+
# pprint(cls.__dict__)
|
|
155
|
+
|
|
156
|
+
def add_type(
|
|
157
|
+
self,
|
|
158
|
+
name: str,
|
|
159
|
+
etype: type,
|
|
160
|
+
conv: Callable[[str], PlainValue],
|
|
161
|
+
strify: Callable[[PlainValue], str],
|
|
162
|
+
aliases: list[str] = [],
|
|
163
|
+
) -> None:
|
|
164
|
+
self.etypes[name] = etype
|
|
165
|
+
self.convs[etype] = conv
|
|
166
|
+
self.strifys[etype] = strify
|
|
167
|
+
|
|
168
|
+
for alias in aliases:
|
|
169
|
+
self.etypes[alias] = etype
|
|
170
|
+
|
|
171
|
+
def add_form(
|
|
172
|
+
self, name: str, ctype: type, sep: str, aliases: list[str] = []
|
|
173
|
+
) -> None:
|
|
174
|
+
if ctype not in self.ctypes:
|
|
175
|
+
raise ValueError(f'Unsupported type: "{ctype}". Available: {self.ctypes}')
|
|
176
|
+
|
|
177
|
+
form = Form(ctype, sep)
|
|
178
|
+
|
|
179
|
+
self.forms[name] = form
|
|
180
|
+
for alias in aliases:
|
|
181
|
+
self.forms[alias] = form
|
|
182
|
+
|
|
183
|
+
def add_flag(
|
|
184
|
+
self, name: str, default: bool = False, aliases: list[str] = []
|
|
185
|
+
) -> None:
|
|
186
|
+
flag = Flag(name, default)
|
|
187
|
+
|
|
188
|
+
self.flags[flag.name] = flag
|
|
189
|
+
for alias in aliases:
|
|
190
|
+
self.flags[alias] = flag
|
|
191
|
+
|
|
192
|
+
def add_by_option(
|
|
193
|
+
self,
|
|
194
|
+
name: str,
|
|
195
|
+
etype: type,
|
|
196
|
+
default: Value = None,
|
|
197
|
+
store: ByOptionStore = 'assign',
|
|
198
|
+
aliases: list[str] = [],
|
|
199
|
+
) -> None:
|
|
200
|
+
opt = ByOption(name, etype, default, store)
|
|
201
|
+
|
|
202
|
+
self.byopts[opt.name] = opt
|
|
203
|
+
for alias in aliases:
|
|
204
|
+
self.byopts[alias] = opt
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
REGISTRY = Registry()
|
|
208
|
+
|
|
209
|
+
# ======================
|
|
210
|
+
# Data, Field and Schema
|
|
211
|
+
# ======================
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class RawData:
|
|
216
|
+
name: str | None
|
|
217
|
+
attrs: dict[str, str]
|
|
218
|
+
content: str | None
|
|
219
|
+
|
|
220
|
+
def __hash__(self) -> int:
|
|
221
|
+
return hash((self.name, frozenset(self.attrs.items()), self.content))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class ParsedData:
|
|
226
|
+
name: Value
|
|
227
|
+
attrs: dict[str, Value]
|
|
228
|
+
content: Value
|
|
229
|
+
|
|
230
|
+
def __hash__(self) -> int:
|
|
231
|
+
return hash((self.name, frozenset(self.attrs.items()), self.content))
|
|
232
|
+
|
|
233
|
+
def asdict(self) -> dict[str, Any]:
|
|
234
|
+
"""
|
|
235
|
+
Convert Data to a dict for usage of Jinja2 context.
|
|
236
|
+
|
|
237
|
+
``self.attrs`` will be automaticlly lifted to top-level context when
|
|
238
|
+
there is no key conflicts. For example:
|
|
239
|
+
|
|
240
|
+
- You can access ``Data.attrs['color']`` by "{{ color }}"" instead
|
|
241
|
+
of "{{ attrs.color }}".
|
|
242
|
+
- You can NOT access ``Data.attrs['name']`` by "{{ name }}" cause
|
|
243
|
+
the variable name is taken by ``Data.name``.
|
|
244
|
+
"""
|
|
245
|
+
ctx = asdict(self)
|
|
246
|
+
for k, v in self.attrs.items():
|
|
247
|
+
if k not in ctx:
|
|
248
|
+
ctx[k] = v
|
|
249
|
+
return ctx
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass
|
|
253
|
+
class Field(Unpicklable):
|
|
254
|
+
#: Type of element.
|
|
255
|
+
etype: type = str
|
|
256
|
+
#: Type of container (if the field holds multiple values).
|
|
257
|
+
ctype: type | None = None
|
|
258
|
+
#: Flags of field.
|
|
259
|
+
flags: dict[str, Value] = dataclass_field(default_factory=dict)
|
|
260
|
+
#: The orginal DSL.
|
|
261
|
+
dsl: str | None = None
|
|
262
|
+
|
|
263
|
+
# Type hints for builtin flags.
|
|
264
|
+
if TYPE_CHECKING:
|
|
265
|
+
required: bool = False
|
|
266
|
+
sep: str | None = None
|
|
267
|
+
|
|
268
|
+
def __hash__(self) -> int:
|
|
269
|
+
flags = {
|
|
270
|
+
k: v if not isinstance(v, list) else tuple(v) for k, v in self.flags.items()
|
|
271
|
+
}
|
|
272
|
+
return hash((self.etype, self.ctype, frozenset(flags.items()), self.dsl))
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_dsl(cls, dsl: str) -> Self:
|
|
276
|
+
self = cls()
|
|
277
|
+
DSLParser(self).parse(dsl)
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
def __post_init__(self) -> None:
|
|
281
|
+
# Init flags and by flags.
|
|
282
|
+
for flag in REGISTRY.flags.values():
|
|
283
|
+
if flag.name not in self.flags:
|
|
284
|
+
self.flags[flag.name] = flag.default
|
|
285
|
+
for opt in REGISTRY.byopts.values():
|
|
286
|
+
if opt.name in self.flags:
|
|
287
|
+
continue
|
|
288
|
+
if opt.store == 'assign':
|
|
289
|
+
self.flags[opt.name] = opt.default
|
|
290
|
+
elif opt.store == 'append':
|
|
291
|
+
self.flags[opt.name] = lst = []
|
|
292
|
+
if opt.default is not None:
|
|
293
|
+
lst.append(opt.default)
|
|
294
|
+
else:
|
|
295
|
+
raise DSLParser.by_option_store_value_error(opt)
|
|
296
|
+
|
|
297
|
+
def parse(self, rawval: str | None) -> Value:
|
|
298
|
+
"""
|
|
299
|
+
Parses the raw input string into the target Value.
|
|
300
|
+
When a None is passed, which means the field is not supplied.
|
|
301
|
+
"""
|
|
302
|
+
if rawval is None:
|
|
303
|
+
if self.required:
|
|
304
|
+
# Same to :meth:`directives.unchanged_required`.
|
|
305
|
+
raise ValueError('Argument required but none supplied')
|
|
306
|
+
if self.ctype:
|
|
307
|
+
# Return a empty container when a None value is optional.
|
|
308
|
+
return self.ctype()
|
|
309
|
+
if self.etype is bool:
|
|
310
|
+
# Special case: A single bool field is valid even when
|
|
311
|
+
# value is not supplied.
|
|
312
|
+
return _bool_conv(rawval)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
# Strip whitespace. TODO: supported unchanged?
|
|
316
|
+
rawval = rawval.strip()
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
conv = REGISTRY.convs[self.etype]
|
|
320
|
+
|
|
321
|
+
if self.ctype is None:
|
|
322
|
+
# Parse as scalar
|
|
323
|
+
return conv(rawval)
|
|
324
|
+
|
|
325
|
+
# Parse as container
|
|
326
|
+
if self.sep == ' ':
|
|
327
|
+
items = rawval.split() # split by arbitrary whitespace
|
|
328
|
+
elif self.sep == '':
|
|
329
|
+
items = list(rawval) # split by char
|
|
330
|
+
else:
|
|
331
|
+
items = rawval.split(self.sep)
|
|
332
|
+
|
|
333
|
+
elems = [conv(x.strip()) for x in items if x.strip() != '']
|
|
334
|
+
|
|
335
|
+
return self.ctype(elems)
|
|
336
|
+
except ValueError as e:
|
|
337
|
+
raise ValueError(f"Failed to parse '{rawval}' as {self.etype}: {e}") from e
|
|
338
|
+
|
|
339
|
+
def __getattr__(self, name: str) -> Value:
|
|
340
|
+
if name in self.flags:
|
|
341
|
+
return self.flags[name]
|
|
342
|
+
raise AttributeError(name)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass
|
|
346
|
+
class DSLParser:
|
|
347
|
+
field: Field
|
|
348
|
+
|
|
349
|
+
def parse(self, dsl: str) -> None:
|
|
350
|
+
"""Parses the DSL string into a Field object."""
|
|
351
|
+
self.field.dsl = dsl
|
|
352
|
+
for mod in self._split_modifiers(dsl):
|
|
353
|
+
if mod.strip():
|
|
354
|
+
self._apply_modifier(mod.strip())
|
|
355
|
+
|
|
356
|
+
def _split_modifiers(self, text: str) -> list[str]:
|
|
357
|
+
"""Splits the DSL string by comma, ignoring commas inside quotes."""
|
|
358
|
+
parts, current, quote_char = [], [], None
|
|
359
|
+
|
|
360
|
+
for ch in text:
|
|
361
|
+
if quote_char:
|
|
362
|
+
current.append(ch)
|
|
363
|
+
if ch == quote_char:
|
|
364
|
+
quote_char = None
|
|
365
|
+
else:
|
|
366
|
+
if ch in ('"', "'"):
|
|
367
|
+
quote_char = ch
|
|
368
|
+
current.append(ch)
|
|
369
|
+
elif ch == ',':
|
|
370
|
+
parts.append(''.join(current))
|
|
371
|
+
current = []
|
|
372
|
+
else:
|
|
373
|
+
current.append(ch)
|
|
374
|
+
|
|
375
|
+
if current:
|
|
376
|
+
parts.append(''.join(current))
|
|
377
|
+
|
|
378
|
+
return parts
|
|
379
|
+
|
|
380
|
+
def _apply_modifier(self, mod: str):
|
|
381
|
+
clean_mod = mod.strip()
|
|
382
|
+
lower_mod = clean_mod.lower()
|
|
383
|
+
|
|
384
|
+
# Match: XXX of XXX (e.g., "list of int")
|
|
385
|
+
if match := re.match(r'^([a-zA-Z_]+)\s+of\s+([a-zA-Z_]+)$', lower_mod):
|
|
386
|
+
form, etype = match.groups()
|
|
387
|
+
|
|
388
|
+
if etype not in REGISTRY.etypes:
|
|
389
|
+
raise ValueError(
|
|
390
|
+
f'Unsupported type: "{etype}". '
|
|
391
|
+
f'Available: {list(REGISTRY.etypes.keys())}'
|
|
392
|
+
)
|
|
393
|
+
if form not in REGISTRY.forms:
|
|
394
|
+
raise ValueError(
|
|
395
|
+
f'Unsupported form: "{form}". '
|
|
396
|
+
f'Available: {list(REGISTRY.forms.keys())}'
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
self.field.etype = REGISTRY.etypes[etype]
|
|
400
|
+
self.field.ctype = REGISTRY.forms[form].ctype
|
|
401
|
+
self.field.flags[REGISTRY._sep_by_option.name] = REGISTRY.forms[form].sep
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Match: Type only (e.g., "int")
|
|
405
|
+
if lower_mod in REGISTRY.etypes:
|
|
406
|
+
self.field.etype = REGISTRY.etypes[lower_mod]
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
# Match: by-option, "XXX by XXX" (e.g., "sep by '|'")
|
|
410
|
+
if match := re.match(r'^([a-zA-Z_]+)\s+by\s+(.+)$', clean_mod, re.IGNORECASE):
|
|
411
|
+
optname, rawval = match.groups()
|
|
412
|
+
|
|
413
|
+
if optname not in REGISTRY.byopts:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f'Unsupported by-option: "{optname}" by. '
|
|
416
|
+
f'Available: {list(REGISTRY.byopts.keys())}'
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
flags = self.field.flags
|
|
420
|
+
opt = REGISTRY.byopts[optname]
|
|
421
|
+
val = REGISTRY.convs[opt.etype](rawval)
|
|
422
|
+
|
|
423
|
+
if opt.store == 'assign':
|
|
424
|
+
flags[opt.name] = val
|
|
425
|
+
elif opt.store == 'append':
|
|
426
|
+
vals = flags[opt.name]
|
|
427
|
+
assert isinstance(vals, list)
|
|
428
|
+
vals.append(val)
|
|
429
|
+
else:
|
|
430
|
+
raise self.by_option_store_value_error(opt)
|
|
431
|
+
|
|
432
|
+
# Deal with special by option.
|
|
433
|
+
if opt == REGISTRY._sep_by_option:
|
|
434
|
+
# ctype default to list if 'sep by' is used without a 'xxx of xxx'.
|
|
435
|
+
if self.field.ctype is None:
|
|
436
|
+
self.field.ctype = list
|
|
437
|
+
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Match: flags.
|
|
441
|
+
if lower_mod in REGISTRY.flags:
|
|
442
|
+
opt = REGISTRY.flags[lower_mod]
|
|
443
|
+
self.field.flags[opt.name] = not opt.default
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
raise ValueError(f"Unknown modifier: '{mod}'")
|
|
447
|
+
|
|
448
|
+
@staticmethod
|
|
449
|
+
def by_option_store_value_error(opt: ByOption) -> ValueError:
|
|
450
|
+
raise ValueError(
|
|
451
|
+
f'Unsupported by-option store: "{opt.store}". '
|
|
452
|
+
f'Available: {ByOptionStore}' # FIXME:
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@dataclass(frozen=True)
|
|
457
|
+
class Schema(Unpicklable):
|
|
458
|
+
name: Field | None
|
|
459
|
+
attrs: dict[str, Field] | Field
|
|
460
|
+
content: Field | None
|
|
461
|
+
|
|
462
|
+
def __hash__(self) -> int:
|
|
463
|
+
if isinstance(self.attrs, Field):
|
|
464
|
+
attrs_hash = hash(self.attrs)
|
|
465
|
+
else:
|
|
466
|
+
attrs_hash = hash(frozenset(self.attrs.items()))
|
|
467
|
+
return hash((self.name, attrs_hash, self.content))
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def from_dsl(
|
|
471
|
+
cls,
|
|
472
|
+
name: str | None = None,
|
|
473
|
+
attrs: dict[str, str] = {},
|
|
474
|
+
content: str | None = None,
|
|
475
|
+
) -> Self:
|
|
476
|
+
name_field = Field.from_dsl(name) if name else None
|
|
477
|
+
attrs_field = {k: Field.from_dsl(v) for k, v in attrs.items()}
|
|
478
|
+
cont_field = Field.from_dsl(content) if content else None
|
|
479
|
+
|
|
480
|
+
return cls(name_field, attrs_field, cont_field)
|
|
481
|
+
|
|
482
|
+
def _parse_single(
|
|
483
|
+
self, field: tuple[str, Field | None], rawval: str | None
|
|
484
|
+
) -> Value:
|
|
485
|
+
if field[1] is None and rawval is not None:
|
|
486
|
+
raise ValueError(
|
|
487
|
+
f'Parsing {field[0]}: no argument is allowed; "{rawval}" supplied'
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
assert field[1] is not None
|
|
492
|
+
return field[1].parse(rawval)
|
|
493
|
+
except Exception as e:
|
|
494
|
+
raise ValueError(f'Parsing {field[0]}: {e}')
|
|
495
|
+
|
|
496
|
+
def parse(self, data: RawData) -> ParsedData:
|
|
497
|
+
if data.name:
|
|
498
|
+
name = self._parse_single(('name', self.name), data.name)
|
|
499
|
+
else:
|
|
500
|
+
name = None
|
|
501
|
+
|
|
502
|
+
attrs = {}
|
|
503
|
+
if isinstance(self.attrs, Field):
|
|
504
|
+
for key, rawval in data.attrs.items():
|
|
505
|
+
attrs[key] = self._parse_single(('attrs.' + key, self.attrs), rawval)
|
|
506
|
+
else:
|
|
507
|
+
rawattrs = data.attrs.copy()
|
|
508
|
+
for key, field in self.attrs.items():
|
|
509
|
+
rawval = rawattrs.pop(key, None)
|
|
510
|
+
attrs[key] = self._parse_single(('attrs.' + key, field), rawval)
|
|
511
|
+
for key, rawval in rawattrs.items():
|
|
512
|
+
raise ValueError(f'Unknown attr: "{key}"')
|
|
513
|
+
|
|
514
|
+
if data.content:
|
|
515
|
+
content = self._parse_single(('content', self.content), data.content)
|
|
516
|
+
else:
|
|
517
|
+
content = None
|
|
518
|
+
|
|
519
|
+
return ParsedData(name, attrs, content)
|
|
520
|
+
|
|
521
|
+
def fields(self) -> Generator[tuple[str, Field]]:
|
|
522
|
+
if self.name:
|
|
523
|
+
yield 'name', self.name
|
|
524
|
+
|
|
525
|
+
if isinstance(self.attrs, Field):
|
|
526
|
+
yield 'attrs', self.attrs
|
|
527
|
+
else:
|
|
528
|
+
for name, field in self.attrs.items():
|
|
529
|
+
yield name, field
|
|
530
|
+
|
|
531
|
+
if self.content:
|
|
532
|
+
yield 'content', self.content
|
|
533
|
+
|
|
534
|
+
def items(self, data: ParsedData) -> Generator[tuple[str, Field, Value]]:
|
|
535
|
+
if self.name:
|
|
536
|
+
yield 'name', self.name, data.name
|
|
537
|
+
|
|
538
|
+
if isinstance(self.attrs, Field):
|
|
539
|
+
for name, val in data.attrs:
|
|
540
|
+
yield name, self.attrs, val
|
|
541
|
+
else:
|
|
542
|
+
for name, field in self.attrs.items():
|
|
543
|
+
yield name, field, data.attrs.get(name)
|
|
544
|
+
|
|
545
|
+
if self.content:
|
|
546
|
+
yield 'content', self.content, data.content
|