@kumwe/studio-rich-text 0.1.0-alpha.6 → 0.1.0-beta.2
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/README.md +19 -5
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/dist/first-party-tools.d.ts +173 -0
- package/dist/first-party-tools.d.ts.map +1 -0
- package/dist/first-party-tools.js +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -564
- package/dist/markdown.d.ts +14 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +12 -0
- package/dist/profiles.d.ts +19 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +1 -0
- package/dist/safe-html.d.ts +16 -0
- package/dist/safe-html.d.ts.map +1 -0
- package/dist/safe-html.js +1 -0
- package/dist/strict-csp-surface.d.ts +10 -0
- package/dist/strict-csp-surface.d.ts.map +1 -0
- package/dist/strict-csp-surface.js +1 -0
- package/dist/studio-rich-text-editor.d.ts +55 -0
- package/dist/studio-rich-text-editor.d.ts.map +1 -0
- package/dist/studio-rich-text-editor.js +1 -0
- package/package.json +7 -6
- package/third-party-licenses/codex-notifier-1.1.2.txt +21 -0
- package/third-party-licenses/codex-tooltip-1.0.6.txt +7 -0
- package/third-party-licenses/editorjs__caret-1.1.0.txt +21 -0
- package/third-party-licenses/editorjs__dom-1.1.0.txt +21 -0
- package/third-party-licenses/editorjs__editorjs-2.31.6.txt +190 -0
- package/third-party-licenses/editorjs__helpers-1.2.2.txt +21 -0
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,564 +1 @@
|
|
|
1
|
-
import StarterKit from '@tiptap/starter-kit';
|
|
2
|
-
const PORTABLE_MARKS = Object.freeze(['bold', 'code', 'italic', 'strike']);
|
|
3
|
-
const PORTABLE_NODES = Object.freeze([
|
|
4
|
-
'blockquote',
|
|
5
|
-
'bulletList',
|
|
6
|
-
'doc',
|
|
7
|
-
'hardBreak',
|
|
8
|
-
'heading',
|
|
9
|
-
'horizontalRule',
|
|
10
|
-
'listItem',
|
|
11
|
-
'orderedList',
|
|
12
|
-
'paragraph',
|
|
13
|
-
'text',
|
|
14
|
-
]);
|
|
15
|
-
const DEFAULT_HEADING_LEVELS = Object.freeze([2, 3, 4]);
|
|
16
|
-
const DEFAULT_MAXIMUM_DOCUMENT_BYTES = 1_048_576;
|
|
17
|
-
const DEFAULT_MAXIMUM_MARKS = 20_000;
|
|
18
|
-
const DEFAULT_MAXIMUM_MARKS_PER_NODE = PORTABLE_MARKS.length;
|
|
19
|
-
const RICH_TEXT_HARD_LIMITS = Object.freeze({
|
|
20
|
-
maximumDepth: 128,
|
|
21
|
-
maximumDocumentBytes: 10_485_760,
|
|
22
|
-
maximumMarks: 400_000,
|
|
23
|
-
maximumMarksPerNode: PORTABLE_MARKS.length,
|
|
24
|
-
maximumNodes: 100_000,
|
|
25
|
-
maximumTextLength: 10_485_760,
|
|
26
|
-
});
|
|
27
|
-
const ATTRIBUTE_HARD_LIMITS = Object.freeze({
|
|
28
|
-
maximumDepth: 32,
|
|
29
|
-
maximumItemsPerArray: 10_000,
|
|
30
|
-
maximumPropertiesPerObject: 1_000,
|
|
31
|
-
maximumStringLength: 1_048_576,
|
|
32
|
-
maximumTotalBytes: RICH_TEXT_HARD_LIMITS.maximumDocumentBytes,
|
|
33
|
-
});
|
|
34
|
-
export const DEFAULT_RICH_TEXT_ATTRIBUTE_LIMITS = Object.freeze({
|
|
35
|
-
maximumDepth: 8,
|
|
36
|
-
maximumItemsPerArray: 256,
|
|
37
|
-
maximumPropertiesPerObject: 64,
|
|
38
|
-
maximumStringLength: 4_096,
|
|
39
|
-
maximumTotalBytes: 65_536,
|
|
40
|
-
});
|
|
41
|
-
export const DEFAULT_RICH_TEXT_PROFILE = Object.freeze({
|
|
42
|
-
allowedAttributes: Object.freeze({
|
|
43
|
-
heading: Object.freeze(['level']),
|
|
44
|
-
orderedList: Object.freeze(['start']),
|
|
45
|
-
}),
|
|
46
|
-
allowedMarks: PORTABLE_MARKS,
|
|
47
|
-
allowedNodes: PORTABLE_NODES,
|
|
48
|
-
attributeLimits: DEFAULT_RICH_TEXT_ATTRIBUTE_LIMITS,
|
|
49
|
-
headingLevels: DEFAULT_HEADING_LEVELS,
|
|
50
|
-
maximumDepth: 32,
|
|
51
|
-
maximumDocumentBytes: DEFAULT_MAXIMUM_DOCUMENT_BYTES,
|
|
52
|
-
maximumMarks: DEFAULT_MAXIMUM_MARKS,
|
|
53
|
-
maximumMarksPerNode: DEFAULT_MAXIMUM_MARKS_PER_NODE,
|
|
54
|
-
maximumNodes: 5_000,
|
|
55
|
-
maximumTextLength: 250_000,
|
|
56
|
-
});
|
|
57
|
-
export function createStudioRichTextExtensions(options = {}) {
|
|
58
|
-
const headingLevels = options.headingLevels ?? DEFAULT_HEADING_LEVELS;
|
|
59
|
-
validateHeadingLevels(headingLevels);
|
|
60
|
-
return [
|
|
61
|
-
StarterKit.configure({
|
|
62
|
-
codeBlock: false,
|
|
63
|
-
heading: { levels: [...headingLevels] },
|
|
64
|
-
link: false,
|
|
65
|
-
underline: false,
|
|
66
|
-
}),
|
|
67
|
-
];
|
|
68
|
-
}
|
|
69
|
-
export function parseRichTextDocument(value, profile = DEFAULT_RICH_TEXT_PROFILE) {
|
|
70
|
-
validateProfile(profile);
|
|
71
|
-
const state = { attributeBytes: 0, markCount: 0, nodeCount: 0, textLength: 0 };
|
|
72
|
-
const attributeLimits = resolveAttributeLimits(profile);
|
|
73
|
-
const node = parseNode(value, '$', 1, profile, attributeLimits, state);
|
|
74
|
-
if (node.type !== 'doc') {
|
|
75
|
-
throw new TypeError('Rich-text document root must have type "doc".');
|
|
76
|
-
}
|
|
77
|
-
const document = { ...node, content: node.content ?? [], type: 'doc' };
|
|
78
|
-
const serialized = JSON.stringify(document);
|
|
79
|
-
if (utf8ByteLength(serialized) > maximumDocumentBytes(profile)) {
|
|
80
|
-
throw new RangeError('Rich-text document exceeds its total-byte limit.');
|
|
81
|
-
}
|
|
82
|
-
return document;
|
|
83
|
-
}
|
|
84
|
-
export function isRichTextDocumentEmpty(document) {
|
|
85
|
-
return document.content.every((node) => nodeText(node).trim().length === 0);
|
|
86
|
-
}
|
|
87
|
-
export function projectRichText(document) {
|
|
88
|
-
const projections = [];
|
|
89
|
-
for (const block of document.content) {
|
|
90
|
-
collectBlockProjections(block, projections);
|
|
91
|
-
}
|
|
92
|
-
return projections;
|
|
93
|
-
}
|
|
94
|
-
function collectBlockProjections(node, projections) {
|
|
95
|
-
switch (node.type) {
|
|
96
|
-
case 'heading':
|
|
97
|
-
case 'paragraph':
|
|
98
|
-
projections.push(projectLeafBlock(node));
|
|
99
|
-
break;
|
|
100
|
-
case 'horizontalRule':
|
|
101
|
-
projections.push({ embeds: [], spans: [], text: '', type: 'horizontalRule' });
|
|
102
|
-
break;
|
|
103
|
-
case 'blockquote':
|
|
104
|
-
case 'bulletList':
|
|
105
|
-
case 'listItem':
|
|
106
|
-
case 'orderedList':
|
|
107
|
-
for (const child of node.content ?? []) {
|
|
108
|
-
collectBlockProjections(child, projections);
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
111
|
-
default:
|
|
112
|
-
throw new TypeError(`Node type "${node.type}" has no renderer projection.`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function projectLeafBlock(node) {
|
|
116
|
-
const embeds = [];
|
|
117
|
-
const spans = [];
|
|
118
|
-
let text = '';
|
|
119
|
-
let offset = 0;
|
|
120
|
-
for (const inline of node.content ?? []) {
|
|
121
|
-
if (inline.type === 'text') {
|
|
122
|
-
const value = inline.text ?? '';
|
|
123
|
-
const length = codePointLength(value);
|
|
124
|
-
const marks = (inline.marks ?? []).map((mark) => mark.type).sort();
|
|
125
|
-
if (marks.length > 0 && length > 0) {
|
|
126
|
-
const previous = spans.at(-1);
|
|
127
|
-
if (previous?.end === offset && sameMarkNames(previous.marks, marks)) {
|
|
128
|
-
previous.end = offset + length;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
spans.push({ end: offset + length, marks, start: offset });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
text += value;
|
|
135
|
-
offset += length;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
embeds.push({ index: offset, kind: inline.type });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return { embeds, spans, text, type: node.type };
|
|
142
|
-
}
|
|
143
|
-
function sameMarkNames(left, right) {
|
|
144
|
-
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
145
|
-
}
|
|
146
|
-
function codePointLength(value) {
|
|
147
|
-
let length = 0;
|
|
148
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
149
|
-
const code = value.charCodeAt(index);
|
|
150
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
151
|
-
const next = value.charCodeAt(index + 1);
|
|
152
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
153
|
-
index += 1;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
length += 1;
|
|
157
|
-
}
|
|
158
|
-
return length;
|
|
159
|
-
}
|
|
160
|
-
function parseNode(value, path, depth, profile, attributeLimits, state) {
|
|
161
|
-
if (!isRecord(value)) {
|
|
162
|
-
throw new TypeError(`${path} must be a rich-text node with a non-empty type.`);
|
|
163
|
-
}
|
|
164
|
-
assertKnownKeys(value, path, ['attrs', 'content', 'marks', 'text', 'type']);
|
|
165
|
-
if (typeof value.type !== 'string' || value.type.length === 0) {
|
|
166
|
-
throw new TypeError(`${path} must be a rich-text node with a non-empty type.`);
|
|
167
|
-
}
|
|
168
|
-
if (!profile.allowedNodes.includes(value.type)) {
|
|
169
|
-
throw new TypeError(`${path} uses disallowed node type "${value.type}".`);
|
|
170
|
-
}
|
|
171
|
-
if (depth > profile.maximumDepth) {
|
|
172
|
-
throw new RangeError(`${path} exceeds the rich-text depth limit.`);
|
|
173
|
-
}
|
|
174
|
-
state.nodeCount += 1;
|
|
175
|
-
if (state.nodeCount > profile.maximumNodes) {
|
|
176
|
-
throw new RangeError('Rich-text document exceeds its node limit.');
|
|
177
|
-
}
|
|
178
|
-
const node = { type: value.type };
|
|
179
|
-
if (value.text !== undefined) {
|
|
180
|
-
if (typeof value.text !== 'string') {
|
|
181
|
-
throw new TypeError(`${path}.text must be a string.`);
|
|
182
|
-
}
|
|
183
|
-
node.text = value.text;
|
|
184
|
-
state.textLength += value.text.length;
|
|
185
|
-
if (state.textLength > profile.maximumTextLength) {
|
|
186
|
-
throw new RangeError('Rich-text document exceeds its text-length limit.');
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (value.attrs !== undefined) {
|
|
190
|
-
node.attrs = parseAttributes(value.attrs, `${path}.attrs`, value.type, profile, attributeLimits, state);
|
|
191
|
-
}
|
|
192
|
-
if (value.content !== undefined) {
|
|
193
|
-
if (!Array.isArray(value.content)) {
|
|
194
|
-
throw new TypeError(`${path}.content must be an array.`);
|
|
195
|
-
}
|
|
196
|
-
assertStructuralArray(value.content, `${path}.content`);
|
|
197
|
-
node.content = value.content.map((child, index) => parseNode(child, `${path}.content[${index}]`, depth + 1, profile, attributeLimits, state));
|
|
198
|
-
}
|
|
199
|
-
if (value.marks !== undefined) {
|
|
200
|
-
if (!Array.isArray(value.marks)) {
|
|
201
|
-
throw new TypeError(`${path}.marks must be an array.`);
|
|
202
|
-
}
|
|
203
|
-
assertStructuralArray(value.marks, `${path}.marks`);
|
|
204
|
-
const maximumPerNode = profile.maximumMarksPerNode ?? DEFAULT_MAXIMUM_MARKS_PER_NODE;
|
|
205
|
-
const maximumMarks = profile.maximumMarks ?? DEFAULT_MAXIMUM_MARKS;
|
|
206
|
-
if (value.marks.length > maximumPerNode) {
|
|
207
|
-
throw new RangeError(`${path}.marks exceeds the per-node mark limit.`);
|
|
208
|
-
}
|
|
209
|
-
if (state.markCount + value.marks.length > maximumMarks) {
|
|
210
|
-
throw new RangeError('Rich-text document exceeds its aggregate mark limit.');
|
|
211
|
-
}
|
|
212
|
-
state.markCount += value.marks.length;
|
|
213
|
-
node.marks = value.marks.map((mark, index) => parseMark(mark, `${path}.marks[${index}]`, profile, attributeLimits, state));
|
|
214
|
-
assertPortableMarkSet(node.marks, `${path}.marks`);
|
|
215
|
-
}
|
|
216
|
-
assertNodeGrammar(node, path, profile);
|
|
217
|
-
return node;
|
|
218
|
-
}
|
|
219
|
-
function parseMark(value, path, profile, attributeLimits, state) {
|
|
220
|
-
if (!isRecord(value)) {
|
|
221
|
-
throw new TypeError(`${path} must be a mark with a non-empty type.`);
|
|
222
|
-
}
|
|
223
|
-
assertKnownKeys(value, path, ['attrs', 'type']);
|
|
224
|
-
if (typeof value.type !== 'string' || value.type.length === 0) {
|
|
225
|
-
throw new TypeError(`${path} must be a mark with a non-empty type.`);
|
|
226
|
-
}
|
|
227
|
-
if (!profile.allowedMarks.includes(value.type)) {
|
|
228
|
-
throw new TypeError(`${path} uses disallowed mark type "${value.type}".`);
|
|
229
|
-
}
|
|
230
|
-
const mark = { type: value.type };
|
|
231
|
-
if (value.attrs !== undefined) {
|
|
232
|
-
mark.attrs = parseAttributes(value.attrs, `${path}.attrs`, `mark:${value.type}`, profile, attributeLimits, state);
|
|
233
|
-
}
|
|
234
|
-
if (mark.attrs !== undefined) {
|
|
235
|
-
throw new TypeError(`${path} cannot carry attributes in the portable rich-text grammar.`);
|
|
236
|
-
}
|
|
237
|
-
return mark;
|
|
238
|
-
}
|
|
239
|
-
function assertNodeGrammar(node, path, profile) {
|
|
240
|
-
switch (node.type) {
|
|
241
|
-
case 'doc':
|
|
242
|
-
assertNoNodeFields(node, path, ['attrs', 'marks', 'text']);
|
|
243
|
-
if (node.content === undefined || node.content.length === 0) {
|
|
244
|
-
throw new TypeError(`${path}.content must contain at least one block node.`);
|
|
245
|
-
}
|
|
246
|
-
assertChildTypes(node.content, path, blockNodeTypes);
|
|
247
|
-
break;
|
|
248
|
-
case 'text':
|
|
249
|
-
assertNoNodeFields(node, path, ['attrs', 'content']);
|
|
250
|
-
if (node.text === undefined) {
|
|
251
|
-
throw new TypeError(`${path}.text is required for a text node.`);
|
|
252
|
-
}
|
|
253
|
-
if (node.text.length === 0) {
|
|
254
|
-
throw new TypeError(`${path}.text cannot be empty.`);
|
|
255
|
-
}
|
|
256
|
-
break;
|
|
257
|
-
case 'paragraph':
|
|
258
|
-
assertNoNodeFields(node, path, ['attrs', 'marks', 'text']);
|
|
259
|
-
assertChildTypes(node.content ?? [], path, inlineNodeTypes);
|
|
260
|
-
break;
|
|
261
|
-
case 'heading': {
|
|
262
|
-
assertNoNodeFields(node, path, ['marks', 'text']);
|
|
263
|
-
assertChildTypes(node.content ?? [], path, inlineNodeTypes);
|
|
264
|
-
const level = node.attrs?.level;
|
|
265
|
-
const levels = profile.headingLevels ?? DEFAULT_HEADING_LEVELS;
|
|
266
|
-
if (typeof level !== 'number' ||
|
|
267
|
-
!Number.isInteger(level) ||
|
|
268
|
-
!levels.includes(level)) {
|
|
269
|
-
throw new TypeError(`${path}.attrs.level must be a configured heading level.`);
|
|
270
|
-
}
|
|
271
|
-
break;
|
|
272
|
-
}
|
|
273
|
-
case 'orderedList': {
|
|
274
|
-
assertNoNodeFields(node, path, ['marks', 'text']);
|
|
275
|
-
assertNonEmptyChildTypes(node.content, path, listItemNodeTypes);
|
|
276
|
-
const start = node.attrs?.start;
|
|
277
|
-
if (start !== undefined && (!Number.isSafeInteger(start) || Number(start) < 1)) {
|
|
278
|
-
throw new TypeError(`${path}.attrs.start must be a positive integer.`);
|
|
279
|
-
}
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case 'bulletList':
|
|
283
|
-
assertNoNodeFields(node, path, ['attrs', 'marks', 'text']);
|
|
284
|
-
assertNonEmptyChildTypes(node.content, path, listItemNodeTypes);
|
|
285
|
-
break;
|
|
286
|
-
case 'listItem':
|
|
287
|
-
assertNoNodeFields(node, path, ['attrs', 'marks', 'text']);
|
|
288
|
-
assertNonEmptyChildTypes(node.content, path, blockNodeTypes);
|
|
289
|
-
if (node.content?.[0]?.type !== 'paragraph') {
|
|
290
|
-
throw new TypeError(`${path}.content must begin with a paragraph node.`);
|
|
291
|
-
}
|
|
292
|
-
break;
|
|
293
|
-
case 'blockquote':
|
|
294
|
-
assertNoNodeFields(node, path, ['attrs', 'marks', 'text']);
|
|
295
|
-
assertNonEmptyChildTypes(node.content, path, blockNodeTypes);
|
|
296
|
-
break;
|
|
297
|
-
case 'hardBreak':
|
|
298
|
-
case 'horizontalRule':
|
|
299
|
-
assertNoNodeFields(node, path, ['attrs', 'content', 'marks', 'text']);
|
|
300
|
-
break;
|
|
301
|
-
default:
|
|
302
|
-
throw new TypeError(`${path} uses a node without a portable grammar.`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
function assertPortableMarkSet(marks, path) {
|
|
306
|
-
const types = new Set();
|
|
307
|
-
for (const mark of marks) {
|
|
308
|
-
if (types.has(mark.type)) {
|
|
309
|
-
throw new TypeError(`${path} cannot contain duplicate ${mark.type} marks.`);
|
|
310
|
-
}
|
|
311
|
-
types.add(mark.type);
|
|
312
|
-
}
|
|
313
|
-
if (types.has('code') && types.size > 1) {
|
|
314
|
-
throw new TypeError(`${path} cannot combine code with another mark.`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
const blockNodeTypes = new Set([
|
|
318
|
-
'blockquote',
|
|
319
|
-
'bulletList',
|
|
320
|
-
'heading',
|
|
321
|
-
'horizontalRule',
|
|
322
|
-
'orderedList',
|
|
323
|
-
'paragraph',
|
|
324
|
-
]);
|
|
325
|
-
const inlineNodeTypes = new Set(['hardBreak', 'text']);
|
|
326
|
-
const listItemNodeTypes = new Set(['listItem']);
|
|
327
|
-
function assertNoNodeFields(node, path, fields) {
|
|
328
|
-
const present = fields.find((field) => node[field] !== undefined);
|
|
329
|
-
if (present !== undefined) {
|
|
330
|
-
throw new TypeError(`${path}.${present} is not valid on a ${node.type} node.`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function assertChildTypes(content, path, allowed) {
|
|
334
|
-
const invalidIndex = content.findIndex((child) => !allowed.has(child.type));
|
|
335
|
-
if (invalidIndex >= 0) {
|
|
336
|
-
throw new TypeError(`${path}.content[${invalidIndex}] is not valid inside this node.`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function assertNonEmptyChildTypes(content, path, allowed) {
|
|
340
|
-
if (content === undefined || content.length === 0) {
|
|
341
|
-
throw new TypeError(`${path}.content must contain at least one child node.`);
|
|
342
|
-
}
|
|
343
|
-
assertChildTypes(content, path, allowed);
|
|
344
|
-
}
|
|
345
|
-
function parseAttributes(value, path, ownerType, profile, limits, state) {
|
|
346
|
-
const attributes = parseJsonObject(value, path, 1, limits, state);
|
|
347
|
-
const allowed = profile.allowedAttributes[ownerType] ?? [];
|
|
348
|
-
for (const key of Object.keys(attributes)) {
|
|
349
|
-
if (!allowed.includes(key)) {
|
|
350
|
-
throw new TypeError(`${path}.${key} is not allowed for ${ownerType}.`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return attributes;
|
|
354
|
-
}
|
|
355
|
-
function parseJsonObject(value, path, depth, limits, state) {
|
|
356
|
-
if (!isRecord(value)) {
|
|
357
|
-
throw new TypeError(`${path} must be an object.`);
|
|
358
|
-
}
|
|
359
|
-
assertAttributeDepth(depth, path, limits);
|
|
360
|
-
const entries = Object.entries(value);
|
|
361
|
-
if (entries.length > limits.maximumPropertiesPerObject) {
|
|
362
|
-
throw new RangeError(`${path} exceeds the attribute property limit.`);
|
|
363
|
-
}
|
|
364
|
-
addAttributeBytes(state, limits, 2);
|
|
365
|
-
const parsed = {};
|
|
366
|
-
for (const [index, [key, entry]] of entries.entries()) {
|
|
367
|
-
assertAttributeKey(key, path, limits);
|
|
368
|
-
addAttributeBytes(state, limits, (index === 0 ? 0 : 1) + jsonByteLength(key) + 1);
|
|
369
|
-
parsed[key] = parseJsonValue(entry, `${path}.${key}`, depth + 1, limits, state);
|
|
370
|
-
}
|
|
371
|
-
return parsed;
|
|
372
|
-
}
|
|
373
|
-
function parseJsonValue(value, path, depth, limits, state) {
|
|
374
|
-
if (typeof value === 'string') {
|
|
375
|
-
if (value.length > limits.maximumStringLength) {
|
|
376
|
-
throw new RangeError(`${path} exceeds the attribute string limit.`);
|
|
377
|
-
}
|
|
378
|
-
addAttributeBytes(state, limits, jsonByteLength(value));
|
|
379
|
-
return value;
|
|
380
|
-
}
|
|
381
|
-
if (value === null || typeof value === 'boolean') {
|
|
382
|
-
addAttributeBytes(state, limits, jsonByteLength(value));
|
|
383
|
-
return value;
|
|
384
|
-
}
|
|
385
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
386
|
-
addAttributeBytes(state, limits, jsonByteLength(value));
|
|
387
|
-
return value;
|
|
388
|
-
}
|
|
389
|
-
if (Array.isArray(value)) {
|
|
390
|
-
assertAttributeDepth(depth, path, limits);
|
|
391
|
-
assertJsonArray(value, path, limits);
|
|
392
|
-
addAttributeBytes(state, limits, 2);
|
|
393
|
-
return value.map((entry, index) => {
|
|
394
|
-
if (index > 0) {
|
|
395
|
-
addAttributeBytes(state, limits, 1);
|
|
396
|
-
}
|
|
397
|
-
return parseJsonValue(entry, `${path}[${index}]`, depth + 1, limits, state);
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
if (isRecord(value)) {
|
|
401
|
-
return parseJsonObject(value, path, depth, limits, state);
|
|
402
|
-
}
|
|
403
|
-
throw new TypeError(`${path} is not JSON-compatible.`);
|
|
404
|
-
}
|
|
405
|
-
function addAttributeBytes(state, limits, count) {
|
|
406
|
-
state.attributeBytes += count;
|
|
407
|
-
if (state.attributeBytes > limits.maximumTotalBytes) {
|
|
408
|
-
throw new RangeError('Rich-text attributes exceed the total-byte limit.');
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
function assertAttributeDepth(depth, path, limits) {
|
|
412
|
-
if (depth > limits.maximumDepth) {
|
|
413
|
-
throw new RangeError(`${path} exceeds the attribute depth limit.`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
function assertAttributeKey(key, path, limits) {
|
|
417
|
-
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
418
|
-
throw new TypeError(`${path}.${key} is a forbidden object key.`);
|
|
419
|
-
}
|
|
420
|
-
if (key.length > limits.maximumStringLength) {
|
|
421
|
-
throw new RangeError(`${path} contains an attribute key that exceeds the string limit.`);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function assertJsonArray(value, path, limits) {
|
|
425
|
-
if (value.length > limits.maximumItemsPerArray) {
|
|
426
|
-
throw new RangeError(`${path} exceeds the attribute item limit.`);
|
|
427
|
-
}
|
|
428
|
-
const keys = Object.keys(value);
|
|
429
|
-
for (const key of keys) {
|
|
430
|
-
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
431
|
-
throw new TypeError(`${path}.${key} is a forbidden object key.`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (keys.length !== value.length || keys.some((key, index) => key !== String(index))) {
|
|
435
|
-
throw new TypeError(`${path} must be a dense JSON array without extra properties.`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
function assertStructuralArray(value, path) {
|
|
439
|
-
if (Object.getPrototypeOf(value) !== Array.prototype ||
|
|
440
|
-
Object.getOwnPropertySymbols(value).length) {
|
|
441
|
-
throw new TypeError(`${path} must be a dense JSON array without extra properties.`);
|
|
442
|
-
}
|
|
443
|
-
const names = Object.getOwnPropertyNames(value);
|
|
444
|
-
if (names.length !== value.length + 1 ||
|
|
445
|
-
names[value.length] !== 'length' ||
|
|
446
|
-
names.slice(0, -1).some((name, index) => name !== String(index))) {
|
|
447
|
-
throw new TypeError(`${path} must be a dense JSON array without extra properties.`);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
function assertKnownKeys(value, path, allowedKeys) {
|
|
451
|
-
const allowed = new Set(allowedKeys);
|
|
452
|
-
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
453
|
-
if (unknown !== undefined) {
|
|
454
|
-
throw new TypeError(`${path}.${unknown} is not a recognized rich-text key.`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
function jsonByteLength(value) {
|
|
458
|
-
const serialized = JSON.stringify(value);
|
|
459
|
-
if (serialized === undefined) {
|
|
460
|
-
throw new TypeError('Attribute value is not JSON-compatible.');
|
|
461
|
-
}
|
|
462
|
-
return utf8ByteLength(serialized);
|
|
463
|
-
}
|
|
464
|
-
function utf8ByteLength(value) {
|
|
465
|
-
let bytes = 0;
|
|
466
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
467
|
-
const code = value.charCodeAt(index);
|
|
468
|
-
if (code <= 0x7f) {
|
|
469
|
-
bytes += 1;
|
|
470
|
-
}
|
|
471
|
-
else if (code <= 0x7ff) {
|
|
472
|
-
bytes += 2;
|
|
473
|
-
}
|
|
474
|
-
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
475
|
-
const next = value.charCodeAt(index + 1);
|
|
476
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
477
|
-
bytes += 4;
|
|
478
|
-
index += 1;
|
|
479
|
-
}
|
|
480
|
-
else {
|
|
481
|
-
bytes += 3;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
bytes += 3;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return bytes;
|
|
489
|
-
}
|
|
490
|
-
function nodeText(node) {
|
|
491
|
-
return `${node.text ?? ''}${(node.content ?? []).map((child) => nodeText(child)).join('')}`;
|
|
492
|
-
}
|
|
493
|
-
function isRecord(value) {
|
|
494
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
|
-
const prototype = Object.getPrototypeOf(value);
|
|
498
|
-
return prototype === Object.prototype || prototype === null;
|
|
499
|
-
}
|
|
500
|
-
function validateProfile(profile) {
|
|
501
|
-
for (const [name, value] of [
|
|
502
|
-
['maximumDepth', profile.maximumDepth],
|
|
503
|
-
['maximumNodes', profile.maximumNodes],
|
|
504
|
-
['maximumTextLength', profile.maximumTextLength],
|
|
505
|
-
]) {
|
|
506
|
-
assertBoundedProfileLimit(name, value, RICH_TEXT_HARD_LIMITS[name]);
|
|
507
|
-
}
|
|
508
|
-
for (const [name, value] of [
|
|
509
|
-
['maximumDocumentBytes', maximumDocumentBytes(profile)],
|
|
510
|
-
['maximumMarks', profile.maximumMarks ?? DEFAULT_MAXIMUM_MARKS],
|
|
511
|
-
['maximumMarksPerNode', profile.maximumMarksPerNode ?? DEFAULT_MAXIMUM_MARKS_PER_NODE],
|
|
512
|
-
]) {
|
|
513
|
-
assertBoundedProfileLimit(name, value, RICH_TEXT_HARD_LIMITS[name]);
|
|
514
|
-
}
|
|
515
|
-
if ((profile.maximumMarksPerNode ?? DEFAULT_MAXIMUM_MARKS_PER_NODE) >
|
|
516
|
-
(profile.maximumMarks ?? DEFAULT_MAXIMUM_MARKS)) {
|
|
517
|
-
throw new RangeError('maximumMarksPerNode cannot exceed maximumMarks.');
|
|
518
|
-
}
|
|
519
|
-
if (!profile.allowedNodes.includes('doc') || !profile.allowedNodes.includes('text')) {
|
|
520
|
-
throw new TypeError('Rich-text profile must allow doc and text nodes.');
|
|
521
|
-
}
|
|
522
|
-
if (new Set(profile.allowedNodes).size !== profile.allowedNodes.length) {
|
|
523
|
-
throw new TypeError('Rich-text profile node names must be unique.');
|
|
524
|
-
}
|
|
525
|
-
if (new Set(profile.allowedMarks).size !== profile.allowedMarks.length) {
|
|
526
|
-
throw new TypeError('Rich-text profile mark names must be unique.');
|
|
527
|
-
}
|
|
528
|
-
const unsupportedNode = profile.allowedNodes.find((name) => !PORTABLE_NODES.includes(name));
|
|
529
|
-
if (unsupportedNode !== undefined) {
|
|
530
|
-
throw new TypeError(`Rich-text profile node "${unsupportedNode}" has no portable grammar.`);
|
|
531
|
-
}
|
|
532
|
-
const unsupportedMark = profile.allowedMarks.find((name) => !PORTABLE_MARKS.includes(name));
|
|
533
|
-
if (unsupportedMark !== undefined) {
|
|
534
|
-
throw new TypeError(`Rich-text profile mark "${unsupportedMark}" has no portable grammar.`);
|
|
535
|
-
}
|
|
536
|
-
validateHeadingLevels(profile.headingLevels ?? DEFAULT_HEADING_LEVELS);
|
|
537
|
-
resolveAttributeLimits(profile);
|
|
538
|
-
}
|
|
539
|
-
function assertBoundedProfileLimit(name, value, hardMaximum) {
|
|
540
|
-
if (!Number.isInteger(value) || value < 1) {
|
|
541
|
-
throw new RangeError(`${name} must be a positive integer.`);
|
|
542
|
-
}
|
|
543
|
-
if (value > hardMaximum) {
|
|
544
|
-
throw new RangeError(`${name} exceeds the immutable safety ceiling of ${hardMaximum}.`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
function validateHeadingLevels(levels) {
|
|
548
|
-
if (levels.length === 0 ||
|
|
549
|
-
new Set(levels).size !== levels.length ||
|
|
550
|
-
levels.some((level) => !Number.isInteger(level) || level < 1 || level > 6)) {
|
|
551
|
-
throw new RangeError('headingLevels must contain unique integer levels from 1 through 6.');
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
function maximumDocumentBytes(profile) {
|
|
555
|
-
return profile.maximumDocumentBytes ?? DEFAULT_MAXIMUM_DOCUMENT_BYTES;
|
|
556
|
-
}
|
|
557
|
-
function resolveAttributeLimits(profile) {
|
|
558
|
-
const limits = { ...DEFAULT_RICH_TEXT_ATTRIBUTE_LIMITS, ...profile.attributeLimits };
|
|
559
|
-
for (const [name, value] of Object.entries(limits)) {
|
|
560
|
-
assertBoundedProfileLimit(name, value, ATTRIBUTE_HARD_LIMITS[name]);
|
|
561
|
-
}
|
|
562
|
-
return limits;
|
|
563
|
-
}
|
|
564
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export{EditorJsRichTextEditorFactory,StudioRichTextEditorFactory}from"./studio-rich-text-editor.js";export{StudioStrictCspRichTextSurfaceAdapter}from"./strict-csp-surface.js";export{exportStudioMarkdown,importStudioMarkdown}from"./markdown.js";export{importSafeHtml,STUDIO_SAFE_HTML_POLICY}from"./safe-html.js";export{DOCUMENTATION_RICH_TEXT_PROFILE,MARKETING_RICH_TEXT_PROFILE,PORTABLE_RICH_TEXT_PROFILE,resolveContainerRichTextProfile,resolveRichTextProfile}from"./profiles.js";const e=Object.freeze([`bold`,`code`,`highlight`,`italic`,`strike`]),t=Object.freeze([`blockquote`,`bulletList`,`callout`,`checklist`,`checklistItem`,`codeBlock`,`doc`,`hardBreak`,`heading`,`horizontalRule`,`listItem`,`orderedList`,`paragraph`,`table`,`tableCell`,`tableRow`,`text`]),n=Object.freeze([2,3,4]),r=1048576,i=2e4,a=e.length,o=Object.freeze({maximumDepth:128,maximumDocumentBytes:10485760,maximumMarks:4e5,maximumMarksPerNode:e.length,maximumNodes:1e5,maximumTextLength:10485760}),s=Object.freeze({maximumDepth:32,maximumItemsPerArray:1e4,maximumPropertiesPerObject:1e3,maximumStringLength:1048576,maximumTotalBytes:o.maximumDocumentBytes});export const DEFAULT_RICH_TEXT_ATTRIBUTE_LIMITS=Object.freeze({maximumDepth:8,maximumItemsPerArray:256,maximumPropertiesPerObject:64,maximumStringLength:4096,maximumTotalBytes:65536});export const DEFAULT_RICH_TEXT_PROFILE=Object.freeze({allowedAttributes:Object.freeze({callout:Object.freeze([`tone`]),checklistItem:Object.freeze([`checked`,`level`]),codeBlock:Object.freeze([`language`]),heading:Object.freeze([`level`]),"mark:highlight":Object.freeze([`tone`]),orderedList:Object.freeze([`start`]),table:Object.freeze([`header`])}),allowedMarks:e,allowedNodes:t,attributeLimits:DEFAULT_RICH_TEXT_ATTRIBUTE_LIMITS,headingLevels:n,maximumDepth:32,maximumDocumentBytes:r,maximumMarks:i,maximumMarksPerNode:a,maximumNodes:5e3,maximumTextLength:25e4});export function parseRichTextDocument(e,t=DEFAULT_RICH_TEXT_PROFILE){V(t);let n=m(e,`$`,1,t,G(t),{attributeBytes:0,markCount:0,nodeCount:0,textLength:0});if(n.type!==`doc`)throw TypeError(`Rich-text document root must have type "doc".`);let r={...n,content:n.content??[],type:`doc`};if(R(JSON.stringify(r))>W(t))throw RangeError(`Rich-text document exceeds its total-byte limit.`);return r}export function isRichTextDocumentEmpty(e){return e.content.every(e=>z(e).trim().length===0)}export function projectRichText(e){let t=[];for(let n of e.content)u(n,t);return t}function u(e,t){switch(e.type){case`checklistItem`:case`heading`:case`paragraph`:case`tableCell`:t.push(d(e));break;case`codeBlock`:t.push({embeds:[],spans:[],text:e.text??``,type:`codeBlock`});break;case`horizontalRule`:t.push({embeds:[],spans:[],text:``,type:`horizontalRule`});break;case`blockquote`:case`bulletList`:case`callout`:case`checklist`:case`listItem`:case`orderedList`:case`table`:case`tableRow`:for(let n of e.content??[])u(n,t);break;default:throw TypeError(`Node type "${e.type}" has no renderer projection.`)}}function d(e){let t=[],n=[],r=``,i=0;for(let a of e.content??[])if(a.type===`text`){let e=a.text??``,t=p(e),o=(a.marks??[]).map(e=>e.type).sort();if(o.length>0&&t>0){let e=n.at(-1);e?.end===i&&f(e.marks,o)?e.end=i+t:n.push({end:i+t,marks:o,start:i})}r+=e,i+=t}else t.push({index:i,kind:a.type});return{embeds:t,spans:n,text:r,type:e.type}}function f(e,t){return e.length===t.length&&e.every((e,n)=>e===t[n])}function p(e){let t=0;for(let n=0;n<e.length;n+=1){let r=e.charCodeAt(n);if(r>=55296&&r<=56319){let t=e.charCodeAt(n+1);t>=56320&&t<=57343&&(n+=1)}t+=1}return t}function m(e,t,n,r,o,s){if(!B(e)||(I(e,t,[`attrs`,`content`,`marks`,`text`,`type`]),typeof e.type!=`string`||e.type.length===0))throw TypeError(`${t} must be a rich-text node with a non-empty type.`);if(!r.allowedNodes.includes(e.type))throw TypeError(`${t} uses disallowed node type "${e.type}".`);if(n>r.maximumDepth)throw RangeError(`${t} exceeds the rich-text depth limit.`);if(s.nodeCount+=1,s.nodeCount>r.maximumNodes)throw RangeError(`Rich-text document exceeds its node limit.`);let c={type:e.type};if(e.text!==void 0){if(typeof e.text!=`string`)throw TypeError(`${t}.text must be a string.`);if(c.text=e.text,s.textLength+=e.text.length,s.textLength>r.maximumTextLength)throw RangeError(`Rich-text document exceeds its text-length limit.`)}if(e.attrs!==void 0&&(c.attrs=O(e.attrs,`${t}.attrs`,e.type,r,o,s)),e.content!==void 0){if(!Array.isArray(e.content))throw TypeError(`${t}.content must be an array.`);F(e.content,`${t}.content`),c.content=e.content.map((e,i)=>m(e,`${t}.content[${i}]`,n+1,r,o,s))}if(e.marks!==void 0){if(!Array.isArray(e.marks))throw TypeError(`${t}.marks must be an array.`);F(e.marks,`${t}.marks`);let n=r.maximumMarksPerNode??a,l=r.maximumMarks??i;if(e.marks.length>n)throw RangeError(`${t}.marks exceeds the per-node mark limit.`);if(s.markCount+e.marks.length>l)throw RangeError(`Rich-text document exceeds its aggregate mark limit.`);s.markCount+=e.marks.length,c.marks=e.marks.map((e,n)=>h(e,`${t}.marks[${n}]`,r,o,s)),_(c.marks,`${t}.marks`)}return g(c,t,r),c}function h(e,t,n,r,i){if(!B(e)||(I(e,t,[`attrs`,`type`]),typeof e.type!=`string`||e.type.length===0))throw TypeError(`${t} must be a mark with a non-empty type.`);if(!n.allowedMarks.includes(e.type))throw TypeError(`${t} uses disallowed mark type "${e.type}".`);let a={type:e.type};if(e.attrs!==void 0&&(a.attrs=O(e.attrs,`${t}.attrs`,`mark:${e.type}`,n,r,i)),a.type===`highlight`){let e=a.attrs?.tone;if(typeof e!=`string`||![`accent`,`danger`,`info`,`success`,`warning`].includes(e))throw TypeError(`${t}.attrs.tone must be a configured highlight tone.`)}else if(a.attrs!==void 0)throw TypeError(`${t} cannot carry attributes in the portable rich-text grammar.`);return a}function g(e,t,r){switch(e.type){case`doc`:if(T(e,t,[`attrs`,`marks`,`text`]),e.content===void 0||e.content.length===0)throw TypeError(`${t}.content must contain at least one block node.`);E(e.content,t,v);break;case`text`:if(T(e,t,[`attrs`,`content`]),e.text===void 0)throw TypeError(`${t}.text is required for a text node.`);if(e.text.length===0)throw TypeError(`${t}.text cannot be empty.`);break;case`paragraph`:T(e,t,[`attrs`,`marks`,`text`]),E(e.content??[],t,y);break;case`heading`:{T(e,t,[`marks`,`text`]),E(e.content??[],t,y);let i=e.attrs?.level,a=r.headingLevels??n;if(typeof i!=`number`||!Number.isInteger(i)||!a.includes(i))throw TypeError(`${t}.attrs.level must be a configured heading level.`);break}case`orderedList`:{T(e,t,[`marks`,`text`]),D(e.content,t,b);let n=e.attrs?.start;if(n!==void 0&&(!Number.isSafeInteger(n)||Number(n)<1))throw TypeError(`${t}.attrs.start must be a positive integer.`);break}case`bulletList`:T(e,t,[`attrs`,`marks`,`text`]),D(e.content,t,b);break;case`listItem`:if(T(e,t,[`attrs`,`marks`,`text`]),D(e.content,t,v),e.content?.[0]?.type!==`paragraph`)throw TypeError(`${t}.content must begin with a paragraph node.`);break;case`blockquote`:T(e,t,[`attrs`,`marks`,`text`]),D(e.content,t,v);break;case`callout`:if(T(e,t,[`marks`,`text`]),D(e.content,t,v),typeof e.attrs?.tone!=`string`||![`danger`,`info`,`success`,`warning`].includes(e.attrs.tone))throw TypeError(`${t}.attrs.tone must be a configured callout tone.`);break;case`checklist`:T(e,t,[`attrs`,`marks`,`text`]),D(e.content,t,x);break;case`checklistItem`:if(T(e,t,[`marks`,`text`]),E(e.content??[],t,y),typeof e.attrs?.checked!=`boolean`)throw TypeError(`${t}.attrs.checked must be a boolean.`);if(!Number.isSafeInteger(e.attrs.level)||Number(e.attrs.level)<0||Number(e.attrs.level)>4)throw TypeError(`${t}.attrs.level must be an integer from zero through four.`);break;case`table`:if(T(e,t,[`marks`,`text`]),D(e.content,t,S),typeof e.attrs?.header!=`boolean`)throw TypeError(`${t}.attrs.header must be a boolean.`);w(e.content,t);break;case`tableRow`:T(e,t,[`attrs`,`marks`,`text`]),D(e.content,t,C);break;case`tableCell`:T(e,t,[`attrs`,`marks`,`text`]),E(e.content??[],t,y);break;case`codeBlock`:if(T(e,t,[`content`,`marks`]),e.text===void 0)throw TypeError(`${t}.text is required for a code block.`);if(typeof e.attrs?.language!=`string`||!/^[A-Za-z0-9][A-Za-z0-9+_.#-]{0,63}$/u.test(e.attrs.language))throw TypeError(`${t}.attrs.language must be a bounded language identifier.`);break;case`hardBreak`:case`horizontalRule`:T(e,t,[`attrs`,`content`,`marks`,`text`]);break;default:throw TypeError(`${t} uses a node without a portable grammar.`)}}function _(e,t){let n=new Set;for(let r of e){if(n.has(r.type))throw TypeError(`${t} cannot contain duplicate ${r.type} marks.`);n.add(r.type)}if(n.has(`code`)&&n.size>1)throw TypeError(`${t} cannot combine code with another mark.`)}const v=new Set([`blockquote`,`bulletList`,`callout`,`checklist`,`codeBlock`,`heading`,`horizontalRule`,`orderedList`,`paragraph`,`table`]),y=new Set([`hardBreak`,`text`]),b=new Set([`listItem`]),x=new Set([`checklistItem`]),S=new Set([`tableRow`]),C=new Set([`tableCell`]);function w(e,t){let n=e?.[0]?.content?.length??0,r=e?.findIndex(e=>e.content?.length!==n)??-1;if(n<1||r>=0)throw TypeError(`${t}.content must be a non-empty rectangular table.`)}function T(e,t,n){let r=n.find(t=>e[t]!==void 0);if(r!==void 0)throw TypeError(`${t}.${r} is not valid on a ${e.type} node.`)}function E(e,t,n){let r=e.findIndex(e=>!n.has(e.type));if(r>=0)throw TypeError(`${t}.content[${r}] is not valid inside this node.`)}function D(e,t,n){if(e===void 0||e.length===0)throw TypeError(`${t}.content must contain at least one child node.`);E(e,t,n)}function O(e,t,n,r,i,a){let o=k(e,t,1,i,a),s=r.allowedAttributes[n]??[];for(let e of Object.keys(o))if(!s.includes(e))throw TypeError(`${t}.${e} is not allowed for ${n}.`);return o}function k(e,t,n,r,i){if(!B(e))throw TypeError(`${t} must be an object.`);M(n,t,r);let a=Object.entries(e);if(a.length>r.maximumPropertiesPerObject)throw RangeError(`${t} exceeds the attribute property limit.`);j(i,r,2);let o={};for(let[e,[s,c]]of a.entries())N(s,t,r),j(i,r,(e===0?0:1)+L(s)+1),o[s]=A(c,`${t}.${s}`,n+1,r,i);return o}function A(e,t,n,r,i){if(typeof e==`string`){if(e.length>r.maximumStringLength)throw RangeError(`${t} exceeds the attribute string limit.`);return j(i,r,L(e)),e}if(e===null||typeof e==`boolean`||typeof e==`number`&&Number.isFinite(e))return j(i,r,L(e)),e;if(Array.isArray(e))return M(n,t,r),P(e,t,r),j(i,r,2),e.map((e,a)=>(a>0&&j(i,r,1),A(e,`${t}[${a}]`,n+1,r,i)));if(B(e))return k(e,t,n,r,i);throw TypeError(`${t} is not JSON-compatible.`)}function j(e,t,n){if(e.attributeBytes+=n,e.attributeBytes>t.maximumTotalBytes)throw RangeError(`Rich-text attributes exceed the total-byte limit.`)}function M(e,t,n){if(e>n.maximumDepth)throw RangeError(`${t} exceeds the attribute depth limit.`)}function N(e,t,n){if(e===`__proto__`||e===`constructor`||e===`prototype`)throw TypeError(`${t}.${e} is a forbidden object key.`);if(e.length>n.maximumStringLength)throw RangeError(`${t} contains an attribute key that exceeds the string limit.`)}function P(e,t,n){if(e.length>n.maximumItemsPerArray)throw RangeError(`${t} exceeds the attribute item limit.`);let r=Object.keys(e);for(let e of r)if(e===`__proto__`||e===`constructor`||e===`prototype`)throw TypeError(`${t}.${e} is a forbidden object key.`);if(r.length!==e.length||r.some((e,t)=>e!==String(t)))throw TypeError(`${t} must be a dense JSON array without extra properties.`)}function F(e,t){if(Object.getPrototypeOf(e)!==Array.prototype||Object.getOwnPropertySymbols(e).length)throw TypeError(`${t} must be a dense JSON array without extra properties.`);let n=Object.getOwnPropertyNames(e);if(n.length!==e.length+1||n[e.length]!==`length`||n.slice(0,-1).some((e,t)=>e!==String(t)))throw TypeError(`${t} must be a dense JSON array without extra properties.`)}function I(e,t,n){let r=new Set(n),i=Object.keys(e).find(e=>!r.has(e));if(i!==void 0)throw TypeError(`${t}.${i} is not a recognized rich-text key.`)}function L(e){let t=JSON.stringify(e);if(t===void 0)throw TypeError(`Attribute value is not JSON-compatible.`);return R(t)}function R(e){let t=0;for(let n=0;n<e.length;n+=1){let r=e.charCodeAt(n);if(r<=127)t+=1;else if(r<=2047)t+=2;else if(r>=55296&&r<=56319){let r=e.charCodeAt(n+1);r>=56320&&r<=57343?(t+=4,n+=1):t+=3}else t+=3}return t}function z(e){return`${e.text??``}${(e.content??[]).map(e=>z(e)).join(``)}`}function B(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function V(r){for(let[e,t]of[[`maximumDepth`,r.maximumDepth],[`maximumNodes`,r.maximumNodes],[`maximumTextLength`,r.maximumTextLength]])H(e,t,o[e]);for(let[e,t]of[[`maximumDocumentBytes`,W(r)],[`maximumMarks`,r.maximumMarks??i],[`maximumMarksPerNode`,r.maximumMarksPerNode??a]])H(e,t,o[e]);if((r.maximumMarksPerNode??a)>(r.maximumMarks??i))throw RangeError(`maximumMarksPerNode cannot exceed maximumMarks.`);if(!r.allowedNodes.includes(`doc`)||!r.allowedNodes.includes(`text`))throw TypeError(`Rich-text profile must allow doc and text nodes.`);if(new Set(r.allowedNodes).size!==r.allowedNodes.length)throw TypeError(`Rich-text profile node names must be unique.`);if(new Set(r.allowedMarks).size!==r.allowedMarks.length)throw TypeError(`Rich-text profile mark names must be unique.`);let s=r.allowedNodes.find(e=>!t.includes(e));if(s!==void 0)throw TypeError(`Rich-text profile node "${s}" has no portable grammar.`);let c=r.allowedMarks.find(t=>!e.includes(t));if(c!==void 0)throw TypeError(`Rich-text profile mark "${c}" has no portable grammar.`);U(r.headingLevels??n),G(r)}function H(e,t,n){if(!Number.isInteger(t)||t<1)throw RangeError(`${e} must be a positive integer.`);if(t>n)throw RangeError(`${e} exceeds the immutable safety ceiling of ${n}.`)}function U(e){if(e.length===0||new Set(e).size!==e.length||e.some(e=>!Number.isInteger(e)||e<1||e>6))throw RangeError(`headingLevels must contain unique integer levels from 1 through 6.`)}function W(e){return e.maximumDocumentBytes??r}function G(e){let t={...DEFAULT_RICH_TEXT_ATTRIBUTE_LIMITS,...e.attributeLimits};for(let[e,n]of Object.entries(t))H(e,n,s[e]);return t}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type StudioRichTextDocument } from './index.js';
|
|
2
|
+
import { type StudioRichTextProfileId } from './profiles.js';
|
|
3
|
+
export interface StudioMarkdownOptions {
|
|
4
|
+
maximumInputCharacters?: number;
|
|
5
|
+
profile?: StudioRichTextProfileId;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Convert bounded Markdown into Studio's canonical grammar. Markdown is an
|
|
9
|
+
* import/export format only; parser state and source text are not canonical.
|
|
10
|
+
*/
|
|
11
|
+
export declare function importStudioMarkdown(source: string, options?: StudioMarkdownOptions): StudioRichTextDocument;
|
|
12
|
+
/** Produce deterministic portable Markdown from a canonical Studio document. */
|
|
13
|
+
export declare function exportStudioMarkdown(document: StudioRichTextDocument): string;
|
|
14
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAG5B,MAAM,YAAY,CAAC;AACpB,OAAO,EAA0B,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAErF,MAAM,WAAW,qBAAqB;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,EAAE,uBAAuB,CAAC;CACnC;AASD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,sBAAsB,CAgFxB;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM,CAG7E"}
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import{parseRichTextDocument as e}from"./index.js";import{resolveRichTextProfile as t}from"./profiles.js";const n=1e7,r=/^(\s{0,12})(\d{1,7})[.)]\s+(.+)$/u,i=/^(\s{0,12})[-+*]\s+(.+)$/u,a=/^(#{2,4})\s+(.+)$/u,o=/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/u;export function importStudioMarkdown(s,c={}){let f=c.maximumInputCharacters??1e6;if(!Number.isSafeInteger(f)||f<1||f>n)throw RangeError(`maximumInputCharacters must be between 1 and ${n}.`);if(s.length>f)throw RangeError(`Markdown input exceeds its configured character limit.`);let p=s.replaceAll(`\r
|
|
2
|
+
`,`
|
|
3
|
+
`).replaceAll(`\r`,`
|
|
4
|
+
`).split(`
|
|
5
|
+
`),m=[],h=[],g=()=>{h.length!==0&&(m.push({content:u(h),type:`paragraph`}),h=[])};for(let e=0;e<p.length;e+=1){let t=p[e]??``;if(t.trim().length===0){g();continue}let n=a.exec(t);if(n!==null){g(),m.push({attrs:{level:n[1]?.length??2},content:d(n[2]??``),type:`heading`});continue}if(o.test(t)){g(),m.push({type:`horizontalRule`});continue}if(t.startsWith(`> `)){g();let t=[];for(;e<p.length&&(p[e]??``).startsWith(`> `);)t.push((p[e]??``).slice(2)),e+=1;--e,m.push({content:[{content:u(t),type:`paragraph`}],type:`blockquote`});continue}if((r.exec(t)??i.exec(t))!==null){g();let n=r.test(t),a=[];for(;e<p.length;){let t=p[e]??``;if((n?r:i).test(t))a.push(t),e+=1;else break}--e,m.push(l(a,n));continue}h.push(t)}g();let _=m.length>0?m:[{type:`paragraph`}];return e({content:_,type:`doc`},t(c.profile))}export function exportStudioMarkdown(t){return e(t).content.map(e=>m(e,0)).join(`
|
|
6
|
+
|
|
7
|
+
`)}function l(e,t){let n=[];for(let a of e){let e=(t?r:i).exec(a);e!==null&&n.push({content:[{content:d(e[t?3:2]??``),type:`paragraph`}],type:`listItem`})}let a={content:n,type:t?`orderedList`:`bulletList`};if(t){let t=r.exec(e[0]??``),n=Number(t?.[2]??`1`);n!==1&&(a.attrs={start:n})}return a}function u(e){let t=[];for(let[n,r]of e.entries()){let i=r.endsWith(` `)||r.endsWith(`\\`),a=i?r.slice(0,-2):r;t.push(...d(a)),n<e.length-1&&(i?t.push({type:`hardBreak`}):t.push({text:` `,type:`text`}))}return p(t)}function d(e){let t=[],n=0,r=``,i=()=>{r.length>0&&(t.push({text:r,type:`text`}),r=``)};for(;n<e.length;){if(e[n]===`\\`&&n+1<e.length){r+=e[n+1]??``,n+=2;continue}let a=f(e,n);if(a===void 0){r+=e[n]??``,n+=1;continue}let o=e.indexOf(a.marker,n+a.marker.length);if(o<=n+a.marker.length){r+=a.marker,n+=a.marker.length;continue}i(),t.push({marks:[{type:a.mark}],text:e.slice(n+a.marker.length,o),type:`text`}),n=o+a.marker.length}return i(),p(t)}function f(e,t){for(let n of[{mark:`bold`,marker:`**`},{mark:`strike`,marker:`~~`},{mark:`code`,marker:"`"},{mark:`italic`,marker:`*`}])if(e.startsWith(n.marker,t))return n}function p(e){let t=[];for(let n of e){if(n.type===`text`&&n.text===``)continue;let e=t.at(-1);e?.type===`text`&&n.type===`text`&&JSON.stringify(e.marks??[])===JSON.stringify(n.marks??[])?e.text=`${e.text??``}${n.text??``}`:t.push(n)}return t}function m(e,t){switch(e.type){case`paragraph`:return g(e.content??[]);case`heading`:return`${`#`.repeat(Number(e.attrs?.level??2))} ${g(e.content??[])}`;case`horizontalRule`:return`---`;case`blockquote`:return(e.content??[]).map(e=>m(e,t)).join(`
|
|
8
|
+
`).split(`
|
|
9
|
+
`).map(e=>`> ${e}`).join(`
|
|
10
|
+
`);case`bulletList`:case`orderedList`:{let n=e.type===`orderedList`?Number(e.attrs?.start??1):1;return(e.content??[]).map((r,i)=>{let a=e.type===`orderedList`?`${n+i}.`:`-`;return`${` `.repeat(t)}${a} ${h(r,t+1)}`}).join(`
|
|
11
|
+
`)}default:return``}}function h(e,t){return(e.content??[]).map((e,n)=>{let r=m(e,t);return n===0?r:`\n${r}`}).join(``)}function g(e){return e.map(e=>{if(e.type===`hardBreak`)return`
|
|
12
|
+
`;if(e.type!==`text`)return``;let t=_(e.text??``);for(let n of e.marks??[]){let e=n.type===`bold`?`**`:n.type===`strike`?`~~`:n.type===`code`?"`":`*`;t=`${e}${t}${e}`}return t}).join(``)}function _(e){return e.replaceAll(`\\`,`\\\\`).replaceAll(/([*`~])/gu,`\\$1`)}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StudioRichTextProfile } from './index.js';
|
|
2
|
+
export type StudioRichTextProfileId = 'studio.rich-text/documentation' | 'studio.rich-text/marketing' | 'studio.rich-text/portable';
|
|
3
|
+
/** Page controls whose authored content is provided by the Studio rich-text boundary. */
|
|
4
|
+
export type StudioRichTextContainerType = 'studio.core/accordion-item' | 'studio.core/dialog' | 'studio.core/notice' | 'studio.core/popover' | 'studio.core/tab';
|
|
5
|
+
/** Smallest interoperable profile and the fail-closed default. */
|
|
6
|
+
export declare const PORTABLE_RICH_TEXT_PROFILE: Readonly<StudioRichTextProfile>;
|
|
7
|
+
/** Content-page prose profile. It intentionally has the same closed grammar with a smaller bound. */
|
|
8
|
+
export declare const MARKETING_RICH_TEXT_PROFILE: Readonly<StudioRichTextProfile>;
|
|
9
|
+
/** Documentation profile. Code is an inert inline mark; executable code is never accepted. */
|
|
10
|
+
export declare const DOCUMENTATION_RICH_TEXT_PROFILE: Readonly<StudioRichTextProfile>;
|
|
11
|
+
/** Resolve only a Studio-owned, versioned profile. Unknown host input never widens the grammar. */
|
|
12
|
+
export declare function resolveRichTextProfile(id?: StudioRichTextProfileId): Readonly<StudioRichTextProfile>;
|
|
13
|
+
/**
|
|
14
|
+
* Select the closed profile used for prose nested in first-party interactive
|
|
15
|
+
* page controls. Unknown runtime input fails closed instead of silently
|
|
16
|
+
* widening the editor grammar.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveContainerRichTextProfile(containerType: StudioRichTextContainerType): StudioRichTextProfileId;
|
|
19
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,uBAAuB,GACjC,gCAAgC,GAAG,4BAA4B,GAAG,2BAA2B,CAAC;AAEhG,yFAAyF;AACzF,MAAM,MAAM,2BAA2B,GACnC,4BAA4B,GAC5B,oBAAoB,GACpB,oBAAoB,GACpB,qBAAqB,GACrB,iBAAiB,CAAC;AAsDtB,kEAAkE;AAClE,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,qBAAqB,CAA2B,CAAC;AAEnG,qGAAqG;AACrG,eAAO,MAAM,2BAA2B,EAAE,QAAQ,CAAC,qBAAqB,CAA2B,CAAC;AAEpG,8FAA8F;AAC9F,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CAAC,qBAAqB,CAG3E,CAAC;AASF,mGAAmG;AACnG,wBAAgB,sBAAsB,CACpC,EAAE,GAAE,uBAAqD,GACxD,QAAQ,CAAC,qBAAqB,CAAC,CAMjC;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,2BAA2B,GACzC,uBAAuB,CAWzB"}
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=Object.freeze([`blockquote`,`bulletList`,`callout`,`checklist`,`checklistItem`,`codeBlock`,`doc`,`hardBreak`,`heading`,`horizontalRule`,`listItem`,`orderedList`,`paragraph`,`table`,`tableCell`,`tableRow`,`text`]),t=Object.freeze([`bold`,`code`,`highlight`,`italic`,`strike`]),n=Object.freeze({maximumDepth:8,maximumItemsPerArray:256,maximumPropertiesPerObject:64,maximumStringLength:4096,maximumTotalBytes:65536});function r(r,i){return Object.freeze({allowedAttributes:Object.freeze({callout:Object.freeze([`tone`]),checklistItem:Object.freeze([`checked`,`level`]),codeBlock:Object.freeze([`language`]),heading:Object.freeze([`level`]),"mark:highlight":Object.freeze([`tone`]),orderedList:Object.freeze([`start`]),table:Object.freeze([`header`])}),allowedMarks:t,allowedNodes:e,attributeLimits:n,headingLevels:Object.freeze([2,3,4]),maximumDepth:32,maximumDocumentBytes:1048576,maximumMarks:2e4,maximumMarksPerNode:t.length,maximumNodes:i,maximumTextLength:r})}export const PORTABLE_RICH_TEXT_PROFILE=r(25e4,5e3);export const MARKETING_RICH_TEXT_PROFILE=r(1e5,2e3);export const DOCUMENTATION_RICH_TEXT_PROFILE=r(5e5,1e4);const a=Object.freeze({"studio.rich-text/documentation":DOCUMENTATION_RICH_TEXT_PROFILE,"studio.rich-text/marketing":MARKETING_RICH_TEXT_PROFILE,"studio.rich-text/portable":PORTABLE_RICH_TEXT_PROFILE});export function resolveRichTextProfile(e=`studio.rich-text/portable`){let t=a[e];if(t===void 0)throw TypeError(`Unknown Studio rich-text profile "${e}".`);return t}export function resolveContainerRichTextProfile(e){switch(e){case`studio.core/accordion-item`:case`studio.core/dialog`:case`studio.core/notice`:case`studio.core/popover`:case`studio.core/tab`:return`studio.rich-text/marketing`;default:throw TypeError(`Unknown Studio rich-text container "${String(e)}".`)}}
|