@particle-academy/fancy-code 0.12.1 → 0.13.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/CHANGELOG.md CHANGED
@@ -14,6 +14,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.13.0] - 2026-10-19
18
+
19
+ ### Added
20
+
21
+ - **Documents render as a reader sees them.** `FileViewer` shows Markdown, HTML
22
+ and plain text through react-fancy's `ContentRenderer`, sanitised (a file is
23
+ untrusted), in the `FauxClient` frame every file viewer now carries: zoom and
24
+ full screen, revealed on hover. Plain text keeps its paragraphs and line
25
+ breaks and is escaped, so `*x*` stays asterisks. `view="source"` shows the
26
+ source in the code editor instead, as do editing (`readOnly={false}`) and
27
+ landing on a `line`. `frameProps` reaches the frame.
28
+ - **Office files are recognised.** `.docx`, `.xlsx` and `.pptx` (by extension
29
+ or MIME type) resolve to `{ kind: "office", officeKind }`. They show
30
+ MediaViewer's no-preview card with a download until their viewers are wired
31
+ in. They used to be handed to the code editor as text.
32
+
33
+ ### Changed
34
+
35
+ - **BREAKING:** a `.md`, `.html` or `.txt` file in `FileViewer` is rendered,
36
+ not shown as source. **What to do:** to keep the source, pass
37
+ `view="source"`. A viewer that lands on a `line` (a link into a file) already
38
+ shows the source, so file links need nothing.
39
+ - **BREAKING (types):** `FileKind` gains `{ kind: "document"; format; language }`
40
+ and `{ kind: "office"; officeKind }`. **What to do:** only if you `switch` on
41
+ `resolveFileKind(...).kind` exhaustively: add the two cases, or a `default`.
42
+ `notes.txt` now resolves to `document`, not `text`.
43
+ - The extension decides over a generic MIME type: a host that serves `app.ts`
44
+ as `text/plain` still gets the code editor.
45
+ - **Requires react-fancy 5.48.1 or later** (peer `>=5.48.1 <6.0.0`, was
46
+ `>=4.9.0`), for the frame and for the sanitiser that strips `<title>`.
47
+
17
48
  ## [0.12.1] - 2026-10-18
18
49
 
19
50
  ### Security
package/dist/index.cjs CHANGED
@@ -1721,10 +1721,45 @@ function MarkdownEditor({
1721
1721
  );
1722
1722
  }
1723
1723
  MarkdownEditor.displayName = "MarkdownEditor";
1724
+ var OFFICE_EXT = { docx: "docx", xlsx: "xlsx", xlsm: "xlsx", pptx: "pptx" };
1725
+ var OFFICE_MIME = {
1726
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
1727
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
1728
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx"
1729
+ };
1730
+ var DOCUMENT_EXT = {
1731
+ md: "markdown",
1732
+ markdown: "markdown",
1733
+ html: "html",
1734
+ htm: "html",
1735
+ txt: "text",
1736
+ text: "text"
1737
+ };
1738
+ var DOCUMENT_MIME = {
1739
+ "text/markdown": "markdown",
1740
+ "text/x-markdown": "markdown",
1741
+ "text/html": "html",
1742
+ "text/plain": "text"
1743
+ };
1744
+ function extensionOf(filename) {
1745
+ const name = (filename ?? "").split(/[?#]/)[0].split("/").pop() ?? "";
1746
+ const dot = name.lastIndexOf(".");
1747
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
1748
+ }
1724
1749
  function resolveFileKind({ filename, mime }) {
1725
1750
  const media = reactFancy.resolveMediaType({ mime, src: filename });
1726
1751
  if (media !== "unknown") return { kind: "media", mediaKind: media };
1727
- return { kind: "text", language: fancyFileCommons.languageFromFilename(filename) };
1752
+ const type = (mime ?? "").split(";")[0].trim().toLowerCase();
1753
+ const ext = extensionOf(filename);
1754
+ const office = OFFICE_MIME[type] ?? OFFICE_EXT[ext];
1755
+ if (office) return { kind: "office", officeKind: office };
1756
+ const language = fancyFileCommons.languageFromFilename(filename);
1757
+ const format = DOCUMENT_EXT[ext] ?? (language === "plaintext" ? DOCUMENT_MIME[type] : void 0);
1758
+ if (format) {
1759
+ const lang = language !== "plaintext" ? language : format === "markdown" ? "markdown" : format === "html" ? "html" : "plaintext";
1760
+ return { kind: "document", format, language: lang };
1761
+ }
1762
+ return { kind: "text", language };
1728
1763
  }
1729
1764
  function FileViewer({
1730
1765
  filename,
@@ -1743,10 +1778,44 @@ function FileViewer({
1743
1778
  minHeight,
1744
1779
  maxHeight,
1745
1780
  mediaProps,
1781
+ view = "rendered",
1782
+ frameProps,
1746
1783
  className,
1747
1784
  style
1748
1785
  }) {
1749
1786
  const resolved = fileKind ?? resolveFileKind({ filename, mime });
1787
+ if (resolved.kind === "document" && view === "rendered" && readOnly && line == null) {
1788
+ return /* @__PURE__ */ jsxRuntime.jsx(
1789
+ "div",
1790
+ {
1791
+ "data-fancy-file-viewer": "",
1792
+ "data-kind": "document",
1793
+ "data-format": resolved.format,
1794
+ className,
1795
+ style,
1796
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.FauxClient, { variant: "device", controls: true, fill: true, bodyClassName: "overflow-auto", ...frameProps, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-5", children: /* @__PURE__ */ jsxRuntime.jsx(
1797
+ reactFancy.ContentRenderer,
1798
+ {
1799
+ value: resolved.format === "text" ? textToHtml(value) : value,
1800
+ format: resolved.format === "markdown" ? "markdown" : "html"
1801
+ }
1802
+ ) }) })
1803
+ }
1804
+ );
1805
+ }
1806
+ if (resolved.kind === "office") {
1807
+ return /* @__PURE__ */ jsxRuntime.jsx(
1808
+ "div",
1809
+ {
1810
+ "data-fancy-file-viewer": "",
1811
+ "data-kind": "office",
1812
+ "data-office-kind": resolved.officeKind,
1813
+ className: reactFancy.cn("overflow-hidden", className),
1814
+ style,
1815
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" })
1816
+ }
1817
+ );
1818
+ }
1750
1819
  if (resolved.kind === "media") {
1751
1820
  const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1752
1821
  const fill = resolved.mediaKind !== "audio";
@@ -1801,6 +1870,10 @@ function FileViewer({
1801
1870
  );
1802
1871
  }
1803
1872
  FileViewer.displayName = "FileViewer";
1873
+ var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1874
+ function textToHtml(text) {
1875
+ 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("");
1876
+ }
1804
1877
 
1805
1878
  // src/engine/markdown-render.ts
1806
1879
  function escapeHtml(s) {