@azlib/editor 0.2.0 → 0.3.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 CHANGED
@@ -132,8 +132,7 @@ var EditorHistory = class {
132
132
  }
133
133
  };
134
134
  //#endregion
135
- //#region src/transforms/html.ts
136
- const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
135
+ //#region src/core/schema.ts
137
136
  const allowedStyleProperties = new Set([
138
137
  "font-family",
139
138
  "font-size",
@@ -164,6 +163,7 @@ const normalizeStyleValue = (property, value) => {
164
163
  return null;
165
164
  };
166
165
  const normalizeStyleAttribute = (styleValue) => {
166
+ if (!styleValue) return null;
167
167
  const entries = styleValue.split(";").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => {
168
168
  const separator = entry.indexOf(":");
169
169
  if (separator < 0) return null;
@@ -177,60 +177,350 @@ const normalizeStyleAttribute = (styleValue) => {
177
177
  if (entries.length === 0) return null;
178
178
  return entries.join(";");
179
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;
180
+ const editorNodeNames = {
181
+ doc: "doc",
182
+ paragraph: "paragraph",
183
+ heading: "heading",
184
+ text: "text",
185
+ bulletList: "bullet_list",
186
+ orderedList: "ordered_list",
187
+ listItem: "list_item",
188
+ hardBreak: "hard_break",
189
+ blockquote: "blockquote",
190
+ codeBlock: "code_block",
191
+ image: "image",
192
+ video: "video"
193
+ };
194
+ const editorMarkNames = {
195
+ strong: "strong",
196
+ em: "em",
197
+ underline: "underline",
198
+ link: "link",
199
+ strike: "strike",
200
+ code: "code",
201
+ style: "style",
202
+ formula: "formula"
203
+ };
204
+ const createEditorSchema = () => new prosemirror_model.Schema({
205
+ nodes: {
206
+ doc: { content: "block+" },
207
+ paragraph: {
208
+ content: "inline*",
209
+ group: "block",
210
+ attrs: {
211
+ style: { default: null },
212
+ dir: { default: null }
213
+ },
214
+ parseDOM: [{
215
+ tag: "p",
216
+ getAttrs: (dom) => {
217
+ if (!(dom instanceof HTMLElement)) return null;
218
+ return {
219
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
220
+ dir: dom.getAttribute("dir") || null
221
+ };
222
+ }
223
+ }],
224
+ toDOM: (node) => {
225
+ const attrs = {};
226
+ if (node.attrs.style) attrs.style = node.attrs.style;
227
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
228
+ return [
229
+ "p",
230
+ attrs,
231
+ 0
232
+ ];
233
+ }
234
+ },
235
+ heading: {
236
+ attrs: {
237
+ level: { default: 1 },
238
+ style: { default: null },
239
+ dir: { default: null }
240
+ },
241
+ content: "inline*",
242
+ group: "block",
243
+ defining: true,
244
+ parseDOM: [
245
+ {
246
+ tag: "h1",
247
+ getAttrs: (dom) => {
248
+ if (!(dom instanceof HTMLElement)) return { level: 1 };
249
+ return {
250
+ level: 1,
251
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
252
+ dir: dom.getAttribute("dir") || null
253
+ };
254
+ }
255
+ },
256
+ {
257
+ tag: "h2",
258
+ getAttrs: (dom) => {
259
+ if (!(dom instanceof HTMLElement)) return { level: 2 };
260
+ return {
261
+ level: 2,
262
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
263
+ dir: dom.getAttribute("dir") || null
264
+ };
265
+ }
266
+ },
267
+ {
268
+ tag: "h3",
269
+ getAttrs: (dom) => {
270
+ if (!(dom instanceof HTMLElement)) return { level: 3 };
271
+ return {
272
+ level: 3,
273
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
274
+ dir: dom.getAttribute("dir") || null
275
+ };
276
+ }
277
+ }
278
+ ],
279
+ toDOM: (node) => {
280
+ const attrs = {};
281
+ if (node.attrs.style) attrs.style = node.attrs.style;
282
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
283
+ return [
284
+ `h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`,
285
+ attrs,
286
+ 0
287
+ ];
210
288
  }
211
- if (name === "href" || name === "src") {
212
- if (!/^(https?:|mailto:|tel:)/i.test(valueAttr.trim())) element.removeAttribute(attribute.name);
213
- return;
289
+ },
290
+ blockquote: {
291
+ content: "block+",
292
+ group: "block",
293
+ attrs: {
294
+ style: { default: null },
295
+ dir: { default: null }
296
+ },
297
+ parseDOM: [{
298
+ tag: "blockquote",
299
+ getAttrs: (dom) => {
300
+ if (!(dom instanceof HTMLElement)) return null;
301
+ return {
302
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
303
+ dir: dom.getAttribute("dir") || null
304
+ };
305
+ }
306
+ }],
307
+ toDOM: (node) => {
308
+ const attrs = {};
309
+ if (node.attrs.style) attrs.style = node.attrs.style;
310
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
311
+ return [
312
+ "blockquote",
313
+ attrs,
314
+ 0
315
+ ];
214
316
  }
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");
317
+ },
318
+ code_block: {
319
+ content: "inline*",
320
+ group: "block",
321
+ code: true,
322
+ defining: true,
323
+ attrs: {
324
+ style: { default: null },
325
+ dir: { default: null }
326
+ },
327
+ parseDOM: [{
328
+ tag: "pre",
329
+ preserveWhitespace: "full",
330
+ getAttrs: (dom) => {
331
+ if (!(dom instanceof HTMLElement)) return null;
332
+ return {
333
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
334
+ dir: dom.getAttribute("dir") || null
335
+ };
336
+ }
337
+ }],
338
+ toDOM: (node) => {
339
+ const attrs = {};
340
+ if (node.attrs.style) attrs.style = node.attrs.style;
341
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
342
+ return [
343
+ "pre",
344
+ attrs,
345
+ ["code", 0]
346
+ ];
347
+ }
348
+ },
349
+ bullet_list: {
350
+ content: "list_item+",
351
+ group: "block",
352
+ parseDOM: [{ tag: "ul" }],
353
+ toDOM: () => ["ul", 0]
354
+ },
355
+ ordered_list: {
356
+ attrs: { order: { default: 1 } },
357
+ content: "list_item+",
358
+ group: "block",
359
+ parseDOM: [{
360
+ tag: "ol",
361
+ getAttrs: (dom) => {
362
+ if (!(dom instanceof HTMLOListElement)) return { order: 1 };
363
+ return { order: dom.start || 1 };
364
+ }
365
+ }],
366
+ toDOM: (node) => [
367
+ "ol",
368
+ { start: node.attrs.order || 1 },
369
+ 0
370
+ ]
371
+ },
372
+ list_item: {
373
+ content: "paragraph block*",
374
+ parseDOM: [{ tag: "li" }],
375
+ toDOM: () => ["li", 0]
376
+ },
377
+ image: {
378
+ inline: true,
379
+ attrs: {
380
+ src: {},
381
+ alt: { default: null }
382
+ },
383
+ group: "inline",
384
+ draggable: true,
385
+ parseDOM: [{
386
+ tag: "img[src]",
387
+ getAttrs: (dom) => {
388
+ if (!(dom instanceof HTMLElement)) return null;
389
+ return {
390
+ src: dom.getAttribute("src"),
391
+ alt: dom.getAttribute("alt")
392
+ };
393
+ }
394
+ }],
395
+ toDOM: (node) => ["img", {
396
+ src: node.attrs.src,
397
+ alt: node.attrs.alt
398
+ }]
399
+ },
400
+ video: {
401
+ inline: true,
402
+ attrs: {
403
+ src: {},
404
+ controls: { default: "true" }
405
+ },
406
+ group: "inline",
407
+ parseDOM: [{
408
+ tag: "video[src]",
409
+ getAttrs: (dom) => {
410
+ if (!(dom instanceof HTMLElement)) return null;
411
+ return {
412
+ src: dom.getAttribute("src"),
413
+ controls: dom.getAttribute("controls") || "true"
414
+ };
415
+ }
416
+ }],
417
+ toDOM: (node) => ["video", {
418
+ src: node.attrs.src,
419
+ controls: node.attrs.controls
420
+ }]
421
+ },
422
+ text: { group: "inline" },
423
+ hard_break: {
424
+ inline: true,
425
+ group: "inline",
426
+ selectable: false,
427
+ parseDOM: [{ tag: "br" }],
428
+ toDOM: () => ["br"]
221
429
  }
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);
430
+ },
431
+ marks: {
432
+ strong: {
433
+ parseDOM: [{ tag: "strong" }, {
434
+ tag: "b",
435
+ getAttrs: () => null
436
+ }],
437
+ toDOM: () => ["strong", 0]
438
+ },
439
+ em: {
440
+ parseDOM: [{ tag: "em" }, {
441
+ tag: "i",
442
+ getAttrs: () => null
443
+ }],
444
+ toDOM: () => ["em", 0]
445
+ },
446
+ underline: {
447
+ parseDOM: [{ tag: "u" }],
448
+ toDOM: () => ["u", 0]
449
+ },
450
+ strike: {
451
+ parseDOM: [
452
+ { tag: "s" },
453
+ { tag: "del" },
454
+ { tag: "strike" }
455
+ ],
456
+ toDOM: () => ["s", 0]
457
+ },
458
+ code: {
459
+ parseDOM: [{ tag: "code" }],
460
+ toDOM: () => ["code", 0]
461
+ },
462
+ style: {
463
+ attrs: { style: { default: null } },
464
+ parseDOM: [{
465
+ tag: "span[style]",
466
+ getAttrs: (dom) => {
467
+ if (!(dom instanceof HTMLElement)) return null;
468
+ const normalized = normalizeStyleAttribute(dom.getAttribute("style") || "");
469
+ return normalized ? { style: normalized } : false;
470
+ }
471
+ }],
472
+ toDOM: (node) => [
473
+ "span",
474
+ { style: node.attrs.style },
475
+ 0
476
+ ]
477
+ },
478
+ link: {
479
+ attrs: {
480
+ href: { default: null },
481
+ title: { default: null },
482
+ style: { default: null }
483
+ },
484
+ inclusive: false,
485
+ parseDOM: [{
486
+ tag: "a",
487
+ getAttrs: (dom) => {
488
+ if (!(dom instanceof HTMLElement)) return false;
489
+ const href = dom.getAttribute("href");
490
+ const style = dom.getAttribute("style");
491
+ if (!href && !style) return false;
492
+ return {
493
+ href: href || null,
494
+ title: dom.getAttribute("title") || null,
495
+ style: normalizeStyleAttribute(style || "")
496
+ };
497
+ }
498
+ }],
499
+ toDOM: (node) => {
500
+ const attrs = {};
501
+ if (node.attrs.href) attrs.href = node.attrs.href;
502
+ if (node.attrs.title) attrs.title = node.attrs.title;
503
+ if (node.attrs.style) attrs.style = node.attrs.style;
504
+ return [
505
+ "a",
506
+ attrs,
507
+ 0
508
+ ];
509
+ }
510
+ },
511
+ formula: {
512
+ parseDOM: [{ tag: "span[data-formula]" }],
513
+ toDOM: () => [
514
+ "span",
515
+ { "data-formula": "true" },
516
+ 0
517
+ ]
229
518
  }
230
- });
231
- const normalized = root.innerHTML.trim();
232
- return normalized.length > 0 ? normalized : "<p></p>";
233
- };
519
+ }
520
+ });
521
+ const editorSchema = createEditorSchema();
522
+ //#endregion
523
+ //#region src/transforms/html.ts
234
524
  const sanitizeHtml = (payload) => isomorphic_dompurify.default.sanitize(payload, {
235
525
  ALLOWED_TAGS: [
236
526
  "p",
@@ -247,6 +537,7 @@ const sanitizeHtml = (payload) => isomorphic_dompurify.default.sanitize(payload,
247
537
  "blockquote",
248
538
  "h1",
249
539
  "h2",
540
+ "h3",
250
541
  "ul",
251
542
  "ol",
252
543
  "li",
@@ -282,19 +573,29 @@ const sanitizeHtml = (payload) => isomorphic_dompurify.default.sanitize(payload,
282
573
  "select"
283
574
  ]
284
575
  });
576
+ const normalizeHtml = (value) => {
577
+ if (typeof globalThis.DOMParser === "undefined") return value.trim().length > 0 ? value : "<p></p>";
578
+ const domDoc = new globalThis.DOMParser().parseFromString(`<body>${value}</body>`, "text/html");
579
+ const pmNode = prosemirror_model.DOMParser.fromSchema(editorSchema).parse(domDoc.body);
580
+ const fragment = prosemirror_model.DOMSerializer.fromSchema(editorSchema).serializeFragment(pmNode.content);
581
+ const container = document.createElement("div");
582
+ container.appendChild(fragment);
583
+ const normalized = container.innerHTML.trim();
584
+ return normalized.length > 0 ? normalized : "<p></p>";
585
+ };
285
586
  const importHtml = (payload) => {
286
587
  const diagnostics = [];
287
588
  const normalized = normalizeHtml(sanitizeHtml(payload));
288
589
  if (normalized !== payload) diagnostics.push(createDiagnostic("SANITIZED_CONTENT", "warning", "Input HTML was sanitized to remove unsupported or unsafe markup."));
289
590
  if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_HTML", "info", "The provided HTML content is empty after normalization."));
290
591
  return {
291
- richText: stripHtml(normalized),
592
+ richText: normalized,
292
593
  sanitizedHtml: normalized,
293
594
  diagnostics
294
595
  };
295
596
  };
296
597
  const exportHtml = (richText) => {
297
- return normalizeHtml(`<p>${richText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")}</p>`);
598
+ return normalizeHtml(richText);
298
599
  };
299
600
  //#endregion
300
601
  //#region src/transforms/diagnostics.ts
@@ -325,26 +626,224 @@ const applyImportFallback = (input, base) => {
325
626
  //#endregion
326
627
  //#region src/transforms/markdown.ts
327
628
  const normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
629
+ const escapeHtml = (text) => {
630
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
631
+ };
632
+ const parseInline = (text) => {
633
+ let html = escapeHtml(text);
634
+ html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
635
+ html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
636
+ html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
637
+ html = html.replace(/_(.*?)_/g, "<em>$1</em>");
638
+ html = html.replace(/~~(.*?)~~/g, "<s>$1</s>");
639
+ html = html.replace(/`(.*?)`/g, "<code>$1</code>");
640
+ html = html.replace(/\[(.*?)\]\((.*?)\)/g, "<a href=\"$2\">$1</a>");
641
+ return html;
642
+ };
643
+ const markdownToHtml = (markdown) => {
644
+ const lines = normalizeMarkdown(markdown).split("\n");
645
+ const blocks = [];
646
+ let currentBlockType = null;
647
+ let blockLines = [];
648
+ const closeCurrentBlock = () => {
649
+ if (!currentBlockType) return;
650
+ if (currentBlockType === "paragraph") {
651
+ const content = parseInline(blockLines.join("\n"));
652
+ blocks.push(`<p>${content}</p>`);
653
+ } else if (currentBlockType === "blockquote") {
654
+ const content = markdownToHtml(blockLines.join("\n"));
655
+ blocks.push(`<blockquote>${content}</blockquote>`);
656
+ } else if (currentBlockType === "code_block") {
657
+ const content = escapeHtml(blockLines.join("\n"));
658
+ blocks.push(`<pre><code>${content}</code></pre>`);
659
+ } else if (currentBlockType === "ul" || currentBlockType === "ol") {
660
+ const tag = currentBlockType;
661
+ const itemBlocks = [];
662
+ let currentItemLines = [];
663
+ const closeItem = () => {
664
+ if (currentItemLines.length > 0) {
665
+ const innerHtml = markdownToHtml(currentItemLines.join("\n"));
666
+ itemBlocks.push(`<li>${innerHtml}</li>`);
667
+ currentItemLines = [];
668
+ }
669
+ };
670
+ for (const line of blockLines) {
671
+ const ulMatch = /^[*-+]\s+(.*)$/.exec(line.trim());
672
+ const olMatch = /^\d+\.\s+(.*)$/.exec(line.trim());
673
+ if (ulMatch) {
674
+ closeItem();
675
+ currentItemLines.push(ulMatch[1]);
676
+ } else if (olMatch) {
677
+ closeItem();
678
+ currentItemLines.push(olMatch[1]);
679
+ } else currentItemLines.push(line.trimStart());
680
+ }
681
+ closeItem();
682
+ blocks.push(`<${tag}>${itemBlocks.join("")}</${tag}>`);
683
+ }
684
+ blockLines = [];
685
+ currentBlockType = null;
686
+ };
687
+ let i = 0;
688
+ while (i < lines.length) {
689
+ const line = lines[i];
690
+ if (line.startsWith("```")) {
691
+ if (currentBlockType === "code_block") closeCurrentBlock();
692
+ else {
693
+ closeCurrentBlock();
694
+ currentBlockType = "code_block";
695
+ }
696
+ i++;
697
+ continue;
698
+ }
699
+ if (currentBlockType === "code_block") {
700
+ blockLines.push(line);
701
+ i++;
702
+ continue;
703
+ }
704
+ const trimmed = line.trim();
705
+ if (trimmed === "") {
706
+ closeCurrentBlock();
707
+ i++;
708
+ continue;
709
+ }
710
+ const headerMatch = /^(#{1,6})\s+(.*)$/.exec(trimmed);
711
+ if (headerMatch) {
712
+ closeCurrentBlock();
713
+ const level = headerMatch[1].length;
714
+ const content = parseInline(headerMatch[2]);
715
+ blocks.push(`<h${level}>${content}</h${level}>`);
716
+ i++;
717
+ continue;
718
+ }
719
+ if (trimmed.startsWith(">")) {
720
+ if (currentBlockType !== "blockquote") {
721
+ closeCurrentBlock();
722
+ currentBlockType = "blockquote";
723
+ }
724
+ const rest = line.startsWith("> ") ? line.slice(2) : line.slice(1);
725
+ blockLines.push(rest);
726
+ i++;
727
+ continue;
728
+ }
729
+ if (/^[*-+]\s+(.*)$/.exec(trimmed)) {
730
+ if (currentBlockType !== "ul") {
731
+ closeCurrentBlock();
732
+ currentBlockType = "ul";
733
+ }
734
+ blockLines.push(line);
735
+ i++;
736
+ continue;
737
+ }
738
+ if (/^\d+\.\s+(.*)$/.exec(trimmed)) {
739
+ if (currentBlockType !== "ol") {
740
+ closeCurrentBlock();
741
+ currentBlockType = "ol";
742
+ }
743
+ blockLines.push(line);
744
+ i++;
745
+ continue;
746
+ }
747
+ if (!currentBlockType) currentBlockType = "paragraph";
748
+ blockLines.push(line);
749
+ i++;
750
+ }
751
+ closeCurrentBlock();
752
+ return blocks.join("");
753
+ };
754
+ const serializeChild = (child, index, parent) => {
755
+ if (child.isText) {
756
+ let text = child.text || "";
757
+ child.marks.forEach((mark) => {
758
+ if (mark.type.name === "strong") text = `**${text}**`;
759
+ else if (mark.type.name === "em") text = `*${text}*`;
760
+ else if (mark.type.name === "strike") text = `~~${text}~~`;
761
+ else if (mark.type.name === "code") text = `\`${text}\``;
762
+ else if (mark.type.name === "link") {
763
+ const href = mark.attrs.href || "";
764
+ text = `[${text}](${href})`;
765
+ }
766
+ });
767
+ return text;
768
+ }
769
+ if (child.type.name === "hard_break") return "\n";
770
+ if (child.type.name === "image") {
771
+ const src = child.attrs.src || "";
772
+ return `![${child.attrs.alt || ""}](${src})`;
773
+ }
774
+ if (child.type.name === "video") return `[Video: ${child.attrs.src || ""}]`;
775
+ let inner = "";
776
+ child.forEach((c, idx) => {
777
+ inner += serializeChild(c, idx, child);
778
+ });
779
+ if (child.type.name === "paragraph") return `${inner}\n\n`;
780
+ if (child.type.name === "heading") return `${"#".repeat(child.attrs.level || 1)} ${inner}\n\n`;
781
+ if (child.type.name === "blockquote") return `${inner.trim().split("\n").map((line) => `> ${line}`).join("\n")}\n\n`;
782
+ if (child.type.name === "code_block") return `\`\`\`\n${inner.trim()}\n\`\`\`\n\n`;
783
+ if (child.type.name === "bullet_list") {
784
+ let listResult = "";
785
+ child.forEach((item) => {
786
+ const indented = serializeNode(item).trim().split("\n").map((line, idx) => {
787
+ if (idx === 0) return `* ${line}`;
788
+ return ` ${line}`;
789
+ }).join("\n");
790
+ listResult += `${indented}\n`;
791
+ });
792
+ return `${listResult}\n`;
793
+ }
794
+ if (child.type.name === "ordered_list") {
795
+ let listResult = "";
796
+ let count = child.attrs.order || 1;
797
+ child.forEach((item) => {
798
+ const indented = serializeNode(item).trim().split("\n").map((line, idx) => {
799
+ if (idx === 0) return `${count}. ${line}`;
800
+ return ` ${line}`;
801
+ }).join("\n");
802
+ listResult += `${indented}\n`;
803
+ count++;
804
+ });
805
+ return `${listResult}\n`;
806
+ }
807
+ if (child.type.name === "list_item") {
808
+ let itemInner = "";
809
+ child.forEach((c, idx) => {
810
+ itemInner += serializeChild(c, idx, child);
811
+ });
812
+ return itemInner;
813
+ }
814
+ return inner;
815
+ };
816
+ const serializeNode = (node) => {
817
+ let result = "";
818
+ node.forEach((child, index) => {
819
+ result += serializeChild(child, index, node);
820
+ });
821
+ return result.trim();
822
+ };
328
823
  const importMarkdown = (payload) => {
329
824
  const diagnostics = [];
330
825
  const normalized = normalizeMarkdown(payload);
331
826
  if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_MARKDOWN", "info", "The provided markdown content is empty."));
332
827
  return {
333
- richText: normalized,
828
+ richText: markdownToHtml(normalized),
334
829
  diagnostics
335
830
  };
336
831
  };
337
- const exportMarkdown = (richText) => normalizeMarkdown(richText);
832
+ const exportMarkdown = (richText) => {
833
+ if (typeof globalThis.DOMParser === "undefined") return richText;
834
+ const domDoc = new globalThis.DOMParser().parseFromString(`<body>${richText}</body>`, "text/html");
835
+ return serializeNode(prosemirror_model.DOMParser.fromSchema(editorSchema).parse(domDoc.body));
836
+ };
338
837
  //#endregion
339
838
  //#region src/transforms/representationSwitch.ts
340
839
  const importRepresentation = (input, base) => {
341
840
  if (input.format === "rich") {
342
- const normalizedHtml = normalizeHtml(exportHtml(input.payload));
841
+ const normalizedHtml = normalizeHtml(input.payload);
343
842
  return {
344
843
  document: {
345
844
  ...base,
346
845
  content: {
347
- richText: input.payload,
846
+ richText: normalizedHtml,
348
847
  html: normalizedHtml
349
848
  }
350
849
  },
@@ -356,7 +855,10 @@ const importRepresentation = (input, base) => {
356
855
  return {
357
856
  document: {
358
857
  ...base,
359
- content: { richText: result.richText },
858
+ content: {
859
+ richText: result.richText,
860
+ html: result.richText
861
+ },
360
862
  metadata: {
361
863
  ...base.metadata,
362
864
  importedFrom: "markdown"
@@ -419,6 +921,11 @@ const createEditor = (config = {}) => {
419
921
  const history = new EditorHistory();
420
922
  let isDestroyed = false;
421
923
  let currentDocument = createDefaultDocument();
924
+ const listeners = {
925
+ change: /* @__PURE__ */ new Set(),
926
+ selectionchange: /* @__PURE__ */ new Set()
927
+ };
928
+ let activeHandler = null;
422
929
  if (config.initialContent) {
423
930
  const initialized = fromInput(config.initialContent, currentDocument);
424
931
  currentDocument = initialized.document;
@@ -431,6 +938,7 @@ const createEditor = (config = {}) => {
431
938
  };
432
939
  const emitChange = () => {
433
940
  config.onChange?.(currentDocument);
941
+ listeners.change.forEach((cb) => cb());
434
942
  };
435
943
  return {
436
944
  getDocument: () => currentDocument,
@@ -440,6 +948,21 @@ const createEditor = (config = {}) => {
440
948
  document: currentDocument,
441
949
  diagnostics: []
442
950
  };
951
+ if (!(config.commandCapabilities ? new Set(config.commandCapabilities) : new Set([
952
+ "bold",
953
+ "italic",
954
+ "underline",
955
+ "heading",
956
+ "list",
957
+ "align",
958
+ "link",
959
+ "undo",
960
+ "redo"
961
+ ])).has(commandName)) return {
962
+ ok: false,
963
+ document: currentDocument,
964
+ diagnostics: [createUnknownCommandDiagnostic(commandName)]
965
+ };
443
966
  const result = executeCommand(registry, commandName, { document: currentDocument }, params);
444
967
  if (commandName === "undo") {
445
968
  const next = history.undo(currentDocument);
@@ -505,141 +1028,27 @@ const createEditor = (config = {}) => {
505
1028
  unmount: () => {},
506
1029
  destroy: () => {
507
1030
  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]
1031
+ listeners.change.clear();
1032
+ listeners.selectionchange.clear();
586
1033
  },
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]
1034
+ isMarkActive: (name) => activeHandler?.isMarkActive(name) ?? false,
1035
+ getActiveBlockType: () => activeHandler?.getActiveBlockType() ?? "paragraph",
1036
+ on: (event, callback) => {
1037
+ listeners[event].add(callback);
1038
+ return () => {
1039
+ listeners[event].delete(callback);
1040
+ };
603
1041
  },
604
- em: {
605
- parseDOM: [{ tag: "em" }, {
606
- tag: "i",
607
- getAttrs: () => null
608
- }],
609
- toDOM: () => ["em", 0]
1042
+ toolbar: config.toolbar,
1043
+ placeholder: config.placeholder,
1044
+ _registerActiveHandler: (handler) => {
1045
+ activeHandler = handler;
610
1046
  },
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
- ]
1047
+ _triggerSelectionChange: () => {
1048
+ listeners.selectionchange.forEach((cb) => cb());
639
1049
  }
640
- }
641
- });
642
- const editorSchema = createEditorSchema();
1050
+ };
1051
+ };
643
1052
  //#endregion
644
1053
  //#region src/core/toolbarModel.ts
645
1054
  const defaultToolbarActions = [
@@ -733,40 +1142,40 @@ const createToolbarIcon = (command, fallback) => {
733
1142
  };
734
1143
  const selectOptionLabels = {
735
1144
  font: {
736
- default: "A",
737
- serif: "As",
738
- monospace: "Am"
1145
+ default: "Font",
1146
+ serif: "Serif",
1147
+ monospace: "Monospace"
739
1148
  },
740
1149
  size: {
741
- small: "S",
742
- normal: "N",
743
- large: "L",
744
- huge: "XL"
1150
+ normal: "Normal (16px)",
1151
+ small: "Small (12px)",
1152
+ large: "Large (20px)",
1153
+ huge: "Huge (28px)"
745
1154
  },
746
1155
  header: {
747
- normal: "P",
748
- h1: "H1",
749
- h2: "H2"
1156
+ normal: "Normal Text",
1157
+ h1: "Heading 1",
1158
+ h2: "Heading 2"
750
1159
  },
751
1160
  script: {
752
- normal: "x",
753
- sub: "x_",
754
- super: "x^"
1161
+ normal: "Script",
1162
+ sub: "Subscript",
1163
+ super: "Superscript"
755
1164
  },
756
1165
  align: {
757
- left: "L",
758
- center: "C",
759
- right: "R",
760
- justify: "J"
1166
+ left: "Align Left",
1167
+ center: "Align Center",
1168
+ right: "Align Right",
1169
+ justify: "Justify"
761
1170
  },
762
1171
  list: {
763
- none: "-",
764
- ordered: "1.",
765
- bullet: "o"
1172
+ none: "No List",
1173
+ ordered: "Numbered List",
1174
+ bullet: "Bulleted List"
766
1175
  },
767
1176
  direction: {
768
- ltr: "->",
769
- rtl: "<-"
1177
+ ltr: "Left-to-Right",
1178
+ rtl: "Right-to-Left"
770
1179
  }
771
1180
  };
772
1181
  const safeUrl = (value) => {
@@ -1106,11 +1515,13 @@ const createSelect = (format, options, editable, onMutate) => {
1106
1515
  });
1107
1516
  return select;
1108
1517
  };
1109
- const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1518
+ const buildToolbar = (editable, requestEmbedValue, onMutate, toolbarOption) => {
1519
+ if (toolbarOption === false) return null;
1110
1520
  const toolbar = document.createElement("div");
1111
1521
  toolbar.className = "az-rich-editor-toolbar";
1112
1522
  toolbar.setAttribute("role", "toolbar");
1113
1523
  toolbar.setAttribute("aria-label", "Rich editor toolbar");
1524
+ const allowed = Array.isArray(toolbarOption) ? new Set(toolbarOption) : null;
1114
1525
  [
1115
1526
  {
1116
1527
  format: "font",
@@ -1123,8 +1534,8 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1123
1534
  {
1124
1535
  format: "size",
1125
1536
  options: [
1126
- "small",
1127
1537
  "normal",
1538
+ "small",
1128
1539
  "large",
1129
1540
  "huge"
1130
1541
  ]
@@ -1166,7 +1577,9 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1166
1577
  format: "direction",
1167
1578
  options: ["ltr", "rtl"]
1168
1579
  }
1169
- ].forEach((config) => toolbar.append(createSelect(config.format, config.options, editable, onMutate)));
1580
+ ].forEach((config) => {
1581
+ if (!allowed || allowed.has(config.format)) toolbar.append(createSelect(config.format, config.options, editable, onMutate));
1582
+ });
1170
1583
  [
1171
1584
  "bold",
1172
1585
  "italic",
@@ -1182,10 +1595,12 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1182
1595
  "video",
1183
1596
  "formula",
1184
1597
  "clean"
1185
- ].forEach((command) => toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate)));
1186
- return toolbar;
1598
+ ].forEach((command) => {
1599
+ if (!allowed || allowed.has(command)) toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate));
1600
+ });
1601
+ return toolbar.childNodes.length > 0 ? toolbar : null;
1187
1602
  };
1188
- const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue }) => {
1603
+ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue, toolbar: toolbarOption, placeholder, onSelectionChange }) => {
1189
1604
  host.innerHTML = "";
1190
1605
  const wrapper = document.createElement("div");
1191
1606
  wrapper.className = "az-rich-editor";
@@ -1195,23 +1610,161 @@ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, re
1195
1610
  editable.setAttribute("role", "textbox");
1196
1611
  editable.setAttribute("aria-multiline", "true");
1197
1612
  editable.setAttribute("contenteditable", disabled ? "false" : "true");
1198
- editable.dataset.placeholder = "Compose an epic...";
1613
+ editable.dataset.placeholder = placeholder ?? "Compose an epic...";
1199
1614
  editable.innerHTML = initialHtml;
1200
1615
  const emitChange = () => onChange(editable.innerHTML);
1201
- const toolbar = buildToolbar(editable, requestEmbedValue, emitChange);
1616
+ const toolbar = buildToolbar(editable, requestEmbedValue, emitChange, toolbarOption);
1202
1617
  const inputHandler = () => emitChange();
1203
1618
  editable.addEventListener("input", inputHandler);
1204
- wrapper.append(toolbar, editable);
1619
+ const keydownHandler = (event) => {
1620
+ const range = getSelectionRange(editable);
1621
+ if (!range) return;
1622
+ if (event.metaKey || event.ctrlKey) {
1623
+ const key = event.key.toLowerCase();
1624
+ if (key === "b") {
1625
+ event.preventDefault();
1626
+ applyButtonCommand("bold", editable, requestEmbedValue);
1627
+ emitChange();
1628
+ } else if (key === "i") {
1629
+ event.preventDefault();
1630
+ applyButtonCommand("italic", editable, requestEmbedValue);
1631
+ emitChange();
1632
+ } else if (key === "u") {
1633
+ event.preventDefault();
1634
+ applyButtonCommand("underline", editable, requestEmbedValue);
1635
+ emitChange();
1636
+ }
1637
+ }
1638
+ if (event.key === "Tab") {
1639
+ if (closestByTag(getBlockElement(range, editable), "li", editable)) {
1640
+ event.preventDefault();
1641
+ applyIndent(editable, range, event.shiftKey ? "-" : "+");
1642
+ emitChange();
1643
+ }
1644
+ }
1645
+ };
1646
+ editable.addEventListener("keydown", keydownHandler);
1647
+ const checkMarkActive = (markName) => {
1648
+ const selection = window.getSelection();
1649
+ if (!selection || selection.rangeCount === 0) return false;
1650
+ const range = selection.getRangeAt(0);
1651
+ if (!editable.contains(range.commonAncestorContainer)) return false;
1652
+ const tags = {
1653
+ strong: ["strong", "b"],
1654
+ bold: ["strong", "b"],
1655
+ em: ["em", "i"],
1656
+ italic: ["em", "i"],
1657
+ underline: ["u"],
1658
+ strike: [
1659
+ "s",
1660
+ "del",
1661
+ "strike"
1662
+ ],
1663
+ code: ["code"],
1664
+ link: ["a"]
1665
+ }[markName];
1666
+ if (!tags) return false;
1667
+ let node = range.startContainer;
1668
+ while (node && node !== editable) {
1669
+ if (node instanceof HTMLElement) {
1670
+ const tagName = node.tagName.toLowerCase();
1671
+ if (tags.includes(tagName)) return true;
1672
+ if (markName === "code" && node.style.fontFamily === "monospace") return true;
1673
+ }
1674
+ node = node.parentNode;
1675
+ }
1676
+ return false;
1677
+ };
1678
+ const checkActiveBlockType = () => {
1679
+ const selection = window.getSelection();
1680
+ if (!selection || selection.rangeCount === 0) return "paragraph";
1681
+ const range = selection.getRangeAt(0);
1682
+ if (!editable.contains(range.commonAncestorContainer)) return "paragraph";
1683
+ let node = range.startContainer;
1684
+ while (node && node !== editable) {
1685
+ if (node instanceof HTMLElement) {
1686
+ const tagName = node.tagName.toLowerCase();
1687
+ if (tagName === "h1" || tagName === "h2" || tagName === "h3") return "heading";
1688
+ if (tagName === "blockquote") return "blockquote";
1689
+ if (tagName === "pre") return "code_block";
1690
+ if (tagName === "ol") return "ordered_list";
1691
+ if (tagName === "ul") return "bullet_list";
1692
+ }
1693
+ node = node.parentNode;
1694
+ }
1695
+ return "paragraph";
1696
+ };
1697
+ const updateToolbarStates = () => {
1698
+ if (!toolbar) return;
1699
+ toolbar.querySelectorAll(".az-rich-editor-toolbar-button").forEach((btn) => {
1700
+ const command = btn.getAttribute("aria-label");
1701
+ if (!command) return;
1702
+ let isActive = false;
1703
+ if (command === "Bold") isActive = checkMarkActive("strong");
1704
+ else if (command === "Italic") isActive = checkMarkActive("em");
1705
+ else if (command === "Underline") isActive = checkMarkActive("underline");
1706
+ else if (command === "Strike") isActive = checkMarkActive("strike");
1707
+ else if (command === "Inline code") isActive = checkMarkActive("code");
1708
+ else if (command === "Blockquote") isActive = checkActiveBlockType() === "blockquote";
1709
+ else if (command === "Code block") isActive = checkActiveBlockType() === "code_block";
1710
+ if (isActive) {
1711
+ btn.setAttribute("data-active", "true");
1712
+ btn.classList.add("active");
1713
+ } else {
1714
+ btn.removeAttribute("data-active");
1715
+ btn.classList.remove("active");
1716
+ }
1717
+ });
1718
+ toolbar.querySelectorAll(".az-rich-editor-toolbar-select").forEach((select) => {
1719
+ const label = select.getAttribute("aria-label");
1720
+ if (label === "header") if (checkActiveBlockType() === "heading") {
1721
+ const selection = window.getSelection();
1722
+ if (selection && selection.rangeCount > 0) {
1723
+ const level = getBlockElement(selection.getRangeAt(0), editable).tagName.toLowerCase();
1724
+ if (level === "h1" || level === "h2" || level === "h3") select.value = level;
1725
+ else select.value = "normal";
1726
+ }
1727
+ } else select.value = "normal";
1728
+ else if (label === "align") {
1729
+ const selection = window.getSelection();
1730
+ if (selection && selection.rangeCount > 0) select.value = getBlockElement(selection.getRangeAt(0), editable).style.textAlign || "left";
1731
+ } else if (label === "direction") {
1732
+ const selection = window.getSelection();
1733
+ if (selection && selection.rangeCount > 0) select.value = getBlockElement(selection.getRangeAt(0), editable).getAttribute("dir") || "ltr";
1734
+ }
1735
+ });
1736
+ };
1737
+ const selectionHandler = () => {
1738
+ const selection = window.getSelection();
1739
+ if (selection && selection.rangeCount > 0) {
1740
+ const range = selection.getRangeAt(0);
1741
+ if (editable.contains(range.commonAncestorContainer)) {
1742
+ onSelectionChange?.();
1743
+ updateToolbarStates();
1744
+ }
1745
+ }
1746
+ };
1747
+ document.addEventListener("selectionchange", selectionHandler);
1748
+ if (toolbar) {
1749
+ wrapper.append(toolbar);
1750
+ updateToolbarStates();
1751
+ }
1752
+ wrapper.append(editable);
1205
1753
  host.append(wrapper);
1206
1754
  return {
1207
1755
  destroy: () => {
1208
1756
  editable.removeEventListener("input", inputHandler);
1757
+ editable.removeEventListener("keydown", keydownHandler);
1758
+ document.removeEventListener("selectionchange", selectionHandler);
1209
1759
  host.innerHTML = "";
1210
1760
  },
1211
1761
  getHtml: () => editable.innerHTML,
1212
1762
  setHtml: (html) => {
1213
1763
  editable.innerHTML = html;
1214
- }
1764
+ updateToolbarStates();
1765
+ },
1766
+ isMarkActive: (markName) => checkMarkActive(markName),
1767
+ getActiveBlockType: () => checkActiveBlockType()
1215
1768
  };
1216
1769
  };
1217
1770
  //#endregion
@@ -1219,6 +1772,7 @@ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, re
1219
1772
  const createDomAdapter = (editor) => {
1220
1773
  let target = null;
1221
1774
  let mountedEditor = null;
1775
+ let unsubscribeChange = null;
1222
1776
  return {
1223
1777
  mount: (nextTarget) => {
1224
1778
  target = nextTarget;
@@ -1228,6 +1782,11 @@ const createDomAdapter = (editor) => {
1228
1782
  host: nextTarget,
1229
1783
  initialHtml: editor.export("html").payload,
1230
1784
  requestEmbedValue: () => null,
1785
+ toolbar: editor.toolbar,
1786
+ placeholder: editor.placeholder,
1787
+ onSelectionChange: () => {
1788
+ editor._triggerSelectionChange?.();
1789
+ },
1231
1790
  onChange: (html) => {
1232
1791
  editor.import({
1233
1792
  format: "html",
@@ -1235,15 +1794,30 @@ const createDomAdapter = (editor) => {
1235
1794
  });
1236
1795
  }
1237
1796
  });
1797
+ editor._registerActiveHandler?.({
1798
+ isMarkActive: (name) => mountedEditor?.isMarkActive(name) ?? false,
1799
+ getActiveBlockType: () => mountedEditor?.getActiveBlockType() ?? "paragraph"
1800
+ });
1801
+ unsubscribeChange = editor.on("change", () => {
1802
+ if (!mountedEditor) return;
1803
+ const nextHtml = editor.export("html").payload;
1804
+ if (mountedEditor.getHtml() !== nextHtml) mountedEditor.setHtml(nextHtml);
1805
+ });
1238
1806
  },
1239
1807
  unmount: () => {
1240
1808
  if (!target) return;
1809
+ unsubscribeChange?.();
1810
+ unsubscribeChange = null;
1811
+ editor._registerActiveHandler?.(null);
1241
1812
  mountedEditor?.destroy();
1242
1813
  mountedEditor = null;
1243
1814
  editor.unmount();
1244
1815
  target = null;
1245
1816
  },
1246
1817
  destroy: () => {
1818
+ unsubscribeChange?.();
1819
+ unsubscribeChange = null;
1820
+ editor._registerActiveHandler?.(null);
1247
1821
  mountedEditor?.destroy();
1248
1822
  mountedEditor = null;
1249
1823
  editor.destroy();
@@ -1253,6 +1827,40 @@ const createDomAdapter = (editor) => {
1253
1827
  };
1254
1828
  //#endregion
1255
1829
  //#region src/adapters/react/useEditorAdapter.tsx
1830
+ const EditorContext = (0, react.createContext)(null);
1831
+ const useEditor = () => {
1832
+ return (0, react.useContext)(EditorContext);
1833
+ };
1834
+ const useEditorState = () => {
1835
+ const editor = (0, react.useContext)(EditorContext);
1836
+ if (!editor) throw new Error("useEditorState must be used inside an EditorProvider");
1837
+ const [document, setDocument] = (0, react.useState)(() => editor.getDocument());
1838
+ const [selectionKey, setSelectionKey] = (0, react.useState)(0);
1839
+ (0, react.useEffect)(() => {
1840
+ const unsubChange = editor.on("change", () => {
1841
+ setDocument(editor.getDocument());
1842
+ });
1843
+ const unsubSelection = editor.on("selectionchange", () => {
1844
+ setSelectionKey((k) => k + 1);
1845
+ });
1846
+ return () => {
1847
+ unsubChange();
1848
+ unsubSelection();
1849
+ };
1850
+ }, [editor]);
1851
+ return {
1852
+ editor,
1853
+ document,
1854
+ isMarkActive: (0, react.useCallback)((name) => editor.isMarkActive(name), [editor, selectionKey]),
1855
+ activeBlockType: (0, react.useCallback)(() => editor.getActiveBlockType(), [editor, selectionKey])()
1856
+ };
1857
+ };
1858
+ function EditorProvider({ editor, children }) {
1859
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditorContext.Provider, {
1860
+ value: editor,
1861
+ children
1862
+ });
1863
+ }
1256
1864
  const useEditorAdapter = (config) => {
1257
1865
  const editor = (0, react.useMemo)(() => createEditor(config), [config]);
1258
1866
  (0, react.useEffect)(() => () => {
@@ -1260,13 +1868,27 @@ const useEditorAdapter = (config) => {
1260
1868
  }, [editor]);
1261
1869
  return editor;
1262
1870
  };
1263
- function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue }) {
1264
- const editor = useEditorAdapter({
1871
+ function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue, toolbar, placeholder, editor: propEditor }) {
1872
+ const contextEditor = useEditor();
1873
+ const fallbackEditor = useEditorAdapter({
1265
1874
  initialContent,
1266
1875
  onChange,
1267
- onError
1876
+ onError,
1877
+ toolbar,
1878
+ placeholder
1268
1879
  });
1880
+ const editor = propEditor ?? contextEditor ?? fallbackEditor;
1269
1881
  const hostRef = (0, react.useRef)(null);
1882
+ const mountedRef = (0, react.useRef)(null);
1883
+ (0, react.useEffect)(() => {
1884
+ if (editor._registerActiveHandler) editor._registerActiveHandler({
1885
+ isMarkActive: (name) => mountedRef.current?.isMarkActive(name) ?? false,
1886
+ getActiveBlockType: () => mountedRef.current?.getActiveBlockType() ?? "paragraph"
1887
+ });
1888
+ return () => {
1889
+ editor._registerActiveHandler?.(null);
1890
+ };
1891
+ }, [editor]);
1270
1892
  (0, react.useEffect)(() => {
1271
1893
  const host = hostRef.current;
1272
1894
  if (!host) return;
@@ -1275,6 +1897,11 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
1275
1897
  initialHtml: editor.export("html").payload,
1276
1898
  disabled,
1277
1899
  requestEmbedValue: onRequestEmbedValue,
1900
+ toolbar: toolbar ?? editor.toolbar,
1901
+ placeholder: placeholder ?? editor.placeholder,
1902
+ onSelectionChange: () => {
1903
+ editor._triggerSelectionChange?.();
1904
+ },
1278
1905
  onChange: (html) => {
1279
1906
  editor.import({
1280
1907
  format: "html",
@@ -1282,13 +1909,17 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
1282
1909
  });
1283
1910
  }
1284
1911
  });
1912
+ mountedRef.current = mounted;
1285
1913
  return () => {
1286
1914
  mounted.destroy();
1915
+ mountedRef.current = null;
1287
1916
  };
1288
1917
  }, [
1289
1918
  disabled,
1290
1919
  editor,
1291
- onRequestEmbedValue
1920
+ onRequestEmbedValue,
1921
+ toolbar,
1922
+ placeholder
1292
1923
  ]);
1293
1924
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1294
1925
  "aria-label": "Rich editor",
@@ -1297,6 +1928,8 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
1297
1928
  });
1298
1929
  }
1299
1930
  //#endregion
1931
+ exports.EditorContext = EditorContext;
1932
+ exports.EditorProvider = EditorProvider;
1300
1933
  exports.RichEditorAdapter = RichEditorAdapter;
1301
1934
  exports.createDomAdapter = createDomAdapter;
1302
1935
  exports.createEditor = createEditor;
@@ -1307,4 +1940,6 @@ exports.editorNodeNames = editorNodeNames;
1307
1940
  exports.editorSchema = editorSchema;
1308
1941
  exports.exportRepresentation = exportRepresentation;
1309
1942
  exports.importRepresentation = importRepresentation;
1943
+ exports.useEditor = useEditor;
1310
1944
  exports.useEditorAdapter = useEditorAdapter;
1945
+ exports.useEditorState = useEditorState;