diff --git a/CHANGELOG.md b/CHANGELOG.md index 93eef956b96..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 diff --git a/docs/requirements/import.md b/docs/requirements/import.md index 9a250e5bd3b..ed9e443fab2 100644 --- a/docs/requirements/import.md +++ b/docs/requirements/import.md @@ -41,4 +41,17 @@ A file that parses with warnings is not affected: it still opens, and its warnin Needs: impl, utest +## Rule-based reference extraction reads labelled BibTeX reference lists +`req~import.pdf.references.labelled~1` + +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 59800cdf172..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 @@ -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,12 @@ 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 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})(.*)"); private static final Pattern PAGES = Pattern.compile(", pp\\. (\\d+--?\\d+)\\.?(.*)"); @@ -60,6 +67,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 +128,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 +136,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(); } @@ -125,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; } @@ -137,18 +158,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 +252,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 +348,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.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) { + 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..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 @@ -269,6 +269,129 @@ 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.labelled~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.labelled~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())); + } + + @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, @@ -297,6 +420,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 00000000000..ad1bdac0e19 Binary files /dev/null and b/jablib/src/test/resources/pdfs/biblatex/alphabetic.pdf differ 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}