zentao-api 0.6.2 → 0.6.3

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.
@@ -0,0 +1,168 @@
1
+ import { deltaFrom, escapeMarkdownMath, escapeHtml, escapeHtmlAttribute, isRecord, resolveDocumentUrl, safeCssColor, safeFontSize, sanitizeUrl, toRecord, toStringValue, } from './shared.js';
2
+ function escapeMarkdownText(value) {
3
+ return value
4
+ .replace(/\\/g, '\\\\')
5
+ .replace(/([`*_[\]<>|~])/g, '\\$1')
6
+ .replace(/(^|\n)([ \t]*)([#>+-]|\d+[.)])(?=\s)/g, '$1$2\\$3');
7
+ }
8
+ function markdownUrl(url) {
9
+ return url
10
+ .replace(/\\/g, '%5C')
11
+ .replace(/ /g, '%20')
12
+ .replace(/\(/g, '%28')
13
+ .replace(/\)/g, '%29')
14
+ .replace(/</g, '%3C')
15
+ .replace(/>/g, '%3E');
16
+ }
17
+ function markdownInlineCode(value) {
18
+ const text = value.replace(/\n/g, ' ');
19
+ const runs = text.match(/`+/g) ?? [];
20
+ const fence = '`'.repeat(Math.max(1, ...runs.map(run => run.length + 1)));
21
+ const needsPadding = text.startsWith('`') ||
22
+ text.endsWith('`') ||
23
+ (text.startsWith(' ') && text.endsWith(' ') && text.trim().length > 0);
24
+ return `${fence}${needsPadding ? ' ' : ''}${text}${needsPadding ? ' ' : ''}${fence}`;
25
+ }
26
+ function mentionText(delta) {
27
+ const mention = toRecord(delta.attributes?.mention);
28
+ if (!Object.keys(mention).length)
29
+ return undefined;
30
+ const label = toStringValue(mention.label) || toStringValue(mention.name);
31
+ return label ? `@${label}` : undefined;
32
+ }
33
+ function referenceFrom(delta) {
34
+ const reference = toRecord(delta.attributes?.reference);
35
+ const pageId = toStringValue(reference.pageId);
36
+ if (!pageId)
37
+ return undefined;
38
+ return {
39
+ pageId,
40
+ params: toRecord(reference.params),
41
+ };
42
+ }
43
+ function renderMarkdownDelta(delta, options, block) {
44
+ if (typeof delta.insert !== 'string')
45
+ return '';
46
+ const attributes = toRecord(delta.attributes);
47
+ const mention = mentionText(delta);
48
+ const latex = toStringValue(attributes.latex);
49
+ const reference = referenceFrom(delta);
50
+ const rawLink = toStringValue(attributes.link);
51
+ // The existing Markdown adapter has no holder matcher and therefore keeps
52
+ // the underlying insert (normally a single space). HTML has a dedicated
53
+ // readable holder representation below.
54
+ let raw = mention ?? delta.insert;
55
+ const linkIsPlainText = !reference && (raw === '' || raw === rawLink);
56
+ if (!reference && raw === '' && rawLink)
57
+ raw = rawLink;
58
+ let referenceTitle;
59
+ if (reference) {
60
+ referenceTitle = options.resolveDocTitle?.(reference.pageId);
61
+ raw = referenceTitle || raw.trim() || reference.pageId;
62
+ }
63
+ if (latex)
64
+ return `$${escapeMarkdownMath(latex)}$`;
65
+ let output = attributes.code
66
+ ? markdownInlineCode(raw)
67
+ : escapeMarkdownText(raw);
68
+ if (attributes.bold)
69
+ output = `**${output}**`;
70
+ if (attributes.italic)
71
+ output = `*${output}*`;
72
+ if (attributes.strike)
73
+ output = `~~${output}~~`;
74
+ // This mirrors the 0.19.5 adapter's readable, escaped HTML fallback.
75
+ if (attributes.underline)
76
+ output = `\\<u>${output}\\</u>`;
77
+ let url = sanitizeUrl(rawLink);
78
+ if (reference) {
79
+ url = resolveDocumentUrl(options, { ...reference, title: referenceTitle }, block);
80
+ }
81
+ if (url) {
82
+ if (!linkIsPlainText)
83
+ output = `[${output}](${markdownUrl(url)})`;
84
+ }
85
+ return output;
86
+ }
87
+ export function deltaToMarkdown(value, options, block) {
88
+ return deltaFrom(value)
89
+ .map(delta => renderMarkdownDelta(delta, options, block))
90
+ .join('');
91
+ }
92
+ function htmlDataAttributes(data) {
93
+ if (!isRecord(data))
94
+ return '';
95
+ return ` data-holder-data="${escapeHtmlAttribute(JSON.stringify(data))}"`;
96
+ }
97
+ function renderHtmlDelta(delta, options, block) {
98
+ if (typeof delta.insert !== 'string')
99
+ return '';
100
+ const attributes = toRecord(delta.attributes);
101
+ const holder = toRecord(attributes.holder);
102
+ if (Object.keys(holder).length) {
103
+ const text = toStringValue(holder.text) || toStringValue(holder.name) || delta.insert;
104
+ const id = toStringValue(holder.id);
105
+ const name = toStringValue(holder.name);
106
+ const hint = toStringValue(holder.hint);
107
+ return `<code class="affine-zui-holder"${id ? ` data-holder-id="${escapeHtmlAttribute(id)}"` : ''}${name ? ` data-holder-name="${escapeHtmlAttribute(name)}"` : ''}${hint ? ` title="${escapeHtmlAttribute(hint)}"` : ''}${htmlDataAttributes(holder.data)}>${escapeHtml(text)}</code>`;
108
+ }
109
+ const mention = toRecord(attributes.mention);
110
+ if (Object.keys(mention).length) {
111
+ const label = toStringValue(mention.label) || toStringValue(mention.name);
112
+ const id = toStringValue(mention.id);
113
+ const type = toStringValue(mention.type);
114
+ return `<span class="affine-zui-mention-label"${id ? ` data-id="${escapeHtmlAttribute(id)}"` : ''}${type ? ` data-type="${escapeHtmlAttribute(type)}"` : ''}>@${escapeHtml(label)}</span>`;
115
+ }
116
+ const latex = toStringValue(attributes.latex);
117
+ if (latex) {
118
+ return `<span class="affine-inline-latex" data-latex="${escapeHtmlAttribute(latex)}">${escapeHtml(latex)}</span>`;
119
+ }
120
+ const reference = referenceFrom(delta);
121
+ const visibleText = reference
122
+ ? options.resolveDocTitle?.(reference.pageId) ||
123
+ delta.insert.trim() ||
124
+ reference.pageId
125
+ : delta.insert;
126
+ let output = escapeHtml(visibleText).replace(/\n/g, '<br>');
127
+ if (attributes.bold)
128
+ output = `<strong>${output}</strong>`;
129
+ if (attributes.italic)
130
+ output = `<em>${output}</em>`;
131
+ if (attributes.strike)
132
+ output = `<del>${output}</del>`;
133
+ if (attributes.underline)
134
+ output = `<u>${output}</u>`;
135
+ if (attributes.code)
136
+ output = `<code>${output}</code>`;
137
+ if (attributes.sub)
138
+ output = `<sub>${output}</sub>`;
139
+ if (attributes.sup)
140
+ output = `<sup>${output}</sup>`;
141
+ const styles = [];
142
+ const color = safeCssColor(attributes.color);
143
+ const background = safeCssColor(attributes.background);
144
+ const fontSize = safeFontSize(attributes.fontSize);
145
+ if (color)
146
+ styles.push(`color:${color}`);
147
+ if (background)
148
+ styles.push(`background-color:${background}`);
149
+ if (fontSize)
150
+ styles.push(`font-size:${fontSize}`);
151
+ if (styles.length)
152
+ output = `<span style="${styles.join(';')}">${output}</span>`;
153
+ let url = sanitizeUrl(attributes.link);
154
+ if (reference) {
155
+ url = resolveDocumentUrl(options, {
156
+ ...reference,
157
+ title: options.resolveDocTitle?.(reference.pageId),
158
+ }, block);
159
+ }
160
+ return url
161
+ ? `<a href="${escapeHtmlAttribute(url)}">${output}</a>`
162
+ : output;
163
+ }
164
+ export function deltaToHtml(value, options, block) {
165
+ return deltaFrom(value)
166
+ .map(delta => renderHtmlDelta(delta, options, block))
167
+ .join('');
168
+ }
@@ -0,0 +1,3 @@
1
+ import type { MarkdownOptions, SnapshotInput } from './types.js';
2
+ /** Convert a BlockSuite 0.19.x snapshot to Markdown without a BlockSuite runtime. */
3
+ export declare function snapshotToMarkdown(input: SnapshotInput, options?: MarkdownOptions): string;
@@ -0,0 +1,402 @@
1
+ import { deltaToMarkdown } from './inline.js';
2
+ import { blockChildren, escapeMarkdownMath, formatFileSize, isEdgelessOnly, isRecord, normalizeSnapshot, pageTitleDelta, plainText, resolveAssetUrl, resolveDocumentUrl, resolveZuiImageUrl, safeLanguage, sanitizeUrl, toRecord, toStringValue, validateSnapshotTree, } from './shared.js';
3
+ const TRANSPARENT_FLAVOURS = new Set([
4
+ 'affine:surface',
5
+ 'affine:zui-layout',
6
+ 'affine:zui-layout-cell',
7
+ 'affine:zui-panel',
8
+ ]);
9
+ const EMBED_LINK_FLAVOURS = new Set([
10
+ 'affine:bookmark',
11
+ 'affine:embed-figma',
12
+ 'affine:embed-github',
13
+ 'affine:embed-loom',
14
+ 'affine:embed-youtube',
15
+ ]);
16
+ function trimBlock(value) {
17
+ return value.replace(/^\n+|\n+$/g, '');
18
+ }
19
+ function trimProse(value) {
20
+ return value.replace(/[ \t]+$/gm, whitespace => whitespace.length >= 2 ? whitespace : '');
21
+ }
22
+ function joinBlocks(blocks) {
23
+ return blocks
24
+ .map(trimBlock)
25
+ .filter(Boolean)
26
+ .join('\n\n');
27
+ }
28
+ function escapeTableCell(value) {
29
+ return value.replace(/(^|[^\\])\|/g, '$1\\|').replace(/\r?\n/g, '<br>');
30
+ }
31
+ function markdownText(value, state, block) {
32
+ return deltaToMarkdown(toStringValue(value), state.options, block);
33
+ }
34
+ function markdownUrl(value, kind = 'link') {
35
+ return sanitizeUrl(value, kind)
36
+ .replace(/\\/g, '%5C')
37
+ .replace(/ /g, '%20')
38
+ .replace(/\(/g, '%28')
39
+ .replace(/\)/g, '%29')
40
+ .replace(/</g, '%3C')
41
+ .replace(/>/g, '%3E');
42
+ }
43
+ function renderTable(rows) {
44
+ if (!rows.length)
45
+ return '';
46
+ const columnCount = Math.max(...rows.map(row => row.length), 0);
47
+ if (!columnCount)
48
+ return '';
49
+ const normalized = rows.map(row => Array.from({ length: columnCount }, (_, index) => escapeTableCell(row[index] ?? '')));
50
+ const widths = Array.from({ length: columnCount }, (_, index) => Math.max(3, ...normalized.map(row => row[index]?.length ?? 0)));
51
+ const line = (row) => `| ${row
52
+ .map((cell, index) => cell.padEnd(widths[index] ?? 3))
53
+ .join(' | ')} |`;
54
+ const header = normalized[0] ?? [];
55
+ const separator = widths.map(width => '-'.repeat(width));
56
+ return [line(header), line(separator), ...normalized.slice(1).map(line)].join('\n');
57
+ }
58
+ function codeFence(text) {
59
+ const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map(run => run.length));
60
+ return '`'.repeat(Math.max(3, longest + 1));
61
+ }
62
+ function indentLines(value, size) {
63
+ const indent = ' '.repeat(size);
64
+ return value
65
+ .split('\n')
66
+ .map(line => (line ? `${indent}${line}` : line))
67
+ .join('\n');
68
+ }
69
+ function indentNestedBlocks(value) {
70
+ const entity = '&#x20;';
71
+ return trimBlock(value)
72
+ .split('\n')
73
+ .map(line => {
74
+ if (!line)
75
+ return line;
76
+ return line.startsWith(entity)
77
+ ? `${entity} ${line.slice(entity.length)}`
78
+ : `${entity} ${line}`;
79
+ })
80
+ .join('\n');
81
+ }
82
+ function renderListGroup(items, state) {
83
+ const type = toStringValue(toRecord(items[0]?.props).type, 'bulleted');
84
+ return items
85
+ .map((item, index) => {
86
+ const props = toRecord(item.props);
87
+ const marker = type === 'numbered' ? `${index + 1}.` : '*';
88
+ const task = type === 'todo' ? `[${props.checked ? 'x' : ' '}] ` : '';
89
+ const text = trimProse(deltaToMarkdown(props.text, state.options, item));
90
+ const lines = text.split('\n');
91
+ let output = `${marker} ${task}${lines[0] ?? ''}`;
92
+ const continuationIndent = ' '.repeat(marker.length + 1);
93
+ if (lines.length > 1) {
94
+ output += `\n${lines
95
+ .slice(1)
96
+ .map(line => `${continuationIndent}${line}`)
97
+ .join('\n')}`;
98
+ }
99
+ const children = renderBlocks(blockChildren(item), state);
100
+ if (children)
101
+ output += `\n${indentLines(children, marker.length + 1)}`;
102
+ return output;
103
+ })
104
+ .join('\n');
105
+ }
106
+ function optionLabel(column, value) {
107
+ const data = toRecord(column.data);
108
+ const options = Array.isArray(data.options)
109
+ ? data.options.filter(isRecord)
110
+ : [];
111
+ return toStringValue(options.find(option => option.id === value)?.value);
112
+ }
113
+ function databaseCell(row, column, cells, state) {
114
+ const columnId = toStringValue(column.id);
115
+ const rowCells = toRecord(cells[toStringValue(row.id)]);
116
+ const cell = toRecord(rowCells[columnId]);
117
+ const value = cell.value;
118
+ switch (column.type) {
119
+ case 'title':
120
+ return deltaToMarkdown(toRecord(row.props).text, state.options, row);
121
+ case 'rich-text':
122
+ return deltaToMarkdown(value, state.options, row);
123
+ case 'date': {
124
+ const date = new Date(typeof value === 'number' ? value : Number.NaN);
125
+ if (Number.isNaN(date.getTime()))
126
+ return '';
127
+ const year = date.getFullYear();
128
+ const month = String(date.getMonth() + 1).padStart(2, '0');
129
+ const day = String(date.getDate()).padStart(2, '0');
130
+ return `${year}-${month}-${day}`;
131
+ }
132
+ case 'select':
133
+ return markdownText(optionLabel(column, value), state, row);
134
+ case 'multi-select':
135
+ return markdownText(Array.isArray(value)
136
+ ? value.map(item => optionLabel(column, item)).filter(Boolean).join(',')
137
+ : '', state, row);
138
+ case 'checkbox':
139
+ return markdownText(value === undefined ? '' : String(value), state, row);
140
+ default:
141
+ return markdownText(value === undefined || value === null ? '' : String(value), state, row);
142
+ }
143
+ }
144
+ function renderDatabase(block, state) {
145
+ const props = toRecord(block.props);
146
+ const columns = Array.isArray(props.columns)
147
+ ? props.columns.filter(isRecord)
148
+ : [];
149
+ const cells = toRecord(props.cells);
150
+ const rows = blockChildren(block);
151
+ const tableRows = [
152
+ columns.map(column => markdownText(column.name, state, block)),
153
+ ...rows.map(row => columns.map(column => databaseCell(row, column, cells, state))),
154
+ ];
155
+ const title = deltaToMarkdown(props.title, state.options, block);
156
+ return joinBlocks([title ? `##### ${title}` : '', renderTable(tableRows)]);
157
+ }
158
+ function renderZuiTable(block, state) {
159
+ const rows = Array.isArray(toRecord(block.props).rows)
160
+ ? toRecord(block.props).rows.filter(isRecord)
161
+ : [];
162
+ return renderTable(rows.map(row => {
163
+ const cells = Array.isArray(row.cells) ? row.cells.filter(isRecord) : [];
164
+ return cells.map(cell => deltaToMarkdown(cell.text, state.options, block));
165
+ }));
166
+ }
167
+ function renderCustomExportNodes(nodes, state) {
168
+ const rendered = nodes.filter(isRecord).map(node => {
169
+ const props = toRecord(node.props);
170
+ if (node.type === 'heading') {
171
+ const depth = Math.min(6, Math.max(1, typeof props.depth === 'number' ? props.depth : 2));
172
+ return `${'#'.repeat(depth)} ${markdownText(props.text, state)}`;
173
+ }
174
+ if (node.type === 'table') {
175
+ const columns = Array.isArray(props.cols)
176
+ ? props.cols.filter(isRecord)
177
+ : [];
178
+ const data = Array.isArray(props.data) ? props.data.filter(isRecord) : [];
179
+ return renderTable([
180
+ columns.map(column => markdownText(column.text ?? column.name, state)),
181
+ ...data.map(row => columns.map(column => {
182
+ const cell = toRecord(row[toStringValue(column.name)]);
183
+ return markdownText(cell.text, state);
184
+ })),
185
+ ]);
186
+ }
187
+ if (node.type === 'link') {
188
+ const url = markdownUrl(props.href);
189
+ const label = markdownText(props.text, state) || url;
190
+ return url ? `[${label}](${url})` : label;
191
+ }
192
+ return markdownText(props.text, state);
193
+ });
194
+ return joinBlocks(rendered);
195
+ }
196
+ function renderCustomBlock(block, state) {
197
+ const contentValue = toRecord(block.props).content;
198
+ if (typeof contentValue === 'string') {
199
+ return markdownText(contentValue, state, block);
200
+ }
201
+ const content = toRecord(contentValue);
202
+ const html = toStringValue(content.html);
203
+ if (html) {
204
+ const fence = codeFence(html);
205
+ return `${fence}html\n${html}\n${fence}`;
206
+ }
207
+ const exported = Array.isArray(content.export) ? content.export : [];
208
+ if (exported.length)
209
+ return renderCustomExportNodes(exported, state);
210
+ const title = markdownText(content.title, state, block);
211
+ const fetcher = content.fetcher;
212
+ const fetcherUrl = typeof fetcher === 'string' ? fetcher : toRecord(fetcher).url;
213
+ const url = markdownUrl(content.exportUrl) || markdownUrl(fetcherUrl);
214
+ const component = markdownText(content.component, state, block);
215
+ return joinBlocks([
216
+ title ? `##### ${title}` : '',
217
+ component ? `[Component: ${component}]` : '',
218
+ url ? `[${url}](${url})` : '',
219
+ ]);
220
+ }
221
+ function renderEmbedLink(block, state) {
222
+ const props = toRecord(block.props);
223
+ const url = markdownUrl(props.url);
224
+ if (!url)
225
+ return '';
226
+ const title = markdownText(props.title, state, block) || url;
227
+ return `[${title}](${url})`;
228
+ }
229
+ function customRenderer(block, state) {
230
+ const renderer = state.options.renderBlock;
231
+ if (!renderer)
232
+ return undefined;
233
+ const context = {
234
+ format: 'markdown',
235
+ renderChildren: children => renderBlocks(children ?? blockChildren(block), state),
236
+ renderInline: text => deltaToMarkdown(text, state.options, block),
237
+ };
238
+ return renderer(block, context);
239
+ }
240
+ function renderUnknown(block, state) {
241
+ const strategy = state.options.unknownBlock ?? 'children';
242
+ if (strategy === 'throw') {
243
+ throw new TypeError(`Unsupported BlockSuite block flavour: ${block.flavour}`);
244
+ }
245
+ return strategy === 'children' ? renderBlocks(blockChildren(block), state) : '';
246
+ }
247
+ function renderBlock(block, state) {
248
+ const overridden = customRenderer(block, state);
249
+ if (overridden !== undefined)
250
+ return overridden;
251
+ const props = toRecord(block.props);
252
+ if (block.flavour === 'affine:page') {
253
+ return renderBlocks(blockChildren(block), state);
254
+ }
255
+ if (block.flavour === 'affine:note') {
256
+ return isEdgelessOnly(block) ? '' : renderBlocks(blockChildren(block), state);
257
+ }
258
+ if (TRANSPARENT_FLAVOURS.has(block.flavour)) {
259
+ return renderBlocks(blockChildren(block), state);
260
+ }
261
+ switch (block.flavour) {
262
+ case 'affine:paragraph': {
263
+ const text = trimProse(deltaToMarkdown(props.text, state.options, block));
264
+ const type = toStringValue(props.type, 'text');
265
+ let own = text;
266
+ if (/^h[1-6]$/.test(type)) {
267
+ own = `${'#'.repeat(Number(type[1]))} ${text}`;
268
+ }
269
+ else if (type === 'quote') {
270
+ own = text
271
+ .split('\n')
272
+ .map(line => `> ${line}`.trimEnd())
273
+ .join('\n');
274
+ }
275
+ const children = renderBlocks(blockChildren(block), state);
276
+ return joinBlocks([own, children ? indentNestedBlocks(children) : '']);
277
+ }
278
+ case 'affine:code': {
279
+ const text = plainText(props.text);
280
+ const fence = codeFence(text);
281
+ const language = safeLanguage(props.language);
282
+ return `${fence}${language}\n${text}\n${fence}`;
283
+ }
284
+ case 'affine:divider':
285
+ return '***';
286
+ case 'affine:latex':
287
+ return `$$\n${escapeMarkdownMath(props.latex)}\n$$`;
288
+ case 'affine:image': {
289
+ const url = resolveAssetUrl(state.options, block, 'image');
290
+ if (!url)
291
+ return '';
292
+ const caption = deltaToMarkdown(toStringValue(props.caption), state.options, block);
293
+ return `![${caption}](${markdownUrl(url, 'image')})`;
294
+ }
295
+ case 'affine:zui-image': {
296
+ const url = resolveZuiImageUrl(state.options, block);
297
+ const caption = deltaToMarkdown(toStringValue(props.caption), state.options, block);
298
+ return url ? `![${caption}](${markdownUrl(url, 'image')})` : '';
299
+ }
300
+ case 'affine:attachment': {
301
+ const name = markdownText(toStringValue(props.name, 'Attachment'), state, block);
302
+ const url = resolveAssetUrl(state.options, block, 'attachment');
303
+ const label = `**${name}**`;
304
+ const attachment = url ? `[${label}](${markdownUrl(url)})` : label;
305
+ const caption = markdownText(props.caption, state, block);
306
+ return `${attachment} (${formatFileSize(props.size)}${caption ? `, ${caption}` : ''})`;
307
+ }
308
+ case 'affine:database':
309
+ return renderDatabase(block, state);
310
+ case 'affine:zui-table':
311
+ return renderZuiTable(block, state);
312
+ case 'affine:zui-holder':
313
+ return renderBlocks(blockChildren(block), state);
314
+ case 'affine:zui-expand':
315
+ return renderBlocks(blockChildren(block), state);
316
+ case 'affine:embed-zui-whiteboard': {
317
+ const image = sanitizeUrl(props.sceneImage, 'image');
318
+ if (image) {
319
+ const caption = markdownText(toStringValue(props.caption, 'whiteboard'), state, block);
320
+ return `![${caption}](${markdownUrl(image, 'image')})`;
321
+ }
322
+ return props.sceneData ? '[Whiteboard]' : '[Empty Whiteboard]';
323
+ }
324
+ case 'affine:embed-zui-custom':
325
+ return renderCustomBlock(block, state);
326
+ case 'affine:embed-zui-html': {
327
+ const html = toStringValue(props.html);
328
+ return html ? `${codeFence(html)}html\n${html}\n${codeFence(html)}` : '';
329
+ }
330
+ case 'affine:embed-zui-iframe': {
331
+ const url = markdownUrl(props.src);
332
+ return url ? `[${url}](${url})` : '';
333
+ }
334
+ case 'affine:embed-zui-component': {
335
+ const name = markdownText(props.name, state, block);
336
+ return name ? `[Component: ${name}]` : '';
337
+ }
338
+ case 'affine:embed-linked-doc': {
339
+ const pageId = toStringValue(props.pageId);
340
+ if (!pageId)
341
+ return '';
342
+ const rawTitle = state.options.resolveDocTitle?.(pageId) ?? 'untitled';
343
+ const title = markdownText(rawTitle, state, block);
344
+ const url = resolveDocumentUrl(state.options, { pageId, params: toRecord(props.params), title: rawTitle }, block);
345
+ return url ? `[${title}](${markdownUrl(url)})` : title;
346
+ }
347
+ case 'affine:embed-synced-doc': {
348
+ const children = blockChildren(block);
349
+ if (children.length)
350
+ return renderBlocks(children, state);
351
+ const pageId = toStringValue(props.pageId);
352
+ if (!pageId)
353
+ return '';
354
+ const rawTitle = state.options.resolveDocTitle?.(pageId) ?? pageId;
355
+ const title = markdownText(rawTitle, state, block);
356
+ const url = resolveDocumentUrl(state.options, { pageId, params: toRecord(props.params), title: rawTitle }, block);
357
+ return url ? `[${title}](${markdownUrl(url)})` : title;
358
+ }
359
+ default:
360
+ return EMBED_LINK_FLAVOURS.has(block.flavour)
361
+ ? renderEmbedLink(block, state)
362
+ : renderUnknown(block, state);
363
+ }
364
+ }
365
+ function renderBlocks(blocks, state) {
366
+ const rendered = [];
367
+ for (let index = 0; index < blocks.length; index += 1) {
368
+ const block = blocks[index];
369
+ if (!block)
370
+ continue;
371
+ if (block.flavour === 'affine:list') {
372
+ const type = toStringValue(toRecord(block.props).type, 'bulleted');
373
+ const group = [block];
374
+ while (index + 1 < blocks.length) {
375
+ const next = blocks[index + 1];
376
+ if (next?.flavour !== 'affine:list' ||
377
+ toStringValue(toRecord(next.props).type, 'bulleted') !== type) {
378
+ break;
379
+ }
380
+ group.push(next);
381
+ index += 1;
382
+ }
383
+ rendered.push(renderListGroup(group, state));
384
+ }
385
+ else {
386
+ rendered.push(renderBlock(block, state));
387
+ }
388
+ }
389
+ return joinBlocks(rendered);
390
+ }
391
+ /** Convert a BlockSuite 0.19.x snapshot to Markdown without a BlockSuite runtime. */
392
+ export function snapshotToMarkdown(input, options = {}) {
393
+ const normalized = normalizeSnapshot(input);
394
+ validateSnapshotTree(normalized.blocks, options);
395
+ const state = { options };
396
+ const title = options.includeTitle === false
397
+ ? ''
398
+ : deltaToMarkdown(pageTitleDelta(normalized), options, normalized.root);
399
+ const body = renderBlocks(normalized.blocks, state);
400
+ const markdown = joinBlocks([title ? `# ${title}` : '', body]);
401
+ return markdown ? `${markdown}\n` : '';
402
+ }
@@ -0,0 +1,37 @@
1
+ import type { BlockSnapshot, ConvertOptions, DeltaInsert, DocSnapshotMeta, DocumentReference, JsonRecord, SnapshotInput } from './types.js';
2
+ export interface NormalizedSnapshot {
3
+ kind: 'doc' | 'block' | 'slice';
4
+ blocks: BlockSnapshot[];
5
+ root?: BlockSnapshot;
6
+ meta?: DocSnapshotMeta;
7
+ documentLike: boolean;
8
+ }
9
+ export declare function isRecord(value: unknown): value is JsonRecord;
10
+ export declare function toRecord(value: unknown): JsonRecord;
11
+ export declare function toStringValue(value: unknown, fallback?: string): string;
12
+ export declare function toFiniteNumber(value: unknown): number | undefined;
13
+ export declare function blockChildren(block: BlockSnapshot): BlockSnapshot[];
14
+ export declare function isBlockSnapshot(value: unknown): value is BlockSnapshot;
15
+ export declare function normalizeSnapshot(input: SnapshotInput): NormalizedSnapshot;
16
+ /**
17
+ * Validate limits iteratively before recursive rendering. Besides making error
18
+ * messages deterministic, this prevents deeply nested untrusted JSON from
19
+ * exhausting the JavaScript call stack.
20
+ */
21
+ export declare function validateSnapshotTree(blocks: readonly BlockSnapshot[], options: ConvertOptions): void;
22
+ export declare function deltaFrom(value: unknown): DeltaInsert[];
23
+ export declare function plainText(value: unknown): string;
24
+ export declare function pageTitleDelta(normalized: NormalizedSnapshot): DeltaInsert[];
25
+ export declare function pageTitleText(normalized: NormalizedSnapshot): string;
26
+ export declare function escapeHtml(value: unknown): string;
27
+ export declare function escapeHtmlAttribute(value: unknown): string;
28
+ export declare function sanitizeUrl(value: unknown, kind?: 'link' | 'image'): string;
29
+ export declare function resolveAssetUrl(options: ConvertOptions, block: BlockSnapshot, kind: 'image' | 'attachment'): string;
30
+ export declare function resolveZuiImageUrl(options: ConvertOptions, block: BlockSnapshot): string;
31
+ export declare function resolveDocumentUrl(options: ConvertOptions, reference: DocumentReference, block?: BlockSnapshot): string;
32
+ export declare function formatFileSize(value: unknown): string;
33
+ export declare function isEdgelessOnly(block: BlockSnapshot): boolean;
34
+ export declare function safeLanguage(value: unknown): string;
35
+ export declare function escapeMarkdownMath(value: unknown): string;
36
+ export declare function safeCssColor(value: unknown): string;
37
+ export declare function safeFontSize(value: unknown): string;