Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/quarto-core/attribution/document.ts
3583 views
1
/*
2
* document.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
basename,
9
dirname,
10
isAbsolute,
11
join,
12
relative,
13
} from "../../deno_ral/path.ts";
14
import {
15
kAbstract,
16
kAuthor,
17
kAuthors,
18
kCsl,
19
kLang,
20
kTitle,
21
} from "../../config/constants.ts";
22
import { Format, Metadata } from "../../config/types.ts";
23
import { parseAuthor } from "../../core/author.ts";
24
import { normalizePath, pathWithForwardSlashes } from "../../core/path.ts";
25
import {
26
CSL,
27
cslDate,
28
CSLExtras,
29
cslNames,
30
CSLType,
31
cslType,
32
kAbstractUrl,
33
kEIssn,
34
kPdfUrl,
35
suggestId,
36
} from "../../core/csl.ts";
37
import {
38
kSiteUrl,
39
kWebsite,
40
} from "../../project/types/website/website-constants.ts";
41
import { resolveAndFormatDate } from "../../core/date.ts";
42
43
const kDOI = "DOI";
44
const kCitation = "citation";
45
const kURL = "URL";
46
const kId = "id";
47
const kCitationKey = "citation-key";
48
const kEditor = "editor";
49
50
const kType = "type";
51
const kCategories = "categories";
52
const kLanguage = "language";
53
const kAvailableDate = "available-date";
54
const kIssued = "issued";
55
const kDate = "date";
56
57
const kPublisher = "publisher";
58
const kContainerTitle = "container-title";
59
const kVolume = "volume";
60
const kIssue = "issue";
61
const kISSN = "issn";
62
const kISBN = "isbn";
63
const kPMCID = "pmcid";
64
const kPMID = "pmid";
65
const kFirstPage = "page-first";
66
const kLastPage = "page-last";
67
const kPage = "page";
68
const kNumber = "number";
69
const kCustom = "custom";
70
const kArchiveCollection = "archive_collection";
71
const kArchiveLocation = "archive_location";
72
73
// Provides an absolute path to the referenced CSL file
74
export const getCSLPath = (input: string, format: Format) => {
75
const cslPath = format.metadata[kCsl] as string;
76
if (cslPath) {
77
if (isAbsolute(cslPath)) {
78
return cslPath;
79
} else {
80
return join(dirname(input), cslPath);
81
}
82
} else {
83
return undefined;
84
}
85
};
86
87
// The default type will be used if the type can't be determined from inspecting
88
// the metadata. This is particularly useful when differentiating between web pages
89
// and blog posts.
90
export function documentCSL(
91
input: string,
92
inputMetadata: Metadata,
93
defaultType: CSLType,
94
outputFile?: string,
95
offset?: string,
96
): { csl: CSL; extras: CSLExtras } {
97
const citationMetadata = citationMeta(inputMetadata);
98
99
// The type
100
const type = citationMetadata[kType]
101
? cslType(citationMetadata[kType] as string)
102
: defaultType;
103
104
// The title
105
const title = (citationMetadata[kTitle] || inputMetadata[kTitle]) as string;
106
const csl: CSL = {
107
title,
108
type,
109
};
110
111
// The citation key
112
const key = citationMetadata[kCitationKey] as string | undefined;
113
if (key) {
114
csl[kCitationKey] = key;
115
}
116
117
// Author
118
const authors = parseAuthor(
119
citationMetadata[kAuthor] || inputMetadata[kAuthor] ||
120
inputMetadata[kAuthors],
121
);
122
csl.author = cslNames(
123
authors?.filter((auth) => auth !== undefined).map((auth) => auth?.name),
124
);
125
126
// Editors
127
const editors = parseAuthor(citationMetadata[kEditor]);
128
csl.editor = cslNames(
129
editors?.filter((editor) => editor !== undefined).map((editor) =>
130
editor?.name
131
),
132
);
133
if (csl.editor && csl.editor.length === 0) {
134
delete csl.editor;
135
}
136
137
// Categories
138
const categories = citationMetadata[kCategories] ||
139
inputMetadata[kCategories];
140
if (categories) {
141
csl.categories = Array.isArray(categories) ? categories : [categories];
142
}
143
144
// Language
145
const language = (citationMetadata[kLanguage] || inputMetadata[kLang]) as
146
| string
147
| undefined;
148
if (language) {
149
csl.language = language;
150
}
151
152
// Date
153
const availableDate = citationMetadata[kAvailableDate] ||
154
resolveAndFormatDate(input, inputMetadata[kDate]);
155
if (availableDate) {
156
csl[kAvailableDate] = cslDate(availableDate);
157
}
158
159
// Issued date
160
const issued = citationMetadata[kIssued] ||
161
resolveAndFormatDate(input, inputMetadata[kDate]);
162
if (issued) {
163
csl.issued = cslDate(issued);
164
}
165
166
// The abstract
167
const abstract = citationMetadata[kAbstract] || inputMetadata[kAbstract];
168
if (abstract) {
169
csl.abstract = abstract as string;
170
}
171
172
// The publisher
173
const publisher = citationMetadata[kPublisher];
174
if (publisher) {
175
csl.publisher = publisher as string;
176
}
177
178
// The publication
179
const containerTitle = citationMetadata[kContainerTitle];
180
if (containerTitle) {
181
csl[kContainerTitle] = containerTitle as string;
182
}
183
184
// The id for this item
185
csl.id = citationMetadata[kId] as string ||
186
suggestId(csl.author, csl.issued);
187
188
// This is a helper function that will search
189
// metadata for the original key, or a transformed
190
// version of it (for example, all upper, then all lower)
191
const findValue = (
192
baseKey: string,
193
metadata: Metadata[],
194
transform: (key: string) => string,
195
) => {
196
const keys = [baseKey, transform(baseKey)];
197
for (const key of keys) {
198
for (const md of metadata) {
199
const value = md[key] as
200
| string
201
| undefined;
202
if (value) {
203
return value;
204
}
205
}
206
}
207
};
208
const lowercase = (key: string) => {
209
return key.toLocaleLowerCase();
210
};
211
const kebabcase = (key: string) => {
212
return key.replaceAll("_", "_");
213
};
214
215
// Url
216
const url = findValue(kURL, [citationMetadata], lowercase);
217
if (url) {
218
csl.URL = url;
219
} else {
220
csl.URL = synthesizeCitationUrl(input, inputMetadata, outputFile, offset);
221
}
222
223
// The DOI
224
const doi = findValue(kDOI, [citationMetadata, inputMetadata], lowercase);
225
if (doi) {
226
csl.DOI = doi;
227
}
228
229
const issue = citationMetadata[kIssue];
230
if (issue) {
231
csl.issue = issue as string;
232
}
233
234
const volume = citationMetadata[kVolume];
235
if (volume) {
236
csl.volume = volume as string;
237
}
238
239
const number = citationMetadata[kNumber];
240
if (number) {
241
csl.number = number as string;
242
}
243
244
const isbn = findValue(kISBN, [citationMetadata], lowercase);
245
if (isbn) {
246
csl.ISBN = isbn as string;
247
}
248
249
const issn = findValue(kISSN, [citationMetadata], lowercase);
250
if (issn) {
251
csl.ISSN = issn as string;
252
}
253
254
const pmcid = findValue(kPMCID, [citationMetadata], lowercase);
255
if (pmcid) {
256
csl.PMCID = pmcid as string;
257
}
258
259
const pmid = findValue(kPMID, [citationMetadata], lowercase);
260
if (pmid) {
261
csl.PMID = pmid as string;
262
}
263
264
const pageRange = pages(citationMetadata);
265
if (pageRange.firstPage) {
266
csl["page-first"] = pageRange.firstPage;
267
}
268
if (pageRange.lastPage) {
269
csl["page-last"] = pageRange.lastPage;
270
}
271
if (pageRange.page) {
272
csl.page = pageRange.page;
273
}
274
275
const archiveCollection = findValue(
276
kArchiveCollection,
277
[citationMetadata],
278
kebabcase,
279
);
280
if (archiveCollection) {
281
csl[kArchiveCollection] = archiveCollection;
282
}
283
284
const archiveLocation = findValue(
285
kArchiveLocation,
286
[citationMetadata],
287
kebabcase,
288
);
289
if (archiveLocation) {
290
csl[kArchiveLocation] = archiveLocation;
291
}
292
293
const forwardStringValue = (key: string) => {
294
if (citationMetadata[key] !== undefined) {
295
csl[key] = citationMetadata[key] as string;
296
}
297
};
298
[
299
"title-short",
300
"annote",
301
"archive",
302
"archive-place",
303
"authority",
304
"call-number",
305
"chapter-number",
306
"citation-number",
307
"citation-label",
308
"collection-number",
309
"collection-title",
310
"container-title-short",
311
"dimensions",
312
"division",
313
"edition",
314
"event-title",
315
"event-place",
316
"first-reference-note-number",
317
"genre",
318
"jurisdiction",
319
"keyword",
320
"locator",
321
"medium",
322
"note",
323
"number",
324
"number-of-pages",
325
"number-of-volumes",
326
"original-publisher",
327
"original-publisher-place",
328
"original-title",
329
"part",
330
"part-title",
331
"printing",
332
"publisher-place",
333
"references",
334
"reviewed-genre",
335
"reviewed-title",
336
"scale",
337
"section",
338
"source",
339
"status",
340
"supplement",
341
"version",
342
"volume-title",
343
"volume-title-short",
344
"year-suffix",
345
].forEach(forwardStringValue);
346
347
const forwardCSLNameValue = (key: string) => {
348
if (citationMetadata[key]) {
349
const authors = parseAuthor(citationMetadata[key]);
350
csl[key] = cslNames(
351
authors?.filter((auth) => auth !== undefined).map((auth) => auth?.name),
352
);
353
}
354
};
355
[
356
"chair",
357
"collection-editor",
358
"compiler",
359
"composer",
360
"container-author",
361
"contributor",
362
"curator",
363
"director",
364
"editor",
365
"editorial-director",
366
"executive-producer",
367
"guest",
368
"host",
369
"interviewer",
370
"illustrator",
371
"narrator",
372
"organizer",
373
"original-author",
374
"performer",
375
"producer",
376
"recipient",
377
"reviewed-author",
378
"script-writer",
379
"series-creator",
380
"translator",
381
].forEach(forwardCSLNameValue);
382
383
const forwardCSLDateValue = (key: string) => {
384
if (citationMetadata[key]) {
385
csl[key] = cslDate(citationMetadata[key]);
386
}
387
};
388
["accessed", "event-date", "original-date", "submitted"].forEach(
389
forwardCSLDateValue,
390
);
391
392
// Forward custom values
393
const custom = citationMetadata[kCustom];
394
if (custom) {
395
// TODO: Could consider supporting note 'cheater codes' which are the old way of doing this
396
csl[kCustom] = custom;
397
}
398
399
// Process anything extra
400
const extras: CSLExtras = {};
401
402
// Process keywords
403
const kwString = citationMetadata.keyword;
404
if (kwString && typeof kwString === "string") {
405
extras.keywords = kwString.split(",");
406
} else if (inputMetadata.keywords) {
407
const kw = inputMetadata.keywords;
408
extras.keywords = Array.isArray(kw) ? kw : [kw];
409
}
410
411
// Process extra URLS
412
if (citationMetadata[kPdfUrl]) {
413
extras[kPdfUrl] = citationMetadata[kPdfUrl] as string;
414
}
415
if (citationMetadata[kAbstractUrl]) {
416
extras[kAbstractUrl] = citationMetadata[kAbstractUrl] as string;
417
}
418
419
if (citationMetadata[kEIssn]) {
420
extras[kEIssn] = citationMetadata[kEIssn] as string;
421
}
422
423
return {
424
csl,
425
extras,
426
};
427
}
428
429
interface PageRange {
430
firstPage?: string;
431
lastPage?: string;
432
page?: string;
433
}
434
435
export function citationMeta(metadata: Metadata): Metadata {
436
if (typeof (metadata[kCitation]) === "object") {
437
return metadata[kCitation] as Record<string, unknown>;
438
} else {
439
return {} as Record<string, unknown>;
440
}
441
}
442
443
export function synthesizeCitationUrl(
444
input: string,
445
metadata: Metadata,
446
outputFile?: string,
447
offset?: string,
448
) {
449
const siteMeta = metadata[kWebsite] as Metadata | undefined;
450
let baseUrl = siteMeta?.[kSiteUrl] as string;
451
452
if (baseUrl && outputFile && offset) {
453
baseUrl = baseUrl.replace(/\/$/, "");
454
const rootDir = normalizePath(join(dirname(input), offset));
455
if (outputFile === "index.html") {
456
const part = pathWithForwardSlashes(relative(rootDir, dirname(input)));
457
if (part.length === 0) {
458
return `${baseUrl}/`;
459
} else {
460
return `${baseUrl}/${part}/`;
461
}
462
} else {
463
const relativePath = relative(
464
rootDir,
465
join(dirname(input), basename(outputFile)),
466
);
467
const part = pathWithForwardSlashes(relativePath);
468
return `${baseUrl}/${part}`;
469
}
470
} else {
471
// The url is unknown
472
return undefined;
473
}
474
}
475
476
function pages(citationMetadata: Metadata): PageRange {
477
let firstPage = citationMetadata[kFirstPage];
478
let lastPage = citationMetadata[kLastPage];
479
let pages = citationMetadata[kPage]
480
? `${citationMetadata[kPage] as string}` // Force pages to string in case user writes `page: 7`
481
: undefined;
482
if (pages && pages.includes("-")) {
483
const pagesSplit = pages.split("-");
484
if (!firstPage) {
485
firstPage = pagesSplit[0];
486
}
487
488
if (!lastPage) {
489
lastPage = pagesSplit[1];
490
}
491
} else if (pages && !firstPage) {
492
firstPage = pages;
493
} else if (!pages) {
494
if (firstPage && lastPage) {
495
pages = `${firstPage} - ${lastPage}`;
496
} else if (firstPage) {
497
pages = `${firstPage}`;
498
}
499
}
500
return {
501
firstPage: firstPage as string,
502
lastPage: lastPage as string,
503
page: pages,
504
};
505
}
506
507