@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 +34 -1
- package/dist/index.cjs +121 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -3
- package/dist/index.d.ts +103 -3
- package/dist/index.js +120 -4
- package/dist/index.js.map +1 -1
- package/docs/FileViewer.md +78 -0
- package/package.json +6 -4
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 >=
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1686
1687
|
}
|
|
1688
|
+
function escapeAttr(s) {
|
|
1689
|
+
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
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;
|