Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/asciidoc/format-asciidoc.ts
6456 views
1
/*
2
* format-asciidoc.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { Format } from "../../config/types.ts";
8
9
import { mergeConfigs } from "../../core/config.ts";
10
import { resolveInputTarget } from "../../project/project-index.ts";
11
import {
12
BookChapterEntry,
13
BookPart,
14
} from "../../project/types/book/book-types.ts";
15
import {
16
bookConfig,
17
bookOutputStem,
18
isBookIndexPage,
19
} from "../../project/types/book/book-shared.ts";
20
import {
21
kBookAppendix,
22
kBookChapters,
23
} from "../../project/types/book/book-constants.ts";
24
import { join, relative } from "../../deno_ral/path.ts";
25
26
import { plaintextFormat } from "../formats-shared.ts";
27
import { dirAndStem } from "../../core/path.ts";
28
import { formatResourcePath } from "../../core/resources.ts";
29
import { ProjectContext } from "../../project/types.ts";
30
import {
31
kOutputFile,
32
kSectionTitleReferences,
33
kShiftHeadingLevelBy,
34
} from "../../config/constants.ts";
35
import { existsSync } from "../../deno_ral/fs.ts";
36
import { ProjectOutputFile } from "../../project/types/types.ts";
37
import { lines } from "../../core/text.ts";
38
import {
39
bookBibliography,
40
generateBibliography,
41
} from "../../project/types/book/book-bibliography.ts";
42
import { citeIndex } from "../../project/project-cites.ts";
43
import { projectOutputDir } from "../../project/project-shared.ts";
44
import { PandocOptions } from "../../command/render/types.ts";
45
import { registerWriterFormatHandler } from "../format-handlers.ts";
46
47
type AsciiDocBookPart = string | {
48
partPath?: string;
49
part?: string;
50
chapters: string[];
51
};
52
53
// Provide the basic asciidoc format
54
export function asciidocFormat(): Format {
55
return mergeConfigs(
56
plaintextFormat("Asciidoc", "adoc"),
57
{
58
pandoc: {
59
// This is required because Pandoc is wrapping asciidoc images which must be on one line
60
wrap: "none",
61
template: formatResourcePath(
62
"asciidoc",
63
join(
64
"pandoc",
65
"template.asciidoc",
66
),
67
),
68
to: "asciidoc",
69
},
70
extensions: {
71
book: asciidocBookExtension,
72
},
73
},
74
);
75
}
76
77
const kFormatOutputDir = "book-asciidoc";
78
const kAsciidocDocType = "asciidoc-doctype";
79
const kAtlasConfigFile = "atlas.json";
80
81
// Ref target marks the refs div so the post process can inject the bibliography
82
const kRefTargetIdentifier = "refs-target-identifier";
83
const kRefTargetIndentifierValue = "// quarto-refs-target-378736AB";
84
const kRefTargetIndentifierMatch = /\/\/ quarto-refs-target-378736AB/g;
85
86
const kUseAsciidocNativeCites = "use-asciidoc-native-cites";
87
88
// This provide book specific behavior for producing asciidoc books
89
const asciidocBookExtension = {
90
multiFile: true,
91
formatOutputDirectory() {
92
return kFormatOutputDir;
93
},
94
filterParams: (_options: PandocOptions) => {
95
return {
96
[kUseAsciidocNativeCites]: true,
97
[kRefTargetIdentifier]: kRefTargetIndentifierValue,
98
};
99
},
100
filterFormat: (source: string, format: Format, project?: ProjectContext) => {
101
if (project) {
102
// If this is the root index page of the book, rename the output
103
const inputFile = relative(project.dir, source);
104
if (isBookIndexPage(inputFile) && !format.pandoc[kOutputFile]) {
105
const title = bookOutputStem(project.dir, project.config);
106
const adocOutputFile = title + ".adoc";
107
format.pandoc[kOutputFile] = adocOutputFile;
108
}
109
return format;
110
} else {
111
return format;
112
}
113
},
114
async onMultiFilePrePrender(
115
isIndex: boolean,
116
format: Format,
117
markdown: string,
118
project: ProjectContext,
119
) {
120
if (isIndex) {
121
// Generate additional markdown to include in the
122
// index page
123
const rootPageMd = await bookRootPageMarkdown(project);
124
const completeMd = markdown + "\n" + rootPageMd;
125
126
// Provide a doctype for the template
127
format.pandoc.variables = format.pandoc.variables || {};
128
format.pandoc.variables[kAsciidocDocType] = "book";
129
130
return { markdown: completeMd, format };
131
} else {
132
// Turn off the TOC on child pages
133
format.pandoc.toc = false;
134
format.pandoc[kShiftHeadingLevelBy] = -1;
135
return { format };
136
}
137
},
138
async bookPostRender(
139
format: Format,
140
context: ProjectContext,
141
_incremental: boolean,
142
outputFiles: ProjectOutputFile[],
143
) {
144
const projDir = projectOutputDir(context);
145
const outDir = join(projDir, kFormatOutputDir);
146
147
// Deal with atlas.json configuration files
148
// which are used by O'Reilly for configuration
149
const atlasInFile = join(context.dir, kAtlasConfigFile);
150
const atlasOutFile = join(outDir, kAtlasConfigFile);
151
if (existsSync(atlasInFile)) {
152
// See if there is an atlas.json file to move to the output
153
// directory
154
Deno.copyFileSync(atlasInFile, atlasOutFile);
155
} else {
156
// Cook up an atlas file based upon the project inputs
157
// and place this in the output dir
158
Deno.writeTextFileSync(atlasOutFile, project2Atlas(projDir, context));
159
}
160
161
// Find the explicit ref target
162
let refsTarget;
163
let indexPage;
164
for (const outputFile of outputFiles) {
165
const path = outputFile.file;
166
if (existsSync(path)) {
167
const contents = Deno.readTextFileSync(path);
168
if (contents.match(kRefTargetIndentifierMatch)) {
169
refsTarget = path;
170
}
171
}
172
const relativePath = relative(outDir, outputFile.file);
173
if (isBookIndexPage(relativePath)) {
174
indexPage = outputFile.file;
175
}
176
}
177
178
// If there is a refs target, then generate the bibliography and
179
// replace the refs target with the rendered references
180
//
181
// If not, just append the bibliography to the index page itself
182
if (refsTarget || indexPage) {
183
// Read the cites
184
const cites: Set<string> = new Set();
185
186
const citeIndexObj = citeIndex(context.dir);
187
for (const key of Object.keys(citeIndexObj)) {
188
const citeArr = citeIndexObj[key];
189
citeArr.forEach((cite) => {
190
cites.add(cite);
191
});
192
}
193
194
// Generate the bibliograp context for this document
195
const biblio = await bookBibliography(outputFiles, context);
196
197
// Add explicitl added cites via nocite
198
if (biblio.nocite) {
199
biblio.nocite.forEach((no) => {
200
cites.add(no);
201
});
202
}
203
204
// Generate the bibliography
205
let bibliographyContents = "";
206
if (biblio.bibliographyPaths && cites.size) {
207
bibliographyContents = await generateBibliography(
208
context,
209
biblio.bibliographyPaths,
210
Array.from(cites),
211
"asciidoc",
212
biblio.csl,
213
);
214
}
215
216
// Clean the generated bibliography
217
// - remove the leading `refs` indicator
218
// - make the bibliography an unordered list
219
// see https://docs.asciidoctor.org/asciidoc/latest/sections/bibliography/
220
const cleanedBibliography = lines(bibliographyContents).filter(
221
(line) => {
222
return line !== "[[refs]]";
223
},
224
).map((line) => {
225
if (line.startsWith("[[ref-")) {
226
return line.replace("[[ref-", "- [[");
227
} else {
228
return ` ${line}`;
229
}
230
}).join("\n").trim();
231
232
if (refsTarget) {
233
// Replace the refs target with the bibliography (or empty to remove it)
234
const refTargetContents = Deno.readTextFileSync(refsTarget);
235
const updatedContents = refTargetContents.replace(
236
kRefTargetIndentifierMatch,
237
cleanedBibliography,
238
);
239
Deno.writeTextFileSync(
240
refsTarget,
241
updatedContents,
242
);
243
} else if (indexPage) {
244
const title = format.language[kSectionTitleReferences] || "References";
245
const titleAdoc = `== ${title}`;
246
247
const indexPageContents = Deno.readTextFileSync(indexPage);
248
const updatedContents =
249
`${indexPageContents}\n\n${titleAdoc}\n\n[[refs]]\n\n${cleanedBibliography}`;
250
Deno.writeTextFileSync(
251
indexPage,
252
updatedContents,
253
);
254
}
255
}
256
},
257
};
258
259
function project2Atlas(projDir: string, context: ProjectContext) {
260
// Cook up an atlas.json file that will be used as a placeholder
261
const bookStem = bookOutputStem(projDir, context.config);
262
const bookTitle = bookConfig("title", context.config);
263
const atlasJson = {
264
branch: "main",
265
"files": [
266
`${bookStem}.adoc`,
267
],
268
"formats": {
269
"pdf": {
270
"version": "web",
271
"color_count": "1",
272
"index": false,
273
"toc": true,
274
"syntaxhighlighting": true,
275
"show_comments": false,
276
},
277
"epub": {
278
"index": false,
279
"toc": true,
280
"epubcheck": true,
281
"syntaxhighlighting": true,
282
"show_comments": false,
283
},
284
"mobi": {
285
"index": false,
286
"toc": true,
287
"syntaxhighlighting": true,
288
"show_comments": false,
289
},
290
"html": {
291
"index": false,
292
"toc": true,
293
"syntaxhighlighting": true,
294
"show_comments": false,
295
"consolidated": false,
296
},
297
},
298
"title": bookTitle,
299
"compat-mode": "false",
300
};
301
return JSON.stringify(atlasJson, undefined, 2);
302
}
303
304
async function bookRootPageMarkdown(project: ProjectContext) {
305
// Read the chapter and appendix inputs
306
const chapters = await chapterInputs(project);
307
const appendices = await appendixInputs(project);
308
309
// Write a book asciidoc file
310
const fileContents = [
311
"\n```{=asciidoc}\n\n",
312
levelOffset("+1"),
313
partsAndChapters(chapters, chapter),
314
partsAndChapters(appendices, appendix),
315
levelOffset("-1"),
316
"```\n",
317
];
318
319
return fileContents.join("\n");
320
}
321
322
function levelOffset(offset: string) {
323
return `:leveloffset: ${offset}\n`;
324
}
325
326
function partsAndChapters(
327
entries: AsciiDocBookPart[],
328
include: (path: string) => string,
329
) {
330
return entries.map((entry) => {
331
if (typeof entry === "string") {
332
return include(entry);
333
} else {
334
const partOutput: string[] = [];
335
336
if (entry.partPath) {
337
partOutput.push(include(entry.partPath));
338
} else {
339
partOutput.push(levelOffset("-1"));
340
partOutput.push(`= ${entry.part}`);
341
partOutput.push(levelOffset("+1"));
342
}
343
344
for (const chap of entry.chapters) {
345
partOutput.push(include(chap));
346
}
347
348
return partOutput.join("\n");
349
}
350
}).join("\n");
351
}
352
353
function chapter(path: string) {
354
return `include::${path}[]\n`;
355
}
356
357
function appendix(path: string) {
358
return `[appendix]\n${chapter(path)}\n`;
359
}
360
361
async function chapterInputs(project: ProjectContext) {
362
const bookContents = bookConfig(
363
kBookChapters,
364
project.config,
365
) as BookChapterEntry[];
366
367
// Find chapter and appendices
368
return await resolveBookInputs(
369
bookContents,
370
project,
371
(input: string) => {
372
// Exclude the index page from the chapter list (since we'll append
373
// this to the index page contents)
374
return !isBookIndexPage(input);
375
},
376
);
377
}
378
379
async function appendixInputs(project: ProjectContext) {
380
const bookApps = bookConfig(
381
kBookAppendix,
382
project.config,
383
) as string[];
384
return bookApps
385
? await resolveBookInputs(
386
bookApps,
387
project,
388
)
389
: [];
390
}
391
392
async function resolveBookInputs(
393
inputs: BookChapterEntry[],
394
project: ProjectContext,
395
filter?: (input: string) => boolean,
396
) {
397
const resolveChapter = async (input: string) => {
398
if (filter && !filter(input)) {
399
return undefined;
400
} else {
401
const target = await resolveInputTarget(
402
project,
403
input,
404
false,
405
);
406
if (target) {
407
const [dir, stem] = dirAndStem(target?.outputHref);
408
const outputFile = join(
409
dir,
410
`${stem}.adoc`,
411
);
412
413
return outputFile;
414
} else {
415
return undefined;
416
}
417
}
418
};
419
420
const outputs: AsciiDocBookPart[] = [];
421
for (const input of inputs) {
422
if (typeof input === "string") {
423
const chapterOutput = await resolveChapter(input);
424
if (chapterOutput) {
425
outputs.push(chapterOutput);
426
}
427
} else {
428
const entry = input as BookPart;
429
430
const resolvedPart = await resolveChapter(entry.part);
431
const entryOutput = {
432
partPath: resolvedPart,
433
part: resolvedPart ? undefined : entry.part,
434
chapters: [] as string[],
435
};
436
for (const chapter of entry.chapters) {
437
const resolved = await resolveChapter(chapter);
438
if (resolved) {
439
entryOutput.chapters.push(resolved);
440
}
441
}
442
outputs.push(entryOutput);
443
}
444
}
445
return outputs;
446
}
447
448
registerWriterFormatHandler((format) => {
449
switch (format) {
450
case "asciidoc":
451
case "asciidoctor":
452
return {
453
format: asciidocFormat(),
454
};
455
}
456
});
457
458