Skip to content

Commit 24ad079

Browse files
committed
update
1 parent 2d2c8cc commit 24ad079

5 files changed

Lines changed: 155 additions & 46 deletions

File tree

src/sphinxnotes/data/__init__.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,17 @@ def current_schema(self) -> Schema:
8888
return cast(Schema, schema)
8989

9090

91-
class AutoFreeDataDirective(FreeDataDirective):
92-
@override
93-
def process_pending_node(self, n: pending_node) -> None:
94-
n.name_external = n.content_external = True
95-
96-
@override
97-
def current_template(self) -> Template:
98-
tmpl = super().current_template()
99-
tmpl.debug = True
100-
tmpl.phase = Phase.Parsed
101-
return tmpl
91+
# class AutoFreeDataDirective(FreeDataDirective):
92+
# @override
93+
# def process_pending_node(self, n: pending_node) -> None:
94+
# n.name_external = n.content_external = True
95+
#
96+
# @override
97+
# def current_template(self) -> Template:
98+
# tmpl = super().current_template()
99+
# tmpl.debug = True
100+
# tmpl.phase = Phase.Parsed
101+
# return tmpl
102102

103103

104104
def setup(app: Sphinx):
@@ -112,12 +112,10 @@ def setup(app: Sphinx):
112112

113113
app.add_config_value('data_template_debug', True, types=bool, rebuild='')
114114

115-
app.add_directive('data.obj', FreeDataDirective)
116-
app.add_directive('data.autobj', AutoFreeDataDirective)
117-
118115
app.add_directive('data.tmpl', TemplateDirective)
119116
app.add_directive('data.template', TemplateDirective)
120117
app.add_directive('data.schema', SchemaDirective)
118+
app.add_directive('data.obj', FreeDataDirective)
121119

122120
app.add_role('data', FreeDataRole())
123121

src/sphinxnotes/data/data.py

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,48 @@
11
from __future__ import annotations
22
import re
3-
from dataclasses import dataclass, asdict
4-
from typing import Any, Callable
3+
from dataclasses import dataclass, asdict, field as datacls_field
4+
from typing import Any, Callable, Generator
5+
import hashlib
6+
import pickle
57

68

7-
type Value = None | int | float | str | bool | list[Value]
8-
9+
type SimpleValue = bool | int | float | str
10+
type Value = None | SimpleValue | list[SimpleValue]
911

1012
@dataclass
11-
class Data(object):
13+
class RawData:
1214
name: str | None
1315
attrs: dict[str, str]
1416
content: str | None
1517

1618

19+
def strify(val: Value) -> str | None:
20+
if val is None:
21+
return None
22+
elif isinstance(val, list):
23+
return str(val[0]) if len(val) > 0 else None
24+
else:
25+
return str(val)
26+
27+
1728
@dataclass
18-
class Parsed(object):
29+
class ParsedData:
1930
name: Value
2031
attrs: dict[str, Value]
2132
content: Value
2233

2334
def ascontext(self) -> dict[str, Any]:
2435
return asdict(self)
2536

37+
@property
38+
def title(self) -> str | None:
39+
return strify(self.name)
40+
41+
@property
42+
def hexdigest(self) -> str:
43+
return hashlib.sha1(pickle.dumps(self)).hexdigest()[:7]
44+
45+
2646

2747
@dataclass
2848
class Form:
@@ -72,15 +92,21 @@ def _bool_conv(v: str | None) -> bool:
7292
),
7393
}
7494

95+
flags: set[str] = set()
96+
7597

7698
@dataclass
7799
class Field:
78100
# Type of element.
79101
etype: type = str
80-
required: bool = False
81102
# Form of elements (if the field holds multiple values).
82103
form: Form | None = None
83104

105+
# Multiple flags.
106+
required: bool = False
107+
flags: set[str] = datacls_field(default_factory=set)
108+
109+
84110
@staticmethod
85111
def from_str(dsl: str) -> Field:
86112
"""Parses the DSL string into a Field object."""
@@ -147,11 +173,6 @@ def _apply_modifier(self, mod: str):
147173
self.etype = Registry.etypes[lower_mod]
148174
return
149175

150-
# Match: Required flag
151-
if lower_mod in ['required', 'req']:
152-
self.required = True
153-
return
154-
155176
# Match: Custom separator (e.g., "sep by '|'")
156177
if match := re.match(r'^sep\s+by\s+(.+)$', clean_mod, re.IGNORECASE):
157178
rawsep = match.group(1).strip()
@@ -160,6 +181,18 @@ def _apply_modifier(self, mod: str):
160181
self.form = Form(form.rtype, self._strip_quotes(rawsep))
161182
return
162183

184+
# Match: Required flag
185+
if lower_mod in ['required', 'req']:
186+
self.required = True
187+
return
188+
189+
# Match: Extras flags.
190+
print('>>>>>>>>>>', Registry.flags, lower_mod)
191+
if lower_mod in Registry.flags:
192+
print('**********', Registry.flags, lower_mod)
193+
self.flags.add(lower_mod)
194+
return
195+
163196
raise ValueError(f"unknown modifier: '{mod}'")
164197

165198
def _strip_quotes(self, s: str) -> str:
@@ -169,6 +202,12 @@ def _strip_quotes(self, s: str) -> str:
169202
return content.replace(r'\n', '\n').replace(r'\t', '\t').replace(r'\,', ',')
170203
return s
171204

205+
def __getattr__(self, name: str) -> Any:
206+
if name not in Registry.flags:
207+
raise AttributeError(name)
208+
return name in self.flags
209+
210+
172211
def parse(self, rawval: str | None) -> Value:
173212
"""
174213
Parses the raw input string into the target Value.
@@ -240,7 +279,7 @@ def _parse_single(
240279
except Exception as e:
241280
raise ValueError(f'parsing {field[0]}: {e}')
242281

243-
def parse(self, data: Data) -> Parsed:
282+
def parse(self, data: RawData) -> ParsedData:
244283
if data.name:
245284
name = self._parse_single(('name', self.name), data.name)
246285
else:
@@ -263,4 +302,23 @@ def parse(self, data: Data) -> Parsed:
263302
else:
264303
content = None
265304

266-
return Parsed(name, attrs, content)
305+
return ParsedData(name, attrs, content)
306+
307+
def items(self, data: ParsedData, pred=Callable[[Field], bool]) -> Generator[tuple[str, Field, Value]]:
308+
def ok(f: Field) -> bool:
309+
return not pred or pred(f)
310+
311+
if self.name and ok(self.name):
312+
yield 'name', self.name, data.name
313+
314+
if isinstance(self.attrs, Field) and ok(self.attrs):
315+
for name, val in data.attrs.items():
316+
yield name, self.attrs, val
317+
else:
318+
for name, field in self.attrs.items():
319+
if not ok(field):
320+
continue
321+
yield name, field, data.attrs.get(name)
322+
323+
if self.content and ok(self.content):
324+
yield 'content', self.content, data.content

src/sphinxnotes/data/render.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@
99
from __future__ import annotations
1010
from typing import override
1111
from abc import abstractmethod, ABC
12+
from dataclasses import dataclass
1213

1314
from docutils import nodes
14-
from docutils.parsers.rst import directives
15+
from docutils.nodes import fully_normalize_name
1516
from sphinx.util import logging
1617
from sphinx.util.docutils import SphinxDirective, SphinxRole
1718
from sphinx.application import Sphinx
1819
from sphinx.transforms import SphinxTransform
20+
from sphinx.util.nodes import make_id
21+
from sphinx.environment import BuildEnvironment
1922

20-
from .data import Field, Schema, Data
23+
24+
from .data import Field, Schema, RawData, ParsedData
2125
from .template import Template, Phase, MarkupParser
2226
from . import extra_contexts
2327
from . import utils
@@ -26,7 +30,6 @@
2630

2731

2832
class BaseNode(nodes.Node):
29-
data: Data
3033
template: Template
3134
schema: Schema
3235

@@ -35,6 +38,7 @@ class BaseNode(nodes.Node):
3538

3639

3740
class pending_node(BaseNode, nodes.Invisible, nodes.meta):
41+
data: RawData
3842
name_external: bool = False
3943
content_external: bool = False
4044

@@ -58,6 +62,7 @@ def render(self, caller: Caller, replace: bool = False) -> rendered_node:
5862

5963
renderer = self._get_caller_markup_parser(caller)
6064
rendered = rendered_node()
65+
rendered.data = parsed
6166
rendered += self.template.render(
6267
renderer,
6368
parsed,
@@ -68,6 +73,7 @@ def render(self, caller: Caller, replace: bool = False) -> rendered_node:
6873
},
6974
)
7075
rendered.update_all_atts(self)
76+
# TODO: check tmpl schema line no
7177

7278
if replace:
7379
self.replace_self(rendered)
@@ -120,9 +126,44 @@ def _get_caller_reporter(self, caller: Caller, e: ValueError) -> Exception:
120126
... # TODO
121127

122128

129+
@dataclass
130+
class _Caller:
131+
v: Caller
132+
133+
@property
134+
def env(self) -> BuildEnvironment:
135+
return self.v.env
136+
137+
@property
138+
def doctree(self) -> nodes.document:
139+
if isinstance(self.v, SphinxDirective):
140+
return self.v.state.document
141+
elif isinstance(self.v, SphinxRole):
142+
return self.v.inliner.document
143+
elif isinstance(self.v, SphinxTransform):
144+
return self.v.document
145+
else:
146+
raise NotImplemented
147+
148+
123149
class rendered_node(BaseNode, nodes.container):
124-
anchor: nodes.Element | None
150+
data: ParsedData
151+
152+
def setup_anchor(self, caller: Caller, ahrnode: nodes.Element, prefix: str = '', term: str | None = None) -> str:
153+
_caller = _Caller(caller)
154+
ahrid = make_id(_caller.env, _caller.doctree, prefix=prefix, term=term)
155+
ahrnode['ids'].append(ahrid)
156+
_caller.doctree.note_explicit_target(ahrnode)
125157

158+
def append_name(name):
159+
ahrnode['names'].append(fully_normalize_name(str(name)))
160+
161+
if isinstance(self.data.name, list):
162+
[append_name(x) for x in self.data.name]
163+
else:
164+
append_name(self.data.name)
165+
166+
return ahrid
126167

127168
class BaseDataMarkup(ABC):
128169
"""Methods to be overrided."""
@@ -133,13 +174,14 @@ def current_template(self) -> Template: ...
133174
@abstractmethod
134175
def current_schema(self) -> Schema: ...
135176

136-
def process_data(self, data: Data) -> None: ...
177+
def process_raw_data(self, data: RawData) -> None: ...
178+
179+
def process_paresd_data(self, data: ParsedData) -> None: ...
137180

138181
def process_pending_node(self, n: pending_node) -> None: ...
139182

140183
def process_rendered_node(self, n: rendered_node) -> None: ...
141184

142-
143185
class BaseDataDirective(BaseDataMarkup, SphinxDirective):
144186
"""Methods overridden from parent."""
145187

@@ -153,12 +195,12 @@ def run(self) -> list[nodes.Node]:
153195
"""Methods used internal."""
154196

155197
def build_pending_node(self) -> pending_node:
156-
data = Data(
198+
data = RawData(
157199
self.arguments[0] if self.arguments else None,
158200
self.options.copy(),
159201
'\n'.join(self.content) if self.has_content else None,
160202
)
161-
self.process_data(data)
203+
self.process_raw_data(data)
162204

163205
tmpl = self.current_template()
164206
schema = self.current_schema()
@@ -172,9 +214,13 @@ def build_pending_node(self) -> pending_node:
172214

173215
def render_pending_node(self, pending: pending_node) -> rendered_node:
174216
rendered = pending.render(self)
217+
self.process_paresd_data(rendered.data)
175218
self.process_rendered_node(rendered)
219+
176220
return rendered
177221

222+
"""Helper methods to be subclasses."""
223+
178224

179225
class BaseDataRole(BaseDataMarkup, SphinxRole):
180226
"""Methods overridden from parent."""
@@ -190,8 +236,8 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
190236

191237
def build_pending_node(self) -> pending_node:
192238
# TODO: self.content. self.options
193-
data = Data(None, {}, self.text)
194-
self.process_data(data)
239+
data = RawData(None, {}, self.text)
240+
self.process_raw_data(data)
195241

196242
tmpl = self.current_template()
197243
schema = self.current_schema()

src/sphinxnotes/data/template.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from jinja2.sandbox import SandboxedEnvironment
1414

15-
from .data import Parsed
15+
from .data import ParsedData
1616
from .utils import Reporter
1717

1818
logger = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ class Template(object):
4242
debug: bool
4343

4444
def render(
45-
self, parser: MarkupParser, data: Parsed, extractx: dict[str, Any]
45+
self, parser: MarkupParser, data: ParsedData, extractx: dict[str, Any]
4646
) -> list[nodes.Node]:
4747
ctx = data.ascontext()
4848
ctx.update(**extractx)

0 commit comments

Comments
 (0)