intenttext 1.0.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.
- intenttext/__init__.py +39 -0
- intenttext/merge.py +85 -0
- intenttext/parser.py +389 -0
- intenttext/query.py +55 -0
- intenttext/renderer.py +144 -0
- intenttext/source.py +32 -0
- intenttext/types.py +68 -0
- intenttext/validate.py +121 -0
- intenttext-1.0.0.dist-info/METADATA +106 -0
- intenttext-1.0.0.dist-info/RECORD +11 -0
- intenttext-1.0.0.dist-info/WHEEL +4 -0
intenttext/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .merge import merge_data, parse_and_merge
|
|
2
|
+
from .parser import parse, parse_safe
|
|
3
|
+
from .query import query
|
|
4
|
+
from .renderer import render_html, render_markdown, render_print
|
|
5
|
+
from .source import to_source
|
|
6
|
+
from .types import (
|
|
7
|
+
InlineSegment,
|
|
8
|
+
IntentBlock,
|
|
9
|
+
IntentDocument,
|
|
10
|
+
IntentMetadata,
|
|
11
|
+
ParseResult,
|
|
12
|
+
ParseWarning,
|
|
13
|
+
ValidationIssue,
|
|
14
|
+
ValidationResult,
|
|
15
|
+
)
|
|
16
|
+
from .validate import validate
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"parse",
|
|
22
|
+
"parse_safe",
|
|
23
|
+
"render_html",
|
|
24
|
+
"render_print",
|
|
25
|
+
"render_markdown",
|
|
26
|
+
"merge_data",
|
|
27
|
+
"parse_and_merge",
|
|
28
|
+
"validate",
|
|
29
|
+
"query",
|
|
30
|
+
"to_source",
|
|
31
|
+
"IntentDocument",
|
|
32
|
+
"IntentBlock",
|
|
33
|
+
"IntentMetadata",
|
|
34
|
+
"InlineSegment",
|
|
35
|
+
"ParseResult",
|
|
36
|
+
"ParseWarning",
|
|
37
|
+
"ValidationResult",
|
|
38
|
+
"ValidationIssue",
|
|
39
|
+
]
|
intenttext/merge.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .parser import parse
|
|
9
|
+
from .types import IntentDocument
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge_data(template: IntentDocument, data: dict[str, Any]) -> IntentDocument:
|
|
13
|
+
doc = deepcopy(template)
|
|
14
|
+
now = datetime.now()
|
|
15
|
+
system_vars = {
|
|
16
|
+
"timestamp": now.isoformat(),
|
|
17
|
+
"date": now.strftime("%d %B %Y"),
|
|
18
|
+
"year": str(now.year),
|
|
19
|
+
}
|
|
20
|
+
merged_data = {**system_vars, **data}
|
|
21
|
+
|
|
22
|
+
for block in doc.blocks:
|
|
23
|
+
block.content = _resolve_string(block.content, merged_data)
|
|
24
|
+
block.original_content = _resolve_string(block.original_content, merged_data)
|
|
25
|
+
block.properties = {
|
|
26
|
+
k: _resolve_string(v, merged_data) if isinstance(v, str) else v
|
|
27
|
+
for k, v in block.properties.items()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_refresh_metadata(doc)
|
|
31
|
+
|
|
32
|
+
return doc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_and_merge(template_source: str, data: dict[str, Any]) -> IntentDocument:
|
|
36
|
+
template = parse(template_source)
|
|
37
|
+
return merge_data(template, data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve_string(text: str, data: dict[str, Any]) -> str:
|
|
41
|
+
def replacer(match: re.Match[str]) -> str:
|
|
42
|
+
path = match.group(1).strip()
|
|
43
|
+
if path in ("page", "pages"):
|
|
44
|
+
return match.group(0)
|
|
45
|
+
value = _get_by_path(data, path)
|
|
46
|
+
return str(value) if value is not None else match.group(0)
|
|
47
|
+
|
|
48
|
+
return re.sub(r"\{\{([^}]+)\}\}", replacer, text)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_by_path(obj: Any, path: str) -> Any:
|
|
52
|
+
parts = path.split(".")
|
|
53
|
+
current = obj
|
|
54
|
+
|
|
55
|
+
for part in parts:
|
|
56
|
+
if current is None:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
if isinstance(current, list):
|
|
60
|
+
try:
|
|
61
|
+
current = current[int(part)]
|
|
62
|
+
except (ValueError, IndexError):
|
|
63
|
+
return None
|
|
64
|
+
elif isinstance(current, dict):
|
|
65
|
+
current = current.get(part)
|
|
66
|
+
else:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return current
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _refresh_metadata(doc: IntentDocument) -> None:
|
|
73
|
+
for block in doc.blocks:
|
|
74
|
+
if block.type == "title":
|
|
75
|
+
doc.metadata.title = block.content
|
|
76
|
+
elif block.type == "summary":
|
|
77
|
+
doc.metadata.summary = block.content
|
|
78
|
+
elif block.type == "agent":
|
|
79
|
+
doc.metadata.agent = block.content
|
|
80
|
+
if "model" in block.properties:
|
|
81
|
+
doc.metadata.model = str(block.properties["model"])
|
|
82
|
+
elif block.type == "context":
|
|
83
|
+
doc.metadata.context.update(
|
|
84
|
+
{k: str(v) for k, v in block.properties.items()}
|
|
85
|
+
)
|
intenttext/parser.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from .types import (
|
|
7
|
+
InlineSegment,
|
|
8
|
+
IntentBlock,
|
|
9
|
+
IntentDocument,
|
|
10
|
+
IntentMetadata,
|
|
11
|
+
ParseResult,
|
|
12
|
+
ParseWarning,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
DOCUMENT_HEADER_KEYWORDS = {
|
|
16
|
+
"agent",
|
|
17
|
+
"context",
|
|
18
|
+
"font",
|
|
19
|
+
"page",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
STRUCTURE_KEYWORDS = {
|
|
23
|
+
"title",
|
|
24
|
+
"summary",
|
|
25
|
+
"section",
|
|
26
|
+
"sub",
|
|
27
|
+
"note",
|
|
28
|
+
"toc",
|
|
29
|
+
"break",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
CONTENT_KEYWORDS = {
|
|
33
|
+
"task",
|
|
34
|
+
"done",
|
|
35
|
+
"ask",
|
|
36
|
+
"quote",
|
|
37
|
+
"info",
|
|
38
|
+
"warning",
|
|
39
|
+
"tip",
|
|
40
|
+
"success",
|
|
41
|
+
"link",
|
|
42
|
+
"image",
|
|
43
|
+
"code",
|
|
44
|
+
"ref",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
WRITER_KEYWORDS = {
|
|
48
|
+
"byline",
|
|
49
|
+
"epigraph",
|
|
50
|
+
"caption",
|
|
51
|
+
"footnote",
|
|
52
|
+
"dedication",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
AGENTIC_KEYWORDS = {
|
|
56
|
+
"step",
|
|
57
|
+
"decision",
|
|
58
|
+
"parallel",
|
|
59
|
+
"loop",
|
|
60
|
+
"call",
|
|
61
|
+
"gate",
|
|
62
|
+
"wait",
|
|
63
|
+
"retry",
|
|
64
|
+
"error",
|
|
65
|
+
"trigger",
|
|
66
|
+
"checkpoint",
|
|
67
|
+
"handoff",
|
|
68
|
+
"audit",
|
|
69
|
+
"emit",
|
|
70
|
+
"result",
|
|
71
|
+
"progress",
|
|
72
|
+
"import",
|
|
73
|
+
"export",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ALL_KEYWORDS = (
|
|
77
|
+
DOCUMENT_HEADER_KEYWORDS
|
|
78
|
+
| STRUCTURE_KEYWORDS
|
|
79
|
+
| CONTENT_KEYWORDS
|
|
80
|
+
| WRITER_KEYWORDS
|
|
81
|
+
| AGENTIC_KEYWORDS
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_ID_COUNTER = 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse(source: str) -> IntentDocument:
|
|
88
|
+
result = parse_safe(source)
|
|
89
|
+
if result.errors:
|
|
90
|
+
raise ValueError(f"Parse errors: {result.errors[0].message}")
|
|
91
|
+
return result.document
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parse_safe(
|
|
95
|
+
source: str,
|
|
96
|
+
unknown_keyword: str = "note",
|
|
97
|
+
max_blocks: int = 10000,
|
|
98
|
+
max_line_length: int = 50000,
|
|
99
|
+
) -> ParseResult:
|
|
100
|
+
warnings: list[ParseWarning] = []
|
|
101
|
+
errors: list[ParseWarning] = []
|
|
102
|
+
blocks: list[IntentBlock] = []
|
|
103
|
+
metadata = IntentMetadata()
|
|
104
|
+
in_code_fence = False
|
|
105
|
+
fence_lines: list[str] = []
|
|
106
|
+
|
|
107
|
+
lines = source.splitlines()
|
|
108
|
+
|
|
109
|
+
for line_num, raw_line in enumerate(lines, 1):
|
|
110
|
+
original = raw_line
|
|
111
|
+
if len(raw_line) > max_line_length:
|
|
112
|
+
raw_line = raw_line[:max_line_length]
|
|
113
|
+
warnings.append(
|
|
114
|
+
ParseWarning(
|
|
115
|
+
line=line_num,
|
|
116
|
+
message=f"Line truncated at {max_line_length} characters",
|
|
117
|
+
code="LINE_TRUNCATED",
|
|
118
|
+
original=original[:100] + "...",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
line = raw_line.strip()
|
|
123
|
+
|
|
124
|
+
if line.startswith("```"):
|
|
125
|
+
if in_code_fence:
|
|
126
|
+
blocks.append(
|
|
127
|
+
IntentBlock(
|
|
128
|
+
id=_generate_id(),
|
|
129
|
+
type="code",
|
|
130
|
+
content="\n".join(fence_lines),
|
|
131
|
+
original_content="\n".join(fence_lines),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
in_code_fence = False
|
|
135
|
+
fence_lines = []
|
|
136
|
+
else:
|
|
137
|
+
in_code_fence = True
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if in_code_fence:
|
|
141
|
+
fence_lines.append(raw_line)
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if not line:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if line.startswith("//"):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if line == "---":
|
|
151
|
+
blocks.append(
|
|
152
|
+
IntentBlock(
|
|
153
|
+
id=_generate_id(),
|
|
154
|
+
type="divider",
|
|
155
|
+
content="",
|
|
156
|
+
original_content="---",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
if line.startswith("|") and line.endswith("|"):
|
|
162
|
+
cells = [c.strip() for c in line[1:-1].split("|")]
|
|
163
|
+
if all(re.match(r"^[-:]+$", c.strip()) for c in cells if c.strip()):
|
|
164
|
+
continue
|
|
165
|
+
if blocks and blocks[-1].type == "table":
|
|
166
|
+
rows = blocks[-1].properties.get("rows", [])
|
|
167
|
+
rows.append(cells)
|
|
168
|
+
blocks[-1].properties["rows"] = rows
|
|
169
|
+
else:
|
|
170
|
+
blocks.append(
|
|
171
|
+
IntentBlock(
|
|
172
|
+
id=_generate_id(),
|
|
173
|
+
type="table",
|
|
174
|
+
content="",
|
|
175
|
+
original_content=line,
|
|
176
|
+
properties={"rows": [cells]},
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
if len(blocks) >= max_blocks:
|
|
182
|
+
warnings.append(
|
|
183
|
+
ParseWarning(
|
|
184
|
+
line=line_num,
|
|
185
|
+
message=f"Max blocks ({max_blocks}) reached, stopping parse",
|
|
186
|
+
code="MAX_BLOCKS_REACHED",
|
|
187
|
+
original=line,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
block = _parse_keyword_line(
|
|
193
|
+
line=line,
|
|
194
|
+
line_num=line_num,
|
|
195
|
+
unknown_keyword=unknown_keyword,
|
|
196
|
+
warnings=warnings,
|
|
197
|
+
errors=errors,
|
|
198
|
+
)
|
|
199
|
+
if block is not None:
|
|
200
|
+
blocks.append(block)
|
|
201
|
+
_update_metadata(metadata, block)
|
|
202
|
+
|
|
203
|
+
if in_code_fence:
|
|
204
|
+
warnings.append(
|
|
205
|
+
ParseWarning(
|
|
206
|
+
line=len(lines),
|
|
207
|
+
message="Unclosed code fence, auto-closed at end of file",
|
|
208
|
+
code="UNCLOSED_CODE_FENCE",
|
|
209
|
+
original="```",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
blocks.append(
|
|
213
|
+
IntentBlock(
|
|
214
|
+
id=_generate_id(),
|
|
215
|
+
type="code",
|
|
216
|
+
content="\n".join(fence_lines),
|
|
217
|
+
original_content="\n".join(fence_lines),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
document = IntentDocument(version="2.0", blocks=blocks, metadata=metadata)
|
|
222
|
+
return ParseResult(document=document, warnings=warnings, errors=errors)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _parse_keyword_line(
|
|
226
|
+
line: str,
|
|
227
|
+
line_num: int,
|
|
228
|
+
unknown_keyword: str,
|
|
229
|
+
warnings: list[ParseWarning],
|
|
230
|
+
errors: list[ParseWarning],
|
|
231
|
+
) -> Optional[IntentBlock]:
|
|
232
|
+
match = re.match(r"^(\w+):\s*(.*)$", line)
|
|
233
|
+
if not match:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
keyword = match.group(1).lower()
|
|
237
|
+
rest = match.group(2)
|
|
238
|
+
|
|
239
|
+
if keyword not in ALL_KEYWORDS:
|
|
240
|
+
if unknown_keyword == "skip":
|
|
241
|
+
warnings.append(
|
|
242
|
+
ParseWarning(
|
|
243
|
+
line=line_num,
|
|
244
|
+
message=f"Unknown keyword '{keyword}' skipped",
|
|
245
|
+
code="UNKNOWN_KEYWORD",
|
|
246
|
+
original=line,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
return None
|
|
250
|
+
if unknown_keyword == "throw":
|
|
251
|
+
errors.append(
|
|
252
|
+
ParseWarning(
|
|
253
|
+
line=line_num,
|
|
254
|
+
message=f"Unknown keyword '{keyword}'",
|
|
255
|
+
code="UNKNOWN_KEYWORD",
|
|
256
|
+
original=line,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
warnings.append(
|
|
262
|
+
ParseWarning(
|
|
263
|
+
line=line_num,
|
|
264
|
+
message=f"Unknown keyword '{keyword}' treated as note",
|
|
265
|
+
code="UNKNOWN_KEYWORD",
|
|
266
|
+
original=line,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
keyword = "note"
|
|
270
|
+
|
|
271
|
+
content, properties = _parse_content_and_properties(rest)
|
|
272
|
+
|
|
273
|
+
if keyword == "done":
|
|
274
|
+
keyword = "task"
|
|
275
|
+
properties["status"] = "done"
|
|
276
|
+
|
|
277
|
+
block_id = str(properties.pop("id", _generate_id()))
|
|
278
|
+
|
|
279
|
+
return IntentBlock(
|
|
280
|
+
id=block_id,
|
|
281
|
+
type=keyword,
|
|
282
|
+
content=_strip_inline(content),
|
|
283
|
+
original_content=content,
|
|
284
|
+
inline=_parse_inline(content),
|
|
285
|
+
properties=properties,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _parse_content_and_properties(rest: str) -> tuple[str, dict[str, Any]]:
|
|
290
|
+
parts = [p.strip() for p in rest.split("|")]
|
|
291
|
+
content = parts[0].strip() if parts else ""
|
|
292
|
+
properties: dict[str, Any] = {}
|
|
293
|
+
|
|
294
|
+
for part in parts[1:]:
|
|
295
|
+
kv_match = re.match(r"^(\w[\w-]*):\s*(.*)$", part.strip())
|
|
296
|
+
if not kv_match:
|
|
297
|
+
continue
|
|
298
|
+
key = kv_match.group(1)
|
|
299
|
+
value: Any = kv_match.group(2).strip()
|
|
300
|
+
|
|
301
|
+
if value.lower() in ("true", "false"):
|
|
302
|
+
value = value.lower() == "true"
|
|
303
|
+
elif key in ("max", "delay", "leading", "depth", "columns"):
|
|
304
|
+
try:
|
|
305
|
+
value = int(value) if "." not in value else float(value)
|
|
306
|
+
except ValueError:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
properties[key] = value
|
|
310
|
+
|
|
311
|
+
return content, properties
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _parse_inline(text: str) -> list[InlineSegment]:
|
|
315
|
+
segments: list[InlineSegment] = []
|
|
316
|
+
pattern = re.compile(
|
|
317
|
+
r"\*([^*]+)\*"
|
|
318
|
+
r"|_([^_]+)_"
|
|
319
|
+
r"|~([^~]+)~"
|
|
320
|
+
r"|\^([^^]+)\^"
|
|
321
|
+
r"|`([^`]+)`"
|
|
322
|
+
r"|\[([^\]]+)\]\(([^)]+)\)"
|
|
323
|
+
r"|\[\^(\d+)\]"
|
|
324
|
+
r"|@(\w+)"
|
|
325
|
+
r"|#(\w+)"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
last_end = 0
|
|
329
|
+
for match in pattern.finditer(text):
|
|
330
|
+
if match.start() > last_end:
|
|
331
|
+
segments.append(InlineSegment(type="text", value=text[last_end : match.start()]))
|
|
332
|
+
|
|
333
|
+
if match.group(1):
|
|
334
|
+
segments.append(InlineSegment(type="bold", value=match.group(1)))
|
|
335
|
+
elif match.group(2):
|
|
336
|
+
segments.append(InlineSegment(type="italic", value=match.group(2)))
|
|
337
|
+
elif match.group(3):
|
|
338
|
+
segments.append(InlineSegment(type="strikethrough", value=match.group(3)))
|
|
339
|
+
elif match.group(4):
|
|
340
|
+
segments.append(InlineSegment(type="highlight", value=match.group(4)))
|
|
341
|
+
elif match.group(5):
|
|
342
|
+
segments.append(InlineSegment(type="code", value=match.group(5)))
|
|
343
|
+
elif match.group(6):
|
|
344
|
+
segments.append(
|
|
345
|
+
InlineSegment(type="link", value=match.group(6), href=match.group(7))
|
|
346
|
+
)
|
|
347
|
+
elif match.group(8):
|
|
348
|
+
segments.append(InlineSegment(type="footnote-ref", value=match.group(8)))
|
|
349
|
+
elif match.group(9):
|
|
350
|
+
segments.append(InlineSegment(type="mention", value=match.group(9)))
|
|
351
|
+
elif match.group(10):
|
|
352
|
+
segments.append(InlineSegment(type="tag", value=match.group(10)))
|
|
353
|
+
|
|
354
|
+
last_end = match.end()
|
|
355
|
+
|
|
356
|
+
if last_end < len(text):
|
|
357
|
+
segments.append(InlineSegment(type="text", value=text[last_end:]))
|
|
358
|
+
|
|
359
|
+
return segments
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _strip_inline(text: str) -> str:
|
|
363
|
+
text = re.sub(r"\*([^*]+)\*", r"\1", text)
|
|
364
|
+
text = re.sub(r"_([^_]+)_", r"\1", text)
|
|
365
|
+
text = re.sub(r"~([^~]+)~", r"\1", text)
|
|
366
|
+
text = re.sub(r"\^([^^]+)\^", r"\1", text)
|
|
367
|
+
text = re.sub(r"`([^`]+)`", r"\1", text)
|
|
368
|
+
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
|
369
|
+
text = re.sub(r"\[\^(\d+)\]", r"[\1]", text)
|
|
370
|
+
return text
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _generate_id() -> str:
|
|
374
|
+
global _ID_COUNTER
|
|
375
|
+
_ID_COUNTER += 1
|
|
376
|
+
return f"b{_ID_COUNTER:04d}"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _update_metadata(metadata: IntentMetadata, block: IntentBlock) -> None:
|
|
380
|
+
if block.type == "title":
|
|
381
|
+
metadata.title = block.content
|
|
382
|
+
elif block.type == "summary":
|
|
383
|
+
metadata.summary = block.content
|
|
384
|
+
elif block.type == "agent":
|
|
385
|
+
metadata.agent = block.content
|
|
386
|
+
if "model" in block.properties:
|
|
387
|
+
metadata.model = str(block.properties["model"])
|
|
388
|
+
elif block.type == "context":
|
|
389
|
+
metadata.context.update({k: str(v) for k, v in block.properties.items()})
|
intenttext/query.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .types import IntentBlock, IntentDocument
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def query(
|
|
9
|
+
doc: IntentDocument,
|
|
10
|
+
type: Optional[str | list[str]] = None,
|
|
11
|
+
section: Optional[str] = None,
|
|
12
|
+
properties: Optional[dict[str, Any]] = None,
|
|
13
|
+
text: Optional[str] = None,
|
|
14
|
+
limit: Optional[int] = None,
|
|
15
|
+
) -> list[IntentBlock]:
|
|
16
|
+
results: list[IntentBlock] = []
|
|
17
|
+
current_section = ""
|
|
18
|
+
|
|
19
|
+
for block in doc.blocks:
|
|
20
|
+
if block.type == "section":
|
|
21
|
+
current_section = block.content
|
|
22
|
+
|
|
23
|
+
if type is not None:
|
|
24
|
+
if isinstance(type, str) and block.type != type:
|
|
25
|
+
continue
|
|
26
|
+
if isinstance(type, list) and block.type not in type:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if section is not None and current_section != section:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
if text is not None and text.lower() not in block.content.lower():
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if properties:
|
|
36
|
+
matches = True
|
|
37
|
+
for key, expected in properties.items():
|
|
38
|
+
actual = block.properties.get(key)
|
|
39
|
+
if callable(getattr(expected, "search", None)):
|
|
40
|
+
if actual is None or expected.search(str(actual)) is None:
|
|
41
|
+
matches = False
|
|
42
|
+
break
|
|
43
|
+
else:
|
|
44
|
+
if str(actual) != str(expected):
|
|
45
|
+
matches = False
|
|
46
|
+
break
|
|
47
|
+
if not matches:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
results.append(block)
|
|
51
|
+
|
|
52
|
+
if limit is not None and len(results) >= limit:
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
return results
|
intenttext/renderer.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
|
|
5
|
+
from .types import IntentBlock, IntentDocument
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BASE_CSS = """
|
|
9
|
+
.it-document { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.6; color: #222; }
|
|
10
|
+
.it-block { margin: 0.6rem 0; }
|
|
11
|
+
.it-section { margin-top: 1.4rem; }
|
|
12
|
+
.it-sub { margin-top: 1rem; }
|
|
13
|
+
.it-callout { padding: 0.75rem 1rem; border-radius: 6px; border-left: 4px solid; }
|
|
14
|
+
.it-callout.info { background: #eef6ff; border-color: #1d70b8; }
|
|
15
|
+
.it-callout.warning { background: #fff7e6; border-color: #b26a00; }
|
|
16
|
+
.it-callout.tip { background: #eefbf2; border-color: #1f7a3d; }
|
|
17
|
+
.it-callout.success { background: #eefbf2; border-color: #1f7a3d; }
|
|
18
|
+
.it-task { display: flex; gap: 0.5rem; }
|
|
19
|
+
.it-task.done { color: #666; text-decoration: line-through; }
|
|
20
|
+
.it-table { width: 100%; border-collapse: collapse; }
|
|
21
|
+
.it-table th, .it-table td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
|
|
22
|
+
.it-table th { background: #f5f5f5; }
|
|
23
|
+
.it-divider { border: 0; border-top: 1px solid #ddd; margin: 1rem 0; }
|
|
24
|
+
""".strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_html(doc: IntentDocument, include_css: bool = True) -> str:
|
|
28
|
+
parts: list[str] = []
|
|
29
|
+
if include_css:
|
|
30
|
+
parts.append(f"<style>{BASE_CSS}</style>")
|
|
31
|
+
|
|
32
|
+
parts.append('<div class="it-document">')
|
|
33
|
+
for block in doc.blocks:
|
|
34
|
+
parts.append(_render_block_html(block))
|
|
35
|
+
parts.append("</div>")
|
|
36
|
+
|
|
37
|
+
return "\n".join(p for p in parts if p)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def render_print(doc: IntentDocument) -> str:
|
|
41
|
+
css = (
|
|
42
|
+
BASE_CSS
|
|
43
|
+
+ "\n@media print { .it-document { font-family: Georgia, serif; } .it-block { break-inside: avoid; } }"
|
|
44
|
+
)
|
|
45
|
+
body = render_html(doc, include_css=False)
|
|
46
|
+
return (
|
|
47
|
+
"<!doctype html><html><head><meta charset=\"utf-8\"/>"
|
|
48
|
+
f"<style>{css}</style></head><body>{body}</body></html>"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_markdown(doc: IntentDocument) -> str:
|
|
53
|
+
lines: list[str] = []
|
|
54
|
+
|
|
55
|
+
for block in doc.blocks:
|
|
56
|
+
t = block.type
|
|
57
|
+
content = block.original_content or block.content
|
|
58
|
+
|
|
59
|
+
if t == "title":
|
|
60
|
+
lines.append(f"# {content}")
|
|
61
|
+
elif t == "section":
|
|
62
|
+
lines.append(f"## {content}")
|
|
63
|
+
elif t == "sub":
|
|
64
|
+
lines.append(f"### {content}")
|
|
65
|
+
elif t == "note":
|
|
66
|
+
lines.append(content)
|
|
67
|
+
elif t == "task":
|
|
68
|
+
checked = str(block.properties.get("status", "")).lower() == "done"
|
|
69
|
+
lines.append(f"- [{'x' if checked else ' '}] {content}")
|
|
70
|
+
elif t == "quote":
|
|
71
|
+
author = block.properties.get("by")
|
|
72
|
+
lines.append(f"> {content}")
|
|
73
|
+
if author:
|
|
74
|
+
lines.append(f"> - {author}")
|
|
75
|
+
elif t in {"info", "warning", "tip"}:
|
|
76
|
+
lines.append(f"> **{t.upper()}:** {content}")
|
|
77
|
+
elif t == "table":
|
|
78
|
+
rows = block.properties.get("rows", [])
|
|
79
|
+
if rows:
|
|
80
|
+
header = rows[0]
|
|
81
|
+
lines.append("| " + " | ".join(str(v) for v in header) + " |")
|
|
82
|
+
lines.append("| " + " | ".join("---" for _ in header) + " |")
|
|
83
|
+
for row in rows[1:]:
|
|
84
|
+
lines.append("| " + " | ".join(str(v) for v in row) + " |")
|
|
85
|
+
elif t == "divider":
|
|
86
|
+
lines.append("---")
|
|
87
|
+
else:
|
|
88
|
+
lines.append(f"{content}")
|
|
89
|
+
|
|
90
|
+
if lines and lines[-1] != "":
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
return "\n".join(lines).strip() + "\n"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _render_block_html(block: IntentBlock) -> str:
|
|
97
|
+
t = block.type
|
|
98
|
+
content = html.escape(block.content)
|
|
99
|
+
|
|
100
|
+
if t == "title":
|
|
101
|
+
return f'<h1 class="it-block it-title">{content}</h1>'
|
|
102
|
+
if t == "summary":
|
|
103
|
+
return f'<p class="it-block it-summary">{content}</p>'
|
|
104
|
+
if t == "section":
|
|
105
|
+
return f'<h2 class="it-block it-section">{content}</h2>'
|
|
106
|
+
if t == "sub":
|
|
107
|
+
return f'<h3 class="it-block it-sub">{content}</h3>'
|
|
108
|
+
if t == "quote":
|
|
109
|
+
by = block.properties.get("by")
|
|
110
|
+
footer = f"<cite>{html.escape(str(by))}</cite>" if by else ""
|
|
111
|
+
return f'<blockquote class="it-block it-quote"><p>{content}</p>{footer}</blockquote>'
|
|
112
|
+
if t in {"note", "info", "warning", "tip", "success"}:
|
|
113
|
+
extra = "it-callout " + (t if t != "note" else "info")
|
|
114
|
+
return f'<div class="it-block {extra}"><span class="it-keyword">{t}:</span> {content}</div>'
|
|
115
|
+
if t == "task":
|
|
116
|
+
done = str(block.properties.get("status", "")).lower() == "done"
|
|
117
|
+
mark = "[x]" if done else "[ ]"
|
|
118
|
+
done_cls = " done" if done else ""
|
|
119
|
+
return f'<div class="it-block it-task{done_cls}"><span>{mark}</span><span>{content}</span></div>'
|
|
120
|
+
if t == "image":
|
|
121
|
+
src = html.escape(str(block.properties.get("src", "")))
|
|
122
|
+
alt = html.escape(str(block.properties.get("alt", block.content)))
|
|
123
|
+
return f'<figure class="it-block it-image"><img src="{src}" alt="{alt}"/></figure>'
|
|
124
|
+
if t == "link":
|
|
125
|
+
href = html.escape(str(block.properties.get("href", "#")))
|
|
126
|
+
return f'<p class="it-block it-link"><a href="{href}">{content}</a></p>'
|
|
127
|
+
if t == "code":
|
|
128
|
+
return f'<pre class="it-block it-code"><code>{content}</code></pre>'
|
|
129
|
+
if t == "table":
|
|
130
|
+
rows = block.properties.get("rows", [])
|
|
131
|
+
if not rows:
|
|
132
|
+
return ""
|
|
133
|
+
header = rows[0]
|
|
134
|
+
body_rows = rows[1:]
|
|
135
|
+
thead = "".join(f"<th>{html.escape(str(c))}</th>" for c in header)
|
|
136
|
+
tbody = "".join(
|
|
137
|
+
"<tr>" + "".join(f"<td>{html.escape(str(c))}</td>" for c in row) + "</tr>"
|
|
138
|
+
for row in body_rows
|
|
139
|
+
)
|
|
140
|
+
return f'<table class="it-block it-table"><thead><tr>{thead}</tr></thead><tbody>{tbody}</tbody></table>'
|
|
141
|
+
if t == "divider":
|
|
142
|
+
return '<hr class="it-block it-divider"/>'
|
|
143
|
+
|
|
144
|
+
return f'<p class="it-block it-{html.escape(t)}">{content}</p>'
|
intenttext/source.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .types import IntentDocument
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def to_source(doc: IntentDocument) -> str:
|
|
7
|
+
lines: list[str] = []
|
|
8
|
+
|
|
9
|
+
for block in doc.blocks:
|
|
10
|
+
if block.type == "divider":
|
|
11
|
+
lines.append("---")
|
|
12
|
+
continue
|
|
13
|
+
|
|
14
|
+
if block.type == "table":
|
|
15
|
+
for row in block.properties.get("rows", []):
|
|
16
|
+
lines.append("| " + " | ".join(str(c) for c in row) + " |")
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
keyword = block.type
|
|
20
|
+
props = dict(block.properties)
|
|
21
|
+
|
|
22
|
+
if block.type == "task" and str(props.get("status", "")).lower() == "done":
|
|
23
|
+
keyword = "done"
|
|
24
|
+
props.pop("status", None)
|
|
25
|
+
|
|
26
|
+
parts = [f"{keyword}: {block.original_content or block.content}".rstrip()]
|
|
27
|
+
for key, value in props.items():
|
|
28
|
+
parts.append(f"{key}: {value}")
|
|
29
|
+
|
|
30
|
+
lines.append(" | ".join(parts))
|
|
31
|
+
|
|
32
|
+
return "\n".join(lines)
|
intenttext/types.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class InlineSegment:
|
|
9
|
+
type: str
|
|
10
|
+
value: str
|
|
11
|
+
href: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class IntentBlock:
|
|
16
|
+
id: str
|
|
17
|
+
type: str
|
|
18
|
+
content: str
|
|
19
|
+
original_content: str
|
|
20
|
+
inline: list[InlineSegment] = field(default_factory=list)
|
|
21
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class IntentMetadata:
|
|
26
|
+
title: Optional[str] = None
|
|
27
|
+
summary: Optional[str] = None
|
|
28
|
+
agent: Optional[str] = None
|
|
29
|
+
model: Optional[str] = None
|
|
30
|
+
language: str = "ltr"
|
|
31
|
+
context: dict[str, str] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class IntentDocument:
|
|
36
|
+
version: str
|
|
37
|
+
blocks: list[IntentBlock]
|
|
38
|
+
metadata: IntentMetadata = field(default_factory=IntentMetadata)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ParseWarning:
|
|
43
|
+
line: int
|
|
44
|
+
message: str
|
|
45
|
+
code: str
|
|
46
|
+
original: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ParseResult:
|
|
51
|
+
document: IntentDocument
|
|
52
|
+
warnings: list[ParseWarning] = field(default_factory=list)
|
|
53
|
+
errors: list[ParseWarning] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ValidationIssue:
|
|
58
|
+
block_id: str
|
|
59
|
+
block_type: str
|
|
60
|
+
type: str
|
|
61
|
+
code: str
|
|
62
|
+
message: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ValidationResult:
|
|
67
|
+
valid: bool
|
|
68
|
+
issues: list[ValidationIssue] = field(default_factory=list)
|
intenttext/validate.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from .types import IntentDocument, ValidationIssue, ValidationResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate(doc: IntentDocument) -> ValidationResult:
|
|
10
|
+
issues: list[ValidationIssue] = []
|
|
11
|
+
|
|
12
|
+
step_ids = {
|
|
13
|
+
block.id
|
|
14
|
+
for block in doc.blocks
|
|
15
|
+
if block.type in {"step", "decision", "parallel", "gate", "call", "result"}
|
|
16
|
+
}
|
|
17
|
+
all_ids = [block.id for block in doc.blocks]
|
|
18
|
+
|
|
19
|
+
# Duplicate block IDs
|
|
20
|
+
seen: set[str] = set()
|
|
21
|
+
for block_id in all_ids:
|
|
22
|
+
if block_id in seen:
|
|
23
|
+
issues.append(
|
|
24
|
+
ValidationIssue(
|
|
25
|
+
block_id=block_id,
|
|
26
|
+
block_type="document",
|
|
27
|
+
type="error",
|
|
28
|
+
code="DUPLICATE_ID",
|
|
29
|
+
message=f"Duplicate block id '{block_id}'",
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
seen.add(block_id)
|
|
33
|
+
|
|
34
|
+
context_vars = set(doc.metadata.context.keys())
|
|
35
|
+
produced_vars = {
|
|
36
|
+
str(block.properties.get("output"))
|
|
37
|
+
for block in doc.blocks
|
|
38
|
+
if "output" in block.properties and str(block.properties.get("output"))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for block in doc.blocks:
|
|
42
|
+
if block.type == "gate" and not block.properties.get("approver"):
|
|
43
|
+
issues.append(
|
|
44
|
+
ValidationIssue(
|
|
45
|
+
block_id=block.id,
|
|
46
|
+
block_type=block.type,
|
|
47
|
+
type="error",
|
|
48
|
+
code="GATE_APPROVER_REQUIRED",
|
|
49
|
+
message="gate: block requires approver property",
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
depends = block.properties.get("depends")
|
|
54
|
+
if isinstance(depends, str) and depends and depends not in step_ids:
|
|
55
|
+
issues.append(
|
|
56
|
+
ValidationIssue(
|
|
57
|
+
block_id=block.id,
|
|
58
|
+
block_type=block.type,
|
|
59
|
+
type="error",
|
|
60
|
+
code="STEP_REF_MISSING",
|
|
61
|
+
message=f"depends references missing step '{depends}'",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if block.type == "decision":
|
|
66
|
+
for key in ("then", "else"):
|
|
67
|
+
ref = block.properties.get(key)
|
|
68
|
+
if isinstance(ref, str) and ref and ref not in step_ids:
|
|
69
|
+
issues.append(
|
|
70
|
+
ValidationIssue(
|
|
71
|
+
block_id=block.id,
|
|
72
|
+
block_type=block.type,
|
|
73
|
+
type="error",
|
|
74
|
+
code="STEP_REF_MISSING",
|
|
75
|
+
message=f"decision {key} references missing step '{ref}'",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if block.type == "parallel":
|
|
80
|
+
steps = block.properties.get("steps")
|
|
81
|
+
if isinstance(steps, str):
|
|
82
|
+
for ref in [s.strip() for s in steps.split(",") if s.strip()]:
|
|
83
|
+
if ref not in step_ids:
|
|
84
|
+
issues.append(
|
|
85
|
+
ValidationIssue(
|
|
86
|
+
block_id=block.id,
|
|
87
|
+
block_type=block.type,
|
|
88
|
+
type="error",
|
|
89
|
+
code="STEP_REF_MISSING",
|
|
90
|
+
message=f"parallel steps references missing step '{ref}'",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
for var in _extract_vars(block):
|
|
95
|
+
# runtime placeholders are intentionally unresolved
|
|
96
|
+
if var in {"page", "pages"}:
|
|
97
|
+
continue
|
|
98
|
+
top = var.split(".")[0]
|
|
99
|
+
if top not in context_vars and top not in produced_vars:
|
|
100
|
+
issues.append(
|
|
101
|
+
ValidationIssue(
|
|
102
|
+
block_id=block.id,
|
|
103
|
+
block_type=block.type,
|
|
104
|
+
type="warning",
|
|
105
|
+
code="VARIABLE_UNRESOLVED",
|
|
106
|
+
message=f"Variable '{{{{{var}}}}}' is not declared in context: or step output",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
has_errors = any(issue.type == "error" for issue in issues)
|
|
111
|
+
return ValidationResult(valid=not has_errors, issues=issues)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract_vars(block) -> Iterable[str]:
|
|
115
|
+
pattern = re.compile(r"\{\{([^}]+)\}\}")
|
|
116
|
+
values = [block.content, block.original_content]
|
|
117
|
+
values.extend(str(v) for v in block.properties.values() if isinstance(v, str))
|
|
118
|
+
|
|
119
|
+
for value in values:
|
|
120
|
+
for match in pattern.finditer(value):
|
|
121
|
+
yield match.group(1).strip()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intenttext
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: IntentText - the semantic document format that is natively JSON
|
|
5
|
+
Project-URL: Homepage, https://github.com/intenttext/IntentText
|
|
6
|
+
Project-URL: Repository, https://github.com/intenttext/intenttext-python
|
|
7
|
+
Project-URL: Documentation, https://github.com/intenttext/IntentText/blob/main/docs/SPEC.md
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# IntentText Python
|
|
16
|
+
|
|
17
|
+
Python implementation of the IntentText parser and renderer.
|
|
18
|
+
|
|
19
|
+
Independent implementation (not a Node.js wrapper), designed for Python workflows and AI stacks.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install intenttext
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from intenttext import parse, render_html, merge_data, validate, query
|
|
31
|
+
|
|
32
|
+
# Parse a document
|
|
33
|
+
source = """
|
|
34
|
+
title: Sprint Planning
|
|
35
|
+
section: Tasks
|
|
36
|
+
task: Write tests | owner: Ahmed | due: Friday
|
|
37
|
+
task: Deploy to staging | owner: Sarah | due: Monday
|
|
38
|
+
gate: Final approval | approver: Lead | timeout: 24h
|
|
39
|
+
""".strip()
|
|
40
|
+
|
|
41
|
+
doc = parse(source)
|
|
42
|
+
|
|
43
|
+
# Query for tasks
|
|
44
|
+
tasks = query(doc, type="task")
|
|
45
|
+
for task in tasks:
|
|
46
|
+
print(f"{task.content} -> {task.properties.get('owner', 'unassigned')}")
|
|
47
|
+
|
|
48
|
+
# Validate workflow semantics
|
|
49
|
+
result = validate(doc)
|
|
50
|
+
if not result.valid:
|
|
51
|
+
for issue in result.issues:
|
|
52
|
+
print(f"[{issue.type.upper()}] {issue.message}")
|
|
53
|
+
|
|
54
|
+
# Render to HTML
|
|
55
|
+
html = render_html(doc)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
- `parse(source: str) -> IntentDocument`
|
|
61
|
+
- `parse_safe(source: str, ...) -> ParseResult`
|
|
62
|
+
- `render_html(doc: IntentDocument, include_css: bool = True) -> str`
|
|
63
|
+
- `render_print(doc: IntentDocument) -> str`
|
|
64
|
+
- `render_markdown(doc: IntentDocument) -> str`
|
|
65
|
+
- `merge_data(template: IntentDocument, data: dict) -> IntentDocument`
|
|
66
|
+
- `parse_and_merge(template_source: str, data: dict) -> IntentDocument`
|
|
67
|
+
- `validate(doc: IntentDocument) -> ValidationResult`
|
|
68
|
+
- `query(doc: IntentDocument, ...) -> list[IntentBlock]`
|
|
69
|
+
- `to_source(doc: IntentDocument) -> str`
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e .[dev]
|
|
75
|
+
pytest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Release (PyPI)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 1) Ensure tests pass
|
|
82
|
+
python3 -m pytest -q
|
|
83
|
+
|
|
84
|
+
# 2) Build source + wheel
|
|
85
|
+
python3 -m pip install -U hatch twine
|
|
86
|
+
hatch build
|
|
87
|
+
|
|
88
|
+
# 3) Validate package metadata and long description
|
|
89
|
+
twine check dist/*
|
|
90
|
+
|
|
91
|
+
# 4) Upload (interactive)
|
|
92
|
+
twine upload dist/*
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Tag-based release flow:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git tag v1.0.0
|
|
99
|
+
git push origin v1.0.0
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
GitHub Action publish workflow uses `PYPI_API_TOKEN` for automated release on `v*` tags.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
intenttext/__init__.py,sha256=2aztUfxzJzY631dzrU4J3Ra-8U-Ayn5qnGub5lJ5c5Y,801
|
|
2
|
+
intenttext/merge.py,sha256=_yO0LXaoXPKQ5nEAoCBYggMaPwuZYCM724IMVGogkAw,2525
|
|
3
|
+
intenttext/parser.py,sha256=1wGxeHcXSk6F66H7k5_vW3rGua__3l89W3qwnKSovKA,10592
|
|
4
|
+
intenttext/query.py,sha256=LpyuaT6mMBGj1iNLYay8AMTsdrqTCjB9aLZLC_E4K4Q,1624
|
|
5
|
+
intenttext/renderer.py,sha256=E8gYJeSBXDdarb_nbKccFtiG4Gpk05ZvRMj_NqBHSuw,5704
|
|
6
|
+
intenttext/source.py,sha256=wkhaEhfHPSYwSLPvIqWmz9TRoNyIgXivt9HEdSoMN8A,909
|
|
7
|
+
intenttext/types.py,sha256=okWFdR-hdTPG8i3owMUn8fWXxKmQ6C9H4LTmjUXh-bY,1345
|
|
8
|
+
intenttext/validate.py,sha256=RJ2DfUpO-Tzl-dPfN8X8AqP-LGNprxAq59GTKXmtD10,4459
|
|
9
|
+
intenttext-1.0.0.dist-info/METADATA,sha256=5IDUqVHA5S_vozjHejSPhvSd261jzSWuK0BFWp8XX9o,2544
|
|
10
|
+
intenttext-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
intenttext-1.0.0.dist-info/RECORD,,
|