Path: blob/main/src/format/reveal/format-reveal.ts
12926 views
/*1* format-reveal.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/5import { join } from "../../deno_ral/path.ts";67import { Document, Element, NodeType } from "../../core/deno-dom.ts";8import {9kBrandMode,10kCodeLineNumbers,11kFrom,12kHtmlMathMethod,13kIncludeInHeader,14kLinkCitations,15kReferenceLocation,16kRevealJsScripts,17kSlideLevel,18} from "../../config/constants.ts";1920import {21Format,22kHtmlPostprocessors,23kMarkdownAfterBody,24kTextHighlightingMode,25Metadata,26PandocFlags,27} from "../../config/types.ts";28import { BrandNamedLogo, Zod } from "../../resources/types/zod/schema-types.ts";2930import { mergeConfigs } from "../../core/config.ts";31import { formatResourcePath } from "../../core/resources.ts";32import { renderEjs } from "../../core/ejs.ts";33import { findParent } from "../../core/html.ts";34import { createHtmlPresentationFormat } from "../formats-shared.ts";35import { pandocFormatWith } from "../../core/pandoc/pandoc-formats.ts";36import { htmlFormatExtras } from "../html/format-html.ts";37import { revealPluginExtras } from "./format-reveal-plugin.ts";38import { RevealPluginScript } from "./format-reveal-plugin-types.ts";39import { revealTheme } from "./format-reveal-theme.ts";40import {41revealMuliplexPreviewFile,42revealMultiplexExtras,43} from "./format-reveal-multiplex.ts";44import {45insertFootnotesTitle,46removeFootnoteBacklinks,47} from "../html/format-html-shared.ts";48import {49HtmlPostProcessResult,50RenderServices,51} from "../../command/render/types.ts";52import {53kAutoAnimateDuration,54kAutoAnimateEasing,55kAutoAnimateUnmatched,56kAutoStretch,57kCenter,58kCenterTitleSlide,59kControlsAuto,60kHashType,61kJumpToSlide,62kPdfMaxPagesPerSlide,63kPdfSeparateFragments,64kPreviewLinksAuto,65kRevealJsConfig,66kScrollable,67kScrollActivationWidth,68kScrollLayout,69kScrollProgress,70kScrollProgressAuto,71kScrollSnap,72kScrollView,73kSlideFooter,74kSlideLogo,75kView,76} from "./constants.ts";77import { revealMetadataFilter } from "./metadata.ts";78import { ProjectContext } from "../../project/types.ts";79import { titleSlidePartial } from "./format-reveal-title.ts";80import { registerWriterFormatHandler } from "../format-handlers.ts";81import { pandocNativeStr } from "../../core/pandoc/codegen.ts";82import { logoAddLeadingSlashes, resolveLogo } from "../../core/brand/brand.ts";8384export function revealResolveFormat(format: Format) {85format.metadata = revealMetadataFilter(format.metadata);8687// map "vertical" navigation mode to "default"88if (format.metadata["navigationMode"] === "vertical") {89format.metadata["navigationMode"] = "default";90}9192// normalize scroll-view to map to revealjs configuration93const scrollView = format.metadata[kScrollView];94if (typeof scrollView === "boolean" && scrollView) {95// if scroll-view is true then set view to scroll by default96// using all default option97format.metadata[kView] = "scroll";98} else if (typeof scrollView === "object") {99// if scroll-view is an object then map to revealjs configuration individually100const scrollViewRecord = scrollView as Record<string, unknown>;101// Only activate scroll by default when ask explicitly102if (scrollViewRecord["activate"] === true) {103format.metadata[kView] = "scroll";104}105if (scrollViewRecord["progress"] !== undefined) {106format.metadata[kScrollProgress] = scrollViewRecord["progress"];107}108if (scrollViewRecord["snap"] !== undefined) {109format.metadata[kScrollSnap] = scrollViewRecord["snap"];110}111if (scrollViewRecord["layout"] !== undefined) {112format.metadata[kScrollLayout] = scrollViewRecord["layout"];113}114if (scrollViewRecord["activation-width"] !== undefined) {115format.metadata[kScrollActivationWidth] =116scrollViewRecord["activation-width"];117}118}119// remove scroll-view from metadata120delete format.metadata[kScrollView];121122// Handle scrollProgress "auto" for the template.123// Pandoc templates render BoolVal as true/false literals, but "auto" needs124// to be a quoted string. A helper variable scrollProgressAuto handles this.125// When no scrollProgress is specified and view is "scroll", default to "auto"126// (RevealJS default) rather than Pandoc's defField default of true.127if (format.metadata[kView] === "scroll") {128if (129format.metadata[kScrollProgress] === "auto" ||130format.metadata[kScrollProgress] === undefined131) {132format.metadata[kScrollProgressAuto] = true;133delete format.metadata[kScrollProgress];134}135}136}137138export function revealjsFormat() {139return mergeConfigs(140createHtmlPresentationFormat("RevealJS", 10, 5),141{142pandoc: {143[kHtmlMathMethod]: {144method: "mathjax",145url:146"https://cdn.jsdelivr.net/npm/[email protected]/MathJax.js?config=TeX-AMS_HTML-full",147},148[kSlideLevel]: 2,149},150render: {151[kCodeLineNumbers]: true,152},153metadata: {154[kAutoStretch]: true,155},156resolveFormat: revealResolveFormat,157formatPreviewFile: revealMuliplexPreviewFile,158formatExtras: async (159input: string,160_markdown: string,161flags: PandocFlags,162format: Format,163libDir: string,164services: RenderServices,165offset: string,166project: ProjectContext,167) => {168// render styles template based on options169const stylesFile = services.temp.createFile({ suffix: ".html" });170const styles = renderEjs(171formatResourcePath("revealjs", "styles.html"),172{ [kScrollable]: format.metadata[kScrollable] },173);174Deno.writeTextFileSync(stylesFile, styles);175176// specify controlsAuto if there is no boolean 'controls'177const metadataOverride: Metadata = {};178const controlsAuto = typeof (format.metadata["controls"]) !== "boolean";179if (controlsAuto) {180metadataOverride.controls = false;181}182183// specify previewLinksAuto if there is no boolean 'previewLinks'184const previewLinksAuto = format.metadata["previewLinks"] === "auto";185if (previewLinksAuto) {186metadataOverride.previewLinks = false;187}188189// additional options not supported by pandoc190const extraConfig: Record<string, unknown> = {191[kControlsAuto]: controlsAuto,192[kPreviewLinksAuto]: previewLinksAuto,193[kPdfSeparateFragments]: !!format.metadata[kPdfSeparateFragments],194[kAutoAnimateEasing]: format.metadata[kAutoAnimateEasing] || "ease",195[kAutoAnimateDuration]: format.metadata[kAutoAnimateDuration] ||1961.0,197[kAutoAnimateUnmatched]:198format.metadata[kAutoAnimateUnmatched] !== undefined199? format.metadata[kAutoAnimateUnmatched]200: true,201[kJumpToSlide]: format.metadata[kJumpToSlide] !== undefined202? !!format.metadata[kJumpToSlide]203: true,204};205206if (format.metadata[kPdfMaxPagesPerSlide]) {207extraConfig[kPdfMaxPagesPerSlide] =208format.metadata[kPdfMaxPagesPerSlide];209}210211// Scroll view settings (view, scrollProgress, scrollSnap, scrollLayout,212// scrollActivationWidth) are rendered by the template via metadata213// variables set in revealResolveFormat().214215// get theme info (including text highlighing mode)216const theme = await revealTheme(217format,218input,219libDir,220project,221);222223const revealPluginData = await revealPluginExtras(224input,225format,226flags,227services.temp,228theme.revealUrl,229theme.revealDestDir,230services.extension,231project,232); // Add plugin scripts to metadata for template to use233234// Provide a template context235const templateDir = formatResourcePath("revealjs", "pandoc");236const partials = [237"toc-slide.html",238titleSlidePartial(format),239];240const templateContext = {241template: join(templateDir, "template.html"),242partials: partials.map((partial) => join(templateDir, partial)),243};244245// start with html format extras and our standard & plugin extras246let extras = mergeConfigs(247// extras for all html formats248await htmlFormatExtras(249input,250flags,251offset,252format,253services.temp,254project,255{256tabby: true,257anchors: false,258copyCode: true,259hoverCitations: true,260hoverFootnotes: true,261hoverXrefs: false,262figResponsive: false,263}, // tippy options264{265parent: "section.slide",266config: {267offset: [0, 0],268maxWidth: 700,269},270},271{272quartoBase: false,273},274),275// default extras for reveal276{277args: [],278pandoc: {},279metadata: {280[kLinkCitations]: true,281[kRevealJsScripts]: revealPluginData.pluginInit.scripts.map(282(script) => {283// escape to avoid pandoc markdown parsing from YAML default file284// https://github.com/quarto-dev/quarto-cli/issues/9117285return pandocNativeStr(script.path).mappedString().value;286},287),288} as Metadata,289metadataOverride,290templateContext,291[kIncludeInHeader]: [292stylesFile,293],294html: {295[kHtmlPostprocessors]: [296revealHtmlPostprocessor(297format,298extraConfig,299revealPluginData.pluginInit,300theme["text-highlighting-mode"],301),302],303[kMarkdownAfterBody]: [revealMarkdownAfterBody(format, input)],304},305},306);307308extras.metadataOverride = {309...extras.metadataOverride,310...theme.metadata,311};312extras.html![kTextHighlightingMode] = theme[kTextHighlightingMode];313314// add plugins315extras = mergeConfigs(316revealPluginData.extras,317extras,318);319320// add multiplex if we have it321const multiplexExtras = revealMultiplexExtras(format, flags);322if (multiplexExtras) {323extras = mergeConfigs(extras, multiplexExtras);324}325326// provide alternate defaults unless the user requests revealjs defaults327if (format.metadata[kRevealJsConfig] !== "default") {328// detect whether we are using vertical slides329const navigationMode = format.metadata["navigationMode"];330const verticalSlides = navigationMode === "default" ||331navigationMode === "grid";332333// if the user set slideNumber to true then provide334// linear slides (if they haven't specified vertical slides)335if (format.metadata["slideNumber"] === true) {336extras.metadataOverride!["slideNumber"] = verticalSlides337? "h.v"338: "c/t";339}340341// opinionated version of reveal config defaults342extras.metadata = {343...extras.metadata,344...revealMetadataFilter({345width: 1050,346height: 700,347margin: 0.1,348center: false,349navigationMode: "linear",350controlsLayout: "edges",351controlsTutorial: false,352hash: true,353history: true,354hashOneBasedIndex: false,355fragmentInURL: false,356transition: "none",357backgroundTransition: "none",358pdfSeparateFragments: false,359}),360};361}362363// Scroll-view defaults (only when view is "scroll").364// Set explicitly so the template $if/$else$ type guards always have365// values and don't depend on Pandoc's defField.366if (format.metadata[kView] === "scroll") {367extras.metadata = {368...extras.metadata,369[kScrollSnap]: "mandatory",370[kScrollLayout]: "full",371[kScrollActivationWidth]: 0,372};373}374375// hash-type: number (as shorthand for -auto_identifiers)376if (format.metadata[kHashType] === "number") {377extras.pandoc = {378...extras.pandoc,379from: pandocFormatWith(380format.pandoc[kFrom] || "markdown",381"",382"-auto_identifiers",383),384};385}386387// return extras388return extras;389},390},391);392}393394function revealMarkdownAfterBody(format: Format, input: string) {395let brandMode: "light" | "dark" = "light";396if (format.metadata[kBrandMode] === "dark") {397brandMode = "dark";398}399const lines: string[] = [];400lines.push("::: {.quarto-auto-generated-content style='display: none;'}\n");401const revealLogo = format402.metadata[kSlideLogo] as (string | { path: string } | undefined);403let logo = resolveLogo(format.render.brand, revealLogo, [404"small",405"medium",406"large",407]);408if (logo && logo[brandMode]) {409logo = logoAddLeadingSlashes(logo, format.render.brand, input);410const modeLogo = logo![brandMode]!;411const altText = modeLogo.alt ? `alt="${modeLogo.alt}" ` : "";412lines.push(413`<img src="${modeLogo.path}" ${altText}class="slide-logo" />`,414);415lines.push("\n");416}417lines.push("::: {.footer .footer-default}");418if (format.metadata[kSlideFooter]) {419lines.push(String(format.metadata[kSlideFooter]));420} else {421lines.push("");422}423lines.push(":::");424lines.push("\n");425lines.push(":::");426lines.push("\n");427428return lines.join("\n");429}430431const handleOutputLocationSlide = (432doc: Document,433slideHeadingTags: string[],434) => {435// find output-location-slide and inject slides as required436const slideOutputs = doc.querySelectorAll(`.${kOutputLocationSlide}`);437for (const slideOutput of slideOutputs) {438// find parent slide439const slideOutputEl = slideOutput as Element;440const parentSlide = findParentSlide(slideOutputEl);441if (parentSlide && parentSlide.parentElement) {442const newSlide = doc.createElement("section");443newSlide.setAttribute(444"id",445parentSlide?.id ? parentSlide.id + "-output" : "",446);447for (const clz of parentSlide.classList) {448newSlide.classList.add(clz);449}450newSlide.classList.add(kOutputLocationSlide);451// repeat header if there is one452if (453slideHeadingTags.includes(parentSlide.firstElementChild?.tagName || "")454) {455const headingEl = doc.createElement(456parentSlide.firstElementChild?.tagName!,457);458headingEl.innerHTML = parentSlide.firstElementChild?.innerHTML || "";459newSlide.appendChild(headingEl);460}461newSlide.appendChild(slideOutputEl);462// Place the new slide after the current one463const nextSlide = parentSlide.nextElementSibling;464parentSlide.parentElement.insertBefore(newSlide, nextSlide);465}466}467};468469const handleHashTypeNumber = (470doc: Document,471format: Format,472) => {473// if we are using 'number' as our hash type then remove the474// title slide id475if (format.metadata[kHashType] === "number") {476const titleSlide = doc.getElementById("title-slide");477if (titleSlide) {478titleSlide.removeAttribute("id");479// required for title-slide-style: pandoc480titleSlide.classList.add("quarto-title-block");481}482}483};484485const handleAutoGeneratedContent = (doc: Document) => {486// Move quarto auto-generated content outside of slides and hide it487// Content is moved with appendChild in quarto-support plugin488const slideContentFromQuarto = doc.querySelector(489".quarto-auto-generated-content",490);491if (slideContentFromQuarto) {492doc.querySelector("div.reveal")?.appendChild(slideContentFromQuarto);493}494};495496type RevealJsPluginInit = {497scripts: RevealPluginScript[];498register: string[];499revealConfig: Record<string, unknown>;500};501502const fixupRevealJsInitialization = (503doc: Document,504extraConfig: Record<string, unknown>,505pluginInit: RevealJsPluginInit,506) => {507// find reveal initialization and perform fixups508const scripts = doc.querySelectorAll("script");509for (const script of scripts) {510const scriptEl = script as Element;511if (512scriptEl.innerText &&513scriptEl.innerText.indexOf("Reveal.initialize({") !== -1514) {515// quote slideNumber516scriptEl.innerText = scriptEl.innerText.replace(517/slideNumber: (h[\.\/]v|c(?:\/t)?)/,518"slideNumber: '$1'",519);520521// quote width and heigh if in %522scriptEl.innerText = scriptEl.innerText.replace(523/width: (\d+(\.\d+)?%)/,524"width: '$1'",525);526scriptEl.innerText = scriptEl.innerText.replace(527/height: (\d+(\.\d+)?%)/,528"height: '$1'",529);530531// plugin registration532if (pluginInit.register.length > 0) {533const kRevealPluginArray = "plugins: [";534scriptEl.innerText = scriptEl.innerText.replace(535kRevealPluginArray,536kRevealPluginArray + pluginInit.register.join(", ") + ",\n",537);538}539540// Write any additional configuration of reveal541const configJs: string[] = [];542Object.keys(extraConfig).forEach((key) => {543configJs.push(544`'${key}': ${JSON.stringify(extraConfig[key])}`,545);546});547548// Plugin initialization549Object.keys(pluginInit.revealConfig).forEach((key) => {550configJs.push(551`'${key}': ${JSON.stringify(pluginInit.revealConfig[key])}`,552);553});554555const configStr = configJs.join(",\n");556557scriptEl.innerText = scriptEl.innerText.replace(558"Reveal.initialize({",559`Reveal.initialize({\n${configStr},\n`,560);561}562}563};564const kOutputLocationSlide = "output-location-slide";565566const handleInvisibleSlides = (doc: Document) => {567// remove slides with data-visibility=hidden568const invisibleSlides = doc.querySelectorAll(569'section.slide[data-visibility="hidden"]',570);571for (let i = invisibleSlides.length - 1; i >= 0; i--) {572const slide = invisibleSlides.item(i);573// remove from toc574const id = (slide as Element).id;575if (id) {576const tocEntry = doc.querySelector(577'nav[role="doc-toc"] a[href="#/' + id + '"]',578);579if (tocEntry) {580tocEntry.parentElement?.remove();581}582}583584// remove slide585slide.parentNode?.removeChild(slide);586}587};588589const handleUntitledSlidesInToc = (doc: Document) => {590// remove from toc all slides that have no title591const tocEntries = Array.from(doc.querySelectorAll(592'nav[role="doc-toc"] ul > li',593));594for (const tocEntry of tocEntries) {595const tocEntryEl = tocEntry as Element;596if (tocEntryEl.textContent.trim() === "") {597tocEntryEl.remove();598}599}600};601602const handleSlideHeadingAttributes = (603doc: Document,604slideHeadingTags: string[],605) => {606// remove all attributes from slide headings (pandoc has already moved607// them to the enclosing section)608const slideHeadings = doc.querySelectorAll("section.slide > :first-child");609slideHeadings.forEach((slideHeading) => {610const slideHeadingEl = slideHeading as Element;611if (slideHeadingTags.includes(slideHeadingEl.tagName)) {612// remove attributes613for (const attrib of slideHeadingEl.getAttributeNames()) {614slideHeadingEl.removeAttribute(attrib);615// if it's auto-animate then do some special handling616if (attrib === "data-auto-animate") {617// link slide titles for animation618slideHeadingEl.setAttribute("data-id", "quarto-animate-title");619// add animation id to code blocks620const codeBlocks = slideHeadingEl.parentElement?.querySelectorAll(621"div.sourceCode > pre > code",622);623if (codeBlocks?.length === 1) {624const codeEl = codeBlocks.item(0) as Element;625const preEl = codeEl.parentElement!;626preEl.setAttribute(627"data-id",628"quarto-animate-code",629);630// markup with highlightjs classes so that are sucessfully targeted by631// autoanimate.js632codeEl.classList.add("hljs");633codeEl.childNodes.forEach((spanNode) => {634if (spanNode.nodeType === NodeType.ELEMENT_NODE) {635const spanEl = spanNode as Element;636spanEl.classList.add("hljs-ln-code");637}638});639}640}641}642}643});644};645646const handleCenteredSlides = (doc: Document, format: Format) => {647// center title slide if requested648// note that disabling title slide centering when the rest of the649// slides are centered doesn't currently work b/c reveal consults650// the global 'center' config as well as the class. to overcome651// this we'd need to always set 'center: false` and then652// put the .center classes onto each slide manually. we're not653// doing this now the odds a user would want all of their654// slides cnetered but NOT the title slide are close to zero655if (format.metadata[kCenterTitleSlide] !== false) {656const titleSlide = doc.getElementById("title-slide") as Element ??657// when hash-type: number, id are removed658doc.querySelector(".reveal .slides section.quarto-title-block");659if (titleSlide) {660titleSlide.classList.add("center");661}662const titleSlides = doc.querySelectorAll(".title-slide");663for (const slide of titleSlides) {664(slide as Element).classList.add("center");665}666}667// center other slides if requested668if (format.metadata[kCenter] === true) {669for (const slide of doc.querySelectorAll("section.slide")) {670const slideEl = slide as Element;671slideEl.classList.add("center");672}673}674};675676const fixupAssistiveMmlInNotes = (doc: Document) => {677// inject css to hide assistive mml in speaker notes (have to do it for each aside b/c the asides are678// slurped into speaker mode one at a time using innerHTML) note that we can remvoe this hack when we begin679// defaulting to MathJax 3 (after Pandoc updates their template to support Reveal 4.2 / MathJax 3)680// see discussion of underlying issue here: https://github.com/hakimel/reveal.js/issues/1726681// hack here: https://stackoverflow.com/questions/35534385/mathjax-config-for-web-mobile-and-assistive682const notes = doc.querySelectorAll("aside.notes");683for (const note of notes) {684const style = doc.createElement("style");685style.setAttribute("type", "text/css");686style.innerHTML = `687span.MJX_Assistive_MathML {688position:absolute!important;689clip: rect(1px, 1px, 1px, 1px);690padding: 1px 0 0 0!important;691border: 0!important;692height: 1px!important;693width: 1px!important;694overflow: hidden!important;695display:block!important;696}`;697note.appendChild(style);698}699};700701const coalesceAsides = (doc: Document, slideFootnotes: boolean) => {702// collect up asides into a single aside703const slides = doc.querySelectorAll("section.slide");704for (const slide of slides) {705const slideEl = slide as Element;706const asides = slideEl.querySelectorAll("aside:not(.notes)");707const asideDivs = slideEl.querySelectorAll("div.aside");708const footnotes = slideEl.querySelectorAll('a[role="doc-noteref"]');709if (asides.length > 0 || asideDivs.length > 0 || footnotes.length > 0) {710const aside = doc.createElement("aside");711// deno-lint-ignore no-explicit-any712const collectAsides = (asideList: any) => {713asideList.forEach((asideEl: Element) => {714const asideDiv = doc.createElement("div");715asideDiv.innerHTML = (asideEl as Element).innerHTML;716aside.appendChild(asideDiv);717});718asideList.forEach((asideEl: Element) => {719asideEl.remove();720});721};722// start with asides and div.aside723collectAsides(asides);724collectAsides(asideDivs);725726// append footnotes727if (slideFootnotes && footnotes.length > 0) {728const ol = doc.createElement("ol");729ol.classList.add("aside-footnotes");730footnotes.forEach((note, index) => {731const noteEl = note as Element;732const href = noteEl.getAttribute("href");733if (href) {734const noteLi = doc.getElementById(href.replace(/^#\//, ""));735if (noteLi) {736// remove backlink737const footnoteBack = noteLi.querySelector(".footnote-back");738if (footnoteBack) {739footnoteBack.remove();740}741ol.appendChild(noteLi);742}743}744const sup = doc.createElement("sup");745sup.innerText = (index + 1) + "";746noteEl.replaceWith(sup);747});748aside.appendChild(ol);749}750751slide.appendChild(aside);752}753}754};755756const handleSlideFootnotes = (757doc: Document,758slideFootnotes: boolean,759format: Format,760slideLevel: number,761) => {762const footnotes = doc.querySelectorAll('section[role="doc-endnotes"]');763if (slideFootnotes) {764// we are using slide based footnotes so remove footnotes slide from end765for (const footnoteSection of footnotes) {766(footnoteSection as Element).remove();767}768} else {769let footnotesId: string | undefined;770const footnotes = doc.querySelectorAll('section[role="doc-endnotes"]');771if (footnotes.length === 1) {772const footnotesEl = footnotes[0] as Element;773footnotesId = footnotesEl?.getAttribute("id") || "footnotes";774footnotesEl.setAttribute("id", footnotesId);775insertFootnotesTitle(doc, footnotesEl, format.language, slideLevel);776footnotesEl.classList.add("smaller");777footnotesEl.classList.add("scrollable");778footnotesEl.classList.remove("center");779removeFootnoteBacklinks(footnotesEl);780}781782// we are keeping footnotes at the end so disable the links (we use popups)783// and tweak the footnotes slide (add a title add smaller/scrollable)784const notes = doc.querySelectorAll('a[role="doc-noteref"]');785for (const note of notes) {786const noteEl = note as Element;787noteEl.setAttribute("data-footnote-href", noteEl.getAttribute("href"));788noteEl.setAttribute("href", footnotesId ? `#/${footnotesId}` : "");789noteEl.setAttribute("onclick", footnotesId ? "" : "return false;");790}791}792};793794const handleRefs = (doc: Document): string | undefined => {795// add scrollable to refs slide796let refsId: string | undefined;797const refs = doc.querySelector("#refs");798if (refs) {799const refsSlide = findParentSlide(refs);800if (refsSlide) {801refsId = refsSlide?.getAttribute("id") || "references";802refsSlide.setAttribute("id", refsId);803}804applyClassesToParentSlide(refs, ["smaller", "scrollable"]);805removeClassesFromParentSlide(refs, ["center"]);806}807return refsId;808};809810const handleScrollable = (doc: Document, format: Format) => {811// #6866: add .scrollable to all sections with ordered lists if format.scrollable is true812if (format.metadata[kScrollable] === true) {813const ol = doc.querySelectorAll("ol");814for (const olEl of ol) {815const olParent = findParent(olEl as Element, (el: Element) => {816return el.nodeName === "SECTION";817});818if (olParent) {819olParent.classList.add("scrollable");820}821}822}823};824825const handleCitationLinks = (doc: Document, refsId: string | undefined) => {826// handle citation links827const cites = doc.querySelectorAll('a[role="doc-biblioref"]');828for (const cite of cites) {829const citeEl = cite as Element;830citeEl.setAttribute("href", refsId ? `#/${refsId}` : "");831citeEl.setAttribute("onclick", refsId ? "" : "return false;");832}833};834835const handleChalkboard = (result: HtmlPostProcessResult, format: Format) => {836// include chalkboard src json if specified837const chalkboard = format.metadata["chalkboard"];838if (typeof chalkboard === "object") {839const chalkboardSrc = (chalkboard as Record<string, unknown>)["src"];840if (typeof chalkboardSrc === "string") {841result.resources.push(chalkboardSrc);842}843}844};845846const handleAnchors = (doc: Document) => {847// Remove anchors on numbered code chunks as they can't work848// because ids are used for sections in revealjs849const codeLinesAnchors = doc.querySelectorAll(850"span[id^='cb'] > a[href^='#c']",851);852codeLinesAnchors.forEach((codeLineAnchor) => {853const codeLineAnchorEl = codeLineAnchor as Element;854codeLineAnchorEl.removeAttribute("href");855});856};857858const handleInterColumnDivSpaces = (doc: Document) => {859// https://github.com/quarto-dev/quarto-cli/issues/8498860// columns with spaces between them can cause861// layout problems when their total width is almost 100%862for (const slide of doc.querySelectorAll("section.slide")) {863for (const column of (slide as Element).querySelectorAll("div.column")) {864const columnEl = column as Element;865let next = columnEl.nextSibling;866while (867next &&868next.nodeType === NodeType.TEXT_NODE &&869next.textContent?.trim() === ""870) {871next.parentElement?.removeChild(next);872next = columnEl.nextSibling;873}874}875}876};877878function revealHtmlPostprocessor(879format: Format,880extraConfig: Record<string, unknown>,881pluginInit: RevealJsPluginInit,882highlightingMode: "light" | "dark",883) {884return (doc: Document): Promise<HtmlPostProcessResult> => {885const result: HtmlPostProcessResult = {886resources: [],887supporting: [],888};889890// Remove blockquote scaffolding added in Lua post-render to prevent Pandoc syntax for applying891if (doc.querySelectorAll("div.blockquote-list-scaffold")) {892const blockquoteListScaffolds = doc.querySelectorAll(893"div.blockquote-list-scaffold",894);895for (const blockquoteListScaffold of blockquoteListScaffolds) {896const blockquoteListScaffoldEL = blockquoteListScaffold as Element;897const blockquoteListScaffoldParent =898blockquoteListScaffoldEL.parentNode;899if (blockquoteListScaffoldParent) {900while (blockquoteListScaffoldEL.firstChild) {901blockquoteListScaffoldParent.insertBefore(902blockquoteListScaffoldEL.firstChild,903blockquoteListScaffoldEL,904);905}906blockquoteListScaffoldParent.removeChild(blockquoteListScaffoldEL);907}908}909}910911// apply highlighting mode to body912doc.body.classList.add("quarto-" + highlightingMode);913914// determine if we are embedding footnotes on slides915const slideFootnotes = format.pandoc[kReferenceLocation] !== "document";916917// compute slide level and slide headings918const slideLevel = format.pandoc[kSlideLevel] || 2;919const slideHeadingTags = Array.from(Array(slideLevel)).map((_e, i) =>920"H" + (i + 1)921);922923handleOutputLocationSlide(doc, slideHeadingTags);924handleHashTypeNumber(doc, format);925fixupRevealJsInitialization(doc, extraConfig, pluginInit);926handleAutoGeneratedContent(doc);927handleInvisibleSlides(doc);928handleUntitledSlidesInToc(doc);929handleSlideHeadingAttributes(doc, slideHeadingTags);930handleCenteredSlides(doc, format);931fixupAssistiveMmlInNotes(doc);932coalesceAsides(doc, slideFootnotes);933handleSlideFootnotes(doc, slideFootnotes, format, slideLevel);934const refsId = handleRefs(doc);935handleScrollable(doc, format);936handleCitationLinks(doc, refsId);937// apply stretch to images as required938applyStretch(doc, format.metadata[kAutoStretch] as boolean);939handleChalkboard(result, format);940handleAnchors(doc);941handleInterColumnDivSpaces(doc);942943// return result944return Promise.resolve(result);945};946}947948function applyStretch(doc: Document, autoStretch: boolean) {949// Add stretch class to images in slides with only one image950const allSlides = doc.querySelectorAll("section.slide");951for (const slide of allSlides) {952const slideEl = slide as Element;953954// opt-out mechanism per slide955if (slideEl.classList.contains("nostretch")) continue;956957const images = slideEl.querySelectorAll("img");958// only target slides with one image959if (images.length === 1) {960const image = images[0];961const imageEl = image as Element;962963// opt-out if nostrech is applied at image level too964if (imageEl.classList.contains("nostretch")) {965imageEl.classList.remove("nostretch");966continue;967}968969if (970// screen out early specials divs (layout panels, columns, fragments, ...)971findParent(imageEl, (el: Element) => {972return el.classList.contains("column") ||973el.classList.contains("quarto-layout-panel") ||974el.classList.contains("fragment") ||975el.classList.contains(kOutputLocationSlide) ||976!!el.className.match(/panel-/);977}) ||978// Do not autostrech if an aside is used979slideEl.querySelectorAll("aside:not(.notes)").length !== 0980) {981continue;982}983984// find the first level node that contains the img985let selNode: Element | undefined;986for (const node of slide.childNodes) {987if (node.contains(image)) {988selNode = node as Element;989break;990}991}992const nodeEl = selNode;993994// Do not apply stretch if this is an inline image among text995if (996!nodeEl || (nodeEl.nodeName === "P" && nodeEl.childNodes.length > 1)997) {998continue;999}10001001const hasStretchClass = function (el: Element): boolean {1002return el.classList.contains("stretch") ||1003el.classList.contains("r-stretch");1004};10051006// Only apply auto stretch on specific known structures1007// and avoid applying automatically on custom divs1008if (1009// on <p><img> (created by Pandoc)1010nodeEl.nodeName === "P" ||1011// on quarto figure divs1012nodeEl.nodeName === "DIV" &&1013nodeEl.classList.contains("quarto-figure") ||1014// on computation output created image1015nodeEl.nodeName === "DIV" && nodeEl.classList.contains("cell") ||1016// on other divs (custom divs) when explicitly opt-in1017nodeEl.nodeName === "DIV" && hasStretchClass(nodeEl)1018) {1019// for custom divs, remove stretch class as it should only be present on img1020if (nodeEl.nodeName === "DIV" && hasStretchClass(nodeEl)) {1021nodeEl.classList.remove("r-stretch");1022nodeEl.classList.remove("stretch");1023}10241025// add stretch class if not already when auto-stretch is set1026if (1027autoStretch === true &&1028!hasStretchClass(imageEl) &&1029// if height is already set, we do nothing1030!imageEl.getAttribute("style")?.match("height:") &&1031!imageEl.hasAttribute("height") &&1032// do not add when .absolute is used1033!imageEl.classList.contains("absolute") &&1034// do not add when image is inside a link1035imageEl.parentElement?.nodeName !== "A"1036) {1037imageEl.classList.add("r-stretch");1038}10391040// If <img class="stretch"> is not a direct child of <section>, move it1041if (1042hasStretchClass(imageEl) &&1043imageEl.parentNode?.nodeName !== "SECTION"1044) {1045// Remove element then maybe remove its parents if empty1046const removeEmpty = function (el: Element) {1047const parentEl = el.parentElement;1048parentEl?.removeChild(el);1049if (1050parentEl?.innerText.trim() === "" &&1051// Stop at section leveal and do not remove empty slides1052parentEl?.nodeName !== "SECTION"1053) {1054removeEmpty(parentEl);1055}1056};10571058// Figure environment ? Get caption, id and alignment1059const quartoFig = slideEl.querySelector("div.quarto-figure");1060const caption = doc.createElement("p");1061if (quartoFig) {1062// Get alignment1063const align = quartoFig.className.match(1064"quarto-figure-(center|left|right)",1065);1066if (align) imageEl.classList.add(align[0]);1067// Get id1068const quartoFigId = quartoFig?.id;1069if (quartoFigId) imageEl.id = quartoFigId;1070// Get Caption1071const figCaption = nodeEl.querySelector("figcaption");1072if (figCaption) {1073caption.classList.add("caption");1074caption.innerHTML = figCaption.innerHTML;1075}1076}10771078// Target position of image1079// first level after the element1080const nextEl = nodeEl.nextElementSibling;1081// Remove image from its parent1082removeEmpty(imageEl);1083// insert at target position1084slideEl.insertBefore(image, nextEl);10851086// If there was a caption processed add it after1087if (caption.classList.contains("caption")) {1088slideEl.insertBefore(1089caption,1090imageEl.nextElementSibling,1091);1092}1093// Remove container if still there1094if (quartoFig) removeEmpty(quartoFig);1095}1096}1097}1098}1099}11001101function applyClassesToParentSlide(1102el: Element,1103classes: string[],1104slideClass = "slide",1105) {1106const slideEl = findParentSlide(el, slideClass);1107if (slideEl) {1108classes.forEach((clz) => slideEl.classList.add(clz));1109}1110}11111112function removeClassesFromParentSlide(1113el: Element,1114classes: string[],1115slideClass = "slide",1116) {1117const slideEl = findParentSlide(el, slideClass);1118if (slideEl) {1119classes.forEach((clz) => slideEl.classList.remove(clz));1120}1121}11221123function findParentSlide(el: Element, slideClass = "slide") {1124return findParent(el, (el: Element) => {1125return el.classList.contains(slideClass);1126});1127}11281129registerWriterFormatHandler((format) => {1130switch (format) {1131case "revealjs":1132return {1133format: revealjsFormat(),1134};1135}1136});113711381139