feishu-docs-cli 0.1.0-beta.10
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 +21 -0
- package/README.md +403 -0
- package/README.zh.md +402 -0
- package/bin/feishu-docs.js +8 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +512 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +197 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +256 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/authorize.d.ts +12 -0
- package/dist/commands/authorize.js +73 -0
- package/dist/commands/authorize.js.map +1 -0
- package/dist/commands/cat.d.ts +6 -0
- package/dist/commands/cat.js +159 -0
- package/dist/commands/cat.js.map +1 -0
- package/dist/commands/cp.d.ts +9 -0
- package/dist/commands/cp.js +70 -0
- package/dist/commands/cp.js.map +1 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.js +120 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/delete.d.ts +6 -0
- package/dist/commands/delete.js +91 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/info.d.ts +6 -0
- package/dist/commands/info.js +69 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/install-skill.d.ts +5 -0
- package/dist/commands/install-skill.js +28 -0
- package/dist/commands/install-skill.js.map +1 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.js +93 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/ls.d.ts +6 -0
- package/dist/commands/ls.js +79 -0
- package/dist/commands/ls.js.map +1 -0
- package/dist/commands/mkdir.d.ts +6 -0
- package/dist/commands/mkdir.js +49 -0
- package/dist/commands/mkdir.js.map +1 -0
- package/dist/commands/mv.d.ts +9 -0
- package/dist/commands/mv.js +72 -0
- package/dist/commands/mv.js.map +1 -0
- package/dist/commands/read.d.ts +6 -0
- package/dist/commands/read.js +439 -0
- package/dist/commands/read.js.map +1 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +92 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/share.d.ts +13 -0
- package/dist/commands/share.js +266 -0
- package/dist/commands/share.js.map +1 -0
- package/dist/commands/spaces.d.ts +6 -0
- package/dist/commands/spaces.js +43 -0
- package/dist/commands/spaces.js.map +1 -0
- package/dist/commands/tree.d.ts +6 -0
- package/dist/commands/tree.js +101 -0
- package/dist/commands/tree.js.map +1 -0
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.js +217 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/wiki.d.ts +6 -0
- package/dist/commands/wiki.js +286 -0
- package/dist/commands/wiki.js.map +1 -0
- package/dist/parser/block-types.d.ts +141 -0
- package/dist/parser/block-types.js +167 -0
- package/dist/parser/block-types.js.map +1 -0
- package/dist/parser/blocks-to-md.d.ts +26 -0
- package/dist/parser/blocks-to-md.js +666 -0
- package/dist/parser/blocks-to-md.js.map +1 -0
- package/dist/parser/text-elements.d.ts +13 -0
- package/dist/parser/text-elements.js +91 -0
- package/dist/parser/text-elements.js.map +1 -0
- package/dist/scopes.d.ts +21 -0
- package/dist/scopes.js +48 -0
- package/dist/scopes.js.map +1 -0
- package/dist/services/block-writer.d.ts +29 -0
- package/dist/services/block-writer.js +131 -0
- package/dist/services/block-writer.js.map +1 -0
- package/dist/services/doc-blocks.d.ts +8 -0
- package/dist/services/doc-blocks.js +26 -0
- package/dist/services/doc-blocks.js.map +1 -0
- package/dist/services/markdown-convert.d.ts +68 -0
- package/dist/services/markdown-convert.js +217 -0
- package/dist/services/markdown-convert.js.map +1 -0
- package/dist/services/wiki-nodes.d.ts +20 -0
- package/dist/services/wiki-nodes.js +46 -0
- package/dist/services/wiki-nodes.js.map +1 -0
- package/dist/types/index.d.ts +236 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/document-resolver.d.ts +26 -0
- package/dist/utils/document-resolver.js +46 -0
- package/dist/utils/document-resolver.js.map +1 -0
- package/dist/utils/drive-types.d.ts +4 -0
- package/dist/utils/drive-types.js +16 -0
- package/dist/utils/drive-types.js.map +1 -0
- package/dist/utils/errors.d.ts +23 -0
- package/dist/utils/errors.js +114 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/member.d.ts +11 -0
- package/dist/utils/member.js +30 -0
- package/dist/utils/member.js.map +1 -0
- package/dist/utils/scope-prompt.d.ts +39 -0
- package/dist/utils/scope-prompt.js +134 -0
- package/dist/utils/scope-prompt.js.map +1 -0
- package/dist/utils/url-parser.d.ts +5 -0
- package/dist/utils/url-parser.js +55 -0
- package/dist/utils/url-parser.js.map +1 -0
- package/dist/utils/validate.d.ts +8 -0
- package/dist/utils/validate.js +15 -0
- package/dist/utils/validate.js.map +1 -0
- package/dist/utils/version.d.ts +12 -0
- package/dist/utils/version.js +128 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +53 -0
- package/skills/feishu-docs/SKILL.md +194 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert Feishu document blocks to Markdown.
|
|
3
|
+
*
|
|
4
|
+
* Input: Block[] (flat array with parent_id + children)
|
|
5
|
+
* Output: Markdown string
|
|
6
|
+
*/
|
|
7
|
+
import { BlockType, isHeading, headingLevel, CODE_LANGUAGES, } from "./block-types.js";
|
|
8
|
+
import { elementsToMarkdown } from "./text-elements.js";
|
|
9
|
+
/**
|
|
10
|
+
* Emoji ID to Unicode mapping for common callout emojis.
|
|
11
|
+
*/
|
|
12
|
+
const EMOJI_MAP = {
|
|
13
|
+
round_pushpin: "\u{1F4CD}",
|
|
14
|
+
bulb: "\u{1F4A1}",
|
|
15
|
+
warning: "\u26A0\uFE0F",
|
|
16
|
+
star: "\u2B50",
|
|
17
|
+
fire: "\u{1F525}",
|
|
18
|
+
check_mark: "\u2705",
|
|
19
|
+
cross_mark: "\u274C",
|
|
20
|
+
info: "\u2139\uFE0F",
|
|
21
|
+
question: "\u2753",
|
|
22
|
+
exclamation: "\u2757",
|
|
23
|
+
memo: "\u{1F4DD}",
|
|
24
|
+
pencil2: "\u270F\uFE0F",
|
|
25
|
+
rocket: "\u{1F680}",
|
|
26
|
+
tada: "\u{1F389}",
|
|
27
|
+
thumbsup: "\u{1F44D}",
|
|
28
|
+
eyes: "\u{1F440}",
|
|
29
|
+
heart: "\u2764\uFE0F",
|
|
30
|
+
zap: "\u26A1",
|
|
31
|
+
bookmark: "\u{1F516}",
|
|
32
|
+
link: "\u{1F517}",
|
|
33
|
+
umbrella_on_ground: "\u26F1\uFE0F",
|
|
34
|
+
umbrella: "\u2602\uFE0F",
|
|
35
|
+
sunny: "\u2600\uFE0F",
|
|
36
|
+
cloud: "\u2601\uFE0F",
|
|
37
|
+
snowflake: "\u2744\uFE0F",
|
|
38
|
+
rainbow: "\u{1F308}",
|
|
39
|
+
bell: "\u{1F514}",
|
|
40
|
+
key: "\u{1F511}",
|
|
41
|
+
lock: "\u{1F512}",
|
|
42
|
+
unlock: "\u{1F513}",
|
|
43
|
+
gear: "\u2699\uFE0F",
|
|
44
|
+
wrench: "\u{1F527}",
|
|
45
|
+
hammer: "\u{1F528}",
|
|
46
|
+
shield: "\u{1F6E1}\uFE0F",
|
|
47
|
+
trophy: "\u{1F3C6}",
|
|
48
|
+
clipboard: "\u{1F4CB}",
|
|
49
|
+
chart_with_upwards_trend: "\u{1F4C8}",
|
|
50
|
+
light_bulb: "\u{1F4A1}",
|
|
51
|
+
magnifying_glass: "\u{1F50D}",
|
|
52
|
+
alarm_clock: "\u23F0",
|
|
53
|
+
hourglass: "\u231B",
|
|
54
|
+
calendar: "\u{1F4C5}",
|
|
55
|
+
inbox_tray: "\u{1F4E5}",
|
|
56
|
+
outbox_tray: "\u{1F4E4}",
|
|
57
|
+
package: "\u{1F4E6}",
|
|
58
|
+
loudspeaker: "\u{1F4E2}",
|
|
59
|
+
thought_balloon: "\u{1F4AD}",
|
|
60
|
+
speech_balloon: "\u{1F4AC}",
|
|
61
|
+
construction: "\u{1F6A7}",
|
|
62
|
+
white_check_mark: "\u2705",
|
|
63
|
+
x: "\u274C",
|
|
64
|
+
bangbang: "\u203C\uFE0F",
|
|
65
|
+
interrobang: "\u2049\uFE0F",
|
|
66
|
+
pushpin: "\u{1F4CC}",
|
|
67
|
+
triangular_flag_on_post: "\u{1F6A9}",
|
|
68
|
+
bomb: "\u{1F4A3}",
|
|
69
|
+
seedling: "\u{1F331}",
|
|
70
|
+
four_leaf_clover: "\u{1F340}",
|
|
71
|
+
dart: "\u{1F3AF}",
|
|
72
|
+
100: "\u{1F4AF}",
|
|
73
|
+
muscle: "\u{1F4AA}",
|
|
74
|
+
clap: "\u{1F44F}",
|
|
75
|
+
wave: "\u{1F44B}",
|
|
76
|
+
point_right: "\u{1F449}",
|
|
77
|
+
point_up: "\u261D\uFE0F",
|
|
78
|
+
pray: "\u{1F64F}",
|
|
79
|
+
};
|
|
80
|
+
function emojiIdToUnicode(emojiId) {
|
|
81
|
+
return EMOJI_MAP[emojiId] || `:${emojiId}:`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a tree from flat block array.
|
|
85
|
+
* Each block gains a `_children` array of child block objects.
|
|
86
|
+
*/
|
|
87
|
+
function buildTree(blocks) {
|
|
88
|
+
const map = new Map();
|
|
89
|
+
for (const block of blocks) {
|
|
90
|
+
map.set(block.block_id, { ...block, _children: [] });
|
|
91
|
+
}
|
|
92
|
+
let root = null;
|
|
93
|
+
for (const block of blocks) {
|
|
94
|
+
const node = map.get(block.block_id);
|
|
95
|
+
if (block.parent_id && map.has(block.parent_id)) {
|
|
96
|
+
map.get(block.parent_id)._children.push(node);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
root = node;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Sort children according to `children` order if available
|
|
103
|
+
for (const node of map.values()) {
|
|
104
|
+
if (node.children && node.children.length > 0) {
|
|
105
|
+
const order = new Map(node.children.map((id, i) => [id, i]));
|
|
106
|
+
node._children.sort((a, b) => {
|
|
107
|
+
const ai = order.get(a.block_id) ?? Infinity;
|
|
108
|
+
const bi = order.get(b.block_id) ?? Infinity;
|
|
109
|
+
return ai - bi;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return root;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Main entry: convert blocks to markdown string.
|
|
117
|
+
*/
|
|
118
|
+
export function blocksToMarkdown(blocks, options = {}) {
|
|
119
|
+
if (!blocks || blocks.length === 0)
|
|
120
|
+
return "";
|
|
121
|
+
const root = buildTree(blocks);
|
|
122
|
+
if (!root)
|
|
123
|
+
return "";
|
|
124
|
+
const lines = [];
|
|
125
|
+
const ctx = {
|
|
126
|
+
imageUrlMap: options.imageUrlMap || new Map(),
|
|
127
|
+
userNameMap: options.userNameMap || new Map(),
|
|
128
|
+
bitableDataMap: options.bitableDataMap || new Map(),
|
|
129
|
+
boardImageMap: options.boardImageMap || new Map(),
|
|
130
|
+
sheetDataMap: options.sheetDataMap || new Map(),
|
|
131
|
+
warnings: [],
|
|
132
|
+
};
|
|
133
|
+
// Extract document title from root PAGE block
|
|
134
|
+
if (root.block_type === BlockType.PAGE) {
|
|
135
|
+
const titleText = getElements(root, "page", ctx);
|
|
136
|
+
if (titleText) {
|
|
137
|
+
lines.push(`# ${titleText}`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const state = { orderedIndex: 0 };
|
|
142
|
+
for (const child of root._children) {
|
|
143
|
+
if (child.block_type === BlockType.ORDERED) {
|
|
144
|
+
renderNode(child, lines, ctx, 0, state);
|
|
145
|
+
state.orderedIndex = (state.orderedIndex ?? 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
state.orderedIndex = 0;
|
|
149
|
+
renderNode(child, lines, ctx, 0, {});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Emit warnings to stderr
|
|
153
|
+
for (const w of ctx.warnings) {
|
|
154
|
+
process.stderr.write(`feishu-docs: warning: ${w}\n`);
|
|
155
|
+
}
|
|
156
|
+
return (lines
|
|
157
|
+
.join("\n")
|
|
158
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
159
|
+
.trim() + "\n");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Render children with ordered list index tracking.
|
|
163
|
+
*/
|
|
164
|
+
function renderChildren(children, lines, ctx, depth) {
|
|
165
|
+
const childState = { orderedIndex: 0 };
|
|
166
|
+
for (const child of children) {
|
|
167
|
+
if (child.block_type === BlockType.ORDERED) {
|
|
168
|
+
renderNode(child, lines, ctx, depth, childState);
|
|
169
|
+
childState.orderedIndex = (childState.orderedIndex ?? 0) + 1;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
childState.orderedIndex = 0;
|
|
173
|
+
renderNode(child, lines, ctx, depth, {});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Render a single node and its children.
|
|
179
|
+
*/
|
|
180
|
+
function renderNode(node, lines, ctx, depth, state) {
|
|
181
|
+
const type = node.block_type;
|
|
182
|
+
const indent = " ".repeat(depth);
|
|
183
|
+
if (type === BlockType.PAGE) {
|
|
184
|
+
renderChildren(node._children, lines, ctx, depth);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (type === BlockType.TEXT) {
|
|
188
|
+
const text = getElements(node, "text", ctx);
|
|
189
|
+
lines.push(indent + text);
|
|
190
|
+
lines.push("");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (isHeading(type)) {
|
|
194
|
+
const level = headingLevel(type);
|
|
195
|
+
const key = `heading${level}`;
|
|
196
|
+
const text = getElements(node, key, ctx);
|
|
197
|
+
const hashes = "#".repeat(Math.min(level, 6));
|
|
198
|
+
lines.push(`${hashes} ${text}`);
|
|
199
|
+
lines.push("");
|
|
200
|
+
// Headings in Feishu can have children (folded/collapsible content)
|
|
201
|
+
renderChildren(node._children, lines, ctx, depth);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (type === BlockType.BULLET) {
|
|
205
|
+
const text = getElements(node, "bullet", ctx);
|
|
206
|
+
lines.push(`${indent}- ${text}`);
|
|
207
|
+
renderChildren(node._children, lines, ctx, depth + 1);
|
|
208
|
+
if (depth === 0)
|
|
209
|
+
lines.push("");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (type === BlockType.ORDERED) {
|
|
213
|
+
const idx = (state.orderedIndex || 0) + 1;
|
|
214
|
+
const text = getElements(node, "ordered", ctx);
|
|
215
|
+
lines.push(`${indent}${idx}. ${text}`);
|
|
216
|
+
renderChildren(node._children, lines, ctx, depth + 1);
|
|
217
|
+
if (depth === 0)
|
|
218
|
+
lines.push("");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (type === BlockType.TODO) {
|
|
222
|
+
const todo = (node.todo || {});
|
|
223
|
+
const text = getElements(node, "todo", ctx);
|
|
224
|
+
const check = todo.done ? "x" : " ";
|
|
225
|
+
lines.push(`${indent}- [${check}] ${text}`);
|
|
226
|
+
renderChildren(node._children, lines, ctx, depth + 1);
|
|
227
|
+
if (depth === 0)
|
|
228
|
+
lines.push("");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (type === BlockType.CODE) {
|
|
232
|
+
const codeData = (node.code || {});
|
|
233
|
+
const langNum = codeData.style?.language;
|
|
234
|
+
const lang = langNum !== undefined
|
|
235
|
+
? CODE_LANGUAGES[langNum] || ""
|
|
236
|
+
: "";
|
|
237
|
+
const text = getElements(node, "code", ctx);
|
|
238
|
+
lines.push(`\`\`\`${lang}`);
|
|
239
|
+
lines.push(text);
|
|
240
|
+
lines.push("```");
|
|
241
|
+
lines.push("");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (type === BlockType.QUOTE) {
|
|
245
|
+
const text = getElements(node, "quote", ctx);
|
|
246
|
+
const quotedLines = text.split("\n").map((l) => `> ${l}`);
|
|
247
|
+
lines.push(...quotedLines);
|
|
248
|
+
for (const child of node._children) {
|
|
249
|
+
const childLines = [];
|
|
250
|
+
renderNode(child, childLines, ctx, 0, {});
|
|
251
|
+
lines.push(...childLines.map((l) => (l ? `> ${l}` : ">")));
|
|
252
|
+
}
|
|
253
|
+
lines.push("");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (type === BlockType.QUOTE_CONTAINER) {
|
|
257
|
+
for (const child of node._children) {
|
|
258
|
+
const childLines = [];
|
|
259
|
+
renderNode(child, childLines, ctx, 0, {});
|
|
260
|
+
lines.push(...childLines.map((l) => (l ? `> ${l}` : ">")));
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (type === BlockType.EQUATION) {
|
|
266
|
+
const eq = (node.equation || {});
|
|
267
|
+
const content = (eq.content || "").trim();
|
|
268
|
+
lines.push(`$$`);
|
|
269
|
+
lines.push(content);
|
|
270
|
+
lines.push(`$$`);
|
|
271
|
+
lines.push("");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (type === BlockType.DIVIDER) {
|
|
275
|
+
lines.push("---");
|
|
276
|
+
lines.push("");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (type === BlockType.IMAGE) {
|
|
280
|
+
const imageData = (node.image || {});
|
|
281
|
+
const fileToken = imageData.token;
|
|
282
|
+
const url = fileToken ? ctx.imageUrlMap.get(fileToken) || "" : "";
|
|
283
|
+
const alt = imageData.alt || "";
|
|
284
|
+
if (url) {
|
|
285
|
+
lines.push(``);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
lines.push(``);
|
|
289
|
+
}
|
|
290
|
+
lines.push("");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (type === BlockType.TABLE) {
|
|
294
|
+
renderTable(node, lines, ctx);
|
|
295
|
+
lines.push("");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (type === BlockType.CALLOUT) {
|
|
299
|
+
const callout = (node.callout || {});
|
|
300
|
+
const emoji = callout.emoji_id
|
|
301
|
+
? emojiIdToUnicode(callout.emoji_id) + " "
|
|
302
|
+
: "";
|
|
303
|
+
let isFirst = true;
|
|
304
|
+
for (const child of node._children) {
|
|
305
|
+
const childLines = [];
|
|
306
|
+
renderNode(child, childLines, ctx, 0, {});
|
|
307
|
+
const first = childLines.shift() || "";
|
|
308
|
+
// Emoji only on the very first line of the callout
|
|
309
|
+
const prefix = isFirst ? emoji : "";
|
|
310
|
+
isFirst = false;
|
|
311
|
+
lines.push(`> ${prefix}${first}`);
|
|
312
|
+
for (const cl of childLines) {
|
|
313
|
+
lines.push(cl ? `> ${cl}` : ">");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
lines.push("");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (type === BlockType.DIAGRAM) {
|
|
320
|
+
const text = getElements(node, "diagram", ctx);
|
|
321
|
+
lines.push("```mermaid");
|
|
322
|
+
lines.push(sanitizeMermaid(text));
|
|
323
|
+
lines.push("```");
|
|
324
|
+
lines.push("");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (type === BlockType.IFRAME) {
|
|
328
|
+
const iframe = (node.iframe || {});
|
|
329
|
+
const url = iframe.component?.url || "";
|
|
330
|
+
lines.push(`[嵌入](${url})`);
|
|
331
|
+
lines.push("");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (type === BlockType.GRID) {
|
|
335
|
+
for (const child of node._children) {
|
|
336
|
+
for (const grandchild of child._children) {
|
|
337
|
+
renderNode(grandchild, lines, ctx, depth, {});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (type === BlockType.GRID_COLUMN) {
|
|
343
|
+
for (const child of node._children) {
|
|
344
|
+
renderNode(child, lines, ctx, depth, {});
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (type === BlockType.TABLE_CELL) {
|
|
349
|
+
// Handled by renderTable
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (type === BlockType.FILE) {
|
|
353
|
+
const fileData = (node.file || {});
|
|
354
|
+
const name = fileData.name || "文件";
|
|
355
|
+
const token = fileData.token || "";
|
|
356
|
+
const url = ctx.imageUrlMap.get(token) || "";
|
|
357
|
+
lines.push(`[${name}](${url || token})`);
|
|
358
|
+
lines.push("");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (type === BlockType.ADDONS) {
|
|
362
|
+
const addOns = node["add_ons"] ||
|
|
363
|
+
{};
|
|
364
|
+
// Try to extract Mermaid diagram from record
|
|
365
|
+
try {
|
|
366
|
+
const record = JSON.parse(addOns.record || "{}");
|
|
367
|
+
if ((record.data &&
|
|
368
|
+
typeof record.data === "string" &&
|
|
369
|
+
record.data.includes("graph")) ||
|
|
370
|
+
record.data?.includes("flowchart") ||
|
|
371
|
+
record.data?.includes("sequenceDiagram") ||
|
|
372
|
+
record.data?.includes("classDiagram") ||
|
|
373
|
+
record.data?.includes("gantt") ||
|
|
374
|
+
record.data?.includes("pie") ||
|
|
375
|
+
record.data?.includes("erDiagram")) {
|
|
376
|
+
lines.push("```mermaid");
|
|
377
|
+
lines.push(sanitizeMermaid(record.data.trim()));
|
|
378
|
+
lines.push("```");
|
|
379
|
+
lines.push("");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// not parseable, skip
|
|
385
|
+
}
|
|
386
|
+
// TOC, Jira, OKR etc. — skip gracefully
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Bitable — render as Markdown table if data available
|
|
390
|
+
if (type === BlockType.BITABLE) {
|
|
391
|
+
const token = node.bitable?.token || "";
|
|
392
|
+
const data = ctx.bitableDataMap.get(token);
|
|
393
|
+
if (data && data.fields.length > 0) {
|
|
394
|
+
lines.push("| " +
|
|
395
|
+
data.fields.map((f) => f.replace(/\|/g, "\\|")).join(" | ") +
|
|
396
|
+
" |");
|
|
397
|
+
lines.push("| " + data.fields.map(() => "---").join(" | ") + " |");
|
|
398
|
+
for (const row of data.records) {
|
|
399
|
+
lines.push("| " +
|
|
400
|
+
row
|
|
401
|
+
.map((c) => String(c).replace(/\|/g, "\\|").replace(/\n/g, " "))
|
|
402
|
+
.join(" | ") +
|
|
403
|
+
" |");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
lines.push(`[多维表格: ${token}]`);
|
|
408
|
+
}
|
|
409
|
+
lines.push("");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Board — whiteboard/flowchart, rendered as image if available
|
|
413
|
+
if (type === BlockType.BOARD) {
|
|
414
|
+
const token = node.board
|
|
415
|
+
?.token || "";
|
|
416
|
+
const imagePath = ctx.boardImageMap.get(token);
|
|
417
|
+
if (imagePath) {
|
|
418
|
+
lines.push(``);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
lines.push(`[画板: ${token}]`);
|
|
422
|
+
}
|
|
423
|
+
lines.push("");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// Sheet — render as Markdown table if data available
|
|
427
|
+
if (type === BlockType.SHEET) {
|
|
428
|
+
const token = node.sheet?.token || "";
|
|
429
|
+
const data = ctx.sheetDataMap.get(token);
|
|
430
|
+
if (data && data.fields.length > 0) {
|
|
431
|
+
if (data.title) {
|
|
432
|
+
lines.push(`**${data.title}**`);
|
|
433
|
+
lines.push("");
|
|
434
|
+
}
|
|
435
|
+
lines.push("| " +
|
|
436
|
+
data.fields.map((f) => f.replace(/\|/g, "\\|")).join(" | ") +
|
|
437
|
+
" |");
|
|
438
|
+
lines.push("| " + data.fields.map(() => "---").join(" | ") + " |");
|
|
439
|
+
for (const row of data.records) {
|
|
440
|
+
lines.push("| " +
|
|
441
|
+
row
|
|
442
|
+
.map((c) => String(c).replace(/\|/g, "\\|").replace(/\n/g, " "))
|
|
443
|
+
.join(" | ") +
|
|
444
|
+
" |");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
lines.push(`[电子表格: ${token}]`);
|
|
449
|
+
}
|
|
450
|
+
lines.push("");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// Task — render as TODO with metadata
|
|
454
|
+
if (type === BlockType.TASK) {
|
|
455
|
+
const task = (node.task || {});
|
|
456
|
+
const taskId = task.task_id || "";
|
|
457
|
+
const text = getElements(node, "task", ctx);
|
|
458
|
+
const summary = text || task.summary || taskId || "未命名任务";
|
|
459
|
+
const parts = [];
|
|
460
|
+
if (task.assignees && task.assignees.length > 0) {
|
|
461
|
+
const names = task.assignees.map((a) => `@${a.name || a.id || "?"}`);
|
|
462
|
+
parts.push(names.join(", "));
|
|
463
|
+
}
|
|
464
|
+
if (task.due) {
|
|
465
|
+
parts.push(`截止: ${task.due}`);
|
|
466
|
+
}
|
|
467
|
+
const meta = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
468
|
+
const check = task.completed ? "x" : " ";
|
|
469
|
+
lines.push(`${indent}- [${check}] ${summary}${meta}`);
|
|
470
|
+
if (depth === 0)
|
|
471
|
+
lines.push("");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// LinkPreview — degrade to hyperlink
|
|
475
|
+
if (type === BlockType.LINK_PREVIEW) {
|
|
476
|
+
const preview = node["link_preview"] || {};
|
|
477
|
+
const url = preview.url || "";
|
|
478
|
+
const title = preview.title || url || "链接";
|
|
479
|
+
lines.push(`[${title}](${url})`);
|
|
480
|
+
lines.push("");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// JiraIssue — render as JIRA link
|
|
484
|
+
if (type === BlockType.JIRA_ISSUE) {
|
|
485
|
+
const jira = node["jira_issue"] || {};
|
|
486
|
+
const key = jira.key || "";
|
|
487
|
+
const url = jira.url || "";
|
|
488
|
+
const summary = jira.summary || key || "JIRA Issue";
|
|
489
|
+
if (url) {
|
|
490
|
+
lines.push(`[JIRA: ${key || summary}](${url})`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
lines.push(`[JIRA: ${key || summary}]`);
|
|
494
|
+
}
|
|
495
|
+
lines.push("");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// WikiCatalog — render children or placeholder
|
|
499
|
+
if (type === BlockType.WIKI_CATALOG) {
|
|
500
|
+
if (node._children.length > 0) {
|
|
501
|
+
renderChildren(node._children, lines, ctx, depth);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
lines.push("[知识库目录]");
|
|
505
|
+
lines.push("");
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// SubPageList — render children or placeholder
|
|
510
|
+
if (type === BlockType.SUB_PAGE_LIST) {
|
|
511
|
+
if (node._children.length > 0) {
|
|
512
|
+
renderChildren(node._children, lines, ctx, depth);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
lines.push("[子页面列表]");
|
|
516
|
+
lines.push("");
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// Agenda blocks — meeting agenda rendering
|
|
521
|
+
if (type === BlockType.AGENDA || type === BlockType.AGENDA_ITEM) {
|
|
522
|
+
renderChildren(node._children, lines, ctx, depth);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (type === BlockType.AGENDA_ITEM_TITLE) {
|
|
526
|
+
const text = getElements(node, "agenda_item_title", ctx);
|
|
527
|
+
lines.push(`${indent}**${text}**`);
|
|
528
|
+
lines.push("");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (type === BlockType.AGENDA_ITEM_CONTENT) {
|
|
532
|
+
renderChildren(node._children, lines, ctx, depth);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// OKR blocks — complex business data, skip silently
|
|
536
|
+
if (type === BlockType.OKR ||
|
|
537
|
+
type === BlockType.OKR_OBJECTIVE ||
|
|
538
|
+
type === BlockType.OKR_KEY_RESULT ||
|
|
539
|
+
type === BlockType.OKR_PROGRESS) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// Synced blocks and AI template — no extractable content, skip silently
|
|
543
|
+
if (type === BlockType.SOURCE_SYNCED ||
|
|
544
|
+
type === BlockType.REFERENCE_SYNCED ||
|
|
545
|
+
type === BlockType.AI_TEMPLATE) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Reference types — show as label with token
|
|
549
|
+
const refTypes = {
|
|
550
|
+
[BlockType.MINDNOTE]: "思维笔记",
|
|
551
|
+
[BlockType.VIEW]: "视图",
|
|
552
|
+
[BlockType.CHAT_CARD]: "群消息卡片",
|
|
553
|
+
[BlockType.ISV]: "三方块",
|
|
554
|
+
};
|
|
555
|
+
if (refTypes[type]) {
|
|
556
|
+
const data = node[Object.keys(node).find((k) => typeof node[k] === "object" &&
|
|
557
|
+
node[k] !== null &&
|
|
558
|
+
node[k]
|
|
559
|
+
?.token) || ""] || {};
|
|
560
|
+
const token = data.token || "";
|
|
561
|
+
lines.push(`[${refTypes[type]}: ${token}]`);
|
|
562
|
+
lines.push("");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// Unknown type
|
|
566
|
+
ctx.warnings.push(`不支持的内容类型: ${type}`);
|
|
567
|
+
lines.push(`[不支持的内容类型: ${type}]`);
|
|
568
|
+
lines.push("");
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get inline text from a block's elements.
|
|
572
|
+
*/
|
|
573
|
+
function getElements(node, key, ctx) {
|
|
574
|
+
const data = node[key] || {};
|
|
575
|
+
return elementsToMarkdown(data.elements, ctx);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Render a table block as Markdown table.
|
|
579
|
+
*/
|
|
580
|
+
function renderTable(node, lines, ctx) {
|
|
581
|
+
const tableData = (node.table || {});
|
|
582
|
+
const property = tableData.property || {};
|
|
583
|
+
const rowSize = property.row_size || 0;
|
|
584
|
+
const colSize = property.column_size || 0;
|
|
585
|
+
if (rowSize === 0 || colSize === 0)
|
|
586
|
+
return;
|
|
587
|
+
// Build 2D grid from table_cell children
|
|
588
|
+
const cells = node._children;
|
|
589
|
+
const grid = [];
|
|
590
|
+
for (let r = 0; r < rowSize; r++) {
|
|
591
|
+
const row = [];
|
|
592
|
+
for (let c = 0; c < colSize; c++) {
|
|
593
|
+
const cellIndex = r * colSize + c;
|
|
594
|
+
const cell = cells[cellIndex];
|
|
595
|
+
if (cell) {
|
|
596
|
+
const text = cellToText(cell, ctx);
|
|
597
|
+
row.push(text);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
row.push("");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
grid.push(row);
|
|
604
|
+
}
|
|
605
|
+
if (grid.length === 0)
|
|
606
|
+
return;
|
|
607
|
+
// Header row
|
|
608
|
+
lines.push("| " + grid[0].map((c) => c.replace(/\|/g, "\\|")).join(" | ") + " |");
|
|
609
|
+
// Separator
|
|
610
|
+
lines.push("| " + grid[0].map(() => "---").join(" | ") + " |");
|
|
611
|
+
// Data rows
|
|
612
|
+
for (let r = 1; r < grid.length; r++) {
|
|
613
|
+
lines.push("| " + grid[r].map((c) => c.replace(/\|/g, "\\|")).join(" | ") + " |");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Render a table_cell block content as inline text.
|
|
618
|
+
*/
|
|
619
|
+
function cellToText(cell, ctx) {
|
|
620
|
+
const parts = [];
|
|
621
|
+
for (const child of cell._children) {
|
|
622
|
+
const key = Object.keys(child).find((k) => typeof child[k] === "object" &&
|
|
623
|
+
child[k]
|
|
624
|
+
?.elements);
|
|
625
|
+
if (key) {
|
|
626
|
+
const data = child[key];
|
|
627
|
+
parts.push(elementsToMarkdown(data.elements, ctx));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return parts.join(" ").replace(/\n/g, " ");
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Sanitize Feishu mermaid content for standard mermaid compatibility.
|
|
634
|
+
*
|
|
635
|
+
* Feishu's mermaid renderer is more lenient than standard mermaid.
|
|
636
|
+
* This function fixes common incompatibilities:
|
|
637
|
+
* - Block labels (alt, else, loop, opt, rect, par, critical, break, note)
|
|
638
|
+
* don't support <br/> in standard mermaid — replace with comma-space
|
|
639
|
+
* - Arrow messages missing space after ':' — add the space
|
|
640
|
+
*/
|
|
641
|
+
function sanitizeMermaid(content) {
|
|
642
|
+
// Block-level keywords whose label text doesn't support <br/> in standard mermaid
|
|
643
|
+
const blockKeywords = /^(\s*(?:alt|else|loop|opt|par|critical|break)\s+)(.+)$/;
|
|
644
|
+
return content
|
|
645
|
+
.split("\n")
|
|
646
|
+
.map((line) => {
|
|
647
|
+
// Fix block labels: replace <br/> and <br> with comma-space,
|
|
648
|
+
// and replace parentheses with full-width versions to avoid parser confusion
|
|
649
|
+
const blockMatch = line.match(blockKeywords);
|
|
650
|
+
if (blockMatch) {
|
|
651
|
+
const cleaned = blockMatch[2]
|
|
652
|
+
.replace(/<br\s*\/?>/gi, ", ")
|
|
653
|
+
.replace(/\(/g, "\uFF08")
|
|
654
|
+
.replace(/\)/g, "\uFF09");
|
|
655
|
+
return blockMatch[1] + cleaned;
|
|
656
|
+
}
|
|
657
|
+
// Fix missing space after ':' in arrow messages (e.g., A->>B:text → A->>B: text)
|
|
658
|
+
const arrowMatch = line.match(/^(\s*\S+\s*-[-.)>x]+[+-]?\s*\S+\s*):(\S)/);
|
|
659
|
+
if (arrowMatch) {
|
|
660
|
+
return line.replace(/^(\s*\S+\s*-[-.)>x]+[+-]?\s*\S+\s*):(\S)/, "$1: $2");
|
|
661
|
+
}
|
|
662
|
+
return line;
|
|
663
|
+
})
|
|
664
|
+
.join("\n");
|
|
665
|
+
}
|
|
666
|
+
//# sourceMappingURL=blocks-to-md.js.map
|