Path: blob/main/src/format/reveal/format-reveal.ts
6456 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,70kScrollSnap,71kScrollView,72kSlideFooter,73kSlideLogo,74kView,75} from "./constants.ts";76import { revealMetadataFilter } from "./metadata.ts";77import { ProjectContext } from "../../project/types.ts";78import { titleSlidePartial } from "./format-reveal-title.ts";79import { registerWriterFormatHandler } from "../format-handlers.ts";80import { pandocNativeStr } from "../../core/pandoc/codegen.ts";81import { logoAddLeadingSlashes, resolveLogo } from "../../core/brand/brand.ts";8283export function revealResolveFormat(format: Format) {84format.metadata = revealMetadataFilter(format.metadata);8586// map "vertical" navigation mode to "default"87if (format.metadata["navigationMode"] === "vertical") {88format.metadata["navigationMode"] = "default";89}9091// normalize scroll-view to map to revealjs configuration92const scrollView = format.metadata[kScrollView];93if (typeof scrollView === "boolean" && scrollView) {94// if scroll-view is true then set view to scroll by default95// using all default option96format.metadata[kView] = "scroll";97} else if (typeof scrollView === "object") {98// if scroll-view is an object then map to revealjs configuration individually99const scrollViewRecord = scrollView as Record<string, unknown>;100// Only activate scroll by default when ask explicitly101if (scrollViewRecord["activate"] === true) {102format.metadata[kView] = "scroll";103}104if (scrollViewRecord["progress"] !== undefined) {105format.metadata[kScrollProgress] = scrollViewRecord["progress"];106}107if (scrollViewRecord["snap"] !== undefined) {108format.metadata[kScrollSnap] = scrollViewRecord["snap"];109}110if (scrollViewRecord["layout"] !== undefined) {111format.metadata[kScrollLayout] = scrollViewRecord["layout"];112}113if (scrollViewRecord["activation-width"] !== undefined) {114format.metadata[kScrollActivationWidth] =115scrollViewRecord["activation-width"];116}117}118// remove scroll-view from metadata119delete format.metadata[kScrollView];120}121122export function revealjsFormat() {123return mergeConfigs(124createHtmlPresentationFormat("RevealJS", 10, 5),125{126pandoc: {127[kHtmlMathMethod]: {128method: "mathjax",129url:130"https://cdn.jsdelivr.net/npm/[email protected]/MathJax.js?config=TeX-AMS_HTML-full",131},132[kSlideLevel]: 2,133},134render: {135[kCodeLineNumbers]: true,136},137metadata: {138[kAutoStretch]: true,139},140resolveFormat: revealResolveFormat,141formatPreviewFile: revealMuliplexPreviewFile,142formatExtras: async (143input: string,144_markdown: string,145flags: PandocFlags,146format: Format,147libDir: string,148services: RenderServices,149offset: string,150project: ProjectContext,151) => {152// render styles template based on options153const stylesFile = services.temp.createFile({ suffix: ".html" });154const styles = renderEjs(155formatResourcePath("revealjs", "styles.html"),156{ [kScrollable]: format.metadata[kScrollable] },157);158Deno.writeTextFileSync(stylesFile, styles);159160// specify controlsAuto if there is no boolean 'controls'161const metadataOverride: Metadata = {};162const controlsAuto = typeof (format.metadata["controls"]) !== "boolean";163if (controlsAuto) {164metadataOverride.controls = false;165}166167// specify previewLinksAuto if there is no boolean 'previewLinks'168const previewLinksAuto = format.metadata["previewLinks"] === "auto";169if (previewLinksAuto) {170metadataOverride.previewLinks = false;171}172173// additional options not supported by pandoc174const extraConfig: Record<string, unknown> = {175[kControlsAuto]: controlsAuto,176[kPreviewLinksAuto]: previewLinksAuto,177[kPdfSeparateFragments]: !!format.metadata[kPdfSeparateFragments],178[kAutoAnimateEasing]: format.metadata[kAutoAnimateEasing] || "ease",179[kAutoAnimateDuration]: format.metadata[kAutoAnimateDuration] ||1801.0,181[kAutoAnimateUnmatched]:182format.metadata[kAutoAnimateUnmatched] !== undefined183? format.metadata[kAutoAnimateUnmatched]184: true,185[kJumpToSlide]: format.metadata[kJumpToSlide] !== undefined186? !!format.metadata[kJumpToSlide]187: true,188};189190if (format.metadata[kPdfMaxPagesPerSlide]) {191extraConfig[kPdfMaxPagesPerSlide] =192format.metadata[kPdfMaxPagesPerSlide];193}194195// pass scroll view settings as they are not yet in revealjs template196if (format.metadata[kView]) {197extraConfig[kView] = format.metadata[kView];198}199if (format.metadata[kScrollProgress] !== undefined) {200extraConfig[kScrollProgress] = format.metadata[kScrollProgress];201}202if (format.metadata[kScrollSnap] !== undefined) {203extraConfig[kScrollSnap] = format.metadata[kScrollSnap];204}205if (format.metadata[kScrollLayout] !== undefined) {206extraConfig[kScrollLayout] = format.metadata[kScrollLayout];207}208if (format.metadata[kScrollActivationWidth] !== undefined) {209extraConfig[kScrollActivationWidth] =210format.metadata[kScrollActivationWidth];211}212213// get theme info (including text highlighing mode)214const theme = await revealTheme(215format,216input,217libDir,218project,219);220221const revealPluginData = await revealPluginExtras(222input,223format,224flags,225services.temp,226theme.revealUrl,227theme.revealDestDir,228services.extension,229project,230); // Add plugin scripts to metadata for template to use231232// Provide a template context233const templateDir = formatResourcePath("revealjs", "pandoc");234const partials = [235"toc-slide.html",236titleSlidePartial(format),237];238const templateContext = {239template: join(templateDir, "template.html"),240partials: partials.map((partial) => join(templateDir, partial)),241};242243// start with html format extras and our standard & plugin extras244let extras = mergeConfigs(245// extras for all html formats246await htmlFormatExtras(247input,248flags,249offset,250format,251services.temp,252project,253{254tabby: true,255anchors: false,256copyCode: true,257hoverCitations: true,258hoverFootnotes: true,259hoverXrefs: false,260figResponsive: false,261}, // tippy options262{263parent: "section.slide",264config: {265offset: [0, 0],266maxWidth: 700,267},268},269{270quartoBase: false,271},272),273// default extras for reveal274{275args: [],276pandoc: {},277metadata: {278[kLinkCitations]: true,279[kRevealJsScripts]: revealPluginData.pluginInit.scripts.map(280(script) => {281// escape to avoid pandoc markdown parsing from YAML default file282// https://github.com/quarto-dev/quarto-cli/issues/9117283return pandocNativeStr(script.path).mappedString().value;284},285),286} as Metadata,287metadataOverride,288templateContext,289[kIncludeInHeader]: [290stylesFile,291],292html: {293[kHtmlPostprocessors]: [294revealHtmlPostprocessor(295format,296extraConfig,297revealPluginData.pluginInit,298theme["text-highlighting-mode"],299),300],301[kMarkdownAfterBody]: [revealMarkdownAfterBody(format, input)],302},303},304);305306extras.metadataOverride = {307...extras.metadataOverride,308...theme.metadata,309};310extras.html![kTextHighlightingMode] = theme[kTextHighlightingMode];311312// add plugins313extras = mergeConfigs(314revealPluginData.extras,315extras,316);317318// add multiplex if we have it319const multiplexExtras = revealMultiplexExtras(format, flags);320if (multiplexExtras) {321extras = mergeConfigs(extras, multiplexExtras);322}323324// provide alternate defaults unless the user requests revealjs defaults325if (format.metadata[kRevealJsConfig] !== "default") {326// detect whether we are using vertical slides327const navigationMode = format.metadata["navigationMode"];328const verticalSlides = navigationMode === "default" ||329navigationMode === "grid";330331// if the user set slideNumber to true then provide332// linear slides (if they haven't specified vertical slides)333if (format.metadata["slideNumber"] === true) {334extras.metadataOverride!["slideNumber"] = verticalSlides335? "h.v"336: "c/t";337}338339// opinionated version of reveal config defaults340extras.metadata = {341...extras.metadata,342...revealMetadataFilter({343width: 1050,344height: 700,345margin: 0.1,346center: false,347navigationMode: "linear",348controlsLayout: "edges",349controlsTutorial: false,350hash: true,351history: true,352hashOneBasedIndex: false,353fragmentInURL: false,354transition: "none",355backgroundTransition: "none",356pdfSeparateFragments: false,357}),358};359}360361// hash-type: number (as shorthand for -auto_identifiers)362if (format.metadata[kHashType] === "number") {363extras.pandoc = {364...extras.pandoc,365from: pandocFormatWith(366format.pandoc[kFrom] || "markdown",367"",368"-auto_identifiers",369),370};371}372373// return extras374return extras;375},376},377);378}379380function revealMarkdownAfterBody(format: Format, input: string) {381let brandMode: "light" | "dark" = "light";382if (format.metadata[kBrandMode] === "dark") {383brandMode = "dark";384}385const lines: string[] = [];386lines.push("::: {.quarto-auto-generated-content style='display: none;'}\n");387const revealLogo = format388.metadata[kSlideLogo] as (string | { path: string } | undefined);389let logo = resolveLogo(format.render.brand, revealLogo, [390"small",391"medium",392"large",393]);394if (logo && logo[brandMode]) {395logo = logoAddLeadingSlashes(logo, format.render.brand, input);396const modeLogo = logo![brandMode]!;397const altText = modeLogo.alt ? `alt="${modeLogo.alt}" ` : "";398lines.push(399`<img src="${modeLogo.path}" ${altText}class="slide-logo" />`,400);401lines.push("\n");402}403lines.push("::: {.footer .footer-default}");404if (format.metadata[kSlideFooter]) {405lines.push(String(format.metadata[kSlideFooter]));406} else {407lines.push("");408}409lines.push(":::");410lines.push("\n");411lines.push(":::");412lines.push("\n");413414return lines.join("\n");415}416417const handleOutputLocationSlide = (418doc: Document,419slideHeadingTags: string[],420) => {421// find output-location-slide and inject slides as required422const slideOutputs = doc.querySelectorAll(`.${kOutputLocationSlide}`);423for (const slideOutput of slideOutputs) {424// find parent slide425const slideOutputEl = slideOutput as Element;426const parentSlide = findParentSlide(slideOutputEl);427if (parentSlide && parentSlide.parentElement) {428const newSlide = doc.createElement("section");429newSlide.setAttribute(430"id",431parentSlide?.id ? parentSlide.id + "-output" : "",432);433for (const clz of parentSlide.classList) {434newSlide.classList.add(clz);435}436newSlide.classList.add(kOutputLocationSlide);437// repeat header if there is one438if (439slideHeadingTags.includes(parentSlide.firstElementChild?.tagName || "")440) {441const headingEl = doc.createElement(442parentSlide.firstElementChild?.tagName!,443);444headingEl.innerHTML = parentSlide.firstElementChild?.innerHTML || "";445newSlide.appendChild(headingEl);446}447newSlide.appendChild(slideOutputEl);448// Place the new slide after the current one449const nextSlide = parentSlide.nextElementSibling;450parentSlide.parentElement.insertBefore(newSlide, nextSlide);451}452}453};454455const handleHashTypeNumber = (456doc: Document,457format: Format,458) => {459// if we are using 'number' as our hash type then remove the460// title slide id461if (format.metadata[kHashType] === "number") {462const titleSlide = doc.getElementById("title-slide");463if (titleSlide) {464titleSlide.removeAttribute("id");465// required for title-slide-style: pandoc466titleSlide.classList.add("quarto-title-block");467}468}469};470471const handleAutoGeneratedContent = (doc: Document) => {472// Move quarto auto-generated content outside of slides and hide it473// Content is moved with appendChild in quarto-support plugin474const slideContentFromQuarto = doc.querySelector(475".quarto-auto-generated-content",476);477if (slideContentFromQuarto) {478doc.querySelector("div.reveal")?.appendChild(slideContentFromQuarto);479}480};481482type RevealJsPluginInit = {483scripts: RevealPluginScript[];484register: string[];485revealConfig: Record<string, unknown>;486};487488const fixupRevealJsInitialization = (489doc: Document,490extraConfig: Record<string, unknown>,491pluginInit: RevealJsPluginInit,492) => {493// find reveal initialization and perform fixups494const scripts = doc.querySelectorAll("script");495for (const script of scripts) {496const scriptEl = script as Element;497if (498scriptEl.innerText &&499scriptEl.innerText.indexOf("Reveal.initialize({") !== -1500) {501// quote slideNumber502scriptEl.innerText = scriptEl.innerText.replace(503/slideNumber: (h[\.\/]v|c(?:\/t)?)/,504"slideNumber: '$1'",505);506507// quote width and heigh if in %508scriptEl.innerText = scriptEl.innerText.replace(509/width: (\d+(\.\d+)?%)/,510"width: '$1'",511);512scriptEl.innerText = scriptEl.innerText.replace(513/height: (\d+(\.\d+)?%)/,514"height: '$1'",515);516517// plugin registration518if (pluginInit.register.length > 0) {519const kRevealPluginArray = "plugins: [";520scriptEl.innerText = scriptEl.innerText.replace(521kRevealPluginArray,522kRevealPluginArray + pluginInit.register.join(", ") + ",\n",523);524}525526// Write any additional configuration of reveal527const configJs: string[] = [];528Object.keys(extraConfig).forEach((key) => {529configJs.push(530`'${key}': ${JSON.stringify(extraConfig[key])}`,531);532});533534// Plugin initialization535Object.keys(pluginInit.revealConfig).forEach((key) => {536configJs.push(537`'${key}': ${JSON.stringify(pluginInit.revealConfig[key])}`,538);539});540541const configStr = configJs.join(",\n");542543scriptEl.innerText = scriptEl.innerText.replace(544"Reveal.initialize({",545`Reveal.initialize({\n${configStr},\n`,546);547}548}549};550const kOutputLocationSlide = "output-location-slide";551552const handleInvisibleSlides = (doc: Document) => {553// remove slides with data-visibility=hidden554const invisibleSlides = doc.querySelectorAll(555'section.slide[data-visibility="hidden"]',556);557for (let i = invisibleSlides.length - 1; i >= 0; i--) {558const slide = invisibleSlides.item(i);559// remove from toc560const id = (slide as Element).id;561if (id) {562const tocEntry = doc.querySelector(563'nav[role="doc-toc"] a[href="#/' + id + '"]',564);565if (tocEntry) {566tocEntry.parentElement?.remove();567}568}569570// remove slide571slide.parentNode?.removeChild(slide);572}573};574575const handleUntitledSlidesInToc = (doc: Document) => {576// remove from toc all slides that have no title577const tocEntries = Array.from(doc.querySelectorAll(578'nav[role="doc-toc"] ul > li',579));580for (const tocEntry of tocEntries) {581const tocEntryEl = tocEntry as Element;582if (tocEntryEl.textContent.trim() === "") {583tocEntryEl.remove();584}585}586};587588const handleSlideHeadingAttributes = (589doc: Document,590slideHeadingTags: string[],591) => {592// remove all attributes from slide headings (pandoc has already moved593// them to the enclosing section)594const slideHeadings = doc.querySelectorAll("section.slide > :first-child");595slideHeadings.forEach((slideHeading) => {596const slideHeadingEl = slideHeading as Element;597if (slideHeadingTags.includes(slideHeadingEl.tagName)) {598// remove attributes599for (const attrib of slideHeadingEl.getAttributeNames()) {600slideHeadingEl.removeAttribute(attrib);601// if it's auto-animate then do some special handling602if (attrib === "data-auto-animate") {603// link slide titles for animation604slideHeadingEl.setAttribute("data-id", "quarto-animate-title");605// add animation id to code blocks606const codeBlocks = slideHeadingEl.parentElement?.querySelectorAll(607"div.sourceCode > pre > code",608);609if (codeBlocks?.length === 1) {610const codeEl = codeBlocks.item(0) as Element;611const preEl = codeEl.parentElement!;612preEl.setAttribute(613"data-id",614"quarto-animate-code",615);616// markup with highlightjs classes so that are sucessfully targeted by617// autoanimate.js618codeEl.classList.add("hljs");619codeEl.childNodes.forEach((spanNode) => {620if (spanNode.nodeType === NodeType.ELEMENT_NODE) {621const spanEl = spanNode as Element;622spanEl.classList.add("hljs-ln-code");623}624});625}626}627}628}629});630};631632const handleCenteredSlides = (doc: Document, format: Format) => {633// center title slide if requested634// note that disabling title slide centering when the rest of the635// slides are centered doesn't currently work b/c reveal consults636// the global 'center' config as well as the class. to overcome637// this we'd need to always set 'center: false` and then638// put the .center classes onto each slide manually. we're not639// doing this now the odds a user would want all of their640// slides cnetered but NOT the title slide are close to zero641if (format.metadata[kCenterTitleSlide] !== false) {642const titleSlide = doc.getElementById("title-slide") as Element ??643// when hash-type: number, id are removed644doc.querySelector(".reveal .slides section.quarto-title-block");645if (titleSlide) {646titleSlide.classList.add("center");647}648const titleSlides = doc.querySelectorAll(".title-slide");649for (const slide of titleSlides) {650(slide as Element).classList.add("center");651}652}653// center other slides if requested654if (format.metadata[kCenter] === true) {655for (const slide of doc.querySelectorAll("section.slide")) {656const slideEl = slide as Element;657slideEl.classList.add("center");658}659}660};661662const fixupAssistiveMmlInNotes = (doc: Document) => {663// inject css to hide assistive mml in speaker notes (have to do it for each aside b/c the asides are664// slurped into speaker mode one at a time using innerHTML) note that we can remvoe this hack when we begin665// defaulting to MathJax 3 (after Pandoc updates their template to support Reveal 4.2 / MathJax 3)666// see discussion of underlying issue here: https://github.com/hakimel/reveal.js/issues/1726667// hack here: https://stackoverflow.com/questions/35534385/mathjax-config-for-web-mobile-and-assistive668const notes = doc.querySelectorAll("aside.notes");669for (const note of notes) {670const style = doc.createElement("style");671style.setAttribute("type", "text/css");672style.innerHTML = `673span.MJX_Assistive_MathML {674position:absolute!important;675clip: rect(1px, 1px, 1px, 1px);676padding: 1px 0 0 0!important;677border: 0!important;678height: 1px!important;679width: 1px!important;680overflow: hidden!important;681display:block!important;682}`;683note.appendChild(style);684}685};686687const coalesceAsides = (doc: Document, slideFootnotes: boolean) => {688// collect up asides into a single aside689const slides = doc.querySelectorAll("section.slide");690for (const slide of slides) {691const slideEl = slide as Element;692const asides = slideEl.querySelectorAll("aside:not(.notes)");693const asideDivs = slideEl.querySelectorAll("div.aside");694const footnotes = slideEl.querySelectorAll('a[role="doc-noteref"]');695if (asides.length > 0 || asideDivs.length > 0 || footnotes.length > 0) {696const aside = doc.createElement("aside");697// deno-lint-ignore no-explicit-any698const collectAsides = (asideList: any) => {699asideList.forEach((asideEl: Element) => {700const asideDiv = doc.createElement("div");701asideDiv.innerHTML = (asideEl as Element).innerHTML;702aside.appendChild(asideDiv);703});704asideList.forEach((asideEl: Element) => {705asideEl.remove();706});707};708// start with asides and div.aside709collectAsides(asides);710collectAsides(asideDivs);711712// append footnotes713if (slideFootnotes && footnotes.length > 0) {714const ol = doc.createElement("ol");715ol.classList.add("aside-footnotes");716footnotes.forEach((note, index) => {717const noteEl = note as Element;718const href = noteEl.getAttribute("href");719if (href) {720const noteLi = doc.getElementById(href.replace(/^#\//, ""));721if (noteLi) {722// remove backlink723const footnoteBack = noteLi.querySelector(".footnote-back");724if (footnoteBack) {725footnoteBack.remove();726}727ol.appendChild(noteLi);728}729}730const sup = doc.createElement("sup");731sup.innerText = (index + 1) + "";732noteEl.replaceWith(sup);733});734aside.appendChild(ol);735}736737slide.appendChild(aside);738}739}740};741742const handleSlideFootnotes = (743doc: Document,744slideFootnotes: boolean,745format: Format,746slideLevel: number,747) => {748const footnotes = doc.querySelectorAll('section[role="doc-endnotes"]');749if (slideFootnotes) {750// we are using slide based footnotes so remove footnotes slide from end751for (const footnoteSection of footnotes) {752(footnoteSection as Element).remove();753}754} else {755let footnotesId: string | undefined;756const footnotes = doc.querySelectorAll('section[role="doc-endnotes"]');757if (footnotes.length === 1) {758const footnotesEl = footnotes[0] as Element;759footnotesId = footnotesEl?.getAttribute("id") || "footnotes";760footnotesEl.setAttribute("id", footnotesId);761insertFootnotesTitle(doc, footnotesEl, format.language, slideLevel);762footnotesEl.classList.add("smaller");763footnotesEl.classList.add("scrollable");764footnotesEl.classList.remove("center");765removeFootnoteBacklinks(footnotesEl);766}767768// we are keeping footnotes at the end so disable the links (we use popups)769// and tweak the footnotes slide (add a title add smaller/scrollable)770const notes = doc.querySelectorAll('a[role="doc-noteref"]');771for (const note of notes) {772const noteEl = note as Element;773noteEl.setAttribute("data-footnote-href", noteEl.getAttribute("href"));774noteEl.setAttribute("href", footnotesId ? `#/${footnotesId}` : "");775noteEl.setAttribute("onclick", footnotesId ? "" : "return false;");776}777}778};779780const handleRefs = (doc: Document): string | undefined => {781// add scrollable to refs slide782let refsId: string | undefined;783const refs = doc.querySelector("#refs");784if (refs) {785const refsSlide = findParentSlide(refs);786if (refsSlide) {787refsId = refsSlide?.getAttribute("id") || "references";788refsSlide.setAttribute("id", refsId);789}790applyClassesToParentSlide(refs, ["smaller", "scrollable"]);791removeClassesFromParentSlide(refs, ["center"]);792}793return refsId;794};795796const handleScrollable = (doc: Document, format: Format) => {797// #6866: add .scrollable to all sections with ordered lists if format.scrollable is true798if (format.metadata[kScrollable] === true) {799const ol = doc.querySelectorAll("ol");800for (const olEl of ol) {801const olParent = findParent(olEl as Element, (el: Element) => {802return el.nodeName === "SECTION";803});804if (olParent) {805olParent.classList.add("scrollable");806}807}808}809};810811const handleCitationLinks = (doc: Document, refsId: string | undefined) => {812// handle citation links813const cites = doc.querySelectorAll('a[role="doc-biblioref"]');814for (const cite of cites) {815const citeEl = cite as Element;816citeEl.setAttribute("href", refsId ? `#/${refsId}` : "");817citeEl.setAttribute("onclick", refsId ? "" : "return false;");818}819};820821const handleChalkboard = (result: HtmlPostProcessResult, format: Format) => {822// include chalkboard src json if specified823const chalkboard = format.metadata["chalkboard"];824if (typeof chalkboard === "object") {825const chalkboardSrc = (chalkboard as Record<string, unknown>)["src"];826if (typeof chalkboardSrc === "string") {827result.resources.push(chalkboardSrc);828}829}830};831832const handleAnchors = (doc: Document) => {833// Remove anchors on numbered code chunks as they can't work834// because ids are used for sections in revealjs835const codeLinesAnchors = doc.querySelectorAll(836"span[id^='cb'] > a[href^='#c']",837);838codeLinesAnchors.forEach((codeLineAnchor) => {839const codeLineAnchorEl = codeLineAnchor as Element;840codeLineAnchorEl.removeAttribute("href");841});842};843844const handleInterColumnDivSpaces = (doc: Document) => {845// https://github.com/quarto-dev/quarto-cli/issues/8498846// columns with spaces between them can cause847// layout problems when their total width is almost 100%848for (const slide of doc.querySelectorAll("section.slide")) {849for (const column of (slide as Element).querySelectorAll("div.column")) {850const columnEl = column as Element;851let next = columnEl.nextSibling;852while (853next &&854next.nodeType === NodeType.TEXT_NODE &&855next.textContent?.trim() === ""856) {857next.parentElement?.removeChild(next);858next = columnEl.nextSibling;859}860}861}862};863864function revealHtmlPostprocessor(865format: Format,866extraConfig: Record<string, unknown>,867pluginInit: RevealJsPluginInit,868highlightingMode: "light" | "dark",869) {870return (doc: Document): Promise<HtmlPostProcessResult> => {871const result: HtmlPostProcessResult = {872resources: [],873supporting: [],874};875876// Remove blockquote scaffolding added in Lua post-render to prevent Pandoc syntax for applying877if (doc.querySelectorAll("div.blockquote-list-scaffold")) {878const blockquoteListScaffolds = doc.querySelectorAll(879"div.blockquote-list-scaffold",880);881for (const blockquoteListScaffold of blockquoteListScaffolds) {882const blockquoteListScaffoldEL = blockquoteListScaffold as Element;883const blockquoteListScaffoldParent =884blockquoteListScaffoldEL.parentNode;885if (blockquoteListScaffoldParent) {886while (blockquoteListScaffoldEL.firstChild) {887blockquoteListScaffoldParent.insertBefore(888blockquoteListScaffoldEL.firstChild,889blockquoteListScaffoldEL,890);891}892blockquoteListScaffoldParent.removeChild(blockquoteListScaffoldEL);893}894}895}896897// apply highlighting mode to body898doc.body.classList.add("quarto-" + highlightingMode);899900// determine if we are embedding footnotes on slides901const slideFootnotes = format.pandoc[kReferenceLocation] !== "document";902903// compute slide level and slide headings904const slideLevel = format.pandoc[kSlideLevel] || 2;905const slideHeadingTags = Array.from(Array(slideLevel)).map((_e, i) =>906"H" + (i + 1)907);908909handleOutputLocationSlide(doc, slideHeadingTags);910handleHashTypeNumber(doc, format);911fixupRevealJsInitialization(doc, extraConfig, pluginInit);912handleAutoGeneratedContent(doc);913handleInvisibleSlides(doc);914handleUntitledSlidesInToc(doc);915handleSlideHeadingAttributes(doc, slideHeadingTags);916handleCenteredSlides(doc, format);917fixupAssistiveMmlInNotes(doc);918coalesceAsides(doc, slideFootnotes);919handleSlideFootnotes(doc, slideFootnotes, format, slideLevel);920const refsId = handleRefs(doc);921handleScrollable(doc, format);922handleCitationLinks(doc, refsId);923// apply stretch to images as required924applyStretch(doc, format.metadata[kAutoStretch] as boolean);925handleChalkboard(result, format);926handleAnchors(doc);927handleInterColumnDivSpaces(doc);928929// return result930return Promise.resolve(result);931};932}933934function applyStretch(doc: Document, autoStretch: boolean) {935// Add stretch class to images in slides with only one image936const allSlides = doc.querySelectorAll("section.slide");937for (const slide of allSlides) {938const slideEl = slide as Element;939940// opt-out mechanism per slide941if (slideEl.classList.contains("nostretch")) continue;942943const images = slideEl.querySelectorAll("img");944// only target slides with one image945if (images.length === 1) {946const image = images[0];947const imageEl = image as Element;948949// opt-out if nostrech is applied at image level too950if (imageEl.classList.contains("nostretch")) {951imageEl.classList.remove("nostretch");952continue;953}954955if (956// screen out early specials divs (layout panels, columns, fragments, ...)957findParent(imageEl, (el: Element) => {958return el.classList.contains("column") ||959el.classList.contains("quarto-layout-panel") ||960el.classList.contains("fragment") ||961el.classList.contains(kOutputLocationSlide) ||962!!el.className.match(/panel-/);963}) ||964// Do not autostrech if an aside is used965slideEl.querySelectorAll("aside:not(.notes)").length !== 0966) {967continue;968}969970// find the first level node that contains the img971let selNode: Element | undefined;972for (const node of slide.childNodes) {973if (node.contains(image)) {974selNode = node as Element;975break;976}977}978const nodeEl = selNode;979980// Do not apply stretch if this is an inline image among text981if (982!nodeEl || (nodeEl.nodeName === "P" && nodeEl.childNodes.length > 1)983) {984continue;985}986987const hasStretchClass = function (el: Element): boolean {988return el.classList.contains("stretch") ||989el.classList.contains("r-stretch");990};991992// Only apply auto stretch on specific known structures993// and avoid applying automatically on custom divs994if (995// on <p><img> (created by Pandoc)996nodeEl.nodeName === "P" ||997// on quarto figure divs998nodeEl.nodeName === "DIV" &&999nodeEl.classList.contains("quarto-figure") ||1000// on computation output created image1001nodeEl.nodeName === "DIV" && nodeEl.classList.contains("cell") ||1002// on other divs (custom divs) when explicitly opt-in1003nodeEl.nodeName === "DIV" && hasStretchClass(nodeEl)1004) {1005// for custom divs, remove stretch class as it should only be present on img1006if (nodeEl.nodeName === "DIV" && hasStretchClass(nodeEl)) {1007nodeEl.classList.remove("r-stretch");1008nodeEl.classList.remove("stretch");1009}10101011// add stretch class if not already when auto-stretch is set1012if (1013autoStretch === true &&1014!hasStretchClass(imageEl) &&1015// if height is already set, we do nothing1016!imageEl.getAttribute("style")?.match("height:") &&1017!imageEl.hasAttribute("height") &&1018// do not add when .absolute is used1019!imageEl.classList.contains("absolute") &&1020// do not add when image is inside a link1021imageEl.parentElement?.nodeName !== "A"1022) {1023imageEl.classList.add("r-stretch");1024}10251026// If <img class="stretch"> is not a direct child of <section>, move it1027if (1028hasStretchClass(imageEl) &&1029imageEl.parentNode?.nodeName !== "SECTION"1030) {1031// Remove element then maybe remove its parents if empty1032const removeEmpty = function (el: Element) {1033const parentEl = el.parentElement;1034parentEl?.removeChild(el);1035if (1036parentEl?.innerText.trim() === "" &&1037// Stop at section leveal and do not remove empty slides1038parentEl?.nodeName !== "SECTION"1039) {1040removeEmpty(parentEl);1041}1042};10431044// Figure environment ? Get caption, id and alignment1045const quartoFig = slideEl.querySelector("div.quarto-figure");1046const caption = doc.createElement("p");1047if (quartoFig) {1048// Get alignment1049const align = quartoFig.className.match(1050"quarto-figure-(center|left|right)",1051);1052if (align) imageEl.classList.add(align[0]);1053// Get id1054const quartoFigId = quartoFig?.id;1055if (quartoFigId) imageEl.id = quartoFigId;1056// Get Caption1057const figCaption = nodeEl.querySelector("figcaption");1058if (figCaption) {1059caption.classList.add("caption");1060caption.innerHTML = figCaption.innerHTML;1061}1062}10631064// Target position of image1065// first level after the element1066const nextEl = nodeEl.nextElementSibling;1067// Remove image from its parent1068removeEmpty(imageEl);1069// insert at target position1070slideEl.insertBefore(image, nextEl);10711072// If there was a caption processed add it after1073if (caption.classList.contains("caption")) {1074slideEl.insertBefore(1075caption,1076imageEl.nextElementSibling,1077);1078}1079// Remove container if still there1080if (quartoFig) removeEmpty(quartoFig);1081}1082}1083}1084}1085}10861087function applyClassesToParentSlide(1088el: Element,1089classes: string[],1090slideClass = "slide",1091) {1092const slideEl = findParentSlide(el, slideClass);1093if (slideEl) {1094classes.forEach((clz) => slideEl.classList.add(clz));1095}1096}10971098function removeClassesFromParentSlide(1099el: Element,1100classes: string[],1101slideClass = "slide",1102) {1103const slideEl = findParentSlide(el, slideClass);1104if (slideEl) {1105classes.forEach((clz) => slideEl.classList.remove(clz));1106}1107}11081109function findParentSlide(el: Element, slideClass = "slide") {1110return findParent(el, (el: Element) => {1111return el.classList.contains(slideClass);1112});1113}11141115registerWriterFormatHandler((format) => {1116switch (format) {1117case "revealjs":1118return {1119format: revealjsFormat(),1120};1121}1122});112311241125