260 lines
7.3 KiB
TypeScript
260 lines
7.3 KiB
TypeScript
import { requireConverter } from "./convert/client.ts";
|
|
import { mountArchive } from "./renderers/archive.ts";
|
|
import { mountConverted } from "./renderers/converted.ts";
|
|
import { mountDocx } from "./renderers/docx.ts";
|
|
import { mountOpenDocument, mountPresentation } from "./renderers/office.ts";
|
|
import { mountRtf } from "./renderers/rtf.ts";
|
|
import { mountCode } from "./renderers/code.ts";
|
|
import { mountHtml } from "./renderers/html.ts";
|
|
import { mountImage } from "./renderers/image.ts";
|
|
import { mountMedia } from "./renderers/media.ts";
|
|
import { mountJson } from "./renderers/json.ts";
|
|
import { mountPdf } from "./renderers/pdf.ts";
|
|
import { mountSvg } from "./renderers/svg.ts";
|
|
import { mountSheet } from "./renderers/sheet.ts";
|
|
import { mountTable } from "./renderers/table.ts";
|
|
import { mountXml } from "./renderers/xml.ts";
|
|
import { mountZip } from "./renderers/zip.ts";
|
|
import { loadOptional } from "./optional/load.ts";
|
|
import type { Renderer, ViewerKind } from "./types.ts";
|
|
|
|
const MB = 1024 * 1024;
|
|
|
|
/** Terminal rung of the ladder: metadata and a download link, never a preview. */
|
|
export const downloadRenderer: Renderer = {
|
|
kind: "binary",
|
|
exts: [],
|
|
tier: 1,
|
|
component: "DownloadCard",
|
|
maxInlineBytes: 0,
|
|
};
|
|
|
|
const builtins: Renderer[] = [
|
|
{
|
|
kind: "markup",
|
|
exts: [".html", ".htm"],
|
|
tier: 2,
|
|
component: "HtmlViewer",
|
|
maxInlineBytes: 2 * MB,
|
|
mount: mountHtml,
|
|
},
|
|
{
|
|
kind: "image",
|
|
exts: [],
|
|
tier: 1,
|
|
component: "ImageViewer",
|
|
maxInlineBytes: 32 * MB,
|
|
streaming: true,
|
|
mount: mountImage,
|
|
},
|
|
{
|
|
kind: "pdf",
|
|
exts: [],
|
|
tier: 1,
|
|
component: "PdfViewer",
|
|
maxInlineBytes: 32 * MB,
|
|
streaming: true,
|
|
mount: mountPdf,
|
|
},
|
|
...(["audio", "video"] as const).map((kind) => ({
|
|
kind,
|
|
exts: [],
|
|
tier: 1 as const,
|
|
component: "MediaViewer",
|
|
maxInlineBytes: 64 * MB,
|
|
streaming: true,
|
|
mount: mountMedia,
|
|
})),
|
|
// Tier 2: in-package parsers, no runtime dependency. Extension-specific, so a
|
|
// parse failure falls through to the source view below.
|
|
{
|
|
kind: "sheet" as const,
|
|
exts: [".csv", ".tsv"],
|
|
tier: 2 as const,
|
|
component: "TableViewer",
|
|
maxInlineBytes: 8 * MB,
|
|
mount: mountTable,
|
|
},
|
|
{
|
|
kind: "code" as const,
|
|
exts: [".json"],
|
|
tier: 2 as const,
|
|
component: "JsonViewer",
|
|
maxInlineBytes: 8 * MB,
|
|
mount: mountJson,
|
|
},
|
|
{
|
|
kind: "code" as const,
|
|
exts: [".xml"],
|
|
tier: 2 as const,
|
|
component: "XmlViewer",
|
|
maxInlineBytes: 8 * MB,
|
|
mount: mountXml,
|
|
},
|
|
{
|
|
kind: "vector" as const,
|
|
exts: [],
|
|
tier: 2 as const,
|
|
component: "SvgViewer",
|
|
maxInlineBytes: 2 * MB,
|
|
mount: mountSvg,
|
|
},
|
|
{
|
|
kind: "archive" as const,
|
|
exts: [".tar", ".gz", ".tgz"],
|
|
tier: 2 as const,
|
|
component: "ArchiveViewer",
|
|
maxInlineBytes: 64 * MB,
|
|
mount: mountArchive,
|
|
},
|
|
...(["text", "code", "markup"] as const).map((kind) => ({
|
|
kind,
|
|
exts: [],
|
|
tier: 1 as const,
|
|
component: "CodeViewer",
|
|
maxInlineBytes: 2 * MB,
|
|
mount: mountCode,
|
|
})),
|
|
];
|
|
|
|
/**
|
|
* Tier 2 renderers backed by an optional dependency. `load` imports the runtime
|
|
* asset the plugin only publishes when the package is installed, so a missing
|
|
* dependency is reported the same way a missing converter is: skip and move on.
|
|
*/
|
|
builtins.push(
|
|
{
|
|
kind: "sheet",
|
|
exts: [".xls", ".xlsx", ".ods"],
|
|
tier: 2,
|
|
component: "SheetViewer",
|
|
maxInlineBytes: 32 * MB,
|
|
load: (options) => loadOptional("viewer-sheet", options?.baseURI),
|
|
mount: mountSheet,
|
|
},
|
|
{
|
|
kind: "archive",
|
|
exts: [".zip"],
|
|
tier: 2,
|
|
component: "ZipViewer",
|
|
maxInlineBytes: 64 * MB,
|
|
load: (options) => loadOptional("viewer-zip", options?.baseURI),
|
|
mount: mountZip,
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Tier 3: the browser cannot render these at all, so they are handed to a
|
|
* server-side converter. Each carries a `load` that asks the server once
|
|
* whether that format is actually convertible here; when conversion is not
|
|
* enabled the load fails and the ladder moves on to the download card.
|
|
*/
|
|
const CONVERTIBLE: [ViewerKind, string[]][] = [
|
|
["doc", [".doc", ".docx", ".dotx", ".odt", ".rtf", ".ppt", ".pptx", ".ppsx", ".potx", ".odp"]],
|
|
["sheet", [".xls", ".xlsx", ".ods"]],
|
|
["binary", [".psd", ".ai", ".tif", ".tiff"]],
|
|
["video", [".avi", ".mkv"]],
|
|
["archive", [".zip", ".rar", ".7z"]],
|
|
];
|
|
|
|
for (const [kind, exts] of CONVERTIBLE)
|
|
for (const ext of exts)
|
|
builtins.push({
|
|
kind,
|
|
exts: [ext],
|
|
tier: 3,
|
|
component: "ConvertedViewer",
|
|
// The preview comes from the conversion endpoint, so the original bytes
|
|
// are never needed here: streaming keeps the runtime from downloading a
|
|
// whole design file or video into memory only to discard it.
|
|
streaming: true,
|
|
maxInlineBytes: Number.POSITIVE_INFINITY,
|
|
load: (options) => requireConverter([ext], options),
|
|
mount: mountConverted,
|
|
});
|
|
|
|
/**
|
|
* In-package office readers. Each recovers content and structure, not a page
|
|
* layout, so all of them sit *behind* the converter's PDF and render only when
|
|
* no converter is reachable — the same trade the DOCX path already makes.
|
|
*
|
|
* `.ods` is the exception in spirit rather than order: a spreadsheet is data,
|
|
* and the table view is the better reading of it, so it is registered ahead of
|
|
* the converter alongside `.xlsx`.
|
|
*/
|
|
builtins.push(
|
|
{
|
|
kind: "doc",
|
|
exts: [".docx"],
|
|
tier: 2,
|
|
component: "DocxViewer",
|
|
maxInlineBytes: 32 * MB,
|
|
load: (options) => loadOptional("viewer-docx", options?.baseURI),
|
|
mount: mountDocx,
|
|
},
|
|
{
|
|
kind: "doc",
|
|
exts: [".pptx", ".ppsx", ".potx"],
|
|
tier: 2,
|
|
component: "SlidesViewer",
|
|
maxInlineBytes: 64 * MB,
|
|
load: (options) => loadOptional("viewer-office", options?.baseURI),
|
|
mount: mountPresentation,
|
|
},
|
|
{
|
|
kind: "doc",
|
|
// `.ods` is here as well as on the spreadsheet reader: SheetJS handles most
|
|
// OpenDocument workbooks, and this is the ladder's next rung when it cannot.
|
|
exts: [".odt", ".odp", ".ods"],
|
|
tier: 2,
|
|
component: "OpenDocumentViewer",
|
|
maxInlineBytes: 64 * MB,
|
|
load: (options) => loadOptional("viewer-office", options?.baseURI),
|
|
mount: mountOpenDocument,
|
|
},
|
|
{
|
|
// No dependency and no converter: RTF is text once the control words go.
|
|
kind: "doc",
|
|
exts: [".rtf"],
|
|
tier: 2,
|
|
component: "RtfViewer",
|
|
maxInlineBytes: 16 * MB,
|
|
mount: mountRtf,
|
|
},
|
|
);
|
|
|
|
export class RendererRegistry {
|
|
private renderers: Renderer[] = [...builtins];
|
|
|
|
fork(): RendererRegistry {
|
|
const registry = new RendererRegistry();
|
|
registry.renderers = [...this.renderers];
|
|
return registry;
|
|
}
|
|
|
|
register(renderer: Renderer): () => void {
|
|
const entry = {
|
|
...renderer,
|
|
exts: renderer.exts.map((ext) => "." + ext.replace(/^\./, "").toLowerCase()),
|
|
};
|
|
this.renderers.unshift(entry);
|
|
return () => {
|
|
this.renderers = this.renderers.filter((item) => item !== entry);
|
|
};
|
|
}
|
|
|
|
/** Extension-specific renderers outrank kind-wide ones. */
|
|
candidates(kind: ViewerKind, ext: string): Renderer[] {
|
|
return [
|
|
...this.renderers.filter((item) => item.exts.includes(ext)),
|
|
...this.renderers.filter((item) => item.kind === kind && item.exts.length === 0),
|
|
];
|
|
}
|
|
}
|
|
|
|
export const defaultRegistry = new RendererRegistry();
|
|
|
|
export function registerRenderer(renderer: Renderer): () => void {
|
|
return defaultRegistry.register(renderer);
|
|
}
|