@particle-academy/fancy-code 0.12.0 → 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 +49 -0
- package/dist/index.cjs +94 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +95 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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,10 +1777,44 @@ 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 ?? {};
|
|
1751
1820
|
const fill = resolved.mediaKind !== "audio";
|
|
@@ -1800,6 +1869,10 @@ function FileViewer({
|
|
|
1800
1869
|
);
|
|
1801
1870
|
}
|
|
1802
1871
|
FileViewer.displayName = "FileViewer";
|
|
1872
|
+
var ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
1873
|
+
function textToHtml(text) {
|
|
1874
|
+
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("");
|
|
1875
|
+
}
|
|
1803
1876
|
|
|
1804
1877
|
// src/engine/markdown-render.ts
|
|
1805
1878
|
function escapeHtml(s) {
|
|
@@ -1808,8 +1881,27 @@ function escapeHtml(s) {
|
|
|
1808
1881
|
function escapeAttr(s) {
|
|
1809
1882
|
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
1810
1883
|
}
|
|
1884
|
+
var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
1885
|
+
function schemeOf(url) {
|
|
1886
|
+
let s = "";
|
|
1887
|
+
for (const ch of url) {
|
|
1888
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1889
|
+
if (code === 9 || code === 10 || code === 13) continue;
|
|
1890
|
+
if (s === "" && code <= 32) continue;
|
|
1891
|
+
s += ch;
|
|
1892
|
+
}
|
|
1893
|
+
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(s);
|
|
1894
|
+
return m ? m[1].toLowerCase() : null;
|
|
1895
|
+
}
|
|
1896
|
+
function safeUrl(url, kind) {
|
|
1897
|
+
const scheme = schemeOf(url);
|
|
1898
|
+
if (scheme === null) return true;
|
|
1899
|
+
if (scheme === "http" || scheme === "https") return true;
|
|
1900
|
+
if (kind === "link") return LINK_SCHEMES.has(scheme);
|
|
1901
|
+
return scheme === "data" && /^data:image\/(png|jpe?g|gif|webp|avif|bmp);/i.test(url.trim());
|
|
1902
|
+
}
|
|
1811
1903
|
function inline(text) {
|
|
1812
|
-
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>");
|
|
1904
|
+
return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => safeUrl(src, "image") ? `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"${title ? ` title="${escapeAttr(title)}"` : ""}>` : alt).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => safeUrl(href, "link") ? `<a href="${escapeAttr(href)}"${title ? ` title="${escapeAttr(title)}"` : ""} rel="noopener">${t}</a>` : t).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>");
|
|
1813
1905
|
}
|
|
1814
1906
|
function renderMarkdown(markdown) {
|
|
1815
1907
|
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|