pyDiffTools 0.1.26__tar.gz → 0.1.28__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 (93) hide show
  1. {pydifftools-0.1.26 → pydifftools-0.1.28}/MANIFEST.in +2 -0
  2. {pydifftools-0.1.26/pyDiffTools.egg-info → pydifftools-0.1.28}/PKG-INFO +1 -1
  3. {pydifftools-0.1.26 → pydifftools-0.1.28/pyDiffTools.egg-info}/PKG-INFO +1 -1
  4. {pydifftools-0.1.26 → pydifftools-0.1.28}/pyDiffTools.egg-info/SOURCES.txt +2 -0
  5. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/command_line.py +9 -0
  6. pydifftools-0.1.28/pydifftools/comment_tags_margin.lua +411 -0
  7. pydifftools-0.1.28/pydifftools/comment_tags_no_comments.lua +328 -0
  8. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/continuous.py +96 -42
  9. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/flowchart/graph.py +252 -69
  10. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/flowchart/watch_graph.py +117 -47
  11. {pydifftools-0.1.26 → pydifftools-0.1.28}/pyproject.toml +3 -1
  12. {pydifftools-0.1.26 → pydifftools-0.1.28}/tests/test_command_line_help.py +14 -0
  13. {pydifftools-0.1.26 → pydifftools-0.1.28}/LICENSE.md +0 -0
  14. {pydifftools-0.1.26 → pydifftools-0.1.28}/README.rst +0 -0
  15. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/_quarto.yml +0 -0
  16. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  17. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  18. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  19. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  20. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  21. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  22. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  23. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  24. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  25. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  26. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  27. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  28. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  29. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  30. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  31. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  32. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  33. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  34. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  35. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  36. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  37. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  38. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/example.qmd +0 -0
  39. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/example.tex +0 -0
  40. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/index.qmd +0 -0
  41. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  42. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  43. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  44. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  45. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/independent.qmd +0 -0
  46. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/index.qmd +0 -0
  47. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  48. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  49. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  50. {pydifftools-0.1.26 → pydifftools-0.1.28}/example_notebook/project1/test_include.qmd +0 -0
  51. {pydifftools-0.1.26 → pydifftools-0.1.28}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  52. {pydifftools-0.1.26 → pydifftools-0.1.28}/pyDiffTools.egg-info/entry_points.txt +0 -0
  53. {pydifftools-0.1.26 → pydifftools-0.1.28}/pyDiffTools.egg-info/requires.txt +0 -0
  54. {pydifftools-0.1.26 → pydifftools-0.1.28}/pyDiffTools.egg-info/top_level.txt +0 -0
  55. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/__init__.py +0 -0
  56. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/browser_lifecycle.py +0 -0
  57. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/check_numbers.py +0 -0
  58. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/command_registry.py +0 -0
  59. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/comment_tags.lua +0 -0
  61. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/comment_toggle.js +0 -0
  62. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/comments.css +0 -0
  63. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/copy_files.py +0 -0
  64. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/diff-doc.js +0 -0
  65. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/doc_contents.py +0 -0
  66. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/flowchart/__init__.py +0 -0
  67. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  68. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/git_gd.py +0 -0
  69. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/git_gd_qt.py +0 -0
  70. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/html_comments.py +0 -0
  71. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/html_uncomments.py +0 -0
  72. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/log_example.py +0 -0
  73. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/match_spaces.py +0 -0
  74. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/notebook/__init__.py +0 -0
  75. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/notebook/fast_build.py +0 -0
  76. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/notebook/tex_to_qmd.py +0 -0
  77. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/onewordify.py +0 -0
  78. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/onewordify_undo.py +0 -0
  79. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/outline.py +0 -0
  80. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/rearrange_tex.py +0 -0
  81. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/searchacro.py +0 -0
  82. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/separate_comments.py +0 -0
  83. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/split_conflict.py +0 -0
  84. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/unseparate_comments.py +0 -0
  85. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/update_check.py +0 -0
  86. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/wrap_sentences.py +0 -0
  87. {pydifftools-0.1.26 → pydifftools-0.1.28}/pydifftools/xml2xlsx.vbs +0 -0
  88. {pydifftools-0.1.26 → pydifftools-0.1.28}/setup.cfg +0 -0
  89. {pydifftools-0.1.26 → pydifftools-0.1.28}/tests/test_browser_lifecycle.py +0 -0
  90. {pydifftools-0.1.26 → pydifftools-0.1.28}/tests/test_continuous_shutdown.py +0 -0
  91. {pydifftools-0.1.26 → pydifftools-0.1.28}/tests/test_rrng.py +0 -0
  92. {pydifftools-0.1.26 → pydifftools-0.1.28}/tests/test_tex_to_qmd.py +0 -0
  93. {pydifftools-0.1.26 → pydifftools-0.1.28}/tests/test_update_check.py +0 -0
@@ -5,5 +5,7 @@ include pydifftools/diff-doc.js
5
5
  include pydifftools/xml2xlsx.vbs
6
6
  include pydifftools/comments.css
7
7
  include pydifftools/comment_tags.lua
8
+ include pydifftools/comment_tags_margin.lua
9
+ include pydifftools/comment_tags_no_comments.lua
8
10
  include pydifftools/comment_toggle.js
9
11
  recursive-include example_notebook *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.26
3
+ Version: 0.1.28
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.26
3
+ Version: 0.1.28
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -51,6 +51,8 @@ pydifftools/command_line.py
51
51
  pydifftools/command_registry.py
52
52
  pydifftools/comment_functions.py
53
53
  pydifftools/comment_tags.lua
54
+ pydifftools/comment_tags_margin.lua
55
+ pydifftools/comment_tags_no_comments.lua
54
56
  pydifftools/comment_toggle.js
55
57
  pydifftools/comments.css
56
58
  pydifftools/continuous.py
@@ -934,6 +934,15 @@ def main(argv=None):
934
934
  parser._pydifft_subparsers[subcommand].print_help()
935
935
  return
936
936
  namespace = parser.parse_args(argv)
937
+ if (
938
+ namespace.command == "cpb"
939
+ and namespace.comments_to_margin
940
+ and namespace.no_comments
941
+ ):
942
+ parser._pydifft_subparsers["cpb"].error(
943
+ "argument --no-comments: not allowed with argument "
944
+ "--comments-to-margin"
945
+ )
937
946
  handler = namespace._handler
938
947
  handler_kwargs = dict(vars(namespace))
939
948
  handler_kwargs.pop("_handler", None)
@@ -0,0 +1,411 @@
1
+ -- comment_tags_margin.lua
2
+ -- PYDIFFTOOLS_SPECIAL_MARGIN_COMMENTS_FILTER
3
+ -- Supports:
4
+ -- Inline: <comment>...</comment>, <comment-left>...</comment-left>,
5
+ -- <comment-right>...</comment-right>
6
+ -- Block: same tags wrapping block content (lists, multiple paras, etc.)
7
+ --
8
+ -- Inline output:
9
+ -- <span class="comment-pin"><span class="comment-right|left">...</span></span>
10
+ --
11
+ -- Block output:
12
+ -- <span class="comment-pin comment-pin-block" data-comment-id="cN"></span>
13
+ -- <div class="comment-overlay comment-right|left" data-comment-id="cN"> ...block content... </div>
14
+ --
15
+ -- Requires JS to position .comment-overlay.
16
+
17
+ local function raw_inline_html(el)
18
+ if el and el.t == "RawInline" and el.format == "html" then
19
+ return el.text:lower()
20
+ end
21
+ return nil
22
+ end
23
+
24
+ local function raw_block_html(el)
25
+ if el and el.t == "RawBlock" and el.format == "html" then
26
+ return el.text:lower()
27
+ end
28
+ return nil
29
+ end
30
+
31
+ local OPENERS = {
32
+ ["<comment>"] = { side = "comment-left", close = "</comment>" },
33
+ ["<comment-right>"] = { side = "comment-left", close = "</comment-right>" },
34
+ ["<comment-left>"] = { side = "comment-left", close = "</comment-left>" },
35
+ }
36
+
37
+ local comment_id = 0
38
+ local function next_id()
39
+ comment_id = comment_id + 1
40
+ return "c" .. tostring(comment_id)
41
+ end
42
+
43
+ local function make_inline_comment(side, content_inlines)
44
+ -- Emit inline comments as overlay-positioned margin bubbles so they stay in
45
+ -- the left margin without forcing a paragraph break at the insertion point.
46
+ local id = next_id()
47
+ local out = pandoc.List()
48
+ out:insert(
49
+ pandoc.RawInline(
50
+ "html",
51
+ '<span class="comment-inline-break-marker comment-inline-break-before">...</span>'
52
+ )
53
+ )
54
+ out:insert(
55
+ pandoc.RawInline(
56
+ "html",
57
+ '<span class="comment-pin comment-pin-block" data-comment-id="' .. id .. '"></span>'
58
+ )
59
+ )
60
+ out:insert(
61
+ pandoc.Span(
62
+ content_inlines,
63
+ pandoc.Attr(
64
+ "",
65
+ { "comment-overlay", side, "comment-inline-margin", "comment-margin-left" },
66
+ { ["data-comment-id"] = id, ["style"] = "font-size: 6pt;" }
67
+ )
68
+ )
69
+ )
70
+ out:insert(
71
+ pandoc.RawInline(
72
+ "html",
73
+ '<span class="comment-inline-break-marker comment-inline-break-after">...</span>'
74
+ )
75
+ )
76
+ return out
77
+ end
78
+
79
+ local function make_block_anchor(id)
80
+ -- RawBlock so we don't introduce a <p> wrapper that changes spacing.
81
+ return pandoc.RawBlock("html",
82
+ '<span class="comment-pin comment-pin-block" data-comment-id="' .. id .. '"></span>')
83
+ end
84
+
85
+ local function make_block_overlay(side, id, content_blocks)
86
+ -- Div can contain BulletList/Para/etc. JS will position this overlay.
87
+ return pandoc.Div(
88
+ content_blocks,
89
+ pandoc.Attr(
90
+ "",
91
+ { "comment-overlay", side, "comment-margin-left" },
92
+ { ["data-comment-id"] = id, ["style"] = "font-size: 6pt;" }
93
+ )
94
+ )
95
+ end
96
+
97
+ local function para_like_t(block)
98
+ return block and (block.t == "Para" or block.t == "Plain")
99
+ end
100
+
101
+ local function clone_para_like(block, inlines)
102
+ if block.t == "Para" then
103
+ return pandoc.Para(inlines)
104
+ else
105
+ return pandoc.Plain(inlines)
106
+ end
107
+ end
108
+
109
+ local function split_inline_block_at_tag(block, tag)
110
+ if not para_like_t(block) then
111
+ return false, nil, nil
112
+ end
113
+
114
+ local tag_index = nil
115
+ for j = 1, #block.content do
116
+ if raw_inline_html(block.content[j]) == tag then
117
+ tag_index = j
118
+ break
119
+ end
120
+ end
121
+ if not tag_index then
122
+ return false, nil, nil
123
+ end
124
+
125
+ local before = pandoc.List()
126
+ local after = pandoc.List()
127
+ for j = 1, tag_index - 1 do
128
+ before:insert(block.content[j])
129
+ end
130
+ for j = tag_index + 1, #block.content do
131
+ after:insert(block.content[j])
132
+ end
133
+
134
+ -- Avoid creating hard visual breaks around opener/closer tags when those
135
+ -- tags sit at line boundaries in markdown source.
136
+ while #before > 0 and (
137
+ before[#before].t == "SoftBreak"
138
+ or before[#before].t == "LineBreak"
139
+ or before[#before].t == "Space"
140
+ ) do
141
+ before:remove(#before)
142
+ end
143
+ while #after > 0 and (
144
+ after[1].t == "SoftBreak"
145
+ or after[1].t == "LineBreak"
146
+ or after[1].t == "Space"
147
+ ) do
148
+ after:remove(1)
149
+ end
150
+
151
+ local before_block = nil
152
+ local after_block = nil
153
+ if #before > 0 then
154
+ before_block = clone_para_like(block, before)
155
+ end
156
+ if #after > 0 then
157
+ after_block = clone_para_like(block, after)
158
+ end
159
+
160
+ return true, before_block, after_block
161
+ end
162
+
163
+
164
+ local function split_block_at_closer(block, close_tag)
165
+ -- Handle direct Para/Plain closer first.
166
+ if para_like_t(block) then
167
+ local found, before, after = split_inline_block_at_tag(block, close_tag)
168
+ if found then
169
+ local after_blocks = pandoc.List()
170
+ if after then
171
+ after_blocks:insert(after)
172
+ end
173
+ return true, before, after_blocks
174
+ end
175
+ return false, nil, pandoc.List()
176
+ end
177
+
178
+ -- Handle list blocks where the closer can appear inside a list item.
179
+ if block.t == "BulletList" then
180
+ local before_items = pandoc.List()
181
+ local after_blocks = pandoc.List()
182
+
183
+ for item_index = 1, #block.content do
184
+ local this_item = block.content[item_index]
185
+ local before_item_blocks = pandoc.List()
186
+
187
+ for block_index = 1, #this_item do
188
+ local found, before, nested_after =
189
+ split_block_at_closer(this_item[block_index], close_tag)
190
+ if found then
191
+ if before then
192
+ before_item_blocks:insert(before)
193
+ end
194
+ if #before_item_blocks > 0 then
195
+ before_items:insert(before_item_blocks)
196
+ end
197
+
198
+ -- Once the comment closes, flatten the remaining list content back
199
+ -- into normal blocks so we do not leak list text into body lists.
200
+ for nested_index = 1, #nested_after do
201
+ after_blocks:insert(nested_after[nested_index])
202
+ end
203
+ for k = block_index + 1, #this_item do
204
+ after_blocks:insert(this_item[k])
205
+ end
206
+ for item_tail = item_index + 1, #block.content do
207
+ for tail_block_index = 1, #block.content[item_tail] do
208
+ after_blocks:insert(block.content[item_tail][tail_block_index])
209
+ end
210
+ end
211
+
212
+ local before_block = nil
213
+ if #before_items > 0 then
214
+ before_block = pandoc.BulletList(before_items)
215
+ end
216
+ return true, before_block, after_blocks
217
+ else
218
+ before_item_blocks:insert(this_item[block_index])
219
+ end
220
+ end
221
+
222
+ if #before_item_blocks > 0 then
223
+ before_items:insert(before_item_blocks)
224
+ end
225
+ end
226
+
227
+ return false, nil, pandoc.List()
228
+ end
229
+
230
+ return false, nil, pandoc.List()
231
+ end
232
+
233
+ -- Detect opener at block level:
234
+ -- 1) RawBlock("<comment>")
235
+ -- 2) anywhere inside Para/Plain as RawInline("<comment>")
236
+ -- Returns: spec, before_block_or_nil, after_block_or_nil
237
+ local function detect_block_opener(block)
238
+ local t = raw_block_html(block)
239
+ local spec = t and OPENERS[t] or nil
240
+ if spec then
241
+ return spec, nil, nil
242
+ end
243
+
244
+ if para_like_t(block) then
245
+ for k, opener_spec in pairs(OPENERS) do
246
+ local found, before, after = split_inline_block_at_tag(block, k)
247
+ if found then
248
+ return opener_spec, before, after
249
+ end
250
+ end
251
+ end
252
+
253
+ return nil, nil, nil
254
+ end
255
+
256
+ -- Detect closer at block level:
257
+ -- 1) RawBlock("</comment>")
258
+ -- 2) anywhere inside Para/Plain as RawInline("</comment>")
259
+ -- Returns: found_bool, before_block_or_nil, after_block_or_nil
260
+ local function strip_block_closer(block, close_tag)
261
+ local t = raw_block_html(block)
262
+ if t == close_tag then
263
+ return true, nil, pandoc.List()
264
+ end
265
+
266
+ local found, before, after_blocks = split_block_at_closer(block, close_tag)
267
+ if found then
268
+ return true, before, after_blocks
269
+ end
270
+
271
+ return false, nil, pandoc.List()
272
+ end
273
+
274
+ -- Inline comments (mid-paragraph)
275
+ function Inlines(inlines)
276
+ local out = pandoc.List()
277
+ local i, n = 1, #inlines
278
+
279
+ while i <= n do
280
+ local t = raw_inline_html(inlines[i])
281
+ local spec = t and OPENERS[t] or nil
282
+
283
+ if not spec then
284
+ out:insert(inlines[i])
285
+ i = i + 1
286
+ else
287
+ local buf = pandoc.List()
288
+ local j = i + 1
289
+ local found = false
290
+
291
+ while j <= n do
292
+ local tj = raw_inline_html(inlines[j])
293
+ if tj == spec.close then
294
+ found = true
295
+ break
296
+ end
297
+ buf:insert(inlines[j])
298
+ j = j + 1
299
+ end
300
+
301
+ if found then
302
+ local margin_comment = make_inline_comment(spec.side, buf)
303
+ for k = 1, #margin_comment do
304
+ out:insert(margin_comment[k])
305
+ end
306
+ i = j + 1
307
+ else
308
+ -- unmatched: leave literal
309
+ out:insert(inlines[i])
310
+ i = i + 1
311
+ end
312
+ end
313
+ end
314
+
315
+ return out
316
+ end
317
+
318
+ -- Block comments (lists / multi-paragraph inside <comment> ... </comment>)
319
+ function Blocks(blocks)
320
+ local out = pandoc.List()
321
+ local i, n = 1, #blocks
322
+
323
+ while i <= n do
324
+ local spec, before_open, after_open = detect_block_opener(blocks[i])
325
+
326
+ if not spec then
327
+ out:insert(blocks[i])
328
+ i = i + 1
329
+ else
330
+ local buf = pandoc.List()
331
+ local inserted_before_open = false
332
+ if before_open then
333
+ -- Keep text before the opener in the main document flow.
334
+ out:insert(before_open)
335
+ inserted_before_open = true
336
+ end
337
+ if after_open then
338
+ -- Everything after the opener belongs inside the bubble body.
339
+ buf:insert(after_open)
340
+ end
341
+
342
+ local j = i + 1
343
+ local found = false
344
+ local after_close_blocks = pandoc.List()
345
+
346
+ while j <= n do
347
+ local is_close, before_close, after_close_candidate =
348
+ strip_block_closer(blocks[j], spec.close)
349
+ if is_close then
350
+ if before_close then
351
+ buf:insert(before_close)
352
+ end
353
+ found = true
354
+ after_close_blocks = after_close_candidate
355
+ break
356
+ else
357
+ buf:insert(blocks[j])
358
+ end
359
+ j = j + 1
360
+ end
361
+
362
+ if found then
363
+ local id = next_id()
364
+ local merged_with_inline_anchor = false
365
+
366
+ -- If the comment opens mid-paragraph and closes before trailing prose,
367
+ -- merge the surrounding prose back into one paragraph and place the
368
+ -- anchor inline at the split point so there is no forced line break.
369
+ if inserted_before_open and #after_close_blocks > 0 then
370
+ if para_like_t(out[#out]) and para_like_t(after_close_blocks[1]) then
371
+ local merged_inlines = pandoc.List()
372
+ for k = 1, #out[#out].content do
373
+ merged_inlines:insert(out[#out].content[k])
374
+ end
375
+ merged_inlines:insert(
376
+ pandoc.RawInline(
377
+ "html",
378
+ '<span class="comment-pin comment-pin-block" data-comment-id="'
379
+ .. id .. '"></span>'
380
+ )
381
+ )
382
+ merged_inlines:insert(pandoc.Space())
383
+ for k = 1, #after_close_blocks[1].content do
384
+ merged_inlines:insert(after_close_blocks[1].content[k])
385
+ end
386
+ out[#out] = clone_para_like(out[#out], merged_inlines)
387
+ after_close_blocks:remove(1)
388
+ merged_with_inline_anchor = true
389
+ end
390
+ end
391
+
392
+ if not merged_with_inline_anchor then
393
+ out:insert(make_block_anchor(id))
394
+ end
395
+ out:insert(make_block_overlay(spec.side, id, buf))
396
+
397
+ -- Keep trailing content after the closer in main flow.
398
+ for after_index = 1, #after_close_blocks do
399
+ out:insert(after_close_blocks[after_index])
400
+ end
401
+ i = j + 1
402
+ else
403
+ -- unmatched opener: preserve original block
404
+ out:insert(blocks[i])
405
+ i = i + 1
406
+ end
407
+ end
408
+ end
409
+
410
+ return out
411
+ end