@jacobbubu/md-to-lark 1.4.7 → 1.4.9

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.
@@ -1,32 +1,104 @@
1
- const CJK_BOLD_TRAILING_PUNCTUATION = ',。;:!?、】【)」』》、';
2
- const BOLD_PUNCTUATION_RE = new RegExp(`(^|[^\\p{Script=Han}\\p{Letter}\\p{Number}*])(\\*\\*([^*\\n]+?)([${CJK_BOLD_TRAILING_PUNCTUATION}])\\*\\*)(?=[\\p{Script=Han}\\p{Letter}\\p{Number}\\[])`, 'gu');
3
- function transformOutsideInlineCode(segment) {
4
- return segment.replace(BOLD_PUNCTUATION_RE, (_match, prefix, _fullBold, content, punctuation) => {
5
- return `${prefix}**${content}**${punctuation}`;
6
- });
1
+ import { unified } from 'unified';
2
+ import remarkParse from 'remark-parse';
3
+ const CJK_BOLD_TRAILING_PUNCTUATION = new Set([',', '。', ';', ':', '!', '?', '、', ')', '》', '】', '」', '』']);
4
+ const NORMALIZATION_NEXT_CHAR_RE = /[\p{Script=Han}\p{Letter}\p{Number}\[]/u;
5
+ const PROTECTED_NODE_TYPES = new Set(['code', 'inlineCode', 'html']);
6
+ function mergeProtectedRanges(ranges) {
7
+ const sorted = [...ranges].sort((a, b) => a.start - b.start || a.end - b.end);
8
+ const merged = [];
9
+ for (const range of sorted) {
10
+ const last = merged[merged.length - 1];
11
+ if (!last || range.start > last.end) {
12
+ merged.push({ ...range });
13
+ continue;
14
+ }
15
+ if (range.end > last.end) {
16
+ last.end = range.end;
17
+ }
18
+ }
19
+ return merged;
20
+ }
21
+ function collectProtectedRanges(markdown) {
22
+ const tree = unified().use(remarkParse).parse(markdown);
23
+ const ranges = [];
24
+ const visit = (node) => {
25
+ if (!node || typeof node !== 'object')
26
+ return;
27
+ const record = node;
28
+ const start = record.position?.start?.offset;
29
+ const end = record.position?.end?.offset;
30
+ if (record.type &&
31
+ PROTECTED_NODE_TYPES.has(record.type) &&
32
+ typeof start === 'number' &&
33
+ typeof end === 'number' &&
34
+ end > start) {
35
+ ranges.push({ start, end });
36
+ return;
37
+ }
38
+ for (const child of record.children ?? []) {
39
+ visit(child);
40
+ }
41
+ };
42
+ visit(tree);
43
+ return mergeProtectedRanges(ranges);
7
44
  }
8
- function normalizeLineOutsideInlineCode(line) {
9
- let output = '';
45
+ function collectCjkBoldNormalizationEdits(segment, baseOffset) {
46
+ const edits = [];
10
47
  let cursor = 0;
11
- while (cursor < line.length) {
12
- const tickIndex = line.indexOf('`', cursor);
13
- if (tickIndex === -1) {
14
- output += transformOutsideInlineCode(line.slice(cursor));
48
+ while (cursor < segment.length - 1) {
49
+ const open = segment.indexOf('**', cursor);
50
+ if (open === -1)
15
51
  break;
52
+ if (open > 0 && segment[open - 1] === '*') {
53
+ cursor = open + 2;
54
+ continue;
16
55
  }
17
- output += transformOutsideInlineCode(line.slice(cursor, tickIndex));
18
- let tickEnd = tickIndex;
19
- while (tickEnd < line.length && line[tickEnd] === '`') {
20
- tickEnd += 1;
56
+ const firstContentChar = segment[open + 2] ?? '';
57
+ if (!firstContentChar || /\s/u.test(firstContentChar)) {
58
+ cursor = open + 2;
59
+ continue;
21
60
  }
22
- const delimiter = line.slice(tickIndex, tickEnd);
23
- const closeIndex = line.indexOf(delimiter, tickEnd);
24
- if (closeIndex === -1) {
25
- output += line.slice(tickIndex);
26
- break;
61
+ let closeSearch = open + 2;
62
+ let matched = false;
63
+ while (closeSearch < segment.length - 1) {
64
+ const close = segment.indexOf('**', closeSearch);
65
+ if (close === -1)
66
+ break;
67
+ const rawContent = segment.slice(open + 2, close);
68
+ if (rawContent.includes('\n')) {
69
+ break;
70
+ }
71
+ if (rawContent.includes('**')) {
72
+ closeSearch = close + 2;
73
+ continue;
74
+ }
75
+ const punctuation = segment[close - 1] ?? '';
76
+ const content = segment.slice(open + 2, close - 1);
77
+ const nextChar = segment[close + 2] ?? '';
78
+ if (content && CJK_BOLD_TRAILING_PUNCTUATION.has(punctuation) && NORMALIZATION_NEXT_CHAR_RE.test(nextChar)) {
79
+ edits.push({
80
+ start: baseOffset + open,
81
+ end: baseOffset + close + 2,
82
+ replacement: `**${content}**${punctuation}`,
83
+ });
84
+ cursor = close + 2;
85
+ matched = true;
86
+ break;
87
+ }
88
+ closeSearch = close + 2;
89
+ }
90
+ if (!matched) {
91
+ cursor = open + 2;
27
92
  }
28
- output += line.slice(tickIndex, closeIndex + delimiter.length);
29
- cursor = closeIndex + delimiter.length;
93
+ }
94
+ return edits;
95
+ }
96
+ function applyTextEdits(source, edits) {
97
+ if (edits.length === 0)
98
+ return source;
99
+ let output = source;
100
+ for (const edit of [...edits].sort((a, b) => b.start - a.start || b.end - a.end)) {
101
+ output = `${output.slice(0, edit.start)}${edit.replacement}${output.slice(edit.end)}`;
30
102
  }
31
103
  return output;
32
104
  }
@@ -47,41 +119,19 @@ export function rewriteLeadingFrontmatterAsCodeFence(markdown) {
47
119
  return hasBom ? `\uFEFF${rewritten}` : rewritten;
48
120
  }
49
121
  export function normalizeChineseBoldClosingPunctuation(markdown) {
50
- const lines = markdown.match(/[^\r\n]*(?:\r?\n|$)/g) ?? [];
51
- let inFence = false;
52
- let activeFenceChar = '';
53
- let activeFenceLength = 0;
54
- let output = '';
55
- for (const chunk of lines) {
56
- if (!chunk)
57
- continue;
58
- const lineBreakMatch = chunk.match(/(\r?\n)$/);
59
- const lineBreak = lineBreakMatch?.[1] ?? '';
60
- const line = lineBreak ? chunk.slice(0, -lineBreak.length) : chunk;
61
- const fenceMatch = line.match(/^[ \t]{0,3}([`~]{3,})/);
62
- if (inFence) {
63
- output += chunk;
64
- if (fenceMatch) {
65
- const fence = fenceMatch[1] ?? '';
66
- if (fence && fence[0] === activeFenceChar && fence.length >= activeFenceLength) {
67
- inFence = false;
68
- activeFenceChar = '';
69
- activeFenceLength = 0;
70
- }
71
- }
72
- continue;
73
- }
74
- if (fenceMatch) {
75
- const fence = fenceMatch[1] ?? '';
76
- inFence = true;
77
- activeFenceChar = fence[0] ?? '';
78
- activeFenceLength = fence.length;
79
- output += chunk;
80
- continue;
122
+ const protectedRanges = collectProtectedRanges(markdown);
123
+ const edits = [];
124
+ let cursor = 0;
125
+ for (const range of protectedRanges) {
126
+ if (cursor < range.start) {
127
+ edits.push(...collectCjkBoldNormalizationEdits(markdown.slice(cursor, range.start), cursor));
81
128
  }
82
- output += normalizeLineOutsideInlineCode(line) + lineBreak;
129
+ cursor = Math.max(cursor, range.end);
83
130
  }
84
- return output;
131
+ if (cursor < markdown.length) {
132
+ edits.push(...collectCjkBoldNormalizationEdits(markdown.slice(cursor), cursor));
133
+ }
134
+ return applyTextEdits(markdown, edits);
85
135
  }
86
136
  export function normalizeMarkdownBeforeParse(markdown) {
87
137
  return normalizeChineseBoldClosingPunctuation(rewriteLeadingFrontmatterAsCodeFence(markdown));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbubu/md-to-lark",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
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",