Path: blob/main/src/format/html/format-html-title.ts
6450 views
/*1* format-html-title.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { existsSync } from "../../deno_ral/fs.ts";7import { dirname, isAbsolute, join } from "../../deno_ral/path.ts";8import { kDateFormat, kTocLocation } from "../../config/constants.ts";9import { Format, Metadata, PandocFlags } from "../../config/types.ts";10import { Document, Element } from "../../core/deno-dom.ts";11import { formatResourcePath } from "../../core/resources.ts";12import { sassLayer } from "../../core/sass.ts";13import { TempContext } from "../../core/temp-types.ts";14import { MarkdownPipeline } from "../../core/markdown-pipeline.ts";15import {16HtmlPostProcessResult,17PandocInputTraits,18RenderedFormat,19} from "../../command/render/types.ts";20import { InternalError } from "../../core/lib/error.ts";2122export const kTitleBlockStyle = "title-block-style";23const kTitleBlockBanner = "title-block-banner";24const ktitleBlockColor = "title-block-banner-color";25const kTitleBlockCategories = "title-block-categories";2627export interface DocumentTitleContext {28pipeline: MarkdownPipeline;29}3031export function documentTitleScssLayer(format: Format) {32if (33format.metadata[kTitleBlockStyle] === false ||34format.metadata[kTitleBlockStyle] === "none" ||35format.metadata[kTitleBlockStyle] === "plain"36) {37return undefined;38} else if (format.metadata[kTitleBlockStyle] === "manuscript") {39// TODO: Tweak style for manuscript40// This code path is here so that we can add manuscript-specific styles41// For now it is just identical to non-manuscript42const titleBlockScss = formatResourcePath(43"html",44join("templates", "title-block.scss"),45);46return sassLayer(titleBlockScss);47} else {48const titleBlockScss = formatResourcePath(49"html",50join("templates", "title-block.scss"),51);52return sassLayer(titleBlockScss);53}54}5556export function documentTitleMetadata(57format: Format,58) {59if (60format.metadata[kTitleBlockStyle] !== false &&61format.metadata[kTitleBlockStyle] !== "none" &&62format.metadata[kDateFormat] === undefined63) {64return {65[kDateFormat]: "long",66};67} else {68return undefined;69}70}7172export function documentTitleIncludeInHeader(73input: string,74format: Format,75temp: TempContext,76) {77// Inject variables78const headingVars: string[] = [];79const containerVars: string[] = [];8081const banner = format.metadata[kTitleBlockBanner] as string | boolean;82if (banner) {83// $title-banner-bg84// $title-banner-color85// $title-banner-image86const titleBlockColor = titleColor(format.metadata[ktitleBlockColor]);87if (titleBlockColor) {88const color = `color: ${titleBlockColor};`;89headingVars.push(color);90containerVars.push(color);91}9293if (banner === true) {94// The default appearance, use navbar color95} else if (isBannerImage(input, banner)) {96// An image background97containerVars.push(`background-image: url(${banner});`);98containerVars.push(`background-size: cover;`);99} else {100containerVars.push(`background: ${banner};`);101}102}103104if (headingVars.length || containerVars.length) {105const styles: string[] = ["<style>"];106if (headingVars.length) {107styles.push(`108.quarto-title-block .quarto-title-banner h1,109.quarto-title-block .quarto-title-banner h2,110.quarto-title-block .quarto-title-banner h3,111.quarto-title-block .quarto-title-banner h4,112.quarto-title-block .quarto-title-banner h5,113.quarto-title-block .quarto-title-banner h6114{115${headingVars.join("\n")}116}`);117}118if (containerVars.length) {119styles.push(`120.quarto-title-block .quarto-title-banner {121${containerVars.join("\n")}122}`);123}124125styles.push("</style>");126const file = temp.createFile({ suffix: ".css" });127Deno.writeTextFileSync(file, styles.join("\n"));128return file;129}130}131132export function documentTitlePartial(133format: Format,134) {135if (136format.metadata[kTitleBlockStyle] === false ||137format.metadata[kTitleBlockStyle] === "none"138) {139return {140partials: [],141templateParams: {},142};143} else {144const partials = [];145const templateParams: Metadata = {};146147// Note whether we should be showing categories148templateParams[kTitleBlockCategories] =149format.metadata[kTitleBlockCategories] !== false ? "true" : "";150151// Select the appropriate title block partial (banner vs no banner)152const banner = format.metadata[kTitleBlockBanner] as string | boolean;153const manuscriptTitle = format.metadata[kTitleBlockStyle] === "manuscript";154155partials.push("_title-meta-author.html");156partials.push("title-metadata.html");157158if (manuscriptTitle) {159partials.push("manuscript/title-block.html");160partials.push("manuscript/title-metadata.html");161} else if (banner) {162partials.push("banner/title-block.html");163} else {164partials.push("title-block.html");165}166167// For banner partials, configure the options and pass them along in the metadata168if (banner || manuscriptTitle) {169// When the toc is on the left, be sure to add the special grid notation170const tocLeft = format.metadata[kTocLocation] === "left";171if (tocLeft) {172templateParams["banner-header-class"] = "toc-left";173}174}175176return {177partials: partials.map((partial) => {178return formatResourcePath("html", join("templates", partial));179}),180templateParams,181};182}183}184185export async function canonicalizeTitlePostprocessor(186doc: Document,187_options: {188inputMetadata: Metadata;189inputTraits: PandocInputTraits;190renderedFormats: RenderedFormat[];191quiet?: boolean;192},193): Promise<HtmlPostProcessResult> {194// https://github.com/quarto-dev/quarto-cli/issues/10567195// this fix cannot happen in `processDocumentTitle` because196// that's too late in the postprocessing order197const titleBlock = doc.querySelector("header.quarto-title-block");198199const main = doc.querySelector("main");200// if no main element exists, this is likely a revealjs presentation201// which will generally have a title slide instead of a title block202// so we don't need to do anything203204if (!titleBlock && main) {205const header = doc.createElement("header");206header.id = "title-block-header";207header.classList.add("quarto-title-block");208main.insertBefore(header, main.firstChild);209const h1s = Array.from(doc.querySelectorAll("h1"));210for (const h1n of h1s) {211const h1 = h1n as Element;212if (h1.classList.contains("quarto-secondary-nav-title")) {213continue;214}215216// Now we need to check whether this is a plausible title element.217if (h1.parentElement?.tagName === "SECTION") {218// If the parent element is a section, then we need to check if there's219// any content before the section. If there is, then this is not a title220if (221h1.parentElement?.parentElement?.firstElementChild !==222h1.parentElement223) {224continue;225}226} else {227// If the parent element is not a section, then we need to check if there's228// any content before the h1. If there is, then this is not a title229if (h1.parentElement?.firstElementChild !== h1) {230continue;231}232}233234const div = doc.createElement("div");235div.classList.add("quarto-title-banner");236h1.classList.add("title");237header.appendChild(h1);238break;239}240}241242return {243resources: [],244supporting: [],245};246}247248export function processDocumentTitle(249input: string,250format: Format,251_flags: PandocFlags,252doc: Document,253) {254const resources: string[] = [];255256// when in banner mode, note on the main content region and257// add any image to resources258const banner = format.metadata[kTitleBlockBanner] as string | boolean;259const manuscriptTitle = format.metadata[kTitleBlockStyle] === "manuscript";260if (banner || manuscriptTitle) {261// Move the header above the content262const headerEl = doc.getElementById("title-block-header");263const contentEl = doc.getElementById("quarto-content");264if (contentEl && headerEl) {265headerEl.remove();266contentEl.parentElement?.insertBefore(headerEl, contentEl);267}268269const mainEl = doc.querySelector("main.content");270mainEl?.classList.add("quarto-banner-title-block");271272if (isBannerImage(input, banner)) {273resources.push(banner as string);274}275276// Decorate the header277const quartoHeaderEl = doc.getElementById("quarto-header");278if (quartoHeaderEl) {279quartoHeaderEl.classList.add("quarto-banner");280}281}282283return resources;284}285286function isBannerImage(input: string, banner: unknown) {287if (typeof banner === "string") {288let path;289290if (isAbsolute(banner)) {291path = banner;292} else {293path = join(dirname(input), banner);294}295return existsSync(path);296} else {297return false;298}299}300301const titleColor = (block: unknown) => {302if (block === "body" || block === "body-bg") {303return undefined;304} else {305return block;306}307};308309const _titleColorClass = (block: unknown) => {310if (block === "body") {311return "body";312} else if (block === "body-bg" || block === undefined) {313return "body-bg";314} else {315return "none";316}317};318319320