@bayonai/rich-text-editor 0.1.2 → 1.0.0

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.
Files changed (151) hide show
  1. package/BEHAVIOR.md +396 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +25 -6
  4. package/dist/core/blockTree.d.ts +14 -0
  5. package/dist/core/blockTree.js +126 -0
  6. package/dist/core/blockTypes.d.ts +6 -0
  7. package/dist/core/blockTypes.js +5 -0
  8. package/dist/core/exportImport.d.ts +59 -0
  9. package/dist/core/exportImport.js +51 -0
  10. package/dist/core/features.d.ts +59 -0
  11. package/dist/core/features.js +57 -0
  12. package/dist/core/imageBlockDiagnostics.d.ts +4 -0
  13. package/dist/core/imageBlockDiagnostics.js +19 -0
  14. package/dist/core/proFeatures.d.ts +60 -0
  15. package/dist/core/proFeatures.js +64 -0
  16. package/dist/{richText.d.ts → core/richText.d.ts} +2 -0
  17. package/dist/core/richText.js +566 -0
  18. package/dist/core/types.d.ts +78 -0
  19. package/dist/index.d.ts +14 -8
  20. package/dist/index.js +8 -5
  21. package/dist/react/editor/RichTextBody.d.ts +28 -0
  22. package/dist/react/editor/RichTextBody.js +131 -0
  23. package/dist/react/editor/RichTextEditor.d.ts +138 -0
  24. package/dist/react/editor/RichTextEditor.js +2925 -0
  25. package/dist/react/editor/RichTextRenderedBlock.d.ts +20 -0
  26. package/dist/react/editor/RichTextRenderedBlock.js +162 -0
  27. package/dist/react/editor/RichTextRenderer.d.ts +13 -0
  28. package/dist/react/editor/RichTextRenderer.js +16 -0
  29. package/dist/react/{RichTextTitleInput.d.ts → editor/RichTextTitleInput.d.ts} +11 -1
  30. package/dist/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +17 -2
  31. package/dist/react/editor/blockActions.d.ts +48 -0
  32. package/dist/react/editor/blockActions.js +495 -0
  33. package/dist/react/editor/editorHistory.d.ts +55 -0
  34. package/dist/react/editor/editorHistory.js +111 -0
  35. package/dist/react/{editorNavigation.d.ts → editor/editorNavigation.d.ts} +2 -0
  36. package/dist/react/{editorNavigation.js → editor/editorNavigation.js} +16 -0
  37. package/dist/react/editor/editorOperations.d.ts +10 -0
  38. package/dist/react/editor/editorOperations.js +3 -0
  39. package/dist/react/editor/editorSelection.d.ts +3 -0
  40. package/dist/react/editor/editorSelection.js +215 -0
  41. package/dist/react/{editorShortcuts.d.ts → editor/editorShortcuts.d.ts} +10 -0
  42. package/dist/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  43. package/dist/react/{RichTextIcons.d.ts → icons/RichTextIcons.d.ts} +3 -0
  44. package/dist/react/{RichTextIcons.js → icons/RichTextIcons.js} +9 -0
  45. package/dist/react/index.d.ts +12 -9
  46. package/dist/react/index.js +7 -6
  47. package/dist/react/{EditorSessionProvider.d.ts → session/EditorSessionProvider.d.ts} +2 -2
  48. package/dist/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  49. package/dist/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  50. package/dist/react/styles/RichTextStyles.js +1362 -0
  51. package/dist/react/{BlockActionTool.d.ts → tools/BlockActionTool.d.ts} +1 -1
  52. package/dist/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  53. package/dist/react/tools/LinkCreationInput.d.ts +9 -0
  54. package/dist/react/tools/LinkCreationInput.js +38 -0
  55. package/dist/react/{SelectionFormatToolbar.d.ts → tools/SelectionFormatToolbar.d.ts} +3 -2
  56. package/dist/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  57. package/dist/react/tools/SpecialBlockOption.d.ts +9 -0
  58. package/dist/react/tools/SpecialBlockOption.js +8 -0
  59. package/dist/react/tools/SpecialBlockTool.d.ts +91 -0
  60. package/dist/react/tools/SpecialBlockTool.js +125 -0
  61. package/dist/react/{TranscriptionControl.d.ts → tools/TranscriptionControl.d.ts} +9 -0
  62. package/dist/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +70 -9
  63. package/dist/react/tools/blockActionToolState.d.ts +41 -0
  64. package/dist/react/tools/blockActionToolState.js +177 -0
  65. package/dist/react/tools/imageBlockDiagnostics.d.ts +2 -0
  66. package/dist/react/tools/imageBlockDiagnostics.js +12 -0
  67. package/dist/{session.d.ts → session/session.d.ts} +1 -1
  68. package/dist-cjs/core/blockTree.js +137 -0
  69. package/dist-cjs/core/blockTypes.js +9 -0
  70. package/dist-cjs/core/exportImport.js +56 -0
  71. package/dist-cjs/core/features.js +62 -0
  72. package/dist-cjs/core/proFeatures.js +70 -0
  73. package/dist-cjs/core/richText.js +578 -0
  74. package/dist-cjs/index.js +22 -6
  75. package/dist-cjs/react/editor/RichTextBody.js +134 -0
  76. package/dist-cjs/react/editor/RichTextEditor.js +2956 -0
  77. package/dist-cjs/react/editor/RichTextRenderedBlock.js +166 -0
  78. package/dist-cjs/react/editor/RichTextRenderer.js +20 -0
  79. package/dist-cjs/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +18 -2
  80. package/dist-cjs/react/editor/blockActions.js +518 -0
  81. package/dist-cjs/react/editor/editorHistory.js +120 -0
  82. package/dist-cjs/react/{editorNavigation.js → editor/editorNavigation.js} +17 -0
  83. package/dist-cjs/react/editor/editorOperations.js +6 -0
  84. package/dist-cjs/react/editor/editorSelection.js +219 -0
  85. package/dist-cjs/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  86. package/dist-cjs/react/{RichTextIcons.js → icons/RichTextIcons.js} +12 -0
  87. package/dist-cjs/react/index.js +9 -7
  88. package/dist-cjs/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  89. package/dist-cjs/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  90. package/dist-cjs/react/styles/RichTextStyles.js +1365 -0
  91. package/dist-cjs/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  92. package/dist-cjs/react/tools/LinkCreationInput.js +41 -0
  93. package/dist-cjs/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  94. package/dist-cjs/react/tools/SpecialBlockOption.js +11 -0
  95. package/dist-cjs/react/tools/SpecialBlockTool.js +129 -0
  96. package/dist-cjs/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +71 -9
  97. package/dist-cjs/react/tools/blockActionToolState.js +186 -0
  98. package/package.json +3 -2
  99. package/dist/react/RichTextBody.d.ts +0 -18
  100. package/dist/react/RichTextBody.js +0 -66
  101. package/dist/react/RichTextEditor.d.ts +0 -45
  102. package/dist/react/RichTextEditor.js +0 -1096
  103. package/dist/react/RichTextRenderedBlock.d.ts +0 -4
  104. package/dist/react/RichTextRenderedBlock.js +0 -36
  105. package/dist/react/RichTextRenderer.d.ts +0 -4
  106. package/dist/react/RichTextRenderer.js +0 -8
  107. package/dist/react/RichTextStyles.js +0 -719
  108. package/dist/react/SpecialBlockOption.d.ts +0 -7
  109. package/dist/react/SpecialBlockOption.js +0 -7
  110. package/dist/react/SpecialBlockTool.d.ts +0 -42
  111. package/dist/react/SpecialBlockTool.js +0 -50
  112. package/dist/react/blockActionToolState.d.ts +0 -18
  113. package/dist/react/blockActionToolState.js +0 -53
  114. package/dist/react/blockActions.d.ts +0 -8
  115. package/dist/react/blockActions.js +0 -111
  116. package/dist/richText.js +0 -297
  117. package/dist/types.d.ts +0 -34
  118. package/dist-cjs/react/RichTextBody.js +0 -69
  119. package/dist-cjs/react/RichTextEditor.js +0 -1108
  120. package/dist-cjs/react/RichTextRenderedBlock.js +0 -39
  121. package/dist-cjs/react/RichTextRenderer.js +0 -11
  122. package/dist-cjs/react/RichTextStyles.js +0 -722
  123. package/dist-cjs/react/SpecialBlockOption.js +0 -10
  124. package/dist-cjs/react/SpecialBlockTool.js +0 -54
  125. package/dist-cjs/react/blockActionToolState.js +0 -58
  126. package/dist-cjs/react/blockActions.js +0 -119
  127. package/dist-cjs/richText.js +0 -307
  128. /package/dist/{types.js → core/types.js} +0 -0
  129. /package/dist/{writingStats.d.ts → core/writingStats.d.ts} +0 -0
  130. /package/dist/{writingStats.js → core/writingStats.js} +0 -0
  131. /package/dist/react/{RichTextDocumentSurface.d.ts → editor/RichTextDocumentSurface.d.ts} +0 -0
  132. /package/dist/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  133. /package/dist/react/{UnsavedChangesDialog.d.ts → session/UnsavedChangesDialog.d.ts} +0 -0
  134. /package/dist/react/{RichTextStyles.d.ts → styles/RichTextStyles.d.ts} +0 -0
  135. /package/dist/react/{richTextBlockStyles.d.ts → styles/richTextBlockStyles.d.ts} +0 -0
  136. /package/dist/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  137. /package/dist/react/{specialBlockStyles.d.ts → styles/specialBlockStyles.d.ts} +0 -0
  138. /package/dist/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  139. /package/dist/{saveControl.d.ts → session/saveControl.d.ts} +0 -0
  140. /package/dist/{saveControl.js → session/saveControl.js} +0 -0
  141. /package/dist/{session.js → session/session.js} +0 -0
  142. /package/dist/{sessionRegistry.d.ts → session/sessionRegistry.d.ts} +0 -0
  143. /package/dist/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
  144. /package/dist-cjs/{types.js → core/types.js} +0 -0
  145. /package/dist-cjs/{writingStats.js → core/writingStats.js} +0 -0
  146. /package/dist-cjs/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  147. /package/dist-cjs/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  148. /package/dist-cjs/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  149. /package/dist-cjs/{saveControl.js → session/saveControl.js} +0 -0
  150. /package/dist-cjs/{session.js → session/session.js} +0 -0
  151. /package/dist-cjs/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
@@ -0,0 +1,566 @@
1
+ const allowedInlineTags = new Set(["a", "br", "code", "em", "strong"]);
2
+ const textBlockTypes = new Set([
3
+ "paragraph",
4
+ "message",
5
+ "heading",
6
+ "quote",
7
+ "bullet",
8
+ "ordered",
9
+ "checkbox",
10
+ "toggle",
11
+ ]);
12
+ const richTextClipboardPrefix = "bounded-rich-text-blocks-v1:";
13
+ const toolBlockStatuses = new Set([
14
+ "pending",
15
+ "success",
16
+ "error",
17
+ ]);
18
+ const invalidToolData = Symbol("invalidToolData");
19
+ const maxToolDataDepth = 8;
20
+ export function createEmptyRichTextBlocks() {
21
+ return [{ id: createRichTextBlockId(), type: "paragraph", markdown: "" }];
22
+ }
23
+ export function createRichTextBlockId() {
24
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
25
+ return crypto.randomUUID();
26
+ }
27
+ return `block-${Date.now()}-${Math.random().toString(16).slice(2)}`;
28
+ }
29
+ /**
30
+ * Sanitizes an array of RichTextBlock objects by:
31
+ * - Filtering out invalid blocks
32
+ * - Ensuring each block has a unique ID
33
+ * - Returning an empty array if no valid blocks remain
34
+ */
35
+ export function sanitizeRichTextBlocks(value) {
36
+ return sanitizeRichTextBlocksWithSeen(value, new Set());
37
+ }
38
+ function sanitizeRichTextBlocksWithSeen(value, seenBlockIds) {
39
+ const blocks = Array.isArray(value) ? value : [];
40
+ const sanitized = blocks
41
+ .flatMap((block) => sanitizeRichTextBlock(block, seenBlockIds))
42
+ .map((block) => {
43
+ if (!seenBlockIds.has(block.id)) {
44
+ seenBlockIds.add(block.id);
45
+ return block;
46
+ }
47
+ const blockWithUniqueId = {
48
+ ...block,
49
+ id: createUniqueRichTextBlockId(seenBlockIds),
50
+ };
51
+ seenBlockIds.add(blockWithUniqueId.id);
52
+ return blockWithUniqueId;
53
+ });
54
+ return sanitized.length > 0 ? sanitized : createEmptyRichTextBlocks();
55
+ }
56
+ export function isRichTextBlocksEmpty(blocks) {
57
+ return sanitizeRichTextBlocks(blocks).every((block) => {
58
+ if (block.type === "divider") {
59
+ return false;
60
+ }
61
+ if (block.type === "checkbox") {
62
+ return markdownToPlainText(block.markdown).trim() === "";
63
+ }
64
+ if (block.type === "image") {
65
+ return !block.url && typeof block.uploadProgress !== "number";
66
+ }
67
+ if (block.type === "tool") {
68
+ return false;
69
+ }
70
+ const text = block.type === "code" ? block.text : block.markdown;
71
+ return markdownToPlainText(text).trim() === "";
72
+ });
73
+ }
74
+ export function richTextBlocksToPlainText(blocks) {
75
+ return sanitizeRichTextBlocks(blocks)
76
+ .map((block) => {
77
+ if (block.type === "divider") {
78
+ return "";
79
+ }
80
+ if (block.type === "checkbox") {
81
+ return [
82
+ markdownToPlainText(block.markdown),
83
+ ...richTextBlocksToPlainText(block.children ?? [])
84
+ .split(" ")
85
+ .filter(Boolean),
86
+ ]
87
+ .filter(Boolean)
88
+ .join(" ");
89
+ }
90
+ if (block.type === "image") {
91
+ return block.alt ?? "";
92
+ }
93
+ if (block.type === "tool") {
94
+ return [
95
+ `[Tool: ${block.title || block.toolType} ${block.status}]`,
96
+ block.markdown ? markdownToPlainText(block.markdown) : "",
97
+ ]
98
+ .filter(Boolean)
99
+ .join(" ");
100
+ }
101
+ if (block.type === "bullet" ||
102
+ block.type === "ordered" ||
103
+ block.type === "toggle") {
104
+ return [
105
+ markdownToPlainText(block.markdown),
106
+ richTextBlocksToPlainText(block.children ?? []),
107
+ ]
108
+ .filter(Boolean)
109
+ .join(" ");
110
+ }
111
+ return block.type === "code"
112
+ ? block.text
113
+ : markdownToPlainText(block.markdown);
114
+ })
115
+ .join(" ")
116
+ .replace(/\s+/g, " ")
117
+ .trim();
118
+ }
119
+ export function encodeRichTextBlocksForClipboard(blocks) {
120
+ const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
121
+ const encoded = encodeURIComponent(JSON.stringify(sanitizedBlocks));
122
+ const plainText = richTextBlocksToPlainText(sanitizedBlocks);
123
+ return `${richTextClipboardPrefix}${encoded}${plainText ? `\n${plainText}` : ""}`;
124
+ }
125
+ export function decodeRichTextBlocksFromClipboardText(value) {
126
+ const [firstLine] = value.split(/\r?\n/, 1);
127
+ if (!firstLine?.startsWith(richTextClipboardPrefix)) {
128
+ return null;
129
+ }
130
+ try {
131
+ const decoded = decodeURIComponent(firstLine.slice(richTextClipboardPrefix.length));
132
+ const parsed = JSON.parse(decoded);
133
+ return Array.isArray(parsed) ? sanitizeRichTextBlocks(parsed) : null;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ export function editorHtmlToMarkdown(html) {
140
+ const sanitized = normalizeTypography(decodeHtmlText(sanitizeRichTextHtml(html)
141
+ .replace(/<br>/g, " \n")
142
+ .replace(/<strong>([\s\S]*?)<\/strong>/g, (_, value) => {
143
+ return `**${decodeHtmlText(stripTags(value))}**`;
144
+ })
145
+ .replace(/<em>([\s\S]*?)<\/em>/g, (_, value) => {
146
+ return `_${decodeHtmlText(stripTags(value))}_`;
147
+ })
148
+ .replace(/<code>([\s\S]*?)<\/code>/g, (_, value) => {
149
+ return `\`${decodeHtmlText(stripTags(value)).replace(/`/g, "\\`")}\``;
150
+ })
151
+ .replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (_, href, value) => {
152
+ return `[${decodeHtmlText(stripTags(value))}](${decodeHtmlText(href)})`;
153
+ })
154
+ .replace(/<a>([\s\S]*?)<\/a>/g, (_, value) => {
155
+ return decodeHtmlText(stripTags(value));
156
+ })
157
+ .replace(/<[^>]+>/g, ""))).replace(/\u00a0/g, " ");
158
+ return stripWhitespaceOnlyMarkdown(sanitized);
159
+ }
160
+ export function markdownToEditorHtml(markdown) {
161
+ const tokens = [];
162
+ let html = escapeHtmlText(normalizeTypography(markdown));
163
+ html = html.replace(/`([^`\n]+)`/g, (_, value) => {
164
+ const token = pushToken(tokens, `<code>${value}</code>`);
165
+ return token;
166
+ });
167
+ html = html.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
168
+ const safeHref = getSafeUrl(decodeHtmlText(href));
169
+ return safeHref ? `<a href="${safeHref}">${label}</a>` : label;
170
+ });
171
+ html = html
172
+ .replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>")
173
+ .replace(/_([^_\n]+)_/g, "<em>$1</em>")
174
+ .replace(/ {2}\n/g, "<br>")
175
+ .replace(/\n/g, "<br>");
176
+ tokens.forEach((value, index) => {
177
+ html = html.replace(tokenPlaceholder(index), value);
178
+ });
179
+ return html;
180
+ }
181
+ export function sanitizeRichTextHtml(html) {
182
+ return html
183
+ .replace(/<!--[\s\S]*?-->/g, "")
184
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
185
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
186
+ .replace(/<b(\s[^>]*)?>/gi, "<strong>")
187
+ .replace(/<\/b>/gi, "</strong>")
188
+ .replace(/<i(\s[^>]*)?>/gi, "<em>")
189
+ .replace(/<\/i>/gi, "</em>")
190
+ .replace(/<\/?([a-z0-9]+)([^>]*)>/gi, (match, rawTag, rawAttrs) => {
191
+ const tag = String(rawTag).toLowerCase();
192
+ const closing = match.startsWith("</");
193
+ if (!allowedInlineTags.has(tag)) {
194
+ return "";
195
+ }
196
+ if (closing) {
197
+ return tag === "br" ? "" : `</${tag}>`;
198
+ }
199
+ if (tag === "a") {
200
+ const href = getSafeAttribute(rawAttrs, "href");
201
+ return href ? `<a href="${href}">` : "<a>";
202
+ }
203
+ return tag === "br" ? "<br>" : `<${tag}>`;
204
+ });
205
+ }
206
+ function sanitizeRichTextBlock(block, seenBlockIds) {
207
+ if (!isRecord(block)) {
208
+ return [];
209
+ }
210
+ const id = readBlockId(block);
211
+ const type = block.type;
212
+ if (type === "code") {
213
+ return [{ id, type, text: String(block.text ?? "") }];
214
+ }
215
+ if (type === "divider") {
216
+ return [{ id, type }];
217
+ }
218
+ if (type === "image") {
219
+ const safeUrl = typeof block.url === "string" && getSafeUrl(block.url)
220
+ ? getSafeUrl(block.url)
221
+ : "";
222
+ const uploadProgress = typeof block.uploadProgress === "number" &&
223
+ Number.isFinite(block.uploadProgress)
224
+ ? clamp(Math.round(block.uploadProgress), 0, 100)
225
+ : null;
226
+ const displaySize = readImageDisplaySize(block.displaySize);
227
+ const customWidth = readImageCustomWidth(block.customWidth);
228
+ if (!safeUrl && uploadProgress === null) {
229
+ return [];
230
+ }
231
+ return [
232
+ {
233
+ id,
234
+ type,
235
+ alignment: readImageAlignment(block.alignment),
236
+ ...(displaySize ? { displaySize } : {}),
237
+ ...(displaySize === "custom" && customWidth
238
+ ? { customWidth }
239
+ : {}),
240
+ ...(safeUrl ? { url: safeUrl } : {}),
241
+ ...(typeof block.alt === "string" && block.alt.trim()
242
+ ? { alt: markdownToPlainText(editorHtmlToMarkdown(block.alt)) }
243
+ : {}),
244
+ ...(uploadProgress !== null ? { uploadProgress } : {}),
245
+ },
246
+ ];
247
+ }
248
+ if (type === "tool") {
249
+ return [sanitizeToolBlock(block, id)];
250
+ }
251
+ if (typeof type === "string" && textBlockTypes.has(type)) {
252
+ const markdown = typeof block.markdown === "string"
253
+ ? block.markdown
254
+ : typeof block.text === "string"
255
+ ? block.text
256
+ : typeof block.html === "string"
257
+ ? editorHtmlToMarkdown(block.html)
258
+ : "";
259
+ if (type === "bullet" || type === "ordered") {
260
+ return [
261
+ withSanitizedChildren({
262
+ id,
263
+ type: type,
264
+ markdown: sanitizeMarkdown(markdown),
265
+ }, block, seenBlockIds),
266
+ ];
267
+ }
268
+ if (type === "checkbox") {
269
+ return [
270
+ withSanitizedChildren({
271
+ id,
272
+ type,
273
+ checked: Boolean(block.checked),
274
+ markdown: sanitizeMarkdown(markdown),
275
+ }, block, seenBlockIds),
276
+ ];
277
+ }
278
+ if (type === "toggle") {
279
+ const children = sanitizeRichTextChildBlocksWithSeen(block.children, seenBlockIds);
280
+ return [
281
+ {
282
+ id,
283
+ type,
284
+ collapsed: Boolean(block.collapsed),
285
+ markdown: sanitizeMarkdown(markdown),
286
+ children,
287
+ },
288
+ ];
289
+ }
290
+ return [
291
+ {
292
+ id,
293
+ type: type,
294
+ markdown: sanitizeMarkdown(markdown),
295
+ },
296
+ ];
297
+ }
298
+ return [];
299
+ }
300
+ function sanitizeToolBlock(block, id) {
301
+ const toolType = readToolType(block.toolType, id);
302
+ const status = readToolStatus(block.status, id);
303
+ const title = typeof block.title === "string" && block.title.trim()
304
+ ? markdownToPlainText(editorHtmlToMarkdown(block.title))
305
+ : "";
306
+ const markdown = typeof block.markdown === "string"
307
+ ? sanitizeMarkdown(block.markdown)
308
+ : typeof block.text === "string"
309
+ ? sanitizeMarkdown(block.text)
310
+ : typeof block.html === "string"
311
+ ? sanitizeMarkdown(editorHtmlToMarkdown(block.html))
312
+ : "";
313
+ const dataResult = "data" in block ? sanitizeToolData(block.data, id) : null;
314
+ return {
315
+ id,
316
+ type: "tool",
317
+ toolType,
318
+ status,
319
+ ...(title ? { title } : {}),
320
+ ...(markdown ? { markdown } : {}),
321
+ ...(dataResult?.value !== undefined ? { data: dataResult.value } : {}),
322
+ };
323
+ }
324
+ function readToolType(value, blockId) {
325
+ if (typeof value === "string" && value.trim()) {
326
+ return value.trim();
327
+ }
328
+ console.warn("[RichText] Tool block has an invalid toolType.", { blockId });
329
+ return "unknown";
330
+ }
331
+ function readToolStatus(value, blockId) {
332
+ if (typeof value === "string" &&
333
+ toolBlockStatuses.has(value)) {
334
+ return value;
335
+ }
336
+ console.warn("[RichText] Tool block has an invalid status.", {
337
+ blockId,
338
+ status: value,
339
+ });
340
+ return "success";
341
+ }
342
+ function sanitizeToolData(value, blockId) {
343
+ const result = sanitizeToolDataValue(value, {
344
+ depth: 0,
345
+ seen: new WeakSet(),
346
+ });
347
+ if (result.changed) {
348
+ console.warn("[RichText] Tool block data contained unsupported values.", {
349
+ blockId,
350
+ });
351
+ }
352
+ return result.value === invalidToolData
353
+ ? null
354
+ : { changed: result.changed, value: result.value };
355
+ }
356
+ function sanitizeToolDataValue(value, context) {
357
+ if (value === null ||
358
+ typeof value === "boolean" ||
359
+ typeof value === "string") {
360
+ return { changed: false, value };
361
+ }
362
+ if (typeof value === "number") {
363
+ return Number.isFinite(value)
364
+ ? { changed: false, value }
365
+ : { changed: true, value: invalidToolData };
366
+ }
367
+ if (typeof value !== "object" || value === undefined) {
368
+ return { changed: true, value: invalidToolData };
369
+ }
370
+ if (context.depth >= maxToolDataDepth) {
371
+ return { changed: true, value: invalidToolData };
372
+ }
373
+ if (context.seen.has(value)) {
374
+ return { changed: true, value: invalidToolData };
375
+ }
376
+ context.seen.add(value);
377
+ if (Array.isArray(value)) {
378
+ let changed = false;
379
+ const sanitizedArray = [];
380
+ value.forEach((item) => {
381
+ const result = sanitizeToolDataValue(item, {
382
+ depth: context.depth + 1,
383
+ seen: context.seen,
384
+ });
385
+ if (result.value === invalidToolData) {
386
+ changed = true;
387
+ return;
388
+ }
389
+ changed = changed || result.changed;
390
+ sanitizedArray.push(result.value);
391
+ });
392
+ context.seen.delete(value);
393
+ return { changed, value: sanitizedArray };
394
+ }
395
+ const prototype = Object.getPrototypeOf(value);
396
+ if (prototype !== Object.prototype && prototype !== null) {
397
+ context.seen.delete(value);
398
+ return { changed: true, value: invalidToolData };
399
+ }
400
+ let changed = false;
401
+ const sanitizedRecord = {};
402
+ Object.entries(value).forEach(([key, item]) => {
403
+ const result = sanitizeToolDataValue(item, {
404
+ depth: context.depth + 1,
405
+ seen: context.seen,
406
+ });
407
+ if (result.value === invalidToolData) {
408
+ changed = true;
409
+ return;
410
+ }
411
+ changed = changed || result.changed;
412
+ sanitizedRecord[key] = result.value;
413
+ });
414
+ context.seen.delete(value);
415
+ return { changed, value: sanitizedRecord };
416
+ }
417
+ function withSanitizedChildren(sanitizedBlock, rawBlock, seenBlockIds) {
418
+ const children = sanitizeRichTextChildBlocksWithSeen(rawBlock.children, seenBlockIds);
419
+ if (children.length === 0) {
420
+ return sanitizedBlock;
421
+ }
422
+ return { ...sanitizedBlock, children };
423
+ }
424
+ function sanitizeRichTextChildBlocksWithSeen(value, seenBlockIds) {
425
+ if (!Array.isArray(value)) {
426
+ return [];
427
+ }
428
+ return value
429
+ .flatMap((block) => sanitizeRichTextBlock(block, seenBlockIds))
430
+ .map((block) => {
431
+ if (!seenBlockIds.has(block.id)) {
432
+ seenBlockIds.add(block.id);
433
+ return block;
434
+ }
435
+ const blockWithUniqueId = {
436
+ ...block,
437
+ id: createUniqueRichTextBlockId(seenBlockIds),
438
+ };
439
+ seenBlockIds.add(blockWithUniqueId.id);
440
+ return blockWithUniqueId;
441
+ });
442
+ }
443
+ function readBlockId(block) {
444
+ return typeof block.id === "string" && block.id.trim()
445
+ ? block.id.trim()
446
+ : createRichTextBlockId();
447
+ }
448
+ function createUniqueRichTextBlockId(existingIds) {
449
+ let id = createRichTextBlockId();
450
+ while (existingIds.has(id)) {
451
+ id = createRichTextBlockId();
452
+ }
453
+ return id;
454
+ }
455
+ function markdownToPlainText(value) {
456
+ return value
457
+ .replace(/`([^`]*)`/g, "$1")
458
+ .replace(/\*\*([^*]*)\*\*/g, "$1")
459
+ .replace(/_([^_]*)_/g, "$1")
460
+ .replace(/\[([^\]]*)\]\([^)]+\)/g, "$1")
461
+ .replace(/[#>*`_[\]()]/g, " ")
462
+ .replace(/\s+/g, " ")
463
+ .trim();
464
+ }
465
+ function sanitizeMarkdown(value) {
466
+ return stripWhitespaceOnlyMarkdown(normalizeTypography(value).replace(/<!--[\s\S]*?-->/g, ""));
467
+ }
468
+ function stripWhitespaceOnlyMarkdown(value) {
469
+ return value.trim().length > 0 ? value : "";
470
+ }
471
+ function normalizeTypography(value) {
472
+ const tokens = [];
473
+ let normalized = value.replace(/`([^`\n]+)`/g, (match) => {
474
+ return pushToken(tokens, match);
475
+ });
476
+ normalized = normalized.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_match, label, href) => {
477
+ return `[${label}](${pushToken(tokens, href)})`;
478
+ });
479
+ normalized = normalized
480
+ .replace(/––/g, "—")
481
+ .replace(/–-/g, "—")
482
+ .replace(/---/g, "—")
483
+ .replace(/--/g, "–");
484
+ tokens.forEach((token, index) => {
485
+ normalized = normalized.replace(tokenPlaceholder(index), token);
486
+ });
487
+ return normalized;
488
+ }
489
+ function stripTags(value) {
490
+ return value.replace(/<[^>]+>/g, "");
491
+ }
492
+ function getSafeAttribute(attrs, name) {
493
+ const value = getPlainAttribute(attrs, name);
494
+ const safeValue = getSafeUrl(value);
495
+ if (!safeValue) {
496
+ return "";
497
+ }
498
+ return escapeAttribute(safeValue);
499
+ }
500
+ function getSafeUrl(value) {
501
+ const trimmed = value.trim();
502
+ if (!trimmed || /^javascript:/i.test(trimmed)) {
503
+ return "";
504
+ }
505
+ return trimmed;
506
+ }
507
+ function getPlainAttribute(attrs, name) {
508
+ const pattern = new RegExp(`${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i");
509
+ const match = attrs.match(pattern);
510
+ return match?.[2] ?? match?.[3] ?? match?.[4] ?? "";
511
+ }
512
+ function escapeAttribute(value) {
513
+ return value
514
+ .replace(/&/g, "&amp;")
515
+ .replace(/</g, "&lt;")
516
+ .replace(/>/g, "&gt;")
517
+ .replace(/"/g, "&quot;");
518
+ }
519
+ function escapeHtmlText(value) {
520
+ return value
521
+ .replace(/&/g, "&amp;")
522
+ .replace(/</g, "&lt;")
523
+ .replace(/>/g, "&gt;");
524
+ }
525
+ function decodeHtmlText(value) {
526
+ return value
527
+ .replace(/&nbsp;/g, " ")
528
+ .replace(/&#160;/g, " ")
529
+ .replace(/&#xA0;/gi, " ")
530
+ .replace(/&amp;/g, "&")
531
+ .replace(/&lt;/g, "<")
532
+ .replace(/&gt;/g, ">")
533
+ .replace(/&quot;/g, '"')
534
+ .replace(/&apos;/g, "'")
535
+ .replace(/&#39;/g, "'");
536
+ }
537
+ function pushToken(tokens, value) {
538
+ const index = tokens.push(value) - 1;
539
+ return tokenPlaceholder(index);
540
+ }
541
+ function tokenPlaceholder(index) {
542
+ return `@@RICHTEXTTOKEN${index}@@`;
543
+ }
544
+ function clamp(value, min, max) {
545
+ return Math.min(Math.max(value, min), max);
546
+ }
547
+ function readImageAlignment(value) {
548
+ return value === "left" || value === "right" ? value : "center";
549
+ }
550
+ function readImageDisplaySize(value) {
551
+ return value === "small" ||
552
+ value === "medium" ||
553
+ value === "full" ||
554
+ value === "custom"
555
+ ? value
556
+ : null;
557
+ }
558
+ function readImageCustomWidth(value) {
559
+ if (typeof value !== "number" || !Number.isFinite(value)) {
560
+ return null;
561
+ }
562
+ return clamp(Math.round(value), 120, 1600);
563
+ }
564
+ function isRecord(value) {
565
+ return typeof value === "object" && value !== null;
566
+ }
@@ -0,0 +1,78 @@
1
+ export type RichTextJsonValue = null | boolean | number | string | RichTextJsonValue[] | {
2
+ [key: string]: RichTextJsonValue;
3
+ };
4
+ export type RichTextToolStatus = "pending" | "success" | "error";
5
+ export type RichTextImageDisplaySize = "small" | "medium" | "full" | "custom";
6
+ export type RichTextBlock = {
7
+ id: string;
8
+ type: "paragraph";
9
+ markdown: string;
10
+ } | {
11
+ id: string;
12
+ type: "message";
13
+ markdown: string;
14
+ } | {
15
+ id: string;
16
+ type: "heading";
17
+ markdown: string;
18
+ } | {
19
+ id: string;
20
+ type: "quote";
21
+ markdown: string;
22
+ } | {
23
+ id: string;
24
+ type: "bullet";
25
+ markdown: string;
26
+ children?: RichTextBlock[];
27
+ } | {
28
+ id: string;
29
+ type: "ordered";
30
+ markdown: string;
31
+ children?: RichTextBlock[];
32
+ } | {
33
+ id: string;
34
+ type: "checkbox";
35
+ checked: boolean;
36
+ markdown: string;
37
+ children?: RichTextBlock[];
38
+ } | {
39
+ id: string;
40
+ type: "toggle";
41
+ collapsed: boolean;
42
+ markdown: string;
43
+ children: RichTextBlock[];
44
+ } | {
45
+ id: string;
46
+ type: "code";
47
+ text: string;
48
+ } | {
49
+ id: string;
50
+ type: "divider";
51
+ } | {
52
+ id: string;
53
+ type: "tool";
54
+ toolType: string;
55
+ status: RichTextToolStatus;
56
+ title?: string;
57
+ markdown?: string;
58
+ data?: RichTextJsonValue;
59
+ } | {
60
+ id: string;
61
+ type: "image";
62
+ alignment?: "center" | "left" | "right";
63
+ alt?: string;
64
+ customWidth?: number;
65
+ displaySize?: RichTextImageDisplaySize;
66
+ uploadProgress?: number;
67
+ url?: string;
68
+ };
69
+ export type RichTextBlockPath = number[];
70
+ export type RichTextSelectionSnapshot = {
71
+ affinity: "start" | "end";
72
+ blockId: string;
73
+ offset: number;
74
+ };
75
+ export type RichTextDocument = {
76
+ contentBlocks: RichTextBlock[];
77
+ title: string;
78
+ };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
- export { canonicalSerialize, createEditorSession, defaultEditorSessionEquals, } from "./session";
2
- export { getEditorSaveControlState } from "./saveControl";
3
- export { createEditorSessionRegistry } from "./sessionRegistry";
4
- export { createEmptyRichTextBlocks, createRichTextBlockId, editorHtmlToMarkdown, isRichTextBlocksEmpty, markdownToEditorHtml, richTextBlocksToPlainText, sanitizeRichTextBlocks, sanitizeRichTextHtml, } from "./richText";
5
- export { getWritingStats } from "./writingStats";
6
- export type { EditorDirtyScope, EditorSession, EditorSessionOptions, EditorSessionValue, } from "./session";
7
- export type { EditorExitAction, EditorSessionRegistry, EditorSessionRegistrySnapshot, } from "./sessionRegistry";
8
- export type { RichTextBlock, RichTextDocument } from "./types";
1
+ export { canonicalSerialize, createEditorSession, defaultEditorSessionEquals, } from "./session/session";
2
+ export { getEditorSaveControlState } from "./session/saveControl";
3
+ export { createEditorSessionRegistry } from "./session/sessionRegistry";
4
+ export { createEmptyRichTextBlocks, createRichTextBlockId, decodeRichTextBlocksFromClipboardText, encodeRichTextBlocksForClipboard, editorHtmlToMarkdown, isRichTextBlocksEmpty, markdownToEditorHtml, richTextBlocksToPlainText, sanitizeRichTextBlocks, sanitizeRichTextHtml, } from "./core/richText";
5
+ export { getRichTextFeatureAccess, isRichTextFeatureEnabled, richTextEditorFeatureCatalog, } from "./core/features";
6
+ export { exportRichTextBlocks, importRichTextBlocks, richTextJsonExportAdapter, richTextJsonImportAdapter, } from "./core/exportImport";
7
+ export { connectRichTextCollaboration, createRichTextAnnotations, instantiateRichTextTemplate, runRichTextSchemaMigrations, } from "./core/proFeatures";
8
+ export { getWritingStats } from "./core/writingStats";
9
+ export type { RichTextExportAdapter, RichTextExportImportResult, RichTextExportPayload, RichTextImportAdapter, } from "./core/exportImport";
10
+ export type { RichTextAnnotation, RichTextCollaborationAdapter, RichTextSchemaMigration, RichTextSchemaMigrationResult, RichTextTemplate, } from "./core/proFeatures";
11
+ export type { RichTextEditorEntitlements, RichTextEditorFeatureId, RichTextEditorFeatureTier, RichTextFeatureAccess, RichTextFeatureGate, RichTextFeatureGateReason, RichTextLockedFeatureMode, } from "./core/features";
12
+ export type { EditorDirtyScope, EditorSession, EditorSessionOptions, EditorSessionValue, } from "./session/session";
13
+ export type { EditorExitAction, EditorSessionRegistry, EditorSessionRegistrySnapshot, } from "./session/sessionRegistry";
14
+ export type { RichTextBlock, RichTextBlockPath, RichTextDocument, RichTextJsonValue, RichTextSelectionSnapshot, RichTextToolStatus, } from "./core/types";
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
- export { canonicalSerialize, createEditorSession, defaultEditorSessionEquals, } from "./session.js";
2
- export { getEditorSaveControlState } from "./saveControl.js";
3
- export { createEditorSessionRegistry } from "./sessionRegistry.js";
4
- export { createEmptyRichTextBlocks, createRichTextBlockId, editorHtmlToMarkdown, isRichTextBlocksEmpty, markdownToEditorHtml, richTextBlocksToPlainText, sanitizeRichTextBlocks, sanitizeRichTextHtml, } from "./richText.js";
5
- export { getWritingStats } from "./writingStats.js";
1
+ export { canonicalSerialize, createEditorSession, defaultEditorSessionEquals, } from "./session/session.js";
2
+ export { getEditorSaveControlState } from "./session/saveControl.js";
3
+ export { createEditorSessionRegistry } from "./session/sessionRegistry.js";
4
+ export { createEmptyRichTextBlocks, createRichTextBlockId, decodeRichTextBlocksFromClipboardText, encodeRichTextBlocksForClipboard, editorHtmlToMarkdown, isRichTextBlocksEmpty, markdownToEditorHtml, richTextBlocksToPlainText, sanitizeRichTextBlocks, sanitizeRichTextHtml, } from "./core/richText.js";
5
+ export { getRichTextFeatureAccess, isRichTextFeatureEnabled, richTextEditorFeatureCatalog, } from "./core/features.js";
6
+ export { exportRichTextBlocks, importRichTextBlocks, richTextJsonExportAdapter, richTextJsonImportAdapter, } from "./core/exportImport.js";
7
+ export { connectRichTextCollaboration, createRichTextAnnotations, instantiateRichTextTemplate, runRichTextSchemaMigrations, } from "./core/proFeatures.js";
8
+ export { getWritingStats } from "./core/writingStats.js";