@particle-academy/fancy-code 0.12.1 → 0.14.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
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { RefObject, ReactNode, CSSProperties } from 'react';
3
3
  import { ThemeColors, DiffAnnotations, LineChangeType, Token, Tokenizer } from '@particle-academy/fancy-file-commons';
4
4
  export { ThemeColors, Token, TokenType, Tokenizer, languageFromFilename } from '@particle-academy/fancy-file-commons';
5
- import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
5
+ import { MediaKind, MediaViewerProps, FauxClientProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
6
6
 
7
7
  interface ThemeDefinition {
8
8
  /** Unique theme name */
@@ -281,7 +281,20 @@ type FileKind = {
281
281
  } | {
282
282
  kind: "media";
283
283
  mediaKind: MediaKind;
284
+ }
285
+ /** Read as a reader sees it (rendered), not as source. `language` is for the source view. */
286
+ | {
287
+ kind: "document";
288
+ format: DocumentFormat;
289
+ language: string;
290
+ }
291
+ /** An Office file: binary, so never text. */
292
+ | {
293
+ kind: "office";
294
+ officeKind: OfficeKind;
284
295
  };
296
+ type DocumentFormat = "markdown" | "html" | "text";
297
+ type OfficeKind = "docx" | "xlsx" | "pptx";
285
298
  interface ResolveFileKindInput {
286
299
  /** File name or path — sniffed for an extension (and used for the media check). */
287
300
  filename?: string;
@@ -354,6 +367,18 @@ interface FileViewerProps {
354
367
  maxHeight?: number;
355
368
  /** Extra props forwarded to `<MediaViewer>` for media files. */
356
369
  mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
370
+ /**
371
+ * How a **document** (Markdown, HTML, plain text) is shown. `"rendered"`
372
+ * (default): as a reader sees it, through react-fancy's `ContentRenderer`
373
+ * (sanitised; a file is untrusted). `"source"`: in the code editor. Editing
374
+ * (`readOnly={false}`) and landing on a `line` always show the source.
375
+ */
376
+ view?: "rendered" | "source";
377
+ /**
378
+ * Passed to the react-fancy `FauxClient` frame a rendered file sits in (zoom
379
+ * and full screen, revealed on hover): `variant`, a controlled `zoom`, …
380
+ */
381
+ frameProps?: Partial<FauxClientProps>;
357
382
  className?: string;
358
383
  style?: CSSProperties;
359
384
  }
@@ -374,7 +399,7 @@ interface FileViewerProps {
374
399
  * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
375
400
  * ```
376
401
  */
377
- declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
402
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, view, frameProps, className, style, }: FileViewerProps): react.JSX.Element;
378
403
  declare namespace FileViewer {
379
404
  var displayName: string;
380
405
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { RefObject, ReactNode, CSSProperties } from 'react';
3
3
  import { ThemeColors, DiffAnnotations, LineChangeType, Token, Tokenizer } from '@particle-academy/fancy-file-commons';
4
4
  export { ThemeColors, Token, TokenType, Tokenizer, languageFromFilename } from '@particle-academy/fancy-file-commons';
5
- import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
5
+ import { MediaKind, MediaViewerProps, FauxClientProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
6
6
 
7
7
  interface ThemeDefinition {
8
8
  /** Unique theme name */
@@ -281,7 +281,20 @@ type FileKind = {
281
281
  } | {
282
282
  kind: "media";
283
283
  mediaKind: MediaKind;
284
+ }
285
+ /** Read as a reader sees it (rendered), not as source. `language` is for the source view. */
286
+ | {
287
+ kind: "document";
288
+ format: DocumentFormat;
289
+ language: string;
290
+ }
291
+ /** An Office file: binary, so never text. */
292
+ | {
293
+ kind: "office";
294
+ officeKind: OfficeKind;
284
295
  };
296
+ type DocumentFormat = "markdown" | "html" | "text";
297
+ type OfficeKind = "docx" | "xlsx" | "pptx";
285
298
  interface ResolveFileKindInput {
286
299
  /** File name or path — sniffed for an extension (and used for the media check). */
287
300
  filename?: string;
@@ -354,6 +367,18 @@ interface FileViewerProps {
354
367
  maxHeight?: number;
355
368
  /** Extra props forwarded to `<MediaViewer>` for media files. */
356
369
  mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
370
+ /**
371
+ * How a **document** (Markdown, HTML, plain text) is shown. `"rendered"`
372
+ * (default): as a reader sees it, through react-fancy's `ContentRenderer`
373
+ * (sanitised; a file is untrusted). `"source"`: in the code editor. Editing
374
+ * (`readOnly={false}`) and landing on a `line` always show the source.
375
+ */
376
+ view?: "rendered" | "source";
377
+ /**
378
+ * Passed to the react-fancy `FauxClient` frame a rendered file sits in (zoom
379
+ * and full screen, revealed on hover): `variant`, a controlled `zoom`, …
380
+ */
381
+ frameProps?: Partial<FauxClientProps>;
357
382
  className?: string;
358
383
  style?: CSSProperties;
359
384
  }
@@ -374,7 +399,7 @@ interface FileViewerProps {
374
399
  * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
375
400
  * ```
376
401
  */
377
- declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
402
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, view, frameProps, className, style, }: FileViewerProps): react.JSX.Element;
378
403
  declare namespace FileViewer {
379
404
  var displayName: string;
380
405
  }
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, ContentRenderer, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
2
+ import { cn, useControllableState, ContentRenderer, resolveMediaType, FauxClient, 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';
@@ -1720,10 +1720,45 @@ function MarkdownEditor({
1720
1720
  );
1721
1721
  }
1722
1722
  MarkdownEditor.displayName = "MarkdownEditor";
1723
+ var OFFICE_EXT = { docx: "docx", xlsx: "xlsx", xlsm: "xlsx", pptx: "pptx" };
1724
+ var OFFICE_MIME = {
1725
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
1726
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
1727
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx"
1728
+ };
1729
+ var DOCUMENT_EXT = {
1730
+ md: "markdown",
1731
+ markdown: "markdown",
1732
+ html: "html",
1733
+ htm: "html",
1734
+ txt: "text",
1735
+ text: "text"
1736
+ };
1737
+ var DOCUMENT_MIME = {
1738
+ "text/markdown": "markdown",
1739
+ "text/x-markdown": "markdown",
1740
+ "text/html": "html",
1741
+ "text/plain": "text"
1742
+ };
1743
+ function extensionOf(filename) {
1744
+ const name = (filename ?? "").split(/[?#]/)[0].split("/").pop() ?? "";
1745
+ const dot = name.lastIndexOf(".");
1746
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
1747
+ }
1723
1748
  function resolveFileKind({ filename, mime }) {
1724
1749
  const media = resolveMediaType({ mime, src: filename });
1725
1750
  if (media !== "unknown") return { kind: "media", mediaKind: media };
1726
- return { kind: "text", language: languageFromFilename(filename) };
1751
+ const type = (mime ?? "").split(";")[0].trim().toLowerCase();
1752
+ const ext = extensionOf(filename);
1753
+ const office = OFFICE_MIME[type] ?? OFFICE_EXT[ext];
1754
+ if (office) return { kind: "office", officeKind: office };
1755
+ const language = languageFromFilename(filename);
1756
+ const format = DOCUMENT_EXT[ext] ?? (language === "plaintext" ? DOCUMENT_MIME[type] : void 0);
1757
+ if (format) {
1758
+ const lang = language !== "plaintext" ? language : format === "markdown" ? "markdown" : format === "html" ? "html" : "plaintext";
1759
+ return { kind: "document", format, language: lang };
1760
+ }
1761
+ return { kind: "text", language };
1727
1762
  }
1728
1763
  function FileViewer({
1729
1764
  filename,
@@ -1742,12 +1777,48 @@ function FileViewer({
1742
1777
  minHeight,
1743
1778
  maxHeight,
1744
1779
  mediaProps,
1780
+ view = "rendered",
1781
+ frameProps,
1745
1782
  className,
1746
1783
  style
1747
1784
  }) {
1748
1785
  const resolved = fileKind ?? resolveFileKind({ filename, mime });
1786
+ if (resolved.kind === "document" && view === "rendered" && readOnly && line == null) {
1787
+ return /* @__PURE__ */ jsx(
1788
+ "div",
1789
+ {
1790
+ "data-fancy-file-viewer": "",
1791
+ "data-kind": "document",
1792
+ "data-format": resolved.format,
1793
+ className,
1794
+ style,
1795
+ children: /* @__PURE__ */ jsx(FauxClient, { variant: "device", controls: true, fill: true, bodyClassName: "overflow-auto", ...frameProps, children: /* @__PURE__ */ jsx("div", { className: "px-6 py-5", children: /* @__PURE__ */ jsx(
1796
+ ContentRenderer,
1797
+ {
1798
+ value: resolved.format === "text" ? textToHtml(value) : value,
1799
+ format: resolved.format === "markdown" ? "markdown" : "html"
1800
+ }
1801
+ ) }) })
1802
+ }
1803
+ );
1804
+ }
1805
+ if (resolved.kind === "office") {
1806
+ return /* @__PURE__ */ jsx(
1807
+ "div",
1808
+ {
1809
+ "data-fancy-file-viewer": "",
1810
+ "data-kind": "office",
1811
+ "data-office-kind": resolved.officeKind,
1812
+ className: cn("overflow-hidden", className),
1813
+ style,
1814
+ children: /* @__PURE__ */ jsx(MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" })
1815
+ }
1816
+ );
1817
+ }
1749
1818
  if (resolved.kind === "media") {
1750
1819
  const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1820
+ restMediaProps.imageProps = { frame: true, frameProps, ...restMediaProps.imageProps };
1821
+ restMediaProps.pdfProps = { frame: true, frameProps, ...restMediaProps.pdfProps };
1751
1822
  const fill = resolved.mediaKind !== "audio";
1752
1823
  return /* @__PURE__ */ jsx(
1753
1824
  "div",
@@ -1800,6 +1871,10 @@ function FileViewer({
1800
1871
  );
1801
1872
  }
1802
1873
  FileViewer.displayName = "FileViewer";
1874
+ var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1875
+ function textToHtml(text) {
1876
+ return text.replace(/\r\n?/g, "\n").split(/\n{2,}/).filter((block) => block.trim() !== "").map((block) => `<p>${block.replace(/[&<>"']/g, (c) => ESCAPES[c]).replace(/\n/g, "<br>")}</p>`).join("");
1877
+ }
1803
1878
 
1804
1879
  // src/engine/markdown-render.ts
1805
1880
  function escapeHtml(s) {