@particle-academy/fancy-code 0.14.0 → 0.15.1

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,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode, CSSProperties } from 'react';
2
+ import { RefObject, ReactNode, CSSProperties, ComponentType } 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
5
  import { MediaKind, MediaViewerProps, FauxClientProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
@@ -379,6 +379,12 @@ interface FileViewerProps {
379
379
  * and full screen, revealed on hover): `variant`, a controlled `zoom`, …
380
380
  */
381
381
  frameProps?: Partial<FauxClientProps>;
382
+ /**
383
+ * An Office file's contents, already read on the server (dark-slide /
384
+ * holy-sheet / last-word), for the viewer registered with
385
+ * `registerOfficeViewer`. Preferred over reading `src` in the browser.
386
+ */
387
+ document?: unknown;
382
388
  className?: string;
383
389
  style?: CSSProperties;
384
390
  }
@@ -399,11 +405,53 @@ interface FileViewerProps {
399
405
  * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
400
406
  * ```
401
407
  */
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;
408
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, view, frameProps, document, className, style, }: FileViewerProps): react.JSX.Element;
403
409
  declare namespace FileViewer {
404
410
  var displayName: string;
405
411
  }
406
412
 
413
+ /** What a registered Office viewer is given. */
414
+ interface OfficeViewerProps {
415
+ /** The file's contents as our reader returns them (a dark-slide deck, a holy-sheet workbook, a last-word doc). */
416
+ document: unknown;
417
+ filename?: string;
418
+ /** Passed through from `FileViewer`, for viewers that sit in the FauxClient frame. */
419
+ frameProps?: Partial<FauxClientProps>;
420
+ }
421
+ interface OfficeViewerOptions {
422
+ /**
423
+ * Read the raw file in the browser, for when the app has only a URL. Give it
424
+ * our JS reader, e.g. `(bytes) => new PptxReader().read(bytes)`. Without it,
425
+ * a file with no server-read `document` gets the download card.
426
+ */
427
+ read?: (bytes: Uint8Array) => unknown;
428
+ }
429
+ interface Registration {
430
+ View: ComponentType<OfficeViewerProps>;
431
+ read?: (bytes: Uint8Array) => unknown;
432
+ }
433
+ /**
434
+ * Show one Office format in `FileViewer` with a kit viewer. Call once at app
435
+ * start, per format you want:
436
+ *
437
+ * ```ts
438
+ * import { registerOfficeViewer } from "@particle-academy/fancy-code";
439
+ * import { DeckFileView } from "@particle-academy/fancy-slides";
440
+ * import { PptxReader } from "@particle-academy/dark-slide";
441
+ *
442
+ * registerOfficeViewer("pptx", DeckFileView, { read: (b) => new PptxReader().read(b) });
443
+ * ```
444
+ *
445
+ * A registry rather than an import because fancy-code must not depend on the
446
+ * slide or sheet packages: an app that has neither (and does not want them)
447
+ * would otherwise fail to build on a file type it never opens.
448
+ */
449
+ declare function registerOfficeViewer(kind: OfficeKind, View: ComponentType<OfficeViewerProps>, options?: OfficeViewerOptions): void;
450
+ /** The viewer registered for a format, if any. */
451
+ declare function officeViewerFor(kind: OfficeKind): Registration | undefined;
452
+ /** Forget every registration (tests, hot reload). */
453
+ declare function clearOfficeViewers(): void;
454
+
407
455
  /**
408
456
  * Diff-gutter model — the pure mapping between fancy-file-commons line
409
457
  * annotations and what the CodeEditor gutter renders per line-number row.
@@ -503,4 +551,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
503
551
  */
504
552
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
505
553
 
506
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type GutterDiffMark, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeDefinition, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
554
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type DocumentFormat, type FileKind, FileViewer, type FileViewerProps, type GutterDiffMark, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type OfficeKind, type OfficeViewerOptions, type OfficeViewerProps, type ResolveFileKindInput, type ThemeDefinition, clearOfficeViewers, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, officeViewerFor, registerLanguage, registerOfficeViewer, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode, CSSProperties } from 'react';
2
+ import { RefObject, ReactNode, CSSProperties, ComponentType } 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
5
  import { MediaKind, MediaViewerProps, FauxClientProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
@@ -379,6 +379,12 @@ interface FileViewerProps {
379
379
  * and full screen, revealed on hover): `variant`, a controlled `zoom`, …
380
380
  */
381
381
  frameProps?: Partial<FauxClientProps>;
382
+ /**
383
+ * An Office file's contents, already read on the server (dark-slide /
384
+ * holy-sheet / last-word), for the viewer registered with
385
+ * `registerOfficeViewer`. Preferred over reading `src` in the browser.
386
+ */
387
+ document?: unknown;
382
388
  className?: string;
383
389
  style?: CSSProperties;
384
390
  }
@@ -399,11 +405,53 @@ interface FileViewerProps {
399
405
  * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
400
406
  * ```
401
407
  */
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;
408
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, view, frameProps, document, className, style, }: FileViewerProps): react.JSX.Element;
403
409
  declare namespace FileViewer {
404
410
  var displayName: string;
405
411
  }
406
412
 
413
+ /** What a registered Office viewer is given. */
414
+ interface OfficeViewerProps {
415
+ /** The file's contents as our reader returns them (a dark-slide deck, a holy-sheet workbook, a last-word doc). */
416
+ document: unknown;
417
+ filename?: string;
418
+ /** Passed through from `FileViewer`, for viewers that sit in the FauxClient frame. */
419
+ frameProps?: Partial<FauxClientProps>;
420
+ }
421
+ interface OfficeViewerOptions {
422
+ /**
423
+ * Read the raw file in the browser, for when the app has only a URL. Give it
424
+ * our JS reader, e.g. `(bytes) => new PptxReader().read(bytes)`. Without it,
425
+ * a file with no server-read `document` gets the download card.
426
+ */
427
+ read?: (bytes: Uint8Array) => unknown;
428
+ }
429
+ interface Registration {
430
+ View: ComponentType<OfficeViewerProps>;
431
+ read?: (bytes: Uint8Array) => unknown;
432
+ }
433
+ /**
434
+ * Show one Office format in `FileViewer` with a kit viewer. Call once at app
435
+ * start, per format you want:
436
+ *
437
+ * ```ts
438
+ * import { registerOfficeViewer } from "@particle-academy/fancy-code";
439
+ * import { DeckFileView } from "@particle-academy/fancy-slides";
440
+ * import { PptxReader } from "@particle-academy/dark-slide";
441
+ *
442
+ * registerOfficeViewer("pptx", DeckFileView, { read: (b) => new PptxReader().read(b) });
443
+ * ```
444
+ *
445
+ * A registry rather than an import because fancy-code must not depend on the
446
+ * slide or sheet packages: an app that has neither (and does not want them)
447
+ * would otherwise fail to build on a file type it never opens.
448
+ */
449
+ declare function registerOfficeViewer(kind: OfficeKind, View: ComponentType<OfficeViewerProps>, options?: OfficeViewerOptions): void;
450
+ /** The viewer registered for a format, if any. */
451
+ declare function officeViewerFor(kind: OfficeKind): Registration | undefined;
452
+ /** Forget every registration (tests, hot reload). */
453
+ declare function clearOfficeViewers(): void;
454
+
407
455
  /**
408
456
  * Diff-gutter model — the pure mapping between fancy-file-commons line
409
457
  * annotations and what the CodeEditor gutter renders per line-number row.
@@ -503,4 +551,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
503
551
  */
504
552
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
505
553
 
506
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type GutterDiffMark, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeDefinition, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
554
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type DocumentFormat, type FileKind, FileViewer, type FileViewerProps, type GutterDiffMark, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type OfficeKind, type OfficeViewerOptions, type OfficeViewerProps, type ResolveFileKindInput, type ThemeDefinition, clearOfficeViewers, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, officeViewerFor, registerLanguage, registerOfficeViewer, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
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, FauxClient, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
2
+ import { cn, useControllableState, ContentRenderer, resolveMediaType, FauxClient, MediaViewer, Callout, 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,6 +1720,18 @@ function MarkdownEditor({
1720
1720
  );
1721
1721
  }
1722
1722
  MarkdownEditor.displayName = "MarkdownEditor";
1723
+
1724
+ // src/office-viewers.ts
1725
+ var registry = /* @__PURE__ */ new Map();
1726
+ function registerOfficeViewer(kind, View, options = {}) {
1727
+ registry.set(kind, { View, read: options.read });
1728
+ }
1729
+ function officeViewerFor(kind) {
1730
+ return registry.get(kind);
1731
+ }
1732
+ function clearOfficeViewers() {
1733
+ registry.clear();
1734
+ }
1723
1735
  var OFFICE_EXT = { docx: "docx", xlsx: "xlsx", xlsm: "xlsx", pptx: "pptx" };
1724
1736
  var OFFICE_MIME = {
1725
1737
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
@@ -1779,6 +1791,7 @@ function FileViewer({
1779
1791
  mediaProps,
1780
1792
  view = "rendered",
1781
1793
  frameProps,
1794
+ document,
1782
1795
  className,
1783
1796
  style
1784
1797
  }) {
@@ -1811,7 +1824,17 @@ function FileViewer({
1811
1824
  "data-office-kind": resolved.officeKind,
1812
1825
  className: cn("overflow-hidden", className),
1813
1826
  style,
1814
- children: /* @__PURE__ */ jsx(MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" })
1827
+ children: /* @__PURE__ */ jsx(
1828
+ OfficeFile,
1829
+ {
1830
+ kind: resolved.officeKind,
1831
+ document,
1832
+ src,
1833
+ mime,
1834
+ filename,
1835
+ frameProps
1836
+ }
1837
+ )
1815
1838
  }
1816
1839
  );
1817
1840
  }
@@ -1871,6 +1894,57 @@ function FileViewer({
1871
1894
  );
1872
1895
  }
1873
1896
  FileViewer.displayName = "FileViewer";
1897
+ function OfficeFile({
1898
+ kind,
1899
+ document,
1900
+ src,
1901
+ mime,
1902
+ filename,
1903
+ frameProps
1904
+ }) {
1905
+ const registration = officeViewerFor(kind);
1906
+ const read = registration?.read;
1907
+ const [state, setState] = useState({ status: "idle" });
1908
+ useEffect(() => {
1909
+ if (!registration || document !== void 0 || !src || !read) return;
1910
+ let live = true;
1911
+ setState({ status: "reading" });
1912
+ fetch(src).then(async (response) => {
1913
+ if (!response.ok) throw new Error(`The file could not be fetched (${response.status}).`);
1914
+ const bytes = new Uint8Array(await response.arrayBuffer());
1915
+ const doc = read(bytes);
1916
+ if (live) setState({ status: "read", document: doc });
1917
+ }).catch((error) => {
1918
+ if (live) setState({ status: "failed", message: error instanceof Error ? error.message : String(error) });
1919
+ });
1920
+ return () => {
1921
+ live = false;
1922
+ };
1923
+ }, [registration, document, src, read]);
1924
+ const fallback = /* @__PURE__ */ jsx(MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" });
1925
+ if (!registration) return fallback;
1926
+ const View = registration.View;
1927
+ if (document !== void 0) return /* @__PURE__ */ jsx(View, { document, filename, frameProps });
1928
+ if (state.status === "read") return /* @__PURE__ */ jsx(View, { document: state.document, filename, frameProps });
1929
+ if (state.status === "reading") {
1930
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full items-center justify-center p-6 text-sm text-zinc-500", children: [
1931
+ "Opening ",
1932
+ filename ?? "the file",
1933
+ "\u2026"
1934
+ ] });
1935
+ }
1936
+ if (state.status === "failed") {
1937
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col gap-2 p-2", children: [
1938
+ /* @__PURE__ */ jsxs(Callout, { color: "red", children: [
1939
+ filename ?? "This file",
1940
+ " could not be opened: ",
1941
+ state.message
1942
+ ] }),
1943
+ fallback
1944
+ ] });
1945
+ }
1946
+ return fallback;
1947
+ }
1874
1948
  var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1875
1949
  function textToHtml(text) {
1876
1950
  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("");
@@ -2002,6 +2076,6 @@ function CodeEditorInputTag(props) {
2002
2076
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
2003
2077
  }
2004
2078
 
2005
- export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
2079
+ export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, clearOfficeViewers, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, officeViewerFor, registerLanguage, registerOfficeViewer, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
2006
2080
  //# sourceMappingURL=index.js.map
2007
2081
  //# sourceMappingURL=index.js.map