@jacobbubu/md-to-lark 1.0.0
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.
- package/LICENSE +15 -0
- package/README.md +171 -0
- package/dist/btt/build-tree.js +79 -0
- package/dist/btt/index.js +1 -0
- package/dist/btt/types.js +1 -0
- package/dist/cli/publish-md-to-lark.js +15 -0
- package/dist/commands/publish-md/args.js +224 -0
- package/dist/commands/publish-md/command.js +97 -0
- package/dist/commands/publish-md/index.js +1 -0
- package/dist/commands/publish-md/input-resolver.js +48 -0
- package/dist/commands/publish-md/mermaid-render.js +17 -0
- package/dist/commands/publish-md/pipeline-transform.js +4 -0
- package/dist/commands/publish-md/preset-loader.js +113 -0
- package/dist/commands/publish-md/presets/medium.js +7 -0
- package/dist/commands/publish-md/presets/zh-format.js +8 -0
- package/dist/commands/publish-md/title-policy.js +93 -0
- package/dist/index.js +1 -0
- package/dist/interop/btt-to-last.js +79 -0
- package/dist/interop/codec-btt-to-last.js +435 -0
- package/dist/interop/codec-last-to-btt.js +383 -0
- package/dist/interop/codec-shared.js +722 -0
- package/dist/interop/index.js +2 -0
- package/dist/interop/last-to-btt.js +17 -0
- package/dist/lark/block-types.js +42 -0
- package/dist/lark/client.js +36 -0
- package/dist/lark/docx/ops.js +596 -0
- package/dist/lark/docx/render-btt.js +156 -0
- package/dist/lark/docx/render-models.js +1 -0
- package/dist/lark/docx/render-payload.js +338 -0
- package/dist/lark/docx/render-post-process.js +98 -0
- package/dist/lark/docx/render-table.js +87 -0
- package/dist/lark/docx/render-types.js +7 -0
- package/dist/lark/index.js +2 -0
- package/dist/lark/types.js +1 -0
- package/dist/last/api.js +1687 -0
- package/dist/last/index.js +3 -0
- package/dist/last/preview-terminal.js +296 -0
- package/dist/last/textual-block-types.js +19 -0
- package/dist/last/to-markdown.js +303 -0
- package/dist/last/types.js +11 -0
- package/dist/pipeline/hast-to-last.js +946 -0
- package/dist/pipeline/index.js +3 -0
- package/dist/pipeline/markdown/md-to-hast.js +34 -0
- package/dist/pipeline/markdown/prepare-markdown.js +1049 -0
- package/dist/preview/index.js +1 -0
- package/dist/preview/markdown-terminal.js +350 -0
- package/dist/publish/asset-adapter.js +123 -0
- package/dist/publish/btt-patch.js +65 -0
- package/dist/publish/common.js +139 -0
- package/dist/publish/ids.js +9 -0
- package/dist/publish/index.js +7 -0
- package/dist/publish/last-normalize.js +327 -0
- package/dist/publish/process-file.js +228 -0
- package/dist/publish/runtime.js +133 -0
- package/dist/publish/stage-cache.js +56 -0
- package/dist/shared/rate-limiter.js +18 -0
- package/dist/shared/retry.js +141 -0
- package/package.json +78 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { LAST_TEXTUAL_BLOCK_TYPE_SET } from './textual-block-types.js';
|
|
2
|
+
const ANSI = {
|
|
3
|
+
reset: '\u001b[0m',
|
|
4
|
+
bold: '\u001b[1m',
|
|
5
|
+
italic: '\u001b[3m',
|
|
6
|
+
underline: '\u001b[4m',
|
|
7
|
+
strike: '\u001b[9m',
|
|
8
|
+
inverse: '\u001b[7m',
|
|
9
|
+
dim: '\u001b[2m',
|
|
10
|
+
blue: '\u001b[34m',
|
|
11
|
+
cyan: '\u001b[36m',
|
|
12
|
+
magenta: '\u001b[35m',
|
|
13
|
+
yellow: '\u001b[33m',
|
|
14
|
+
red: '\u001b[31m',
|
|
15
|
+
green: '\u001b[32m',
|
|
16
|
+
gray: '\u001b[90m',
|
|
17
|
+
};
|
|
18
|
+
const INLINE_COLOR_TO_ANSI = {
|
|
19
|
+
light_pink: ANSI.magenta,
|
|
20
|
+
light_orange: ANSI.yellow,
|
|
21
|
+
light_yellow: ANSI.yellow,
|
|
22
|
+
light_green: ANSI.green,
|
|
23
|
+
light_blue: ANSI.blue,
|
|
24
|
+
light_purple: ANSI.magenta,
|
|
25
|
+
light_gray: ANSI.gray,
|
|
26
|
+
dark_pink: ANSI.magenta,
|
|
27
|
+
dark_orange: ANSI.yellow,
|
|
28
|
+
dark_yellow: ANSI.yellow,
|
|
29
|
+
dark_green: ANSI.green,
|
|
30
|
+
dark_blue: ANSI.blue,
|
|
31
|
+
dark_purple: ANSI.magenta,
|
|
32
|
+
dark_gray: ANSI.gray,
|
|
33
|
+
dark_silver_gray: ANSI.gray,
|
|
34
|
+
};
|
|
35
|
+
function isTextualBlock(block) {
|
|
36
|
+
return LAST_TEXTUAL_BLOCK_TYPE_SET.has(block.type);
|
|
37
|
+
}
|
|
38
|
+
function withAnsi(enabled, text, ...codes) {
|
|
39
|
+
if (!enabled || text.length === 0)
|
|
40
|
+
return text;
|
|
41
|
+
const starts = codes.filter((x) => Boolean(x));
|
|
42
|
+
if (starts.length === 0)
|
|
43
|
+
return text;
|
|
44
|
+
return `${starts.join('')}${text}${ANSI.reset}`;
|
|
45
|
+
}
|
|
46
|
+
function blockTypeTag(ctx, block) {
|
|
47
|
+
if (!ctx.options.showTypeTag)
|
|
48
|
+
return '';
|
|
49
|
+
return withAnsi(ctx.options.color, `[${block.type}]`, ANSI.dim);
|
|
50
|
+
}
|
|
51
|
+
function blockIdTag(ctx, block) {
|
|
52
|
+
if (!ctx.options.showBlockId)
|
|
53
|
+
return '';
|
|
54
|
+
return withAnsi(ctx.options.color, `(${block.id})`, ANSI.gray);
|
|
55
|
+
}
|
|
56
|
+
function inlineToText(ctx, inline) {
|
|
57
|
+
const colorEnabled = ctx.options.color;
|
|
58
|
+
const applyTextRunStyle = (text) => {
|
|
59
|
+
const marks = inline.marks;
|
|
60
|
+
const textColor = marks.textColor ? INLINE_COLOR_TO_ANSI[marks.textColor] : undefined;
|
|
61
|
+
const decorated = withAnsi(colorEnabled, text, marks.bold ? ANSI.bold : undefined, marks.italic ? ANSI.italic : undefined, marks.underline ? ANSI.underline : undefined, marks.strikethrough ? ANSI.strike : undefined, marks.inlineCode ? ANSI.inverse : undefined, textColor);
|
|
62
|
+
if (marks.link?.url) {
|
|
63
|
+
return `${decorated}${withAnsi(colorEnabled, `(${marks.link.url})`, ANSI.blue, ANSI.underline)}`;
|
|
64
|
+
}
|
|
65
|
+
return decorated;
|
|
66
|
+
};
|
|
67
|
+
switch (inline.kind) {
|
|
68
|
+
case 'text_run':
|
|
69
|
+
return applyTextRunStyle(inline.text ?? '');
|
|
70
|
+
case 'mention_user':
|
|
71
|
+
return withAnsi(colorEnabled, `@${inline.userId ?? 'unknown'}`, ANSI.cyan);
|
|
72
|
+
case 'equation':
|
|
73
|
+
return withAnsi(colorEnabled, `$${inline.latex ?? ''}$`, ANSI.magenta);
|
|
74
|
+
case 'mention_doc': {
|
|
75
|
+
const title = inline.title ?? inline.token ?? 'doc';
|
|
76
|
+
const suffix = inline.url ? `(${inline.url})` : '';
|
|
77
|
+
return withAnsi(colorEnabled, `[doc:${title}]${suffix}`, ANSI.blue, ANSI.underline);
|
|
78
|
+
}
|
|
79
|
+
case 'reminder': {
|
|
80
|
+
const expire = inline.expireTime ? `@${inline.expireTime}` : '';
|
|
81
|
+
return withAnsi(colorEnabled, `[reminder${expire}]`, ANSI.yellow);
|
|
82
|
+
}
|
|
83
|
+
case 'inline_block':
|
|
84
|
+
return withAnsi(colorEnabled, `[inline:${inline.blockId ?? 'block'}]`, ANSI.dim);
|
|
85
|
+
case 'inline_file':
|
|
86
|
+
return withAnsi(colorEnabled, `[file:${inline.fileToken ?? 'token'}]`, ANSI.dim);
|
|
87
|
+
case 'link_preview': {
|
|
88
|
+
const text = inline.title ?? inline.url ?? 'link_preview';
|
|
89
|
+
return withAnsi(colorEnabled, `[link:${text}]`, ANSI.blue, ANSI.underline);
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function textualBlockText(ctx, block) {
|
|
96
|
+
return block.payload.inlines.map((inline) => inlineToText(ctx, inline)).join('');
|
|
97
|
+
}
|
|
98
|
+
function indent(depth) {
|
|
99
|
+
return ' '.repeat(Math.max(0, depth));
|
|
100
|
+
}
|
|
101
|
+
function pushLine(ctx, line = '') {
|
|
102
|
+
ctx.lines.push(line);
|
|
103
|
+
}
|
|
104
|
+
function renderTextualBlock(ctx, block, meta) {
|
|
105
|
+
const gap = `${blockTypeTag(ctx, block)} ${blockIdTag(ctx, block)}`.trim();
|
|
106
|
+
const suffix = gap.length > 0 ? ` ${gap}` : '';
|
|
107
|
+
const text = textualBlockText(ctx, block);
|
|
108
|
+
const baseIndent = indent(meta.depth);
|
|
109
|
+
const headingLevelMap = {
|
|
110
|
+
heading1: 1,
|
|
111
|
+
heading2: 2,
|
|
112
|
+
heading3: 3,
|
|
113
|
+
heading4: 4,
|
|
114
|
+
heading5: 5,
|
|
115
|
+
heading6: 6,
|
|
116
|
+
heading7: 7,
|
|
117
|
+
heading8: 8,
|
|
118
|
+
heading9: 9,
|
|
119
|
+
};
|
|
120
|
+
if (block.type === 'page') {
|
|
121
|
+
if (text.trim().length > 0) {
|
|
122
|
+
pushLine(ctx, `${withAnsi(ctx.options.color, text, ANSI.bold)}${suffix}`);
|
|
123
|
+
if (ctx.options.sectionGapLines > 0) {
|
|
124
|
+
for (let i = 0; i < ctx.options.sectionGapLines; i += 1) {
|
|
125
|
+
pushLine(ctx, '');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (block.type === 'text') {
|
|
132
|
+
pushLine(ctx, `${baseIndent}${text}${suffix}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const headingLevel = headingLevelMap[block.type];
|
|
136
|
+
if (headingLevel) {
|
|
137
|
+
const hashes = '#'.repeat(headingLevel);
|
|
138
|
+
pushLine(ctx, `${baseIndent}${withAnsi(ctx.options.color, `${hashes} ${text}`, ANSI.bold)}${suffix}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (block.type === 'bullet') {
|
|
142
|
+
pushLine(ctx, `${baseIndent}• ${text}${suffix}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (block.type === 'ordered') {
|
|
146
|
+
const n = meta.orderedNumber ?? 1;
|
|
147
|
+
pushLine(ctx, `${baseIndent}${n}. ${text}${suffix}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (block.type === 'quote') {
|
|
151
|
+
pushLine(ctx, `${baseIndent}> ${text}${suffix}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (block.type === 'todo') {
|
|
155
|
+
const done = block.payload.style.done === true;
|
|
156
|
+
const marker = done ? '[x]' : '[ ]';
|
|
157
|
+
pushLine(ctx, `${baseIndent}${marker} ${text}${suffix}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (block.type === 'code') {
|
|
161
|
+
const lang = block.payload.style.language ?? 'text';
|
|
162
|
+
const header = withAnsi(ctx.options.color, `\`\`\`${lang}`, ANSI.dim);
|
|
163
|
+
pushLine(ctx, `${baseIndent}${header}${suffix}`);
|
|
164
|
+
pushLine(ctx, `${baseIndent}${text}`);
|
|
165
|
+
pushLine(ctx, `${baseIndent}${withAnsi(ctx.options.color, '\`\`\`', ANSI.dim)}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
pushLine(ctx, `${baseIndent}${text}${suffix}`);
|
|
169
|
+
}
|
|
170
|
+
function renderContainerPlaceholder(ctx, block, depth) {
|
|
171
|
+
const prefix = indent(depth);
|
|
172
|
+
const info = [`[${block.type}]`];
|
|
173
|
+
if ('payload' in block) {
|
|
174
|
+
if (block.type === 'image' || block.type === 'board') {
|
|
175
|
+
const token = block.payload.token ?? 'no-token';
|
|
176
|
+
const width = block.payload.width ?? 0;
|
|
177
|
+
const height = block.payload.height ?? 0;
|
|
178
|
+
info.push(`token=${token}`);
|
|
179
|
+
if (width > 0 && height > 0) {
|
|
180
|
+
info.push(`${width}x${height}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (block.type === 'table') {
|
|
184
|
+
const rows = block.payload.rowSize ?? 0;
|
|
185
|
+
const cols = block.payload.columnSize ?? 0;
|
|
186
|
+
info.push(`rows=${rows}`);
|
|
187
|
+
info.push(`cols=${cols}`);
|
|
188
|
+
}
|
|
189
|
+
if (block.type === 'grid') {
|
|
190
|
+
info.push(`columns=${block.payload.columnSize ?? 0}`);
|
|
191
|
+
}
|
|
192
|
+
if (block.type === 'grid_column') {
|
|
193
|
+
info.push(`widthRatio=${block.payload.widthRatio ?? 0}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const tags = [blockTypeTag(ctx, block), blockIdTag(ctx, block)].filter((x) => x.length > 0).join(' ');
|
|
197
|
+
const tail = tags.length > 0 ? ` ${tags}` : '';
|
|
198
|
+
pushLine(ctx, `${prefix}${withAnsi(ctx.options.color, info.join(' '), ANSI.dim)}${tail}`);
|
|
199
|
+
}
|
|
200
|
+
function renderBlock(ctx, blockId, meta) {
|
|
201
|
+
const block = ctx.model.blocks[blockId];
|
|
202
|
+
if (!block) {
|
|
203
|
+
pushLine(ctx, `${indent(meta.depth)}${withAnsi(ctx.options.color, `[missing:${blockId}]`, ANSI.red)}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (isTextualBlock(block)) {
|
|
207
|
+
renderTextualBlock(ctx, block, meta);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
renderContainerPlaceholder(ctx, block, meta.depth);
|
|
211
|
+
}
|
|
212
|
+
if (block.children.length === 0) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let orderedCounter = 1;
|
|
216
|
+
for (const childId of block.children) {
|
|
217
|
+
const child = ctx.model.blocks[childId];
|
|
218
|
+
let orderedNumber;
|
|
219
|
+
if (child?.type === 'ordered') {
|
|
220
|
+
orderedNumber = orderedCounter;
|
|
221
|
+
orderedCounter += 1;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
orderedCounter = 1;
|
|
225
|
+
}
|
|
226
|
+
const childDepth = block.type === 'bullet' || block.type === 'ordered' ? meta.depth + 1 : meta.depth;
|
|
227
|
+
renderBlock(ctx, childId, {
|
|
228
|
+
depth: childDepth,
|
|
229
|
+
...(orderedNumber !== undefined ? { orderedNumber } : {}),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function topLevelIds(model) {
|
|
234
|
+
if ('mode' in model && model.mode === 'fragment') {
|
|
235
|
+
return [...model.topLevel];
|
|
236
|
+
}
|
|
237
|
+
const root = model.blocks[model.rootId];
|
|
238
|
+
if (!root)
|
|
239
|
+
return [];
|
|
240
|
+
return [...root.children];
|
|
241
|
+
}
|
|
242
|
+
export function renderLASTToTerminal(model, options = {}) {
|
|
243
|
+
const ctx = {
|
|
244
|
+
model,
|
|
245
|
+
options: {
|
|
246
|
+
color: options.color ?? true,
|
|
247
|
+
showTypeTag: options.showTypeTag ?? false,
|
|
248
|
+
showBlockId: options.showBlockId ?? false,
|
|
249
|
+
sectionGapLines: options.sectionGapLines ?? 1,
|
|
250
|
+
},
|
|
251
|
+
lines: [],
|
|
252
|
+
};
|
|
253
|
+
const headerMode = 'mode' in model && model.mode === 'fragment' ? 'fragment' : 'document';
|
|
254
|
+
const rootLabel = headerMode === 'document'
|
|
255
|
+
? `root=${model.rootId}`
|
|
256
|
+
: `topLevel=${model.topLevel.length}`;
|
|
257
|
+
pushLine(ctx, withAnsi(ctx.options.color, `LAST Terminal Preview | ${headerMode} | blocks=${Object.keys(model.blocks).length} | ${rootLabel}`, ANSI.dim));
|
|
258
|
+
pushLine(ctx, '');
|
|
259
|
+
if (headerMode === 'document') {
|
|
260
|
+
const doc = model;
|
|
261
|
+
const root = doc.blocks[doc.rootId];
|
|
262
|
+
if (root && isTextualBlock(root)) {
|
|
263
|
+
renderTextualBlock(ctx, root, { depth: 0 });
|
|
264
|
+
if (ctx.options.sectionGapLines > 0) {
|
|
265
|
+
for (let i = 0; i < ctx.options.sectionGapLines; i += 1) {
|
|
266
|
+
pushLine(ctx, '');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let orderedCounter = 1;
|
|
272
|
+
for (const blockId of topLevelIds(model)) {
|
|
273
|
+
const block = model.blocks[blockId];
|
|
274
|
+
let orderedNumber;
|
|
275
|
+
if (block?.type === 'ordered') {
|
|
276
|
+
orderedNumber = orderedCounter;
|
|
277
|
+
orderedCounter += 1;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
orderedCounter = 1;
|
|
281
|
+
}
|
|
282
|
+
renderBlock(ctx, blockId, {
|
|
283
|
+
depth: 0,
|
|
284
|
+
...(orderedNumber !== undefined ? { orderedNumber } : {}),
|
|
285
|
+
});
|
|
286
|
+
if (ctx.options.sectionGapLines > 0) {
|
|
287
|
+
for (let i = 0; i < ctx.options.sectionGapLines; i += 1) {
|
|
288
|
+
pushLine(ctx, '');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
while (ctx.lines.length > 0 && ctx.lines[ctx.lines.length - 1]?.trim() === '') {
|
|
293
|
+
ctx.lines.pop();
|
|
294
|
+
}
|
|
295
|
+
return `${ctx.lines.join('\n')}\n`;
|
|
296
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const LAST_TEXTUAL_BLOCK_TYPES = [
|
|
2
|
+
'page',
|
|
3
|
+
'text',
|
|
4
|
+
'heading1',
|
|
5
|
+
'heading2',
|
|
6
|
+
'heading3',
|
|
7
|
+
'heading4',
|
|
8
|
+
'heading5',
|
|
9
|
+
'heading6',
|
|
10
|
+
'heading7',
|
|
11
|
+
'heading8',
|
|
12
|
+
'heading9',
|
|
13
|
+
'bullet',
|
|
14
|
+
'ordered',
|
|
15
|
+
'code',
|
|
16
|
+
'quote',
|
|
17
|
+
'todo',
|
|
18
|
+
];
|
|
19
|
+
export const LAST_TEXTUAL_BLOCK_TYPE_SET = new Set(LAST_TEXTUAL_BLOCK_TYPES);
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { LAST_TEXTUAL_BLOCK_TYPE_SET } from './textual-block-types.js';
|
|
2
|
+
function isTextual(block) {
|
|
3
|
+
return LAST_TEXTUAL_BLOCK_TYPE_SET.has(block.type);
|
|
4
|
+
}
|
|
5
|
+
function topLevelIds(model) {
|
|
6
|
+
if ('mode' in model && model.mode === 'fragment') {
|
|
7
|
+
return [...model.topLevel];
|
|
8
|
+
}
|
|
9
|
+
const root = model.blocks[model.rootId];
|
|
10
|
+
if (!root)
|
|
11
|
+
return [];
|
|
12
|
+
return [...root.children];
|
|
13
|
+
}
|
|
14
|
+
function escapeTextForMarkdown(text) {
|
|
15
|
+
return text.replace(/\\/g, '\\\\').replace(/([*_\[\]~`])/g, '\\$1');
|
|
16
|
+
}
|
|
17
|
+
function escapeCodeInline(text) {
|
|
18
|
+
return text.replace(/`/g, '\\`');
|
|
19
|
+
}
|
|
20
|
+
function applyInlineMarks(inline, base) {
|
|
21
|
+
let value = base;
|
|
22
|
+
const marks = inline.marks;
|
|
23
|
+
if (marks.inlineCode) {
|
|
24
|
+
value = `\`${escapeCodeInline(value)}\``;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (marks.strikethrough) {
|
|
28
|
+
value = `~~${value}~~`;
|
|
29
|
+
}
|
|
30
|
+
if (marks.bold) {
|
|
31
|
+
value = `**${value}**`;
|
|
32
|
+
}
|
|
33
|
+
if (marks.italic) {
|
|
34
|
+
value = `*${value}*`;
|
|
35
|
+
}
|
|
36
|
+
if (marks.underline) {
|
|
37
|
+
value = `<u>${value}</u>`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (marks.link?.url) {
|
|
41
|
+
const label = value.length > 0 ? value : marks.link.url;
|
|
42
|
+
value = `[${label}](${marks.link.url})`;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function inlineToMarkdown(inline) {
|
|
47
|
+
switch (inline.kind) {
|
|
48
|
+
case 'text_run': {
|
|
49
|
+
const text = inline.text ?? '';
|
|
50
|
+
return applyInlineMarks(inline, escapeTextForMarkdown(text));
|
|
51
|
+
}
|
|
52
|
+
case 'mention_user': {
|
|
53
|
+
return applyInlineMarks(inline, `@${inline.userId ?? 'user'}`);
|
|
54
|
+
}
|
|
55
|
+
case 'equation': {
|
|
56
|
+
return applyInlineMarks(inline, `$${inline.latex ?? ''}$`);
|
|
57
|
+
}
|
|
58
|
+
case 'mention_doc': {
|
|
59
|
+
const title = inline.title ?? inline.token ?? 'doc';
|
|
60
|
+
const text = inline.url ? `[${title}](${inline.url})` : `[doc:${title}]`;
|
|
61
|
+
return applyInlineMarks(inline, text);
|
|
62
|
+
}
|
|
63
|
+
case 'reminder': {
|
|
64
|
+
const part = inline.expireTime ? `@${inline.expireTime}` : '';
|
|
65
|
+
return applyInlineMarks(inline, `[reminder${part}]`);
|
|
66
|
+
}
|
|
67
|
+
case 'inline_block': {
|
|
68
|
+
return applyInlineMarks(inline, `[inline:${inline.blockId ?? 'block'}]`);
|
|
69
|
+
}
|
|
70
|
+
case 'inline_file': {
|
|
71
|
+
return applyInlineMarks(inline, `[file:${inline.fileToken ?? 'token'}]`);
|
|
72
|
+
}
|
|
73
|
+
case 'link_preview': {
|
|
74
|
+
const text = inline.title ?? inline.url ?? 'link_preview';
|
|
75
|
+
return applyInlineMarks(inline, inline.url ? `[${text}](${inline.url})` : text);
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function textualInlinesToMarkdown(block) {
|
|
82
|
+
return block.payload.inlines.map((inline) => inlineToMarkdown(inline)).join('');
|
|
83
|
+
}
|
|
84
|
+
function renderCodeBlock(block, indent) {
|
|
85
|
+
const lang = block.payload.style.language ?? 'text';
|
|
86
|
+
const raw = block.payload.inlines
|
|
87
|
+
.map((inline) => {
|
|
88
|
+
if (inline.kind === 'text_run') {
|
|
89
|
+
return inline.text ?? '';
|
|
90
|
+
}
|
|
91
|
+
return '';
|
|
92
|
+
})
|
|
93
|
+
.join('');
|
|
94
|
+
const lines = raw.replace(/\n$/, '').split('\n');
|
|
95
|
+
const out = [];
|
|
96
|
+
out.push(`${indent}\`\`\`${lang}`);
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
out.push(`${indent}${line}`);
|
|
99
|
+
}
|
|
100
|
+
out.push(`${indent}\`\`\``);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function tableCellText(ctx, cellId) {
|
|
104
|
+
const cell = ctx.model.blocks[cellId];
|
|
105
|
+
if (!cell)
|
|
106
|
+
return '';
|
|
107
|
+
const firstTextual = cell.children
|
|
108
|
+
.map((childId) => ctx.model.blocks[childId])
|
|
109
|
+
.find((child) => Boolean(child && isTextual(child)));
|
|
110
|
+
if (!firstTextual) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
return textualInlinesToMarkdown(firstTextual).replace(/\n+/g, ' ').trim();
|
|
114
|
+
}
|
|
115
|
+
function renderTable(ctx, block, indent) {
|
|
116
|
+
const rowSize = block.payload.rowSize ?? 0;
|
|
117
|
+
const columnSize = block.payload.columnSize ?? 0;
|
|
118
|
+
if (rowSize <= 0 || columnSize <= 0) {
|
|
119
|
+
return [`${indent}| table |`, `${indent}| --- |`];
|
|
120
|
+
}
|
|
121
|
+
const cells = block.payload.cells && block.payload.cells.length > 0 ? block.payload.cells : block.children;
|
|
122
|
+
const matrix = [];
|
|
123
|
+
for (let r = 0; r < rowSize; r += 1) {
|
|
124
|
+
const row = [];
|
|
125
|
+
for (let c = 0; c < columnSize; c += 1) {
|
|
126
|
+
const index = r * columnSize + c;
|
|
127
|
+
const cellId = cells[index];
|
|
128
|
+
row.push(cellId ? tableCellText(ctx, cellId) : '');
|
|
129
|
+
}
|
|
130
|
+
matrix.push(row);
|
|
131
|
+
}
|
|
132
|
+
if (matrix.length === 0) {
|
|
133
|
+
return [`${indent}| table |`, `${indent}| --- |`];
|
|
134
|
+
}
|
|
135
|
+
const out = [];
|
|
136
|
+
out.push(`${indent}| ${(matrix[0] ?? []).join(' | ')} |`);
|
|
137
|
+
out.push(`${indent}| ${new Array(columnSize).fill('---').join(' | ')} |`);
|
|
138
|
+
for (let i = 1; i < matrix.length; i += 1) {
|
|
139
|
+
const row = matrix[i];
|
|
140
|
+
if (!row)
|
|
141
|
+
continue;
|
|
142
|
+
out.push(`${indent}| ${row.join(' | ')} |`);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function renderUnsupportedBlock(block, indent, includeUnsupportedComment) {
|
|
147
|
+
if (!includeUnsupportedComment) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return [`${indent}<!-- unsupported:${block.type}:${block.id} -->`];
|
|
151
|
+
}
|
|
152
|
+
function renderNestedChildren(ctx, block, depth, inListItem) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
let orderedCounter = 1;
|
|
155
|
+
for (const childId of block.children) {
|
|
156
|
+
const child = ctx.model.blocks[childId];
|
|
157
|
+
if (!child)
|
|
158
|
+
continue;
|
|
159
|
+
let number;
|
|
160
|
+
if (child.type === 'ordered') {
|
|
161
|
+
number = orderedCounter;
|
|
162
|
+
orderedCounter += 1;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
orderedCounter = 1;
|
|
166
|
+
}
|
|
167
|
+
const childDepth = inListItem ? depth + 1 : depth;
|
|
168
|
+
lines.push(...renderBlock(ctx, child, childDepth, number));
|
|
169
|
+
}
|
|
170
|
+
return lines;
|
|
171
|
+
}
|
|
172
|
+
function renderTextualBlock(ctx, block, depth, orderedNumber) {
|
|
173
|
+
const indent = ' '.repeat(Math.max(0, depth));
|
|
174
|
+
const text = textualInlinesToMarkdown(block);
|
|
175
|
+
switch (block.type) {
|
|
176
|
+
case 'page':
|
|
177
|
+
return text.trim().length > 0 ? [`${text}`] : [];
|
|
178
|
+
case 'text': {
|
|
179
|
+
const lines = text.split('\n');
|
|
180
|
+
return lines.map((line) => `${indent}${line}`);
|
|
181
|
+
}
|
|
182
|
+
case 'heading1':
|
|
183
|
+
case 'heading2':
|
|
184
|
+
case 'heading3':
|
|
185
|
+
case 'heading4':
|
|
186
|
+
case 'heading5':
|
|
187
|
+
case 'heading6':
|
|
188
|
+
case 'heading7':
|
|
189
|
+
case 'heading8':
|
|
190
|
+
case 'heading9': {
|
|
191
|
+
const levelRaw = Number(block.type.replace('heading', ''));
|
|
192
|
+
const level = Number.isFinite(levelRaw) ? Math.max(1, Math.min(6, levelRaw)) : 1;
|
|
193
|
+
return [`${indent}${'#'.repeat(level)} ${text}`.trimEnd()];
|
|
194
|
+
}
|
|
195
|
+
case 'bullet': {
|
|
196
|
+
const first = `${indent}- ${text}`.trimEnd();
|
|
197
|
+
const children = renderNestedChildren(ctx, block, depth, true);
|
|
198
|
+
return [first, ...children];
|
|
199
|
+
}
|
|
200
|
+
case 'ordered': {
|
|
201
|
+
const number = orderedNumber ?? 1;
|
|
202
|
+
const first = `${indent}${number}. ${text}`.trimEnd();
|
|
203
|
+
const children = renderNestedChildren(ctx, block, depth, true);
|
|
204
|
+
return [first, ...children];
|
|
205
|
+
}
|
|
206
|
+
case 'todo': {
|
|
207
|
+
const done = block.payload.style.done === true ? 'x' : ' ';
|
|
208
|
+
const first = `${indent}- [${done}] ${text}`.trimEnd();
|
|
209
|
+
const children = renderNestedChildren(ctx, block, depth, true);
|
|
210
|
+
return [first, ...children];
|
|
211
|
+
}
|
|
212
|
+
case 'quote': {
|
|
213
|
+
const quoteLines = text.split('\n').map((line) => `${indent}> ${line}`.trimEnd());
|
|
214
|
+
const childLines = renderNestedChildren(ctx, block, depth, false).map((line) => `${indent}> ${line}`);
|
|
215
|
+
return [...quoteLines, ...childLines];
|
|
216
|
+
}
|
|
217
|
+
case 'code': {
|
|
218
|
+
return renderCodeBlock(block, indent);
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
return [`${indent}${text}`.trimEnd()];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function renderBlock(ctx, block, depth, orderedNumber) {
|
|
225
|
+
if (ctx.stack.has(block.id)) {
|
|
226
|
+
return [`${' '.repeat(Math.max(0, depth))}<!-- cycle:${block.id} -->`];
|
|
227
|
+
}
|
|
228
|
+
ctx.stack.add(block.id);
|
|
229
|
+
let lines = [];
|
|
230
|
+
if (isTextual(block)) {
|
|
231
|
+
lines = renderTextualBlock(ctx, block, depth, orderedNumber);
|
|
232
|
+
}
|
|
233
|
+
else if (block.type === 'table') {
|
|
234
|
+
lines = renderTable(ctx, block, ' '.repeat(Math.max(0, depth)));
|
|
235
|
+
}
|
|
236
|
+
else if (block.type === 'divider') {
|
|
237
|
+
lines = [`${' '.repeat(Math.max(0, depth))}---`];
|
|
238
|
+
}
|
|
239
|
+
else if (block.type === 'quote_container') {
|
|
240
|
+
lines = renderNestedChildren(ctx, block, depth, false).map((line) => `${' '.repeat(Math.max(0, depth))}> ${line}`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
lines = renderUnsupportedBlock(block, ' '.repeat(Math.max(0, depth)), ctx.options.includeUnsupportedComment);
|
|
244
|
+
const childLines = renderNestedChildren(ctx, block, depth, false);
|
|
245
|
+
if (childLines.length > 0) {
|
|
246
|
+
lines.push(...childLines);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
ctx.stack.delete(block.id);
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
function normalizeBlankLines(lines) {
|
|
253
|
+
const out = [];
|
|
254
|
+
let prevBlank = true;
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const isBlank = line.trim().length === 0;
|
|
257
|
+
if (isBlank && prevBlank) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
out.push(line);
|
|
261
|
+
prevBlank = isBlank;
|
|
262
|
+
}
|
|
263
|
+
while (out.length > 0 && out[0]?.trim().length === 0) {
|
|
264
|
+
out.shift();
|
|
265
|
+
}
|
|
266
|
+
while (out.length > 0 && out[out.length - 1]?.trim().length === 0) {
|
|
267
|
+
out.pop();
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
export function serializeLASTToMarkdown(model, options = {}) {
|
|
272
|
+
const ctx = {
|
|
273
|
+
model,
|
|
274
|
+
options: {
|
|
275
|
+
includeUnsupportedComment: options.includeUnsupportedComment ?? true,
|
|
276
|
+
},
|
|
277
|
+
stack: new Set(),
|
|
278
|
+
};
|
|
279
|
+
const lines = [];
|
|
280
|
+
let orderedCounter = 1;
|
|
281
|
+
for (const blockId of topLevelIds(model)) {
|
|
282
|
+
const block = model.blocks[blockId];
|
|
283
|
+
if (!block)
|
|
284
|
+
continue;
|
|
285
|
+
let number;
|
|
286
|
+
if (block.type === 'ordered') {
|
|
287
|
+
number = orderedCounter;
|
|
288
|
+
orderedCounter += 1;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
orderedCounter = 1;
|
|
292
|
+
}
|
|
293
|
+
const chunk = renderBlock(ctx, block, 0, number);
|
|
294
|
+
if (chunk.length > 0) {
|
|
295
|
+
if (lines.length > 0) {
|
|
296
|
+
lines.push('');
|
|
297
|
+
}
|
|
298
|
+
lines.push(...chunk);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const normalized = normalizeBlankLines(lines);
|
|
302
|
+
return `${normalized.join('\n')}\n`;
|
|
303
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAST (Lark AST) schema for the pipeline:
|
|
3
|
+
* Markdown (GFM) -> HAST -> LAST -> Feishu Docx
|
|
4
|
+
*
|
|
5
|
+
* Design goals:
|
|
6
|
+
* 1) Unambiguous rendering to Feishu docx blocks.
|
|
7
|
+
* 2) Regex-capable find/replace in top-level text block scopes.
|
|
8
|
+
* 3) Future selector-based rewrite support.
|
|
9
|
+
*/
|
|
10
|
+
export const LAST_SCHEMA = 'LAST';
|
|
11
|
+
export const LAST_VERSION = '1.0.0';
|