Path: blob/main/src/format/asciidoc/format-asciidoc.ts
6456 views
/*1* format-asciidoc.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { Format } from "../../config/types.ts";78import { mergeConfigs } from "../../core/config.ts";9import { resolveInputTarget } from "../../project/project-index.ts";10import {11BookChapterEntry,12BookPart,13} from "../../project/types/book/book-types.ts";14import {15bookConfig,16bookOutputStem,17isBookIndexPage,18} from "../../project/types/book/book-shared.ts";19import {20kBookAppendix,21kBookChapters,22} from "../../project/types/book/book-constants.ts";23import { join, relative } from "../../deno_ral/path.ts";2425import { plaintextFormat } from "../formats-shared.ts";26import { dirAndStem } from "../../core/path.ts";27import { formatResourcePath } from "../../core/resources.ts";28import { ProjectContext } from "../../project/types.ts";29import {30kOutputFile,31kSectionTitleReferences,32kShiftHeadingLevelBy,33} from "../../config/constants.ts";34import { existsSync } from "../../deno_ral/fs.ts";35import { ProjectOutputFile } from "../../project/types/types.ts";36import { lines } from "../../core/text.ts";37import {38bookBibliography,39generateBibliography,40} from "../../project/types/book/book-bibliography.ts";41import { citeIndex } from "../../project/project-cites.ts";42import { projectOutputDir } from "../../project/project-shared.ts";43import { PandocOptions } from "../../command/render/types.ts";44import { registerWriterFormatHandler } from "../format-handlers.ts";4546type AsciiDocBookPart = string | {47partPath?: string;48part?: string;49chapters: string[];50};5152// Provide the basic asciidoc format53export function asciidocFormat(): Format {54return mergeConfigs(55plaintextFormat("Asciidoc", "adoc"),56{57pandoc: {58// This is required because Pandoc is wrapping asciidoc images which must be on one line59wrap: "none",60template: formatResourcePath(61"asciidoc",62join(63"pandoc",64"template.asciidoc",65),66),67to: "asciidoc",68},69extensions: {70book: asciidocBookExtension,71},72},73);74}7576const kFormatOutputDir = "book-asciidoc";77const kAsciidocDocType = "asciidoc-doctype";78const kAtlasConfigFile = "atlas.json";7980// Ref target marks the refs div so the post process can inject the bibliography81const kRefTargetIdentifier = "refs-target-identifier";82const kRefTargetIndentifierValue = "// quarto-refs-target-378736AB";83const kRefTargetIndentifierMatch = /\/\/ quarto-refs-target-378736AB/g;8485const kUseAsciidocNativeCites = "use-asciidoc-native-cites";8687// This provide book specific behavior for producing asciidoc books88const asciidocBookExtension = {89multiFile: true,90formatOutputDirectory() {91return kFormatOutputDir;92},93filterParams: (_options: PandocOptions) => {94return {95[kUseAsciidocNativeCites]: true,96[kRefTargetIdentifier]: kRefTargetIndentifierValue,97};98},99filterFormat: (source: string, format: Format, project?: ProjectContext) => {100if (project) {101// If this is the root index page of the book, rename the output102const inputFile = relative(project.dir, source);103if (isBookIndexPage(inputFile) && !format.pandoc[kOutputFile]) {104const title = bookOutputStem(project.dir, project.config);105const adocOutputFile = title + ".adoc";106format.pandoc[kOutputFile] = adocOutputFile;107}108return format;109} else {110return format;111}112},113async onMultiFilePrePrender(114isIndex: boolean,115format: Format,116markdown: string,117project: ProjectContext,118) {119if (isIndex) {120// Generate additional markdown to include in the121// index page122const rootPageMd = await bookRootPageMarkdown(project);123const completeMd = markdown + "\n" + rootPageMd;124125// Provide a doctype for the template126format.pandoc.variables = format.pandoc.variables || {};127format.pandoc.variables[kAsciidocDocType] = "book";128129return { markdown: completeMd, format };130} else {131// Turn off the TOC on child pages132format.pandoc.toc = false;133format.pandoc[kShiftHeadingLevelBy] = -1;134return { format };135}136},137async bookPostRender(138format: Format,139context: ProjectContext,140_incremental: boolean,141outputFiles: ProjectOutputFile[],142) {143const projDir = projectOutputDir(context);144const outDir = join(projDir, kFormatOutputDir);145146// Deal with atlas.json configuration files147// which are used by O'Reilly for configuration148const atlasInFile = join(context.dir, kAtlasConfigFile);149const atlasOutFile = join(outDir, kAtlasConfigFile);150if (existsSync(atlasInFile)) {151// See if there is an atlas.json file to move to the output152// directory153Deno.copyFileSync(atlasInFile, atlasOutFile);154} else {155// Cook up an atlas file based upon the project inputs156// and place this in the output dir157Deno.writeTextFileSync(atlasOutFile, project2Atlas(projDir, context));158}159160// Find the explicit ref target161let refsTarget;162let indexPage;163for (const outputFile of outputFiles) {164const path = outputFile.file;165if (existsSync(path)) {166const contents = Deno.readTextFileSync(path);167if (contents.match(kRefTargetIndentifierMatch)) {168refsTarget = path;169}170}171const relativePath = relative(outDir, outputFile.file);172if (isBookIndexPage(relativePath)) {173indexPage = outputFile.file;174}175}176177// If there is a refs target, then generate the bibliography and178// replace the refs target with the rendered references179//180// If not, just append the bibliography to the index page itself181if (refsTarget || indexPage) {182// Read the cites183const cites: Set<string> = new Set();184185const citeIndexObj = citeIndex(context.dir);186for (const key of Object.keys(citeIndexObj)) {187const citeArr = citeIndexObj[key];188citeArr.forEach((cite) => {189cites.add(cite);190});191}192193// Generate the bibliograp context for this document194const biblio = await bookBibliography(outputFiles, context);195196// Add explicitl added cites via nocite197if (biblio.nocite) {198biblio.nocite.forEach((no) => {199cites.add(no);200});201}202203// Generate the bibliography204let bibliographyContents = "";205if (biblio.bibliographyPaths && cites.size) {206bibliographyContents = await generateBibliography(207context,208biblio.bibliographyPaths,209Array.from(cites),210"asciidoc",211biblio.csl,212);213}214215// Clean the generated bibliography216// - remove the leading `refs` indicator217// - make the bibliography an unordered list218// see https://docs.asciidoctor.org/asciidoc/latest/sections/bibliography/219const cleanedBibliography = lines(bibliographyContents).filter(220(line) => {221return line !== "[[refs]]";222},223).map((line) => {224if (line.startsWith("[[ref-")) {225return line.replace("[[ref-", "- [[");226} else {227return ` ${line}`;228}229}).join("\n").trim();230231if (refsTarget) {232// Replace the refs target with the bibliography (or empty to remove it)233const refTargetContents = Deno.readTextFileSync(refsTarget);234const updatedContents = refTargetContents.replace(235kRefTargetIndentifierMatch,236cleanedBibliography,237);238Deno.writeTextFileSync(239refsTarget,240updatedContents,241);242} else if (indexPage) {243const title = format.language[kSectionTitleReferences] || "References";244const titleAdoc = `== ${title}`;245246const indexPageContents = Deno.readTextFileSync(indexPage);247const updatedContents =248`${indexPageContents}\n\n${titleAdoc}\n\n[[refs]]\n\n${cleanedBibliography}`;249Deno.writeTextFileSync(250indexPage,251updatedContents,252);253}254}255},256};257258function project2Atlas(projDir: string, context: ProjectContext) {259// Cook up an atlas.json file that will be used as a placeholder260const bookStem = bookOutputStem(projDir, context.config);261const bookTitle = bookConfig("title", context.config);262const atlasJson = {263branch: "main",264"files": [265`${bookStem}.adoc`,266],267"formats": {268"pdf": {269"version": "web",270"color_count": "1",271"index": false,272"toc": true,273"syntaxhighlighting": true,274"show_comments": false,275},276"epub": {277"index": false,278"toc": true,279"epubcheck": true,280"syntaxhighlighting": true,281"show_comments": false,282},283"mobi": {284"index": false,285"toc": true,286"syntaxhighlighting": true,287"show_comments": false,288},289"html": {290"index": false,291"toc": true,292"syntaxhighlighting": true,293"show_comments": false,294"consolidated": false,295},296},297"title": bookTitle,298"compat-mode": "false",299};300return JSON.stringify(atlasJson, undefined, 2);301}302303async function bookRootPageMarkdown(project: ProjectContext) {304// Read the chapter and appendix inputs305const chapters = await chapterInputs(project);306const appendices = await appendixInputs(project);307308// Write a book asciidoc file309const fileContents = [310"\n```{=asciidoc}\n\n",311levelOffset("+1"),312partsAndChapters(chapters, chapter),313partsAndChapters(appendices, appendix),314levelOffset("-1"),315"```\n",316];317318return fileContents.join("\n");319}320321function levelOffset(offset: string) {322return `:leveloffset: ${offset}\n`;323}324325function partsAndChapters(326entries: AsciiDocBookPart[],327include: (path: string) => string,328) {329return entries.map((entry) => {330if (typeof entry === "string") {331return include(entry);332} else {333const partOutput: string[] = [];334335if (entry.partPath) {336partOutput.push(include(entry.partPath));337} else {338partOutput.push(levelOffset("-1"));339partOutput.push(`= ${entry.part}`);340partOutput.push(levelOffset("+1"));341}342343for (const chap of entry.chapters) {344partOutput.push(include(chap));345}346347return partOutput.join("\n");348}349}).join("\n");350}351352function chapter(path: string) {353return `include::${path}[]\n`;354}355356function appendix(path: string) {357return `[appendix]\n${chapter(path)}\n`;358}359360async function chapterInputs(project: ProjectContext) {361const bookContents = bookConfig(362kBookChapters,363project.config,364) as BookChapterEntry[];365366// Find chapter and appendices367return await resolveBookInputs(368bookContents,369project,370(input: string) => {371// Exclude the index page from the chapter list (since we'll append372// this to the index page contents)373return !isBookIndexPage(input);374},375);376}377378async function appendixInputs(project: ProjectContext) {379const bookApps = bookConfig(380kBookAppendix,381project.config,382) as string[];383return bookApps384? await resolveBookInputs(385bookApps,386project,387)388: [];389}390391async function resolveBookInputs(392inputs: BookChapterEntry[],393project: ProjectContext,394filter?: (input: string) => boolean,395) {396const resolveChapter = async (input: string) => {397if (filter && !filter(input)) {398return undefined;399} else {400const target = await resolveInputTarget(401project,402input,403false,404);405if (target) {406const [dir, stem] = dirAndStem(target?.outputHref);407const outputFile = join(408dir,409`${stem}.adoc`,410);411412return outputFile;413} else {414return undefined;415}416}417};418419const outputs: AsciiDocBookPart[] = [];420for (const input of inputs) {421if (typeof input === "string") {422const chapterOutput = await resolveChapter(input);423if (chapterOutput) {424outputs.push(chapterOutput);425}426} else {427const entry = input as BookPart;428429const resolvedPart = await resolveChapter(entry.part);430const entryOutput = {431partPath: resolvedPart,432part: resolvedPart ? undefined : entry.part,433chapters: [] as string[],434};435for (const chapter of entry.chapters) {436const resolved = await resolveChapter(chapter);437if (resolved) {438entryOutput.chapters.push(resolved);439}440}441outputs.push(entryOutput);442}443}444return outputs;445}446447registerWriterFormatHandler((format) => {448switch (format) {449case "asciidoc":450case "asciidoctor":451return {452format: asciidocFormat(),453};454}455});456457458