slack-markdown-parser 2.3.0__tar.gz → 2.3.2__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.
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/CHANGELOG.md +14 -0
- {slack_markdown_parser-2.3.0/slack_markdown_parser.egg-info → slack_markdown_parser-2.3.2}/PKG-INFO +4 -4
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/README-ja.md +1 -1
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/README.md +3 -2
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/docs/spec-ja.md +1 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/docs/spec.md +1 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/pyproject.toml +5 -3
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser/__init__.py +1 -1
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser/converter.py +270 -82
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2/slack_markdown_parser.egg-info}/PKG-INFO +4 -4
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser.egg-info/requires.txt +0 -1
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/LICENSE +0 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/MANIFEST.in +0 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/setup.cfg +0 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser/py.typed +0 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser.egg-info/SOURCES.txt +0 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser.egg-info/dependency_links.txt +0 -0
- {slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser.egg-info/top_level.txt +0 -0
|
@@ -6,6 +6,20 @@ The format is based on Keep a Changelog, and the project follows Semantic Versio
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.3.2] - 2026-04-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Stopped `preserve_visual_blank_lines` from keeping ordered-list context open after list items, including continued items, nested lists, and ordered-list paragraph interruption edge cases.
|
|
14
|
+
- Avoided false-positive list-context detection for indented non-list lines and thematic breaks, so visible blank-line preservation no longer regresses on inputs like ` 1. not-a-list` or `- - -`.
|
|
15
|
+
|
|
16
|
+
## [2.3.1] - 2026-04-10
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Stopped `preserve_visual_blank_lines` from inserting visual-only blank-line placeholders inside fenced code blocks.
|
|
21
|
+
- Kept fallback plain text stable when visual blank-line placeholders are enabled alongside parser-added spacing markers around emphasis or inline code.
|
|
22
|
+
|
|
9
23
|
## [2.3.0] - 2026-04-10
|
|
10
24
|
|
|
11
25
|
### Changed
|
{slack_markdown_parser-2.3.0/slack_markdown_parser.egg-info → slack_markdown_parser-2.3.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-markdown-parser
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Convert LLM Markdown into Slack Block Kit markdown/table messages
|
|
5
5
|
Author: darkgaldragon
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,7 +26,6 @@ Requires-Dist: pip-audit>=2.7.0; extra == "dev"
|
|
|
26
26
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
27
27
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
28
28
|
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
29
|
-
Requires-Dist: twine>=5.1.0; extra == "dev"
|
|
30
29
|
Dynamic: license-file
|
|
31
30
|
|
|
32
31
|
# slack-markdown-parser
|
|
@@ -210,8 +209,9 @@ Markdown segments with lines that contain only a non-breaking space. Those
|
|
|
210
209
|
placeholder lines are removed again when generating preview plain text, so Slack
|
|
211
210
|
notifications and logs stay close to the original Markdown source.
|
|
212
211
|
The current implementation deliberately skips blank runs that sit immediately
|
|
213
|
-
before setext-heading underlines or
|
|
214
|
-
boundaries can change Markdown meaning in newer
|
|
212
|
+
after list-item content, before setext-heading underlines, or before
|
|
213
|
+
reference-link definitions, because those boundaries can change Markdown meaning in newer
|
|
214
|
+
Slack Markdown rendering or keep list formatting open in some clients.
|
|
215
215
|
|
|
216
216
|
### Utility functions
|
|
217
217
|
|
|
@@ -161,7 +161,7 @@ QA | ~~保留~~ | Team C
|
|
|
161
161
|
|
|
162
162
|
`preserve_visual_blank_lines=True` は、非テーブル領域の内部空行を「改行を見えやすくするための補助行」に置き換えるオプションです。
|
|
163
163
|
この補助行はプレビュー文字列を作るときに取り除かれるので、通知文やログは元の Markdown に近い形を保てます。
|
|
164
|
-
|
|
164
|
+
また、リスト項目の内容直後、setext 見出しの下線直前、参照リンク定義直前には補助行を入れず、Markdown の意味が変わったり一部クライアントでリスト解釈が継続したりしないようにしています。
|
|
165
165
|
|
|
166
166
|
### ユーティリティ関数(公開関数)
|
|
167
167
|
|
|
@@ -179,8 +179,9 @@ Markdown segments with lines that contain only a non-breaking space. Those
|
|
|
179
179
|
placeholder lines are removed again when generating preview plain text, so Slack
|
|
180
180
|
notifications and logs stay close to the original Markdown source.
|
|
181
181
|
The current implementation deliberately skips blank runs that sit immediately
|
|
182
|
-
before setext-heading underlines or
|
|
183
|
-
boundaries can change Markdown meaning in newer
|
|
182
|
+
after list-item content, before setext-heading underlines, or before
|
|
183
|
+
reference-link definitions, because those boundaries can change Markdown meaning in newer
|
|
184
|
+
Slack Markdown rendering or keep list formatting open in some clients.
|
|
184
185
|
|
|
185
186
|
### Utility functions
|
|
186
187
|
|
|
@@ -205,6 +205,7 @@ LLM は外枠パイプの省略、区切り行の欠落、列数の不一致な
|
|
|
205
205
|
- 見える行に挟まれた内部空行だけを書き換える
|
|
206
206
|
- 先頭・末尾の空行のまとまりはそのまま残す
|
|
207
207
|
- table 領域には適用しない
|
|
208
|
+
- リスト項目の内容直後の空行はそのまま残す
|
|
208
209
|
- setext 見出しの下線直前の空行はそのまま残す
|
|
209
210
|
- 参照リンク定義直前の空行はそのまま残す
|
|
210
211
|
- プレビュー文字列と `blocks_to_plain_text` では補助用のマーカーを除去し、元の空行構造に戻す
|
|
@@ -207,6 +207,7 @@ This workaround is intentionally narrow:
|
|
|
207
207
|
- Only blank lines between visible lines are rewritten
|
|
208
208
|
- Leading and trailing blank runs are left untouched
|
|
209
209
|
- Table segments are not modified by this option
|
|
210
|
+
- Blank runs immediately after list-item content are left untouched
|
|
210
211
|
- Blank runs immediately before setext-heading underlines are left untouched
|
|
211
212
|
- Blank runs immediately before reference-link definitions are left untouched
|
|
212
213
|
- Preview text and `blocks_to_plain_text` remove those placeholder markers again, preserving the original blank lines in plain-text output
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slack-markdown-parser"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.2"
|
|
8
8
|
description = "Convert LLM Markdown into Slack Block Kit markdown/table messages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -33,7 +33,6 @@ dev = [
|
|
|
33
33
|
"pytest>=8.0.0",
|
|
34
34
|
"pytest-cov>=5.0.0",
|
|
35
35
|
"ruff>=0.6.0",
|
|
36
|
-
"twine>=5.1.0",
|
|
37
36
|
]
|
|
38
37
|
|
|
39
38
|
[project.urls]
|
|
@@ -57,4 +56,7 @@ target-version = "py310"
|
|
|
57
56
|
|
|
58
57
|
[tool.ruff.lint]
|
|
59
58
|
select = ["E", "F", "I", "UP", "B"]
|
|
60
|
-
ignore = ["E501"
|
|
59
|
+
ignore = ["E501"] # line length is enforced by black
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
{slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2}/slack_markdown_parser/converter.py
RENAMED
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import html
|
|
10
10
|
import re
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
ZWSP = "\u200b"
|
|
14
14
|
NBSP = "\u00a0"
|
|
@@ -42,6 +42,12 @@ PROTECTED_UNDERSCORE_SPAN_PATTERN = re.compile(
|
|
|
42
42
|
)
|
|
43
43
|
REFERENCE_DEFINITION_PATTERN = re.compile(r"^[ \t]{0,3}\[[^\]\n]+\]:")
|
|
44
44
|
SETEXT_HEADING_UNDERLINE_PATTERN = re.compile(r"^[ \t]{0,3}(?:=+|-+)\s*$")
|
|
45
|
+
THEMATIC_BREAK_PATTERN = re.compile(
|
|
46
|
+
r"^[ \t]{0,3}(?P<char>[-_*])(?:[ \t]*\1){2,}[ \t]*$"
|
|
47
|
+
)
|
|
48
|
+
LIST_ITEM_PATTERN = re.compile(
|
|
49
|
+
r"^(?P<indent>[ \t]*)(?P<marker>\d+[.)]|[-+*])(?P<spacing>[ \t]+|$)"
|
|
50
|
+
)
|
|
45
51
|
DOUBLE_UNDERSCORE_EMPHASIS_PATTERN = re.compile(
|
|
46
52
|
r"(?<![\\0-9A-Za-z_])__(?=\S)(.+?\S)__(?![0-9A-Za-z_])"
|
|
47
53
|
)
|
|
@@ -118,7 +124,7 @@ def _nested_code_space_strategy(
|
|
|
118
124
|
source: str,
|
|
119
125
|
start: int,
|
|
120
126
|
end: int,
|
|
121
|
-
boundary_chars:
|
|
127
|
+
boundary_chars: set[str] | None = None,
|
|
122
128
|
) -> str | None:
|
|
123
129
|
boundary_chars = boundary_chars or {*VISIBLE_BOUNDARY_CHARS, ZWSP}
|
|
124
130
|
neighbors = []
|
|
@@ -153,8 +159,20 @@ def _normalize_markdown_block_plain_text(text: str) -> str:
|
|
|
153
159
|
return re.sub(r"<(https?://[^>\s|]+)>", r"\1", text)
|
|
154
160
|
|
|
155
161
|
|
|
162
|
+
def _build_markdown_block_plain_text(
|
|
163
|
+
text: str, synthetic_space_indices: list[int] | None = None
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Build fallback/plain text for a markdown block before visual-only rewrites."""
|
|
166
|
+
return _normalize_markdown_block_plain_text(
|
|
167
|
+
_strip_synthetic_spaces_from_plain_text(
|
|
168
|
+
strip_zero_width_spaces(text),
|
|
169
|
+
synthetic_space_indices,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
156
174
|
def _strip_synthetic_spaces_from_plain_text(
|
|
157
|
-
text: str, synthetic_space_indices:
|
|
175
|
+
text: str, synthetic_space_indices: list[int] | None = None
|
|
158
176
|
) -> str:
|
|
159
177
|
if not text or not synthetic_space_indices:
|
|
160
178
|
return text
|
|
@@ -165,13 +183,148 @@ def _strip_synthetic_spaces_from_plain_text(
|
|
|
165
183
|
)
|
|
166
184
|
|
|
167
185
|
|
|
168
|
-
def
|
|
186
|
+
def _indent_width(text: str) -> int:
|
|
187
|
+
width = 0
|
|
188
|
+
for char in text:
|
|
189
|
+
if char == " ":
|
|
190
|
+
width += 1
|
|
191
|
+
elif char == "\t":
|
|
192
|
+
# Treat tabs conservatively for list-continuation heuristics.
|
|
193
|
+
width += 4
|
|
194
|
+
else:
|
|
195
|
+
break
|
|
196
|
+
return width
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _list_item_content_indent(match: re.Match[str]) -> int:
|
|
200
|
+
spacing = match.group("spacing") or ""
|
|
201
|
+
spacing_width = _indent_width(spacing) if spacing else 1
|
|
202
|
+
return (
|
|
203
|
+
_indent_width(match.group("indent") or "")
|
|
204
|
+
+ len(match.group("marker") or "")
|
|
205
|
+
+ spacing_width
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _is_ordered_list_marker(marker: str) -> bool:
|
|
210
|
+
return bool(marker) and marker[0].isdigit()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_thematic_break_line(line: str) -> bool:
|
|
214
|
+
return bool(THEMATIC_BREAK_PATTERN.match(line))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _ordered_list_marker_starts_at_one(marker: str) -> bool:
|
|
218
|
+
if not _is_ordered_list_marker(marker):
|
|
219
|
+
return False
|
|
220
|
+
return marker[:-1] == "1"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _starts_root_list_item(lines: list[str], marker_index: int) -> bool:
|
|
224
|
+
line = lines[marker_index]
|
|
225
|
+
if _is_thematic_break_line(line):
|
|
226
|
+
return False
|
|
227
|
+
match = LIST_ITEM_PATTERN.match(line)
|
|
228
|
+
if not match or _indent_width(match.group("indent") or "") > 3:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
marker = match.group("marker") or ""
|
|
232
|
+
if marker_index == 0 or not lines[marker_index - 1].strip(" \t\r"):
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
if not _is_ordered_list_marker(marker):
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
return _ordered_list_marker_starts_at_one(marker)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _line_belongs_to_list_context(
|
|
242
|
+
lines: list[str], *, marker_index: int, target_index: int
|
|
243
|
+
) -> bool:
|
|
244
|
+
marker_match = LIST_ITEM_PATTERN.match(lines[marker_index])
|
|
245
|
+
if not marker_match:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
list_indent_stack = [
|
|
249
|
+
(
|
|
250
|
+
_indent_width(marker_match.group("indent") or ""),
|
|
251
|
+
_list_item_content_indent(marker_match),
|
|
252
|
+
)
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
for idx in range(marker_index + 1, target_index + 1):
|
|
256
|
+
line = lines[idx]
|
|
257
|
+
if not line.strip(" \t\r"):
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
line_indent = _indent_width(line)
|
|
261
|
+
nested_match = LIST_ITEM_PATTERN.match(line)
|
|
262
|
+
if nested_match and not _is_thematic_break_line(line):
|
|
263
|
+
marker_indent = _indent_width(nested_match.group("indent") or "")
|
|
264
|
+
while list_indent_stack and marker_indent < list_indent_stack[-1][0]:
|
|
265
|
+
list_indent_stack.pop()
|
|
266
|
+
|
|
267
|
+
if not list_indent_stack:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
if marker_indent == list_indent_stack[-1][0]:
|
|
271
|
+
list_indent_stack[-1] = (
|
|
272
|
+
marker_indent,
|
|
273
|
+
_list_item_content_indent(nested_match),
|
|
274
|
+
)
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
if marker_indent >= list_indent_stack[-1][1]:
|
|
278
|
+
list_indent_stack.append(
|
|
279
|
+
(
|
|
280
|
+
marker_indent,
|
|
281
|
+
_list_item_content_indent(nested_match),
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
while len(list_indent_stack) > 1 and line_indent < list_indent_stack[-1][0]:
|
|
289
|
+
list_indent_stack.pop()
|
|
290
|
+
|
|
291
|
+
if line_indent >= list_indent_stack[-1][1]:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _blank_run_follows_list_context(lines: list[str], blank_start: int) -> bool:
|
|
300
|
+
previous_visible_index = blank_start - 1
|
|
301
|
+
if previous_visible_index < 0 or not lines[previous_visible_index].strip(" \t\r"):
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
block_start = previous_visible_index
|
|
305
|
+
while block_start > 0 and lines[block_start - 1].strip(" \t\r"):
|
|
306
|
+
block_start -= 1
|
|
307
|
+
|
|
308
|
+
for marker_index in range(previous_visible_index, block_start - 1, -1):
|
|
309
|
+
if not _starts_root_list_item(lines, marker_index):
|
|
310
|
+
continue
|
|
311
|
+
if _line_belongs_to_list_context(
|
|
312
|
+
lines, marker_index=marker_index, target_index=previous_visible_index
|
|
313
|
+
):
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _inject_visual_blank_line_placeholders_in_chunk(
|
|
320
|
+
text: str,
|
|
321
|
+
) -> tuple[str, list[int]]:
|
|
169
322
|
"""Replace internal blank lines with NBSP-only lines for Slack rendering."""
|
|
170
323
|
if not text or "\n" not in text:
|
|
171
324
|
return text, []
|
|
172
325
|
|
|
173
326
|
lines = text.split("\n")
|
|
174
|
-
rewritten:
|
|
327
|
+
rewritten: list[tuple[str, bool]] = []
|
|
175
328
|
i = 0
|
|
176
329
|
|
|
177
330
|
while i < len(lines):
|
|
@@ -188,6 +341,9 @@ def _inject_visual_blank_line_placeholders(text: str) -> tuple[str, List[int]]:
|
|
|
188
341
|
lines[blank_start - 1].strip(" \t\r")
|
|
189
342
|
)
|
|
190
343
|
has_visible_line_after = i < len(lines) and bool(lines[i].strip(" \t\r"))
|
|
344
|
+
blank_run_follows_list_context = has_visible_line_before and (
|
|
345
|
+
_blank_run_follows_list_context(lines, blank_start)
|
|
346
|
+
)
|
|
191
347
|
next_visible_starts_reference_definition = has_visible_line_after and bool(
|
|
192
348
|
REFERENCE_DEFINITION_PATTERN.match(lines[i])
|
|
193
349
|
)
|
|
@@ -203,6 +359,7 @@ def _inject_visual_blank_line_placeholders(text: str) -> tuple[str, List[int]]:
|
|
|
203
359
|
if (
|
|
204
360
|
has_visible_line_before
|
|
205
361
|
and has_visible_line_after
|
|
362
|
+
and not blank_run_follows_list_context
|
|
206
363
|
and not next_visible_starts_reference_definition
|
|
207
364
|
and not next_visible_starts_setext_heading
|
|
208
365
|
):
|
|
@@ -210,8 +367,8 @@ def _inject_visual_blank_line_placeholders(text: str) -> tuple[str, List[int]]:
|
|
|
210
367
|
else:
|
|
211
368
|
rewritten.extend((line, False) for line in lines[blank_start:i])
|
|
212
369
|
|
|
213
|
-
rebuilt_parts:
|
|
214
|
-
synthetic_indices:
|
|
370
|
+
rebuilt_parts: list[str] = []
|
|
371
|
+
synthetic_indices: list[int] = []
|
|
215
372
|
offset = 0
|
|
216
373
|
|
|
217
374
|
for idx, (line, is_synthetic) in enumerate(rewritten):
|
|
@@ -226,8 +383,33 @@ def _inject_visual_blank_line_placeholders(text: str) -> tuple[str, List[int]]:
|
|
|
226
383
|
return "".join(rebuilt_parts), synthetic_indices
|
|
227
384
|
|
|
228
385
|
|
|
386
|
+
def _inject_visual_blank_line_placeholders(text: str) -> tuple[str, list[int]]:
|
|
387
|
+
"""Replace internal blank lines outside fenced code blocks."""
|
|
388
|
+
if not text or "\n" not in text:
|
|
389
|
+
return text, []
|
|
390
|
+
|
|
391
|
+
rebuilt_parts: list[str] = []
|
|
392
|
+
synthetic_indices: list[int] = []
|
|
393
|
+
offset = 0
|
|
394
|
+
|
|
395
|
+
for is_fenced, chunk in _split_fenced_code_chunks(text):
|
|
396
|
+
if is_fenced:
|
|
397
|
+
rebuilt_parts.append(chunk)
|
|
398
|
+
offset += len(chunk)
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
rewritten_chunk, chunk_indices = (
|
|
402
|
+
_inject_visual_blank_line_placeholders_in_chunk(chunk)
|
|
403
|
+
)
|
|
404
|
+
rebuilt_parts.append(rewritten_chunk)
|
|
405
|
+
synthetic_indices.extend(offset + idx for idx in chunk_indices)
|
|
406
|
+
offset += len(rewritten_chunk)
|
|
407
|
+
|
|
408
|
+
return "".join(rebuilt_parts), synthetic_indices
|
|
409
|
+
|
|
410
|
+
|
|
229
411
|
def _strip_synthetic_blank_line_placeholders(
|
|
230
|
-
text: str, synthetic_blank_line_indices:
|
|
412
|
+
text: str, synthetic_blank_line_indices: list[int] | None = None
|
|
231
413
|
) -> str:
|
|
232
414
|
if not text or not synthetic_blank_line_indices:
|
|
233
415
|
return text
|
|
@@ -238,12 +420,12 @@ def _strip_synthetic_blank_line_placeholders(
|
|
|
238
420
|
)
|
|
239
421
|
|
|
240
422
|
|
|
241
|
-
def _remove_synthetic_space_markers(text: str) -> tuple[str,
|
|
423
|
+
def _remove_synthetic_space_markers(text: str) -> tuple[str, list[int]]:
|
|
242
424
|
if not text or SYNTH_SPACE_MARKER not in text:
|
|
243
425
|
return text, []
|
|
244
426
|
|
|
245
|
-
cleaned:
|
|
246
|
-
synthetic_indices:
|
|
427
|
+
cleaned: list[str] = []
|
|
428
|
+
synthetic_indices: list[int] = []
|
|
247
429
|
mark_next_space = False
|
|
248
430
|
|
|
249
431
|
for char in text:
|
|
@@ -327,7 +509,7 @@ def normalize_bare_urls_for_slack_markdown(text: str) -> str:
|
|
|
327
509
|
return text
|
|
328
510
|
|
|
329
511
|
def wrap_chunk(chunk: str) -> str:
|
|
330
|
-
parts:
|
|
512
|
+
parts: list[str] = []
|
|
331
513
|
cursor = 0
|
|
332
514
|
length = len(chunk)
|
|
333
515
|
|
|
@@ -391,7 +573,7 @@ def sanitize_slack_text(text: str) -> str:
|
|
|
391
573
|
return SLACK_ANGLE_TOKEN_PATTERN.sub(replace_invalid_token, cleaned)
|
|
392
574
|
|
|
393
575
|
|
|
394
|
-
def _match_fence_open(line: str) ->
|
|
576
|
+
def _match_fence_open(line: str) -> tuple[str, int] | None:
|
|
395
577
|
match = FENCE_OPEN_PATTERN.match(line)
|
|
396
578
|
if not match:
|
|
397
579
|
return None
|
|
@@ -408,13 +590,13 @@ def _is_fence_close(line: str, fence: tuple[str, int]) -> bool:
|
|
|
408
590
|
)
|
|
409
591
|
|
|
410
592
|
|
|
411
|
-
def _split_fenced_code_chunks(text: str) ->
|
|
412
|
-
chunks:
|
|
593
|
+
def _split_fenced_code_chunks(text: str) -> list[tuple[bool, str]]:
|
|
594
|
+
chunks: list[tuple[bool, str]] = []
|
|
413
595
|
if not text:
|
|
414
596
|
return chunks
|
|
415
597
|
|
|
416
|
-
current:
|
|
417
|
-
active_fence:
|
|
598
|
+
current: list[str] = []
|
|
599
|
+
active_fence: tuple[str, int] | None = None
|
|
418
600
|
|
|
419
601
|
for line in text.splitlines(keepends=True):
|
|
420
602
|
opening_fence = _match_fence_open(line) if active_fence is None else None
|
|
@@ -440,7 +622,7 @@ def _split_fenced_code_chunks(text: str) -> List[tuple[bool, str]]:
|
|
|
440
622
|
|
|
441
623
|
|
|
442
624
|
def _normalize_underscore_emphasis_chunk(text: str) -> str:
|
|
443
|
-
protected_spans:
|
|
625
|
+
protected_spans: list[str] = []
|
|
444
626
|
|
|
445
627
|
def protect(match: re.Match[str]) -> str:
|
|
446
628
|
token = f"\ufff0{len(protected_spans)}\ufff1"
|
|
@@ -478,7 +660,7 @@ def add_zero_width_spaces_to_markdown(text: str) -> str:
|
|
|
478
660
|
return formatted
|
|
479
661
|
|
|
480
662
|
|
|
481
|
-
def _format_markdown_with_spacing_metadata(text: str) -> tuple[str,
|
|
663
|
+
def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, list[int]]:
|
|
482
664
|
"""Return formatted markdown text plus synthetic visible-space positions."""
|
|
483
665
|
if not text:
|
|
484
666
|
return text, []
|
|
@@ -576,12 +758,12 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
576
758
|
suffix = STRIP_RIGHT_ZWSP_MARKER if after_char == ZWSP else ""
|
|
577
759
|
return f"{prefix}{adjusted_text}{suffix}"
|
|
578
760
|
|
|
579
|
-
def wrap_segment(segment: str) -> tuple[str,
|
|
761
|
+
def wrap_segment(segment: str) -> tuple[str, list[int]]:
|
|
580
762
|
if not segment:
|
|
581
763
|
return segment, []
|
|
582
764
|
|
|
583
765
|
placeholder_map: dict[str, dict[str, str]] = {}
|
|
584
|
-
protected_parts:
|
|
766
|
+
protected_parts: list[str] = []
|
|
585
767
|
last_end = 0
|
|
586
768
|
|
|
587
769
|
for idx, match in enumerate(INLINE_CODE_SPAN_PATTERN.finditer(segment)):
|
|
@@ -651,8 +833,8 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
651
833
|
return _remove_synthetic_space_markers(protected_segment)
|
|
652
834
|
|
|
653
835
|
chunks = _split_fenced_code_chunks(text)
|
|
654
|
-
combined_parts:
|
|
655
|
-
combined_indices:
|
|
836
|
+
combined_parts: list[str] = []
|
|
837
|
+
combined_indices: list[int] = []
|
|
656
838
|
offset = 0
|
|
657
839
|
for is_fenced, chunk in chunks:
|
|
658
840
|
if is_fenced:
|
|
@@ -671,7 +853,7 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
671
853
|
add_zero_width_spaces = add_zero_width_spaces_to_markdown
|
|
672
854
|
|
|
673
855
|
|
|
674
|
-
def _split_markdown_table_cells(line: str) ->
|
|
856
|
+
def _split_markdown_table_cells(line: str) -> list[str]:
|
|
675
857
|
"""Split markdown table cells while preserving pipes inside <...|...> links."""
|
|
676
858
|
working = line.strip()
|
|
677
859
|
if not working:
|
|
@@ -682,8 +864,8 @@ def _split_markdown_table_cells(line: str) -> List[str]:
|
|
|
682
864
|
if working.endswith("|"):
|
|
683
865
|
working = working[:-1]
|
|
684
866
|
|
|
685
|
-
cells:
|
|
686
|
-
current:
|
|
867
|
+
cells: list[str] = []
|
|
868
|
+
current: list[str] = []
|
|
687
869
|
in_angle = False
|
|
688
870
|
escaped = False
|
|
689
871
|
cursor = 0
|
|
@@ -733,8 +915,8 @@ def _count_cell_words(cell_text: str) -> int:
|
|
|
733
915
|
|
|
734
916
|
|
|
735
917
|
def _split_heading_prefix_and_first_cell(
|
|
736
|
-
heading_prefix: str, reference_cell:
|
|
737
|
-
) ->
|
|
918
|
+
heading_prefix: str, reference_cell: str | None
|
|
919
|
+
) -> tuple[str, str] | None:
|
|
738
920
|
tokens = [token for token in heading_prefix.strip().split() if token]
|
|
739
921
|
if len(tokens) < 2:
|
|
740
922
|
return None
|
|
@@ -753,8 +935,8 @@ def _split_heading_prefix_and_first_cell(
|
|
|
753
935
|
|
|
754
936
|
|
|
755
937
|
def _split_heading_and_table_row(
|
|
756
|
-
line: str, next_line:
|
|
757
|
-
) ->
|
|
938
|
+
line: str, next_line: str | None = None
|
|
939
|
+
) -> tuple[str, str] | None:
|
|
758
940
|
"""Split lines like '# Heading |a|b|' into heading and table row."""
|
|
759
941
|
if "|" not in line:
|
|
760
942
|
return None
|
|
@@ -812,7 +994,7 @@ def _split_heading_and_table_row(
|
|
|
812
994
|
if not explicit_cells:
|
|
813
995
|
return None
|
|
814
996
|
|
|
815
|
-
reference_cell:
|
|
997
|
+
reference_cell: str | None = None
|
|
816
998
|
if (
|
|
817
999
|
next_line
|
|
818
1000
|
and "|" in next_line
|
|
@@ -848,10 +1030,10 @@ def normalize_markdown_tables(markdown_text: str) -> str:
|
|
|
848
1030
|
return markdown_text
|
|
849
1031
|
|
|
850
1032
|
lines = markdown_text.splitlines()
|
|
851
|
-
normalized:
|
|
852
|
-
buffer:
|
|
1033
|
+
normalized: list[str] = []
|
|
1034
|
+
buffer: list[str] = []
|
|
853
1035
|
|
|
854
|
-
def is_table_block(candidates:
|
|
1036
|
+
def is_table_block(candidates: list[str]) -> bool:
|
|
855
1037
|
if len(candidates) < 2:
|
|
856
1038
|
return False
|
|
857
1039
|
if any(
|
|
@@ -859,7 +1041,7 @@ def normalize_markdown_tables(markdown_text: str) -> str:
|
|
|
859
1041
|
):
|
|
860
1042
|
return True
|
|
861
1043
|
|
|
862
|
-
column_counts:
|
|
1044
|
+
column_counts: list[int] = []
|
|
863
1045
|
for line in candidates:
|
|
864
1046
|
working = line.strip()
|
|
865
1047
|
if "|" not in working:
|
|
@@ -916,7 +1098,7 @@ def normalize_markdown_tables(markdown_text: str) -> str:
|
|
|
916
1098
|
normalized.extend(buffer)
|
|
917
1099
|
buffer = []
|
|
918
1100
|
|
|
919
|
-
active_fence:
|
|
1101
|
+
active_fence: tuple[str, int] | None = None
|
|
920
1102
|
|
|
921
1103
|
for idx, line in enumerate(lines):
|
|
922
1104
|
opening_fence = _match_fence_open(line) if active_fence is None else None
|
|
@@ -964,13 +1146,13 @@ def looks_like_markdown_table(text: str) -> bool:
|
|
|
964
1146
|
return table_like_lines >= 2
|
|
965
1147
|
|
|
966
1148
|
|
|
967
|
-
def _create_table_cell(text: str) ->
|
|
1149
|
+
def _create_table_cell(text: str) -> dict[str, Any]:
|
|
968
1150
|
"""Build Slack rich_text cell from markdown fragment."""
|
|
969
1151
|
clean_text = strip_zero_width_spaces(text or "")
|
|
970
1152
|
clean_text = clean_text.replace("\\|", "|")
|
|
971
1153
|
if not clean_text.strip():
|
|
972
1154
|
clean_text = "-"
|
|
973
|
-
elements:
|
|
1155
|
+
elements: list[dict[str, Any]] = []
|
|
974
1156
|
last_index = 0
|
|
975
1157
|
|
|
976
1158
|
for match in TABLE_TOKEN_PATTERN.finditer(clean_text):
|
|
@@ -979,7 +1161,7 @@ def _create_table_cell(text: str) -> Dict[str, Any]:
|
|
|
979
1161
|
if prefix:
|
|
980
1162
|
elements.append({"type": "text", "text": prefix})
|
|
981
1163
|
|
|
982
|
-
element:
|
|
1164
|
+
element: dict[str, Any]
|
|
983
1165
|
markdown_label = match.group("markdown_label")
|
|
984
1166
|
markdown_url = match.group("markdown_url")
|
|
985
1167
|
angle_url = match.group("angle_url")
|
|
@@ -995,7 +1177,7 @@ def _create_table_cell(text: str) -> Dict[str, Any]:
|
|
|
995
1177
|
"text": angle_label or angle_url,
|
|
996
1178
|
}
|
|
997
1179
|
else:
|
|
998
|
-
style:
|
|
1180
|
+
style: dict[str, bool] = {}
|
|
999
1181
|
content = token
|
|
1000
1182
|
|
|
1001
1183
|
if content.startswith("`") and content.endswith("`"):
|
|
@@ -1031,13 +1213,13 @@ def _create_table_cell(text: str) -> Dict[str, Any]:
|
|
|
1031
1213
|
}
|
|
1032
1214
|
|
|
1033
1215
|
|
|
1034
|
-
def extract_plain_text_from_table_cell(cell:
|
|
1216
|
+
def extract_plain_text_from_table_cell(cell: dict[str, Any]) -> str:
|
|
1035
1217
|
"""Extract plain text from a Slack table cell object."""
|
|
1036
1218
|
if not isinstance(cell, dict):
|
|
1037
1219
|
return ""
|
|
1038
1220
|
|
|
1039
1221
|
if cell.get("type") == "rich_text":
|
|
1040
|
-
texts:
|
|
1222
|
+
texts: list[str] = []
|
|
1041
1223
|
for element in cell.get("elements", []):
|
|
1042
1224
|
if not isinstance(element, dict):
|
|
1043
1225
|
continue
|
|
@@ -1055,13 +1237,13 @@ def extract_plain_text_from_table_cell(cell: Dict[str, Any]) -> str:
|
|
|
1055
1237
|
return str(cell.get("text", ""))
|
|
1056
1238
|
|
|
1057
1239
|
|
|
1058
|
-
def markdown_table_to_slack_table(table_markdown: str) ->
|
|
1240
|
+
def markdown_table_to_slack_table(table_markdown: str) -> dict[str, Any] | None:
|
|
1059
1241
|
"""Convert markdown table text to Slack table block."""
|
|
1060
1242
|
lines = [
|
|
1061
1243
|
line.rstrip() for line in table_markdown.strip().splitlines() if line.strip()
|
|
1062
1244
|
]
|
|
1063
|
-
rows:
|
|
1064
|
-
expected_columns:
|
|
1245
|
+
rows: list[list[dict[str, Any]]] = []
|
|
1246
|
+
expected_columns: int | None = None
|
|
1065
1247
|
|
|
1066
1248
|
for line in lines:
|
|
1067
1249
|
if TABLE_SEPARATOR_PATTERN.match(line):
|
|
@@ -1092,15 +1274,15 @@ def markdown_table_to_slack_table(table_markdown: str) -> Optional[Dict[str, Any
|
|
|
1092
1274
|
markdown_table_to_table_block = markdown_table_to_slack_table
|
|
1093
1275
|
|
|
1094
1276
|
|
|
1095
|
-
def split_markdown_into_segments(markdown_text: str) ->
|
|
1277
|
+
def split_markdown_into_segments(markdown_text: str) -> list[dict[str, str]]:
|
|
1096
1278
|
"""Split markdown into alternating text/table segments."""
|
|
1097
|
-
segments:
|
|
1279
|
+
segments: list[dict[str, str]] = []
|
|
1098
1280
|
if not markdown_text:
|
|
1099
1281
|
return segments
|
|
1100
1282
|
|
|
1101
1283
|
lines = markdown_text.splitlines()
|
|
1102
|
-
current:
|
|
1103
|
-
current_is_table:
|
|
1284
|
+
current: list[str] = []
|
|
1285
|
+
current_is_table: bool | None = None
|
|
1104
1286
|
|
|
1105
1287
|
def flush() -> None:
|
|
1106
1288
|
nonlocal current, current_is_table
|
|
@@ -1114,7 +1296,7 @@ def split_markdown_into_segments(markdown_text: str) -> List[Dict[str, str]]:
|
|
|
1114
1296
|
current = []
|
|
1115
1297
|
current_is_table = None
|
|
1116
1298
|
|
|
1117
|
-
active_fence:
|
|
1299
|
+
active_fence: tuple[str, int] | None = None
|
|
1118
1300
|
|
|
1119
1301
|
for line in lines:
|
|
1120
1302
|
stripped = line.strip()
|
|
@@ -1147,7 +1329,7 @@ def split_markdown_into_segments(markdown_text: str) -> List[Dict[str, str]]:
|
|
|
1147
1329
|
|
|
1148
1330
|
def convert_markdown_to_slack_blocks(
|
|
1149
1331
|
markdown_text: str, *, preserve_visual_blank_lines: bool = False
|
|
1150
|
-
) ->
|
|
1332
|
+
) -> list[dict[str, Any]]:
|
|
1151
1333
|
"""Convert markdown text into Slack markdown/table blocks."""
|
|
1152
1334
|
if not markdown_text:
|
|
1153
1335
|
return []
|
|
@@ -1157,7 +1339,7 @@ def convert_markdown_to_slack_blocks(
|
|
|
1157
1339
|
markdown_text = normalize_underscore_emphasis(markdown_text)
|
|
1158
1340
|
markdown_text = normalize_bare_urls_for_slack_markdown(markdown_text)
|
|
1159
1341
|
markdown_text = normalize_markdown_tables(markdown_text)
|
|
1160
|
-
blocks:
|
|
1342
|
+
blocks: list[dict[str, Any]] = []
|
|
1161
1343
|
|
|
1162
1344
|
for segment in split_markdown_into_segments(markdown_text):
|
|
1163
1345
|
content = segment.get("content", "")
|
|
@@ -1171,13 +1353,15 @@ def convert_markdown_to_slack_blocks(
|
|
|
1171
1353
|
continue
|
|
1172
1354
|
|
|
1173
1355
|
formatted, synthetic_indices = _format_markdown_with_spacing_metadata(content)
|
|
1174
|
-
|
|
1356
|
+
plain_text = _build_markdown_block_plain_text(formatted, synthetic_indices)
|
|
1357
|
+
synthetic_blank_line_indices: list[int] = []
|
|
1175
1358
|
if preserve_visual_blank_lines:
|
|
1176
1359
|
formatted, synthetic_blank_line_indices = (
|
|
1177
1360
|
_inject_visual_blank_line_placeholders(formatted)
|
|
1178
1361
|
)
|
|
1179
1362
|
if formatted.strip():
|
|
1180
1363
|
block = _AnnotatedSlackBlock({"type": "markdown", "text": formatted})
|
|
1364
|
+
block._plain_text = plain_text
|
|
1181
1365
|
block._synthetic_space_indices = synthetic_indices
|
|
1182
1366
|
block._synthetic_blank_line_indices = synthetic_blank_line_indices
|
|
1183
1367
|
blocks.append(block)
|
|
@@ -1189,10 +1373,10 @@ def convert_markdown_to_slack_blocks(
|
|
|
1189
1373
|
convert_markdown_text_to_blocks = convert_markdown_to_slack_blocks
|
|
1190
1374
|
|
|
1191
1375
|
|
|
1192
|
-
def split_blocks_by_table(blocks:
|
|
1376
|
+
def split_blocks_by_table(blocks: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
|
|
1193
1377
|
"""Split blocks into multiple messages to satisfy one-table-per-message constraint."""
|
|
1194
|
-
messages:
|
|
1195
|
-
current_message:
|
|
1378
|
+
messages: list[list[dict[str, Any]]] = []
|
|
1379
|
+
current_message: list[dict[str, Any]] = []
|
|
1196
1380
|
|
|
1197
1381
|
for block in blocks or []:
|
|
1198
1382
|
if isinstance(block, dict) and block.get("type") == "table":
|
|
@@ -1213,7 +1397,7 @@ def convert_markdown_to_slack_messages(
|
|
|
1213
1397
|
markdown_text: str,
|
|
1214
1398
|
*,
|
|
1215
1399
|
preserve_visual_blank_lines: bool = False,
|
|
1216
|
-
) ->
|
|
1400
|
+
) -> list[list[dict[str, Any]]]:
|
|
1217
1401
|
"""Convert markdown text into a list of Slack message block groups."""
|
|
1218
1402
|
blocks = convert_markdown_to_slack_blocks(
|
|
1219
1403
|
markdown_text, preserve_visual_blank_lines=preserve_visual_blank_lines
|
|
@@ -1227,9 +1411,9 @@ def convert_markdown_to_slack_payloads(
|
|
|
1227
1411
|
markdown_text: str,
|
|
1228
1412
|
*,
|
|
1229
1413
|
preserve_visual_blank_lines: bool = False,
|
|
1230
|
-
) ->
|
|
1414
|
+
) -> list[dict[str, Any]]:
|
|
1231
1415
|
"""Convert markdown text into Slack-ready payload dicts with fallback text."""
|
|
1232
|
-
payloads:
|
|
1416
|
+
payloads: list[dict[str, Any]] = []
|
|
1233
1417
|
for blocks in convert_markdown_to_slack_messages(
|
|
1234
1418
|
markdown_text, preserve_visual_blank_lines=preserve_visual_blank_lines
|
|
1235
1419
|
):
|
|
@@ -1238,31 +1422,33 @@ def convert_markdown_to_slack_payloads(
|
|
|
1238
1422
|
return payloads
|
|
1239
1423
|
|
|
1240
1424
|
|
|
1241
|
-
def blocks_to_plain_text(blocks:
|
|
1425
|
+
def blocks_to_plain_text(blocks: list[dict[str, Any]]) -> str:
|
|
1242
1426
|
"""Build plain text representation from Slack blocks."""
|
|
1243
|
-
parts:
|
|
1427
|
+
parts: list[str] = []
|
|
1244
1428
|
|
|
1245
1429
|
for block in blocks or []:
|
|
1246
1430
|
block_type = block.get("type") if isinstance(block, dict) else None
|
|
1247
1431
|
|
|
1248
1432
|
if block_type == "markdown":
|
|
1249
|
-
text = block
|
|
1250
|
-
if text:
|
|
1251
|
-
|
|
1252
|
-
|
|
1433
|
+
text = getattr(block, "_plain_text", None) or ""
|
|
1434
|
+
if not text:
|
|
1435
|
+
raw_text = block.get("text", "")
|
|
1436
|
+
if raw_text:
|
|
1437
|
+
text = _normalize_markdown_block_plain_text(
|
|
1253
1438
|
_strip_synthetic_blank_line_placeholders(
|
|
1254
1439
|
_strip_synthetic_spaces_from_plain_text(
|
|
1255
|
-
strip_zero_width_spaces(
|
|
1440
|
+
strip_zero_width_spaces(raw_text),
|
|
1256
1441
|
getattr(block, "_synthetic_space_indices", None),
|
|
1257
1442
|
),
|
|
1258
1443
|
getattr(block, "_synthetic_blank_line_indices", None),
|
|
1259
|
-
)
|
|
1444
|
+
)
|
|
1260
1445
|
)
|
|
1261
|
-
|
|
1446
|
+
if text:
|
|
1447
|
+
parts.append(text)
|
|
1262
1448
|
elif block_type == "table":
|
|
1263
1449
|
rows = block.get("rows") or []
|
|
1264
1450
|
for row in rows:
|
|
1265
|
-
cell_texts:
|
|
1451
|
+
cell_texts: list[str] = []
|
|
1266
1452
|
if not isinstance(row, list):
|
|
1267
1453
|
continue
|
|
1268
1454
|
for cell in row:
|
|
@@ -1279,28 +1465,30 @@ def blocks_to_plain_text(blocks: List[Dict[str, Any]]) -> str:
|
|
|
1279
1465
|
return "\n".join([p for p in parts if p]).strip()
|
|
1280
1466
|
|
|
1281
1467
|
|
|
1282
|
-
def build_fallback_text_from_blocks(blocks:
|
|
1468
|
+
def build_fallback_text_from_blocks(blocks: list[dict[str, Any]]) -> str:
|
|
1283
1469
|
"""Build Slack fallback text from block structure."""
|
|
1284
|
-
plain_parts:
|
|
1470
|
+
plain_parts: list[str] = []
|
|
1285
1471
|
|
|
1286
1472
|
for block in blocks or []:
|
|
1287
1473
|
if not isinstance(block, dict):
|
|
1288
1474
|
continue
|
|
1289
1475
|
|
|
1290
1476
|
if block.get("type") == "markdown":
|
|
1291
|
-
text =
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1477
|
+
text = getattr(block, "_plain_text", None) or ""
|
|
1478
|
+
if not text:
|
|
1479
|
+
text = _normalize_markdown_block_plain_text(
|
|
1480
|
+
_strip_synthetic_blank_line_placeholders(
|
|
1481
|
+
_strip_synthetic_spaces_from_plain_text(
|
|
1482
|
+
strip_zero_width_spaces(block.get("text", "")),
|
|
1483
|
+
getattr(block, "_synthetic_space_indices", None),
|
|
1484
|
+
),
|
|
1485
|
+
getattr(block, "_synthetic_blank_line_indices", None),
|
|
1296
1486
|
),
|
|
1297
|
-
|
|
1298
|
-
),
|
|
1299
|
-
)
|
|
1487
|
+
)
|
|
1300
1488
|
if text.strip():
|
|
1301
1489
|
plain_parts.append(text)
|
|
1302
1490
|
elif block.get("type") == "table":
|
|
1303
|
-
table_lines:
|
|
1491
|
+
table_lines: list[str] = []
|
|
1304
1492
|
for row in block.get("rows", []):
|
|
1305
1493
|
if not isinstance(row, list):
|
|
1306
1494
|
continue
|
|
@@ -1314,9 +1502,9 @@ def build_fallback_text_from_blocks(blocks: List[Dict[str, Any]]) -> str:
|
|
|
1314
1502
|
|
|
1315
1503
|
|
|
1316
1504
|
# Backward-compatible helper retained for existing imports.
|
|
1317
|
-
def parse_markdown_table(table_text: str) ->
|
|
1505
|
+
def parse_markdown_table(table_text: str) -> list[list[str]]:
|
|
1318
1506
|
"""Parse markdown table into row/cell text matrix."""
|
|
1319
|
-
rows:
|
|
1507
|
+
rows: list[list[str]] = []
|
|
1320
1508
|
for line in [line for line in table_text.strip().splitlines() if line.strip()]:
|
|
1321
1509
|
if TABLE_SEPARATOR_PATTERN.match(line.strip()):
|
|
1322
1510
|
continue
|
{slack_markdown_parser-2.3.0 → slack_markdown_parser-2.3.2/slack_markdown_parser.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-markdown-parser
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Convert LLM Markdown into Slack Block Kit markdown/table messages
|
|
5
5
|
Author: darkgaldragon
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,7 +26,6 @@ Requires-Dist: pip-audit>=2.7.0; extra == "dev"
|
|
|
26
26
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
27
27
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
28
28
|
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
29
|
-
Requires-Dist: twine>=5.1.0; extra == "dev"
|
|
30
29
|
Dynamic: license-file
|
|
31
30
|
|
|
32
31
|
# slack-markdown-parser
|
|
@@ -210,8 +209,9 @@ Markdown segments with lines that contain only a non-breaking space. Those
|
|
|
210
209
|
placeholder lines are removed again when generating preview plain text, so Slack
|
|
211
210
|
notifications and logs stay close to the original Markdown source.
|
|
212
211
|
The current implementation deliberately skips blank runs that sit immediately
|
|
213
|
-
before setext-heading underlines or
|
|
214
|
-
boundaries can change Markdown meaning in newer
|
|
212
|
+
after list-item content, before setext-heading underlines, or before
|
|
213
|
+
reference-link definitions, because those boundaries can change Markdown meaning in newer
|
|
214
|
+
Slack Markdown rendering or keep list formatting open in some clients.
|
|
215
215
|
|
|
216
216
|
### Utility functions
|
|
217
217
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|