@aeriz/wysiwyg 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +610 -96
  2. package/angular/README.md +112 -0
  3. package/angular/aeriz-editor.directive.ts +243 -0
  4. package/angular/aeriz-wysiwyg.module.ts +14 -0
  5. package/angular/index.ts +5 -0
  6. package/dist/adapters/jquery.cjs +1 -0
  7. package/dist/adapters/jquery.js +91 -0
  8. package/dist/adapters/react.cjs +1 -0
  9. package/dist/adapters/react.js +66 -0
  10. package/dist/adapters/shared.cjs +1 -0
  11. package/dist/adapters/shared.js +87 -0
  12. package/dist/adapters/svelte.cjs +1 -0
  13. package/dist/adapters/svelte.js +29 -0
  14. package/dist/adapters/vue.cjs +1 -0
  15. package/dist/adapters/vue.js +110 -0
  16. package/dist/adapters/vue2.cjs +1 -0
  17. package/dist/adapters/vue2.js +117 -0
  18. package/dist/adapters/wc.cjs +1 -0
  19. package/dist/adapters/wc.js +207 -0
  20. package/dist/aeriz-wysiwyg-adapter.js +169 -0
  21. package/dist/aeriz-wysiwyg-adapter.min.js +1 -0
  22. package/dist/aeriz-wysiwyg-jquery.js +275 -0
  23. package/dist/aeriz-wysiwyg-jquery.min.js +1 -0
  24. package/dist/aeriz-wysiwyg-wc.js +401 -0
  25. package/dist/aeriz-wysiwyg-wc.min.js +1 -0
  26. package/dist/aeriz-wysiwyg.cjs +6912 -0
  27. package/dist/aeriz-wysiwyg.css +37 -129
  28. package/dist/aeriz-wysiwyg.esm.js +1170 -149
  29. package/dist/aeriz-wysiwyg.esm.min.js +2057 -1209
  30. package/dist/aeriz-wysiwyg.js +1169 -148
  31. package/dist/aeriz-wysiwyg.min.cjs +54 -0
  32. package/dist/aeriz-wysiwyg.min.css +1 -1
  33. package/dist/aeriz-wysiwyg.min.js +44 -13
  34. package/dist/server.cjs +32 -0
  35. package/dist/server.js +666 -0
  36. package/dist-types/adapters/jquery.d.ts +34 -0
  37. package/dist-types/adapters/jquery.d.ts.map +1 -0
  38. package/dist-types/adapters/react.d.ts +28 -0
  39. package/dist-types/adapters/react.d.ts.map +1 -0
  40. package/dist-types/adapters/shared.d.ts +83 -0
  41. package/dist-types/adapters/shared.d.ts.map +1 -0
  42. package/dist-types/adapters/svelte.d.ts +38 -0
  43. package/dist-types/adapters/svelte.d.ts.map +1 -0
  44. package/dist-types/adapters/vue.d.ts +189 -0
  45. package/dist-types/adapters/vue.d.ts.map +1 -0
  46. package/dist-types/adapters/vue2.d.ts +175 -0
  47. package/dist-types/adapters/vue2.d.ts.map +1 -0
  48. package/dist-types/adapters/wc.d.ts +96 -0
  49. package/dist-types/adapters/wc.d.ts.map +1 -0
  50. package/dist-types/cjs/adapters/jquery.d.cts +34 -0
  51. package/dist-types/cjs/adapters/react.d.cts +28 -0
  52. package/dist-types/cjs/adapters/shared.d.cts +83 -0
  53. package/dist-types/cjs/adapters/svelte.d.cts +38 -0
  54. package/dist-types/cjs/adapters/vue.d.cts +189 -0
  55. package/dist-types/cjs/adapters/vue2.d.cts +175 -0
  56. package/dist-types/cjs/adapters/wc.d.cts +96 -0
  57. package/dist-types/cjs/core/AerizEditor.d.cts +357 -0
  58. package/dist-types/cjs/core/AerizViewer.d.cts +61 -0
  59. package/dist-types/cjs/core/block-meta.d.cts +32 -0
  60. package/dist-types/cjs/core/blocks.d.cts +368 -0
  61. package/dist-types/cjs/core/fonts.d.cts +60 -0
  62. package/dist-types/cjs/core/highlight.d.cts +44 -0
  63. package/dist-types/cjs/core/html-mini.d.cts +65 -0
  64. package/dist-types/cjs/core/i18n.d.cts +209 -0
  65. package/dist-types/cjs/core/import-html.d.cts +39 -0
  66. package/dist-types/cjs/core/import-markdown.d.cts +37 -0
  67. package/dist-types/cjs/core/markdown-export.d.cts +30 -0
  68. package/dist-types/cjs/core/plain-text.d.cts +17 -0
  69. package/dist-types/cjs/core/render-string.d.cts +68 -0
  70. package/dist-types/cjs/core/snapshot.d.cts +51 -0
  71. package/dist-types/cjs/index.d.cts +20 -0
  72. package/dist-types/cjs/server/index.d.cts +37 -0
  73. package/dist-types/cjs/types/index.d.cts +61 -0
  74. package/dist-types/cjs/ui/Toolbar.d.cts +21 -0
  75. package/dist-types/cjs/ui/dialogs.d.cts +74 -0
  76. package/dist-types/cjs/ui/icons.d.cts +2 -0
  77. package/dist-types/cjs/ui/popovers.d.cts +70 -0
  78. package/dist-types/core/AerizEditor.d.ts +30 -10
  79. package/dist-types/core/AerizEditor.d.ts.map +1 -1
  80. package/dist-types/core/AerizViewer.d.ts +16 -4
  81. package/dist-types/core/AerizViewer.d.ts.map +1 -1
  82. package/dist-types/core/block-meta.d.ts +32 -0
  83. package/dist-types/core/block-meta.d.ts.map +1 -0
  84. package/dist-types/core/blocks.d.ts +62 -7
  85. package/dist-types/core/blocks.d.ts.map +1 -1
  86. package/dist-types/core/fonts.d.ts +11 -0
  87. package/dist-types/core/fonts.d.ts.map +1 -1
  88. package/dist-types/core/html-mini.d.ts +65 -0
  89. package/dist-types/core/html-mini.d.ts.map +1 -0
  90. package/dist-types/core/import-html.d.ts.map +1 -1
  91. package/dist-types/core/markdown-export.d.ts +30 -0
  92. package/dist-types/core/markdown-export.d.ts.map +1 -0
  93. package/dist-types/core/plain-text.d.ts +17 -0
  94. package/dist-types/core/plain-text.d.ts.map +1 -0
  95. package/dist-types/core/render-string.d.ts +68 -0
  96. package/dist-types/core/render-string.d.ts.map +1 -0
  97. package/dist-types/core/snapshot.d.ts +51 -0
  98. package/dist-types/core/snapshot.d.ts.map +1 -0
  99. package/dist-types/index.d.ts +19 -10
  100. package/dist-types/index.d.ts.map +1 -1
  101. package/dist-types/server/index.d.ts +37 -0
  102. package/dist-types/server/index.d.ts.map +1 -0
  103. package/dist-types/types/index.d.ts +21 -0
  104. package/dist-types/types/index.d.ts.map +1 -1
  105. package/dist-types/ui/Toolbar.d.ts +1 -1
  106. package/dist-types/ui/popovers.d.ts +1 -1
  107. package/dist-types/ui/popovers.d.ts.map +1 -1
  108. package/package.json +213 -58
@@ -882,27 +882,80 @@ function pxFromCss(css) {
882
882
  return null;
883
883
  }
884
884
  let loaded = false;
885
+ const unavailable = /* @__PURE__ */ new Set();
886
+ const listeners = /* @__PURE__ */ new Set();
887
+ const SAMPLE_TEXT = "가A";
888
+ function getAvailableFontFamilies() {
889
+ if (unavailable.size === 0) return FONT_FAMILIES;
890
+ return FONT_FAMILIES.filter((f) => !unavailable.has(f.id));
891
+ }
892
+ function subscribeFontAvailability(listener) {
893
+ listeners.add(listener);
894
+ return () => {
895
+ listeners.delete(listener);
896
+ };
897
+ }
898
+ function isInstalledLocally(stack) {
899
+ const family = firstFamilyToken(stack);
900
+ if (!family) return false;
901
+ const ctx = document.createElement("canvas").getContext("2d");
902
+ if (!ctx) return false;
903
+ const widthOf = (spec) => {
904
+ ctx.font = `16px ${spec}`;
905
+ return ctx.measureText("가나다ABCwi").width;
906
+ };
907
+ return ["monospace", "serif"].some(
908
+ (fallback) => widthOf(`"${family}", ${fallback}`) !== widthOf(fallback)
909
+ );
910
+ }
911
+ function markUnavailable(fonts) {
912
+ let changed = false;
913
+ for (const font of fonts) {
914
+ if (unavailable.has(font.id)) continue;
915
+ if (isInstalledLocally(font.stack)) continue;
916
+ unavailable.add(font.id);
917
+ changed = true;
918
+ }
919
+ if (!changed) return;
920
+ for (const listener of listeners) listener();
921
+ }
922
+ function verifyLoaded(fonts) {
923
+ const set = document.fonts;
924
+ if (!set || typeof set.load !== "function") return;
925
+ for (const font of fonts) {
926
+ const family = firstFamilyToken(font.stack);
927
+ if (!family) continue;
928
+ set.load(`16px "${family}"`, SAMPLE_TEXT).then((faces) => {
929
+ if (faces.length === 0) markUnavailable([font]);
930
+ }).catch(() => markUnavailable([font]));
931
+ }
932
+ }
885
933
  function ensureFontFamiliesLoaded() {
886
934
  if (loaded) return;
887
935
  loaded = true;
888
936
  if (typeof document === "undefined") return;
889
- const googleParams = FONT_FAMILIES.map((f) => f.googleFonts).filter((p) => typeof p === "string" && p.length > 0);
890
- if (googleParams.length > 0) {
891
- const url = "https://fonts.googleapis.com/css2?" + googleParams.map((p) => `family=${p}`).join("&") + "&display=swap";
892
- appendStylesheet(url);
937
+ const googleFonts = FONT_FAMILIES.filter((f) => f.googleFonts);
938
+ if (googleFonts.length > 0) {
939
+ const url = "https://fonts.googleapis.com/css2?" + googleFonts.map((f) => `family=${f.googleFonts}`).join("&") + "&display=swap";
940
+ appendStylesheet(url, googleFonts);
893
941
  }
894
- for (const f of FONT_FAMILIES) {
895
- if (f.cssUrl) appendStylesheet(f.cssUrl);
942
+ for (const font of FONT_FAMILIES) {
943
+ if (font.cssUrl) appendStylesheet(font.cssUrl, [font]);
896
944
  }
897
945
  }
898
- function appendStylesheet(href) {
946
+ function appendStylesheet(href, provides) {
899
947
  const existing = document.querySelector(
900
948
  `link[rel="stylesheet"][href="${href}"]`
901
949
  );
902
- if (existing) return;
950
+ if (existing) {
951
+ verifyLoaded(provides);
952
+ return;
953
+ }
903
954
  const link = document.createElement("link");
904
955
  link.rel = "stylesheet";
905
956
  link.href = href;
957
+ link.addEventListener("error", () => markUnavailable(provides));
958
+ link.addEventListener("load", () => verifyLoaded(provides));
906
959
  document.head.appendChild(link);
907
960
  }
908
961
  function findFontIdFromCss(css) {
@@ -1630,9 +1683,9 @@ const LANG_BY_ID = (() => {
1630
1683
  return m;
1631
1684
  })();
1632
1685
  function highlightCode(code, langId) {
1633
- if (!langId) return escapeHtml$1(code);
1686
+ if (!langId) return escapeHtml$2(code);
1634
1687
  const lang = LANG_BY_ID.get(langId);
1635
- if (!lang) return escapeHtml$1(code);
1688
+ if (!lang) return escapeHtml$2(code);
1636
1689
  if (lang.custom) return lang.custom(code);
1637
1690
  return tokenize(code, lang);
1638
1691
  }
@@ -1646,14 +1699,14 @@ function tokenize(code, lang) {
1646
1699
  if (lang.lineComment && code.startsWith(lang.lineComment, i)) {
1647
1700
  const nl = code.indexOf("\n", i);
1648
1701
  const stop = nl === -1 ? n : nl;
1649
- out += `<span class="hl-comment">${escapeHtml$1(code.slice(i, stop))}</span>`;
1702
+ out += `<span class="hl-comment">${escapeHtml$2(code.slice(i, stop))}</span>`;
1650
1703
  i = stop;
1651
1704
  continue;
1652
1705
  }
1653
1706
  if (lang.blockComment && code.startsWith(lang.blockComment.start, i)) {
1654
1707
  const closeAt = code.indexOf(lang.blockComment.end, i + lang.blockComment.start.length);
1655
1708
  const stop = closeAt === -1 ? n : closeAt + lang.blockComment.end.length;
1656
- out += `<span class="hl-comment">${escapeHtml$1(code.slice(i, stop))}</span>`;
1709
+ out += `<span class="hl-comment">${escapeHtml$2(code.slice(i, stop))}</span>`;
1657
1710
  i = stop;
1658
1711
  continue;
1659
1712
  }
@@ -1673,7 +1726,7 @@ function tokenize(code, lang) {
1673
1726
  if (c === "\n" && sd !== "`") break;
1674
1727
  j++;
1675
1728
  }
1676
- out += `<span class="hl-string">${escapeHtml$1(code.slice(i, j))}</span>`;
1729
+ out += `<span class="hl-string">${escapeHtml$2(code.slice(i, j))}</span>`;
1677
1730
  i = j;
1678
1731
  continue;
1679
1732
  }
@@ -1681,7 +1734,7 @@ function tokenize(code, lang) {
1681
1734
  if (/[0-9]/.test(ch)) {
1682
1735
  const m = NUMBER_RE.exec(code.slice(i));
1683
1736
  if (m) {
1684
- out += `<span class="hl-number">${escapeHtml$1(m[0])}</span>`;
1737
+ out += `<span class="hl-number">${escapeHtml$2(m[0])}</span>`;
1685
1738
  i += m[0].length;
1686
1739
  continue;
1687
1740
  }
@@ -1692,22 +1745,22 @@ function tokenize(code, lang) {
1692
1745
  const word = code.slice(i, j);
1693
1746
  const lookup = ci ? word.toLowerCase() : word;
1694
1747
  if (lang.keywords.has(lookup)) {
1695
- out += `<span class="hl-keyword">${escapeHtml$1(word)}</span>`;
1748
+ out += `<span class="hl-keyword">${escapeHtml$2(word)}</span>`;
1696
1749
  } else if ((_b = lang.builtins) == null ? void 0 : _b.has(lookup)) {
1697
- out += `<span class="hl-builtin">${escapeHtml$1(word)}</span>`;
1750
+ out += `<span class="hl-builtin">${escapeHtml$2(word)}</span>`;
1698
1751
  } else {
1699
1752
  let k = j;
1700
1753
  while (k < n && (code[k] === " " || code[k] === " ")) k++;
1701
1754
  if (code[k] === "(") {
1702
- out += `<span class="hl-fn">${escapeHtml$1(word)}</span>`;
1755
+ out += `<span class="hl-fn">${escapeHtml$2(word)}</span>`;
1703
1756
  } else {
1704
- out += escapeHtml$1(word);
1757
+ out += escapeHtml$2(word);
1705
1758
  }
1706
1759
  }
1707
1760
  i = j;
1708
1761
  continue;
1709
1762
  }
1710
- out += escapeHtml$1(ch);
1763
+ out += escapeHtml$2(ch);
1711
1764
  i++;
1712
1765
  }
1713
1766
  return out;
@@ -1721,14 +1774,14 @@ function highlightHtml(code) {
1721
1774
  if (code.startsWith("<!--", i)) {
1722
1775
  const end = code.indexOf("-->", i + 4);
1723
1776
  const stop = end === -1 ? n : end + 3;
1724
- out += `<span class="hl-comment">${escapeHtml$1(code.slice(i, stop))}</span>`;
1777
+ out += `<span class="hl-comment">${escapeHtml$2(code.slice(i, stop))}</span>`;
1725
1778
  i = stop;
1726
1779
  continue;
1727
1780
  }
1728
1781
  if (code[i] === "<") {
1729
1782
  const end = code.indexOf(">", i);
1730
1783
  if (end === -1) {
1731
- out += escapeHtml$1(code.slice(i));
1784
+ out += escapeHtml$2(code.slice(i));
1732
1785
  break;
1733
1786
  }
1734
1787
  const tag = code.slice(i, end + 1);
@@ -1736,7 +1789,7 @@ function highlightHtml(code) {
1736
1789
  i = end + 1;
1737
1790
  continue;
1738
1791
  }
1739
- out += escapeHtml$1(code[i] ?? "");
1792
+ out += escapeHtml$2(code[i] ?? "");
1740
1793
  i++;
1741
1794
  }
1742
1795
  return out;
@@ -1746,27 +1799,27 @@ function highlightHtmlTag(tag) {
1746
1799
  let result = '<span class="hl-punct">&lt;</span>';
1747
1800
  const nameMatch = /^\/?[a-zA-Z][a-zA-Z0-9-]*/.exec(inner);
1748
1801
  if (nameMatch) {
1749
- result += `<span class="hl-tag">${escapeHtml$1(nameMatch[0])}</span>`;
1802
+ result += `<span class="hl-tag">${escapeHtml$2(nameMatch[0])}</span>`;
1750
1803
  let rest = inner.slice(nameMatch[0].length);
1751
1804
  const ATTR_RE = /\s+([a-zA-Z_:][a-zA-Z0-9_:.-]*)(\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
1752
1805
  let last = 0;
1753
1806
  let m;
1754
1807
  while (m = ATTR_RE.exec(rest)) {
1755
- result += escapeHtml$1(rest.slice(last, m.index));
1756
- result += escapeHtml$1(rest.slice(m.index, m.index + m[0].indexOf(m[1] ?? "")));
1757
- result += `<span class="hl-attr">${escapeHtml$1(m[1] ?? "")}</span>`;
1808
+ result += escapeHtml$2(rest.slice(last, m.index));
1809
+ result += escapeHtml$2(rest.slice(m.index, m.index + m[0].indexOf(m[1] ?? "")));
1810
+ result += `<span class="hl-attr">${escapeHtml$2(m[1] ?? "")}</span>`;
1758
1811
  const valuePart = rest.slice(m.index + (m[0].indexOf(m[1] ?? "") + (m[1] ?? "").length), m.index + m[0].length);
1759
1812
  if (valuePart.includes("=")) {
1760
1813
  const eqIdx = valuePart.indexOf("=");
1761
- result += escapeHtml$1(valuePart.slice(0, eqIdx + 1));
1814
+ result += escapeHtml$2(valuePart.slice(0, eqIdx + 1));
1762
1815
  const valueStr = valuePart.slice(eqIdx + 1);
1763
- result += `<span class="hl-string">${escapeHtml$1(valueStr)}</span>`;
1816
+ result += `<span class="hl-string">${escapeHtml$2(valueStr)}</span>`;
1764
1817
  }
1765
1818
  last = m.index + m[0].length;
1766
1819
  }
1767
- result += escapeHtml$1(rest.slice(last));
1820
+ result += escapeHtml$2(rest.slice(last));
1768
1821
  } else {
1769
- result += escapeHtml$1(inner);
1822
+ result += escapeHtml$2(inner);
1770
1823
  }
1771
1824
  result += '<span class="hl-punct">&gt;</span>';
1772
1825
  return result;
@@ -1780,20 +1833,20 @@ function highlightCss(code) {
1780
1833
  if (code.startsWith("/*", i)) {
1781
1834
  const end = code.indexOf("*/", i + 2);
1782
1835
  const stop = end === -1 ? n : end + 2;
1783
- out += `<span class="hl-comment">${escapeHtml$1(code.slice(i, stop))}</span>`;
1836
+ out += `<span class="hl-comment">${escapeHtml$2(code.slice(i, stop))}</span>`;
1784
1837
  i = stop;
1785
1838
  continue;
1786
1839
  }
1787
1840
  const ch = code[i] ?? "";
1788
1841
  if (ch === "{") {
1789
1842
  inBlock = true;
1790
- out += escapeHtml$1(ch);
1843
+ out += escapeHtml$2(ch);
1791
1844
  i++;
1792
1845
  continue;
1793
1846
  }
1794
1847
  if (ch === "}") {
1795
1848
  inBlock = false;
1796
- out += escapeHtml$1(ch);
1849
+ out += escapeHtml$2(ch);
1797
1850
  i++;
1798
1851
  continue;
1799
1852
  }
@@ -1805,7 +1858,7 @@ function highlightCss(code) {
1805
1858
  else j++;
1806
1859
  }
1807
1860
  j = Math.min(j + 1, n);
1808
- out += `<span class="hl-string">${escapeHtml$1(code.slice(i, j))}</span>`;
1861
+ out += `<span class="hl-string">${escapeHtml$2(code.slice(i, j))}</span>`;
1809
1862
  i = j;
1810
1863
  continue;
1811
1864
  }
@@ -1814,17 +1867,17 @@ function highlightCss(code) {
1814
1867
  while (j < n && /[a-zA-Z0-9_-]/.test(code[j] ?? "")) j++;
1815
1868
  const word = code.slice(i, j);
1816
1869
  if (ch === "@") {
1817
- out += `<span class="hl-keyword">${escapeHtml$1(word)}</span>`;
1870
+ out += `<span class="hl-keyword">${escapeHtml$2(word)}</span>`;
1818
1871
  } else if (inBlock && /^[a-zA-Z-]+$/.test(word)) {
1819
1872
  let k = j;
1820
1873
  while (k < n && (code[k] === " " || code[k] === " ")) k++;
1821
1874
  if (code[k] === ":") {
1822
- out += `<span class="hl-prop">${escapeHtml$1(word)}</span>`;
1875
+ out += `<span class="hl-prop">${escapeHtml$2(word)}</span>`;
1823
1876
  } else {
1824
- out += escapeHtml$1(word);
1877
+ out += escapeHtml$2(word);
1825
1878
  }
1826
1879
  } else {
1827
- out += `<span class="hl-tag">${escapeHtml$1(word)}</span>`;
1880
+ out += `<span class="hl-tag">${escapeHtml$2(word)}</span>`;
1828
1881
  }
1829
1882
  i = j;
1830
1883
  continue;
@@ -1832,12 +1885,12 @@ function highlightCss(code) {
1832
1885
  if (/[0-9]/.test(ch)) {
1833
1886
  const m = /^[0-9.]+(px|em|rem|%|vh|vw|s|ms|deg|fr)?/.exec(code.slice(i));
1834
1887
  if (m) {
1835
- out += `<span class="hl-number">${escapeHtml$1(m[0])}</span>`;
1888
+ out += `<span class="hl-number">${escapeHtml$2(m[0])}</span>`;
1836
1889
  i += m[0].length;
1837
1890
  continue;
1838
1891
  }
1839
1892
  }
1840
- out += escapeHtml$1(ch);
1893
+ out += escapeHtml$2(ch);
1841
1894
  i++;
1842
1895
  }
1843
1896
  return out;
@@ -1848,22 +1901,22 @@ function highlightMarkdown(code) {
1848
1901
  function highlightMdLine(line) {
1849
1902
  const h = /^(#{1,6})\s+(.*)$/.exec(line);
1850
1903
  if (h) {
1851
- return `<span class="hl-keyword">${escapeHtml$1(h[1] ?? "")}</span> <span class="hl-tag">${highlightMdInline(h[2] ?? "")}</span>`;
1904
+ return `<span class="hl-keyword">${escapeHtml$2(h[1] ?? "")}</span> <span class="hl-tag">${highlightMdInline(h[2] ?? "")}</span>`;
1852
1905
  }
1853
1906
  if (line.startsWith(">")) {
1854
- return `<span class="hl-comment">${escapeHtml$1(line)}</span>`;
1907
+ return `<span class="hl-comment">${escapeHtml$2(line)}</span>`;
1855
1908
  }
1856
1909
  if (line.startsWith("```")) {
1857
- return `<span class="hl-keyword">${escapeHtml$1(line)}</span>`;
1910
+ return `<span class="hl-keyword">${escapeHtml$2(line)}</span>`;
1858
1911
  }
1859
1912
  const li = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
1860
1913
  if (li) {
1861
- return `${li[1] ?? ""}<span class="hl-keyword">${escapeHtml$1(li[2] ?? "")}</span> ${highlightMdInline(li[3] ?? "")}`;
1914
+ return `${li[1] ?? ""}<span class="hl-keyword">${escapeHtml$2(li[2] ?? "")}</span> ${highlightMdInline(li[3] ?? "")}`;
1862
1915
  }
1863
1916
  return highlightMdInline(line);
1864
1917
  }
1865
1918
  function highlightMdInline(s) {
1866
- let out = escapeHtml$1(s);
1919
+ let out = escapeHtml$2(s);
1867
1920
  out = out.replace(/\*\*([^*]+)\*\*/g, '<span class="hl-keyword">**$1**</span>');
1868
1921
  out = out.replace(new RegExp("(?<!\\*)\\*([^*]+)\\*(?!\\*)", "g"), '<span class="hl-string">*$1*</span>');
1869
1922
  out = out.replace(/`([^`]+)`/g, '<span class="hl-builtin">`$1`</span>');
@@ -1873,7 +1926,7 @@ function highlightMdInline(s) {
1873
1926
  );
1874
1927
  return out;
1875
1928
  }
1876
- function escapeHtml$1(s) {
1929
+ function escapeHtml$2(s) {
1877
1930
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1878
1931
  }
1879
1932
  function preserveSelection(el) {
@@ -2144,31 +2197,42 @@ function openFontFamilyDropdown(anchor, onPick, currentId) {
2144
2197
  popover.className = "aw-popover aw-font-family-popover";
2145
2198
  popover.dataset.popover = "true";
2146
2199
  preserveSelection(popover);
2147
- for (const font of FONT_FAMILIES) {
2148
- const btn = document.createElement("button");
2149
- btn.type = "button";
2150
- btn.className = "aw-popover-item aw-font-family-item";
2151
- if (currentId && currentId === font.id) {
2152
- btn.classList.add("aw-popover-item-active");
2200
+ const render = () => {
2201
+ popover.replaceChildren();
2202
+ for (const font of getAvailableFontFamilies()) {
2203
+ const btn = document.createElement("button");
2204
+ btn.type = "button";
2205
+ btn.className = "aw-popover-item aw-font-family-item";
2206
+ if (currentId && currentId === font.id) {
2207
+ btn.classList.add("aw-popover-item-active");
2208
+ }
2209
+ btn.title = font.label;
2210
+ const name = document.createElement("span");
2211
+ name.className = "aw-font-family-name";
2212
+ name.textContent = font.label;
2213
+ const preview = document.createElement("span");
2214
+ preview.className = "aw-font-family-preview";
2215
+ preview.textContent = "가A";
2216
+ preview.style.fontFamily = font.stack;
2217
+ btn.appendChild(name);
2218
+ btn.appendChild(preview);
2219
+ preserveSelection(btn);
2220
+ btn.addEventListener("click", (e) => {
2221
+ e.stopPropagation();
2222
+ popover.remove();
2223
+ onPick({ id: font.id, label: font.label, stack: font.stack });
2224
+ });
2225
+ popover.appendChild(btn);
2153
2226
  }
2154
- btn.title = font.label;
2155
- const name = document.createElement("span");
2156
- name.className = "aw-font-family-name";
2157
- name.textContent = font.label;
2158
- const preview = document.createElement("span");
2159
- preview.className = "aw-font-family-preview";
2160
- preview.textContent = "가A";
2161
- preview.style.fontFamily = font.stack;
2162
- btn.appendChild(name);
2163
- btn.appendChild(preview);
2164
- preserveSelection(btn);
2165
- btn.addEventListener("click", (e) => {
2166
- e.stopPropagation();
2167
- popover.remove();
2168
- onPick({ id: font.id, label: font.label, stack: font.stack });
2169
- });
2170
- popover.appendChild(btn);
2171
- }
2227
+ };
2228
+ render();
2229
+ const unsubscribe = subscribeFontAvailability(() => {
2230
+ if (!popover.isConnected) {
2231
+ unsubscribe();
2232
+ return;
2233
+ }
2234
+ render();
2235
+ });
2172
2236
  anchorPopover(popover, anchor);
2173
2237
  }
2174
2238
  function openCodeLanguageDropdown(anchor, onPick, currentId) {
@@ -2499,6 +2563,24 @@ const INHERITED_PROPERTIES = /* @__PURE__ */ new Set([
2499
2563
  "stroke-width",
2500
2564
  "text-anchor"
2501
2565
  ]);
2566
+ const CURRENT_COLOR_DEFAULTED = /* @__PURE__ */ new Set([
2567
+ "border-block-end-color",
2568
+ "border-block-start-color",
2569
+ "border-bottom-color",
2570
+ "border-inline-end-color",
2571
+ "border-inline-start-color",
2572
+ "border-left-color",
2573
+ "border-right-color",
2574
+ "border-top-color",
2575
+ "caret-color",
2576
+ "column-rule-color",
2577
+ "row-rule-color",
2578
+ "outline-color",
2579
+ "text-decoration-color",
2580
+ "text-emphasis-color",
2581
+ "-webkit-text-fill-color",
2582
+ "-webkit-text-stroke-color"
2583
+ ]);
2502
2584
  async function importHtmlWithComputedStyles(html, options = {}) {
2503
2585
  const settleMs = options.settleMs ?? 1e3;
2504
2586
  const timeoutMs = options.timeoutMs ?? 3e4;
@@ -2655,8 +2737,8 @@ function createBaselineProbe(iframeDoc, iframeWin, mountTarget) {
2655
2737
  };
2656
2738
  return { get, cleanup };
2657
2739
  }
2658
- function computeMinimalInline(el, parent, baselineTagName, getBaseline, win) {
2659
- const cs = win.getComputedStyle(el);
2740
+ function computeMinimalInline(el, parent, baselineTagName, getBaseline, win, pseudo) {
2741
+ const cs = win.getComputedStyle(el, pseudo ?? null);
2660
2742
  const parentCs = parent ? win.getComputedStyle(parent) : null;
2661
2743
  const baseline = getBaseline(baselineTagName);
2662
2744
  let cssText = "";
@@ -2665,6 +2747,8 @@ function computeMinimalInline(el, parent, baselineTagName, getBaseline, win) {
2665
2747
  if (!prop) continue;
2666
2748
  const val = cs.getPropertyValue(prop);
2667
2749
  if (!val) continue;
2750
+ if (pseudo && prop === "content") continue;
2751
+ if (CURRENT_COLOR_DEFAULTED.has(prop) && val === cs.getPropertyValue("color")) continue;
2668
2752
  if (INHERITED_PROPERTIES.has(prop)) {
2669
2753
  if (parentCs && parentCs.getPropertyValue(prop) === val) continue;
2670
2754
  } else {
@@ -2674,6 +2758,56 @@ function computeMinimalInline(el, parent, baselineTagName, getBaseline, win) {
2674
2758
  }
2675
2759
  return cssText;
2676
2760
  }
2761
+ function textFromCssContent(value) {
2762
+ let text = "";
2763
+ const segments = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g;
2764
+ let match;
2765
+ while ((match = segments.exec(value)) !== null) {
2766
+ const raw = match[1] ?? match[2] ?? "";
2767
+ text += raw.replace(
2768
+ /\\(?:([0-9a-fA-F]{1,6})\s?|(.))/g,
2769
+ (_all, hex, ch) => hex ? String.fromCodePoint(parseInt(hex, 16)) : ch ?? ""
2770
+ );
2771
+ }
2772
+ return text;
2773
+ }
2774
+ const NO_PHRASING_CHILDREN = /* @__PURE__ */ new Set([
2775
+ "TABLE",
2776
+ "THEAD",
2777
+ "TBODY",
2778
+ "TFOOT",
2779
+ "TR",
2780
+ "UL",
2781
+ "OL",
2782
+ "DL",
2783
+ "SELECT",
2784
+ "OPTGROUP"
2785
+ ]);
2786
+ function materializePseudo(orig, copy, which, getBaseline, win) {
2787
+ if (NO_PHRASING_CHILDREN.has(copy.tagName)) return;
2788
+ const selector = which === "before" ? "::before" : "::after";
2789
+ const cs = win.getComputedStyle(orig, selector);
2790
+ const content = cs.getPropertyValue("content");
2791
+ if (!content || content === "none" || content === "normal") return;
2792
+ if (cs.getPropertyValue("display") === "none") return;
2793
+ const text = textFromCssContent(content);
2794
+ const url = /url\((["']?)([^"')]+)\1\)/.exec(content);
2795
+ const cssText = computeMinimalInline(orig, orig, "span", getBaseline, win, selector);
2796
+ if (!text && !url && !cssText) return;
2797
+ const doc = copy.ownerDocument;
2798
+ const span = doc.createElement("span");
2799
+ span.setAttribute("data-imported-pseudo", which);
2800
+ if (cssText) span.setAttribute("style", cssText);
2801
+ if (url == null ? void 0 : url[2]) {
2802
+ const img = doc.createElement("img");
2803
+ img.setAttribute("src", url[2]);
2804
+ img.setAttribute("alt", "");
2805
+ span.appendChild(img);
2806
+ }
2807
+ if (text) span.appendChild(doc.createTextNode(text));
2808
+ if (which === "before") copy.insertBefore(span, copy.firstChild);
2809
+ else copy.appendChild(span);
2810
+ }
2677
2811
  function applyComputedStylesParallel(orig, copy, origParent, getBaseline, win) {
2678
2812
  if (orig.nodeType === 1 && copy.nodeType === 1) {
2679
2813
  const cssText = computeMinimalInline(
@@ -2697,6 +2831,10 @@ function applyComputedStylesParallel(orig, copy, origParent, getBaseline, win) {
2697
2831
  const cc = copyChildren[i];
2698
2832
  if (oc && cc) applyComputedStylesParallel(oc, cc, orig, getBaseline, win);
2699
2833
  }
2834
+ if (orig.nodeType === 1 && copy.nodeType === 1) {
2835
+ materializePseudo(orig, copy, "before", getBaseline, win);
2836
+ materializePseudo(orig, copy, "after", getBaseline, win);
2837
+ }
2700
2838
  }
2701
2839
  function markdownToBlocks(md) {
2702
2840
  const html = markdownToHtml(md);
@@ -2729,9 +2867,9 @@ function markdownToHtml(md) {
2729
2867
  i++;
2730
2868
  }
2731
2869
  if (i < lines.length) i++;
2732
- const langAttr = lang ? ` data-language="${escapeAttr(lang)}"` : "";
2870
+ const langAttr = lang ? ` data-language="${escapeAttr$1(lang)}"` : "";
2733
2871
  blocks.push(
2734
- `<pre><code${langAttr}>${escapeHtml(codeLines.join("\n"))}</code></pre>`
2872
+ `<pre><code${langAttr}>${escapeHtml$1(codeLines.join("\n"))}</code></pre>`
2735
2873
  );
2736
2874
  continue;
2737
2875
  }
@@ -2865,42 +3003,115 @@ function joinSoftBreaks(lines) {
2865
3003
  }
2866
3004
  return parts.join("");
2867
3005
  }
3006
+ const CODE_SLOT = String.fromCharCode(57600);
3007
+ const CODE_SLOT_RE = new RegExp(CODE_SLOT + "([0-9]+)" + CODE_SLOT, "g");
2868
3008
  function parseInline(src) {
2869
3009
  const codes = [];
2870
3010
  let working = src.replace(/(`+)([\s\S]+?)\1/g, (_m, _b, code) => {
2871
- const idx = codes.push(`<code>${escapeHtml(code)}</code>`) - 1;
2872
- return `\0CODE${idx}\0`;
3011
+ const idx = codes.push(`<code>${escapeHtml$1(code)}</code>`) - 1;
3012
+ return `${CODE_SLOT}${idx}${CODE_SLOT}`;
2873
3013
  });
2874
- working = escapeHtml(working);
3014
+ working = escapeHtml$1(working);
2875
3015
  working = working.replace(
2876
3016
  /&lt;(https?:\/\/[^\s&]+)&gt;/g,
2877
- (_m, url) => `<a href="${escapeAttr(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`
3017
+ (_m, url) => `<a href="${escapeAttr$1(url)}" target="_blank" rel="noopener">${escapeHtml$1(url)}</a>`
2878
3018
  );
2879
3019
  working = working.replace(
2880
3020
  /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
2881
- (_m, alt, url) => `<img src="${escapeAttr(url)}" alt="${escapeAttr(alt)}">`
3021
+ (_m, alt, url) => `<img src="${escapeAttr$1(url)}" alt="${escapeAttr$1(alt)}">`
2882
3022
  );
2883
3023
  working = working.replace(
2884
3024
  /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
2885
- (_m, text, url) => `<a href="${escapeAttr(url)}" target="_blank" rel="noopener">${text}</a>`
3025
+ (_m, text, url) => `<a href="${escapeAttr$1(url)}" target="_blank" rel="noopener">${text}</a>`
2886
3026
  );
2887
3027
  working = working.replace(/\*\*([^*\n]+?)\*\*/g, "<strong>$1</strong>");
2888
3028
  working = working.replace(/__([^_\n]+?)__/g, "<strong>$1</strong>");
2889
3029
  working = working.replace(/(^|[^*])\*([^*\n]+?)\*(?!\*)/g, "$1<em>$2</em>");
2890
3030
  working = working.replace(/(^|[^_])_([^_\n]+?)_(?!_)/g, "$1<em>$2</em>");
2891
3031
  working = working.replace(/~~([^~\n]+?)~~/g, "<s>$1</s>");
2892
- working = working.replace(/CODE(\d+)/g, (_m, n) => {
3032
+ working = working.replace(CODE_SLOT_RE, (_m, n) => {
2893
3033
  const idx = Number(n);
2894
3034
  return codes[idx] ?? "";
2895
3035
  });
2896
3036
  return working;
2897
3037
  }
2898
- function escapeHtml(s) {
3038
+ function escapeHtml$1(s) {
2899
3039
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2900
3040
  }
2901
- function escapeAttr(s) {
3041
+ function escapeAttr$1(s) {
2902
3042
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
2903
3043
  }
3044
+ function formatBytes(bytes) {
3045
+ if (bytes < 1024) return `${bytes} B`;
3046
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3047
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
3048
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
3049
+ }
3050
+ function fileExtensionLabel(name, mime) {
3051
+ if (name) {
3052
+ const idx = name.lastIndexOf(".");
3053
+ if (idx > 0 && idx < name.length - 1) {
3054
+ return name.slice(idx + 1).toUpperCase();
3055
+ }
3056
+ }
3057
+ if (mime) {
3058
+ const sub = mime.split("/").pop() ?? "";
3059
+ return sub.toUpperCase();
3060
+ }
3061
+ return "";
3062
+ }
3063
+ function hostnameFromUrl(url) {
3064
+ try {
3065
+ return new URL(url).hostname;
3066
+ } catch {
3067
+ return "";
3068
+ }
3069
+ }
3070
+ function defaultFaviconUrl(url) {
3071
+ const host = hostnameFromUrl(url);
3072
+ if (!host) return "";
3073
+ return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=64`;
3074
+ }
3075
+ const FILE_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></svg>';
3076
+ const FILE_ACTION_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
3077
+ const YOUTUBE_IFRAME_ALLOW = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
3078
+ const YOUTUBE_EMBED_BASE = "https://www.youtube-nocookie.com/embed/";
3079
+ const FILE_FALLBACK_NAME = "파일";
3080
+ const BLOCK_ID_ATTR = "data-block-id";
3081
+ const SNAPSHOT_VERSION = 1;
3082
+ function snapshotVersionOf(snapshot) {
3083
+ const raw = snapshot.version;
3084
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : 1;
3085
+ }
3086
+ const KNOWN_KINDS = /* @__PURE__ */ new Set([
3087
+ "paragraph",
3088
+ "heading",
3089
+ "bulletList",
3090
+ "orderedList",
3091
+ "blockquote",
3092
+ "codeBlock",
3093
+ "image",
3094
+ "table",
3095
+ "hr",
3096
+ "linkCard",
3097
+ "youtube",
3098
+ "file",
3099
+ "importedHtml",
3100
+ "unknown"
3101
+ ]);
3102
+ function upgradeSnapshots(input) {
3103
+ return input.map((snap) => upgradeSnapshot(snap));
3104
+ }
3105
+ function upgradeSnapshot(snapshot) {
3106
+ const kind = KNOWN_KINDS.has(snapshot.kind) ? snapshot.kind : "unknown";
3107
+ const version = Math.max(snapshotVersionOf(snapshot), 1);
3108
+ return {
3109
+ ...snapshot,
3110
+ kind,
3111
+ version,
3112
+ html: typeof snapshot.html === "string" ? snapshot.html : ""
3113
+ };
3114
+ }
2904
3115
  const ALL_INLINE_MARKS = [
2905
3116
  "bold",
2906
3117
  "italic",
@@ -3144,12 +3355,9 @@ const YOUTUBE = {
3144
3355
  if (attrs == null ? void 0 : attrs.wrapperStyle) wrap.setAttribute("style", attrs.wrapperStyle);
3145
3356
  const iframe = document.createElement("iframe");
3146
3357
  if (videoId) {
3147
- iframe.src = `https://www.youtube-nocookie.com/embed/${videoId}`;
3358
+ iframe.src = `${YOUTUBE_EMBED_BASE}${videoId}`;
3148
3359
  }
3149
- iframe.setAttribute(
3150
- "allow",
3151
- "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
3152
- );
3360
+ iframe.setAttribute("allow", YOUTUBE_IFRAME_ALLOW);
3153
3361
  iframe.setAttribute("allowfullscreen", "");
3154
3362
  iframe.setAttribute("frameborder", "0");
3155
3363
  iframe.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
@@ -3237,18 +3445,6 @@ const LINK_CARD = {
3237
3445
  },
3238
3446
  isEmpty: (el) => !el.getAttribute("href")
3239
3447
  };
3240
- function hostnameFromUrl(url) {
3241
- try {
3242
- return new URL(url).hostname;
3243
- } catch {
3244
- return "";
3245
- }
3246
- }
3247
- function defaultFaviconUrl(url) {
3248
- const host = hostnameFromUrl(url);
3249
- if (!host) return "";
3250
- return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=64`;
3251
- }
3252
3448
  const FILE = {
3253
3449
  kind: "file",
3254
3450
  label: "파일",
@@ -3287,12 +3483,12 @@ const FILE = {
3287
3483
  const iconSpan = document.createElement("span");
3288
3484
  iconSpan.className = "aw-file-icon";
3289
3485
  iconSpan.setAttribute("aria-hidden", "true");
3290
- iconSpan.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></svg>';
3486
+ iconSpan.innerHTML = FILE_ICON_SVG;
3291
3487
  const meta = document.createElement("span");
3292
3488
  meta.className = "aw-file-meta";
3293
3489
  const nameSpan = document.createElement("span");
3294
3490
  nameSpan.className = "aw-file-name";
3295
- nameSpan.textContent = (attrs == null ? void 0 : attrs.fileName) ?? "파일";
3491
+ nameSpan.textContent = (attrs == null ? void 0 : attrs.fileName) ?? FILE_FALLBACK_NAME;
3296
3492
  meta.appendChild(nameSpan);
3297
3493
  const infoParts = [];
3298
3494
  const ext = fileExtensionLabel(attrs == null ? void 0 : attrs.fileName, attrs == null ? void 0 : attrs.fileType);
@@ -3307,7 +3503,7 @@ const FILE = {
3307
3503
  const action = document.createElement("span");
3308
3504
  action.className = "aw-file-action";
3309
3505
  action.setAttribute("aria-hidden", "true");
3310
- action.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
3506
+ action.innerHTML = FILE_ACTION_SVG;
3311
3507
  a.appendChild(iconSpan);
3312
3508
  a.appendChild(meta);
3313
3509
  a.appendChild(action);
@@ -3315,25 +3511,6 @@ const FILE = {
3315
3511
  },
3316
3512
  isEmpty: (el) => !el.getAttribute("href")
3317
3513
  };
3318
- function formatBytes(bytes) {
3319
- if (bytes < 1024) return `${bytes} B`;
3320
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3321
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
3322
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
3323
- }
3324
- function fileExtensionLabel(name, mime) {
3325
- if (name) {
3326
- const idx = name.lastIndexOf(".");
3327
- if (idx > 0 && idx < name.length - 1) {
3328
- return name.slice(idx + 1).toUpperCase();
3329
- }
3330
- }
3331
- if (mime) {
3332
- const sub = mime.split("/").pop() ?? "";
3333
- return sub.toUpperCase();
3334
- }
3335
- return "";
3336
- }
3337
3514
  const IMPORTED_HTML = {
3338
3515
  kind: "importedHtml",
3339
3516
  label: "HTML",
@@ -3368,7 +3545,11 @@ const UNKNOWN = {
3368
3545
  allowsColor: false,
3369
3546
  allowsFontSize: false,
3370
3547
  matches: () => null,
3371
- create: () => fillEmpty(document.createElement("p")),
3548
+ // `<div>` 로 만든다. unknown 은 미래 버전의 블록이 강등되어 오는 자리이고,
3549
+ // 그 html 에 블록 레벨 요소가 들어 있을 수 있다. `<p>` 로 감싸면 브라우저가
3550
+ // 단락을 조기에 닫아 내용이 래퍼 밖으로 튀어나간다 (문자열 렌더러
3551
+ // render-string.ts 도 같은 이유로 div 를 쓴다 — 두 경로가 일치해야 한다).
3552
+ create: () => fillEmpty(document.createElement("div")),
3372
3553
  isEmpty: (el) => isElementBlank(el)
3373
3554
  };
3374
3555
  const BLOCK_TYPES = [
@@ -3398,7 +3579,6 @@ function classifyBlockElement(el) {
3398
3579
  }
3399
3580
  return { type: UNKNOWN, match: { kind: "unknown", attrs: {} } };
3400
3581
  }
3401
- const BLOCK_ID_ATTR = "data-block-id";
3402
3582
  class BlockIdRegistry {
3403
3583
  constructor() {
3404
3584
  __publicField(this, "counter", 0);
@@ -3455,6 +3635,7 @@ function assignMissingIds(root, ids) {
3455
3635
  }
3456
3636
  }
3457
3637
  function normalizeContent(root) {
3638
+ restoreAlignFromClasses(root);
3458
3639
  for (const node of Array.from(root.childNodes)) {
3459
3640
  if (node.nodeType !== Node.TEXT_NODE) continue;
3460
3641
  const text = node.textContent ?? "";
@@ -3529,13 +3710,25 @@ function getBlockLabel(match) {
3529
3710
  }
3530
3711
  }
3531
3712
  function serializeBlock(block) {
3713
+ const style = block.element.getAttribute("style");
3532
3714
  return {
3715
+ version: SNAPSHOT_VERSION,
3533
3716
  id: block.id,
3534
3717
  kind: block.kind,
3535
3718
  attrs: { ...block.attrs },
3536
- html: block.element.innerHTML
3719
+ html: innerHtmlWithoutChrome(block.element),
3720
+ // 빈 문자열은 굳이 싣지 않는다 — 스냅샷이 커지기만 한다.
3721
+ ...style ? { style } : {}
3537
3722
  };
3538
3723
  }
3724
+ function innerHtmlWithoutChrome(element) {
3725
+ if (!element.querySelector(".aw-block-align-controls, .aw-resize-handle")) {
3726
+ return element.innerHTML;
3727
+ }
3728
+ const clone = element.cloneNode(true);
3729
+ stripEditorOnlyNodes(clone);
3730
+ return clone.innerHTML;
3731
+ }
3539
3732
  function serializeBlocks(blocks) {
3540
3733
  return blocks.map(serializeBlock);
3541
3734
  }
@@ -3543,6 +3736,7 @@ function renderBlock(snapshot) {
3543
3736
  const type = BLOCK_TYPE_BY_KIND[snapshot.kind] ?? BLOCK_TYPE_BY_KIND.unknown;
3544
3737
  const el = type.create(snapshot.attrs);
3545
3738
  el.setAttribute(BLOCK_ID_ATTR, snapshot.id);
3739
+ if (snapshot.style) el.setAttribute("style", snapshot.style);
3546
3740
  if (snapshot.html.length > 0) {
3547
3741
  el.innerHTML = snapshot.html;
3548
3742
  }
@@ -3550,7 +3744,7 @@ function renderBlock(snapshot) {
3550
3744
  }
3551
3745
  function renderBlocks(root, snapshots) {
3552
3746
  while (root.firstChild) root.removeChild(root.firstChild);
3553
- for (const snap of snapshots) {
3747
+ for (const snap of upgradeSnapshots(snapshots)) {
3554
3748
  root.appendChild(renderBlock(snap));
3555
3749
  }
3556
3750
  }
@@ -3886,6 +4080,57 @@ function parseStyleDeclarations(s) {
3886
4080
  }
3887
4081
  return out;
3888
4082
  }
4083
+ const ALIGN_CLASS_BY_VALUE = {
4084
+ left: "aw-align-left",
4085
+ center: "aw-align-center",
4086
+ right: "aw-align-right",
4087
+ justify: "aw-align-justify"
4088
+ };
4089
+ function applyBlockClassNames(root) {
4090
+ for (const child of Array.from(root.children)) {
4091
+ if (!(child instanceof HTMLElement)) continue;
4092
+ const { match } = classifyBlockElement(child);
4093
+ child.classList.add("aw-block", `aw-block-${match.kind}`);
4094
+ }
4095
+ for (const el of Array.from(root.querySelectorAll("[style]"))) {
4096
+ if (el.closest("[data-imported-html]")) continue;
4097
+ hoistTextAlignToClass(el);
4098
+ }
4099
+ }
4100
+ function hoistTextAlignToClass(el) {
4101
+ var _a;
4102
+ const value = el.style.textAlign.trim().toLowerCase();
4103
+ const className = ALIGN_CLASS_BY_VALUE[value];
4104
+ const stale = Object.values(ALIGN_CLASS_BY_VALUE).filter(
4105
+ (name) => name !== className && el.classList.contains(name)
4106
+ );
4107
+ for (const name of stale) el.classList.remove(name);
4108
+ if (!className) return;
4109
+ el.classList.add(className);
4110
+ el.style.removeProperty("text-align");
4111
+ if (((_a = el.getAttribute("style")) == null ? void 0 : _a.trim()) === "") el.removeAttribute("style");
4112
+ }
4113
+ function restoreAlignFromClasses(root) {
4114
+ var _a;
4115
+ for (const [value, className] of Object.entries(ALIGN_CLASS_BY_VALUE)) {
4116
+ for (const el of Array.from(
4117
+ root.querySelectorAll(`.${className}`)
4118
+ )) {
4119
+ if (!el.style.textAlign) el.style.textAlign = value;
4120
+ el.classList.remove(className);
4121
+ if (((_a = el.getAttribute("class")) == null ? void 0 : _a.trim()) === "") el.removeAttribute("class");
4122
+ }
4123
+ }
4124
+ }
4125
+ function stripEditorOnlyNodes(root) {
4126
+ const chrome = root.querySelectorAll(
4127
+ ".aw-block-align-controls, .aw-resize-handle"
4128
+ );
4129
+ for (const el of Array.from(chrome)) el.remove();
4130
+ for (const el of Array.from(root.querySelectorAll("[contenteditable]"))) {
4131
+ el.removeAttribute("contenteditable");
4132
+ }
4133
+ }
3889
4134
  function stripEditorOnlyAttrs(root) {
3890
4135
  for (const el of Array.from(root.querySelectorAll("[data-block-kind]"))) {
3891
4136
  el.removeAttribute("data-block-kind");
@@ -3980,6 +4225,7 @@ const BLOCK_TOGGLE_MAP = {
3980
4225
  codeBlock: "codeBlock"
3981
4226
  };
3982
4227
  const DRAG_MIME = "application/x-aeriz-block";
4228
+ const MIN_BLOCK_WIDTH = 80;
3983
4229
  class AerizEditor {
3984
4230
  constructor(options) {
3985
4231
  __publicField(this, "root");
@@ -4138,20 +4384,37 @@ class AerizEditor {
4138
4384
  this.sync();
4139
4385
  }
4140
4386
  /**
4141
- * 본문 HTML 을 반환한다. 외부 컨텍스트에서도 에디터에서 보이는 그대로
4142
- * 동일한 시각으로 렌더되도록, 호출 시점의 테마 변수(--aw-primary 등) 를
4143
- * 읽어 블록 단위 인라인 스타일에 베이크한다. 에디터 내부 DOM 자체는
4144
- * 변형되지 않으며(clone 트리에 적용), data-block-kind / data-block-label /
4145
- * data-block-focused 같은 에디터 전용 속성은 출력에서 제거된다.
4387
+ * 본문 HTML 을 반환한다.
4388
+ *
4389
+ * 두 가지 스타일 전략을 지원한다 (`options.styles`, 기본값은 생성 시
4390
+ * `EditorOptions.htmlStyles`, 그것도 없으면 `'inline'`).
4391
+ *
4392
+ * - `'inline'` — 호출 시점의 테마 변수(--aw-primary 등) 를 읽어 블록 단위
4393
+ * 인라인 스타일에 베이크한다. 스타일시트 없이 어디에 붙여도(이메일 본문,
4394
+ * 다른 페이지) 에디터에서 보이는 그대로 렌더된다.
4395
+ * - `'class'` — 테마 스타일을 베이크하지 않고 `aw-block-*` / `aw-align-*`
4396
+ * 클래스만 남긴다. `style` 속성을 지우는 서버 sanitizer 아래에서 서식을
4397
+ * 보존해야 할 때 쓴다. 출력은 `aeriz-wysiwyg.min.css` 를 로드한 페이지의
4398
+ * `.aeriz-wysiwyg > .aw-content` 안에 넣는 것을 전제로 한다.
4399
+ *
4400
+ * 어느 쪽이든 에디터 내부 DOM 은 변형되지 않으며(clone 트리에 적용),
4401
+ * data-block-kind / data-block-focused 같은 에디터 전용 속성과 리사이즈
4402
+ * 핸들 · 정렬 컨트롤 같은 편집 보조 마크업은 출력에서 제거된다.
4146
4403
  * data-block-id 는 보존.
4147
4404
  */
4148
- getHTML() {
4405
+ getHTML(options = {}) {
4149
4406
  const clone = this.content.cloneNode(true);
4150
- const theme = readBlockStyleTheme(this.root);
4151
- const mode = this.currentTheme();
4152
- if (mode) theme.mode = mode;
4153
- applyBlockInlineStyles(clone, theme);
4407
+ const styles = options.styles ?? this.options.htmlStyles ?? "inline";
4408
+ if (styles === "class") {
4409
+ applyBlockClassNames(clone);
4410
+ } else {
4411
+ const theme = readBlockStyleTheme(this.root);
4412
+ const mode = this.currentTheme();
4413
+ if (mode) theme.mode = mode;
4414
+ applyBlockInlineStyles(clone, theme);
4415
+ }
4154
4416
  stripEditorOnlyAttrs(clone);
4417
+ stripEditorOnlyNodes(clone);
4155
4418
  return clone.innerHTML;
4156
4419
  }
4157
4420
  /** 가공 없는 원본 innerHTML. 직렬화 검증/디버그 용도. */
@@ -4815,15 +5078,20 @@ class AerizEditor {
4815
5078
  * 이미지 / 유튜브 wrapper 의 너비를 드래그로 조정. 높이는 자동 (이미지는
4816
5079
  * 종횡비 유지, 유튜브는 CSS aspect-ratio 16/9). mouseup 에서 cursor 복귀
4817
5080
  * + flush.
5081
+ *
5082
+ * 너비는 본문 안쪽 폭을 상한으로 한다. 상한이 없으면 오른쪽으로 계속 끌 때
5083
+ * wrapper 가 본문보다 넓어지고, 그만큼 에디터 전체가 가로로 늘어난다
5084
+ * (드래그를 멈춰도 늘어난 상태가 남는다).
4818
5085
  */
4819
5086
  startBlockResize(block, startX) {
4820
5087
  const initialWidth = block.getBoundingClientRect().width;
5088
+ const maxWidth = this.contentInnerWidth();
4821
5089
  document.body.style.cursor = "nwse-resize";
4822
5090
  block.classList.add("aw-resizing");
4823
5091
  const onMove = (ev) => {
4824
5092
  const delta = ev.clientX - startX;
4825
- const newWidth = Math.max(80, initialWidth + delta);
4826
- block.style.width = `${Math.round(newWidth)}px`;
5093
+ const wanted = Math.max(MIN_BLOCK_WIDTH, initialWidth + delta);
5094
+ block.style.width = `${Math.round(Math.min(wanted, maxWidth))}px`;
4827
5095
  };
4828
5096
  const onUp = () => {
4829
5097
  document.removeEventListener("mousemove", onMove);
@@ -4835,6 +5103,15 @@ class AerizEditor {
4835
5103
  document.addEventListener("mousemove", onMove);
4836
5104
  document.addEventListener("mouseup", onUp);
4837
5105
  }
5106
+ /**
5107
+ * 본문의 padding 을 제외한 실제 컨텐츠 폭. 블록 리사이즈의 상한이다.
5108
+ * 에디터가 아주 좁을 때도 최소 폭은 확보되도록 하한을 둔다.
5109
+ */
5110
+ contentInnerWidth() {
5111
+ const cs = getComputedStyle(this.content);
5112
+ const inner = this.content.clientWidth - (Number.parseFloat(cs.paddingLeft) || 0) - (Number.parseFloat(cs.paddingRight) || 0);
5113
+ return Math.max(MIN_BLOCK_WIDTH, inner);
5114
+ }
4838
5115
  currentTheme() {
4839
5116
  const t = this.root.dataset.theme;
4840
5117
  return t === "dark" || t === "light" ? t : void 0;
@@ -5330,7 +5607,7 @@ class AerizEditor {
5330
5607
  positionHandle(handle, blockEl) {
5331
5608
  const blockRect = blockEl.getBoundingClientRect();
5332
5609
  const wrapRect = this.contentWrap.getBoundingClientRect();
5333
- const HANDLE_HEIGHT = 22;
5610
+ const HANDLE_HEIGHT = 18;
5334
5611
  const verticalShift = blockRect.height < HANDLE_HEIGHT ? (blockRect.height - HANDLE_HEIGHT) / 2 : 0;
5335
5612
  const top = blockRect.top - wrapRect.top + this.contentWrap.scrollTop + verticalShift;
5336
5613
  handle.style.top = `${top}px`;
@@ -5751,7 +6028,7 @@ function queryCommandStateSafe(command) {
5751
6028
  return false;
5752
6029
  }
5753
6030
  }
5754
- const BLOCK_LEVEL_TAGS = /* @__PURE__ */ new Set([
6031
+ const BLOCK_LEVEL_TAGS$1 = /* @__PURE__ */ new Set([
5755
6032
  "P",
5756
6033
  "DIV",
5757
6034
  "SECTION",
@@ -5784,7 +6061,7 @@ const BLOCK_LEVEL_TAGS = /* @__PURE__ */ new Set([
5784
6061
  "ADDRESS"
5785
6062
  ]);
5786
6063
  function isBlockLevelTag(tagName) {
5787
- return BLOCK_LEVEL_TAGS.has(tagName.toUpperCase());
6064
+ return BLOCK_LEVEL_TAGS$1.has(tagName.toUpperCase());
5788
6065
  }
5789
6066
  class AerizViewer {
5790
6067
  constructor(options) {
@@ -5792,10 +6069,17 @@ class AerizViewer {
5792
6069
  __publicField(this, "content");
5793
6070
  __publicField(this, "blockIds", new BlockIdRegistry());
5794
6071
  __publicField(this, "idFor", (el) => this.blockIds.ensure(el));
6072
+ /**
6073
+ * 생성 시 받은 options 객체를 그대로 들고 있는다. `htmlStyles` 를 여기서
6074
+ * 매번 다시 읽으므로, 호출자가 이 객체를 갱신하면 재생성 없이 출력 전략이
6075
+ * 바뀐다 (AerizEditor 와 같은 규약).
6076
+ */
6077
+ __publicField(this, "options");
5795
6078
  __publicField(this, "destroyed", false);
5796
6079
  if (!(options.element instanceof HTMLElement)) {
5797
6080
  throw new TypeError("AerizViewer: options.element must be an HTMLElement");
5798
6081
  }
6082
+ this.options = options;
5799
6083
  this.root = document.createElement("div");
5800
6084
  this.root.className = "aeriz-wysiwyg aeriz-wysiwyg-viewer";
5801
6085
  if (options.theme) {
@@ -5834,16 +6118,25 @@ class AerizViewer {
5834
6118
  return findBlockFrom(node, this.content, this.idFor);
5835
6119
  }
5836
6120
  /**
5837
- * 외부 컨텍스트(이메일, 다른 페이지의 HTML 영역 등) 에서도 동일한 시각으로
5838
- * 보이도록 portable 인라인 스타일을 입힌 자급자족적 HTML 을 반환한다.
6121
+ * 본문 HTML 을 반환한다. 전략은 `AerizEditor.getHTML()` 과 동일하다.
6122
+ *
6123
+ * - `'inline'` (기본) — 외부 컨텍스트(이메일, 다른 페이지의 HTML 영역 등)
6124
+ * 에서도 동일한 시각으로 보이도록 portable 인라인 스타일을 베이크한다.
6125
+ * - `'class'` — 클래스만 남긴다. `style` 속성을 지우는 sanitizer 대응.
5839
6126
  */
5840
- getHTML() {
6127
+ getHTML(options = {}) {
5841
6128
  const clone = this.content.cloneNode(true);
5842
- const theme = readBlockStyleTheme(this.root);
5843
- const mode = this.root.dataset.theme;
5844
- if (mode === "light" || mode === "dark") theme.mode = mode;
5845
- applyBlockInlineStyles(clone, theme);
6129
+ const styles = options.styles ?? this.options.htmlStyles ?? "inline";
6130
+ if (styles === "class") {
6131
+ applyBlockClassNames(clone);
6132
+ } else {
6133
+ const theme = readBlockStyleTheme(this.root);
6134
+ const mode = this.root.dataset.theme;
6135
+ if (mode === "light" || mode === "dark") theme.mode = mode;
6136
+ applyBlockInlineStyles(clone, theme);
6137
+ }
5846
6138
  stripEditorOnlyAttrs(clone);
6139
+ stripEditorOnlyNodes(clone);
5847
6140
  return clone.innerHTML;
5848
6141
  }
5849
6142
  setTheme(theme) {
@@ -5858,11 +6151,721 @@ class AerizViewer {
5858
6151
  return listBlocks(this.content, this.idFor);
5859
6152
  }
5860
6153
  }
6154
+ const VOID_TAGS = /* @__PURE__ */ new Set([
6155
+ "area",
6156
+ "base",
6157
+ "br",
6158
+ "col",
6159
+ "embed",
6160
+ "hr",
6161
+ "img",
6162
+ "input",
6163
+ "link",
6164
+ "meta",
6165
+ "param",
6166
+ "source",
6167
+ "track",
6168
+ "wbr"
6169
+ ]);
6170
+ const DROPPED_RAW_TAGS = /* @__PURE__ */ new Set(["script", "style"]);
6171
+ const IMPLIED_END = /* @__PURE__ */ new Map([
6172
+ ["li", /* @__PURE__ */ new Set(["li"])],
6173
+ ["p", /* @__PURE__ */ new Set(["p", "div", "ul", "ol", "blockquote", "pre", "table", "h1", "h2", "h3", "h4", "h5", "h6"])],
6174
+ ["td", /* @__PURE__ */ new Set(["td", "th", "tr"])],
6175
+ ["th", /* @__PURE__ */ new Set(["td", "th", "tr"])],
6176
+ ["tr", /* @__PURE__ */ new Set(["tr"])],
6177
+ ["option", /* @__PURE__ */ new Set(["option"])]
6178
+ ]);
6179
+ const NAMED_ENTITIES = {
6180
+ amp: "&",
6181
+ lt: "<",
6182
+ gt: ">",
6183
+ quot: '"',
6184
+ apos: "'",
6185
+ nbsp: " ",
6186
+ ensp: " ",
6187
+ emsp: " ",
6188
+ thinsp: " ",
6189
+ zwnj: "‌",
6190
+ zwj: "‍",
6191
+ hellip: "…",
6192
+ mdash: "—",
6193
+ ndash: "–",
6194
+ lsquo: "‘",
6195
+ rsquo: "’",
6196
+ ldquo: "“",
6197
+ rdquo: "”",
6198
+ middot: "·",
6199
+ bull: "•",
6200
+ copy: "©",
6201
+ reg: "®",
6202
+ trade: "™",
6203
+ deg: "°",
6204
+ plusmn: "±",
6205
+ times: "×",
6206
+ divide: "÷",
6207
+ laquo: "«",
6208
+ raquo: "»"
6209
+ };
6210
+ function decodeEntities(input) {
6211
+ if (!input.includes("&")) return input;
6212
+ return input.replace(
6213
+ /&(#[Xx][0-9A-Fa-f]+|#\d+|[A-Za-z][A-Za-z0-9]{1,31});/g,
6214
+ (whole, body) => {
6215
+ if (body.charCodeAt(0) === 35) {
6216
+ const isHex = body[1] === "x" || body[1] === "X";
6217
+ const digits = isHex ? body.slice(2) : body.slice(1);
6218
+ const code = Number.parseInt(digits, isHex ? 16 : 10);
6219
+ if (!Number.isFinite(code) || code < 0 || code > 1114111) return whole;
6220
+ if (code >= 55296 && code <= 57343) return whole;
6221
+ try {
6222
+ return String.fromCodePoint(code);
6223
+ } catch {
6224
+ return whole;
6225
+ }
6226
+ }
6227
+ const named = NAMED_ENTITIES[body.toLowerCase()];
6228
+ return named ?? whole;
6229
+ }
6230
+ );
6231
+ }
6232
+ function escapeHtml(input) {
6233
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6234
+ }
6235
+ function escapeAttr(input) {
6236
+ return escapeHtml(input).replace(/"/g, "&quot;");
6237
+ }
6238
+ function parseAttrs(source) {
6239
+ const attrs = {};
6240
+ const re = /([^\s/=>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
6241
+ let m;
6242
+ while ((m = re.exec(source)) !== null) {
6243
+ const name = m[1];
6244
+ if (!name) continue;
6245
+ const raw = m[2] ?? m[3] ?? m[4] ?? "";
6246
+ attrs[name.toLowerCase()] = decodeEntities(raw);
6247
+ }
6248
+ return attrs;
6249
+ }
6250
+ function parseHtmlFragment(html) {
6251
+ const root = [];
6252
+ const stack = [];
6253
+ const childrenOfTop = () => {
6254
+ const top = stack[stack.length - 1];
6255
+ return top ? top.children : root;
6256
+ };
6257
+ const pushText = (value) => {
6258
+ if (value.length === 0) return;
6259
+ const target = childrenOfTop();
6260
+ const last = target[target.length - 1];
6261
+ if (last && last.type === "text") last.value += value;
6262
+ else target.push({ type: "text", value });
6263
+ };
6264
+ const closeTag = (tag) => {
6265
+ var _a;
6266
+ for (let i = stack.length - 1; i >= 0; i--) {
6267
+ if (((_a = stack[i]) == null ? void 0 : _a.tag) === tag) {
6268
+ stack.length = i;
6269
+ return;
6270
+ }
6271
+ }
6272
+ };
6273
+ let cursor = 0;
6274
+ while (cursor < html.length) {
6275
+ const lt = html.indexOf("<", cursor);
6276
+ if (lt < 0) {
6277
+ pushText(decodeEntities(html.slice(cursor)));
6278
+ break;
6279
+ }
6280
+ if (lt > cursor) pushText(decodeEntities(html.slice(cursor, lt)));
6281
+ if (html.startsWith("<!--", lt)) {
6282
+ const end = html.indexOf("-->", lt + 4);
6283
+ cursor = end < 0 ? html.length : end + 3;
6284
+ continue;
6285
+ }
6286
+ if (html.startsWith("<!", lt) || html.startsWith("<?", lt)) {
6287
+ const end = html.indexOf(">", lt + 2);
6288
+ cursor = end < 0 ? html.length : end + 1;
6289
+ continue;
6290
+ }
6291
+ const gt = html.indexOf(">", lt + 1);
6292
+ if (gt < 0) {
6293
+ pushText(decodeEntities(html.slice(lt)));
6294
+ break;
6295
+ }
6296
+ const inner = html.slice(lt + 1, gt);
6297
+ cursor = gt + 1;
6298
+ if (inner.startsWith("/")) {
6299
+ const tag2 = inner.slice(1).trim().toLowerCase();
6300
+ if (tag2) closeTag(tag2);
6301
+ continue;
6302
+ }
6303
+ const nameMatch = /^([A-Za-z][^\s/>]*)/.exec(inner);
6304
+ if (!nameMatch || !nameMatch[1]) {
6305
+ pushText(decodeEntities(html.slice(lt, cursor)));
6306
+ continue;
6307
+ }
6308
+ const tag = nameMatch[1].toLowerCase();
6309
+ const rest = inner.slice(nameMatch[1].length);
6310
+ const selfClosing = /\/\s*$/.test(rest);
6311
+ const attrs = parseAttrs(selfClosing ? rest.replace(/\/\s*$/, "") : rest);
6312
+ if (DROPPED_RAW_TAGS.has(tag)) {
6313
+ const closeRe = new RegExp(`</${tag}\\s*>`, "i");
6314
+ const remainder = html.slice(cursor);
6315
+ const found = closeRe.exec(remainder);
6316
+ cursor = found ? cursor + found.index + found[0].length : html.length;
6317
+ continue;
6318
+ }
6319
+ const top = stack[stack.length - 1];
6320
+ if (top) {
6321
+ const closers = IMPLIED_END.get(top.tag);
6322
+ if (closers == null ? void 0 : closers.has(tag)) stack.pop();
6323
+ }
6324
+ const node = { type: "element", tag, attrs, children: [] };
6325
+ childrenOfTop().push(node);
6326
+ if (!selfClosing && !VOID_TAGS.has(tag)) stack.push(node);
6327
+ }
6328
+ return root;
6329
+ }
6330
+ function serializeNodes(nodes) {
6331
+ let out = "";
6332
+ for (const node of nodes) {
6333
+ if (node.type === "text") {
6334
+ out += escapeHtml(node.value);
6335
+ continue;
6336
+ }
6337
+ const attrs = Object.entries(node.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`).join("");
6338
+ if (VOID_TAGS.has(node.tag)) {
6339
+ out += `<${node.tag}${attrs}>`;
6340
+ continue;
6341
+ }
6342
+ out += `<${node.tag}${attrs}>${serializeNodes(node.children)}</${node.tag}>`;
6343
+ }
6344
+ return out;
6345
+ }
6346
+ function textOf(nodes) {
6347
+ let out = "";
6348
+ for (const node of nodes) {
6349
+ if (node.type === "text") {
6350
+ out += node.value;
6351
+ continue;
6352
+ }
6353
+ if (node.tag === "br") {
6354
+ out += "\n";
6355
+ continue;
6356
+ }
6357
+ out += textOf(node.children);
6358
+ }
6359
+ return out;
6360
+ }
6361
+ const BLOCK_LEVEL_TAGS = /* @__PURE__ */ new Set([
6362
+ "address",
6363
+ "article",
6364
+ "aside",
6365
+ "blockquote",
6366
+ "div",
6367
+ "dd",
6368
+ "dl",
6369
+ "dt",
6370
+ "fieldset",
6371
+ "figcaption",
6372
+ "figure",
6373
+ "footer",
6374
+ "form",
6375
+ "h1",
6376
+ "h2",
6377
+ "h3",
6378
+ "h4",
6379
+ "h5",
6380
+ "h6",
6381
+ "header",
6382
+ "hr",
6383
+ "li",
6384
+ "main",
6385
+ "nav",
6386
+ "ol",
6387
+ "p",
6388
+ "pre",
6389
+ "section",
6390
+ "table",
6391
+ "tbody",
6392
+ "td",
6393
+ "tfoot",
6394
+ "th",
6395
+ "thead",
6396
+ "tr",
6397
+ "ul"
6398
+ ]);
6399
+ function textWithBlockBreaks(nodes) {
6400
+ let out = "";
6401
+ for (const node of nodes) {
6402
+ if (node.type === "text") {
6403
+ out += node.value;
6404
+ continue;
6405
+ }
6406
+ if (node.tag === "br") {
6407
+ out += "\n";
6408
+ continue;
6409
+ }
6410
+ const isBlock = BLOCK_LEVEL_TAGS.has(node.tag);
6411
+ if (isBlock && out !== "" && !out.endsWith("\n")) out += "\n";
6412
+ out += textWithBlockBreaks(node.children);
6413
+ if (isBlock && !out.endsWith("\n")) out += "\n";
6414
+ }
6415
+ return out;
6416
+ }
6417
+ function childElements(nodes, tag) {
6418
+ return nodes.filter(
6419
+ (n) => n.type === "element" && n.tag === tag
6420
+ );
6421
+ }
6422
+ function renderBlocksToHTML(snapshots, options = {}) {
6423
+ const withIds = options.blockIds !== false;
6424
+ const body = upgradeSnapshots(snapshots).map((snap) => renderNormalizedBlock(snap, { blockIds: withIds })).join("");
6425
+ if (options.wrapper !== "content") return body;
6426
+ const themeAttr = options.theme ? ` data-theme="${options.theme}"` : "";
6427
+ return `<div class="aeriz-wysiwyg aeriz-wysiwyg-viewer"${themeAttr}><div class="aw-content-wrap"><div class="aw-content">${body}</div></div></div>`;
6428
+ }
6429
+ function renderBlockToHTML(snapshot, options = {}) {
6430
+ return renderNormalizedBlock(upgradeSnapshot(snapshot), options);
6431
+ }
6432
+ function renderNormalizedBlock(snapshot, options = {}) {
6433
+ const idAttr = options.blockIds === false ? "" : ` ${BLOCK_ID_ATTR}="${escapeAttr(snapshot.id)}"`;
6434
+ const inner = snapshot.html;
6435
+ const styleAttr = snapshot.style ? ` style="${escapeAttr(snapshot.style)}"` : "";
6436
+ switch (snapshot.kind) {
6437
+ case "paragraph":
6438
+ return `<p${idAttr}${styleAttr}>${inner || "<br>"}</p>`;
6439
+ case "unknown":
6440
+ return `<div${idAttr}${styleAttr}>${inner || "<br>"}</div>`;
6441
+ case "heading": {
6442
+ const level = clampHeadingLevel(snapshot.attrs.level);
6443
+ return `<h${level}${idAttr}${styleAttr}>${inner || "<br>"}</h${level}>`;
6444
+ }
6445
+ case "bulletList":
6446
+ return `<ul${idAttr}${styleAttr}>${inner || "<li><br></li>"}</ul>`;
6447
+ case "orderedList":
6448
+ return `<ol${idAttr}${styleAttr}>${inner || "<li><br></li>"}</ol>`;
6449
+ case "blockquote":
6450
+ return `<blockquote${idAttr}${styleAttr}>${inner || "<p><br></p>"}</blockquote>`;
6451
+ case "codeBlock": {
6452
+ if (inner) return `<pre${idAttr}${styleAttr}>${inner}</pre>`;
6453
+ const lang = snapshot.attrs.language;
6454
+ const langAttr = lang ? ` data-language="${escapeAttr(lang)}"` : "";
6455
+ return `<pre${idAttr}${styleAttr}><code${langAttr}></code></pre>`;
6456
+ }
6457
+ case "table":
6458
+ return `<table${idAttr}${styleAttr}>${inner || emptyTableBody(snapshot.attrs.rows, snapshot.attrs.cols)}</table>`;
6459
+ case "hr":
6460
+ return `<hr${idAttr}${styleAttr}>`;
6461
+ case "image":
6462
+ return inner ? `<figure${idAttr}${styleAttr}>${stripChrome(inner)}</figure>` : renderImage(snapshot.attrs, idAttr, styleAttr);
6463
+ case "youtube": {
6464
+ if (!inner) return renderYoutube(snapshot.attrs, idAttr, styleAttr);
6465
+ const videoId = snapshot.attrs.videoId;
6466
+ const idDataAttr = videoId ? ` data-youtube-id="${escapeAttr(videoId)}"` : "";
6467
+ return `<div class="aw-youtube-embed"${idDataAttr}${idAttr}${styleAttr}>${stripChrome(inner)}</div>`;
6468
+ }
6469
+ case "linkCard":
6470
+ return renderLinkCard(snapshot.attrs, idAttr, styleAttr, inner);
6471
+ case "file":
6472
+ return renderFile(snapshot.attrs, idAttr, styleAttr, inner);
6473
+ case "importedHtml": {
6474
+ const style = snapshot.attrs.importedStyle;
6475
+ const cls = snapshot.attrs.importedClass;
6476
+ const importedStyle = snapshot.style ? styleAttr : style ? ` style="${escapeAttr(style)}"` : "";
6477
+ const classAttr = cls ? ` class="${escapeAttr(cls)}"` : "";
6478
+ return `<div data-imported-html="true"${classAttr}${importedStyle}${idAttr}>${inner}</div>`;
6479
+ }
6480
+ default: {
6481
+ return "";
6482
+ }
6483
+ }
6484
+ }
6485
+ const CHROME_CLASS = /aw-block-align-controls|aw-resize-handle/;
6486
+ function stripChrome(html) {
6487
+ if (!CHROME_CLASS.test(html)) return html;
6488
+ const kept = parseHtmlFragment(html).filter(
6489
+ (node) => !(node.type === "element" && CHROME_CLASS.test(node.attrs["class"] ?? ""))
6490
+ );
6491
+ return serializeNodes(kept);
6492
+ }
6493
+ function clampHeadingLevel(level) {
6494
+ if (level === void 0) return 1;
6495
+ if (level < 1) return 1;
6496
+ if (level > 6) return 6;
6497
+ return level;
6498
+ }
6499
+ function emptyTableBody(rows, cols) {
6500
+ const r = Math.max(1, Number.isFinite(rows) ? rows : 1);
6501
+ const c = Math.max(1, Number.isFinite(cols) ? cols : 1);
6502
+ const cells = "<td><br></td>".repeat(c);
6503
+ return `<tbody>${`<tr>${cells}</tr>`.repeat(r)}</tbody>`;
6504
+ }
6505
+ function renderImage(attrs, idAttr, blockStyleAttr) {
6506
+ const styleAttr = blockStyleAttr || (attrs.wrapperStyle ? ` style="${escapeAttr(attrs.wrapperStyle)}"` : "");
6507
+ const srcAttr = attrs.src ? ` src="${escapeAttr(attrs.src)}"` : "";
6508
+ const altAttr = ` alt="${escapeAttr(attrs.alt ?? "")}"`;
6509
+ return `<figure${idAttr}${styleAttr}><img${srcAttr}${altAttr}></figure>`;
6510
+ }
6511
+ function renderYoutube(attrs, idAttr, blockStyleAttr) {
6512
+ const videoId = attrs.videoId ?? "";
6513
+ const styleAttr = blockStyleAttr || (attrs.wrapperStyle ? ` style="${escapeAttr(attrs.wrapperStyle)}"` : "");
6514
+ const idDataAttr = videoId ? ` data-youtube-id="${escapeAttr(videoId)}"` : "";
6515
+ const srcAttr = videoId ? ` src="${escapeAttr(`${YOUTUBE_EMBED_BASE}${videoId}`)}"` : "";
6516
+ return `<div class="aw-youtube-embed"${idDataAttr}${idAttr}${styleAttr}><iframe${srcAttr} allow="${escapeAttr(YOUTUBE_IFRAME_ALLOW)}" allowfullscreen frameborder="0" referrerpolicy="strict-origin-when-cross-origin"></iframe></div>`;
6517
+ }
6518
+ function renderLinkCard(attrs, idAttr, styleAttr, inner) {
6519
+ const href = attrs.href ?? "#";
6520
+ const title = attrs.text && attrs.text.length > 0 ? attrs.text : hostnameFromUrl(href) || href;
6521
+ const description = attrs.description ?? "";
6522
+ const image = attrs.image ?? "";
6523
+ const favicon = attrs.favicon ?? defaultFaviconUrl(href);
6524
+ const classes = image ? "aw-link-card aw-link-card-with-image" : "aw-link-card";
6525
+ const open = `<a class="${classes}" href="${escapeAttr(href)}" target="_blank" rel="noopener"${idAttr}${styleAttr}>`;
6526
+ if (inner) return `${open}${stripChrome(inner)}</a>`;
6527
+ let out = open;
6528
+ if (image) {
6529
+ out += `<img class="aw-link-card-image" src="${escapeAttr(image)}" alt="" loading="lazy">`;
6530
+ }
6531
+ out += '<span class="aw-link-card-body">';
6532
+ out += `<span class="aw-link-card-title">${escapeHtml(title)}</span>`;
6533
+ if (description) {
6534
+ out += `<span class="aw-link-card-desc">${escapeHtml(description)}</span>`;
6535
+ }
6536
+ out += '<span class="aw-link-card-url">';
6537
+ if (favicon) {
6538
+ out += `<img class="aw-link-card-favicon" src="${escapeAttr(favicon)}" alt="" loading="lazy">`;
6539
+ }
6540
+ out += `<span class="aw-link-card-url-text">${escapeHtml(href)}</span>`;
6541
+ out += "</span></span></a>";
6542
+ return out;
6543
+ }
6544
+ function renderFile(attrs, idAttr, styleAttr, inner) {
6545
+ const hrefAttr = attrs.fileSrc ? ` href="${escapeAttr(attrs.fileSrc)}"` : "";
6546
+ const nameAttr = attrs.fileName ? ` data-file-name="${escapeAttr(attrs.fileName)}"` : "";
6547
+ const sizeAttr = attrs.fileSize !== void 0 ? ` data-file-size="${escapeAttr(String(attrs.fileSize))}"` : "";
6548
+ const typeAttr = attrs.fileType ? ` data-file-type="${escapeAttr(attrs.fileType)}"` : "";
6549
+ const downloadAttr = ` download="${escapeAttr(attrs.fileName ?? "")}"`;
6550
+ const infoParts = [];
6551
+ const ext = fileExtensionLabel(attrs.fileName, attrs.fileType);
6552
+ if (ext) infoParts.push(ext);
6553
+ if (attrs.fileSize !== void 0) infoParts.push(formatBytes(attrs.fileSize));
6554
+ const open = `<a class="aw-file-attachment" target="_blank" rel="noopener"${hrefAttr}${downloadAttr}${nameAttr}${sizeAttr}${typeAttr}${idAttr}${styleAttr}>`;
6555
+ if (inner) return `${open}${stripChrome(inner)}</a>`;
6556
+ let out = open;
6557
+ out += `<span class="aw-file-icon" aria-hidden="true">${FILE_ICON_SVG}</span>`;
6558
+ out += '<span class="aw-file-meta">';
6559
+ out += `<span class="aw-file-name">${escapeHtml(attrs.fileName ?? FILE_FALLBACK_NAME)}</span>`;
6560
+ if (infoParts.length > 0) {
6561
+ out += `<span class="aw-file-info">${escapeHtml(infoParts.join(" · "))}</span>`;
6562
+ }
6563
+ out += "</span>";
6564
+ out += `<span class="aw-file-action" aria-hidden="true">${FILE_ACTION_SVG}</span>`;
6565
+ out += "</a>";
6566
+ return out;
6567
+ }
6568
+ const INLINE_ESCAPE = /([\\`*_[\]<])/g;
6569
+ const LINE_START_ESCAPE = /^([#>|+-]|\d+\.)/;
6570
+ function blocksToMarkdown(snapshots, options = {}) {
6571
+ const bullet = options.bullet ?? "-";
6572
+ const gfmTables = options.gfmTables !== false;
6573
+ const chunks = [];
6574
+ for (const snap of upgradeSnapshots(snapshots)) {
6575
+ const chunk = blockToMarkdown(snap, bullet, gfmTables);
6576
+ if (chunk !== "") chunks.push(chunk);
6577
+ }
6578
+ return chunks.length > 0 ? `${chunks.join("\n\n")}
6579
+ ` : "";
6580
+ }
6581
+ function blockToMarkdown(snapshot, bullet, gfmTables) {
6582
+ var _a, _b, _c;
6583
+ const nodes = parseHtmlFragment(snapshot.html);
6584
+ switch (snapshot.kind) {
6585
+ case "paragraph":
6586
+ case "unknown": {
6587
+ const text = inlineToMarkdown(nodes).trim();
6588
+ return text === "" ? "" : escapeLineStart(text);
6589
+ }
6590
+ case "heading": {
6591
+ const level = Math.min(6, Math.max(1, snapshot.attrs.level ?? 1));
6592
+ const text = inlineToMarkdown(nodes).trim();
6593
+ return text === "" ? "" : `${"#".repeat(level)} ${text}`;
6594
+ }
6595
+ case "bulletList":
6596
+ return listToMarkdown(nodes, { ordered: false, bullet, depth: 0 });
6597
+ case "orderedList":
6598
+ return listToMarkdown(nodes, { ordered: true, bullet, depth: 0 });
6599
+ case "blockquote": {
6600
+ const paragraphs = childElements(nodes, "p");
6601
+ const source = paragraphs.length > 0 ? paragraphs.map((p) => inlineToMarkdown(p.children).trim()) : [inlineToMarkdown(nodes).trim()];
6602
+ const lines = source.filter((line) => line !== "").join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`);
6603
+ return lines.length > 0 && lines[0] !== void 0 ? lines.join("\n") : "";
6604
+ }
6605
+ case "codeBlock": {
6606
+ const code = childElements(nodes, "code")[0];
6607
+ const body = textOf(code ? code.children : nodes).replace(/\n$/, "");
6608
+ const lang = snapshot.attrs.language ?? (code == null ? void 0 : code.attrs["data-language"]) ?? "";
6609
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(body) + 1));
6610
+ return `${fence}${lang}
6611
+ ${body}
6612
+ ${fence}`;
6613
+ }
6614
+ case "image": {
6615
+ const src = snapshot.attrs.src ?? "";
6616
+ if (src === "") return "";
6617
+ return `![${escapeInline(snapshot.attrs.alt ?? "")}](${encodeUrl(src)})`;
6618
+ }
6619
+ case "table":
6620
+ return gfmTables ? tableToMarkdown(nodes) : `<table>${snapshot.html}</table>`;
6621
+ case "hr":
6622
+ return "---";
6623
+ case "linkCard": {
6624
+ const href = snapshot.attrs.href ?? "";
6625
+ if (href === "") return "";
6626
+ const label = ((_a = snapshot.attrs.text) == null ? void 0 : _a.trim()) || href;
6627
+ const line = `[${escapeInline(label)}](${encodeUrl(href)})`;
6628
+ const desc = (_b = snapshot.attrs.description) == null ? void 0 : _b.trim();
6629
+ return desc ? `${line}
6630
+
6631
+ ${escapeLineStart(escapeInline(desc))}` : line;
6632
+ }
6633
+ case "youtube": {
6634
+ const videoId = snapshot.attrs.videoId;
6635
+ if (!videoId) return "";
6636
+ return `[YouTube](${YOUTUBE_EMBED_BASE}${encodeURIComponent(videoId)})`;
6637
+ }
6638
+ case "file": {
6639
+ const src = snapshot.attrs.fileSrc ?? "";
6640
+ if (src === "") return "";
6641
+ const name = ((_c = snapshot.attrs.fileName) == null ? void 0 : _c.trim()) || src;
6642
+ return `[${escapeInline(name)}](${encodeUrl(src)})`;
6643
+ }
6644
+ case "importedHtml":
6645
+ return snapshot.html.trim();
6646
+ default: {
6647
+ return "";
6648
+ }
6649
+ }
6650
+ }
6651
+ function listToMarkdown(nodes, ctx) {
6652
+ const items = childElements(nodes, "li");
6653
+ if (items.length === 0) return "";
6654
+ const indent = " ".repeat(ctx.depth);
6655
+ const lines = [];
6656
+ items.forEach((li, index) => {
6657
+ const inline = [];
6658
+ const nested = [];
6659
+ for (const child of li.children) {
6660
+ if (child.type === "element" && (child.tag === "ul" || child.tag === "ol")) {
6661
+ nested.push(child);
6662
+ } else {
6663
+ inline.push(child);
6664
+ }
6665
+ }
6666
+ const marker = ctx.ordered ? `${index + 1}.` : ctx.bullet;
6667
+ const text = inlineToMarkdown(inline).trim();
6668
+ const continuation = `${indent}${" ".repeat(marker.length + 1)}`;
6669
+ const body = text.split("\n").join(`
6670
+ ${continuation}`);
6671
+ lines.push(`${indent}${marker} ${body}`);
6672
+ for (const sub of nested) {
6673
+ const subMd = listToMarkdown(sub.children, {
6674
+ ordered: sub.tag === "ol",
6675
+ bullet: ctx.bullet,
6676
+ depth: ctx.depth + 1
6677
+ });
6678
+ if (subMd !== "") lines.push(subMd);
6679
+ }
6680
+ });
6681
+ return lines.join("\n");
6682
+ }
6683
+ function tableToMarkdown(nodes) {
6684
+ const rows = [];
6685
+ const collectRows = (list) => {
6686
+ for (const node of list) {
6687
+ if (node.type !== "element") continue;
6688
+ if (node.tag === "tr") {
6689
+ rows.push(node);
6690
+ continue;
6691
+ }
6692
+ if (node.tag === "tbody" || node.tag === "thead" || node.tag === "tfoot") {
6693
+ collectRows(node.children);
6694
+ }
6695
+ }
6696
+ };
6697
+ collectRows(nodes);
6698
+ if (rows.length === 0) return "";
6699
+ const grid = rows.map(
6700
+ (tr) => tr.children.filter(
6701
+ (c) => c.type === "element" && (c.tag === "td" || c.tag === "th")
6702
+ ).map((cell) => cellToMarkdown(cell.children))
6703
+ );
6704
+ const width = grid.reduce((max, row) => Math.max(max, row.length), 0);
6705
+ if (width === 0) return "";
6706
+ const pad = (row) => row.length === width ? row : [...row, ...Array(width - row.length).fill("")];
6707
+ const [head, ...body] = grid.map(pad);
6708
+ const headRow = head ?? Array(width).fill("");
6709
+ const lines = [
6710
+ `| ${headRow.join(" | ")} |`,
6711
+ `| ${Array(width).fill("---").join(" | ")} |`,
6712
+ ...body.map((row) => `| ${row.join(" | ")} |`)
6713
+ ];
6714
+ return lines.join("\n");
6715
+ }
6716
+ function cellToMarkdown(nodes) {
6717
+ return inlineToMarkdown(nodes, { inTableCell: true }).replace(/\n+/g, "<br>").replace(/\|/g, "\\|").trim();
6718
+ }
6719
+ function inlineToMarkdown(nodes, ctx = {}) {
6720
+ let out = "";
6721
+ for (const node of nodes) {
6722
+ if (node.type === "text") {
6723
+ out += escapeInline(node.value);
6724
+ continue;
6725
+ }
6726
+ const children = () => inlineToMarkdown(node.children, ctx);
6727
+ switch (node.tag) {
6728
+ case "br":
6729
+ out += ctx.inTableCell ? "\n" : " \n";
6730
+ break;
6731
+ case "b":
6732
+ case "strong": {
6733
+ const inner = children();
6734
+ out += inner.trim() === "" ? inner : `**${inner}**`;
6735
+ break;
6736
+ }
6737
+ case "i":
6738
+ case "em": {
6739
+ const inner = children();
6740
+ out += inner.trim() === "" ? inner : `*${inner}*`;
6741
+ break;
6742
+ }
6743
+ case "s":
6744
+ case "del":
6745
+ case "strike": {
6746
+ const inner = children();
6747
+ out += inner.trim() === "" ? inner : `~~${inner}~~`;
6748
+ break;
6749
+ }
6750
+ case "code": {
6751
+ const raw = textOf(node.children);
6752
+ const fence = "`".repeat(longestBacktickRun(raw) + 1);
6753
+ const padding = raw.startsWith("`") || raw.endsWith("`") ? " " : "";
6754
+ out += `${fence}${padding}${raw}${padding}${fence}`;
6755
+ break;
6756
+ }
6757
+ case "a": {
6758
+ const href = node.attrs["href"];
6759
+ const inner = children();
6760
+ out += href ? `[${inner}](${encodeUrl(href)})` : inner;
6761
+ break;
6762
+ }
6763
+ case "img": {
6764
+ const src = node.attrs["src"];
6765
+ if (src) out += `![${escapeInline(node.attrs["alt"] ?? "")}](${encodeUrl(src)})`;
6766
+ break;
6767
+ }
6768
+ case "u":
6769
+ case "sub":
6770
+ case "sup":
6771
+ case "mark":
6772
+ out += `<${node.tag}>${children()}</${node.tag}>`;
6773
+ break;
6774
+ default:
6775
+ out += children();
6776
+ break;
6777
+ }
6778
+ }
6779
+ return out;
6780
+ }
6781
+ function escapeInline(text) {
6782
+ return text.replace(INLINE_ESCAPE, "\\$1");
6783
+ }
6784
+ function escapeLineStart(text) {
6785
+ return text.replace(LINE_START_ESCAPE, "\\$1");
6786
+ }
6787
+ function longestBacktickRun(text) {
6788
+ let longest = 0;
6789
+ let current = 0;
6790
+ for (const ch of text) {
6791
+ if (ch === "`") {
6792
+ current += 1;
6793
+ if (current > longest) longest = current;
6794
+ } else {
6795
+ current = 0;
6796
+ }
6797
+ }
6798
+ return longest;
6799
+ }
6800
+ function encodeUrl(url) {
6801
+ const trimmed = url.trim();
6802
+ if (/[\s()]/.test(trimmed)) {
6803
+ return `<${trimmed.replace(/[<>]/g, "")}>`;
6804
+ }
6805
+ return trimmed;
6806
+ }
6807
+ function blocksToPlainText(snapshots, options = {}) {
6808
+ const blockSeparator = options.blockSeparator ?? "\n\n";
6809
+ const cellSeparator = options.cellSeparator ?? " ";
6810
+ const chunks = [];
6811
+ for (const snap of upgradeSnapshots(snapshots)) {
6812
+ const text = blockToPlainText(snap, cellSeparator);
6813
+ if (text !== "") chunks.push(text);
6814
+ }
6815
+ return chunks.join(blockSeparator);
6816
+ }
6817
+ function blockToPlainText(snapshot, cellSeparator) {
6818
+ switch (snapshot.kind) {
6819
+ case "hr":
6820
+ case "youtube":
6821
+ return "";
6822
+ case "image":
6823
+ return normalize(snapshot.attrs.alt ?? "");
6824
+ case "linkCard":
6825
+ return normalize(snapshot.attrs.text ?? snapshot.attrs.href ?? "");
6826
+ case "file":
6827
+ return normalize(snapshot.attrs.fileName ?? "");
6828
+ case "bulletList":
6829
+ case "orderedList": {
6830
+ const items = childElements(parseHtmlFragment(snapshot.html), "li");
6831
+ return items.map((li) => normalize(textWithBlockBreaks(li.children))).filter((line) => line !== "").join("\n");
6832
+ }
6833
+ case "table":
6834
+ return tableToPlainText(parseHtmlFragment(snapshot.html), cellSeparator);
6835
+ default:
6836
+ return normalize(textWithBlockBreaks(parseHtmlFragment(snapshot.html)));
6837
+ }
6838
+ }
6839
+ function tableToPlainText(nodes, cellSeparator) {
6840
+ const rows = [];
6841
+ const collect = (list) => {
6842
+ for (const node of list) {
6843
+ if (node.type !== "element") continue;
6844
+ if (node.tag === "tr") rows.push(node);
6845
+ else if (["tbody", "thead", "tfoot"].includes(node.tag)) {
6846
+ collect(node.children);
6847
+ }
6848
+ }
6849
+ };
6850
+ collect(nodes);
6851
+ return rows.map(
6852
+ (tr) => tr.children.filter(
6853
+ (c) => c.type === "element" && (c.tag === "td" || c.tag === "th")
6854
+ ).map((cell) => normalize(textWithBlockBreaks(cell.children)).replace(/\n+/g, " ")).join(cellSeparator)
6855
+ ).filter((line) => line.replace(new RegExp(escapeRegExp(cellSeparator), "g"), "").trim() !== "").join("\n");
6856
+ }
6857
+ function normalize(text) {
6858
+ return text.replace(/[​-‍]/g, "").split("\n").map((line) => line.replace(/[^\S\n]+/g, " ").trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
6859
+ }
6860
+ function escapeRegExp(input) {
6861
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6862
+ }
5861
6863
  if (typeof globalThis.AerizWysiwyg === "object") {
5862
6864
  globalThis.AerizEditor = globalThis.AerizWysiwyg.AerizEditor;
5863
6865
  globalThis.AerizViewer = globalThis.AerizWysiwyg.AerizViewer;
5864
6866
  }
5865
6867
  export {
6868
+ ALIGN_CLASS_BY_VALUE,
5866
6869
  ALL_INLINE_MARKS,
5867
6870
  AerizEditor,
5868
6871
  AerizViewer,
@@ -5871,9 +6874,16 @@ export {
5871
6874
  BLOCK_TYPE_BY_KIND,
5872
6875
  BlockIdRegistry,
5873
6876
  DEFAULT_TOOLBAR,
6877
+ SNAPSHOT_VERSION,
6878
+ applyBlockClassNames,
5874
6879
  applyBlockInlineStyles,
5875
6880
  assignMissingIds,
6881
+ blocksToMarkdown,
6882
+ blocksToPlainText,
5876
6883
  classifyBlockElement,
6884
+ decodeEntities,
6885
+ escapeAttr,
6886
+ escapeHtml,
5877
6887
  extractInlineContent,
5878
6888
  fillInlineContent,
5879
6889
  findBlockFrom,
@@ -5881,11 +6891,22 @@ export {
5881
6891
  isElementBlank,
5882
6892
  listBlocks,
5883
6893
  normalizeContent,
6894
+ parseHtmlFragment,
5884
6895
  placeCaretAtStart,
5885
6896
  readBlockStyleTheme,
5886
6897
  renderBlock,
6898
+ renderBlockToHTML,
5887
6899
  renderBlocks,
6900
+ renderBlocksToHTML,
6901
+ restoreAlignFromClasses,
5888
6902
  serializeBlock,
5889
6903
  serializeBlocks,
5890
- stripEditorOnlyAttrs
6904
+ serializeNodes,
6905
+ snapshotVersionOf,
6906
+ stripEditorOnlyAttrs,
6907
+ stripEditorOnlyNodes,
6908
+ textOf,
6909
+ textWithBlockBreaks,
6910
+ upgradeSnapshot,
6911
+ upgradeSnapshots
5891
6912
  };