@behackl/citation-js-extras 0.2.0 → 0.2.1

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
@@ -74,7 +74,9 @@ const rendered = bib.formatHtml(bib.entries, {
74
74
  `myMathRenderer` is an application-supplied function returning **trusted HTML**
75
75
  (e.g. MathJax SVG or KaTeX output). No math renderer is bundled. Configure it for
76
76
  untrusted TeX as appropriate; the callback output is inserted verbatim, not
77
- sanitized. Exceptions propagate to the caller. `renderMath` also works with
77
+ sanitized. Treat renderer output as untrusted HTML unless you control both the
78
+ renderer configuration and the TeX: substitute it only after the surrounding
79
+ HTML has been sanitized (see below). Exceptions propagate to the caller. `renderMath` also works with
78
80
  `formatEntry`; it has no effect unless `preserveMath` was enabled.
79
81
 
80
82
  Supported delimiters are `$…$`, `$$…$$`, `\(…\)`, and `\[…\]`.
@@ -100,6 +102,46 @@ Restoration runs after title linking, badges, and URL linkification. Neither
100
102
  renderer output nor restored TeX is fed back through these HTML helpers. Disabling
101
103
  preservation (the default) retains the previous behavior.
102
104
 
105
+ If `renderMath` throws, the error names the entry it came from
106
+ (`entry doe:2024: Undefined control sequence: \\kk`), so a broken formula can be
107
+ traced to a line in the `.bib` file.
108
+
109
+ ### Sanitizing formatted output
110
+
111
+ Rendered mathematics does **not** survive an HTML sanitizer with default
112
+ settings: KaTeX and MathJax output relies on `class`, `style` and MathML
113
+ elements that `rehype-sanitize` strips, leaving empty boxes. Sanitize the
114
+ citation HTML *first*, then insert the trusted renderer output:
115
+
116
+ ```js
117
+ const math = [];
118
+ const html = bib.formatHtml(entries, {
119
+ // Stand in for each formula while the HTML is sanitized ...
120
+ renderMath: (tex, { display }) => {
121
+ math.push(renderWithKatex(tex, display));
122
+ return `mathplaceholder${math.length - 1}end`;
123
+ },
124
+ });
125
+ const safe = sanitize(html);
126
+ // ... then substitute the rendered markup back in.
127
+ const final = safe.replace(/mathplaceholder(\d+)end/g, (_, i) => math[Number(i)]);
128
+ ```
129
+
130
+ Two related gotchas when using `rehype-sanitize`:
131
+
132
+ - its `defaultSchema` allows `className` only with an explicit value list *per
133
+ tag*, and that per-tag rule overrides anything added under `'*'`; to keep
134
+ badge or list classes, merge your class names into the existing entry for
135
+ that tag
136
+ - pick a placeholder that cannot occur in your bibliography (append characters
137
+ until it is absent from the source)
138
+
139
+ This recipe deliberately reinserts renderer output *after* sanitizing, so it is
140
+ only as safe as the renderer: a renderer configured to emit arbitrary markup
141
+ (for instance KaTeX with `trust: true`) can reintroduce `<script>`. Restrict the
142
+ renderer configuration, or sanitize its output separately with a schema that
143
+ keeps the elements and attributes mathematics needs.
144
+
103
145
  ## Development checks
104
146
 
105
147
  ```sh
@@ -125,6 +167,13 @@ Use Node 24 LTS for these development checks, matching CI and publishing.
125
167
  | `cslStyle` | `string?` | CSL style — a registered template name, raw XML, or a file path. Defaults to `'apa'`. |
126
168
  | `customFields` | `string[]?` | BibTeX field names to preserve. These appear on each entry under `.custom`. |
127
169
  | `preserveMath` | `boolean?` | Preserve math in display-text fields through CSL formatting. Defaults to `false`. |
170
+ | `titleLink` | `string[]?` | Default fields used for the title link, checked in order. Defaults to `['url', 'doi', 'arxiv']`. |
171
+ | `badges` | `BadgeConfig[]?` | Default badge configuration, used by every `formatHtml`/`formatEntry` call. No badges are rendered unless set. |
172
+ | `linkifyUrls` | `boolean?` | Default for URL linkification. Defaults to `true`. |
173
+
174
+ `titleLink`, `badges` and `linkifyUrls` are usually the same for every call, so
175
+ they can be set once here; a value passed to `formatHtml`/`formatEntry` overrides
176
+ the default for that call.
128
177
 
129
178
  ### `bib.entries`
130
179
 
@@ -174,10 +223,14 @@ bib.formatHtml(entries, {
174
223
 
175
224
  Entries are formatted in one citeproc pass, so style-dependent state (for example numeric labels in Vancouver) remains correct.
176
225
 
226
+ An empty entry list returns an empty string, not an empty wrapper element.
227
+
177
228
  ### `bib.formatEntry(entry, options?)`
178
229
 
179
230
  Render a single entry as an HTML string (no list wrapper). The title link targets the actual CSL title text, regardless of italics.
180
231
 
232
+ Accepts the same title linking, badge and `linkifyUrls` options as `formatHtml` and applies them identically (`list`/`listAttributes` are ignored — there is no wrapper). Bare URLs are linkified unless `linkifyUrls: false` is set, either per call or on the constructor.
233
+
181
234
  For citation styles that depend on multi-entry context (numbered labels, ibid behavior, etc.), prefer `formatHtml(...)`.
182
235
 
183
236
  Title links are only created for safe URL schemes (`http`, `https`, `mailto`) or normalized DOI/arXiv links.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BibEntry, BibliographyOptions, FormatOptions } from "./types.js";
2
- export type { BadgeConfig, BibEntry, BibliographyOptions, FormatOptions, MathRenderer } from "./types.js";
2
+ export type { BadgeConfig, BibEntry, BibliographyOptions, FormatDefaults, FormatOptions, MathRenderer, } from "./types.js";
3
3
  export declare class Bibliography {
4
4
  /** The CSL template name to use for formatting. */
5
5
  readonly templateName: string;
@@ -7,6 +7,8 @@ export declare class Bibliography {
7
7
  readonly entries: BibEntry[];
8
8
  private readonly customFieldNames;
9
9
  private readonly math?;
10
+ /** Formatting defaults from the constructor; each call may override them. */
11
+ private readonly formatDefaults;
10
12
  constructor(options: BibliographyOptions);
11
13
  /**
12
14
  * Return entries whose custom fields match **all** given key/value pairs.
@@ -28,9 +30,26 @@ export declare class Bibliography {
28
30
  }): BibEntry[];
29
31
  /**
30
32
  * Format a single entry as an HTML string (no wrapper element).
31
- * Applies title linking and badge injection.
33
+ * Applies title linking, badge injection and URL linkification.
32
34
  */
33
35
  formatEntry(entry: BibEntry, options?: FormatOptions): string;
36
+ /**
37
+ * Decorate, linkify and restore math for one rendered entry. Shared by
38
+ * `formatEntry` and `formatHtml` so both honour the same options.
39
+ *
40
+ * Linkification runs before math restoration: rendered MathML carries an
41
+ * xmlns URL that must not be turned into a link. Math is restored per entry
42
+ * so a failing formula can be reported with its citation key.
43
+ */
44
+ private buildEntryHtml;
45
+ /** Per-call options win over the defaults given to the constructor. */
46
+ private mergeOptions;
47
+ /**
48
+ * Restore protected math, naming the entry when a renderer rejects a formula.
49
+ * Without the citation key, `renderMath` failures are near-impossible to
50
+ * trace back to a line in the .bib file.
51
+ */
52
+ private restoreMath;
34
53
  /**
35
54
  * Format a list of entries as a complete HTML bibliography.
36
55
  */
package/dist/index.js CHANGED
@@ -11,9 +11,16 @@ export class Bibliography {
11
11
  entries;
12
12
  customFieldNames;
13
13
  math;
14
+ /** Formatting defaults from the constructor; each call may override them. */
15
+ formatDefaults;
14
16
  constructor(options) {
15
17
  const bibData = maybeReadFile(options.data);
16
18
  this.customFieldNames = options.customFields ?? [];
19
+ this.formatDefaults = {
20
+ titleLink: options.titleLink,
21
+ badges: options.badges,
22
+ linkifyUrls: options.linkifyUrls,
23
+ };
17
24
  // Register CSL style
18
25
  this.templateName = this.registerStyle(options.cslStyle);
19
26
  // Two-pass parse: raw (preserves all fields) + CSL (for formatting)
@@ -81,14 +88,53 @@ export class Bibliography {
81
88
  // -------------------------------------------------------------------------
82
89
  /**
83
90
  * Format a single entry as an HTML string (no wrapper element).
84
- * Applies title linking and badge injection.
91
+ * Applies title linking, badge injection and URL linkification.
85
92
  */
86
93
  formatEntry(entry, options = {}) {
94
+ const merged = this.mergeOptions(options);
87
95
  const rendered = this.renderCslEntries([entry]);
88
96
  const raw = rendered[0]?.[1] ?? "";
89
97
  const innerHtml = unwrapCslEntry(raw) ?? raw.trim();
90
- const decorated = this.decorateEntryHtml(entry, innerHtml, options);
91
- return this.math ? this.math.restore(decorated, options.renderMath) : decorated;
98
+ return this.buildEntryHtml(entry, innerHtml, merged);
99
+ }
100
+ /**
101
+ * Decorate, linkify and restore math for one rendered entry. Shared by
102
+ * `formatEntry` and `formatHtml` so both honour the same options.
103
+ *
104
+ * Linkification runs before math restoration: rendered MathML carries an
105
+ * xmlns URL that must not be turned into a link. Math is restored per entry
106
+ * so a failing formula can be reported with its citation key.
107
+ */
108
+ buildEntryHtml(entry, innerRaw, merged) {
109
+ let inner = this.decorateEntryHtml(entry, innerRaw, merged);
110
+ if (merged.linkifyUrls !== false)
111
+ inner = linkifyBareUrls(inner);
112
+ return this.restoreMath(inner, merged, entry);
113
+ }
114
+ /** Per-call options win over the defaults given to the constructor. */
115
+ mergeOptions(options) {
116
+ return {
117
+ ...options,
118
+ titleLink: options.titleLink ?? this.formatDefaults.titleLink,
119
+ badges: options.badges ?? this.formatDefaults.badges,
120
+ linkifyUrls: options.linkifyUrls ?? this.formatDefaults.linkifyUrls,
121
+ };
122
+ }
123
+ /**
124
+ * Restore protected math, naming the entry when a renderer rejects a formula.
125
+ * Without the citation key, `renderMath` failures are near-impossible to
126
+ * trace back to a line in the .bib file.
127
+ */
128
+ restoreMath(html, options, entry) {
129
+ if (!this.math)
130
+ return html;
131
+ try {
132
+ return this.math.restore(html, options.renderMath);
133
+ }
134
+ catch (error) {
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ throw new Error(entry ? `entry ${entry.key}: ${message}` : message, { cause: error });
137
+ }
92
138
  }
93
139
  /**
94
140
  * Format a list of entries as a complete HTML bibliography.
@@ -96,8 +142,9 @@ export class Bibliography {
96
142
  formatHtml(entries, options = {}) {
97
143
  if (entries.length === 0)
98
144
  return "";
99
- const tag = options.list ?? "ol";
100
- const attrs = options.listAttributes ?? (tag === "ol" ? { reversed: true } : {});
145
+ const merged = this.mergeOptions(options);
146
+ const tag = merged.list ?? "ol";
147
+ const attrs = merged.listAttributes ?? (tag === "ol" ? { reversed: true } : {});
101
148
  const attrStr = renderAttributes(attrs);
102
149
  // Render all entries in one citeproc run so style-dependent numbering/state
103
150
  // (e.g. vancouver left-margin labels) remains correct.
@@ -111,14 +158,10 @@ export class Bibliography {
111
158
  ?? rendered[index]?.[1]
112
159
  ?? "";
113
160
  const innerRaw = unwrapCslEntry(raw) ?? raw.trim();
114
- const inner = this.decorateEntryHtml(entry, innerRaw, options);
161
+ const inner = this.buildEntryHtml(entry, innerRaw, merged);
115
162
  return `<${itemTag} data-csl-entry-id="${escapeAttr(entry.key)}" class="csl-entry">${inner}</${itemTag}>`;
116
163
  });
117
- let html = `<${tag}${attrStr} class="csl-bib-body">\n${items.join("\n")}\n</${tag}>`;
118
- if (options.linkifyUrls !== false) {
119
- html = linkifyBareUrls(html);
120
- }
121
- return this.math ? this.math.restore(html, options.renderMath) : html;
164
+ return `<${tag}${attrStr} class="csl-bib-body">\n${items.join("\n")}\n</${tag}>`;
122
165
  }
123
166
  // -------------------------------------------------------------------------
124
167
  // Private helpers
package/dist/types.d.ts CHANGED
@@ -45,14 +45,20 @@ export interface BadgeConfig {
45
45
  export type MathRenderer = (tex: string, context: {
46
46
  display: boolean;
47
47
  }) => string;
48
- /** Options passed to {@link Bibliography.formatHtml}. */
48
+ /**
49
+ * Options passed to {@link Bibliography.formatHtml} or
50
+ * {@link Bibliography.formatEntry}. `list` and `listAttributes` only apply to
51
+ * `formatHtml`, which is the only method emitting a wrapper element.
52
+ */
49
53
  export interface FormatOptions {
50
54
  /** Render protected math as trusted HTML; requires preserveMath at construction.
51
55
  * Without this callback, escaped original TeX delimiters are restored.
56
+ * Not settable on the constructor.
52
57
  */
53
58
  renderMath?: MathRenderer;
54
59
  /**
55
60
  * Fields to use for linking the title, checked in order.
61
+ * Overrides the constructor default, if any.
56
62
  * A `doi` value is expanded to `https://doi.org/<value>`, an `arxiv`
57
63
  * value to `https://arxiv.org/abs/<value>`, and direct values are only
58
64
  * accepted when they use `http`, `https`, or `mailto`.
@@ -60,7 +66,12 @@ export interface FormatOptions {
60
66
  * @default ['url', 'doi', 'arxiv']
61
67
  */
62
68
  titleLink?: string[];
63
- /** Badge configurations to append to each entry. */
69
+ /**
70
+ * Badge configurations to append to each entry.
71
+ * Overrides the constructor default, if any.
72
+ *
73
+ * @default [] (no badges)
74
+ */
64
75
  badges?: BadgeConfig[];
65
76
  /**
66
77
  * Wrapper list element.
@@ -77,6 +88,7 @@ export interface FormatOptions {
77
88
  /**
78
89
  * Auto-linkify bare `http(s)://` URLs in the rendered output that aren't
79
90
  * already inside `<a>`, `<script>`, or `<style>` tags.
91
+ * Overrides the constructor default, if any.
80
92
  *
81
93
  * @default true
82
94
  */
@@ -98,8 +110,13 @@ export interface BibEntry {
98
110
  /** The full raw BibTeX properties (unfiltered). */
99
111
  raw: Record<string, any>;
100
112
  }
113
+ /**
114
+ * Formatting options that can be set once on the constructor and overridden
115
+ * by the options of an individual `formatHtml`/`formatEntry` call.
116
+ */
117
+ export type FormatDefaults = Pick<FormatOptions, "titleLink" | "badges" | "linkifyUrls">;
101
118
  /** Options for constructing a {@link Bibliography}. */
102
- export interface BibliographyOptions {
119
+ export interface BibliographyOptions extends FormatDefaults {
103
120
  /** Preserve math in supported display-text fields before CSL conversion.
104
121
  * Opt-in; entries' CSL text contains internal placeholders until HTML formatting.
105
122
  * Raw/custom fields remain unchanged. Unclosed or empty math throws.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@behackl/citation-js-extras",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",