@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.cjs +113 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +114 -102
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
-
/**
|
|
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
|
|
237
|
-
*
|
|
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
|
|
245
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
237
|
-
*
|
|
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
|
|
245
|
-
*
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1641
|
-
}
|
|
1642
|
-
function escapeAttr(s) {
|
|
1643
|
-
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
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*>\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
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1781
|
+
}
|
|
1782
|
+
function escapeAttr(s) {
|
|
1783
|
+
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
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*>\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]);
|