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.
- package/dist/browser/zentao-api.global.js +25 -2
- package/dist/modules/generated.d.ts +552 -70
- package/dist/modules/generated.js +723 -129
- package/dist/modules/override.js +35 -0
- package/dist/modules/registry.d.ts +579 -130
- package/dist/modules/resolve.d.ts +3 -3
- package/dist/modules/resolve.js +4 -4
- package/dist/request/index.js +2 -2
- package/dist/types/module.d.ts +3 -1
- package/dist/types/options.d.ts +1 -0
- package/dist/utils/doc-helper/html.d.ts +3 -0
- package/dist/utils/doc-helper/html.js +442 -0
- package/dist/utils/doc-helper/index.d.ts +3 -0
- package/dist/utils/doc-helper/index.js +2 -0
- package/dist/utils/doc-helper/inline.d.ts +3 -0
- package/dist/utils/doc-helper/inline.js +168 -0
- package/dist/utils/doc-helper/markdown.d.ts +3 -0
- package/dist/utils/doc-helper/markdown.js +402 -0
- package/dist/utils/doc-helper/shared.d.ts +37 -0
- package/dist/utils/doc-helper/shared.js +305 -0
- package/dist/utils/doc-helper/types.d.ts +86 -0
- package/dist/utils/doc-helper/types.js +1 -0
- package/dist/version.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
const DEFAULT_MAX_DEPTH = 256;
|
|
2
|
+
const MAX_ALLOWED_DEPTH = 1000;
|
|
3
|
+
const DEFAULT_MAX_BLOCKS = 50_000;
|
|
4
|
+
const MAX_ALLOWED_BLOCKS = 1_000_000;
|
|
5
|
+
export function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
export function toRecord(value) {
|
|
9
|
+
return isRecord(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
export function toStringValue(value, fallback = '') {
|
|
12
|
+
return typeof value === 'string' ? value : fallback;
|
|
13
|
+
}
|
|
14
|
+
export function toFiniteNumber(value) {
|
|
15
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
16
|
+
? value
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
export function blockChildren(block) {
|
|
20
|
+
return Array.isArray(block.children)
|
|
21
|
+
? block.children.filter(isBlockSnapshot)
|
|
22
|
+
: [];
|
|
23
|
+
}
|
|
24
|
+
export function isBlockSnapshot(value) {
|
|
25
|
+
return isRecord(value) && typeof value.flavour === 'string';
|
|
26
|
+
}
|
|
27
|
+
function parseJson(value) {
|
|
28
|
+
let parsed = value;
|
|
29
|
+
// The product loader accepts content that was JSON-stringified more than once.
|
|
30
|
+
for (let depth = 0; depth < 3 && typeof parsed === 'string'; depth += 1) {
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(parsed);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
throw new TypeError(`Invalid BlockSuite snapshot JSON: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
export function normalizeSnapshot(input) {
|
|
42
|
+
let value = typeof input === 'string' ? parseJson(input) : input;
|
|
43
|
+
if (isRecord(value) && !isBlockSnapshot(value) && 'snapshot' in value) {
|
|
44
|
+
value = value.snapshot;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value) && value.every(isBlockSnapshot)) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'slice',
|
|
49
|
+
blocks: value,
|
|
50
|
+
documentLike: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (isRecord(value) && isBlockSnapshot(value.blocks)) {
|
|
54
|
+
const root = value.blocks;
|
|
55
|
+
return {
|
|
56
|
+
kind: 'doc',
|
|
57
|
+
blocks: [root],
|
|
58
|
+
root,
|
|
59
|
+
meta: isRecord(value.meta) ? value.meta : undefined,
|
|
60
|
+
documentLike: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (isRecord(value) && Array.isArray(value.content)) {
|
|
64
|
+
const blocks = value.content.filter(isBlockSnapshot);
|
|
65
|
+
if (blocks.length !== value.content.length) {
|
|
66
|
+
throw new TypeError('Invalid BlockSuite slice: content must contain blocks');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
kind: 'slice',
|
|
70
|
+
blocks,
|
|
71
|
+
documentLike: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (isBlockSnapshot(value)) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'block',
|
|
77
|
+
blocks: [value],
|
|
78
|
+
root: value,
|
|
79
|
+
documentLike: value.flavour === 'affine:page',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
throw new TypeError('Invalid BlockSuite snapshot: expected a DocSnapshot, BlockSnapshot, SliceSnapshot, or JSON string');
|
|
83
|
+
}
|
|
84
|
+
function conversionLimit(value, fallback, maximum, name) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return fallback;
|
|
87
|
+
if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > maximum) {
|
|
88
|
+
throw new TypeError(`${name} must be an integer between 1 and ${maximum}`);
|
|
89
|
+
}
|
|
90
|
+
return Number(value);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate limits iteratively before recursive rendering. Besides making error
|
|
94
|
+
* messages deterministic, this prevents deeply nested untrusted JSON from
|
|
95
|
+
* exhausting the JavaScript call stack.
|
|
96
|
+
*/
|
|
97
|
+
export function validateSnapshotTree(blocks, options) {
|
|
98
|
+
const maxDepth = conversionLimit(options.maxDepth, DEFAULT_MAX_DEPTH, MAX_ALLOWED_DEPTH, 'maxDepth');
|
|
99
|
+
const maxBlocks = conversionLimit(options.maxBlocks, DEFAULT_MAX_BLOCKS, MAX_ALLOWED_BLOCKS, 'maxBlocks');
|
|
100
|
+
const active = new WeakSet();
|
|
101
|
+
const stack = [];
|
|
102
|
+
for (let index = blocks.length - 1; index >= 0; index -= 1) {
|
|
103
|
+
const block = blocks[index];
|
|
104
|
+
if (block)
|
|
105
|
+
stack.push({ block, depth: 1 });
|
|
106
|
+
}
|
|
107
|
+
let count = 0;
|
|
108
|
+
while (stack.length) {
|
|
109
|
+
const frame = stack.pop();
|
|
110
|
+
if (!frame)
|
|
111
|
+
continue;
|
|
112
|
+
if (frame.exit) {
|
|
113
|
+
active.delete(frame.block);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (active.has(frame.block)) {
|
|
117
|
+
throw new TypeError('Invalid BlockSuite snapshot: cyclic block children');
|
|
118
|
+
}
|
|
119
|
+
count += 1;
|
|
120
|
+
if (count > maxBlocks) {
|
|
121
|
+
throw new RangeError(`BlockSuite snapshot exceeds maxBlocks (${maxBlocks})`);
|
|
122
|
+
}
|
|
123
|
+
if (frame.depth > maxDepth) {
|
|
124
|
+
throw new RangeError(`BlockSuite snapshot exceeds maxDepth (${maxDepth})`);
|
|
125
|
+
}
|
|
126
|
+
active.add(frame.block);
|
|
127
|
+
stack.push({ ...frame, exit: true });
|
|
128
|
+
const children = blockChildren(frame.block);
|
|
129
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
130
|
+
const child = children[index];
|
|
131
|
+
if (child)
|
|
132
|
+
stack.push({ block: child, depth: frame.depth + 1 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function deltaFrom(value) {
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
return value.filter(isRecord);
|
|
139
|
+
}
|
|
140
|
+
if (isRecord(value) && Array.isArray(value.delta)) {
|
|
141
|
+
return value.delta.filter(isRecord);
|
|
142
|
+
}
|
|
143
|
+
if (typeof value === 'string') {
|
|
144
|
+
return [{ insert: value }];
|
|
145
|
+
}
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
export function plainText(value) {
|
|
149
|
+
return deltaFrom(value)
|
|
150
|
+
.map(delta => (typeof delta.insert === 'string' ? delta.insert : ''))
|
|
151
|
+
.join('');
|
|
152
|
+
}
|
|
153
|
+
export function pageTitleDelta(normalized) {
|
|
154
|
+
const root = normalized.root;
|
|
155
|
+
if (root?.flavour === 'affine:page') {
|
|
156
|
+
const rootTitle = deltaFrom(toRecord(root.props).title);
|
|
157
|
+
if (rootTitle.length > 0)
|
|
158
|
+
return rootTitle;
|
|
159
|
+
}
|
|
160
|
+
const metaTitle = normalized.meta?.title;
|
|
161
|
+
return typeof metaTitle === 'string' ? [{ insert: metaTitle }] : [];
|
|
162
|
+
}
|
|
163
|
+
export function pageTitleText(normalized) {
|
|
164
|
+
const metaTitle = normalized.meta?.title;
|
|
165
|
+
if (typeof metaTitle === 'string')
|
|
166
|
+
return metaTitle;
|
|
167
|
+
return plainText(toRecord(normalized.root?.props).title);
|
|
168
|
+
}
|
|
169
|
+
export function escapeHtml(value) {
|
|
170
|
+
return String(value ?? '')
|
|
171
|
+
.replace(/&/g, '&')
|
|
172
|
+
.replace(/</g, '<')
|
|
173
|
+
.replace(/>/g, '>');
|
|
174
|
+
}
|
|
175
|
+
export function escapeHtmlAttribute(value) {
|
|
176
|
+
return escapeHtml(value)
|
|
177
|
+
.replace(/"/g, '"')
|
|
178
|
+
.replace(/'/g, ''');
|
|
179
|
+
}
|
|
180
|
+
export function sanitizeUrl(value, kind = 'link') {
|
|
181
|
+
if (typeof value !== 'string')
|
|
182
|
+
return '';
|
|
183
|
+
const url = value.trim().replace(/[\u0000-\u001F\u007F]/g, '');
|
|
184
|
+
if (!url)
|
|
185
|
+
return '';
|
|
186
|
+
if (url.startsWith('/') ||
|
|
187
|
+
url.startsWith('./') ||
|
|
188
|
+
url.startsWith('../') ||
|
|
189
|
+
url.startsWith('#') ||
|
|
190
|
+
url.startsWith('?')) {
|
|
191
|
+
return url;
|
|
192
|
+
}
|
|
193
|
+
const scheme = /^([a-z][a-z\d+.-]*):/i.exec(url)?.[1]?.toLowerCase();
|
|
194
|
+
if (!scheme)
|
|
195
|
+
return url;
|
|
196
|
+
if (['http', 'https', 'mailto', 'tel', 'blob'].includes(scheme))
|
|
197
|
+
return url;
|
|
198
|
+
if (kind === 'image' && scheme === 'data' && /^data:image\//i.test(url)) {
|
|
199
|
+
return url;
|
|
200
|
+
}
|
|
201
|
+
return '';
|
|
202
|
+
}
|
|
203
|
+
export function resolveAssetUrl(options, block, kind) {
|
|
204
|
+
const props = toRecord(block.props);
|
|
205
|
+
const sourceId = toStringValue(props.sourceId);
|
|
206
|
+
if (!sourceId)
|
|
207
|
+
return '';
|
|
208
|
+
const resolved = options.resolveAssetUrl?.(sourceId, block, kind);
|
|
209
|
+
return sanitizeUrl(resolved, kind === 'image' ? 'image' : 'link');
|
|
210
|
+
}
|
|
211
|
+
export function resolveZuiImageUrl(options, block) {
|
|
212
|
+
const src = toStringValue(toRecord(block.props).src);
|
|
213
|
+
if (!src)
|
|
214
|
+
return '';
|
|
215
|
+
const resolved = options.resolveZuiImageUrl
|
|
216
|
+
? options.resolveZuiImageUrl(src, block)
|
|
217
|
+
: src;
|
|
218
|
+
return sanitizeUrl(resolved, 'image');
|
|
219
|
+
}
|
|
220
|
+
function appendReferenceParams(url, params) {
|
|
221
|
+
const search = new URLSearchParams();
|
|
222
|
+
for (const [key, raw] of Object.entries(params)) {
|
|
223
|
+
if (raw === undefined || raw === null || raw === '')
|
|
224
|
+
continue;
|
|
225
|
+
if (Array.isArray(raw)) {
|
|
226
|
+
const values = raw.filter((item) => ['string', 'number', 'boolean'].includes(typeof item));
|
|
227
|
+
if (values.length)
|
|
228
|
+
search.set(key, values.join(','));
|
|
229
|
+
}
|
|
230
|
+
else if (['string', 'number', 'boolean'].includes(typeof raw)) {
|
|
231
|
+
search.set(key, String(raw));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const query = search.toString();
|
|
235
|
+
if (!query)
|
|
236
|
+
return url;
|
|
237
|
+
return `${url}${url.includes('?') ? '&' : '?'}${query}`;
|
|
238
|
+
}
|
|
239
|
+
export function resolveDocumentUrl(options, reference, block) {
|
|
240
|
+
const custom = options.resolveDocLink?.(reference, block);
|
|
241
|
+
if (custom !== undefined)
|
|
242
|
+
return sanitizeUrl(custom);
|
|
243
|
+
const base = options.docLinkBaseUrl?.replace(/\/$/, '');
|
|
244
|
+
if (!base)
|
|
245
|
+
return '';
|
|
246
|
+
return sanitizeUrl(appendReferenceParams(`${base}/${encodeURIComponent(reference.pageId)}`, reference.params));
|
|
247
|
+
}
|
|
248
|
+
export function formatFileSize(value) {
|
|
249
|
+
const bytes = toFiniteNumber(value) ?? 0;
|
|
250
|
+
if (bytes === 0)
|
|
251
|
+
return '0 B';
|
|
252
|
+
const units = ['B', 'kB', 'MB', 'GB', 'TB'];
|
|
253
|
+
const index = Math.min(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), units.length - 1);
|
|
254
|
+
const size = bytes / 1024 ** index;
|
|
255
|
+
const rounded = size >= 10 || index === 0 ? size.toFixed(0) : size.toFixed(1);
|
|
256
|
+
return `${rounded} ${units[index]}`;
|
|
257
|
+
}
|
|
258
|
+
export function isEdgelessOnly(block) {
|
|
259
|
+
return toRecord(block.props).displayMode === 'edgeless';
|
|
260
|
+
}
|
|
261
|
+
export function safeLanguage(value) {
|
|
262
|
+
return typeof value === 'string'
|
|
263
|
+
? value.trim().replace(/[^a-z\d_+#.-]/gi, '').slice(0, 50)
|
|
264
|
+
: '';
|
|
265
|
+
}
|
|
266
|
+
export function escapeMarkdownMath(value) {
|
|
267
|
+
return toStringValue(value)
|
|
268
|
+
.replace(/\$/g, '\\$')
|
|
269
|
+
.replace(/</g, '\\lt{}')
|
|
270
|
+
.replace(/>/g, '\\gt{}')
|
|
271
|
+
.replace(/(\]\(\s*)([^)\s]*)/gi, (match, prefix, target) => {
|
|
272
|
+
const decoded = target
|
|
273
|
+
.replace(/&#x([\da-f]+);/gi, (_, hex) => {
|
|
274
|
+
const code = Number.parseInt(hex, 16);
|
|
275
|
+
return code <= 0x7f ? String.fromCharCode(code) : '';
|
|
276
|
+
})
|
|
277
|
+
.replace(/&#(\d+);/g, (_, decimal) => {
|
|
278
|
+
const code = Number.parseInt(decimal, 10);
|
|
279
|
+
return code <= 0x7f ? String.fromCharCode(code) : '';
|
|
280
|
+
})
|
|
281
|
+
.replace(/:/gi, ':')
|
|
282
|
+
.replace(/[\u0000-\u0020\u007f]/g, '')
|
|
283
|
+
.toLowerCase();
|
|
284
|
+
if (!/^(?:javascript|vbscript|data):/.test(decoded))
|
|
285
|
+
return match;
|
|
286
|
+
return `${prefix}${target.replace(/:|&(?:#0*58|#x0*3a|colon);/i, '%3A')}`;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
export function safeCssColor(value) {
|
|
290
|
+
if (typeof value !== 'string')
|
|
291
|
+
return '';
|
|
292
|
+
const color = value.trim();
|
|
293
|
+
if (/^(#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d\s.,%+-]+\)|var\(--[\w-]+\)|[a-z]+)$/i.test(color)) {
|
|
294
|
+
return color;
|
|
295
|
+
}
|
|
296
|
+
return '';
|
|
297
|
+
}
|
|
298
|
+
export function safeFontSize(value) {
|
|
299
|
+
if (typeof value !== 'string')
|
|
300
|
+
return '';
|
|
301
|
+
const size = value.trim();
|
|
302
|
+
return /^(?:\d+(?:\.\d+)?(?:px|em|rem|%|pt)|var\(--[\w-]+\))$/i.test(size)
|
|
303
|
+
? size
|
|
304
|
+
: '';
|
|
305
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type JsonRecord = Record<string, unknown>;
|
|
2
|
+
export interface DeltaInsert {
|
|
3
|
+
insert?: unknown;
|
|
4
|
+
attributes?: JsonRecord;
|
|
5
|
+
retain?: number;
|
|
6
|
+
delete?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TextSnapshot {
|
|
9
|
+
'$blocksuite:internal:text$'?: true;
|
|
10
|
+
delta: DeltaInsert[];
|
|
11
|
+
}
|
|
12
|
+
export interface BlockSnapshot {
|
|
13
|
+
type?: 'block' | string;
|
|
14
|
+
id?: string;
|
|
15
|
+
flavour: string;
|
|
16
|
+
version?: number;
|
|
17
|
+
props?: JsonRecord;
|
|
18
|
+
children?: BlockSnapshot[];
|
|
19
|
+
}
|
|
20
|
+
export interface DocSnapshotMeta extends JsonRecord {
|
|
21
|
+
id?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
createDate?: number;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface DocSnapshot {
|
|
27
|
+
type?: 'page' | string;
|
|
28
|
+
meta?: DocSnapshotMeta;
|
|
29
|
+
blocks: BlockSnapshot;
|
|
30
|
+
}
|
|
31
|
+
export interface SliceSnapshot {
|
|
32
|
+
type?: 'slice' | string;
|
|
33
|
+
content: BlockSnapshot[];
|
|
34
|
+
}
|
|
35
|
+
export type Snapshot = DocSnapshot | BlockSnapshot | SliceSnapshot;
|
|
36
|
+
export interface SnapshotEnvelope {
|
|
37
|
+
snapshot: Snapshot | readonly BlockSnapshot[];
|
|
38
|
+
}
|
|
39
|
+
export type SnapshotInput = Snapshot | readonly BlockSnapshot[] | SnapshotEnvelope | string;
|
|
40
|
+
export type OutputFormat = 'markdown' | 'html';
|
|
41
|
+
export type UnknownBlockStrategy = 'children' | 'omit' | 'throw';
|
|
42
|
+
export interface DocumentReference {
|
|
43
|
+
pageId: string;
|
|
44
|
+
params: JsonRecord;
|
|
45
|
+
title?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RenderBlockContext {
|
|
48
|
+
format: OutputFormat;
|
|
49
|
+
renderChildren(children?: readonly BlockSnapshot[]): string;
|
|
50
|
+
renderInline(text: unknown): string;
|
|
51
|
+
}
|
|
52
|
+
export type BlockRenderer = (block: BlockSnapshot, context: RenderBlockContext) => string | undefined;
|
|
53
|
+
export interface ConvertOptions {
|
|
54
|
+
/** Include the page title. Defaults to true. */
|
|
55
|
+
includeTitle?: boolean;
|
|
56
|
+
/** Maximum nested block depth. Defaults to 256 and cannot exceed 1000. */
|
|
57
|
+
maxDepth?: number;
|
|
58
|
+
/** Maximum number of blocks rendered from one snapshot. Defaults to 50,000. */
|
|
59
|
+
maxBlocks?: number;
|
|
60
|
+
/** Used for standard image/attachment sourceId values. */
|
|
61
|
+
resolveAssetUrl?: (sourceId: string, block: BlockSnapshot, kind: 'image' | 'attachment') => string | undefined;
|
|
62
|
+
/** Resolve editor-specific affine:zui-image src placeholders. */
|
|
63
|
+
resolveZuiImageUrl?: (src: string, block: BlockSnapshot) => string | undefined;
|
|
64
|
+
/** Resolve linked-document references without loading a BlockSuite collection. */
|
|
65
|
+
resolveDocLink?: (reference: DocumentReference, block?: BlockSnapshot) => string | undefined;
|
|
66
|
+
/** Resolve the visible label of a linked document. */
|
|
67
|
+
resolveDocTitle?: (pageId: string) => string | undefined;
|
|
68
|
+
/** Base URL used when resolveDocLink is not supplied. */
|
|
69
|
+
docLinkBaseUrl?: string;
|
|
70
|
+
/** Override built-in rendering. Return undefined to continue normally. */
|
|
71
|
+
renderBlock?: BlockRenderer;
|
|
72
|
+
/** Unknown containers recurse by default; unknown leaves produce no output. */
|
|
73
|
+
unknownBlock?: UnknownBlockStrategy;
|
|
74
|
+
}
|
|
75
|
+
export type MarkdownOptions = ConvertOptions;
|
|
76
|
+
export interface HtmlOptions extends ConvertOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Defaults to true for a DocSnapshot/page root and false for block/slice input.
|
|
79
|
+
*/
|
|
80
|
+
fullDocument?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Render stored HTML from affine:embed-zui-html or zui-custom as trusted HTML.
|
|
83
|
+
* Disabled by default.
|
|
84
|
+
*/
|
|
85
|
+
allowUnsafeHtml?: boolean;
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/version.js
CHANGED