Path: blob/main/src/format/html/format-html-bootstrap.ts
6450 views
/*1* format-html-bootstrap.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { Document, Element, NodeList } from "../../core/deno-dom.ts";7import { join } from "../../deno_ral/path.ts";89import { renderEjs } from "../../core/ejs.ts";10import { formatResourcePath } from "../../core/resources.ts";11import { findParent } from "../../core/html.ts";1213import {14kCodeLinks,15kCodeLinksTitle,16kContentMode,17kDisableArticleLayout,18kFormatLinks,19kGrid,20kHtmlMathMethod,21kIncludeInHeader,22kLaunchBinderTitle,23kLaunchDevContainerTitle,24kLinkCitations,25kNotebookLinks,26kOtherLinks,27kOtherLinksTitle,28kQuartoTemplateParams,29kRelatedFormatsTitle,30kSectionDivs,31kTocDepth,32kTocExpand,33kTocLocation,34} from "../../config/constants.ts";35import {36Format,37FormatExtras,38kBodyEnvelope,39kDependencies,40kHtmlFinalizers,41kHtmlPostprocessors,42kSassBundles,43Metadata,44OtherLink,45SassLayer,46} from "../../config/types.ts";47import { PandocFlags } from "../../config/types.ts";48import { hasTableOfContents } from "../../config/toc.ts";4950import { resolveBootstrapScss } from "./format-html-scss.ts";51import {52formatHasArticleLayout,53formatHasFullLayout,54formatPageLayout,55hasMarginCites,56hasMarginFigCaps,57hasMarginRefs,58kAppendixStyle,59kBootstrapDependencyName,60kDocumentCss,61kPageLayout,62kPageLayoutCustom,63setMainColumn,64} from "./format-html-shared.ts";65import {66HtmlPostProcessor,67HtmlPostProcessResult,68PandocInputTraits,69RenderedFormat,70RenderServices,71} from "../../command/render/types.ts";72import { processDocumentAppendix } from "./format-html-appendix.ts";73import {74documentTitleIncludeInHeader,75documentTitleMetadata,76documentTitlePartial,77documentTitleScssLayer,78processDocumentTitle,79} from "./format-html-title.ts";80import { darkModeDefault } from "./format-html-info.ts";8182import { kTemplatePartials } from "../../command/render/template.ts";83import { isHtmlOutput } from "../../config/format.ts";84import { emplaceNotebookPreviews } from "./format-html-notebook.ts";85import { ProjectContext } from "../../project/types.ts";86import { AlternateLink, otherFormatLinks } from "./format-html-links.ts";87import { warning } from "../../deno_ral/log.ts";88import { binderUrl } from "../../core/container.ts";89import { codeSpacesUrl } from "../../core/container.ts";9091export function bootstrapFormatDependency() {92const boostrapResource = (resource: string) =>93formatResourcePath(94"html",95join("bootstrap", "dist", resource),96);97const bootstrapDependency = (resource: string) => ({98name: resource,99path: boostrapResource(resource),100});101102return {103name: kBootstrapDependencyName,104stylesheets: [105bootstrapDependency("bootstrap-icons.css"),106],107scripts: [108bootstrapDependency("bootstrap.min.js"),109],110resources: [111bootstrapDependency("bootstrap-icons.woff"),112],113};114}115116export function bootstrapExtras(117input: string,118flags: PandocFlags,119format: Format,120services: RenderServices,121offset: string | undefined,122project: ProjectContext,123quiet?: boolean,124): FormatExtras {125const toc = hasTableOfContents(flags, format);126const tocLocation = toc127? format.metadata[kTocLocation] || "right"128: undefined;129130const renderTemplate = (template: string, pageLayout: string) => {131return renderEjs(formatResourcePath("html", `templates/${template}`), {132toc,133tocLocation,134pageLayout,135});136};137138const pageLayout = formatPageLayout(format);139const bodyEnvelope = formatHasArticleLayout(format)140? {141before: renderTemplate("before-body-article.ejs", pageLayout),142afterPreamble: renderTemplate(143"after-body-article-preamble.ejs",144pageLayout,145),146afterPostamble: renderTemplate(147"after-body-article-postamble.ejs",148pageLayout,149),150}151: {152before: renderTemplate("before-body-custom.ejs", kPageLayoutCustom),153afterPreamble: renderTemplate(154"after-body-custom-preamble.ejs",155kPageLayoutCustom,156),157afterPostamble: renderTemplate(158"after-body-custom-postamble.ejs",159kPageLayoutCustom,160),161};162163// Gather the title data for this page164const { partials, templateParams } = documentTitlePartial(165format,166);167const sassLayers: SassLayer[] = [];168const titleSassLayer = documentTitleScssLayer(format);169if (titleSassLayer) {170sassLayers.push(titleSassLayer);171}172const includeInHeader: string[] = [];173const titleInclude = documentTitleIncludeInHeader(174input,175format,176services.temp,177);178if (titleInclude) {179includeInHeader.push(titleInclude);180}181182const titleMetadata = documentTitleMetadata(format);183184const scssBundles = resolveBootstrapScss(input, format, sassLayers);185186return {187pandoc: {188[kSectionDivs]: true,189[kHtmlMathMethod]: "mathjax",190},191metadata: {192[kDocumentCss]: false,193[kLinkCitations]: true,194[kTemplatePartials]: partials,195[kQuartoTemplateParams]: templateParams,196...titleMetadata,197},198[kIncludeInHeader]: includeInHeader,199html: {200[kSassBundles]: scssBundles,201[kDependencies]: [bootstrapFormatDependency()],202[kBodyEnvelope]: bodyEnvelope,203[kHtmlPostprocessors]: [204bootstrapHtmlPostprocessor(205input,206format,207flags,208services,209offset,210project,211quiet,212),213],214[kHtmlFinalizers]: [215bootstrapHtmlFinalizer(format, flags),216],217},218};219}220221// Find any elements that are using fancy layouts (columns)222const getColumnLayoutElements = (doc: Document) => {223return doc.querySelectorAll(kColumnSelector);224};225226const removeColumnClasses = (el: Element) => {227for (const cls of allColumnClz) {228el.classList.remove(cls);229}230};231232const removeNestedColumnLayouts = (doc: Document) => {233const columnNodes = doc.querySelectorAll(kColumnSelector);234columnNodes.forEach((columnNode) => {235const columnEl = columnNode as Element;236237// Process nested column layouts238if (isInColumnLayout(columnEl, doc, nonScreenColumnClz)) {239removeColumnClasses(columnEl);240}241});242};243244const cleanNonsensicalMarginCaps = (doc: Document) => {245const marginCapNodes = doc.querySelectorAll(".margin-caption,.margin-ref");246marginCapNodes.forEach((capNode) => {247const capEl = capNode as Element;248if (isInColumnLayout(capEl, doc, removeMarginClz)) {249capEl.classList.remove("margin-caption");250capEl.classList.remove("margin-ref");251}252});253};254255const isInColumnLayout = (256el: Element,257doc: Document,258clzList: string[],259): boolean => {260const parent = el.parentElement;261if (!parent) {262return false;263}264265for (const cls of parent.classList) {266if (clzList.includes(cls)) {267return true;268}269}270return isInColumnLayout(parent, doc, clzList);271};272273const kColumnSelector =274'[class^="column-"], [class*=" column-"], aside:not(.footnotes):not(.sidebar), [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]';275276function bootstrapHtmlPostprocessor(277input: string,278format: Format,279flags: PandocFlags,280services: RenderServices,281offset: string | undefined,282project: ProjectContext,283quiet?: boolean,284): HtmlPostProcessor {285return async (286doc: Document,287options: {288inputMetadata: Metadata;289inputTraits: PandocInputTraits;290renderedFormats: RenderedFormat[];291quiet?: boolean;292},293): Promise<HtmlPostProcessResult> => {294// Resources used in this post processor295const resources: string[] = [];296const supporting: string[] = [];297298// use display-7 style for title299const title = doc.querySelector("header > .title");300if (title) {301title.classList.add("display-7");302}303304// add 'lead' to subtitle305const subtitle = doc.querySelector("header > .subtitle");306if (subtitle) {307subtitle.classList.add("lead");308}309310// add 'blockquote' class to blockquotes311const blockquotes = doc.querySelectorAll("blockquote");312for (let i = 0; i < blockquotes.length; i++) {313const classList = (blockquotes[i] as Element).classList;314classList.add("blockquote");315}316317// add figure classes to figures318const figures = doc.querySelectorAll("figure");319for (let i = 0; i < figures.length; i++) {320const figure = figures[i] as Element;321figure.classList.add("figure");322const images = figure.querySelectorAll("img");323for (let j = 0; j < images.length; j++) {324(images[j] as Element).classList.add("figure-img");325}326// const captions = figure.querySelectorAll("figcaption");327// for (let j = 0; j < captions.length; j++) {328// (captions[j] as Element).classList.add("figure-caption");329// }330}331332// Ensure that any magin figures / images are marked as fluid333// Attempt to fix https://github.com/quarto-dev/quarto-cli/issues/5516334const marginImgNodes = doc.querySelectorAll(335".column-margin .cell-output-display img:not(.img-fluid)",336);337for (const marginImgNode of marginImgNodes) {338const marginImgEl = marginImgNode as Element;339marginImgEl.classList.add("img-fluid");340}341342// move the toc if there is a sidebar343const toc = doc.querySelector('nav[role="doc-toc"]');344345const tocTarget = doc.getElementById("quarto-toc-target");346347const useDoubleToc =348(format.metadata[kTocLocation] as string)?.includes("-body") ?? false;349350if (toc && tocTarget) {351if (useDoubleToc) {352// Clone the TOC353// Leave it where it is in the document, and just mutate it354const clonedToc = toc.cloneNode(true) as Element;355clonedToc.id = "TOC-body";356const tocActionsEl = clonedToc.querySelector(".toc-actions");357if (tocActionsEl) {358tocActionsEl.remove();359}360361toc.parentElement?.insertBefore(clonedToc, toc);362}363364// activate selection behavior for this365toc.classList.add("toc-active");366367const expanded = format.metadata[kTocExpand];368if (expanded !== undefined) {369if (expanded === true) {370toc.setAttribute("data-toc-expanded", 99);371} else if (expanded) {372toc.setAttribute("data-toc-expanded", expanded);373} else {374toc.setAttribute("data-toc-expanded", -1);375}376}377// add nav-link class to the TOC links378const tocLinks = doc.querySelectorAll('nav#TOC[role="doc-toc"] > ul a');379380for (let i = 0; i < tocLinks.length; i++) {381// Mark the toc links as nav-links382const tocLink = tocLinks[i] as Element;383tocLink.classList.add("nav-link");384if (i === 0) {385tocLink.classList.add("active");386}387388// move the raw href to the target attribute (need the raw value, not the full path)389if (!tocLink.hasAttribute("data-scroll-target")) {390tocLink.setAttribute(391"data-scroll-target",392tocLink.getAttribute("href")?.replaceAll(":", "\\:"),393);394}395}396397// default collapse non-top level TOC nodes398const tocDepth = format.pandoc[kTocDepth] || 3;399if (tocDepth > 1) {400const ulSelector = "ul ".repeat(tocDepth - 1).trim();401402const nestedUls = toc.querySelectorAll(ulSelector);403for (let i = 0; i < nestedUls.length; i++) {404const ul = nestedUls[i] as Element;405ul.classList.add("collapse");406}407}408409toc.remove();410tocTarget.replaceWith(toc);411} else {412tocTarget?.remove();413}414415// Inject links to other formats if there is another416// format that of this file that has been rendered417if (format.render[kFormatLinks] !== false) {418processAlternateFormatLinks(input, options, doc, format, resources);419}420421// Look for included / embedded notebooks and include those422if (format.render[kNotebookLinks] !== false) {423const renderedHtml = options.renderedFormats.find((renderedFormat) => {424return isHtmlOutput(renderedFormat.format.pandoc, true);425});426const notebookResults = await emplaceNotebookPreviews(427input,428doc,429format,430services,431project,432renderedHtml?.path,433quiet,434);435if (notebookResults) {436resources.push(...notebookResults.resources);437supporting.push(...notebookResults.supporting);438}439}440441// Process additional links for this document442await processOtherLinks(doc, format, project);443444// default treatment for computational tables445const addTableClasses = (table: Element, computational = false) => {446table.classList.add("table");447if (computational) {448table.classList.add("table-sm");449table.classList.add("table-striped");450table.classList.add("small");451}452};453454// add .table class to pandoc tables455const tableHeaders = doc.querySelectorAll("tbody > tr:first-child.odd");456for (let i = 0; i < tableHeaders.length; i++) {457const th = tableHeaders[i];458if (th.parentNode?.parentNode) {459const table = th.parentNode.parentNode as Element;460table.removeAttribute("style");461// see https://github.com/quarto-dev/quarto-cli/issues/6945462// for a why we want to check for 'plain' here463addTableClasses(464table,465!!findParent(466table,467(el) =>468el.classList.contains("cell") && !el.classList.contains("plain"),469),470);471}472}473474// add .table class to pandas tables475const pandasTables = doc.querySelectorAll("table.dataframe");476for (let i = 0; i < pandasTables.length; i++) {477const table = pandasTables[i] as Element;478table.removeAttribute("border");479addTableClasses(table, true);480const headerRows = table.querySelectorAll("tr");481for (let r = 0; r < headerRows.length; r++) {482(headerRows[r] as Element).removeAttribute("style");483}484if (485table.previousElementSibling &&486table.previousElementSibling.tagName === "STYLE"487) {488table.previousElementSibling.remove();489}490}491492// add .table class to DataFrames.jl tables493const dataFramesTables = doc.querySelectorAll("table.data-frame");494for (let i = 0; i < dataFramesTables.length; i++) {495const table = dataFramesTables[i] as Element;496addTableClasses(table, true);497}498499// provide data-anchor-id to headings500const sections = doc.querySelectorAll('section[class^="level"]');501for (let i = 0; i < sections.length; i++) {502const section = sections[i] as Element;503const heading = section.querySelector("h2") ||504section.querySelector("h3") || section.querySelector("h4") ||505section.querySelector("h5") || section.querySelector("h6");506if (heading) {507heading.setAttribute("data-anchor-id", section.id);508}509}510511// Process the title elements of this document512const titleResourceFiles = processDocumentTitle(513input,514format,515flags,516doc,517);518resources.push(...titleResourceFiles);519520// put quarto-html-before-body script at top of body521const beforeBodyScript = doc.querySelector(522"script#quarto-html-before-body",523);524if (beforeBodyScript) {525doc.body.insertBefore(beforeBodyScript, doc.body.firstChild);526}527528// Process the elements of this document into an appendix529if (530format.metadata[kAppendixStyle] !== false &&531format.metadata[kAppendixStyle] !== "none"532) {533await processDocumentAppendix(534input,535options.inputTraits,536format,537flags,538doc,539offset,540);541}542// no resource refs543return Promise.resolve({ resources, supporting });544};545}546547function createLinkChild(formatLink: AlternateLink, doc: Document) {548const link = doc.createElement("a");549link.setAttribute("href", formatLink.href);550if (formatLink.attr) {551for (const key of Object.keys(formatLink.attr)) {552const value = formatLink.attr[key];553link.setAttribute(key, value);554}555}556557if (formatLink.dlAttrValue) {558link.setAttribute("download", formatLink.dlAttrValue);559}560561const icon = doc.createElement("i");562icon.classList.add("bi");563icon.classList.add(`bi-${formatLink.icon}`);564link.appendChild(icon);565link.appendChild(doc.createTextNode(formatLink.title));566567return link;568}569570async function processOtherLinks(571doc: Document,572format: Format,573context?: ProjectContext,574) {575const processLinks = (576otherLinks: OtherLink[],577clz: string,578title: string,579) => {580const dlLinkTarget = getLinkTarget(doc, kLinkProvidersOtherLinks);581if (otherLinks.length > 0 && dlLinkTarget) {582const containerEl = doc.createElement("div");583containerEl.classList.add(clz);584585const heading = dlLinkTarget.makeHeadingEl(586title,587);588containerEl.appendChild(heading);589590const getAttrs = (otherLink: OtherLink) => {591if (otherLink.rel || otherLink.target) {592const attrs: Record<string, string> = {};593if (otherLink.rel) {594attrs.rel = otherLink.rel;595}596if (otherLink.target) {597attrs.target = otherLink.target;598}599return attrs;600} else {601return undefined;602}603};604605const linkList = dlLinkTarget.makeContainerEl();606let order = 0;607for (let i = 0; i < otherLinks.length; i++) {608const otherLink = otherLinks[i];609const alternateLink: AlternateLink = {610icon: otherLink.icon || "link-45deg",611href: otherLink.href,612title: otherLink.text,613order: ++order,614attr: getAttrs(otherLink),615};616const li = dlLinkTarget.makeItemEl(617createLinkChild(alternateLink, doc),618i,619otherLinks.length,620);621if (linkList) {622linkList.appendChild(li);623} else {624containerEl.appendChild(li);625}626}627if (linkList) {628containerEl.appendChild(linkList);629}630631dlLinkTarget.targetEl.appendChild(containerEl);632}633};634635const resolveCodeLinks = async (636metadata: Metadata,637context?: ProjectContext,638): Promise<OtherLink[]> => {639const codeLinks = metadata[kCodeLinks] as640| boolean641| string642| string[]643| OtherLink[];644if (codeLinks !== undefined) {645if (typeof codeLinks === "boolean") {646return [];647} else if (typeof codeLinks === "string") {648if (!context) {649throw new Error(650`The code-link value '${codeLinks}' is only supported from within a project.`,651);652}653const resolvedCodeLink = await resolveCodeLink(codeLinks, context);654if (resolvedCodeLink) {655return [resolvedCodeLink];656} else {657throw new Error(`Unknown code-link value '${codeLinks}'`);658}659} else {660const outputLinks: OtherLink[] = [];661for (const codeLink of codeLinks) {662if (typeof codeLink === "string") {663if (!context) {664throw new Error(665`The code-link value '${codeLink}' is only supported from within a project.`,666);667}668const resolvedCodeLink = await resolveCodeLink(codeLink, context);669if (resolvedCodeLink) {670outputLinks.push(resolvedCodeLink);671} else {672throw new Error(`Unknown code-link value '${codeLink}'`);673}674} else {675outputLinks.push(codeLink);676}677}678return outputLinks;679}680}681return [];682};683684const resolveCodeLink = async (685link: string,686context: ProjectContext,687): Promise<OtherLink | undefined> => {688if (link === "repo") {689const env = await context.environment();690if (env.github.repoUrl) {691return {692icon: "github",693text: "GitHub Repo",694href: env.github.repoUrl,695target: "_blank",696};697} else {698warning(699"The 'repo' code link is not able to be created as the project isn't a GitHub project.",700);701}702} else if (link === "devcontainer") {703const env = await context.environment();704if (705env.github.organization && env.github.repository && env.github.repoUrl706) {707const containerUrl = codeSpacesUrl(env.github.repoUrl);708return {709icon: "github",710text: format.language[kLaunchDevContainerTitle] ||711"Launch Dev Container",712href: containerUrl,713target: "_blank",714};715} else {716warning(717"The 'devcontainer' code link is not able to be created as the project isn't a GitHub project.",718);719}720} else if (link === "binder") {721const env = await context.environment();722if (env.github.organization && env.github.repository) {723const containerUrl = binderUrl(724env.github.organization,725env.github.repository,726{727// TODO: figure out open file path (if support in vscode/rstudio)728// openFile: extname(source) === ".ipynb" ? source : undefined729editor: env.codeEnvironment,730},731);732return {733icon: "journals",734text: format.language[kLaunchBinderTitle] ||735"Launch Binder",736href: containerUrl,737target: "_blank",738};739} else {740warning(741"The 'binder' code link is not able to be created as the project isn't a GitHub project.",742);743}744}745};746747const codeLinks = await resolveCodeLinks(format.metadata, context);748749const otherLinkOptions = [{750links: (format.metadata[kOtherLinks] || []) as OtherLink[],751clz: "quarto-other-links",752title: format.language[kOtherLinksTitle] || "Other Links",753}, {754links: codeLinks,755clz: "quarto-code-links",756title: format.language[kCodeLinksTitle] || "Code Links",757}];758otherLinkOptions.forEach((linkDesc) => {759processLinks(linkDesc.links, linkDesc.clz, linkDesc.title);760});761}762763type selector = string;764interface LinkProvider {765makeHeadingEl: (doc: Document, text?: string) => Element;766makeContainerEl: (_doc: Document) => Element | undefined;767makeItemEl: (doc: Document, el: Element, idx: number, len: number) => Element;768}769770const bannerHeadingLinkProvider = {771makeHeadingEl: (doc: Document, text?: string) => {772const headingEl = doc.createElement("div");773headingEl.classList.add("quarto-title-meta-heading");774if (text) {775headingEl.innerText = text;776}777return headingEl;778},779makeContainerEl: (_doc: Document) => {780return undefined;781},782makeItemEl: (doc: Document, el: Element) => {783const itemEl = doc.createElement("div");784itemEl.classList.add("quarto-title-meta-contents");785786const pEl = doc.createElement("p");787pEl.appendChild(el);788itemEl.appendChild(pEl);789return itemEl;790},791};792793const bannerTextLinkProvider = {794makeHeadingEl: (doc: Document, text?: string) => {795const headingEl = doc.createElement("div");796headingEl.classList.add("quarto-title-meta-heading");797if (text) {798headingEl.innerText = text;799}800return headingEl;801},802makeContainerEl: (doc: Document) => {803const headingEl = doc.createElement("div");804headingEl.classList.add("quarto-title-meta-contents");805return headingEl;806},807makeItemEl: (doc: Document, el: Element, idx: number, len: number) => {808const itemEl = doc.createElement("span");809810itemEl.appendChild(el);811if (idx < len - 1) {812itemEl.append(doc.createTextNode(","));813itemEl.setAttribute("style", "padding-right: 0.5em;");814}815816return itemEl;817},818};819820const kLinkProvidersOtherLinks: Record<selector, LinkProvider> = {821"quarto-other-links-target": bannerHeadingLinkProvider,822"quarto-other-links-text-target": bannerTextLinkProvider,823};824825const kLinkProvidersOtherFormats: Record<selector, LinkProvider> = {826"quarto-other-formats-target": bannerHeadingLinkProvider,827"quarto-other-formats-text-target": bannerTextLinkProvider,828};829830function getLinkTarget(831doc: Document,832linkProviders?: Record<selector, LinkProvider>,833) {834// Look for an explicit target835if (linkProviders) {836for (const sel of Object.keys(linkProviders)) {837const explicitTarget = doc.querySelector(`.${sel}`);838if (explicitTarget !== null) {839const linkProvider = linkProviders[sel];840return {841targetEl: explicitTarget,842makeHeadingEl: (text?: string) => {843return linkProvider.makeHeadingEl(doc, text);844},845makeContainerEl: () => {846return linkProvider.makeContainerEl(doc);847},848makeItemEl: (el: Element, idx: number, len: number) => {849return linkProvider.makeItemEl(doc, el, idx, len);850},851};852}853}854}855856// Now search for a place to put the links857let dlLinkTarget = doc.querySelector(`nav[role="doc-toc"]`);858if (dlLinkTarget === null) {859dlLinkTarget = doc.getElementById("quarto-sidebar-toc-left");860}861if (dlLinkTarget === null) {862dlLinkTarget = doc.getElementById(kMarginSidebarId);863}864if (dlLinkTarget !== null) {865return {866targetEl: dlLinkTarget,867makeHeadingEl: (text?: string) => {868const headingEl = doc.createElement("h2");869if (text) {870headingEl.innerText = text;871}872return headingEl;873},874makeContainerEl: () => {875return doc.createElement("ul");876},877makeItemEl: (el: Element) => {878const liEl = doc.createElement("li");879liEl.appendChild(el);880return liEl;881},882};883}884}885886function processAlternateFormatLinks(887input: string,888options: {889inputMetadata: Metadata;890inputTraits: PandocInputTraits;891renderedFormats: RenderedFormat[];892},893doc: Document,894format: Format,895resources: string[],896) {897if (options.renderedFormats.length > 1) {898const dlLinkTarget = getLinkTarget(doc, kLinkProvidersOtherFormats);899if (dlLinkTarget) {900const containerEl = doc.createElement("div");901containerEl.classList.add("quarto-alternate-formats");902903const heading = dlLinkTarget.makeHeadingEl(904format.language[kRelatedFormatsTitle],905);906containerEl.appendChild(heading);907908const otherLinks = otherFormatLinks(909input,910format,911options.renderedFormats,912);913914const formatList = dlLinkTarget.makeContainerEl();915const sortedLinks = otherLinks.sort(({ order: a }, { order: b }) =>916a - b917);918for (let i = 0; i < sortedLinks.length; i++) {919const alternateLink = sortedLinks[i];920const link = doc.createElement("a");921link.setAttribute("href", alternateLink.href);922if (alternateLink.dlAttrValue) {923link.setAttribute("download", alternateLink.dlAttrValue);924}925if (alternateLink.attr) {926for (const key of Object.keys(alternateLink.attr)) {927const value = alternateLink.attr[key];928link.setAttribute(key, value);929}930}931932const icon = doc.createElement("i");933icon.classList.add("bi");934icon.classList.add(`bi-${alternateLink.icon}`);935link.appendChild(icon);936link.appendChild(doc.createTextNode(alternateLink.title));937938const li = dlLinkTarget.makeItemEl(link, i, sortedLinks.length);939if (formatList) {940formatList.appendChild(li);941} else {942containerEl.appendChild(li);943}944945resources.push(alternateLink.href);946}947948if (otherLinks.length > 0) {949if (formatList) {950containerEl.appendChild(formatList);951}952dlLinkTarget.targetEl.appendChild(containerEl);953}954}955}956}957958function bootstrapHtmlFinalizer(format: Format, flags: PandocFlags) {959return (doc: Document): Promise<void> => {960const { citesInMargin, refsInMargin } = processColumnElements(961doc,962format,963flags,964);965966if (format.metadata[kDisableArticleLayout]) {967const stripColumnClasses = (el: Element) => {968const stripClz: string[] = [];969el.classList.forEach((clz) => {970if (971clz === "margin-caption" || clz === "margin-ref" ||972clz.startsWith("column-") || clz === "page-columns" ||973clz === "page-full"974) {975stripClz.push(clz);976}977});978el.classList.remove(...stripClz);979for (const childEl of el.children) {980stripColumnClasses(childEl);981}982};983984const mainEl = doc.body.querySelector("main");985if (mainEl) {986stripColumnClasses(mainEl);987}988}989990// provide heading for footnotes (but only if there is one section, there could991// be multiple if they used reference-location: block/section)992if (refsInMargin) {993const footNoteSectionEl = doc.querySelector("section.footnotes");994if (footNoteSectionEl) {995footNoteSectionEl.remove();996}997}998999// Purge the bibliography if we're using refs in margin1000if (citesInMargin) {1001const bibliographyDiv = doc.querySelector("div#refs");1002if (bibliographyDiv) {1003bibliographyDiv.remove();1004}1005}10061007const fullLayout = formatHasFullLayout(format);1008if (fullLayout) {1009// If we're in a full layout, get rid of empty sidebar elements1010const leftSidebar = hasContents(kSidebarId, doc);1011if (!leftSidebar) {1012const sidebarEl = doc.getElementById(kSidebarId);1013sidebarEl?.remove();1014}10151016const column = suggestColumn(doc);1017setMainColumn(doc, column);1018}10191020// Note whether we need a narrow or wide margin layout1021const hasToc = !!format.pandoc.toc;1022const leftSidebar = doc.getElementById("quarto-sidebar");1023const hasLeftContent = leftSidebar && leftSidebar.children.length > 0;1024const rightSidebar = doc.getElementById("quarto-margin-sidebar");1025const hasRightContent = rightSidebar && rightSidebar.children.length > 0;1026const hasMarginContent =1027doc.querySelectorAll(".column-margin").length > 0 ||1028doc.querySelectorAll(".margin-caption").length > 0 ||1029doc.querySelectorAll(".margin-ref").length > 0;10301031if (rightSidebar && !hasRightContent && !hasMarginContent && !hasToc) {1032rightSidebar.remove();1033}10341035// Set the content mode for the grid system1036const gridObj = format.metadata[kGrid] as Metadata;1037let contentMode = "auto";1038if (gridObj) {1039contentMode =1040gridObj[kContentMode] as ("auto" | "standard" | "full" | "slim");1041}10421043if (contentMode === undefined || contentMode === "auto") {1044const hasColumnElements = getColumnLayoutElements(doc).length > 0;1045if (hasColumnElements) {1046if (hasLeftContent && hasMarginContent) {1047// Slim down the content area so there are sizable margins1048// for the column element1049doc.body.classList.add("slimcontent");1050} else if (1051hasRightContent || hasMarginContent || fullLayout || hasToc1052) {1053// Use the default layout, so don't add any classes1054} else {1055doc.body.classList.add("fullcontent");1056}1057} else {1058if (!hasRightContent && !hasMarginContent && !hasToc) {1059doc.body.classList.add("fullcontent");1060} else {1061// Use the deafult layout, don't add any classes1062}1063}1064} else {1065if (contentMode === "slim") {1066doc.body.classList.add("slimcontent");1067} else if (contentMode === "full") {1068doc.body.classList.add("fullcontent");1069}1070}10711072// start body with light or dark class for proper display when JS is disabled1073let initialLightDarkClass = "quarto-light";1074if (darkModeDefault(format)) {1075initialLightDarkClass = "quarto-dark";1076}1077doc.body.classList.add(initialLightDarkClass);10781079// If there is no margin content and no toc in the right margin1080// then lower the z-order so everything else can get on top1081// of the sidebar1082const isFullLayout = format.metadata[kPageLayout] === "full";1083const marginSidebarEl = doc.getElementById("quarto-margin-sidebar");1084if (1085(!hasMarginContent && isFullLayout && !hasRightContent) ||1086marginSidebarEl?.childElementCount === 01087) {1088marginSidebarEl?.classList.add("zindex-bottom");1089}1090return Promise.resolve();1091};1092}10931094function processColumnElements(1095doc: Document,1096format: Format,1097flags: PandocFlags,1098) {1099// Clean nested columns - the outer layout will win1100removeNestedColumnLayouts(doc);11011102// Remove any margin captions that don't make sense (since the right1103// margin is occluded by the element with the caption)1104cleanNonsensicalMarginCaps(doc);11051106// Margin and column elements are only functional in article based layouts1107if (!formatHasArticleLayout(format)) {1108return {1109citesInMargin: false,1110refsInMargin: false,1111};1112}11131114// Process captions that may appear in the margin1115processMarginCaptions(doc);11161117// Process margin elements that may appear in callouts1118processMarginElsInCallouts(doc);11191120// Process margin elements that may appear in tabsets1121processMarginElsInTabsets(doc);11221123// Process non-margin figures - forward the column class1124// down into the figure so that the caption remains in the document1125// flow and the figure itself takes the column sizing1126processFigureOutputs(doc);11271128// Group margin elements by their parents and wrap them in a container1129// Be sure to ignore containers which are already processed1130// and should be left alone1131const marginProcessors: MarginNodeProcessor[] = [1132simpleMarginProcessor,1133];11341135// If figure captions are enabled, look out for them in callouts1136if (hasMarginFigCaps(format)) {1137marginProcessors.push(figCapInCalloutMarginProcessor);1138}11391140// If margin footnotes are enabled move them1141const refsInMargin = hasMarginRefs(format, flags);1142if (refsInMargin) {1143marginProcessors.unshift(footnoteMarginProcessor);1144}11451146// If margin cites are enabled, move them1147const citesInMargin = hasMarginCites(format);1148if (citesInMargin) {1149marginProcessors.push(referenceMarginProcessor);1150}1151processMarginNodes(doc, marginProcessors);11521153// If margin footnotes are enabled, remove any containers provided1154if (refsInMargin) {1155const footnoteContainer = doc.getElementById("footnotes");1156// Since it has margin footnotes, remove the end notes section1157if (footnoteContainer) {1158footnoteContainer.remove();1159}1160}11611162const columnLayouts = getColumnLayoutElements(doc);11631164// If there are any of these elements, we need to be sure that their1165// parents have acess to the grid system, so make the parent full screen width1166// and apply the grid system to it (now the child 'column-' element can be positioned1167// anywhere in the grid system)1168if (columnLayouts && columnLayouts.length > 0) {1169const processEl = (el: Element) => {1170if (el.tagName === "DIV" && el.id === "quarto-content") {1171return false;1172} else if (el.tagName === "BODY") {1173return false;1174} else {1175return true;1176}1177};11781179const ensureInGrid = (el: Element, setLayout: boolean) => {1180if (processEl(el)) {1181// Add the grid system. Children of the grid system1182// are placed into the body-content column by default1183// (CSS implements this)1184if (1185!el.classList.contains("quarto-layout-row") &&1186!el.classList.contains("page-columns")1187) {1188el.classList.add("page-columns");1189}11901191// Mark full width1192if (setLayout && !el.classList.contains("page-full")) {1193el.classList.add("page-full");1194}11951196// Process parents up to the main tag1197const parent = el.parentElement;1198if (parent) {1199ensureInGrid(parent, true);1200}1201}1202};12031204columnLayouts.forEach((node) => {1205const el = node as Element;1206if (el.parentElement) {1207ensureInGrid(el.parentElement, true);1208}1209});1210}12111212return {1213citesInMargin,1214refsInMargin,1215};1216}12171218const processMarginNodes = (1219doc: Document,1220processors: MarginNodeProcessor[],1221) => {1222const marginSelector = processors.map((proc) => proc.selector).join(1223", ",1224);1225const marginNodes = doc.querySelectorAll(marginSelector);1226marginNodes.forEach((marginNode) => {1227const marginEl = marginNode as Element;1228for (const processor of processors) {1229if (processor.canProcess(marginEl)) {1230processor.process(marginEl, doc);1231break;1232}1233}1234marginEl.classList.remove("column-margin");1235});1236};12371238const findQuartoFigure = (el: Element): Element | undefined => {1239if (1240el.classList.contains("quarto-figure") ||1241el.classList.contains("quarto-layout-panel") ||1242el.classList.contains("quarto-float")1243) {1244return el;1245} else if (el.parentElement) {1246return findQuartoFigure(el.parentElement);1247} else {1248return undefined;1249}1250};12511252const moveClassToCaption = (container: Element, sel: string) => {1253const target = container.querySelector(sel);1254if (target) {1255target.classList.add("margin-caption");1256return true;1257} else {1258return false;1259}1260};12611262const removeCaptionClass = (el: Element) => {1263// Remove this since it will place the contents in the margin if it remains present1264el.classList.remove("margin-caption");1265};12661267const processLayoutPanelMarginCaption = (captionContainer: Element) => {1268const figure = captionContainer.querySelector("figure");1269if (figure) {1270// It is a figure panel, find a direct child caption of the outer figure.1271for (const child of figure.children) {1272if (child.tagName === "FIGCAPTION") {1273child.classList.add("margin-caption");1274removeCaptionClass(captionContainer);1275break;1276}1277}1278} else {1279// it is not a figure panel, find the panel caption1280const caption = captionContainer.querySelector(".panel-caption");1281if (caption) {1282caption.classList.add("margin-caption");1283removeCaptionClass(captionContainer);1284}1285}1286};12871288const processFigureMarginCaption = (1289captionContainer: Element,1290doc: Document,1291) => {1292// First try finding a fig caption1293const foundCaption = moveClassToCaption(captionContainer, "figcaption");1294if (!foundCaption) {1295// find a table caption and copy the contents into a div with style figure-caption1296// note that for tables, our grid inception approach isn't going to work, so1297// we make a copy of the caption contents and place that in the same container as the1298// table and bind it to the grid1299const captionEl = captionContainer.querySelector("caption");1300if (captionEl) {1301const parentDivEl = captionEl?.parentElement?.parentElement;1302if (parentDivEl) {1303captionEl.classList.add("hidden");13041305const divCopy = doc.createElement("div");1306divCopy.classList.add("figure-caption");1307divCopy.classList.add("margin-caption");1308divCopy.innerHTML = captionEl.innerHTML;1309parentDivEl.appendChild(divCopy);1310removeCaptionClass(captionContainer);1311}1312}1313} else {1314removeCaptionClass(captionContainer);1315}1316};13171318const processTableMarginCaption = (1319captionContainer: Element,1320doc: Document,1321) => {1322// Find a caption1323const caption = captionContainer.querySelector("caption");1324if (caption) {1325const marginCapEl = doc.createElement("DIV");1326marginCapEl.classList.add("quarto-table-caption");1327marginCapEl.classList.add("margin-caption");1328marginCapEl.innerHTML = caption.innerHTML;13291330captionContainer.parentElement?.insertBefore(1331marginCapEl,1332captionContainer.nextElementSibling,1333);13341335caption.remove();1336removeCaptionClass(captionContainer);1337}1338};13391340// Process any captions that appear in margins1341const processMarginCaptions = (doc: Document) => {1342// Identify elements that already appear in the margin1343// and in this case, remove the margin-caption class1344// since we do not want to further process the caption into the margin1345const captionsAlreadyInMargin = doc.querySelectorAll(1346".column-margin .margin-caption",1347);1348captionsAlreadyInMargin.forEach((node) => {1349const el = node as Element;1350el.classList.remove("margin-caption");1351});13521353// Forward caption class from parents to the child fig caps1354const marginCaptions = doc.querySelectorAll(".margin-caption");1355marginCaptions.forEach((node) => {1356const figureEl = node as Element;1357const captionContainer = findQuartoFigure(figureEl);1358if (captionContainer) {1359// Deal with layout panels (we will only handle the main caption not the internals)1360const isLayoutPanel = captionContainer.classList.contains(1361"quarto-layout-panel",1362);13631364// has explicitly set cap location1365const explicitCapLoc = captionContainer.getAttribute("data-cap-location");1366if (explicitCapLoc == null || explicitCapLoc == "margin") {1367if (isLayoutPanel) {1368processLayoutPanelMarginCaption(captionContainer);1369} else {1370processFigureMarginCaption(captionContainer, doc);1371}1372}1373} else {1374// Deal with table margin captions1375if (figureEl.classList.contains("tbl-parent")) {1376// This is table panel, so only grab the main caption1377const capDivEl = figureEl.querySelector("div.panel-caption");1378if (capDivEl) {1379capDivEl.classList.add("margin-caption");1380capDivEl.remove();1381figureEl.appendChild(capDivEl);1382}1383} else {1384// This is just a table, grab that caption1385const table = figureEl.querySelector("table");1386if (table) {1387processTableMarginCaption(table, doc);1388}1389}1390}1391removeCaptionClass(figureEl);1392});1393};13941395const processMarginElsInCallouts = (doc: Document) => {1396const calloutNodes = doc.querySelectorAll("div.callout");1397calloutNodes.forEach((calloutNode) => {1398const calloutEl = calloutNode as Element;1399const collapseEl = calloutEl.querySelector(".callout-collapse");1400const isSimple = calloutEl.classList.contains("callout-style-simple");14011402//Get the collapse classes (if any) to forward long1403const collapseClasses: string[] = [];1404if (collapseEl) {1405collapseEl.classList.forEach((clz) => collapseClasses.push(clz));1406}14071408const marginNodes = calloutEl.querySelectorAll(1409".callout-body-container .column-margin, .callout-body-container aside:not(.footnotes):not(.sidebar), .callout-body-container .aside:not(.footnotes)",1410);14111412if (marginNodes.length > 0) {1413const marginArr = Array.from(marginNodes);1414marginArr.reverse().forEach((marginNode) => {1415const marginEl = marginNode as Element;1416collapseClasses.forEach((clz) => {1417marginEl.classList.add(clz);1418});1419marginEl.classList.add("callout-margin-content");1420if (isSimple) {1421marginEl.classList.add("callout-margin-content-simple");1422}14231424calloutEl.after(marginEl);1425});1426}1427});1428};14291430const figCapInCalloutMarginProcessor: MarginNodeProcessor = {1431selector: ".callout",1432canProcess(el: Element) {1433const hasFigCap = el.querySelector("figcaption");1434return hasFigCap !== null;1435},1436process(el: Element, doc: Document) {1437const collapseEl = el.querySelector(".callout-collapse");1438const isSimple = el.classList.contains("callout-style-simple");1439//Get the collapse classes (if any) to forward long1440const collapseClasses: string[] = [];1441if (collapseEl) {1442collapseEl.classList.forEach((clz) => collapseClasses.push(clz));1443}14441445const figNodes = el.querySelectorAll("figure");1446for (const figNode of Array.from(figNodes).reverse()) {1447const figEl = figNode as Element;1448const figCaptionEl = figEl.querySelector("figcaption");14491450// Usually the figure id is on the parent div1451let figureId = figEl.id;1452if (figureId === "") {1453figureId = figEl.parentElement?.id || "";1454}14551456const captionId = figureId + "-caption";1457figEl.setAttribute("aria-labelledby", captionId);14581459if (figCaptionEl !== null) {1460// Move the caption contents into a div1461figCaptionEl.remove();1462const div = doc.createElement("DIV");1463div.id = captionId;1464div.classList.add("margin-figure-caption");1465div.classList.add("column-margin");1466collapseClasses.forEach((clz) => {1467div.classList.add(clz);1468});14691470div.classList.add("callout-margin-content");1471if (isSimple) {1472div.classList.add("callout-margin-content-simple");1473}14741475figCaptionEl.childNodes.forEach((node) => {1476div.append(node);1477});1478el.parentElement?.insertBefore(div, el.nextElementSibling);1479}1480}1481},1482};14831484const kPreviewFigColumnForwarding = [".grid"];14851486const isInsideAbout = (el: Element) =>1487!!findParent(1488el,1489(parent) =>1490Array.from(parent.classList).some((x) => x.startsWith("quarto-about-")),1491);14921493const processFigureOutputs = (doc: Document) => {1494// For any non-margin figures, we want to actually place the figure itself1495// into the column, and leave the caption as is, if possible1496const columnEls = doc.querySelectorAll(1497'[class^="column-"]:not(.column-margin), [class*=" column-"]:not(.column-margin)',1498);14991500const moveColumnClasses = (fromEl: Element, toEl: Element) => {1501const clzList: string[] = [];1502for (const clz of fromEl.classList) {1503if (clz.startsWith("column-")) {1504clzList.push(clz);1505}1506}1507fromEl.classList.remove(...clzList);1508toEl.classList.add(...clzList);1509};15101511for (const columnNode of columnEls) {1512// See if this is a code cell with a single figure output1513const columnEl = columnNode as Element;15141515// See if there are any classes which should prohibit forwarding1516// the column information1517if (1518kPreviewFigColumnForwarding.some((sel) => {1519return columnEl.querySelector(sel) !== null;1520})1521) {1522// There are matching ignore selectors, just skip1523// this column1524continue;1525}15261527// If there is a single figure, then forward the column class onto that1528const figures = columnEl.querySelectorAll("figure img.figure-img");15291530if (1531figures && figures.length === 1 && !isInsideAbout(figures[0] as Element)1532) {1533moveColumnClasses(columnEl, figures[0] as Element);1534} else {1535const layoutFigures = columnEl.querySelectorAll(1536".quarto-layout-panel > figure.figure .quarto-layout-row",1537);1538if (1539layoutFigures && layoutFigures.length === 1 &&1540!isInsideAbout(layoutFigures[0] as Element)1541) {1542moveColumnClasses(columnEl, layoutFigures[0] as Element);1543}1544}1545}1546};15471548const processMarginElsInTabsets = (doc: Document) => {1549// Move margin elements inside tabsets into a separate container that appears1550// before the tabset- this will hold the margin content1551// quarto.js will detect tab changed events and propery show and hide elements1552// by marking them with a collapse class.15531554const tabSetNodes = doc.querySelectorAll("div.panel-tabset");1555tabSetNodes.forEach((tabsetNode) => {1556const tabSetEl = tabsetNode as Element;1557const tabNodes = tabSetEl.querySelectorAll("div.tab-pane");15581559const marginEls: Element[] = [];1560let count = 0;1561tabNodes.forEach((tabNode) => {1562const tabEl = tabNode as Element;1563const tabId = tabEl.id;15641565const marginNodes = tabEl.querySelectorAll(1566".column-margin, aside:not(.footnotes):not(.sidebar), .aside:not(.footnotes)",1567);15681569if (tabId && marginNodes.length > 0) {1570const marginArr = Array.from(marginNodes);1571marginArr.forEach((marginNode) => {1572const marginEl = marginNode as Element;1573marginEl.classList.add("tabset-margin-content");1574marginEl.classList.add(`${tabId}-tab-margin-content`);1575if (count > 0) {1576marginEl.classList.add("collapse");1577}1578marginEls.push(marginEl);1579});1580}1581count++;1582});15831584if (marginEls) {1585const containerEl = doc.createElement("div");1586containerEl.classList.add("tabset-margin-container");1587marginEls.forEach((marginEl) => {1588containerEl.appendChild(marginEl);1589});1590tabSetEl.before(containerEl);1591}1592});1593};15941595interface MarginNodeProcessor {1596selector: string;1597canProcess(el: Element): boolean;1598process(el: Element, doc: Document): void;1599}16001601const simpleMarginProcessor: MarginNodeProcessor = {1602selector: ".column-margin:not(.column-container)",1603canProcess(el: Element) {1604return el.classList.contains("column-margin") &&1605!el.classList.contains("column-container");1606},1607process(el: Element, doc: Document) {1608el.classList.remove("column-margin");16091610const kPopMarginElOutOfTags = ["DD"];16111612// Specially deal with DD1613if (1614el.parentElement &&1615kPopMarginElOutOfTags.includes(el.parentElement?.tagName)1616) {1617const parentElement = el.parentElement;1618// This is in a tag which itself can't be a container1619// pop it out1620// make a container which is next to the parent and1621// place that in the margin1622// For examples of this, see:1623// https://github.com/quarto-dev/quarto-cli/issues/88621624const marginContainer = doc.createElement("DIV");1625el.remove();1626marginContainer.appendChild(el);1627parentElement.parentElement?.insertBefore(1628marginContainer,1629parentElement.nextSibling,1630);1631addContentToMarginContainerForEl(marginContainer, marginContainer, doc);1632} else {1633addContentToMarginContainerForEl(el, el, doc);1634}1635},1636};16371638const footnoteMarginProcessor: MarginNodeProcessor = {1639selector: ".footnote-ref",1640canProcess(el: Element) {1641return el.classList.contains("footnote-ref");1642},1643process(el: Element, doc: Document) {1644if (el.hasAttribute("href")) {1645const target = el.getAttribute("href");1646if (target) {1647// First try to grab a the citation or footnote.1648const refId = target.slice(1);1649const refContentsEl = doc.getElementById(refId);16501651if (refContentsEl) {1652// Find and remove the backlink1653const backLinkEl = refContentsEl.querySelector(".footnote-back");1654if (backLinkEl) {1655backLinkEl.remove();1656}16571658const invalidParentTags = [1659"SPAN",1660"EM",1661"STRONG",1662"DEL",1663"H1",1664"H2",1665"H3",1666"H4",1667"H5",1668"H6",1669];1670const findValidParentEl = (el: Element): Element | undefined => {1671if (1672el.parentElement &&1673!invalidParentTags.includes(el.parentElement.tagName)1674) {1675return el.parentElement;1676} else if (el.parentElement) {1677return findValidParentEl(el.parentElement);1678} else {1679return undefined;1680}1681};16821683// Prepend the footnote mark1684if (refContentsEl.childNodes.length > 0) {1685const firstChild = refContentsEl.childNodes[0];1686// Prepend the reference identified (e.g. <sup>1</sup> and a non breaking space)1687firstChild.insertBefore(1688doc.createTextNode("\u00A0"),1689firstChild.firstChild,1690);16911692firstChild.insertBefore(1693el.firstChild.cloneNode(true),1694firstChild.firstChild,1695);1696}1697const validParent = findValidParentEl(el);16981699if (refContentsEl.tagName === "LI") {1700// Ensure that there is a list to place this footnote within1701const containerEl = doc.createElement("DIV");1702containerEl.id = refContentsEl.id;1703containerEl.append(...refContentsEl.childNodes);17041705addContentToMarginContainerForEl(1706validParent || el,1707containerEl,1708doc,1709);1710} else {1711addContentToMarginContainerForEl(1712validParent || el,1713refContentsEl,1714doc,1715);1716}1717}1718}1719}1720},1721};17221723const referenceMarginProcessor: MarginNodeProcessor = {1724selector: "a[role='doc-biblioref']",1725canProcess(el: Element) {1726return el.hasAttribute("role") &&1727el.getAttribute("role") === "doc-biblioref";1728},1729process(el: Element, doc: Document) {1730if (el.hasAttribute("href")) {1731const target = el.getAttribute("href");1732if (target) {1733// First try to grab a the citation.1734const refId = target.slice(1);1735const refContentsEl = doc.getElementById(refId);17361737// Walks up the parent stack until a figure element is found1738const findCaptionEl = (el: Element): Element | undefined => {1739if (el.parentElement?.tagName === "FIGCAPTION") {1740return el.parentElement;1741} else if (el.parentElement) {1742return findCaptionEl(el.parentElement);1743} else {1744return undefined;1745}1746};17471748const findNonSpanParentEl = (el: Element): Element | undefined => {1749if (el.parentElement && el.parentElement.tagName !== "SPAN") {1750return el.parentElement;1751} else if (el.parentElement) {1752return findNonSpanParentEl(el.parentElement);1753} else {1754return undefined;1755}1756};17571758// The parent is a figcaption that contains the reference.1759// The parent.parent is the figure1760const figureCaptionEl = findCaptionEl(el);1761if (refContentsEl && figureCaptionEl) {1762if (figureCaptionEl.classList.contains("margin-caption")) {1763figureCaptionEl.appendChild(refContentsEl.cloneNode(true));1764} else {1765addContentToMarginContainerForEl(1766figureCaptionEl,1767refContentsEl,1768doc,1769);1770}1771} else if (refContentsEl) {1772const nonSpanParent = findNonSpanParentEl(el);1773if (nonSpanParent) {1774addContentToMarginContainerForEl(1775nonSpanParent,1776refContentsEl,1777doc,1778);1779}1780}1781}1782}1783},1784};17851786// Tests whether element is a margin container1787const isContainer = (el: Element | null) => {1788return (1789el &&1790el.tagName === "DIV" &&1791el.classList.contains("column-container") &&1792el.classList.contains("column-margin")1793);1794};17951796const isAlreadyInMargin = (el: Element): boolean => {1797const elInMargin = el.classList.contains("column-margin") ||1798(el.classList.contains("aside") &&1799!el.classList.contains("footnotes")) ||1800el.classList.contains("margin-caption");1801if (elInMargin) {1802return true;1803} else if (el.parentElement !== null) {1804return isAlreadyInMargin(el.parentElement);1805} else {1806return false;1807}1808};18091810// Creates a margin container1811const createMarginContainer = (doc: Document) => {1812const container = doc.createElement("div");1813container.classList.add("no-row-height");1814container.classList.add("column-margin");1815container.classList.add("column-container");1816return container;1817};18181819const marginContainerForEl = (el: Element, doc: Document) => {1820// The elements direct parent is in the margin1821if (el.parentElement && isAlreadyInMargin(el.parentElement)) {1822return el.parentElement;1823}18241825// If the container would be directly adjacent to another container1826// we should use that adjacent container1827if (el.nextElementSibling && isContainer(el.nextElementSibling)) {1828return el.nextElementSibling;1829}1830if (el.previousElementSibling && isContainer(el.previousElementSibling)) {1831return el.previousElementSibling;1832}18331834// Find the callout parent and create a container for the callout there1835// Walks up the parent stack until a callout element is found1836const findCalloutEl = (el: Element): Element | undefined => {1837if (el.parentElement?.classList.contains("callout")) {1838return el.parentElement;1839} else if (el.parentElement) {1840return findCalloutEl(el.parentElement);1841} else {1842return undefined;1843}1844};1845const calloutEl = findCalloutEl(el);1846if (calloutEl) {1847const container = createMarginContainer(doc);1848calloutEl.parentNode?.insertBefore(1849container,1850calloutEl.nextElementSibling,1851);1852return container;1853}18541855// Check for a list or table1856const list = findOutermostParentElOfType(el, ["OL", "UL", "TABLE"]);1857if (list) {1858if (list.nextElementSibling && isContainer(list.nextElementSibling)) {1859return list.nextElementSibling;1860} else {1861const container = createMarginContainer(doc);1862if (list.parentNode) {1863list.parentNode.insertBefore(container, list.nextElementSibling);1864}1865return container;1866}1867}18681869// Deal with a paragraph1870const parentEl = el.parentElement;1871const cantContainBlockTags = ["P", "BLOCKQUOTE"];1872if (parentEl && cantContainBlockTags.includes(parentEl.tagName)) {1873// See if this para has a parent div with a container1874if (1875parentEl.parentElement &&1876parentEl.parentElement.tagName === "DIV" &&1877parentEl.nextElementSibling &&1878isContainer(parentEl.nextElementSibling)1879) {1880return parentEl.nextElementSibling;1881} else {1882const container = createMarginContainer(doc);1883const wrapper = doc.createElement("div");1884parentEl.replaceWith(wrapper);1885wrapper.appendChild(parentEl);1886wrapper.appendChild(container);1887return container;1888}1889}18901891// We couldn't find a container, so just cook one up and return1892const container = createMarginContainer(doc);1893el.parentNode?.insertBefore(container, el.nextElementSibling);1894return container;1895};18961897const addContentToMarginContainerForEl = (1898el: Element,1899content: Element,1900doc: Document,1901) => {1902const container = marginContainerForEl(el, doc);1903if (container) {1904container.appendChild(content);1905}1906};19071908const addNodesToMarginContainerForEl = (1909el: Element,1910nodes: NodeList,1911doc: Document,1912) => {1913const container = marginContainerForEl(el, doc);1914if (container) {1915container.append(...nodes);1916}1917};19181919const findOutermostParentElOfType = (1920el: Element,1921tagNames: string[],1922): Element | undefined => {1923let outEl = undefined;1924if (el.parentElement) {1925if (el.parentElement.tagName === "MAIN") {1926return outEl;1927} else {1928if (tagNames.includes(el.parentElement.tagName)) {1929outEl = el.parentElement;1930}1931outEl = findOutermostParentElOfType(el.parentElement, tagNames) || outEl;1932return outEl;1933}1934} else {1935return undefined;1936}1937};19381939const hasContents = (id: string, doc: Document) => {1940const el = doc.getElementById(id);1941// Does the element exist1942if (el === null) {1943return false;1944}19451946// Does it have any element children?1947if (el.children.length > 0) {1948return true;1949}19501951// If it doesn't have any element children1952// see if there is any text1953return !!el.innerText.trim();1954};19551956// Suggests a default column by inspecting sidebars1957// if there are none or some, take up the extra space!1958function suggestColumn(doc: Document) {1959const leftSidebar = hasContents(kSidebarId, doc);1960const leftToc = hasContents(kTocLeftSidebarId, doc);1961const rightSidebar = hasContents(kMarginSidebarId, doc);19621963const columnClasses = getColumnClasses(doc);1964const leftContent = [...fullOccludeClz, ...leftOccludeClz].some((clz) => {1965return columnClasses.has(clz);1966});1967const rightContent = [...fullOccludeClz, ...rightOccludeClz].some((clz) => {1968return columnClasses.has(clz);1969});19701971const leftUsed = leftSidebar || leftContent || leftToc;1972const rightUsed = rightSidebar || rightContent;19731974if (leftUsed && rightUsed) {1975return "column-body";1976} else if (leftUsed) {1977return "column-page-right";1978} else if (rightUsed) {1979return "column-page-left";1980} else {1981return "column-page";1982}1983}1984const kSidebarId = "quarto-sidebar";1985const kMarginSidebarId = "quarto-margin-sidebar";1986const kTocLeftSidebarId = "quarto-sidebar-toc-left";19871988const fullOccludeClz = [1989"column-page",1990"column-screen",1991"column-screen-inset",1992];1993const leftOccludeClz = [1994"column-page-left",1995"column-screen-inset-left",1996"column-screen-left",1997];1998const rightOccludeClz = [1999"column-margin",2000"column-page-right",2001"column-screen-inset-right",2002"column-screen-right",2003"margin-caption",2004"margin-ref",2005];20062007const allColumnClz = [2008"column-body-outset",2009"column-body-outset-left",2010"column-body-outset-right",2011"column-page-inset",2012"column-page-inset-left",2013"column-page-inset-right",2014"column-page",2015"column-page-left",2016"column-page-right",2017"column-screen-inset",2018"column-screen-inset-left",2019"column-screen-inset-right",2020"column-screen",2021"column-screen-left",2022"column-screen-right",2023"column-margin",2024];20252026const removeMarginClz = [2027"column-body-outset",2028"column-body-outset-right",2029"column-page-inset",2030"column-page-inset-right",2031"column-page",2032"column-page-right",2033"column-screen-inset",2034"column-screen-inset-right",2035"column-screen",2036"column-screen-right",2037"column-margin",2038];20392040const nonScreenColumnClz = [2041"column-body-outset",2042"column-body-outset-left",2043"column-body-outset-right",2044"column-page-inset",2045"column-page-inset-left",2046"column-page-inset-right",2047"column-page",2048"column-page-left",2049"column-page-right",2050"column-screen-inset",2051"column-screen-inset-left",2052"column-screen-inset-right",2053"column-screen-left",2054"column-screen-right",2055"column-margin",2056];20572058const getColumnClasses = (doc: Document) => {2059const classes = new Set<string>();2060const colNodes = getColumnLayoutElements(doc);2061for (const colNode of colNodes) {2062const colEl = colNode as Element;2063colEl.classList.forEach((clz) => {2064if (2065clz === "margin-caption" || clz === "margin-ref" ||2066clz.startsWith("column-")2067) {2068classes.add(clz);2069}2070});2071}2072return classes;2073};207420752076