@azlib/editor 0.2.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1310 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let isomorphic_dompurify = require("isomorphic-dompurify");
25
+ isomorphic_dompurify = __toESM(isomorphic_dompurify, 1);
26
+ let prosemirror_model = require("prosemirror-model");
27
+ let react = require("react");
28
+ let react_jsx_runtime = require("react/jsx-runtime");
29
+ //#region src/core/errors.ts
30
+ const createDiagnostic = (code, severity, message, location) => ({
31
+ code,
32
+ severity,
33
+ message,
34
+ location
35
+ });
36
+ const createRecoverableError = (code, message, details) => ({
37
+ code,
38
+ message,
39
+ severity: "warning",
40
+ recoverable: true,
41
+ details
42
+ });
43
+ //#endregion
44
+ //#region src/core/commandDiagnostics.ts
45
+ const createUnknownCommandDiagnostic = (commandName) => createDiagnostic("UNKNOWN_COMMAND", "warning", `Command '${commandName}' is not available in current editor capabilities.`);
46
+ const createNoHistoryDiagnostic = (type) => createDiagnostic("NO_HISTORY", "info", `No ${type} history entry is available for this editor session.`);
47
+ //#endregion
48
+ //#region src/core/formattingCommands.ts
49
+ const withCommandMetadata = (commandName) => ({ document }) => ({ document: {
50
+ ...document,
51
+ revision: document.revision + 1,
52
+ metadata: {
53
+ ...document.metadata,
54
+ lastCommand: commandName
55
+ }
56
+ } });
57
+ const createFormattingCommandHandlers = () => ({
58
+ bold: withCommandMetadata("bold"),
59
+ italic: withCommandMetadata("italic"),
60
+ underline: withCommandMetadata("underline"),
61
+ heading: withCommandMetadata("heading"),
62
+ list: withCommandMetadata("list"),
63
+ align: withCommandMetadata("align"),
64
+ link: withCommandMetadata("link")
65
+ });
66
+ //#endregion
67
+ //#region src/core/commands.ts
68
+ const createCommandRegistry = (capabilities) => {
69
+ const allowed = new Set(capabilities && capabilities.length > 0 ? capabilities : [
70
+ "bold",
71
+ "italic",
72
+ "underline",
73
+ "heading",
74
+ "list",
75
+ "align",
76
+ "link",
77
+ "undo",
78
+ "redo"
79
+ ]);
80
+ const registry = /* @__PURE__ */ new Map();
81
+ const formattingHandlers = createFormattingCommandHandlers();
82
+ const registerIfAllowed = (name, handler) => {
83
+ if (allowed.has(name)) registry.set(name, handler);
84
+ };
85
+ Object.entries(formattingHandlers).forEach(([commandName, handler]) => {
86
+ registerIfAllowed(commandName, handler);
87
+ });
88
+ return registry;
89
+ };
90
+ const executeCommand = (registry, commandName, context, params) => {
91
+ const handler = registry.get(commandName);
92
+ if (!handler) return {
93
+ document: context.document,
94
+ diagnostics: [createUnknownCommandDiagnostic(commandName)]
95
+ };
96
+ return handler(context, params);
97
+ };
98
+ //#endregion
99
+ //#region src/core/history.ts
100
+ var EditorHistory = class {
101
+ undoStack = [];
102
+ redoStack = [];
103
+ record(snapshot) {
104
+ this.undoStack.push(snapshot);
105
+ this.redoStack.length = 0;
106
+ }
107
+ undo(current) {
108
+ const previous = this.undoStack.pop();
109
+ if (!previous) return current;
110
+ this.redoStack.push(current);
111
+ return {
112
+ ...previous,
113
+ revision: current.revision + 1,
114
+ metadata: {
115
+ ...previous.metadata,
116
+ lastCommand: "undo"
117
+ }
118
+ };
119
+ }
120
+ redo(current) {
121
+ const next = this.redoStack.pop();
122
+ if (!next) return current;
123
+ this.undoStack.push(current);
124
+ return {
125
+ ...next,
126
+ revision: current.revision + 1,
127
+ metadata: {
128
+ ...next.metadata,
129
+ lastCommand: "redo"
130
+ }
131
+ };
132
+ }
133
+ };
134
+ //#endregion
135
+ //#region src/transforms/html.ts
136
+ const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
137
+ const allowedStyleProperties = new Set([
138
+ "font-family",
139
+ "font-size",
140
+ "text-align",
141
+ "margin-left"
142
+ ]);
143
+ const normalizeStyleValue = (property, value) => {
144
+ const next = value.trim().toLowerCase();
145
+ if (property === "font-family") return next === "serif" || next === "monospace" ? next : null;
146
+ if (property === "font-size") return [
147
+ "12px",
148
+ "16px",
149
+ "20px",
150
+ "28px"
151
+ ].includes(next) ? next : null;
152
+ if (property === "text-align") return [
153
+ "left",
154
+ "center",
155
+ "right",
156
+ "justify"
157
+ ].includes(next) ? next : null;
158
+ if (property === "margin-left") {
159
+ const match = /^([0-9]{1,3})px$/.exec(next);
160
+ if (!match) return null;
161
+ const amount = Number.parseInt(match[1], 10);
162
+ return amount >= 0 && amount % 24 === 0 ? `${amount}px` : null;
163
+ }
164
+ return null;
165
+ };
166
+ const normalizeStyleAttribute = (styleValue) => {
167
+ const entries = styleValue.split(";").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => {
168
+ const separator = entry.indexOf(":");
169
+ if (separator < 0) return null;
170
+ const property = entry.slice(0, separator).trim().toLowerCase();
171
+ const value = entry.slice(separator + 1).trim();
172
+ if (!allowedStyleProperties.has(property)) return null;
173
+ const normalized = normalizeStyleValue(property, value);
174
+ if (!normalized) return null;
175
+ return `${property}:${normalized}`;
176
+ }).filter((entry) => Boolean(entry));
177
+ if (entries.length === 0) return null;
178
+ return entries.join(";");
179
+ };
180
+ const normalizeHtml = (value) => {
181
+ if (typeof DOMParser === "undefined") return value.trim().length > 0 ? value : "<p></p>";
182
+ const doc = new DOMParser().parseFromString(`<div>${value}</div>`, "text/html");
183
+ const root = doc.body.firstElementChild;
184
+ if (!root) return "<p></p>";
185
+ root.querySelectorAll("b").forEach((element) => {
186
+ const replacement = doc.createElement("strong");
187
+ replacement.innerHTML = element.innerHTML;
188
+ element.replaceWith(replacement);
189
+ });
190
+ root.querySelectorAll("i").forEach((element) => {
191
+ const replacement = doc.createElement("em");
192
+ replacement.innerHTML = element.innerHTML;
193
+ element.replaceWith(replacement);
194
+ });
195
+ root.querySelectorAll("strike").forEach((element) => {
196
+ const replacement = doc.createElement("s");
197
+ replacement.innerHTML = element.innerHTML;
198
+ element.replaceWith(replacement);
199
+ });
200
+ root.querySelectorAll("*").forEach((element) => {
201
+ const tag = element.tagName.toLowerCase();
202
+ Array.from(element.attributes).forEach((attribute) => {
203
+ const name = attribute.name.toLowerCase();
204
+ const valueAttr = attribute.value;
205
+ if (name === "style") {
206
+ const normalized = normalizeStyleAttribute(valueAttr);
207
+ if (normalized) element.setAttribute("style", normalized);
208
+ else element.removeAttribute("style");
209
+ return;
210
+ }
211
+ if (name === "href" || name === "src") {
212
+ if (!/^(https?:|mailto:|tel:)/i.test(valueAttr.trim())) element.removeAttribute(attribute.name);
213
+ return;
214
+ }
215
+ if (name === "target" || name === "rel" || name === "alt" || name === "data-formula" || name === "controls" || name === "dir") return;
216
+ element.removeAttribute(attribute.name);
217
+ });
218
+ if (tag === "a") {
219
+ element.setAttribute("rel", "noopener noreferrer");
220
+ element.setAttribute("target", "_blank");
221
+ }
222
+ if (tag === "video") element.setAttribute("controls", "true");
223
+ });
224
+ Array.from(root.childNodes).forEach((node) => {
225
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent?.trim().length ?? 0) > 0) {
226
+ const paragraph = doc.createElement("p");
227
+ paragraph.textContent = node.textContent ?? "";
228
+ root.replaceChild(paragraph, node);
229
+ }
230
+ });
231
+ const normalized = root.innerHTML.trim();
232
+ return normalized.length > 0 ? normalized : "<p></p>";
233
+ };
234
+ const sanitizeHtml = (payload) => isomorphic_dompurify.default.sanitize(payload, {
235
+ ALLOWED_TAGS: [
236
+ "p",
237
+ "br",
238
+ "b",
239
+ "i",
240
+ "strike",
241
+ "strong",
242
+ "em",
243
+ "u",
244
+ "s",
245
+ "code",
246
+ "pre",
247
+ "blockquote",
248
+ "h1",
249
+ "h2",
250
+ "ul",
251
+ "ol",
252
+ "li",
253
+ "a",
254
+ "img",
255
+ "video",
256
+ "span",
257
+ "sub",
258
+ "sup",
259
+ "div"
260
+ ],
261
+ ALLOWED_ATTR: [
262
+ "href",
263
+ "src",
264
+ "alt",
265
+ "target",
266
+ "rel",
267
+ "style",
268
+ "data-formula",
269
+ "controls",
270
+ "dir"
271
+ ],
272
+ FORBID_TAGS: [
273
+ "script",
274
+ "style",
275
+ "iframe",
276
+ "object",
277
+ "embed",
278
+ "form",
279
+ "input",
280
+ "button",
281
+ "textarea",
282
+ "select"
283
+ ]
284
+ });
285
+ const importHtml = (payload) => {
286
+ const diagnostics = [];
287
+ const normalized = normalizeHtml(sanitizeHtml(payload));
288
+ if (normalized !== payload) diagnostics.push(createDiagnostic("SANITIZED_CONTENT", "warning", "Input HTML was sanitized to remove unsupported or unsafe markup."));
289
+ if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_HTML", "info", "The provided HTML content is empty after normalization."));
290
+ return {
291
+ richText: stripHtml(normalized),
292
+ sanitizedHtml: normalized,
293
+ diagnostics
294
+ };
295
+ };
296
+ const exportHtml = (richText) => {
297
+ return normalizeHtml(`<p>${richText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")}</p>`);
298
+ };
299
+ //#endregion
300
+ //#region src/transforms/diagnostics.ts
301
+ const createUnsupportedMarkupDiagnostic = (details) => createDiagnostic("UNSUPPORTED_MARKUP", "warning", details ?? "Some markup could not be fully represented and was normalized.");
302
+ const createRecoverableParseDiagnostic = (format, details) => createDiagnostic("RECOVERABLE_PARSE", "warning", details ?? `Some ${format.toUpperCase()} content was partially recovered during import.`);
303
+ //#endregion
304
+ //#region src/transforms/importFallback.ts
305
+ const applyImportFallback = (input, base) => {
306
+ const payload = input.payload.trim();
307
+ const diagnostics = [createRecoverableParseDiagnostic(input.format === "html" ? "html" : "markdown")];
308
+ const stripped = input.format === "html" ? payload.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim() : payload;
309
+ if (stripped.length === 0) diagnostics.push(createUnsupportedMarkupDiagnostic("Input produced no recoverable text content after normalization."));
310
+ return {
311
+ document: {
312
+ ...base,
313
+ content: {
314
+ richText: stripped,
315
+ html: input.format === "html" ? payload : base.content.html
316
+ },
317
+ metadata: {
318
+ ...base.metadata,
319
+ importFallback: true
320
+ }
321
+ },
322
+ diagnostics
323
+ };
324
+ };
325
+ //#endregion
326
+ //#region src/transforms/markdown.ts
327
+ const normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
328
+ const importMarkdown = (payload) => {
329
+ const diagnostics = [];
330
+ const normalized = normalizeMarkdown(payload);
331
+ if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_MARKDOWN", "info", "The provided markdown content is empty."));
332
+ return {
333
+ richText: normalized,
334
+ diagnostics
335
+ };
336
+ };
337
+ const exportMarkdown = (richText) => normalizeMarkdown(richText);
338
+ //#endregion
339
+ //#region src/transforms/representationSwitch.ts
340
+ const importRepresentation = (input, base) => {
341
+ if (input.format === "rich") {
342
+ const normalizedHtml = normalizeHtml(exportHtml(input.payload));
343
+ return {
344
+ document: {
345
+ ...base,
346
+ content: {
347
+ richText: input.payload,
348
+ html: normalizedHtml
349
+ }
350
+ },
351
+ diagnostics: []
352
+ };
353
+ }
354
+ if (input.format === "markdown") try {
355
+ const result = importMarkdown(input.payload);
356
+ return {
357
+ document: {
358
+ ...base,
359
+ content: { richText: result.richText },
360
+ metadata: {
361
+ ...base.metadata,
362
+ importedFrom: "markdown"
363
+ }
364
+ },
365
+ diagnostics: result.diagnostics
366
+ };
367
+ } catch {
368
+ return applyImportFallback(input, base);
369
+ }
370
+ try {
371
+ const result = importHtml(input.payload);
372
+ return {
373
+ document: {
374
+ ...base,
375
+ content: {
376
+ richText: result.richText,
377
+ html: result.sanitizedHtml
378
+ },
379
+ metadata: {
380
+ ...base.metadata,
381
+ importedFrom: "html"
382
+ }
383
+ },
384
+ diagnostics: result.diagnostics
385
+ };
386
+ } catch {
387
+ return applyImportFallback(input, base);
388
+ }
389
+ };
390
+ const exportRepresentation = (document, format) => {
391
+ if (format === "rich") return {
392
+ format,
393
+ payload: document.content.richText,
394
+ diagnostics: []
395
+ };
396
+ if (format === "markdown") return {
397
+ format,
398
+ payload: exportMarkdown(document.content.richText),
399
+ diagnostics: []
400
+ };
401
+ return {
402
+ format,
403
+ payload: document.content.html ? normalizeHtml(document.content.html) : normalizeHtml(exportHtml(document.content.richText)),
404
+ diagnostics: []
405
+ };
406
+ };
407
+ //#endregion
408
+ //#region src/core/createEditor.ts
409
+ const createDefaultDocument = () => ({
410
+ id: crypto.randomUUID(),
411
+ content: { richText: "" },
412
+ metadata: {},
413
+ revision: 0
414
+ });
415
+ const fromInput = (input, base) => importRepresentation(input, base);
416
+ const toExport = (document, format) => exportRepresentation(document, format);
417
+ const createEditor = (config = {}) => {
418
+ const registry = createCommandRegistry(config.commandCapabilities);
419
+ const history = new EditorHistory();
420
+ let isDestroyed = false;
421
+ let currentDocument = createDefaultDocument();
422
+ if (config.initialContent) {
423
+ const initialized = fromInput(config.initialContent, currentDocument);
424
+ currentDocument = initialized.document;
425
+ if (initialized.diagnostics.length > 0) config.onError?.(createRecoverableError("INITIAL_CONTENT_DIAGNOSTIC", "Initial content was normalized during editor initialization.", { diagnostics: initialized.diagnostics }));
426
+ }
427
+ const ensureActive = () => {
428
+ if (!isDestroyed) return true;
429
+ config.onError?.(createRecoverableError("EDITOR_DESTROYED", "This editor instance has already been destroyed."));
430
+ return false;
431
+ };
432
+ const emitChange = () => {
433
+ config.onChange?.(currentDocument);
434
+ };
435
+ return {
436
+ getDocument: () => currentDocument,
437
+ execute: (commandName, params) => {
438
+ if (!ensureActive()) return {
439
+ ok: false,
440
+ document: currentDocument,
441
+ diagnostics: []
442
+ };
443
+ const result = executeCommand(registry, commandName, { document: currentDocument }, params);
444
+ if (commandName === "undo") {
445
+ const next = history.undo(currentDocument);
446
+ if (next === currentDocument) return {
447
+ ok: false,
448
+ document: currentDocument,
449
+ diagnostics: [createNoHistoryDiagnostic("undo")]
450
+ };
451
+ currentDocument = next;
452
+ emitChange();
453
+ return {
454
+ ok: true,
455
+ document: currentDocument,
456
+ diagnostics: []
457
+ };
458
+ }
459
+ if (commandName === "redo") {
460
+ const next = history.redo(currentDocument);
461
+ if (next === currentDocument) return {
462
+ ok: false,
463
+ document: currentDocument,
464
+ diagnostics: [createNoHistoryDiagnostic("redo")]
465
+ };
466
+ currentDocument = next;
467
+ emitChange();
468
+ return {
469
+ ok: true,
470
+ document: currentDocument,
471
+ diagnostics: []
472
+ };
473
+ }
474
+ history.record(currentDocument);
475
+ currentDocument = result.document;
476
+ emitChange();
477
+ return {
478
+ ok: (result.diagnostics?.length ?? 0) === 0,
479
+ document: currentDocument,
480
+ diagnostics: result.diagnostics ?? []
481
+ };
482
+ },
483
+ export: (format) => toExport(currentDocument, format),
484
+ import: (input) => {
485
+ if (!ensureActive()) return {
486
+ ok: false,
487
+ document: currentDocument,
488
+ diagnostics: []
489
+ };
490
+ const result = fromInput(input, {
491
+ ...currentDocument,
492
+ revision: currentDocument.revision + 1
493
+ });
494
+ currentDocument = result.document;
495
+ emitChange();
496
+ return {
497
+ ok: result.diagnostics.length === 0,
498
+ document: currentDocument,
499
+ diagnostics: result.diagnostics
500
+ };
501
+ },
502
+ mount: (target) => {
503
+ if (!ensureActive()) return;
504
+ },
505
+ unmount: () => {},
506
+ destroy: () => {
507
+ isDestroyed = true;
508
+ }
509
+ };
510
+ };
511
+ //#endregion
512
+ //#region src/core/schema.ts
513
+ const editorNodeNames = {
514
+ doc: "doc",
515
+ paragraph: "paragraph",
516
+ heading: "heading",
517
+ text: "text",
518
+ bulletList: "bullet_list",
519
+ orderedList: "ordered_list",
520
+ listItem: "list_item",
521
+ hardBreak: "hard_break"
522
+ };
523
+ const editorMarkNames = {
524
+ strong: "strong",
525
+ em: "em",
526
+ underline: "underline",
527
+ link: "link"
528
+ };
529
+ const createEditorSchema = () => new prosemirror_model.Schema({
530
+ nodes: {
531
+ doc: { content: "block+" },
532
+ paragraph: {
533
+ content: "inline*",
534
+ group: "block",
535
+ parseDOM: [{ tag: "p" }],
536
+ toDOM: () => ["p", 0]
537
+ },
538
+ heading: {
539
+ attrs: { level: { default: 1 } },
540
+ content: "inline*",
541
+ group: "block",
542
+ defining: true,
543
+ parseDOM: [
544
+ {
545
+ tag: "h1",
546
+ attrs: { level: 1 }
547
+ },
548
+ {
549
+ tag: "h2",
550
+ attrs: { level: 2 }
551
+ },
552
+ {
553
+ tag: "h3",
554
+ attrs: { level: 3 }
555
+ }
556
+ ],
557
+ toDOM: (node) => [`h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`, 0]
558
+ },
559
+ bullet_list: {
560
+ content: "list_item+",
561
+ group: "block",
562
+ parseDOM: [{ tag: "ul" }],
563
+ toDOM: () => ["ul", 0]
564
+ },
565
+ ordered_list: {
566
+ attrs: { order: { default: 1 } },
567
+ content: "list_item+",
568
+ group: "block",
569
+ parseDOM: [{
570
+ tag: "ol",
571
+ getAttrs: (dom) => {
572
+ if (!(dom instanceof HTMLOListElement)) return { order: 1 };
573
+ return { order: dom.start || 1 };
574
+ }
575
+ }],
576
+ toDOM: (node) => [
577
+ "ol",
578
+ { start: node.attrs.order || 1 },
579
+ 0
580
+ ]
581
+ },
582
+ list_item: {
583
+ content: "paragraph block*",
584
+ parseDOM: [{ tag: "li" }],
585
+ toDOM: () => ["li", 0]
586
+ },
587
+ text: { group: "inline" },
588
+ hard_break: {
589
+ inline: true,
590
+ group: "inline",
591
+ selectable: false,
592
+ parseDOM: [{ tag: "br" }],
593
+ toDOM: () => ["br"]
594
+ }
595
+ },
596
+ marks: {
597
+ strong: {
598
+ parseDOM: [{ tag: "strong" }, {
599
+ tag: "b",
600
+ getAttrs: () => null
601
+ }],
602
+ toDOM: () => ["strong", 0]
603
+ },
604
+ em: {
605
+ parseDOM: [{ tag: "em" }, {
606
+ tag: "i",
607
+ getAttrs: () => null
608
+ }],
609
+ toDOM: () => ["em", 0]
610
+ },
611
+ underline: {
612
+ parseDOM: [{ tag: "u" }],
613
+ toDOM: () => ["u", 0]
614
+ },
615
+ link: {
616
+ attrs: {
617
+ href: {},
618
+ title: { default: null }
619
+ },
620
+ inclusive: false,
621
+ parseDOM: [{
622
+ tag: "a[href]",
623
+ getAttrs: (dom) => {
624
+ if (!(dom instanceof HTMLAnchorElement)) return false;
625
+ return {
626
+ href: dom.getAttribute("href"),
627
+ title: dom.getAttribute("title")
628
+ };
629
+ }
630
+ }],
631
+ toDOM: (node) => [
632
+ "a",
633
+ {
634
+ href: node.attrs.href,
635
+ title: node.attrs.title
636
+ },
637
+ 0
638
+ ]
639
+ }
640
+ }
641
+ });
642
+ const editorSchema = createEditorSchema();
643
+ //#endregion
644
+ //#region src/core/toolbarModel.ts
645
+ const defaultToolbarActions = [
646
+ {
647
+ command: "bold",
648
+ label: "Bold",
649
+ group: "inline"
650
+ },
651
+ {
652
+ command: "italic",
653
+ label: "Italic",
654
+ group: "inline"
655
+ },
656
+ {
657
+ command: "underline",
658
+ label: "Underline",
659
+ group: "inline"
660
+ },
661
+ {
662
+ command: "heading",
663
+ label: "Heading",
664
+ group: "block"
665
+ },
666
+ {
667
+ command: "list",
668
+ label: "List",
669
+ group: "block"
670
+ },
671
+ {
672
+ command: "align",
673
+ label: "Align",
674
+ group: "block"
675
+ },
676
+ {
677
+ command: "link",
678
+ label: "Link",
679
+ group: "inline"
680
+ },
681
+ {
682
+ command: "undo",
683
+ label: "Undo",
684
+ group: "history"
685
+ },
686
+ {
687
+ command: "redo",
688
+ label: "Redo",
689
+ group: "history"
690
+ }
691
+ ];
692
+ //#endregion
693
+ //#region src/rich-text/mountEditor.ts
694
+ const commandLabels = {
695
+ bold: "Bold",
696
+ italic: "Italic",
697
+ underline: "Underline",
698
+ strike: "Strike",
699
+ code: "Inline code",
700
+ blockquote: "Blockquote",
701
+ "code-block": "Code block",
702
+ "indent-": "Outdent",
703
+ "indent+": "Indent",
704
+ link: "Link",
705
+ image: "Image",
706
+ video: "Video",
707
+ formula: "Formula",
708
+ clean: "Remove formatting"
709
+ };
710
+ const commandIconSvg = {
711
+ bold: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 2.5h4.5a2.5 2.5 0 010 5H4z\"/><path d=\"M4 7.5h5a3 3 0 010 6H4z\"/></svg>",
712
+ italic: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\"><path d=\"M9.8 2.5H5.5\"/><path d=\"M10.5 13.5H6.2\"/><path d=\"M9.3 2.5 6.7 13.5\"/></svg>",
713
+ underline: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M4 2.5v4.2a4 4 0 008 0V2.5\"/><path d=\"M3 13.5h10\"/></svg>",
714
+ strike: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M4 4.5c.8-1.2 2-1.9 3.7-1.9 2 0 3.3 1 3.3 2.5 0 3-6.5 1.8-6.5 4.6 0 1.5 1.3 2.3 3.4 2.3 1.6 0 2.9-.6 3.8-1.7\"/><path d=\"M2.5 8h11\"/></svg>",
715
+ code: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4 2.5 8 6 12\"/><path d=\"M10 4 13.5 8 10 12\"/><path d=\"M8.7 2.8 7.3 13.2\"/></svg>",
716
+ blockquote: "<svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M3 3h4v4H4.8c.1 1.8-.7 3.2-1.8 4L2 10c1-.7 1.5-1.7 1.4-3H3zM9 3h4v4h-2.2c.1 1.8-.7 3.2-1.8 4L8 10c1-.7 1.5-1.7 1.4-3H9z\"/></svg>",
717
+ "code-block": "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"2.5\" width=\"11\" height=\"11\" rx=\"1.8\"/><path d=\"M6.2 6 4.5 8l1.7 2\"/><path d=\"M9.8 6 11.5 8l-1.7 2\"/><path d=\"M8.6 5.5 7.4 10.5\"/></svg>",
718
+ "indent-": "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 4h4\"/><path d=\"M9 8h4\"/><path d=\"M9 12h4\"/><path d=\"M7 5.5 4 8l3 2.5\"/></svg>",
719
+ "indent+": "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 4h4\"/><path d=\"M3 8h4\"/><path d=\"M3 12h4\"/><path d=\"M9 5.5 12 8l-3 2.5\"/></svg>",
720
+ link: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M6.3 9.7 4.9 11a2.4 2.4 0 103.4 3.4l1.5-1.4\"/><path d=\"M9.7 6.3 11 4.9a2.4 2.4 0 10-3.4-3.4L6.1 2.9\"/><path d=\"M5.9 10.1 10.1 5.9\"/></svg>",
721
+ image: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.3\" y=\"2.5\" width=\"11.4\" height=\"11\" rx=\"1.8\"/><circle cx=\"6\" cy=\"6\" r=\"1.2\" fill=\"currentColor\" stroke=\"none\"/><path d=\"m3.8 11 2.8-2.8 2.2 2.2 1.6-1.6 1.8 2.2\"/></svg>",
722
+ video: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.2\" y=\"3\" width=\"8.8\" height=\"10\" rx=\"1.6\"/><path d=\"M11 6.2 13.8 4.8v6.4L11 9.8z\"/><path d=\"M6.2 6.4v3.2l2.7-1.6z\" fill=\"currentColor\" stroke=\"none\"/></svg>",
723
+ formula: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 4h5\"/><path d=\"M3 12h5\"/><path d=\"M8 4 5 8l3 4\"/><path d=\"M10.3 5.8v4.4\"/><path d=\"M12.5 8h-4.4\"/></svg>",
724
+ clean: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.8 12.5h6.8\"/><path d=\"m4.2 3.3 6 6\"/><path d=\"m10.2 2.7 3 3-2 2-3-3z\"/><path d=\"M6.2 11.2 4.1 13.3\"/></svg>"
725
+ };
726
+ const createToolbarIcon = (command, fallback) => {
727
+ const icon = document.createElement("span");
728
+ icon.className = "az-rich-editor-toolbar-icon";
729
+ const svgMarkup = commandIconSvg[command];
730
+ if (svgMarkup) icon.innerHTML = svgMarkup;
731
+ else icon.textContent = fallback;
732
+ return icon;
733
+ };
734
+ const selectOptionLabels = {
735
+ font: {
736
+ default: "A",
737
+ serif: "As",
738
+ monospace: "Am"
739
+ },
740
+ size: {
741
+ small: "S",
742
+ normal: "N",
743
+ large: "L",
744
+ huge: "XL"
745
+ },
746
+ header: {
747
+ normal: "P",
748
+ h1: "H1",
749
+ h2: "H2"
750
+ },
751
+ script: {
752
+ normal: "x",
753
+ sub: "x_",
754
+ super: "x^"
755
+ },
756
+ align: {
757
+ left: "L",
758
+ center: "C",
759
+ right: "R",
760
+ justify: "J"
761
+ },
762
+ list: {
763
+ none: "-",
764
+ ordered: "1.",
765
+ bullet: "o"
766
+ },
767
+ direction: {
768
+ ltr: "->",
769
+ rtl: "<-"
770
+ }
771
+ };
772
+ const safeUrl = (value) => {
773
+ const trimmed = value.trim();
774
+ if (!trimmed) return null;
775
+ if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return trimmed;
776
+ return null;
777
+ };
778
+ const closestElement = (node, root) => {
779
+ let current = node;
780
+ while (current && current !== root) {
781
+ if (current instanceof HTMLElement) return current;
782
+ current = current.parentNode;
783
+ }
784
+ return null;
785
+ };
786
+ const closestByTag = (node, tagName, root) => {
787
+ const desired = tagName.toLowerCase();
788
+ let current = node;
789
+ while (current && current !== root) {
790
+ if (current instanceof HTMLElement && current.tagName.toLowerCase() === desired) return current;
791
+ current = current.parentNode;
792
+ }
793
+ return null;
794
+ };
795
+ const unwrapElement = (element) => {
796
+ const parent = element.parentNode;
797
+ if (!parent) return;
798
+ while (element.firstChild) parent.insertBefore(element.firstChild, element);
799
+ parent.removeChild(element);
800
+ };
801
+ const placeCaretInside = (element) => {
802
+ const selection = window.getSelection();
803
+ if (!selection) return;
804
+ const range = document.createRange();
805
+ range.selectNodeContents(element);
806
+ range.collapse(false);
807
+ selection.removeAllRanges();
808
+ selection.addRange(range);
809
+ };
810
+ const getSelectionRange = (root) => {
811
+ const selection = window.getSelection();
812
+ if (!selection || selection.rangeCount === 0) return null;
813
+ const range = selection.getRangeAt(0);
814
+ if (!root.contains(range.commonAncestorContainer)) return null;
815
+ return range;
816
+ };
817
+ const wrapRange = (range, tagName, attrs) => {
818
+ const wrapper = document.createElement(tagName);
819
+ if (attrs) Object.entries(attrs).forEach(([key, value]) => wrapper.setAttribute(key, value));
820
+ if (range.collapsed) {
821
+ wrapper.append(document.createTextNode("​"));
822
+ range.insertNode(wrapper);
823
+ placeCaretInside(wrapper);
824
+ return wrapper;
825
+ }
826
+ const fragment = range.extractContents();
827
+ wrapper.append(fragment);
828
+ range.insertNode(wrapper);
829
+ const selection = window.getSelection();
830
+ if (selection) {
831
+ const nextRange = document.createRange();
832
+ nextRange.selectNodeContents(wrapper);
833
+ selection.removeAllRanges();
834
+ selection.addRange(nextRange);
835
+ }
836
+ return wrapper;
837
+ };
838
+ const blockTags = new Set([
839
+ "p",
840
+ "div",
841
+ "h1",
842
+ "h2",
843
+ "blockquote",
844
+ "pre",
845
+ "li"
846
+ ]);
847
+ const getBlockElement = (range, root) => {
848
+ const start = closestElement(range.startContainer, root);
849
+ if (!start) return root;
850
+ let current = start;
851
+ while (current && current !== root) {
852
+ if (blockTags.has(current.tagName.toLowerCase())) return current;
853
+ current = current.parentElement;
854
+ }
855
+ return root;
856
+ };
857
+ const replaceBlockTag = (block, tagName) => {
858
+ if (block.tagName.toLowerCase() === tagName.toLowerCase()) return block;
859
+ const replacement = document.createElement(tagName);
860
+ while (block.firstChild) replacement.append(block.firstChild);
861
+ Array.from(block.attributes).forEach((attribute) => {
862
+ if (attribute.name === "style" || attribute.name === "dir") replacement.setAttribute(attribute.name, attribute.value);
863
+ });
864
+ block.parentNode?.replaceChild(replacement, block);
865
+ return replacement;
866
+ };
867
+ const applyInlineTag = (tagName, root, range) => {
868
+ const existing = closestByTag(range.commonAncestorContainer, tagName, root);
869
+ if (existing) {
870
+ unwrapElement(existing);
871
+ return;
872
+ }
873
+ wrapRange(range, tagName);
874
+ };
875
+ const applyScript = (value, root, range) => {
876
+ const sub = closestByTag(range.commonAncestorContainer, "sub", root);
877
+ const sup = closestByTag(range.commonAncestorContainer, "sup", root);
878
+ if (sub) unwrapElement(sub);
879
+ if (sup) unwrapElement(sup);
880
+ if (value === "sub") wrapRange(range, "sub");
881
+ if (value === "super") wrapRange(range, "sup");
882
+ };
883
+ const applyHeader = (value, root, range) => {
884
+ const block = getBlockElement(range, root);
885
+ if (value === "h1" || value === "h2") {
886
+ replaceBlockTag(block, value);
887
+ return;
888
+ }
889
+ replaceBlockTag(block, "p");
890
+ };
891
+ const toggleBlockTag = (tagName, root, range) => {
892
+ const block = getBlockElement(range, root);
893
+ if (block.tagName.toLowerCase() === tagName) {
894
+ replaceBlockTag(block, "p");
895
+ return;
896
+ }
897
+ replaceBlockTag(block, tagName);
898
+ };
899
+ const ensureList = (tagName, root, range, remove) => {
900
+ const block = getBlockElement(range, root);
901
+ const li = closestByTag(block, "li", root);
902
+ if (li) {
903
+ const list = li.parentElement;
904
+ if (!list) return;
905
+ if (remove || list.tagName.toLowerCase() === tagName) {
906
+ const paragraph = document.createElement("p");
907
+ paragraph.textContent = li.textContent ?? "";
908
+ list.parentNode?.insertBefore(paragraph, list);
909
+ li.remove();
910
+ if (!list.querySelector("li")) list.remove();
911
+ return;
912
+ }
913
+ const nextList = document.createElement(tagName);
914
+ const nextLi = document.createElement("li");
915
+ nextLi.textContent = li.textContent ?? "";
916
+ nextList.append(nextLi);
917
+ list.parentNode?.insertBefore(nextList, list.nextSibling);
918
+ li.remove();
919
+ if (!list.querySelector("li")) list.remove();
920
+ return;
921
+ }
922
+ if (remove) return;
923
+ const list = document.createElement(tagName);
924
+ const listItem = document.createElement("li");
925
+ listItem.innerHTML = block.innerHTML;
926
+ list.append(listItem);
927
+ block.parentNode?.replaceChild(list, block);
928
+ };
929
+ const applyIndent = (root, range, direction) => {
930
+ const block = getBlockElement(range, root);
931
+ const current = Number.parseInt(block.style.marginLeft || "0", 10) || 0;
932
+ const next = direction === "+" ? current + 24 : Math.max(0, current - 24);
933
+ block.style.marginLeft = next === 0 ? "" : `${next}px`;
934
+ };
935
+ const applyAlign = (root, range, align) => {
936
+ const block = getBlockElement(range, root);
937
+ block.style.textAlign = align;
938
+ };
939
+ const applyDirection = (root, range, value) => {
940
+ getBlockElement(range, root).setAttribute("dir", value);
941
+ };
942
+ const clearFormatting = (root, range) => {
943
+ if (range.collapsed) {
944
+ const block = getBlockElement(range, root);
945
+ const text = block.textContent ?? "";
946
+ block.replaceChildren(document.createTextNode(text));
947
+ return;
948
+ }
949
+ const selectedText = range.toString();
950
+ range.deleteContents();
951
+ range.insertNode(document.createTextNode(selectedText));
952
+ };
953
+ const insertNodeAtRange = (range, node) => {
954
+ range.deleteContents();
955
+ range.insertNode(node);
956
+ const selection = window.getSelection();
957
+ if (selection) {
958
+ const after = document.createRange();
959
+ after.setStartAfter(node);
960
+ after.collapse(true);
961
+ selection.removeAllRanges();
962
+ selection.addRange(after);
963
+ }
964
+ };
965
+ const applyEmbed = async (type, range, requestEmbedValue) => {
966
+ const value = await Promise.resolve(requestEmbedValue?.(type) ?? null);
967
+ if (!value || value.trim().length === 0) return;
968
+ if (type === "formula") {
969
+ const formula = document.createElement("span");
970
+ formula.setAttribute("data-formula", "true");
971
+ formula.textContent = value.trim();
972
+ insertNodeAtRange(range, formula);
973
+ return;
974
+ }
975
+ const url = safeUrl(value);
976
+ if (!url) return;
977
+ if (type === "link") {
978
+ const selected = range.toString().trim();
979
+ const anchor = document.createElement("a");
980
+ anchor.href = url;
981
+ anchor.rel = "noopener noreferrer";
982
+ anchor.target = "_blank";
983
+ anchor.textContent = selected.length > 0 ? selected : url;
984
+ insertNodeAtRange(range, anchor);
985
+ return;
986
+ }
987
+ if (type === "image") {
988
+ const image = document.createElement("img");
989
+ image.src = url;
990
+ image.alt = "embedded image";
991
+ insertNodeAtRange(range, image);
992
+ return;
993
+ }
994
+ const video = document.createElement("video");
995
+ video.setAttribute("controls", "true");
996
+ video.src = url;
997
+ insertNodeAtRange(range, video);
998
+ };
999
+ const applyButtonCommand = async (command, editable, requestEmbedValue) => {
1000
+ editable.focus();
1001
+ const range = getSelectionRange(editable);
1002
+ if (!range) return;
1003
+ switch (command) {
1004
+ case "bold":
1005
+ applyInlineTag("strong", editable, range);
1006
+ break;
1007
+ case "italic":
1008
+ applyInlineTag("em", editable, range);
1009
+ break;
1010
+ case "underline":
1011
+ applyInlineTag("u", editable, range);
1012
+ break;
1013
+ case "strike":
1014
+ applyInlineTag("s", editable, range);
1015
+ break;
1016
+ case "code":
1017
+ applyInlineTag("code", editable, range);
1018
+ break;
1019
+ case "blockquote":
1020
+ toggleBlockTag("blockquote", editable, range);
1021
+ break;
1022
+ case "code-block":
1023
+ toggleBlockTag("pre", editable, range);
1024
+ break;
1025
+ case "indent-":
1026
+ applyIndent(editable, range, "-");
1027
+ break;
1028
+ case "indent+":
1029
+ applyIndent(editable, range, "+");
1030
+ break;
1031
+ case "link":
1032
+ await applyEmbed("link", range, requestEmbedValue);
1033
+ break;
1034
+ case "image":
1035
+ await applyEmbed("image", range, requestEmbedValue);
1036
+ break;
1037
+ case "video":
1038
+ await applyEmbed("video", range, requestEmbedValue);
1039
+ break;
1040
+ case "formula":
1041
+ await applyEmbed("formula", range, requestEmbedValue);
1042
+ break;
1043
+ case "clean":
1044
+ clearFormatting(editable, range);
1045
+ break;
1046
+ default: break;
1047
+ }
1048
+ };
1049
+ const applySelectCommand = (command, value, editable) => {
1050
+ editable.focus();
1051
+ const range = getSelectionRange(editable);
1052
+ if (!range) return;
1053
+ switch (command) {
1054
+ case "font":
1055
+ if (value === "default") clearFormatting(editable, range);
1056
+ else wrapRange(range, "span", { style: `font-family:${value === "serif" ? "serif" : "monospace"}` });
1057
+ break;
1058
+ case "size":
1059
+ wrapRange(range, "span", { style: value === "small" ? "font-size:12px" : value === "large" ? "font-size:20px" : value === "huge" ? "font-size:28px" : "font-size:16px" });
1060
+ break;
1061
+ case "header":
1062
+ applyHeader(value, editable, range);
1063
+ break;
1064
+ case "script":
1065
+ applyScript(value, editable, range);
1066
+ break;
1067
+ case "align":
1068
+ applyAlign(editable, range, value);
1069
+ break;
1070
+ case "list":
1071
+ ensureList(value === "ordered" ? "ol" : "ul", editable, range, value === "none");
1072
+ break;
1073
+ case "direction":
1074
+ applyDirection(editable, range, value === "rtl" ? "rtl" : "ltr");
1075
+ break;
1076
+ default: break;
1077
+ }
1078
+ };
1079
+ const renderButton = (command, editable, requestEmbedValue, onMutate) => {
1080
+ const button = document.createElement("button");
1081
+ button.type = "button";
1082
+ button.className = "az-rich-editor-toolbar-button";
1083
+ button.setAttribute("aria-label", commandLabels[command] ?? command);
1084
+ button.title = commandLabels[command] ?? command;
1085
+ button.append(createToolbarIcon(command, command));
1086
+ button.addEventListener("click", async () => {
1087
+ await applyButtonCommand(command, editable, requestEmbedValue);
1088
+ onMutate();
1089
+ });
1090
+ return button;
1091
+ };
1092
+ const createSelect = (format, options, editable, onMutate) => {
1093
+ const select = document.createElement("select");
1094
+ select.className = "az-rich-editor-toolbar-select";
1095
+ select.setAttribute("aria-label", format);
1096
+ options.forEach((optionValue) => {
1097
+ const option = document.createElement("option");
1098
+ option.value = optionValue;
1099
+ option.textContent = selectOptionLabels[format]?.[optionValue] ?? optionValue;
1100
+ option.title = optionValue;
1101
+ select.append(option);
1102
+ });
1103
+ select.addEventListener("change", () => {
1104
+ applySelectCommand(format, select.value, editable);
1105
+ onMutate();
1106
+ });
1107
+ return select;
1108
+ };
1109
+ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1110
+ const toolbar = document.createElement("div");
1111
+ toolbar.className = "az-rich-editor-toolbar";
1112
+ toolbar.setAttribute("role", "toolbar");
1113
+ toolbar.setAttribute("aria-label", "Rich editor toolbar");
1114
+ [
1115
+ {
1116
+ format: "font",
1117
+ options: [
1118
+ "default",
1119
+ "serif",
1120
+ "monospace"
1121
+ ]
1122
+ },
1123
+ {
1124
+ format: "size",
1125
+ options: [
1126
+ "small",
1127
+ "normal",
1128
+ "large",
1129
+ "huge"
1130
+ ]
1131
+ },
1132
+ {
1133
+ format: "header",
1134
+ options: [
1135
+ "normal",
1136
+ "h1",
1137
+ "h2"
1138
+ ]
1139
+ },
1140
+ {
1141
+ format: "script",
1142
+ options: [
1143
+ "normal",
1144
+ "sub",
1145
+ "super"
1146
+ ]
1147
+ },
1148
+ {
1149
+ format: "align",
1150
+ options: [
1151
+ "left",
1152
+ "center",
1153
+ "right",
1154
+ "justify"
1155
+ ]
1156
+ },
1157
+ {
1158
+ format: "list",
1159
+ options: [
1160
+ "none",
1161
+ "ordered",
1162
+ "bullet"
1163
+ ]
1164
+ },
1165
+ {
1166
+ format: "direction",
1167
+ options: ["ltr", "rtl"]
1168
+ }
1169
+ ].forEach((config) => toolbar.append(createSelect(config.format, config.options, editable, onMutate)));
1170
+ [
1171
+ "bold",
1172
+ "italic",
1173
+ "underline",
1174
+ "strike",
1175
+ "code",
1176
+ "blockquote",
1177
+ "code-block",
1178
+ "indent-",
1179
+ "indent+",
1180
+ "link",
1181
+ "image",
1182
+ "video",
1183
+ "formula",
1184
+ "clean"
1185
+ ].forEach((command) => toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate)));
1186
+ return toolbar;
1187
+ };
1188
+ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue }) => {
1189
+ host.innerHTML = "";
1190
+ const wrapper = document.createElement("div");
1191
+ wrapper.className = "az-rich-editor";
1192
+ const editable = document.createElement("div");
1193
+ editable.className = "az-rich-editor-input";
1194
+ editable.setAttribute("aria-label", "Rich editor input");
1195
+ editable.setAttribute("role", "textbox");
1196
+ editable.setAttribute("aria-multiline", "true");
1197
+ editable.setAttribute("contenteditable", disabled ? "false" : "true");
1198
+ editable.dataset.placeholder = "Compose an epic...";
1199
+ editable.innerHTML = initialHtml;
1200
+ const emitChange = () => onChange(editable.innerHTML);
1201
+ const toolbar = buildToolbar(editable, requestEmbedValue, emitChange);
1202
+ const inputHandler = () => emitChange();
1203
+ editable.addEventListener("input", inputHandler);
1204
+ wrapper.append(toolbar, editable);
1205
+ host.append(wrapper);
1206
+ return {
1207
+ destroy: () => {
1208
+ editable.removeEventListener("input", inputHandler);
1209
+ host.innerHTML = "";
1210
+ },
1211
+ getHtml: () => editable.innerHTML,
1212
+ setHtml: (html) => {
1213
+ editable.innerHTML = html;
1214
+ }
1215
+ };
1216
+ };
1217
+ //#endregion
1218
+ //#region src/adapters/dom/createDomAdapter.ts
1219
+ const createDomAdapter = (editor) => {
1220
+ let target = null;
1221
+ let mountedEditor = null;
1222
+ return {
1223
+ mount: (nextTarget) => {
1224
+ target = nextTarget;
1225
+ editor.mount(nextTarget);
1226
+ if (!(nextTarget instanceof HTMLElement)) return;
1227
+ mountedEditor = mountRichTextEditor({
1228
+ host: nextTarget,
1229
+ initialHtml: editor.export("html").payload,
1230
+ requestEmbedValue: () => null,
1231
+ onChange: (html) => {
1232
+ editor.import({
1233
+ format: "html",
1234
+ payload: html
1235
+ });
1236
+ }
1237
+ });
1238
+ },
1239
+ unmount: () => {
1240
+ if (!target) return;
1241
+ mountedEditor?.destroy();
1242
+ mountedEditor = null;
1243
+ editor.unmount();
1244
+ target = null;
1245
+ },
1246
+ destroy: () => {
1247
+ mountedEditor?.destroy();
1248
+ mountedEditor = null;
1249
+ editor.destroy();
1250
+ target = null;
1251
+ }
1252
+ };
1253
+ };
1254
+ //#endregion
1255
+ //#region src/adapters/react/useEditorAdapter.tsx
1256
+ const useEditorAdapter = (config) => {
1257
+ const editor = (0, react.useMemo)(() => createEditor(config), [config]);
1258
+ (0, react.useEffect)(() => () => {
1259
+ editor.destroy();
1260
+ }, [editor]);
1261
+ return editor;
1262
+ };
1263
+ function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue }) {
1264
+ const editor = useEditorAdapter({
1265
+ initialContent,
1266
+ onChange,
1267
+ onError
1268
+ });
1269
+ const hostRef = (0, react.useRef)(null);
1270
+ (0, react.useEffect)(() => {
1271
+ const host = hostRef.current;
1272
+ if (!host) return;
1273
+ const mounted = mountRichTextEditor({
1274
+ host,
1275
+ initialHtml: editor.export("html").payload,
1276
+ disabled,
1277
+ requestEmbedValue: onRequestEmbedValue,
1278
+ onChange: (html) => {
1279
+ editor.import({
1280
+ format: "html",
1281
+ payload: html
1282
+ });
1283
+ }
1284
+ });
1285
+ return () => {
1286
+ mounted.destroy();
1287
+ };
1288
+ }, [
1289
+ disabled,
1290
+ editor,
1291
+ onRequestEmbedValue
1292
+ ]);
1293
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1294
+ "aria-label": "Rich editor",
1295
+ className,
1296
+ ref: hostRef
1297
+ });
1298
+ }
1299
+ //#endregion
1300
+ exports.RichEditorAdapter = RichEditorAdapter;
1301
+ exports.createDomAdapter = createDomAdapter;
1302
+ exports.createEditor = createEditor;
1303
+ exports.createEditorSchema = createEditorSchema;
1304
+ exports.defaultToolbarActions = defaultToolbarActions;
1305
+ exports.editorMarkNames = editorMarkNames;
1306
+ exports.editorNodeNames = editorNodeNames;
1307
+ exports.editorSchema = editorSchema;
1308
+ exports.exportRepresentation = exportRepresentation;
1309
+ exports.importRepresentation = importRepresentation;
1310
+ exports.useEditorAdapter = useEditorAdapter;