@particle-academy/fancy-code 0.5.0 → 0.6.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/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @particle-academy/fancy-code
12
12
  # or: yarn add @particle-academy/fancy-code
13
13
  ```
14
14
 
15
- **Peer dependencies:** `react >= 18`, `react-dom >= 18`, `@particle-academy/react-fancy >= 1.5`
15
+ **Peer dependencies:** `react >= 18`, `react-dom >= 18`, `@particle-academy/react-fancy >= 4.9` (the `FileViewer` delegates media to react-fancy's `<MediaViewer>`, added in 4.9)
16
16
 
17
17
  ## Setup
18
18
 
@@ -44,11 +44,38 @@ function App() {
44
44
 
45
45
  The component ships with no default border or rounding — add your own via `className` for standalone use, or embed directly into IDE layouts without visual conflicts.
46
46
 
47
+ ## Unified file viewer
48
+
49
+ Showing arbitrary files from a tree? `FileViewer` renders text in the
50
+ `CodeEditor` and delegates **media** (image / video / audio / PDF) to
51
+ react-fancy's `<MediaViewer>` — so an image no longer renders as binary text.
52
+
53
+ ```tsx
54
+ import { FileViewer } from "@particle-academy/fancy-code";
55
+
56
+ // text → CodeEditor (read-only, language picked from the filename)
57
+ <FileViewer filename="app.tsx" value={source} />
58
+
59
+ // media → <MediaViewer> (pass the file URL as `src`)
60
+ <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
61
+ ```
62
+
63
+ Need to branch your own chrome (tabs, save buttons)? Call `resolveFileKind`
64
+ yourself — it's the same text-vs-media decision `FileViewer` makes internally:
65
+
66
+ ```tsx
67
+ import { resolveFileKind } from "@particle-academy/fancy-code";
68
+
69
+ resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
70
+ resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
71
+ ```
72
+
47
73
  ## Documentation
48
74
 
49
75
  | Topic | Doc |
50
76
  |-------|-----|
51
77
  | Full component API (props, sub-components, `useCodeEditor` hook) | [docs/CodeEditor.md](./docs/CodeEditor.md) |
78
+ | Unified file viewer (text + media), `resolveFileKind` | [docs/FileViewer.md](./docs/FileViewer.md) |
52
79
  | Built-in languages + registering custom ones | [docs/languages.md](./docs/languages.md) |
53
80
  | Built-in themes + custom theme registration | [docs/themes.md](./docs/themes.md) |
54
81
 
@@ -85,3 +112,9 @@ import { CodeEditor } from "@particle-academy/fancy-code";
85
112
  ## License
86
113
 
87
114
  MIT
115
+
116
+ ---
117
+
118
+ ## ⭐ Star Fancy UI
119
+
120
+ If this package is useful to you, a quick ⭐ on the repo really helps us build a better kit. Thank you!
package/dist/index.cjs CHANGED
@@ -1079,6 +1079,7 @@ function tokenizeMarkdown(source) {
1079
1079
  return tokens;
1080
1080
  }
1081
1081
  function pushInline(line, base, out) {
1082
+ if (line.length > 5e3) return;
1082
1083
  const spans = [];
1083
1084
  const add = (type, start, end) => {
1084
1085
  spans.push(tok(type, base + start, base + end));
@@ -1684,8 +1685,11 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1684
1685
  function escapeHtml2(s) {
1685
1686
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1686
1687
  }
1688
+ function escapeAttr(s) {
1689
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1690
+ }
1687
1691
  function inline(text) {
1688
- 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>");
1692
+ 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>");
1689
1693
  }
1690
1694
  function renderMarkdown(markdown) {
1691
1695
  const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
@@ -1719,7 +1723,7 @@ function renderMarkdown(markdown) {
1719
1723
  i++;
1720
1724
  }
1721
1725
  i++;
1722
- out.push(`<pre><code${lang ? ` class="language-${escapeHtml2(lang)}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1726
+ out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml2(lang))}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1723
1727
  continue;
1724
1728
  }
1725
1729
  const esc = escapeHtml2(line);
@@ -1829,6 +1833,118 @@ function MarkdownEditor({
1829
1833
  );
1830
1834
  }
1831
1835
  MarkdownEditor.displayName = "MarkdownEditor";
1836
+ var EXT_LANGUAGE = {
1837
+ js: "javascript",
1838
+ mjs: "javascript",
1839
+ cjs: "javascript",
1840
+ jsx: "javascript",
1841
+ json: "javascript",
1842
+ // the JS tokenizer highlights JSON acceptably
1843
+ jsonc: "javascript",
1844
+ ts: "typescript",
1845
+ mts: "typescript",
1846
+ cts: "typescript",
1847
+ tsx: "typescript",
1848
+ html: "html",
1849
+ htm: "html",
1850
+ xml: "html",
1851
+ xhtml: "html",
1852
+ vue: "html",
1853
+ php: "php",
1854
+ phtml: "php",
1855
+ py: "python",
1856
+ pyw: "python",
1857
+ go: "go",
1858
+ md: "markdown",
1859
+ markdown: "markdown",
1860
+ mkd: "markdown",
1861
+ mdx: "markdown"
1862
+ };
1863
+ function basename(filename) {
1864
+ return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1865
+ }
1866
+ function languageFromFilename(filename) {
1867
+ if (!filename) return "plaintext";
1868
+ const seg = basename(filename);
1869
+ const dot = seg.lastIndexOf(".");
1870
+ if (dot <= 0) return "plaintext";
1871
+ const ext = seg.slice(dot + 1).toLowerCase();
1872
+ return EXT_LANGUAGE[ext] ?? "plaintext";
1873
+ }
1874
+ function resolveFileKind({ filename, mime }) {
1875
+ const media = reactFancy.resolveMediaType({ mime, src: filename });
1876
+ if (media !== "unknown") return { kind: "media", mediaKind: media };
1877
+ return { kind: "text", language: languageFromFilename(filename) };
1878
+ }
1879
+ function FileViewer({
1880
+ filename,
1881
+ mime,
1882
+ src,
1883
+ value = "",
1884
+ onChange,
1885
+ readOnly = true,
1886
+ theme = "auto",
1887
+ fileKind,
1888
+ lineNumbers = true,
1889
+ wordWrap = false,
1890
+ minHeight,
1891
+ maxHeight,
1892
+ mediaProps,
1893
+ className,
1894
+ style
1895
+ }) {
1896
+ const resolved = fileKind ?? resolveFileKind({ filename, mime });
1897
+ if (resolved.kind === "media") {
1898
+ const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1899
+ const fill = resolved.mediaKind !== "audio";
1900
+ return /* @__PURE__ */ jsxRuntime.jsx(
1901
+ "div",
1902
+ {
1903
+ "data-fancy-file-viewer": "",
1904
+ "data-kind": "media",
1905
+ "data-media-kind": resolved.mediaKind,
1906
+ className: reactFancy.cn("overflow-hidden", className),
1907
+ style,
1908
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1909
+ reactFancy.MediaViewer,
1910
+ {
1911
+ src: src ?? "",
1912
+ mime,
1913
+ alt: filename,
1914
+ kind: resolved.mediaKind,
1915
+ className: reactFancy.cn(fill && "h-full w-full", mediaClassName),
1916
+ ...restMediaProps
1917
+ }
1918
+ )
1919
+ }
1920
+ );
1921
+ }
1922
+ return /* @__PURE__ */ jsxRuntime.jsx(
1923
+ "div",
1924
+ {
1925
+ "data-fancy-file-viewer": "",
1926
+ "data-kind": "text",
1927
+ className,
1928
+ style,
1929
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1930
+ CodeEditor,
1931
+ {
1932
+ value,
1933
+ onChange: readOnly ? void 0 : onChange,
1934
+ language: resolved.language,
1935
+ theme,
1936
+ readOnly,
1937
+ lineNumbers,
1938
+ wordWrap,
1939
+ minHeight,
1940
+ maxHeight,
1941
+ children: /* @__PURE__ */ jsxRuntime.jsx(CodeEditor.Panel, {})
1942
+ }
1943
+ )
1944
+ }
1945
+ );
1946
+ }
1947
+ FileViewer.displayName = "FileViewer";
1832
1948
  function useCodeEditorInputTagAdapter() {
1833
1949
  const ctx = useCodeEditor();
1834
1950
  return react.useMemo(() => reactFancy.textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
@@ -1840,14 +1956,17 @@ function CodeEditorInputTag(props) {
1840
1956
 
1841
1957
  exports.CodeEditor = CodeEditor;
1842
1958
  exports.CodeEditorInputTag = CodeEditorInputTag;
1959
+ exports.FileViewer = FileViewer;
1843
1960
  exports.MarkdownEditor = MarkdownEditor;
1844
1961
  exports.getLanguage = getLanguage;
1845
1962
  exports.getRegisteredLanguages = getRegisteredLanguages;
1846
1963
  exports.getRegisteredThemes = getRegisteredThemes;
1847
1964
  exports.getTheme = getTheme;
1965
+ exports.languageFromFilename = languageFromFilename;
1848
1966
  exports.registerLanguage = registerLanguage;
1849
1967
  exports.registerTheme = registerTheme;
1850
1968
  exports.renderMarkdown = renderMarkdown;
1969
+ exports.resolveFileKind = resolveFileKind;
1851
1970
  exports.tokenizeMarkdown = tokenizeMarkdown;
1852
1971
  exports.useCodeEditor = useCodeEditor;
1853
1972
  exports.useCodeEditorInputTagAdapter = useCodeEditorInputTagAdapter;