sphinxnotes-data 1.0a0__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/data/__init__.py +32 -0
- sphinxnotes/data/adhoc.py +88 -0
- sphinxnotes/data/api.py +37 -0
- sphinxnotes/data/data.py +447 -0
- sphinxnotes/data/extra_contexts.py +45 -0
- sphinxnotes/data/meta.py +35 -0
- sphinxnotes/data/preset.py +44 -0
- sphinxnotes/data/py.typed +0 -0
- sphinxnotes/data/render.py +390 -0
- sphinxnotes/data/template.py +118 -0
- sphinxnotes/data/utils/__init__.py +98 -0
- sphinxnotes/data/utils/context_proxy.py +149 -0
- sphinxnotes/data/utils/freestyle.py +93 -0
- sphinxnotes_data-1.0a0.dist-info/METADATA +78 -0
- sphinxnotes_data-1.0a0.dist-info/RECORD +18 -0
- sphinxnotes_data-1.0a0.dist-info/WHEEL +5 -0
- sphinxnotes_data-1.0a0.dist-info/licenses/LICENSE +29 -0
- sphinxnotes_data-1.0a0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sphinxnotes.data
|
|
3
|
+
~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
:copyright: Copyright 2025 by the Shengyu Zhang.
|
|
6
|
+
:license: BSD, see LICENSE for details.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from sphinx.util import logging
|
|
13
|
+
|
|
14
|
+
from . import meta
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sphinx.application import Sphinx
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
def setup(app: Sphinx):
|
|
22
|
+
meta.pre_setup(app)
|
|
23
|
+
|
|
24
|
+
from . import template
|
|
25
|
+
from . import render
|
|
26
|
+
from . import adhoc
|
|
27
|
+
|
|
28
|
+
template.setup(app)
|
|
29
|
+
render.setup(app)
|
|
30
|
+
adhoc.setup(app)
|
|
31
|
+
|
|
32
|
+
return meta.post_setup(app)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sphinxnotes.data.adhoc
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
This extension allow user define, validate, and render data in a document
|
|
6
|
+
on the fly
|
|
7
|
+
|
|
8
|
+
(Yes, Use markup language (rst/md) entirely, instead of Python).
|
|
9
|
+
|
|
10
|
+
:copyright: Copyright 2025 by the Shengyu Zhang.
|
|
11
|
+
:license: BSD, see LICENSE for details.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from typing import TYPE_CHECKING, cast, override
|
|
17
|
+
|
|
18
|
+
from docutils import nodes
|
|
19
|
+
from docutils.parsers.rst import directives
|
|
20
|
+
from sphinx.util.docutils import SphinxDirective
|
|
21
|
+
|
|
22
|
+
from .data import Field, Schema
|
|
23
|
+
from .template import Template, Phase
|
|
24
|
+
from .render import BaseDataDefineDirective
|
|
25
|
+
from .utils.freestyle import FreeStyleDirective, FreeStyleOptionSpec
|
|
26
|
+
from . import preset
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from sphinx.application import Sphinx
|
|
30
|
+
|
|
31
|
+
# Keys of env.temp_data.
|
|
32
|
+
TEMPLATE_KEY = 'sphinxnotes-data:template'
|
|
33
|
+
SCHEMA_KEY = 'sphinxnotes-data:schema'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TemplateDefineDirective(SphinxDirective):
|
|
37
|
+
option_spec = {
|
|
38
|
+
'on': Phase.option_spec,
|
|
39
|
+
'debug': directives.flag,
|
|
40
|
+
}
|
|
41
|
+
has_content = True
|
|
42
|
+
|
|
43
|
+
def run(self) -> list[nodes.Node]:
|
|
44
|
+
self.env.temp_data[TEMPLATE_KEY] = Template(
|
|
45
|
+
text='\n'.join(self.content),
|
|
46
|
+
phase=self.options.get('on', Phase.default()),
|
|
47
|
+
debug='debug' in self.options,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SchemaDefineDirective(FreeStyleDirective):
|
|
54
|
+
optional_arguments = 1
|
|
55
|
+
option_spec = FreeStyleOptionSpec()
|
|
56
|
+
has_content = True
|
|
57
|
+
|
|
58
|
+
def run(self) -> list[nodes.Node]:
|
|
59
|
+
name = Field.from_dsl(self.arguments[0]) if self.arguments else None
|
|
60
|
+
attrs = {}
|
|
61
|
+
for k, v in self.options.items():
|
|
62
|
+
attrs[k] = Field.from_dsl(v)
|
|
63
|
+
content = Field.from_dsl(self.content[0]) if self.content else None
|
|
64
|
+
|
|
65
|
+
self.env.temp_data[SCHEMA_KEY] = Schema(name, attrs, content)
|
|
66
|
+
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FreeDataDefineDirective(BaseDataDefineDirective, FreeStyleDirective):
|
|
71
|
+
optional_arguments = 1
|
|
72
|
+
has_content = True
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
def current_template(self) -> Template:
|
|
76
|
+
tmpl = self.env.temp_data.get(TEMPLATE_KEY, preset.Directive.template())
|
|
77
|
+
return cast(Template, tmpl)
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def current_schema(self) -> Schema:
|
|
81
|
+
schema = self.env.temp_data.get(SCHEMA_KEY, preset.Directive.schema())
|
|
82
|
+
return cast(Schema, schema)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def setup(app: Sphinx):
|
|
86
|
+
app.add_directive('template', TemplateDefineDirective)
|
|
87
|
+
app.add_directive('schema', SchemaDefineDirective)
|
|
88
|
+
app.add_directive('data', FreeDataDefineDirective)
|
sphinxnotes/data/api.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sphinxnotes.data.api
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Python API for other Sphinx extesions.
|
|
6
|
+
|
|
7
|
+
:copyright: Copyright 2025 by the Shengyu Zhang.
|
|
8
|
+
:license: BSD, see LICENSE for details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .data import RawData, Value, ValueWrapper, Data, Field, Schema
|
|
12
|
+
|
|
13
|
+
RawData = RawData
|
|
14
|
+
Value = Value
|
|
15
|
+
ValueWrapper = ValueWrapper
|
|
16
|
+
Data = Data
|
|
17
|
+
Field = Field
|
|
18
|
+
Schema = Schema
|
|
19
|
+
|
|
20
|
+
from .template import Phase, Template
|
|
21
|
+
|
|
22
|
+
Phase = Phase
|
|
23
|
+
Template = Template
|
|
24
|
+
|
|
25
|
+
from .render import Caller, pending_node, RenderedNode, rendered_node, rendered_inline_node, BaseDataDefiner, BaseDataDefineRole, BaseDataDefineDirective, StrictDataDefineDirective
|
|
26
|
+
|
|
27
|
+
Caller = Caller
|
|
28
|
+
pending_node = pending_node
|
|
29
|
+
RenderedNode = RenderedNode
|
|
30
|
+
rendered_node = rendered_node
|
|
31
|
+
rendered_inline_node = rendered_inline_node
|
|
32
|
+
BaseDataDefiner = BaseDataDefiner
|
|
33
|
+
BaseDataDefineRole = BaseDataDefineRole
|
|
34
|
+
BaseDataDefineDirective = BaseDataDefineDirective
|
|
35
|
+
BaseDataDefineDirective = BaseDataDefineDirective
|
|
36
|
+
StrictDataDefineDirective = StrictDataDefineDirective
|
|
37
|
+
|
sphinxnotes/data/data.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, asdict, field as dataclass_field
|
|
5
|
+
from ast import literal_eval
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Any, Callable, Generator, Self, Literal as Lit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
#########################################
|
|
12
|
+
# Basic classes: Value, Form, Flag, ... #
|
|
13
|
+
#########################################
|
|
14
|
+
|
|
15
|
+
type PlainValue = bool | int | float | str
|
|
16
|
+
type Value = None | PlainValue | list[PlainValue]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ValueWrapper:
|
|
21
|
+
v: Value
|
|
22
|
+
|
|
23
|
+
def as_plain(self) -> PlainValue | None:
|
|
24
|
+
if self.v is None:
|
|
25
|
+
return None
|
|
26
|
+
if isinstance(self.v, list):
|
|
27
|
+
if len(self.v) == 0:
|
|
28
|
+
return None
|
|
29
|
+
return self.v[0]
|
|
30
|
+
return self.v
|
|
31
|
+
|
|
32
|
+
def as_str(self) -> str | None:
|
|
33
|
+
return str(self.as_plain())
|
|
34
|
+
|
|
35
|
+
def as_list(self) -> list[PlainValue]:
|
|
36
|
+
if self.v is None:
|
|
37
|
+
return []
|
|
38
|
+
elif isinstance(self.v, list):
|
|
39
|
+
return [x for x in self.v]
|
|
40
|
+
else:
|
|
41
|
+
return [self.v]
|
|
42
|
+
|
|
43
|
+
def as_str_list(self) -> list[str]:
|
|
44
|
+
if self.v is None:
|
|
45
|
+
return []
|
|
46
|
+
elif isinstance(self.v, list):
|
|
47
|
+
return [str(x) for x in self.v]
|
|
48
|
+
else:
|
|
49
|
+
return [str(self.v)]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Form:
|
|
54
|
+
"""Defines how to split the string and the container type."""
|
|
55
|
+
|
|
56
|
+
ctype: type
|
|
57
|
+
sep: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Flag:
|
|
62
|
+
name: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class BoolFlag(Flag):
|
|
67
|
+
default: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
type FlagStore = Lit['assign'] | Lit['append']
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class OperFlag(Flag):
|
|
75
|
+
etype: type = str
|
|
76
|
+
default: Value = None
|
|
77
|
+
store: FlagStore = 'assign'
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
############
|
|
81
|
+
# Registry #
|
|
82
|
+
############
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Registry:
|
|
86
|
+
"""Stores supported element types and element forms (containers)."""
|
|
87
|
+
|
|
88
|
+
etypes: dict[str, type] = {
|
|
89
|
+
'bool': bool,
|
|
90
|
+
'flag': bool,
|
|
91
|
+
'int': int,
|
|
92
|
+
'integer': int,
|
|
93
|
+
'float': float,
|
|
94
|
+
'number': float,
|
|
95
|
+
'num': float,
|
|
96
|
+
'str': str,
|
|
97
|
+
'string': str,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
"""Internal type converters."""
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _bool_conv(v: str | None) -> bool:
|
|
104
|
+
v = v.lower().strip() if v is not None else None
|
|
105
|
+
if v in ('true', 'yes', '1', 'on', 'y', ''):
|
|
106
|
+
return True
|
|
107
|
+
if v in ('false', 'no', '0', 'off', 'n', None):
|
|
108
|
+
return False
|
|
109
|
+
# Same to :meth:`directives.flag`.
|
|
110
|
+
raise ValueError(f'no argument is allowed; "{v}" supplied')
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _str_conv(v: str) -> str:
|
|
114
|
+
try:
|
|
115
|
+
vv = literal_eval(v)
|
|
116
|
+
except (ValueError, SyntaxError):
|
|
117
|
+
return v
|
|
118
|
+
return vv if isinstance(vv, str) else v
|
|
119
|
+
|
|
120
|
+
convs: dict[type, Callable[[str], Any]] = {
|
|
121
|
+
bool: _bool_conv,
|
|
122
|
+
int: int,
|
|
123
|
+
float: float,
|
|
124
|
+
str: _str_conv,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
forms: dict[str, Form] = {
|
|
128
|
+
'list': Form(ctype=list, sep=','),
|
|
129
|
+
'lines': Form(ctype=list, sep='\n'),
|
|
130
|
+
'words': Form(ctype=list, sep=' '),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
"""Builtin flags."""
|
|
134
|
+
|
|
135
|
+
_required_flag = BoolFlag('required')
|
|
136
|
+
_sep_flag = OperFlag('sep', etype=str)
|
|
137
|
+
|
|
138
|
+
flags: dict[str, BoolFlag] = {
|
|
139
|
+
'required': _required_flag,
|
|
140
|
+
'require': _required_flag,
|
|
141
|
+
'req': _required_flag,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
byflags: dict[str, OperFlag] = {
|
|
145
|
+
'separate': _sep_flag,
|
|
146
|
+
'sep': _sep_flag,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
##########################
|
|
151
|
+
# Data, Field and Schema #
|
|
152
|
+
##########################
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class RawData:
|
|
157
|
+
name: str | None
|
|
158
|
+
attrs: dict[str, str]
|
|
159
|
+
content: str | None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class Data:
|
|
164
|
+
name: Value
|
|
165
|
+
attrs: dict[str, Value]
|
|
166
|
+
content: Value
|
|
167
|
+
|
|
168
|
+
def ascontext(self) -> dict[str, Any]:
|
|
169
|
+
return asdict(self)
|
|
170
|
+
|
|
171
|
+
def title(self) -> str | None:
|
|
172
|
+
return ValueWrapper(self.name).as_str()
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Field:
|
|
176
|
+
#: Type of element.
|
|
177
|
+
etype: type = str
|
|
178
|
+
#: Type of container (if the field holds multiple values).
|
|
179
|
+
ctype: type | None = None
|
|
180
|
+
#: Flags of field.
|
|
181
|
+
flags: dict[str, Value] = dataclass_field(default_factory=dict)
|
|
182
|
+
|
|
183
|
+
# Type hints for builtin flags.
|
|
184
|
+
if TYPE_CHECKING:
|
|
185
|
+
required: bool = False
|
|
186
|
+
sep: str | None = None
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_dsl(cls, dsl: str) -> Self:
|
|
190
|
+
self = cls()
|
|
191
|
+
DSLParser(self).parse(dsl)
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def __post_init__(self) -> None:
|
|
195
|
+
# Init flags and by flags.
|
|
196
|
+
for flag in Registry.flags.values():
|
|
197
|
+
if flag.name not in self.flags:
|
|
198
|
+
self.flags[flag.name] = flag.default
|
|
199
|
+
for flag in Registry.byflags.values():
|
|
200
|
+
if flag.name not in self.flags:
|
|
201
|
+
self.flags[flag.name] = flag.default
|
|
202
|
+
|
|
203
|
+
def parse(self, rawval: str | None) -> Value:
|
|
204
|
+
"""
|
|
205
|
+
Parses the raw input string into the target Value.
|
|
206
|
+
When a None is passed, which means the field is not supplied.
|
|
207
|
+
"""
|
|
208
|
+
if rawval is None:
|
|
209
|
+
if self.required:
|
|
210
|
+
# Same to :meth:`directives.unchanged_required`.
|
|
211
|
+
raise ValueError('argument required but none supplied')
|
|
212
|
+
if self.ctype:
|
|
213
|
+
# Return a empty container when a None value is optional.
|
|
214
|
+
return self.ctype()
|
|
215
|
+
if self.etype is bool:
|
|
216
|
+
# Special case: A single bool field is valid even when
|
|
217
|
+
# value is not supplied.
|
|
218
|
+
return Registry._bool_conv(rawval)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
# Strip whitespace. TODO: supported unchanged?
|
|
222
|
+
rawval = rawval.strip()
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
conv = Registry.convs[self.etype]
|
|
226
|
+
|
|
227
|
+
if self.ctype is None:
|
|
228
|
+
# Parse as scalar
|
|
229
|
+
return conv(rawval)
|
|
230
|
+
|
|
231
|
+
# Parse as container
|
|
232
|
+
if self.sep == ' ':
|
|
233
|
+
items = rawval.split() # split by arbitrary whitespace
|
|
234
|
+
elif self.sep == '':
|
|
235
|
+
items = list(rawval) # split by char
|
|
236
|
+
else:
|
|
237
|
+
items = rawval.split(self.sep)
|
|
238
|
+
|
|
239
|
+
elems = [conv(x.strip()) for x in items if x.strip() != '']
|
|
240
|
+
|
|
241
|
+
return self.ctype(elems)
|
|
242
|
+
except ValueError as e:
|
|
243
|
+
raise ValueError(f"failed to parse '{rawval}' as {self.etype}: {e}")
|
|
244
|
+
|
|
245
|
+
def __getattr__(self, name: str) -> Value:
|
|
246
|
+
if name in self.flags:
|
|
247
|
+
return self.flags[name]
|
|
248
|
+
raise AttributeError(name)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class DSLParser:
|
|
253
|
+
field: Field
|
|
254
|
+
|
|
255
|
+
def parse(self, dsl: str) -> None:
|
|
256
|
+
"""Parses the DSL string into a Field object."""
|
|
257
|
+
# Initialize form as None, implied scalar unless modifiers change it.
|
|
258
|
+
for mod in self._split_modifiers(dsl):
|
|
259
|
+
if mod.strip():
|
|
260
|
+
self._apply_modifier(mod.strip())
|
|
261
|
+
|
|
262
|
+
def _split_modifiers(self, text: str) -> list[str]:
|
|
263
|
+
"""Splits the DSL string by comma, ignoring commas inside quotes."""
|
|
264
|
+
parts, current, quote_char = [], [], None
|
|
265
|
+
|
|
266
|
+
for ch in text:
|
|
267
|
+
if quote_char:
|
|
268
|
+
current.append(ch)
|
|
269
|
+
if ch == quote_char:
|
|
270
|
+
quote_char = None
|
|
271
|
+
else:
|
|
272
|
+
if ch in ('"', "'"):
|
|
273
|
+
quote_char = ch
|
|
274
|
+
current.append(ch)
|
|
275
|
+
elif ch == ',':
|
|
276
|
+
parts.append(''.join(current))
|
|
277
|
+
current = []
|
|
278
|
+
else:
|
|
279
|
+
current.append(ch)
|
|
280
|
+
|
|
281
|
+
if current:
|
|
282
|
+
parts.append(''.join(current))
|
|
283
|
+
|
|
284
|
+
return parts
|
|
285
|
+
|
|
286
|
+
def _apply_modifier(self, mod: str):
|
|
287
|
+
clean_mod = mod.strip()
|
|
288
|
+
lower_mod = clean_mod.lower()
|
|
289
|
+
|
|
290
|
+
# Match: XXX of XXX (e.g., "list of int")
|
|
291
|
+
if match := re.match(r'^([a-zA-Z_]+)\s+of\s+([a-zA-Z_]+)$', lower_mod):
|
|
292
|
+
form, etype = match.groups()
|
|
293
|
+
|
|
294
|
+
if etype not in Registry.etypes:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f'unsupported type: "{etype}". '
|
|
297
|
+
f'available: {list(Registry.etypes.keys())}'
|
|
298
|
+
)
|
|
299
|
+
if form not in Registry.forms:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f'unsupported form: "{form}". '
|
|
302
|
+
f'available: {list(Registry.forms.keys())}'
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
self.field.etype = Registry.etypes[etype]
|
|
306
|
+
self.field.ctype = Registry.forms[form].ctype
|
|
307
|
+
self.field.flags[Registry._sep_flag.name] = Registry.forms[form].sep
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# Match: Type only (e.g., "int")
|
|
311
|
+
if lower_mod in Registry.etypes:
|
|
312
|
+
self.field.etype = Registry.etypes[lower_mod]
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
# Match: XXX by XXX (e.g., "sep by '|'")
|
|
316
|
+
if match := re.match(r'^([a-zA-Z_]+)\s+by\s+(.+)$', clean_mod, re.IGNORECASE):
|
|
317
|
+
flagname, rawval = match.groups()
|
|
318
|
+
|
|
319
|
+
if flagname not in Registry.byflags:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f'unsupported flag: "{flagname}" by. '
|
|
322
|
+
f'available: {list(Registry.byflags.keys())}'
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
flags = self.field.flags
|
|
326
|
+
flag = Registry.byflags[flagname]
|
|
327
|
+
val = Registry.convs[flag.etype](rawval)
|
|
328
|
+
|
|
329
|
+
if flag.store == 'assign':
|
|
330
|
+
flags[flag.name] = val
|
|
331
|
+
elif flag.store == 'append':
|
|
332
|
+
if flags[flag.name] is None:
|
|
333
|
+
flags[flag.name] = []
|
|
334
|
+
vals = flags[flag.name]
|
|
335
|
+
assert isinstance(vals, list)
|
|
336
|
+
vals.append(val)
|
|
337
|
+
else:
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f'unsupported flag store: "{flag.store}". available: {FlagStore}'
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Deal with builtin flags.
|
|
343
|
+
if flag == Registry._sep_flag:
|
|
344
|
+
# ctype default to list if 'sep by' is used without a 'xxx of xxx'.
|
|
345
|
+
if self.field.ctype is None:
|
|
346
|
+
self.field.ctype = list
|
|
347
|
+
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
# Match: bool flags.
|
|
351
|
+
if lower_mod in Registry.flags:
|
|
352
|
+
flag = Registry.flags[lower_mod]
|
|
353
|
+
self.field.flags[flag.name] = not flag.default
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
raise ValueError(f"unknown modifier: '{mod}'")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@dataclass(frozen=True)
|
|
360
|
+
class Schema(object):
|
|
361
|
+
name: Field | None
|
|
362
|
+
attrs: dict[str, Field]
|
|
363
|
+
content: Field | None
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def from_dsl(
|
|
367
|
+
cls,
|
|
368
|
+
name: str | None = None,
|
|
369
|
+
attrs: dict[str, str] = {},
|
|
370
|
+
content: str | None = None,
|
|
371
|
+
) -> Self:
|
|
372
|
+
name_field = Field.from_dsl(name) if name else None
|
|
373
|
+
attrs_field = {k: Field.from_dsl(v) for k, v in attrs.items()}
|
|
374
|
+
cont_field = Field.from_dsl(content) if content else None
|
|
375
|
+
|
|
376
|
+
return cls(name_field, attrs_field, cont_field)
|
|
377
|
+
|
|
378
|
+
def _parse_single(
|
|
379
|
+
self, field: tuple[str, Field | None], rawval: str | None
|
|
380
|
+
) -> Value:
|
|
381
|
+
if field[1] is None and rawval is not None:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f'parsing {field[0]}: no argument is allowed; "{rawval}" supplied'
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
assert field[1] is not None
|
|
388
|
+
return field[1].parse(rawval)
|
|
389
|
+
except Exception as e:
|
|
390
|
+
raise ValueError(f'parsing {field[0]}: {e}')
|
|
391
|
+
|
|
392
|
+
def parse(self, data: RawData) -> Data:
|
|
393
|
+
if data.name:
|
|
394
|
+
name = self._parse_single(('name', self.name), data.name)
|
|
395
|
+
else:
|
|
396
|
+
name = None
|
|
397
|
+
|
|
398
|
+
attrs = {}
|
|
399
|
+
if isinstance(self.attrs, Field):
|
|
400
|
+
for key, rawval in data.attrs.items():
|
|
401
|
+
attrs[key] = self._parse_single(('attrs.' + key, self.attrs), rawval)
|
|
402
|
+
else:
|
|
403
|
+
rawattrs = data.attrs.copy()
|
|
404
|
+
for key, field in self.attrs.items():
|
|
405
|
+
rawval = rawattrs.pop(key)
|
|
406
|
+
attrs[key] = self._parse_single(('attrs.' + key, field), rawval)
|
|
407
|
+
for key, rawval in rawattrs.items():
|
|
408
|
+
raise ValueError(f'unknown attr: "{key}"')
|
|
409
|
+
|
|
410
|
+
if data.content:
|
|
411
|
+
content = self._parse_single(('content', self.content), data.content)
|
|
412
|
+
else:
|
|
413
|
+
content = None
|
|
414
|
+
|
|
415
|
+
return Data(name, attrs, content)
|
|
416
|
+
|
|
417
|
+
def fields(self, pred: Callable[[Field], bool] | None = None) -> Generator[tuple[str, Field]]:
|
|
418
|
+
def ok(f: Field) -> bool:
|
|
419
|
+
return not pred or pred(f)
|
|
420
|
+
|
|
421
|
+
if self.name and ok(self.name):
|
|
422
|
+
yield 'name', self.name
|
|
423
|
+
|
|
424
|
+
for name, field in self.attrs.items():
|
|
425
|
+
if ok(field):
|
|
426
|
+
yield name, field
|
|
427
|
+
|
|
428
|
+
if self.content and ok(self.content):
|
|
429
|
+
yield 'content', self.content
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def items(
|
|
433
|
+
self, data: Data, pred: Callable[[Field], bool] | None = None
|
|
434
|
+
) -> Generator[tuple[str, Field, Value]]:
|
|
435
|
+
def ok(f: Field) -> bool:
|
|
436
|
+
return not pred or pred(f)
|
|
437
|
+
|
|
438
|
+
if self.name and ok(self.name):
|
|
439
|
+
yield 'name', self.name, data.name
|
|
440
|
+
|
|
441
|
+
for name, field in self.attrs.items():
|
|
442
|
+
if not ok(field):
|
|
443
|
+
continue
|
|
444
|
+
yield name, field, data.attrs.get(name)
|
|
445
|
+
|
|
446
|
+
if self.content and ok(self.content):
|
|
447
|
+
yield 'content', self.content, data.content
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from docutils import nodes
|
|
5
|
+
from sphinx.util.docutils import SphinxDirective, SphinxRole
|
|
6
|
+
from sphinx.transforms import SphinxTransform
|
|
7
|
+
|
|
8
|
+
from .utils import find_current_document, find_current_section
|
|
9
|
+
from .utils.context_proxy import proxy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def markup(v: SphinxDirective | SphinxRole | nodes.Element) -> dict[str, Any]:
|
|
13
|
+
ctx = {}
|
|
14
|
+
|
|
15
|
+
if isinstance(v, nodes.Element):
|
|
16
|
+
ctx['_markup'] = {}
|
|
17
|
+
else:
|
|
18
|
+
isdir = isinstance(v, SphinxDirective)
|
|
19
|
+
ctx['_markup'] = {
|
|
20
|
+
'type': 'directive' if isdir else 'role',
|
|
21
|
+
'name': v.name,
|
|
22
|
+
'lineno': v.lineno,
|
|
23
|
+
'rawtext': v.block_text if isdir else v.rawtext,
|
|
24
|
+
}
|
|
25
|
+
return ctx
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def doctree(v: SphinxDirective | SphinxRole | nodes.Node) -> dict[str, Any]:
|
|
29
|
+
ctx = {}
|
|
30
|
+
if isinstance(v, nodes.Node):
|
|
31
|
+
ctx['_doc'] = proxy(find_current_document(v))
|
|
32
|
+
ctx['_section'] = proxy(find_current_section(v))
|
|
33
|
+
else:
|
|
34
|
+
isdir = isinstance(v, SphinxDirective)
|
|
35
|
+
state = v.state if isdir else v.inliner
|
|
36
|
+
ctx['_doc'] = proxy(state.document)
|
|
37
|
+
ctx['_section'] = proxy(find_current_section(state.parent))
|
|
38
|
+
return ctx
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sphinx(v: SphinxDirective | SphinxRole | SphinxTransform) -> dict[str, Any]:
|
|
42
|
+
ctx = {}
|
|
43
|
+
ctx['_env'] = proxy(v.env)
|
|
44
|
+
ctx['_config'] = proxy(v.config)
|
|
45
|
+
return ctx
|
sphinxnotes/data/meta.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# This file is generated from sphinx-notes/cookiecutter.
|
|
2
|
+
# DO NOT EDIT!!!
|
|
3
|
+
|
|
4
|
+
################################################################################
|
|
5
|
+
# Project meta infos.
|
|
6
|
+
################################################################################
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
|
|
11
|
+
__project__ = 'sphinxnotes-dataview'
|
|
12
|
+
__author__ = 'Shengyu Zhang'
|
|
13
|
+
__desc__ = 'Create and view data in Sphinx documentation'
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__ = metadata.version('sphinxnotes-dataview')
|
|
17
|
+
except metadata.PackageNotFoundError:
|
|
18
|
+
__version__ = 'unknown'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
################################################################################
|
|
22
|
+
# Sphinx extension utils.
|
|
23
|
+
################################################################################
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pre_setup(app):
|
|
27
|
+
app.require_sphinx('7.0')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def post_setup(app):
|
|
31
|
+
return {
|
|
32
|
+
'version': __version__,
|
|
33
|
+
'parallel_read_safe': True,
|
|
34
|
+
'parallel_write_safe': True,
|
|
35
|
+
}
|