@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/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode } from 'react';
3
- import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
2
+ import { RefObject, ReactNode, CSSProperties } from 'react';
3
+ import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
4
4
 
5
5
  interface ThemeColors {
6
6
  background: string;
@@ -225,6 +225,106 @@ declare namespace MarkdownEditor {
225
225
  var displayName: string;
226
226
  }
227
227
 
228
+ /**
229
+ * The decision a file viewer makes about a file: render it as editable text, or
230
+ * hand it to a media viewer. `media` carries the resolved {@link MediaKind};
231
+ * `text` carries the editor language id.
232
+ */
233
+ type FileKind = {
234
+ kind: "text";
235
+ language: string;
236
+ } | {
237
+ kind: "media";
238
+ mediaKind: MediaKind;
239
+ };
240
+ interface ResolveFileKindInput {
241
+ /** File name or path — sniffed for an extension (and used for the media check). */
242
+ filename?: string;
243
+ /** MIME type — preferred over the filename when known. */
244
+ mime?: string;
245
+ }
246
+ /**
247
+ * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
248
+ * (no syntax highlighting) for unknown or extension-less files.
249
+ */
250
+ declare function languageFromFilename(filename?: string): string;
251
+ /**
252
+ * Decide whether a file is previewable media (image / video / audio / PDF) or
253
+ * text, from its MIME type (preferred) and/or filename. Media wins when the
254
+ * type resolves to one; otherwise it's text and the editor language is derived
255
+ * from the extension.
256
+ *
257
+ * Exposed so consumers can branch their own chrome (tabs, save buttons) the
258
+ * same way `<FileViewer>` does internally.
259
+ *
260
+ * ```ts
261
+ * resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
262
+ * resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
263
+ * resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
264
+ * ```
265
+ */
266
+ declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
267
+
268
+ interface FileViewerProps {
269
+ /**
270
+ * File name or path — used to detect the file kind / editor language, and as
271
+ * a label / alt text. Pass `mime` too when you have it (it's preferred).
272
+ */
273
+ filename?: string;
274
+ /** MIME type — preferred over `filename` for detection. */
275
+ mime?: string;
276
+ /**
277
+ * Source URL for **media** files (image / video / audio / PDF) —
278
+ * `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
279
+ */
280
+ src?: string;
281
+ /** Text content for **text** files. */
282
+ value?: string;
283
+ /** Called when text content changes (ignored when `readOnly`). */
284
+ onChange?: (value: string) => void;
285
+ /**
286
+ * Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
287
+ * read-only regardless. Set `false` (with `onChange`) to edit text.
288
+ */
289
+ readOnly?: boolean;
290
+ /** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
291
+ theme?: string;
292
+ /** Force the file kind, bypassing detection. */
293
+ fileKind?: FileKind;
294
+ /** Show line numbers in the text editor (default `true`). */
295
+ lineNumbers?: boolean;
296
+ /** Wrap long lines in the text editor (default `false`). */
297
+ wordWrap?: boolean;
298
+ /** Editor height bounds (px). */
299
+ minHeight?: number;
300
+ maxHeight?: number;
301
+ /** Extra props forwarded to `<MediaViewer>` for media files. */
302
+ mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
303
+ className?: string;
304
+ style?: CSSProperties;
305
+ }
306
+
307
+ /**
308
+ * A unified file viewer: renders text files in a read-only `CodeEditor` (with
309
+ * syntax highlighting picked from the filename) and media files — image, video,
310
+ * audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
311
+ * made by {@link resolveFileKind}; pass `fileKind` to override it, or call
312
+ * `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
313
+ *
314
+ * Give the container a height for media files (PDF/image/video fill their box).
315
+ *
316
+ * ```tsx
317
+ * // text
318
+ * <FileViewer filename="app.tsx" value={source} />
319
+ * // media
320
+ * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
321
+ * ```
322
+ */
323
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
324
+ declare namespace FileViewer {
325
+ var displayName: string;
326
+ }
327
+
228
328
  /**
229
329
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
330
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -309,4 +409,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
309
409
  */
310
410
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
411
 
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 };
412
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode } from 'react';
3
- import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
2
+ import { RefObject, ReactNode, CSSProperties } from 'react';
3
+ import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
4
4
 
5
5
  interface ThemeColors {
6
6
  background: string;
@@ -225,6 +225,106 @@ declare namespace MarkdownEditor {
225
225
  var displayName: string;
226
226
  }
227
227
 
228
+ /**
229
+ * The decision a file viewer makes about a file: render it as editable text, or
230
+ * hand it to a media viewer. `media` carries the resolved {@link MediaKind};
231
+ * `text` carries the editor language id.
232
+ */
233
+ type FileKind = {
234
+ kind: "text";
235
+ language: string;
236
+ } | {
237
+ kind: "media";
238
+ mediaKind: MediaKind;
239
+ };
240
+ interface ResolveFileKindInput {
241
+ /** File name or path — sniffed for an extension (and used for the media check). */
242
+ filename?: string;
243
+ /** MIME type — preferred over the filename when known. */
244
+ mime?: string;
245
+ }
246
+ /**
247
+ * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
248
+ * (no syntax highlighting) for unknown or extension-less files.
249
+ */
250
+ declare function languageFromFilename(filename?: string): string;
251
+ /**
252
+ * Decide whether a file is previewable media (image / video / audio / PDF) or
253
+ * text, from its MIME type (preferred) and/or filename. Media wins when the
254
+ * type resolves to one; otherwise it's text and the editor language is derived
255
+ * from the extension.
256
+ *
257
+ * Exposed so consumers can branch their own chrome (tabs, save buttons) the
258
+ * same way `<FileViewer>` does internally.
259
+ *
260
+ * ```ts
261
+ * resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
262
+ * resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
263
+ * resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
264
+ * ```
265
+ */
266
+ declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
267
+
268
+ interface FileViewerProps {
269
+ /**
270
+ * File name or path — used to detect the file kind / editor language, and as
271
+ * a label / alt text. Pass `mime` too when you have it (it's preferred).
272
+ */
273
+ filename?: string;
274
+ /** MIME type — preferred over `filename` for detection. */
275
+ mime?: string;
276
+ /**
277
+ * Source URL for **media** files (image / video / audio / PDF) —
278
+ * `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
279
+ */
280
+ src?: string;
281
+ /** Text content for **text** files. */
282
+ value?: string;
283
+ /** Called when text content changes (ignored when `readOnly`). */
284
+ onChange?: (value: string) => void;
285
+ /**
286
+ * Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
287
+ * read-only regardless. Set `false` (with `onChange`) to edit text.
288
+ */
289
+ readOnly?: boolean;
290
+ /** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
291
+ theme?: string;
292
+ /** Force the file kind, bypassing detection. */
293
+ fileKind?: FileKind;
294
+ /** Show line numbers in the text editor (default `true`). */
295
+ lineNumbers?: boolean;
296
+ /** Wrap long lines in the text editor (default `false`). */
297
+ wordWrap?: boolean;
298
+ /** Editor height bounds (px). */
299
+ minHeight?: number;
300
+ maxHeight?: number;
301
+ /** Extra props forwarded to `<MediaViewer>` for media files. */
302
+ mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
303
+ className?: string;
304
+ style?: CSSProperties;
305
+ }
306
+
307
+ /**
308
+ * A unified file viewer: renders text files in a read-only `CodeEditor` (with
309
+ * syntax highlighting picked from the filename) and media files — image, video,
310
+ * audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
311
+ * made by {@link resolveFileKind}; pass `fileKind` to override it, or call
312
+ * `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
313
+ *
314
+ * Give the container a height for media files (PDF/image/video fill their box).
315
+ *
316
+ * ```tsx
317
+ * // text
318
+ * <FileViewer filename="app.tsx" value={source} />
319
+ * // media
320
+ * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
321
+ * ```
322
+ */
323
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
324
+ declare namespace FileViewer {
325
+ var displayName: string;
326
+ }
327
+
228
328
  /**
229
329
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
330
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -309,4 +409,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
309
409
  */
310
410
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
411
 
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 };
412
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
2
- import { cn, useControllableState, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
2
+ import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/components/CodeEditor/CodeEditor.tsx
@@ -1077,6 +1077,7 @@ function tokenizeMarkdown(source) {
1077
1077
  return tokens;
1078
1078
  }
1079
1079
  function pushInline(line, base, out) {
1080
+ if (line.length > 5e3) return;
1080
1081
  const spans = [];
1081
1082
  const add = (type, start, end) => {
1082
1083
  spans.push(tok(type, base + start, base + end));
@@ -1682,8 +1683,11 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1682
1683
  function escapeHtml2(s) {
1683
1684
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1684
1685
  }
1686
+ function escapeAttr(s) {
1687
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1688
+ }
1685
1689
  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>");
1690
+ 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>");
1687
1691
  }
1688
1692
  function renderMarkdown(markdown) {
1689
1693
  const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
@@ -1717,7 +1721,7 @@ function renderMarkdown(markdown) {
1717
1721
  i++;
1718
1722
  }
1719
1723
  i++;
1720
- out.push(`<pre><code${lang ? ` class="language-${escapeHtml2(lang)}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1724
+ out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml2(lang))}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1721
1725
  continue;
1722
1726
  }
1723
1727
  const esc = escapeHtml2(line);
@@ -1827,6 +1831,118 @@ function MarkdownEditor({
1827
1831
  );
1828
1832
  }
1829
1833
  MarkdownEditor.displayName = "MarkdownEditor";
1834
+ var EXT_LANGUAGE = {
1835
+ js: "javascript",
1836
+ mjs: "javascript",
1837
+ cjs: "javascript",
1838
+ jsx: "javascript",
1839
+ json: "javascript",
1840
+ // the JS tokenizer highlights JSON acceptably
1841
+ jsonc: "javascript",
1842
+ ts: "typescript",
1843
+ mts: "typescript",
1844
+ cts: "typescript",
1845
+ tsx: "typescript",
1846
+ html: "html",
1847
+ htm: "html",
1848
+ xml: "html",
1849
+ xhtml: "html",
1850
+ vue: "html",
1851
+ php: "php",
1852
+ phtml: "php",
1853
+ py: "python",
1854
+ pyw: "python",
1855
+ go: "go",
1856
+ md: "markdown",
1857
+ markdown: "markdown",
1858
+ mkd: "markdown",
1859
+ mdx: "markdown"
1860
+ };
1861
+ function basename(filename) {
1862
+ return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1863
+ }
1864
+ function languageFromFilename(filename) {
1865
+ if (!filename) return "plaintext";
1866
+ const seg = basename(filename);
1867
+ const dot = seg.lastIndexOf(".");
1868
+ if (dot <= 0) return "plaintext";
1869
+ const ext = seg.slice(dot + 1).toLowerCase();
1870
+ return EXT_LANGUAGE[ext] ?? "plaintext";
1871
+ }
1872
+ function resolveFileKind({ filename, mime }) {
1873
+ const media = resolveMediaType({ mime, src: filename });
1874
+ if (media !== "unknown") return { kind: "media", mediaKind: media };
1875
+ return { kind: "text", language: languageFromFilename(filename) };
1876
+ }
1877
+ function FileViewer({
1878
+ filename,
1879
+ mime,
1880
+ src,
1881
+ value = "",
1882
+ onChange,
1883
+ readOnly = true,
1884
+ theme = "auto",
1885
+ fileKind,
1886
+ lineNumbers = true,
1887
+ wordWrap = false,
1888
+ minHeight,
1889
+ maxHeight,
1890
+ mediaProps,
1891
+ className,
1892
+ style
1893
+ }) {
1894
+ const resolved = fileKind ?? resolveFileKind({ filename, mime });
1895
+ if (resolved.kind === "media") {
1896
+ const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1897
+ const fill = resolved.mediaKind !== "audio";
1898
+ return /* @__PURE__ */ jsx(
1899
+ "div",
1900
+ {
1901
+ "data-fancy-file-viewer": "",
1902
+ "data-kind": "media",
1903
+ "data-media-kind": resolved.mediaKind,
1904
+ className: cn("overflow-hidden", className),
1905
+ style,
1906
+ children: /* @__PURE__ */ jsx(
1907
+ MediaViewer,
1908
+ {
1909
+ src: src ?? "",
1910
+ mime,
1911
+ alt: filename,
1912
+ kind: resolved.mediaKind,
1913
+ className: cn(fill && "h-full w-full", mediaClassName),
1914
+ ...restMediaProps
1915
+ }
1916
+ )
1917
+ }
1918
+ );
1919
+ }
1920
+ return /* @__PURE__ */ jsx(
1921
+ "div",
1922
+ {
1923
+ "data-fancy-file-viewer": "",
1924
+ "data-kind": "text",
1925
+ className,
1926
+ style,
1927
+ children: /* @__PURE__ */ jsx(
1928
+ CodeEditor,
1929
+ {
1930
+ value,
1931
+ onChange: readOnly ? void 0 : onChange,
1932
+ language: resolved.language,
1933
+ theme,
1934
+ readOnly,
1935
+ lineNumbers,
1936
+ wordWrap,
1937
+ minHeight,
1938
+ maxHeight,
1939
+ children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
1940
+ }
1941
+ )
1942
+ }
1943
+ );
1944
+ }
1945
+ FileViewer.displayName = "FileViewer";
1830
1946
  function useCodeEditorInputTagAdapter() {
1831
1947
  const ctx = useCodeEditor();
1832
1948
  return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
@@ -1836,6 +1952,6 @@ function CodeEditorInputTag(props) {
1836
1952
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1837
1953
  }
1838
1954
 
1839
- export { CodeEditor, CodeEditorInputTag, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1955
+ export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1840
1956
  //# sourceMappingURL=index.js.map
1841
1957
  //# sourceMappingURL=index.js.map