@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,17 @@
|
|
|
1
|
+
import { buildBTT } from '../btt/build-tree.js';
|
|
2
|
+
import { buildLASTToBTTBlockIdMap, collectBlocksRootFirst, isLASTFragment, sortChildrenByTreeOrder, toDocumentModelForBTT, toRawBlock, } from './codec-last-to-btt.js';
|
|
3
|
+
export function convertLASTToBTT(lastDoc, options) {
|
|
4
|
+
const normalized = toDocumentModelForBTT(lastDoc);
|
|
5
|
+
const lastToBttBlockId = buildLASTToBTTBlockIdMap(normalized);
|
|
6
|
+
const rawBlocks = collectBlocksRootFirst(normalized).map((block) => toRawBlock(block, lastToBttBlockId));
|
|
7
|
+
const docId = options?.documentId ?? String(lastDoc.id);
|
|
8
|
+
return buildBTT(docId, rawBlocks);
|
|
9
|
+
}
|
|
10
|
+
export function summarizeLASTDocument(lastDoc) {
|
|
11
|
+
const topLevel = sortChildrenByTreeOrder(lastDoc);
|
|
12
|
+
return {
|
|
13
|
+
blockCount: Object.keys(lastDoc.blocks).length,
|
|
14
|
+
rootId: isLASTFragment(lastDoc) ? '(fragment)' : lastDoc.rootId,
|
|
15
|
+
topLevelCount: topLevel.length,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const LARK_BLOCK_TYPE_NAME = {
|
|
2
|
+
1: 'Page',
|
|
3
|
+
2: 'Text',
|
|
4
|
+
3: 'Heading1',
|
|
5
|
+
4: 'Heading2',
|
|
6
|
+
5: 'Heading3',
|
|
7
|
+
6: 'Heading4',
|
|
8
|
+
7: 'Heading5',
|
|
9
|
+
8: 'Heading6',
|
|
10
|
+
9: 'Heading7',
|
|
11
|
+
10: 'Heading8',
|
|
12
|
+
11: 'Heading9',
|
|
13
|
+
12: 'Bullet',
|
|
14
|
+
13: 'Ordered',
|
|
15
|
+
14: 'Code',
|
|
16
|
+
15: 'Quote',
|
|
17
|
+
16: 'MentionDoc',
|
|
18
|
+
17: 'TodoList',
|
|
19
|
+
18: 'Bitable',
|
|
20
|
+
19: 'Callout',
|
|
21
|
+
20: 'ChatCard',
|
|
22
|
+
21: 'Diagram',
|
|
23
|
+
22: 'Divider',
|
|
24
|
+
23: 'File',
|
|
25
|
+
24: 'Grid',
|
|
26
|
+
25: 'GridColumn',
|
|
27
|
+
26: 'Iframe',
|
|
28
|
+
27: 'Image',
|
|
29
|
+
28: 'Widget',
|
|
30
|
+
29: 'MindNote',
|
|
31
|
+
30: 'Sheet',
|
|
32
|
+
31: 'Table',
|
|
33
|
+
32: 'TableCell',
|
|
34
|
+
33: 'View',
|
|
35
|
+
34: 'QuoteContainer',
|
|
36
|
+
40: 'AddOns',
|
|
37
|
+
43: 'Board',
|
|
38
|
+
999: 'SyncedBlock',
|
|
39
|
+
};
|
|
40
|
+
export function getLarkBlockTypeName(blockType) {
|
|
41
|
+
return LARK_BLOCK_TYPE_NAME[blockType] ?? `Unknown(${blockType})`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function trimSlashSuffix(input) {
|
|
2
|
+
return input.endsWith('/') ? input.slice(0, -1) : input;
|
|
3
|
+
}
|
|
4
|
+
function assertNonEmpty(value, name) {
|
|
5
|
+
if (!value) {
|
|
6
|
+
throw new Error(`${name} is required.`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function parseTokenType(raw) {
|
|
10
|
+
if (raw === 'tenant' || raw === 'user') {
|
|
11
|
+
return raw;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Unsupported LARK_TOKEN_TYPE "${raw}". Expected "tenant" or "user".`);
|
|
14
|
+
}
|
|
15
|
+
function pickEnv(env, key, fallback = '') {
|
|
16
|
+
return (env[key] ?? fallback).trim();
|
|
17
|
+
}
|
|
18
|
+
export function createLarkClientConfigFromEnv(env) {
|
|
19
|
+
const baseUrl = trimSlashSuffix(pickEnv(env, 'LARK_BASE_URL', 'https://open.feishu.cn'));
|
|
20
|
+
const appId = pickEnv(env, 'LARK_APP_ID');
|
|
21
|
+
const appSecret = pickEnv(env, 'LARK_APP_SECRET');
|
|
22
|
+
const tokenType = parseTokenType(pickEnv(env, 'LARK_TOKEN_TYPE', 'tenant'));
|
|
23
|
+
const userAccessToken = pickEnv(env, 'LARK_USER_ACCESS_TOKEN');
|
|
24
|
+
assertNonEmpty(appId, 'LARK_APP_ID');
|
|
25
|
+
assertNonEmpty(appSecret, 'LARK_APP_SECRET');
|
|
26
|
+
if (tokenType === 'user') {
|
|
27
|
+
assertNonEmpty(userAccessToken, 'LARK_USER_ACCESS_TOKEN');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
baseUrl,
|
|
31
|
+
appId,
|
|
32
|
+
appSecret,
|
|
33
|
+
tokenType,
|
|
34
|
+
userAccessToken,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { withRetry } from '../../shared/retry.js';
|
|
4
|
+
const TABLE_CREATE_MAX_ROW_SIZE = 9;
|
|
5
|
+
const TABLE_CREATE_MAX_COLUMN_SIZE = 9;
|
|
6
|
+
const DOCX_CREATE_CHILDREN_BATCH_SIZE = 50;
|
|
7
|
+
// Feishu table expansion requests are sensitive: 9x10 / 10x9 direct-like batch expansion
|
|
8
|
+
// can return invalid param. Keep expansion as single-step insert per request.
|
|
9
|
+
const TABLE_EXPAND_BATCH_REQUEST_SIZE = 1;
|
|
10
|
+
function toObjectRecord(value) {
|
|
11
|
+
return value && typeof value === 'object' ? value : null;
|
|
12
|
+
}
|
|
13
|
+
function getString(record, key) {
|
|
14
|
+
const value = record[key];
|
|
15
|
+
return typeof value === 'string' ? value : '';
|
|
16
|
+
}
|
|
17
|
+
function getArray(record, key) {
|
|
18
|
+
const value = record[key];
|
|
19
|
+
return Array.isArray(value) ? value : [];
|
|
20
|
+
}
|
|
21
|
+
function assertSuccessEnvelope(response, label) {
|
|
22
|
+
if (!response || typeof response !== 'object')
|
|
23
|
+
return;
|
|
24
|
+
const envelope = response;
|
|
25
|
+
const codeValue = envelope.code;
|
|
26
|
+
if (typeof codeValue === 'number' && codeValue !== 0) {
|
|
27
|
+
const msg = typeof envelope.msg === 'string' ? envelope.msg : '';
|
|
28
|
+
throw new Error(`${label} failed: code=${codeValue} msg=${msg}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getResponseData(response) {
|
|
32
|
+
if (!response || typeof response !== 'object')
|
|
33
|
+
return {};
|
|
34
|
+
const envelope = response;
|
|
35
|
+
const data = envelope.data;
|
|
36
|
+
if (!data || typeof data !== 'object')
|
|
37
|
+
return {};
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
function toDriveFile(value) {
|
|
41
|
+
if (!value || typeof value !== 'object')
|
|
42
|
+
return null;
|
|
43
|
+
const row = value;
|
|
44
|
+
if (typeof row.token !== 'string')
|
|
45
|
+
return null;
|
|
46
|
+
if (typeof row.name !== 'string')
|
|
47
|
+
return null;
|
|
48
|
+
if (typeof row.type !== 'string')
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
token: row.token,
|
|
52
|
+
name: row.name,
|
|
53
|
+
type: row.type,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function toDocxBlock(value) {
|
|
57
|
+
if (!value || typeof value !== 'object')
|
|
58
|
+
return null;
|
|
59
|
+
const row = value;
|
|
60
|
+
if (typeof row.block_id !== 'string')
|
|
61
|
+
return null;
|
|
62
|
+
if (typeof row.block_type !== 'number')
|
|
63
|
+
return null;
|
|
64
|
+
const children = Array.isArray(row.children)
|
|
65
|
+
? row.children.filter((item) => typeof item === 'string')
|
|
66
|
+
: undefined;
|
|
67
|
+
return {
|
|
68
|
+
block_id: row.block_id,
|
|
69
|
+
block_type: row.block_type,
|
|
70
|
+
...(typeof row.parent_id === 'string' ? { parent_id: row.parent_id } : {}),
|
|
71
|
+
...(children ? { children } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function deepClone(value) {
|
|
75
|
+
return JSON.parse(JSON.stringify(value));
|
|
76
|
+
}
|
|
77
|
+
function toPositiveInt(value) {
|
|
78
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
return Math.round(value);
|
|
81
|
+
}
|
|
82
|
+
function toNonNegativeInt(value) {
|
|
83
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
return Math.round(value);
|
|
86
|
+
}
|
|
87
|
+
function buildTableCreatePlan(block) {
|
|
88
|
+
if (block.block_type !== 31)
|
|
89
|
+
return null;
|
|
90
|
+
const table = toObjectRecord(block.table);
|
|
91
|
+
const property = toObjectRecord(table?.property);
|
|
92
|
+
if (!table || !property)
|
|
93
|
+
return null;
|
|
94
|
+
const targetRowSize = Math.max(1, toPositiveInt(property.row_size) ?? 1);
|
|
95
|
+
const targetColumnSize = Math.max(1, toPositiveInt(property.column_size) ?? 1);
|
|
96
|
+
const initialRowSize = Math.min(targetRowSize, TABLE_CREATE_MAX_ROW_SIZE);
|
|
97
|
+
const initialColumnSize = Math.min(targetColumnSize, TABLE_CREATE_MAX_COLUMN_SIZE);
|
|
98
|
+
if (initialRowSize === targetRowSize && initialColumnSize === targetColumnSize) {
|
|
99
|
+
return {
|
|
100
|
+
createPayload: block,
|
|
101
|
+
targetRowSize,
|
|
102
|
+
targetColumnSize,
|
|
103
|
+
initialRowSize,
|
|
104
|
+
initialColumnSize,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const createPayload = deepClone(block);
|
|
108
|
+
const nextTable = createPayload.table;
|
|
109
|
+
const nextProperty = (nextTable.property ?? {});
|
|
110
|
+
nextProperty.row_size = initialRowSize;
|
|
111
|
+
nextProperty.column_size = initialColumnSize;
|
|
112
|
+
if (Array.isArray(nextProperty.column_width)) {
|
|
113
|
+
nextProperty.column_width = nextProperty.column_width.slice(0, initialColumnSize);
|
|
114
|
+
}
|
|
115
|
+
nextTable.property = nextProperty;
|
|
116
|
+
return {
|
|
117
|
+
createPayload,
|
|
118
|
+
targetRowSize,
|
|
119
|
+
targetColumnSize,
|
|
120
|
+
initialRowSize,
|
|
121
|
+
initialColumnSize,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function findRootBlockId(documentId, blocks) {
|
|
125
|
+
const byPageType = blocks.find((item) => item.block_type === 1);
|
|
126
|
+
if (byPageType)
|
|
127
|
+
return byPageType.block_id;
|
|
128
|
+
const normalized = normalizeDocumentId(documentId);
|
|
129
|
+
const byDocumentId = blocks.find((item) => item.block_id === normalized || item.block_id === documentId);
|
|
130
|
+
if (byDocumentId)
|
|
131
|
+
return byDocumentId.block_id;
|
|
132
|
+
throw new Error(`Unable to locate root page block for document "${documentId}"`);
|
|
133
|
+
}
|
|
134
|
+
function isBatchDeleteOutOfRangeError(error) {
|
|
135
|
+
if (!(error instanceof Error))
|
|
136
|
+
return false;
|
|
137
|
+
return /1770001|invalid param|start_index|end_index|out of range|no child/i.test(error.message);
|
|
138
|
+
}
|
|
139
|
+
function isInvalidParamError(error) {
|
|
140
|
+
if (!(error instanceof Error))
|
|
141
|
+
return false;
|
|
142
|
+
return /1770001|invalid param/i.test(error.message);
|
|
143
|
+
}
|
|
144
|
+
function isCreateInvalidParamError(error) {
|
|
145
|
+
return isInvalidParamError(error);
|
|
146
|
+
}
|
|
147
|
+
export function normalizeDocumentId(documentId) {
|
|
148
|
+
const trimmed = documentId.trim();
|
|
149
|
+
return trimmed.startsWith('doc_') ? trimmed.slice('doc_'.length) : trimmed;
|
|
150
|
+
}
|
|
151
|
+
export async function listFolderChildren(client, folderToken, authOptions, limiter) {
|
|
152
|
+
const files = [];
|
|
153
|
+
let pageToken = '';
|
|
154
|
+
let hasMore = true;
|
|
155
|
+
while (hasMore) {
|
|
156
|
+
const params = {
|
|
157
|
+
folder_token: folderToken,
|
|
158
|
+
page_size: 200,
|
|
159
|
+
};
|
|
160
|
+
if (pageToken) {
|
|
161
|
+
params.page_token = pageToken;
|
|
162
|
+
}
|
|
163
|
+
await limiter.wait();
|
|
164
|
+
const response = await withRetry('drive.file.list', async () => client.drive.file.list({
|
|
165
|
+
params,
|
|
166
|
+
}, authOptions));
|
|
167
|
+
assertSuccessEnvelope(response, 'drive.file.list');
|
|
168
|
+
const data = getResponseData(response);
|
|
169
|
+
const entries = getArray(data, 'files');
|
|
170
|
+
for (const item of entries) {
|
|
171
|
+
const parsed = toDriveFile(item);
|
|
172
|
+
if (parsed)
|
|
173
|
+
files.push(parsed);
|
|
174
|
+
}
|
|
175
|
+
hasMore = Boolean(data.has_more);
|
|
176
|
+
pageToken = getString(data, 'next_page_token');
|
|
177
|
+
}
|
|
178
|
+
return files;
|
|
179
|
+
}
|
|
180
|
+
export async function createDocument(client, folderToken, title, authOptions, limiter) {
|
|
181
|
+
await limiter.wait();
|
|
182
|
+
const response = await withRetry('docx.document.create', async () => client.docx.document.create({
|
|
183
|
+
data: {
|
|
184
|
+
folder_token: folderToken,
|
|
185
|
+
title,
|
|
186
|
+
},
|
|
187
|
+
}, authOptions));
|
|
188
|
+
assertSuccessEnvelope(response, 'docx.document.create');
|
|
189
|
+
const data = getResponseData(response);
|
|
190
|
+
const document = toObjectRecord(data.document);
|
|
191
|
+
if (!document) {
|
|
192
|
+
throw new Error(`docx.document.create returned no document for title="${title}"`);
|
|
193
|
+
}
|
|
194
|
+
const documentId = getString(document, 'document_id');
|
|
195
|
+
if (!documentId) {
|
|
196
|
+
throw new Error(`docx.document.create returned empty document_id for title="${title}"`);
|
|
197
|
+
}
|
|
198
|
+
return documentId;
|
|
199
|
+
}
|
|
200
|
+
export async function listAllDocumentBlocks(client, documentId, authOptions, limiter) {
|
|
201
|
+
const blocks = [];
|
|
202
|
+
let pageToken = '';
|
|
203
|
+
let hasMore = true;
|
|
204
|
+
while (hasMore) {
|
|
205
|
+
const params = {
|
|
206
|
+
page_size: 500,
|
|
207
|
+
document_revision_id: -1,
|
|
208
|
+
};
|
|
209
|
+
if (pageToken) {
|
|
210
|
+
params.page_token = pageToken;
|
|
211
|
+
}
|
|
212
|
+
await limiter.wait();
|
|
213
|
+
const response = await withRetry('docx.documentBlock.list', async () => client.docx.documentBlock.list({
|
|
214
|
+
path: {
|
|
215
|
+
document_id: documentId,
|
|
216
|
+
},
|
|
217
|
+
params,
|
|
218
|
+
}, authOptions));
|
|
219
|
+
assertSuccessEnvelope(response, 'docx.documentBlock.list');
|
|
220
|
+
const data = getResponseData(response);
|
|
221
|
+
const items = getArray(data, 'items');
|
|
222
|
+
for (const item of items) {
|
|
223
|
+
const parsed = toDocxBlock(item);
|
|
224
|
+
if (parsed)
|
|
225
|
+
blocks.push(parsed);
|
|
226
|
+
}
|
|
227
|
+
hasMore = Boolean(data.has_more);
|
|
228
|
+
pageToken = getString(data, 'page_token');
|
|
229
|
+
}
|
|
230
|
+
return blocks;
|
|
231
|
+
}
|
|
232
|
+
export async function clearDocumentContent(client, documentId, authOptions, limiter) {
|
|
233
|
+
const blocks = await listAllDocumentBlocks(client, documentId, authOptions, limiter);
|
|
234
|
+
const rootId = findRootBlockId(documentId, blocks);
|
|
235
|
+
const rootBlock = blocks.find((item) => item.block_id === rootId);
|
|
236
|
+
let remaining = rootBlock?.children?.length ?? 0;
|
|
237
|
+
if (remaining <= 0) {
|
|
238
|
+
return rootId;
|
|
239
|
+
}
|
|
240
|
+
let loopGuard = 0;
|
|
241
|
+
while (remaining > 0) {
|
|
242
|
+
if (loopGuard > 2000) {
|
|
243
|
+
throw new Error(`clearDocumentContent exceeded safety loop count for document "${documentId}"`);
|
|
244
|
+
}
|
|
245
|
+
loopGuard += 1;
|
|
246
|
+
const deleteCount = Math.min(remaining, 50);
|
|
247
|
+
await limiter.wait();
|
|
248
|
+
let response;
|
|
249
|
+
try {
|
|
250
|
+
response = await withRetry('docx.documentBlockChildren.batchDelete', async () => client.docx.documentBlockChildren.batchDelete({
|
|
251
|
+
path: {
|
|
252
|
+
document_id: documentId,
|
|
253
|
+
block_id: rootId,
|
|
254
|
+
},
|
|
255
|
+
data: {
|
|
256
|
+
start_index: 0,
|
|
257
|
+
end_index: deleteCount,
|
|
258
|
+
},
|
|
259
|
+
}, authOptions));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (isBatchDeleteOutOfRangeError(error)) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
assertSuccessEnvelope(response, 'docx.documentBlockChildren.batchDelete');
|
|
268
|
+
remaining -= deleteCount;
|
|
269
|
+
}
|
|
270
|
+
return rootId;
|
|
271
|
+
}
|
|
272
|
+
export async function clearBlockChildrenByKnownCount(client, documentId, blockId, childCount, authOptions, limiter) {
|
|
273
|
+
let remaining = Math.max(0, Math.trunc(childCount));
|
|
274
|
+
while (remaining > 0) {
|
|
275
|
+
const deleteCount = Math.min(remaining, 50);
|
|
276
|
+
await limiter.wait();
|
|
277
|
+
const response = await withRetry('docx.documentBlockChildren.batchDelete(cell)', async () => client.docx.documentBlockChildren.batchDelete({
|
|
278
|
+
path: {
|
|
279
|
+
document_id: documentId,
|
|
280
|
+
block_id: blockId,
|
|
281
|
+
},
|
|
282
|
+
data: {
|
|
283
|
+
start_index: 0,
|
|
284
|
+
end_index: deleteCount,
|
|
285
|
+
},
|
|
286
|
+
}, authOptions));
|
|
287
|
+
assertSuccessEnvelope(response, 'docx.documentBlockChildren.batchDelete(cell)');
|
|
288
|
+
remaining -= deleteCount;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function batchUpdateDocumentBlocks(client, documentId, requests, authOptions, limiter) {
|
|
292
|
+
if (requests.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
await limiter.wait();
|
|
295
|
+
let response;
|
|
296
|
+
try {
|
|
297
|
+
response = await withRetry('docx.documentBlock.batchUpdate', async () => client.docx.documentBlock.batchUpdate({
|
|
298
|
+
path: {
|
|
299
|
+
document_id: documentId,
|
|
300
|
+
},
|
|
301
|
+
data: {
|
|
302
|
+
requests: requests,
|
|
303
|
+
},
|
|
304
|
+
}, authOptions));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (requests.length > 1 && isInvalidParamError(error)) {
|
|
308
|
+
const mid = Math.floor(requests.length / 2);
|
|
309
|
+
const left = requests.slice(0, mid);
|
|
310
|
+
const right = requests.slice(mid);
|
|
311
|
+
await batchUpdateDocumentBlocks(client, documentId, left, authOptions, limiter);
|
|
312
|
+
await batchUpdateDocumentBlocks(client, documentId, right, authOptions, limiter);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
assertSuccessEnvelope(response, 'docx.documentBlock.batchUpdate');
|
|
318
|
+
}
|
|
319
|
+
async function expandTableToTargetSize(client, documentId, tableBlockId, initialRowSize, initialColumnSize, targetRowSize, targetColumnSize, authOptions, limiter) {
|
|
320
|
+
const rowRequests = [];
|
|
321
|
+
for (let rowIndex = initialRowSize; rowIndex < targetRowSize; rowIndex += 1) {
|
|
322
|
+
rowRequests.push({
|
|
323
|
+
block_id: tableBlockId,
|
|
324
|
+
insert_table_row: {
|
|
325
|
+
row_index: rowIndex,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
for (let start = 0; start < rowRequests.length; start += TABLE_EXPAND_BATCH_REQUEST_SIZE) {
|
|
330
|
+
const requests = rowRequests.slice(start, start + TABLE_EXPAND_BATCH_REQUEST_SIZE);
|
|
331
|
+
await batchUpdateDocumentBlocks(client, documentId, requests, authOptions, limiter);
|
|
332
|
+
}
|
|
333
|
+
const columnRequests = [];
|
|
334
|
+
for (let columnIndex = initialColumnSize; columnIndex < targetColumnSize; columnIndex += 1) {
|
|
335
|
+
columnRequests.push({
|
|
336
|
+
block_id: tableBlockId,
|
|
337
|
+
insert_table_column: {
|
|
338
|
+
column_index: columnIndex,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
for (let start = 0; start < columnRequests.length; start += TABLE_EXPAND_BATCH_REQUEST_SIZE) {
|
|
343
|
+
const requests = columnRequests.slice(start, start + TABLE_EXPAND_BATCH_REQUEST_SIZE);
|
|
344
|
+
await batchUpdateDocumentBlocks(client, documentId, requests, authOptions, limiter);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
export async function createDocumentChildren(client, documentId, parentBlockId, children, authOptions, limiter) {
|
|
348
|
+
const created = [];
|
|
349
|
+
const createAndCollect = async (createPayloads) => {
|
|
350
|
+
if (createPayloads.length === 0)
|
|
351
|
+
return [];
|
|
352
|
+
const payloads = [...createPayloads];
|
|
353
|
+
const payloadTypes = createPayloads.map((payload) => typeof payload.block_type === 'number' ? payload.block_type : Number.NaN);
|
|
354
|
+
await limiter.wait();
|
|
355
|
+
let response;
|
|
356
|
+
try {
|
|
357
|
+
response = await withRetry('docx.documentBlockChildren.create', async () => client.docx.documentBlockChildren.create({
|
|
358
|
+
path: {
|
|
359
|
+
document_id: documentId,
|
|
360
|
+
block_id: parentBlockId,
|
|
361
|
+
},
|
|
362
|
+
data: {
|
|
363
|
+
children: payloads,
|
|
364
|
+
},
|
|
365
|
+
}, authOptions));
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (payloads.length > 1 && isCreateInvalidParamError(error)) {
|
|
369
|
+
const mid = Math.floor(payloads.length / 2);
|
|
370
|
+
const left = await createAndCollect(payloads.slice(0, mid));
|
|
371
|
+
const right = await createAndCollect(payloads.slice(mid));
|
|
372
|
+
return [...left, ...right];
|
|
373
|
+
}
|
|
374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
375
|
+
throw new Error(`docx.documentBlockChildren.create failed for block_types=${JSON.stringify(payloadTypes)} parent=${parentBlockId}: ${message}`);
|
|
376
|
+
}
|
|
377
|
+
assertSuccessEnvelope(response, 'docx.documentBlockChildren.create');
|
|
378
|
+
const data = getResponseData(response);
|
|
379
|
+
const createdRaw = getArray(data, 'children');
|
|
380
|
+
const parsed = [];
|
|
381
|
+
for (const item of createdRaw) {
|
|
382
|
+
const block = toDocxBlock(item);
|
|
383
|
+
if (block) {
|
|
384
|
+
parsed.push(block);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return parsed;
|
|
388
|
+
};
|
|
389
|
+
const flushNonTableBatch = async (batch) => {
|
|
390
|
+
if (batch.length === 0)
|
|
391
|
+
return;
|
|
392
|
+
const parsed = await createAndCollect(batch);
|
|
393
|
+
for (const block of parsed) {
|
|
394
|
+
created.push(block);
|
|
395
|
+
}
|
|
396
|
+
batch.length = 0;
|
|
397
|
+
};
|
|
398
|
+
const nonTableBatch = [];
|
|
399
|
+
for (const child of children) {
|
|
400
|
+
const tablePlan = buildTableCreatePlan(child);
|
|
401
|
+
if (!tablePlan) {
|
|
402
|
+
nonTableBatch.push(child);
|
|
403
|
+
if (nonTableBatch.length >= DOCX_CREATE_CHILDREN_BATCH_SIZE) {
|
|
404
|
+
await flushNonTableBatch(nonTableBatch);
|
|
405
|
+
}
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
await flushNonTableBatch(nonTableBatch);
|
|
409
|
+
const createPayload = tablePlan.createPayload;
|
|
410
|
+
const payloadBlockType = typeof createPayload.block_type === 'number' ? createPayload.block_type : Number.NaN;
|
|
411
|
+
const parsed = await createAndCollect([createPayload]);
|
|
412
|
+
for (const block of parsed) {
|
|
413
|
+
created.push(block);
|
|
414
|
+
}
|
|
415
|
+
const createdTable = parsed.find((item) => item.block_type === 31);
|
|
416
|
+
const createdTableBlockId = createdTable?.block_id ?? '';
|
|
417
|
+
if (!createdTableBlockId) {
|
|
418
|
+
const tableProperty = payloadBlockType === 31 ? (toObjectRecord(toObjectRecord(createPayload.table)?.property) ?? null) : null;
|
|
419
|
+
throw new Error(`Failed to resolve table block id after create for table expansion. block_type=${String(payloadBlockType)} table.property=${JSON.stringify(tableProperty)}`);
|
|
420
|
+
}
|
|
421
|
+
await expandTableToTargetSize(client, documentId, createdTableBlockId, tablePlan.initialRowSize, tablePlan.initialColumnSize, tablePlan.targetRowSize, tablePlan.targetColumnSize, authOptions, limiter);
|
|
422
|
+
}
|
|
423
|
+
await flushNonTableBatch(nonTableBatch);
|
|
424
|
+
return created;
|
|
425
|
+
}
|
|
426
|
+
export async function resolveCreatedTableCellIds(client, documentId, tableBlockId, createdTableBlock, expectedCellCount, authOptions, limiter) {
|
|
427
|
+
const fromCreate = (createdTableBlock?.children ?? []).filter((item) => typeof item === 'string');
|
|
428
|
+
if (fromCreate.length >= expectedCellCount) {
|
|
429
|
+
return fromCreate;
|
|
430
|
+
}
|
|
431
|
+
const fetchedTableBlock = await getDocumentBlockById(client, documentId, tableBlockId, authOptions, limiter);
|
|
432
|
+
const fromGet = (fetchedTableBlock?.children ?? []).filter((item) => typeof item === 'string');
|
|
433
|
+
if (fromGet.length >= expectedCellCount) {
|
|
434
|
+
return fromGet;
|
|
435
|
+
}
|
|
436
|
+
const blocks = await listAllDocumentBlocks(client, documentId, authOptions, limiter);
|
|
437
|
+
const tableBlockFromList = blocks.find((item) => item.block_id === tableBlockId);
|
|
438
|
+
const resolved = (tableBlockFromList?.children ?? []).filter((item) => typeof item === 'string');
|
|
439
|
+
if (resolved.length < expectedCellCount) {
|
|
440
|
+
throw new Error(`Table "${tableBlockId}" created ${resolved.length} cells but expected at least ${expectedCellCount}.`);
|
|
441
|
+
}
|
|
442
|
+
return resolved;
|
|
443
|
+
}
|
|
444
|
+
export async function patchTextBlockElements(client, documentId, textBlockId, elements, authOptions, limiter, align) {
|
|
445
|
+
await limiter.wait();
|
|
446
|
+
let response;
|
|
447
|
+
const operationName = align === undefined ? 'docx.documentBlock.patch(update_text_elements)' : 'docx.documentBlock.patch(update_text)';
|
|
448
|
+
const data = align === undefined
|
|
449
|
+
? {
|
|
450
|
+
update_text_elements: {
|
|
451
|
+
elements: elements,
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
: {
|
|
455
|
+
update_text: {
|
|
456
|
+
elements: elements,
|
|
457
|
+
style: {
|
|
458
|
+
align,
|
|
459
|
+
},
|
|
460
|
+
fields: [1],
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
try {
|
|
464
|
+
response = await withRetry(operationName, async () => client.docx.documentBlock.patch({
|
|
465
|
+
path: {
|
|
466
|
+
document_id: documentId,
|
|
467
|
+
block_id: textBlockId,
|
|
468
|
+
},
|
|
469
|
+
data,
|
|
470
|
+
}, authOptions));
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
const sample = JSON.stringify(elements).slice(0, 600);
|
|
474
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
475
|
+
throw new Error(`${operationName} failed for block=${textBlockId} align=${align ?? 'default'} sample=${sample}: ${message}`);
|
|
476
|
+
}
|
|
477
|
+
assertSuccessEnvelope(response, operationName);
|
|
478
|
+
}
|
|
479
|
+
export async function uploadBinaryToNode(client, parentType, parentNode, filePath, authOptions, limiter) {
|
|
480
|
+
const absolutePath = path.resolve(filePath);
|
|
481
|
+
try {
|
|
482
|
+
await access(absolutePath);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
throw new Error(`Asset file does not exist: ${absolutePath}`);
|
|
486
|
+
}
|
|
487
|
+
const fileBuffer = await readFile(absolutePath);
|
|
488
|
+
const fileName = path.basename(absolutePath);
|
|
489
|
+
await limiter.wait();
|
|
490
|
+
const response = await withRetry('drive.media.uploadAll', async () => client.drive.media.uploadAll({
|
|
491
|
+
data: {
|
|
492
|
+
file_name: fileName,
|
|
493
|
+
parent_type: parentType,
|
|
494
|
+
parent_node: parentNode,
|
|
495
|
+
size: fileBuffer.byteLength,
|
|
496
|
+
file: fileBuffer,
|
|
497
|
+
},
|
|
498
|
+
}, authOptions));
|
|
499
|
+
if (!response || typeof response !== 'object') {
|
|
500
|
+
throw new Error(`drive.media.uploadAll returned empty response for "${absolutePath}"`);
|
|
501
|
+
}
|
|
502
|
+
const row = response;
|
|
503
|
+
const fileToken = getString(row, 'file_token');
|
|
504
|
+
if (!fileToken) {
|
|
505
|
+
throw new Error(`drive.media.uploadAll returned empty file_token for "${absolutePath}"`);
|
|
506
|
+
}
|
|
507
|
+
return fileToken;
|
|
508
|
+
}
|
|
509
|
+
export function isRelationMismatchError(error) {
|
|
510
|
+
if (!(error instanceof Error))
|
|
511
|
+
return false;
|
|
512
|
+
return /1770013|relation mismatch/i.test(error.message);
|
|
513
|
+
}
|
|
514
|
+
export async function replaceImageBlock(client, documentId, blockId, imageToken, authOptions, limiter) {
|
|
515
|
+
await limiter.wait();
|
|
516
|
+
const response = await withRetry('docx.documentBlock.batchUpdate(replace_image)', async () => client.docx.documentBlock.batchUpdate({
|
|
517
|
+
path: {
|
|
518
|
+
document_id: documentId,
|
|
519
|
+
},
|
|
520
|
+
data: {
|
|
521
|
+
requests: [
|
|
522
|
+
{
|
|
523
|
+
block_id: blockId,
|
|
524
|
+
replace_image: {
|
|
525
|
+
token: imageToken,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
}, authOptions));
|
|
531
|
+
assertSuccessEnvelope(response, 'docx.documentBlock.batchUpdate(replace_image)');
|
|
532
|
+
}
|
|
533
|
+
export async function replaceFileBlock(client, documentId, blockId, fileToken, authOptions, limiter) {
|
|
534
|
+
await limiter.wait();
|
|
535
|
+
const response = await withRetry('docx.documentBlock.batchUpdate(replace_file)', async () => client.docx.documentBlock.batchUpdate({
|
|
536
|
+
path: {
|
|
537
|
+
document_id: documentId,
|
|
538
|
+
},
|
|
539
|
+
data: {
|
|
540
|
+
requests: [
|
|
541
|
+
{
|
|
542
|
+
block_id: blockId,
|
|
543
|
+
replace_file: {
|
|
544
|
+
token: fileToken,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
}, authOptions));
|
|
550
|
+
assertSuccessEnvelope(response, 'docx.documentBlock.batchUpdate(replace_file)');
|
|
551
|
+
}
|
|
552
|
+
export async function createBoardPlantumlNode(client, whiteboardId, plantUmlCode, options, authOptions, limiter) {
|
|
553
|
+
const trimmedWhiteboardId = whiteboardId.trim();
|
|
554
|
+
if (!trimmedWhiteboardId) {
|
|
555
|
+
throw new Error('createBoardPlantumlNode requires non-empty whiteboard id.');
|
|
556
|
+
}
|
|
557
|
+
const data = {
|
|
558
|
+
plant_uml_code: plantUmlCode,
|
|
559
|
+
};
|
|
560
|
+
const syntaxType = toNonNegativeInt(options?.syntaxType);
|
|
561
|
+
const styleType = toNonNegativeInt(options?.styleType);
|
|
562
|
+
const diagramType = toNonNegativeInt(options?.diagramType);
|
|
563
|
+
if (syntaxType !== undefined) {
|
|
564
|
+
data.syntax_type = syntaxType;
|
|
565
|
+
}
|
|
566
|
+
if (styleType !== undefined) {
|
|
567
|
+
data.style_type = styleType;
|
|
568
|
+
}
|
|
569
|
+
if (diagramType !== undefined) {
|
|
570
|
+
data.diagram_type = diagramType;
|
|
571
|
+
}
|
|
572
|
+
await limiter.wait();
|
|
573
|
+
const response = await withRetry('board.v1.whiteboardNode.createPlantuml', async () => client.board.v1.whiteboardNode.createPlantuml({
|
|
574
|
+
path: {
|
|
575
|
+
whiteboard_id: trimmedWhiteboardId,
|
|
576
|
+
},
|
|
577
|
+
data: data,
|
|
578
|
+
}, authOptions));
|
|
579
|
+
assertSuccessEnvelope(response, 'board.v1.whiteboardNode.createPlantuml');
|
|
580
|
+
}
|
|
581
|
+
export async function getRawDocumentBlockById(client, documentId, blockId, authOptions, limiter) {
|
|
582
|
+
await limiter.wait();
|
|
583
|
+
const response = await withRetry('docx.documentBlock.get', async () => client.docx.documentBlock.get({
|
|
584
|
+
path: {
|
|
585
|
+
document_id: documentId,
|
|
586
|
+
block_id: blockId,
|
|
587
|
+
},
|
|
588
|
+
}, authOptions));
|
|
589
|
+
assertSuccessEnvelope(response, 'docx.documentBlock.get');
|
|
590
|
+
const data = getResponseData(response);
|
|
591
|
+
return toObjectRecord(data.block);
|
|
592
|
+
}
|
|
593
|
+
export async function getDocumentBlockById(client, documentId, blockId, authOptions, limiter) {
|
|
594
|
+
const rawBlock = await getRawDocumentBlockById(client, documentId, blockId, authOptions, limiter);
|
|
595
|
+
return toDocxBlock(rawBlock);
|
|
596
|
+
}
|