Ship file viewer, editor improvements, and private package releases
This commit is contained in:
1 parent
645914955c
commit
a4a31032b9
158 files changed
+10523
-137
No files matched your search
@@ -0,0 +1,259 @@
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user