Path: blob/main/src/format/html/format-html-appendix.ts
6450 views
/*1* format-html-appendix.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { PandocInputTraits } from "../../command/render/types.ts";7import {8kAppendixAttributionBibTex,9kAppendixAttributionCiteAs,10kAppendixViewLicense,11kLang,12kPositionedRefs,13kSectionTitleCitation,14kSectionTitleCopyright,15kSectionTitleReuse,16} from "../../config/constants.ts";17import { Format, PandocFlags } from "../../config/types.ts";18import { renderBibTex, renderHtml } from "../../core/bibliography.ts";19import { Document, Element } from "../../core/deno-dom.ts";20import {21documentCSL,22getCSLPath,23} from "../../quarto-core/attribution/document.ts";24import {25createCodeBlock,26createCodeCopyButton,27hasMarginCites,28hasMarginRefs,29insertFootnotesTitle,30insertReferencesTitle,31insertTitle,32kAppendixCiteAs,33kAppendixStyle,34kCitation,35kCopyright,36kLicense,37} from "./format-html-shared.ts";3839const kAppendixCreativeCommonsLic = [40"CC BY",41"CC BY-SA",42"CC BY-ND",43"CC BY-NC",44"CC BY-NC-SA",45"CC BY-NC-ND",46];4748const kAppendixCCZero = "CC0";4950const kStylePlain = "plain";51const kStyleDefault = "default";5253const kAppendixHeadingClass = "quarto-appendix-heading";54const kAppendixContentsClass = "quarto-appendix-contents";55const kQuartoSecondaryLabelClass = "quarto-appendix-secondary-label";56const kQuartoCiteAsClass = "quarto-appendix-citeas";57const kQuartoCiteBibtexClass = "quarto-appendix-bibtex";58const kAppendixId = "quarto-appendix";5960export async function processDocumentAppendix(61input: string,62inputTraits: PandocInputTraits,63format: Format,64flags: PandocFlags,65doc: Document,66offset?: string,67) {68// Don't do anything at all if the appendix-style is false or 'none'69if (70format.metadata.book || // It never makes sense to process the appendix when we're in a book71format.metadata[kAppendixStyle] === false ||72format.metadata[kAppendixStyle] === "none"73) {74return;75}76const appendixStyle = parseStyle(77format.metadata[kAppendixStyle] as string,78);7980// The main content region81let mainEl = doc.querySelector("main.content");82if (mainEl === null) {83// The content region84mainEl = doc.querySelector("#quarto-content");85}86if (mainEl === null) {87mainEl = doc.querySelector(".page-layout-custom");88}8990if (mainEl) {91const appendixEl = doc.createElement("DIV");92appendixEl.setAttribute("id", kAppendixId);93if (appendixStyle !== kStylePlain) {94appendixEl.classList.add(appendixStyle);95}9697const headingClasses = ["anchored", kAppendixHeadingClass];9899// Gather the sections that should be included100// in the Appendix101const appendixSections: Element[] = [];102const addSection = (fn: (sectionEl: Element) => void, title?: string) => {103const containerEl = doc.createElement("SECTION");104containerEl.classList.add(105kAppendixContentsClass,106);107fn(containerEl);108109if (title) {110insertTitle(111doc,112containerEl,113title,1142,115headingClasses,116);117}118119appendixSections.push(containerEl);120};121122// Move the refs into the appendix123if (!hasMarginCites(format) && !inputTraits[kPositionedRefs]) {124const refsEl = doc.getElementById("refs");125if (refsEl) {126const findRefTitle = (refsEl: Element) => {127const parentEl = refsEl.parentElement;128if (129parentEl && parentEl.tagName === "SECTION" &&130parentEl.childElementCount === 2 // The section has only the heading + the refs div131) {132const headingEl = parentEl.querySelector("h1, h2, h3, h4, h5, h6");133if (headingEl) {134headingEl.remove();135return headingEl.innerText;136}137}138};139const existingTitle = findRefTitle(refsEl);140addSection((sectionEl) => {141sectionEl.setAttribute("role", "doc-bibliography");142sectionEl.id = "quarto-bibliography";143sectionEl.appendChild(refsEl);144145if (existingTitle) {146insertTitle(doc, sectionEl, existingTitle, 2, headingClasses);147} else {148insertReferencesTitle(149doc,150sectionEl,151format.language,1522,153headingClasses,154);155}156});157}158}159160// Move the footnotes into the appendix161if (!hasMarginRefs(format, flags)) {162const footnoteEls = doc.querySelectorAll('section[role="doc-endnotes"]');163if (footnoteEls && footnoteEls.length === 1) {164const footnotesEl = footnoteEls.item(0) as Element;165footnotesEl.tagName = "SECTION";166insertFootnotesTitle(167doc,168footnotesEl,169format.language,1702,171headingClasses,172);173appendixSections.push(footnotesEl);174}175}176177// Place Re-use, if appropriate178if (format.metadata[kLicense]) {179addSection((sectionEl) => {180const contentsDiv = doc.createElement("DIV");181sectionEl.id = "quarto-reuse";182contentsDiv.classList.add(183kAppendixContentsClass,184);185186// Note: We should ultimately replace this with a template187// based approach that emits the appendix using a partial188//189// this will allow us to not include the following code.190const normalizedLicense = (license: unknown) => {191if (typeof license === "string") {192const creativeCommons = creativeCommonsLicense(license);193if (creativeCommons) {194const licenseUrlInfo = creativeCommonsUrl(195creativeCommons.base,196format.metadata[kLang] as string | undefined,197creativeCommons.version,198);199return licenseUrlInfo;200} else {201return { text: license };202}203} else {204const licenseObj = license as Record<string, unknown>;205return {206text: licenseObj.text as string,207url: licenseObj.url,208type: licenseObj.type,209inlineLink: false,210};211}212};213const normalizedLicenses = (licenses: unknown) => {214if (Array.isArray(licenses)) {215return licenses.map((license) => {216return normalizedLicense(license);217});218} else {219return [normalizedLicense(licenses)];220}221};222223const license = format.metadata[kLicense];224const normalized = normalizedLicenses(license);225for (const normalLicense of normalized) {226const licenseEl = doc.createElement("DIV");227228if (normalLicense.url && normalLicense.inlineLink) {229const linkEl = doc.createElement("A");230linkEl.innerText = normalLicense.text;231linkEl.setAttribute("rel", "license");232linkEl.setAttribute("href", normalLicense.url);233licenseEl.appendChild(linkEl);234} else {235licenseEl.innerText = normalLicense.text;236if (normalLicense.url) {237const linkEl = doc.createElement("A");238linkEl.innerText = `(${239format.language[kAppendixViewLicense] || "View License"240})`;241linkEl.setAttribute("rel", "license");242linkEl.setAttribute("href", normalLicense.url);243licenseEl.appendChild(linkEl);244}245}246247contentsDiv.appendChild(licenseEl);248}249250sectionEl.appendChild(contentsDiv);251}, format.language[kSectionTitleReuse] || "Reuse");252}253254if (format.metadata[kCopyright]) {255// Note: We should ultimately replace this with a template256// based approach that emits the appendix using a partial257//258// this will allow us to not include the following code.259const normalizedCopyright = (copyright: unknown) => {260if (typeof copyright === "string") {261return copyright;262} else if (copyright) {263return (copyright as { statement?: string }).statement;264}265};266const copyrightRaw = format.metadata[kCopyright];267const copyright = normalizedCopyright(copyrightRaw);268if (copyright) {269addSection((sectionEl) => {270const contentsDiv = doc.createElement("DIV");271sectionEl.id = "quarto-copyright";272contentsDiv.classList.add(273kAppendixContentsClass,274);275276const licenseEl = doc.createElement("DIV");277licenseEl.innerText = copyright;278contentsDiv.appendChild(licenseEl);279280sectionEl.appendChild(contentsDiv);281}, format.language[kSectionTitleCopyright] || "Copyright");282}283}284285// Place the citation for this document itself, if appropriate286if (format.metadata[kCitation]) {287// Render the citation data for this document288const cite = await generateCite(input, format, offset);289if (cite?.bibtex || cite?.html) {290addSection((sectionEl) => {291const contentsDiv = doc.createElement("DIV");292sectionEl.appendChild(contentsDiv);293sectionEl.id = "quarto-citation";294295if (cite?.bibtex) {296// Add the bibtext representation to the appendix297const bibTexLabelEl = doc.createElement("DIV");298bibTexLabelEl.classList.add(kQuartoSecondaryLabelClass);299bibTexLabelEl.innerText =300format.language[kAppendixAttributionBibTex] ||301"BibLaTeX citation";302contentsDiv.appendChild(bibTexLabelEl);303304const bibTexDiv = createCodeBlock(doc, cite.bibtex, "bibtex");305bibTexDiv.classList.add(kQuartoCiteBibtexClass);306contentsDiv.appendChild(bibTexDiv);307308const copyButton = createCodeCopyButton(doc, format);309bibTexDiv.appendChild(copyButton);310}311312if (cite?.html) {313// Add the cite as to the appendix314const citeLabelEl = doc.createElement("DIV");315citeLabelEl.classList.add(kQuartoSecondaryLabelClass);316citeLabelEl.innerText =317format.language[kAppendixAttributionCiteAs] ||318"For attribution, please cite this work as:";319contentsDiv.appendChild(citeLabelEl);320const entry = extractCiteEl(cite.html, doc);321if (entry) {322entry.classList.add(kQuartoCiteAsClass);323contentsDiv.appendChild(entry);324}325}326}, format.language[kSectionTitleCitation] || "Citation");327}328}329330// Move any sections that are marked as appendices331// We do this last so that the other elements will have already been332// moved from the document and won't inadvertently be captured333// (for example if the last section is an appendix it could capture334// the references335const appendixSectionNodes = doc.querySelectorAll("section.appendix");336const appendixSectionEls: Element[] = [];337for (const appendixSectionNode of appendixSectionNodes) {338const appendSectionEl = appendixSectionNode as Element;339340// Add the whole thing341if (appendSectionEl) {342// Remove from the TOC since it appears in the appendix343if (appendSectionEl.id) {344const selector = `#TOC a[href="#${appendSectionEl.id}"]`;345const tocEl = doc.querySelector(selector);346if (tocEl && tocEl.parentElement) {347tocEl.parentElement.remove();348}349}350351// Extract the header352const extractHeaderEl = () => {353const headerEl = appendSectionEl.querySelector(354"h1, h2, h3, h4, h5, h6",355);356// Always hoist any heading up to h2357if (headerEl) {358headerEl.remove();359const h2 = doc.createElement("h2");360h2.innerHTML = headerEl.innerHTML;361if (appendSectionEl.id) {362h2.classList.add("anchored");363}364return h2;365} else {366const h2 = doc.createElement("h2");367return h2;368}369};370const headerEl = extractHeaderEl();371headerEl.classList.add(kAppendixHeadingClass);372373// Move the contents of the section into a div374const containerDivEl = doc.createElement("DIV");375containerDivEl.classList.add(376kAppendixContentsClass,377);378while (appendSectionEl.childNodes.length > 0) {379containerDivEl.appendChild(appendSectionEl.childNodes[0]);380}381382appendSectionEl.appendChild(headerEl);383appendSectionEl.appendChild(containerDivEl);384appendixSectionEls.push(appendSectionEl);385}386}387// Place the user decorated appendixes at the front of the list388// of appendixes389if (appendixSectionEls.length > 0) {390appendixSections.unshift(...appendixSectionEls);391}392393// Insert the sections394appendixSections.forEach((el) => {395appendixEl.appendChild(el);396});397398// Only add the appendix if it has at least one section399if (appendixEl.childElementCount > 0) {400mainEl.appendChild(appendixEl);401}402}403}404405const kCiteAsStyleBibtex = "bibtex";406const kCiteAsStyleDisplay = "display";407408function citeStyleTester(format: Format) {409const citeStyle = format.metadata[kAppendixCiteAs];410const resolvedStyles: string[] = [];411if (citeStyle === undefined || citeStyle === true) {412resolvedStyles.push(...[kCiteAsStyleDisplay, kCiteAsStyleBibtex]);413} else {414if (Array.isArray(citeStyle)) {415resolvedStyles.push(...citeStyle);416} else {417resolvedStyles.push(citeStyle as string);418}419}420return {421hasCiteAs: () => {422return resolvedStyles.length > 0;423},424hasCiteAsStyle: (style: string) => {425return resolvedStyles.includes(style);426},427};428}429430function parseStyle(style?: string) {431switch (style) {432case "plain":433return "plain";434default:435return kStyleDefault;436}437}438439const kCcPattern = /(CC BY[^\s]*)\s*(\S*)/;440function creativeCommonsLicense(441license?: string,442) {443if (license) {444const match = license.toUpperCase().match(kCcPattern);445if (match) {446const base = match[1];447const version = match[2];448if (kAppendixCreativeCommonsLic.includes(base)) {449return {450base: base as451| "CC BY"452| "CC BY-SA"453| "CC BY-ND"454| "CC BY-NC"455| "CC BY-NC-ND"456| "CC BY-NC-SA",457version: version || "4.0",458};459}460} else if (license === kAppendixCCZero) {461// special case for this creative commons license462return {463base: kAppendixCCZero,464version: "1.0",465};466} else {467return undefined;468}469} else {470return undefined;471}472}473474function creativeCommonsUrl(license: string, lang?: string, version?: string) {475// Special case for CC0 as different URL476if (license === kAppendixCCZero) {477return {478url: `https://creativecommons.org/publicdomain/zero/${version}/`,479text: `CC0 ${version}`,480inlineLink: true,481};482}483const licenseType = license.substring(3);484if (lang && lang !== "en") {485return {486url:487`https://creativecommons.org/licenses/${licenseType.toLowerCase()}/${version}/deed.${488lang.toLowerCase().replace("-", "_")489}`,490text: `CC ${licenseType} ${version}`,491inlineLink: true,492};493} else {494return {495url:496`https://creativecommons.org/licenses/${licenseType.toLowerCase()}/${version}/`,497text: `CC ${licenseType} ${version}`,498inlineLink: true,499};500}501}502503async function generateCite(input: string, format: Format, offset?: string) {504const citeStyle = citeStyleTester(format);505if (citeStyle.hasCiteAs()) {506const { csl } = documentCSL(507input,508format.metadata,509"webpage",510format.pandoc["output-file"],511offset,512);513if (csl) {514// Render the HTML and BibTeX form of this document515const cslPath = getCSLPath(input, format);516return {517html: citeStyle.hasCiteAsStyle(kCiteAsStyleDisplay)518? await renderHtml(csl, cslPath)519: undefined,520bibtex: citeStyle.hasCiteAsStyle(kCiteAsStyleBibtex)521? await renderBibTex(csl)522: undefined,523};524} else {525return undefined;526}527} else {528return {};529}530}531532// The removes any addition left margin markup that is added533// to the rendered citation (e.g. a number or so on)534function extractCiteEl(html: string, doc: Document) {535const htmlDiv = doc.createElement("DIV");536htmlDiv.innerHTML = html;537const entry = htmlDiv.querySelector(".csl-entry");538if (entry) {539const leftMarginEl = entry.querySelector(".csl-left-margin");540if (leftMarginEl) {541leftMarginEl.remove();542const rightEl = entry.querySelector(".csl-right-inline");543if (rightEl) {544rightEl.classList.remove("csl-right-inline");545}546}547return entry;548} else {549return undefined;550}551}552553554