slack-markdown-parser 2.2.2__tar.gz → 2.2.3__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.2.2 → slack_markdown_parser-2.2.3}/CHANGELOG.md +12 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/PKG-INFO +1 -1
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/pyproject.toml +1 -1
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser/__init__.py +1 -1
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser/converter.py +131 -107
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser.egg-info/PKG-INFO +1 -1
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/test_converter.py +20 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/CONTRIBUTING.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/LICENSE +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/MANIFEST.in +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/README-ja.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/README.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/slack-client-manual-checklist.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/slack-nested-modifier-findings.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/slack-render-test-workflow.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/spec-ja.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/spec.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/setup.cfg +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser/py.typed +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser.egg-info/SOURCES.txt +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser.egg-info/dependency_links.txt +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser.egg-info/requires.txt +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser.egg-info/top_level.txt +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/fixtures/llm_markdown_p0_corpus.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/fixtures/slack_cjk_inner_code_matrix.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/fixtures/slack_nested_modifier_matrix.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/fixtures/slack_nested_modifier_matrix_parens.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/fixtures/slack_nested_modifier_matrix_quotes.md +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/test_llm_markdown_p0_corpus.py +0 -0
- {slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/test_nested_modifier_matrix.py +0 -0
|
@@ -6,6 +6,18 @@ The format is based on Keep a Changelog, and the project follows Semantic Versio
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.2.3] - 2026-03-11
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Reduced fallback text generation overhead by removing the redundant second plain-text pass during payload assembly.
|
|
14
|
+
- Simplified nested modifier formatting internals so placeholder handling scales more predictably on long messages.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Preserved multi-backtick inline-code spans when splitting table cells and heading-plus-table lines, so pipes inside code spans no longer break parsing.
|
|
19
|
+
- Extended Slack table rich-text cell conversion to keep multi-backtick code spans intact in rendered table cells.
|
|
20
|
+
|
|
9
21
|
## [2.2.2] - 2026-03-10
|
|
10
22
|
|
|
11
23
|
### Added
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/slack_markdown_parser/converter.py
RENAMED
|
@@ -23,6 +23,14 @@ CONTROL_CHAR_PATTERN = re.compile(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]")
|
|
|
23
23
|
SLACK_ANGLE_TOKEN_PATTERN = re.compile(r"<[^>\n]+>")
|
|
24
24
|
BARE_URL_PATTERN = re.compile(r"https?://[^\s<]+", re.IGNORECASE)
|
|
25
25
|
FENCE_OPEN_PATTERN = re.compile(r"^[ \t]{0,3}(`{3,}|~{3,})([^\n]*)$")
|
|
26
|
+
MARKDOWN_LINK_PATTERN = re.compile(r"\[[^\]\n]+\]\([^\)\n]+\)")
|
|
27
|
+
INLINE_CODE_SPAN_PATTERN = re.compile(r"(?<!`)`[^`\n]+`(?!`)", flags=re.DOTALL)
|
|
28
|
+
EMPHASIS_PATTERNS = (
|
|
29
|
+
re.compile(r"(?<!\*)\*\*(.+?)\*\*(?!\*)", flags=re.DOTALL),
|
|
30
|
+
re.compile(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", flags=re.DOTALL),
|
|
31
|
+
re.compile(r"~~(.+?)~~", flags=re.DOTALL),
|
|
32
|
+
)
|
|
33
|
+
INLINE_CODE_PLACEHOLDER_PATTERN = re.compile(r"\ufff0code\d+\ufff1")
|
|
26
34
|
PROTECTED_UNDERSCORE_SPAN_PATTERN = re.compile(
|
|
27
35
|
r"`[^`\n]+`"
|
|
28
36
|
r"|\[[^\]\n]+\]\([^\)\n]+\)"
|
|
@@ -43,9 +51,14 @@ LOOSE_TABLE_SEPARATOR_PATTERN = re.compile(
|
|
|
43
51
|
r"^\s*\|?\s*:?-{3,}\s*(\|\s*:?-{3,}\s*)+\|?\s*$"
|
|
44
52
|
)
|
|
45
53
|
TABLE_TOKEN_PATTERN = re.compile(
|
|
46
|
-
r"\[([^\]\n]+)\]\((https?://[^\s)]+)\)"
|
|
47
|
-
r"|<(https?://[^>\s|]+)(?:\|([^>\n]+))?>"
|
|
48
|
-
r"|(
|
|
54
|
+
r"\[(?P<markdown_label>[^\]\n]+)\]\((?P<markdown_url>https?://[^\s)]+)\)"
|
|
55
|
+
r"|<(?P<angle_url>https?://[^>\s|]+)(?:\|(?P<angle_label>[^>\n]+))?>"
|
|
56
|
+
r"|(?P<token>"
|
|
57
|
+
r"(?P<code>(?P<code_delimiter>`+)(?P<code_text>[^\n]+?)(?P=code_delimiter))"
|
|
58
|
+
r"|~~[^~]+~~"
|
|
59
|
+
r"|\*\*[^*]+\*\*"
|
|
60
|
+
r"|(?<!\*)\*[^*]+\*(?!\*)"
|
|
61
|
+
r")"
|
|
49
62
|
)
|
|
50
63
|
ALLOWED_SLACK_ANGLE_TOKEN_PATTERNS = (
|
|
51
64
|
re.compile(r"^<https?://[^>\s|]+(?:\|[^>\n]+)?>$"),
|
|
@@ -130,10 +143,6 @@ def _needs_inner_code_spacing(char: str, boundary_chars: set[str]) -> bool:
|
|
|
130
143
|
return bool(char) and char not in boundary_chars and char.isalnum()
|
|
131
144
|
|
|
132
145
|
|
|
133
|
-
def _normalize_synthetic_visible_spaces_for_plain_output(text: str) -> str:
|
|
134
|
-
return text
|
|
135
|
-
|
|
136
|
-
|
|
137
146
|
def _normalize_markdown_block_plain_text(text: str) -> str:
|
|
138
147
|
if not text:
|
|
139
148
|
return text
|
|
@@ -178,14 +187,23 @@ def _is_allowed_slack_angle_token(token: str) -> bool:
|
|
|
178
187
|
return any(pattern.match(token) for pattern in ALLOWED_SLACK_ANGLE_TOKEN_PATTERNS)
|
|
179
188
|
|
|
180
189
|
|
|
190
|
+
def _find_inline_code_span_end(text: str, start: int) -> int | None:
|
|
191
|
+
delimiter_end = start
|
|
192
|
+
while delimiter_end < len(text) and text[delimiter_end] == "`":
|
|
193
|
+
delimiter_end += 1
|
|
194
|
+
|
|
195
|
+
delimiter = text[start:delimiter_end]
|
|
196
|
+
closing = text.find(delimiter, delimiter_end)
|
|
197
|
+
if closing == -1:
|
|
198
|
+
return None
|
|
199
|
+
return closing + len(delimiter)
|
|
200
|
+
|
|
201
|
+
|
|
181
202
|
def normalize_bare_urls_for_slack_markdown(text: str) -> str:
|
|
182
203
|
"""Wrap bare URLs in autolink syntax for stable Slack markdown rendering."""
|
|
183
204
|
if not text:
|
|
184
205
|
return text
|
|
185
206
|
|
|
186
|
-
markdown_link_pattern = re.compile(r"\[[^\]\n]+\]\([^\)\n]+\)")
|
|
187
|
-
bare_url_pattern = BARE_URL_PATTERN
|
|
188
|
-
|
|
189
207
|
def wrap_chunk(chunk: str) -> str:
|
|
190
208
|
parts: List[str] = []
|
|
191
209
|
cursor = 0
|
|
@@ -204,24 +222,20 @@ def normalize_bare_urls_for_slack_markdown(text: str) -> str:
|
|
|
204
222
|
continue
|
|
205
223
|
|
|
206
224
|
if char == "[":
|
|
207
|
-
link_match =
|
|
225
|
+
link_match = MARKDOWN_LINK_PATTERN.match(chunk, cursor)
|
|
208
226
|
if link_match:
|
|
209
227
|
parts.append(link_match.group(0))
|
|
210
228
|
cursor = link_match.end()
|
|
211
229
|
continue
|
|
212
230
|
|
|
213
231
|
if char == "`":
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
closing = chunk.find(delimiter, delimiter_end)
|
|
219
|
-
if closing != -1:
|
|
220
|
-
parts.append(chunk[cursor : closing + len(delimiter)])
|
|
221
|
-
cursor = closing + len(delimiter)
|
|
232
|
+
code_span_end = _find_inline_code_span_end(chunk, cursor)
|
|
233
|
+
if code_span_end is not None:
|
|
234
|
+
parts.append(chunk[cursor:code_span_end])
|
|
235
|
+
cursor = code_span_end
|
|
222
236
|
continue
|
|
223
237
|
|
|
224
|
-
url_match =
|
|
238
|
+
url_match = BARE_URL_PATTERN.match(chunk, cursor)
|
|
225
239
|
if url_match:
|
|
226
240
|
parts.append(f"<{url_match.group(0)}>")
|
|
227
241
|
cursor = url_match.end()
|
|
@@ -233,9 +247,6 @@ def normalize_bare_urls_for_slack_markdown(text: str) -> str:
|
|
|
233
247
|
return "".join(parts)
|
|
234
248
|
|
|
235
249
|
chunks = _split_fenced_code_chunks(text)
|
|
236
|
-
if not chunks:
|
|
237
|
-
return wrap_chunk(text)
|
|
238
|
-
|
|
239
250
|
return "".join(
|
|
240
251
|
chunk if is_fenced else wrap_chunk(chunk) for is_fenced, chunk in chunks
|
|
241
252
|
)
|
|
@@ -330,9 +341,6 @@ def normalize_underscore_emphasis(text: str) -> str:
|
|
|
330
341
|
return text
|
|
331
342
|
|
|
332
343
|
chunks = _split_fenced_code_chunks(text)
|
|
333
|
-
if not chunks:
|
|
334
|
-
return text
|
|
335
|
-
|
|
336
344
|
return "".join(
|
|
337
345
|
chunk if is_fenced else _normalize_underscore_emphasis_chunk(chunk)
|
|
338
346
|
for is_fenced, chunk in chunks
|
|
@@ -354,12 +362,6 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
354
362
|
return text, []
|
|
355
363
|
|
|
356
364
|
boundary_chars = {*VISIBLE_BOUNDARY_CHARS, ZWSP, SYNTH_SPACE_MARKER}
|
|
357
|
-
inline_code_pattern = re.compile(r"(?<!`)`[^`\n]+`(?!`)", flags=re.DOTALL)
|
|
358
|
-
emphasis_patterns = [
|
|
359
|
-
re.compile(r"(?<!\*)\*\*(.+?)\*\*(?!\*)", flags=re.DOTALL),
|
|
360
|
-
re.compile(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", flags=re.DOTALL),
|
|
361
|
-
re.compile(r"~~(.+?)~~", flags=re.DOTALL),
|
|
362
|
-
]
|
|
363
365
|
|
|
364
366
|
def wrap_match(match: re.Match[str], source: str) -> str:
|
|
365
367
|
start, end = match.start(), match.end()
|
|
@@ -384,44 +386,41 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
384
386
|
before_char = source[start - 1] if start > 0 else ""
|
|
385
387
|
after_char = source[end] if end < len(source) else ""
|
|
386
388
|
strategy = _nested_code_space_strategy(source, start, end, boundary_chars)
|
|
387
|
-
resolved_text =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
resolved_text = INLINE_CODE_PLACEHOLDER_PATTERN.sub(
|
|
390
|
+
lambda placeholder_match: replacements[placeholder_match.group(0)]["raw"],
|
|
391
|
+
match.group(0),
|
|
392
|
+
)
|
|
391
393
|
has_ascii_word = bool(re.search(r"[A-Za-z0-9]", resolved_text))
|
|
392
394
|
adjusted_text = match.group(0)
|
|
393
395
|
|
|
394
396
|
if strategy in {"ja_zh", "ko"}:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
+ adjusted_text[position + len(placeholder) :]
|
|
423
|
-
)
|
|
424
|
-
search_from = position + len(replacement_text)
|
|
397
|
+
|
|
398
|
+
def add_inner_spacing(placeholder_match: re.Match[str]) -> str:
|
|
399
|
+
before_inner = (
|
|
400
|
+
adjusted_text[placeholder_match.start() - 1]
|
|
401
|
+
if placeholder_match.start() > 0
|
|
402
|
+
else ""
|
|
403
|
+
)
|
|
404
|
+
after_inner = (
|
|
405
|
+
adjusted_text[placeholder_match.end()]
|
|
406
|
+
if placeholder_match.end() < len(adjusted_text)
|
|
407
|
+
else ""
|
|
408
|
+
)
|
|
409
|
+
prefix = (
|
|
410
|
+
f"{SYNTH_SPACE_MARKER} "
|
|
411
|
+
if _needs_inner_code_spacing(before_inner, boundary_chars)
|
|
412
|
+
else ""
|
|
413
|
+
)
|
|
414
|
+
suffix = (
|
|
415
|
+
f"{SYNTH_SPACE_MARKER} "
|
|
416
|
+
if _needs_inner_code_spacing(after_inner, boundary_chars)
|
|
417
|
+
else ""
|
|
418
|
+
)
|
|
419
|
+
return f"{prefix}{placeholder_match.group(0)}{suffix}"
|
|
420
|
+
|
|
421
|
+
adjusted_text = INLINE_CODE_PLACEHOLDER_PATTERN.sub(
|
|
422
|
+
add_inner_spacing, adjusted_text
|
|
423
|
+
)
|
|
425
424
|
|
|
426
425
|
if strategy == "ja_zh":
|
|
427
426
|
prefix = (
|
|
@@ -459,7 +458,7 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
459
458
|
protected_parts: List[str] = []
|
|
460
459
|
last_end = 0
|
|
461
460
|
|
|
462
|
-
for idx, match in enumerate(
|
|
461
|
+
for idx, match in enumerate(INLINE_CODE_SPAN_PATTERN.finditer(segment)):
|
|
463
462
|
placeholder = f"\ufff0code{idx}\ufff1"
|
|
464
463
|
protected_parts.append(segment[last_end : match.start()])
|
|
465
464
|
protected_parts.append(placeholder)
|
|
@@ -473,32 +472,36 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
473
472
|
protected_segment = "".join(protected_parts)
|
|
474
473
|
|
|
475
474
|
placeholders_inside_emphasis: set[str] = set()
|
|
476
|
-
for pattern in
|
|
475
|
+
for pattern in EMPHASIS_PATTERNS:
|
|
477
476
|
for match in pattern.finditer(protected_segment):
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
477
|
+
placeholders_inside_emphasis.update(
|
|
478
|
+
placeholder.group(0)
|
|
479
|
+
for placeholder in INLINE_CODE_PLACEHOLDER_PATTERN.finditer(
|
|
480
|
+
match.group(0)
|
|
481
|
+
)
|
|
482
|
+
)
|
|
482
483
|
|
|
483
484
|
for placeholder in placeholders_inside_emphasis:
|
|
484
485
|
placeholder_map[placeholder]["wrapped"] = placeholder_map[placeholder][
|
|
485
486
|
"raw"
|
|
486
487
|
]
|
|
487
488
|
|
|
488
|
-
for pattern in
|
|
489
|
+
for pattern in EMPHASIS_PATTERNS:
|
|
489
490
|
protected_segment = pattern.sub(
|
|
490
491
|
lambda m, s=protected_segment: (
|
|
491
492
|
wrap_nested_code_emphasis_match(m, s, placeholder_map)
|
|
492
|
-
if
|
|
493
|
+
if INLINE_CODE_PLACEHOLDER_PATTERN.search(m.group(0))
|
|
493
494
|
else wrap_match(m, s)
|
|
494
495
|
),
|
|
495
496
|
protected_segment,
|
|
496
497
|
)
|
|
497
498
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
499
|
+
protected_segment = INLINE_CODE_PLACEHOLDER_PATTERN.sub(
|
|
500
|
+
lambda placeholder_match: placeholder_map[placeholder_match.group(0)][
|
|
501
|
+
"wrapped"
|
|
502
|
+
],
|
|
503
|
+
protected_segment,
|
|
504
|
+
)
|
|
502
505
|
|
|
503
506
|
protected_segment = re.sub(
|
|
504
507
|
f"{ZWSP}{re.escape(SYNTH_SPACE_MARKER)} ",
|
|
@@ -522,9 +525,6 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
522
525
|
return _remove_synthetic_space_markers(protected_segment)
|
|
523
526
|
|
|
524
527
|
chunks = _split_fenced_code_chunks(text)
|
|
525
|
-
if not chunks:
|
|
526
|
-
return wrap_segment(text)
|
|
527
|
-
|
|
528
528
|
combined_parts: List[str] = []
|
|
529
529
|
combined_indices: List[int] = []
|
|
530
530
|
offset = 0
|
|
@@ -545,11 +545,6 @@ def _format_markdown_with_spacing_metadata(text: str) -> tuple[str, List[int]]:
|
|
|
545
545
|
add_zero_width_spaces = add_zero_width_spaces_to_markdown
|
|
546
546
|
|
|
547
547
|
|
|
548
|
-
def _is_strict_markdown_table_line(line: str) -> bool:
|
|
549
|
-
stripped = line.strip()
|
|
550
|
-
return bool(stripped) and stripped.startswith("|") and stripped.endswith("|")
|
|
551
|
-
|
|
552
|
-
|
|
553
548
|
def _split_markdown_table_cells(line: str) -> List[str]:
|
|
554
549
|
"""Split markdown table cells while preserving pipes inside <...|...> links."""
|
|
555
550
|
working = line.strip()
|
|
@@ -564,37 +559,43 @@ def _split_markdown_table_cells(line: str) -> List[str]:
|
|
|
564
559
|
cells: List[str] = []
|
|
565
560
|
current: List[str] = []
|
|
566
561
|
in_angle = False
|
|
567
|
-
in_inline_code = False
|
|
568
562
|
escaped = False
|
|
563
|
+
cursor = 0
|
|
569
564
|
|
|
570
|
-
|
|
565
|
+
while cursor < len(working):
|
|
566
|
+
ch = working[cursor]
|
|
571
567
|
if escaped:
|
|
572
568
|
current.append(ch)
|
|
573
569
|
escaped = False
|
|
570
|
+
cursor += 1
|
|
574
571
|
continue
|
|
575
572
|
|
|
576
573
|
if ch == "\\":
|
|
577
574
|
current.append(ch)
|
|
578
575
|
escaped = True
|
|
576
|
+
cursor += 1
|
|
579
577
|
continue
|
|
580
578
|
|
|
581
579
|
if ch == "`":
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
580
|
+
code_span_end = _find_inline_code_span_end(working, cursor)
|
|
581
|
+
if code_span_end is not None:
|
|
582
|
+
current.append(working[cursor:code_span_end])
|
|
583
|
+
cursor = code_span_end
|
|
584
|
+
continue
|
|
585
585
|
|
|
586
|
-
if
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
in_angle = False
|
|
586
|
+
if ch == "<":
|
|
587
|
+
in_angle = True
|
|
588
|
+
elif ch == ">" and in_angle:
|
|
589
|
+
in_angle = False
|
|
591
590
|
|
|
592
|
-
if ch == "|" and not in_angle
|
|
591
|
+
if ch == "|" and not in_angle:
|
|
593
592
|
cells.append("".join(current).strip())
|
|
594
593
|
current = []
|
|
594
|
+
cursor += 1
|
|
595
595
|
continue
|
|
596
596
|
|
|
597
597
|
current.append(ch)
|
|
598
|
+
cursor += 1
|
|
598
599
|
|
|
599
600
|
cells.append("".join(current).strip())
|
|
600
601
|
return cells
|
|
@@ -632,14 +633,33 @@ def _split_heading_and_table_row(
|
|
|
632
633
|
if "|" not in line:
|
|
633
634
|
return None
|
|
634
635
|
|
|
635
|
-
|
|
636
|
+
in_angle = False
|
|
637
|
+
escaped = False
|
|
636
638
|
first_pipe = -1
|
|
637
|
-
|
|
639
|
+
cursor = 0
|
|
640
|
+
while cursor < len(line):
|
|
641
|
+
ch = line[cursor]
|
|
642
|
+
if escaped:
|
|
643
|
+
escaped = False
|
|
644
|
+
cursor += 1
|
|
645
|
+
continue
|
|
646
|
+
if ch == "\\":
|
|
647
|
+
escaped = True
|
|
648
|
+
cursor += 1
|
|
649
|
+
continue
|
|
638
650
|
if ch == "`":
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
651
|
+
code_span_end = _find_inline_code_span_end(line, cursor)
|
|
652
|
+
if code_span_end is not None:
|
|
653
|
+
cursor = code_span_end
|
|
654
|
+
continue
|
|
655
|
+
if ch == "<":
|
|
656
|
+
in_angle = True
|
|
657
|
+
elif ch == ">" and in_angle:
|
|
658
|
+
in_angle = False
|
|
659
|
+
elif ch == "|" and not in_angle:
|
|
660
|
+
first_pipe = cursor
|
|
642
661
|
break
|
|
662
|
+
cursor += 1
|
|
643
663
|
|
|
644
664
|
if first_pipe < 0:
|
|
645
665
|
return None
|
|
@@ -833,8 +853,13 @@ def _create_table_cell(text: str) -> Dict[str, Any]:
|
|
|
833
853
|
if prefix:
|
|
834
854
|
elements.append({"type": "text", "text": prefix})
|
|
835
855
|
|
|
836
|
-
markdown_label, markdown_url, angle_url, angle_label, token = match.groups()
|
|
837
856
|
element: Dict[str, Any]
|
|
857
|
+
markdown_label = match.group("markdown_label")
|
|
858
|
+
markdown_url = match.group("markdown_url")
|
|
859
|
+
angle_url = match.group("angle_url")
|
|
860
|
+
angle_label = match.group("angle_label")
|
|
861
|
+
token = match.group("token") or ""
|
|
862
|
+
|
|
838
863
|
if markdown_label and markdown_url:
|
|
839
864
|
element = {"type": "link", "url": markdown_url, "text": markdown_label}
|
|
840
865
|
elif angle_url:
|
|
@@ -845,10 +870,11 @@ def _create_table_cell(text: str) -> Dict[str, Any]:
|
|
|
845
870
|
}
|
|
846
871
|
else:
|
|
847
872
|
style: Dict[str, bool] = {}
|
|
848
|
-
content = token
|
|
873
|
+
content = token
|
|
849
874
|
|
|
850
875
|
if content.startswith("`") and content.endswith("`"):
|
|
851
|
-
|
|
876
|
+
delimiter_len = len(match.group("code_delimiter") or "`")
|
|
877
|
+
content = content[delimiter_len:-delimiter_len]
|
|
852
878
|
style["code"] = True
|
|
853
879
|
elif content.startswith("~~") and content.endswith("~~"):
|
|
854
880
|
content = content[2:-2]
|
|
@@ -1066,8 +1092,6 @@ def convert_markdown_to_slack_payloads(
|
|
|
1066
1092
|
payloads: List[Dict[str, Any]] = []
|
|
1067
1093
|
for blocks in convert_markdown_to_slack_messages(markdown_text):
|
|
1068
1094
|
fallback_text = build_fallback_text_from_blocks(blocks).strip()
|
|
1069
|
-
if not fallback_text:
|
|
1070
|
-
fallback_text = blocks_to_plain_text(blocks).strip()
|
|
1071
1095
|
payloads.append({"blocks": blocks, "text": fallback_text or " "})
|
|
1072
1096
|
return payloads
|
|
1073
1097
|
|
|
@@ -562,6 +562,15 @@ def test_heading_with_inline_code_pipe_is_not_split() -> None:
|
|
|
562
562
|
assert "Title" in blocks[0].get("text", "")
|
|
563
563
|
|
|
564
564
|
|
|
565
|
+
def test_heading_with_multi_backtick_inline_code_pipe_is_not_split() -> None:
|
|
566
|
+
raw = "# Title ``a|b``\n\nsome text\n"
|
|
567
|
+
|
|
568
|
+
blocks = convert_markdown_to_slack_blocks(raw)
|
|
569
|
+
assert len(blocks) == 1
|
|
570
|
+
assert all(b.get("type") == "markdown" for b in blocks)
|
|
571
|
+
assert blocks[0]["text"] == raw.strip()
|
|
572
|
+
|
|
573
|
+
|
|
565
574
|
def test_code_fence_with_table_like_rows_stays_markdown() -> None:
|
|
566
575
|
raw = """```
|
|
567
576
|
a | b | c
|
|
@@ -596,3 +605,14 @@ def test_escaped_pipe_in_table_cell_strips_backslash() -> None:
|
|
|
596
605
|
table = _first_table(convert_markdown_to_slack_blocks(raw))
|
|
597
606
|
cell_text = extract_plain_text_from_table_cell(table["rows"][1][0])
|
|
598
607
|
assert cell_text == "A | B"
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_multi_backtick_code_span_keeps_pipe_inside_table_cell() -> None:
|
|
611
|
+
raw = "| A | B |\n|---|---|\n| left | ``x|y`` |\n"
|
|
612
|
+
|
|
613
|
+
table = _first_table(convert_markdown_to_slack_blocks(raw))
|
|
614
|
+
code_cell = table["rows"][1][1]["elements"][0]["elements"][0]
|
|
615
|
+
|
|
616
|
+
assert extract_plain_text_from_table_cell(table["rows"][1][1]) == "x|y"
|
|
617
|
+
assert code_cell["text"] == "x|y"
|
|
618
|
+
assert code_cell["style"] == {"code": True}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/slack-client-manual-checklist.md
RENAMED
|
File without changes
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/slack-nested-modifier-findings.md
RENAMED
|
File without changes
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/docs/slack-render-test-workflow.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/fixtures/llm_markdown_p0_corpus.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/test_llm_markdown_p0_corpus.py
RENAMED
|
File without changes
|
{slack_markdown_parser-2.2.2 → slack_markdown_parser-2.2.3}/tests/test_nested_modifier_matrix.py
RENAMED
|
File without changes
|