@cyanheads/pubmed-mcp-server 2.10.21 → 2.10.22

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 (86) hide show
  1. package/AGENTS.md +2 -2
  2. package/CLAUDE.md +2 -2
  3. package/Dockerfile +3 -3
  4. package/README.md +8 -6
  5. package/changelog/2.10.x/2.10.22.md +44 -0
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/mcp-server/tools/definitions/_budget.d.ts +35 -6
  9. package/dist/mcp-server/tools/definitions/_budget.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/_budget.js +26 -3
  11. package/dist/mcp-server/tools/definitions/_budget.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/_notices.d.ts +33 -0
  13. package/dist/mcp-server/tools/definitions/_notices.d.ts.map +1 -0
  14. package/dist/mcp-server/tools/definitions/_notices.js +35 -0
  15. package/dist/mcp-server/tools/definitions/_notices.js.map +1 -0
  16. package/dist/mcp-server/tools/definitions/_schemas.d.ts +14 -0
  17. package/dist/mcp-server/tools/definitions/_schemas.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/_schemas.js +9 -0
  19. package/dist/mcp-server/tools/definitions/_schemas.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts +3 -0
  21. package/dist/mcp-server/tools/definitions/fetch-articles.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/fetch-articles.tool.js +28 -15
  23. package/dist/mcp-server/tools/definitions/fetch-articles.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts +1 -0
  25. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js +107 -48
  27. package/dist/mcp-server/tools/definitions/fetch-fulltext.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/definitions/format-citations.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/format-citations.tool.js +3 -16
  30. package/dist/mcp-server/tools/definitions/format-citations.tool.js.map +1 -1
  31. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts +1 -1
  32. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js +66 -76
  34. package/dist/mcp-server/tools/definitions/lookup-mesh.tool.js.map +1 -1
  35. package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.d.ts +6 -5
  36. package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.js +11 -17
  38. package/dist/mcp-server/tools/definitions/pubmed-europepmc-fetch.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts +4 -4
  40. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.d.ts.map +1 -1
  41. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js +9 -8
  42. package/dist/mcp-server/tools/definitions/pubmed-europepmc-search.tool.js.map +1 -1
  43. package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts +4 -4
  44. package/dist/mcp-server/tools/definitions/spell-check.tool.d.ts.map +1 -1
  45. package/dist/mcp-server/tools/definitions/spell-check.tool.js +12 -8
  46. package/dist/mcp-server/tools/definitions/spell-check.tool.js.map +1 -1
  47. package/dist/services/error-contracts.d.ts +1 -1
  48. package/dist/services/error-contracts.d.ts.map +1 -1
  49. package/dist/services/error-contracts.js +1 -1
  50. package/dist/services/error-contracts.js.map +1 -1
  51. package/dist/services/europe-pmc/europe-pmc-service.d.ts +31 -11
  52. package/dist/services/europe-pmc/europe-pmc-service.d.ts.map +1 -1
  53. package/dist/services/europe-pmc/europe-pmc-service.js +122 -47
  54. package/dist/services/europe-pmc/europe-pmc-service.js.map +1 -1
  55. package/dist/services/europe-pmc/types.d.ts +5 -1
  56. package/dist/services/europe-pmc/types.d.ts.map +1 -1
  57. package/dist/services/ncbi/formatting/citation-formatter.d.ts.map +1 -1
  58. package/dist/services/ncbi/formatting/citation-formatter.js +204 -110
  59. package/dist/services/ncbi/formatting/citation-formatter.js.map +1 -1
  60. package/dist/services/ncbi/ncbi-service.d.ts +22 -0
  61. package/dist/services/ncbi/ncbi-service.d.ts.map +1 -1
  62. package/dist/services/ncbi/ncbi-service.js +113 -17
  63. package/dist/services/ncbi/ncbi-service.js.map +1 -1
  64. package/dist/services/ncbi/parsing/article-parser.d.ts.map +1 -1
  65. package/dist/services/ncbi/parsing/article-parser.js +11 -4
  66. package/dist/services/ncbi/parsing/article-parser.js.map +1 -1
  67. package/dist/services/ncbi/parsing/pmc-article-parser.d.ts +5 -1
  68. package/dist/services/ncbi/parsing/pmc-article-parser.d.ts.map +1 -1
  69. package/dist/services/ncbi/parsing/pmc-article-parser.js +85 -31
  70. package/dist/services/ncbi/parsing/pmc-article-parser.js.map +1 -1
  71. package/dist/services/ncbi/parsing/pmc-xml-helpers.d.ts +13 -0
  72. package/dist/services/ncbi/parsing/pmc-xml-helpers.d.ts.map +1 -1
  73. package/dist/services/ncbi/parsing/pmc-xml-helpers.js +108 -10
  74. package/dist/services/ncbi/parsing/pmc-xml-helpers.js.map +1 -1
  75. package/dist/services/ncbi/parsing/text-helpers.d.ts +10 -0
  76. package/dist/services/ncbi/parsing/text-helpers.d.ts.map +1 -1
  77. package/dist/services/ncbi/parsing/text-helpers.js +19 -0
  78. package/dist/services/ncbi/parsing/text-helpers.js.map +1 -1
  79. package/dist/services/ncbi/response-handler.d.ts +0 -11
  80. package/dist/services/ncbi/response-handler.d.ts.map +1 -1
  81. package/dist/services/ncbi/response-handler.js +21 -33
  82. package/dist/services/ncbi/response-handler.js.map +1 -1
  83. package/dist/services/ncbi/types.d.ts +30 -30
  84. package/dist/services/ncbi/types.d.ts.map +1 -1
  85. package/package.json +5 -5
  86. package/server.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/europe-pmc/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/europe-pmc/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"citation-formatter.d.ts","sourceRoot":"","sources":["../../../../src/services/ncbi/formatting/citation-formatter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,aAAa,EAIb,wBAAwB,EAEzB,MAAM,aAAa,CAAC;AAErB,yCAAyC;AACzC,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,YAChC,cAAc,EACd,2BAA2B,EAC3B,uBAAuB,EACvB,WAAW,EACX,2BAA2B,CACnB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG,wBAAwB,GAAG;IAAE,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAQxF,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,EAAE,CAExE;AAonBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAkExD;AAwDD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAwDxD;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAkH3D;AAMD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAwHxD;AAsGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CA2D9D;AAMD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAanF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,aAAa,EAAE,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB"}
1
+ {"version":3,"file":"citation-formatter.d.ts","sourceRoot":"","sources":["../../../../src/services/ncbi/formatting/citation-formatter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,aAAa,EAIb,wBAAwB,EAEzB,MAAM,aAAa,CAAC;AAErB,yCAAyC;AACzC,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,YAChC,cAAc,EACd,2BAA2B,EAC3B,uBAAuB,EACvB,WAAW,EACX,2BAA2B,CACnB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG,wBAAwB,GAAG;IAAE,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAQxF,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,EAAE,CAExE;AAguBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAmExD;AAkDD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAyDxD;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqH3D;AAmBD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAuGxD;AAqGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CA2D9D;AAMD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAanF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,aAAa,EAAE,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB"}
@@ -191,24 +191,6 @@ function stripTrailingPeriod(text) {
191
191
  function terminate(text) {
192
192
  return text.endsWith('.') ? text : `${text}.`;
193
193
  }
194
- /**
195
- * Split a pages string like "45-67" into start and end components.
196
- * Handles en-dashes, em-dashes, and hyphens. Expands PubMed's truncated-end
197
- * convention (e.g., "737-8" → { start: "737", end: "738" }, "1639-41" →
198
- * "1639"/"1641") so downstream RIS/BibTeX consumers see absolute page numbers.
199
- */
200
- function splitPages(pages) {
201
- if (!pages)
202
- return {};
203
- const parts = pages.split(/[-\u2013\u2014]/).map((p) => p.trim());
204
- let [start, end] = parts;
205
- if (start && end && end.length < start.length) {
206
- end = start.slice(0, start.length - end.length) + end;
207
- }
208
- if (start && end)
209
- return { start, end };
210
- return start ? { start } : {};
211
- }
212
194
  /**
213
195
  * The electronic article locator that Vancouver, BibTeX, and RIS print in place
214
196
  * of a page range, or undefined.
@@ -230,14 +212,31 @@ function articleLocator(journal) {
230
212
  * `662953`, Springer `42`). A page that matches no locator stays a page, though
231
213
  * some are article numbers the record cannot identify (an Elsevier `105471`
232
214
  * beside an `S…` pii looks exactly like a Lancet page `2413`). (#217)
215
+ *
216
+ * A dashless `MedlinePgn` also matches the locator with its hyphens removed:
217
+ * American Society for Microbiology journals deposit `e0364621` beside pii
218
+ * `e03646-21`. The locator's form is the one cited, as the publisher numbers
219
+ * the article. Pagination holding a dash is a range and never matches. (#231)
233
220
  */
234
221
  function articleNumber(journal) {
235
- if (!journal?.elocationId)
222
+ const locator = journal?.elocationId;
223
+ if (!locator)
236
224
  return;
237
- return !journal.pages || journal.pages === journal.elocationId ? journal.elocationId : undefined;
225
+ const { pages } = journal;
226
+ if (!pages || pages === locator)
227
+ return locator;
228
+ return !PAGE_DASH.test(pages) && pages === locator.replaceAll('-', '') ? locator : undefined;
238
229
  }
239
- /** One range inside NLM's pagination: a start, a dash, an end. */
240
- const PAGE_RANGE = /([^\s,;\u{2013}\u{2014}-]+)\s*[-\u{2013}\u{2014}]\s*([^\s,;\u{2013}\u{2014}-]+)/gu;
230
+ /** A dash NLM's pagination puts between the ends of a range. */
231
+ const PAGE_DASH = /[-\u{2013}\u{2014}]/u;
232
+ /**
233
+ * One page or range inside NLM's pagination: a start, then optionally a dash
234
+ * and an end. An item starts only after a separator, so a long run without a
235
+ * dash is read once rather than retried from each of its characters.
236
+ */
237
+ const PAGE_ITEM = /(?<![^\s,;\u{2013}\u{2014}-])([^\s,;\u{2013}\u{2014}-]+)(?:\s*[-\u{2013}\u{2014}]\s*([^\s,;\u{2013}\u{2014}-]+))?/gu;
238
+ /** The digits a page ends on, matched from the start of their run only. */
239
+ const TRAILING_DIGITS = /(?<!\d)\d+$/;
241
240
  /**
242
241
  * The end of a range as NLM's elision means it. An end made of digits is
243
242
  * written against the start's digits and takes the start's prefix (`637-41` →
@@ -245,21 +244,48 @@ const PAGE_RANGE = /([^\s,;\u{2013}\u{2014}-]+)\s*[-\u{2013}\u{2014}]\s*([^\s,;\
245
244
  * whole (`878.e8`, `e65`, `xv`).
246
245
  */
247
246
  function fullPageEnd(start, end) {
248
- const startParts = /^(.*?)(\d+)$/.exec(start);
249
- if (!startParts || !/^\d+$/.test(end))
247
+ const digits = /^\d+$/.test(end) ? TRAILING_DIGITS.exec(start) : null;
248
+ if (!digits)
250
249
  return end;
251
- const [, prefix = '', digits = ''] = startParts;
252
- const elided = end.length < digits.length;
253
- return `${prefix}${elided ? digits.slice(0, digits.length - end.length) : ''}${end}`;
250
+ const [run] = digits;
251
+ const elided = end.length < run.length;
252
+ return `${start.slice(0, digits.index)}${elided ? run.slice(0, run.length - end.length) : ''}${end}`;
254
253
  }
255
254
  /**
256
- * APA 7 pagination: each range written in full with an en dash (`637-41` →
257
- * `637–641`, `S5-15` → `S5–S15`), the rest of NLM's text as given — a list's
258
- * separators and labels too (`1407-8; author reply 1409-10` → `1407–1408;
259
- * author reply 1409–1410`).
255
+ * NLM's pagination with each range written in full (`637-41` → `637-641`,
256
+ * `S5-15` → `S5-S15`), joined by `dash` or by NLM's own when none is given; the
257
+ * rest of NLM's text stays as given — a list's separators and labels too
258
+ * (`1407-8; author reply 1409-10` → `1407-1408; author reply 1409-1410`). APA
259
+ * joins with an en dash (`637–641`); BibTeX keeps NLM's. (#217, #228)
260
260
  */
261
- function formatPagesApa(pages) {
262
- return pages.replace(PAGE_RANGE, (_, start, end) => `${start}\u{2013}${fullPageEnd(start, end)}`);
261
+ function expandPageRanges(pages, dash) {
262
+ return pages.replace(PAGE_ITEM, (item, start, end) => {
263
+ if (end === undefined)
264
+ return item;
265
+ const join = dash ?? item.slice(start.length, item.length - end.length);
266
+ return `${start}${join}${fullPageEnd(start, end)}`;
267
+ });
268
+ }
269
+ /** A token that can be a page: it carries a digit or is a Roman numeral, unlike a label (`author reply`). */
270
+ const PAGE_TOKEN = /\d|^[ivxlcdm]+$/i;
271
+ /**
272
+ * The first and last page of NLM's pagination, for RIS `SP` and `EP`: the start
273
+ * of the first page or range, and the end of the last range written in full or
274
+ * the last listed page (`1308, 1310-4` → `1308`/`1314`, `xii-xv` → `xii`/`xv`,
275
+ * `S5-15` → `S5`/`S15`). A single page or article number has no end, and a
276
+ * label in a list (`author reply`) is not a page. NCBI's own `StartPage` and
277
+ * `EndPage` agree on every shape. (#228)
278
+ */
279
+ function splitPages(pages) {
280
+ const items = [...pages.matchAll(PAGE_ITEM)];
281
+ const [, start] = items[0] ?? [];
282
+ const pageItems = items.filter(([, first = '', last]) => last !== undefined || PAGE_TOKEN.test(first));
283
+ const final = pageItems.at(-1);
284
+ if (!final || (pageItems.length === 1 && final[2] === undefined)) {
285
+ return { start, end: undefined };
286
+ }
287
+ const [, first = '', last] = final;
288
+ return { start, end: last === undefined ? first : fullPageEnd(first, last) };
263
289
  }
264
290
  /**
265
291
  * Words APA 7 keeps lowercase in a title unless one opens it or its subtitle:
@@ -347,6 +373,63 @@ function titleCaseJournal(title, minorWords) {
347
373
  })
348
374
  .join('');
349
375
  }
376
+ /** NLM's year qualifier: `Journal of women's health (2002)`. */
377
+ const YEAR_QUALIFIER = /^\d{4}$/;
378
+ /** An edition or format qualifier: `(Clinical research ed.)`, `(Landmark edition)`, `(Print)`. */
379
+ const EDITION_QUALIFIER = /(?:\bed\.|\bedition)$|(?:^|: )(?:Online|Print)$/i;
380
+ /** An abbreviation naming an edition: `Med Clin (Engl Ed)`, `Angew Chem Int Ed Engl`. */
381
+ const EDITION_ABBREVIATION = /\b(?:Ed|Edn|Edition)\b/i;
382
+ /**
383
+ * A place qualifier, `Place, Region` with an optional year: `London, England`,
384
+ * `New York, N.Y.`, `Chicago, Ill. : 1978`. Neither part holds a parenthesis,
385
+ * semicolon, or digit, and the region absorbs any space before the year's
386
+ * colon, so no two parts compete for a character and a near-miss fails in
387
+ * linear time.
388
+ */
389
+ const PLACE_QUALIFIER = /^[^,():;\d]+,[^():;\d]+(?::\s*\d{4})?$/;
390
+ /**
391
+ * The parenthetical a title ends on, a nested one read as one unit
392
+ * (`Bulletin (Hospital for Joint Diseases (New York, N.Y.))`), with the text
393
+ * before it; undefined when the title does not end on a balanced one.
394
+ */
395
+ function trailingParenthetical(title) {
396
+ if (!title.endsWith(')'))
397
+ return;
398
+ let depth = 0;
399
+ for (let i = title.length - 1; i >= 0; i--) {
400
+ if (title[i] === ')')
401
+ depth++;
402
+ else if (title[i] === '(' && --depth === 0) {
403
+ return { base: title.slice(0, i).trimEnd(), inner: title.slice(i + 1, -1) };
404
+ }
405
+ }
406
+ return;
407
+ }
408
+ /**
409
+ * The journal name APA and MLA cite: NLM's title less the place, year, or
410
+ * edition NLM appends to tell same-named journals apart (`Lancet (London,
411
+ * England)` → `Lancet`, `Journal of women's health (2002)`, `BMJ (Clinical
412
+ * research ed.)`). The journal prints no such text. An edition stays when the
413
+ * ISO abbreviation names it too, as it is then a journal of its own (`Medicina
414
+ * clinica (English ed.)`, `Med Clin (Engl Ed)`). Any other parenthetical names
415
+ * the journal and stays: an issuing body (`Proceedings (Baylor University.
416
+ * Medical Center)`), a subject, a one-word place. Over NLM's journal list the
417
+ * rule is wrong on 18 of the 2,005 titles NLM's catalog settles — a place-shaped
418
+ * issuing body (`Collegian (Royal College of Nursing, Australia)`) is dropped,
419
+ * and a place with no region (`Medical record (New York : 1934)`) is kept. (#227)
420
+ */
421
+ function citedJournalName(title, isoAbbreviation) {
422
+ const group = trailingParenthetical(title);
423
+ if (!group?.base)
424
+ return title;
425
+ const { base, inner } = group;
426
+ if (YEAR_QUALIFIER.test(inner))
427
+ return base;
428
+ if (EDITION_QUALIFIER.test(inner)) {
429
+ return EDITION_ABBREVIATION.test(isoAbbreviation ?? '') ? title : base;
430
+ }
431
+ return PLACE_QUALIFIER.test(inner) ? base : title;
432
+ }
350
433
  /**
351
434
  * Author keywords, then MeSH descriptor names, each term once: a term equal to
352
435
  * an earlier one ignoring case is dropped, so the author's spelling and position
@@ -368,9 +451,11 @@ function mergedKeywords(article) {
368
451
  return [...terms.values()];
369
452
  }
370
453
  /**
371
- * Collapse internal whitespace (including embedded newlines from structured
372
- * abstracts) to single spaces. Strict RIS parsers treat blank lines as record
373
- * terminators, so abstract text must be flattened before emission.
454
+ * Collapse each whitespace run to one space and trim the ends. The parser trims
455
+ * only a field's ends, so a line break inside a title, name, or keyword — or
456
+ * between a structured abstract's paragraphs — would otherwise reach a
457
+ * citation: an untagged RIS line, or a blank one that strict parsers read as
458
+ * the end of the record. Every style writes record text through this. (#233)
374
459
  */
375
460
  function collapseWhitespace(text) {
376
461
  return text.replace(/\s+/g, ' ').trim();
@@ -420,8 +505,23 @@ function escapeBibtex(text) {
420
505
  // ---------------------------------------------------------------------------
421
506
  // Author formatters
422
507
  // ---------------------------------------------------------------------------
508
+ /*
509
+ * A generational suffix (`Jr`, `2nd`) renders verbatim as NCBI writes it, as
510
+ * NCBI's own citation export prints it. It qualifies a surname, so a name with
511
+ * no surname renders without it. Inverted name forms set it off with a comma
512
+ * (`Frenck, R. W., Jr`); natural-order and NLM forms append it after a space
513
+ * (`R. W. Frenck Jr`, `Frenck RW Jr`). (#226)
514
+ */
515
+ /** `, Jr` for an inverted name form; `''` without a suffix or a surname. */
516
+ function invertedSuffix(author) {
517
+ return author.lastName && author.suffix ? `, ${author.suffix}` : '';
518
+ }
519
+ /** ` Jr` for a natural-order or NLM name form; `''` without a suffix or a surname. */
520
+ function trailingSuffix(author) {
521
+ return author.lastName && author.suffix ? ` ${author.suffix}` : '';
522
+ }
423
523
  /**
424
- * Format a single author in APA style: `Last, F. M.`
524
+ * Format a single author in APA style: `Last, F. M.`, or `Last, F. M., Jr`.
425
525
  * Collective/group authors return the group name directly.
426
526
  */
427
527
  function formatAuthorApa(author) {
@@ -436,7 +536,7 @@ function formatAuthorApa(author) {
436
536
  .map((part) => `${part[0]}.`)
437
537
  .join(' ');
438
538
  if (!initials)
439
- return last;
539
+ return `${last}${invertedSuffix(author)}`;
440
540
  // Extract only letter characters (Unicode-aware: preserve Á, Ö, É, etc.),
441
541
  // format each as "X." separated by spaces.
442
542
  const formatted = Array.from(initials.replace(/[^\p{L}]/gu, ''))
@@ -444,7 +544,7 @@ function formatAuthorApa(author) {
444
544
  .join(' ');
445
545
  if (!last)
446
546
  return formatted;
447
- return `${last}, ${formatted}`;
547
+ return `${last}, ${formatted}${invertedSuffix(author)}`;
448
548
  }
449
549
  /**
450
550
  * Format the full author list for APA 7th edition.
@@ -471,21 +571,19 @@ function formatAuthorsApa(authors) {
471
571
  }
472
572
  /**
473
573
  * Format a single author in MLA style.
474
- * First listed author: `Last, First Middle.`
475
- * Subsequent authors: `First Middle Last`
574
+ * First listed author: `Last, First Middle`, or `Last, First Middle, Jr`.
575
+ * Subsequent authors: `First Middle Last`, or `First Middle Last Jr`.
476
576
  */
477
577
  function formatAuthorMla(author, isFirst) {
478
578
  if (author.collectiveName)
479
579
  return author.collectiveName;
480
580
  const last = author.lastName ?? '';
481
581
  const first = author.firstName ?? '';
482
- if (!last && !first)
483
- return '';
484
- if (!first)
485
- return last;
486
582
  if (!last)
487
583
  return first;
488
- return isFirst ? `${last}, ${first}` : `${first} ${last}`;
584
+ if (isFirst)
585
+ return `${first ? `${last}, ${first}` : last}${invertedSuffix(author)}`;
586
+ return `${first ? `${first} ${last}` : last}${trailingSuffix(author)}`;
489
587
  }
490
588
  /**
491
589
  * Format the full author list for MLA 9th edition.
@@ -508,27 +606,28 @@ function formatAuthorsMla(authors) {
508
606
  return `${formatAuthorMla(first, true)}, et al.`;
509
607
  }
510
608
  /**
511
- * Format a single author in BibTeX style: `{Last}, {First}`
609
+ * Format a single author in BibTeX style: `{Last}, First`, or the three-part
610
+ * `{Last}, Jr, First` when the name carries a suffix — the comma after `Jr`
611
+ * stays even with no first name, so the suffix is never read as one.
512
612
  */
513
613
  function formatAuthorBibtex(author) {
514
614
  if (author.collectiveName)
515
615
  return `{${escapeBibtex(author.collectiveName)}}`;
516
616
  const last = author.lastName ? escapeBibtex(author.lastName) : '';
517
617
  const first = author.firstName ? escapeBibtex(author.firstName) : '';
518
- if (!last && !first)
519
- return '';
520
- if (!first)
521
- return `{${last}}`;
522
618
  if (!last)
523
619
  return first;
524
- return `{${last}}, ${first}`;
620
+ if (author.suffix)
621
+ return `{${last}}, ${escapeBibtex(author.suffix)},${first && ` ${first}`}`;
622
+ return first ? `{${last}}, ${first}` : `{${last}}`;
525
623
  }
526
624
  // ---------------------------------------------------------------------------
527
625
  // APA 7th Edition
528
626
  // ---------------------------------------------------------------------------
529
627
  /**
530
628
  * Format an editor in APA's "In E. E. Editor (Ed.)," position: initials first,
531
- * surname last — the inverse of the author position.
629
+ * surname last — the inverse of the author position — and a suffix after the
630
+ * surname (`M. P. Adam Jr`).
532
631
  */
533
632
  function formatEditorApa(editor) {
534
633
  if (editor.collectiveName)
@@ -544,7 +643,7 @@ function formatEditorApa(editor) {
544
643
  .map((c) => `${c}.`)
545
644
  .join(' ')
546
645
  : '';
547
- return [initials, editor.lastName].filter(Boolean).join(' ');
646
+ return [initials, editor.lastName].filter(Boolean).join(' ') + trailingSuffix(editor);
548
647
  }
549
648
  /** APA editor list: comma-separated, `& ` before the last name. */
550
649
  function formatEditorsApa(editors) {
@@ -609,7 +708,7 @@ function formatApaBook(article) {
609
708
  const url = bookAccessUrl(article);
610
709
  if (url)
611
710
  parts.push(url);
612
- return parts.join(' ');
711
+ return collapseWhitespace(parts.join(' '));
613
712
  }
614
713
  /**
615
714
  * Format a PubMed article as an APA 7th edition citation.
@@ -654,7 +753,8 @@ export function formatApa(article) {
654
753
  // Journal, volume, issue, pages
655
754
  const journal = article.journalInfo;
656
755
  if (journal?.title) {
657
- let journalPart = `*${titleCaseJournal(journal.title, APA_MINOR_WORDS)}*`;
756
+ const name = citedJournalName(journal.title, journal.isoAbbreviation);
757
+ let journalPart = `*${titleCaseJournal(name, APA_MINOR_WORDS)}*`;
658
758
  if (journal.volume) {
659
759
  journalPart += `, *${journal.volume}*`;
660
760
  if (journal.issue) {
@@ -667,7 +767,7 @@ export function formatApa(article) {
667
767
  journalPart += `, Article ${number}`;
668
768
  }
669
769
  else if (journal.pages) {
670
- journalPart += `, ${formatPagesApa(journal.pages)}`;
770
+ journalPart += `, ${expandPageRanges(journal.pages, '\u{2013}')}`;
671
771
  }
672
772
  journalPart += '.';
673
773
  parts.push(journalPart);
@@ -685,18 +785,14 @@ export function formatApa(article) {
685
785
  if (retractions.length > 0) {
686
786
  parts.push(`(Retraction published ${retractions.join('; ')})`);
687
787
  }
688
- return parts.join(' ');
788
+ return collapseWhitespace(parts.join(' '));
689
789
  }
690
790
  // ---------------------------------------------------------------------------
691
791
  // MLA 9th Edition
692
792
  // ---------------------------------------------------------------------------
693
- /** MLA renders editors first-name-first after `edited by`. */
793
+ /** MLA renders editors first-name-first after `edited by`, as it does a non-first author. */
694
794
  function formatEditorsMla(editors) {
695
- const names = editors
696
- .map((editor) => editor.collectiveName
697
- ? editor.collectiveName
698
- : [editor.firstName, editor.lastName].filter(Boolean).join(' '))
699
- .filter(Boolean);
795
+ const names = editors.map((editor) => formatAuthorMla(editor, false)).filter(Boolean);
700
796
  if (names.length === 0)
701
797
  return '';
702
798
  if (names.length === 1)
@@ -740,7 +836,7 @@ function formatMlaBook(article) {
740
836
  detailParts.push(date);
741
837
  if (detailParts.length)
742
838
  parts.push(terminate(detailParts.join(', ')));
743
- return parts.join(' ');
839
+ return collapseWhitespace(parts.join(' '));
744
840
  }
745
841
  /**
746
842
  * Format a PubMed article as an MLA 9th edition citation.
@@ -770,7 +866,8 @@ export function formatMla(article) {
770
866
  const journal = article.journalInfo;
771
867
  if (journal?.title) {
772
868
  const detailParts = [];
773
- detailParts.push(`*${titleCaseJournal(journal.title, MLA_MINOR_WORDS)}*`);
869
+ const name = citedJournalName(journal.title, journal.isoAbbreviation);
870
+ detailParts.push(`*${titleCaseJournal(name, MLA_MINOR_WORDS)}*`);
774
871
  if (journal.volume) {
775
872
  detailParts.push(`vol. ${journal.volume}`);
776
873
  }
@@ -798,7 +895,7 @@ export function formatMla(article) {
798
895
  if (article.doi) {
799
896
  parts.push(`https://doi.org/${article.doi}.`);
800
897
  }
801
- return parts.join(' ');
898
+ return collapseWhitespace(parts.join(' '));
802
899
  }
803
900
  // ---------------------------------------------------------------------------
804
901
  // BibTeX
@@ -875,11 +972,12 @@ export function formatBibtex(article) {
875
972
  if (journal?.issue) {
876
973
  fields.push(['number', escapeBibtex(journal.issue)]);
877
974
  }
878
- // Pages — or, with no pagination, biblatex's `eid` for the article number.
879
- // Classic BibTeX has no article-number field and overloading `pages` is
880
- // imprecise; `eid` is broadly supported (acmart included).
975
+ // Pages, each range written in full (#228) — or, with no pagination,
976
+ // biblatex's `eid` for the article number. Classic BibTeX has no
977
+ // article-number field and overloading `pages` is imprecise; `eid` is broadly
978
+ // supported (acmart included).
881
979
  if (journal?.pages) {
882
- fields.push(['pages', escapeBibtex(journal.pages)]);
980
+ fields.push(['pages', escapeBibtex(expandPageRanges(journal.pages))]);
883
981
  }
884
982
  else if (!book) {
885
983
  const locator = articleLocator(journal);
@@ -920,14 +1018,30 @@ export function formatBibtex(article) {
920
1018
  if (keywords.length > 0) {
921
1019
  fields.push(['keywords', keywords.map((k) => `{${escapeBibtex(k)}}`).join(', ')]);
922
1020
  }
923
- // Build entry
1021
+ // Build entry — each value on its own line, whitespace runs collapsed (#233)
924
1022
  const maxKeyLen = Math.max(...fields.map(([k]) => k.length));
925
- const fieldLines = fields.map(([k, v]) => ` ${k.padEnd(maxKeyLen)} = {${v}}`).join(',\n');
1023
+ const fieldLines = fields
1024
+ .map(([k, v]) => ` ${k.padEnd(maxKeyLen)} = {${collapseWhitespace(v)}}`)
1025
+ .join(',\n');
926
1026
  return `@${entryType}{${key},\n${fieldLines}\n}`;
927
1027
  }
928
1028
  // ---------------------------------------------------------------------------
929
1029
  // RIS
930
1030
  // ---------------------------------------------------------------------------
1031
+ /**
1032
+ * An `AU` or `A2` value: `Lastname, Firstname`, or `Lastname, Firstname, Suffix`.
1033
+ * Positional, so a suffixed name with no first name keeps the empty slot
1034
+ * (`Frenck, , Jr`) rather than reading the suffix as a first name.
1035
+ */
1036
+ function formatNameRis(person) {
1037
+ if (person.collectiveName)
1038
+ return person.collectiveName;
1039
+ const last = person.lastName ?? '';
1040
+ const first = person.firstName ?? '';
1041
+ if (last && person.suffix)
1042
+ return `${last}, ${first}, ${person.suffix}`;
1043
+ return first ? `${last}, ${first}` : last;
1044
+ }
931
1045
  /**
932
1046
  * Format a PubMed article as a RIS record.
933
1047
  *
@@ -936,9 +1050,12 @@ export function formatBibtex(article) {
936
1050
  */
937
1051
  export function formatRis(article) {
938
1052
  const lines = [];
1053
+ // Every value on its own tag line: a line break inside one would leave an
1054
+ // untagged line, or a blank one that ends the record for strict parsers (#233)
939
1055
  const tag = (code, value) => {
940
- if (value)
941
- lines.push(`${code} - ${value}`);
1056
+ const text = value && collapseWhitespace(value);
1057
+ if (text)
1058
+ lines.push(`${code} - ${text}`);
942
1059
  };
943
1060
  const book = isBookRecord(article) ? article.book : undefined;
944
1061
  // Type of reference — the record type for a book, else the publication types
@@ -949,20 +1066,8 @@ export function formatRis(article) {
949
1066
  : firstMappedType(article.publicationTypes, RIS_REFERENCE_TYPE, 'JOUR');
950
1067
  lines.push(`TY - ${refType}`);
951
1068
  // Authors — one AU tag per author
952
- if (article.authors?.length) {
953
- for (const author of article.authors) {
954
- if (author.collectiveName) {
955
- tag('AU', author.collectiveName);
956
- }
957
- else {
958
- const last = author.lastName ?? '';
959
- const first = author.firstName ?? '';
960
- if (last || first) {
961
- tag('AU', first ? `${last}, ${first}` : last);
962
- }
963
- }
964
- }
965
- }
1069
+ for (const author of article.authors ?? [])
1070
+ tag('AU', formatNameRis(author));
966
1071
  // Title
967
1072
  tag('TI', article.title);
968
1073
  // Container — the journal, or the book a chapter sits in. `BT` is the book
@@ -977,14 +1082,8 @@ export function formatRis(article) {
977
1082
  if (book) {
978
1083
  if (article.recordType === 'book-chapter')
979
1084
  tag('BT', book.title);
980
- for (const editor of book.editors ?? []) {
981
- const last = editor.lastName ?? '';
982
- const first = editor.firstName ?? '';
983
- if (editor.collectiveName)
984
- tag('A2', editor.collectiveName);
985
- else if (last || first)
986
- tag('A2', first ? `${last}, ${first}` : last);
987
- }
1085
+ for (const editor of book.editors ?? [])
1086
+ tag('A2', formatNameRis(editor));
988
1087
  tag('PB', book.publisher);
989
1088
  tag('CY', book.publisherLocation);
990
1089
  tag('ET', book.edition);
@@ -1001,7 +1100,7 @@ export function formatRis(article) {
1001
1100
  // Volume & Issue
1002
1101
  tag('VL', journal?.volume);
1003
1102
  tag('IS', journal?.issue);
1004
- // Pages — split into start/end, expanding PubMed's truncated-end convention.
1103
+ // Pages — the first page and the last, written in full ({@link splitPages}).
1005
1104
  // With no pagination, the article number goes on `C7` (the attested RIS
1006
1105
  // convention for Article Number), never on SP/EP, which hold absolute pages.
1007
1106
  if (journal?.pages) {
@@ -1037,11 +1136,8 @@ export function formatRis(article) {
1037
1136
  for (const kw of mergedKeywords(article)) {
1038
1137
  tag('KW', kw);
1039
1138
  }
1040
- // Abstract — collapse internal whitespace so blank lines don't break strict
1041
- // RIS parsers that terminate records at blank lines
1042
- if (article.abstractText) {
1043
- tag('AB', collapseWhitespace(article.abstractText));
1044
- }
1139
+ // Abstract — a structured abstract's paragraphs flatten onto the one line
1140
+ tag('AB', article.abstractText);
1045
1141
  // End of record (trailing space per RIS spec)
1046
1142
  lines.push('ER - ');
1047
1143
  return lines.join('\n');
@@ -1051,8 +1147,8 @@ export function formatRis(article) {
1051
1147
  // ---------------------------------------------------------------------------
1052
1148
  /**
1053
1149
  * Format a single author in Vancouver style: `Surname AB` — surname, a space,
1054
- * then initials with no periods or internal spaces. Collective/group authors
1055
- * return their name directly.
1150
+ * then initials with no periods or internal spaces, then any suffix after a
1151
+ * space (`Frenck RW Jr`). Collective/group authors return their name directly.
1056
1152
  */
1057
1153
  function formatAuthorVancouver(author) {
1058
1154
  if (author.collectiveName)
@@ -1073,9 +1169,7 @@ function formatAuthorVancouver(author) {
1073
1169
  : '';
1074
1170
  if (!last)
1075
1171
  return initials;
1076
- if (!initials)
1077
- return last;
1078
- return `${last} ${initials}`;
1172
+ return `${initials ? `${last} ${initials}` : last}${trailingSuffix(author)}`;
1079
1173
  }
1080
1174
  /**
1081
1175
  * Format the full author list for Vancouver: list every author for six or
@@ -1136,7 +1230,7 @@ function formatVancouverBook(article) {
1136
1230
  const url = bookshelfUrl(book);
1137
1231
  if (url)
1138
1232
  segments.push(`Available from: ${url}`);
1139
- return segments.join(' ');
1233
+ return collapseWhitespace(segments.join(' '));
1140
1234
  }
1141
1235
  /**
1142
1236
  * The notices Vancouver notes after the reference, each with Citing Medicine's
@@ -1217,7 +1311,7 @@ export function formatVancouver(article) {
1217
1311
  segments.push(notes.length > 0 ? `doi: ${article.doi}.` : `doi: ${article.doi}`);
1218
1312
  }
1219
1313
  segments.push(...notes);
1220
- return segments.join(' ');
1314
+ return collapseWhitespace(segments.join(' '));
1221
1315
  }
1222
1316
  // ---------------------------------------------------------------------------
1223
1317
  // Dispatchers