Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/csl.ts
3562 views
1
/*
2
* csl.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { formatDate, parsePandocDate } from "./date.ts";
8
9
export const kPdfUrl = "pdf-url";
10
export const kAbstractUrl = "abstract-url";
11
export const kEIssn = "eissn";
12
13
export interface CSLExtras {
14
[kPdfUrl]?: string;
15
[kAbstractUrl]?: string;
16
[kEIssn]?: string;
17
keywords?: string[];
18
}
19
20
export interface CSL extends Record<string, unknown> {
21
// The id. This is technically required, but some providers (like crossref) don't provide
22
// one
23
id?: string;
24
25
"citation-key"?: string;
26
27
// Enumeration, one of the type ids from https://api.crossref.org/v1/types
28
type: CSLType;
29
30
// Name of work's publisher
31
publisher?: string;
32
33
// Title
34
title?: string;
35
36
// DOI of the work
37
DOI?: string;
38
39
// URL form of the work's DOI
40
URL?: string;
41
42
// Array of Contributors
43
author?: CSLName[];
44
45
editor?: CSLName[];
46
47
// Earliest of published-print and published-online
48
issued?: CSLDate;
49
50
// Full titles of the containing work (usually a book or journal)
51
"container-title"?: string;
52
53
// Short titles of the containing work (usually a book or journal)
54
"container-title-short"?: string;
55
56
// Issue number of an article's journal
57
issue?: string;
58
59
// The number of this item (for example, report number)
60
number?: string;
61
62
// Volume number of an article's journal
63
volume?: string;
64
65
// Pages numbers of an article within its journal
66
page?: string;
67
68
// First page of the range of pages the item (e.g. a journal article)
69
// covers in a container (e.g. a journal issue)
70
"page-first"?: string;
71
72
// Last page of the range of pages the item (e.g. a journal article)
73
// covers in a container (e.g. a journal issue)
74
// WARNING THIS IS NOT STRICTLY CSL
75
"page-last"?: string;
76
77
// These properties are often not included in CSL entries and are here
78
// primarily because they may need to be sanitized
79
ISSN?: string;
80
ISBN?: string;
81
PMID?: string;
82
"original-title"?: string;
83
"collection-title"?: string;
84
"short-title"?: string;
85
subtitle?: string;
86
subject?: string;
87
archive?: string;
88
license?: [];
89
90
// Date the item was initially available (e.g. the online publication date of a
91
// journal article before its formal publication date; the date a treaty
92
// was made available for signing)
93
"available-date"?: CSLDate;
94
95
"abstract"?: string;
96
97
"language"?: string;
98
99
categories?: string[];
100
}
101
102
export interface CSLName {
103
// “family” - surname minus any particles and suffixes
104
family: string;
105
106
// “given” - given names, either full (“John Edward”) or initialized (“J. E.”)
107
given: string;
108
109
// “dropping-particle” - name particles that are dropped when only the surname
110
// is shown (“van” in “Ludwig van Beethoven”, which becomes “Beethoven”, or “von”
111
// in “Alexander von Humboldt”, which becomes “Humboldt”)
112
["dropping-particle"]?: string;
113
114
// “non-dropping-particle” - name particles that are not dropped when only the
115
// surname is shown (“van” in the Dutch surname “van Gogh”) but which may be
116
// treated separately from the family name, e.g. for sorting
117
["non-dropping-particle"]?: string;
118
119
// “suffix” - name suffix, e.g. “Jr.” in “John Smith Jr.” and “III” in “Bill Gates III”
120
suffix?: string;
121
122
// A 'literal' representation of the name. May be displayed verbatim in contexts
123
literal?: string;
124
}
125
126
export interface CSLDate {
127
"date-parts"?: Array<[number, number?, number?]>;
128
["iso-8601"]: string;
129
// The raw input
130
raw?: string;
131
// A 'literal' representation of the name. May be displayed verbatim in contexts
132
literal?: string;
133
}
134
135
export function suggestId(author: CSLName[], date?: CSLDate) {
136
// Try to get the last name
137
let citeIdLeading = "";
138
if (author && author.length > 0) {
139
if (author[0].family) {
140
citeIdLeading = author[0].family;
141
} else if (author[0].literal) {
142
citeIdLeading = author[0].literal;
143
}
144
}
145
146
// Try to get the publication year
147
let datePart = "";
148
if (date && date["date-parts"] && date["date-parts"].length > 0) {
149
const yearIssued = date["date-parts"][0][0];
150
// Sometimes, data arrives with a null value, ignore null
151
if (yearIssued) {
152
datePart = yearIssued + "";
153
}
154
}
155
156
// Create a deduplicated string against the existing entries
157
let suggestedId = `${
158
citeIdLeading.replaceAll(/\s+/g, "_").toLowerCase()
159
}${datePart}`;
160
if (suggestedId.length === 0) {
161
suggestedId = "untitled";
162
}
163
return suggestedId;
164
}
165
166
// Converts a csl date to an EDTF date.
167
// See https://www.loc.gov/standards/datetime/
168
// Currently omits time component so this isn't truly level 0
169
export function cslDateToEDTFDate(date: CSLDate) {
170
if (date["date-parts"] && date["date-parts"].length > 0) {
171
const paddedParts = date["date-parts"][0].map((part) => {
172
const partStr = part?.toString();
173
if (partStr?.length === 1) {
174
return `0${partStr}`;
175
}
176
return partStr;
177
});
178
return paddedParts.join("-");
179
}
180
}
181
182
export function cslNames(authors: unknown) {
183
const cslNames: CSLName[] = [];
184
const authorList = Array.isArray(authors) ? authors : [authors];
185
authorList.forEach((auth) => {
186
if (auth) {
187
const name = authorToCslName(auth);
188
if (name) {
189
cslNames.push(name);
190
}
191
}
192
});
193
return cslNames;
194
}
195
196
const isCslDate = (dateRaw: unknown) => {
197
if (typeof dateRaw === "object") {
198
// deno-lint-ignore no-explicit-any
199
return ((dateRaw as any)["date-parts"] !== undefined) &&
200
// deno-lint-ignore no-explicit-any
201
(dateRaw as any)["iso-8601"] !== undefined;
202
} else {
203
return false;
204
}
205
};
206
207
export function cslDate(dateRaw: unknown): CSLDate | undefined {
208
const toDateArray = (dateArr: number[]): CSLDate | undefined => {
209
if (dateArr.length === 0) {
210
return undefined;
211
} else if (dateArr.length === 1) {
212
return {
213
"date-parts": [[
214
dateArr[0],
215
]],
216
"iso-8601": `${dateArr[0]}`,
217
};
218
} else if (dateArr.length === 2) {
219
return {
220
"date-parts": [[
221
dateArr[0],
222
dateArr[1],
223
]],
224
"iso-8601": `${dateArr[0]}-${dateArr[1]}`,
225
};
226
} else if (dateArr.length >= 3) {
227
return {
228
"date-parts": [[
229
dateArr[0],
230
dateArr[1],
231
dateArr[2],
232
]],
233
"iso-8601": `${dateArr[0]}-${dateArr[1]}-${dateArr[2]}`,
234
};
235
}
236
};
237
238
if (Array.isArray(dateRaw)) {
239
const dateArr = dateRaw as number[];
240
return toDateArray(dateArr);
241
} else if (typeof dateRaw === "number") {
242
const parseNumeric = (dateStr: string) => {
243
let dateParsed = dateStr;
244
const chomps = [4, 2, 2];
245
const date: number[] = [];
246
for (const chomp of chomps) {
247
if (dateParsed.length >= chomp) {
248
const part = dateParsed.substring(0, chomp);
249
if (!isNaN(+part)) {
250
date.push(+part);
251
dateParsed = dateParsed.substring(chomp);
252
} else {
253
break;
254
}
255
} else {
256
break;
257
}
258
}
259
if (date.length > 0) {
260
return date;
261
} else {
262
return undefined;
263
}
264
};
265
266
const dateStr = String(dateRaw);
267
const dateArr = parseNumeric(dateStr);
268
if (dateArr) {
269
return toDateArray(dateArr);
270
}
271
} else if (typeof dateRaw === "string") {
272
// Look for an explicit month year like 1999-04
273
const match = dateRaw.match(/^(\d\d\d\d)[-/](\d\d)$/);
274
if (match) {
275
return {
276
"date-parts": [[
277
parseInt(match[1]),
278
parseInt(match[2]),
279
]],
280
"iso-8601": `${match[1]}-${match[2]}`,
281
};
282
} else {
283
// Trying parsing format strings
284
const date = parsePandocDate(dateRaw);
285
if (date) {
286
const formatted = formatDate(date, "YYYY-MM-DD");
287
return {
288
"date-parts": [[
289
date.getFullYear(),
290
date.getMonth() + 1,
291
date.getDate(),
292
]],
293
literal: formatDate(date, "YYYY-MM-DD"),
294
raw: dateRaw,
295
"iso-8601": formatted,
296
};
297
}
298
}
299
300
return undefined;
301
} else if (isCslDate(dateRaw)) {
302
return dateRaw as CSLDate;
303
} else if (dateRaw !== null && typeof dateRaw === "object") {
304
const dateParts: number[] = [];
305
if ("year" in dateRaw) {
306
dateParts.push((dateRaw as { year: number })["year"]);
307
if ("month" in dateRaw) {
308
dateParts.push((dateRaw as { month: number })["month"]);
309
if ("day" in dateRaw) {
310
dateParts.push((dateRaw as { day: number })["day"]);
311
}
312
}
313
return {
314
"date-parts": [dateParts as [number, number?, number?]],
315
"iso-8601": dateParts.join("-"),
316
};
317
} else {
318
return undefined;
319
}
320
}
321
}
322
323
function authorToCslName(
324
author: unknown,
325
): CSLName | undefined {
326
if (typeof author === "string") {
327
const parts = author.split(" ");
328
if (parts.length > 0) {
329
const given = parts.shift() || "";
330
const family = parts.length > 0 ? parts.join(" ") : "";
331
return {
332
family,
333
given,
334
};
335
}
336
} else {
337
return author as CSLName;
338
}
339
}
340
341
export type CSLType =
342
| "article"
343
| "article-journal"
344
| "article-magazine"
345
| "article-newspaper"
346
| "bill"
347
| "book"
348
| "broadcast"
349
| "chapter"
350
| "classic"
351
| "collection"
352
| "dataset"
353
| "document"
354
| "entry"
355
| "entry-dictionary"
356
| "entry-encyclopedia"
357
| "event"
358
| "figure"
359
| "graphic"
360
| "hearing"
361
| "interview"
362
| "legal_case"
363
| "legislation"
364
| "manuscript"
365
| "map"
366
| "motion_picture"
367
| "musical_score"
368
| "pamphlet"
369
| "paper-conference"
370
| "patent"
371
| "performance"
372
| "periodical"
373
| "personal_communication"
374
| "post"
375
| "post-weblog"
376
| "regulation"
377
| "report"
378
| "review"
379
| "review-book"
380
| "software"
381
| "song"
382
| "speech"
383
| "standard"
384
| "thesis"
385
| "treaty"
386
| "webpage";
387
388
export function cslType(type: string) {
389
if (types.includes(type)) {
390
return type as CSLType;
391
} else {
392
if (type === "journal") {
393
return "article-journal";
394
} else if (type === "conference") {
395
return "paper-conference";
396
} else if (type === "dissertation") {
397
return "thesis";
398
}
399
return "article";
400
}
401
}
402
403
const types = [
404
"article",
405
"article-journal",
406
"article-magazine",
407
"article-newspaper",
408
"bill",
409
"book",
410
"broadcast",
411
"chapter",
412
"classic",
413
"collection",
414
"dataset",
415
"document",
416
"entry",
417
"entry-dictionary",
418
"entry-encyclopedia",
419
"event",
420
"figure",
421
"graphic",
422
"hearing",
423
"interview",
424
"legal_case",
425
"legislation",
426
"manuscript",
427
"map",
428
"motion_picture",
429
"musical_score",
430
"pamphlet",
431
"paper-conference",
432
"patent",
433
"performance",
434
"periodical",
435
"personal_communication",
436
"post",
437
"post-weblog",
438
"regulation",
439
"report",
440
"review",
441
"review-book",
442
"software",
443
"song",
444
"speech",
445
"standard",
446
"thesis",
447
"treaty",
448
"webpage",
449
];
450
451