zentao-api 0.6.2 → 0.6.4

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,7 +1,7 @@
1
- import type { ListPagerInfo, ModuleAction, ModuleActionRequest, ModuleDefinition } from '../types/index.js';
1
+ import type { ListPagerInfo, ModuleAction, ModuleActionRequest, ModuleDefinition, RequestProcessOptions } from '../types/index.js';
2
2
  /** 将模块名、动作名和调用参数解析为实际 API 请求路径、查询参数和请求体。 */
3
3
  export declare function resolveActionRequest(module: ModuleDefinition, actionName: string, params?: Record<string, unknown>): ModuleActionRequest;
4
4
  /** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
5
- export declare function extractResult(action: ModuleAction, response: Record<string, unknown>, params?: Record<string, unknown>): unknown;
5
+ export declare function extractResult(action: ModuleAction, response: Record<string, unknown>, params?: Record<string, unknown>, options?: RequestProcessOptions): unknown;
6
6
  /** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
7
- export declare function extractPager(action: ModuleAction, response: Record<string, unknown>, params?: Record<string, unknown>): ListPagerInfo | undefined;
7
+ export declare function extractPager(action: ModuleAction, response: Record<string, unknown>, params?: Record<string, unknown>, options?: RequestProcessOptions): ListPagerInfo | undefined;
@@ -214,12 +214,12 @@ export function resolveActionRequest(module, actionName, params = {}) {
214
214
  };
215
215
  }
216
216
  /** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
217
- export function extractResult(action, response, params = {}) {
217
+ export function extractResult(action, response, params = {}, options = {}) {
218
218
  const getter = action.resultGetter;
219
219
  if (!getter)
220
220
  return response.data ?? response;
221
221
  if (typeof getter === 'function')
222
- return getter(response, params);
222
+ return getter(response, params, options);
223
223
  if (typeof getter === 'string')
224
224
  return getNestedValue(response, getter);
225
225
  const result = {};
@@ -229,12 +229,12 @@ export function extractResult(action, response, params = {}) {
229
229
  return result;
230
230
  }
231
231
  /** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
232
- export function extractPager(action, response, params = {}) {
232
+ export function extractPager(action, response, params = {}, options = {}) {
233
233
  const getter = action.pagerGetter;
234
234
  if (!getter)
235
235
  return response.pager;
236
236
  if (typeof getter === 'function')
237
- return getter(response, params);
237
+ return getter(response, params, options);
238
238
  if (typeof getter === 'string')
239
239
  return getNestedValue(response, getter);
240
240
  const page = getNestedValue(response, getter.pageID);
@@ -148,9 +148,9 @@ function normalizeResponse(command, raw, options) {
148
148
  }
149
149
  const record = raw;
150
150
  const status = record.status === 'fail' ? 'fail' : 'success';
151
- const data = applyProcessing(extractResult(command.action, record, command.params), options);
151
+ const data = applyProcessing(extractResult(command.action, record, command.params, options), options);
152
152
  const rawMessage = record.message;
153
- const pager = extractPager(command.action, record, command.params);
153
+ const pager = extractPager(command.action, record, command.params, options);
154
154
  const response = {
155
155
  status,
156
156
  message: stringifyMessage(rawMessage),
@@ -1,4 +1,5 @@
1
1
  import type { HttpMethod } from './client.js';
2
+ import type { RequestProcessOptions } from './options.js';
2
3
  import type { Pager } from './response.js';
3
4
  /** 模块动作类型:基础 CRUD 或自定义动作。 */
4
5
  export type ModuleActionType = 'list' | 'get' | 'create' | 'update' | 'delete' | 'action';
@@ -78,8 +79,9 @@ export type ModuleActionResultFieldMap = Readonly<Record<string, string>>;
78
79
  * 从原始响应中提取数据时使用的函数形态。
79
80
  * @param data 原始响应对象。
80
81
  * @param params 触发本次请求的原始调用参数。
82
+ * @param options 本次请求的数据处理选项;直接调用 getter 时可以省略。
81
83
  */
82
- export type ModuleActionGetterFn<T> = (data: unknown, params: Record<string, unknown>) => T;
84
+ export type ModuleActionGetterFn<T, O = RequestProcessOptions> = (data: unknown, params: Record<string, unknown>, options?: O) => T;
83
85
  /** 禅道模块中的单个 API 动作定义。 */
84
86
  export interface ModuleAction {
85
87
  /** 动作名称,例如 `list`、`get`、`close`。 */
@@ -55,3 +55,4 @@ export interface RequestOptions extends ProcessListOptions {
55
55
  /** 是否返回原始响应体,默认 false。 */
56
56
  raw?: boolean;
57
57
  }
58
+ export type RequestProcessOptions = ProcessListOptions & Pick<RequestOptions, 'convertSingle'>;
@@ -0,0 +1,3 @@
1
+ import type { HtmlOptions, SnapshotInput } from './types.js';
2
+ /** Convert a BlockSuite 0.19.x snapshot to semantic, escaped HTML. */
3
+ export declare function snapshotToHtml(input: SnapshotInput, options?: HtmlOptions): string;
@@ -0,0 +1,442 @@
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
+ }
@@ -0,0 +1,3 @@
1
+ export { snapshotToHtml } from './html.js';
2
+ export { snapshotToMarkdown } from './markdown.js';
3
+ export type { BlockRenderer, BlockSnapshot, ConvertOptions, DeltaInsert, DocSnapshot, DocSnapshotMeta, DocumentReference, HtmlOptions, JsonRecord, MarkdownOptions, OutputFormat, RenderBlockContext, SliceSnapshot, Snapshot, SnapshotEnvelope, SnapshotInput, TextSnapshot, UnknownBlockStrategy, } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { snapshotToHtml } from './html.js';
2
+ export { snapshotToMarkdown } from './markdown.js';
@@ -0,0 +1,3 @@
1
+ import type { BlockSnapshot, ConvertOptions } from './types.js';
2
+ export declare function deltaToMarkdown(value: unknown, options: ConvertOptions, block?: BlockSnapshot): string;
3
+ export declare function deltaToHtml(value: unknown, options: ConvertOptions, block?: BlockSnapshot): string;