@behackl/citation-js-extras 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +92 -6
- package/dist/types.d.ts +24 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ See [Mathematics and sanitizing](docs/math.md).
|
|
|
96
96
|
| how entries read | the CSL style (`cslStyle`) and locale (`lang`) | [Layout](docs/layout.md#styles-and-locales) |
|
|
97
97
|
| the markup around entries | `list`, `listAttributes`, `itemAttributes`, `badgeListClassName` | [Layout](docs/layout.md#markup) |
|
|
98
98
|
| a layout of your own | `links(entry)`, `appendBadges`, `wrapVariable` | [Layout](docs/layout.md#laying-out-entries-yourself) |
|
|
99
|
+
| a "Copy BibTeX" button | `bibtex(entry)` | [Layout](docs/layout.md#copying-an-entrys-bibtex) |
|
|
99
100
|
| what reaches the page | `sanitize`, `renderMath` | [Mathematics](docs/math.md) |
|
|
100
101
|
| order and selection | `sort`, `filter`, or array methods on `bib.entries` | [API](docs/api.md#bibsortentries-options) |
|
|
101
102
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { BibEntry, BibliographyOptions, EntryLinks, FormatOptions } from "./types.js";
|
|
2
|
-
export type { BadgeConfig, BadgeFunction, BibEntry, BibliographyOptions, EntryLinks, FormatDefaults, FormatOptions, HtmlAttributes, Link, MathRenderer, } from "./types.js";
|
|
1
|
+
import type { BibEntry, BibliographyOptions, BibtexOptions, EntryLinks, FormatOptions } from "./types.js";
|
|
2
|
+
export type { BadgeConfig, BadgeFunction, BadgeLink, BibEntry, BibliographyOptions, BibtexOptions, EntryLinks, FormatDefaults, FormatOptions, HtmlAttributes, Link, MathRenderer, TitleLink, } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Ready-made badges for the identifiers of mathematical bibliographies. Their
|
|
5
5
|
* matchers accept the spellings found in real exports (`doi:10…`,
|
|
@@ -44,6 +44,7 @@ export declare class Bibliography {
|
|
|
44
44
|
/** All parsed entries. */
|
|
45
45
|
readonly entries: BibEntry[];
|
|
46
46
|
private readonly customFieldNames;
|
|
47
|
+
private readonly bibtexEntries;
|
|
47
48
|
private readonly math?;
|
|
48
49
|
/** Formatting defaults from the constructor; each call may override them. */
|
|
49
50
|
private readonly formatDefaults;
|
|
@@ -83,6 +84,21 @@ export declare class Bibliography {
|
|
|
83
84
|
* rendering badges yourself, pair it with `appendBadges: false`.
|
|
84
85
|
*/
|
|
85
86
|
links(entry: BibEntry, options?: FormatOptions): EntryLinks;
|
|
87
|
+
/**
|
|
88
|
+
* The entry as BibTeX that stands on its own, e.g. for readers to copy:
|
|
89
|
+
* `@string` abbreviations are resolved and the fields of `crossref` parents
|
|
90
|
+
* filled in using biblatex's title-remapping rules. Missing parents leave
|
|
91
|
+
* `crossref` unresolved, so the copy may still require its parent.
|
|
92
|
+
* Field values are the TeX of the `.bib` file. Requires this bibliography's
|
|
93
|
+
* original key and raw object; shallow entry copies are accepted.
|
|
94
|
+
*/
|
|
95
|
+
bibtex(entry: BibEntry, options?: BibtexOptions): string;
|
|
96
|
+
/**
|
|
97
|
+
* An entry's fields with those inherited through `crossref`, using the same
|
|
98
|
+
* biblatex title-remapping rules as citation-js (`plugin-bibtex`'s
|
|
99
|
+
* `mapping/crossref.js`). Keep the mapping and exclusion tests in sync.
|
|
100
|
+
*/
|
|
101
|
+
private withInherited;
|
|
86
102
|
/** Per-call options win over the defaults given to the constructor. */
|
|
87
103
|
private mergeOptions;
|
|
88
104
|
/**
|
package/dist/index.js
CHANGED
|
@@ -80,6 +80,7 @@ export class Bibliography {
|
|
|
80
80
|
/** All parsed entries. */
|
|
81
81
|
entries;
|
|
82
82
|
customFieldNames;
|
|
83
|
+
bibtexEntries = new Map();
|
|
83
84
|
math;
|
|
84
85
|
/** Formatting defaults from the constructor; each call may override them. */
|
|
85
86
|
formatDefaults;
|
|
@@ -97,12 +98,11 @@ export class Bibliography {
|
|
|
97
98
|
// Two-pass parse: raw (preserves all fields) + CSL (for formatting)
|
|
98
99
|
const { plugins } = Cite;
|
|
99
100
|
const rawEntries = plugins.input.chainLink(bibData);
|
|
100
|
-
const rawMap = new Map();
|
|
101
101
|
const duplicates = new Set();
|
|
102
102
|
for (const entry of rawEntries) {
|
|
103
|
-
if (
|
|
103
|
+
if (this.bibtexEntries.has(entry.label))
|
|
104
104
|
duplicates.add(entry.label);
|
|
105
|
-
|
|
105
|
+
this.bibtexEntries.set(entry.label, entry);
|
|
106
106
|
}
|
|
107
107
|
// Raw fields are merged by key, so a duplicate would silently give one
|
|
108
108
|
// entry the other's fields.
|
|
@@ -120,7 +120,7 @@ export class Bibliography {
|
|
|
120
120
|
: new Cite(bibData);
|
|
121
121
|
this.entries = cite.data.map((csl) => {
|
|
122
122
|
const key = String(csl["citation-key"] || csl.id);
|
|
123
|
-
const raw =
|
|
123
|
+
const raw = this.bibtexEntries.get(key)?.properties ?? {};
|
|
124
124
|
const custom = {};
|
|
125
125
|
for (const f of this.customFieldNames) {
|
|
126
126
|
const value = raw[f.toLowerCase()] ?? raw[f];
|
|
@@ -205,9 +205,62 @@ export class Bibliography {
|
|
|
205
205
|
links(entry, options = {}) {
|
|
206
206
|
return this.resolveLinks(entry, this.mergeOptions(options));
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* The entry as BibTeX that stands on its own, e.g. for readers to copy:
|
|
210
|
+
* `@string` abbreviations are resolved and the fields of `crossref` parents
|
|
211
|
+
* filled in using biblatex's title-remapping rules. Missing parents leave
|
|
212
|
+
* `crossref` unresolved, so the copy may still require its parent.
|
|
213
|
+
* Field values are the TeX of the `.bib` file. Requires this bibliography's
|
|
214
|
+
* original key and raw object; shallow entry copies are accepted.
|
|
215
|
+
*/
|
|
216
|
+
bibtex(entry, options = {}) {
|
|
217
|
+
const source = this.bibtexEntries.get(entry.key);
|
|
218
|
+
if (!source || source.properties !== entry.raw) {
|
|
219
|
+
throw new Error(`entry ${entry.key}: not in this bibliography`);
|
|
220
|
+
}
|
|
221
|
+
const { type } = source;
|
|
222
|
+
const fields = this.withInherited(type, entry.raw);
|
|
223
|
+
if (fields !== entry.raw)
|
|
224
|
+
delete fields.crossref;
|
|
225
|
+
const exclude = new Set((options.exclude ?? []).map(field => field.toLowerCase()));
|
|
226
|
+
const lines = Object.entries(fields)
|
|
227
|
+
.filter(([name, value]) => value != null && !exclude.has(name))
|
|
228
|
+
.map(([name, value]) => ` ${name} = ${bibtexValue(name, value)},`);
|
|
229
|
+
return [`@${type}{${entry.key},`, ...lines, "}"].join("\n");
|
|
230
|
+
}
|
|
208
231
|
// -------------------------------------------------------------------------
|
|
209
232
|
// Private helpers
|
|
210
233
|
// -------------------------------------------------------------------------
|
|
234
|
+
/**
|
|
235
|
+
* An entry's fields with those inherited through `crossref`, using the same
|
|
236
|
+
* biblatex title-remapping rules as citation-js (`plugin-bibtex`'s
|
|
237
|
+
* `mapping/crossref.js`). Keep the mapping and exclusion tests in sync.
|
|
238
|
+
*/
|
|
239
|
+
withInherited(type, raw) {
|
|
240
|
+
const parent = raw.crossref == null ? undefined : this.bibtexEntries.get(String(raw.crossref));
|
|
241
|
+
if (!parent || parent.properties === raw)
|
|
242
|
+
return raw;
|
|
243
|
+
const inherited = { ...this.withInherited(parent.type, parent.properties) };
|
|
244
|
+
for (const field of NOT_INHERITED)
|
|
245
|
+
delete inherited[field];
|
|
246
|
+
if ((parent.type === "mvbook" || parent.type === "book") && BOOK_PARTS.includes(type)) {
|
|
247
|
+
inherited.bookauthor = inherited.author;
|
|
248
|
+
}
|
|
249
|
+
const [prefix, children] = TITLE_INHERITANCE[parent.type] ?? [];
|
|
250
|
+
if (prefix && children.includes(type)) {
|
|
251
|
+
inherited[`${prefix}title`] = inherited.title;
|
|
252
|
+
inherited[`${prefix}subtitle`] = inherited.subtitle;
|
|
253
|
+
if (prefix !== "journal")
|
|
254
|
+
inherited[`${prefix}titleaddon`] = inherited.titleaddon;
|
|
255
|
+
for (const field of TITLE_FIELDS)
|
|
256
|
+
delete inherited[field];
|
|
257
|
+
}
|
|
258
|
+
const fields = { ...raw };
|
|
259
|
+
for (const [name, value] of Object.entries(inherited))
|
|
260
|
+
if (!Object.hasOwn(fields, name))
|
|
261
|
+
fields[name] = value;
|
|
262
|
+
return fields;
|
|
263
|
+
}
|
|
211
264
|
/** Per-call options win over the defaults given to the constructor. */
|
|
212
265
|
mergeOptions(options) {
|
|
213
266
|
const merged = { ...options };
|
|
@@ -397,6 +450,39 @@ export function linkifyBareUrls(html) {
|
|
|
397
450
|
// ---------------------------------------------------------------------------
|
|
398
451
|
// Fields and links
|
|
399
452
|
// ---------------------------------------------------------------------------
|
|
453
|
+
/**
|
|
454
|
+
* Biblatex's non-inherited metadata. Intentionally uses `shorthand` and
|
|
455
|
+
* `shorthandintro`: plugin-bibtex 0.7.21 misspells these as `shortand` and
|
|
456
|
+
* `shortandintro`, so this exclusion differs from that version's implementation.
|
|
457
|
+
*/
|
|
458
|
+
const NOT_INHERITED = [
|
|
459
|
+
"ids", "crossref", "xref", "entryset", "entrysubtype", "execute", "label", "options", "presort",
|
|
460
|
+
"related", "relatedoptions", "relatedstring", "relatedtype", "shorthand", "shorthandintro", "sortkey",
|
|
461
|
+
];
|
|
462
|
+
const TITLE_FIELDS = ["title", "subtitle", "titleaddon", "shorttitle", "sorttitle", "indextitle", "indexsorttitle"];
|
|
463
|
+
const BOOK_PARTS = ["inbook", "bookinbook", "suppbook"];
|
|
464
|
+
const COLLECTION_PARTS = ["incollection", "inreference", "suppcollection"];
|
|
465
|
+
/** A parent's `title` becomes `<prefix>title` in children of these types. */
|
|
466
|
+
const TITLE_INHERITANCE = {
|
|
467
|
+
mvbook: ["main", ["book", ...BOOK_PARTS]],
|
|
468
|
+
mvcollection: ["main", ["collection", "reference", ...COLLECTION_PARTS]],
|
|
469
|
+
mvreference: ["main", ["collection", "reference", ...COLLECTION_PARTS]],
|
|
470
|
+
mvproceedings: ["main", ["proceedings", "inproceedings"]],
|
|
471
|
+
book: ["book", BOOK_PARTS],
|
|
472
|
+
collection: ["book", COLLECTION_PARTS],
|
|
473
|
+
reference: ["book", COLLECTION_PARTS],
|
|
474
|
+
proceedings: ["book", ["inproceedings"]],
|
|
475
|
+
periodical: ["journal", ["article", "suppperiodical"]],
|
|
476
|
+
};
|
|
477
|
+
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
478
|
+
/** A field value in braces, with the whitespace BibTeX would compress anyway compressed. */
|
|
479
|
+
function bibtexValue(name, value) {
|
|
480
|
+
const text = String(value).replace(/\s*\n\s*/g, " ");
|
|
481
|
+
// citation-js reads `month = mar` as "03"; the abbreviation is what BibTeX styles expect.
|
|
482
|
+
if (name === "month" && /^(?:0[1-9]|1[0-2])$/.test(text))
|
|
483
|
+
return MONTHS[Number(text) - 1];
|
|
484
|
+
return `{${text}}`;
|
|
485
|
+
}
|
|
400
486
|
/**
|
|
401
487
|
* The value of a BibTeX field, and the only place fields are read. Names are
|
|
402
488
|
* case-insensitive. A field named like an eprint archive falls back to
|
|
@@ -497,7 +583,7 @@ function decorate(html, titleLinked, links, options) {
|
|
|
497
583
|
if (links.title && !titleLinked)
|
|
498
584
|
out += ` ${linkHtml(links.title, escapeHtml(links.title.url), options)}`;
|
|
499
585
|
if (links.badges.length && options.appendBadges !== false) {
|
|
500
|
-
const badges = links.badges.map(link => linkHtml(link, escapeHtml(link.label
|
|
586
|
+
const badges = links.badges.map(link => linkHtml(link, escapeHtml(link.label), options));
|
|
501
587
|
out += ` <span class="${escapeAttr(options.badgeListClassName ?? "bib-links")}">${badges.join(" ")}</span>`;
|
|
502
588
|
}
|
|
503
589
|
return out;
|
|
@@ -626,7 +712,7 @@ function renderAttributes(attrs) {
|
|
|
626
712
|
for (const [k, v] of Object.entries(attrs)) {
|
|
627
713
|
if (v === true)
|
|
628
714
|
parts.push(k);
|
|
629
|
-
else if (v !== false)
|
|
715
|
+
else if (v !== false && v !== undefined)
|
|
630
716
|
parts.push(`${k}="${escapeAttr(String(v))}"`);
|
|
631
717
|
}
|
|
632
718
|
return parts.length ? " " + parts.join(" ") : "";
|
package/dist/types.d.ts
CHANGED
|
@@ -68,8 +68,11 @@ export interface BadgeConfig {
|
|
|
68
68
|
}
|
|
69
69
|
/** Computes a badge's label or URL from the matched field value. */
|
|
70
70
|
export type BadgeFunction = (value: string, entry: BibEntry) => string;
|
|
71
|
-
/**
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* HTML attributes; `true` renders a valueless attribute, `false` and
|
|
73
|
+
* `undefined` omit it.
|
|
74
|
+
*/
|
|
75
|
+
export type HtmlAttributes = Record<string, string | boolean | undefined>;
|
|
73
76
|
/** A link the library resolved for an entry: its title link or a badge. */
|
|
74
77
|
export interface Link {
|
|
75
78
|
kind: "title" | "badge";
|
|
@@ -84,10 +87,21 @@ export interface Link {
|
|
|
84
87
|
className?: string;
|
|
85
88
|
entry: BibEntry;
|
|
86
89
|
}
|
|
90
|
+
/** An entry's title link. */
|
|
91
|
+
export interface TitleLink extends Link {
|
|
92
|
+
kind: "title";
|
|
93
|
+
label?: never;
|
|
94
|
+
className?: never;
|
|
95
|
+
}
|
|
96
|
+
/** A badge link, which always has a label. */
|
|
97
|
+
export interface BadgeLink extends Link {
|
|
98
|
+
kind: "badge";
|
|
99
|
+
label: string;
|
|
100
|
+
}
|
|
87
101
|
/** The links of one entry, as returned by `Bibliography.links`. */
|
|
88
102
|
export interface EntryLinks {
|
|
89
|
-
title?:
|
|
90
|
-
badges:
|
|
103
|
+
title?: TitleLink;
|
|
104
|
+
badges: BadgeLink[];
|
|
91
105
|
}
|
|
92
106
|
/** Synchronous renderer returning trusted HTML. Sanitize untrusted renderer output. */
|
|
93
107
|
export type MathRenderer = (tex: string, context: {
|
|
@@ -174,7 +188,7 @@ export interface FormatOptions {
|
|
|
174
188
|
* `className`; an `href` replaces the URL (it must still be `http(s)`,
|
|
175
189
|
* `mailto` or relative, or the link is dropped).
|
|
176
190
|
*/
|
|
177
|
-
linkAttributes?: (link:
|
|
191
|
+
linkAttributes?: (link: TitleLink | BadgeLink) => HtmlAttributes;
|
|
178
192
|
/**
|
|
179
193
|
* Wrap the rendered HTML of each CSL variable in the bibliography (and the
|
|
180
194
|
* title outside its link), e.g. to mark it for CSS:
|
|
@@ -200,6 +214,11 @@ export interface FormatOptions {
|
|
|
200
214
|
*/
|
|
201
215
|
linkifyUrls?: boolean;
|
|
202
216
|
}
|
|
217
|
+
/** Options for {@link Bibliography.bibtex}. */
|
|
218
|
+
export interface BibtexOptions {
|
|
219
|
+
/** Fields to leave out, case-insensitively, e.g. private ones like `status` or `file`. */
|
|
220
|
+
exclude?: string[];
|
|
221
|
+
}
|
|
203
222
|
/** A bibliography entry enriched with custom BibTeX fields. */
|
|
204
223
|
export interface BibEntry {
|
|
205
224
|
/** The CSL-JSON object used by citation-js for formatting. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@behackl/citation-js-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Preserve custom BibTeX fields through citation-js and render academic bibliographies with linked titles, badges, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc",
|
|
21
21
|
"test": "vitest run",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
22
23
|
"test:watch": "vitest",
|
|
23
24
|
"test:package": "pnpm build && node scripts/test-package.mjs",
|
|
24
25
|
"prepublishOnly": "tsc"
|