@particle-academy/fancy-code 0.4.7 → 0.5.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
@@ -177,6 +177,64 @@ declare const CodeEditor: typeof CodeEditorRoot & {
177
177
 
178
178
  declare function useCodeEditor(): CodeEditorContextValue;
179
179
 
180
+ type MarkdownEditorMode = "split" | "edit" | "preview";
181
+ interface MarkdownEditorProps {
182
+ /** Controlled markdown value. */
183
+ value?: string;
184
+ /** Initial value (uncontrolled). */
185
+ defaultValue?: string;
186
+ /** Called with the new markdown whenever the document changes. */
187
+ onValueChange?: (markdown: string) => void;
188
+ /**
189
+ * Layout: `split` (editor + live preview side by side), `edit` (editor only),
190
+ * or `preview` (rendered only). Default `split`.
191
+ */
192
+ mode?: MarkdownEditorMode;
193
+ /** Editor theme — `"light" | "dark" | "auto"` or a registered name. Default `auto`. */
194
+ theme?: string;
195
+ /** Prevent editing. */
196
+ readOnly?: boolean;
197
+ /** Show line numbers in the editor (default false — prose reads better without). */
198
+ lineNumbers?: boolean;
199
+ /** Wrap long lines in the editor (default true for prose). */
200
+ wordWrap?: boolean;
201
+ /** Placeholder shown when empty. */
202
+ placeholder?: string;
203
+ /** Editor height bounds (px). */
204
+ minHeight?: number;
205
+ maxHeight?: number;
206
+ className?: string;
207
+ /**
208
+ * Swap the markdown → HTML renderer used by the preview pane (e.g. a full
209
+ * CommonMark library). Defaults to the built-in lightweight renderer.
210
+ */
211
+ renderPreview?: (markdown: string) => string;
212
+ }
213
+ /**
214
+ * A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
215
+ * `markdown` language) with an optional live preview pane. Controlled via
216
+ * `value` + `onValueChange`. The preview renders with a small built-in
217
+ * dependency-free renderer; pass `renderPreview` to use your own.
218
+ *
219
+ * ```tsx
220
+ * <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
221
+ * ```
222
+ */
223
+ declare function MarkdownEditor({ value, defaultValue, onValueChange, mode, theme, readOnly, lineNumbers, wordWrap, placeholder, minHeight, maxHeight, className, renderPreview, }: MarkdownEditorProps): react.JSX.Element;
224
+ declare namespace MarkdownEditor {
225
+ var displayName: string;
226
+ }
227
+
228
+ /**
229
+ * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
+ * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
231
+ * links, images, blockquotes, ordered/unordered lists, hr, and paragraphs. Input
232
+ * is HTML-escaped first, so raw HTML in the source is shown as text (safe by
233
+ * default). Not a full CommonMark implementation; swap in a real renderer
234
+ * downstream if you need one.
235
+ */
236
+ declare function renderMarkdown(markdown: string): string;
237
+
180
238
  type TokenType = "keyword" | "string" | "comment" | "number" | "operator" | "tag" | "attribute" | "attributeValue" | "punctuation" | "function" | "type" | "variable" | "plain";
181
239
  interface Token {
182
240
  type: TokenType;
@@ -185,6 +243,14 @@ interface Token {
185
243
  }
186
244
  type Tokenizer = (source: string) => Token[];
187
245
 
246
+ /**
247
+ * Minimal, dependency-free Markdown tokenizer for editor highlighting. Line-based
248
+ * for block constructs (fences, headings, blockquotes, list markers) plus a few
249
+ * safe, non-overlapping inline spans (code, bold/italic, links). Good enough to
250
+ * read structure at a glance — not a full CommonMark parse.
251
+ */
252
+ declare function tokenizeMarkdown(source: string): Token[];
253
+
188
254
  interface LanguageDefinition {
189
255
  /** Display name shown in UI (e.g., "JavaScript") */
190
256
  name: string;
@@ -243,4 +309,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
243
309
  */
244
310
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
245
311
 
246
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, useCodeEditor, useCodeEditorInputTagAdapter };
312
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.d.ts CHANGED
@@ -177,6 +177,64 @@ declare const CodeEditor: typeof CodeEditorRoot & {
177
177
 
178
178
  declare function useCodeEditor(): CodeEditorContextValue;
179
179
 
180
+ type MarkdownEditorMode = "split" | "edit" | "preview";
181
+ interface MarkdownEditorProps {
182
+ /** Controlled markdown value. */
183
+ value?: string;
184
+ /** Initial value (uncontrolled). */
185
+ defaultValue?: string;
186
+ /** Called with the new markdown whenever the document changes. */
187
+ onValueChange?: (markdown: string) => void;
188
+ /**
189
+ * Layout: `split` (editor + live preview side by side), `edit` (editor only),
190
+ * or `preview` (rendered only). Default `split`.
191
+ */
192
+ mode?: MarkdownEditorMode;
193
+ /** Editor theme — `"light" | "dark" | "auto"` or a registered name. Default `auto`. */
194
+ theme?: string;
195
+ /** Prevent editing. */
196
+ readOnly?: boolean;
197
+ /** Show line numbers in the editor (default false — prose reads better without). */
198
+ lineNumbers?: boolean;
199
+ /** Wrap long lines in the editor (default true for prose). */
200
+ wordWrap?: boolean;
201
+ /** Placeholder shown when empty. */
202
+ placeholder?: string;
203
+ /** Editor height bounds (px). */
204
+ minHeight?: number;
205
+ maxHeight?: number;
206
+ className?: string;
207
+ /**
208
+ * Swap the markdown → HTML renderer used by the preview pane (e.g. a full
209
+ * CommonMark library). Defaults to the built-in lightweight renderer.
210
+ */
211
+ renderPreview?: (markdown: string) => string;
212
+ }
213
+ /**
214
+ * A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
215
+ * `markdown` language) with an optional live preview pane. Controlled via
216
+ * `value` + `onValueChange`. The preview renders with a small built-in
217
+ * dependency-free renderer; pass `renderPreview` to use your own.
218
+ *
219
+ * ```tsx
220
+ * <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
221
+ * ```
222
+ */
223
+ declare function MarkdownEditor({ value, defaultValue, onValueChange, mode, theme, readOnly, lineNumbers, wordWrap, placeholder, minHeight, maxHeight, className, renderPreview, }: MarkdownEditorProps): react.JSX.Element;
224
+ declare namespace MarkdownEditor {
225
+ var displayName: string;
226
+ }
227
+
228
+ /**
229
+ * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
+ * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
231
+ * links, images, blockquotes, ordered/unordered lists, hr, and paragraphs. Input
232
+ * is HTML-escaped first, so raw HTML in the source is shown as text (safe by
233
+ * default). Not a full CommonMark implementation; swap in a real renderer
234
+ * downstream if you need one.
235
+ */
236
+ declare function renderMarkdown(markdown: string): string;
237
+
180
238
  type TokenType = "keyword" | "string" | "comment" | "number" | "operator" | "tag" | "attribute" | "attributeValue" | "punctuation" | "function" | "type" | "variable" | "plain";
181
239
  interface Token {
182
240
  type: TokenType;
@@ -185,6 +243,14 @@ interface Token {
185
243
  }
186
244
  type Tokenizer = (source: string) => Token[];
187
245
 
246
+ /**
247
+ * Minimal, dependency-free Markdown tokenizer for editor highlighting. Line-based
248
+ * for block constructs (fences, headings, blockquotes, list markers) plus a few
249
+ * safe, non-overlapping inline spans (code, bold/italic, links). Good enough to
250
+ * read structure at a glance — not a full CommonMark parse.
251
+ */
252
+ declare function tokenizeMarkdown(source: string): Token[];
253
+
188
254
  interface LanguageDefinition {
189
255
  /** Display name shown in UI (e.g., "JavaScript") */
190
256
  name: string;
@@ -243,4 +309,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
243
309
  */
244
310
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
245
311
 
246
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, useCodeEditor, useCodeEditorInputTagAdapter };
312
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.js CHANGED
@@ -1035,7 +1035,79 @@ var tokenizeGo = (source) => {
1035
1035
  return tokens;
1036
1036
  };
1037
1037
 
1038
+ // src/engine/tokenizer.ts
1039
+ function tok(type, s, e) {
1040
+ return { type, start: s, end: e };
1041
+ }
1042
+
1043
+ // src/engine/tokenizers/markdown.ts
1044
+ function tokenizeMarkdown(source) {
1045
+ const tokens = [];
1046
+ const lines = source.split("\n");
1047
+ let offset = 0;
1048
+ let inFence = false;
1049
+ for (const line of lines) {
1050
+ const base = offset;
1051
+ const len = line.length;
1052
+ offset += len + 1;
1053
+ if (/^\s*(```|~~~)/.test(line)) {
1054
+ tokens.push(tok("string", base, base + len));
1055
+ inFence = !inFence;
1056
+ continue;
1057
+ }
1058
+ if (inFence) {
1059
+ tokens.push(tok("string", base, base + len));
1060
+ continue;
1061
+ }
1062
+ if (/^#{1,6}\s/.test(line)) {
1063
+ tokens.push(tok("keyword", base, base + len));
1064
+ continue;
1065
+ }
1066
+ if (/^\s*>/.test(line)) {
1067
+ tokens.push(tok("comment", base, base + len));
1068
+ continue;
1069
+ }
1070
+ const list = line.match(/^(\s*)([-*+]|\d+\.)\s/);
1071
+ if (list) {
1072
+ const start = base + list[1].length;
1073
+ tokens.push(tok("operator", start, start + list[2].length));
1074
+ }
1075
+ pushInline(line, base, tokens);
1076
+ }
1077
+ return tokens;
1078
+ }
1079
+ function pushInline(line, base, out) {
1080
+ const spans = [];
1081
+ const add = (type, start, end) => {
1082
+ spans.push(tok(type, base + start, base + end));
1083
+ };
1084
+ let m;
1085
+ const code = /`[^`]+`/g;
1086
+ while (m = code.exec(line)) add("string", m.index, m.index + m[0].length);
1087
+ const emph = /(\*\*|__)(?=\S)([\s\S]*?\S)\1|(\*|_)(?=\S)([\s\S]*?\S)\3/g;
1088
+ while (m = emph.exec(line)) add("type", m.index, m.index + m[0].length);
1089
+ const link = /!?\[[^\]]*\]\(([^)]+)\)/g;
1090
+ while (m = link.exec(line)) {
1091
+ add("tag", m.index, m.index + m[0].length - m[1].length - 1);
1092
+ const urlStart = m.index + m[0].length - m[1].length - 1;
1093
+ add("attributeValue", urlStart, urlStart + m[1].length);
1094
+ }
1095
+ spans.sort((a, b) => a.start - b.start || b.end - a.end);
1096
+ let lastEnd = -1;
1097
+ for (const s of spans) {
1098
+ if (s.start >= lastEnd) {
1099
+ out.push(s);
1100
+ lastEnd = s.end;
1101
+ }
1102
+ }
1103
+ }
1104
+
1038
1105
  // src/languages/builtin.ts
1106
+ registerLanguage({
1107
+ name: "Markdown",
1108
+ aliases: ["md", "markdown", "mkd"],
1109
+ tokenize: tokenizeMarkdown
1110
+ });
1039
1111
  registerLanguage({
1040
1112
  name: "JavaScript",
1041
1113
  aliases: ["js", "javascript", "jsx"],
@@ -1605,6 +1677,156 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1605
1677
  Panel: CodeEditorPanel,
1606
1678
  StatusBar: CodeEditorStatusBar
1607
1679
  });
1680
+
1681
+ // src/engine/markdown-render.ts
1682
+ function escapeHtml2(s) {
1683
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1684
+ }
1685
+ function inline(text) {
1686
+ return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => `<img src="${src}" alt="${alt}"${title ? ` title="${title}"` : ""}>`).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => `<a href="${href}"${title ? ` title="${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>");
1687
+ }
1688
+ function renderMarkdown(markdown) {
1689
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1690
+ const out = [];
1691
+ let i = 0;
1692
+ let para = [];
1693
+ let listType = null;
1694
+ const flushPara = () => {
1695
+ if (para.length) {
1696
+ out.push(`<p>${inline(para.join(" "))}</p>`);
1697
+ para = [];
1698
+ }
1699
+ };
1700
+ const closeList = () => {
1701
+ if (listType) {
1702
+ out.push(`</${listType}>`);
1703
+ listType = null;
1704
+ }
1705
+ };
1706
+ while (i < lines.length) {
1707
+ const line = lines[i];
1708
+ const fence = line.match(/^\s*(```|~~~)(.*)$/);
1709
+ if (fence) {
1710
+ flushPara();
1711
+ closeList();
1712
+ const lang = fence[2].trim();
1713
+ const body = [];
1714
+ i++;
1715
+ while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
1716
+ body.push(lines[i]);
1717
+ i++;
1718
+ }
1719
+ i++;
1720
+ out.push(`<pre><code${lang ? ` class="language-${escapeHtml2(lang)}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1721
+ continue;
1722
+ }
1723
+ const esc = escapeHtml2(line);
1724
+ if (/^\s*$/.test(line)) {
1725
+ flushPara();
1726
+ closeList();
1727
+ i++;
1728
+ continue;
1729
+ }
1730
+ const heading = esc.match(/^(#{1,6})\s+(.*)$/);
1731
+ if (heading) {
1732
+ flushPara();
1733
+ closeList();
1734
+ const level = heading[1].length;
1735
+ out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1736
+ i++;
1737
+ continue;
1738
+ }
1739
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
1740
+ flushPara();
1741
+ closeList();
1742
+ out.push("<hr>");
1743
+ i++;
1744
+ continue;
1745
+ }
1746
+ const quote = esc.match(/^\s*&gt;\s?(.*)$/);
1747
+ if (quote) {
1748
+ flushPara();
1749
+ closeList();
1750
+ out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
1751
+ i++;
1752
+ continue;
1753
+ }
1754
+ const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
1755
+ const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
1756
+ if (ol || ul) {
1757
+ flushPara();
1758
+ const type = ol ? "ol" : "ul";
1759
+ if (listType !== type) {
1760
+ closeList();
1761
+ out.push(`<${type}>`);
1762
+ listType = type;
1763
+ }
1764
+ out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
1765
+ i++;
1766
+ continue;
1767
+ }
1768
+ closeList();
1769
+ para.push(esc);
1770
+ i++;
1771
+ }
1772
+ flushPara();
1773
+ closeList();
1774
+ return out.join("\n");
1775
+ }
1776
+ function MarkdownEditor({
1777
+ value,
1778
+ defaultValue = "",
1779
+ onValueChange,
1780
+ mode = "split",
1781
+ theme = "auto",
1782
+ readOnly = false,
1783
+ lineNumbers = false,
1784
+ wordWrap = true,
1785
+ placeholder,
1786
+ minHeight,
1787
+ maxHeight,
1788
+ className,
1789
+ renderPreview = renderMarkdown
1790
+ }) {
1791
+ const [content, setContent] = useControllableState(value, defaultValue, onValueChange);
1792
+ const showEditor = mode !== "preview";
1793
+ const showPreview = mode !== "edit";
1794
+ return /* @__PURE__ */ jsxs(
1795
+ "div",
1796
+ {
1797
+ "data-fancy-markdown-editor": "",
1798
+ "data-mode": mode,
1799
+ className: cn("fancy-md-editor", className),
1800
+ children: [
1801
+ showEditor && /* @__PURE__ */ jsx("div", { className: "fancy-md-editor-pane", children: /* @__PURE__ */ jsx(
1802
+ CodeEditor,
1803
+ {
1804
+ value: content,
1805
+ onChange: setContent,
1806
+ language: "markdown",
1807
+ theme,
1808
+ readOnly,
1809
+ lineNumbers,
1810
+ wordWrap,
1811
+ placeholder,
1812
+ minHeight,
1813
+ maxHeight,
1814
+ children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
1815
+ }
1816
+ ) }),
1817
+ showPreview && /* @__PURE__ */ jsx(
1818
+ "div",
1819
+ {
1820
+ "data-fancy-markdown-preview": "",
1821
+ className: "fancy-md-preview",
1822
+ dangerouslySetInnerHTML: { __html: renderPreview(content) }
1823
+ }
1824
+ )
1825
+ ]
1826
+ }
1827
+ );
1828
+ }
1829
+ MarkdownEditor.displayName = "MarkdownEditor";
1608
1830
  function useCodeEditorInputTagAdapter() {
1609
1831
  const ctx = useCodeEditor();
1610
1832
  return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
@@ -1614,6 +1836,6 @@ function CodeEditorInputTag(props) {
1614
1836
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1615
1837
  }
1616
1838
 
1617
- export { CodeEditor, CodeEditorInputTag, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, useCodeEditor, useCodeEditorInputTagAdapter };
1839
+ export { CodeEditor, CodeEditorInputTag, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1618
1840
  //# sourceMappingURL=index.js.map
1619
1841
  //# sourceMappingURL=index.js.map