@citestyle/registry 1.0.0 → 1.1.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/registry.js +85 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citestyle/registry",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Citation registry: year-suffixes, numbering, sorting, disambiguation (~5-8KB)",
5
5
  "author": "Uniweb",
6
6
  "homepage": "https://github.com/uniweb/csl/tree/main/packages/registry",
@@ -30,8 +30,8 @@
30
30
  "README.md"
31
31
  ],
32
32
  "dependencies": {
33
- "@citestyle/core": "1.0.0",
34
- "@citestyle/types": "1.0.0"
33
+ "@citestyle/core": "1.0.1",
34
+ "@citestyle/types": "1.0.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "vitest": "^3.1.1"
package/src/registry.js CHANGED
@@ -87,8 +87,22 @@ export function createRegistry(style, options = {}) {
87
87
  * Format a citation cluster.
88
88
  * Resolves item IDs to items and delegates to the compiled style's citation().
89
89
  *
90
- * @param {Array<{id: string, item?: object, locator?: string, label?: string, prefix?: string, suffix?: string}>} cites
91
- * @param {object} [ctx] - Format context
90
+ * **Author suppression** (CSL `suppress-author` mode) is implemented as a
91
+ * post-render strip: render normally, then remove the
92
+ * `<span class="csl-author">…</span>` wrappers and any orphaned
93
+ * surrounding punctuation (leading "Smith, " or trailing space). The
94
+ * compiler doesn't emit a per-mode citation branch yet — the standard
95
+ * CSL way — so this layer fills the gap. Two ways to trigger it:
96
+ *
97
+ * - `ctx.mode === 'suppress-author'` (whole cluster, CSL-canonical)
98
+ * - `cite.suppressAuthor === true` on every cite in a single-cite cluster
99
+ *
100
+ * Mixed-suppression clusters (some cites suppressed, others not) aren't
101
+ * supported here — the cluster renderer would need per-cite re-rendering,
102
+ * which is more rework than the current consumer demands.
103
+ *
104
+ * @param {Array<{id: string, item?: object, locator?: string, label?: string, prefix?: string, suffix?: string, suppressAuthor?: boolean}>} cites
105
+ * @param {object} [ctx] - Format context. `mode: 'suppress-author'` strips the author from the rendered cluster.
92
106
  * @returns {import('@citestyle/types').FormattedCitation}
93
107
  */
94
108
  function cite(cites, ctx = {}) {
@@ -114,6 +128,7 @@ export function createRegistry(style, options = {}) {
114
128
  if (c.label != null) citeObj.label = c.label
115
129
  if (c.prefix != null) citeObj.prefix = c.prefix
116
130
  if (c.suffix != null) citeObj.suffix = c.suffix
131
+ if (c.suppressAuthor) citeObj.suppressAuthor = true
117
132
 
118
133
  // Attach per-item disambiguation overrides
119
134
  const disambig = disambigState.get(String(item.id))
@@ -122,16 +137,26 @@ export function createRegistry(style, options = {}) {
122
137
  return citeObj
123
138
  })
124
139
 
140
+ // Decide whether to suppress the author across the whole cluster.
141
+ const allCitesSuppress = resolvedCites.length > 0 &&
142
+ resolvedCites.every(c => c.suppressAuthor)
143
+ const suppressAuthor = ctx.mode === 'suppress-author' || allCitesSuppress
144
+
125
145
  // Apply cite collapsing
126
146
  const collapse = style.meta?.collapse
147
+ let result
127
148
  if (collapse === 'citation-number' && resolvedCites.length > 1) {
128
- return collapseNumericCitation(resolvedCites, ctx)
129
- }
130
- if ((collapse === 'year' || collapse === 'year-suffix' || collapse === 'year-suffix-ranged') && resolvedCites.length > 1) {
131
- return collapseAuthorDateCitation(resolvedCites, ctx, collapse)
149
+ result = collapseNumericCitation(resolvedCites, ctx)
150
+ } else if ((collapse === 'year' || collapse === 'year-suffix' || collapse === 'year-suffix-ranged') && resolvedCites.length > 1) {
151
+ result = collapseAuthorDateCitation(resolvedCites, ctx, collapse)
152
+ } else {
153
+ result = style.citation(resolvedCites, ctx)
132
154
  }
133
155
 
134
- return style.citation(resolvedCites, ctx)
156
+ if (suppressAuthor) {
157
+ result = stripAuthor(result)
158
+ }
159
+ return result
135
160
  }
136
161
 
137
162
  // ── Numeric cite collapsing ──────────────────────────────────────────────
@@ -565,6 +590,59 @@ export function createRegistry(style, options = {}) {
565
590
 
566
591
  // ── Helper functions ──────────────────────────────────────────────────────
567
592
 
593
+ /**
594
+ * Strip the author from a formatted citation result.
595
+ *
596
+ * Citation styles that opt into `<span class="csl-author">` wrappers
597
+ * (every shipped style does today) can have their author portion
598
+ * removed by string-substitution: drop the wrapper and clean up
599
+ * orphaned surrounding punctuation. The result is the
600
+ * "(2024, p. 42)" / "(2024)" form authors reach for when the author's
601
+ * name already appears in the running prose.
602
+ *
603
+ * Cleanup rules:
604
+ * - "Author, " (author span followed by comma + space) -> ""
605
+ * - "Author " (author span followed by space) -> ""
606
+ * - bare "Author" with no trailing punctuation -> ""
607
+ *
608
+ * The text form is rebuilt from the stripped HTML rather than parallel-
609
+ * stripped — keeps the two views in sync without duplicating the
610
+ * regex set.
611
+ */
612
+ function stripAuthor(result) {
613
+ if (!result || typeof result.html !== 'string') return result
614
+
615
+ let html = result.html
616
+ // Remove every csl-author span plus a trailing ", " or " " if present.
617
+ // Multi-cite clusters can have more than one — flag g.
618
+ html = html.replace(
619
+ /<span class="csl-author"[^>]*>[\s\S]*?<\/span>(,\s*|\s+)?/g,
620
+ '',
621
+ )
622
+
623
+ // Tidy any leftover doubled spaces or stray opening punctuation.
624
+ html = html
625
+ .replace(/\(\s+/g, '(')
626
+ .replace(/\s{2,}/g, ' ')
627
+ .replace(/\s+,/g, ',')
628
+ .replace(/\(,\s*/g, '(')
629
+
630
+ const text = htmlToPlainText(html)
631
+ return { text, html }
632
+ }
633
+
634
+ /** Strip HTML tags and decode the entity subset citestyle emits. */
635
+ function htmlToPlainText(html) {
636
+ if (!html) return ''
637
+ return String(html)
638
+ .replace(/<[^>]+>/g, '')
639
+ .replace(/&amp;/g, '&')
640
+ .replace(/&lt;/g, '<')
641
+ .replace(/&gt;/g, '>')
642
+ .replace(/&quot;/g, '"')
643
+ .replace(/&#39;/g, "'")
644
+ }
645
+
568
646
  /**
569
647
  * Apply subsequent-author-substitute to formatted bibliography entries.
570
648
  */