@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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import DOMPurify from "isomorphic-dompurify";
2
- import { Schema } from "prosemirror-model";
3
- import { useEffect, useMemo, useRef } from "react";
2
+ import { DOMParser, DOMSerializer, Schema } from "prosemirror-model";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/core/errors.ts
6
6
  const createDiagnostic = (code, severity, message, location) => ({
@@ -108,8 +108,7 @@ var EditorHistory = class {
108
108
  }
109
109
  };
110
110
  //#endregion
111
- //#region src/transforms/html.ts
112
- const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
111
+ //#region src/core/schema.ts
113
112
  const allowedStyleProperties = new Set([
114
113
  "font-family",
115
114
  "font-size",
@@ -140,6 +139,7 @@ const normalizeStyleValue = (property, value) => {
140
139
  return null;
141
140
  };
142
141
  const normalizeStyleAttribute = (styleValue) => {
142
+ if (!styleValue) return null;
143
143
  const entries = styleValue.split(";").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => {
144
144
  const separator = entry.indexOf(":");
145
145
  if (separator < 0) return null;
@@ -153,60 +153,350 @@ const normalizeStyleAttribute = (styleValue) => {
153
153
  if (entries.length === 0) return null;
154
154
  return entries.join(";");
155
155
  };
156
- const normalizeHtml = (value) => {
157
- if (typeof DOMParser === "undefined") return value.trim().length > 0 ? value : "<p></p>";
158
- const doc = new DOMParser().parseFromString(`<div>${value}</div>`, "text/html");
159
- const root = doc.body.firstElementChild;
160
- if (!root) return "<p></p>";
161
- root.querySelectorAll("b").forEach((element) => {
162
- const replacement = doc.createElement("strong");
163
- replacement.innerHTML = element.innerHTML;
164
- element.replaceWith(replacement);
165
- });
166
- root.querySelectorAll("i").forEach((element) => {
167
- const replacement = doc.createElement("em");
168
- replacement.innerHTML = element.innerHTML;
169
- element.replaceWith(replacement);
170
- });
171
- root.querySelectorAll("strike").forEach((element) => {
172
- const replacement = doc.createElement("s");
173
- replacement.innerHTML = element.innerHTML;
174
- element.replaceWith(replacement);
175
- });
176
- root.querySelectorAll("*").forEach((element) => {
177
- const tag = element.tagName.toLowerCase();
178
- Array.from(element.attributes).forEach((attribute) => {
179
- const name = attribute.name.toLowerCase();
180
- const valueAttr = attribute.value;
181
- if (name === "style") {
182
- const normalized = normalizeStyleAttribute(valueAttr);
183
- if (normalized) element.setAttribute("style", normalized);
184
- else element.removeAttribute("style");
185
- return;
156
+ const editorNodeNames = {
157
+ doc: "doc",
158
+ paragraph: "paragraph",
159
+ heading: "heading",
160
+ text: "text",
161
+ bulletList: "bullet_list",
162
+ orderedList: "ordered_list",
163
+ listItem: "list_item",
164
+ hardBreak: "hard_break",
165
+ blockquote: "blockquote",
166
+ codeBlock: "code_block",
167
+ image: "image",
168
+ video: "video"
169
+ };
170
+ const editorMarkNames = {
171
+ strong: "strong",
172
+ em: "em",
173
+ underline: "underline",
174
+ link: "link",
175
+ strike: "strike",
176
+ code: "code",
177
+ style: "style",
178
+ formula: "formula"
179
+ };
180
+ const createEditorSchema = () => new Schema({
181
+ nodes: {
182
+ doc: { content: "block+" },
183
+ paragraph: {
184
+ content: "inline*",
185
+ group: "block",
186
+ attrs: {
187
+ style: { default: null },
188
+ dir: { default: null }
189
+ },
190
+ parseDOM: [{
191
+ tag: "p",
192
+ getAttrs: (dom) => {
193
+ if (!(dom instanceof HTMLElement)) return null;
194
+ return {
195
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
196
+ dir: dom.getAttribute("dir") || null
197
+ };
198
+ }
199
+ }],
200
+ toDOM: (node) => {
201
+ const attrs = {};
202
+ if (node.attrs.style) attrs.style = node.attrs.style;
203
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
204
+ return [
205
+ "p",
206
+ attrs,
207
+ 0
208
+ ];
209
+ }
210
+ },
211
+ heading: {
212
+ attrs: {
213
+ level: { default: 1 },
214
+ style: { default: null },
215
+ dir: { default: null }
216
+ },
217
+ content: "inline*",
218
+ group: "block",
219
+ defining: true,
220
+ parseDOM: [
221
+ {
222
+ tag: "h1",
223
+ getAttrs: (dom) => {
224
+ if (!(dom instanceof HTMLElement)) return { level: 1 };
225
+ return {
226
+ level: 1,
227
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
228
+ dir: dom.getAttribute("dir") || null
229
+ };
230
+ }
231
+ },
232
+ {
233
+ tag: "h2",
234
+ getAttrs: (dom) => {
235
+ if (!(dom instanceof HTMLElement)) return { level: 2 };
236
+ return {
237
+ level: 2,
238
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
239
+ dir: dom.getAttribute("dir") || null
240
+ };
241
+ }
242
+ },
243
+ {
244
+ tag: "h3",
245
+ getAttrs: (dom) => {
246
+ if (!(dom instanceof HTMLElement)) return { level: 3 };
247
+ return {
248
+ level: 3,
249
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
250
+ dir: dom.getAttribute("dir") || null
251
+ };
252
+ }
253
+ }
254
+ ],
255
+ toDOM: (node) => {
256
+ const attrs = {};
257
+ if (node.attrs.style) attrs.style = node.attrs.style;
258
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
259
+ return [
260
+ `h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`,
261
+ attrs,
262
+ 0
263
+ ];
186
264
  }
187
- if (name === "href" || name === "src") {
188
- if (!/^(https?:|mailto:|tel:)/i.test(valueAttr.trim())) element.removeAttribute(attribute.name);
189
- return;
265
+ },
266
+ blockquote: {
267
+ content: "block+",
268
+ group: "block",
269
+ attrs: {
270
+ style: { default: null },
271
+ dir: { default: null }
272
+ },
273
+ parseDOM: [{
274
+ tag: "blockquote",
275
+ getAttrs: (dom) => {
276
+ if (!(dom instanceof HTMLElement)) return null;
277
+ return {
278
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
279
+ dir: dom.getAttribute("dir") || null
280
+ };
281
+ }
282
+ }],
283
+ toDOM: (node) => {
284
+ const attrs = {};
285
+ if (node.attrs.style) attrs.style = node.attrs.style;
286
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
287
+ return [
288
+ "blockquote",
289
+ attrs,
290
+ 0
291
+ ];
190
292
  }
191
- if (name === "target" || name === "rel" || name === "alt" || name === "data-formula" || name === "controls" || name === "dir") return;
192
- element.removeAttribute(attribute.name);
193
- });
194
- if (tag === "a") {
195
- element.setAttribute("rel", "noopener noreferrer");
196
- element.setAttribute("target", "_blank");
293
+ },
294
+ code_block: {
295
+ content: "inline*",
296
+ group: "block",
297
+ code: true,
298
+ defining: true,
299
+ attrs: {
300
+ style: { default: null },
301
+ dir: { default: null }
302
+ },
303
+ parseDOM: [{
304
+ tag: "pre",
305
+ preserveWhitespace: "full",
306
+ getAttrs: (dom) => {
307
+ if (!(dom instanceof HTMLElement)) return null;
308
+ return {
309
+ style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
310
+ dir: dom.getAttribute("dir") || null
311
+ };
312
+ }
313
+ }],
314
+ toDOM: (node) => {
315
+ const attrs = {};
316
+ if (node.attrs.style) attrs.style = node.attrs.style;
317
+ if (node.attrs.dir) attrs.dir = node.attrs.dir;
318
+ return [
319
+ "pre",
320
+ attrs,
321
+ ["code", 0]
322
+ ];
323
+ }
324
+ },
325
+ bullet_list: {
326
+ content: "list_item+",
327
+ group: "block",
328
+ parseDOM: [{ tag: "ul" }],
329
+ toDOM: () => ["ul", 0]
330
+ },
331
+ ordered_list: {
332
+ attrs: { order: { default: 1 } },
333
+ content: "list_item+",
334
+ group: "block",
335
+ parseDOM: [{
336
+ tag: "ol",
337
+ getAttrs: (dom) => {
338
+ if (!(dom instanceof HTMLOListElement)) return { order: 1 };
339
+ return { order: dom.start || 1 };
340
+ }
341
+ }],
342
+ toDOM: (node) => [
343
+ "ol",
344
+ { start: node.attrs.order || 1 },
345
+ 0
346
+ ]
347
+ },
348
+ list_item: {
349
+ content: "paragraph block*",
350
+ parseDOM: [{ tag: "li" }],
351
+ toDOM: () => ["li", 0]
352
+ },
353
+ image: {
354
+ inline: true,
355
+ attrs: {
356
+ src: {},
357
+ alt: { default: null }
358
+ },
359
+ group: "inline",
360
+ draggable: true,
361
+ parseDOM: [{
362
+ tag: "img[src]",
363
+ getAttrs: (dom) => {
364
+ if (!(dom instanceof HTMLElement)) return null;
365
+ return {
366
+ src: dom.getAttribute("src"),
367
+ alt: dom.getAttribute("alt")
368
+ };
369
+ }
370
+ }],
371
+ toDOM: (node) => ["img", {
372
+ src: node.attrs.src,
373
+ alt: node.attrs.alt
374
+ }]
375
+ },
376
+ video: {
377
+ inline: true,
378
+ attrs: {
379
+ src: {},
380
+ controls: { default: "true" }
381
+ },
382
+ group: "inline",
383
+ parseDOM: [{
384
+ tag: "video[src]",
385
+ getAttrs: (dom) => {
386
+ if (!(dom instanceof HTMLElement)) return null;
387
+ return {
388
+ src: dom.getAttribute("src"),
389
+ controls: dom.getAttribute("controls") || "true"
390
+ };
391
+ }
392
+ }],
393
+ toDOM: (node) => ["video", {
394
+ src: node.attrs.src,
395
+ controls: node.attrs.controls
396
+ }]
397
+ },
398
+ text: { group: "inline" },
399
+ hard_break: {
400
+ inline: true,
401
+ group: "inline",
402
+ selectable: false,
403
+ parseDOM: [{ tag: "br" }],
404
+ toDOM: () => ["br"]
197
405
  }
198
- if (tag === "video") element.setAttribute("controls", "true");
199
- });
200
- Array.from(root.childNodes).forEach((node) => {
201
- if (node.nodeType === Node.TEXT_NODE && (node.textContent?.trim().length ?? 0) > 0) {
202
- const paragraph = doc.createElement("p");
203
- paragraph.textContent = node.textContent ?? "";
204
- root.replaceChild(paragraph, node);
406
+ },
407
+ marks: {
408
+ strong: {
409
+ parseDOM: [{ tag: "strong" }, {
410
+ tag: "b",
411
+ getAttrs: () => null
412
+ }],
413
+ toDOM: () => ["strong", 0]
414
+ },
415
+ em: {
416
+ parseDOM: [{ tag: "em" }, {
417
+ tag: "i",
418
+ getAttrs: () => null
419
+ }],
420
+ toDOM: () => ["em", 0]
421
+ },
422
+ underline: {
423
+ parseDOM: [{ tag: "u" }],
424
+ toDOM: () => ["u", 0]
425
+ },
426
+ strike: {
427
+ parseDOM: [
428
+ { tag: "s" },
429
+ { tag: "del" },
430
+ { tag: "strike" }
431
+ ],
432
+ toDOM: () => ["s", 0]
433
+ },
434
+ code: {
435
+ parseDOM: [{ tag: "code" }],
436
+ toDOM: () => ["code", 0]
437
+ },
438
+ style: {
439
+ attrs: { style: { default: null } },
440
+ parseDOM: [{
441
+ tag: "span[style]",
442
+ getAttrs: (dom) => {
443
+ if (!(dom instanceof HTMLElement)) return null;
444
+ const normalized = normalizeStyleAttribute(dom.getAttribute("style") || "");
445
+ return normalized ? { style: normalized } : false;
446
+ }
447
+ }],
448
+ toDOM: (node) => [
449
+ "span",
450
+ { style: node.attrs.style },
451
+ 0
452
+ ]
453
+ },
454
+ link: {
455
+ attrs: {
456
+ href: { default: null },
457
+ title: { default: null },
458
+ style: { default: null }
459
+ },
460
+ inclusive: false,
461
+ parseDOM: [{
462
+ tag: "a",
463
+ getAttrs: (dom) => {
464
+ if (!(dom instanceof HTMLElement)) return false;
465
+ const href = dom.getAttribute("href");
466
+ const style = dom.getAttribute("style");
467
+ if (!href && !style) return false;
468
+ return {
469
+ href: href || null,
470
+ title: dom.getAttribute("title") || null,
471
+ style: normalizeStyleAttribute(style || "")
472
+ };
473
+ }
474
+ }],
475
+ toDOM: (node) => {
476
+ const attrs = {};
477
+ if (node.attrs.href) attrs.href = node.attrs.href;
478
+ if (node.attrs.title) attrs.title = node.attrs.title;
479
+ if (node.attrs.style) attrs.style = node.attrs.style;
480
+ return [
481
+ "a",
482
+ attrs,
483
+ 0
484
+ ];
485
+ }
486
+ },
487
+ formula: {
488
+ parseDOM: [{ tag: "span[data-formula]" }],
489
+ toDOM: () => [
490
+ "span",
491
+ { "data-formula": "true" },
492
+ 0
493
+ ]
205
494
  }
206
- });
207
- const normalized = root.innerHTML.trim();
208
- return normalized.length > 0 ? normalized : "<p></p>";
209
- };
495
+ }
496
+ });
497
+ const editorSchema = createEditorSchema();
498
+ //#endregion
499
+ //#region src/transforms/html.ts
210
500
  const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
211
501
  ALLOWED_TAGS: [
212
502
  "p",
@@ -223,6 +513,7 @@ const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
223
513
  "blockquote",
224
514
  "h1",
225
515
  "h2",
516
+ "h3",
226
517
  "ul",
227
518
  "ol",
228
519
  "li",
@@ -258,19 +549,29 @@ const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
258
549
  "select"
259
550
  ]
260
551
  });
552
+ const normalizeHtml = (value) => {
553
+ if (typeof globalThis.DOMParser === "undefined") return value.trim().length > 0 ? value : "<p></p>";
554
+ const domDoc = new globalThis.DOMParser().parseFromString(`<body>${value}</body>`, "text/html");
555
+ const pmNode = DOMParser.fromSchema(editorSchema).parse(domDoc.body);
556
+ const fragment = DOMSerializer.fromSchema(editorSchema).serializeFragment(pmNode.content);
557
+ const container = document.createElement("div");
558
+ container.appendChild(fragment);
559
+ const normalized = container.innerHTML.trim();
560
+ return normalized.length > 0 ? normalized : "<p></p>";
561
+ };
261
562
  const importHtml = (payload) => {
262
563
  const diagnostics = [];
263
564
  const normalized = normalizeHtml(sanitizeHtml(payload));
264
565
  if (normalized !== payload) diagnostics.push(createDiagnostic("SANITIZED_CONTENT", "warning", "Input HTML was sanitized to remove unsupported or unsafe markup."));
265
566
  if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_HTML", "info", "The provided HTML content is empty after normalization."));
266
567
  return {
267
- richText: stripHtml(normalized),
568
+ richText: normalized,
268
569
  sanitizedHtml: normalized,
269
570
  diagnostics
270
571
  };
271
572
  };
272
573
  const exportHtml = (richText) => {
273
- return normalizeHtml(`<p>${richText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")}</p>`);
574
+ return normalizeHtml(richText);
274
575
  };
275
576
  //#endregion
276
577
  //#region src/transforms/diagnostics.ts
@@ -301,26 +602,224 @@ const applyImportFallback = (input, base) => {
301
602
  //#endregion
302
603
  //#region src/transforms/markdown.ts
303
604
  const normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
605
+ const escapeHtml = (text) => {
606
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
607
+ };
608
+ const parseInline = (text) => {
609
+ let html = escapeHtml(text);
610
+ html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
611
+ html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
612
+ html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
613
+ html = html.replace(/_(.*?)_/g, "<em>$1</em>");
614
+ html = html.replace(/~~(.*?)~~/g, "<s>$1</s>");
615
+ html = html.replace(/`(.*?)`/g, "<code>$1</code>");
616
+ html = html.replace(/\[(.*?)\]\((.*?)\)/g, "<a href=\"$2\">$1</a>");
617
+ return html;
618
+ };
619
+ const markdownToHtml = (markdown) => {
620
+ const lines = normalizeMarkdown(markdown).split("\n");
621
+ const blocks = [];
622
+ let currentBlockType = null;
623
+ let blockLines = [];
624
+ const closeCurrentBlock = () => {
625
+ if (!currentBlockType) return;
626
+ if (currentBlockType === "paragraph") {
627
+ const content = parseInline(blockLines.join("\n"));
628
+ blocks.push(`<p>${content}</p>`);
629
+ } else if (currentBlockType === "blockquote") {
630
+ const content = markdownToHtml(blockLines.join("\n"));
631
+ blocks.push(`<blockquote>${content}</blockquote>`);
632
+ } else if (currentBlockType === "code_block") {
633
+ const content = escapeHtml(blockLines.join("\n"));
634
+ blocks.push(`<pre><code>${content}</code></pre>`);
635
+ } else if (currentBlockType === "ul" || currentBlockType === "ol") {
636
+ const tag = currentBlockType;
637
+ const itemBlocks = [];
638
+ let currentItemLines = [];
639
+ const closeItem = () => {
640
+ if (currentItemLines.length > 0) {
641
+ const innerHtml = markdownToHtml(currentItemLines.join("\n"));
642
+ itemBlocks.push(`<li>${innerHtml}</li>`);
643
+ currentItemLines = [];
644
+ }
645
+ };
646
+ for (const line of blockLines) {
647
+ const ulMatch = /^[*-+]\s+(.*)$/.exec(line.trim());
648
+ const olMatch = /^\d+\.\s+(.*)$/.exec(line.trim());
649
+ if (ulMatch) {
650
+ closeItem();
651
+ currentItemLines.push(ulMatch[1]);
652
+ } else if (olMatch) {
653
+ closeItem();
654
+ currentItemLines.push(olMatch[1]);
655
+ } else currentItemLines.push(line.trimStart());
656
+ }
657
+ closeItem();
658
+ blocks.push(`<${tag}>${itemBlocks.join("")}</${tag}>`);
659
+ }
660
+ blockLines = [];
661
+ currentBlockType = null;
662
+ };
663
+ let i = 0;
664
+ while (i < lines.length) {
665
+ const line = lines[i];
666
+ if (line.startsWith("```")) {
667
+ if (currentBlockType === "code_block") closeCurrentBlock();
668
+ else {
669
+ closeCurrentBlock();
670
+ currentBlockType = "code_block";
671
+ }
672
+ i++;
673
+ continue;
674
+ }
675
+ if (currentBlockType === "code_block") {
676
+ blockLines.push(line);
677
+ i++;
678
+ continue;
679
+ }
680
+ const trimmed = line.trim();
681
+ if (trimmed === "") {
682
+ closeCurrentBlock();
683
+ i++;
684
+ continue;
685
+ }
686
+ const headerMatch = /^(#{1,6})\s+(.*)$/.exec(trimmed);
687
+ if (headerMatch) {
688
+ closeCurrentBlock();
689
+ const level = headerMatch[1].length;
690
+ const content = parseInline(headerMatch[2]);
691
+ blocks.push(`<h${level}>${content}</h${level}>`);
692
+ i++;
693
+ continue;
694
+ }
695
+ if (trimmed.startsWith(">")) {
696
+ if (currentBlockType !== "blockquote") {
697
+ closeCurrentBlock();
698
+ currentBlockType = "blockquote";
699
+ }
700
+ const rest = line.startsWith("> ") ? line.slice(2) : line.slice(1);
701
+ blockLines.push(rest);
702
+ i++;
703
+ continue;
704
+ }
705
+ if (/^[*-+]\s+(.*)$/.exec(trimmed)) {
706
+ if (currentBlockType !== "ul") {
707
+ closeCurrentBlock();
708
+ currentBlockType = "ul";
709
+ }
710
+ blockLines.push(line);
711
+ i++;
712
+ continue;
713
+ }
714
+ if (/^\d+\.\s+(.*)$/.exec(trimmed)) {
715
+ if (currentBlockType !== "ol") {
716
+ closeCurrentBlock();
717
+ currentBlockType = "ol";
718
+ }
719
+ blockLines.push(line);
720
+ i++;
721
+ continue;
722
+ }
723
+ if (!currentBlockType) currentBlockType = "paragraph";
724
+ blockLines.push(line);
725
+ i++;
726
+ }
727
+ closeCurrentBlock();
728
+ return blocks.join("");
729
+ };
730
+ const serializeChild = (child, index, parent) => {
731
+ if (child.isText) {
732
+ let text = child.text || "";
733
+ child.marks.forEach((mark) => {
734
+ if (mark.type.name === "strong") text = `**${text}**`;
735
+ else if (mark.type.name === "em") text = `*${text}*`;
736
+ else if (mark.type.name === "strike") text = `~~${text}~~`;
737
+ else if (mark.type.name === "code") text = `\`${text}\``;
738
+ else if (mark.type.name === "link") {
739
+ const href = mark.attrs.href || "";
740
+ text = `[${text}](${href})`;
741
+ }
742
+ });
743
+ return text;
744
+ }
745
+ if (child.type.name === "hard_break") return "\n";
746
+ if (child.type.name === "image") {
747
+ const src = child.attrs.src || "";
748
+ return `![${child.attrs.alt || ""}](${src})`;
749
+ }
750
+ if (child.type.name === "video") return `[Video: ${child.attrs.src || ""}]`;
751
+ let inner = "";
752
+ child.forEach((c, idx) => {
753
+ inner += serializeChild(c, idx, child);
754
+ });
755
+ if (child.type.name === "paragraph") return `${inner}\n\n`;
756
+ if (child.type.name === "heading") return `${"#".repeat(child.attrs.level || 1)} ${inner}\n\n`;
757
+ if (child.type.name === "blockquote") return `${inner.trim().split("\n").map((line) => `> ${line}`).join("\n")}\n\n`;
758
+ if (child.type.name === "code_block") return `\`\`\`\n${inner.trim()}\n\`\`\`\n\n`;
759
+ if (child.type.name === "bullet_list") {
760
+ let listResult = "";
761
+ child.forEach((item) => {
762
+ const indented = serializeNode(item).trim().split("\n").map((line, idx) => {
763
+ if (idx === 0) return `* ${line}`;
764
+ return ` ${line}`;
765
+ }).join("\n");
766
+ listResult += `${indented}\n`;
767
+ });
768
+ return `${listResult}\n`;
769
+ }
770
+ if (child.type.name === "ordered_list") {
771
+ let listResult = "";
772
+ let count = child.attrs.order || 1;
773
+ child.forEach((item) => {
774
+ const indented = serializeNode(item).trim().split("\n").map((line, idx) => {
775
+ if (idx === 0) return `${count}. ${line}`;
776
+ return ` ${line}`;
777
+ }).join("\n");
778
+ listResult += `${indented}\n`;
779
+ count++;
780
+ });
781
+ return `${listResult}\n`;
782
+ }
783
+ if (child.type.name === "list_item") {
784
+ let itemInner = "";
785
+ child.forEach((c, idx) => {
786
+ itemInner += serializeChild(c, idx, child);
787
+ });
788
+ return itemInner;
789
+ }
790
+ return inner;
791
+ };
792
+ const serializeNode = (node) => {
793
+ let result = "";
794
+ node.forEach((child, index) => {
795
+ result += serializeChild(child, index, node);
796
+ });
797
+ return result.trim();
798
+ };
304
799
  const importMarkdown = (payload) => {
305
800
  const diagnostics = [];
306
801
  const normalized = normalizeMarkdown(payload);
307
802
  if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_MARKDOWN", "info", "The provided markdown content is empty."));
308
803
  return {
309
- richText: normalized,
804
+ richText: markdownToHtml(normalized),
310
805
  diagnostics
311
806
  };
312
807
  };
313
- const exportMarkdown = (richText) => normalizeMarkdown(richText);
808
+ const exportMarkdown = (richText) => {
809
+ if (typeof globalThis.DOMParser === "undefined") return richText;
810
+ const domDoc = new globalThis.DOMParser().parseFromString(`<body>${richText}</body>`, "text/html");
811
+ return serializeNode(DOMParser.fromSchema(editorSchema).parse(domDoc.body));
812
+ };
314
813
  //#endregion
315
814
  //#region src/transforms/representationSwitch.ts
316
815
  const importRepresentation = (input, base) => {
317
816
  if (input.format === "rich") {
318
- const normalizedHtml = normalizeHtml(exportHtml(input.payload));
817
+ const normalizedHtml = normalizeHtml(input.payload);
319
818
  return {
320
819
  document: {
321
820
  ...base,
322
821
  content: {
323
- richText: input.payload,
822
+ richText: normalizedHtml,
324
823
  html: normalizedHtml
325
824
  }
326
825
  },
@@ -332,7 +831,10 @@ const importRepresentation = (input, base) => {
332
831
  return {
333
832
  document: {
334
833
  ...base,
335
- content: { richText: result.richText },
834
+ content: {
835
+ richText: result.richText,
836
+ html: result.richText
837
+ },
336
838
  metadata: {
337
839
  ...base.metadata,
338
840
  importedFrom: "markdown"
@@ -395,6 +897,11 @@ const createEditor = (config = {}) => {
395
897
  const history = new EditorHistory();
396
898
  let isDestroyed = false;
397
899
  let currentDocument = createDefaultDocument();
900
+ const listeners = {
901
+ change: /* @__PURE__ */ new Set(),
902
+ selectionchange: /* @__PURE__ */ new Set()
903
+ };
904
+ let activeHandler = null;
398
905
  if (config.initialContent) {
399
906
  const initialized = fromInput(config.initialContent, currentDocument);
400
907
  currentDocument = initialized.document;
@@ -407,6 +914,7 @@ const createEditor = (config = {}) => {
407
914
  };
408
915
  const emitChange = () => {
409
916
  config.onChange?.(currentDocument);
917
+ listeners.change.forEach((cb) => cb());
410
918
  };
411
919
  return {
412
920
  getDocument: () => currentDocument,
@@ -416,6 +924,21 @@ const createEditor = (config = {}) => {
416
924
  document: currentDocument,
417
925
  diagnostics: []
418
926
  };
927
+ if (!(config.commandCapabilities ? new Set(config.commandCapabilities) : new Set([
928
+ "bold",
929
+ "italic",
930
+ "underline",
931
+ "heading",
932
+ "list",
933
+ "align",
934
+ "link",
935
+ "undo",
936
+ "redo"
937
+ ])).has(commandName)) return {
938
+ ok: false,
939
+ document: currentDocument,
940
+ diagnostics: [createUnknownCommandDiagnostic(commandName)]
941
+ };
419
942
  const result = executeCommand(registry, commandName, { document: currentDocument }, params);
420
943
  if (commandName === "undo") {
421
944
  const next = history.undo(currentDocument);
@@ -481,141 +1004,27 @@ const createEditor = (config = {}) => {
481
1004
  unmount: () => {},
482
1005
  destroy: () => {
483
1006
  isDestroyed = true;
484
- }
485
- };
486
- };
487
- //#endregion
488
- //#region src/core/schema.ts
489
- const editorNodeNames = {
490
- doc: "doc",
491
- paragraph: "paragraph",
492
- heading: "heading",
493
- text: "text",
494
- bulletList: "bullet_list",
495
- orderedList: "ordered_list",
496
- listItem: "list_item",
497
- hardBreak: "hard_break"
498
- };
499
- const editorMarkNames = {
500
- strong: "strong",
501
- em: "em",
502
- underline: "underline",
503
- link: "link"
504
- };
505
- const createEditorSchema = () => new Schema({
506
- nodes: {
507
- doc: { content: "block+" },
508
- paragraph: {
509
- content: "inline*",
510
- group: "block",
511
- parseDOM: [{ tag: "p" }],
512
- toDOM: () => ["p", 0]
513
- },
514
- heading: {
515
- attrs: { level: { default: 1 } },
516
- content: "inline*",
517
- group: "block",
518
- defining: true,
519
- parseDOM: [
520
- {
521
- tag: "h1",
522
- attrs: { level: 1 }
523
- },
524
- {
525
- tag: "h2",
526
- attrs: { level: 2 }
527
- },
528
- {
529
- tag: "h3",
530
- attrs: { level: 3 }
531
- }
532
- ],
533
- toDOM: (node) => [`h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`, 0]
534
- },
535
- bullet_list: {
536
- content: "list_item+",
537
- group: "block",
538
- parseDOM: [{ tag: "ul" }],
539
- toDOM: () => ["ul", 0]
540
- },
541
- ordered_list: {
542
- attrs: { order: { default: 1 } },
543
- content: "list_item+",
544
- group: "block",
545
- parseDOM: [{
546
- tag: "ol",
547
- getAttrs: (dom) => {
548
- if (!(dom instanceof HTMLOListElement)) return { order: 1 };
549
- return { order: dom.start || 1 };
550
- }
551
- }],
552
- toDOM: (node) => [
553
- "ol",
554
- { start: node.attrs.order || 1 },
555
- 0
556
- ]
557
- },
558
- list_item: {
559
- content: "paragraph block*",
560
- parseDOM: [{ tag: "li" }],
561
- toDOM: () => ["li", 0]
1007
+ listeners.change.clear();
1008
+ listeners.selectionchange.clear();
562
1009
  },
563
- text: { group: "inline" },
564
- hard_break: {
565
- inline: true,
566
- group: "inline",
567
- selectable: false,
568
- parseDOM: [{ tag: "br" }],
569
- toDOM: () => ["br"]
570
- }
571
- },
572
- marks: {
573
- strong: {
574
- parseDOM: [{ tag: "strong" }, {
575
- tag: "b",
576
- getAttrs: () => null
577
- }],
578
- toDOM: () => ["strong", 0]
1010
+ isMarkActive: (name) => activeHandler?.isMarkActive(name) ?? false,
1011
+ getActiveBlockType: () => activeHandler?.getActiveBlockType() ?? "paragraph",
1012
+ on: (event, callback) => {
1013
+ listeners[event].add(callback);
1014
+ return () => {
1015
+ listeners[event].delete(callback);
1016
+ };
579
1017
  },
580
- em: {
581
- parseDOM: [{ tag: "em" }, {
582
- tag: "i",
583
- getAttrs: () => null
584
- }],
585
- toDOM: () => ["em", 0]
1018
+ toolbar: config.toolbar,
1019
+ placeholder: config.placeholder,
1020
+ _registerActiveHandler: (handler) => {
1021
+ activeHandler = handler;
586
1022
  },
587
- underline: {
588
- parseDOM: [{ tag: "u" }],
589
- toDOM: () => ["u", 0]
590
- },
591
- link: {
592
- attrs: {
593
- href: {},
594
- title: { default: null }
595
- },
596
- inclusive: false,
597
- parseDOM: [{
598
- tag: "a[href]",
599
- getAttrs: (dom) => {
600
- if (!(dom instanceof HTMLAnchorElement)) return false;
601
- return {
602
- href: dom.getAttribute("href"),
603
- title: dom.getAttribute("title")
604
- };
605
- }
606
- }],
607
- toDOM: (node) => [
608
- "a",
609
- {
610
- href: node.attrs.href,
611
- title: node.attrs.title
612
- },
613
- 0
614
- ]
1023
+ _triggerSelectionChange: () => {
1024
+ listeners.selectionchange.forEach((cb) => cb());
615
1025
  }
616
- }
617
- });
618
- const editorSchema = createEditorSchema();
1026
+ };
1027
+ };
619
1028
  //#endregion
620
1029
  //#region src/core/toolbarModel.ts
621
1030
  const defaultToolbarActions = [
@@ -709,40 +1118,40 @@ const createToolbarIcon = (command, fallback) => {
709
1118
  };
710
1119
  const selectOptionLabels = {
711
1120
  font: {
712
- default: "A",
713
- serif: "As",
714
- monospace: "Am"
1121
+ default: "Font",
1122
+ serif: "Serif",
1123
+ monospace: "Monospace"
715
1124
  },
716
1125
  size: {
717
- small: "S",
718
- normal: "N",
719
- large: "L",
720
- huge: "XL"
1126
+ normal: "Normal (16px)",
1127
+ small: "Small (12px)",
1128
+ large: "Large (20px)",
1129
+ huge: "Huge (28px)"
721
1130
  },
722
1131
  header: {
723
- normal: "P",
724
- h1: "H1",
725
- h2: "H2"
1132
+ normal: "Normal Text",
1133
+ h1: "Heading 1",
1134
+ h2: "Heading 2"
726
1135
  },
727
1136
  script: {
728
- normal: "x",
729
- sub: "x_",
730
- super: "x^"
1137
+ normal: "Script",
1138
+ sub: "Subscript",
1139
+ super: "Superscript"
731
1140
  },
732
1141
  align: {
733
- left: "L",
734
- center: "C",
735
- right: "R",
736
- justify: "J"
1142
+ left: "Align Left",
1143
+ center: "Align Center",
1144
+ right: "Align Right",
1145
+ justify: "Justify"
737
1146
  },
738
1147
  list: {
739
- none: "-",
740
- ordered: "1.",
741
- bullet: "o"
1148
+ none: "No List",
1149
+ ordered: "Numbered List",
1150
+ bullet: "Bulleted List"
742
1151
  },
743
1152
  direction: {
744
- ltr: "->",
745
- rtl: "<-"
1153
+ ltr: "Left-to-Right",
1154
+ rtl: "Right-to-Left"
746
1155
  }
747
1156
  };
748
1157
  const safeUrl = (value) => {
@@ -1082,11 +1491,13 @@ const createSelect = (format, options, editable, onMutate) => {
1082
1491
  });
1083
1492
  return select;
1084
1493
  };
1085
- const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1494
+ const buildToolbar = (editable, requestEmbedValue, onMutate, toolbarOption) => {
1495
+ if (toolbarOption === false) return null;
1086
1496
  const toolbar = document.createElement("div");
1087
1497
  toolbar.className = "az-rich-editor-toolbar";
1088
1498
  toolbar.setAttribute("role", "toolbar");
1089
1499
  toolbar.setAttribute("aria-label", "Rich editor toolbar");
1500
+ const allowed = Array.isArray(toolbarOption) ? new Set(toolbarOption) : null;
1090
1501
  [
1091
1502
  {
1092
1503
  format: "font",
@@ -1099,8 +1510,8 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1099
1510
  {
1100
1511
  format: "size",
1101
1512
  options: [
1102
- "small",
1103
1513
  "normal",
1514
+ "small",
1104
1515
  "large",
1105
1516
  "huge"
1106
1517
  ]
@@ -1142,7 +1553,9 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1142
1553
  format: "direction",
1143
1554
  options: ["ltr", "rtl"]
1144
1555
  }
1145
- ].forEach((config) => toolbar.append(createSelect(config.format, config.options, editable, onMutate)));
1556
+ ].forEach((config) => {
1557
+ if (!allowed || allowed.has(config.format)) toolbar.append(createSelect(config.format, config.options, editable, onMutate));
1558
+ });
1146
1559
  [
1147
1560
  "bold",
1148
1561
  "italic",
@@ -1158,10 +1571,12 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1158
1571
  "video",
1159
1572
  "formula",
1160
1573
  "clean"
1161
- ].forEach((command) => toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate)));
1162
- return toolbar;
1574
+ ].forEach((command) => {
1575
+ if (!allowed || allowed.has(command)) toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate));
1576
+ });
1577
+ return toolbar.childNodes.length > 0 ? toolbar : null;
1163
1578
  };
1164
- const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue }) => {
1579
+ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue, toolbar: toolbarOption, placeholder, onSelectionChange }) => {
1165
1580
  host.innerHTML = "";
1166
1581
  const wrapper = document.createElement("div");
1167
1582
  wrapper.className = "az-rich-editor";
@@ -1171,23 +1586,161 @@ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, re
1171
1586
  editable.setAttribute("role", "textbox");
1172
1587
  editable.setAttribute("aria-multiline", "true");
1173
1588
  editable.setAttribute("contenteditable", disabled ? "false" : "true");
1174
- editable.dataset.placeholder = "Compose an epic...";
1589
+ editable.dataset.placeholder = placeholder ?? "Compose an epic...";
1175
1590
  editable.innerHTML = initialHtml;
1176
1591
  const emitChange = () => onChange(editable.innerHTML);
1177
- const toolbar = buildToolbar(editable, requestEmbedValue, emitChange);
1592
+ const toolbar = buildToolbar(editable, requestEmbedValue, emitChange, toolbarOption);
1178
1593
  const inputHandler = () => emitChange();
1179
1594
  editable.addEventListener("input", inputHandler);
1180
- wrapper.append(toolbar, editable);
1595
+ const keydownHandler = (event) => {
1596
+ const range = getSelectionRange(editable);
1597
+ if (!range) return;
1598
+ if (event.metaKey || event.ctrlKey) {
1599
+ const key = event.key.toLowerCase();
1600
+ if (key === "b") {
1601
+ event.preventDefault();
1602
+ applyButtonCommand("bold", editable, requestEmbedValue);
1603
+ emitChange();
1604
+ } else if (key === "i") {
1605
+ event.preventDefault();
1606
+ applyButtonCommand("italic", editable, requestEmbedValue);
1607
+ emitChange();
1608
+ } else if (key === "u") {
1609
+ event.preventDefault();
1610
+ applyButtonCommand("underline", editable, requestEmbedValue);
1611
+ emitChange();
1612
+ }
1613
+ }
1614
+ if (event.key === "Tab") {
1615
+ if (closestByTag(getBlockElement(range, editable), "li", editable)) {
1616
+ event.preventDefault();
1617
+ applyIndent(editable, range, event.shiftKey ? "-" : "+");
1618
+ emitChange();
1619
+ }
1620
+ }
1621
+ };
1622
+ editable.addEventListener("keydown", keydownHandler);
1623
+ const checkMarkActive = (markName) => {
1624
+ const selection = window.getSelection();
1625
+ if (!selection || selection.rangeCount === 0) return false;
1626
+ const range = selection.getRangeAt(0);
1627
+ if (!editable.contains(range.commonAncestorContainer)) return false;
1628
+ const tags = {
1629
+ strong: ["strong", "b"],
1630
+ bold: ["strong", "b"],
1631
+ em: ["em", "i"],
1632
+ italic: ["em", "i"],
1633
+ underline: ["u"],
1634
+ strike: [
1635
+ "s",
1636
+ "del",
1637
+ "strike"
1638
+ ],
1639
+ code: ["code"],
1640
+ link: ["a"]
1641
+ }[markName];
1642
+ if (!tags) return false;
1643
+ let node = range.startContainer;
1644
+ while (node && node !== editable) {
1645
+ if (node instanceof HTMLElement) {
1646
+ const tagName = node.tagName.toLowerCase();
1647
+ if (tags.includes(tagName)) return true;
1648
+ if (markName === "code" && node.style.fontFamily === "monospace") return true;
1649
+ }
1650
+ node = node.parentNode;
1651
+ }
1652
+ return false;
1653
+ };
1654
+ const checkActiveBlockType = () => {
1655
+ const selection = window.getSelection();
1656
+ if (!selection || selection.rangeCount === 0) return "paragraph";
1657
+ const range = selection.getRangeAt(0);
1658
+ if (!editable.contains(range.commonAncestorContainer)) return "paragraph";
1659
+ let node = range.startContainer;
1660
+ while (node && node !== editable) {
1661
+ if (node instanceof HTMLElement) {
1662
+ const tagName = node.tagName.toLowerCase();
1663
+ if (tagName === "h1" || tagName === "h2" || tagName === "h3") return "heading";
1664
+ if (tagName === "blockquote") return "blockquote";
1665
+ if (tagName === "pre") return "code_block";
1666
+ if (tagName === "ol") return "ordered_list";
1667
+ if (tagName === "ul") return "bullet_list";
1668
+ }
1669
+ node = node.parentNode;
1670
+ }
1671
+ return "paragraph";
1672
+ };
1673
+ const updateToolbarStates = () => {
1674
+ if (!toolbar) return;
1675
+ toolbar.querySelectorAll(".az-rich-editor-toolbar-button").forEach((btn) => {
1676
+ const command = btn.getAttribute("aria-label");
1677
+ if (!command) return;
1678
+ let isActive = false;
1679
+ if (command === "Bold") isActive = checkMarkActive("strong");
1680
+ else if (command === "Italic") isActive = checkMarkActive("em");
1681
+ else if (command === "Underline") isActive = checkMarkActive("underline");
1682
+ else if (command === "Strike") isActive = checkMarkActive("strike");
1683
+ else if (command === "Inline code") isActive = checkMarkActive("code");
1684
+ else if (command === "Blockquote") isActive = checkActiveBlockType() === "blockquote";
1685
+ else if (command === "Code block") isActive = checkActiveBlockType() === "code_block";
1686
+ if (isActive) {
1687
+ btn.setAttribute("data-active", "true");
1688
+ btn.classList.add("active");
1689
+ } else {
1690
+ btn.removeAttribute("data-active");
1691
+ btn.classList.remove("active");
1692
+ }
1693
+ });
1694
+ toolbar.querySelectorAll(".az-rich-editor-toolbar-select").forEach((select) => {
1695
+ const label = select.getAttribute("aria-label");
1696
+ if (label === "header") if (checkActiveBlockType() === "heading") {
1697
+ const selection = window.getSelection();
1698
+ if (selection && selection.rangeCount > 0) {
1699
+ const level = getBlockElement(selection.getRangeAt(0), editable).tagName.toLowerCase();
1700
+ if (level === "h1" || level === "h2" || level === "h3") select.value = level;
1701
+ else select.value = "normal";
1702
+ }
1703
+ } else select.value = "normal";
1704
+ else if (label === "align") {
1705
+ const selection = window.getSelection();
1706
+ if (selection && selection.rangeCount > 0) select.value = getBlockElement(selection.getRangeAt(0), editable).style.textAlign || "left";
1707
+ } else if (label === "direction") {
1708
+ const selection = window.getSelection();
1709
+ if (selection && selection.rangeCount > 0) select.value = getBlockElement(selection.getRangeAt(0), editable).getAttribute("dir") || "ltr";
1710
+ }
1711
+ });
1712
+ };
1713
+ const selectionHandler = () => {
1714
+ const selection = window.getSelection();
1715
+ if (selection && selection.rangeCount > 0) {
1716
+ const range = selection.getRangeAt(0);
1717
+ if (editable.contains(range.commonAncestorContainer)) {
1718
+ onSelectionChange?.();
1719
+ updateToolbarStates();
1720
+ }
1721
+ }
1722
+ };
1723
+ document.addEventListener("selectionchange", selectionHandler);
1724
+ if (toolbar) {
1725
+ wrapper.append(toolbar);
1726
+ updateToolbarStates();
1727
+ }
1728
+ wrapper.append(editable);
1181
1729
  host.append(wrapper);
1182
1730
  return {
1183
1731
  destroy: () => {
1184
1732
  editable.removeEventListener("input", inputHandler);
1733
+ editable.removeEventListener("keydown", keydownHandler);
1734
+ document.removeEventListener("selectionchange", selectionHandler);
1185
1735
  host.innerHTML = "";
1186
1736
  },
1187
1737
  getHtml: () => editable.innerHTML,
1188
1738
  setHtml: (html) => {
1189
1739
  editable.innerHTML = html;
1190
- }
1740
+ updateToolbarStates();
1741
+ },
1742
+ isMarkActive: (markName) => checkMarkActive(markName),
1743
+ getActiveBlockType: () => checkActiveBlockType()
1191
1744
  };
1192
1745
  };
1193
1746
  //#endregion
@@ -1195,6 +1748,7 @@ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, re
1195
1748
  const createDomAdapter = (editor) => {
1196
1749
  let target = null;
1197
1750
  let mountedEditor = null;
1751
+ let unsubscribeChange = null;
1198
1752
  return {
1199
1753
  mount: (nextTarget) => {
1200
1754
  target = nextTarget;
@@ -1204,6 +1758,11 @@ const createDomAdapter = (editor) => {
1204
1758
  host: nextTarget,
1205
1759
  initialHtml: editor.export("html").payload,
1206
1760
  requestEmbedValue: () => null,
1761
+ toolbar: editor.toolbar,
1762
+ placeholder: editor.placeholder,
1763
+ onSelectionChange: () => {
1764
+ editor._triggerSelectionChange?.();
1765
+ },
1207
1766
  onChange: (html) => {
1208
1767
  editor.import({
1209
1768
  format: "html",
@@ -1211,15 +1770,30 @@ const createDomAdapter = (editor) => {
1211
1770
  });
1212
1771
  }
1213
1772
  });
1773
+ editor._registerActiveHandler?.({
1774
+ isMarkActive: (name) => mountedEditor?.isMarkActive(name) ?? false,
1775
+ getActiveBlockType: () => mountedEditor?.getActiveBlockType() ?? "paragraph"
1776
+ });
1777
+ unsubscribeChange = editor.on("change", () => {
1778
+ if (!mountedEditor) return;
1779
+ const nextHtml = editor.export("html").payload;
1780
+ if (mountedEditor.getHtml() !== nextHtml) mountedEditor.setHtml(nextHtml);
1781
+ });
1214
1782
  },
1215
1783
  unmount: () => {
1216
1784
  if (!target) return;
1785
+ unsubscribeChange?.();
1786
+ unsubscribeChange = null;
1787
+ editor._registerActiveHandler?.(null);
1217
1788
  mountedEditor?.destroy();
1218
1789
  mountedEditor = null;
1219
1790
  editor.unmount();
1220
1791
  target = null;
1221
1792
  },
1222
1793
  destroy: () => {
1794
+ unsubscribeChange?.();
1795
+ unsubscribeChange = null;
1796
+ editor._registerActiveHandler?.(null);
1223
1797
  mountedEditor?.destroy();
1224
1798
  mountedEditor = null;
1225
1799
  editor.destroy();
@@ -1229,6 +1803,40 @@ const createDomAdapter = (editor) => {
1229
1803
  };
1230
1804
  //#endregion
1231
1805
  //#region src/adapters/react/useEditorAdapter.tsx
1806
+ const EditorContext = createContext(null);
1807
+ const useEditor = () => {
1808
+ return useContext(EditorContext);
1809
+ };
1810
+ const useEditorState = () => {
1811
+ const editor = useContext(EditorContext);
1812
+ if (!editor) throw new Error("useEditorState must be used inside an EditorProvider");
1813
+ const [document, setDocument] = useState(() => editor.getDocument());
1814
+ const [selectionKey, setSelectionKey] = useState(0);
1815
+ useEffect(() => {
1816
+ const unsubChange = editor.on("change", () => {
1817
+ setDocument(editor.getDocument());
1818
+ });
1819
+ const unsubSelection = editor.on("selectionchange", () => {
1820
+ setSelectionKey((k) => k + 1);
1821
+ });
1822
+ return () => {
1823
+ unsubChange();
1824
+ unsubSelection();
1825
+ };
1826
+ }, [editor]);
1827
+ return {
1828
+ editor,
1829
+ document,
1830
+ isMarkActive: useCallback((name) => editor.isMarkActive(name), [editor, selectionKey]),
1831
+ activeBlockType: useCallback(() => editor.getActiveBlockType(), [editor, selectionKey])()
1832
+ };
1833
+ };
1834
+ function EditorProvider({ editor, children }) {
1835
+ return /* @__PURE__ */ jsx(EditorContext.Provider, {
1836
+ value: editor,
1837
+ children
1838
+ });
1839
+ }
1232
1840
  const useEditorAdapter = (config) => {
1233
1841
  const editor = useMemo(() => createEditor(config), [config]);
1234
1842
  useEffect(() => () => {
@@ -1236,13 +1844,27 @@ const useEditorAdapter = (config) => {
1236
1844
  }, [editor]);
1237
1845
  return editor;
1238
1846
  };
1239
- function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue }) {
1240
- const editor = useEditorAdapter({
1847
+ function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue, toolbar, placeholder, editor: propEditor }) {
1848
+ const contextEditor = useEditor();
1849
+ const fallbackEditor = useEditorAdapter({
1241
1850
  initialContent,
1242
1851
  onChange,
1243
- onError
1852
+ onError,
1853
+ toolbar,
1854
+ placeholder
1244
1855
  });
1856
+ const editor = propEditor ?? contextEditor ?? fallbackEditor;
1245
1857
  const hostRef = useRef(null);
1858
+ const mountedRef = useRef(null);
1859
+ useEffect(() => {
1860
+ if (editor._registerActiveHandler) editor._registerActiveHandler({
1861
+ isMarkActive: (name) => mountedRef.current?.isMarkActive(name) ?? false,
1862
+ getActiveBlockType: () => mountedRef.current?.getActiveBlockType() ?? "paragraph"
1863
+ });
1864
+ return () => {
1865
+ editor._registerActiveHandler?.(null);
1866
+ };
1867
+ }, [editor]);
1246
1868
  useEffect(() => {
1247
1869
  const host = hostRef.current;
1248
1870
  if (!host) return;
@@ -1251,6 +1873,11 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
1251
1873
  initialHtml: editor.export("html").payload,
1252
1874
  disabled,
1253
1875
  requestEmbedValue: onRequestEmbedValue,
1876
+ toolbar: toolbar ?? editor.toolbar,
1877
+ placeholder: placeholder ?? editor.placeholder,
1878
+ onSelectionChange: () => {
1879
+ editor._triggerSelectionChange?.();
1880
+ },
1254
1881
  onChange: (html) => {
1255
1882
  editor.import({
1256
1883
  format: "html",
@@ -1258,13 +1885,17 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
1258
1885
  });
1259
1886
  }
1260
1887
  });
1888
+ mountedRef.current = mounted;
1261
1889
  return () => {
1262
1890
  mounted.destroy();
1891
+ mountedRef.current = null;
1263
1892
  };
1264
1893
  }, [
1265
1894
  disabled,
1266
1895
  editor,
1267
- onRequestEmbedValue
1896
+ onRequestEmbedValue,
1897
+ toolbar,
1898
+ placeholder
1268
1899
  ]);
1269
1900
  return /* @__PURE__ */ jsx("div", {
1270
1901
  "aria-label": "Rich editor",
@@ -1273,6 +1904,6 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
1273
1904
  });
1274
1905
  }
1275
1906
  //#endregion
1276
- export { RichEditorAdapter, createDomAdapter, createEditor, createEditorSchema, defaultToolbarActions, editorMarkNames, editorNodeNames, editorSchema, exportRepresentation, importRepresentation, useEditorAdapter };
1907
+ export { EditorContext, EditorProvider, RichEditorAdapter, createDomAdapter, createEditor, createEditorSchema, defaultToolbarActions, editorMarkNames, editorNodeNames, editorSchema, exportRepresentation, importRepresentation, useEditor, useEditorAdapter, useEditorState };
1277
1908
 
1278
1909
  //# sourceMappingURL=index.mjs.map