markdown-flow 0.4.0__tar.gz → 0.4.1__tar.gz
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.
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/PKG-INFO +7 -3
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/README.md +6 -2
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/__init__.py +1 -1
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/constants.py +13 -0
- markdown_flow-0.4.1/markdown_flow/parser/document_blocks.py +357 -0
- markdown_flow-0.4.1/markdown_flow/parser/document_context.py +447 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/preprocessor.py +38 -31
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow.egg-info/PKG-INFO +7 -3
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow.egg-info/SOURCES.txt +1 -0
- markdown_flow-0.4.0/markdown_flow/parser/document_context.py +0 -131
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/LICENSE +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/constants_system_prompt.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/core.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/enums.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/escaping.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/exceptions.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/__init__.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/classifier.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/format.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/patterns.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/sanitize.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/stream.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/formatter/types.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/higher_priority_prompt.md +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/llm.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/models.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/__init__.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/code_fence_utils.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/html_comment_utils.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/interaction.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/json_parser.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/output.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/validation.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/parser/variable.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/providers/__init__.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/providers/config.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/providers/openai.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/system_prompt.md +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/tag_filter.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow/variable_names.py +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow.egg-info/dependency_links.txt +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/markdown_flow.egg-info/top_level.txt +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/pyproject.toml +0 -0
- {markdown_flow-0.4.0 → markdown_flow-0.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: An agent library designed to parse and process MarkdownFlow documents
|
|
5
5
|
Project-URL: Homepage, https://github.com/ai-shifu/markdown-flow-agent-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ai-shifu/markdown-flow-agent-py/issues
|
|
@@ -708,8 +708,12 @@ literal. Only a leading assignment marker sets the interaction's variable;
|
|
|
708
708
|
markers elsewhere in its body remain literal option text. An unterminated
|
|
709
709
|
marker keeps the remaining text opaque, without restarting at an inner `?[`.
|
|
710
710
|
|
|
711
|
-
At document level, complete inline code
|
|
712
|
-
|
|
711
|
+
At document level, complete inline code, inline math, link resources, indented
|
|
712
|
+
code, and raw-text HTML bodies (`script`, `style`, `pre`, `textarea`) bound local
|
|
713
|
+
variable examples. Quote/list-contained fences and ordinary HTML blocks also keep
|
|
714
|
+
unfinished examples local. Reference-definition destinations and titles retain
|
|
715
|
+
their complete variable examples. Physical CRLF, LF and CR boundaries preserve
|
|
716
|
+
original offsets and marker text. Complete variable examples there still support collection and replacement,
|
|
713
717
|
but an unclosed example cannot consume text beyond that markup span. Escaped
|
|
714
718
|
document openers stay literal. A true raw marker owns its entire name before any
|
|
715
719
|
code or HTML inside that name is considered; interaction bodies keep the raw
|
|
@@ -690,8 +690,12 @@ literal. Only a leading assignment marker sets the interaction's variable;
|
|
|
690
690
|
markers elsewhere in its body remain literal option text. An unterminated
|
|
691
691
|
marker keeps the remaining text opaque, without restarting at an inner `?[`.
|
|
692
692
|
|
|
693
|
-
At document level, complete inline code
|
|
694
|
-
|
|
693
|
+
At document level, complete inline code, inline math, link resources, indented
|
|
694
|
+
code, and raw-text HTML bodies (`script`, `style`, `pre`, `textarea`) bound local
|
|
695
|
+
variable examples. Quote/list-contained fences and ordinary HTML blocks also keep
|
|
696
|
+
unfinished examples local. Reference-definition destinations and titles retain
|
|
697
|
+
their complete variable examples. Physical CRLF, LF and CR boundaries preserve
|
|
698
|
+
original offsets and marker text. Complete variable examples there still support collection and replacement,
|
|
695
699
|
but an unclosed example cannot consume text beyond that markup span. Escaped
|
|
696
700
|
document openers stay literal. A true raw marker owns its entire name before any
|
|
697
701
|
code or HTML inside that name is considered; interaction bodies keep the raw
|
|
@@ -48,6 +48,19 @@ COMPILED_INLINE_HTML_REGEX = re.compile(
|
|
|
48
48
|
r"|</[A-Za-z][A-Za-z0-9-]*" + _HTML_SPACE + r"*>"
|
|
49
49
|
r"|<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<![A-Z]+" + _HTML_SPACE + r"+[^>]*>|<!\[CDATA\[[\s\S]*?\]\]>)"
|
|
50
50
|
)
|
|
51
|
+
COMPILED_INDENTED_CODE_REGEX = re.compile(r"(?: {4}| {0,3}\t)")
|
|
52
|
+
COMPILED_RAW_HTML_START_REGEX = re.compile(r" {0,3}<(script|style|pre|textarea)(?=[ \t\r\n\f>])", re.IGNORECASE)
|
|
53
|
+
COMPILED_RAW_HTML_END_REGEX = {name: re.compile(r"</" + name + r"[ \t\r\n]*>", re.IGNORECASE) for name in ("script", "style", "pre", "textarea")}
|
|
54
|
+
COMPILED_DOLLAR_RUN_REGEX = re.compile(r"\$+")
|
|
55
|
+
COMPILED_BLANK_LINE_REGEX = re.compile(r"(?:\r\n|\r(?!\n)|(?<!\r)\n)[ \t]*(?:\r\n|\r(?!\n)|(?<!\r)\n)")
|
|
56
|
+
COMPILED_ATX_START_REGEX = re.compile(r"^ {0,3}#{1,6}(?=[ \t\r\n]|$)", re.MULTILINE)
|
|
57
|
+
COMPILED_PHYSICAL_LINE_END_REGEX = re.compile(r"\r\n|\r|\n")
|
|
58
|
+
COMPILED_CONTAINER_LIST_REGEX = re.compile(r"(?:[-+*]|[0-9]{1,9}[.)])(?=[ \t])")
|
|
59
|
+
COMPILED_DOCUMENT_FENCE_REGEX = re.compile(r"(`{3,})([^`]*$)|(~{3,})(.*$)")
|
|
60
|
+
COMPILED_DOCUMENT_TAG_REGEX = re.compile(r" {0,3}</?([A-Za-z][A-Za-z0-9-]*)(?=[ \t/>]|$)")
|
|
61
|
+
COMPILED_REFERENCE_PREFIX_REGEX = re.compile(r" {0,3}\[((?:\\[^\r\n]|[^\]\\\r\n])+)]\:[ \t]*")
|
|
62
|
+
COMPILED_DOCUMENT_BREAK_REGEX = re.compile(r" {0,3}(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,})$")
|
|
63
|
+
COMPILED_SETEXT_REGEX = re.compile(r" {0,3}(?:=+|-+)[ \t]*$")
|
|
51
64
|
|
|
52
65
|
# Canonical user_input key for non-assignment interactions (?[A|B] without a
|
|
53
66
|
# variable). Clients should submit {DEFAULT_NON_ASSIGNMENT_INPUT_KEY: values};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Physical Markdown block boundaries with original source offsets."""
|
|
2
|
+
|
|
3
|
+
from bisect import bisect_right
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from re import Pattern
|
|
7
|
+
|
|
8
|
+
from ..constants import (
|
|
9
|
+
COMPILED_ATX_START_REGEX,
|
|
10
|
+
COMPILED_CONTAINER_LIST_REGEX,
|
|
11
|
+
COMPILED_DOCUMENT_BREAK_REGEX,
|
|
12
|
+
COMPILED_DOCUMENT_FENCE_REGEX,
|
|
13
|
+
COMPILED_DOCUMENT_TAG_REGEX,
|
|
14
|
+
COMPILED_PHYSICAL_LINE_END_REGEX,
|
|
15
|
+
COMPILED_REFERENCE_PREFIX_REGEX,
|
|
16
|
+
COMPILED_SETEXT_REGEX,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
HTML_BLOCK_TAGS = frozenset(
|
|
21
|
+
[
|
|
22
|
+
"address",
|
|
23
|
+
"article",
|
|
24
|
+
"aside",
|
|
25
|
+
"base",
|
|
26
|
+
"basefont",
|
|
27
|
+
"blockquote",
|
|
28
|
+
"body",
|
|
29
|
+
"caption",
|
|
30
|
+
"center",
|
|
31
|
+
"col",
|
|
32
|
+
"colgroup",
|
|
33
|
+
"dd",
|
|
34
|
+
"details",
|
|
35
|
+
"dialog",
|
|
36
|
+
"dir",
|
|
37
|
+
"div",
|
|
38
|
+
"dl",
|
|
39
|
+
"dt",
|
|
40
|
+
"fieldset",
|
|
41
|
+
"figcaption",
|
|
42
|
+
"figure",
|
|
43
|
+
"footer",
|
|
44
|
+
"form",
|
|
45
|
+
"frame",
|
|
46
|
+
"frameset",
|
|
47
|
+
"h1",
|
|
48
|
+
"h2",
|
|
49
|
+
"h3",
|
|
50
|
+
"h4",
|
|
51
|
+
"h5",
|
|
52
|
+
"h6",
|
|
53
|
+
"head",
|
|
54
|
+
"header",
|
|
55
|
+
"hr",
|
|
56
|
+
"html",
|
|
57
|
+
"iframe",
|
|
58
|
+
"legend",
|
|
59
|
+
"li",
|
|
60
|
+
"link",
|
|
61
|
+
"main",
|
|
62
|
+
"menu",
|
|
63
|
+
"menuitem",
|
|
64
|
+
"nav",
|
|
65
|
+
"noframes",
|
|
66
|
+
"ol",
|
|
67
|
+
"optgroup",
|
|
68
|
+
"option",
|
|
69
|
+
"p",
|
|
70
|
+
"param",
|
|
71
|
+
"search",
|
|
72
|
+
"section",
|
|
73
|
+
"summary",
|
|
74
|
+
"table",
|
|
75
|
+
"tbody",
|
|
76
|
+
"td",
|
|
77
|
+
"tfoot",
|
|
78
|
+
"th",
|
|
79
|
+
"thead",
|
|
80
|
+
"title",
|
|
81
|
+
"tr",
|
|
82
|
+
"track",
|
|
83
|
+
"ul",
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class PhysicalLine:
|
|
90
|
+
"""One CRLF, LF or CR line in the original source."""
|
|
91
|
+
|
|
92
|
+
start: int
|
|
93
|
+
content_end: int
|
|
94
|
+
end: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def physical_lines(text: str) -> list[PhysicalLine]:
|
|
98
|
+
"""Index physical Markdown lines without normalizing marker text."""
|
|
99
|
+
lines: list[PhysicalLine] = []
|
|
100
|
+
start = 0
|
|
101
|
+
for ending in COMPILED_PHYSICAL_LINE_END_REGEX.finditer(text):
|
|
102
|
+
lines.append(PhysicalLine(start, ending.start(), ending.end()))
|
|
103
|
+
start = ending.end()
|
|
104
|
+
lines.append(PhysicalLine(start, len(text), len(text)))
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def container_opening(line: str) -> tuple[int, tuple[int, ...]]:
|
|
109
|
+
"""Read only explicit quote/list prefixes, leaving ordinary indentation."""
|
|
110
|
+
at = 0
|
|
111
|
+
steps: list[int] = []
|
|
112
|
+
while at < len(line):
|
|
113
|
+
before_space = at
|
|
114
|
+
while at - before_space < 3 and at < len(line) and line[at] == " ":
|
|
115
|
+
at += 1
|
|
116
|
+
if at < len(line) and line[at] == ">":
|
|
117
|
+
steps.append(0)
|
|
118
|
+
at += 1
|
|
119
|
+
if at < len(line) and line[at] in " \t":
|
|
120
|
+
at += 1
|
|
121
|
+
continue
|
|
122
|
+
marker = COMPILED_CONTAINER_LIST_REGEX.match(line, at)
|
|
123
|
+
if marker is None:
|
|
124
|
+
return before_space, tuple(steps)
|
|
125
|
+
at = marker.end()
|
|
126
|
+
padding_start = at
|
|
127
|
+
while at < len(line) and line[at] in " \t":
|
|
128
|
+
at += 1
|
|
129
|
+
if at - padding_start > 4:
|
|
130
|
+
at = padding_start + 1
|
|
131
|
+
columns = 0
|
|
132
|
+
for character in line[before_space:at]:
|
|
133
|
+
columns += 4 - columns % 4 if character == "\t" else 1
|
|
134
|
+
steps.append(columns)
|
|
135
|
+
return at, tuple(steps)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def fence_opening(line: str) -> tuple[str, int, tuple[int, ...]] | None:
|
|
139
|
+
"""Use zero for a quote step and a column count for a list step."""
|
|
140
|
+
at, steps = container_opening(line)
|
|
141
|
+
before = at
|
|
142
|
+
while at - before < 3 and at < len(line) and line[at] == " ":
|
|
143
|
+
at += 1
|
|
144
|
+
fence = COMPILED_DOCUMENT_FENCE_REGEX.match(line, at)
|
|
145
|
+
if fence is None:
|
|
146
|
+
return None
|
|
147
|
+
run = fence.group(1) or fence.group(3)
|
|
148
|
+
return run[0], len(run), steps
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def container_content_start(line: str, steps: tuple[int, ...], *, partial: bool = False) -> int | None:
|
|
152
|
+
"""Match the opening quote/list containers on a continuation line."""
|
|
153
|
+
at = 0
|
|
154
|
+
for step in steps:
|
|
155
|
+
if step == 0:
|
|
156
|
+
before = at
|
|
157
|
+
while at - before < 3 and at < len(line) and line[at] == " ":
|
|
158
|
+
at += 1
|
|
159
|
+
if at >= len(line) or line[at] != ">":
|
|
160
|
+
return at if partial and at == len(line) else None
|
|
161
|
+
at += 1
|
|
162
|
+
if at < len(line) and line[at] in " \t":
|
|
163
|
+
at += 1
|
|
164
|
+
else:
|
|
165
|
+
columns = 0
|
|
166
|
+
while columns < step:
|
|
167
|
+
if at >= len(line) or line[at] not in " \t":
|
|
168
|
+
return at if partial and at == len(line) else None
|
|
169
|
+
columns += 4 - columns % 4 if line[at] == "\t" else 1
|
|
170
|
+
at += 1
|
|
171
|
+
return at
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class BlockSpan:
|
|
176
|
+
"""An owned literal region and its local example boundaries."""
|
|
177
|
+
|
|
178
|
+
end: int
|
|
179
|
+
opening_end: int
|
|
180
|
+
body_end: int
|
|
181
|
+
marker: str = ""
|
|
182
|
+
size: int = 0
|
|
183
|
+
steps: tuple[int, ...] = ()
|
|
184
|
+
closed: bool = False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class DocumentBlocks:
|
|
188
|
+
"""Read the block boundaries needed by variable document scanners."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, text: str, html: Pattern[str]) -> None:
|
|
191
|
+
"""Index once using the caller's existing native HTML grammar."""
|
|
192
|
+
self.text = text
|
|
193
|
+
self.html_pattern = html
|
|
194
|
+
self.lines = physical_lines(text)
|
|
195
|
+
self.closed_fence_lines: set[int] = set()
|
|
196
|
+
self.container_paragraphs = self._container_paragraphs()
|
|
197
|
+
self.starts = [line.start for line in self.lines]
|
|
198
|
+
self.by_start = {line.start: index for index, line in enumerate(self.lines)}
|
|
199
|
+
|
|
200
|
+
def line_at(self, offset: int) -> PhysicalLine:
|
|
201
|
+
"""Return the original physical line containing an offset."""
|
|
202
|
+
return self.lines[max(0, bisect_right(self.starts, offset) - 1)]
|
|
203
|
+
|
|
204
|
+
def blank(self, index: int) -> bool:
|
|
205
|
+
"""Test Markdown blank padding rather than all Unicode whitespace."""
|
|
206
|
+
line = self.lines[index]
|
|
207
|
+
return not self.text[line.start : line.content_end].strip(" \t")
|
|
208
|
+
|
|
209
|
+
def fence(self, start: int, *, final: bool = True) -> BlockSpan | None:
|
|
210
|
+
"""Read plain or quote/list-contained fenced code."""
|
|
211
|
+
index = self.by_start.get(start)
|
|
212
|
+
if index is None:
|
|
213
|
+
return None
|
|
214
|
+
first = self.lines[index]
|
|
215
|
+
opening = fence_opening(self.text[start : first.content_end])
|
|
216
|
+
if opening is None:
|
|
217
|
+
return None
|
|
218
|
+
marker, size, steps = opening
|
|
219
|
+
previous = first
|
|
220
|
+
for line in self.lines[index + 1 :]:
|
|
221
|
+
value = self.text[line.start : line.content_end]
|
|
222
|
+
content = container_content_start(value, steps)
|
|
223
|
+
if content is None:
|
|
224
|
+
if not final and line.end == line.content_end == len(self.text) and container_content_start(value, steps, partial=True) is not None:
|
|
225
|
+
return BlockSpan(len(self.text), first.end, len(self.text), marker, size, steps)
|
|
226
|
+
if not value.strip(" \t"):
|
|
227
|
+
continue
|
|
228
|
+
return BlockSpan(previous.content_end, first.end, previous.content_end, marker, size, steps)
|
|
229
|
+
candidate = value[content:].lstrip(" ")
|
|
230
|
+
if len(value[content:]) - len(candidate) <= 3:
|
|
231
|
+
run = candidate.rstrip(" \t")
|
|
232
|
+
if len(run) >= size and run == marker * len(run):
|
|
233
|
+
self.closed_fence_lines.add(line.start)
|
|
234
|
+
return BlockSpan(line.content_end, first.end, line.start, marker, size, steps, closed=True)
|
|
235
|
+
previous = line
|
|
236
|
+
return BlockSpan(len(self.text), first.end, len(self.text), marker, size, steps)
|
|
237
|
+
|
|
238
|
+
def ordinary_html(self, start: int) -> BlockSpan | None:
|
|
239
|
+
"""Bound native type-six/seven HTML blocks at the first blank line."""
|
|
240
|
+
index = self.by_start.get(start)
|
|
241
|
+
if index is None:
|
|
242
|
+
return None
|
|
243
|
+
first = self.lines[index]
|
|
244
|
+
opening = COMPILED_DOCUMENT_TAG_REGEX.match(self.text[start : first.content_end])
|
|
245
|
+
if opening is None:
|
|
246
|
+
return None
|
|
247
|
+
tag_start = self.text.find("<", start, first.content_end)
|
|
248
|
+
block_tag = opening.group(1).lower() in HTML_BLOCK_TAGS
|
|
249
|
+
if not block_tag and index and not self.blank(index - 1):
|
|
250
|
+
return None
|
|
251
|
+
token = self.html_pattern.match(self.text, tag_start)
|
|
252
|
+
token_end = token.end() if token is not None else start
|
|
253
|
+
if not block_tag and (token is None or token_end > first.content_end or self.text[token_end : first.content_end].strip(" \t")):
|
|
254
|
+
return None
|
|
255
|
+
previous = self.line_at(token_end)
|
|
256
|
+
scan = max(index + 1, self.by_start.get(previous.start, index))
|
|
257
|
+
for at in range(scan, len(self.lines)):
|
|
258
|
+
line = self.lines[at]
|
|
259
|
+
if line.start >= token_end and self.blank(at):
|
|
260
|
+
return BlockSpan(previous.content_end, token_end, previous.content_end, closed=line.end > line.content_end)
|
|
261
|
+
previous = line
|
|
262
|
+
return BlockSpan(len(self.text), token_end, len(self.text))
|
|
263
|
+
|
|
264
|
+
def indented_may_start(self, index: int) -> bool:
|
|
265
|
+
"""Allow code after a blank or a confirmed non-paragraph block."""
|
|
266
|
+
if index == 0 or self.blank(index - 1):
|
|
267
|
+
return True
|
|
268
|
+
previous = self.lines[index - 1]
|
|
269
|
+
value = self.text[previous.start : previous.content_end]
|
|
270
|
+
return bool(
|
|
271
|
+
previous.start in self.closed_fence_lines
|
|
272
|
+
or COMPILED_ATX_START_REGEX.match(value) is not None
|
|
273
|
+
or COMPILED_DOCUMENT_BREAK_REGEX.fullmatch(value) is not None
|
|
274
|
+
or (index > 1 and not self.blank(index - 2) and COMPILED_SETEXT_REGEX.fullmatch(value) is not None)
|
|
275
|
+
or (value.startswith("__MDFLOW_CODE_BLOCK_") and value.endswith("__"))
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def reference_prefix(self, start: int) -> int | None:
|
|
279
|
+
"""Read the bounded single-line label prefix of a reference definition."""
|
|
280
|
+
index = self.by_start.get(start)
|
|
281
|
+
if index is None:
|
|
282
|
+
return None
|
|
283
|
+
line = self.lines[index]
|
|
284
|
+
match = COMPILED_REFERENCE_PREFIX_REGEX.match(self.text[start : line.content_end])
|
|
285
|
+
if match is None or len(match.group(1)) > 999 or not match.group(1).strip(" \t"):
|
|
286
|
+
return None
|
|
287
|
+
return start + match.end()
|
|
288
|
+
|
|
289
|
+
def interaction_limit(self, start: int, marker_end: Callable[[int], int | None], *, final: bool = True) -> tuple[int, bool]:
|
|
290
|
+
"""Stop incomplete document controls at a blank outside opaque markers."""
|
|
291
|
+
at = start
|
|
292
|
+
while at < len(self.text):
|
|
293
|
+
index = self.by_start.get(at)
|
|
294
|
+
if index is not None and self.blank(index) and (final or self.lines[index].end > self.lines[index].content_end):
|
|
295
|
+
return at, False
|
|
296
|
+
if self.text.startswith("{{", at):
|
|
297
|
+
end = marker_end(at)
|
|
298
|
+
if end is None:
|
|
299
|
+
return len(self.text), False
|
|
300
|
+
at = end
|
|
301
|
+
elif self.text[at] == "\\" and at + 1 < len(self.text) and self.text[at + 1] in "|/.]":
|
|
302
|
+
at += 2
|
|
303
|
+
elif self.text[at] == "]":
|
|
304
|
+
return at, True
|
|
305
|
+
else:
|
|
306
|
+
at += 1
|
|
307
|
+
return len(self.text), False
|
|
308
|
+
|
|
309
|
+
def _container_paragraphs(self) -> set[int]:
|
|
310
|
+
"""Keep same-container and lazy continuation lines in one paragraph."""
|
|
311
|
+
boundaries: set[int] = set()
|
|
312
|
+
active: tuple[int, ...] = ()
|
|
313
|
+
paragraph = False
|
|
314
|
+
for index, line in enumerate(self.lines):
|
|
315
|
+
value = self.text[line.start : line.content_end]
|
|
316
|
+
if not value.strip(" \t"):
|
|
317
|
+
active, paragraph = (), False
|
|
318
|
+
continue
|
|
319
|
+
content = container_content_start(value, active) if active else 0
|
|
320
|
+
remaining = value[content:] if content is not None else value
|
|
321
|
+
base = active if content is not None else ()
|
|
322
|
+
after, steps = container_opening(remaining)
|
|
323
|
+
if steps and (not paragraph or self._explicit_interrupt(remaining)):
|
|
324
|
+
boundaries.add(index)
|
|
325
|
+
active = base + steps
|
|
326
|
+
remaining = remaining[after:]
|
|
327
|
+
if not remaining.strip(" \t"):
|
|
328
|
+
boundaries.add(index)
|
|
329
|
+
paragraph = False
|
|
330
|
+
else:
|
|
331
|
+
paragraph = True
|
|
332
|
+
if fence_opening(remaining) is not None or COMPILED_ATX_START_REGEX.match(remaining) is not None or COMPILED_DOCUMENT_BREAK_REGEX.fullmatch(remaining) is not None:
|
|
333
|
+
paragraph = False
|
|
334
|
+
return boundaries
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _explicit_interrupt(value: str) -> bool:
|
|
338
|
+
at = 0
|
|
339
|
+
while at < min(3, len(value)) and value[at] == " ":
|
|
340
|
+
at += 1
|
|
341
|
+
if at < len(value) and value[at] == ">":
|
|
342
|
+
return True
|
|
343
|
+
marker = COMPILED_CONTAINER_LIST_REGEX.match(value, at)
|
|
344
|
+
if marker is None or (value[at].isdigit() and marker.group() not in ("1.", "1)")):
|
|
345
|
+
return False
|
|
346
|
+
return bool(value[marker.end() :].strip(" \t"))
|
|
347
|
+
|
|
348
|
+
def paragraph_interrupts(self, index: int) -> bool:
|
|
349
|
+
"""Report a new quote/list paragraph without breaking its own inline spans."""
|
|
350
|
+
return index in self.container_paragraphs
|
|
351
|
+
|
|
352
|
+
def inline_block_interrupts(self, index: int) -> bool:
|
|
353
|
+
"""Report confirmed thematic and ordinary-HTML paragraph interruptions."""
|
|
354
|
+
line = self.lines[index]
|
|
355
|
+
value = self.text[line.start : line.content_end]
|
|
356
|
+
tag = COMPILED_DOCUMENT_TAG_REGEX.match(value)
|
|
357
|
+
return COMPILED_DOCUMENT_BREAK_REGEX.fullmatch(value) is not None or (tag is not None and tag.group(1).lower() in HTML_BLOCK_TAGS)
|