Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/html/format-html-appendix.ts
6450 views
1
/*
2
* format-html-appendix.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { PandocInputTraits } from "../../command/render/types.ts";
8
import {
9
kAppendixAttributionBibTex,
10
kAppendixAttributionCiteAs,
11
kAppendixViewLicense,
12
kLang,
13
kPositionedRefs,
14
kSectionTitleCitation,
15
kSectionTitleCopyright,
16
kSectionTitleReuse,
17
} from "../../config/constants.ts";
18
import { Format, PandocFlags } from "../../config/types.ts";
19
import { renderBibTex, renderHtml } from "../../core/bibliography.ts";
20
import { Document, Element } from "../../core/deno-dom.ts";
21
import {
22
documentCSL,
23
getCSLPath,
24
} from "../../quarto-core/attribution/document.ts";
25
import {
26
createCodeBlock,
27
createCodeCopyButton,
28
hasMarginCites,
29
hasMarginRefs,
30
insertFootnotesTitle,
31
insertReferencesTitle,
32
insertTitle,
33
kAppendixCiteAs,
34
kAppendixStyle,
35
kCitation,
36
kCopyright,
37
kLicense,
38
} from "./format-html-shared.ts";
39
40
const kAppendixCreativeCommonsLic = [
41
"CC BY",
42
"CC BY-SA",
43
"CC BY-ND",
44
"CC BY-NC",
45
"CC BY-NC-SA",
46
"CC BY-NC-ND",
47
];
48
49
const kAppendixCCZero = "CC0";
50
51
const kStylePlain = "plain";
52
const kStyleDefault = "default";
53
54
const kAppendixHeadingClass = "quarto-appendix-heading";
55
const kAppendixContentsClass = "quarto-appendix-contents";
56
const kQuartoSecondaryLabelClass = "quarto-appendix-secondary-label";
57
const kQuartoCiteAsClass = "quarto-appendix-citeas";
58
const kQuartoCiteBibtexClass = "quarto-appendix-bibtex";
59
const kAppendixId = "quarto-appendix";
60
61
export async function processDocumentAppendix(
62
input: string,
63
inputTraits: PandocInputTraits,
64
format: Format,
65
flags: PandocFlags,
66
doc: Document,
67
offset?: string,
68
) {
69
// Don't do anything at all if the appendix-style is false or 'none'
70
if (
71
format.metadata.book || // It never makes sense to process the appendix when we're in a book
72
format.metadata[kAppendixStyle] === false ||
73
format.metadata[kAppendixStyle] === "none"
74
) {
75
return;
76
}
77
const appendixStyle = parseStyle(
78
format.metadata[kAppendixStyle] as string,
79
);
80
81
// The main content region
82
let mainEl = doc.querySelector("main.content");
83
if (mainEl === null) {
84
// The content region
85
mainEl = doc.querySelector("#quarto-content");
86
}
87
if (mainEl === null) {
88
mainEl = doc.querySelector(".page-layout-custom");
89
}
90
91
if (mainEl) {
92
const appendixEl = doc.createElement("DIV");
93
appendixEl.setAttribute("id", kAppendixId);
94
if (appendixStyle !== kStylePlain) {
95
appendixEl.classList.add(appendixStyle);
96
}
97
98
const headingClasses = ["anchored", kAppendixHeadingClass];
99
100
// Gather the sections that should be included
101
// in the Appendix
102
const appendixSections: Element[] = [];
103
const addSection = (fn: (sectionEl: Element) => void, title?: string) => {
104
const containerEl = doc.createElement("SECTION");
105
containerEl.classList.add(
106
kAppendixContentsClass,
107
);
108
fn(containerEl);
109
110
if (title) {
111
insertTitle(
112
doc,
113
containerEl,
114
title,
115
2,
116
headingClasses,
117
);
118
}
119
120
appendixSections.push(containerEl);
121
};
122
123
// Move the refs into the appendix
124
if (!hasMarginCites(format) && !inputTraits[kPositionedRefs]) {
125
const refsEl = doc.getElementById("refs");
126
if (refsEl) {
127
const findRefTitle = (refsEl: Element) => {
128
const parentEl = refsEl.parentElement;
129
if (
130
parentEl && parentEl.tagName === "SECTION" &&
131
parentEl.childElementCount === 2 // The section has only the heading + the refs div
132
) {
133
const headingEl = parentEl.querySelector("h1, h2, h3, h4, h5, h6");
134
if (headingEl) {
135
headingEl.remove();
136
return headingEl.innerText;
137
}
138
}
139
};
140
const existingTitle = findRefTitle(refsEl);
141
addSection((sectionEl) => {
142
sectionEl.setAttribute("role", "doc-bibliography");
143
sectionEl.id = "quarto-bibliography";
144
sectionEl.appendChild(refsEl);
145
146
if (existingTitle) {
147
insertTitle(doc, sectionEl, existingTitle, 2, headingClasses);
148
} else {
149
insertReferencesTitle(
150
doc,
151
sectionEl,
152
format.language,
153
2,
154
headingClasses,
155
);
156
}
157
});
158
}
159
}
160
161
// Move the footnotes into the appendix
162
if (!hasMarginRefs(format, flags)) {
163
const footnoteEls = doc.querySelectorAll('section[role="doc-endnotes"]');
164
if (footnoteEls && footnoteEls.length === 1) {
165
const footnotesEl = footnoteEls.item(0) as Element;
166
footnotesEl.tagName = "SECTION";
167
insertFootnotesTitle(
168
doc,
169
footnotesEl,
170
format.language,
171
2,
172
headingClasses,
173
);
174
appendixSections.push(footnotesEl);
175
}
176
}
177
178
// Place Re-use, if appropriate
179
if (format.metadata[kLicense]) {
180
addSection((sectionEl) => {
181
const contentsDiv = doc.createElement("DIV");
182
sectionEl.id = "quarto-reuse";
183
contentsDiv.classList.add(
184
kAppendixContentsClass,
185
);
186
187
// Note: We should ultimately replace this with a template
188
// based approach that emits the appendix using a partial
189
//
190
// this will allow us to not include the following code.
191
const normalizedLicense = (license: unknown) => {
192
if (typeof license === "string") {
193
const creativeCommons = creativeCommonsLicense(license);
194
if (creativeCommons) {
195
const licenseUrlInfo = creativeCommonsUrl(
196
creativeCommons.base,
197
format.metadata[kLang] as string | undefined,
198
creativeCommons.version,
199
);
200
return licenseUrlInfo;
201
} else {
202
return { text: license };
203
}
204
} else {
205
const licenseObj = license as Record<string, unknown>;
206
return {
207
text: licenseObj.text as string,
208
url: licenseObj.url,
209
type: licenseObj.type,
210
inlineLink: false,
211
};
212
}
213
};
214
const normalizedLicenses = (licenses: unknown) => {
215
if (Array.isArray(licenses)) {
216
return licenses.map((license) => {
217
return normalizedLicense(license);
218
});
219
} else {
220
return [normalizedLicense(licenses)];
221
}
222
};
223
224
const license = format.metadata[kLicense];
225
const normalized = normalizedLicenses(license);
226
for (const normalLicense of normalized) {
227
const licenseEl = doc.createElement("DIV");
228
229
if (normalLicense.url && normalLicense.inlineLink) {
230
const linkEl = doc.createElement("A");
231
linkEl.innerText = normalLicense.text;
232
linkEl.setAttribute("rel", "license");
233
linkEl.setAttribute("href", normalLicense.url);
234
licenseEl.appendChild(linkEl);
235
} else {
236
licenseEl.innerText = normalLicense.text;
237
if (normalLicense.url) {
238
const linkEl = doc.createElement("A");
239
linkEl.innerText = `(${
240
format.language[kAppendixViewLicense] || "View License"
241
})`;
242
linkEl.setAttribute("rel", "license");
243
linkEl.setAttribute("href", normalLicense.url);
244
licenseEl.appendChild(linkEl);
245
}
246
}
247
248
contentsDiv.appendChild(licenseEl);
249
}
250
251
sectionEl.appendChild(contentsDiv);
252
}, format.language[kSectionTitleReuse] || "Reuse");
253
}
254
255
if (format.metadata[kCopyright]) {
256
// Note: We should ultimately replace this with a template
257
// based approach that emits the appendix using a partial
258
//
259
// this will allow us to not include the following code.
260
const normalizedCopyright = (copyright: unknown) => {
261
if (typeof copyright === "string") {
262
return copyright;
263
} else if (copyright) {
264
return (copyright as { statement?: string }).statement;
265
}
266
};
267
const copyrightRaw = format.metadata[kCopyright];
268
const copyright = normalizedCopyright(copyrightRaw);
269
if (copyright) {
270
addSection((sectionEl) => {
271
const contentsDiv = doc.createElement("DIV");
272
sectionEl.id = "quarto-copyright";
273
contentsDiv.classList.add(
274
kAppendixContentsClass,
275
);
276
277
const licenseEl = doc.createElement("DIV");
278
licenseEl.innerText = copyright;
279
contentsDiv.appendChild(licenseEl);
280
281
sectionEl.appendChild(contentsDiv);
282
}, format.language[kSectionTitleCopyright] || "Copyright");
283
}
284
}
285
286
// Place the citation for this document itself, if appropriate
287
if (format.metadata[kCitation]) {
288
// Render the citation data for this document
289
const cite = await generateCite(input, format, offset);
290
if (cite?.bibtex || cite?.html) {
291
addSection((sectionEl) => {
292
const contentsDiv = doc.createElement("DIV");
293
sectionEl.appendChild(contentsDiv);
294
sectionEl.id = "quarto-citation";
295
296
if (cite?.bibtex) {
297
// Add the bibtext representation to the appendix
298
const bibTexLabelEl = doc.createElement("DIV");
299
bibTexLabelEl.classList.add(kQuartoSecondaryLabelClass);
300
bibTexLabelEl.innerText =
301
format.language[kAppendixAttributionBibTex] ||
302
"BibLaTeX citation";
303
contentsDiv.appendChild(bibTexLabelEl);
304
305
const bibTexDiv = createCodeBlock(doc, cite.bibtex, "bibtex");
306
bibTexDiv.classList.add(kQuartoCiteBibtexClass);
307
contentsDiv.appendChild(bibTexDiv);
308
309
const copyButton = createCodeCopyButton(doc, format);
310
bibTexDiv.appendChild(copyButton);
311
}
312
313
if (cite?.html) {
314
// Add the cite as to the appendix
315
const citeLabelEl = doc.createElement("DIV");
316
citeLabelEl.classList.add(kQuartoSecondaryLabelClass);
317
citeLabelEl.innerText =
318
format.language[kAppendixAttributionCiteAs] ||
319
"For attribution, please cite this work as:";
320
contentsDiv.appendChild(citeLabelEl);
321
const entry = extractCiteEl(cite.html, doc);
322
if (entry) {
323
entry.classList.add(kQuartoCiteAsClass);
324
contentsDiv.appendChild(entry);
325
}
326
}
327
}, format.language[kSectionTitleCitation] || "Citation");
328
}
329
}
330
331
// Move any sections that are marked as appendices
332
// We do this last so that the other elements will have already been
333
// moved from the document and won't inadvertently be captured
334
// (for example if the last section is an appendix it could capture
335
// the references
336
const appendixSectionNodes = doc.querySelectorAll("section.appendix");
337
const appendixSectionEls: Element[] = [];
338
for (const appendixSectionNode of appendixSectionNodes) {
339
const appendSectionEl = appendixSectionNode as Element;
340
341
// Add the whole thing
342
if (appendSectionEl) {
343
// Remove from the TOC since it appears in the appendix
344
if (appendSectionEl.id) {
345
const selector = `#TOC a[href="#${appendSectionEl.id}"]`;
346
const tocEl = doc.querySelector(selector);
347
if (tocEl && tocEl.parentElement) {
348
tocEl.parentElement.remove();
349
}
350
}
351
352
// Extract the header
353
const extractHeaderEl = () => {
354
const headerEl = appendSectionEl.querySelector(
355
"h1, h2, h3, h4, h5, h6",
356
);
357
// Always hoist any heading up to h2
358
if (headerEl) {
359
headerEl.remove();
360
const h2 = doc.createElement("h2");
361
h2.innerHTML = headerEl.innerHTML;
362
if (appendSectionEl.id) {
363
h2.classList.add("anchored");
364
}
365
return h2;
366
} else {
367
const h2 = doc.createElement("h2");
368
return h2;
369
}
370
};
371
const headerEl = extractHeaderEl();
372
headerEl.classList.add(kAppendixHeadingClass);
373
374
// Move the contents of the section into a div
375
const containerDivEl = doc.createElement("DIV");
376
containerDivEl.classList.add(
377
kAppendixContentsClass,
378
);
379
while (appendSectionEl.childNodes.length > 0) {
380
containerDivEl.appendChild(appendSectionEl.childNodes[0]);
381
}
382
383
appendSectionEl.appendChild(headerEl);
384
appendSectionEl.appendChild(containerDivEl);
385
appendixSectionEls.push(appendSectionEl);
386
}
387
}
388
// Place the user decorated appendixes at the front of the list
389
// of appendixes
390
if (appendixSectionEls.length > 0) {
391
appendixSections.unshift(...appendixSectionEls);
392
}
393
394
// Insert the sections
395
appendixSections.forEach((el) => {
396
appendixEl.appendChild(el);
397
});
398
399
// Only add the appendix if it has at least one section
400
if (appendixEl.childElementCount > 0) {
401
mainEl.appendChild(appendixEl);
402
}
403
}
404
}
405
406
const kCiteAsStyleBibtex = "bibtex";
407
const kCiteAsStyleDisplay = "display";
408
409
function citeStyleTester(format: Format) {
410
const citeStyle = format.metadata[kAppendixCiteAs];
411
const resolvedStyles: string[] = [];
412
if (citeStyle === undefined || citeStyle === true) {
413
resolvedStyles.push(...[kCiteAsStyleDisplay, kCiteAsStyleBibtex]);
414
} else {
415
if (Array.isArray(citeStyle)) {
416
resolvedStyles.push(...citeStyle);
417
} else {
418
resolvedStyles.push(citeStyle as string);
419
}
420
}
421
return {
422
hasCiteAs: () => {
423
return resolvedStyles.length > 0;
424
},
425
hasCiteAsStyle: (style: string) => {
426
return resolvedStyles.includes(style);
427
},
428
};
429
}
430
431
function parseStyle(style?: string) {
432
switch (style) {
433
case "plain":
434
return "plain";
435
default:
436
return kStyleDefault;
437
}
438
}
439
440
const kCcPattern = /(CC BY[^\s]*)\s*(\S*)/;
441
function creativeCommonsLicense(
442
license?: string,
443
) {
444
if (license) {
445
const match = license.toUpperCase().match(kCcPattern);
446
if (match) {
447
const base = match[1];
448
const version = match[2];
449
if (kAppendixCreativeCommonsLic.includes(base)) {
450
return {
451
base: base as
452
| "CC BY"
453
| "CC BY-SA"
454
| "CC BY-ND"
455
| "CC BY-NC"
456
| "CC BY-NC-ND"
457
| "CC BY-NC-SA",
458
version: version || "4.0",
459
};
460
}
461
} else if (license === kAppendixCCZero) {
462
// special case for this creative commons license
463
return {
464
base: kAppendixCCZero,
465
version: "1.0",
466
};
467
} else {
468
return undefined;
469
}
470
} else {
471
return undefined;
472
}
473
}
474
475
function creativeCommonsUrl(license: string, lang?: string, version?: string) {
476
// Special case for CC0 as different URL
477
if (license === kAppendixCCZero) {
478
return {
479
url: `https://creativecommons.org/publicdomain/zero/${version}/`,
480
text: `CC0 ${version}`,
481
inlineLink: true,
482
};
483
}
484
const licenseType = license.substring(3);
485
if (lang && lang !== "en") {
486
return {
487
url:
488
`https://creativecommons.org/licenses/${licenseType.toLowerCase()}/${version}/deed.${
489
lang.toLowerCase().replace("-", "_")
490
}`,
491
text: `CC ${licenseType} ${version}`,
492
inlineLink: true,
493
};
494
} else {
495
return {
496
url:
497
`https://creativecommons.org/licenses/${licenseType.toLowerCase()}/${version}/`,
498
text: `CC ${licenseType} ${version}`,
499
inlineLink: true,
500
};
501
}
502
}
503
504
async function generateCite(input: string, format: Format, offset?: string) {
505
const citeStyle = citeStyleTester(format);
506
if (citeStyle.hasCiteAs()) {
507
const { csl } = documentCSL(
508
input,
509
format.metadata,
510
"webpage",
511
format.pandoc["output-file"],
512
offset,
513
);
514
if (csl) {
515
// Render the HTML and BibTeX form of this document
516
const cslPath = getCSLPath(input, format);
517
return {
518
html: citeStyle.hasCiteAsStyle(kCiteAsStyleDisplay)
519
? await renderHtml(csl, cslPath)
520
: undefined,
521
bibtex: citeStyle.hasCiteAsStyle(kCiteAsStyleBibtex)
522
? await renderBibTex(csl)
523
: undefined,
524
};
525
} else {
526
return undefined;
527
}
528
} else {
529
return {};
530
}
531
}
532
533
// The removes any addition left margin markup that is added
534
// to the rendered citation (e.g. a number or so on)
535
function extractCiteEl(html: string, doc: Document) {
536
const htmlDiv = doc.createElement("DIV");
537
htmlDiv.innerHTML = html;
538
const entry = htmlDiv.querySelector(".csl-entry");
539
if (entry) {
540
const leftMarginEl = entry.querySelector(".csl-left-margin");
541
if (leftMarginEl) {
542
leftMarginEl.remove();
543
const rightEl = entry.querySelector(".csl-right-inline");
544
if (rightEl) {
545
rightEl.classList.remove("csl-right-inline");
546
}
547
}
548
return entry;
549
} else {
550
return undefined;
551
}
552
}
553
554