@particle-academy/fancy-code 0.10.0 → 0.11.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.d.cts CHANGED
@@ -228,21 +228,30 @@ interface MarkdownEditorProps {
228
228
  wordWrap?: boolean;
229
229
  /** Placeholder shown when empty. */
230
230
  placeholder?: string;
231
- /** Editor height bounds (px). */
231
+ /**
232
+ * Height bounds (px). `maxHeight` constrains the PREVIEW pane as well as the
233
+ * editor — it previously reached only the editor, so a long document rendered
234
+ * at full height instead of scrolling.
235
+ */
232
236
  minHeight?: number;
233
237
  maxHeight?: number;
234
238
  className?: string;
235
239
  /**
236
- * Swap the markdown → HTML renderer used by the preview pane (e.g. a full
237
- * CommonMark library). Defaults to the built-in lightweight renderer.
240
+ * Swap the markdown → HTML renderer used by the preview pane.
241
+ *
242
+ * Rarely needed now: the preview renders through react-fancy's
243
+ * `<ContentRenderer format="markdown">`, which is a full CommonMark + GFM
244
+ * parse. Supply this only to impose your own pipeline — the returned HTML is
245
+ * injected as-is, so it must already be sanitised.
238
246
  */
239
247
  renderPreview?: (markdown: string) => string;
240
248
  }
241
249
  /**
242
250
  * A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
243
251
  * `markdown` language) with an optional live preview pane. Controlled via
244
- * `value` + `onValueChange`. The preview renders with a small built-in
245
- * dependency-free renderer; pass `renderPreview` to use your own.
252
+ * `value` + `onValueChange`. The preview renders through react-fancy's
253
+ * `<ContentRenderer format="markdown">` — a full CommonMark + GFM parse,
254
+ * sanitised by default. Pass `renderPreview` to impose your own pipeline.
246
255
  *
247
256
  * ```tsx
248
257
  * <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
package/dist/index.d.ts CHANGED
@@ -228,21 +228,30 @@ interface MarkdownEditorProps {
228
228
  wordWrap?: boolean;
229
229
  /** Placeholder shown when empty. */
230
230
  placeholder?: string;
231
- /** Editor height bounds (px). */
231
+ /**
232
+ * Height bounds (px). `maxHeight` constrains the PREVIEW pane as well as the
233
+ * editor — it previously reached only the editor, so a long document rendered
234
+ * at full height instead of scrolling.
235
+ */
232
236
  minHeight?: number;
233
237
  maxHeight?: number;
234
238
  className?: string;
235
239
  /**
236
- * Swap the markdown → HTML renderer used by the preview pane (e.g. a full
237
- * CommonMark library). Defaults to the built-in lightweight renderer.
240
+ * Swap the markdown → HTML renderer used by the preview pane.
241
+ *
242
+ * Rarely needed now: the preview renders through react-fancy's
243
+ * `<ContentRenderer format="markdown">`, which is a full CommonMark + GFM
244
+ * parse. Supply this only to impose your own pipeline — the returned HTML is
245
+ * injected as-is, so it must already be sanitised.
238
246
  */
239
247
  renderPreview?: (markdown: string) => string;
240
248
  }
241
249
  /**
242
250
  * A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
243
251
  * `markdown` language) with an optional live preview pane. Controlled via
244
- * `value` + `onValueChange`. The preview renders with a small built-in
245
- * dependency-free renderer; pass `renderPreview` to use your own.
252
+ * `value` + `onValueChange`. The preview renders through react-fancy's
253
+ * `<ContentRenderer format="markdown">` — a full CommonMark + GFM parse,
254
+ * sanitised by default. Pass `renderPreview` to impose your own pipeline.
246
255
  *
247
256
  * ```tsx
248
257
  * <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useMemo, useState, useCallback, useRef, useEffect, useSyncExternalStore, useContext } from 'react';
2
- import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
2
+ import { cn, useControllableState, ContentRenderer, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
3
3
  import { annotateLines, highlightCode, tok, tokenizeHtml, LIGHT_COLORS, DARK_COLORS, languageFromFilename } from '@particle-academy/fancy-file-commons';
4
4
  export { languageFromFilename } from '@particle-academy/fancy-file-commons';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -1634,105 +1634,6 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1634
1634
  Panel: CodeEditorPanel,
1635
1635
  StatusBar: CodeEditorStatusBar
1636
1636
  });
1637
-
1638
- // src/engine/markdown-render.ts
1639
- function escapeHtml(s) {
1640
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1641
- }
1642
- function escapeAttr(s) {
1643
- return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1644
- }
1645
- function inline(text) {
1646
- return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"${title ? ` title="${escapeAttr(title)}"` : ""}>`).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => `<a href="${escapeAttr(href)}"${title ? ` title="${escapeAttr(title)}"` : ""} rel="noopener">${t}</a>`).replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`).replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "<strong>$2</strong>").replace(/(\*|_)(?=\S)([\s\S]*?\S)\1/g, "<em>$2</em>");
1647
- }
1648
- function renderMarkdown(markdown) {
1649
- const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1650
- const out = [];
1651
- let i = 0;
1652
- let para = [];
1653
- let listType = null;
1654
- const flushPara = () => {
1655
- if (para.length) {
1656
- out.push(`<p>${inline(para.join(" "))}</p>`);
1657
- para = [];
1658
- }
1659
- };
1660
- const closeList = () => {
1661
- if (listType) {
1662
- out.push(`</${listType}>`);
1663
- listType = null;
1664
- }
1665
- };
1666
- while (i < lines.length) {
1667
- const line = lines[i];
1668
- const fence = line.match(/^\s*(```|~~~)(.*)$/);
1669
- if (fence) {
1670
- flushPara();
1671
- closeList();
1672
- const lang = fence[2].trim();
1673
- const body = [];
1674
- i++;
1675
- while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
1676
- body.push(lines[i]);
1677
- i++;
1678
- }
1679
- i++;
1680
- out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
1681
- continue;
1682
- }
1683
- const esc = escapeHtml(line);
1684
- if (/^\s*$/.test(line)) {
1685
- flushPara();
1686
- closeList();
1687
- i++;
1688
- continue;
1689
- }
1690
- const heading = esc.match(/^(#{1,6})\s+(.*)$/);
1691
- if (heading) {
1692
- flushPara();
1693
- closeList();
1694
- const level = heading[1].length;
1695
- out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1696
- i++;
1697
- continue;
1698
- }
1699
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
1700
- flushPara();
1701
- closeList();
1702
- out.push("<hr>");
1703
- i++;
1704
- continue;
1705
- }
1706
- const quote = esc.match(/^\s*&gt;\s?(.*)$/);
1707
- if (quote) {
1708
- flushPara();
1709
- closeList();
1710
- out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
1711
- i++;
1712
- continue;
1713
- }
1714
- const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
1715
- const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
1716
- if (ol || ul) {
1717
- flushPara();
1718
- const type = ol ? "ol" : "ul";
1719
- if (listType !== type) {
1720
- closeList();
1721
- out.push(`<${type}>`);
1722
- listType = type;
1723
- }
1724
- out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
1725
- i++;
1726
- continue;
1727
- }
1728
- closeList();
1729
- para.push(esc);
1730
- i++;
1731
- }
1732
- flushPara();
1733
- closeList();
1734
- return out.join("\n");
1735
- }
1736
1637
  function MarkdownEditor({
1737
1638
  value,
1738
1639
  defaultValue = "",
@@ -1746,7 +1647,7 @@ function MarkdownEditor({
1746
1647
  minHeight,
1747
1648
  maxHeight,
1748
1649
  className,
1749
- renderPreview = renderMarkdown
1650
+ renderPreview
1750
1651
  }) {
1751
1652
  const [content, setContent] = useControllableState(value, defaultValue, onValueChange);
1752
1653
  const showEditor = mode !== "preview";
@@ -1779,7 +1680,19 @@ function MarkdownEditor({
1779
1680
  {
1780
1681
  "data-fancy-markdown-preview": "",
1781
1682
  className: "fancy-md-preview",
1782
- dangerouslySetInnerHTML: { __html: renderPreview(content) }
1683
+ style: maxHeight ? { maxHeight, overflowY: "auto" } : void 0,
1684
+ children: renderPreview ? (
1685
+ // The host imposed its own pipeline; its HTML is injected as-is and
1686
+ // is its own responsibility to sanitise.
1687
+ /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderPreview(content) } })
1688
+ ) : (
1689
+ // The kit's own renderer — a full CommonMark + GFM parse that
1690
+ // sanitises by default. `marked` already sits in this tree as a
1691
+ // react-fancy runtime dependency, so the built-in renderer this
1692
+ // replaced was a second, weaker markdown implementation next to a
1693
+ // complete one.
1694
+ /* @__PURE__ */ jsx(ContentRenderer, { value: content, format: "markdown" })
1695
+ )
1783
1696
  }
1784
1697
  )
1785
1698
  ]
@@ -1861,6 +1774,105 @@ function FileViewer({
1861
1774
  );
1862
1775
  }
1863
1776
  FileViewer.displayName = "FileViewer";
1777
+
1778
+ // src/engine/markdown-render.ts
1779
+ function escapeHtml(s) {
1780
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1781
+ }
1782
+ function escapeAttr(s) {
1783
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1784
+ }
1785
+ function inline(text) {
1786
+ return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"${title ? ` title="${escapeAttr(title)}"` : ""}>`).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => `<a href="${escapeAttr(href)}"${title ? ` title="${escapeAttr(title)}"` : ""} rel="noopener">${t}</a>`).replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`).replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "<strong>$2</strong>").replace(/(\*|_)(?=\S)([\s\S]*?\S)\1/g, "<em>$2</em>");
1787
+ }
1788
+ function renderMarkdown(markdown) {
1789
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1790
+ const out = [];
1791
+ let i = 0;
1792
+ let para = [];
1793
+ let listType = null;
1794
+ const flushPara = () => {
1795
+ if (para.length) {
1796
+ out.push(`<p>${inline(para.join(" "))}</p>`);
1797
+ para = [];
1798
+ }
1799
+ };
1800
+ const closeList = () => {
1801
+ if (listType) {
1802
+ out.push(`</${listType}>`);
1803
+ listType = null;
1804
+ }
1805
+ };
1806
+ while (i < lines.length) {
1807
+ const line = lines[i];
1808
+ const fence = line.match(/^\s*(```|~~~)(.*)$/);
1809
+ if (fence) {
1810
+ flushPara();
1811
+ closeList();
1812
+ const lang = fence[2].trim();
1813
+ const body = [];
1814
+ i++;
1815
+ while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
1816
+ body.push(lines[i]);
1817
+ i++;
1818
+ }
1819
+ i++;
1820
+ out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
1821
+ continue;
1822
+ }
1823
+ const esc = escapeHtml(line);
1824
+ if (/^\s*$/.test(line)) {
1825
+ flushPara();
1826
+ closeList();
1827
+ i++;
1828
+ continue;
1829
+ }
1830
+ const heading = esc.match(/^(#{1,6})\s+(.*)$/);
1831
+ if (heading) {
1832
+ flushPara();
1833
+ closeList();
1834
+ const level = heading[1].length;
1835
+ out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1836
+ i++;
1837
+ continue;
1838
+ }
1839
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
1840
+ flushPara();
1841
+ closeList();
1842
+ out.push("<hr>");
1843
+ i++;
1844
+ continue;
1845
+ }
1846
+ const quote = esc.match(/^\s*&gt;\s?(.*)$/);
1847
+ if (quote) {
1848
+ flushPara();
1849
+ closeList();
1850
+ out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
1851
+ i++;
1852
+ continue;
1853
+ }
1854
+ const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
1855
+ const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
1856
+ if (ol || ul) {
1857
+ flushPara();
1858
+ const type = ol ? "ol" : "ul";
1859
+ if (listType !== type) {
1860
+ closeList();
1861
+ out.push(`<${type}>`);
1862
+ listType = type;
1863
+ }
1864
+ out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
1865
+ i++;
1866
+ continue;
1867
+ }
1868
+ closeList();
1869
+ para.push(esc);
1870
+ i++;
1871
+ }
1872
+ flushPara();
1873
+ closeList();
1874
+ return out.join("\n");
1875
+ }
1864
1876
  function useCodeEditorInputTagAdapter() {
1865
1877
  const ctx = useCodeEditor();
1866
1878
  return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);