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.
Files changed (47) hide show
  1. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/PKG-INFO +1 -1
  2. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/__init__.py +1 -1
  3. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/format.py +17 -1
  4. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/stream.py +29 -8
  5. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/PKG-INFO +1 -1
  6. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_formatter.py +36 -5
  7. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_formatter_stream.py +35 -11
  8. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/LICENSE +0 -0
  9. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/README.md +0 -0
  10. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/constants.py +0 -0
  11. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/constants_system_prompt.py +0 -0
  12. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/core.py +0 -0
  13. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/enums.py +0 -0
  14. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/exceptions.py +0 -0
  15. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/__init__.py +0 -0
  16. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/classifier.py +0 -0
  17. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/patterns.py +0 -0
  18. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/formatter/types.py +0 -0
  19. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/llm.py +0 -0
  20. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/models.py +0 -0
  21. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/__init__.py +0 -0
  22. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/code_fence_utils.py +0 -0
  23. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/html_comment_utils.py +0 -0
  24. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/interaction.py +0 -0
  25. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/json_parser.py +0 -0
  26. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/output.py +0 -0
  27. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/preprocessor.py +0 -0
  28. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/validation.py +0 -0
  29. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/parser/variable.py +0 -0
  30. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/providers/__init__.py +0 -0
  31. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/providers/config.py +0 -0
  32. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/providers/openai.py +0 -0
  33. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/tag_filter.py +0 -0
  34. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow/utils.py +0 -0
  35. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/SOURCES.txt +0 -0
  36. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/dependency_links.txt +0 -0
  37. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/markdown_flow.egg-info/top_level.txt +0 -0
  38. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/pyproject.toml +0 -0
  39. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/setup.cfg +0 -0
  40. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_dynamic_interaction.py +0 -0
  41. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_html_comment_utils.py +0 -0
  42. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_markdownflow_basic.py +0 -0
  43. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_parser_interaction.py +0 -0
  44. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_parser_output.py +0 -0
  45. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_parser_variable.py +0 -0
  46. {markdown_flow-0.2.64 → markdown_flow-0.2.65}/tests/test_preprocessor.py +0 -0
  47. {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.64
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
@@ -88,5 +88,5 @@ __all__ = [
88
88
  "replace_variables_in_text",
89
89
  ]
90
90
 
91
- __version__ = "0.2.64"
91
+ __version__ = "0.2.65"
92
92
  # __version__ = "0.2.45-alpha-1"
@@ -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 are skipped
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 are skipped (multi-line block internals handled by classifier's IsContinuation)
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=line + "\n", type=cr.type, number=self._current_number))
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
- # Empty line is skipped
84
- if remaining.strip() == "":
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
- cr = self._classifier.classify_line(remaining)
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.64
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 test_empty_lines_filtered(self):
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
- for elem in result:
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 test_empty_lines_filtered(self):
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
- for elem in all_elements:
231
- assert elem.content.strip() != "", (
232
- f"Empty line should be filtered, got element with content {elem.content!r}"
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.strip() == "Hello"
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