Path: blob/main/src/format/html/format-html-notebook-preview.ts
6450 views
/*1* format-html-notebook-preview.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/5import { asArray } from "../../core/array.ts";6import * as ld from "../../core/lodash.ts";78import {9kDownloadUrl,10kNotebookPreviewOptions,11} from "../../config/constants.ts";12import { Format, NotebookPreviewDescriptor } from "../../config/types.ts";1314import { RenderServices } from "../../command/render/types.ts";1516import {17basename,18dirname,19isAbsolute,20join,21relative,22} from "../../deno_ral/path.ts";23import { pathWithForwardSlashes } from "../../core/path.ts";24import { ProjectContext } from "../../project/types.ts";25import { projectIsBook } from "../../project/project-shared.ts";26import {27kHtmlPreview,28NotebookPreviewOptions,29} from "../../render/notebook/notebook-types.ts";30import { kRenderedIPynb } from "../../render/notebook/notebook-types.ts";31import { InternalError } from "../../core/lib/error.ts";32import { logProgress } from "../../core/log.ts";3334export interface NotebookPreview {35title: string;36href: string;37filename?: string;38supporting?: string[];39resources?: string[];40order?: number;41}4243export interface NotebookPreviewTask {44input: string;45nbPath: string;46title?: string;47nbPreviewFile?: string;48order?: number;49callback?: (nbPreview: NotebookPreview) => void;50}5152export const notebookPreviewer = (53nbView: boolean | NotebookPreviewDescriptor | NotebookPreviewDescriptor[],54format: Format,55services: RenderServices,56project: ProjectContext,57) => {58const isBook = projectIsBook(project);59const previewQueue: NotebookPreviewTask[] = [];60const outputDir = project?.config?.project["output-dir"];6162const nbDescriptors: Record<string, NotebookPreviewDescriptor> = {};63if (nbView) {64if (typeof nbView !== "boolean") {65asArray(nbView).forEach((view) => {66const existingView = nbDescriptors[view.notebook];67nbDescriptors[view.notebook] = {68...existingView,69...view,70};71});72}73}74const descriptor = (75notebook: string,76) => {77return nbDescriptors[notebook];78};7980const enQueuePreview = (81input: string,82nbAbsPath: string,83title?: string,84order?: number,85callback?: (nbPreview: NotebookPreview) => void,86) => {87if (88!previewQueue.find((work) => {89return work.nbPath === nbAbsPath;90})91) {92// Try to provide a title93previewQueue.push({94input,95nbPath: nbAbsPath,96title: title,97callback,98order,99});100}101};102103const renderPreviews = async (output?: string, quiet?: boolean) => {104const rendered: Record<string, NotebookPreview> = {};105106const nbOptions = format107.metadata[kNotebookPreviewOptions] as NotebookPreviewOptions;108109const notebookPaths = previewQueue.map((work) => (work.nbPath));110const uniquePaths = ld.uniq(notebookPaths) as string[];111const toRenderPaths = uniquePaths.filter((nbPath) => {112return services.notebook.get(nbPath, project) === undefined;113});114const haveRenderedPaths: string[] = [];115if (toRenderPaths.length > 0 && !quiet) {116logProgress(117`Rendering notebook previews`,118);119}120const total = previewQueue.length;121let renderCount = 0;122for (let i = 0; i < total; i++) {123const work = previewQueue[i];124const { nbPath, input, title } = work;125if (126toRenderPaths.includes(nbPath) && !haveRenderedPaths.includes(nbPath) &&127!quiet128) {129logProgress(130`[${++renderCount}/${toRenderPaths.length}] ${basename(nbPath)}`,131);132haveRenderedPaths.push(nbPath);133}134135const nbDir = dirname(nbPath);136const filename = basename(nbPath);137const inputDir = dirname(input);138139if (nbView !== false) {140// Read options for this notebook141const descriptor: NotebookPreviewDescriptor | undefined =142nbDescriptors[relative(dirname(input), nbPath)];143const nbAbsPath = isAbsolute(nbPath) ? nbPath : join(inputDir, nbPath);144const nbContext = services.notebook;145const notebook = nbContext.get(nbAbsPath, project);146147const resolvedTitle = descriptor?.title || title ||148notebook?.metadata?.title || basename(nbAbsPath);149150const notebookIsQmd = !nbAbsPath.endsWith(".ipynb");151152// Ensure this has an rendered ipynb and an html preview153if (!notebook || !notebook[kHtmlPreview] || !notebook[kRenderedIPynb]) {154// Render an ipynb if needed155if (156(!notebook || !notebook[kRenderedIPynb]) &&157!descriptor?.[kDownloadUrl] &&158!isBook &&159!notebookIsQmd160) {161const renderedIpynb = await nbContext.render(162nbAbsPath,163format,164kRenderedIPynb,165services,166{167title: resolvedTitle,168filename: basename(nbAbsPath),169},170project,171);172if (renderedIpynb && (!project || !outputDir)) {173nbContext.preserve(nbAbsPath, kRenderedIPynb);174}175}176177// Render the HTML preview, if needed178if (!notebook || !notebook[kHtmlPreview]) {179const backHref = nbOptions && nbOptions.back && output180? relative(dirname(nbAbsPath), output)181: undefined;182183let downloadHref = basename(nbAbsPath);184let downloadFileName = basename(nbAbsPath);185// If this is an ipynb and there is a rendered version of it186// use that instead.187if (188notebook && notebook[kRenderedIPynb] &&189!notebookIsQmd190) {191downloadHref = relative(192dirname(nbAbsPath),193notebook[kRenderedIPynb].hrefPath,194);195downloadFileName = basename(notebook[kRenderedIPynb].hrefPath);196}197198const htmlPreview = await nbContext.render(199nbAbsPath,200format,201kHtmlPreview,202services,203{204title: resolvedTitle,205filename: basename(nbAbsPath),206backHref,207downloadHref,208downloadFile: downloadFileName,209},210project,211);212if (htmlPreview && (!project || !outputDir)) {213nbContext.preserve(nbAbsPath, kHtmlPreview);214}215}216}217218const renderedNotebook = nbContext.get(nbAbsPath, project);219if (!renderedNotebook || !renderedNotebook[kHtmlPreview]) {220throw new InternalError(221"We just ensured that notebooks had rendered previews, but the preview then didn't exist.",222);223}224225// Forward along resources and supporting files from previews226const supporting: string[] = [];227const resources: string[] = [];228if (renderedNotebook[kRenderedIPynb]) {229const renderedIpynb = renderedNotebook[kRenderedIPynb];230if (renderedIpynb) {231if (project) {232supporting.push(233relative(project.dir, renderedIpynb.hrefPath),234);235} else {236supporting.push(renderedIpynb.hrefPath);237}238supporting.push(...renderedIpynb.supporting);239resources.push(...renderedIpynb.resourceFiles.files);240}241}242243if (renderedNotebook[kHtmlPreview]) {244const htmlPreview = renderedNotebook[kHtmlPreview];245if (htmlPreview) {246if (project) {247supporting.push(relative(project.dir, htmlPreview.hrefPath));248} else {249supporting.push(htmlPreview.hrefPath);250}251supporting.push(...htmlPreview.supporting);252resources.push(...htmlPreview.resourceFiles.files);253}254}255256// Compute the final preview information that will be used257// to form links to this notebook258const nbPreview = {259title: resolvedTitle,260href: descriptor?.url ||261relative(inputDir, renderedNotebook[kHtmlPreview].hrefPath),262supporting,263resources,264order: work.order,265};266rendered[work.nbPath] = nbPreview;267if (work.callback) {268work.callback(nbPreview);269}270} else {271const nbPreview = {272href: pathWithForwardSlashes(join(nbDir, filename)),273title: title || filename,274filename,275order: work.order,276};277rendered[work.nbPath] = nbPreview;278if (work.callback) {279work.callback(nbPreview);280}281}282}283return rendered;284};285286return {287enQueuePreview,288renderPreviews,289descriptor,290};291};292293294