@jacobbubu/md-to-lark 1.4.9 → 1.4.11

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.
@@ -223,6 +223,36 @@ function mergeAdjacentTextRuns(inlines) {
223
223
  }
224
224
  return merged;
225
225
  }
226
+ function trimBoundaryNewlinesFromInlines(inlines) {
227
+ const trimmed = inlines.map((inline) => ({ ...inline }));
228
+ let start = 0;
229
+ let end = trimmed.length;
230
+ while (start < end) {
231
+ const inline = trimmed[start];
232
+ if (!inline || inline.kind !== 'text_run')
233
+ break;
234
+ const nextText = (inline.text ?? '').replace(/^(?:\r?\n)+/, '');
235
+ if (nextText.length === 0) {
236
+ start += 1;
237
+ continue;
238
+ }
239
+ inline.text = nextText;
240
+ break;
241
+ }
242
+ while (end > start) {
243
+ const inline = trimmed[end - 1];
244
+ if (!inline || inline.kind !== 'text_run')
245
+ break;
246
+ const nextText = (inline.text ?? '').replace(/(?:\r?\n)+$/, '');
247
+ if (nextText.length === 0) {
248
+ end -= 1;
249
+ continue;
250
+ }
251
+ inline.text = nextText;
252
+ break;
253
+ }
254
+ return mergeAdjacentTextRuns(trimmed.slice(start, end));
255
+ }
226
256
  function hasClassName(element, expected) {
227
257
  return getClassNames(element).includes(expected);
228
258
  }
@@ -674,17 +704,7 @@ function convertTable(ctx, table, parentId) {
674
704
  return [tableId];
675
705
  }
676
706
  function convertBlockquote(ctx, blockquote, parentId) {
677
- const quoteText = trimBoundaryNewlines(toString(blockquote));
678
- const inlines = quoteText.length
679
- ? [
680
- {
681
- id: nextInlineId(ctx),
682
- kind: 'text_run',
683
- marks: createDefaultMarks(),
684
- text: quoteText,
685
- },
686
- ]
687
- : [];
707
+ const inlines = trimBoundaryNewlinesFromInlines(parseInlineNodes(ctx, getChildren(blockquote)));
688
708
  return [createTextualBlock(ctx, 'quote', parentId, inlines)];
689
709
  }
690
710
  function trimBoundaryNewlines(value) {
@@ -3,6 +3,9 @@ import remarkParse from 'remark-parse';
3
3
  const CJK_BOLD_TRAILING_PUNCTUATION = new Set([',', '。', ';', ':', '!', '?', '、', ')', '》', '】', '」', '』']);
4
4
  const NORMALIZATION_NEXT_CHAR_RE = /[\p{Script=Han}\p{Letter}\p{Number}\[]/u;
5
5
  const PROTECTED_NODE_TYPES = new Set(['code', 'inlineCode', 'html']);
6
+ function parseMarkdown(markdown) {
7
+ return unified().use(remarkParse).parse(markdown);
8
+ }
6
9
  function mergeProtectedRanges(ranges) {
7
10
  const sorted = [...ranges].sort((a, b) => a.start - b.start || a.end - b.end);
8
11
  const merged = [];
@@ -19,7 +22,7 @@ function mergeProtectedRanges(ranges) {
19
22
  return merged;
20
23
  }
21
24
  function collectProtectedRanges(markdown) {
22
- const tree = unified().use(remarkParse).parse(markdown);
25
+ const tree = parseMarkdown(markdown);
23
26
  const ranges = [];
24
27
  const visit = (node) => {
25
28
  if (!node || typeof node !== 'object')
@@ -42,19 +45,84 @@ function collectProtectedRanges(markdown) {
42
45
  visit(tree);
43
46
  return mergeProtectedRanges(ranges);
44
47
  }
45
- function collectCjkBoldNormalizationEdits(segment, baseOffset) {
48
+ function collectParsedStrongRanges(markdown) {
49
+ const tree = parseMarkdown(markdown);
50
+ const ranges = [];
51
+ const visit = (node) => {
52
+ if (!node || typeof node !== 'object')
53
+ return;
54
+ const record = node;
55
+ const start = record.position?.start?.offset;
56
+ const end = record.position?.end?.offset;
57
+ if (record.type === 'strong' && typeof start === 'number' && typeof end === 'number' && end > start) {
58
+ const raw = markdown.slice(start, end);
59
+ if (raw.startsWith('**') && raw.endsWith('**')) {
60
+ ranges.push({ start, end });
61
+ }
62
+ }
63
+ for (const child of record.children ?? []) {
64
+ visit(child);
65
+ }
66
+ };
67
+ visit(tree);
68
+ return ranges.sort((a, b) => a.start - b.start || a.end - b.end);
69
+ }
70
+ function canDelimiterCloseCjkBoldCandidate(markdown, close, blockedOpeningOffsets) {
71
+ const punctuation = markdown[close - 1] ?? '';
72
+ const nextChar = markdown[close + 2] ?? '';
73
+ if (!CJK_BOLD_TRAILING_PUNCTUATION.has(punctuation) || !NORMALIZATION_NEXT_CHAR_RE.test(nextChar)) {
74
+ return false;
75
+ }
76
+ let searchFrom = close - 1;
77
+ while (searchFrom >= 0) {
78
+ const open = markdown.lastIndexOf('**', searchFrom);
79
+ if (open === -1)
80
+ return false;
81
+ if (blockedOpeningOffsets.has(open) || (open > 0 && markdown[open - 1] === '*')) {
82
+ searchFrom = open - 1;
83
+ continue;
84
+ }
85
+ const rawContent = markdown.slice(open + 2, close);
86
+ if (rawContent.includes('\n')) {
87
+ return false;
88
+ }
89
+ if (rawContent.includes('**')) {
90
+ searchFrom = open - 1;
91
+ continue;
92
+ }
93
+ const firstContentChar = markdown[open + 2] ?? '';
94
+ const content = markdown.slice(open + 2, close - 1);
95
+ return Boolean(content &&
96
+ firstContentChar &&
97
+ !/\s/u.test(firstContentChar) &&
98
+ !CJK_BOLD_TRAILING_PUNCTUATION.has(firstContentChar));
99
+ }
100
+ return false;
101
+ }
102
+ function collectTrustedStrongClosingDelimiters(markdown) {
103
+ const trustedClosingDelimiters = new Set();
104
+ for (const range of collectParsedStrongRanges(markdown)) {
105
+ if (canDelimiterCloseCjkBoldCandidate(markdown, range.start, trustedClosingDelimiters)) {
106
+ continue;
107
+ }
108
+ trustedClosingDelimiters.add(range.end - 2);
109
+ }
110
+ return trustedClosingDelimiters;
111
+ }
112
+ function collectCjkBoldNormalizationEdits(segment, baseOffset, blockedOpeningOffsets) {
46
113
  const edits = [];
47
114
  let cursor = 0;
48
115
  while (cursor < segment.length - 1) {
49
116
  const open = segment.indexOf('**', cursor);
50
117
  if (open === -1)
51
118
  break;
52
- if (open > 0 && segment[open - 1] === '*') {
119
+ const absoluteOpen = baseOffset + open;
120
+ if (blockedOpeningOffsets.has(absoluteOpen) || (open > 0 && segment[open - 1] === '*')) {
53
121
  cursor = open + 2;
54
122
  continue;
55
123
  }
56
124
  const firstContentChar = segment[open + 2] ?? '';
57
- if (!firstContentChar || /\s/u.test(firstContentChar)) {
125
+ if (!firstContentChar || /\s/u.test(firstContentChar) || CJK_BOLD_TRAILING_PUNCTUATION.has(firstContentChar)) {
58
126
  cursor = open + 2;
59
127
  continue;
60
128
  }
@@ -120,16 +188,17 @@ export function rewriteLeadingFrontmatterAsCodeFence(markdown) {
120
188
  }
121
189
  export function normalizeChineseBoldClosingPunctuation(markdown) {
122
190
  const protectedRanges = collectProtectedRanges(markdown);
191
+ const trustedStrongClosingDelimiters = collectTrustedStrongClosingDelimiters(markdown);
123
192
  const edits = [];
124
193
  let cursor = 0;
125
194
  for (const range of protectedRanges) {
126
195
  if (cursor < range.start) {
127
- edits.push(...collectCjkBoldNormalizationEdits(markdown.slice(cursor, range.start), cursor));
196
+ edits.push(...collectCjkBoldNormalizationEdits(markdown.slice(cursor, range.start), cursor, trustedStrongClosingDelimiters));
128
197
  }
129
198
  cursor = Math.max(cursor, range.end);
130
199
  }
131
200
  if (cursor < markdown.length) {
132
- edits.push(...collectCjkBoldNormalizationEdits(markdown.slice(cursor), cursor));
201
+ edits.push(...collectCjkBoldNormalizationEdits(markdown.slice(cursor), cursor, trustedStrongClosingDelimiters));
133
202
  }
134
203
  return applyTextEdits(markdown, edits);
135
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbubu/md-to-lark",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "Publish Markdown to Feishu docs with a stable pipeline.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",