Skip to content

Commit f93239c

Browse files
authored
Split page ranges for Google Scholar reference tags (#15118)
Bibliography entries in `citation_reference` tags never carried `citation_firstpage` or `citation_lastpage`. Pandoc's CSL for a bibliography entry has only `page`, and the split into first and last page ran only for the document's own citation. One `cslPageRange` helper now handles both, which also fixes en-dash ranges, page lists and spaced ranges in the document citation. - Add `cslPageRange` in core/csl.ts, accepting hyphen, en-dash and em-dash separators - Use it for the document citation, and for bibliography entries that lack explicit `page-first`/`page-last` - Write only the first page for a page list such as `1-3, 7-9`
1 parent b3eb5f5 commit f93239c

7 files changed

Lines changed: 113 additions & 22 deletions

File tree

‎news/changelog-1.11.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ All changes included in 1.11:
6363
- ([#15051](https://github.com/quarto-dev/quarto-cli/pull/15051)): Fix a callout's `title` option also rendering as an HTML `title` attribute in html, revealjs and epub output, which showed the title as a tooltip over the whole callout.
6464
- ([#15108](https://github.com/quarto-dev/quarto-cli/pull/15108)): Fix Google Scholar metadata showing formatting markup such as `<i>` in `citation_reference` tags for bibliography entries, and `undefined` as the book title of a chapter without one.
6565
- ([#15115](https://github.com/quarto-dev/quarto-cli/pull/15115)): Fix Google Scholar writing two `citation_reference` tags for a reference defined in two bibliography files. The tag now describes the entry Pandoc renders.
66+
- ([#15118](https://github.com/quarto-dev/quarto-cli/pull/15118)): Fix Google Scholar metadata missing the first and last page of bibliography entries, and giving wrong pages for a citation `page` written with an en-dash or as a page list.
6667

6768
### `pdf`
6869

‎src/core/csl.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ export function cslTextToPlain(value: string): string {
142142
return value.replace(cslRichTextTags, "");
143143
}
144144

145+
// Splits a CSL `page` value ("97-111", "97–111", "42") into its first and
146+
// last page. A page list ("1-3, 7-9") yields only the first page.
147+
export function cslPageRange(page: string) {
148+
const [range, ...others] = page.split(/[,&]/);
149+
const match = range.match(/^\s*(.*?)\s*[-–—]+\s*(.*?)\s*$/);
150+
const first = (match ? match[1] : range.trim()) || undefined;
151+
const last = (match && others.length === 0 ? match[2] : "") || undefined;
152+
return { first, last };
153+
}
154+
145155
export function suggestId(author: CSLName[], date?: CSLDate) {
146156
// Try to get the last name
147157
let citeIdLeading = "";

‎src/format/html/format-html-meta.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
CSL,
1717
cslDateToEDTFDate,
1818
CSLExtras,
19+
cslPageRange,
1920
cslTextToPlain,
2021
kAbstractUrl,
2122
kEIssn,
@@ -205,12 +206,16 @@ function googleScholarMeta(
205206
write("citation_language", csl.language);
206207
}
207208

208-
if (csl["page-first"]) {
209-
write("citation_firstpage", csl["page-first"]);
209+
// Bibliography entries from Pandoc carry only `page`.
210+
const pageRange = cslPageRange(`${csl.page ?? ""}`);
211+
const firstPage = csl["page-first"] || pageRange.first;
212+
if (firstPage) {
213+
write("citation_firstpage", firstPage);
210214
}
211215

212-
if (csl["page-last"]) {
213-
write("citation_lastpage", csl["page-last"]);
216+
const lastPage = csl["page-last"] || pageRange.last;
217+
if (lastPage) {
218+
write("citation_lastpage", lastPage);
214219
}
215220

216221
const type = csl.type;

‎src/quarto-core/attribution/document.ts‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
cslDate,
2828
CSLExtras,
2929
cslNames,
30+
cslPageRange,
3031
CSLType,
3132
cslType,
3233
kAbstractUrl,
@@ -485,23 +486,14 @@ function pages(citationMetadata: Metadata): PageRange {
485486
let pages = citationMetadata[kPage]
486487
? `${citationMetadata[kPage] as string}` // Force pages to string in case user writes `page: 7`
487488
: undefined;
488-
if (pages && pages.includes("-")) {
489-
const pagesSplit = pages.split("-");
490-
if (!firstPage) {
491-
firstPage = pagesSplit[0];
492-
}
493-
494-
if (!lastPage) {
495-
lastPage = pagesSplit[1];
496-
}
497-
} else if (pages && !firstPage) {
498-
firstPage = pages;
499-
} else if (!pages) {
500-
if (firstPage && lastPage) {
501-
pages = `${firstPage} - ${lastPage}`;
502-
} else if (firstPage) {
503-
pages = `${firstPage}`;
504-
}
489+
if (pages) {
490+
const range = cslPageRange(pages);
491+
firstPage = firstPage || range.first;
492+
lastPage = lastPage || range.last;
493+
} else if (firstPage && lastPage) {
494+
pages = `${firstPage} - ${lastPage}`;
495+
} else if (firstPage) {
496+
pages = `${firstPage}`;
505497
}
506498
return {
507499
firstPage: firstPage as string,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@article{ranged,
2+
author = {Doe, Jane},
3+
title = {Ranged Article},
4+
journal = {Journal of Tests},
5+
year = {2020},
6+
pages = {97--111}
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Scholar reference metadata splits a page range
3+
format: html
4+
google-scholar: true
5+
bibliography: references.bib
6+
_quarto:
7+
tests:
8+
html:
9+
ensureHtmlElements:
10+
- ['meta[name="citation_reference"][content*="citation_firstpage=97;"]', 'meta[name="citation_reference"][content*="citation_lastpage=111;"]']
11+
- []
12+
---
13+
14+
This article cites a work with a page range [@ranged].

‎tests/unit/scholar-metadata.test.ts‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { assertEquals, assertStringIncludes } from "testing/asserts";
88
import { Metadata } from "../../src/config/types.ts";
99
import { bibliographyCslJson } from "../../src/core/bibliography.ts";
10-
import { parseHtml } from "../../src/core/deno-dom.ts";
10+
import { Element, parseHtml } from "../../src/core/deno-dom.ts";
1111
import { join } from "../../src/deno_ral/path.ts";
1212
import { metadataPostProcessor } from "../../src/format/html/format-html-meta.ts";
1313
import { unitTest } from "../test.ts";
@@ -196,3 +196,65 @@ unitTest(
196196
});
197197
},
198198
);
199+
200+
function metaContent(
201+
doc: Awaited<ReturnType<typeof scholarMetadata>>,
202+
name: string,
203+
) {
204+
return doc.querySelector(`meta[name="${name}"]`)?.getAttribute("content");
205+
}
206+
207+
unitTest("Scholar metadata - split the citation page range", async () => {
208+
for (
209+
const [page, firstPage, lastPage] of [
210+
["97-111", "97", "111"],
211+
["97–111", "97", "111"],
212+
["97 - 111", "97", "111"],
213+
["1-3, 7-9", "1", undefined],
214+
["42", "42", undefined],
215+
]
216+
) {
217+
const doc = await scholarMetadata({
218+
title: "An article",
219+
citation: { page },
220+
});
221+
assertEquals(metaContent(doc, "citation_firstpage"), firstPage, page);
222+
assertEquals(metaContent(doc, "citation_lastpage"), lastPage, page);
223+
}
224+
});
225+
226+
unitTest("Scholar metadata - split reference page ranges", async () => {
227+
await withTempDir(async (dir) => {
228+
Deno.writeTextFileSync(
229+
join(dir, "references.yaml"),
230+
`---
231+
references:
232+
- {id: endash, type: article-journal, title: En dash, page: "97–111"}
233+
- {id: single, type: article-journal, title: Single, page: "42"}
234+
- {id: list, type: article-journal, title: List, page: "1-3, 7-9"}
235+
---
236+
`,
237+
);
238+
const doc = await scholarMetadata(
239+
{ title: "An article", bibliography: "references.yaml" },
240+
join(dir, "test.qmd"),
241+
'<span class="citation" data-cites="endash single list"></span>',
242+
);
243+
const references = Object.fromEntries(
244+
[...doc.querySelectorAll('meta[name="citation_reference"]')].map(
245+
(element) => {
246+
const content = (element as Element).getAttribute("content")!;
247+
return [content.match(/citation_title=([^;]+);/)![1], content];
248+
},
249+
),
250+
);
251+
assertStringIncludes(
252+
references["En dash"],
253+
"citation_firstpage=97; citation_lastpage=111;",
254+
);
255+
assertStringIncludes(references["Single"], "citation_firstpage=42;");
256+
assertEquals(references["Single"].includes("citation_lastpage"), false);
257+
assertStringIncludes(references["List"], "citation_firstpage=1;");
258+
assertEquals(references["List"].includes("citation_lastpage"), false);
259+
});
260+
});

0 commit comments

Comments
 (0)