zentao-api 0.6.4 → 0.6.6

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.
@@ -1,442 +0,0 @@
1
- import { deltaToHtml } from './inline.js';
2
- import { blockChildren, escapeHtml, escapeHtmlAttribute, formatFileSize, isEdgelessOnly, isRecord, normalizeSnapshot, pageTitleText, plainText, resolveAssetUrl, resolveDocumentUrl, resolveZuiImageUrl, safeLanguage, sanitizeUrl, toFiniteNumber, toRecord, toStringValue, validateSnapshotTree, } from './shared.js';
3
- const TRANSPARENT_FLAVOURS = new Set([
4
- 'affine:surface',
5
- 'affine:zui-layout-cell',
6
- ]);
7
- const EMBED_LINK_FLAVOURS = new Set([
8
- 'affine:bookmark',
9
- 'affine:embed-figma',
10
- 'affine:embed-github',
11
- 'affine:embed-loom',
12
- 'affine:embed-youtube',
13
- ]);
14
- function joinHtml(parts) {
15
- return parts.filter(Boolean).join('\n');
16
- }
17
- function attribute(name, value) {
18
- return value === undefined || value === null || value === ''
19
- ? ''
20
- : ` ${name}="${escapeHtmlAttribute(value)}"`;
21
- }
22
- function dataAttribute(name, value) {
23
- return isRecord(value)
24
- ? attribute(name, JSON.stringify(value))
25
- : attribute(name, value);
26
- }
27
- function positiveInteger(value) {
28
- const number = toFiniteNumber(value);
29
- return number !== undefined && number > 0
30
- ? Math.min(10000, Math.round(number))
31
- : undefined;
32
- }
33
- function safeDimension(value) {
34
- if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
35
- return String(value);
36
- }
37
- if (typeof value !== 'string')
38
- return '';
39
- const dimension = value.trim();
40
- return /^(?:\d+(?:\.\d+)?(?:px|%|em|rem|vh|vw)?|auto)$/i.test(dimension)
41
- ? dimension
42
- : '';
43
- }
44
- function safeAlign(value) {
45
- return ['left', 'center', 'right'].includes(String(value))
46
- ? String(value)
47
- : '';
48
- }
49
- function renderListGroup(items, state) {
50
- const type = toStringValue(toRecord(items[0]?.props).type, 'bulleted');
51
- const tag = type === 'numbered' ? 'ol' : 'ul';
52
- const className = `${escapeHtmlAttribute(type)}-list`;
53
- const listItems = items.map(item => {
54
- const props = toRecord(item.props);
55
- const checkbox = type === 'todo'
56
- ? `<input type="checkbox" disabled${props.checked ? ' checked' : ''}> `
57
- : '';
58
- return `<li class="affine-list-block-container">${checkbox}${deltaToHtml(props.text, state.options, item)}${renderBlocks(blockChildren(item), state)}</li>`;
59
- });
60
- return `<${tag} class="${className}">${listItems.join('')}</${tag}>`;
61
- }
62
- function optionLabel(column, value) {
63
- const data = toRecord(column.data);
64
- const options = Array.isArray(data.options)
65
- ? data.options.filter(isRecord)
66
- : [];
67
- return toStringValue(options.find(option => option.id === value)?.value);
68
- }
69
- function databaseCell(row, column, cells, state) {
70
- const columnId = toStringValue(column.id);
71
- const rowCells = toRecord(cells[toStringValue(row.id)]);
72
- const cell = toRecord(rowCells[columnId]);
73
- const value = cell.value;
74
- switch (column.type) {
75
- case 'title':
76
- return deltaToHtml(toRecord(row.props).text, state.options, row);
77
- case 'rich-text':
78
- return deltaToHtml(value, state.options, row);
79
- case 'date': {
80
- const date = new Date(typeof value === 'number' ? value : Number.NaN);
81
- if (Number.isNaN(date.getTime()))
82
- return '';
83
- const year = date.getFullYear();
84
- const month = String(date.getMonth() + 1).padStart(2, '0');
85
- const day = String(date.getDate()).padStart(2, '0');
86
- return `${year}-${month}-${day}`;
87
- }
88
- case 'select':
89
- return escapeHtml(optionLabel(column, value));
90
- case 'multi-select':
91
- return escapeHtml(Array.isArray(value)
92
- ? value.map(item => optionLabel(column, item)).filter(Boolean).join(',')
93
- : '');
94
- case 'checkbox':
95
- return value === undefined ? '' : escapeHtml(String(value));
96
- default:
97
- return value === undefined || value === null
98
- ? ''
99
- : escapeHtml(String(value));
100
- }
101
- }
102
- function renderDatabase(block, state) {
103
- const props = toRecord(block.props);
104
- const columns = Array.isArray(props.columns)
105
- ? props.columns.filter(isRecord)
106
- : [];
107
- const cells = toRecord(props.cells);
108
- const header = `<tr>${columns
109
- .map(column => `<th>${escapeHtml(toStringValue(column.name))}</th>`)
110
- .join('')}</tr>`;
111
- const body = blockChildren(block)
112
- .map(row => `<tr>${columns
113
- .map(column => `<td>${databaseCell(row, column, cells, state)}</td>`)
114
- .join('')}</tr>`)
115
- .join('');
116
- const title = deltaToHtml(props.title, state.options, block);
117
- return joinHtml([
118
- title ? `<h5>${title}</h5>` : '',
119
- `<table class="affine-database-block-container"><thead>${header}</thead><tbody>${body}</tbody></table>`,
120
- ]);
121
- }
122
- function renderZuiTable(block, state) {
123
- const props = toRecord(block.props);
124
- const rows = Array.isArray(props.rows) ? props.rows.filter(isRecord) : [];
125
- const firstRowHead = props.firstRowHead !== false;
126
- const body = rows
127
- .map((row, rowIndex) => {
128
- const cells = Array.isArray(row.cells) ? row.cells.filter(isRecord) : [];
129
- return `<tr>${cells
130
- .map(cell => {
131
- const tag = cell.head ?? (rowIndex === 0 && firstRowHead) ? 'th' : 'td';
132
- const colspan = positiveInteger(cell.colspan);
133
- const rowspan = positiveInteger(cell.rowspan);
134
- const width = safeDimension(cell.width);
135
- const align = safeAlign(cell.align);
136
- const style = align ? `text-align:${align}` : '';
137
- return `<${tag}${attribute('colspan', colspan)}${attribute('rowspan', rowspan)}${attribute('width', width)}${attribute('style', style)}>${deltaToHtml(cell.text, state.options, block)}</${tag}>`;
138
- })
139
- .join('')}</tr>`;
140
- })
141
- .join('');
142
- return `<table class="affine-table-block-container"${attribute('data-block-id', block.id)}><tbody>${body}</tbody></table>`;
143
- }
144
- function renderCustomExportNodes(nodes, state) {
145
- return nodes
146
- .filter(isRecord)
147
- .map(node => {
148
- const props = toRecord(node.props);
149
- if (node.type === 'heading') {
150
- const depth = Math.min(6, Math.max(1, typeof props.depth === 'number' ? props.depth : 2));
151
- return `<h${depth}>${escapeHtml(toStringValue(props.text))}</h${depth}>`;
152
- }
153
- if (node.type === 'table') {
154
- const columns = Array.isArray(props.cols)
155
- ? props.cols.filter(isRecord)
156
- : [];
157
- const data = Array.isArray(props.data) ? props.data.filter(isRecord) : [];
158
- const header = `<tr>${columns
159
- .map(column => `<th>${escapeHtml(toStringValue(column.text) || toStringValue(column.name))}</th>`)
160
- .join('')}</tr>`;
161
- const body = data
162
- .map(row => `<tr>${columns
163
- .map(column => {
164
- const cell = toRecord(row[toStringValue(column.name)]);
165
- return `<td>${escapeHtml(toStringValue(cell.text))}</td>`;
166
- })
167
- .join('')}</tr>`)
168
- .join('');
169
- return `<table class="affine-zui-custom-block-table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
170
- }
171
- if (node.type === 'link') {
172
- const url = sanitizeUrl(props.href);
173
- const label = toStringValue(props.text) || url;
174
- return url
175
- ? `<a href="${escapeHtmlAttribute(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`
176
- : escapeHtml(label);
177
- }
178
- return `<div>${escapeHtml(toStringValue(props.text))}</div>`;
179
- })
180
- .join('');
181
- }
182
- function renderCustomBlock(block, state) {
183
- const contentValue = toRecord(block.props).content;
184
- if (typeof contentValue === 'string') {
185
- return `<div class="affine-zui-custom-block">${escapeHtml(contentValue)}</div>`;
186
- }
187
- const content = toRecord(contentValue);
188
- const html = toStringValue(content.html);
189
- if (html) {
190
- return state.options.allowUnsafeHtml
191
- ? html
192
- : `<pre class="affine-zui-html-source"><code>${escapeHtml(html)}</code></pre>`;
193
- }
194
- const exported = Array.isArray(content.export) ? content.export : [];
195
- if (exported.length) {
196
- return `<div class="affine-zui-custom-block">${renderCustomExportNodes(exported, state)}</div>`;
197
- }
198
- const title = toStringValue(content.title);
199
- const fetcher = content.fetcher;
200
- const fetcherUrl = typeof fetcher === 'string' ? fetcher : toRecord(fetcher).url;
201
- const url = sanitizeUrl(content.exportUrl) || sanitizeUrl(fetcherUrl);
202
- const component = toStringValue(content.component);
203
- return `<div class="affine-zui-custom-block">${title ? `<h5>${escapeHtml(title)}</h5>` : ''}${component
204
- ? `<div class="affine-zui-component-placeholder" data-component="${escapeHtmlAttribute(component)}">[Component: ${escapeHtml(component)}]</div>`
205
- : ''}${url
206
- ? `<a href="${escapeHtmlAttribute(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`
207
- : ''}</div>`;
208
- }
209
- function renderEmbedLink(block) {
210
- const props = toRecord(block.props);
211
- const url = sanitizeUrl(props.url);
212
- if (!url)
213
- return '';
214
- const title = toStringValue(props.title) || url;
215
- return `<div class="affine-paragraph-block-container"><a href="${escapeHtmlAttribute(url)}">${escapeHtml(title)}</a></div>`;
216
- }
217
- function renderHolder(block) {
218
- const props = toRecord(block.props);
219
- const text = toStringValue(props.text) || toStringValue(props.name);
220
- return `<pre class="affine-zui-holder-block-container"${attribute('data-holder-name', props.name)}${dataAttribute('data-holder-data', props.data)}><code class="affine-zui-holder-block-content">${escapeHtml(text)}</code></pre>`;
221
- }
222
- function customRenderer(block, state) {
223
- const renderer = state.options.renderBlock;
224
- if (!renderer)
225
- return undefined;
226
- const context = {
227
- format: 'html',
228
- renderChildren: children => renderBlocks(children ?? blockChildren(block), state),
229
- renderInline: text => deltaToHtml(text, state.options, block),
230
- };
231
- return renderer(block, context);
232
- }
233
- function renderUnknown(block, state) {
234
- const strategy = state.options.unknownBlock ?? 'children';
235
- if (strategy === 'throw') {
236
- throw new TypeError(`Unsupported BlockSuite block flavour: ${block.flavour}`);
237
- }
238
- return strategy === 'children' ? renderBlocks(blockChildren(block), state) : '';
239
- }
240
- function renderBlock(block, state) {
241
- const overridden = customRenderer(block, state);
242
- if (overridden !== undefined)
243
- return overridden;
244
- const props = toRecord(block.props);
245
- if (block.flavour === 'affine:page') {
246
- return renderBlocks(blockChildren(block), state);
247
- }
248
- if (block.flavour === 'affine:note') {
249
- return isEdgelessOnly(block) ? '' : renderBlocks(blockChildren(block), state);
250
- }
251
- if (TRANSPARENT_FLAVOURS.has(block.flavour)) {
252
- return renderBlocks(blockChildren(block), state);
253
- }
254
- switch (block.flavour) {
255
- case 'affine:paragraph': {
256
- const text = deltaToHtml(props.text, state.options, block);
257
- const type = toStringValue(props.type, 'text');
258
- const align = safeAlign(props.align);
259
- const style = align ? attribute('style', `text-align:${align}`) : '';
260
- let own;
261
- if (/^h[1-6]$/.test(type)) {
262
- own = `<${type}${style}>${text}</${type}>`;
263
- }
264
- else if (type === 'quote') {
265
- own = `<blockquote class="quote"><p${style}>${text}</p></blockquote>`;
266
- }
267
- else {
268
- own = `<p${style}>${text}</p>`;
269
- }
270
- const children = renderBlocks(blockChildren(block), state);
271
- return `<div class="affine-paragraph-block-container">${own}${children
272
- ? `<div class="affine-block-children-container">${children}</div>`
273
- : ''}</div>`;
274
- }
275
- case 'affine:code': {
276
- const text = plainText(props.text);
277
- const language = safeLanguage(props.language);
278
- return `<pre><code${language ? ` class="language-${escapeHtmlAttribute(language)}"` : ''}>${escapeHtml(text)}</code></pre>`;
279
- }
280
- case 'affine:divider':
281
- return '<hr>';
282
- case 'affine:latex':
283
- return `<div class="affine-latex-block" data-latex="${escapeHtmlAttribute(toStringValue(props.latex))}">${escapeHtml(toStringValue(props.latex))}</div>`;
284
- case 'affine:image': {
285
- const url = resolveAssetUrl(state.options, block, 'image');
286
- if (!url)
287
- return '';
288
- const width = positiveInteger(props.width);
289
- const height = positiveInteger(props.height);
290
- const caption = toStringValue(props.caption);
291
- return `<figure class="affine-image-block-container"><img src="${escapeHtmlAttribute(url)}" alt="${escapeHtmlAttribute(caption)}"${attribute('title', caption)}${attribute('width', width)}${attribute('height', height)}></figure>`;
292
- }
293
- case 'affine:zui-image': {
294
- const url = resolveZuiImageUrl(state.options, block);
295
- if (!url)
296
- return '';
297
- return `<figure class="affine-image-block-container"${safeAlign(props.align)
298
- ? attribute('style', `text-align:${safeAlign(props.align)}`)
299
- : ''}><img src="${escapeHtmlAttribute(url)}" alt="${escapeHtmlAttribute(toStringValue(props.caption))}"${attribute('width', positiveInteger(props.width))}${attribute('height', positiveInteger(props.height))}></figure>`;
300
- }
301
- case 'affine:attachment': {
302
- const name = toStringValue(props.name, 'Attachment');
303
- const url = resolveAssetUrl(state.options, block, 'attachment');
304
- const caption = toStringValue(props.caption);
305
- const label = url
306
- ? `<a href="${escapeHtmlAttribute(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(name)}</a>`
307
- : escapeHtml(name);
308
- return `<div class="affine-attachment-block-container"><p><strong>${label}</strong> <em>(${escapeHtml(formatFileSize(props.size))}${caption ? `, ${escapeHtml(caption)}` : ''})</em></p></div>`;
309
- }
310
- case 'affine:database':
311
- return renderDatabase(block, state);
312
- case 'affine:zui-table':
313
- return renderZuiTable(block, state);
314
- case 'affine:zui-holder':
315
- return joinHtml([
316
- renderHolder(block),
317
- renderBlocks(blockChildren(block), state),
318
- ]);
319
- case 'affine:zui-layout':
320
- return `<div class="affine-zui-layout"${attribute('data-layout', props.type)}>${renderBlocks(blockChildren(block), state)}</div>`;
321
- case 'affine:zui-panel':
322
- return `<aside class="affine-zui-panel"${attribute('data-type', props.type)}>${renderBlocks(blockChildren(block), state)}</aside>`;
323
- case 'affine:zui-expand': {
324
- const title = deltaToHtml(props.title, state.options, block);
325
- return `<details class="affine-zui-expand"${props.open === false ? '' : ' open'}><summary>${title}</summary>${renderBlocks(blockChildren(block), state)}</details>`;
326
- }
327
- case 'affine:embed-zui-whiteboard': {
328
- const image = sanitizeUrl(props.sceneImage, 'image');
329
- if (image) {
330
- return `<img class="affine-whiteboard-block-image" src="${escapeHtmlAttribute(image)}" alt="${escapeHtmlAttribute(toStringValue(props.caption, 'whiteboard'))}">`;
331
- }
332
- return `<div class="affine-whiteboard-block-container">${props.sceneData ? '[Whiteboard]' : '[Empty Whiteboard]'}</div>`;
333
- }
334
- case 'affine:embed-zui-custom':
335
- return renderCustomBlock(block, state);
336
- case 'affine:embed-zui-html': {
337
- const html = toStringValue(props.html);
338
- if (!html)
339
- return '';
340
- return state.options.allowUnsafeHtml
341
- ? html
342
- : `<pre class="affine-zui-html-source"><code>${escapeHtml(html)}</code></pre>`;
343
- }
344
- case 'affine:embed-zui-iframe': {
345
- const url = sanitizeUrl(props.src);
346
- if (!url)
347
- return '';
348
- const sandbox = toStringValue(props.sandbox);
349
- let sandboxTokens = sandbox
350
- .split(/\s+/)
351
- .filter(token => [
352
- 'allow-forms',
353
- 'allow-modals',
354
- 'allow-pointer-lock',
355
- 'allow-popups',
356
- 'allow-same-origin',
357
- 'allow-scripts',
358
- ].includes(token));
359
- // A same-origin scripted frame can remove its own sandbox attribute.
360
- if (sandboxTokens.includes('allow-scripts') &&
361
- sandboxTokens.includes('allow-same-origin')) {
362
- sandboxTokens = sandboxTokens.filter(token => token !== 'allow-same-origin');
363
- }
364
- const sandboxAttribute = sandboxTokens.length
365
- ? attribute('sandbox', sandboxTokens.join(' '))
366
- : ' sandbox';
367
- return `<iframe src="${escapeHtmlAttribute(url)}"${sandboxAttribute}${attribute('width', safeDimension(props.width))}${attribute('height', safeDimension(props.height))}${props.allowfullscreen ? ' allowfullscreen' : ''}></iframe>`;
368
- }
369
- case 'affine:embed-zui-component': {
370
- const name = toStringValue(props.name);
371
- return name
372
- ? `<div class="affine-zui-component-placeholder" data-component="${escapeHtmlAttribute(name)}">[Component: ${escapeHtml(name)}]</div>`
373
- : '';
374
- }
375
- case 'affine:embed-linked-doc': {
376
- const pageId = toStringValue(props.pageId);
377
- if (!pageId)
378
- return '';
379
- const title = state.options.resolveDocTitle?.(pageId) ?? 'untitled';
380
- const url = resolveDocumentUrl(state.options, { pageId, params: toRecord(props.params), title }, block);
381
- return url
382
- ? `<a href="${escapeHtmlAttribute(url)}">${escapeHtml(title)}</a>`
383
- : escapeHtml(title);
384
- }
385
- case 'affine:embed-synced-doc': {
386
- const children = blockChildren(block);
387
- if (children.length)
388
- return renderBlocks(children, state);
389
- const pageId = toStringValue(props.pageId);
390
- if (!pageId)
391
- return '';
392
- const title = state.options.resolveDocTitle?.(pageId) ?? pageId;
393
- const url = resolveDocumentUrl(state.options, { pageId, params: toRecord(props.params), title }, block);
394
- return url
395
- ? `<a class="affine-synced-doc-link" href="${escapeHtmlAttribute(url)}">${escapeHtml(title)}</a>`
396
- : `<span class="affine-synced-doc-link">${escapeHtml(title)}</span>`;
397
- }
398
- default:
399
- return EMBED_LINK_FLAVOURS.has(block.flavour)
400
- ? renderEmbedLink(block)
401
- : renderUnknown(block, state);
402
- }
403
- }
404
- function renderBlocks(blocks, state) {
405
- const rendered = [];
406
- for (let index = 0; index < blocks.length; index += 1) {
407
- const block = blocks[index];
408
- if (!block)
409
- continue;
410
- if (block.flavour === 'affine:list') {
411
- const type = toStringValue(toRecord(block.props).type, 'bulleted');
412
- const group = [block];
413
- while (index + 1 < blocks.length) {
414
- const next = blocks[index + 1];
415
- if (next?.flavour !== 'affine:list' ||
416
- toStringValue(toRecord(next.props).type, 'bulleted') !== type) {
417
- break;
418
- }
419
- group.push(next);
420
- index += 1;
421
- }
422
- rendered.push(renderListGroup(group, state));
423
- }
424
- else {
425
- rendered.push(renderBlock(block, state));
426
- }
427
- }
428
- return joinHtml(rendered);
429
- }
430
- /** Convert a BlockSuite 0.19.x snapshot to semantic, escaped HTML. */
431
- export function snapshotToHtml(input, options = {}) {
432
- const normalized = normalizeSnapshot(input);
433
- validateSnapshotTree(normalized.blocks, options);
434
- const state = { options };
435
- const title = pageTitleText(normalized);
436
- const heading = options.includeTitle === false || !title ? '' : `<h1>${escapeHtml(title)}</h1>`;
437
- const body = joinHtml([heading, renderBlocks(normalized.blocks, state)]);
438
- const fullDocument = options.fullDocument ?? normalized.documentLike;
439
- if (!fullDocument)
440
- return body;
441
- return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title></head><body><main class="blocksuite-document">${body}</main></body></html>`;
442
- }