markdown-flow 0.4.0__tar.gz → 0.4.2.dev1__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 (44) hide show
  1. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/PKG-INFO +7 -3
  2. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/README.md +6 -2
  3. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/__init__.py +1 -1
  4. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/constants.py +13 -0
  5. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/core.py +40 -23
  6. markdown_flow-0.4.2.dev1/markdown_flow/parser/document_blocks.py +678 -0
  7. markdown_flow-0.4.2.dev1/markdown_flow/parser/document_context.py +466 -0
  8. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/output.py +32 -0
  9. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/preprocessor.py +42 -31
  10. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow.egg-info/PKG-INFO +7 -3
  11. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow.egg-info/SOURCES.txt +1 -0
  12. markdown_flow-0.4.0/markdown_flow/parser/document_context.py +0 -131
  13. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/LICENSE +0 -0
  14. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/constants_system_prompt.py +0 -0
  15. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/enums.py +0 -0
  16. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/escaping.py +0 -0
  17. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/exceptions.py +0 -0
  18. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/__init__.py +0 -0
  19. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/classifier.py +0 -0
  20. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/format.py +0 -0
  21. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/patterns.py +0 -0
  22. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/sanitize.py +0 -0
  23. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/stream.py +0 -0
  24. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/formatter/types.py +0 -0
  25. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/higher_priority_prompt.md +0 -0
  26. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/llm.py +0 -0
  27. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/models.py +0 -0
  28. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/__init__.py +0 -0
  29. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/code_fence_utils.py +0 -0
  30. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/html_comment_utils.py +0 -0
  31. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/interaction.py +0 -0
  32. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/json_parser.py +0 -0
  33. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/validation.py +0 -0
  34. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/parser/variable.py +0 -0
  35. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/providers/__init__.py +0 -0
  36. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/providers/config.py +0 -0
  37. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/providers/openai.py +0 -0
  38. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/system_prompt.md +0 -0
  39. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/tag_filter.py +0 -0
  40. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow/variable_names.py +0 -0
  41. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow.egg-info/dependency_links.txt +0 -0
  42. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/markdown_flow.egg-info/top_level.txt +0 -0
  43. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/pyproject.toml +0 -0
  44. {markdown_flow-0.4.0 → markdown_flow-0.4.2.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-flow
3
- Version: 0.4.0
3
+ Version: 0.4.2.dev1
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 and HTML are read before markers inside
712
- them. Complete variable examples there still support collection and replacement,
711
+ At document level, complete inline code, inline math, link resources, URI/email
712
+ angle autolinks, indented 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 and HTML are read before markers inside
694
- them. Complete variable examples there still support collection and replacement,
693
+ At document level, complete inline code, inline math, link resources, URI/email
694
+ angle autolinks, indented 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
@@ -118,4 +118,4 @@ __all__ = [
118
118
  "format_variable_assignment",
119
119
  ]
120
120
 
121
- __version__ = "0.4.0"
121
+ __version__ = "0.4.2.dev1"
@@ -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};
@@ -71,9 +71,8 @@ def _translation_marker_spans(text: str) -> list[tuple[int, int]]:
71
71
  return [(marker.start - (marker.start > 0 and text[marker.start - 1] == "%"), marker.end) for marker in find_variable_markers(text)]
72
72
 
73
73
 
74
- def _mask_translation_markers(text: str, markers: dict[str, str]) -> str:
75
- """Replace whole markers with collision-free tokens, without reading inside keys."""
76
- tokens = {raw: token for token, raw in markers.items()}
74
+ def _mask_translation_markers(text: str, tokens: dict[str, str]) -> str:
75
+ """Replace whole markers using the interaction's shared raw-to-token index."""
77
76
  parts: list[str] = []
78
77
  at = 0
79
78
  for start, end in _translation_marker_spans(text):
@@ -83,23 +82,29 @@ def _mask_translation_markers(text: str, markers: dict[str, str]) -> str:
83
82
  return "".join(parts)
84
83
 
85
84
 
86
- def _restore_translation_markers(original: str, translated: Any, markers: dict[str, str]) -> str:
87
- """Accept a translated field only when all its marker tokens survive in order."""
85
+ def _translation_token_pattern(markers: dict[str, str]) -> re.Pattern[str] | None:
86
+ """Match only the generated token namespace, without enumerating every token."""
87
+ if not markers:
88
+ return None
89
+ prefix = next(iter(markers)).rsplit("_", 3)[0] + "_"
90
+ return re.compile(re.escape(prefix) + r"[0-9]+__")
91
+
92
+
93
+ def _restore_translation_markers(original: str, translated: Any, markers: dict[str, str], token_pattern: re.Pattern[str] | None = None) -> str:
94
+ """Accept a translated field only when its known marker tokens survive in order."""
95
+ if not isinstance(translated, str):
96
+ translated = original
97
+ if not markers:
98
+ return translated
99
+ token_pattern = token_pattern if token_pattern is not None else _translation_token_pattern(markers)
100
+ assert token_pattern is not None
88
101
 
89
102
  def token_sequence(text: str) -> list[str]:
90
- positions = []
91
- for token in markers:
92
- at = 0
93
- while (at := text.find(token, at)) >= 0:
94
- positions.append((at, token))
95
- at += len(token)
96
- return [token for _, token in sorted(positions)]
103
+ return [match.group() for match in token_pattern.finditer(text) if match.group() in markers]
97
104
 
98
- if not isinstance(translated, str) or token_sequence(original) != token_sequence(translated):
105
+ if token_sequence(original) != token_sequence(translated):
99
106
  translated = original
100
- for token, raw in markers.items():
101
- translated = translated.replace(token, raw)
102
- return translated
107
+ return token_pattern.sub(lambda match: markers.get(match.group(), match.group()), translated)
103
108
 
104
109
 
105
110
  def _find_translation_text(text: str, old: str) -> int:
@@ -1537,22 +1542,33 @@ class MarkdownFlow:
1537
1542
 
1538
1543
  # Keep marker source out of the translation request. The prefix cannot
1539
1544
  # collide with author text, and equal raw markers share the same token.
1540
- prefix = "__MDFLOW_VARIABLE_"
1541
- while prefix in interaction_content:
1542
- prefix = "_" + prefix
1545
+ base = "__MDFLOW_VARIABLE_"
1546
+ used_nonces: set[str] = set()
1547
+ collision = False
1548
+ for match in re.finditer(re.escape(base) + r"(?:N([0-9]+)_)?", interaction_content):
1549
+ collision = True
1550
+ if match.group(1) is not None:
1551
+ used_nonces.add(match.group(1))
1552
+ prefix = base
1553
+ if collision:
1554
+ nonce = 0
1555
+ while str(nonce) in used_nonces:
1556
+ nonce += 1
1557
+ prefix = f"{base}N{nonce}_"
1543
1558
  raw_markers = dict.fromkeys(interaction_content[start:end] for start, end in _translation_marker_spans(interaction_content))
1544
1559
  markers = {f"{prefix}{i}__": raw for i, raw in enumerate(raw_markers)}
1545
1560
  interaction_info["_translation_markers"] = markers
1561
+ tokens_by_raw = {raw: token for token, raw in markers.items()}
1546
1562
  translatable: dict[str, str | list[str]] = {}
1547
1563
 
1548
1564
  # Extract button display text
1549
1565
  if interaction_info.get("buttons"):
1550
- button_texts = [_mask_translation_markers(btn["display"], markers) for btn in interaction_info["buttons"]]
1566
+ button_texts = [_mask_translation_markers(btn["display"], tokens_by_raw) for btn in interaction_info["buttons"]]
1551
1567
  translatable["buttons"] = button_texts
1552
1568
 
1553
1569
  # Extract question text
1554
1570
  if interaction_info.get("question"):
1555
- translatable["question"] = _mask_translation_markers(interaction_info["question"], markers)
1571
+ translatable["question"] = _mask_translation_markers(interaction_info["question"], tokens_by_raw)
1556
1572
 
1557
1573
  # Convert to JSON
1558
1574
 
@@ -1627,6 +1643,7 @@ class MarkdownFlow:
1627
1643
  return original_content
1628
1644
 
1629
1645
  markers = interaction_info.get("_translation_markers", {})
1646
+ token_pattern = _translation_token_pattern(markers)
1630
1647
  prefix = ""
1631
1648
  reconstructed = original_content
1632
1649
  marker = COMPILED_LAYER2_VARIABLE_REGEX.match(original_content[2:-1]) if original_content.startswith("?[") else None
@@ -1640,7 +1657,7 @@ class MarkdownFlow:
1640
1657
  if i < len(translated["buttons"]):
1641
1658
  old_display = button["display"]
1642
1659
  original_display = original.get("buttons", [])[i]
1643
- new_display = _restore_translation_markers(original_display, translated["buttons"][i], markers)
1660
+ new_display = _restore_translation_markers(original_display, translated["buttons"][i], markers, token_pattern)
1644
1661
 
1645
1662
  # Detect if translation happened
1646
1663
  translation_happened = False
@@ -1680,7 +1697,7 @@ class MarkdownFlow:
1680
1697
  # Replace question text
1681
1698
  if "question" in translated and interaction_info.get("question"):
1682
1699
  old_question = interaction_info["question"]
1683
- new_question = _restore_translation_markers(original["question"], translated["question"], markers)
1700
+ new_question = _restore_translation_markers(original["question"], translated["question"], markers, token_pattern)
1684
1701
  reconstructed = _replace_translation_text(reconstructed, f"...{old_question}", f"...{new_question}")
1685
1702
 
1686
1703
  return prefix + reconstructed