@behackl/citation-js-extras 0.2.0 → 0.3.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 CHANGED
@@ -2,263 +2,115 @@
2
2
 
3
3
  ![NPM Version](https://img.shields.io/npm/v/%40behackl%2Fcitation-js-extras)
4
4
 
5
- Preserve custom BibTeX fields through [citation-js](https://citation.js.org/) and render academic bibliographies with linked titles, configurable badges, and more.
6
-
7
- ## The problem
8
-
9
- citation-js converts BibTeX to CSL-JSON, but silently **drops all non-standard fields** during the conversion. There is no plugin hook or configuration option to preserve them. Fields like `arxiv`, `mrnumber`, `publication-status`, or project identifiers are lost.
10
-
11
- This package solves the problem with a two-pass parsing strategy: one pass extracts the raw BibTeX fields, the other produces CSL-JSON for formatting. The results are merged so you get the best of both worlds.
5
+ Turn a BibTeX file into an HTML publication list with
6
+ [citation-js](https://citation.js.org/): any CSL style, titles linked to the
7
+ paper, badges for DOI, arXiv, MathSciNet and zbMATH, and mathematics in titles.
8
+
9
+ - **Every BibTeX field stays available.** citation-js drops fields it doesn't
10
+ know (`status`, `project`, `mrnumber`, …); here they stay on each entry, for
11
+ grouping, filtering and links.
12
+ - **Links, printed once.** The title links to the paper and identifiers become
13
+ badges. The style doesn't print a linked DOI or URL a second time as text.
14
+ - **Exports work as they are.** `doi:10…` and `https://doi.org/10…`, arXiv ids
15
+ in biblatex's `eprint` field, titles with quotes, `\emph` or `&`.
16
+ - **Mathematics.** `$…$` in titles survives the formatting and is typeset by
17
+ the renderer you pass in, such as KaTeX or MathJax.
18
+ - **Your layout.** The markup is configurable; links are available as data;
19
+ hooks reach into the formatted citation.
12
20
 
13
21
  ## Install
14
22
 
15
23
  ```bash
16
24
  npm install @behackl/citation-js-extras citation-js
17
- # or
18
- pnpm add @behackl/citation-js-extras citation-js
19
25
  ```
20
26
 
21
- `citation-js` is a **peer dependency** — you bring your own version.
27
+ `citation-js` is a peer dependency.
22
28
 
23
29
  ## Quick start
24
30
 
25
- ```ts
26
- import { Bibliography } from "@behackl/citation-js-extras";
27
-
28
- const bib = new Bibliography({
29
- data: "./references.bib", // file path or raw BibTeX string
30
- cslStyle: "./my-style.csl", // optional: file path, raw XML, or registered template name
31
- customFields: ["publication-status", "arxiv", "mrnumber"],
32
- });
33
-
34
- // Filter and sort
35
- const published = bib.filter({ "publication-status": "published" });
36
- const sorted = bib.sort(published, { by: "year", order: "desc" });
37
-
38
- // Render HTML
39
- const html = bib.formatHtml(sorted, {
40
- titleLink: ["url", "doi", "arxiv"],
41
- badges: [
42
- { field: "doi", label: "doi", url: "https://doi.org/$1", className: "badge-doi" },
43
- {
44
- field: "arxiv",
45
- label: "arXiv",
46
- url: "https://arxiv.org/abs/$1",
47
- match: /^(.+?)(?:v\d+)?$/,
48
- className: "badge-arxiv",
49
- },
50
- ],
51
- });
52
- ```
53
-
54
- ## Preserving mathematics
55
-
56
- Citation.js normally converts TeX math to text, losing delimiters and potentially
57
- complex expressions. Enable preservation before parsing:
58
-
59
- ```ts
60
- const bib = new Bibliography({
61
- data: "./references.bib",
62
- preserveMath: true,
63
- });
64
-
65
- // Restore HTML-escaped original TeX for client-side MathJax:
66
- const html = bib.formatHtml(bib.entries);
67
-
68
- // Or typeset at build time with your own synchronous renderer:
69
- const rendered = bib.formatHtml(bib.entries, {
70
- renderMath: (tex, { display }) => myMathRenderer(tex, display),
71
- });
72
- ```
73
-
74
- `myMathRenderer` is an application-supplied function returning **trusted HTML**
75
- (e.g. MathJax SVG or KaTeX output). No math renderer is bundled. Configure it for
76
- untrusted TeX as appropriate; the callback output is inserted verbatim, not
77
- sanitized. Exceptions propagate to the caller. `renderMath` also works with
78
- `formatEntry`; it has no effect unless `preserveMath` was enabled.
79
-
80
- Supported delimiters are `$…$`, `$$…$$`, `\(…\)`, and `\[…\]`.
81
- Escape literal dollars as `\$`. Empty or unclosed expressions throw with the
82
- citation key and field name. This is a delimiter scanner, not a TeX validator:
83
- unsupported commands and mathematical validity are the renderer's responsibility.
84
-
85
- Protection covers `title`, `subtitle`, `titleaddon`, `shorttitle`, `booktitle`,
86
- `booksubtitle`, `booktitleaddon`, `maintitle`, `mainsubtitle`, `maintitleaddon`,
87
- `journaltitle`, `journalsubtitle`, `journal`, `note`, `annote`, `abstract`, and
88
- `howpublished`. Fields still need to be supported by Citation.js and the chosen
89
- CSL style to appear in the output. Names, identifiers, URLs, and custom metadata
90
- are not protected. BibTeX strings and concatenations are resolved before protection;
91
- inherited cross-reference text is protected during conversion.
92
-
93
- Original `.raw` and `.custom` values remain unchanged. With preservation enabled,
94
- `.csl` contains internal placeholders: use the formatting methods for HTML and
95
- raw fields for original source text, not `.csl` for plain-text exports. Placeholders
96
- also mean CSL title-based sorting/disambiguation operates on protected text rather
97
- than mathematical meaning. Existing caller-controlled ordering is retained.
98
-
99
- Restoration runs after title linking, badges, and URL linkification. Neither
100
- renderer output nor restored TeX is fed back through these HTML helpers. Disabling
101
- preservation (the default) retains the previous behavior.
102
-
103
- ## Development checks
104
-
105
- ```sh
106
- pnpm install --frozen-lockfile
107
- pnpm test # Unit tests and actual MathJax SVG integration (base + AMS)
108
- pnpm test:package # Build, pack, install into a temporary consumer, and test exports
109
- ```
110
-
111
- The package check validates ESM imports, TypeScript declarations under both
112
- NodeNext and Bundler resolution, and MathJax rendering through the installed
113
- tarball. Its temporary consumer is removed afterwards. Installation prefers the
114
- local cache but may need registry access on a fresh machine. CI runs both checks.
115
- MathJax is a development-only dependency, not a runtime dependency for consumers.
116
- Use Node 24 LTS for these development checks, matching CI and publishing.
117
-
118
- ## API
119
-
120
- ### `new Bibliography(options)`
121
-
122
- | Option | Type | Description |
123
- |---|---|---|
124
- | `data` | `string` | BibTeX input — a raw string or a file path. |
125
- | `cslStyle` | `string?` | CSL style — a registered template name, raw XML, or a file path. Defaults to `'apa'`. |
126
- | `customFields` | `string[]?` | BibTeX field names to preserve. These appear on each entry under `.custom`. |
127
- | `preserveMath` | `boolean?` | Preserve math in display-text fields through CSL formatting. Defaults to `false`. |
128
-
129
- ### `bib.entries`
130
-
131
- All parsed entries as `BibEntry[]`:
132
-
133
- ```ts
134
- interface BibEntry {
135
- csl: Record<string, any>; // CSL-JSON data (for citation-js)
136
- key: string; // BibTeX citation key
137
- year: number | null; // extracted from CSL `issued`
138
- custom: Record<string, string>; // declared custom fields
139
- raw: Record<string, any>; // all raw BibTeX properties
31
+ ```bibtex
32
+ @article{lindqvist2025,
33
+ author = {Lindqvist, Maja and Sato, Ren},
34
+ title = {Sobolev estimates for $L^p$ averages},
35
+ journal = {Annals of Invented Analysis},
36
+ volume = {12},
37
+ pages = {1--44},
38
+ year = {2025},
39
+ doi = {10.5555/aia.2025.12},
40
+ eprint = {2501.01234},
41
+ eprinttype = {arxiv},
42
+ status = {published},
140
43
  }
141
44
  ```
142
45
 
143
- ### `bib.filter(criteria)`
144
-
145
- Filter entries by custom field values. All criteria must match (AND logic).
146
-
147
- ```ts
148
- bib.filter({ "publication-status": "published" });
149
- bib.filter({ "publication-status": "published", project: "ABC-123" });
150
- ```
151
-
152
- ### `bib.sort(entries, options?)`
153
-
154
- Return a sorted **copy** of the entries (the input is not mutated).
155
-
156
46
  ```ts
157
- bib.sort(entries); // by year, descending (default)
158
- bib.sort(entries, { by: "year", order: "asc" });
159
- ```
160
-
161
- ### `bib.formatHtml(entries, options?)`
47
+ import { Bibliography, badgePresets } from "@behackl/citation-js-extras";
162
48
 
163
- Render entries as a complete HTML bibliography list.
164
-
165
- ```ts
166
- bib.formatHtml(entries, {
167
- titleLink: ["url", "doi", "arxiv"],
168
- badges: [ /* ... */ ],
169
- list: "ol", // 'ol', 'ul', or 'div' (div uses <div class="csl-entry"> children)
170
- listAttributes: { reversed: true },
171
- linkifyUrls: true,
49
+ const bib = new Bibliography({
50
+ data: "./publications.bib", // a file path or BibTeX text
51
+ cslStyle: "apa", // a built-in style, a .csl file, or CSL XML
52
+ customFields: ["status"], // non-standard fields to keep on `entry.custom`
53
+ badges: [badgePresets.doi, badgePresets.arxiv],
172
54
  });
173
- ```
174
-
175
- Entries are formatted in one citeproc pass, so style-dependent state (for example numeric labels in Vancouver) remains correct.
176
-
177
- ### `bib.formatEntry(entry, options?)`
178
-
179
- Render a single entry as an HTML string (no list wrapper). The title link targets the actual CSL title text, regardless of italics.
180
55
 
181
- For citation styles that depend on multi-entry context (numbered labels, ibid behavior, etc.), prefer `formatHtml(...)`.
182
-
183
- Title links are only created for safe URL schemes (`http`, `https`, `mailto`) or normalized DOI/arXiv links.
184
-
185
- ### Badges
186
-
187
- Badges are small inline links appended to each entry. They are configured declaratively:
188
-
189
- ```ts
190
- interface BadgeConfig {
191
- field: string; // BibTeX field name to read
192
- label: string; // display text (e.g. "doi", "arXiv")
193
- url: string; // URL template — $1 is replaced by the field value
194
- match?: RegExp; // optional: validate/transform the field value
195
- className?: string; // CSS class(es) for the <a> element
196
- }
197
- ```
198
-
199
- The `url` template uses `$1` as a placeholder for the field value:
200
-
201
- ```ts
202
- { field: "doi", label: "doi", url: "https://doi.org/$1" }
203
- // doi: "10.1234/example" → href="https://doi.org/10.1234/example"
56
+ const published = bib.sort(bib.filter({ status: "published" }), { by: "date" });
57
+ const html = bib.formatHtml(published);
204
58
  ```
205
59
 
206
- When `match` is provided, the field value is tested against the regex. If it doesn't match, the badge is skipped. If it matches, `$1` in the URL is replaced by the **first capture group** (or the full match if there are no capture groups):
207
-
208
- ```ts
209
- // Strip version suffix from arXiv IDs:
210
- { field: "arxiv", label: "arXiv",
211
- url: "https://arxiv.org/abs/$1",
212
- match: /^(.+?)(?:v\d+)?$/ }
213
- // "2301.00001v3" → capture group "2301.00001" → href=".../2301.00001"
214
-
215
- // Only link if the field looks like a valid identifier:
216
- { field: "zbl", label: "zbMATH",
217
- url: "https://zbmath.org/?q=an:$1",
218
- match: /^(\d+\.\d+)$/ }
219
- // "7654.12345" → match → linked
220
- // "not-a-number" → no match → badge skipped
60
+ ```html
61
+ <ol reversed class="csl-bib-body">
62
+ <li data-csl-entry-id="lindqvist2025" class="csl-entry">Lindqvist, M., &#38; Sato, R. (2025).
63
+ <a href="https://doi.org/10.5555/aia.2025.12">Sobolev estimates for $L^p$ averages</a>.
64
+ <i>Annals of Invented Analysis</i>, <i>12</i>, 1–44.
65
+ <span class="bib-links"><a href="https://doi.org/10.5555/aia.2025.12">DOI</a> <a href="https://arxiv.org/abs/2501.01234">arXiv</a></span></li>
66
+ </ol>
221
67
  ```
222
68
 
223
- Badge labels are HTML-escaped before rendering. Generated badge links are emitted only for `http(s)` and `mailto:` URLs; unsafe schemes are skipped.
69
+ Line breaks added for readability. APA would normally print the DOI after the
70
+ journal; here it is linked as the title link and as a badge instead.
224
71
 
225
- ### `linkifyBareUrls(html)`
72
+ ## Mathematics
226
73
 
227
- Standalone utility: auto-linkify bare `http(s)://` URLs in HTML text nodes that aren't already inside `<a>`, `<script>`, or `<style>` tags. Trailing punctuation is kept outside the link.
74
+ With `preserveMath`, formulas in titles reach your renderer intact:
228
75
 
229
76
  ```ts
230
- import { linkifyBareUrls } from "@behackl/citation-js-extras";
231
-
232
- linkifyBareUrls("See https://example.com.");
233
- // → 'See <a href="https://example.com">https://example.com</a>.'
234
- ```
77
+ import katex from "katex";
235
78
 
236
- ## Custom CSL styles
237
-
238
- Pass a file path or raw XML to `cslStyle`. You can also pass the name of any template already registered with citation-js:
239
-
240
- ```ts
241
- const bib = new Bibliography({
242
- data: bibtex,
243
- cslStyle: "./styles/my-department.csl",
244
- customFields: ["publication-status"],
79
+ const bib = new Bibliography({ data: "./publications.bib", preserveMath: true });
80
+ const html = bib.formatHtml(bib.entries, {
81
+ renderMath: (tex, { display }) => katex.renderToString(tex, { displayMode: display }),
82
+ sanitize: (html) => mySanitizer(html), // optional; runs before the math is inserted
245
83
  });
246
84
  ```
247
85
 
248
- The style is registered with citation-js and used for all formatting calls.
249
-
250
- Raw CSL XML styles are internally registered under deterministic content-hash names to avoid collisions between multiple `Bibliography` instances.
86
+ See [Mathematics and sanitizing](docs/math.md).
251
87
 
252
- ## How it works
88
+ ## Customising
253
89
 
254
- citation-js has a hardcoded list of ~106 BibTeX → CSL field mappings. Any field not in that list is silently dropped. There is no plugin API to extend this mapping.
255
-
256
- This package works around the limitation with a **two-pass parse**:
257
-
258
- 1. `Cite.plugins.input.chainLink(bibData)` — returns raw BibTeX entries with **all** fields preserved (but no CSL conversion).
259
- 2. `new Cite(bibData)` — returns CSL-JSON entries (needed for formatted output via citeproc) but with custom fields stripped.
260
-
261
- The results are merged by citation key, giving you CSL-formatted output **and** access to every custom BibTeX field.
90
+ | To change | Use | Details |
91
+ |---|---|---|
92
+ | which fields you can read | `customFields`; `entry.raw` has every field | [API](docs/api.md#new-bibliographyoptions) |
93
+ | which links follow an entry | `badges`: presets, `$1` templates, or functions | [Links and badges](docs/links.md#badges) |
94
+ | where the title links to | `titleLink` | [Links and badges](docs/links.md#title-links) |
95
+ | attributes of the links (class, `target`, a base path) | `linkAttributes` | [Links and badges](docs/links.md#link-attributes) |
96
+ | how entries read | the CSL style (`cslStyle`) and locale (`lang`) | [Layout](docs/layout.md#styles-and-locales) |
97
+ | the markup around entries | `list`, `listAttributes`, `itemAttributes`, `badgeListClassName` | [Layout](docs/layout.md#markup) |
98
+ | a layout of your own | `links(entry)`, `appendBadges`, `wrapVariable` | [Layout](docs/layout.md#laying-out-entries-yourself) |
99
+ | what reaches the page | `sanitize`, `renderMath` | [Mathematics](docs/math.md) |
100
+ | order and selection | `sort`, `filter`, or array methods on `bib.entries` | [API](docs/api.md#bibsortentries-options) |
101
+
102
+ Formatting options can be given to the constructor as defaults, or to each
103
+ `formatHtml`/`formatEntry` call.
104
+
105
+ ## Documentation
106
+
107
+ - [API reference](docs/api.md)
108
+ - [Links and badges](docs/links.md)
109
+ - [Mathematics and sanitizing](docs/math.md)
110
+ - [Layout: markup, styles, custom layouts](docs/layout.md)
111
+ - [How it works](docs/how-it-works.md)
112
+
113
+ [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md)
262
114
 
263
115
  ## License
264
116
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,43 @@
1
- import type { BibEntry, BibliographyOptions, FormatOptions } from "./types.js";
2
- export type { BadgeConfig, BibEntry, BibliographyOptions, FormatOptions, MathRenderer } from "./types.js";
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";
3
+ /**
4
+ * Ready-made badges for the identifiers of mathematical bibliographies. Their
5
+ * matchers accept the spellings found in real exports (`doi:10…`,
6
+ * `https://doi.org/10…`, `arXiv:2301.00001v2`, `MR1234567`). Use them as they
7
+ * are, or spread one to change a property:
8
+ *
9
+ * ```ts
10
+ * badges: [{ ...badgePresets.doi, className: "badge" }, badgePresets.arxiv]
11
+ * ```
12
+ *
13
+ * The title link uses the same preset for these fields, so both always agree.
14
+ */
15
+ export declare const badgePresets: Readonly<{
16
+ doi: Readonly<{
17
+ field: "doi";
18
+ label: "DOI";
19
+ url: "https://doi.org/$1";
20
+ match: RegExp;
21
+ }>;
22
+ arxiv: Readonly<{
23
+ field: "arxiv";
24
+ label: "arXiv";
25
+ url: "https://arxiv.org/abs/$1";
26
+ match: RegExp;
27
+ }>;
28
+ mrnumber: Readonly<{
29
+ field: "mrnumber";
30
+ label: "MR";
31
+ url: "https://mathscinet.ams.org/mathscinet-getitem?mr=$1";
32
+ match: RegExp;
33
+ }>;
34
+ zbl: Readonly<{
35
+ field: "zbl";
36
+ label: "zbMATH";
37
+ url: "https://zbmath.org/?q=an:$1";
38
+ match: RegExp;
39
+ }>;
40
+ }>;
3
41
  export declare class Bibliography {
4
42
  /** The CSL template name to use for formatting. */
5
43
  readonly templateName: string;
@@ -7,6 +45,9 @@ export declare class Bibliography {
7
45
  readonly entries: BibEntry[];
8
46
  private readonly customFieldNames;
9
47
  private readonly math?;
48
+ /** Formatting defaults from the constructor; each call may override them. */
49
+ private readonly formatDefaults;
50
+ private rendering?;
10
51
  constructor(options: BibliographyOptions);
11
52
  /**
12
53
  * Return entries whose custom fields match **all** given key/value pairs.
@@ -16,31 +57,63 @@ export declare class Bibliography {
16
57
  */
17
58
  filter(criteria: Record<string, string>): BibEntry[];
18
59
  /**
19
- * Return a sorted **copy** of the given entries.
60
+ * Return a sorted **copy** of the given entries. Ties keep their input order.
20
61
  *
21
62
  * @param entries - entries to sort (not mutated)
22
- * @param by - `'year'` (default) or a custom field name
63
+ * @param by - `'year'` (default), `'date'` (year, then month, then day; a
64
+ * missing part counts as 0), or a custom field name
23
65
  * @param order - `'desc'` (default) or `'asc'`
24
66
  */
25
67
  sort(entries: BibEntry[], { by, order }?: {
26
- by?: string;
68
+ by?: "year" | "date" | (string & {});
27
69
  order?: "asc" | "desc";
28
70
  }): BibEntry[];
29
71
  /**
30
72
  * Format a single entry as an HTML string (no wrapper element).
31
- * Applies title linking and badge injection.
73
+ * Applies title linking, badge injection and URL linkification.
32
74
  */
33
75
  formatEntry(entry: BibEntry, options?: FormatOptions): string;
34
76
  /**
35
77
  * Format a list of entries as a complete HTML bibliography.
36
78
  */
37
79
  formatHtml(entries: BibEntry[], options?: FormatOptions): string;
80
+ /**
81
+ * The links an entry gets with these options: its title link and badges.
82
+ * The same resolution the formatted HTML uses, so the two always agree; for
83
+ * rendering badges yourself, pair it with `appendBadges: false`.
84
+ */
85
+ links(entry: BibEntry, options?: FormatOptions): EntryLinks;
86
+ /** Per-call options win over the defaults given to the constructor. */
87
+ private mergeOptions;
88
+ /**
89
+ * Entry HTML without wrapper, shared by `formatEntry` and `formatHtml` so
90
+ * both honour the same options. Links are resolved first: they decide what
91
+ * the style may print and what is added afterwards.
92
+ */
93
+ private renderItems;
94
+ /**
95
+ * Called by citeproc for every variable it renders. Everything the library
96
+ * adds to the citation text happens here, one variable at a time, instead of
97
+ * by searching the finished HTML:
98
+ *
99
+ * - the title link, around the exact output of the title variable, so no
100
+ * quotes, markup or capitalisation the style applies can hide the title;
101
+ * - bare URLs in a variable (e.g. in a `note`) become links. Formulas are
102
+ * still placeholders here, so MathML's xmlns URL is never linked;
103
+ * - the consumer's `wrapVariable`, outermost.
104
+ */
105
+ private wrapVariable;
106
+ /** Sanitize while formulas are placeholders, then insert rendered math. */
107
+ private finish;
108
+ private resolveLinks;
38
109
  private registerStyle;
110
+ /**
111
+ * Render entries with citeproc, as `cite.format("bibliography")` does, but
112
+ * through an engine that has our variable wrapper: citeproc only accepts it
113
+ * when the engine is created. Styles and locales come from citation-js's
114
+ * registries, and the data is prepared the same way.
115
+ */
39
116
  private renderCslEntries;
40
- private decorateEntryHtml;
41
- private resolveTitleLink;
42
- private linkTitle;
43
- private renderBadges;
44
117
  }
45
118
  /**
46
119
  * Auto-linkify bare `http(s)://` URLs in HTML that aren't already inside