From 8c8615c622f3c843d63e77ba1326c7f6c3473c56 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 11:34:46 +0200 Subject: [PATCH 1/4] Rule-based PDF reference extraction: support biblatex standard styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alphabetic labels ([AL26], [Buc+23]) are split into references, and the biblatex layout (Authors. “Title”. In: …) is parsed into article, inproceedings, and misc entries. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/requirements/import.md | 8 + .../pdf/RuleBasedBibliographyPdfImporter.java | 185 ++++++++++++++++-- .../RuleBasedBibliographyPdfImporterTest.java | 108 +++++++++- .../resources/pdfs/biblatex/alphabetic.bib | 75 +++++++ .../resources/pdfs/biblatex/alphabetic.pdf | Bin 0 -> 29911 bytes .../resources/pdfs/biblatex/alphabetic.tex | 9 + 6 files changed, 364 insertions(+), 21 deletions(-) create mode 100644 jablib/src/test/resources/pdfs/biblatex/alphabetic.bib create mode 100644 jablib/src/test/resources/pdfs/biblatex/alphabetic.pdf create mode 100644 jablib/src/test/resources/pdfs/biblatex/alphabetic.tex diff --git a/docs/requirements/import.md b/docs/requirements/import.md index 9a250e5bd3b..1b40d1d129e 100644 --- a/docs/requirements/import.md +++ b/docs/requirements/import.md @@ -41,4 +41,12 @@ A file that parses with warnings is not affected: it still opens, and its warnin Needs: impl, utest +## Rule-based reference extraction reads the biblatex standard styles +`req~import.pdf.references.biblatex~1` + +The rule-based extraction of a PDF's reference list recognizes references typeset with the biblatex standard styles, labelled numerically (`[12]`) or alphabetically (`[AL26]`, `[Buc+23]`). +Field values such as the DOI are kept as printed, and each entry carries the printed reference in `comment`, so faults of the typeset list stay visible. + +Needs: impl, utest + diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java index 59800cdf172..b63581367af 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java @@ -4,7 +4,9 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,7 +37,7 @@ /// Parses the references from the "References" section from a PDF. /// -/// Currently, IEEE two column format is supported. +/// Currently, IEEE two column format and the biblatex standard styles (e.g., `alphabetic`, `numeric`) are supported. /// /// To extract a [BibEntry] matching the PDF, see [PdfContentImporter]. /// @@ -45,7 +47,9 @@ public class RuleBasedBibliographyPdfImporter extends BibliographyFromPdfImporte private static final Logger LOGGER = LoggerFactory.getLogger(RuleBasedBibliographyPdfImporter.class); - private static final Pattern REFERENCE_PATTERN = Pattern.compile("\\[(\\d+)\\](.*?)(?=\\[|$)", Pattern.DOTALL); + /// Numeric labels (`[12]`) and biblatex-alphabetic labels (`[AL26]`, `[Buc+23]`, `[adr26]`, `[Kop+18a]`) + private static final String LABEL = "\\[(\\d+|\\p{L}[\\p{L}'-]*\\+?\\d{2}[a-z]?)\\]"; + private static final Pattern REFERENCE_PATTERN = Pattern.compile(LABEL + "(.*?)(?=" + LABEL + "|$)", Pattern.DOTALL); private static final Pattern YEAR_AT_END = Pattern.compile(", (\\d{4})\\.$"); private static final Pattern YEAR = Pattern.compile(", (\\d{4})(.*)"); private static final Pattern PAGES = Pattern.compile(", pp\\. (\\d+--?\\d+)\\.?(.*)"); @@ -60,6 +64,20 @@ public class RuleBasedBibliographyPdfImporter extends BibliographyFromPdfImporte private static final Pattern AUTHORS_AND_TITLE_AT_BEGINNING = Pattern.compile("^([^“]+), “(.*?)(”,|,”) "); private static final Pattern TITLE = Pattern.compile("“(.*?)”, (.*)"); + // biblatex standard styles: `Authors. “Title”. In: Container. issn: …. doi: …. url: … (visited on …).` + private static final Pattern BIBLATEX_AUTHORS_AND_TITLE = Pattern.compile("^(.+?)\\. “(.+?)”\\.? ?(.*)$"); + // Titles of books and online resources are printed in italics, not in quotes + private static final Pattern BIBLATEX_AUTHORS_AND_UNQUOTED_TITLE = Pattern.compile("^(.*?\\p{L}{2})\\. ?(.+?)\\. (.*)$"); + private static final Pattern BIBLATEX_FIELD_LABEL = Pattern.compile("\\b(issn|isbn|doi|url): ", Pattern.CASE_INSENSITIVE); + private static final Pattern BIBLATEX_VISITED_ON = Pattern.compile("\\(visited on [^)]*\\)"); + private static final Pattern BIBLATEX_ARTICLE = Pattern.compile("^In: (?:(.*?[^-\\s]) )?(?:(\\d+)(?:\\.(\\S+))? )?\\([^)]*?(\\d{4})\\)(?:, pp?\\. (\\S+?))?\\.?$"); + private static final Pattern BIBLATEX_PAGES_AT_END = Pattern.compile(", pp?\\. (\\S+?)\\.?$"); + private static final Pattern BIBLATEX_YEAR_AT_END = Pattern.compile("(?:^|[ ,])(\\d{4})\\.?$"); + private static final Pattern BIBLATEX_VOLUME_AND_SERIES_AT_END = Pattern.compile("\\. Vol\\. (\\S+?)(?:\\. (.+))?$"); + private static final Pattern BIBLATEX_EDITORS_AT_END = Pattern.compile("\\. Ed\\. by (.+)$"); + // The page number printed below the last reference on a page ends up after the reference's final dot + private static final Pattern TRAILING_PAGE_NUMBER = Pattern.compile("\\.\\s+\\d{1,4}$"); + @Nullable private final CitationKeyPatternPreferences citationKeyPatternPreferences; private final NormalizeUnicodeFormatter normalizeUnicodeFormatter = new NormalizeUnicodeFormatter(); @@ -107,7 +125,7 @@ public ParserResult importDatabase(Path filePath, PDDocument document) throws IO } @VisibleForTesting - record IntermediateData(String number, String reference) { + record IntermediateData(String label, String reference) { } /// In: `[1] ...\n...\n...[2]...\n...\n...\n[3]...` @@ -115,7 +133,7 @@ public List getEntriesFromPDFContent(String contents) { List referencesStrings = getIntermediateData(contents); return referencesStrings.stream() - .map(data -> parsePlainCitation(data.number(), data.reference())) + .map(data -> parsePlainCitation(data.label(), data.reference())) .toList(); } @@ -137,18 +155,19 @@ public Optional parsePlainCitation(String reference) { /// Example: `J. Knaster et al., “Overview of the IFMIF/EVEDA project”, Nucl. Fusion, vol. 57, p. 102016, 2017. doi:10.1088/ 1741-4326/aa6a6a` @VisibleForTesting - BibEntry parsePlainCitation(String number, String reference) { + BibEntry parsePlainCitation(String label, String reference) { reference = normalizeUnicodeFormatter.format(reference); - String originalReference = "[" + number + "] " + reference; + String originalReference = "[" + label + "] " + reference; + + if (BIBLATEX_AUTHORS_AND_TITLE.matcher(reference).find() + || (!reference.contains("“") && BIBLATEX_FIELD_LABEL.matcher(reference).find())) { + return parseBiblatexCitation(label, reference).withField(StandardField.COMMENT, originalReference); + } + BibEntry result = new BibEntry(StandardEntryType.Article) - .withCitationKey(number); + .withCitationKey(label); - reference = reference - .replace(".-", "-") - // Unicode en dash (used as page separator) - .replace("–", "-") - // Remove "- " introduced by linebreaks in the PDF - .replaceAll("([^ ])- ", "$1"); + reference = removeLineBreakArtifacts(reference); // Move URL to URL field Matcher urlPatternMatcher = URLCleanup.URL_PATTERN.matcher(reference); @@ -230,13 +249,7 @@ BibEntry parsePlainCitation(String number, String reference) { // Y. Shimosaki et al., “Lattice design for 5 MeV – 125 mA CW RFQ operation in LIPAc”, in Proc. IPAC’19, Mel- bourne, Australia matcher = AUTHORS_AND_TITLE_AT_BEGINNING.matcher(reference); if (matcher.find()) { - String authors = matcher.group(1).replaceAll("et al\\.?", "and others"); - - // Alternative: AuthorList.fixAuthorFirstNameFirst(authors) only - // However, this does not work with special cases. Thus, we do a simple transformation only. - String fixedAuthors = AuthorListParser.normalizeSimply(authors).orElseGet(() -> AuthorList.fixAuthorFirstNameFirst(authors)); - - result.setField(StandardField.AUTHOR, fixedAuthors); + result.setField(StandardField.AUTHOR, normalizeAuthors(matcher.group(1))); result.setField(StandardField.TITLE, matcher.group(2).replaceAll("et al\\.?", "and others")); reference = reference.substring(matcher.end()).trim(); } else { @@ -332,6 +345,138 @@ BibEntry parsePlainCitation(String number, String reference) { return result; } + private static String removeLineBreakArtifacts(String reference) { + return reference + .replace(".-", "-") + // Unicode en dash (used as page separator) + .replace("–", "-") + // Remove "- " introduced by linebreaks in the PDF + .replaceAll("([^ ])- ", "$1"); + } + + private static String normalizeAuthors(String authorsText) { + String authors = authorsText.replaceAll("et al\\.?", "and others"); + // Alternative: AuthorList.fixAuthorFirstNameFirst(authors) only + // However, this does not work with special cases. Thus, we do a simple transformation only. + return AuthorListParser.normalizeSimply(authors).orElseGet(() -> AuthorList.fixAuthorFirstNameFirst(authors)); + } + + /// Parses a reference formatted by one of the biblatex standard styles. + /// + /// [impl->req~import.pdf.references.biblatex~1] + /// + /// Example: `Aisha Alansari and Hamzah Luqman. “Large language models hallucination: A comprehensive survey”. In: Computer Science Review 61 (2026), p. 100970. issn: 1574-0137. doi: 10.1016/j.cosrev.2026.100970.` + private static BibEntry parseBiblatexCitation(String label, String reference) { + BibEntry result = new BibEntry(StandardEntryType.Misc).withCitationKey(label); + reference = TRAILING_PAGE_NUMBER.matcher(reference.trim()).replaceFirst("."); + + // issn, isbn, doi, and url come last. Their values are taken as printed: + // there, a line break at a "-" does not hyphenate, the "-" is part of the value. + List fieldLabels = BIBLATEX_FIELD_LABEL.matcher(reference).results().toList(); + for (int i = 0; i < fieldLabels.size(); i++) { + MatchResult fieldLabel = fieldLabels.get(i); + Field field = switch (fieldLabel.group(1).toLowerCase(Locale.ROOT)) { + case "issn" -> + StandardField.ISSN; + case "isbn" -> + StandardField.ISBN; + case "doi" -> + StandardField.DOI; + default -> + StandardField.URL; + }; + int valueEnd = i + 1 < fieldLabels.size() ? fieldLabels.get(i + 1).start() : reference.length(); + String value = BIBLATEX_VISITED_ON.matcher(reference.substring(fieldLabel.end(), valueEnd)).replaceAll("") + .replaceAll("\\s", "") + .replaceAll("\\.$", ""); + result.setField(field, value); + } + + String head = fieldLabels.isEmpty() ? reference : reference.substring(0, fieldLabels.getFirst().start()); + String rest = removeLineBreakArtifacts(head).trim(); + Matcher authorsAndTitle = BIBLATEX_AUTHORS_AND_TITLE.matcher(rest); + if (!authorsAndTitle.matches()) { + authorsAndTitle = BIBLATEX_AUTHORS_AND_UNQUOTED_TITLE.matcher(rest); + } + if (authorsAndTitle.matches()) { + result.setField(StandardField.AUTHOR, normalizeBiblatexNames(authorsAndTitle.group(1))); + result.setField(StandardField.TITLE, authorsAndTitle.group(2)); + rest = authorsAndTitle.group(3).trim(); + } + + Matcher article = BIBLATEX_ARTICLE.matcher(rest); + if (article.matches()) { + result.setType(StandardEntryType.Article); + setFieldIfPresent(result, StandardField.JOURNAL, article.group(1)); + setFieldIfPresent(result, StandardField.VOLUME, article.group(2)); + setFieldIfPresent(result, StandardField.NUMBER, article.group(3)); + result.setField(StandardField.YEAR, article.group(4)); + setFieldIfPresent(result, StandardField.PAGES, article.group(5)); + return result; + } + + boolean inContainer = rest.startsWith("In: "); + if (inContainer) { + result.setType(StandardEntryType.InProceedings); + rest = rest.substring("In: ".length()); + } + rest = takeFromEnd(rest, BIBLATEX_PAGES_AT_END, result, StandardField.PAGES); + rest = takeFromEnd(rest, BIBLATEX_YEAR_AT_END, result, StandardField.YEAR); + if (rest.endsWith(",")) { + // "Publisher, 2019" or "Location: Publisher, 2019" + rest = rest.substring(0, rest.length() - 1); + int publisherStart = rest.lastIndexOf(". ") + 1; + String publisher = rest.substring(publisherStart).trim(); + int locationEnd = publisher.indexOf(": "); + if (locationEnd >= 0) { + result.setField(StandardField.LOCATION, publisher.substring(0, locationEnd)); + publisher = publisher.substring(locationEnd + 2); + } + result.setField(StandardField.PUBLISHER, publisher); + rest = rest.substring(0, publisherStart); + } + if (!inContainer) { + return result; + } + + rest = rest.replaceAll("\\.$", ""); + rest = takeFromEnd(rest, BIBLATEX_VOLUME_AND_SERIES_AT_END, result, StandardField.VOLUME, StandardField.SERIES); + Matcher editors = BIBLATEX_EDITORS_AT_END.matcher(rest); + if (editors.find()) { + result.setField(StandardField.EDITOR, normalizeBiblatexNames(editors.group(1))); + rest = rest.substring(0, editors.start()); + } + setFieldIfPresent(result, StandardField.BOOKTITLE, rest); + return result; + } + + /// biblatex prints names as "Given Family", thus a comma only separates names: "A, B, and C". + private static String normalizeBiblatexNames(String names) { + return names.replace(", and ", " and ") + .replace(", ", " and ") + .replaceAll(" et al\\.?$", " and others"); + } + + /// Moves the groups of `pattern`, which has to match at the end of `reference`, into `fields`. + /// + /// @return `reference` without the matched part + private static String takeFromEnd(String reference, Pattern pattern, BibEntry entry, Field... fields) { + Matcher matcher = pattern.matcher(reference); + if (!matcher.find()) { + return reference; + } + for (int i = 0; i < fields.length; i++) { + setFieldIfPresent(entry, fields[i], matcher.group(i + 1)); + } + return reference.substring(0, matcher.start()).trim(); + } + + private static void setFieldIfPresent(BibEntry entry, Field field, @Nullable String value) { + if (value != null && !value.isBlank()) { + entry.setField(field, value.trim()); + } + } + /// @param pattern A pattern matching two groups: The first one to take, the second one to leave at the end of the string private static EntryUpdateResult updateEntryAndReferenceIfMatches(String reference, Pattern pattern, BibEntry result, Field field) { diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java index 9b37d595684..58d59e19d33 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java @@ -269,6 +269,112 @@ void ieeePaper() throws URISyntaxException { assertEquals(List.of(ALVER2007, ALVER2007A, KOPP2012, KOPPP2018, KOENIG2023), parserResult.getDatabase().getEntries()); } + @Test + void biblatexAlphabetic() throws URISyntaxException { + // [utest->req~import.pdf.references.biblatex~1] + Path file = Path.of(RuleBasedBibliographyPdfImporterTest.class.getResource("/pdfs/biblatex/alphabetic.pdf").toURI()); + ParserResult parserResult = ruleBasedBibliographyPdfImporter.importDatabase(file); + + BibEntry adr26 = new BibEntry(StandardEntryType.Article) + .withCitationKey("adr26") + .withField(StandardField.AUTHOR, "adr") + .withField(StandardField.TITLE, "Architectural Decision Records") + .withField(StandardField.YEAR, "2026") + .withField(StandardField.URL, "https://adr.github.io") + .withField(StandardField.COMMENT, "[adr26] adr. “Architectural Decision Records”. In: (2026). url: https:// adr.github.io."); + BibEntry al26 = new BibEntry(StandardEntryType.Article) + .withCitationKey("AL26") + .withField(StandardField.AUTHOR, "Aisha Alansari and Hamzah Luqman") + .withField(StandardField.TITLE, "Large language models hallucination: A comprehensive survey") + .withField(StandardField.JOURNAL, "Computer Science Review") + .withField(StandardField.VOLUME, "61") + .withField(StandardField.YEAR, "2026") + .withField(StandardField.PAGES, "100970") + .withField(StandardField.ISSN, "1574-0137") + .withField(StandardField.DOI, "https://doi.org/10.1016/j.cosrev.2026.100970") + .withField(StandardField.URL, "https://www.sciencedirect.com/science/article/pii/S157401372600078X") + .withField(StandardField.COMMENT, "[AL26] Aisha Alansari and Hamzah Luqman. “Large language models hal- lucination: A comprehensive survey”. In: Computer Science Review 61 (2026), p. 100970. issn: 1574-0137. doi: https://doi.org/10. 1016/j.cosrev.2026.100970. url: https://www.sciencedirect. com/science/article/pii/S157401372600078X."); + BibEntry atn25 = new BibEntry(StandardEntryType.Article) + .withCitationKey("ATN25") + .withField(StandardField.AUTHOR, "Dang Anh-Hoang and others") + .withField(StandardField.TITLE, "Survey and analysis of hallucinations in large language models: attribution to prompting strategies or model behavior") + .withField(StandardField.JOURNAL, "Frontiers in Artificial Intelligence Volume 8 - 2025") + .withField(StandardField.YEAR, "2025") + .withField(StandardField.ISSN, "2624-8212") + .withField(StandardField.DOI, "10.3389/frai.2025.1622292") + .withField(StandardField.URL, "https://www.frontiersin.org/journals/artificial-intelligence/articles/10.3389/frai.2025.1622292") + .withField(StandardField.COMMENT, "[ATN25] Dang Anh-Hoang et al. “Survey and analysis of hallucinations in large language models: attribution to prompting strategies or model behavior”. In: Frontiers in Artificial Intelligence Volume 8 - 2025 (2025). issn: 2624-8212. doi: 10.3389/frai.2025.1622292. url: https://www.frontiersin.org/journals/artificial-intelligence/ articles/10.3389/frai.2025.1622292."); + BibEntry buc23 = new BibEntry(StandardEntryType.Article) + .withCitationKey("Buc+23") + .withField(StandardField.AUTHOR, "Georg Buchgeher and others") + .withField(StandardField.TITLE, "Using Architecture Decision Records in Open Source Projects—An MSR Study on GitHub") + .withField(StandardField.JOURNAL, "IEEE Access") + .withField(StandardField.VOLUME, "11") + .withField(StandardField.YEAR, "2023") + .withField(StandardField.PAGES, "63725-63740") + .withField(StandardField.DOI, "10.1109/ACCESS.2023.3287654") + .withField(StandardField.COMMENT, "[Buc+23] Georg Buchgeher et al. “Using Architecture Decision Records in Open Source Projects—An MSR Study on GitHub”. In: IEEE Access 11 (2023), pp. 63725–63740. doi: 10.1109/ACCESS.2023.3287654."); + BibEntry jb05 = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("JB05") + .withField(StandardField.AUTHOR, "A. Jansen and J. Bosch") + .withField(StandardField.TITLE, "Software Architecture as a Set of Architectural Design Decisions") + .withField(StandardField.BOOKTITLE, "5th Working IEEE/IFIP Conference on Software Architecture (WICSA’05)") + .withField(StandardField.YEAR, "2005") + .withField(StandardField.PAGES, "109-120") + .withField(StandardField.DOI, "10.1109/WICSA.2005.61") + .withField(StandardField.COMMENT, "[JB05] A. Jansen and J. Bosch. “Software Architecture as a Set of Ar- chitectural Design Decisions”. In: 5th Working IEEE/IFIP Confer- ence on Software Architecture (WICSA’05). 2005, pp. 109–120. doi: 10.1109/WICSA.2005.61."); + BibEntry ka19 = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("KA19") + .withField(StandardField.AUTHOR, "Oliver Kopp and Anita Armbruster") + .withField(StandardField.TITLE, "Generalized Markdown Architectural Decision Records: Capturing the Essence of Decisions") + .withField(StandardField.BOOKTITLE, "Proceedings of the 11th Central European Workshop on Services and their Composition") + .withField(StandardField.VOLUME, "2339") + .withField(StandardField.SERIES, "CEUR Workshop Proceedings") + .withField(StandardField.PUBLISHER, "CEUR-WS.org") + .withField(StandardField.YEAR, "2019") + .withField(StandardField.PAGES, "55-57") + .withField(StandardField.URL, "https://ceur-ws.org/Vol-2339/paper11.pdf") + .withField(StandardField.COMMENT, "[KA19] Oliver Kopp and Anita Armbruster. “Generalized Markdown Ar- chitectural Decision Records: Capturing the Essence of Decisions”. In: Proceedings of the 11th Central European Workshop on Services and their Composition. Vol. 2339. CEUR Workshop Proceedings. CEUR-WS.org, 2019, pp. 55–57. url: https://ceur-ws.org/Vol- 2339/paper11.pdf."); + BibEntry kaz18 = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("KAZ18") + .withField(StandardField.AUTHOR, "Oliver Kopp and others") + .withField(StandardField.TITLE, "Markdown Architectural Decision Records: Format and Tool Support") + .withField(StandardField.BOOKTITLE, "Central-European Workshop on Services and their Composition") + .withField(StandardField.YEAR, "2018") + .withField(StandardField.URL, "https://api.semanticscholar.org/CorpusID:4802254") + .withField(StandardField.COMMENT, "[KAZ18] Oliver Kopp et al. “Markdown Architectural Decision Records: For- mat and Tool Support”. In: Central-European Workshop on Services and their Composition. 2018. url: https://api.semanticscholar. org/CorpusID:4802254."); + BibEntry kop18 = new BibEntry(StandardEntryType.Misc) + .withCitationKey("Kop+18") + .withField(StandardField.AUTHOR, "Oliver Kopp and others") + .withField(StandardField.TITLE, "Markdown Architectural Decision Records (MADR) Version 1.4.0") + .withField(StandardField.YEAR, "2018") + .withField(StandardField.URL, "https://github.com/adr/madr/tree/release/v1") + .withField(StandardField.COMMENT, "[Kop+18] Oliver Kopp et al. Markdown Architectural Decision Records (MADR) Version 1.4.0. 2018. url: https://github.com/adr/madr/tree/ release/v1 (visited on 06/15/2026). 1"); + + assertEquals(List.of(adr26, al26, atn25, buc23, jb05, ka19, kaz18, kop18), parserResult.getDatabase().getEntries()); + } + + @Test + void biblatexInProceedingsWithEditorsSeriesAndLocation() { + // [utest->req~import.pdf.references.biblatex~1] + BibEntry expected = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("Mül+20") + .withField(StandardField.AUTHOR, "Hans Müller and Eva Schmidt and Jan Kurz") + .withField(StandardField.TITLE, "A Title") + .withField(StandardField.BOOKTITLE, "Proceedings of Something") + .withField(StandardField.EDITOR, "Anna Weber and Bernd Koch") + .withField(StandardField.VOLUME, "12") + .withField(StandardField.SERIES, "LNCS") + .withField(StandardField.LOCATION, "Berlin") + .withField(StandardField.PUBLISHER, "Springer") + .withField(StandardField.YEAR, "2020") + .withField(StandardField.PAGES, "1-10") + .withField(StandardField.DOI, "10.1007/978-3-030-00000-0_1") + .withField(StandardField.COMMENT, "[Mül+20] Hans Müller, Eva Schmidt, and Jan Kurz. “A Title”. In: Proceedings of Something. Ed. by Anna Weber and Bernd Koch. Vol. 12. LNCS. Berlin: Springer, 2020, pp. 1–10. doi: 10.1007/978-3-030-00000-0_1."); + List intermediateData = RuleBasedBibliographyPdfImporter.getIntermediateData(expected.getField(StandardField.COMMENT).get()); + assertEquals(expected, ruleBasedBibliographyPdfImporter.parsePlainCitation(intermediateData.getFirst().label(), intermediateData.getFirst().reference())); + } + static Stream references() { return Stream.of( KOENIG2023, @@ -297,6 +403,6 @@ void references(BibEntry expectedEntry) { List intermediateDataList = RuleBasedBibliographyPdfImporter.getIntermediateData(expectedEntry.getField(StandardField.COMMENT).get()); assertEquals(1, intermediateDataList.size()); RuleBasedBibliographyPdfImporter.IntermediateData intermediateData = intermediateDataList.getFirst(); - assertEquals(expectedEntry, ruleBasedBibliographyPdfImporter.parsePlainCitation(intermediateData.number(), intermediateData.reference())); + assertEquals(expectedEntry, ruleBasedBibliographyPdfImporter.parsePlainCitation(intermediateData.label(), intermediateData.reference())); } } diff --git a/jablib/src/test/resources/pdfs/biblatex/alphabetic.bib b/jablib/src/test/resources/pdfs/biblatex/alphabetic.bib new file mode 100644 index 00000000000..4c1e959e3e2 --- /dev/null +++ b/jablib/src/test/resources/pdfs/biblatex/alphabetic.bib @@ -0,0 +1,75 @@ +@Article{AL26, + author = {Aisha Alansari and Hamzah Luqman}, + title = {Large language models hallucination: A comprehensive survey}, + journal = {Computer Science Review}, + year = {2026}, + volume = {61}, + pages = {100970}, + issn = {1574-0137}, + doi = {https://doi.org/10.1016/j.cosrev.2026.100970}, + url = {https://www.sciencedirect.com/science/article/pii/S157401372600078X}, +} + +@Article{ATN25, + author = {Dang Anh-Hoang and Vu Tran and Le-Minh Nguyen}, + title = {Survey and analysis of hallucinations in large language models: attribution to prompting strategies or model behavior}, + journal = {Frontiers in Artificial Intelligence Volume 8 - 2025}, + year = {2025}, + issn = {2624-8212}, + doi = {10.3389/frai.2025.1622292}, + url = {https://www.frontiersin.org/journals/artificial-intelligence/articles/10.3389/frai.2025.1622292}, +} + +@Article{Buc+23, + author = {Georg Buchgeher and Stefan Schöberl and Verena Geist and Bernhard Dorninger and Philipp Haindl and Rainer Weinreich}, + title = {Using Architecture Decision Records in Open Source Projects---An MSR Study on GitHub}, + journal = {IEEE Access}, + year = {2023}, + volume = {11}, + pages = {63725--63740}, + doi = {10.1109/ACCESS.2023.3287654}, +} + +@InProceedings{JB05, + author = {Jansen, A. and Bosch, J.}, + title = {Software Architecture as a Set of Architectural Design Decisions}, + booktitle = {5th Working IEEE/IFIP Conference on Software Architecture (WICSA'05)}, + year = {2005}, + pages = {109--120}, + doi = {10.1109/WICSA.2005.61}, +} + +@InProceedings{KA19, + author = {Oliver Kopp and Anita Armbruster}, + title = {Generalized Markdown Architectural Decision Records: Capturing the Essence of Decisions}, + booktitle = {Proceedings of the 11th Central European Workshop on Services and their Composition}, + year = {2019}, + volume = {2339}, + series = {CEUR Workshop Proceedings}, + publisher = {CEUR-WS.org}, + pages = {55--57}, + url = {https://ceur-ws.org/Vol-2339/paper11.pdf}, +} + +@InProceedings{KAZ18, + author = {Oliver Kopp and Anita Armbruster and Olaf Zimmermann}, + title = {Markdown Architectural Decision Records: Format and Tool Support}, + booktitle = {Central-European Workshop on Services and their Composition}, + year = {2018}, + url = {https://api.semanticscholar.org/CorpusID:4802254}, +} + +@Misc{Kop+18, + author = {Oliver Kopp and Anita Armbruster and Olaf Zimmermann and Stefan Kapferer}, + title = {Markdown Architectural Decision Records (MADR) Version 1.4.0}, + year = {2018}, + url = {https://github.com/adr/madr/tree/release/v1}, + urldate = {2026-06-15}, +} + +@Article{adr26, + author = {adr}, + title = {Architectural Decision Records}, + year = {2026}, + url = {https://adr.github.io}, +} diff --git a/jablib/src/test/resources/pdfs/biblatex/alphabetic.pdf b/jablib/src/test/resources/pdfs/biblatex/alphabetic.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ad1bdac0e19e3dce458bcefdf2455d2454846b75 GIT binary patch literal 29911 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 G{(k{dJa#Dn literal 0 HcmV?d00001 diff --git a/jablib/src/test/resources/pdfs/biblatex/alphabetic.tex b/jablib/src/test/resources/pdfs/biblatex/alphabetic.tex new file mode 100644 index 00000000000..301b31a51e0 --- /dev/null +++ b/jablib/src/test/resources/pdfs/biblatex/alphabetic.tex @@ -0,0 +1,9 @@ +% Source of alphabetic.pdf: lualatex alphabetic && biber alphabetic && lualatex alphabetic +\documentclass{article} +\usepackage[style=alphabetic,maxnames=2]{biblatex} +\usepackage{hyperref} +\addbibresource{alphabetic.bib} +\begin{document} +\nocite{*} +\printbibliography +\end{document} From f8e0ffaf23a229fae59e6cbf8907185e71c05e9e Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 11:40:16 +0200 Subject: [PATCH 2/4] Add CHANGELOG entry Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93eef956b96..47a885c89db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a "Jump to field" button to the entry editor toolbar, triggering the same action as the Ctrl+J shortcut. [#16169](https://github.com/JabRef/jabref/pull/16169) - The `jabkit` `--input` option (and positional input argument) now accepts http(s)/ftp URLs, downloading the file before processing. [#16165](https://github.com/JabRef/jabref/pull/16165) - We added a `jabkit pdf extract-references` command that extracts the references from the end of one or more PDFs and outputs them as BibTeX, mirroring the GUI's "Extract references" action. Each PDF can be given as a local file or as an http(s)/ftp URL. [#16186](https://github.com/JabRef/jabref/pull/16186) +- Extracting references from a PDF now recognizes bibliographies typeset with biblatex, including alphabetic labels such as `[AL26]`. [#775](https://github.com/JabRef/jabref-koppor/pull/775) - We added a PDF preview with extracted metadata in the unlinked files dialog. [#16159](https://github.com/JabRef/jabref/issues/16159) - We added a `HayagrivaImporter`, allowing users to import bibliographic entries from Hayagriva YAML files (used by Typst). [#15714](https://github.com/JabRef/jabref/issues/15714) - We added autofocus and clipboard prefill to the commit message field in the Git Commit dialog. [#16340](https://github.com/JabRef/jabref/issues/16340) From 563f6fae2664b27648ca04f6c6dfc3c7d2b35e2e Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 12:36:38 +0200 Subject: [PATCH 3/4] Move CHANGELOG entry to Unreleased Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47a885c89db..5040238ae97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added - We added `jabkit git merge-driver`, a Git merge driver that merges `.bib` files semantically. [#16838](https://github.com/JabRef/jabref/pull/16838) +- Extracting references from a PDF now recognizes bibliographies typeset with biblatex, including alphabetic labels such as `[AL26]`. [#775](https://github.com/JabRef/jabref-koppor/pull/775) ### Changed @@ -90,7 +91,6 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a "Jump to field" button to the entry editor toolbar, triggering the same action as the Ctrl+J shortcut. [#16169](https://github.com/JabRef/jabref/pull/16169) - The `jabkit` `--input` option (and positional input argument) now accepts http(s)/ftp URLs, downloading the file before processing. [#16165](https://github.com/JabRef/jabref/pull/16165) - We added a `jabkit pdf extract-references` command that extracts the references from the end of one or more PDFs and outputs them as BibTeX, mirroring the GUI's "Extract references" action. Each PDF can be given as a local file or as an http(s)/ftp URL. [#16186](https://github.com/JabRef/jabref/pull/16186) -- Extracting references from a PDF now recognizes bibliographies typeset with biblatex, including alphabetic labels such as `[AL26]`. [#775](https://github.com/JabRef/jabref-koppor/pull/775) - We added a PDF preview with extracted metadata in the unlinked files dialog. [#16159](https://github.com/JabRef/jabref/issues/16159) - We added a `HayagrivaImporter`, allowing users to import bibliographic entries from Hayagriva YAML files (used by Typst). [#15714](https://github.com/JabRef/jabref/issues/15714) - We added autofocus and clipboard prefill to the commit message field in the Git Commit dialog. [#16340](https://github.com/JabRef/jabref/issues/16340) From 610ff431572a03d581edcfa1593aab2d498f0bfa Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 12:43:48 +0200 Subject: [PATCH 4/4] Narrow reference extraction requirement to labelled reference lists Splitting also handles alpha.bst's "[BSG+ 23]" labels; fields are read for IEEE and biblatex layouts only; author-year lists are unsupported. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/requirements/import.md | 13 ++++++++---- .../pdf/RuleBasedBibliographyPdfImporter.java | 11 ++++++---- .../RuleBasedBibliographyPdfImporterTest.java | 21 +++++++++++++++++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/docs/requirements/import.md b/docs/requirements/import.md index 1b40d1d129e..ed9e443fab2 100644 --- a/docs/requirements/import.md +++ b/docs/requirements/import.md @@ -41,11 +41,16 @@ A file that parses with warnings is not affected: it still opens, and its warnin Needs: impl, utest -## Rule-based reference extraction reads the biblatex standard styles -`req~import.pdf.references.biblatex~1` +## Rule-based reference extraction reads labelled BibTeX reference lists +`req~import.pdf.references.labelled~1` -The rule-based extraction of a PDF's reference list recognizes references typeset with the biblatex standard styles, labelled numerically (`[12]`) or alphabetically (`[AL26]`, `[Buc+23]`). -Field values such as the DOI are kept as printed, and each entry carries the printed reference in `comment`, so faults of the typeset list stay visible. +The rule-based extraction splits a PDF's reference list into references at their labels, as typeset by BibTeX and biblatex styles: numeric (`[12]`) or alphabetic (`[AL26]`, `[Buc+23]`, `[BSG+ 23]`). +Each entry carries the printed reference in `comment`. + +Fields are read for two layouts: IEEE (`J. Knaster et al., “Title”, Nucl. Fusion, vol. 57, …`) and the biblatex standard styles (`Authors. “Title”. In: …`). +There, field values such as the DOI are kept as printed, so faults of the typeset list stay visible. + +Reference lists without labels, such as those of author-year styles, are not supported. Needs: impl, utest diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java index b63581367af..0cc40269419 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporter.java @@ -47,8 +47,11 @@ public class RuleBasedBibliographyPdfImporter extends BibliographyFromPdfImporte private static final Logger LOGGER = LoggerFactory.getLogger(RuleBasedBibliographyPdfImporter.class); - /// Numeric labels (`[12]`) and biblatex-alphabetic labels (`[AL26]`, `[Buc+23]`, `[adr26]`, `[Kop+18a]`) - private static final String LABEL = "\\[(\\d+|\\p{L}[\\p{L}'-]*\\+?\\d{2}[a-z]?)\\]"; + /// Numeric labels (`[12]`) and alphabetic labels of BibTeX and biblatex styles (`[AL26]`, `[Buc+23]`, `[adr26]`, `[Kop+18a]`). + /// BibTeX's `alpha.bst` typesets "et al." as a raised "+", which the PDF text shows as `[BSG+ 23]`. + /// + /// [impl->req~import.pdf.references.labelled~1] + private static final String LABEL = "\\[(\\d+|\\p{L}[\\p{L}'-]*(?:\\+ ?)?\\d{2}[a-z]?)\\]"; private static final Pattern REFERENCE_PATTERN = Pattern.compile(LABEL + "(.*?)(?=" + LABEL + "|$)", Pattern.DOTALL); private static final Pattern YEAR_AT_END = Pattern.compile(", (\\d{4})\\.$"); private static final Pattern YEAR = Pattern.compile(", (\\d{4})(.*)"); @@ -143,7 +146,7 @@ static List getIntermediateData(String contents) { Matcher matcher = REFERENCE_PATTERN.matcher(contents); while (matcher.find()) { String reference = matcher.group(2).replaceAll("\\r?\\n", " ").trim(); - referencesStrings.add(new IntermediateData(matcher.group(1), reference)); + referencesStrings.add(new IntermediateData(matcher.group(1).replace(" ", ""), reference)); } return referencesStrings; } @@ -363,7 +366,7 @@ private static String normalizeAuthors(String authorsText) { /// Parses a reference formatted by one of the biblatex standard styles. /// - /// [impl->req~import.pdf.references.biblatex~1] + /// [impl->req~import.pdf.references.labelled~1] /// /// Example: `Aisha Alansari and Hamzah Luqman. “Large language models hallucination: A comprehensive survey”. In: Computer Science Review 61 (2026), p. 100970. issn: 1574-0137. doi: 10.1016/j.cosrev.2026.100970.` private static BibEntry parseBiblatexCitation(String label, String reference) { diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java index 58d59e19d33..300ebe60e06 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/RuleBasedBibliographyPdfImporterTest.java @@ -271,7 +271,7 @@ void ieeePaper() throws URISyntaxException { @Test void biblatexAlphabetic() throws URISyntaxException { - // [utest->req~import.pdf.references.biblatex~1] + // [utest->req~import.pdf.references.labelled~1] Path file = Path.of(RuleBasedBibliographyPdfImporterTest.class.getResource("/pdfs/biblatex/alphabetic.pdf").toURI()); ParserResult parserResult = ruleBasedBibliographyPdfImporter.importDatabase(file); @@ -356,7 +356,7 @@ void biblatexAlphabetic() throws URISyntaxException { @Test void biblatexInProceedingsWithEditorsSeriesAndLocation() { - // [utest->req~import.pdf.references.biblatex~1] + // [utest->req~import.pdf.references.labelled~1] BibEntry expected = new BibEntry(StandardEntryType.InProceedings) .withCitationKey("Mül+20") .withField(StandardField.AUTHOR, "Hans Müller and Eva Schmidt and Jan Kurz") @@ -375,6 +375,23 @@ void biblatexInProceedingsWithEditorsSeriesAndLocation() { assertEquals(expected, ruleBasedBibliographyPdfImporter.parsePlainCitation(intermediateData.getFirst().label(), intermediateData.getFirst().reference())); } + @Test + void splitsAtBibtexAlphaLabels() { + // [utest->req~import.pdf.references.labelled~1] + String contents = """ + [AL26] Aisha Alansari and Hamzah Luqman. Large language models hallucination: A comprehensive survey. Computer Science Review, + 61:100970, 2026. + [BSG+ 23] Georg Buchgeher, Stefan Schöberl, Verena Geist, Bernhard + Dorninger, Philipp Haindl, and Rainer Weinreich. Using architecture + decision records in open source projects—an msr study on github. + IEEE Access, 11:63725–63740, 2023. + """; + assertEquals(List.of( + new RuleBasedBibliographyPdfImporter.IntermediateData("AL26", "Aisha Alansari and Hamzah Luqman. Large language models hallucination: A comprehensive survey. Computer Science Review, 61:100970, 2026."), + new RuleBasedBibliographyPdfImporter.IntermediateData("BSG+23", "Georg Buchgeher, Stefan Schöberl, Verena Geist, Bernhard Dorninger, Philipp Haindl, and Rainer Weinreich. Using architecture decision records in open source projects—an msr study on github. IEEE Access, 11:63725–63740, 2023.")), + RuleBasedBibliographyPdfImporter.getIntermediateData(contents)); + } + static Stream references() { return Stream.of( KOENIG2023,