@bayonai/rich-text-editor 0.1.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +37 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +5 -0
  5. package/dist/react/BlockActionTool.d.ts +15 -0
  6. package/dist/react/BlockActionTool.js +37 -0
  7. package/dist/react/EditorSessionProvider.d.ts +28 -0
  8. package/dist/react/EditorSessionProvider.js +74 -0
  9. package/dist/react/RichTextBody.d.ts +18 -0
  10. package/dist/react/RichTextBody.js +66 -0
  11. package/dist/react/RichTextDocumentSurface.d.ts +6 -0
  12. package/dist/react/RichTextDocumentSurface.js +5 -0
  13. package/dist/react/RichTextEditor.d.ts +45 -0
  14. package/dist/react/RichTextEditor.js +1096 -0
  15. package/dist/react/RichTextIcons.d.ts +21 -0
  16. package/dist/react/RichTextIcons.js +55 -0
  17. package/dist/react/RichTextRenderedBlock.d.ts +4 -0
  18. package/dist/react/RichTextRenderedBlock.js +36 -0
  19. package/dist/react/RichTextRenderer.d.ts +4 -0
  20. package/dist/react/RichTextRenderer.js +8 -0
  21. package/dist/react/RichTextStyles.d.ts +1 -0
  22. package/dist/react/RichTextStyles.js +719 -0
  23. package/dist/react/RichTextTitleInput.d.ts +20 -0
  24. package/dist/react/RichTextTitleInput.js +39 -0
  25. package/dist/react/SelectionFormatToolbar.d.ts +34 -0
  26. package/dist/react/SelectionFormatToolbar.js +18 -0
  27. package/dist/react/SpecialBlockOption.d.ts +7 -0
  28. package/dist/react/SpecialBlockOption.js +7 -0
  29. package/dist/react/SpecialBlockTool.d.ts +42 -0
  30. package/dist/react/SpecialBlockTool.js +50 -0
  31. package/dist/react/TranscriptionControl.d.ts +19 -0
  32. package/dist/react/TranscriptionControl.js +129 -0
  33. package/dist/react/UnsavedChangesDialog.d.ts +9 -0
  34. package/dist/react/UnsavedChangesDialog.js +13 -0
  35. package/dist/react/blockActionToolState.d.ts +18 -0
  36. package/dist/react/blockActionToolState.js +53 -0
  37. package/dist/react/blockActions.d.ts +8 -0
  38. package/dist/react/blockActions.js +111 -0
  39. package/dist/react/editorNavigation.d.ts +19 -0
  40. package/dist/react/editorNavigation.js +39 -0
  41. package/dist/react/editorShortcuts.d.ts +20 -0
  42. package/dist/react/editorShortcuts.js +25 -0
  43. package/dist/react/index.d.ts +9 -0
  44. package/dist/react/index.js +6 -0
  45. package/dist/react/richTextBlockStyles.d.ts +7 -0
  46. package/dist/react/richTextBlockStyles.js +7 -0
  47. package/dist/react/specialBlockStyles.d.ts +15 -0
  48. package/dist/react/specialBlockStyles.js +9 -0
  49. package/dist/richText.d.ts +15 -0
  50. package/dist/richText.js +297 -0
  51. package/dist/saveControl.d.ts +8 -0
  52. package/dist/saveControl.js +9 -0
  53. package/dist/session.d.ts +27 -0
  54. package/dist/session.js +78 -0
  55. package/dist/sessionRegistry.d.ts +24 -0
  56. package/dist/sessionRegistry.js +87 -0
  57. package/dist/types.d.ts +34 -0
  58. package/dist/types.js +1 -0
  59. package/dist/writingStats.d.ts +5 -0
  60. package/dist/writingStats.js +9 -0
  61. package/dist-cjs/index.js +22 -0
  62. package/dist-cjs/package.json +3 -0
  63. package/dist-cjs/react/BlockActionTool.js +40 -0
  64. package/dist-cjs/react/EditorSessionProvider.js +79 -0
  65. package/dist-cjs/react/RichTextBody.js +69 -0
  66. package/dist-cjs/react/RichTextDocumentSurface.js +8 -0
  67. package/dist-cjs/react/RichTextEditor.js +1108 -0
  68. package/dist-cjs/react/RichTextIcons.js +74 -0
  69. package/dist-cjs/react/RichTextRenderedBlock.js +39 -0
  70. package/dist-cjs/react/RichTextRenderer.js +11 -0
  71. package/dist-cjs/react/RichTextStyles.js +722 -0
  72. package/dist-cjs/react/RichTextTitleInput.js +44 -0
  73. package/dist-cjs/react/SelectionFormatToolbar.js +22 -0
  74. package/dist-cjs/react/SpecialBlockOption.js +10 -0
  75. package/dist-cjs/react/SpecialBlockTool.js +54 -0
  76. package/dist-cjs/react/TranscriptionControl.js +133 -0
  77. package/dist-cjs/react/UnsavedChangesDialog.js +16 -0
  78. package/dist-cjs/react/blockActionToolState.js +58 -0
  79. package/dist-cjs/react/blockActions.js +119 -0
  80. package/dist-cjs/react/editorNavigation.js +45 -0
  81. package/dist-cjs/react/editorShortcuts.js +28 -0
  82. package/dist-cjs/react/index.js +17 -0
  83. package/dist-cjs/react/richTextBlockStyles.js +10 -0
  84. package/dist-cjs/react/specialBlockStyles.js +12 -0
  85. package/dist-cjs/richText.js +307 -0
  86. package/dist-cjs/saveControl.js +12 -0
  87. package/dist-cjs/session.js +83 -0
  88. package/dist-cjs/sessionRegistry.js +90 -0
  89. package/dist-cjs/types.js +2 -0
  90. package/dist-cjs/writingStats.js +12 -0
  91. package/package.json +45 -0
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEmptyRichTextBlocks = createEmptyRichTextBlocks;
4
+ exports.createRichTextBlockId = createRichTextBlockId;
5
+ exports.sanitizeRichTextBlocks = sanitizeRichTextBlocks;
6
+ exports.isRichTextBlocksEmpty = isRichTextBlocksEmpty;
7
+ exports.richTextBlocksToPlainText = richTextBlocksToPlainText;
8
+ exports.editorHtmlToMarkdown = editorHtmlToMarkdown;
9
+ exports.markdownToEditorHtml = markdownToEditorHtml;
10
+ exports.sanitizeRichTextHtml = sanitizeRichTextHtml;
11
+ const allowedInlineTags = new Set(["a", "br", "code", "em", "strong"]);
12
+ const textBlockTypes = new Set(["paragraph", "heading", "quote", "checkbox"]);
13
+ function createEmptyRichTextBlocks() {
14
+ return [{ id: createRichTextBlockId(), type: "paragraph", markdown: "" }];
15
+ }
16
+ function createRichTextBlockId() {
17
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
18
+ return crypto.randomUUID();
19
+ }
20
+ return `block-${Date.now()}-${Math.random().toString(16).slice(2)}`;
21
+ }
22
+ /**
23
+ * Sanitizes an array of RichTextBlock objects by:
24
+ * - Filtering out invalid blocks
25
+ * - Ensuring each block has a unique ID
26
+ * - Returning an empty array if no valid blocks remain
27
+ */
28
+ function sanitizeRichTextBlocks(value) {
29
+ const blocks = Array.isArray(value) ? value : [];
30
+ const seenBlockIds = new Set();
31
+ const sanitized = blocks
32
+ .flatMap((block) => sanitizeRichTextBlock(block))
33
+ .map((block) => {
34
+ if (!seenBlockIds.has(block.id)) {
35
+ seenBlockIds.add(block.id);
36
+ return block;
37
+ }
38
+ const blockWithUniqueId = {
39
+ ...block,
40
+ id: createUniqueRichTextBlockId(seenBlockIds),
41
+ };
42
+ seenBlockIds.add(blockWithUniqueId.id);
43
+ return blockWithUniqueId;
44
+ });
45
+ return sanitized.length > 0 ? sanitized : createEmptyRichTextBlocks();
46
+ }
47
+ function isRichTextBlocksEmpty(blocks) {
48
+ return sanitizeRichTextBlocks(blocks).every((block) => {
49
+ if (block.type === "divider") {
50
+ return false;
51
+ }
52
+ if (block.type === "checkbox") {
53
+ return markdownToPlainText(block.markdown).trim() === "";
54
+ }
55
+ if (block.type === "image") {
56
+ return !block.assetId && !block.alt?.trim();
57
+ }
58
+ const text = block.type === "code" ? block.text : block.markdown;
59
+ return markdownToPlainText(text).trim() === "";
60
+ });
61
+ }
62
+ function richTextBlocksToPlainText(blocks) {
63
+ return sanitizeRichTextBlocks(blocks)
64
+ .map((block) => {
65
+ if (block.type === "divider") {
66
+ return "";
67
+ }
68
+ if (block.type === "checkbox") {
69
+ return markdownToPlainText(block.markdown);
70
+ }
71
+ if (block.type === "image") {
72
+ return block.alt ?? "";
73
+ }
74
+ return block.type === "code"
75
+ ? block.text
76
+ : markdownToPlainText(block.markdown);
77
+ })
78
+ .join(" ")
79
+ .replace(/\s+/g, " ")
80
+ .trim();
81
+ }
82
+ function editorHtmlToMarkdown(html) {
83
+ const sanitized = normalizeTypography(decodeHtmlText(sanitizeRichTextHtml(html)
84
+ .replace(/<br>/g, " \n")
85
+ .replace(/<strong>([\s\S]*?)<\/strong>/g, (_, value) => {
86
+ return `**${decodeHtmlText(stripTags(value))}**`;
87
+ })
88
+ .replace(/<em>([\s\S]*?)<\/em>/g, (_, value) => {
89
+ return `_${decodeHtmlText(stripTags(value))}_`;
90
+ })
91
+ .replace(/<code>([\s\S]*?)<\/code>/g, (_, value) => {
92
+ return `\`${decodeHtmlText(stripTags(value)).replace(/`/g, "\\`")}\``;
93
+ })
94
+ .replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (_, href, value) => {
95
+ return `[${decodeHtmlText(stripTags(value))}](${decodeHtmlText(href)})`;
96
+ })
97
+ .replace(/<a>([\s\S]*?)<\/a>/g, (_, value) => {
98
+ return decodeHtmlText(stripTags(value));
99
+ })
100
+ .replace(/<[^>]+>/g, "")))
101
+ .replace(/\u00a0/g, " ")
102
+ .trim();
103
+ return sanitized;
104
+ }
105
+ function markdownToEditorHtml(markdown) {
106
+ const tokens = [];
107
+ let html = escapeHtmlText(normalizeTypography(markdown));
108
+ html = html.replace(/`([^`\n]+)`/g, (_, value) => {
109
+ const token = pushToken(tokens, `<code>${value}</code>`);
110
+ return token;
111
+ });
112
+ html = html.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
113
+ const safeHref = getSafeUrl(decodeHtmlText(href));
114
+ return safeHref ? `<a href="${safeHref}">${label}</a>` : label;
115
+ });
116
+ html = html
117
+ .replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>")
118
+ .replace(/_([^_\n]+)_/g, "<em>$1</em>")
119
+ .replace(/ {2}\n/g, "<br>")
120
+ .replace(/\n/g, "<br>");
121
+ tokens.forEach((value, index) => {
122
+ html = html.replace(tokenPlaceholder(index), value);
123
+ });
124
+ return html;
125
+ }
126
+ function sanitizeRichTextHtml(html) {
127
+ return html
128
+ .replace(/<!--[\s\S]*?-->/g, "")
129
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
130
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
131
+ .replace(/<b(\s[^>]*)?>/gi, "<strong>")
132
+ .replace(/<\/b>/gi, "</strong>")
133
+ .replace(/<i(\s[^>]*)?>/gi, "<em>")
134
+ .replace(/<\/i>/gi, "</em>")
135
+ .replace(/<\/?([a-z0-9]+)([^>]*)>/gi, (match, rawTag, rawAttrs) => {
136
+ const tag = String(rawTag).toLowerCase();
137
+ const closing = match.startsWith("</");
138
+ if (!allowedInlineTags.has(tag)) {
139
+ return "";
140
+ }
141
+ if (closing) {
142
+ return tag === "br" ? "" : `</${tag}>`;
143
+ }
144
+ if (tag === "a") {
145
+ const href = getSafeAttribute(rawAttrs, "href");
146
+ return href ? `<a href="${href}">` : "<a>";
147
+ }
148
+ return tag === "br" ? "<br>" : `<${tag}>`;
149
+ });
150
+ }
151
+ function sanitizeRichTextBlock(block) {
152
+ if (!isRecord(block)) {
153
+ return [];
154
+ }
155
+ const id = readBlockId(block);
156
+ const type = block.type;
157
+ if (type === "code") {
158
+ return [{ id, type, text: String(block.text ?? "") }];
159
+ }
160
+ if (type === "divider") {
161
+ return [{ id, type }];
162
+ }
163
+ if (type === "image") {
164
+ return [
165
+ {
166
+ id,
167
+ type,
168
+ ...(typeof block.assetId === "string" && block.assetId.trim()
169
+ ? { assetId: block.assetId.trim() }
170
+ : {}),
171
+ ...(typeof block.alt === "string" && block.alt.trim()
172
+ ? { alt: markdownToPlainText(editorHtmlToMarkdown(block.alt)) }
173
+ : {}),
174
+ },
175
+ ];
176
+ }
177
+ if (typeof type === "string" && textBlockTypes.has(type)) {
178
+ const markdown = typeof block.markdown === "string"
179
+ ? block.markdown
180
+ : typeof block.text === "string"
181
+ ? block.text
182
+ : typeof block.html === "string"
183
+ ? editorHtmlToMarkdown(block.html)
184
+ : "";
185
+ if (type === "checkbox") {
186
+ return [
187
+ {
188
+ id,
189
+ type,
190
+ checked: Boolean(block.checked),
191
+ markdown: sanitizeMarkdown(markdown),
192
+ },
193
+ ];
194
+ }
195
+ return [
196
+ {
197
+ id,
198
+ type: type,
199
+ markdown: sanitizeMarkdown(markdown),
200
+ },
201
+ ];
202
+ }
203
+ return [];
204
+ }
205
+ function readBlockId(block) {
206
+ return typeof block.id === "string" && block.id.trim()
207
+ ? block.id.trim()
208
+ : createRichTextBlockId();
209
+ }
210
+ function createUniqueRichTextBlockId(existingIds) {
211
+ let id = createRichTextBlockId();
212
+ while (existingIds.has(id)) {
213
+ id = createRichTextBlockId();
214
+ }
215
+ return id;
216
+ }
217
+ function markdownToPlainText(value) {
218
+ return value
219
+ .replace(/`([^`]*)`/g, "$1")
220
+ .replace(/\*\*([^*]*)\*\*/g, "$1")
221
+ .replace(/_([^_]*)_/g, "$1")
222
+ .replace(/\[([^\]]*)\]\([^)]+\)/g, "$1")
223
+ .replace(/[#>*`_[\]()]/g, " ")
224
+ .replace(/\s+/g, " ")
225
+ .trim();
226
+ }
227
+ function sanitizeMarkdown(value) {
228
+ return normalizeTypography(value)
229
+ .replace(/<!--[\s\S]*?-->/g, "")
230
+ .trim();
231
+ }
232
+ function normalizeTypography(value) {
233
+ const tokens = [];
234
+ let normalized = value.replace(/`([^`\n]+)`/g, (match) => {
235
+ return pushToken(tokens, match);
236
+ });
237
+ normalized = normalized.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_match, label, href) => {
238
+ return `[${label}](${pushToken(tokens, href)})`;
239
+ });
240
+ normalized = normalized
241
+ .replace(/––/g, "—")
242
+ .replace(/–-/g, "—")
243
+ .replace(/---/g, "—")
244
+ .replace(/--/g, "–");
245
+ tokens.forEach((token, index) => {
246
+ normalized = normalized.replace(tokenPlaceholder(index), token);
247
+ });
248
+ return normalized;
249
+ }
250
+ function stripTags(value) {
251
+ return value.replace(/<[^>]+>/g, "");
252
+ }
253
+ function getSafeAttribute(attrs, name) {
254
+ const value = getPlainAttribute(attrs, name);
255
+ const safeValue = getSafeUrl(value);
256
+ if (!safeValue) {
257
+ return "";
258
+ }
259
+ return escapeAttribute(safeValue);
260
+ }
261
+ function getSafeUrl(value) {
262
+ const trimmed = value.trim();
263
+ if (!trimmed || /^javascript:/i.test(trimmed)) {
264
+ return "";
265
+ }
266
+ return trimmed;
267
+ }
268
+ function getPlainAttribute(attrs, name) {
269
+ const pattern = new RegExp(`${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i");
270
+ const match = attrs.match(pattern);
271
+ return match?.[2] ?? match?.[3] ?? match?.[4] ?? "";
272
+ }
273
+ function escapeAttribute(value) {
274
+ return value
275
+ .replace(/&/g, "&amp;")
276
+ .replace(/</g, "&lt;")
277
+ .replace(/>/g, "&gt;")
278
+ .replace(/"/g, "&quot;");
279
+ }
280
+ function escapeHtmlText(value) {
281
+ return value
282
+ .replace(/&/g, "&amp;")
283
+ .replace(/</g, "&lt;")
284
+ .replace(/>/g, "&gt;");
285
+ }
286
+ function decodeHtmlText(value) {
287
+ return value
288
+ .replace(/&nbsp;/g, " ")
289
+ .replace(/&#160;/g, " ")
290
+ .replace(/&#xA0;/gi, " ")
291
+ .replace(/&amp;/g, "&")
292
+ .replace(/&lt;/g, "<")
293
+ .replace(/&gt;/g, ">")
294
+ .replace(/&quot;/g, '"')
295
+ .replace(/&apos;/g, "'")
296
+ .replace(/&#39;/g, "'");
297
+ }
298
+ function pushToken(tokens, value) {
299
+ const index = tokens.push(value) - 1;
300
+ return tokenPlaceholder(index);
301
+ }
302
+ function tokenPlaceholder(index) {
303
+ return `@@RICHTEXTTOKEN${index}@@`;
304
+ }
305
+ function isRecord(value) {
306
+ return typeof value === "object" && value !== null;
307
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getEditorSaveControlState = getEditorSaveControlState;
4
+ function getEditorSaveControlState({ isDirty, isSaving, saveLabel, }) {
5
+ if (isSaving) {
6
+ return { disabled: true, label: "Saving..." };
7
+ }
8
+ if (!isDirty) {
9
+ return { disabled: true, label: "No changes to save" };
10
+ }
11
+ return { disabled: false, label: saveLabel };
12
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEditorSession = createEditorSession;
4
+ exports.defaultEditorSessionEquals = defaultEditorSessionEquals;
5
+ exports.canonicalSerialize = canonicalSerialize;
6
+ function createEditorSession({ dirtyScope = "all", equals, initialBaselineValue, initialValue, onSave, }) {
7
+ let baseline = cloneValue(initialBaselineValue ?? initialValue);
8
+ let currentValue = cloneValue(initialValue);
9
+ let saving = false;
10
+ let saveHandler = onSave;
11
+ const listeners = new Set();
12
+ const valuesEqual = equals ??
13
+ ((left, right) => defaultEditorSessionEquals(left, right, dirtyScope));
14
+ function emitChange() {
15
+ listeners.forEach((listener) => listener());
16
+ }
17
+ return {
18
+ getValue: () => currentValue,
19
+ isDirty: () => !valuesEqual(currentValue, baseline),
20
+ isSaving: () => saving,
21
+ markClean: (value) => {
22
+ baseline = cloneValue(value ?? currentValue);
23
+ emitChange();
24
+ },
25
+ reset: () => {
26
+ currentValue = cloneValue(baseline);
27
+ emitChange();
28
+ return currentValue;
29
+ },
30
+ save: async () => {
31
+ if (saving) {
32
+ return;
33
+ }
34
+ saving = true;
35
+ emitChange();
36
+ try {
37
+ const valueToSave = cloneValue(currentValue);
38
+ const result = await saveHandler(valueToSave);
39
+ baseline = cloneValue(valueToSave);
40
+ return result;
41
+ }
42
+ finally {
43
+ saving = false;
44
+ emitChange();
45
+ }
46
+ },
47
+ setOnSave: (nextOnSave) => {
48
+ saveHandler = nextOnSave;
49
+ },
50
+ subscribe: (listener) => {
51
+ listeners.add(listener);
52
+ return () => listeners.delete(listener);
53
+ },
54
+ update: (value) => {
55
+ currentValue = cloneValue(value);
56
+ emitChange();
57
+ },
58
+ };
59
+ }
60
+ function defaultEditorSessionEquals(left, right, dirtyScope = "all") {
61
+ if (dirtyScope === "content") {
62
+ return (canonicalSerialize(left.document) === canonicalSerialize(right.document));
63
+ }
64
+ return canonicalSerialize(left) === canonicalSerialize(right);
65
+ }
66
+ function canonicalSerialize(value) {
67
+ return JSON.stringify(sortValue(value));
68
+ }
69
+ function sortValue(value) {
70
+ if (Array.isArray(value)) {
71
+ return value.map(sortValue);
72
+ }
73
+ if (value && typeof value === "object") {
74
+ return Object.fromEntries(Object.entries(value)
75
+ .filter(([, entryValue]) => entryValue !== undefined)
76
+ .sort(([left], [right]) => left.localeCompare(right))
77
+ .map(([key, entryValue]) => [key, sortValue(entryValue)]));
78
+ }
79
+ return value;
80
+ }
81
+ function cloneValue(value) {
82
+ return JSON.parse(JSON.stringify(value));
83
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEditorSessionRegistry = createEditorSessionRegistry;
4
+ const cleanSnapshot = {
5
+ dirty: false,
6
+ error: null,
7
+ pendingExit: false,
8
+ saving: false,
9
+ };
10
+ function createEditorSessionRegistry() {
11
+ const sessions = new Set();
12
+ const sessionUnsubscribers = new Map();
13
+ const listeners = new Set();
14
+ let pendingAction = null;
15
+ let snapshot = cleanSnapshot;
16
+ function emitChange(changes = {}) {
17
+ snapshot = {
18
+ ...snapshot,
19
+ ...changes,
20
+ dirty: Array.from(sessions).some((session) => session.isDirty()),
21
+ };
22
+ listeners.forEach((listener) => listener());
23
+ }
24
+ function clearPendingExit() {
25
+ pendingAction = null;
26
+ emitChange({ error: null, pendingExit: false, saving: false });
27
+ }
28
+ return {
29
+ discardAndExit: () => {
30
+ const action = pendingAction;
31
+ sessions.forEach((session) => {
32
+ if (session.isDirty()) {
33
+ session.reset();
34
+ }
35
+ });
36
+ clearPendingExit();
37
+ void action?.();
38
+ },
39
+ getSnapshot: () => snapshot,
40
+ register: (session) => {
41
+ sessions.add(session);
42
+ sessionUnsubscribers.set(session, session.subscribe(() => emitChange()));
43
+ emitChange();
44
+ return () => {
45
+ sessionUnsubscribers.get(session)?.();
46
+ sessionUnsubscribers.delete(session);
47
+ sessions.delete(session);
48
+ emitChange();
49
+ };
50
+ },
51
+ requestExit: async (action) => {
52
+ if (!Array.from(sessions).some((session) => session.isDirty())) {
53
+ await action();
54
+ return true;
55
+ }
56
+ pendingAction = action;
57
+ emitChange({ error: null, pendingExit: true });
58
+ return false;
59
+ },
60
+ saveAndExit: async () => {
61
+ const action = pendingAction;
62
+ if (!action || snapshot.saving) {
63
+ return;
64
+ }
65
+ emitChange({ error: null, saving: true });
66
+ try {
67
+ for (const session of sessions) {
68
+ if (session.isDirty()) {
69
+ await session.save();
70
+ }
71
+ }
72
+ clearPendingExit();
73
+ await action();
74
+ }
75
+ catch (error) {
76
+ emitChange({
77
+ error: error instanceof Error ? error.message : "Unable to save changes.",
78
+ pendingExit: true,
79
+ saving: false,
80
+ });
81
+ throw error;
82
+ }
83
+ },
84
+ stay: clearPendingExit,
85
+ subscribe: (listener) => {
86
+ listeners.add(listener);
87
+ return () => listeners.delete(listener);
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getWritingStats = getWritingStats;
4
+ const richText_1 = require("./richText");
5
+ function getWritingStats(value) {
6
+ const plainText = (0, richText_1.richTextBlocksToPlainText)(value);
7
+ const wordCount = plainText ? plainText.split(" ").length : 0;
8
+ return {
9
+ readingMinutes: Math.max(1, Math.ceil(wordCount / 225)),
10
+ wordCount,
11
+ };
12
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@bayonai/rich-text-editor",
3
+ "version": "0.1.2",
4
+ "description": "Reusable rich text editing, rendering, and unsaved-change session controls.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist-cjs/index.js"
15
+ },
16
+ "./react": {
17
+ "types": "./dist/react/index.d.ts",
18
+ "import": "./dist/react/index.js",
19
+ "require": "./dist-cjs/react/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "dist-cjs",
25
+ "CHANGELOG.md",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json && node scripts/write-esm-extensions.mjs && tsc -p tsconfig.cjs.json && node scripts/write-cjs.mjs",
30
+ "lint": "tsc --noEmit -p tsconfig.json",
31
+ "test": "vitest run"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^19.0.0",
35
+ "react-dom": "^19.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/react": "^19",
39
+ "@types/react-dom": "^19",
40
+ "react": "19.2.4",
41
+ "react-dom": "19.2.4",
42
+ "typescript": "^5",
43
+ "vitest": "^4.1.6"
44
+ }
45
+ }