Skip to content

Commit 40cdbfa

Browse files
committed
Generate asciidoc bibliography for books
1 parent 7e77dc0 commit 40cdbfa

File tree

4 files changed

+253
-88
lines changed

4 files changed

+253
-88
lines changed

src/format/asciidoc/format-asciidoc.ts

Lines changed: 154 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,26 @@ import {
1919
bookConfig,
2020
isBookIndexPage,
2121
} from "../../project/types/book/book-shared.ts";
22-
import { join } from "path/mod.ts";
22+
import { join, relative } from "path/mod.ts";
2323

2424
import { plaintextFormat } from "../formats-shared.ts";
2525
import { dirAndStem } from "../../core/path.ts";
2626
import { formatResourcePath } from "../../core/resources.ts";
2727
import { ProjectContext } from "../../project/types.ts";
28-
import { kShiftHeadingLevelBy } from "../../config/constants.ts";
28+
import {
29+
kSectionTitleReferences,
30+
kShiftHeadingLevelBy,
31+
} from "../../config/constants.ts";
32+
import { existsSync } from "fs/mod.ts";
33+
import { ProjectOutputFile } from "../../project/types/types.ts";
34+
import { lines } from "../../core/text.ts";
35+
import {
36+
bookBibliography,
37+
generateBibliography,
38+
} from "../../project/types/book/book-bibliography.ts";
39+
import { citeIndex } from "../../project/project-cites.ts";
40+
import { projectOutputDir } from "../../project/project-shared.ts";
41+
import { PandocOptions } from "../../command/render/types.ts";
2942

3043
type AsciiDocBookPart = string | {
3144
partPath?: string;
@@ -50,6 +63,8 @@ const kFormatOutputDir = "asciidoc";
5063
// Ref target marks the refs div so the post process can inject the bibliography
5164
const kRefTargetIdentifier = "refs-target-identifier";
5265
const kRefTargetIndentifierValue = "// quarto-refs-target-378736AB";
66+
const kRefTargetIndentifierMatch = /\/\/ quarto-refs-target-378736AB/g;
67+
5368
const kUseAsciidocNativeCites = "use-asciidoc-native-cites";
5469

5570
// This provide book specific behavior for producing asciidoc books
@@ -93,37 +108,116 @@ const asciidocBookExtension = {
93108
return { format };
94109
}
95110
},
96-
async bookPostProcess(_format: Format, _project: ProjectContext) {
111+
async bookPostRender(
112+
format: Format,
113+
context: ProjectContext,
114+
_incremental: boolean,
115+
outputFiles: ProjectOutputFile[],
116+
) {
117+
// Find the explicit ref target
118+
let refsTarget;
119+
let indexPage;
120+
const projDir = projectOutputDir(context);
121+
const outDir = join(projDir, kFormatOutputDir);
122+
for (const outputFile of outputFiles) {
123+
const path = outputFile.file;
124+
if (existsSync(path)) {
125+
const contents = Deno.readTextFileSync(path);
126+
if (contents.match(kRefTargetIndentifierMatch)) {
127+
refsTarget = path;
128+
}
129+
}
130+
const relativePath = relative(outDir, outputFile.file);
131+
if (isBookIndexPage(relativePath)) {
132+
indexPage = outputFile.file;
133+
}
134+
}
135+
136+
// If there is a refs target, then generate the bibliography and
137+
// replace the refs target with the rendered references
138+
//
139+
// If not, just append the bibliography to the index page itself
140+
if (refsTarget || indexPage) {
141+
// Read the cites
142+
const cites: Set<string> = new Set();
143+
144+
const citeIndexObj = citeIndex(context.dir);
145+
for (const key of Object.keys(citeIndexObj)) {
146+
const citeArr = citeIndexObj[key];
147+
citeArr.forEach((cite) => {
148+
cites.add(cite);
149+
});
150+
}
151+
152+
// Generate the bibliograp context for this document
153+
const biblio = await bookBibliography(outputFiles, context);
154+
155+
// Add explicitl added cites via nocite
156+
if (biblio.nocite) {
157+
biblio.nocite.forEach((no) => {
158+
cites.add(no);
159+
});
160+
}
161+
162+
// Generate the bibliography
163+
let bibliographyContents = "";
164+
if (biblio.bibliographyPaths && cites.size) {
165+
bibliographyContents = await generateBibliography(
166+
context,
167+
biblio.bibliographyPaths,
168+
Array.from(cites),
169+
"asciidoc",
170+
biblio.csl,
171+
);
172+
}
173+
174+
// Clean the generated bibliography
175+
// - remove the leading `refs` indicator
176+
// - make the bibliography an unordered list
177+
// see https://docs.asciidoctor.org/asciidoc/latest/sections/bibliography/
178+
const cleanedBibliography = lines(bibliographyContents).filter(
179+
(line) => {
180+
return line !== "[[refs]]";
181+
},
182+
).map((line) => {
183+
if (line.startsWith("[[ref-")) {
184+
return line.replace("[[ref-", "- [[");
185+
} else {
186+
return ` ${line}`;
187+
}
188+
}).join("\n").trim();
189+
190+
if (refsTarget) {
191+
// Replace the refs target with the bibliography (or empty to remove it)
192+
const refTargetContents = Deno.readTextFileSync(refsTarget);
193+
const updatedContents = refTargetContents.replace(
194+
kRefTargetIndentifierMatch,
195+
cleanedBibliography,
196+
);
197+
Deno.writeTextFileSync(
198+
refsTarget,
199+
updatedContents,
200+
);
201+
} else if (indexPage) {
202+
const title = format.language[kSectionTitleReferences] || "References";
203+
const titleAdoc = `== ${title}`;
204+
205+
const indexPageContents = Deno.readTextFileSync(indexPage);
206+
const updatedContents =
207+
`${indexPageContents}\n\n${titleAdoc}\n\n[[refs]]\n\n${cleanedBibliography}`;
208+
Deno.writeTextFileSync(
209+
indexPage,
210+
updatedContents,
211+
);
212+
}
213+
}
97214
},
98215
};
99216

100217
async function bookRootPageMarkdown(project: ProjectContext) {
101-
const bookContents = bookConfig(
102-
kBookChapters,
103-
project.config,
104-
) as BookChapterEntry[];
105-
106-
// Find chapter and appendices
107-
const chapters = await resolveBookInputs(
108-
bookContents,
109-
project,
110-
(input: string) => {
111-
// Exclude the index page from the chapter list (since we'll append
112-
// this to the index page contents)
113-
return !isBookIndexPage(input);
114-
},
115-
);
116-
117-
const bookApps = bookConfig(
118-
kBookAppendix,
119-
project.config,
120-
) as string[];
121-
const appendices = bookApps
122-
? await resolveBookInputs(
123-
bookApps,
124-
project,
125-
)
126-
: [];
218+
// Read the chapter and appendix inputs
219+
const chapters = await chapterInputs(project);
220+
const appendices = await appendixInputs(project);
127221

128222
// Write a book asciidoc file
129223
const fileContents = [
@@ -177,6 +271,37 @@ function appendix(path: string) {
177271
return `[appendix]\n${chapter(path)}\n`;
178272
}
179273

274+
async function chapterInputs(project: ProjectContext) {
275+
const bookContents = bookConfig(
276+
kBookChapters,
277+
project.config,
278+
) as BookChapterEntry[];
279+
280+
// Find chapter and appendices
281+
return await resolveBookInputs(
282+
bookContents,
283+
project,
284+
(input: string) => {
285+
// Exclude the index page from the chapter list (since we'll append
286+
// this to the index page contents)
287+
return !isBookIndexPage(input);
288+
},
289+
);
290+
}
291+
292+
async function appendixInputs(project: ProjectContext) {
293+
const bookApps = bookConfig(
294+
kBookAppendix,
295+
project.config,
296+
) as string[];
297+
return bookApps
298+
? await resolveBookInputs(
299+
bookApps,
300+
project,
301+
)
302+
: [];
303+
}
304+
180305
async function resolveBookInputs(
181306
inputs: BookChapterEntry[],
182307
project: ProjectContext,

src/project/types/book/book-bibliography.ts

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,79 @@ import { pandocBinaryPath } from "../../../core/resources.ts";
2121
import { kBibliography, kCsl, kNoCite } from "../../../config/constants.ts";
2222
import { Metadata } from "../../../config/types.ts";
2323

24-
import { kProjectRender, ProjectContext } from "../../types.ts";
25-
import { projectOutputDir } from "../../project-shared.ts";
24+
import { kProjectRender, kProjectType, ProjectContext } from "../../types.ts";
25+
import {
26+
projectFormatOutputDir,
27+
projectOutputDir,
28+
} from "../../project-shared.ts";
2629
import {
2730
inputFileForOutputFile,
2831
inputTargetIndex,
2932
resolveInputTarget,
3033
} from "../../project-index.ts";
3134
import { WebsiteProjectOutputFile } from "../website/website.ts";
3235
import { bookMultiFileHtmlOutputs } from "./book-extension.ts";
36+
import { ProjectOutputFile } from "../types.ts";
37+
import { projectType } from "../project-types.ts";
38+
39+
export async function bookBibliography(
40+
outputFiles: ProjectOutputFile[],
41+
context: ProjectContext,
42+
) {
43+
// determine the bibliography, csl, and nocite based on the first file
44+
const file = outputFiles[0];
45+
const bibliography = file.format.metadata[kBibliography] as string[];
46+
const nocite = typeof (file.format.metadata[kNoCite]) === "string"
47+
? file.format.metadata[kNoCite] as string
48+
: undefined;
49+
const projType = projectType(context.config?.project?.[kProjectType]);
50+
51+
if (!bibliography) {
52+
return {
53+
bibliographyPaths: [],
54+
};
55+
}
56+
57+
// We need to be sure we're properly resolving the bibliography
58+
// path from the metadata using the path of the file that provided the
59+
// metadata (the same goes for the CSL file, if present)
60+
// The relative path to the output file
61+
const fileRelativePath = relative(
62+
projectFormatOutputDir(file.format, context, projType),
63+
file.file,
64+
);
65+
// The path to the input file
66+
const inputfile = await inputFileForOutputFile(context, fileRelativePath);
67+
const bibliographyPaths: string[] = [];
68+
let csl = file.format.metadata[kCsl] as string;
69+
if (inputfile) {
70+
// Use the dirname from the input file to resolve the bibliography paths
71+
const firstFileDir = dirname(inputfile);
72+
bibliographyPaths.push(
73+
...bibliography.map((file) => join(firstFileDir, file)),
74+
);
75+
76+
// Fixes https://github.com/quarto-dev/quarto-cli/issues/2755
77+
if (csl) {
78+
const cslAbsPath = pathWithForwardSlashes(join(firstFileDir, csl));
79+
if (safeExistsSync(cslAbsPath)) {
80+
csl = cslAbsPath;
81+
}
82+
}
83+
84+
return {
85+
csl,
86+
bibliographyPaths,
87+
nocite: nocite
88+
? nocite.split(",").map((x) => x.trim().replace(/^@/, ""))
89+
: [],
90+
};
91+
} else {
92+
throw new Error(
93+
"Unable to determine proper path to use when computing bibliography path.",
94+
);
95+
}
96+
}
3397

3498
export async function bookBibliographyPostRender(
3599
context: ProjectContext,
@@ -53,44 +117,10 @@ export async function bookBibliographyPostRender(
53117

54118
// bail if there is no target refs file
55119
if (refsHtml && outputFiles.length > 0) {
56-
// determine the bibliography, csl, and nocite based on the first file
57-
const file = outputFiles[0];
58-
const bibliography = file.format.metadata[kBibliography] as string[];
59-
const nocite = typeof (file.format.metadata[kNoCite]) === "string"
60-
? file.format.metadata[kNoCite] as string
61-
: undefined;
62-
if (!bibliography) {
63-
return;
64-
}
65-
66-
// We need to be sure we're properly resolving the bibliography
67-
// path from the metadata using the path of the file that provided the
68-
// metadata (the same goes for the CSL file, if present)
69-
// The relative path to the output file
70-
const fileRelativePath = relative(projectOutputDir(context), file.file);
71-
// The path to the input file
72-
const inputfile = await inputFileForOutputFile(context, fileRelativePath);
73-
const bibliographyPaths: string[] = [];
74-
let csl = file.format.metadata[kCsl] as string;
75-
if (inputfile) {
76-
// Use the dirname from the input file to resolve the bibliography paths
77-
const firstFileDir = dirname(inputfile);
78-
bibliographyPaths.push(
79-
...bibliography.map((file) => join(firstFileDir, file)),
80-
);
81-
82-
// Fixes https://github.com/quarto-dev/quarto-cli/issues/2755
83-
if (csl) {
84-
const cslAbsPath = pathWithForwardSlashes(join(firstFileDir, csl));
85-
if (safeExistsSync(cslAbsPath)) {
86-
csl = cslAbsPath;
87-
}
88-
}
89-
} else {
90-
throw new Error(
91-
"Unable to determine proper path to use when computing bibliography path.",
92-
);
93-
}
120+
const { bibliographyPaths, csl, nocite } = await bookBibliography(
121+
outputFiles,
122+
context,
123+
);
94124

95125
// find all of the refs in each document and fixup their links to point
96126
// to the shared bibliography output. note these refs so we can generate
@@ -150,7 +180,7 @@ export async function bookBibliographyPostRender(
150180
// include citeids from nocite
151181
if (nocite) {
152182
citeIds.push(
153-
...nocite.split(",").map((x) => x.trim().replace(/^@/, "")),
183+
...nocite,
154184
);
155185
}
156186

@@ -159,11 +189,12 @@ export async function bookBibliographyPostRender(
159189
// refs div in the references file
160190

161191
// genereate bibliography html
162-
const biblioHtml = await generateBibliographyHTML(
192+
const biblioHtml = await generateBibliography(
163193
context,
164194
bibliographyPaths,
165-
csl,
166195
citeIds,
196+
"html",
197+
csl,
167198
);
168199
const newRefsDiv = refsOutputFile.doc.createElement("div");
169200
newRefsDiv.innerHTML = biblioHtml;
@@ -181,11 +212,12 @@ export async function bookBibliographyPostRender(
181212
}
182213
}
183214

184-
async function generateBibliographyHTML(
215+
export async function generateBibliography(
185216
context: ProjectContext,
186217
bibliography: string[],
187-
csl: string,
188218
citeIds: string[],
219+
to: string,
220+
csl?: string,
189221
) {
190222
const biblioPaths = bibliography.map((biblio) => {
191223
if (isAbsolute(biblio)) {
@@ -212,7 +244,7 @@ async function generateBibliographyHTML(
212244
"--from",
213245
"markdown",
214246
"--to",
215-
"html",
247+
to,
216248
"--citeproc",
217249
],
218250
cwd: context.dir,

0 commit comments

Comments
 (0)