markdown-flow 0.2.64__tar.gz → 0.2.65__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.2.64 → markdown_flow-0.2.65}/PKG-INFO +1 -1
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/__init__.py +1 -1
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/format.py +17 -1
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/stream.py +29 -8
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/PKG-INFO +1 -1
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_formatter.py +36 -5
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_formatter_stream.py +35 -11
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/LICENSE +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/README.md +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/constants.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/constants_system_prompt.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/core.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/enums.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/exceptions.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/__init__.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/classifier.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/patterns.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/types.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/llm.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/models.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/__init__.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/code_fence_utils.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/html_comment_utils.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/interaction.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/json_parser.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/output.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/preprocessor.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/validation.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/variable.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/providers/__init__.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/providers/config.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/providers/openai.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/tag_filter.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/utils.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/SOURCES.txt +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/dependency_links.txt +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/top_level.txt +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/pyproject.toml +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/setup.cfg +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_dynamic_interaction.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_html_comment_utils.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_markdownflow_basic.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_parser_interaction.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_parser_output.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_parser_variable.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_preprocessor.py +0 -0
- {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_preserved_simple.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.65
|
|
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
|
|
@@ -34,14 +34,26 @@ def format_content(content: str) -> list[FormattedElement]:
|
|
|
34
34
|
if len(lines) > 0 and lines[-1] == "" and content.endswith("\n"):
|
|
35
35
|
lines = lines[:-1]
|
|
36
36
|
|
|
37
|
+
pending_newlines = 0
|
|
38
|
+
|
|
37
39
|
for i, line in enumerate(lines):
|
|
38
40
|
# Non-last lines get \n appended; last line only if original content didn't end with \n
|
|
39
41
|
line_content = line + "\n" if i < len(lines) - 1 else line
|
|
40
42
|
|
|
41
|
-
# Empty lines
|
|
43
|
+
# Empty lines: buffer the \n instead of discarding
|
|
42
44
|
if line.strip() == "":
|
|
45
|
+
pending_newlines += 1
|
|
43
46
|
continue
|
|
44
47
|
|
|
48
|
+
# Consume pending newlines: append to previous element or prepend to current
|
|
49
|
+
if pending_newlines > 0:
|
|
50
|
+
extra = "\n" * pending_newlines
|
|
51
|
+
if elements:
|
|
52
|
+
elements[-1].content += extra
|
|
53
|
+
else:
|
|
54
|
+
line_content = extra + line_content
|
|
55
|
+
pending_newlines = 0
|
|
56
|
+
|
|
45
57
|
cr = c.classify_line(line)
|
|
46
58
|
|
|
47
59
|
if cr.is_append:
|
|
@@ -58,6 +70,10 @@ def format_content(content: str) -> list[FormattedElement]:
|
|
|
58
70
|
last_type = cr.type
|
|
59
71
|
started = True
|
|
60
72
|
|
|
73
|
+
# Trailing empty lines: append to last element
|
|
74
|
+
if pending_newlines > 0 and elements:
|
|
75
|
+
elements[-1].content += "\n" * pending_newlines
|
|
76
|
+
|
|
61
77
|
return elements
|
|
62
78
|
|
|
63
79
|
|
|
@@ -27,6 +27,7 @@ class StreamFormatter:
|
|
|
27
27
|
self._last_html_number: int = 0
|
|
28
28
|
self._last_type: str = ""
|
|
29
29
|
self._started: bool = False
|
|
30
|
+
self._pending_newlines: int = 0
|
|
30
31
|
|
|
31
32
|
# ------------------------------------------------------------------
|
|
32
33
|
# Public API
|
|
@@ -52,10 +53,25 @@ class StreamFormatter:
|
|
|
52
53
|
line = combined[:nl_idx]
|
|
53
54
|
combined = combined[nl_idx + 1 :]
|
|
54
55
|
|
|
55
|
-
# Empty lines
|
|
56
|
+
# Empty lines: buffer the \n instead of discarding
|
|
56
57
|
if line.strip() == "":
|
|
58
|
+
self._pending_newlines += 1
|
|
57
59
|
continue
|
|
58
60
|
|
|
61
|
+
# Consume pending newlines
|
|
62
|
+
line_content = line + "\n"
|
|
63
|
+
if self._pending_newlines > 0:
|
|
64
|
+
extra = "\n" * self._pending_newlines
|
|
65
|
+
if elements:
|
|
66
|
+
elements[-1].content += extra
|
|
67
|
+
elif self._started:
|
|
68
|
+
# Previous elements already returned; prepend to current line
|
|
69
|
+
line_content = extra + line_content
|
|
70
|
+
else:
|
|
71
|
+
# Leading empty lines: prepend to first element
|
|
72
|
+
line_content = extra + line_content
|
|
73
|
+
self._pending_newlines = 0
|
|
74
|
+
|
|
59
75
|
cr = self._classifier.classify_line(line)
|
|
60
76
|
|
|
61
77
|
if cr.is_append:
|
|
@@ -66,7 +82,7 @@ class StreamFormatter:
|
|
|
66
82
|
if cr.type == ElementType.HTML and not cr.is_continuation and not cr.is_append:
|
|
67
83
|
self._last_html_number = self._current_number
|
|
68
84
|
|
|
69
|
-
elements.append(FormattedElement(content=
|
|
85
|
+
elements.append(FormattedElement(content=line_content, type=cr.type, number=self._current_number))
|
|
70
86
|
self._last_type = cr.type
|
|
71
87
|
self._started = True
|
|
72
88
|
|
|
@@ -74,17 +90,22 @@ class StreamFormatter:
|
|
|
74
90
|
|
|
75
91
|
def flush(self) -> list[FormattedElement]:
|
|
76
92
|
"""Release remaining buffered content at stream end."""
|
|
77
|
-
if not self._line_buffer:
|
|
78
|
-
return []
|
|
79
|
-
|
|
80
93
|
remaining = self._line_buffer
|
|
81
94
|
self._line_buffer = ""
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
if not remaining or remaining.strip() == "":
|
|
97
|
+
# No real content to flush; any pending newlines are trailing
|
|
98
|
+
# They cannot be attached since caller must handle via returned list
|
|
99
|
+
self._pending_newlines = 0
|
|
85
100
|
return []
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
# Consume pending newlines: prepend to remaining content
|
|
103
|
+
if self._pending_newlines > 0:
|
|
104
|
+
extra = "\n" * self._pending_newlines
|
|
105
|
+
remaining = extra + remaining
|
|
106
|
+
self._pending_newlines = 0
|
|
107
|
+
|
|
108
|
+
cr = self._classifier.classify_line(remaining.lstrip("\n"))
|
|
88
109
|
|
|
89
110
|
if cr.is_append:
|
|
90
111
|
self._current_number = self._last_html_number
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.65
|
|
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
|
|
@@ -330,12 +330,43 @@ class TestFormatContent:
|
|
|
330
330
|
assert len(result) == 2
|
|
331
331
|
assert result[0].number != result[1].number
|
|
332
332
|
|
|
333
|
-
def
|
|
333
|
+
def test_empty_lines_preserved_not_standalone(self):
|
|
334
|
+
"""Empty lines don't generate standalone elements but their \\n is preserved."""
|
|
334
335
|
content = "\ntext line\n\n<div>html</div>\n\nmore text\n"
|
|
335
336
|
result = format_content(content)
|
|
336
337
|
|
|
337
|
-
|
|
338
|
-
assert elem.content.strip() != "", (
|
|
339
|
-
f"Empty line should be filtered, got element with content {elem.content!r}"
|
|
340
|
-
)
|
|
338
|
+
# Still 3 elements (no standalone empty-line elements)
|
|
341
339
|
assert len(result) == 3 # text, html, text
|
|
340
|
+
|
|
341
|
+
# Leading \n is prepended to first element
|
|
342
|
+
assert result[0].content.startswith("\n")
|
|
343
|
+
assert "text line" in result[0].content
|
|
344
|
+
|
|
345
|
+
# \n between text and html is appended to text element
|
|
346
|
+
assert result[0].content.endswith("\n\n")
|
|
347
|
+
|
|
348
|
+
# \n between html and more text is appended to html element
|
|
349
|
+
assert result[1].content.endswith("\n\n")
|
|
350
|
+
|
|
351
|
+
def test_multiple_consecutive_empty_lines(self):
|
|
352
|
+
"""Multiple consecutive empty lines are all preserved."""
|
|
353
|
+
content = "first\n\n\nsecond\n"
|
|
354
|
+
result = format_content(content)
|
|
355
|
+
assert len(result) == 2
|
|
356
|
+
# Two empty lines → two extra \n appended to first element
|
|
357
|
+
assert result[0].content == "first\n\n\n"
|
|
358
|
+
|
|
359
|
+
def test_trailing_empty_lines(self):
|
|
360
|
+
"""Trailing empty lines are appended to last element."""
|
|
361
|
+
content = "hello\n\n\n"
|
|
362
|
+
result = format_content(content)
|
|
363
|
+
assert len(result) == 1
|
|
364
|
+
assert result[0].content == "hello\n\n\n"
|
|
365
|
+
|
|
366
|
+
def test_leading_empty_lines_prepend(self):
|
|
367
|
+
"""Leading empty lines are prepended to first element."""
|
|
368
|
+
content = "\n\nhello\n"
|
|
369
|
+
result = format_content(content)
|
|
370
|
+
assert len(result) == 1
|
|
371
|
+
# "hello" is the last line after trailing \n stripping, so no trailing \n
|
|
372
|
+
assert result[0].content == "\n\nhello"
|
|
@@ -222,32 +222,56 @@ class TestStreamFormatterRandomChunk:
|
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
class TestStreamFormatterEmptyLineFiltering:
|
|
225
|
-
def
|
|
225
|
+
def test_empty_lines_preserved_not_standalone(self):
|
|
226
|
+
"""Empty lines don't generate standalone elements but \\n is preserved."""
|
|
226
227
|
f = StreamFormatter()
|
|
227
228
|
content = "text line\n\n<div>html</div>\n\n"
|
|
228
229
|
all_elements = f.process(content) + f.flush()
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
assert len(all_elements) == 2 # text + html
|
|
231
|
+
assert len(all_elements) == 2 # text + html, no standalone empty elements
|
|
232
|
+
# \n between text and html appended to text element
|
|
233
|
+
assert all_elements[0].content == "text line\n\n"
|
|
235
234
|
|
|
236
235
|
def test_leading_empty_lines(self):
|
|
236
|
+
"""Leading empty lines are prepended to first element."""
|
|
237
237
|
f = StreamFormatter()
|
|
238
238
|
content = "\n\nHello\n"
|
|
239
239
|
all_elements = f.process(content) + f.flush()
|
|
240
240
|
|
|
241
241
|
assert len(all_elements) == 1
|
|
242
|
-
assert all_elements[0].content
|
|
242
|
+
assert all_elements[0].content == "\n\nHello\n"
|
|
243
243
|
|
|
244
244
|
def test_empty_lines_between_html_and_text(self):
|
|
245
|
+
"""Empty lines between elements are preserved on previous element."""
|
|
245
246
|
f = StreamFormatter()
|
|
246
247
|
content = "<div>slide1</div>\n\n\nNarration text\n\n<div>slide2</div>\n"
|
|
247
248
|
all_elements = f.process(content) + f.flush()
|
|
248
249
|
|
|
249
|
-
for elem in all_elements:
|
|
250
|
-
assert elem.content.strip() != "", (
|
|
251
|
-
f"Empty line should be filtered, got element with content {elem.content!r}"
|
|
252
|
-
)
|
|
253
250
|
assert len(all_elements) == 3 # html1, text, html2
|
|
251
|
+
# Two empty lines appended to html1
|
|
252
|
+
assert all_elements[0].content == "<div>slide1</div>\n\n\n"
|
|
253
|
+
# One empty line appended to narration text
|
|
254
|
+
assert all_elements[1].content == "Narration text\n\n"
|
|
255
|
+
|
|
256
|
+
def test_cross_chunk_empty_lines(self):
|
|
257
|
+
"""Empty lines spanning chunk boundaries are preserved."""
|
|
258
|
+
f = StreamFormatter()
|
|
259
|
+
|
|
260
|
+
elems1 = f.process("Hello\n")
|
|
261
|
+
assert len(elems1) == 1
|
|
262
|
+
|
|
263
|
+
elems2 = f.process("\n") # empty line in separate chunk
|
|
264
|
+
assert len(elems2) == 0 # no standalone element
|
|
265
|
+
|
|
266
|
+
elems3 = f.process("World\n")
|
|
267
|
+
assert len(elems3) == 1
|
|
268
|
+
# Pending \n prepended to next element (can't modify already-returned element)
|
|
269
|
+
assert elems3[0].content == "\nWorld\n"
|
|
270
|
+
|
|
271
|
+
def test_multiple_consecutive_empty_lines(self):
|
|
272
|
+
"""Multiple consecutive empty lines are all preserved."""
|
|
273
|
+
f = StreamFormatter()
|
|
274
|
+
content = "first\n\n\nsecond\n"
|
|
275
|
+
all_elements = f.process(content) + f.flush()
|
|
276
|
+
assert len(all_elements) == 2
|
|
277
|
+
assert all_elements[0].content == "first\n\n\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|