@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,156 @@
|
|
|
1
|
+
import { createDocumentChildren, } from './ops.js';
|
|
2
|
+
import { DEFAULT_MERMAID_RENDER_CONFIG, } from './render-types.js';
|
|
3
|
+
import { applyCreatedBoardMermaid, applyCreatedFileBlock, applyCreatedImageBlock } from './render-post-process.js';
|
|
4
|
+
import { renderCreatedTableNode } from './render-table.js';
|
|
5
|
+
import { buildCreatePayloadFromRawBlock, createRenderBatchEntry, getSourceBlockId, toObjectRecord, } from './render-payload.js';
|
|
6
|
+
const RENDER_LEAF_BATCH_SIZE = 50;
|
|
7
|
+
async function renderBTTNodesToDocument(client, documentId, parentBlockId, nodes, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError) {
|
|
8
|
+
const batch = [];
|
|
9
|
+
const flushBatch = async () => {
|
|
10
|
+
if (batch.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const entries = [...batch];
|
|
13
|
+
batch.length = 0;
|
|
14
|
+
let created = [];
|
|
15
|
+
try {
|
|
16
|
+
created = await createDocumentChildren(client, documentId, parentBlockId, entries.map((entry) => entry.createPayload), authOptions, docxLimiter);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!continueOnError) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
await renderBTTNodeToDocument(client, documentId, parentBlockId, entry.node, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (created.length !== entries.length) {
|
|
28
|
+
const errorText = `Batch create count mismatch under parent=${parentBlockId}: expected=${entries.length} actual=${created.length}`;
|
|
29
|
+
if (!continueOnError) {
|
|
30
|
+
throw new Error(errorText);
|
|
31
|
+
}
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
report.failedNodes.push({
|
|
34
|
+
sourceBlockId: entry.sourceBlockId,
|
|
35
|
+
blockType: entry.node.blockType,
|
|
36
|
+
parentBlockId,
|
|
37
|
+
error: errorText,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
report.createdBlockCount += created.length;
|
|
43
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
44
|
+
const entry = entries[index];
|
|
45
|
+
const createdBlock = created[index];
|
|
46
|
+
if (!entry || !createdBlock)
|
|
47
|
+
continue;
|
|
48
|
+
const createdBlockId = createdBlock.block_id;
|
|
49
|
+
if (!createdBlockId)
|
|
50
|
+
continue;
|
|
51
|
+
if (entry.node.children.length === 0)
|
|
52
|
+
continue;
|
|
53
|
+
await renderBTTNodesToDocument(client, documentId, createdBlockId, entry.node.children, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
for (const node of nodes) {
|
|
57
|
+
const entry = createRenderBatchEntry(node);
|
|
58
|
+
if (entry) {
|
|
59
|
+
batch.push(entry);
|
|
60
|
+
if (batch.length >= RENDER_LEAF_BATCH_SIZE) {
|
|
61
|
+
await flushBatch();
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
await flushBatch();
|
|
66
|
+
await renderBTTNodeToDocument(client, documentId, parentBlockId, node, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
67
|
+
}
|
|
68
|
+
await flushBatch();
|
|
69
|
+
}
|
|
70
|
+
async function renderBTTNodeToDocument(client, documentId, parentBlockId, node, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError) {
|
|
71
|
+
const rawBlockRecord = toObjectRecord(node.rawBlock);
|
|
72
|
+
if (!rawBlockRecord)
|
|
73
|
+
return;
|
|
74
|
+
const sourceBlockId = getSourceBlockId(node, rawBlockRecord);
|
|
75
|
+
try {
|
|
76
|
+
const createPayload = buildCreatePayloadFromRawBlock(rawBlockRecord);
|
|
77
|
+
if (!createPayload) {
|
|
78
|
+
await renderBTTNodesToDocument(client, documentId, parentBlockId, node.children, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let createdBlocks;
|
|
82
|
+
try {
|
|
83
|
+
createdBlocks = await createDocumentChildren(client, documentId, parentBlockId, [createPayload], authOptions, docxLimiter);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
throw new Error(`Failed to render BTT node source=${String(rawBlockRecord.block_id ?? node.blockId)} type=${String(createPayload.block_type)}: ${message}`);
|
|
88
|
+
}
|
|
89
|
+
report.createdBlockCount += createdBlocks.length;
|
|
90
|
+
const createdBlock = createdBlocks[0];
|
|
91
|
+
const createdBlockId = createdBlock?.block_id;
|
|
92
|
+
if (!createdBlockId) {
|
|
93
|
+
throw new Error(`Failed to create block for source "${String(rawBlockRecord.block_id ?? node.blockId)}".`);
|
|
94
|
+
}
|
|
95
|
+
const mermaidPatch = mermaidRenderConfig.target === 'board'
|
|
96
|
+
? (mermaidByBlockId.get(node.blockId) ?? mermaidByBlockId.get(sourceBlockId))
|
|
97
|
+
: undefined;
|
|
98
|
+
if (node.blockType === 43 && mermaidPatch) {
|
|
99
|
+
await applyCreatedBoardMermaid(client, documentId, createdBlockId, mermaidPatch, mermaidRenderConfig, authOptions, docxLimiter);
|
|
100
|
+
}
|
|
101
|
+
if (node.blockType === 27) {
|
|
102
|
+
const mapping = await applyCreatedImageBlock(client, documentId, createdBlockId, sourceBlockId, rawBlockRecord, authOptions, docxLimiter, mediaLimiter);
|
|
103
|
+
if (mapping) {
|
|
104
|
+
report.mediaTokenMappings.push(mapping);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (node.blockType === 23) {
|
|
108
|
+
const mapping = await applyCreatedFileBlock(client, documentId, createdBlockId, createdBlock, sourceBlockId, rawBlockRecord, authOptions, docxLimiter, mediaLimiter);
|
|
109
|
+
if (mapping) {
|
|
110
|
+
report.mediaTokenMappings.push(mapping);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (node.blockType !== 31) {
|
|
114
|
+
await renderBTTNodesToDocument(client, documentId, createdBlockId, node.children, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await renderCreatedTableNode({
|
|
118
|
+
client,
|
|
119
|
+
documentId,
|
|
120
|
+
node,
|
|
121
|
+
createdBlock,
|
|
122
|
+
createdBlockId,
|
|
123
|
+
authOptions,
|
|
124
|
+
docxLimiter,
|
|
125
|
+
renderChildren: async (nextParentBlockId, nextNodes) => {
|
|
126
|
+
await renderBTTNodesToDocument(client, documentId, nextParentBlockId, nextNodes, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
132
|
+
report.failedNodes.push({
|
|
133
|
+
sourceBlockId,
|
|
134
|
+
blockType: node.blockType,
|
|
135
|
+
parentBlockId,
|
|
136
|
+
error: errorText,
|
|
137
|
+
});
|
|
138
|
+
if (continueOnError) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function renderBTTToDocument(client, documentId, parentBlockId, rootNode, authOptions, docxLimiter, mediaLimiter, options) {
|
|
145
|
+
const report = {
|
|
146
|
+
createdBlockCount: 0,
|
|
147
|
+
mediaTokenMappings: [],
|
|
148
|
+
failedNodes: [],
|
|
149
|
+
};
|
|
150
|
+
const continueOnError = Boolean(options?.continueOnError);
|
|
151
|
+
const mermaidByBlockId = options?.mermaidByBlockId ?? new Map();
|
|
152
|
+
const mermaidRenderConfig = options?.mermaidRender ?? DEFAULT_MERMAID_RENDER_CONFIG;
|
|
153
|
+
const initialNodes = rootNode.blockType === 1 ? rootNode.children : [rootNode];
|
|
154
|
+
await renderBTTNodesToDocument(client, documentId, parentBlockId, initialNodes, authOptions, docxLimiter, mediaLimiter, mermaidByBlockId, mermaidRenderConfig, report, continueOnError);
|
|
155
|
+
return report;
|
|
156
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const TEXTUAL_BLOCK_PAYLOAD_KEY_BY_TYPE = {
|
|
2
|
+
2: 'text',
|
|
3
|
+
3: 'heading1',
|
|
4
|
+
4: 'heading2',
|
|
5
|
+
5: 'heading3',
|
|
6
|
+
6: 'heading4',
|
|
7
|
+
7: 'heading5',
|
|
8
|
+
8: 'heading6',
|
|
9
|
+
9: 'heading7',
|
|
10
|
+
10: 'heading8',
|
|
11
|
+
11: 'heading9',
|
|
12
|
+
12: 'bullet',
|
|
13
|
+
13: 'ordered',
|
|
14
|
+
14: 'code',
|
|
15
|
+
15: 'quote',
|
|
16
|
+
17: 'todo',
|
|
17
|
+
};
|
|
18
|
+
function getTextualPayloadKey(blockType) {
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(TEXTUAL_BLOCK_PAYLOAD_KEY_BY_TYPE, blockType)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return TEXTUAL_BLOCK_PAYLOAD_KEY_BY_TYPE[blockType];
|
|
23
|
+
}
|
|
24
|
+
function deepClone(value) {
|
|
25
|
+
return JSON.parse(JSON.stringify(value));
|
|
26
|
+
}
|
|
27
|
+
export function toObjectRecord(value) {
|
|
28
|
+
return value && typeof value === 'object' ? value : null;
|
|
29
|
+
}
|
|
30
|
+
function toPositiveInt(value) {
|
|
31
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
return Math.round(value);
|
|
34
|
+
}
|
|
35
|
+
function isAllowedLinkUrl(value) {
|
|
36
|
+
return /^(https?:\/\/|mailto:|tel:)/i.test(value.trim());
|
|
37
|
+
}
|
|
38
|
+
function sanitizeTextElementStyle(style) {
|
|
39
|
+
for (const key of Object.keys(style)) {
|
|
40
|
+
const value = style[key];
|
|
41
|
+
if (value === false || value === null || value === undefined) {
|
|
42
|
+
delete style[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const link = toObjectRecord(style.link);
|
|
46
|
+
if (link) {
|
|
47
|
+
const url = typeof link.url === 'string' ? link.url.trim() : '';
|
|
48
|
+
if (!isAllowedLinkUrl(url)) {
|
|
49
|
+
delete style.link;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
link.url = url;
|
|
53
|
+
style.link = link;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function sanitizeTextElementsForCreate(elementsRaw) {
|
|
58
|
+
if (!Array.isArray(elementsRaw))
|
|
59
|
+
return;
|
|
60
|
+
for (const element of elementsRaw) {
|
|
61
|
+
if (!element || typeof element !== 'object')
|
|
62
|
+
continue;
|
|
63
|
+
const record = element;
|
|
64
|
+
for (const key of Object.keys(record)) {
|
|
65
|
+
const inline = toObjectRecord(record[key]);
|
|
66
|
+
if (!inline)
|
|
67
|
+
continue;
|
|
68
|
+
const style = toObjectRecord(inline.text_element_style);
|
|
69
|
+
if (!style)
|
|
70
|
+
continue;
|
|
71
|
+
sanitizeTextElementStyle(style);
|
|
72
|
+
if (Object.keys(style).length === 0) {
|
|
73
|
+
delete inline.text_element_style;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function sanitizeTextualStyleForCreate(blockType, payload) {
|
|
79
|
+
const payloadKey = getTextualPayloadKey(blockType);
|
|
80
|
+
if (!payloadKey)
|
|
81
|
+
return;
|
|
82
|
+
const textPayload = toObjectRecord(payload[payloadKey]);
|
|
83
|
+
if (!textPayload)
|
|
84
|
+
return;
|
|
85
|
+
sanitizeTextElementsForCreate(textPayload.elements);
|
|
86
|
+
const style = toObjectRecord(textPayload.style);
|
|
87
|
+
if (!style)
|
|
88
|
+
return;
|
|
89
|
+
if (blockType === 14) {
|
|
90
|
+
const language = typeof style.language === 'number' ? style.language : undefined;
|
|
91
|
+
textPayload.style = language === undefined ? {} : { language };
|
|
92
|
+
}
|
|
93
|
+
else if (blockType === 17) {
|
|
94
|
+
textPayload.style = Object.prototype.hasOwnProperty.call(style, 'done') ? { done: Boolean(style.done) } : {};
|
|
95
|
+
}
|
|
96
|
+
else if (blockType >= 3 && blockType <= 11) {
|
|
97
|
+
textPayload.style = typeof style.sequence === 'string' ? { sequence: style.sequence } : {};
|
|
98
|
+
}
|
|
99
|
+
else if (blockType === 2) {
|
|
100
|
+
const align = typeof style.align === 'number' ? style.align : undefined;
|
|
101
|
+
textPayload.style = align === undefined ? {} : { align };
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
delete textPayload.style;
|
|
105
|
+
}
|
|
106
|
+
if (toObjectRecord(textPayload.style) && Object.keys(textPayload.style).length === 0) {
|
|
107
|
+
delete textPayload.style;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function canUseElementsOnlyPatch(rawBlock) {
|
|
111
|
+
const blockType = typeof rawBlock.block_type === 'number' ? rawBlock.block_type : Number.NaN;
|
|
112
|
+
if (!Number.isFinite(blockType))
|
|
113
|
+
return true;
|
|
114
|
+
const payloadKey = getTextualPayloadKey(blockType);
|
|
115
|
+
if (!payloadKey)
|
|
116
|
+
return true;
|
|
117
|
+
const textPayload = toObjectRecord(rawBlock[payloadKey]);
|
|
118
|
+
if (!textPayload)
|
|
119
|
+
return true;
|
|
120
|
+
const style = toObjectRecord(textPayload.style);
|
|
121
|
+
if (!style)
|
|
122
|
+
return true;
|
|
123
|
+
const effectiveEntries = Object.entries(style).filter(([, value]) => value !== undefined && value !== null);
|
|
124
|
+
if (effectiveEntries.length === 0)
|
|
125
|
+
return true;
|
|
126
|
+
for (const [key, value] of effectiveEntries) {
|
|
127
|
+
if (key === 'align' && (value === 1 || value === 2 || value === 3 || value === 'left' || value === 'center' || value === 'right')) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
export function extractTextAlignForPatchFromRawBlock(rawBlock) {
|
|
135
|
+
const blockType = typeof rawBlock.block_type === 'number' ? rawBlock.block_type : Number.NaN;
|
|
136
|
+
if (!Number.isFinite(blockType))
|
|
137
|
+
return undefined;
|
|
138
|
+
const payloadKey = getTextualPayloadKey(blockType);
|
|
139
|
+
if (!payloadKey)
|
|
140
|
+
return undefined;
|
|
141
|
+
const textPayload = toObjectRecord(rawBlock[payloadKey]);
|
|
142
|
+
if (!textPayload)
|
|
143
|
+
return undefined;
|
|
144
|
+
const style = toObjectRecord(textPayload.style);
|
|
145
|
+
if (!style || !Object.prototype.hasOwnProperty.call(style, 'align'))
|
|
146
|
+
return undefined;
|
|
147
|
+
const rawAlign = style.align;
|
|
148
|
+
if (rawAlign === 1 || rawAlign === 2 || rawAlign === 3)
|
|
149
|
+
return rawAlign;
|
|
150
|
+
if (rawAlign === 'left')
|
|
151
|
+
return 1;
|
|
152
|
+
if (rawAlign === 'center')
|
|
153
|
+
return 2;
|
|
154
|
+
if (rawAlign === 'right')
|
|
155
|
+
return 3;
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function sanitizeImagePayload(payload, key) {
|
|
159
|
+
const imageLike = toObjectRecord(payload[key]);
|
|
160
|
+
if (!imageLike)
|
|
161
|
+
return;
|
|
162
|
+
if (typeof imageLike.token === 'string' && imageLike.token.trim().length === 0) {
|
|
163
|
+
delete imageLike.token;
|
|
164
|
+
}
|
|
165
|
+
if (typeof imageLike.width === 'number' && imageLike.width <= 0) {
|
|
166
|
+
delete imageLike.width;
|
|
167
|
+
}
|
|
168
|
+
if (typeof imageLike.height === 'number' && imageLike.height <= 0) {
|
|
169
|
+
delete imageLike.height;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function sanitizeTablePayloadForCreate(payload) {
|
|
173
|
+
const table = toObjectRecord(payload.table);
|
|
174
|
+
if (!table)
|
|
175
|
+
return;
|
|
176
|
+
const property = toObjectRecord(table.property);
|
|
177
|
+
if (!property)
|
|
178
|
+
return;
|
|
179
|
+
const rowSize = toPositiveInt(property.row_size);
|
|
180
|
+
const columnSize = toPositiveInt(property.column_size);
|
|
181
|
+
if (rowSize !== undefined) {
|
|
182
|
+
property.row_size = rowSize;
|
|
183
|
+
}
|
|
184
|
+
if (columnSize !== undefined) {
|
|
185
|
+
property.column_size = columnSize;
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(property.merge_info) && property.merge_info.length === 0) {
|
|
188
|
+
delete property.merge_info;
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(property.column_width)) {
|
|
191
|
+
const normalized = property.column_width
|
|
192
|
+
.filter((item) => typeof item === 'number' && Number.isFinite(item) && item > 0)
|
|
193
|
+
.map((item) => Math.round(item));
|
|
194
|
+
const truncated = columnSize !== undefined ? normalized.slice(0, columnSize) : normalized;
|
|
195
|
+
if (truncated.length === 0) {
|
|
196
|
+
delete property.column_width;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
property.column_width = truncated;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (property.header_row === false) {
|
|
203
|
+
delete property.header_row;
|
|
204
|
+
}
|
|
205
|
+
if (property.header_column === false) {
|
|
206
|
+
delete property.header_column;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function ensureTextElementsPayload(blockType, payload) {
|
|
210
|
+
const payloadKey = getTextualPayloadKey(blockType);
|
|
211
|
+
if (!payloadKey)
|
|
212
|
+
return;
|
|
213
|
+
const textPayload = toObjectRecord(payload[payloadKey]);
|
|
214
|
+
if (!textPayload) {
|
|
215
|
+
payload[payloadKey] = {
|
|
216
|
+
elements: [{ text_run: { content: ' ' } }],
|
|
217
|
+
};
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const elements = Array.isArray(textPayload.elements) ? textPayload.elements : [];
|
|
221
|
+
if (elements.length === 0) {
|
|
222
|
+
textPayload.elements = [{ text_run: { content: ' ' } }];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
export function buildCreatePayloadFromRawBlock(rawBlock) {
|
|
226
|
+
const blockType = typeof rawBlock.block_type === 'number' ? rawBlock.block_type : Number.NaN;
|
|
227
|
+
if (!Number.isFinite(blockType)) {
|
|
228
|
+
throw new Error(`Invalid raw block_type: ${String(rawBlock.block_type)}`);
|
|
229
|
+
}
|
|
230
|
+
if (blockType === 1) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const payload = {
|
|
234
|
+
block_type: blockType,
|
|
235
|
+
};
|
|
236
|
+
for (const [key, value] of Object.entries(rawBlock)) {
|
|
237
|
+
if (key === 'block_id' || key === 'parent_id' || key === 'children' || key === 'block_type') {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
payload[key] = deepClone(value);
|
|
241
|
+
}
|
|
242
|
+
if (blockType === 31) {
|
|
243
|
+
const table = toObjectRecord(payload.table);
|
|
244
|
+
if (table) {
|
|
245
|
+
delete table.cells;
|
|
246
|
+
}
|
|
247
|
+
sanitizeTablePayloadForCreate(payload);
|
|
248
|
+
}
|
|
249
|
+
if (blockType === 27) {
|
|
250
|
+
const image = toObjectRecord(payload.image);
|
|
251
|
+
if (image) {
|
|
252
|
+
delete image.local_path;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (blockType === 23) {
|
|
256
|
+
const file = toObjectRecord(payload.file);
|
|
257
|
+
if (file) {
|
|
258
|
+
delete file.local_path;
|
|
259
|
+
delete file.media_kind;
|
|
260
|
+
delete file.token;
|
|
261
|
+
delete file.file_token;
|
|
262
|
+
delete file.name;
|
|
263
|
+
const viewType = toPositiveInt(file.view_type);
|
|
264
|
+
if (viewType === undefined) {
|
|
265
|
+
delete file.view_type;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
file.view_type = viewType;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
sanitizeTextualStyleForCreate(blockType, payload);
|
|
273
|
+
if (blockType === 27)
|
|
274
|
+
sanitizeImagePayload(payload, 'image');
|
|
275
|
+
if (blockType === 43)
|
|
276
|
+
sanitizeImagePayload(payload, 'board');
|
|
277
|
+
ensureTextElementsPayload(blockType, payload);
|
|
278
|
+
return payload;
|
|
279
|
+
}
|
|
280
|
+
export function getExpectedTableCellCount(tableNode) {
|
|
281
|
+
const raw = toObjectRecord(tableNode.rawBlock);
|
|
282
|
+
const table = toObjectRecord(raw?.table);
|
|
283
|
+
const property = toObjectRecord(table?.property);
|
|
284
|
+
const rowSize = Math.max(1, toPositiveInt(property?.row_size) ?? 0);
|
|
285
|
+
const columnSize = Math.max(1, toPositiveInt(property?.column_size) ?? 0);
|
|
286
|
+
const expectedByProperty = rowSize > 0 && columnSize > 0 ? rowSize * columnSize : 0;
|
|
287
|
+
const expectedByChildren = tableNode.children.filter((item) => item.blockType === 32).length;
|
|
288
|
+
return Math.max(expectedByProperty, expectedByChildren);
|
|
289
|
+
}
|
|
290
|
+
export function extractTextElementsForPatchFromRawBlock(rawBlock) {
|
|
291
|
+
const blockType = typeof rawBlock.block_type === 'number' ? rawBlock.block_type : Number.NaN;
|
|
292
|
+
if (!Number.isFinite(blockType))
|
|
293
|
+
return null;
|
|
294
|
+
const payloadKey = getTextualPayloadKey(blockType);
|
|
295
|
+
if (!payloadKey)
|
|
296
|
+
return null;
|
|
297
|
+
const textPayload = toObjectRecord(rawBlock[payloadKey]);
|
|
298
|
+
if (!textPayload)
|
|
299
|
+
return null;
|
|
300
|
+
const rawElements = Array.isArray(textPayload.elements) ? deepClone(textPayload.elements) : [];
|
|
301
|
+
sanitizeTextElementsForCreate(rawElements);
|
|
302
|
+
if (rawElements.length > 0) {
|
|
303
|
+
return rawElements;
|
|
304
|
+
}
|
|
305
|
+
return [{ text_run: { content: ' ' } }];
|
|
306
|
+
}
|
|
307
|
+
export function getSourceBlockId(node, rawBlockRecord) {
|
|
308
|
+
return typeof rawBlockRecord?.block_id === 'string' && rawBlockRecord.block_id.trim().length > 0
|
|
309
|
+
? rawBlockRecord.block_id
|
|
310
|
+
: node.blockId;
|
|
311
|
+
}
|
|
312
|
+
function isBatchSafeBlockType(blockType) {
|
|
313
|
+
if (blockType === 1 || blockType === 23 || blockType === 27 || blockType === 31 || blockType === 32 || blockType === 43) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
export function createRenderBatchEntry(node) {
|
|
319
|
+
if (!isBatchSafeBlockType(node.blockType))
|
|
320
|
+
return null;
|
|
321
|
+
const rawBlockRecord = toObjectRecord(node.rawBlock);
|
|
322
|
+
if (!rawBlockRecord)
|
|
323
|
+
return null;
|
|
324
|
+
let createPayload = null;
|
|
325
|
+
try {
|
|
326
|
+
createPayload = buildCreatePayloadFromRawBlock(rawBlockRecord);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (!createPayload)
|
|
332
|
+
return null;
|
|
333
|
+
return {
|
|
334
|
+
node,
|
|
335
|
+
createPayload,
|
|
336
|
+
sourceBlockId: getSourceBlockId(node, rawBlockRecord),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createBoardPlantumlNode, getDocumentBlockById, getRawDocumentBlockById, isRelationMismatchError, replaceFileBlock, replaceImageBlock, uploadBinaryToNode, } from './ops.js';
|
|
2
|
+
import { toObjectRecord } from './render-payload.js';
|
|
3
|
+
function extractWhiteboardId(rawBlock) {
|
|
4
|
+
const board = toObjectRecord(rawBlock?.board);
|
|
5
|
+
if (!board)
|
|
6
|
+
return '';
|
|
7
|
+
const candidates = [board.token, board.whiteboard_id, board.board_token, board.id];
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
10
|
+
return candidate.trim();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
export async function applyCreatedBoardMermaid(client, documentId, createdBlockId, mermaidPatch, mermaidRenderConfig, authOptions, docxLimiter) {
|
|
16
|
+
const createdRawBlock = await getRawDocumentBlockById(client, documentId, createdBlockId, authOptions, docxLimiter);
|
|
17
|
+
const whiteboardId = extractWhiteboardId(createdRawBlock);
|
|
18
|
+
if (!whiteboardId) {
|
|
19
|
+
throw new Error(`Unable to resolve whiteboard id from created board block "${createdBlockId}".`);
|
|
20
|
+
}
|
|
21
|
+
await createBoardPlantumlNode(client, whiteboardId, mermaidPatch.code, mermaidRenderConfig.board, authOptions, docxLimiter);
|
|
22
|
+
}
|
|
23
|
+
export async function applyCreatedImageBlock(client, documentId, createdBlockId, sourceBlockId, rawBlockRecord, authOptions, docxLimiter, mediaLimiter) {
|
|
24
|
+
const image = toObjectRecord(rawBlockRecord.image);
|
|
25
|
+
const localPath = image && typeof image.local_path === 'string' ? image.local_path : '';
|
|
26
|
+
if (!localPath)
|
|
27
|
+
return null;
|
|
28
|
+
let imageToken = await uploadBinaryToNode(client, 'docx_image', createdBlockId, localPath, authOptions, mediaLimiter);
|
|
29
|
+
try {
|
|
30
|
+
await replaceImageBlock(client, documentId, createdBlockId, imageToken, authOptions, docxLimiter);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (!isRelationMismatchError(error)) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
imageToken = await uploadBinaryToNode(client, 'docx_image', createdBlockId, localPath, authOptions, mediaLimiter);
|
|
37
|
+
await replaceImageBlock(client, documentId, createdBlockId, imageToken, authOptions, docxLimiter);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
kind: 'image',
|
|
41
|
+
sourceBlockId,
|
|
42
|
+
createdBlockId,
|
|
43
|
+
localPath,
|
|
44
|
+
token: imageToken,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function resolveFileTargetBlockId(client, documentId, createdBlockId, createdBlock, authOptions, docxLimiter) {
|
|
48
|
+
let fileTargetBlockId = Array.isArray(createdBlock?.children) &&
|
|
49
|
+
typeof createdBlock.children[0] === 'string' &&
|
|
50
|
+
createdBlock.children[0].trim()
|
|
51
|
+
? createdBlock.children[0].trim()
|
|
52
|
+
: '';
|
|
53
|
+
if (!fileTargetBlockId) {
|
|
54
|
+
const fetchedCreatedBlock = await getDocumentBlockById(client, documentId, createdBlockId, authOptions, docxLimiter);
|
|
55
|
+
fileTargetBlockId =
|
|
56
|
+
fetchedCreatedBlock &&
|
|
57
|
+
Array.isArray(fetchedCreatedBlock.children) &&
|
|
58
|
+
typeof fetchedCreatedBlock.children[0] === 'string' &&
|
|
59
|
+
fetchedCreatedBlock.children[0].trim()
|
|
60
|
+
? fetchedCreatedBlock.children[0].trim()
|
|
61
|
+
: createdBlockId;
|
|
62
|
+
}
|
|
63
|
+
return fileTargetBlockId;
|
|
64
|
+
}
|
|
65
|
+
export async function applyCreatedFileBlock(client, documentId, createdBlockId, createdBlock, sourceBlockId, rawBlockRecord, authOptions, docxLimiter, mediaLimiter) {
|
|
66
|
+
const file = toObjectRecord(rawBlockRecord.file);
|
|
67
|
+
const localPath = file && typeof file.local_path === 'string' ? file.local_path : '';
|
|
68
|
+
const existingToken = file && typeof file.file_token === 'string' && file.file_token.trim().length > 0
|
|
69
|
+
? file.file_token.trim()
|
|
70
|
+
: file && typeof file.token === 'string' && file.token.trim().length > 0
|
|
71
|
+
? file.token.trim()
|
|
72
|
+
: '';
|
|
73
|
+
const fileTargetBlockId = await resolveFileTargetBlockId(client, documentId, createdBlockId, createdBlock, authOptions, docxLimiter);
|
|
74
|
+
if (localPath) {
|
|
75
|
+
let fileToken = await uploadBinaryToNode(client, 'docx_file', fileTargetBlockId, localPath, authOptions, mediaLimiter);
|
|
76
|
+
try {
|
|
77
|
+
await replaceFileBlock(client, documentId, fileTargetBlockId, fileToken, authOptions, docxLimiter);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!isRelationMismatchError(error)) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
fileToken = await uploadBinaryToNode(client, 'docx_file', fileTargetBlockId, localPath, authOptions, mediaLimiter);
|
|
84
|
+
await replaceFileBlock(client, documentId, fileTargetBlockId, fileToken, authOptions, docxLimiter);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
kind: 'file',
|
|
88
|
+
sourceBlockId,
|
|
89
|
+
createdBlockId: fileTargetBlockId,
|
|
90
|
+
localPath,
|
|
91
|
+
token: fileToken,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (existingToken) {
|
|
95
|
+
await replaceFileBlock(client, documentId, fileTargetBlockId, existingToken, authOptions, docxLimiter);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { clearBlockChildrenByKnownCount, getDocumentBlockById, listAllDocumentBlocks, patchTextBlockElements, resolveCreatedTableCellIds, } from './ops.js';
|
|
2
|
+
import { canUseElementsOnlyPatch, extractTextAlignForPatchFromRawBlock, extractTextElementsForPatchFromRawBlock, getExpectedTableCellCount, toObjectRecord, } from './render-payload.js';
|
|
3
|
+
function isNoChildrenDeleteError(error) {
|
|
4
|
+
if (!(error instanceof Error))
|
|
5
|
+
return false;
|
|
6
|
+
return /1770001|invalid param|start_index|end_index|out of range|no child/i.test(error.message);
|
|
7
|
+
}
|
|
8
|
+
async function deleteFirstChildIfPresent(client, documentId, blockId, authOptions, limiter) {
|
|
9
|
+
try {
|
|
10
|
+
await clearBlockChildrenByKnownCount(client, documentId, blockId, 1, authOptions, limiter);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (isNoChildrenDeleteError(error)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function resolveCellTextBlockId(client, documentId, createdCellId, authOptions, docxLimiter, blockById, ensureBlockMapFromList) {
|
|
20
|
+
let textBlockId = Array.isArray(blockById.get(createdCellId)?.children) && blockById.get(createdCellId)?.children?.[0]
|
|
21
|
+
? (blockById.get(createdCellId)?.children?.[0] ?? '')
|
|
22
|
+
: '';
|
|
23
|
+
if (!textBlockId) {
|
|
24
|
+
await ensureBlockMapFromList();
|
|
25
|
+
textBlockId =
|
|
26
|
+
Array.isArray(blockById.get(createdCellId)?.children) && blockById.get(createdCellId)?.children?.[0]
|
|
27
|
+
? (blockById.get(createdCellId)?.children?.[0] ?? '')
|
|
28
|
+
: '';
|
|
29
|
+
}
|
|
30
|
+
if (!textBlockId) {
|
|
31
|
+
const fetchedCell = await getDocumentBlockById(client, documentId, createdCellId, authOptions, docxLimiter);
|
|
32
|
+
if (fetchedCell) {
|
|
33
|
+
blockById.set(createdCellId, fetchedCell);
|
|
34
|
+
}
|
|
35
|
+
textBlockId =
|
|
36
|
+
fetchedCell && Array.isArray(fetchedCell.children) && fetchedCell.children[0] ? fetchedCell.children[0] : '';
|
|
37
|
+
}
|
|
38
|
+
return textBlockId;
|
|
39
|
+
}
|
|
40
|
+
export async function renderCreatedTableNode(params) {
|
|
41
|
+
const { client, documentId, createdBlockId, createdBlock, node, authOptions, docxLimiter, renderChildren } = params;
|
|
42
|
+
const sourceCellNodes = node.children.filter((item) => item.blockType === 32);
|
|
43
|
+
const nonCellNodes = node.children.filter((item) => item.blockType !== 32);
|
|
44
|
+
const expectedCellCount = getExpectedTableCellCount(node);
|
|
45
|
+
const resolvedCellIds = await resolveCreatedTableCellIds(client, documentId, createdBlockId, createdBlock, Math.max(expectedCellCount, sourceCellNodes.length), authOptions, docxLimiter);
|
|
46
|
+
const blockById = new Map(createdBlock ? [[createdBlock.block_id, createdBlock]] : []);
|
|
47
|
+
let listHydrated = false;
|
|
48
|
+
const ensureBlockMapFromList = async () => {
|
|
49
|
+
if (listHydrated)
|
|
50
|
+
return;
|
|
51
|
+
const listed = await listAllDocumentBlocks(client, documentId, authOptions, docxLimiter);
|
|
52
|
+
for (const entry of listed) {
|
|
53
|
+
blockById.set(entry.block_id, entry);
|
|
54
|
+
}
|
|
55
|
+
listHydrated = true;
|
|
56
|
+
};
|
|
57
|
+
for (let i = 0; i < sourceCellNodes.length; i += 1) {
|
|
58
|
+
const sourceCellNode = sourceCellNodes[i];
|
|
59
|
+
const createdCellId = resolvedCellIds[i];
|
|
60
|
+
if (!sourceCellNode || !createdCellId)
|
|
61
|
+
continue;
|
|
62
|
+
let consumedByPatch = false;
|
|
63
|
+
if (sourceCellNode.children.length === 1) {
|
|
64
|
+
const onlySourceChild = sourceCellNode.children[0];
|
|
65
|
+
const sourceRaw = toObjectRecord(onlySourceChild?.rawBlock);
|
|
66
|
+
if (sourceRaw && canUseElementsOnlyPatch(sourceRaw)) {
|
|
67
|
+
const elements = extractTextElementsForPatchFromRawBlock(sourceRaw);
|
|
68
|
+
if (elements) {
|
|
69
|
+
const textBlockId = await resolveCellTextBlockId(client, documentId, createdCellId, authOptions, docxLimiter, blockById, ensureBlockMapFromList);
|
|
70
|
+
if (textBlockId) {
|
|
71
|
+
const align = extractTextAlignForPatchFromRawBlock(sourceRaw);
|
|
72
|
+
await patchTextBlockElements(client, documentId, textBlockId, elements, authOptions, docxLimiter, align);
|
|
73
|
+
consumedByPatch = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (consumedByPatch) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (sourceCellNode.children.length > 0) {
|
|
82
|
+
await deleteFirstChildIfPresent(client, documentId, createdCellId, authOptions, docxLimiter);
|
|
83
|
+
}
|
|
84
|
+
await renderChildren(createdCellId, sourceCellNode.children);
|
|
85
|
+
}
|
|
86
|
+
await renderChildren(createdBlockId, nonCellNodes);
|
|
87
|
+
}
|