Path: blob/main/src/format/html/format-html-notebook.ts
6450 views
/*1* format-html-notebook.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/5import { Document, Element } from "../../core/deno-dom.ts";6import * as ld from "../../core/lodash.ts";78import {9kNotebookLinks,10kNotebookView,11kNotebookViewStyle,12kRelatedNotebooksTitle,13kSourceNotebookPrefix,14} from "../../config/constants.ts";15import { Format } from "../../config/types.ts";1617import {18HtmlPostProcessResult,19RenderServices,20} from "../../command/render/types.ts";2122import { basename, dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";23import { ProjectContext } from "../../project/types.ts";24import {25NotebookPreview,26notebookPreviewer,27} from "./format-html-notebook-preview.ts";28import { projectIsBook } from "../../project/project-shared.ts";2930const kQuartoNbClass = "quarto-notebook";31const kQuartoCellContainerClass = "cell-container";32const kQuartoCellDecoratorClass = "cell-decorator";3334// Post processes the notebook and adds 'notebook' style affordances35export function notebookViewPostProcessor() {36return (doc: Document): Promise<HtmlPostProcessResult> => {37doc.body.classList.add(kQuartoNbClass);38const cells = doc.querySelectorAll("div.cell");39let cellCount = 0;40for (const cell of cells) {41const cellEl = cell as Element;42const count = cellEl.getAttribute("data-execution_count") || ++cellCount;43const isMarkdown = cellEl.classList.contains("markdown");44const hasCodeFolding = cellEl.querySelector("details.code-fold") !== null;4546if (!isMarkdown) {47const containerNode = doc.createElement("div");48containerNode.classList.add(kQuartoCellContainerClass);49containerNode.classList.add("column-page-left");50if (hasCodeFolding) {51containerNode.classList.add("code-fold");52}5354const decoratorNode = doc.createElement("div");55decoratorNode.classList.add(kQuartoCellDecoratorClass);5657const contentsEl = doc.createElement("pre");58contentsEl.appendChild(doc.createTextNode(`In [${count}]:`));59decoratorNode.appendChild(contentsEl);6061containerNode.appendChild(decoratorNode);6263const prevSibling = cellEl.previousElementSibling;64if (65prevSibling &&66prevSibling.tagName === "DIV" &&67prevSibling.classList.contains("cell-code")68) {69// If the previous sibling is a cell-code, that is the code cell70// for this output and we should grab it too71cell.parentElement?.insertBefore(containerNode, cell);7273// Grab the previous element too74const wrapperDiv = doc.createElement("div");75containerNode.appendChild(wrapperDiv);7677// move the cells78wrapperDiv.appendChild(prevSibling.cloneNode(true));79prevSibling.remove();8081wrapperDiv.appendChild(cell);82} else {83cell.parentElement?.insertBefore(containerNode, cell);84containerNode.appendChild(cell);85}86}87}8889const resources: string[] = [];90const supporting: string[] = [];91return Promise.resolve({92resources,93supporting,94});95};96}9798// Processes embeds within an HTML page and emits notebook previews as apprpriate99// Perhaps in render services or elsewhere, we can pass a notebook renderer that will100// demand render a notebook (or use an already rendered notebook if it was discovered as a part of101// a project and rendered to the correct format)102//103export async function emplaceNotebookPreviews(104input: string,105doc: Document,106format: Format,107services: RenderServices,108project: ProjectContext,109output?: string,110quiet?: boolean,111) {112// The notebook view configuration data113const notebookView = format.render[kNotebookView] ?? true;114// The view style for the notebook115const notebookViewStyle = format.render[kNotebookViewStyle];116117// Embedded notebooks don't currently resolve notebooks118if (notebookViewStyle === "notebook") {119return { resources: [], supporting: [] };120}121122// Books don't currently support notebook previews123const isBook = projectIsBook(project);124if (notebookView !== false && !isBook) {125// Utilities and settings for dealing with notebook links126const inline = format.render[kNotebookLinks] === "inline" ||127format.render[kNotebookLinks] === true;128const global = format.render[kNotebookLinks] === "global" ||129format.render[kNotebookLinks] === true;130const addInlineLineNotebookLink = inlineLinkGenerator(doc, format);131132// Helper interface for creating notebook previews133const previewer = notebookPreviewer(134notebookView,135format,136services,137project,138);139140// Process the root document itself, looking for141// computational cells provided by this document itself and if142// needed, synthesizing a notebook for them143// (only do this if this is a root document144// and input itself is in the list of notebooks)145const inputNbName = basename(input);146if (previewer.descriptor(inputNbName)) {147const computationalNodes = doc.querySelectorAll("div.cell");148for (const computationalNode of computationalNodes) {149const computeEl = computationalNode as Element;150const cellId = computeEl.getAttribute("id");151previewer.enQueuePreview(152input,153input,154undefined, // title155undefined, // order156(nbPreview) => {157// If this is a cell _in_ a source notebook, it will not be parented158// by an embed cell159if (inline) {160if (161!computeEl.parentElement?.classList.contains(162"quarto-embed-nb-cell",163)164) {165addInlineLineNotebookLink(166computeEl,167nbPreview,168cellId,169);170}171}172},173);174}175}176177// For any notebooks explicitly provided, ensure they are rendered178if (typeof notebookView !== "boolean") {179const nbs = Array.isArray(notebookView) ? notebookView : [notebookView];180for (const nb of nbs) {181// Filter out the root article notebook, since that was resolved182// above.183if (nb.url === undefined && inputNbName !== nb.notebook) {184const nbAbsPath = isAbsolute(nb.notebook)185? nb.notebook186: join(dirname(input), nb.notebook);187previewer.enQueuePreview(input, nbAbsPath, nb.title, nb.order);188}189}190}191192// Process embedded notebook contents,193// emitting links to the notebooks inline (where the embedded content is located)194const notebookDivNodes = doc.querySelectorAll("[data-notebook]");195for (const nbDivNode of notebookDivNodes) {196const nbDivEl = nbDivNode as Element;197const notebookPath = nbDivEl.getAttribute("data-notebook");198nbDivEl.removeAttribute("data-notebook");199200const notebookCellId = nbDivEl.getAttribute("data-notebook-cellId");201nbDivEl.removeAttribute("data-notebook-cellId");202203const title = nbDivEl.getAttribute("data-notebook-title");204nbDivEl.removeAttribute("data-notebook-title");205206if (notebookPath) {207previewer.enQueuePreview(208input,209nbAbsPath(input, notebookPath),210title === null ? undefined : title,211undefined, // order212(nbPreview) => {213// Add a decoration to this div node214if (inline) {215addInlineLineNotebookLink(nbDivEl, nbPreview, notebookCellId);216}217},218);219}220}221222// Render the notebook previews223const previews = await previewer.renderPreviews(output, quiet);224225// Get the preview notebooks in the correct order226const previewNotebooks = Object.values(previews).sort((a, b) => {227if (a.order !== undefined && b.order !== undefined) {228return a.order - b.order;229} else if (a.order !== undefined && b.order === undefined) {230return -1;231} else if (a.order === undefined && b.order !== undefined) {232return 1;233} else {234return a.title.localeCompare(b.title);235}236});237238// Emit global links to the notebooks239if (global && previewNotebooks.length > 0) {240const containerEl = doc.createElement("div");241containerEl.classList.add("quarto-alternate-notebooks");242243const heading = doc.createElement("h2");244if (format.language[kRelatedNotebooksTitle]) {245heading.innerText = format.language[kRelatedNotebooksTitle];246}247containerEl.appendChild(heading);248249const formatList = doc.createElement("ul");250containerEl.appendChild(formatList);251ld.uniqBy(252previewNotebooks,253(nbPath: { href: string; title?: string }) => {254return nbPath.href;255},256).forEach((nbPath: NotebookPreview) => {257const li = doc.createElement("li");258259const link = doc.createElement("a");260link.setAttribute("href", nbPath.href);261if (nbPath.filename) {262link.setAttribute("download", nbPath.filename);263}264265const icon = doc.createElement("i");266icon.classList.add("bi");267icon.classList.add(`bi-journal-code`);268link.appendChild(icon);269link.appendChild(270doc.createTextNode(nbPath.title),271);272273li.appendChild(link);274formatList.appendChild(li);275});276let dlLinkTarget = doc.querySelector(`nav[role="doc-toc"]`);277if (dlLinkTarget === null) {278dlLinkTarget = doc.querySelector("#quarto-margin-sidebar");279}280281if (dlLinkTarget) {282dlLinkTarget.appendChild(containerEl);283}284}285286const supporting: string[] = [];287const resources: string[] = [];288for (const notebookPath of Object.keys(previews)) {289const nbPath = previews[notebookPath];290// If there is a view configured for this, then291// include it in the supporting dir292if (nbPath.supporting) {293supporting.push(...nbPath.supporting);294}295296if (nbPath.resources) {297resources.push(...nbPath.resources.map((file) => {298return project ? relative(project?.dir, file) : file;299}));300}301302// This is the notebook itself303resources.push(notebookPath);304}305306return {307resources,308supporting,309};310}311}312313const nbAbsPath = (input: string, nbPath: string) => {314if (isAbsolute(nbPath)) {315return nbPath;316}317318// Ensure that the input path is absolute319const inputAbsPath = isAbsolute(input) ? input : join(Deno.cwd(), input);320321// Ensure that the notebook path is absolute322return join(dirname(inputAbsPath), nbPath);323};324325const inlineLinkGenerator = (doc: Document, format: Format) => {326let count = 1;327return (328nbDivEl: Element,329nbPreview: NotebookPreview,330cellId?: string | null,331) => {332const id = "nblink-" + count++;333334const nbLinkEl = doc.createElement("a");335nbLinkEl.classList.add("quarto-notebook-link");336nbLinkEl.setAttribute("id", `${id}`);337338if (nbPreview.filename) {339nbLinkEl.setAttribute("download", nbPreview.filename);340nbLinkEl.setAttribute("href", nbPreview.href);341} else {342if (cellId) {343nbLinkEl.setAttribute(344"href",345`${nbPreview.href}#${cellId}`,346);347} else {348nbLinkEl.setAttribute("href", `${nbPreview.href}`);349}350}351nbLinkEl.appendChild(352doc.createTextNode(353`${format.language[kSourceNotebookPrefix]}: ${nbPreview.title}`,354),355);356357// If there is a figure caption, place the source after that358// otherwise just place it at the bottom of the notebook div359const nbParentEl = nbDivEl.parentElement;360if (nbParentEl?.tagName.toLocaleLowerCase() === "figure") {361const figCapEl = nbDivEl.parentElement?.querySelector("figcaption");362if (figCapEl) {363figCapEl.after(nbLinkEl);364} else {365nbDivEl.appendChild(nbLinkEl);366}367} else {368nbDivEl.appendChild(nbLinkEl);369}370};371};372373374