From 1d4907ed9d6df0c3137bcbcf7092a03ab23cbd0f Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 13 Jul 2026 17:44:34 +0200 Subject: [PATCH 001/102] Replace Hayagriva layout exporter with a programmatic writer Extract the importer's field mapping into HayagrivaMapping and add HayagrivaEntryWriter, whose read-modify-write merge only rewrites the YAML paths whose value actually changed, so structured titles, person details, foreign serial-number schemes, and unknown keys survive a rewrite. Register the new HayagrivaExporter instead of the layout template and drop the hayagrivayaml layout files and the HayagrivaType layout formatter. Groundwork for directory-as-library (PLAN.md on branch directory-as-library), which will persist per-PDF Hayagriva sidecars. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 1 + .../logic/exporter/ExporterFactory.java | 2 +- .../logic/exporter/HayagrivaEntryWriter.java | 300 ++++++++++++++++ .../logic/exporter/HayagrivaExporter.java | 65 ++++ .../fileformat/HayagrivaImporter.java | 198 +--------- .../importer/fileformat/HayagrivaMapping.java | 261 ++++++++++++++ .../org/jabref/logic/layout/LayoutEntry.java | 3 - .../logic/layout/format/HayagrivaType.java | 37 -- .../layout/hayagrivayaml.begin.layout | 0 .../resource/layout/hayagrivayaml.end.layout | 0 .../resource/layout/hayagrivayaml.layout | 147 -------- .../exporter/HayagrivaEntryWriterTest.java | 199 +++++++++++ .../logic/exporter/HayagrivaExporterTest.java | 252 +++++++++++++ .../exporter/HayagrivaYamlExporterTest.java | 337 ------------------ .../fileformat/HayagrivaImporterTest.java | 27 +- .../fileformat/HayagrivaRoundTripTest.java | 127 +++++++ 16 files changed, 1217 insertions(+), 739 deletions(-) create mode 100644 jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java create mode 100644 jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java create mode 100644 jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java delete mode 100644 jablib/src/main/java/org/jabref/logic/layout/format/HayagrivaType.java delete mode 100644 jablib/src/main/resources/resource/layout/hayagrivayaml.begin.layout delete mode 100644 jablib/src/main/resources/resource/layout/hayagrivayaml.end.layout delete mode 100644 jablib/src/main/resources/resource/layout/hayagrivayaml.layout create mode 100644 jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java create mode 100644 jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java delete mode 100644 jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java create mode 100644 jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 580de82195d6..727b22c35c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed +- The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [TODO] - The Hayagriva YAML exporter now writes all fields the new Hayagriva importer reads. [#16190](https://github.com/JabRef/jabref/pull/16190) - We increased the size of arrow buttons in the Entry Preview preferences tab to improve visibility. [#16028](https://github.com/JabRef/jabref/issues/16028) - We now render the entry preview with regular JavaFX components instead of an embedded browser. Text in the preview can be selected with mouse and keyboard and copied; without a selection, "Copy selection" copies the whole preview. [#16145](https://github.com/JabRef/jabref/pull/16145) diff --git a/jablib/src/main/java/org/jabref/logic/exporter/ExporterFactory.java b/jablib/src/main/java/org/jabref/logic/exporter/ExporterFactory.java index c4c2a562f750..83a5a557a1c0 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/ExporterFactory.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/ExporterFactory.java @@ -54,7 +54,7 @@ public static ExporterFactory create(CliPreferences preferences) { exporters.add(new TemplateExporter("RIS", "ris", "ris", "ris", StandardFileType.RIS, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); exporters.add(new TemplateExporter("MIS Quarterly", "misq", "misq", "misq", StandardFileType.RTF, layoutPreferences, saveOrder)); exporters.add(new TemplateExporter("CSL YAML", "yaml", "yaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); - exporters.add(new TemplateExporter("Hayagriva YAML", "hayagrivayaml", "hayagrivayaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); + exporters.add(new HayagrivaExporter()); exporters.add(new OpenOfficeDocumentCreator()); exporters.add(new OpenDocumentSpreadsheetCreator()); exporters.add(new MSBibExporter()); diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java new file mode 100644 index 000000000000..d451b4e3365b --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java @@ -0,0 +1,300 @@ +package org.jabref.logic.exporter; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.SequencedMap; +import java.util.Set; + +import org.jabref.logic.importer.fileformat.HayagrivaMapping; +import org.jabref.model.entry.AuthorList; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.StandardEntryType; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.dataformat.yaml.YAMLMapper; +import tools.jackson.dataformat.yaml.YAMLWriteFeature; + +/// Serializes [BibEntry]s to Hayagriva YAML, symmetric to +/// [org.jabref.logic.importer.fileformat.HayagrivaImporter] via the shared [HayagrivaMapping]. +/// +/// The core operation is a read-modify-write merge: [#mergeIntoNode] diffs the entry against +/// what the given YAML node currently yields on import and only rewrites the YAML paths whose +/// value actually changed. Everything JabRef does not own survives a rewrite — structured titles +/// (`{value, short}` keeps its `short`), person details (`prefix`/`suffix`/`alias`), additional +/// `serial-number` schemes, and entry fields unknown to JabRef. Known losses: YAML `#` comments +/// (dropped by the parser) and details of a `parent` node once one of its source fields +/// (journal/series/booktitle) changes, because parents are then re-synthesized from the entry. +@NullMarked +public class HayagrivaEntryWriter { + + /// When the entry has a journal, these keys describe the journal and belong inside the + /// synthesized `periodical` parent instead of the entry itself. + private static final Set JOURNAL_RELOCATED_KEYS = Set.of("volume", "issue", "publisher"); + + private static final YAMLMapper MAPPER = new YAMLMapper(YAMLFactory.builder() + .disable(YAMLWriteFeature.WRITE_DOC_START_MARKER) + .enable(YAMLWriteFeature.MINIMIZE_QUOTES) + .build()); + + public String serialize(SequencedMap keyedEntries) { + ObjectNode root = MAPPER.createObjectNode(); + keyedEntries.forEach((citationKey, entry) -> root.set(citationKey, toEntryNode(entry))); + return MAPPER.writeValueAsString(root); + } + + public ObjectNode toEntryNode(BibEntry entry) { + return mergeIntoNode(entry, MAPPER.createObjectNode()); + } + + /// Merges the entry into an existing Hayagriva entry node (read-modify-write, see class doc). + /// On an empty node this produces a fresh entry in canonical key order. + public ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { + BibEntry current = HayagrivaMapping.toBibEntry("current", node); + + mergeType(node, entry); + mergeFormattable(node, "title", StandardField.TITLE, entry, current); + mergePersons(node, "author", StandardField.AUTHOR, entry, current); + mergePersons(node, "editor", StandardField.EDITOR, entry, current); + + boolean hasJournal = entry.hasField(StandardField.JOURNAL); + HayagrivaMapping.SCALAR_FIELDS.forEach((key, field) -> { + if (hasJournal && JOURNAL_RELOCATED_KEYS.contains(key)) { + node.remove(key); + } else { + mergeScalar(node, key, field, entry, current); + } + }); + + mergeFormattable(node, "url", StandardField.URL, entry, current); + mergeSerialNumber(node, entry, current); + removeStaleSerialFallback(node, entry, current); + mergeAffiliated(node, entry, current); + mergeParent(node, entry, current); + return node; + } + + private void mergeType(ObjectNode node, BibEntry entry) { + @Nullable EntryType currentType = HayagrivaMapping.scalarText(node.get("type")) + .map(type -> HayagrivaMapping.TYPE_TO_ENTRY_TYPE.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) + .orElse(null); + // An equivalent existing type string (e.g. `post`, importing as Misc) is kept as-is + if (!entry.getType().equals(currentType)) { + node.put("type", HayagrivaMapping.ENTRY_TYPE_TO_TYPE.getOrDefault(entry.getType(), "misc")); + } + } + + private void mergeScalar(ObjectNode node, String key, Field field, BibEntry entry, BibEntry current) { + Optional target = entry.getField(field); + if (target.equals(current.getField(field))) { + return; + } + target.ifPresentOrElse(value -> node.put(key, value), () -> node.remove(key)); + } + + private void mergeFormattable(ObjectNode node, String key, Field field, BibEntry entry, BibEntry current) { + Optional target = entry.getField(field); + if (target.equals(current.getField(field))) { + return; + } + target.ifPresentOrElse(value -> { + if (node.get(key) instanceof ObjectNode structured) { + structured.put("value", value); + } else { + node.put(key, value); + } + }, () -> node.remove(key)); + } + + private void mergePersons(ObjectNode node, String key, Field field, BibEntry entry, BibEntry current) { + Optional target = entry.getField(field); + if (target.equals(current.getField(field))) { + return; + } + target.ifPresentOrElse(value -> node.set(key, personList(value)), () -> node.remove(key)); + } + + private JsonNode personList(String bibtexPersons) { + ArrayNode names = MAPPER.createArrayNode(); + AuthorList.parse(bibtexPersons).getAuthors() + .forEach(author -> names.add(author.getFamilyGiven(false))); + return names; + } + + private void mergeSerialNumber(ObjectNode node, BibEntry entry, BibEntry current) { + boolean changed = !arxivEprint(entry).equals(arxivEprint(current)) + || HayagrivaMapping.SERIAL_NUMBER_FIELDS.values().stream() + .anyMatch(field -> !entry.getField(field).equals(current.getField(field))); + if (!changed) { + return; + } + + JsonNode existing = node.get("serial-number"); + ObjectNode serialNumber; + if (existing instanceof ObjectNode existingObject) { + serialNumber = existingObject; + } else { + serialNumber = MAPPER.createObjectNode(); + // A bare serial number is preserved under its explicit key when the node has to + // become a map to hold further identifiers + HayagrivaMapping.scalarText(existing).ifPresent(bare -> serialNumber.put("serial", bare)); + } + + HayagrivaMapping.SERIAL_NUMBER_FIELDS.forEach((key, field) -> + entry.getField(field).ifPresentOrElse(value -> serialNumber.put(key, value), () -> serialNumber.remove(key))); + arxivEprint(entry).ifPresentOrElse(value -> serialNumber.put("arxiv", value), () -> serialNumber.remove("arxiv")); + + if (serialNumber.isEmpty()) { + node.remove("serial-number"); + } else { + node.set("serial-number", serialNumber); + } + } + + /// A cleared NUMBER must also clear the `serial-number` fallbacks it may have been imported + /// from, otherwise the deletion would be undone on the next import. + private void removeStaleSerialFallback(ObjectNode node, BibEntry entry, BibEntry current) { + if (entry.hasField(StandardField.NUMBER) || current.getField(StandardField.NUMBER).isEmpty()) { + return; + } + JsonNode serialNumber = node.get("serial-number"); + if (serialNumber instanceof ObjectNode serialNumberObject) { + serialNumberObject.remove("serial"); + if (serialNumberObject.isEmpty()) { + node.remove("serial-number"); + } + } else if (serialNumber != null) { + node.remove("serial-number"); + } + } + + private Optional arxivEprint(BibEntry entry) { + boolean isArxiv = entry.getField(StandardField.EPRINTTYPE) + .map(eprintType -> "arxiv".equalsIgnoreCase(eprintType)) + .orElse(false); + return isArxiv ? entry.getField(StandardField.EPRINT) : Optional.empty(); + } + + private void mergeAffiliated(ObjectNode node, BibEntry entry, BibEntry current) { + SequencedMap existingByRole = new LinkedHashMap<>(); + if (node.get("affiliated") instanceof ArrayNode existing) { + for (JsonNode item : existing.values()) { + if (item.isObject()) { + HayagrivaMapping.scalarText(item.get("role")) + .ifPresent(role -> existingByRole.putIfAbsent(role.toLowerCase(Locale.ROOT), item)); + } + } + } + // Existing roles first, so untouched items keep their position; spec roles cover fields + // added in JabRef + LinkedHashSet roles = new LinkedHashSet<>(existingByRole.keySet()); + roles.addAll(HayagrivaMapping.AFFILIATED_ROLES); + + boolean changed = false; + List result = new ArrayList<>(); + for (String role : roles) { + Field field = FieldFactory.parseField(role); + Optional target = entry.getField(field); + if (target.equals(current.getField(field))) { + JsonNode item = existingByRole.get(role); + if (item != null) { + result.add(item); + } + continue; + } + changed = true; + target.ifPresent(value -> { + ObjectNode item = MAPPER.createObjectNode(); + item.put("role", role); + item.set("names", personList(value)); + result.add(item); + }); + } + + if (!changed) { + return; + } + if (result.isEmpty()) { + node.remove("affiliated"); + return; + } + ArrayNode affiliated = MAPPER.createArrayNode(); + result.forEach(affiliated::add); + node.set("affiliated", affiliated); + } + + private void mergeParent(ObjectNode node, BibEntry entry, BibEntry current) { + boolean journalDetailsChanged = entry.hasField(StandardField.JOURNAL) + && (fieldDiffers(StandardField.VOLUME, entry, current) + || fieldDiffers(StandardField.NUMBER, entry, current) + || fieldDiffers(StandardField.PUBLISHER, entry, current)); + boolean changed = fieldDiffers(StandardField.JOURNAL, entry, current) + || fieldDiffers(StandardField.SERIES, entry, current) + || fieldDiffers(StandardField.BOOKTITLE, entry, current) + || journalDetailsChanged; + if (!changed) { + return; + } + + List parents = new ArrayList<>(); + entry.getField(StandardField.JOURNAL).ifPresent(journal -> { + ObjectNode parent = MAPPER.createObjectNode(); + parent.put("type", "periodical"); + parent.put("title", journal); + entry.getField(StandardField.VOLUME).ifPresent(volume -> parent.put("volume", volume)); + entry.getField(StandardField.NUMBER).ifPresent(issue -> parent.put("issue", issue)); + entry.getField(StandardField.PUBLISHER).ifPresent(publisher -> parent.put("publisher", publisher)); + parents.add(parent); + }); + entry.getField(StandardField.BOOKTITLE).ifPresent(booktitle -> { + ObjectNode parent = MAPPER.createObjectNode(); + parent.put("type", booktitleParentType(entry.getType())); + parent.put("title", booktitle); + parents.add(parent); + }); + // For book parts a `book` parent re-imports as booktitle, so a series written next to a + // booktitle survives in the YAML but not a re-import (documented asymmetry) + entry.getField(StandardField.SERIES).ifPresent(series -> { + ObjectNode parent = MAPPER.createObjectNode(); + parent.put("type", "book"); + parent.put("title", series); + parents.add(parent); + }); + + if (parents.isEmpty()) { + node.remove("parent"); + } else if (parents.size() == 1) { + node.set("parent", parents.getFirst()); + } else { + ArrayNode parentArray = MAPPER.createArrayNode(); + parents.forEach(parentArray::add); + node.set("parent", parentArray); + } + } + + private String booktitleParentType(EntryType entryType) { + if (StandardEntryType.InProceedings == entryType) { + return "proceedings"; + } + if (StandardEntryType.InBook == entryType) { + return "book"; + } + return "anthology"; + } + + private boolean fieldDiffers(Field field, BibEntry entry, BibEntry current) { + return !entry.getField(field).equals(current.getField(field)); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java new file mode 100644 index 000000000000..2439dfca0228 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java @@ -0,0 +1,65 @@ +package org.jabref.logic.exporter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedMap; + +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import tools.jackson.core.JacksonException; + +import static java.util.function.Predicate.not; + +/// Exports entries as one Hayagriva YAML document (a map of citation key to entry), delegating +/// the field mapping to [HayagrivaEntryWriter] so that exports read back losslessly through the +/// Hayagriva importer. +@NullMarked +public class HayagrivaExporter extends Exporter { + + private final HayagrivaEntryWriter entryWriter = new HayagrivaEntryWriter(); + + public HayagrivaExporter() { + super("hayagrivayaml", "Hayagriva YAML", StandardFileType.YAML); + } + + @Override + public void export(@NonNull BibDatabaseContext databaseContext, Path file, @NonNull List entries) throws IOException { + if (entries.isEmpty()) { + return; + } + try { + Files.writeString(file, entryWriter.serialize(keyedUniquely(entries)), StandardCharsets.UTF_8); + } catch (JacksonException e) { + throw new IOException("Could not serialize entries to Hayagriva YAML", e); + } + } + + /// The top-level YAML keys are the citation keys, which YAML requires to be unique within + /// the document; duplicates and missing keys get a positional fallback. + private SequencedMap keyedUniquely(List entries) { + SequencedMap keyedEntries = new LinkedHashMap<>(); + int position = 0; + for (BibEntry entry : entries) { + position++; + String key = entry.getCitationKey() + .map(String::trim) + .filter(not(String::isEmpty)) + .orElse("entry-" + position); + String uniqueKey = key; + int suffix = 1; + while (keyedEntries.containsKey(uniqueKey)) { + uniqueKey = key + "-" + suffix++; + } + keyedEntries.put(uniqueKey, entry); + } + return keyedEntries; + } +} diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java index 1a6ca2f5a8cc..8dd42894fc21 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java @@ -7,11 +7,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.jabref.logic.importer.Importer; import org.jabref.logic.importer.ParserResult; @@ -19,14 +17,8 @@ import org.jabref.logic.util.FileType; import org.jabref.logic.util.StandardFileType; import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.FieldFactory; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.field.UnknownField; -import org.jabref.model.entry.types.EntryType; -import org.jabref.model.entry.types.StandardEntryType; import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; import tools.jackson.core.JacksonException; import tools.jackson.core.StreamReadFeature; import tools.jackson.databind.JsonNode; @@ -38,29 +30,16 @@ /// `url` may be a string or a map with a `value` key, `author`/`editor` may be a string, a list of /// strings, or a list of maps, `parent` may be a map or a list of maps, and `serial-number` may be /// a scalar or a map. Parsing therefore works on the untyped node tree instead of fixed POJOs, -/// extracting what JabRef can represent and ignoring the rest. +/// extracting what JabRef can represent and ignoring the rest. The actual field mapping lives in +/// [HayagrivaMapping], shared with the Hayagriva writer. @NullMarked public class HayagrivaImporter extends Importer { - private static final Map TYPES_MAP = Map.ofEntries( - Map.entry("anthology", StandardEntryType.Collection), - Map.entry("anthos", StandardEntryType.InCollection), - Map.entry("article", StandardEntryType.Article), - Map.entry("book", StandardEntryType.Book), - Map.entry("chapter", StandardEntryType.InBook), - Map.entry("conference", StandardEntryType.InProceedings), - Map.entry("misc", StandardEntryType.Misc), - Map.entry("proceedings", StandardEntryType.Proceedings), - Map.entry("reference", StandardEntryType.Reference), - Map.entry("report", StandardEntryType.Report), - Map.entry("thesis", StandardEntryType.Thesis), - Map.entry("web", StandardEntryType.Online) - ); - /// All entry types of the Hayagriva specification /// (); used for - /// format recognition. Types without a BibTeX equivalent in [#TYPES_MAP] are imported as - /// [StandardEntryType#Misc]. + /// format recognition. Types without a BibTeX equivalent in + /// [HayagrivaMapping#TYPE_TO_ENTRY_TYPE] are imported as + /// [org.jabref.model.entry.types.StandardEntryType#Misc]. private static final Set RECOGNIZED_TYPES = Set.of( "anthology", "anthos", "article", "artwork", "audio", "blog", "book", "case", "chapter", "conference", "entry", "exhibition", "legislation", "manuscript", "misc", @@ -68,10 +47,6 @@ public class HayagrivaImporter extends Importer { "reference", "report", "repository", "scene", "thesis", "thread", "video", "web" ); - private static final Set JOURNAL_PARENT_TYPES = Set.of("periodical", "newspaper", "blog"); - private static final Set BOOKTITLE_PARENT_TYPES = Set.of("proceedings", "anthology", "conference"); - private static final Set BOOK_PART_TYPES = Set.of(StandardEntryType.InBook, StandardEntryType.InCollection, StandardEntryType.InProceedings); - /// Generous room for leading comments plus the first entry, whose `type` key conventionally /// appears within its first few lines. private static final int MAX_LOOKAHEAD_CHARS = 3_000; @@ -98,172 +73,13 @@ public ParserResult importDatabase(BufferedReader input) throws IOException { for (Map.Entry property : root.properties()) { JsonNode entryNode = property.getValue(); if (entryNode.isObject()) { - bibEntries.add(toBibEntry(property.getKey(), entryNode)); + bibEntries.add(HayagrivaMapping.toBibEntry(property.getKey(), entryNode)); } } return new ParserResult(bibEntries); } - private BibEntry toBibEntry(String citationKey, JsonNode entryNode) { - EntryType entryType = scalarText(entryNode.get("type")) - .map(type -> TYPES_MAP.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) - .orElse(StandardEntryType.Misc); - BibEntry bibEntry = new BibEntry(entryType); - bibEntry.setCitationKey(citationKey); - - formattedText(entryNode.get("title")).ifPresent(title -> bibEntry.setField(StandardField.TITLE, title)); - formattedText(entryNode.get("url")).ifPresent(url -> bibEntry.setField(StandardField.URL, url)); - // A leading `~` (Hayagriva's approximate-date marker) is kept: JabRef stores the raw - // string even when it cannot parse it into a structured date, so nothing is lost. - scalarText(entryNode.get("date")).ifPresent(date -> bibEntry.setField(StandardField.DATE, date)); - persons(entryNode.get("author")).ifPresent(authors -> bibEntry.setField(StandardField.AUTHOR, authors)); - persons(entryNode.get("editor")).ifPresent(editors -> bibEntry.setField(StandardField.EDITOR, editors)); - scalarText(entryNode.get("volume")).ifPresent(volume -> bibEntry.setField(StandardField.VOLUME, volume)); - scalarText(entryNode.get("issue")).ifPresent(issue -> bibEntry.setField(StandardField.NUMBER, issue)); - scalarText(entryNode.get("page-range")).ifPresent(pages -> bibEntry.setField(StandardField.PAGES, pages)); - scalarText(entryNode.get("chapter")).ifPresent(chapter -> bibEntry.setField(StandardField.CHAPTER, chapter)); - scalarText(entryNode.get("edition")).ifPresent(edition -> bibEntry.setField(StandardField.EDITION, edition)); - scalarText(entryNode.get("publisher")).ifPresent(publisher -> bibEntry.setField(StandardField.PUBLISHER, publisher)); - scalarText(entryNode.get("location")).ifPresent(location -> bibEntry.setField(StandardField.ADDRESS, location)); - scalarText(entryNode.get("organization")).ifPresent(organization -> bibEntry.setField(StandardField.INSTITUTION, organization)); - scalarText(entryNode.get("note")).ifPresent(note -> bibEntry.setField(StandardField.NOTE, note)); - scalarText(entryNode.get("abstract")).ifPresent(abstractText -> bibEntry.setField(StandardField.ABSTRACT, abstractText)); - scalarText(entryNode.get("language")).ifPresent(language -> bibEntry.setField(StandardField.LANGUAGE, language)); - scalarText(entryNode.get("page-total")).ifPresent(pageTotal -> bibEntry.setField(StandardField.PAGETOTAL, pageTotal)); - scalarText(entryNode.get("volume-total")).ifPresent(volumeTotal -> bibEntry.setField(StandardField.VOLUMES, volumeTotal)); - scalarText(entryNode.get("runtime")).ifPresent(runtime -> bibEntry.setField(new UnknownField("runtime"), runtime)); - scalarText(entryNode.get("time-range")).ifPresent(timeRange -> bibEntry.setField(new UnknownField("time-range"), timeRange)); - - applyAffiliated(bibEntry, entryNode.get("affiliated")); - - JsonNode serialNumber = entryNode.get("serial-number"); - if (serialNumber != null && serialNumber.isObject()) { - scalarText(serialNumber.get("doi")).ifPresent(doi -> bibEntry.setField(StandardField.DOI, doi)); - scalarText(serialNumber.get("isbn")).ifPresent(isbn -> bibEntry.setField(StandardField.ISBN, isbn)); - scalarText(serialNumber.get("issn")).ifPresent(issn -> bibEntry.setField(StandardField.ISSN, issn)); - scalarText(serialNumber.get("pmid")).ifPresent(pmid -> bibEntry.setField(StandardField.PMID, pmid)); - scalarText(serialNumber.get("arxiv")).ifPresent(arxiv -> { - bibEntry.setField(StandardField.EPRINT, arxiv); - bibEntry.setField(StandardField.EPRINTTYPE, "arxiv"); - }); - scalarText(serialNumber.get("serial")).ifPresent(serial -> setIfAbsent(bibEntry, StandardField.NUMBER, serial)); - } else { - // A bare serial number (an RFC number, a court docket, a version string, ...) has no - // dedicated BibTeX field; number is the closest match, but an explicit issue wins - scalarText(serialNumber).ifPresent(serial -> setIfAbsent(bibEntry, StandardField.NUMBER, serial)); - } - - applyParents(bibEntry, entryNode.get("parent")); - - return bibEntry; - } - - /// A Hayagriva `parent` describes what an entry is contained in and has no direct BibTeX - /// equivalent. The parent's `type` picks the target field, mirroring the Hayagriva exporter - /// layout where possible (journal is exported as a `periodical` parent, series as a `book` - /// parent). A missing or unmapped parent type falls back by entry type: articles are most - /// likely contained in a journal, everything else in some book-like container. The first - /// parent providing a value for a field wins; nested parents are not descended into. - private void applyParents(BibEntry bibEntry, @Nullable JsonNode parentNode) { - if (parentNode == null) { - return; - } - - List parents = parentNode.isArray() ? List.copyOf(parentNode.values()) : List.of(parentNode); - for (JsonNode parent : parents) { - if (!parent.isObject()) { - continue; - } - String parentType = scalarText(parent.get("type")).map(type -> type.toLowerCase(Locale.ROOT)).orElse(""); - - StandardField targetField; - if (JOURNAL_PARENT_TYPES.contains(parentType)) { - targetField = StandardField.JOURNAL; - } else if ("book".equals(parentType)) { - targetField = BOOK_PART_TYPES.contains(bibEntry.getType()) ? StandardField.BOOKTITLE : StandardField.SERIES; - } else if (BOOKTITLE_PARENT_TYPES.contains(parentType)) { - targetField = StandardField.BOOKTITLE; - } else { - targetField = bibEntry.getType() == StandardEntryType.Article ? StandardField.JOURNAL : StandardField.BOOKTITLE; - } - - formattedText(parent.get("title")).ifPresent(title -> setIfAbsent(bibEntry, targetField, title)); - if (targetField == StandardField.JOURNAL) { - scalarText(parent.get("volume")).ifPresent(volume -> setIfAbsent(bibEntry, StandardField.VOLUME, volume)); - scalarText(parent.get("issue")).ifPresent(issue -> setIfAbsent(bibEntry, StandardField.NUMBER, issue)); - scalarText(parent.get("publisher")).ifPresent(publisher -> setIfAbsent(bibEntry, StandardField.PUBLISHER, publisher)); - } - } - } - - /// `affiliated` lists further people by their role (director, translator, ...). Roles with a - /// matching BibTeX field (e.g. translator) land there; the rest are kept as custom fields - /// named after the role, so no contributor information is lost on import. - private void applyAffiliated(BibEntry bibEntry, @Nullable JsonNode affiliatedNode) { - if (affiliatedNode == null || !affiliatedNode.isArray()) { - return; - } - for (JsonNode affiliation : affiliatedNode.values()) { - if (!affiliation.isObject()) { - continue; - } - scalarText(affiliation.get("role")).ifPresent(role -> - persons(affiliation.get("names")).ifPresent(names -> - bibEntry.setField(FieldFactory.parseField(role.toLowerCase(Locale.ROOT)), names))); - } - } - - private static void setIfAbsent(BibEntry entry, StandardField field, String value) { - if (!entry.hasField(field)) { - entry.setField(field, value); - } - } - - /// Persons may be a single scalar, a list of scalars, a map, or a list of maps. - private Optional persons(@Nullable JsonNode node) { - if (node == null) { - return Optional.empty(); - } - List personNodes = node.isArray() ? List.copyOf(node.values()) : List.of(node); - String formatted = personNodes.stream() - .map(this::formatPerson) - .flatMap(Optional::stream) - .collect(Collectors.joining(" and ")); - return formatted.isEmpty() ? Optional.empty() : Optional.of(formatted); - } - - /// Hayagriva person strings already use BibTeX's structured "Family, Given" form and are kept - /// verbatim; the structured map form is converted to it. - private Optional formatPerson(JsonNode personNode) { - if (personNode.isObject()) { - // Structured form: `name` is the family name, next to optional `given-name`, `prefix`, `suffix`, `alias` - return scalarText(personNode.get("name")) - .map(name -> scalarText(personNode.get("given-name")) - .map(givenName -> name + ", " + givenName) - .orElse(name)); - } - return scalarText(personNode); - } - - /// Returns the text of a scalar node (string, number, boolean); empty for containers and null. - private static Optional scalarText(@Nullable JsonNode node) { - if (node == null || node.isNull() || node.isObject() || node.isArray()) { - return Optional.empty(); - } - String text = node.asString().trim(); - return text.isEmpty() ? Optional.empty() : Optional.of(text); - } - - /// Reads a Hayagriva "formattable string": either a plain scalar or a map holding the actual - /// string under `value` (next to `short`, `verbatim`, ... which JabRef cannot represent). - private static Optional formattedText(@Nullable JsonNode node) { - if (node != null && node.isObject()) { - return scalarText(node.get("value")); - } - return scalarText(node); - } - @Override public String getId() { return "hayagrivayaml"; @@ -326,7 +142,7 @@ public boolean isRecognizedFormat(Reader input) throws IOException { } for (JsonNode entryNode : root.values()) { - if (entryNode.isObject() && scalarText(entryNode.get("type")) + if (entryNode.isObject() && HayagrivaMapping.scalarText(entryNode.get("type")) .map(type -> RECOGNIZED_TYPES.contains(type.toLowerCase(Locale.ROOT))) .orElse(false)) { return true; diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java new file mode 100644 index 000000000000..222a18284b1f --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java @@ -0,0 +1,261 @@ +package org.jabref.logic.importer.fileformat; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.SequencedMap; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.StandardEntryType; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; + +/// Field and type mapping between the Hayagriva YAML format +/// () and [BibEntry], shared by +/// [HayagrivaImporter] and [org.jabref.logic.exporter.HayagrivaEntryWriter] so that reading and +/// writing stay symmetric. +@NullMarked +public final class HayagrivaMapping { + + public static final Map TYPE_TO_ENTRY_TYPE = Map.ofEntries( + Map.entry("anthology", StandardEntryType.Collection), + Map.entry("anthos", StandardEntryType.InCollection), + Map.entry("article", StandardEntryType.Article), + Map.entry("book", StandardEntryType.Book), + Map.entry("chapter", StandardEntryType.InBook), + Map.entry("conference", StandardEntryType.InProceedings), + Map.entry("misc", StandardEntryType.Misc), + Map.entry("proceedings", StandardEntryType.Proceedings), + Map.entry("reference", StandardEntryType.Reference), + Map.entry("report", StandardEntryType.Report), + Map.entry("thesis", StandardEntryType.Thesis), + Map.entry("web", StandardEntryType.Online) + ); + + /// Inverse of [#TYPE_TO_ENTRY_TYPE], extended by BibTeX types whose closest Hayagriva type + /// re-imports as a different (but equivalent) BibTeX type, e.g. TechReport -> `report` -> + /// Report. Unlisted types are written as `misc`. + public static final Map ENTRY_TYPE_TO_TYPE; + + public static final Set JOURNAL_PARENT_TYPES = Set.of("periodical", "newspaper", "blog"); + public static final Set BOOKTITLE_PARENT_TYPES = Set.of("proceedings", "anthology", "conference"); + public static final Set BOOK_PART_TYPES = Set.of(StandardEntryType.InBook, StandardEntryType.InCollection, StandardEntryType.InProceedings); + + public static final Field RUNTIME_FIELD = new UnknownField("runtime"); + public static final Field TIME_RANGE_FIELD = new UnknownField("time-range"); + + /// Hayagriva keys holding a plain scalar, with their BibEntry field, in canonical write + /// order. `title` and `url` are "formattable strings" (scalar or map with a `value` key) and + /// are handled separately, as are the person lists and `serial-number`. + public static final SequencedMap SCALAR_FIELDS; + + /// `serial-number` sub-keys with a dedicated BibEntry field. `arxiv` is handled separately + /// because it involves the EPRINT/EPRINTTYPE field pair. + public static final SequencedMap SERIAL_NUMBER_FIELDS; + + /// The roles enumerated by the Hayagriva specification. Both directions use the role name as + /// the BibEntry field name; [#applyAffiliated] additionally accepts arbitrary role names. + public static final List AFFILIATED_ROLES = List.of( + "translator", "afterword", "foreword", "introduction", "annotator", "commentator", + "holder", "compiler", "founder", "collaborator", "organizer", "cast-member", + "composer", "producer", "executive-producer", "writer", "cinematography", "director", + "illustrator", "narrator"); + + static { + Map inverseTypes = new HashMap<>(); + TYPE_TO_ENTRY_TYPE.forEach((hayagrivaType, entryType) -> inverseTypes.put(entryType, hayagrivaType)); + inverseTypes.put(StandardEntryType.TechReport, "report"); + inverseTypes.put(StandardEntryType.MastersThesis, "thesis"); + inverseTypes.put(StandardEntryType.PhdThesis, "thesis"); + inverseTypes.put(StandardEntryType.WWW, "web"); + inverseTypes.put(StandardEntryType.Conference, "conference"); + ENTRY_TYPE_TO_TYPE = Map.copyOf(inverseTypes); + + SequencedMap scalars = new LinkedHashMap<>(); + scalars.put("date", StandardField.DATE); + scalars.put("language", StandardField.LANGUAGE); + scalars.put("publisher", StandardField.PUBLISHER); + scalars.put("location", StandardField.ADDRESS); + scalars.put("organization", StandardField.INSTITUTION); + scalars.put("edition", StandardField.EDITION); + scalars.put("volume", StandardField.VOLUME); + scalars.put("volume-total", StandardField.VOLUMES); + scalars.put("issue", StandardField.NUMBER); + scalars.put("chapter", StandardField.CHAPTER); + scalars.put("page-range", StandardField.PAGES); + scalars.put("page-total", StandardField.PAGETOTAL); + scalars.put("runtime", RUNTIME_FIELD); + scalars.put("time-range", TIME_RANGE_FIELD); + scalars.put("note", StandardField.NOTE); + scalars.put("abstract", StandardField.ABSTRACT); + SCALAR_FIELDS = Collections.unmodifiableSequencedMap(scalars); + + SequencedMap serialNumbers = new LinkedHashMap<>(); + serialNumbers.put("doi", StandardField.DOI); + serialNumbers.put("isbn", StandardField.ISBN); + serialNumbers.put("issn", StandardField.ISSN); + serialNumbers.put("pmid", StandardField.PMID); + SERIAL_NUMBER_FIELDS = Collections.unmodifiableSequencedMap(serialNumbers); + } + + private HayagrivaMapping() { + } + + public static BibEntry toBibEntry(String citationKey, JsonNode entryNode) { + EntryType entryType = scalarText(entryNode.get("type")) + .map(type -> TYPE_TO_ENTRY_TYPE.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) + .orElse(StandardEntryType.Misc); + BibEntry bibEntry = new BibEntry(entryType); + bibEntry.setCitationKey(citationKey); + + formattedText(entryNode.get("title")).ifPresent(title -> bibEntry.setField(StandardField.TITLE, title)); + formattedText(entryNode.get("url")).ifPresent(url -> bibEntry.setField(StandardField.URL, url)); + persons(entryNode.get("author")).ifPresent(authors -> bibEntry.setField(StandardField.AUTHOR, authors)); + persons(entryNode.get("editor")).ifPresent(editors -> bibEntry.setField(StandardField.EDITOR, editors)); + + // A leading `~` (Hayagriva's approximate-date marker) is kept in `date`: JabRef stores + // the raw string even when it cannot parse it into a structured date, so nothing is lost. + SCALAR_FIELDS.forEach((key, field) -> + scalarText(entryNode.get(key)).ifPresent(value -> bibEntry.setField(field, value))); + + applyAffiliated(bibEntry, entryNode.get("affiliated")); + applySerialNumber(bibEntry, entryNode.get("serial-number")); + applyParents(bibEntry, entryNode.get("parent")); + + return bibEntry; + } + + private static void applySerialNumber(BibEntry bibEntry, @Nullable JsonNode serialNumber) { + if (serialNumber != null && serialNumber.isObject()) { + SERIAL_NUMBER_FIELDS.forEach((key, field) -> + scalarText(serialNumber.get(key)).ifPresent(value -> bibEntry.setField(field, value))); + scalarText(serialNumber.get("arxiv")).ifPresent(arxiv -> { + bibEntry.setField(StandardField.EPRINT, arxiv); + bibEntry.setField(StandardField.EPRINTTYPE, "arxiv"); + }); + scalarText(serialNumber.get("serial")).ifPresent(serial -> setIfAbsent(bibEntry, StandardField.NUMBER, serial)); + } else { + // A bare serial number (an RFC number, a court docket, a version string, ...) has no + // dedicated BibTeX field; number is the closest match, but an explicit issue wins + scalarText(serialNumber).ifPresent(serial -> setIfAbsent(bibEntry, StandardField.NUMBER, serial)); + } + } + + /// A Hayagriva `parent` describes what an entry is contained in and has no direct BibTeX + /// equivalent. The parent's `type` picks the target field, mirroring what the writer emits + /// (journal as a `periodical` parent, series as a `book` parent). A missing or unmapped + /// parent type falls back by entry type: articles are most likely contained in a journal, + /// everything else in some book-like container. The first parent providing a value for a + /// field wins; nested parents are not descended into. + static void applyParents(BibEntry bibEntry, @Nullable JsonNode parentNode) { + if (parentNode == null) { + return; + } + + List parents = parentNode.isArray() ? List.copyOf(parentNode.values()) : List.of(parentNode); + for (JsonNode parent : parents) { + if (!parent.isObject()) { + continue; + } + String parentType = scalarText(parent.get("type")).map(type -> type.toLowerCase(Locale.ROOT)).orElse(""); + + StandardField targetField; + if (JOURNAL_PARENT_TYPES.contains(parentType)) { + targetField = StandardField.JOURNAL; + } else if ("book".equals(parentType)) { + targetField = BOOK_PART_TYPES.contains(bibEntry.getType()) ? StandardField.BOOKTITLE : StandardField.SERIES; + } else if (BOOKTITLE_PARENT_TYPES.contains(parentType)) { + targetField = StandardField.BOOKTITLE; + } else { + targetField = bibEntry.getType() == StandardEntryType.Article ? StandardField.JOURNAL : StandardField.BOOKTITLE; + } + + formattedText(parent.get("title")).ifPresent(title -> setIfAbsent(bibEntry, targetField, title)); + if (targetField == StandardField.JOURNAL) { + scalarText(parent.get("volume")).ifPresent(volume -> setIfAbsent(bibEntry, StandardField.VOLUME, volume)); + scalarText(parent.get("issue")).ifPresent(issue -> setIfAbsent(bibEntry, StandardField.NUMBER, issue)); + scalarText(parent.get("publisher")).ifPresent(publisher -> setIfAbsent(bibEntry, StandardField.PUBLISHER, publisher)); + } + } + } + + /// `affiliated` lists further people by their role (director, translator, ...). Roles with a + /// matching BibTeX field (e.g. translator) land there; the rest are kept as custom fields + /// named after the role, so no contributor information is lost on import. + static void applyAffiliated(BibEntry bibEntry, @Nullable JsonNode affiliatedNode) { + if (affiliatedNode == null || !affiliatedNode.isArray()) { + return; + } + for (JsonNode affiliation : affiliatedNode.values()) { + if (!affiliation.isObject()) { + continue; + } + scalarText(affiliation.get("role")).ifPresent(role -> + persons(affiliation.get("names")).ifPresent(names -> + bibEntry.setField(FieldFactory.parseField(role.toLowerCase(Locale.ROOT)), names))); + } + } + + private static void setIfAbsent(BibEntry entry, StandardField field, String value) { + if (!entry.hasField(field)) { + entry.setField(field, value); + } + } + + /// Persons may be a single scalar, a list of scalars, a map, or a list of maps. + private static Optional persons(@Nullable JsonNode node) { + if (node == null) { + return Optional.empty(); + } + List personNodes = node.isArray() ? List.copyOf(node.values()) : List.of(node); + String formatted = personNodes.stream() + .map(HayagrivaMapping::formatPerson) + .flatMap(Optional::stream) + .collect(Collectors.joining(" and ")); + return formatted.isEmpty() ? Optional.empty() : Optional.of(formatted); + } + + /// Hayagriva person strings already use BibTeX's structured "Family, Given" form and are kept + /// verbatim; the structured map form is converted to it. + private static Optional formatPerson(JsonNode personNode) { + if (personNode.isObject()) { + // Structured form: `name` is the family name, next to optional `given-name`, `prefix`, `suffix`, `alias` + return scalarText(personNode.get("name")) + .map(name -> scalarText(personNode.get("given-name")) + .map(givenName -> name + ", " + givenName) + .orElse(name)); + } + return scalarText(personNode); + } + + /// Returns the text of a scalar node (string, number, boolean); empty for containers and null. + public static Optional scalarText(@Nullable JsonNode node) { + if (node == null || node.isNull() || node.isObject() || node.isArray()) { + return Optional.empty(); + } + String text = node.asString().trim(); + return text.isEmpty() ? Optional.empty() : Optional.of(text); + } + + /// Reads a Hayagriva "formattable string": either a plain scalar or a map holding the actual + /// string under `value` (next to `short`, `verbatim`, ... which JabRef cannot represent). + public static Optional formattedText(@Nullable JsonNode node) { + if (node != null && node.isObject()) { + return scalarText(node.get("value")); + } + return scalarText(node); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/jablib/src/main/java/org/jabref/logic/layout/LayoutEntry.java index 250e3e989468..eae9fa02a9bb 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/jablib/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -53,7 +53,6 @@ import org.jabref.logic.layout.format.GetOpenOfficeType; import org.jabref.logic.layout.format.HTMLChars; import org.jabref.logic.layout.format.HTMLParagraphs; -import org.jabref.logic.layout.format.HayagrivaType; import org.jabref.logic.layout.format.IfPlural; import org.jabref.logic.layout.format.Iso690FormatDate; import org.jabref.logic.layout.format.Iso690NamesAuthors; @@ -561,8 +560,6 @@ private LayoutFormatter getLayoutFormatterByName(String name) { new ShortMonthFormatter(); case "ReplaceWithEscapedDoubleQuotes" -> new ReplaceWithEscapedDoubleQuotes(); - case "HayagrivaType" -> - new HayagrivaType(); default -> null; }; diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/HayagrivaType.java b/jablib/src/main/java/org/jabref/logic/layout/format/HayagrivaType.java deleted file mode 100644 index 1a5c9e3d9927..000000000000 --- a/jablib/src/main/java/org/jabref/logic/layout/format/HayagrivaType.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.jabref.logic.layout.format; - -import org.jabref.logic.layout.LayoutFormatter; -import org.jabref.model.entry.types.StandardEntryType; - -public class HayagrivaType implements LayoutFormatter { - - @Override - public String format(String value) { - return switch (StandardEntryType.valueOf(value)) { - case Article, - Conference -> - "article"; - case Book -> - "book"; - case InBook -> - "chapter"; - case Report, - TechReport -> - "report"; - case Thesis, - MastersThesis, - PhdThesis -> - "thesis"; - case WWW, - Online -> - "web"; - case Proceedings -> - "proceedings"; - case Reference -> - "reference"; - - default -> - "Misc"; - }; - } -} diff --git a/jablib/src/main/resources/resource/layout/hayagrivayaml.begin.layout b/jablib/src/main/resources/resource/layout/hayagrivayaml.begin.layout deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/jablib/src/main/resources/resource/layout/hayagrivayaml.end.layout b/jablib/src/main/resources/resource/layout/hayagrivayaml.end.layout deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/jablib/src/main/resources/resource/layout/hayagrivayaml.layout b/jablib/src/main/resources/resource/layout/hayagrivayaml.layout deleted file mode 100644 index 053fb494068b..000000000000 --- a/jablib/src/main/resources/resource/layout/hayagrivayaml.layout +++ /dev/null @@ -1,147 +0,0 @@ -\citationkey: -\begin{entrytype} type: \format[HayagrivaType]{\entrytype}\end{entrytype} -\begin{title} title: "\title"\end{title} -\begin{author} - author: - - \format[Authors(LastFirst, MiddleInitial, Sep =\n - , LastSep =\n - )]{\author} -\end{author} -\begin{date} date: \date\end{date} -\begin{journal} - parent: - type: periodical - title: \journal -\end{journal} - \begin{journal&&volume}volume: \volume\end{journal&&volume} - \begin{journal&&number}issue: \number\end{journal&&number} - \begin{journal&&publisher}publisher: \publisher\end{journal&&publisher} -\begin{series} - parent: - type: book - title: "\series" -\end{series} -\begin{editor&&number} issue: \number\end{editor&&number} -\begin{editor&&publisher} publisher: \publisher\end{editor&&publisher} -\begin{editor} editor: \editor\end{editor} -\begin{address} location: \address\end{address} -\begin{institution} organization: \institution\end{institution} -\begin{edition} edition: \edition\end{edition} -\begin{!journal&&volume} volume: \volume\end{!journal&&volume} -\begin{chapter} chapter: \chapter\end{chapter} -\begin{pages} page-range: \pages\end{pages} -\begin{pagetotal} page-total: \pagetotal\end{pagetotal} -\begin{volumes} volume-total: \volumes\end{volumes} -\begin{language} language: \language\end{language} -\begin{runtime} runtime: "\runtime"\end{runtime} -\begin{time-range} time-range: "\format{\time-range}"\end{time-range} -\begin{url} url: \url\end{url} -\begin{doi||isbn||issn||pmid} - serial-number: -\end{doi||isbn||issn||pmid} -\begin{doi} doi: "\doi"\end{doi} -\begin{isbn} isbn: "\isbn"\end{isbn} -\begin{issn} issn: "\issn"\end{issn} -\begin{pmid} pmid: "\pmid"\end{pmid} -\begin{note} note: \note\end{note} -\begin{translator||annotator||commentator||holder||compiler||founder||collaborator||organizer||composer||producer||writer||cinematography||director||illustrator||narrator||afterword||foreword||introduction||cast-member||executive-producer} - affiliated: -\end{translator||annotator||commentator||holder||compiler||founder||collaborator||organizer||composer||producer||writer||cinematography||director||illustrator||narrator||afterword||foreword||introduction||cast-member||executive-producer} -\begin{translator} - - role: translator - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\translator} -\end{translator} -\begin{annotator} - - role: annotator - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\annotator} -\end{annotator} -\begin{commentator} - - role: commentator - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\commentator} -\end{commentator} -\begin{holder} - - role: holder - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\holder} -\end{holder} -\begin{compiler} - - role: compiler - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\compiler} -\end{compiler} -\begin{founder} - - role: founder - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\founder} -\end{founder} -\begin{collaborator} - - role: collaborator - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\collaborator} -\end{collaborator} -\begin{organizer} - - role: organizer - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\organizer} -\end{organizer} -\begin{composer} - - role: composer - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\composer} -\end{composer} -\begin{producer} - - role: producer - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\producer} -\end{producer} -\begin{writer} - - role: writer - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\writer} -\end{writer} -\begin{cinematography} - - role: cinematography - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\cinematography} -\end{cinematography} -\begin{director} - - role: director - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\director} -\end{director} -\begin{illustrator} - - role: illustrator - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\illustrator} -\end{illustrator} -\begin{narrator} - - role: narrator - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\narrator} -\end{narrator} -\begin{afterword} - - role: afterword - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\afterword} -\end{afterword} -\begin{foreword} - - role: foreword - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\foreword} -\end{foreword} -\begin{introduction} - - role: introduction - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\introduction} -\end{introduction} -\begin{cast-member} - - role: cast-member - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\cast-member} -\end{cast-member} -\begin{executive-producer} - - role: executive-producer - names: - - \format[Authors(LastFirst, FullName, Sep =\n - , LastSep =\n - )]{\executive-producer} -\end{executive-producer} diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java new file mode 100644 index 000000000000..6a7fe6ca1e17 --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java @@ -0,0 +1,199 @@ +package org.jabref.logic.exporter; + +import org.jabref.logic.importer.fileformat.HayagrivaMapping; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.dataformat.yaml.YAMLMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HayagrivaEntryWriterTest { + + private final HayagrivaEntryWriter writer = new HayagrivaEntryWriter(); + private final YAMLMapper mapper = new YAMLMapper(); + + private ObjectNode parseEntryNode(String yaml) { + return (ObjectNode) mapper.readTree(yaml).get("key"); + } + + @Test + void mergePreservesUnknownKeysWhileUpdatingChangedField() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + tongus: 2 + note: old note + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.NOTE, "new note"); + + writer.mergeIntoNode(entry, node); + + assertEquals("new note", node.get("note").asString()); + assertEquals(2, node.get("tongus").asInt()); + } + + @Test + void mergeKeepsStructuredTitleFormWhenTitleChanges() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: + value: Long Original Title + short: Short Title + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.TITLE, "Corrected Title"); + + writer.mergeIntoNode(entry, node); + + assertEquals("Corrected Title", node.get("title").get("value").asString()); + assertEquals("Short Title", node.get("title").get("short").asString()); + } + + @Test + void mergeKeepsStructuredPersonFormWhenUnchanged() { + ObjectNode node = parseEntryNode(""" + key: + type: article + author: + - name: "Mädje" + given-name: "Laurenz" + alias: "laurmaedje" + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.NOTE, "unrelated change"); + + writer.mergeIntoNode(entry, node); + + assertEquals("laurmaedje", node.get("author").get(0).get("alias").asString()); + } + + @Test + void mergeRemovesKeyOfClearedField() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + note: to be removed + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.clearField(StandardField.NOTE); + + writer.mergeIntoNode(entry, node); + + assertFalse(node.has("note")); + } + + @Test + void mergeKeepsEquivalentTypeString() { + ObjectNode node = parseEntryNode(""" + key: + type: post + title: Some Title + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.NOTE, "unrelated change"); + + writer.mergeIntoNode(entry, node); + + assertEquals("post", node.get("type").asString()); + } + + @Test + void mergePreservesForeignSerialNumberSchemes() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + serial-number: + isbn: "978-0747551003" + serial: "3" + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.DOI, "10.1000/example"); + + writer.mergeIntoNode(entry, node); + + assertEquals("10.1000/example", node.get("serial-number").get("doi").asString()); + assertEquals("978-0747551003", node.get("serial-number").get("isbn").asString()); + assertEquals("3", node.get("serial-number").get("serial").asString()); + } + + @Test + void mergePreservesBareSerialNumberAsExplicitSerialWhenIdentifierIsAdded() { + ObjectNode node = parseEntryNode(""" + key: + type: reference + title: Some Title + serial-number: RFC 2845 + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.DOI, "10.1000/example"); + + writer.mergeIntoNode(entry, node); + + assertEquals("RFC 2845", node.get("serial-number").get("serial").asString()); + assertEquals("10.1000/example", node.get("serial-number").get("doi").asString()); + } + + @Test + void mergeRemovesSerialFallbackWhenNumberIsCleared() { + ObjectNode node = parseEntryNode(""" + key: + type: reference + title: Some Title + serial-number: RFC 2845 + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.clearField(StandardField.NUMBER); + + writer.mergeIntoNode(entry, node); + + assertFalse(node.has("serial-number")); + } + + @Test + void freshWriteRelocatesJournalDetailsIntoParent() { + BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) + .withField(StandardField.TITLE, "Some Title") + .withField(StandardField.JOURNAL, "Some Journal") + .withField(StandardField.VOLUME, "13") + .withField(StandardField.NUMBER, "3"); + + ObjectNode node = writer.toEntryNode(entry); + + assertFalse(node.has("volume")); + assertFalse(node.has("issue")); + assertEquals("13", node.get("parent").get("volume").asString()); + assertEquals("3", node.get("parent").get("issue").asString()); + assertEquals("Some Journal", node.get("parent").get("title").asString()); + } + + @Test + void mergeLeavesParentUntouchedWhenSourceFieldsUnchanged() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + parent: + type: periodical + title: + value: Physical Review B + verbatim: true + volume: 102 + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.NOTE, "unrelated change"); + + writer.mergeIntoNode(entry, node); + + assertTrue(node.get("parent").get("title").get("verbatim").asBoolean()); + } +} diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java new file mode 100644 index 000000000000..dec54787fe43 --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java @@ -0,0 +1,252 @@ +package org.jabref.logic.exporter; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HayagrivaExporterTest { + + private final HayagrivaExporter exporter = new HayagrivaExporter(); + private final BibDatabaseContext databaseContext = new BibDatabaseContext(); + + private List export(List entries, Path tempDir) throws IOException { + Path file = tempDir.resolve("export.yml"); + Files.createFile(file); + exporter.export(databaseContext, file, entries); + return Files.readAllLines(file); + } + + @Test + void exportForNoEntriesWritesNothing(@TempDir Path tempDir) throws IOException { + assertEquals(List.of(), export(List.of(), tempDir)); + } + + @Test + void exportsCorrectContent(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Author, Test") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.URL, "http://example.com") + .withField(StandardField.DATE, "2020-10-14"); + + List expected = List.of( + "test:", + " type: article", + " title: Test Title", + " author:", + " - \"Author, Test\"", + " date: 2020-10-14", + " url: http://example.com"); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void exportsCorrectMultipleAuthors(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Author, Test and One, Other") + .withField(StandardField.TITLE, "Test Title"); + + List expected = List.of( + "test:", + " type: article", + " title: Test Title", + " author:", + " - \"Author, Test\"", + " - \"One, Other\""); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void unmappedTypeExportsAsLowercaseMisc(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Dataset) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test Title"); + + List expected = List.of( + "test:", + " type: misc", + " title: Test Title"); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void exportsNonAsciiContentVerbatim(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "潤一郎, 谷崎") + .withField(StandardField.TITLE, "細雪"); + + List expected = List.of( + "test:", + " type: article", + " title: 細雪", + " author:", + " - \"潤一郎, 谷崎\""); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void journalBecomesPeriodicalParentCarryingVolumeIssueAndPublisher(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.JOURNAL, "Physical Review B") + .withField(StandardField.VOLUME, "102") + .withField(StandardField.NUMBER, "16") + .withField(StandardField.PUBLISHER, "American Physical Society"); + + List expected = List.of( + "test:", + " type: article", + " title: Test Title", + " parent:", + " type: periodical", + " title: Physical Review B", + " volume: 102", + " issue: 16", + " publisher: American Physical Society"); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void booktitleBecomesProceedingsParentForInProceedings(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.BOOKTITLE, "Proceedings of Testing"); + + List expected = List.of( + "test:", + " type: conference", + " title: Test Title", + " parent:", + " type: proceedings", + " title: Proceedings of Testing"); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void seriesBecomesBookParent(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.SERIES, "Annals of Mathematics Studies"); + + List expected = List.of( + "test:", + " type: book", + " title: Test Title", + " parent:", + " type: book", + " title: Annals of Mathematics Studies"); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void exportsMultipleEntriesAsSingleYamlDocument(@TempDir Path tempDir) throws IOException { + BibEntry first = new BibEntry(StandardEntryType.Article) + .withCitationKey("first") + .withField(StandardField.TITLE, "First Title"); + BibEntry second = new BibEntry(StandardEntryType.Article) + .withCitationKey("second") + .withField(StandardField.TITLE, "Second Title"); + + List expected = List.of( + "first:", + " type: article", + " title: First Title", + "second:", + " type: article", + " title: Second Title"); + + assertEquals(expected, export(List.of(first, second), tempDir)); + } + + @Test + void exportsIdentifiersNestedUnderSerialNumber(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DOI, "10.1109/EDOC.2018.00030") + .withField(StandardField.ISSN, "0896-3207"); + + List expected = List.of( + "test:", + " type: article", + " title: Test Title", + " serial-number:", + " doi: 10.1109/EDOC.2018.00030", + " issn: 0896-3207"); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void exportsAffiliatedRoles(@TempDir Path tempDir) throws IOException { + BibEntry entry = new BibEntry(StandardEntryType.Misc) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.TRANSLATOR, "Translator, Some") + .withField(new UnknownField("director"), "Director, Other"); + + List expected = List.of( + "test:", + " type: misc", + " title: Test Title", + " affiliated:", + " - role: translator", + " names:", + " - \"Translator, Some\"", + " - role: director", + " names:", + " - \"Director, Other\""); + + assertEquals(expected, export(List.of(entry), tempDir)); + } + + @Test + void missingAndDuplicateCitationKeysGetUniqueKeys(@TempDir Path tempDir) throws IOException { + BibEntry unkeyed = new BibEntry(StandardEntryType.Misc) + .withField(StandardField.TITLE, "No Key"); + BibEntry first = new BibEntry(StandardEntryType.Misc) + .withCitationKey("dup") + .withField(StandardField.TITLE, "First"); + BibEntry second = new BibEntry(StandardEntryType.Misc) + .withCitationKey("dup") + .withField(StandardField.TITLE, "Second"); + + List expected = List.of( + "entry-1:", + " type: misc", + " title: No Key", + "dup:", + " type: misc", + " title: First", + "dup-1:", + " type: misc", + " title: Second"); + + assertEquals(expected, export(List.of(unkeyed, first, second), tempDir)); + } +} diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java deleted file mode 100644 index 453d462a7cd3..000000000000 --- a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java +++ /dev/null @@ -1,337 +0,0 @@ -package org.jabref.logic.exporter; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.TransformerException; - -import org.jabref.logic.layout.LayoutFormatterPreferences; -import org.jabref.logic.util.StandardFileType; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; -import org.jabref.model.metadata.SaveOrder; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; -import org.junit.jupiter.api.parallel.ResourceLock; -import org.mockito.Answers; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; - -@Execution(ExecutionMode.SAME_THREAD) -@ResourceLock("exporter") -class HayagrivaYamlExporterTest { - - private static Exporter hayagrivaYamlExporter; - private static BibDatabaseContext databaseContext; - - @BeforeAll - static void setUp() { - hayagrivaYamlExporter = new TemplateExporter( - "Hayagriva YAML", - "hayagrivayaml", - "hayagrivayaml", - null, - StandardFileType.YAML, - mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS), - SaveOrder.getDefaultSaveOrder(), - BlankLineBehaviour.DELETE_BLANKS); - - databaseContext = new BibDatabaseContext(); - } - - @Test - final void exportForNoEntriesWritesNothing(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - Path file = tempFile.resolve("ThisIsARandomlyNamedFile"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, tempFile, List.of()); - assertEquals(List.of(), Files.readAllLines(file)); - } - - @Test - final void exportsCorrectContent(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "Test Title") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"Test Title\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " url: http://example.com"); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void exportsCorrectMultipleAuthors(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author and Other One") - .withField(StandardField.TITLE, "Test Title") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"Test Title\"", - " author:", - " - Author, Test", - " - One, Other", - " date: 2020-10-14", - " url: http://example.com"); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void formatsContentCorrect(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Misc) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "Test Title") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: Misc", - " title: \"Test Title\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " url: http://example.com"); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - void passesModifiedCharset(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "谷崎 潤一郎") - .withField(StandardField.TITLE, "細雪") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"細雪\"", - " author:", - " - 潤一郎, 谷崎", - " date: 2020-10-14", - " url: http://example.com"); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - void passesModifiedCharsetNull(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "谷崎 潤一郎") - .withField(StandardField.TITLE, "細雪") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"細雪\"", - " author:", - " - 潤一郎, 谷崎", - " date: 2020-10-14", - " url: http://example.com"); - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void exportsCorrectParentField(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "Test Title") - .withField(StandardField.JOURNAL, "Test Publisher") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"Test Title\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " parent:", - " type: periodical", - " title: Test Publisher", - " url: http://example.com"); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void exportsMultipleEntriesAsSingleYamlDocument(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry first = new BibEntry(StandardEntryType.Article) - .withCitationKey("first") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "First Title") - .withField(StandardField.URL, "http://example.com/1") - .withField(StandardField.DATE, "2020-10-14"); - - BibEntry second = new BibEntry(StandardEntryType.Article) - .withCitationKey("second") - .withField(StandardField.AUTHOR, "Other One") - .withField(StandardField.TITLE, "Second Title") - .withField(StandardField.URL, "http://example.com/2") - .withField(StandardField.DATE, "2021-05-03"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(first, second)); - - List expected = List.of( - "first:", - " type: article", - " title: \"First Title\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " url: http://example.com/1", - "second:", - " type: article", - " title: \"Second Title\"", - " author:", - " - One, Other", - " date: 2021-05-03", - " url: http://example.com/2"); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void exportsDoiNestedUnderSerialNumber(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "Test Title") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14") - .withField(StandardField.DOI, "10.1109/EDOC.2018.00030"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"Test Title\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " url: http://example.com", - " serial-number:", - " doi: \"10.1109/EDOC.2018.00030\""); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void exportsIsbnNestedUnderSerialNumber(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Book) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "Test Book") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14") - .withField(StandardField.ISBN, "978-3-16-148410-0"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: book", - " title: \"Test Book\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " url: http://example.com", - " serial-number:", - " isbn: \"978-3-16-148410-0\""); - - assertEquals(expected, Files.readAllLines(file)); - } - - @Test - final void exportsIssnNestedUnderSerialNumber(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withCitationKey("test") - .withField(StandardField.AUTHOR, "Test Author") - .withField(StandardField.TITLE, "Test Article") - .withField(StandardField.URL, "http://example.com") - .withField(StandardField.DATE, "2020-10-14") - .withField(StandardField.ISSN, "0896-3207"); - - Path file = tempFile.resolve("RandomFileName"); - Files.createFile(file); - hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); - - List expected = List.of( - "test:", - " type: article", - " title: \"Test Article\"", - " author:", - " - Author, Test", - " date: 2020-10-14", - " url: http://example.com", - " serial-number:", - " issn: \"0896-3207\""); - - assertEquals(expected, Files.readAllLines(file)); - } -} - diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java index 2c27d34f30d1..1dac269e92e9 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java @@ -12,22 +12,13 @@ import java.util.Optional; import java.util.stream.Stream; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.TransformerException; - -import org.jabref.logic.exporter.BlankLineBehaviour; -import org.jabref.logic.exporter.Exporter; -import org.jabref.logic.exporter.SaveException; -import org.jabref.logic.exporter.TemplateExporter; +import org.jabref.logic.exporter.HayagrivaExporter; import org.jabref.logic.importer.ParserResult; -import org.jabref.logic.layout.LayoutFormatterPreferences; -import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; import org.jabref.model.entry.types.StandardEntryType; -import org.jabref.model.metadata.SaveOrder; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -35,35 +26,25 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Answers; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; class HayagrivaImporterTest { - private static Exporter hayagrivaYamlExporter; + private static HayagrivaExporter hayagrivaYamlExporter; private static HayagrivaImporter hayagrivaImporter; private static BibDatabaseContext databaseContext; @BeforeAll static void setUp() { - hayagrivaYamlExporter = new TemplateExporter( - "Hayagriva YAML", - "hayagrivayaml", - "hayagrivayaml", - null, - StandardFileType.YAML, - mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS), - SaveOrder.getDefaultSaveOrder(), - BlankLineBehaviour.DELETE_BLANKS); + hayagrivaYamlExporter = new HayagrivaExporter(); hayagrivaImporter = new HayagrivaImporter(); databaseContext = new BibDatabaseContext(); } - private BibEntry roundTrip(BibEntry entry, Path tempDir) throws IOException, SaveException, ParserConfigurationException, TransformerException { + private BibEntry roundTrip(BibEntry entry, Path tempDir) throws IOException { Path file = tempDir.resolve("roundtrip.yml"); Files.createFile(file); hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java new file mode 100644 index 000000000000..dc1093f1d9df --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java @@ -0,0 +1,127 @@ +package org.jabref.logic.importer.fileformat; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import org.jabref.logic.exporter.HayagrivaExporter; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/// Guards the symmetry of [HayagrivaMapping]: everything the Hayagriva writer emits must read +/// back as the identical [BibEntry], and re-exporting an imported file must be stable. +class HayagrivaRoundTripTest { + + @TempDir + Path tempDir; + + private final HayagrivaExporter exporter = new HayagrivaExporter(); + private final HayagrivaImporter importer = new HayagrivaImporter(); + + private List exportAndReimport(List entries) throws IOException { + Path file = tempDir.resolve("roundtrip-" + entries.hashCode() + ".yml"); + Files.deleteIfExists(file); + Files.createFile(file); + exporter.export(new BibDatabaseContext(), file, entries); + return importer.importDatabase(file).getDatabase().getEntries(); + } + + static Stream writerAuthoredEntries() { + return Stream.of( + Arguments.of(new BibEntry(StandardEntryType.Article) + .withCitationKey("kinetics") + .withField(StandardField.TITLE, "Kinetics and luminescence") + .withField(StandardField.AUTHOR, "Doan, T. D. and Haug, Hartmut") + .withField(StandardField.DATE, "2020-10-14") + .withField(StandardField.JOURNAL, "Physical Review B") + .withField(StandardField.VOLUME, "102") + .withField(StandardField.NUMBER, "16") + .withField(StandardField.PUBLISHER, "American Physical Society") + .withField(StandardField.PAGES, "165126-165139") + .withField(StandardField.PAGETOTAL, "13") + .withField(StandardField.DOI, "10.1103/PhysRevB.102.165126") + .withField(StandardField.NOTE, "A note on the entry") + .withField(StandardField.ABSTRACT, "An abstract text")), + Arguments.of(new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("zygos") + .withField(StandardField.TITLE, "Achieving Low Tail Latency") + .withField(StandardField.AUTHOR, "Prekas, George and Kogias, Marios") + .withField(StandardField.BOOKTITLE, "Proceedings of the 26th Symposium on Operating Systems Principles") + .withField(StandardField.PAGES, "325-341")), + Arguments.of(new BibEntry(StandardEntryType.Book) + .withCitationKey("donne") + .withField(StandardField.TITLE, "The Anniversaries") + .withField(StandardField.EDITOR, "Stringer, Gary A. and Pebworth, Ted-Larry") + .withField(StandardField.ADDRESS, "Bloomington") + .withField(StandardField.PUBLISHER, "Indiana University Press") + .withField(StandardField.EDITION, "2") + .withField(StandardField.VOLUMES, "7") + .withField(StandardField.SERIES, "The Variorum Edition") + .withField(StandardField.ISBN, "978-0747551003") + .withField(StandardField.LANGUAGE, "en-US")), + Arguments.of(new BibEntry(StandardEntryType.InBook) + .withCitationKey("lambchapter") + .withField(StandardField.TITLE, "A chapter") + .withField(StandardField.BOOKTITLE, "The life of a friendly lamb") + .withField(StandardField.CHAPTER, "3") + .withField(StandardField.PAGES, "10-12")), + Arguments.of(new BibEntry(StandardEntryType.Misc) + .withCitationKey("terminator") + .withField(StandardField.TITLE, "Terminator 2") + .withField(StandardField.PUBLISHER, "Carolco Pictures") + .withField(StandardField.TRANSLATOR, "Translator, Some") + .withField(new UnknownField("director"), "Cameron, James") + .withField(new UnknownField("cast-member"), "Schwarzenegger, Arnold and Hamilton, Linda") + .withField(new UnknownField("runtime"), "137:00") + .withField(new UnknownField("time-range"), "17:05-17:48")), + Arguments.of(new BibEntry(StandardEntryType.Online) + .withCitationKey("mattermost") + .withField(StandardField.TITLE, "Mattermost Privacy Policy") + .withField(StandardField.URL, "https://mattermost.com/privacy-policy/")), + Arguments.of(new BibEntry(StandardEntryType.Report) + .withCitationKey("unhdr") + .withField(StandardField.TITLE, "Human Development Report 2019") + .withField(StandardField.INSTITUTION, "United Nations Development Programme") + .withField(StandardField.ISSN, "2412-3129")), + Arguments.of(new BibEntry(StandardEntryType.Article) + .withCitationKey("habitable") + .withField(StandardField.TITLE, "Defining the Really Habitable Zone") + .withField(StandardField.EPRINT, "2003.13722") + .withField(StandardField.EPRINTTYPE, "arxiv") + .withField(StandardField.URL, "https://arxiv.org/abs/2003.13722"))); + } + + @ParameterizedTest + @MethodSource("writerAuthoredEntries") + void writerAuthoredEntryRoundTripsExactly(BibEntry entry) throws IOException { + assertEquals(List.of(entry), exportAndReimport(List.of(entry))); + } + + /// The upstream `basic.yml` fixture uses constructs JabRef normalizes on first import (e.g. + /// corporate author names, `serial` numbers becoming `issue`), so the guarantee starts after + /// one import/export cycle: from then on, further cycles must be the identity. + @Test + void basicFixtureIsStableAfterFirstCycle() throws IOException, URISyntaxException { + Path fixture = Path.of(HayagrivaRoundTripTest.class.getResource("basic.yml").toURI()); + List firstGeneration = importer.importDatabase(fixture).getDatabase().getEntries(); + + List secondGeneration = exportAndReimport(firstGeneration); + List thirdGeneration = exportAndReimport(secondGeneration); + + assertEquals(secondGeneration, thirdGeneration); + } +} From 36526b90a9234ab0cc2f8a90c4f695fa9d797cfc Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 13 Jul 2026 17:46:39 +0200 Subject: [PATCH 002/102] Link CHANGELOG entry to its pull request Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 727b22c35c2a..c57446dc9892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed -- The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [TODO] +- The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [koppor#736](https://github.com/JabRef/jabref-koppor/pull/736) - The Hayagriva YAML exporter now writes all fields the new Hayagriva importer reads. [#16190](https://github.com/JabRef/jabref/pull/16190) - We increased the size of arrow buttons in the Entry Preview preferences tab to improve visibility. [#16028](https://github.com/JabRef/jabref/issues/16028) - We now render the entry preview with regular JavaFX components instead of an embedded browser. Text in the preview can be selected with mouse and keyboard and copied; without a selection, "Copy selection" copies the whole preview. [#16145](https://github.com/JabRef/jabref/pull/16145) From 29028c3f9ef9d2f7edd3a01795085fb8b8b7c729 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 13 Jul 2026 22:13:15 +0200 Subject: [PATCH 003/102] Align chained calls to the IntelliJ code style Co-Authored-By: Claude Fable 5 --- .../java/org/jabref/logic/exporter/HayagrivaEntryWriter.java | 4 ++-- .../jabref/logic/importer/fileformat/HayagrivaImporter.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java index d451b4e3365b..f69cf7f7c552 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java @@ -88,8 +88,8 @@ public ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { private void mergeType(ObjectNode node, BibEntry entry) { @Nullable EntryType currentType = HayagrivaMapping.scalarText(node.get("type")) - .map(type -> HayagrivaMapping.TYPE_TO_ENTRY_TYPE.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) - .orElse(null); + .map(type -> HayagrivaMapping.TYPE_TO_ENTRY_TYPE.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) + .orElse(null); // An equivalent existing type string (e.g. `post`, importing as Misc) is kept as-is if (!entry.getType().equals(currentType)) { node.put("type", HayagrivaMapping.ENTRY_TYPE_TO_TYPE.getOrDefault(entry.getType(), "misc")); diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java index 8dd42894fc21..3d3a9370b920 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaImporter.java @@ -143,8 +143,8 @@ public boolean isRecognizedFormat(Reader input) throws IOException { for (JsonNode entryNode : root.values()) { if (entryNode.isObject() && HayagrivaMapping.scalarText(entryNode.get("type")) - .map(type -> RECOGNIZED_TYPES.contains(type.toLowerCase(Locale.ROOT))) - .orElse(false)) { + .map(type -> RECOGNIZED_TYPES.contains(type.toLowerCase(Locale.ROOT))) + .orElse(false)) { return true; } } From 050edbf0c343a10253581fbfca8969f8b68dfeac Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 13 Jul 2026 22:17:14 +0200 Subject: [PATCH 004/102] Use plain PR-number link text in CHANGELOG (heylogs forge-ref rule) Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c57446dc9892..913d835ee2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed -- The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [koppor#736](https://github.com/JabRef/jabref-koppor/pull/736) +- The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [#736](https://github.com/JabRef/jabref-koppor/pull/736) - The Hayagriva YAML exporter now writes all fields the new Hayagriva importer reads. [#16190](https://github.com/JabRef/jabref/pull/16190) - We increased the size of arrow buttons in the Entry Preview preferences tab to improve visibility. [#16028](https://github.com/JabRef/jabref/issues/16028) - We now render the entry preview with regular JavaFX components instead of an embedded browser. Text in the preview can be selected with mouse and keyboard and copied; without a selection, "Copy selection" copies the whole preview. [#16145](https://github.com/JabRef/jabref/pull/16145) From c4066e39a819547e7bf1af3e13420238fb713ea1 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 13 Jul 2026 22:26:08 +0200 Subject: [PATCH 005/102] Make tools.jackson.databind transitive in jablib The Hayagriva writer's public API in exported packages now carries JsonNode/ObjectNode types, so consumers of jablib need readability of tools.jackson.databind (flagged by checkModuleDirectivesScope). Co-Authored-By: Claude Fable 5 --- jablib/src/main/java/module-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/java/module-info.java b/jablib/src/main/java/module-info.java index e8683caefb8b..ed0d86228a19 100644 --- a/jablib/src/main/java/module-info.java +++ b/jablib/src/main/java/module-info.java @@ -200,7 +200,7 @@ // region: data mapping requires jdk.xml.dom; requires com.google.gson; - requires tools.jackson.databind; + requires transitive tools.jackson.databind; requires tools.jackson.dataformat.yaml; requires tools.jackson.core; requires transitive com.fasterxml.jackson.annotation; From 166c04e7e82c6cd6dc9a7372f6b36b843c2fa554 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Fri, 17 Jul 2026 01:35:53 +0200 Subject: [PATCH 006/102] Map comment fields and the year/month date alias in Hayagriva JabRef's Comment field and the per-user comment fields were silently dropped on export because they have no Hayagriva key; they are now written as comment/comment- extension keys, which the Hayagriva parser ignores, so the files stay loadable by Typst. Entries carrying only the BibTeX year/month fields lost their date entirely; the writer now resolves date through the field aliases. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Q5Y5GTihE9rZxKrvPqnHV4 --- CHANGELOG.md | 1 + .../logic/exporter/HayagrivaEntryWriter.java | 29 +++++++- .../importer/fileformat/HayagrivaMapping.java | 22 +++++++ .../exporter/HayagrivaEntryWriterTest.java | 66 +++++++++++++++++++ .../fileformat/HayagrivaImporterTest.java | 19 ++++++ .../fileformat/HayagrivaRoundTripTest.java | 8 ++- 6 files changed, 142 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 913d835ee2c7..caee68752e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed - The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [#736](https://github.com/JabRef/jabref-koppor/pull/736) +- Hayagriva YAML import and export now cover JabRef's "Comment" field and per-user comment fields (written as `comment`/`comment-` extension keys, which the Hayagriva parser ignores), and entries carrying only BibTeX `year`/`month` fields get their `date` written. [#736](https://github.com/JabRef/jabref-koppor/pull/736) - The Hayagriva YAML exporter now writes all fields the new Hayagriva importer reads. [#16190](https://github.com/JabRef/jabref/pull/16190) - We increased the size of arrow buttons in the Entry Preview preferences tab to improve visibility. [#16028](https://github.com/JabRef/jabref/issues/16028) - We now render the entry preview with regular JavaFX components instead of an embedded browser. Text in the preview can be selected with mouse and keyboard and copied; without a selection, "Copy selection" copies the whole preview. [#16145](https://github.com/JabRef/jabref/pull/16145) diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java index f69cf7f7c552..5b6aa3f614e4 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java @@ -5,8 +5,10 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.SequencedMap; +import java.util.SequencedSet; import java.util.Set; import org.jabref.logic.importer.fileformat.HayagrivaMapping; @@ -78,6 +80,7 @@ public ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { } }); + mergeUserComments(node, entry, current); mergeFormattable(node, "url", StandardField.URL, entry, current); mergeSerialNumber(node, entry, current); removeStaleSerialFallback(node, entry, current); @@ -97,13 +100,35 @@ private void mergeType(ObjectNode node, BibEntry entry) { } private void mergeScalar(ObjectNode node, String key, Field field, BibEntry entry, BibEntry current) { - Optional target = entry.getField(field); - if (target.equals(current.getField(field))) { + Optional target = scalarValue(entry, field); + if (target.equals(scalarValue(current, field))) { return; } target.ifPresentOrElse(value -> node.put(key, value), () -> node.remove(key)); } + /// `date` is resolved through the field aliases, so an entry carrying only the BibTeX + /// YEAR/MONTH/DAY fields still gets its `date` written (normalized, e.g. `2020-05`). + private Optional scalarValue(BibEntry entry, Field field) { + return field == StandardField.DATE ? entry.getFieldOrAlias(field) : entry.getField(field); + } + + /// JabRef's per-user comment fields are written as equally named `comment-` extension + /// keys, symmetric to [HayagrivaMapping#applyUserComments]. + private void mergeUserComments(ObjectNode node, BibEntry entry, BibEntry current) { + SequencedSet keys = new LinkedHashSet<>(); + for (Map.Entry property : node.properties()) { + if (property.getKey().startsWith(HayagrivaMapping.USER_COMMENT_PREFIX)) { + keys.add(property.getKey()); + } + } + entry.getFields().stream() + .map(Field::getName) + .filter(name -> name.startsWith(HayagrivaMapping.USER_COMMENT_PREFIX)) + .forEach(keys::add); + keys.forEach(key -> mergeScalar(node, key, FieldFactory.parseField(key), entry, current)); + } + private void mergeFormattable(ObjectNode node, String key, Field field, BibEntry entry, BibEntry current) { Optional target = entry.getField(field); if (target.equals(current.getField(field))) { diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java index 222a18284b1f..e8ee9f4fbad3 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java @@ -57,9 +57,18 @@ public final class HayagrivaMapping { public static final Field RUNTIME_FIELD = new UnknownField("runtime"); public static final Field TIME_RANGE_FIELD = new UnknownField("time-range"); + /// Prefix of JabRef's per-user comment fields + /// ([org.jabref.model.entry.field.UserSpecificCommentField]), mapped to equally named + /// extension keys (see [#SCALAR_FIELDS] on extension keys). + public static final String USER_COMMENT_PREFIX = "comment-"; + /// Hayagriva keys holding a plain scalar, with their BibEntry field, in canonical write /// order. `title` and `url` are "formattable strings" (scalar or map with a `value` key) and /// are handled separately, as are the person lists and `serial-number`. + /// + /// `comment` (like the per-user `comment-` keys, see [#USER_COMMENT_PREFIX]) is not + /// part of the Hayagriva specification but a JabRef extension key: the Hayagriva parser + /// silently ignores keys it does not know, so such files stay loadable by Typst. public static final SequencedMap SCALAR_FIELDS; /// `serial-number` sub-keys with a dedicated BibEntry field. `arxiv` is handled separately @@ -101,6 +110,7 @@ public final class HayagrivaMapping { scalars.put("time-range", TIME_RANGE_FIELD); scalars.put("note", StandardField.NOTE); scalars.put("abstract", StandardField.ABSTRACT); + scalars.put("comment", StandardField.COMMENT); SCALAR_FIELDS = Collections.unmodifiableSequencedMap(scalars); SequencedMap serialNumbers = new LinkedHashMap<>(); @@ -131,6 +141,7 @@ public static BibEntry toBibEntry(String citationKey, JsonNode entryNode) { SCALAR_FIELDS.forEach((key, field) -> scalarText(entryNode.get(key)).ifPresent(value -> bibEntry.setField(field, value))); + applyUserComments(bibEntry, entryNode); applyAffiliated(bibEntry, entryNode.get("affiliated")); applySerialNumber(bibEntry, entryNode.get("serial-number")); applyParents(bibEntry, entryNode.get("parent")); @@ -192,6 +203,17 @@ static void applyParents(BibEntry bibEntry, @Nullable JsonNode parentNode) { } } + /// Per-user comment extension keys (`comment-`, see [#SCALAR_FIELDS]) map to the + /// equally named [org.jabref.model.entry.field.UserSpecificCommentField]s. + static void applyUserComments(BibEntry bibEntry, JsonNode entryNode) { + for (Map.Entry property : entryNode.properties()) { + if (property.getKey().startsWith(USER_COMMENT_PREFIX)) { + scalarText(property.getValue()).ifPresent(value -> + bibEntry.setField(FieldFactory.parseField(property.getKey()), value)); + } + } + } + /// `affiliated` lists further people by their role (director, translator, ...). Roles with a /// matching BibTeX field (e.g. translator) land there; the rest are kept as custom fields /// named after the role, so no contributor information is lost on import. diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java index 6a7fe6ca1e17..df172f37d2b3 100644 --- a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java +++ b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java @@ -3,6 +3,7 @@ import org.jabref.logic.importer.fileformat.HayagrivaMapping; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UserSpecificCommentField; import org.junit.jupiter.api.Test; import tools.jackson.databind.node.ObjectNode; @@ -159,6 +160,71 @@ void mergeRemovesSerialFallbackWhenNumberIsCleared() { assertFalse(node.has("serial-number")); } + @Test + void mergeWritesCommentAndPerUserComment() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.COMMENT, "shared comment"); + entry.setField(new UserSpecificCommentField("koppor"), "per-user comment"); + + writer.mergeIntoNode(entry, node); + + assertEquals("shared comment", node.get("comment").asString()); + assertEquals("per-user comment", node.get("comment-koppor").asString()); + } + + @Test + void mergeRemovesClearedComments() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + comment: to be removed + comment-koppor: also to be removed + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.clearField(StandardField.COMMENT); + entry.clearField(new UserSpecificCommentField("koppor")); + + writer.mergeIntoNode(entry, node); + + assertFalse(node.has("comment")); + assertFalse(node.has("comment-koppor")); + } + + @Test + void freshWriteSynthesizesDateFromYearAndMonth() { + BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) + .withField(StandardField.TITLE, "Some Title") + .withField(StandardField.YEAR, "1020") + .withField(StandardField.MONTH, "may"); + + ObjectNode node = writer.toEntryNode(entry); + + assertEquals("1020-05", node.get("date").asString()); + } + + @Test + void mergeKeepsDateWhenYearAliasIsUnchanged() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + date: 2020 + """); + BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) + .withField(StandardField.TITLE, "Some Title") + .withField(StandardField.YEAR, "2020"); + + writer.mergeIntoNode(entry, node); + + assertEquals("2020", node.get("date").asString()); + } + @Test void freshWriteRelocatesJournalDetailsIntoParent() { BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java index 1dac269e92e9..02f52f5df12e 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaImporterTest.java @@ -18,6 +18,7 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.field.UserSpecificCommentField; import org.jabref.model.entry.types.StandardEntryType; import org.junit.jupiter.api.BeforeAll; @@ -290,6 +291,24 @@ void keepsApproximateDateMarker() throws Exception { } } + @Test + void importsCommentExtensionKeys() throws Exception { + String yaml = """ + test: + type: Book + comment: A shared comment + comment-koppor: A per-user comment + """; + try (BufferedReader reader = new BufferedReader(Reader.of(yaml))) { + ParserResult result = hayagrivaImporter.importDatabase(reader); + Optional entry = result.getDatabase().getEntryByCitationKey("test"); + assertEquals(Optional.of("A shared comment"), + entry.flatMap(bibEntry -> bibEntry.getField(StandardField.COMMENT))); + assertEquals(Optional.of("A per-user comment"), + entry.flatMap(bibEntry -> bibEntry.getField(new UserSpecificCommentField("koppor")))); + } + } + @Test void importDatabaseReturnsErrorResultForMalformedYaml() throws Exception { try (BufferedReader reader = new BufferedReader(Reader.of("a: [unclosed"))) { diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java index dc1093f1d9df..3e8bee92400a 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java @@ -12,6 +12,7 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.field.UserSpecificCommentField; import org.jabref.model.entry.types.StandardEntryType; import org.junit.jupiter.api.Test; @@ -102,7 +103,12 @@ static Stream writerAuthoredEntries() { .withField(StandardField.TITLE, "Defining the Really Habitable Zone") .withField(StandardField.EPRINT, "2003.13722") .withField(StandardField.EPRINTTYPE, "arxiv") - .withField(StandardField.URL, "https://arxiv.org/abs/2003.13722"))); + .withField(StandardField.URL, "https://arxiv.org/abs/2003.13722")), + Arguments.of(new BibEntry(StandardEntryType.Article) + .withCitationKey("commented") + .withField(StandardField.TITLE, "An entry carrying comments") + .withField(StandardField.COMMENT, "A shared comment") + .withField(new UserSpecificCommentField("koppor"), "A per-user comment"))); } @ParameterizedTest From 1c2da8a277b47143e383f3cc73a2c7c71fa1b0c9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 3 Sep 2026 04:16:20 +0200 Subject: [PATCH 007/102] Fix merge idempotence and API surface of the Hayagriva writer A merge of an unchanged entry dropped a top-level volume next to a periodical parent, and clearing the journal lost the volume/issue it carried; number-looking strings were written unquoted and read back as numbers; a series next to a booktitle did not survive a re-import. The node-level API and the transitive Jackson requirement are no longer exposed. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vr3E1Gg5DRU4LQDDVnhPys --- CHANGELOG.md | 4 +- jablib/src/main/java/module-info.java | 2 +- .../logic/exporter/HayagrivaEntryWriter.java | 136 +++++----- .../logic/exporter/HayagrivaExporter.java | 3 +- .../importer/fileformat/HayagrivaMapping.java | 42 ++-- .../exporter/HayagrivaEntryWriterTest.java | 236 +++++++++++++++--- .../logic/exporter/HayagrivaExporterTest.java | 4 +- .../fileformat/HayagrivaRoundTripTest.java | 7 +- 8 files changed, 307 insertions(+), 127 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c129d136f8..9ca34530ef33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,8 +69,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed -- The Hayagriva YAML exporter is now implemented programmatically instead of via a layout template: re-exporting an imported Hayagriva file preserves structured data JabRef cannot represent (short titles, person aliases, additional identifiers), `misc` entries export with a lowercase type, and journal details are written into the periodical parent. The `HayagrivaType` custom-layout formatter was removed. [#736](https://github.com/JabRef/jabref-koppor/pull/736) -- Hayagriva YAML import and export now cover JabRef's "Comment" field and per-user comment fields (written as `comment`/`comment-` extension keys, which the Hayagriva parser ignores), and entries carrying only BibTeX `year`/`month` fields get their `date` written. [#736](https://github.com/JabRef/jabref-koppor/pull/736) +- We changed the Hayagriva YAML export to keep data JabRef cannot edit (short titles, person aliases, additional identifiers) when re-exporting an imported file, to write comments, and to derive `date` from the BibTeX year/month fields. [#736](https://github.com/JabRef/jabref-koppor/pull/736) - We changed the default macOS shortcuts for "Search document identifier online" and "Focus group list" to not insert special characters. [#16528](https://github.com/JabRef/jabref/issues/16528) - We changed the extension of backup files from `.bak` to `.bib`, so that they can be opened in JabRef. [#11454](https://github.com/JabRef/jabref/issues/11454) - We changed the delete and rename file dialogs to state that undo does not restore files on disk. [#16680](https://github.com/JabRef/jabref/pull/16680) @@ -197,6 +196,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Removed +- We removed the `HayagrivaType` layout formatter, since the Hayagriva YAML export no longer uses a layout template. [#736](https://github.com/JabRef/jabref-koppor/pull/736) - We removed the redundant "Look up BibTeX entries in all open libraries" setting from the LibreOffice panel, which is now the toggle of "Look up BibTeX entries in the currently selected library only". [#16484](https://github.com/JabRef/jabref/pull/16484) - We removed the entry editor tabs "Required fields", "Optional fields", "Optional fields 2", "Deprecated fields", "Other fields", and "Comments"; their content is part of the new "Main" tab. [#12711](https://github.com/JabRef/jabref/issues/12711) - We removed the default custom entry editor tabs "General" and "Abstract"; their content is part of the new "Main" tab. User-defined custom tabs are kept. [#12711](https://github.com/JabRef/jabref/issues/12711) diff --git a/jablib/src/main/java/module-info.java b/jablib/src/main/java/module-info.java index 52668b3205b7..755f6ef7f35b 100644 --- a/jablib/src/main/java/module-info.java +++ b/jablib/src/main/java/module-info.java @@ -206,7 +206,7 @@ // region: data mapping requires jdk.xml.dom; requires com.google.gson; - requires transitive tools.jackson.databind; + requires tools.jackson.databind; requires tools.jackson.dataformat.yaml; requires tools.jackson.core; requires transitive com.fasterxml.jackson.annotation; diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java index 5b6aa3f614e4..46f439a2fd7f 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java @@ -5,11 +5,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Optional; import java.util.SequencedMap; import java.util.SequencedSet; import java.util.Set; +import java.util.stream.Stream; import org.jabref.logic.importer.fileformat.HayagrivaMapping; import org.jabref.model.entry.AuthorList; @@ -21,7 +21,6 @@ import org.jabref.model.entry.types.StandardEntryType; import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; @@ -46,9 +45,12 @@ public class HayagrivaEntryWriter { /// synthesized `periodical` parent instead of the entry itself. private static final Set JOURNAL_RELOCATED_KEYS = Set.of("volume", "issue", "publisher"); + /// Number-looking strings (`edition: 1.10`, `volume: 1e3`) must stay quoted, otherwise the + /// YAML reader resolves them as numbers and the text changes (`1.10` -> `1.1`). private static final YAMLMapper MAPPER = new YAMLMapper(YAMLFactory.builder() .disable(YAMLWriteFeature.WRITE_DOC_START_MARKER) .enable(YAMLWriteFeature.MINIMIZE_QUOTES) + .enable(YAMLWriteFeature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) .build()); public String serialize(SequencedMap keyedEntries) { @@ -57,13 +59,13 @@ public String serialize(SequencedMap keyedEntries) { return MAPPER.writeValueAsString(root); } - public ObjectNode toEntryNode(BibEntry entry) { + ObjectNode toEntryNode(BibEntry entry) { return mergeIntoNode(entry, MAPPER.createObjectNode()); } /// Merges the entry into an existing Hayagriva entry node (read-modify-write, see class doc). /// On an empty node this produces a fresh entry in canonical key order. - public ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { + ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { BibEntry current = HayagrivaMapping.toBibEntry("current", node); mergeType(node, entry); @@ -72,9 +74,21 @@ public ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { mergePersons(node, "editor", StandardField.EDITOR, entry, current); boolean hasJournal = entry.hasField(StandardField.JOURNAL); + boolean journalRemoved = !hasJournal && current.hasField(StandardField.JOURNAL); HayagrivaMapping.SCALAR_FIELDS.forEach((key, field) -> { - if (hasJournal && JOURNAL_RELOCATED_KEYS.contains(key)) { - node.remove(key); + if (!JOURNAL_RELOCATED_KEYS.contains(key)) { + mergeScalar(node, key, field, entry, current); + } else if (hasJournal) { + // Journal details live in the periodical parent (see mergeParent). A top-level + // value wins on import, so an unchanged one is left where it is — removing it + // would silently surface the parent's (different) value. + if (fieldDiffers(field, entry, current)) { + node.remove(key); + } + } else if (journalRemoved) { + // The parent goes away with the journal, so the details it carried must + // resurface at the top level even though their value did not change + scalarValue(entry, field).ifPresentOrElse(value -> node.put(key, value), () -> node.remove(key)); } else { mergeScalar(node, key, field, entry, current); } @@ -89,12 +103,13 @@ public ObjectNode mergeIntoNode(BibEntry entry, ObjectNode node) { return node; } + /// An equivalent existing type string (e.g. `post`, importing as Misc) is kept as-is. private void mergeType(ObjectNode node, BibEntry entry) { - @Nullable EntryType currentType = HayagrivaMapping.scalarText(node.get("type")) - .map(type -> HayagrivaMapping.TYPE_TO_ENTRY_TYPE.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) - .orElse(null); - // An equivalent existing type string (e.g. `post`, importing as Misc) is kept as-is - if (!entry.getType().equals(currentType)) { + boolean typeUnchanged = HayagrivaMapping.scalarText(node.get("type")) + .map(type -> HayagrivaMapping.TYPE_TO_ENTRY_TYPE.getOrDefault(type.toLowerCase(Locale.ROOT), StandardEntryType.Misc)) + .filter(entry.getType()::equals) + .isPresent(); + if (!typeUnchanged) { node.put("type", HayagrivaMapping.ENTRY_TYPE_TO_TYPE.getOrDefault(entry.getType(), "misc")); } } @@ -116,16 +131,10 @@ private Optional scalarValue(BibEntry entry, Field field) { /// JabRef's per-user comment fields are written as equally named `comment-` extension /// keys, symmetric to [HayagrivaMapping#applyUserComments]. private void mergeUserComments(ObjectNode node, BibEntry entry, BibEntry current) { - SequencedSet keys = new LinkedHashSet<>(); - for (Map.Entry property : node.properties()) { - if (property.getKey().startsWith(HayagrivaMapping.USER_COMMENT_PREFIX)) { - keys.add(property.getKey()); - } - } - entry.getFields().stream() - .map(Field::getName) - .filter(name -> name.startsWith(HayagrivaMapping.USER_COMMENT_PREFIX)) - .forEach(keys::add); + List keys = Stream.concat(node.propertyNames().stream(), entry.getFields().stream().map(Field::getName)) + .filter(name -> name.startsWith(HayagrivaMapping.USER_COMMENT_PREFIX)) + .distinct() + .toList(); keys.forEach(key -> mergeScalar(node, key, FieldFactory.parseField(key), entry, current)); } @@ -161,7 +170,7 @@ private JsonNode personList(String bibtexPersons) { private void mergeSerialNumber(ObjectNode node, BibEntry entry, BibEntry current) { boolean changed = !arxivEprint(entry).equals(arxivEprint(current)) || HayagrivaMapping.SERIAL_NUMBER_FIELDS.values().stream() - .anyMatch(field -> !entry.getField(field).equals(current.getField(field))); + .anyMatch(field -> fieldDiffers(field, entry, current)); if (!changed) { return; } @@ -194,37 +203,38 @@ private void removeStaleSerialFallback(ObjectNode node, BibEntry entry, BibEntry if (entry.hasField(StandardField.NUMBER) || current.getField(StandardField.NUMBER).isEmpty()) { return; } - JsonNode serialNumber = node.get("serial-number"); - if (serialNumber instanceof ObjectNode serialNumberObject) { - serialNumberObject.remove("serial"); - if (serialNumberObject.isEmpty()) { + node.optional("serial-number").ifPresent(serialNumber -> { + if (serialNumber instanceof ObjectNode serialNumberObject) { + serialNumberObject.remove("serial"); + if (serialNumberObject.isEmpty()) { + node.remove("serial-number"); + } + } else { node.remove("serial-number"); } - } else if (serialNumber != null) { - node.remove("serial-number"); - } + }); } private Optional arxivEprint(BibEntry entry) { - boolean isArxiv = entry.getField(StandardField.EPRINTTYPE) - .map(eprintType -> "arxiv".equalsIgnoreCase(eprintType)) - .orElse(false); - return isArxiv ? entry.getField(StandardField.EPRINT) : Optional.empty(); + return entry.getField(StandardField.EPRINTTYPE) + .filter("arxiv"::equalsIgnoreCase) + .flatMap(_ -> entry.getField(StandardField.EPRINT)); } private void mergeAffiliated(ObjectNode node, BibEntry entry, BibEntry current) { + // Last item per role wins, like on import SequencedMap existingByRole = new LinkedHashMap<>(); if (node.get("affiliated") instanceof ArrayNode existing) { for (JsonNode item : existing.values()) { if (item.isObject()) { HayagrivaMapping.scalarText(item.get("role")) - .ifPresent(role -> existingByRole.putIfAbsent(role.toLowerCase(Locale.ROOT), item)); + .ifPresent(role -> existingByRole.put(role.toLowerCase(Locale.ROOT), item)); } } } // Existing roles first, so untouched items keep their position; spec roles cover fields // added in JabRef - LinkedHashSet roles = new LinkedHashSet<>(existingByRole.keySet()); + SequencedSet roles = new LinkedHashSet<>(existingByRole.keySet()); roles.addAll(HayagrivaMapping.AFFILIATED_ROLES); boolean changed = false; @@ -233,10 +243,7 @@ private void mergeAffiliated(ObjectNode node, BibEntry entry, BibEntry current) Field field = FieldFactory.parseField(role); Optional target = entry.getField(field); if (target.equals(current.getField(field))) { - JsonNode item = existingByRole.get(role); - if (item != null) { - result.add(item); - } + Optional.ofNullable(existingByRole.get(role)).ifPresent(result::add); continue; } changed = true; @@ -255,9 +262,7 @@ private void mergeAffiliated(ObjectNode node, BibEntry entry, BibEntry current) node.remove("affiliated"); return; } - ArrayNode affiliated = MAPPER.createArrayNode(); - result.forEach(affiliated::add); - node.set("affiliated", affiliated); + node.set("affiliated", MAPPER.createArrayNode().addAll(result)); } private void mergeParent(ObjectNode node, BibEntry entry, BibEntry current) { @@ -275,48 +280,43 @@ private void mergeParent(ObjectNode node, BibEntry entry, BibEntry current) { List parents = new ArrayList<>(); entry.getField(StandardField.JOURNAL).ifPresent(journal -> { - ObjectNode parent = MAPPER.createObjectNode(); - parent.put("type", "periodical"); - parent.put("title", journal); + ObjectNode parent = parentNode("periodical", journal); entry.getField(StandardField.VOLUME).ifPresent(volume -> parent.put("volume", volume)); entry.getField(StandardField.NUMBER).ifPresent(issue -> parent.put("issue", issue)); entry.getField(StandardField.PUBLISHER).ifPresent(publisher -> parent.put("publisher", publisher)); parents.add(parent); }); - entry.getField(StandardField.BOOKTITLE).ifPresent(booktitle -> { - ObjectNode parent = MAPPER.createObjectNode(); - parent.put("type", booktitleParentType(entry.getType())); - parent.put("title", booktitle); - parents.add(parent); - }); - // For book parts a `book` parent re-imports as booktitle, so a series written next to a - // booktitle survives in the YAML but not a re-import (documented asymmetry) - entry.getField(StandardField.SERIES).ifPresent(series -> { - ObjectNode parent = MAPPER.createObjectNode(); - parent.put("type", "book"); - parent.put("title", series); + Optional seriesParent = entry.getField(StandardField.SERIES).map(series -> parentNode("book", series)); + entry.getField(StandardField.BOOKTITLE).ifPresentOrElse(booktitle -> { + ObjectNode parent = parentNode(booktitleParentType(entry.getType()), booktitle); + // Hayagriva nests containers: the series is the parent of the proceedings/book the + // part appears in, which is also what makes both fields survive a re-import + seriesParent.ifPresent(series -> parent.set("parent", series)); parents.add(parent); - }); + }, () -> seriesParent.ifPresent(parents::add)); if (parents.isEmpty()) { node.remove("parent"); } else if (parents.size() == 1) { node.set("parent", parents.getFirst()); } else { - ArrayNode parentArray = MAPPER.createArrayNode(); - parents.forEach(parentArray::add); - node.set("parent", parentArray); + node.set("parent", MAPPER.createArrayNode().addAll(parents)); } } + private ObjectNode parentNode(String type, String title) { + ObjectNode parent = MAPPER.createObjectNode(); + parent.put("type", type); + parent.put("title", title); + return parent; + } + private String booktitleParentType(EntryType entryType) { - if (StandardEntryType.InProceedings == entryType) { - return "proceedings"; - } - if (StandardEntryType.InBook == entryType) { - return "book"; - } - return "anthology"; + return switch (entryType) { + case StandardEntryType.InProceedings -> "proceedings"; + case StandardEntryType.InBook -> "book"; + default -> "anthology"; + }; } private boolean fieldDiffers(Field field, BibEntry entry, BibEntry current) { diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java index 2439dfca0228..e921a9d5f09e 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaExporter.java @@ -12,7 +12,6 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; -import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; import tools.jackson.core.JacksonException; @@ -31,7 +30,7 @@ public HayagrivaExporter() { } @Override - public void export(@NonNull BibDatabaseContext databaseContext, Path file, @NonNull List entries) throws IOException { + public void export(BibDatabaseContext databaseContext, Path file, List entries) throws IOException { if (entries.isEmpty()) { return; } diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java index e8ee9f4fbad3..dc1d658d03c7 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/HayagrivaMapping.java @@ -50,13 +50,6 @@ public final class HayagrivaMapping { /// Report. Unlisted types are written as `misc`. public static final Map ENTRY_TYPE_TO_TYPE; - public static final Set JOURNAL_PARENT_TYPES = Set.of("periodical", "newspaper", "blog"); - public static final Set BOOKTITLE_PARENT_TYPES = Set.of("proceedings", "anthology", "conference"); - public static final Set BOOK_PART_TYPES = Set.of(StandardEntryType.InBook, StandardEntryType.InCollection, StandardEntryType.InProceedings); - - public static final Field RUNTIME_FIELD = new UnknownField("runtime"); - public static final Field TIME_RANGE_FIELD = new UnknownField("time-range"); - /// Prefix of JabRef's per-user comment fields /// ([org.jabref.model.entry.field.UserSpecificCommentField]), mapped to equally named /// extension keys (see [#SCALAR_FIELDS] on extension keys). @@ -83,6 +76,13 @@ public final class HayagrivaMapping { "composer", "producer", "executive-producer", "writer", "cinematography", "director", "illustrator", "narrator"); + private static final Set JOURNAL_PARENT_TYPES = Set.of("periodical", "newspaper", "blog"); + private static final Set BOOKTITLE_PARENT_TYPES = Set.of("proceedings", "anthology", "conference"); + private static final Set BOOK_PART_TYPES = Set.of(StandardEntryType.InBook, StandardEntryType.InCollection, StandardEntryType.InProceedings); + + private static final Field RUNTIME_FIELD = new UnknownField("runtime"); + private static final Field TIME_RANGE_FIELD = new UnknownField("time-range"); + static { Map inverseTypes = new HashMap<>(); TYPE_TO_ENTRY_TYPE.forEach((hayagrivaType, entryType) -> inverseTypes.put(entryType, hayagrivaType)); @@ -170,17 +170,10 @@ private static void applySerialNumber(BibEntry bibEntry, @Nullable JsonNode seri /// (journal as a `periodical` parent, series as a `book` parent). A missing or unmapped /// parent type falls back by entry type: articles are most likely contained in a journal, /// everything else in some book-like container. The first parent providing a value for a - /// field wins; nested parents are not descended into. + /// field wins. Nesting is only followed one level down, for the series of a booktitle + /// container (the writer's shape for book parts with both fields). static void applyParents(BibEntry bibEntry, @Nullable JsonNode parentNode) { - if (parentNode == null) { - return; - } - - List parents = parentNode.isArray() ? List.copyOf(parentNode.values()) : List.of(parentNode); - for (JsonNode parent : parents) { - if (!parent.isObject()) { - continue; - } + for (JsonNode parent : parentNodes(parentNode)) { String parentType = scalarText(parent.get("type")).map(type -> type.toLowerCase(Locale.ROOT)).orElse(""); StandardField targetField; @@ -195,6 +188,10 @@ static void applyParents(BibEntry bibEntry, @Nullable JsonNode parentNode) { } formattedText(parent.get("title")).ifPresent(title -> setIfAbsent(bibEntry, targetField, title)); + if (targetField == StandardField.BOOKTITLE) { + parentNodes(parent.get("parent")).forEach(series -> + formattedText(series.get("title")).ifPresent(title -> setIfAbsent(bibEntry, StandardField.SERIES, title))); + } if (targetField == StandardField.JOURNAL) { scalarText(parent.get("volume")).ifPresent(volume -> setIfAbsent(bibEntry, StandardField.VOLUME, volume)); scalarText(parent.get("issue")).ifPresent(issue -> setIfAbsent(bibEntry, StandardField.NUMBER, issue)); @@ -203,6 +200,15 @@ static void applyParents(BibEntry bibEntry, @Nullable JsonNode parentNode) { } } + /// `parent` is either a single map or a list of maps. + private static List parentNodes(@Nullable JsonNode parentNode) { + if (parentNode == null) { + return List.of(); + } + List parents = parentNode.isArray() ? List.copyOf(parentNode.values()) : List.of(parentNode); + return parents.stream().filter(JsonNode::isObject).toList(); + } + /// Per-user comment extension keys (`comment-`, see [#SCALAR_FIELDS]) map to the /// equally named [org.jabref.model.entry.field.UserSpecificCommentField]s. static void applyUserComments(BibEntry bibEntry, JsonNode entryNode) { @@ -274,7 +280,7 @@ public static Optional scalarText(@Nullable JsonNode node) { /// Reads a Hayagriva "formattable string": either a plain scalar or a map holding the actual /// string under `value` (next to `short`, `verbatim`, ... which JabRef cannot represent). - public static Optional formattedText(@Nullable JsonNode node) { + private static Optional formattedText(@Nullable JsonNode node) { if (node != null && node.isObject()) { return scalarText(node.get("value")); } diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java index df172f37d2b3..cbe922b9f225 100644 --- a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java +++ b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaEntryWriterTest.java @@ -1,17 +1,20 @@ package org.jabref.logic.exporter; +import java.net.URISyntaxException; +import java.nio.file.Path; + import org.jabref.logic.importer.fileformat.HayagrivaMapping; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UserSpecificCommentField; +import org.jabref.model.entry.types.StandardEntryType; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ObjectNode; import tools.jackson.dataformat.yaml.YAMLMapper; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; class HayagrivaEntryWriterTest { @@ -22,6 +25,22 @@ private ObjectNode parseEntryNode(String yaml) { return (ObjectNode) mapper.readTree(yaml).get("key"); } + /// The upstream fixture exercises every construct the mapping knows: merging an unchanged + /// entry back into its node must not touch the node. + @Test + void mergingUnchangedFixtureEntriesIsIdentity() throws URISyntaxException { + Path fixture = Path.of(HayagrivaEntryWriterTest.class.getResource("/org/jabref/logic/importer/fileformat/basic.yml").toURI()); + JsonNode root = mapper.readTree(fixture.toFile()); + + ObjectNode merged = mapper.createObjectNode(); + root.properties().forEach(property -> { + ObjectNode node = (ObjectNode) property.getValue(); + merged.set(property.getKey(), writer.mergeIntoNode(HayagrivaMapping.toBibEntry(property.getKey(), node), node.deepCopy())); + }); + + assertEquals(root, merged); + } + @Test void mergePreservesUnknownKeysWhileUpdatingChangedField() { ObjectNode node = parseEntryNode(""" @@ -36,8 +55,13 @@ void mergePreservesUnknownKeysWhileUpdatingChangedField() { writer.mergeIntoNode(entry, node); - assertEquals("new note", node.get("note").asString()); - assertEquals(2, node.get("tongus").asInt()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + tongus: 2 + note: new note + """), node); } @Test @@ -54,8 +78,13 @@ void mergeKeepsStructuredTitleFormWhenTitleChanges() { writer.mergeIntoNode(entry, node); - assertEquals("Corrected Title", node.get("title").get("value").asString()); - assertEquals("Short Title", node.get("title").get("short").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + title: + value: Corrected Title + short: Short Title + """), node); } @Test @@ -73,7 +102,15 @@ void mergeKeepsStructuredPersonFormWhenUnchanged() { writer.mergeIntoNode(entry, node); - assertEquals("laurmaedje", node.get("author").get(0).get("alias").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + author: + - name: "Mädje" + given-name: "Laurenz" + alias: "laurmaedje" + note: unrelated change + """), node); } @Test @@ -89,7 +126,11 @@ void mergeRemovesKeyOfClearedField() { writer.mergeIntoNode(entry, node); - assertFalse(node.has("note")); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + """), node); } @Test @@ -104,7 +145,12 @@ void mergeKeepsEquivalentTypeString() { writer.mergeIntoNode(entry, node); - assertEquals("post", node.get("type").asString()); + assertEquals(parseEntryNode(""" + key: + type: post + title: Some Title + note: unrelated change + """), node); } @Test @@ -122,9 +168,15 @@ void mergePreservesForeignSerialNumberSchemes() { writer.mergeIntoNode(entry, node); - assertEquals("10.1000/example", node.get("serial-number").get("doi").asString()); - assertEquals("978-0747551003", node.get("serial-number").get("isbn").asString()); - assertEquals("3", node.get("serial-number").get("serial").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + serial-number: + isbn: "978-0747551003" + serial: "3" + doi: 10.1000/example + """), node); } @Test @@ -140,8 +192,14 @@ void mergePreservesBareSerialNumberAsExplicitSerialWhenIdentifierIsAdded() { writer.mergeIntoNode(entry, node); - assertEquals("RFC 2845", node.get("serial-number").get("serial").asString()); - assertEquals("10.1000/example", node.get("serial-number").get("doi").asString()); + assertEquals(parseEntryNode(""" + key: + type: reference + title: Some Title + serial-number: + serial: RFC 2845 + doi: 10.1000/example + """), node); } @Test @@ -157,7 +215,11 @@ void mergeRemovesSerialFallbackWhenNumberIsCleared() { writer.mergeIntoNode(entry, node); - assertFalse(node.has("serial-number")); + assertEquals(parseEntryNode(""" + key: + type: reference + title: Some Title + """), node); } @Test @@ -173,8 +235,13 @@ void mergeWritesCommentAndPerUserComment() { writer.mergeIntoNode(entry, node); - assertEquals("shared comment", node.get("comment").asString()); - assertEquals("per-user comment", node.get("comment-koppor").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + comment: shared comment + comment-koppor: per-user comment + """), node); } @Test @@ -192,20 +259,28 @@ void mergeRemovesClearedComments() { writer.mergeIntoNode(entry, node); - assertFalse(node.has("comment")); - assertFalse(node.has("comment-koppor")); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + """), node); } @Test void freshWriteSynthesizesDateFromYearAndMonth() { - BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) + BibEntry entry = new BibEntry(StandardEntryType.Article) .withField(StandardField.TITLE, "Some Title") .withField(StandardField.YEAR, "1020") .withField(StandardField.MONTH, "may"); ObjectNode node = writer.toEntryNode(entry); - assertEquals("1020-05", node.get("date").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + date: "1020-05" + """), node); } @Test @@ -216,18 +291,23 @@ void mergeKeepsDateWhenYearAliasIsUnchanged() { title: Some Title date: 2020 """); - BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) + BibEntry entry = new BibEntry(StandardEntryType.Article) .withField(StandardField.TITLE, "Some Title") .withField(StandardField.YEAR, "2020"); writer.mergeIntoNode(entry, node); - assertEquals("2020", node.get("date").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + date: 2020 + """), node); } @Test void freshWriteRelocatesJournalDetailsIntoParent() { - BibEntry entry = new BibEntry(org.jabref.model.entry.types.StandardEntryType.Article) + BibEntry entry = new BibEntry(StandardEntryType.Article) .withField(StandardField.TITLE, "Some Title") .withField(StandardField.JOURNAL, "Some Journal") .withField(StandardField.VOLUME, "13") @@ -235,11 +315,38 @@ void freshWriteRelocatesJournalDetailsIntoParent() { ObjectNode node = writer.toEntryNode(entry); - assertFalse(node.has("volume")); - assertFalse(node.has("issue")); - assertEquals("13", node.get("parent").get("volume").asString()); - assertEquals("3", node.get("parent").get("issue").asString()); - assertEquals("Some Journal", node.get("parent").get("title").asString()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + parent: + type: periodical + title: Some Journal + volume: "13" + issue: "3" + """), node); + } + + @Test + void freshWriteNestsSeriesUnderBooktitleParent() { + BibEntry entry = new BibEntry(StandardEntryType.InProceedings) + .withField(StandardField.TITLE, "Some Title") + .withField(StandardField.BOOKTITLE, "Some Proceedings") + .withField(StandardField.SERIES, "LNCS"); + + ObjectNode node = writer.toEntryNode(entry); + + assertEquals(parseEntryNode(""" + key: + type: conference + title: Some Title + parent: + type: proceedings + title: Some Proceedings + parent: + type: book + title: LNCS + """), node); } @Test @@ -260,6 +367,75 @@ void mergeLeavesParentUntouchedWhenSourceFieldsUnchanged() { writer.mergeIntoNode(entry, node); - assertTrue(node.get("parent").get("title").get("verbatim").asBoolean()); + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + parent: + type: periodical + title: + value: Physical Review B + verbatim: true + volume: 102 + note: unrelated change + """), node); + } + + /// A top-level `volume` wins over the parent's on import, so leaving it in place is what + /// keeps the entry stable; removing it would surface the parent's different value. + @Test + void mergeKeepsUnchangedTopLevelVolumeNextToPeriodicalParent() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + volume: 1-2 + parent: + type: periodical + title: Some Journal + volume: 61-62 + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.setField(StandardField.NOTE, "unrelated change"); + + writer.mergeIntoNode(entry, node); + + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + volume: 1-2 + parent: + type: periodical + title: Some Journal + volume: 61-62 + note: unrelated change + """), node); + } + + @Test + void mergeMovesUnchangedJournalDetailsToTopLevelWhenJournalIsCleared() { + ObjectNode node = parseEntryNode(""" + key: + type: article + title: Some Title + parent: + type: periodical + title: Some Journal + volume: "13" + issue: "3" + """); + BibEntry entry = HayagrivaMapping.toBibEntry("key", node); + entry.clearField(StandardField.JOURNAL); + + writer.mergeIntoNode(entry, node); + + assertEquals(parseEntryNode(""" + key: + type: article + title: Some Title + volume: "13" + issue: "3" + """), node); } } diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java index dec54787fe43..b17ea7396ae3 100644 --- a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaExporterTest.java @@ -120,8 +120,8 @@ void journalBecomesPeriodicalParentCarryingVolumeIssueAndPublisher(@TempDir Path " parent:", " type: periodical", " title: Physical Review B", - " volume: 102", - " issue: 16", + " volume: \"102\"", + " issue: \"16\"", " publisher: American Physical Society"); assertEquals(expected, export(List.of(entry), tempDir)); diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java index 3e8bee92400a..39d7216e7773 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/HayagrivaRoundTripTest.java @@ -34,9 +34,7 @@ class HayagrivaRoundTripTest { private final HayagrivaImporter importer = new HayagrivaImporter(); private List exportAndReimport(List entries) throws IOException { - Path file = tempDir.resolve("roundtrip-" + entries.hashCode() + ".yml"); - Files.deleteIfExists(file); - Files.createFile(file); + Path file = Files.createTempFile(tempDir, "roundtrip", ".yml"); exporter.export(new BibDatabaseContext(), file, entries); return importer.importDatabase(file).getDatabase().getEntries(); } @@ -62,6 +60,7 @@ static Stream writerAuthoredEntries() { .withField(StandardField.TITLE, "Achieving Low Tail Latency") .withField(StandardField.AUTHOR, "Prekas, George and Kogias, Marios") .withField(StandardField.BOOKTITLE, "Proceedings of the 26th Symposium on Operating Systems Principles") + .withField(StandardField.SERIES, "SOSP") .withField(StandardField.PAGES, "325-341")), Arguments.of(new BibEntry(StandardEntryType.Book) .withCitationKey("donne") @@ -69,7 +68,7 @@ static Stream writerAuthoredEntries() { .withField(StandardField.EDITOR, "Stringer, Gary A. and Pebworth, Ted-Larry") .withField(StandardField.ADDRESS, "Bloomington") .withField(StandardField.PUBLISHER, "Indiana University Press") - .withField(StandardField.EDITION, "2") + .withField(StandardField.EDITION, "1.10") .withField(StandardField.VOLUMES, "7") .withField(StandardField.SERIES, "The Variorum Edition") .withField(StandardField.ISBN, "978-0747551003") From d45ccb4310e6c34a31e790d6ba47208f3b006bb9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 7 Sep 2026 00:13:29 +0200 Subject: [PATCH 008/102] Apply IntelliJ formatter to HayagrivaEntryWriter Fixes the CI format check after the upstream resync merge. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry --- .../org/jabref/logic/exporter/HayagrivaEntryWriter.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java index 46f439a2fd7f..271ac32dd295 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java @@ -313,9 +313,12 @@ private ObjectNode parentNode(String type, String title) { private String booktitleParentType(EntryType entryType) { return switch (entryType) { - case StandardEntryType.InProceedings -> "proceedings"; - case StandardEntryType.InBook -> "book"; - default -> "anthology"; + case StandardEntryType.InProceedings -> + "proceedings"; + case StandardEntryType.InBook -> + "book"; + default -> + "anthology"; }; } From 5ca6827a2952c37e51856ce1ca6d0bc93581a75c Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 7 Sep 2026 10:39:52 +0200 Subject: [PATCH 009/102] Merge origin/main; require jackson.databind transitively Resync #736 with upstream and fix the module-directive scope check. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry --- jablib/src/main/java/module-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/java/module-info.java b/jablib/src/main/java/module-info.java index cd2a85eecfb1..0964d4be419d 100644 --- a/jablib/src/main/java/module-info.java +++ b/jablib/src/main/java/module-info.java @@ -215,7 +215,7 @@ // region: data mapping requires jdk.xml.dom; requires com.google.gson; - requires tools.jackson.databind; + requires transitive tools.jackson.databind; requires tools.jackson.dataformat.yaml; requires tools.jackson.core; requires transitive com.fasterxml.jackson.annotation; From 104f4904ddbf676b74dcf5523327f7c551778423 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 16 Sep 2026 09:15:48 +0000 Subject: [PATCH 010/102] Fix typo: --remote-label to --remove-label in merge-queue labeling workflow (#17182) Co-authored-by: Claude Sonnet 5 --- .github/workflows/on-pr-merge-queued.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on-pr-merge-queued.yml b/.github/workflows/on-pr-merge-queued.yml index 504797df3ca8..ccc24ac0afae 100644 --- a/.github/workflows/on-pr-merge-queued.yml +++ b/.github/workflows/on-pr-merge-queued.yml @@ -17,7 +17,7 @@ jobs: ref="${GITHUB_REF_NAME}" if [[ "$ref" =~ ^gh-readonly-queue/[^/]+/pr-([0-9]+)- ]]; then pr="${BASH_REMATCH[1]}" - gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --remove-label "status: ready-for-review" --remote-label "status: awaiting-second-review" --add-label "status: to-be-merged" + gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --remove-label "status: ready-for-review" --remove-label "status: awaiting-second-review" --add-label "status: to-be-merged" else echo "Cannot extract PR number from ref: $ref" >&2 exit 1 From 5bfed6ebeef40df7998e1b06731d70fd0758e458 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 16 Sep 2026 13:01:57 +0000 Subject: [PATCH 011/102] New Crowdin updates (#17185) * New translations jabref_en.properties for (French) * New translations jabref_en.properties for (Spanish) * New translations jabref_en.properties for (German) * New translations jabref_en.properties for (Finnish) * New translations jabref_en.properties for (Italian) * New translations jabref_en.properties for (Russian) * New translations jabref_en.properties for (Chinese Simplified) * New translations jabref_en.properties for (Portuguese, Brazilian) --- .../main/resources/l10n/JabRef_de.properties | 25 ++++------- .../main/resources/l10n/JabRef_es.properties | 25 ++++------- .../main/resources/l10n/JabRef_fi.properties | 17 +++----- .../main/resources/l10n/JabRef_fr.properties | 34 +++++++-------- .../main/resources/l10n/JabRef_it.properties | 34 +++++++-------- .../resources/l10n/JabRef_pt_BR.properties | 25 ++++------- .../main/resources/l10n/JabRef_ru.properties | 43 +++++++++++++++++++ .../resources/l10n/JabRef_zh_CN.properties | 25 ++++------- 8 files changed, 115 insertions(+), 113 deletions(-) diff --git a/jablib/src/main/resources/l10n/JabRef_de.properties b/jablib/src/main/resources/l10n/JabRef_de.properties index 30a006b69650..9a25f9dd4690 100644 --- a/jablib/src/main/resources/l10n/JabRef_de.properties +++ b/jablib/src/main/resources/l10n/JabRef_de.properties @@ -3207,10 +3207,8 @@ Next=Weiter # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=Klicken Sie auf das "Datei" Menü # Step 2 -Click\ on\ "Preferences"=Klicken Sie auf "Einstellungen" # Step 3 Select\ the\ "Linked\ files"\ tab=Wählen Sie die Registerkarte "Verknüpfte Dateien" @@ -3241,42 +3239,35 @@ Double\ click\ on\ the\ "Ding_2006"\ entry=Doppelklicken Sie auf den Eintrag "Di Let's\ start\ by\ selecting\ an\ entry\ to\ work\ with.\ Double\ click\ on\ the\ entry\ titled\ "Chocolate\ and\ Prevention\ of\ Cardiovascular\ Disease\:\ A\ Systematic\ Review"\ by\ Ding\ et\ al.=Beginnen wir damit, einen Eintrag auszuwählen, mit dem gearbeitet werden soll. Doppelklicken Sie auf den Eintrag ""Chocolate and Prevention of Cardiovascular Disease\: A Systematic Review" von Ding et al. # Step 4 -Click\ on\ the\ "General"\ tab=Klicken Sie auf die Registerkarte "Allgemein" -Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "General"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Nun müssen wir auf den Eintragseditor zugreifen. Klicken Sie auf die Registerkarte "Allgemein", um die Eintragsdetails anzuzeigen und zu bearbeiten. # Step 5 -PDF\ file\ management\ area=PDF-Dateiverwaltungsbereich -This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Dies ist der Bereich der PDF-Dateiverwaltung, in dem Sie Dateien mit Ihren Bibliographieeinträgen verknüpfen können. Beachten Sie die drei Tasten auf der rechten Seite – jeder bietet eine andere Möglichkeit, PDF-Dateien hinzuzufügen. Lassen Sie uns Schritt für Schritt jede Methode erkunden. # Step 6 -Drag\ and\ drop\ files=Dateien per Drag & Drop hochladen -You\ can\ drag\ and\ drop\ PDF\ files\ directly\ onto\ the\ file\ list\ area.\ This\ is\ often\ the\ quickest\ way\ to\ link\ files\ that\ are\ already\ on\ your\ computer.=Sie können PDF-Dateien direkt in den Dateilistenbereich ziehen und ablegen. Dies ist oft der schnellste Weg, um Dateien zu verknüpfen, die bereits auf Ihrem Computer sind. -Try\ dragging\ a\ PDF\ file\ here\ to\ continue\ the\ walkthrough.=Ziehen Sie eine PDF-Datei hierher, um den Durchgang fortzusetzen. # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=Klicken Sie auf "Hinzufügen", um eine Datei von Ihrem Computer zu verknüpfen -Click\ the\ "Add"\ button\ (first\ button\ with\ a\ plus\ icon)\ to\ link\ a\ PDF\ file\ that\ you\ already\ have\ on\ your\ computer.\ This\ will\ open\ a\ dialog\ where\ you\ can\ browse\ and\ select\ the\ file.=Klicken Sie auf die Schaltfläche "Hinzufügen" (erste Schaltfläche mit einem Pluszeichen), um eine PDF-Datei zu verlinken, die Sie bereits auf Ihrem Computer haben. Dies öffnet einen Dialog, in dem Sie die Datei durchsuchen und auswählen können. - -# Step 8 Browse\ for\ your\ PDF\ file=Durchsuchen Sie Ihre PDF-Datei Use\ the\ "Browse"\ button\ to\ select\ a\ PDF\ file\ from\ your\ computer.\ Click\ the\ folder\ icon\ next\ to\ the\ "Link"\ field\ to\ open\ the\ file\ browser.=Verwenden Sie die Schaltfläche "Durchsuchen", um eine PDF-Datei von Ihrem Computer auszuwählen. Klicken Sie auf das Ordnersymbol neben dem Feld "Link", um den Dateibrowser zu öffnen. -# Step 9 +# Step 8 Add\ a\ description\ for\ the\ file=Beschreibung für die Datei hinzufügen Enter\ a\ meaningful\ description\ for\ this\ file\ in\ the\ "Description"\ field.\ This\ helps\ you\ identify\ the\ file\ later.=Geben Sie eine aussagekräftige Beschreibung für diese Datei im Feld "Beschreibung" ein, damit Sie die Datei später identifizieren können. -# Step 10 +# Step 9 Select\ the\ file\ type=Wählen Sie den Dateityp Choose\ the\ appropriate\ file\ type\ from\ the\ "Filetype"\ dropdown.\ Usually\ "PDF"\ is\ the\ correct\ choice\ for\ research\ papers.=Wählen Sie den passenden Dateityp aus der Dropdown-Liste "Dateitypen". -# Step 11 +# Step 10 Optionally\ add\ source\ URL=Optional Quell-URL hinzufügen If\ you\ downloaded\ this\ file\ from\ a\ website,\ you\ can\ add\ the\ source\ URL\ in\ the\ "Source\ URL"\ field.\ This\ is\ optional\ but\ helpful\ for\ tracking\ where\ you\ found\ the\ file.=Wenn Sie diese Datei von einer Webseite heruntergeladen haben, können Sie die Quell-URL im Feld "Quell-URL" hinzufügen. Dies ist optional, aber hilfreich, um zu verfolgen, wo Sie die Datei gefunden haben. -# Step 12 +# Step 11 Complete\ the\ file\ linking=Vervollständige die Datei-Verknüpfung Now\ click\ "Add"\ to\ complete\ the\ file\ linking\ process,\ or\ "Cancel"\ to\ try\ another\ method.=Klicken Sie nun auf "Hinzufügen", um den Datei-Verknüpfungs-Prozess abzuschließen, oder auf "Abbrechen", um eine andere Methode auszuprobieren. +# Step 12 +PDF\ file\ management\ area=PDF-Dateiverwaltungsbereich +This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Dies ist der Bereich der PDF-Dateiverwaltung, in dem Sie Dateien mit Ihren Bibliographieeinträgen verknüpfen können. Beachten Sie die drei Tasten auf der rechten Seite – jeder bietet eine andere Möglichkeit, PDF-Dateien hinzuzufügen. Lassen Sie uns Schritt für Schritt jede Methode erkunden. + # Step 13 Click\ "Get\ fulltext"\ to\ find\ PDFs\ automatically=Klicken Sie auf "Volltext abrufen", um PDFs automatisch zu finden Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=Klicken Sie auf den "Volltext"-Button (zweite Schaltfläche mit einem Download-Symbol), um JabRef automatisch nach der PDF-Datei suchen und mit Online-Abrufen herunterladen zu lassen. Dies funktioniert, wenn Ihr Eintrag richtige Metadaten wie DOI oder Titel hat. diff --git a/jablib/src/main/resources/l10n/JabRef_es.properties b/jablib/src/main/resources/l10n/JabRef_es.properties index 03fcbbdf54e2..3babfdf92e4d 100644 --- a/jablib/src/main/resources/l10n/JabRef_es.properties +++ b/jablib/src/main/resources/l10n/JabRef_es.properties @@ -2781,10 +2781,8 @@ Next=Siguiente # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=Haga clic en el menú "Archivo" # Step 2 -Click\ on\ "Preferences"=Haga clic en "Preferencias" # Step 3 Select\ the\ "Linked\ files"\ tab=Seleccione la pestaña "Archivos enlazados" @@ -2815,42 +2813,35 @@ Double\ click\ on\ the\ "Ding_2006"\ entry=Haga doble clic en la entrada "Ding_2 Let's\ start\ by\ selecting\ an\ entry\ to\ work\ with.\ Double\ click\ on\ the\ entry\ titled\ "Chocolate\ and\ Prevention\ of\ Cardiovascular\ Disease\:\ A\ Systematic\ Review"\ by\ Ding\ et\ al.=Empecemos seleccionando una entrada con la que trabajar. Haga doble clic en la entrada titulada "Chocolate and Prevention of Cardiovascular Disease\: A Systematic Review" de Ding et al. # Step 4 -Click\ on\ the\ "General"\ tab=Haga clic en la pestaña "General" -Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "General"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Ahora tenemos que acceder al editor de entradas. Haga clic en la pestaña "General" para ver y editar los detalles de la entrada. # Step 5 -PDF\ file\ management\ area=Área de gestión de archivos PDF -This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Esta es la sección de administración de archivos PDF, donde puede vincular archivos a sus entradas bibliográficas. Observe los tres botones a la derecha; cada uno ofrece una forma diferente de agregar archivos PDF. Exploremos cada método paso a paso. # Step 6 -Drag\ and\ drop\ files=Arrastrar y soltar archivos -You\ can\ drag\ and\ drop\ PDF\ files\ directly\ onto\ the\ file\ list\ area.\ This\ is\ often\ the\ quickest\ way\ to\ link\ files\ that\ are\ already\ on\ your\ computer.=Puedes arrastrar y soltar archivos PDF directamente en la lista de archivos. Esta suele ser la forma más rápida de vincular archivos que ya tienes en tu ordenador. -Try\ dragging\ a\ PDF\ file\ here\ to\ continue\ the\ walkthrough.=Intente arrastrar un archivo PDF aquí para continuar con el tutorial. # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=Haga clic en "Agregar" para vincular un archivo desde su computadora -Click\ the\ "Add"\ button\ (first\ button\ with\ a\ plus\ icon)\ to\ link\ a\ PDF\ file\ that\ you\ already\ have\ on\ your\ computer.\ This\ will\ open\ a\ dialog\ where\ you\ can\ browse\ and\ select\ the\ file.=Haz clic en el botón "Añadir" (el primer botón con el símbolo "+") para vincular un archivo PDF que ya tengas en tu ordenador. Se abrirá un cuadro de diálogo donde podrás explorar y seleccionar el archivo. - -# Step 8 Browse\ for\ your\ PDF\ file=Busque su archivo PDF Use\ the\ "Browse"\ button\ to\ select\ a\ PDF\ file\ from\ your\ computer.\ Click\ the\ folder\ icon\ next\ to\ the\ "Link"\ field\ to\ open\ the\ file\ browser.=Utilice el botón "Explorar" para seleccionar un archivo PDF de su ordenador. Haga clic en el icono de carpeta junto al campo "Enlace" para abrir el explorador de archivos. -# Step 9 +# Step 8 Add\ a\ description\ for\ the\ file=Añadir una descripción para el archivo Enter\ a\ meaningful\ description\ for\ this\ file\ in\ the\ "Description"\ field.\ This\ helps\ you\ identify\ the\ file\ later.=Ingrese una descripción significativa para este archivo en el campo "Descripción". Esto le ayudará a identificarlo más adelante. -# Step 10 +# Step 9 Select\ the\ file\ type=Seleccione el tipo de archivo Choose\ the\ appropriate\ file\ type\ from\ the\ "Filetype"\ dropdown.\ Usually\ "PDF"\ is\ the\ correct\ choice\ for\ research\ papers.=Seleccione el tipo de archivo adecuado en el menú desplegable "Tipo de archivo". Normalmente, "PDF" es la opción correcta para trabajos de investigación. -# Step 11 +# Step 10 Optionally\ add\ source\ URL=Opcionalmente, agregue la URL de origen If\ you\ downloaded\ this\ file\ from\ a\ website,\ you\ can\ add\ the\ source\ URL\ in\ the\ "Source\ URL"\ field.\ This\ is\ optional\ but\ helpful\ for\ tracking\ where\ you\ found\ the\ file.=Si descargaste este archivo de un sitio web, puedes agregar la URL de origen en el campo "URL de origen". Esto es opcional, pero útil para saber dónde encontraste el archivo. -# Step 12 +# Step 11 Complete\ the\ file\ linking= Now\ click\ "Add"\ to\ complete\ the\ file\ linking\ process,\ or\ "Cancel"\ to\ try\ another\ method.=Ahora haga clic en "Agregar" para completar el proceso de vinculación de archivos, o "Cancelar" para probar otro método. +# Step 12 +PDF\ file\ management\ area=Área de gestión de archivos PDF +This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Esta es la sección de administración de archivos PDF, donde puede vincular archivos a sus entradas bibliográficas. Observe los tres botones a la derecha; cada uno ofrece una forma diferente de agregar archivos PDF. Exploremos cada método paso a paso. + # Step 13 Click\ "Get\ fulltext"\ to\ find\ PDFs\ automatically=Haga clic en "Obtener texto completo" para buscar archivos PDF automáticamente Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=Haga clic en el botón "Obtener texto completo" (el segundo botón con un icono de descarga) para que JabRef busque y descargue automáticamente el PDF mediante buscadores en línea. Esto funciona cuando su entrada tiene metadatos adecuados, como el DOI o el título. diff --git a/jablib/src/main/resources/l10n/JabRef_fi.properties b/jablib/src/main/resources/l10n/JabRef_fi.properties index 0d39329b7091..e875d986180a 100644 --- a/jablib/src/main/resources/l10n/JabRef_fi.properties +++ b/jablib/src/main/resources/l10n/JabRef_fi.properties @@ -1817,10 +1817,8 @@ Next=Seuraava # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=Klikkaa "Tiedosto" -valikkoa # Step 2 -Click\ on\ "Preferences"=Klikkaa "Asetukset" # Step 3 Select\ the\ "Linked\ files"\ tab=Valitse "Linkitetyt tiedostot" -välilehti @@ -1846,31 +1844,28 @@ Welcome\ to\ PDF\ linking\ walkthrough=Tervetuloa PDF-linkityksen ohjeeseen Double\ click\ on\ the\ "Ding_2006"\ entry=Tuplaklikkaa kohtaa "Ding_2006" # Step 4 -Click\ on\ the\ "General"\ tab=Klikkaa "Yleiset"-välilehteä # Step 5 -PDF\ file\ management\ area=PDF-tiedostojen hallinta-alue # Step 6 -Drag\ and\ drop\ files=Vedä ja pudota tiedosto(t) # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=Napsauta "Lisää" linkittääksesi tiedoston tietokoneeltasi +Browse\ for\ your\ PDF\ file=Selaa PDF-tiedostoasi # Step 8 -Browse\ for\ your\ PDF\ file=Selaa PDF-tiedostoasi +Add\ a\ description\ for\ the\ file=Lisää tiedoston kuvaus # Step 9 -Add\ a\ description\ for\ the\ file=Lisää tiedoston kuvaus +Select\ the\ file\ type=Valitse tiedostotyyppi # Step 10 -Select\ the\ file\ type=Valitse tiedostotyyppi +Optionally\ add\ source\ URL=Lisää halutessasi lähde URL-osoite # Step 11 -Optionally\ add\ source\ URL=Lisää halutessasi lähde URL-osoite +Complete\ the\ file\ linking=Viimeistele tiedostojen linkittäminen # Step 12 -Complete\ the\ file\ linking=Viimeistele tiedostojen linkittäminen +PDF\ file\ management\ area=PDF-tiedostojen hallinta-alue # Step 13 diff --git a/jablib/src/main/resources/l10n/JabRef_fr.properties b/jablib/src/main/resources/l10n/JabRef_fr.properties index c992b201b759..64edbdb73e5e 100644 --- a/jablib/src/main/resources/l10n/JabRef_fr.properties +++ b/jablib/src/main/resources/l10n/JabRef_fr.properties @@ -2068,6 +2068,9 @@ Pre-selected\ fetchers=Outils de collecte présélectionnés Custom\ API\ key=Clef API personnalisée API\ Key\ for\ %0=Clef API pour %0 Test\ connection=Test de connexion +Connection\ failed=Échec de la connexion +Connection\ successful=Connexion réussie +The\ model\ %0\ was\ not\ found\ on\ the\ server.\ If\ you\ use\ Ollama,\ download\ it\ with\:\ %1=Le modèle %0 n'a pas été trouvé sur le serveur. Si vous utilisez Ollama, téléchargez-le avec \: %1 API\ Key=Clef API Configure\ API\ key=Configurer la clef API API\ Key\ Valid=Clef API valide @@ -3176,6 +3179,7 @@ Failed\ to\ create\ entry\ from\ URL=Impossible de créer une entrée à partir Failed\ to\ fetch\ the\ URL.\nThe\ following\ error\ was\ encountered\:\n%0=Impossible de récupérer l'URL.\nL'erreur suivante a été rencontrée \:\n%0 Failed\ to\ interpret\ citations.\nThe\ following\ error\ was\ encountered\:\n%0=Impossible d'interpréter les citations.\nL'erreur suivante a été rencontrée \:\n%0 Failed\ to\ interpret\ citations=Impossible d'interpréter les citations +Parsing\ citations\ with\ LLM=Analyse des citations par LLM Failed\ to\ lookup\ identifier=Échec de la recherche de l'identifiant Fetching...=Récupération en cours... Failed\ to\ migrate\ AI\ chat\ history.\ See\ logs\ for\ details.=Échec de la migration de l'historique des tchats d'IA. Voir les logs pour plus de détails. @@ -3226,10 +3230,8 @@ Next=Suivant # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=Cliquez sur le menu "Fichier" # Step 2 -Click\ on\ "Preferences"=Cliquez sur "Préférences" # Step 3 Select\ the\ "Linked\ files"\ tab=Sélectionnez l'onglet "Documents liés" @@ -3260,42 +3262,40 @@ Double\ click\ on\ the\ "Ding_2006"\ entry=Double-cliquez sur l'entrée "Ding_20 Let's\ start\ by\ selecting\ an\ entry\ to\ work\ with.\ Double\ click\ on\ the\ entry\ titled\ "Chocolate\ and\ Prevention\ of\ Cardiovascular\ Disease\:\ A\ Systematic\ Review"\ by\ Ding\ et\ al.=Commençons par sélectionner une entrée avec laquelle travailler. Double-cliquez sur l'entrée intitulée "Chocolat and Prevention of Cardiovascular Disease\: A Systematic Review" de Ding et al. # Step 4 -Click\ on\ the\ "General"\ tab=Cliquez sur l'onglet "Général" -Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "General"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Maintenant, nous devons accéder à l'éditeur d'entrée. Cliquez sur l'onglet "General" pour voir et modifier les détails de l'entrée. +Click\ on\ the\ "Main"\ tab=Cliquez sur l'onglet "Principal" +Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "Main"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Nous avons à présent besoin d'accéder à l'éditeur d'entrée. Cliquez sur l'onglet "Principal" pour voir et modifier les détails de l'entrée. # Step 5 -PDF\ file\ management\ area=Espace de gestion des fichiers PDF -This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Il s'agit de la zone de gestion de fichiers PDF où vous pouvez lier des fichiers à vos entrées bibliographiques. Notez les trois boutons sur le côté droit \: chacun offre une façon différente d'ajouter des fichiers PDF. Examinons chaque méthode, étape par étape. +Expand\ "%0"=Développer «%0» +The\ "%0"\ section\ contains\ the\ fields\ used\ to\ link\ PDFs\ and\ other\ files.=La section "%0" contient les champs utilisés pour lier des PDF et autres fichiers. # Step 6 -Drag\ and\ drop\ files=Glisser et déposer des fichiers -You\ can\ drag\ and\ drop\ PDF\ files\ directly\ onto\ the\ file\ list\ area.\ This\ is\ often\ the\ quickest\ way\ to\ link\ files\ that\ are\ already\ on\ your\ computer.=Vous pouvez glisser-déposer des fichiers PDF directement dans la zone de liste des fichiers. C'est souvent la façon la plus rapide de lier des fichiers qui sont déjà sur votre ordinateur. -Try\ dragging\ a\ PDF\ file\ here\ to\ continue\ the\ walkthrough.=Essayez de faire glisser un fichier PDF ici pour continuer la procédure pas-à-pas. +Click\ "%0"\ to\ add\ a\ PDF\ file.\ This\ opens\ a\ dialog\ where\ you\ can\ enter\ the\ file\ details.\ If\ the\ "File"\ field\ is\ already\ shown,\ click\ its\ "+"\ button\ instead.=Cliquez sur «%0» pour ajouter un fichier PDF. Cela ouvre une boîte de dialogue où vous pouvez entrer les détails du fichier. Si le champ "Fichier" est déjà affiché, cliquez sur le bouton "+" à la place. # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=Cliquez sur "Ajouter" pour lier un fichier présent sur votre ordinateur -Click\ the\ "Add"\ button\ (first\ button\ with\ a\ plus\ icon)\ to\ link\ a\ PDF\ file\ that\ you\ already\ have\ on\ your\ computer.\ This\ will\ open\ a\ dialog\ where\ you\ can\ browse\ and\ select\ the\ file.=Cliquez sur le bouton "Ajouter" (premier bouton avec une icône plus) pour lier un fichier PDF que vous avez déjà sur votre ordinateur. Cela ouvrira une boîte de dialogue où vous pourrez naviguer et sélectionner ce fichier. - -# Step 8 Browse\ for\ your\ PDF\ file=Parcourir vers votre fichier PDF Use\ the\ "Browse"\ button\ to\ select\ a\ PDF\ file\ from\ your\ computer.\ Click\ the\ folder\ icon\ next\ to\ the\ "Link"\ field\ to\ open\ the\ file\ browser.=Utilisez le bouton "Parcourir" pour sélectionner un fichier PDF sur votre ordinateur. Cliquez sur l'icône dossier à côté du champ "Lien" pour ouvrir le navigateur de fichiers. -# Step 9 +# Step 8 Add\ a\ description\ for\ the\ file=Ajouter une description pour ce fichier Enter\ a\ meaningful\ description\ for\ this\ file\ in\ the\ "Description"\ field.\ This\ helps\ you\ identify\ the\ file\ later.=Entrez une description significative pour ce fichier dans le champ "Description". Cela vous aidera plus tard à identifier ce fichier. -# Step 10 +# Step 9 Select\ the\ file\ type=Sélectionnez le type de fichier Choose\ the\ appropriate\ file\ type\ from\ the\ "Filetype"\ dropdown.\ Usually\ "PDF"\ is\ the\ correct\ choice\ for\ research\ papers.=Choisissez le type de fichier approprié dans le menu déroulant « Type de fichier ». Habituellement « PDF » est le bon choix pour les articles de recherche. -# Step 11 +# Step 10 Optionally\ add\ source\ URL=Optionnellement, ajouter l'URL source If\ you\ downloaded\ this\ file\ from\ a\ website,\ you\ can\ add\ the\ source\ URL\ in\ the\ "Source\ URL"\ field.\ This\ is\ optional\ but\ helpful\ for\ tracking\ where\ you\ found\ the\ file.=Si vous avez téléchargé ce fichier à partir d'un site Web, vous pouvez ajouter l'URL source dans le champ "URL source". Ceci est facultatif, mais utile pour se souvenir de l'endroit où vous avez trouvé le fichier. -# Step 12 +# Step 11 Complete\ the\ file\ linking=Compléter le lien de fichier Now\ click\ "Add"\ to\ complete\ the\ file\ linking\ process,\ or\ "Cancel"\ to\ try\ another\ method.=Maintenant, cliquez sur "Ajouter" pour terminer le processus de liaison de fichier, ou "Annuler" pour essayer une autre méthode. +# Step 12 +PDF\ file\ management\ area=Espace de gestion des fichiers PDF +This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Il s'agit de la zone de gestion de fichiers PDF où vous pouvez lier des fichiers à vos entrées bibliographiques. Notez les trois boutons sur le côté droit \: chacun offre une façon différente d'ajouter des fichiers PDF. Examinons chaque méthode, étape par étape. + # Step 13 Click\ "Get\ fulltext"\ to\ find\ PDFs\ automatically=Cliquez sur "Obtenir le document" pour trouver automatiquement les PDF Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=Cliquez sur le bouton « Obtenir le document » (deuxième bouton avec une icône de téléchargement) pour que JabRef recherche automatiquement le PDF et le télécharge à l'aide des extracteurs en ligne. Cela fonctionne lorsque votre entrée a des métadonnées comme un DOI ou un titre. diff --git a/jablib/src/main/resources/l10n/JabRef_it.properties b/jablib/src/main/resources/l10n/JabRef_it.properties index 8dafd11c38de..92df8e71434e 100644 --- a/jablib/src/main/resources/l10n/JabRef_it.properties +++ b/jablib/src/main/resources/l10n/JabRef_it.properties @@ -2064,6 +2064,9 @@ Pre-selected\ fetchers=Recuperatori preselezionati Custom\ API\ key=Chiave API personalizzata API\ Key\ for\ %0=Chiave API per %0 Test\ connection=Test connessione +Connection\ failed=Connessione fallita +Connection\ successful=Connessione riuscita +The\ model\ %0\ was\ not\ found\ on\ the\ server.\ If\ you\ use\ Ollama,\ download\ it\ with\:\ %1=Il modello %0 non è stato trovato sul server. Se usi Ollama, scaricalo con\: %1 API\ Key=Chiave API Configure\ API\ key=Configura chiave API API\ Key\ Valid=Chiave API valida @@ -3159,6 +3162,7 @@ Failed\ to\ create\ entry\ from\ URL=Impossibile creare la voce dall'URL Failed\ to\ fetch\ the\ URL.\nThe\ following\ error\ was\ encountered\:\n%0=Impossibile recuperare l'URL.\nÈ stato riscontrato il seguente errore\:\n%0 Failed\ to\ interpret\ citations.\nThe\ following\ error\ was\ encountered\:\n%0=Impossibile interpretare le citazioni.\nÈ stato riscontrato il seguente errore\:\n%0 Failed\ to\ interpret\ citations=Impossibile interpretare le citazioni +Parsing\ citations\ with\ LLM=Analisi citazioni con LLM Failed\ to\ lookup\ identifier=Impossibile cercare l'identificatore Fetching...=Recupero in corso... Failed\ to\ migrate\ AI\ chat\ history.\ See\ logs\ for\ details.=Migrazione della cronologia chat AI non riuscita. Vedere i registri per i dettagli. @@ -3183,10 +3187,8 @@ Next=Successivo # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=Clicca sul menu "File" # Step 2 -Click\ on\ "Preferences"=Clicca su "Preferenze" # Step 3 Select\ the\ "Linked\ files"\ tab=Seleziona la scheda "File collegati" @@ -3217,42 +3219,40 @@ Double\ click\ on\ the\ "Ding_2006"\ entry=Fare doppio clic sulla voce "Ding_200 Let's\ start\ by\ selecting\ an\ entry\ to\ work\ with.\ Double\ click\ on\ the\ entry\ titled\ "Chocolate\ and\ Prevention\ of\ Cardiovascular\ Disease\:\ A\ Systematic\ Review"\ by\ Ding\ et\ al.=Iniziamo selezionando una voce con cui lavorare. Doppio click sulla voce intitolata "Chocolate and Prevention of Cardiovascular Disease\: A Systematic Review" di Ding et al. # Step 4 -Click\ on\ the\ "General"\ tab=Clicca sulla scheda "Generale" -Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "General"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Ora abbiamo bisogno di accedere all'editor degli elementi. Fare clic sulla scheda "Generale" per visualizzare e modificare i dettagli dell'elemento. +Click\ on\ the\ "Main"\ tab=Clicca sulla scheda "Generale" +Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "Main"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Ora abbiamo bisogno di accedere all'editor degli elementi. Fare clic sulla scheda "Generale" per visualizzare e modificare i dettagli dell'elemento. # Step 5 -PDF\ file\ management\ area=Area di gestione file PDF -This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Questa è l'area di gestione file PDF dove è possibile collegare i file alle voci bibliografiche. Notare i tre pulsanti sul lato destro, ciascuno offre un modo diverso per aggiungere file PDF. Esploriamo ogni metodo passo dopo passo. +Expand\ "%0"=Espandi "%0" +The\ "%0"\ section\ contains\ the\ fields\ used\ to\ link\ PDFs\ and\ other\ files.=La sezione "%0" contiene i campi utilizzati per collegare PDF e altri file. # Step 6 -Drag\ and\ drop\ files=Trascina e rilascia i file -You\ can\ drag\ and\ drop\ PDF\ files\ directly\ onto\ the\ file\ list\ area.\ This\ is\ often\ the\ quickest\ way\ to\ link\ files\ that\ are\ already\ on\ your\ computer.=È possibile trascinare e rilasciare i file PDF direttamente nell'area dell'elenco dei file. Questo è spesso il modo più veloce per collegare i file che sono già sul computer. -Try\ dragging\ a\ PDF\ file\ here\ to\ continue\ the\ walkthrough.=Prova a trascinare un file PDF qui per continuare la guida passo passo. +Click\ "%0"\ to\ add\ a\ PDF\ file.\ This\ opens\ a\ dialog\ where\ you\ can\ enter\ the\ file\ details.\ If\ the\ "File"\ field\ is\ already\ shown,\ click\ its\ "+"\ button\ instead.=Fare clic su "%0" per aggiungere un file PDF. Questo apre una finestra di dialogo dove è possibile inserire i dettagli del file. Se il campo "File" è già mostrato, fare clic invece sul suo pulsante "+". # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=Fare clic su "Aggiungi" per collegare un file dal computer -Click\ the\ "Add"\ button\ (first\ button\ with\ a\ plus\ icon)\ to\ link\ a\ PDF\ file\ that\ you\ already\ have\ on\ your\ computer.\ This\ will\ open\ a\ dialog\ where\ you\ can\ browse\ and\ select\ the\ file.=Fare clic sul pulsante "Aggiungi" (primo pulsante con un'icona più) per collegare un file PDF che hai già sul computer. Questo aprirà una finestra di dialogo in cui puoi navigare e selezionare il file. - -# Step 8 Browse\ for\ your\ PDF\ file=Naviga verso il tuo file PDF Use\ the\ "Browse"\ button\ to\ select\ a\ PDF\ file\ from\ your\ computer.\ Click\ the\ folder\ icon\ next\ to\ the\ "Link"\ field\ to\ open\ the\ file\ browser.=Utilizzare il pulsante "Sfoglia" per selezionare un file PDF dal computer. Fare clic sull'icona della cartella accanto al campo "Link" per aprire il navigatore dei file. -# Step 9 +# Step 8 Add\ a\ description\ for\ the\ file=Aggiunge una descrizione per il file Enter\ a\ meaningful\ description\ for\ this\ file\ in\ the\ "Description"\ field.\ This\ helps\ you\ identify\ the\ file\ later.=Inserisci una descrizione significativa per questo file nel campo "Descrizione". Questo ti aiuta a identificare il file più tardi. -# Step 10 +# Step 9 Select\ the\ file\ type=Seleziona il tipo di file Choose\ the\ appropriate\ file\ type\ from\ the\ "Filetype"\ dropdown.\ Usually\ "PDF"\ is\ the\ correct\ choice\ for\ research\ papers.=Scegli il tipo di file appropriato dal menu a discesa "Filetype". Di solito "PDF" è la scelta corretta per i documenti di ricerca. -# Step 11 +# Step 10 Optionally\ add\ source\ URL=Opzionalmente aggiungi URL sorgente If\ you\ downloaded\ this\ file\ from\ a\ website,\ you\ can\ add\ the\ source\ URL\ in\ the\ "Source\ URL"\ field.\ This\ is\ optional\ but\ helpful\ for\ tracking\ where\ you\ found\ the\ file.=Se hai scaricato questo file da un sito web, puoi aggiungere l'URL sorgente nel campo "URL sorgente". Questo è facoltativo ma utile per tracciare dove hai trovato il file. -# Step 12 +# Step 11 Complete\ the\ file\ linking=Completa il collegamento del file Now\ click\ "Add"\ to\ complete\ the\ file\ linking\ process,\ or\ "Cancel"\ to\ try\ another\ method.=Ora fare clic su "Aggiungi" per completare il processo di collegamento dei file, o "Annulla" per provare un altro metodo. +# Step 12 +PDF\ file\ management\ area=Area di gestione file PDF +This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Questa è l'area di gestione file PDF dove è possibile collegare i file alle voci bibliografiche. Notare i tre pulsanti sul lato destro, ciascuno offre un modo diverso per aggiungere file PDF. Esploriamo ogni metodo passo dopo passo. + # Step 13 Click\ "Get\ fulltext"\ to\ find\ PDFs\ automatically=Fare clic su "Ottieni testo completo" per trovare automaticamente i PDF Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=Fare clic sul pulsante "Get fulltext" (secondo pulsante con un'icona di download) per consentire a JabRef di cercare e scaricare automaticamente il PDF utilizzando i fetcher online. Funziona quando la tua voce ha metadati adeguati come DOI o titolo. diff --git a/jablib/src/main/resources/l10n/JabRef_pt_BR.properties b/jablib/src/main/resources/l10n/JabRef_pt_BR.properties index 642f41389460..5aa9339b7932 100644 --- a/jablib/src/main/resources/l10n/JabRef_pt_BR.properties +++ b/jablib/src/main/resources/l10n/JabRef_pt_BR.properties @@ -2768,10 +2768,8 @@ Next=Próximo # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=Clique no menu "Arquivo" # Step 2 -Click\ on\ "Preferences"=Clique em "Preferências" # Step 3 Select\ the\ "Linked\ files"\ tab=Selecione a guia "Arquivos vinculados" @@ -2802,42 +2800,35 @@ Double\ click\ on\ the\ "Ding_2006"\ entry=Clique duas vezes na entrada "Ding_20 Let's\ start\ by\ selecting\ an\ entry\ to\ work\ with.\ Double\ click\ on\ the\ entry\ titled\ "Chocolate\ and\ Prevention\ of\ Cardiovascular\ Disease\:\ A\ Systematic\ Review"\ by\ Ding\ et\ al.=Vamos começar selecionando uma entrada para trabalhar. Clique duas vezes na entrada intitulada "Chocolate e Prevenção da Doença Cardiovascular\: Uma Revisão sistemática" por Ding et al. # Step 4 -Click\ on\ the\ "General"\ tab=Clique na guia "Geral" -Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "General"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=Agora precisamos acessar o editor de entrada. Clique na aba "Geral" para visualizar e editar os detalhes da entrada. # Step 5 -PDF\ file\ management\ area=Área de gerenciamento de arquivo PDF -This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Esta é a área de gerenciamento de arquivos PDF onde você pode vincular arquivos a suas entradas de bibliografia. Observe que os três botões no lado direito — cada um oferece uma maneira diferente de adicionar arquivos PDF. Vamos explorar cada método passo a passo. # Step 6 -Drag\ and\ drop\ files=Arraste e solte arquivos -You\ can\ drag\ and\ drop\ PDF\ files\ directly\ onto\ the\ file\ list\ area.\ This\ is\ often\ the\ quickest\ way\ to\ link\ files\ that\ are\ already\ on\ your\ computer.=Você pode arrastar e soltar arquivos PDF diretamente na área da lista de arquivos. Muitas vezes é a maneira mais rápida de vincular arquivos que já estão no seu computador. -Try\ dragging\ a\ PDF\ file\ here\ to\ continue\ the\ walkthrough.=Tente arrastar aqui um arquivo PDF para continuar o passeio. # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=Clique em "Adicionar" para vincular um arquivo do seu computador -Click\ the\ "Add"\ button\ (first\ button\ with\ a\ plus\ icon)\ to\ link\ a\ PDF\ file\ that\ you\ already\ have\ on\ your\ computer.\ This\ will\ open\ a\ dialog\ where\ you\ can\ browse\ and\ select\ the\ file.=Clique no botão "Adicionar" (primeiro botão com um ícone de mais) para vincular um arquivo PDF que você já tem no seu computador. Isto irá abrir um diálogo onde você pode procurar e selecionar o arquivo. - -# Step 8 Browse\ for\ your\ PDF\ file=Procure seu arquivo PDF Use\ the\ "Browse"\ button\ to\ select\ a\ PDF\ file\ from\ your\ computer.\ Click\ the\ folder\ icon\ next\ to\ the\ "Link"\ field\ to\ open\ the\ file\ browser.=Use o botão "Browse" para selecionar um arquivo PDF do seu computador. Clique no ícone da pasta ao lado do campo "Link" para abrir o navegador de arquivos. -# Step 9 +# Step 8 Add\ a\ description\ for\ the\ file=Adicione uma descrição para o arquivo Enter\ a\ meaningful\ description\ for\ this\ file\ in\ the\ "Description"\ field.\ This\ helps\ you\ identify\ the\ file\ later.=Insira uma descrição significativa para este arquivo no campo "Descrição". Isso ajuda a identificar o arquivo mais tarde. -# Step 10 +# Step 9 Select\ the\ file\ type=Selecione o tipo de arquivo Choose\ the\ appropriate\ file\ type\ from\ the\ "Filetype"\ dropdown.\ Usually\ "PDF"\ is\ the\ correct\ choice\ for\ research\ papers.=Escolha o tipo de arquivo apropriado no menu suspenso "Filetype". Geralmente "PDF" é a escolha correta para papéis de pesquisa. -# Step 11 +# Step 10 Optionally\ add\ source\ URL=Opcionalmente adicione URL de origem If\ you\ downloaded\ this\ file\ from\ a\ website,\ you\ can\ add\ the\ source\ URL\ in\ the\ "Source\ URL"\ field.\ This\ is\ optional\ but\ helpful\ for\ tracking\ where\ you\ found\ the\ file.=Se você fez o download deste arquivo de um site, você pode adicionar a URL de origem no campo "URL Fonte". Isso é opcional, mas útil para rastrear onde encontrou o arquivo. -# Step 12 +# Step 11 Complete\ the\ file\ linking=Completar o link do arquivo Now\ click\ "Add"\ to\ complete\ the\ file\ linking\ process,\ or\ "Cancel"\ to\ try\ another\ method.=Agora clique em "Adicionar" para completar o processo de vinculação de arquivo, ou "Cancelar" para tentar outro método. +# Step 12 +PDF\ file\ management\ area=Área de gerenciamento de arquivo PDF +This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=Esta é a área de gerenciamento de arquivos PDF onde você pode vincular arquivos a suas entradas de bibliografia. Observe que os três botões no lado direito — cada um oferece uma maneira diferente de adicionar arquivos PDF. Vamos explorar cada método passo a passo. + # Step 13 Click\ "Get\ fulltext"\ to\ find\ PDFs\ automatically=Clique em "Obter texto completo" para encontrar PDFs automaticamente Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=Clique no botão "Get fulltext" (segundo botão com um ícone de download) para deixar o JabRef automaticamente procurar e baixar o PDF usando fetchers online. Isso funciona quando sua entrada possui metadados adequados, como DOI ou título. diff --git a/jablib/src/main/resources/l10n/JabRef_ru.properties b/jablib/src/main/resources/l10n/JabRef_ru.properties index b4d6ab299e83..a6aaa859358f 100644 --- a/jablib/src/main/resources/l10n/JabRef_ru.properties +++ b/jablib/src/main/resources/l10n/JabRef_ru.properties @@ -1,5 +1,6 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=Не удается отслеживать изменения файла. Закройте файлы и перезапустите программу. Вы можете столкнуться с ошибками, если продолжите текущую сессию. +Citation\ count\ fetcher\:=Инструмент выборки количества цитирований\: %0/%1\ entries=%0/%1 записей @@ -16,6 +17,10 @@ Abbreviate\ names=Сокращения названий Abbreviation=Сокращение Abbreviations=Аббревиатуры +Abbreviate\ (default)=Сокращать (по умолчанию) +Abbreviate\ (dotless)=Сокращать (без точек) +Abbreviate\ (shortest\ unique)=Сокращать (кратчайшее уникальное) +Abbreviate\ (LTWA)=Сокращать (LTWA) Abstract=Резюме @@ -124,6 +129,7 @@ Copy\ title=Копировать заглавие Copy\ field\ content=Скопировать содержимое поля Copy\ citation\ (html)=Копировать цитирование (html) Copy\ citation\ (text)=Копировать цитирование (текст) +Copy\ citation\ (Markdown)=Копировать цитирование (Markdown) Copy\ citation\ key=Копировать ключ цитаты Copy\ citation\ key\ and\ link=Скопировать ключ цитаты и ссылку @@ -252,6 +258,7 @@ Extract\ references\ from\ file\ (offline)=Извлечь источники и Extract\ references\ from\ file\ (online)=Извлечь источники из файла (онлайн) Extract\ References\ (offline)=Извлечь источники (в автономном режиме) Extract\ References\ (online)=Извлечь источники (онлайн) +Insert\ related\ work\ comments=Вставить комментарии из раздела «Связанные работы» Processing...=Обработка... Processing\ "%0"...=Обработка "%0"... @@ -745,6 +752,7 @@ Redownload\ file(s)=Перезагрузить файл(ы) Copy\ linked\ file(s)=Скопировать связанный(е) файл(ы) # TODO: "folder" vs. "directory" consistency OCR\ failed=Ошибка оптического распознавания текста +OCR\ finished\ successfully.=Оптическое распознавание текста успешно завершено. Perform\ OCR=Выполнить оптическое распознавание текста Performing\ OCR=Выполнение оптического распознавания текста Could\ not\ start\ OCR\ process=Не удалось запустить оптическое распознавание текста @@ -753,6 +761,11 @@ OCR\ timed\ out=Время оптического распознавания т OCR\ was\ cancelled=Оптическое распознавание текста было отменено Could\ not\ find\ a\ file\ to\ OCR=Не удалось найти файл для оптического распознавания текста OCR\ failed.\ See\ the\ logs\ for\ the\ details=Ошибка оптического распознавания текста. Смотрите журнал событий для деталей +OCR=Оптическое распознавание текста +Browse\ engine\ path=Выбрать путь к движку +OCR\ engine=Движок оптического распознавания текста +Engine\ path=Путь к движку +Engine\ selection=Выбор движка Copy\ linked\ file(s)\ to\ folder...=Скопировать связанный файл(ы) в папку... Copied\ %0\ file(s).\ Failed\:\ %1=Скопировано %0 файл(ы). С ошибкой\: %1 Successfully\ copied\ %0\ file(s)\ to\ %1.=%0 файлов успешно скопировано в %1. @@ -974,6 +987,7 @@ field\ is\ absent=поле отсутствует Consistency\ check\ completed=Проверка согласованности завершена +%0/%1\ entry\ types=%0/%1 типов записей Checking\ consistency\ for\ entry\ type\ %0\ of\ %1=Проверка согласованности для типа записи %0 из %1 Check\ consistency=Проверка согласованности @@ -1067,6 +1081,7 @@ Problem\ modifying\ citation=Ошибка при модификации цита Problem\ collecting\ citations=Возникла проблема при сборе ссылок Citation=Цитата Select\ style=Выбор стиля +Citation\ format=Формат цитирования Journal=Журнал Journals=Журналы Title=Заголовок @@ -1154,6 +1169,7 @@ Clean\ up=Очистка Automatically\ assign\ new\ entry\ to\ selected\ groups=Автоматически назначать новую запись выбранным группам %0\ mode=Режим %0 Move\ DOIs\ from\ 'note'\ field\ and\ 'URL'\ field\ to\ 'DOI'\ field\ and\ remove\ http\ prefix=Переместить DOI из поля 'note' и поля 'URL' в поле 'DOI' и удалить префикс http +For\ arXiv\ entries,\ keep\ the\ DOI\ and\ remove\ the\ redundant\ 'eprint'\ fields=Для записей arXiv сохранять DOI и удалять избыточные поля 'eprint' Move\ URL\ in\ 'note'\ field\ to\ 'URL'\ field=Переместить URL из поля 'note' в поле 'URL' Make\ paths\ of\ linked\ files\ relative\ (if\ possible)=Создавать относительные пути для связанных файлов (если возможно) Only\ rename\ files\ that\ have\ a\ relative\ path=Только переименовать файлы, имеющие относительный путь @@ -1390,6 +1406,7 @@ Automatically\ setting\ file\ links=Автоуказание ссылок на Default\ library\ mode=Режим библиотеки по умолчанию Save\ actions=Действия при сохранении +Enable\ field\ formatters=Включить форматоры полей Other\ fields=Прочие поля Show\ only\ set\ fields=Показать только установленные поля @@ -1624,6 +1641,7 @@ No\ entries\ selected\ for\ citation=Не выбрано записей для Show\ articles\ related\ by\ citation=Показать статьи, связанные с цитатой Error\ while\ fetching\ cited\ entries\:\ %0=Ошибка при получении цитируемых записей\: %0 Error\ while\ fetching\ citing\ entries\:\ %0=Ошибка при получении цитирующих записей\: %0 +Error\ while\ looking\ up\ DOI\:\ %0=Ошибка при поиске DOI\: %0 Found\ identical\ ranges=Найдены идентичные диапазоны Found\ overlapping\ ranges=Найдены пересекающиеся диапазоны @@ -1769,6 +1787,7 @@ Entered\ database\ has\ obsolete\ structure\ and\ is\ no\ longer\ supported.=В However,\ a\ new\ database\ was\ created\ alongside\ the\ pre-3.6\ one.=Вместе с тем была создана новая база данных до 3.6. Opens\ a\ link\ where\ the\ current\ development\ version\ can\ be\ downloaded=Открывает ссылку, где текущая версия для разработчиков может быть загружена See\ what\ has\ been\ changed\ in\ the\ JabRef\ versions=Посмотрите, что было изменено в версиях JabRef +Referenced\ citation\ key\ '%0'\ does\ not\ exist=Указанный ключ цитирования '%0' не существует Full\ text\ document\ for\ entry\ %0\ already\ linked.=Полный текст документа для записи %0 уже привязан. Download\ full\ text\ documents=Скачать полный текст документов last\ four\ nonpunctuation\ characters\ should\ be\ numerals=последние четыре знака должны быть цифрами (если это не знаки препинания) @@ -1809,6 +1828,7 @@ File\ directory\ pattern=Шаблон каталога файлов Update\ with\ bibliographic\ information\ from\ the\ web=Обновление библиографической информации из Интернета Could\ not\ find\ any\ bibliographic\ information.=Не удалось найти никакой библиографической информации. +Citation\ key\ deviates\ from\ generated\ key\ %0=Ключ цитирования отличается от сгенерированного ключа %0 Select\ all\ customized\ types\ to\ be\ stored\ in\ local\ preferences\:=Выберите все настраиваемые типы для хранения в локальных настройках\: Different\ customization,\ current\ settings\ will\ be\ overwritten=Разные настройки, текущие настройки будут перезаписаны @@ -1827,6 +1847,7 @@ Please\ also\ add\ all\ steps\ to\ reproduce\ this\ issue,\ if\ possible.=Пож Document\ viewer=Просмотр документа +Enable\ preview=Включить предварительный просмотр Live=В режиме реального времени Locked=Заблокировано @@ -1848,6 +1869,7 @@ Rename\ failed=Не удалось переименовать Remove\ line\ breaks=Удалить разрывы строк Removes\ all\ line\ breaks\ in\ the\ field\ content.=Удаляет все разрывы строк в содержимом поля. +Pre-selected\ fetchers=Предварительно выбранные инструменты выборки Remove\ hyphenated\ line\ breaks=Удалить переносы строк с использованием дефиса @@ -1975,8 +1997,12 @@ Allow\ integers\ in\ 'edition'\ field\ in\ BibTeX\ mode=Разрешить це # EntryEditor tabs +LaTeX\ citations=Цитирования в LaTeX Search\ for\ citations\ in\ LaTeX\ files...=Поиск цитат в файлах LaTeX... LaTeX\ Citations\ Search\ Results=Результаты поиска по цитатам LaTeX +Citation\ marker=Маркер цитирования +No\ citations\ were\ found\ in\ the\ related\ work\ text.=В тексте раздела «Связанные работы» не найдено библиографических ссылок. +Processed\ related\ work\ comments.\ Inserted\:\ %0,\ unchanged\:\ %1.=Обработаны комментарии из раздела «Связанные работы». Вставлено\: %0, без изменений\: %1. LaTeX\ files\ directory\:=Папка с файлами LaTeX\: LaTeX\ files\ found\:=Найдены файлы LaTeX\: Search\ citations\ for\ this\ entry\ in\ LaTeX\ files=Поиск цитат для этой записи в файлах LaTeX @@ -2513,6 +2539,7 @@ Chatting\ parameters=Параметры чата Chunked=Фрагментированный Citation\ Key=Ключ цитаты Cosine\ Similarity=Косинусное сходство +Default\ response\ engine=Механизм ответов по умолчанию Default\ summarization\ algorithm=Алгоритм сводок по умолчанию Default\ token\ estimation\ algorithm=Алгоритм оценки токенов по умолчанию Downloading\ local\ embedding\ model...=Загрузка локальной встраиваемой модели... @@ -2611,6 +2638,8 @@ File\ "%0"\ cannot\ be\ added\!=Файл "%0" не может быть доба +Looking\ up\ DOI...=Поиск DOI... +No\ DOI\ found.=DOI не найден. Related\ articles=Связанные статьи @@ -2618,6 +2647,7 @@ Related\ articles=Связанные статьи +Enable=Включить @@ -2625,6 +2655,7 @@ Related\ articles=Связанные статьи +Citation\ Entry=Найденная запись @@ -2634,9 +2665,16 @@ Related\ articles=Связанные статьи # citations tab +Total\ Citations\:\ %0=Всего цитирований\: %0 +Citations=Цитирования # NewEntry* +Choose\ Entry\ Type=Выберите тип записи +Failed\ to\ interpret\ citations.\nThe\ following\ error\ was\ encountered\:\n%0=Не удалось распознать библиографические ссылки.\nВозникла следующая ошибка\:\n%0 +Interpret\ Citations=Распознать библиографические ссылки +Interpret\ citations...=Распознать библиографические ссылки... +Interpret\ citations=Распознать библиографические ссылки # Walkthrough Menu @@ -2679,6 +2717,7 @@ Related\ articles=Связанные статьи # Step 12 # Step 13 +Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=Нажмите кнопку «Получить весь текст» (вторую кнопку со значком загрузки), чтобы JabRef автоматически нашёл и скачал PDF с помощью онлайн-инструментов выборки. Это работает, если у вашей записи есть корректные метаданные, например DOI или заголовок. # Step 14 @@ -2746,6 +2785,7 @@ Related\ articles=Связанные статьи # Step 16 # Step 17 +**Quick\ reference\:**\n-\ Press\ %0\ to\ jump\ to\ search\n-\ Use\ **field\ \=\ value**\ for\ field\ searches\n-\ Combine\ with\ **AND**,\ **OR**,\ **NOT**\n-\ Enable\ **regex**\ for\ pattern\ matching=**Краткая справка\:**\n- Нажмите %0, чтобы перейти к поиску\n- Используйте **поле \= значение** для поиска по полям\n- Комбинируйте с помощью **AND**, **OR**, **NOT**\n- Включите **regex** для сопоставления с шаблоном # Walkthrough side effects @@ -2774,6 +2814,7 @@ Related\ articles=Связанные статьи # Push to Applications Dialog # Online Services Dialog +Enable\ Grobid\ (metadata\ extraction\ service)=Включить Grobid (служба извлечения метаданных) # Entry Table Customization Dialog @@ -2781,8 +2822,10 @@ Related\ articles=Связанные статьи +Select\ citation\ fetcher=Выберите инструмент выборки цитирований +Routing\ to\ DOI\ fetcher=Перенаправление к инструменту выборки по DOI diff --git a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties index 26636b4e1590..8d95c399454e 100644 --- a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties @@ -2986,10 +2986,8 @@ Next=下一步 # Walkthrough: Configure main file directory # Step 1 -Click\ on\ "File"\ menu=点击"文件"菜单 # Step 2 -Click\ on\ "Preferences"=点击"首选项" # Step 3 Select\ the\ "Linked\ files"\ tab=选择"链接文件"标签页 @@ -3020,42 +3018,35 @@ Double\ click\ on\ the\ "Ding_2006"\ entry=双击"Ding_2006"条目 Let's\ start\ by\ selecting\ an\ entry\ to\ work\ with.\ Double\ click\ on\ the\ entry\ titled\ "Chocolate\ and\ Prevention\ of\ Cardiovascular\ Disease\:\ A\ Systematic\ Review"\ by\ Ding\ et\ al.=让我们从选择一个条目开始操作。双击标题为"巧克力与心血管疾病预防:一项系统综述"的条目,作者为 Ding 等人。 # Step 4 -Click\ on\ the\ "General"\ tab=点击"常规"标签页 -Now\ we\ need\ to\ access\ the\ entry\ editor.\ Click\ on\ the\ "General"\ tab\ to\ view\ and\ edit\ the\ entry\ details.=现在我们需要访问条目编辑器。点击"常规"标签页以查看和编辑条目详细信息。 # Step 5 -PDF\ file\ management\ area=PDF 文件管理区域 -This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=这是 PDF 文件管理区域,您可以在此将文件链接到您的书目条目。请注意右侧的三个按钮——每个都提供了一种添加 PDF 文件的不同方式。让我们逐步探索每种方法。 # Step 6 -Drag\ and\ drop\ files=拖放文件 -You\ can\ drag\ and\ drop\ PDF\ files\ directly\ onto\ the\ file\ list\ area.\ This\ is\ often\ the\ quickest\ way\ to\ link\ files\ that\ are\ already\ on\ your\ computer.=您可以直接将 PDF 文件拖放到文件列表区域。这通常是链接您计算机上已有文件的最快方式。 -Try\ dragging\ a\ PDF\ file\ here\ to\ continue\ the\ walkthrough.=请尝试在此处拖放一个 PDF 文件以继续引导教程。 # Step 7 -Click\ "Add"\ to\ link\ a\ file\ from\ your\ computer=点击"添加"以链接您计算机上的文件 -Click\ the\ "Add"\ button\ (first\ button\ with\ a\ plus\ icon)\ to\ link\ a\ PDF\ file\ that\ you\ already\ have\ on\ your\ computer.\ This\ will\ open\ a\ dialog\ where\ you\ can\ browse\ and\ select\ the\ file.=点击"添加"按钮(第一个带加号图标的按钮)以链接您计算机上已有的 PDF 文件。这将打开一个对话框,您可以在其中浏览并选择文件。 - -# Step 8 Browse\ for\ your\ PDF\ file=浏览您的 PDF 文件 Use\ the\ "Browse"\ button\ to\ select\ a\ PDF\ file\ from\ your\ computer.\ Click\ the\ folder\ icon\ next\ to\ the\ "Link"\ field\ to\ open\ the\ file\ browser.=使用"浏览"按钮从您的计算机中选择一个 PDF 文件。点击"链接"字段旁边的文件夹图标以打开文件浏览器。 -# Step 9 +# Step 8 Add\ a\ description\ for\ the\ file=为文件添加描述 Enter\ a\ meaningful\ description\ for\ this\ file\ in\ the\ "Description"\ field.\ This\ helps\ you\ identify\ the\ file\ later.=在“描述”字段中为此文件输入有意义的描述。这有助于您日后识别该文件。 -# Step 10 +# Step 9 Select\ the\ file\ type=选择文件类型 Choose\ the\ appropriate\ file\ type\ from\ the\ "Filetype"\ dropdown.\ Usually\ "PDF"\ is\ the\ correct\ choice\ for\ research\ papers.=从“文件类型”下拉菜单中选择合适的文件类型。对于研究论文,通常“PDF”是正确的选择。 -# Step 11 +# Step 10 Optionally\ add\ source\ URL=可选添加来源URL If\ you\ downloaded\ this\ file\ from\ a\ website,\ you\ can\ add\ the\ source\ URL\ in\ the\ "Source\ URL"\ field.\ This\ is\ optional\ but\ helpful\ for\ tracking\ where\ you\ found\ the\ file.=如果您是从网站下载的此文件,可以在“来源URL”字段中添加该URL。此步骤是可选的,但有助于追踪您找到该文件的位置。 -# Step 12 +# Step 11 Complete\ the\ file\ linking=完成文件链接 Now\ click\ "Add"\ to\ complete\ the\ file\ linking\ process,\ or\ "Cancel"\ to\ try\ another\ method.=现在点击“添加”以完成文件链接过程,或点击“取消”尝试其他方法。 +# Step 12 +PDF\ file\ management\ area=PDF 文件管理区域 +This\ is\ the\ PDF\ file\ management\ area\ where\ you\ can\ link\ files\ to\ your\ bibliography\ entries.\ Notice\ the\ three\ buttons\ on\ the\ right\ side—each\ offers\ a\ different\ way\ to\ add\ PDF\ files.\ Let's\ explore\ each\ method\ step\ by\ step.=这是 PDF 文件管理区域,您可以在此将文件链接到您的书目条目。请注意右侧的三个按钮——每个都提供了一种添加 PDF 文件的不同方式。让我们逐步探索每种方法。 + # Step 13 Click\ "Get\ fulltext"\ to\ find\ PDFs\ automatically=点击“获取全文”以自动查找PDF Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ to\ let\ JabRef\ automatically\ search\ for\ and\ download\ the\ PDF\ using\ online\ fetchers.\ This\ works\ when\ your\ entry\ has\ proper\ metadata\ like\ DOI\ or\ title.=点击“获取全文”按钮(第二个带有下载图标的按钮),让JabRef使用在线抓取器自动搜索并下载PDF。当您的条目具有正确的元数据(如DOI或标题)时,此功能有效。 From 0e4e21211c6d2dda09be9772d8d44a64e38e1292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:57:20 +0000 Subject: [PATCH 012/102] Chore(deps): Bump jablib/src/main/resources/csl-styles (#17186) Bumps [jablib/src/main/resources/csl-styles](https://github.com/citation-style-language/styles) from `5ad4733` to `90e657c`. - [Release notes](https://github.com/citation-style-language/styles/releases) - [Commits](https://github.com/citation-style-language/styles/compare/5ad473309db057f70865f5bf3df4a8bb50617bb9...90e657c8622ec1c6f27788e041b355efac659397) --- updated-dependencies: - dependency-name: jablib/src/main/resources/csl-styles dependency-version: 90e657c8622ec1c6f27788e041b355efac659397 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- jablib/src/main/resources/csl-styles | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/resources/csl-styles b/jablib/src/main/resources/csl-styles index 5ad473309db0..90e657c8622e 160000 --- a/jablib/src/main/resources/csl-styles +++ b/jablib/src/main/resources/csl-styles @@ -1 +1 @@ -Subproject commit 5ad473309db057f70865f5bf3df4a8bb50617bb9 +Subproject commit 90e657c8622ec1c6f27788e041b355efac659397 From 8057af8f9d56555bbc337cef8139d70b95f5bfaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:57:42 +0000 Subject: [PATCH 013/102] Chore(deps): Bump jablib/src/main/resources/csl-locales (#17187) Bumps [jablib/src/main/resources/csl-locales](https://github.com/citation-style-language/locales) from `9ded661` to `a89adec`. - [Release notes](https://github.com/citation-style-language/locales/releases) - [Commits](https://github.com/citation-style-language/locales/compare/9ded661fc7bb995a5453a2025ca826ff404a4fbd...a89adece41013402236e2c9020972d7e931fbab8) --- updated-dependencies: - dependency-name: jablib/src/main/resources/csl-locales dependency-version: a89adece41013402236e2c9020972d7e931fbab8 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- jablib/src/main/resources/csl-locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/resources/csl-locales b/jablib/src/main/resources/csl-locales index 9ded661fc7bb..a89adece4101 160000 --- a/jablib/src/main/resources/csl-locales +++ b/jablib/src/main/resources/csl-locales @@ -1 +1 @@ -Subproject commit 9ded661fc7bb995a5453a2025ca826ff404a4fbd +Subproject commit a89adece41013402236e2c9020972d7e931fbab8 From 9019aa1a8930388bd211be4404eac34fceb697b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:57:42 +0000 Subject: [PATCH 014/102] Chore(deps): Bump docker/build-push-action from 7.3.0 to 7.4.0 (#17196) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.3.0 to 7.4.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dockerimages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dockerimages.yml b/.github/workflows/dockerimages.yml index 73f8c972b1ab..fcb2c395c7a0 100644 --- a/.github/workflows/dockerimages.yml +++ b/.github/workflows/dockerimages.yml @@ -71,7 +71,7 @@ jobs: id: gitversion uses: gittools/actions/gitversion/execute@7417b1089e2c7de93510f1901d656ddf60bb024f # v4.7.0 - name: Build and push - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: push: > ${{ From 4b9aff91117742dee199e48d2ade796a8e69f466 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:58:43 +0000 Subject: [PATCH 015/102] Chore(deps): Bump anthropics/claude-code-action from 1.0.219 to 1.0.226 (#17190) Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.219 to 1.0.226. - [Release notes](https://github.com/anthropics/claude-code-action/releases) - [Commits](https://github.com/anthropics/claude-code-action/compare/5ccc3a35a6367cdb8e6fbd0728287467540ecfe2...7b0b255830a1fab6e602658672acad11c12d841d) --- updated-dependencies: - dependency-name: anthropics/claude-code-action dependency-version: 1.0.226 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 1abbb5659773..fb378ee26dcd 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,6 +39,6 @@ jobs: - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@5ccc3a35a6367cdb8e6fbd0728287467540ecfe2 # v1 + uses: anthropics/claude-code-action@7b0b255830a1fab6e602658672acad11c12d841d # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} From cf882611d4acc3ec02b9b296e3ac282543ba4f02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:59:46 +0000 Subject: [PATCH 016/102] Chore(deps): Bump codecov/codecov-action from 7.0.0 to 7.1.0 (#17195) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/fb8b3582c8e4def4969c97caa2f19720cb33a72f...0b35c9ecc4f0529d0eb674914510c22f85b196b4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests-code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-code.yml b/.github/workflows/tests-code.yml index f8caf2f614e4..eff6f42dd1c5 100644 --- a/.github/workflows/tests-code.yml +++ b/.github/workflows/tests-code.yml @@ -689,7 +689,7 @@ jobs: CI: "true" CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} DBMS: "postgresql" - - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + - uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7 if: (github.ref == 'refs/heads/main') && (steps.checksecrets.outputs.secretspresent == 'YES') with: token: ${{ secrets.CODECOV_TOKEN }} From cad638fb8749a5c3bdb307cfbb5d4db4e397ccb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:01:44 +0000 Subject: [PATCH 017/102] Chore(deps): Bump actions/setup-java in /.github/actions/setup-gradle (#17192) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/dd06d9cba3e5552c54d9f8ea23572deb30010f7c...de7274f081f381c8f8158605e0321c36c376e2e6) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/setup-gradle/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-gradle/action.yml b/.github/actions/setup-gradle/action.yml index 27bd05739b38..f12a6537d4d2 100644 --- a/.github/actions/setup-gradle/action.yml +++ b/.github/actions/setup-gradle/action.yml @@ -15,7 +15,7 @@ runs: using: composite steps: - name: Setup JDK - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: java-version: ${{ inputs.javaVersion }} distribution: ${{ inputs.distribution }} From 6f2828092a2307922649cccf78b7d5610c00145e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:01:48 +0000 Subject: [PATCH 018/102] Chore(deps): Bump com.autonomousapps:dependency-analysis-gradle-plugin (#17188) Bumps [com.autonomousapps:dependency-analysis-gradle-plugin](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin) from 3.19.1 to 3.19.2. - [Release notes](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/releases) - [Changelog](https://github.com/autonomousapps/dependency-analysis-gradle-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/compare/v3.19.1...v3.19.2) --- updated-dependencies: - dependency-name: com.autonomousapps:dependency-analysis-gradle-plugin dependency-version: 3.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build-logic/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index bd453eb2cd0e..d6d35888b2e0 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -17,7 +17,7 @@ repositories { dependencies { implementation("com.adarshr:gradle-test-logger-plugin:4.0.0") - implementation("com.autonomousapps:dependency-analysis-gradle-plugin:3.19.1") + implementation("com.autonomousapps:dependency-analysis-gradle-plugin:3.19.2") implementation("com.github.andygoossens:gradle-modernizer-plugin:1.15.0") implementation("com.gradleup.shadow:shadow-gradle-plugin:9.6.1") implementation("de.undercouch.download:de.undercouch.download.gradle.plugin:5.7.0") From b51cf6100b345344a4c7c2eee688047e99b014e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:07:40 +0000 Subject: [PATCH 019/102] Chore(deps): Bump com.autonomousapps:dependency-analysis-gradle-plugin (#17194) Bumps [com.autonomousapps:dependency-analysis-gradle-plugin](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin) from 3.19.1 to 3.19.2. - [Release notes](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/releases) - [Changelog](https://github.com/autonomousapps/dependency-analysis-gradle-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/compare/v3.19.1...v3.19.2) --- updated-dependencies: - dependency-name: com.autonomousapps:dependency-analysis-gradle-plugin dependency-version: 3.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 3b6ccf517cdf648780003a25269047b201667a6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:07:44 +0000 Subject: [PATCH 020/102] Chore(deps-dev): Bump org.apache.maven.plugins:maven-deploy-plugin (#17189) Bumps [org.apache.maven.plugins:maven-deploy-plugin](https://github.com/apache/maven-deploy-plugin) from 3.1.4 to 3.2.0. - [Release notes](https://github.com/apache/maven-deploy-plugin/releases) - [Commits](https://github.com/apache/maven-deploy-plugin/compare/maven-deploy-plugin-3.1.4...maven-deploy-plugin-3.2.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-deploy-plugin dependency-version: 3.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- jablib-examples/maven3/doi-to-bibtex/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib-examples/maven3/doi-to-bibtex/pom.xml b/jablib-examples/maven3/doi-to-bibtex/pom.xml index 4e39b47db103..c3e1ef9d9125 100644 --- a/jablib-examples/maven3/doi-to-bibtex/pom.xml +++ b/jablib-examples/maven3/doi-to-bibtex/pom.xml @@ -91,7 +91,7 @@ maven-deploy-plugin - 3.1.4 + 3.2.0 maven-project-info-reports-plugin From 7ba1b8a882c4e7f5081035b0609f7fd7b1e91e8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:08:38 +0000 Subject: [PATCH 021/102] Chore(deps): Update thollander/actions-comment-pull-request requirement to e4a76dd2b0a3c2027c3fd84147a67c22ee4c90fa (#17191) Updates the requirements on [thollander/actions-comment-pull-request](https://github.com/thollander/actions-comment-pull-request) to permit the latest version. - [Release notes](https://github.com/thollander/actions-comment-pull-request/releases) - [Commits](https://github.com/thollander/actions-comment-pull-request/commits/e4a76dd2b0a3c2027c3fd84147a67c22ee4c90fa) --- updated-dependencies: - dependency-name: thollander/actions-comment-pull-request dependency-version: e4a76dd2b0a3c2027c3fd84147a67c22ee4c90fa dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/on-issue-labeled.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on-issue-labeled.yml b/.github/workflows/on-issue-labeled.yml index a68599a66fed..55aa20566852 100644 --- a/.github/workflows/on-issue-labeled.yml +++ b/.github/workflows/on-issue-labeled.yml @@ -162,7 +162,7 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" - name: Comment on issue - uses: thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74 # v3 + uses: thollander/actions-comment-pull-request@e4a76dd2b0a3c2027c3fd84147a67c22ee4c90fa # v3 with: message: ${{ steps.wisdom.outputs.message }} comment-tag: ${{ env.LABEL }} From 54480a687cc0cbc7cf0c6d6ec7e8f9ece0e3a8ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:12:18 +0000 Subject: [PATCH 022/102] Chore(deps): Bump docker/setup-buildx-action from 4.3.0 to 4.4.0 (#17193) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...594f3bf4285d9ea8dc53c9a0c9c4092420091003) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dockerimages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dockerimages.yml b/.github/workflows/dockerimages.yml index fcb2c395c7a0..4c421d70a010 100644 --- a/.github/workflows/dockerimages.yml +++ b/.github/workflows/dockerimages.yml @@ -56,7 +56,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4 - name: Fetch all history for all tags and branches uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: From b6ba5a0b1e4977894c9fb3d5334013b8424cfcd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:15:59 +0000 Subject: [PATCH 023/102] Chore(deps): Bump net.bytebuddy:byte-buddy in /versions (#17197) Bumps [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) from 1.18.13 to 1.18.14. - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.13...byte-buddy-1.18.14) --- updated-dependencies: - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index e82be8d4538c..d4bf6af13f0d 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -100,7 +100,7 @@ dependencies.constraints { api("jakarta.inject:jakarta.inject-api:2.0.1") api("jakarta.validation:jakarta.validation-api:3.1.1") api("jakarta.ws.rs:jakarta.ws.rs-api:4.0.0") - api("net.bytebuddy:byte-buddy:1.18.13") + api("net.bytebuddy:byte-buddy:1.18.14") api("net.harawata:appdirs:1.5.0") api("net.java.dev.jna:jna-jpms:5.19.1") api("net.java.dev.jna:jna-platform:5.19.1") From 0d7fee518e31bff8f643d4b24cc35baf8a70567a Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 17 Sep 2026 06:48:56 +0000 Subject: [PATCH 024/102] Fix BibTeX source color on macOS Tahoe (#17198) * fix(theme): keep BibTeX keywords opaque * build(jpackage): include java.instrument * revert runtime adding --- .../resources/org/jabref/gui/theme/internal/jabref-base.css | 2 +- jabgui/src/main/themes.jabref.org | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jabgui/src/main/resources/org/jabref/gui/theme/internal/jabref-base.css b/jabgui/src/main/resources/org/jabref/gui/theme/internal/jabref-base.css index 40649e862dfe..e92ac83797a5 100644 --- a/jabgui/src/main/resources/org/jabref/gui/theme/internal/jabref-base.css +++ b/jabgui/src/main/resources/org/jabref/gui/theme/internal/jabref-base.css @@ -1731,7 +1731,7 @@ We want to have a look that matches our icons in the tool-bar */ } .bibtex-code-area .bibtex-keyword { - -fx-fill: -color-info !important; + -fx-fill: -color-bibtex-keyword !important; } .bibtex-code-area .bibtex-field { diff --git a/jabgui/src/main/themes.jabref.org b/jabgui/src/main/themes.jabref.org index 5d7f4344c7c5..82ae85e86b8a 160000 --- a/jabgui/src/main/themes.jabref.org +++ b/jabgui/src/main/themes.jabref.org @@ -1 +1 @@ -Subproject commit 5d7f4344c7c5c81195ed3574b641fb06e182ab4e +Subproject commit 82ae85e86b8a348bd935ae13f72a12f160f12fdd From b5899dd8196b0229d200a901c4129f1be52f01fa Mon Sep 17 00:00:00 2001 From: Faneesh Juneja Date: Thu, 17 Sep 2026 09:01:37 +0000 Subject: [PATCH 025/102] SLR: Add native query field for catalog specific searches (#17199) * Release v6.0-beta.1 * add native query to catalogs tab * fix formatting * changelog * javadoc cleanup * fix formatting * formatting again * add tests and address qodo's review * apply OpenRewrite fixes * fix alphabetical ordering * address review * Apply suggestion from @subhramit --------- Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Co-authored-by: Subhramit Basu --- CHANGELOG.md | 1 + .../gui/slr/ManageStudyDefinitionView.java | 34 ++++-- .../slr/ManageStudyDefinitionViewModel.java | 37 ++++++- .../org/jabref/gui/slr/StudyCatalogItem.java | 24 +++- .../jabref/gui/slr/ManageStudyDefinition.fxml | 3 + .../ManageStudyDefinitionViewModelTest.java | 103 ++++++++++++++++++ .../main/resources/l10n/JabRef_en.properties | 2 + 7 files changed, 192 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 469effa5bd93..4c34c8c71b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added a native query field for catalogs on the SLR dialog. [#17199](https://github.com/JabRef/jabref/pull/17199) - We made almost everything in JabRef navigatable by keyboard, including adding visiual indicators. [#17059](https://github.com/JabRef/jabref/pull/17059) - We added a fetcher for Software Heritage identifiers (SWHID). [#16809](https://github.com/JabRef/jabref/issues/16809) - We added the community themes from [themes.jabref.org](https://themes.jabref.org/) (Everforest, Nord, Papers, Chocolate Honey, and Dino Girl's collection) as selectable themes. [#15915](https://github.com/JabRef/jabref/issues/15915) diff --git a/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java b/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java index 722ec7ad6b61..70d7848bd1c7 100644 --- a/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java +++ b/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java @@ -11,6 +11,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; @@ -25,6 +26,7 @@ import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; +import javafx.util.Callback; import org.jabref.gui.DialogService; import org.jabref.gui.actions.ActionHelper; @@ -74,6 +76,7 @@ public class ManageStudyDefinitionView extends BaseDialog @FXML private TableView catalogTable; @FXML private TableColumn catalogEnabledColumn; @FXML private TableColumn catalogColumn; + @FXML private TableColumn catalogNativeQueryColumn; @FXML private TableColumn catalogReasonColumn; @FXML private Label directoryWarning; @@ -264,7 +267,26 @@ private void initCatalogsTab() { catalogReasonColumn.setReorderable(false); catalogReasonColumn.setCellValueFactory(param -> param.getValue().reasonProperty()); - catalogReasonColumn.setCellFactory(_ -> { + catalogReasonColumn.setCellFactory(createEditableTextFieldCellFactory(StudyCatalogItem::reasonProperty)); + Label catalogReasonHeader = new Label(catalogReasonColumn.getText()); + catalogReasonHeader.setTooltip(new Tooltip(Localization.lang("Click a cell to edit the reason"))); + catalogReasonColumn.setGraphic(catalogReasonHeader); + catalogReasonColumn.setText(""); + + catalogNativeQueryColumn.setReorderable(false); + catalogNativeQueryColumn.setCellValueFactory(param -> param.getValue().nativeQueryProperty()); + catalogNativeQueryColumn.setCellFactory(createEditableTextFieldCellFactory(StudyCatalogItem::nativeQueryProperty)); + Label catalogNativeQueryHeader = new Label(catalogNativeQueryColumn.getText()); + catalogNativeQueryHeader.setTooltip(new Tooltip(Localization.lang("Click a cell to edit the native query"))); + catalogNativeQueryColumn.setGraphic(catalogNativeQueryHeader); + catalogNativeQueryColumn.setText(""); + + catalogTable.setItems(viewModel.getCatalogs()); + } + + private Callback, TableCell> createEditableTextFieldCellFactory( + Function propertyExtractor) { + return _ -> { TextField textField = new TextField(); TableCell cell = new TableCell<>() { @Override @@ -283,19 +305,13 @@ protected void updateItem(String item, boolean empty) { TableView tableView = cell.getTableView(); int index = cell.getIndex(); if (tableView != null && index >= 0 && index < tableView.getItems().size()) { - tableView.getItems().get(index).setReason(textField.getText()); + propertyExtractor.apply(tableView.getItems().get(index)).setValue(textField.getText()); } } }); textField.setOnAction(_ -> cell.getTableView().requestFocus()); return cell; - }); - Label catalogReasonHeader = new Label(catalogReasonColumn.getText()); - catalogReasonHeader.setTooltip(new Tooltip(Localization.lang("Click a cell to edit the reason"))); - catalogReasonColumn.setGraphic(catalogReasonHeader); - catalogReasonColumn.setText(""); - - catalogTable.setItems(viewModel.getCatalogs()); + }; } private void initValidationBindings() { diff --git a/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java b/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java index f76065ba8375..7cf32f5f2396 100644 --- a/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -34,6 +35,7 @@ import org.jabref.logic.importer.fetcher.IEEE; import org.jabref.logic.importer.fetcher.SpringerNatureWebFetcher; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.strings.StringUtil; import org.jabref.model.study.Study; import org.jabref.model.study.StudyCatalog; import org.jabref.model.study.StudyQuery; @@ -132,7 +134,8 @@ public ManageStudyDefinitionViewModel(@NonNull Study study, StudyCatalog savedCatalog = catalogsByName.get(name); boolean enabled = savedCatalog != null && savedCatalog.isEnabled(); String reason = savedCatalog != null ? savedCatalog.getReason() : ""; - return new StudyCatalogItem(name, enabled, reason); + String nativeQuery = findNativeQueryForCatalog(name); + return new StudyCatalogItem(name, enabled, reason, nativeQuery); }) .toList()); @@ -143,6 +146,16 @@ public ManageStudyDefinitionViewModel(@NonNull Study study, initializeValidationBindings(); } + private String findNativeQueryForCatalog(String catalogName) { + return queries.stream() + .flatMap(query -> query.getCatalogSpecific().entrySet().stream()) + .filter(entry -> entry.getKey().equalsIgnoreCase(catalogName)) + .map(Map.Entry::getValue) + .filter(StringUtil::isNotBlank) + .findFirst() + .orElse(""); + } + private void initializeValidationBindings() { titleValidationMessage.bind(Bindings.when(title.isEmpty()) .then(Localization.lang("Study title is required")) @@ -264,6 +277,7 @@ public SlrStudyAndDirectory saveStudy() { /// Builds a [Study] from the current UI state without persisting it. public Study buildStudy() { + applyNativeQueryOverrides(); return new Study( authors, title.getValueSafe(), @@ -275,6 +289,27 @@ public Study buildStudy() { .toList()); } + private void applyNativeQueryOverrides() { + List enabledCatalogs = catalogs.stream() + .filter(StudyCatalogItem::isEnabled) + .toList(); + for (StudyQuery query : queries) { + Map original = query.getCatalogSpecific(); + Map updated = new LinkedHashMap<>(original); + for (StudyCatalogItem catalog : enabledCatalogs) { + String name = catalog.getName(); + updated.keySet().removeIf(key -> key.equalsIgnoreCase(name)); + String nativeQuery = catalog.getNativeQuery(); + if (StringUtil.isNotBlank(nativeQuery)) { + updated.put(name, nativeQuery); + } + } + if (!updated.equals(original)) { + query.setCatalogSpecific(updated); + } + } + } + public Property titleProperty() { return title; } diff --git a/jabgui/src/main/java/org/jabref/gui/slr/StudyCatalogItem.java b/jabgui/src/main/java/org/jabref/gui/slr/StudyCatalogItem.java index fd2f702e12c7..386dd78f9b53 100644 --- a/jabgui/src/main/java/org/jabref/gui/slr/StudyCatalogItem.java +++ b/jabgui/src/main/java/org/jabref/gui/slr/StudyCatalogItem.java @@ -15,15 +15,21 @@ public class StudyCatalogItem { private final StringProperty name; private final BooleanProperty enabled; private final StringProperty reason; + private final StringProperty nativeQuery; public StudyCatalogItem(String name, boolean enabled) { this(name, enabled, ""); } public StudyCatalogItem(String name, boolean enabled, String reason) { + this(name, enabled, reason, ""); + } + + public StudyCatalogItem(String name, boolean enabled, String reason, String nativeQuery) { this.name = new SimpleStringProperty(name); this.enabled = new SimpleBooleanProperty(enabled); this.reason = new SimpleStringProperty(reason); + this.nativeQuery = new SimpleStringProperty(nativeQuery); } public String getName() { @@ -62,12 +68,25 @@ public StringProperty reasonProperty() { return reason; } + public String getNativeQuery() { + return nativeQuery.getValue(); + } + + public void setNativeQuery(String nativeQuery) { + this.nativeQuery.setValue(nativeQuery); + } + + public StringProperty nativeQueryProperty() { + return nativeQuery; + } + @Override public String toString() { return "StudyCatalogItem{" + "name=" + name.get() + ", enabled=" + enabled.get() + ", reason=" + reason.get() + + ", nativeQuery=" + nativeQuery.get() + '}'; } @@ -82,11 +101,12 @@ public boolean equals(Object o) { StudyCatalogItem that = (StudyCatalogItem) o; return Objects.equals(getName(), that.getName()) && Objects.equals(isEnabled(), that.isEnabled()) && - Objects.equals(getReason(), that.getReason()); + Objects.equals(getReason(), that.getReason()) && + Objects.equals(getNativeQuery(), that.getNativeQuery()); } @Override public int hashCode() { - return Objects.hash(getName(), isEnabled(), getReason()); + return Objects.hash(getName(), isEnabled(), getReason(), getNativeQuery()); } } diff --git a/jabgui/src/main/resources/org/jabref/gui/slr/ManageStudyDefinition.fxml b/jabgui/src/main/resources/org/jabref/gui/slr/ManageStudyDefinition.fxml index 4e7b75a43806..04d7f50615af 100644 --- a/jabgui/src/main/resources/org/jabref/gui/slr/ManageStudyDefinition.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/slr/ManageStudyDefinition.fxml @@ -235,6 +235,9 @@ + catalogs = List.of(new StudyCatalog("ACM Portal", true)); + Study study = new Study(List.of("Name"), "title", List.of("RQ1"), List.of(query1, query2), catalogs); + ManageStudyDefinitionViewModel viewModel = new ManageStudyDefinitionViewModel( + study, tempDir, importFormatPreferences, importerPreferences, workspacePreferences, gitPreferences, dialogService); + + viewModel.getCatalogs().stream() + .filter(item -> "ACM Portal".equals(item.getName())) + .findFirst() + .orElseThrow() + .setNativeQuery("ti:Test"); + + Study builtStudy = viewModel.buildStudy(); + for (StudyQuery query : builtStudy.getQueries()) { + assertEquals(Map.of("ACM Portal", "ti:Test"), query.getCatalogSpecific()); + } + } + + @Test + void blankNativeQueryRemovesCatalogSpecificKeyInsteadOfWritingEmptyString(@TempDir Path tempDir) { + StudyQuery query = new StudyQuery("Q1"); + query.getCatalogSpecific().put("ACM Portal", "ti:Existing"); + List catalogs = List.of(new StudyCatalog("ACM Portal", true)); + Study study = new Study(List.of("Name"), "title", List.of("RQ1"), List.of(query), catalogs); + ManageStudyDefinitionViewModel viewModel = new ManageStudyDefinitionViewModel( + study, tempDir, importFormatPreferences, importerPreferences, workspacePreferences, gitPreferences, dialogService); + + viewModel.getCatalogs().stream() + .filter(item -> "ACM Portal".equals(item.getName())) + .findFirst() + .orElseThrow() + .setNativeQuery(""); + + Study builtStudy = viewModel.buildStudy(); + assertEquals(Map.of(), builtStudy.getQueries().getFirst().getCatalogSpecific()); + } + + @Test + void disabledCatalogNativeQueryIsNotApplied(@TempDir Path tempDir) { + StudyQuery query = new StudyQuery("Q1"); + Study study = new Study(List.of("Name"), "title", List.of("RQ1"), List.of(query), List.of()); + ManageStudyDefinitionViewModel viewModel = new ManageStudyDefinitionViewModel( + study, tempDir, importFormatPreferences, importerPreferences, workspacePreferences, gitPreferences, dialogService); + + // "arXiv" is not part of the study's catalogs, so it stays disabled by default + viewModel.getCatalogs().stream() + .filter(item -> "arXiv".equals(item.getName())) + .findFirst() + .orElseThrow() + .setNativeQuery("ti:Test"); + + Study builtStudy = viewModel.buildStudy(); + assertEquals(Map.of(), builtStudy.getQueries().getFirst().getCatalogSpecific()); + } + + @Test + void applyNativeQueryOverridesMatchesCatalogNameCaseInsensitively(@TempDir Path tempDir) { + StudyQuery query = new StudyQuery("Q1"); + query.getCatalogSpecific().put("acm portal", "ti:Old"); + List catalogs = List.of(new StudyCatalog("ACM Portal", true)); + Study study = new Study(List.of("Name"), "title", List.of("RQ1"), List.of(query), catalogs); + ManageStudyDefinitionViewModel viewModel = new ManageStudyDefinitionViewModel( + study, tempDir, importFormatPreferences, importerPreferences, workspacePreferences, gitPreferences, dialogService); + + viewModel.getCatalogs().stream() + .filter(item -> "ACM Portal".equals(item.getName())) + .findFirst() + .orElseThrow() + .setNativeQuery("ti:New"); + + Study builtStudy = viewModel.buildStudy(); + // The differently-cased key must be replaced, not kept alongside the new one + assertEquals(Map.of("ACM Portal", "ti:New"), builtStudy.getQueries().getFirst().getCatalogSpecific()); + } + + @Test + void existingStudyLoadPicksFirstNonBlankCatalogSpecificOverrideInQueryOrder(@TempDir Path tempDir) { + StudyQuery query1 = new StudyQuery("Q1"); + query1.getCatalogSpecific().put("ACM Portal", " "); + StudyQuery query2 = new StudyQuery("Q2"); + query2.getCatalogSpecific().put("ACM Portal", "ti:First"); + StudyQuery query3 = new StudyQuery("Q3"); + query3.getCatalogSpecific().put("ACM Portal", "ti:Second"); + List catalogs = List.of(new StudyCatalog("ACM Portal", true)); + Study study = new Study(List.of("Name"), "title", List.of("RQ1"), List.of(query1, query2, query3), catalogs); + + ManageStudyDefinitionViewModel viewModel = new ManageStudyDefinitionViewModel( + study, tempDir, importFormatPreferences, importerPreferences, workspacePreferences, gitPreferences, dialogService); + + String nativeQuery = viewModel.getCatalogs().stream() + .filter(item -> "ACM Portal".equals(item.getName())) + .findFirst() + .map(StudyCatalogItem::getNativeQuery) + .orElse(""); + + assertEquals("ti:First", nativeQuery); + } + private ManageStudyDefinitionViewModel getManageStudyDefinitionViewModel(Path tempDir) { List catalogs = List.of( new StudyCatalog("ACM Portal", true)); diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 3a765f49dfbb..dd38548fac4e 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -3692,6 +3692,8 @@ The\ default\ entry\ type\ will\ be\ used\ since\ the\ invalid\ key\ was\ passed Catalog\ Reason=Catalog Reason Click\ a\ cell\ to\ edit\ the\ reason=Click a cell to edit the reason +Native\ query=Native query +Click\ a\ cell\ to\ edit\ the\ native\ query=Click a cell to edit the native query Specify\ a\ subcommand\ (consistency,\ integrity)\ or\ an\ input\ file.=Specify a subcommand (consistency, integrity) or an input file. Specify\ a\ subcommand\ (reset,\ import,\ export).=Specify a subcommand (reset, import, export). From eafab1279f0fde8cc3462dff3972870718562031 Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 18 Sep 2026 13:41:04 +0000 Subject: [PATCH 026/102] New Crowdin updates (#17206) * New translations jabref_en.properties for (French) * New translations jabref_en.properties for (Italian) --- jablib/src/main/resources/l10n/JabRef_fr.properties | 2 ++ jablib/src/main/resources/l10n/JabRef_it.properties | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jablib/src/main/resources/l10n/JabRef_fr.properties b/jablib/src/main/resources/l10n/JabRef_fr.properties index 64edbdb73e5e..eec052082902 100644 --- a/jablib/src/main/resources/l10n/JabRef_fr.properties +++ b/jablib/src/main/resources/l10n/JabRef_fr.properties @@ -3692,6 +3692,8 @@ The\ default\ entry\ type\ will\ be\ used\ since\ the\ invalid\ key\ was\ passed Catalog\ Reason=Motif du catalogue Click\ a\ cell\ to\ edit\ the\ reason=Cliquez sur une cellule pour modifier le motif +Native\ query=Requête native +Click\ a\ cell\ to\ edit\ the\ native\ query=Cliquez sur une cellule pour modifier la requête native Specify\ a\ subcommand\ (consistency,\ integrity)\ or\ an\ input\ file.=Spécifiez une sous-commande (cohérence, intégrité) ou un fichier d'entrée. Specify\ a\ subcommand\ (reset,\ import,\ export).=Spécifiez une sous-commande (réinitialiser, importer, exporter). diff --git a/jablib/src/main/resources/l10n/JabRef_it.properties b/jablib/src/main/resources/l10n/JabRef_it.properties index 92df8e71434e..6602db64fe53 100644 --- a/jablib/src/main/resources/l10n/JabRef_it.properties +++ b/jablib/src/main/resources/l10n/JabRef_it.properties @@ -3621,6 +3621,8 @@ The\ default\ entry\ type\ will\ be\ used\ since\ the\ invalid\ key\ was\ passed Catalog\ Reason=Motivo Del Catalogo Click\ a\ cell\ to\ edit\ the\ reason=Fare clic su una cella per modificare il motivo +Native\ query=Query nativa +Click\ a\ cell\ to\ edit\ the\ native\ query=Fare clic su una cella per modificare la query nativa Specify\ a\ subcommand\ (consistency,\ integrity)\ or\ an\ input\ file.=Specifica un sottocomando (consistenza, integrità) o un file di input. Specify\ a\ subcommand\ (reset,\ import,\ export).=Specifica un sottocomando (reset, importazione, esportazione). From 591d2bdd6b83b4ae0eec3eb55769757ca1aeca3d Mon Sep 17 00:00:00 2001 From: Subhramit Basu Date: Fri, 18 Sep 2026 17:01:05 +0000 Subject: [PATCH 027/102] =?UTF-8?q?=F0=9F=9A=A7=20Fix=20columns=20sync=20o?= =?UTF-8?q?n=20maintable=20(#17204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix columns sync on maintable Signed-off-by: subhramit * Add changelog entry Signed-off-by: subhramit * Address qodo Signed-off-by: subhramit * Add javadoc Signed-off-by: subhramit * Add more javadoc Signed-off-by: subhramit * More javadoc Signed-off-by: subhramit * Merge pull request #29 from JabRef/alternate-fix-columns Alternate fix columns * Format Signed-off-by: subhramit * Add more tests for two open maintables Signed-off-by: subhramit --------- Signed-off-by: subhramit --- CHANGELOG.md | 1 + .../main/java/org/jabref/gui/LibraryTab.java | 4 + .../gui/maintable/ColumnPreferences.java | 11 +- .../maintable/ColumnPreferencesApplier.java | 125 ++++++++++ ...le.java => ColumnPreferencesRecorder.java} | 55 +++-- .../org/jabref/gui/maintable/MainTable.java | 51 ++-- .../gui/maintable/MainTableColumnFactory.java | 7 +- .../gui/maintable/MainTableColumnModel.java | 22 ++ .../maintable/MainTableHeaderContextMenu.java | 1 + .../preferences/table/TableTabViewModel.java | 9 +- .../jabref/gui/search/SearchResultsTable.java | 4 +- .../ColumnPreferencesApplierTest.java | 233 ++++++++++++++++++ .../ColumnPreferencesRecorderTest.java | 91 +++++++ .../table/TableTabViewModelTest.java | 58 +++++ 14 files changed, 602 insertions(+), 70 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesApplier.java rename jabgui/src/main/java/org/jabref/gui/maintable/{PersistenceVisualStateTable.java => ColumnPreferencesRecorder.java} (51%) create mode 100644 jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesApplierTest.java create mode 100644 jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesRecorderTest.java create mode 100644 jabgui/src/test/java/org/jabref/gui/preferences/table/TableTabViewModelTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c34c8c71b51..c8ba88c02fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -161,6 +161,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the font size was wrong after a walkthrough had been running. [#17108](https://github.com/JabRef/jabref/pull/17108) - We fixed preferences walkthroughs on macOS and made the Groups and PDF-link walkthroughs run through reliably. [#17140](https://github.com/JabRef/jabref/issues/17140) - We fixed an issue where main table columns could not be resized while "Fit table horizontally on screen" was enabled. Resizing a column now adjusts only the columns to its right, and column widths keep their proportions when the window is resized. [#10516](https://github.com/JabRef/jabref/issues/10516) +- We fixed an issue where entry table column changes would not take effect before restarting JabRef. [#17204](https://github.com/JabRef/jabref/pull/17204) - We fixed an issue where the sidebar width jumped after repeatedly showing and hiding a side panel. [#17100](https://github.com/JabRef/jabref/issues/17100) - We fixed an issue where entries imported in the background could not be selected or updated in the main table. [#16893](https://github.com/JabRef/jabref/pull/16893) - We fixed an issue where editing a library's string constants could not be undone. [#16936](https://github.com/JabRef/jabref/pull/16936) diff --git a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java index b62c098fa69f..10901698ec59 100644 --- a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java +++ b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java @@ -955,6 +955,10 @@ private void onClosed(Event event) { LOGGER.error("Problem when shutting down Git pull scheduler", e); } + if (mainTable != null) { + mainTable.dispose(); + } + if (tableModel != null) { tableModel.unbind(); } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java b/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java index 87ec1d85ce2d..41975e234d80 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java @@ -2,6 +2,7 @@ import java.util.List; +import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -15,7 +16,9 @@ public class ColumnPreferences { public ColumnPreferences(List columns, List columnSortOrder) { - this.columns = FXCollections.observableArrayList(columns); + // Report width and sort type changes, so that they are persisted without per-column listeners + this.columns = FXCollections.observableArrayList(column -> new Observable[] {column.widthProperty(), column.sortTypeProperty()}); + this.columns.addAll(columns); this.columnSortOrder = FXCollections.observableArrayList(columnSortOrder); } @@ -54,12 +57,10 @@ public ObservableList getColumnSortOrder() { } public void setColumns(List list) { - columns.clear(); - columns.addAll(list); + columns.setAll(list); } public void setColumnSortOrder(List list) { - columnSortOrder.clear(); - columnSortOrder.addAll(list); + columnSortOrder.setAll(list); } } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesApplier.java b/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesApplier.java new file mode 100644 index 000000000000..99cd35583004 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesApplier.java @@ -0,0 +1,125 @@ +package org.jabref.gui.maintable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; + +import org.jabref.gui.maintable.columns.MainTableColumn; + +import org.jspecify.annotations.NullMarked; + +/// Applies the column preferences (columns, sort order, resize policy) to the main table: initially and whenever +/// they change, e.g., in the preferences dialog. +/// +/// This is the preferences-to-table direction. The opposite direction is [ColumnPreferencesRecorder]. +/// Both directions compare columns by [ColumnPreferencesRecorder#toPersistedModels(List)]. Each change is +/// applied with a single `setAll`, so that the write-back of the table equals the preferences and ends the round trip. +/// +/// All main tables share the same preferences. Hence, reordering, adding, removing or sorting columns in one library +/// tab is applied to the main tables of all other open library tabs as well. This is intended: the column preferences +/// are global, so all tables show the state that will be restored at the next start. +/// +/// New columns are created from the model instances held by the preferences. Resized widths are persisted only +/// because table and preferences share these instances (see [ColumnPreferences]). +@NullMarked +class ColumnPreferencesApplier { + + private final TableView table; + private final MainTableColumnFactory columnFactory; + private final MainTablePreferences mainTablePreferences; + + private final ListChangeListener columnsListener = _ -> applyColumns(); + private final ListChangeListener sortOrderListener = _ -> applySortOrder(); + private final ChangeListener resizeColumnsListener = (_, _, resizeColumnsToFit) -> applyResizePolicy(resizeColumnsToFit); + + ColumnPreferencesApplier(TableView table, + MainTableColumnFactory columnFactory, + MainTablePreferences mainTablePreferences) { + this.table = table; + this.columnFactory = columnFactory; + this.mainTablePreferences = mainTablePreferences; + } + + /// Creates the columns, applies the resize policy and follows later changes of the preferences. + /// + /// The initial sort order is not applied here, because sorting a large library is expensive. The caller decides + /// when to call [#applySortOrder()]. + void bind() { + table.getColumns().setAll(columnFactory.createColumns()); + applyResizePolicy(mainTablePreferences.getResizeColumnsToFit()); + + mainTablePreferences.getColumnPreferences().getColumns().addListener(columnsListener); + mainTablePreferences.getColumnPreferences().getColumnSortOrder().addListener(sortOrderListener); + mainTablePreferences.resizeColumnsToFitProperty().addListener(resizeColumnsListener); + } + + /// The preferences outlive the table, so the listeners have to be removed when the library tab closes. + void unbind() { + mainTablePreferences.getColumnPreferences().getColumns().removeListener(columnsListener); + mainTablePreferences.getColumnPreferences().getColumnSortOrder().removeListener(sortOrderListener); + mainTablePreferences.resizeColumnsToFitProperty().removeListener(resizeColumnsListener); + } + + void applySortOrder() { + List> sortOrder = configured(mainTablePreferences.getColumnPreferences().getColumnSortOrder()) + .stream() + .flatMap(model -> findColumn(model).stream()) + .collect(Collectors.toCollection(ArrayList::new)); + findMatchCategoryColumn().ifPresent(sortOrder::addFirst); + if (sortOrder.equals(table.getSortOrder())) { + return; + } + table.getSortOrder().setAll(sortOrder); + } + + private void applyColumns() { + List configuredColumns = configured(mainTablePreferences.getColumnPreferences().getColumns()); + if (configuredColumns.equals(ColumnPreferencesRecorder.toPersistedModels(table.getColumns()))) { + return; + } + + List> columns = new ArrayList<>(); + // Reusing the instance keeps it in the sort order. A new instance would briefly empty the sort order. + findMatchCategoryColumn().ifPresent(columns::add); + configuredColumns.stream() + .flatMap(model -> findColumn(model) + .or(() -> Optional.ofNullable(columnFactory.createColumn(model))) + .stream()) + .forEach(columns::add); + table.getColumns().setAll(columns); + } + + private void applyResizePolicy(boolean resizeColumnsToFit) { + if (resizeColumnsToFit) { + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS); + } else { + table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); + } + } + + private static List configured(List models) { + return models.stream() + .filter(MainTableColumnModel::isConfigurable) + .toList(); + } + + private Optional> findMatchCategoryColumn() { + return table.getColumns().stream() + .filter(column -> column instanceof MainTableColumn mainTableColumn + && mainTableColumn.getModel().getType() == MainTableColumnModel.Type.MATCH_CATEGORY) + .findFirst(); + } + + private Optional> findColumn(MainTableColumnModel model) { + return table.getColumns().stream() + .filter(column -> column instanceof MainTableColumn mainTableColumn + && model.equals(mainTableColumn.getModel())) + .findFirst(); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java b/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesRecorder.java similarity index 51% rename from jabgui/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java rename to jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesRecorder.java index 84fb24c1d645..33d2ad1533d3 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/ColumnPreferencesRecorder.java @@ -14,38 +14,53 @@ import org.slf4j.LoggerFactory; /// Keep track of changes made to the columns (reordering, resorting, resizing). -public class PersistenceVisualStateTable { +/// +/// This is the table-to-preferences direction. The opposite direction is [ColumnPreferencesApplier]. +/// +/// Resizing and changing the sort type need no listener here: the preferences receive the column models of the +/// table itself, and [ColumnPreferences] reports changes of their properties. +public class ColumnPreferencesRecorder { - private static final Logger LOGGER = LoggerFactory.getLogger(PersistenceVisualStateTable.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ColumnPreferencesRecorder.class); protected final TableView table; protected final ColumnPreferences preferences; - public PersistenceVisualStateTable(TableView table, ColumnPreferences preferences) { + private final InvalidationListener columnsListener = _ -> updateColumns(); + private final ListChangeListener> sortOrderListener = _ -> updateSortOrder(); + + public ColumnPreferencesRecorder(TableView table, ColumnPreferences preferences) { this.table = table; this.preferences = preferences; } - public void addListeners() { - table.getColumns().addListener((InvalidationListener) _ -> updateColumns()); - table.getSortOrder().addListener((ListChangeListener>) _ -> updateSortOrder()); + public void bind() { + table.getColumns().addListener(columnsListener); + table.getSortOrder().addListener(sortOrderListener); + } - // As we store the ColumnModels of the MainTable, we need to add the listener to the ColumnModel properties, - // since the value is bound to the model after the listener to the column itself is called. + public void unbind() { + table.getColumns().removeListener(columnsListener); + table.getSortOrder().removeListener(sortOrderListener); + } - table.getColumns().stream() - .map(col -> ((MainTableColumn) col).getModel()) - .forEach(model -> { - model.widthProperty().addListener(_ -> updateColumns()); - model.sortTypeProperty().addListener(_ -> updateColumns()); - }); + /// Returns the models of the given columns as they are stored in the preferences. + /// + /// [ColumnPreferencesApplier] compares with the same projection. If both directions used different ones, a change + /// written by one direction would be reverted by the other. + static List toPersistedModels(List> columns) { + return columns.stream() + .filter(col -> col instanceof MainTableColumn) + .map(column -> ((MainTableColumn) column).getModel()) + .filter(MainTableColumnModel::isConfigurable) + .collect(Collectors.toList()); } /// Stores shown columns, their width and their [TableColumn.SortType] in preferences. /// The conversion to the "real" string in the preferences is made at /// [org.jabref.logic.preferences.JabRefCliPreferences#getColumnSortTypesAsStringList(ColumnPreferences)] private void updateColumns() { - List list = toList(table.getColumns()); + List list = toPersistedModels(table.getColumns()); LOGGER.debug("Updating columns to {}", list); preferences.setColumns(list); } @@ -56,14 +71,6 @@ private void updateColumns() { /// on other changes. private void updateSortOrder() { LOGGER.debug("Updating sort order"); - preferences.setColumnSortOrder(toList(table.getSortOrder())); - } - - private List toList(List> columns) { - return columns.stream() - .filter(col -> col instanceof MainTableColumn) - .map(column -> ((MainTableColumn) column).getModel()) - .filter(model -> model.getType() != MainTableColumnModel.Type.MATCH_CATEGORY) - .collect(Collectors.toList()); + preferences.setColumnSortOrder(toPersistedModels(table.getSortOrder())); } } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java b/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java index 6aa8623cecb1..836686fa251f 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -3,10 +3,8 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -49,8 +47,6 @@ import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.libraryproperties.LibraryPropertiesAction; -import org.jabref.gui.maintable.columns.LibraryColumn; -import org.jabref.gui.maintable.columns.MainTableColumn; import org.jabref.gui.mergeentries.MergeWithFetchedEntryAction; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.preview.ClipboardContentGenerator; @@ -99,6 +95,8 @@ public class MainTable extends TableView { private final FilePreferences filePreferences; private final ImportHandler importHandler; private final ClipboardContentGenerator clipboardContentGenerator; + private final ColumnPreferencesApplier columnPreferencesApplier; + private final ColumnPreferencesRecorder columnPreferencesRecorder; private long lastKeyPressTime; private String columnSearchTerm; @@ -148,8 +146,8 @@ public MainTable(MainTableDataModel model, stateManager, taskExecutor); - this.getColumns().addAll(mainTableColumnFactory.createColumns()); - this.getColumns().removeIf(LibraryColumn.class::isInstance); + this.columnPreferencesApplier = new ColumnPreferencesApplier(this, mainTableColumnFactory, mainTablePreferences); + columnPreferencesApplier.bind(); new ViewModelTableRowFactory() .withOnMouseClickedEvent((entry, event) -> { @@ -183,15 +181,11 @@ public MainTable(MainTableDataModel model, // force match category column to be the first sort order, (match_category column is always the first column) this.getSortOrder().addFirst(getColumns().getFirst()); this.getSortOrder().addListener((ListChangeListener>) _ -> { - if (!this.getSortOrder().getFirst().equals(getColumns().getFirst())) { + if (this.getSortOrder().isEmpty() || !this.getSortOrder().getFirst().equals(getColumns().getFirst())) { this.getSortOrder().addFirst(getColumns().getFirst()); } }); - if (mainTablePreferences.getResizeColumnsToFit()) { - this.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS); - } - this.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); this.setItems(model.getEntriesFilteredAndSorted()); @@ -259,10 +253,11 @@ public MainTable(MainTableDataModel model, }) ); - UiTaskExecutor.runInJavaFXThread(() -> restoreConfiguredSortOrder(mainTablePreferences)); + UiTaskExecutor.runInJavaFXThread(columnPreferencesApplier::applySortOrder); // Store visual state - new PersistenceVisualStateTable(this, mainTablePreferences.getColumnPreferences()).addListeners(); + this.columnPreferencesRecorder = new ColumnPreferencesRecorder(this, mainTablePreferences.getColumnPreferences()); + columnPreferencesRecorder.bind(); setupKeyBindings(keyBindingRepository); @@ -280,30 +275,6 @@ public MainTable(MainTableDataModel model, new MainTableHeaderContextMenu(this, mainTableColumnFactory, tabContainer, dialogService).show(true); } - private void restoreConfiguredSortOrder(MainTablePreferences mainTablePreferences) { - List> restoredSortOrder = - new ArrayList<>(mainTablePreferences.getColumnPreferences() - .getColumnSortOrder() - .stream() - .map(columnModel -> - this.getColumns().stream() - .map(column -> (MainTableColumn) column) - .filter(column -> column.getModel().equals(columnModel)) - .findFirst() - .orElse(null)) - .filter(Objects::nonNull) - .map(column -> (TableColumn) column) - .toList()); - - if (restoredSortOrder.isEmpty()) { - return; - } - - restoredSortOrder.forEach(column -> LOGGER.trace("Adding sort order for col {} ", column)); - restoredSortOrder.addFirst(getColumns().getFirst()); - this.getSortOrder().setAll(restoredSortOrder); - } - /// This is called, if a user starts typing some characters into the keyboard with focus on main table. The [MainTable] will scroll to the cell with the same starting column value and typed string /// If the user presses any other special key as well, e.g. alt or shift we don't jump /// @@ -639,6 +610,12 @@ public void addSelectionListener(ListChangeListener> columns = new ArrayList<>(); columns.add(createMatchCategoryColumn(new MainTableColumnModel(MainTableColumnModel.Type.MATCH_CATEGORY))); - columnPreferences.getColumns().forEach(column -> columns.add(createColumn(column))); + columnPreferences.getColumns().stream() + .filter(MainTableColumnModel::isConfigurable) + .map(this::createColumn) + .filter(Objects::nonNull) + .forEach(columns::add); return columns; } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java b/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java index 0967b2d0fb7a..de975cfb2194 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java @@ -186,6 +186,28 @@ public List getSortCriteria() { .toList(); } + /// Returns whether this model may appear in the user-editable entry-table column preferences. + /// + /// Internal helper columns such as `MATCH_CATEGORY`, and typed columns missing their qualifier, + /// are valid implementation details but must not be persisted or reconstructed from user input. + public boolean isConfigurable() { + return switch (getType()) { + case MATCH_CATEGORY, + LIBRARY_NAME -> + false; + case INDEX, + FILES, + GROUPS, + GROUP_ICONS, + LINKED_IDENTIFIER -> + true; + case EXTRAFILE, + NORMALFIELD, + SPECIALFIELD -> + !getQualifier().isBlank(); + }; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java index 526a6ee9dc81..5735fb51a3ec 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java @@ -48,6 +48,7 @@ public void show(boolean show) { mainTable.setOnContextMenuRequested(event -> { // Display the menu if header is clicked, otherwise, remove from display. if (show && isColumnHeaderTarget(event.getTarget())) { + constructItems(); this.show(mainTable, event.getScreenX(), event.getScreenY()); } else if (this.isShowing()) { this.hide(); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java index db8145d178c3..6f0babf869e3 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java @@ -209,6 +209,11 @@ public void insertColumnInList() { return; } + if (!addColumnProperty.getValue().isConfigurable()) { + addColumnProperty.setValue(null); + return; + } + if (columnsListProperty.getValue().stream().filter(item -> item.equals(addColumnProperty.getValue())).findAny().isEmpty()) { columnsListProperty.add(addColumnProperty.getValue()); addColumnProperty.setValue(null); @@ -245,7 +250,9 @@ public void moveColumnDown() { @Override public void storeSettings() { - mainTablePreferences.getColumnPreferences().setColumns(columnsListProperty.getValue()); + mainTablePreferences.getColumnPreferences().setColumns(columnsListProperty.getValue().stream() + .filter(MainTableColumnModel::isConfigurable) + .toList()); mainTablePreferences.setResizeColumnsToFit(autoResizeColumnsProperty.getValue()); mainTablePreferences.setExtraFileColumnsEnabled(extraFileColumnsEnabledProperty.getValue()); diff --git a/jabgui/src/main/java/org/jabref/gui/search/SearchResultsTable.java b/jabgui/src/main/java/org/jabref/gui/search/SearchResultsTable.java index 1d6ca5a8d27c..ce507f937a18 100644 --- a/jabgui/src/main/java/org/jabref/gui/search/SearchResultsTable.java +++ b/jabgui/src/main/java/org/jabref/gui/search/SearchResultsTable.java @@ -10,9 +10,9 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; import org.jabref.gui.maintable.BibEntryTableViewModel; +import org.jabref.gui.maintable.ColumnPreferencesRecorder; import org.jabref.gui.maintable.MainTableColumnFactory; import org.jabref.gui.maintable.MainTablePreferences; -import org.jabref.gui.maintable.PersistenceVisualStateTable; import org.jabref.gui.maintable.columns.LibraryColumn; import org.jabref.gui.maintable.columns.MainTableColumn; import org.jabref.gui.preferences.GuiPreferences; @@ -65,7 +65,7 @@ public SearchResultsTable(SearchResultsTableDataModel model, model.getEntriesFilteredAndSorted().comparatorProperty().bind(this.comparatorProperty()); // Store visual state - new PersistenceVisualStateTable(this, preferences.getSearchDialogColumnPreferences()).addListeners(); + new ColumnPreferencesRecorder(this, preferences.getSearchDialogColumnPreferences()).bind(); database.getDatabase().registerListener(this); } diff --git a/jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesApplierTest.java b/jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesApplierTest.java new file mode 100644 index 000000000000..54b7866069a6 --- /dev/null +++ b/jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesApplierTest.java @@ -0,0 +1,233 @@ +package org.jabref.gui.maintable; + +import java.util.ArrayList; +import java.util.List; + +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; + +import org.jabref.gui.maintable.columns.MainTableColumn; +import org.jabref.gui.testutils.JavaFxExtension; +import org.jabref.model.entry.field.SpecialField; +import org.jabref.model.entry.field.StandardField; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@NullMarked +@ExtendWith(JavaFxExtension.class) +class ColumnPreferencesApplierTest { + + private TableView table; + private ColumnPreferences columnPreferences; + private MainTablePreferences mainTablePreferences; + private ColumnPreferencesApplier applier; + private MainTableColumnFactory columnFactory; + private MainTableColumnModel titleColumn; + private MainTableColumnModel yearColumn; + private MainTableColumnModel relevanceColumn; + + @BeforeEach + void setUp() { + titleColumn = new MainTableColumnModel(MainTableColumnModel.Type.NORMALFIELD, StandardField.TITLE.getName()); + yearColumn = new MainTableColumnModel(MainTableColumnModel.Type.NORMALFIELD, StandardField.YEAR.getName()); + relevanceColumn = new MainTableColumnModel(MainTableColumnModel.Type.SPECIALFIELD, SpecialField.RANKING.getName()); + + columnPreferences = new ColumnPreferences(List.of(titleColumn, yearColumn), List.of(titleColumn)); + mainTablePreferences = new MainTablePreferences(columnPreferences, false, false); + table = new TableView<>(); + + columnFactory = mock(MainTableColumnFactory.class); + when(columnFactory.createColumn(any(MainTableColumnModel.class))).thenAnswer(invocation -> new MainTableColumn<>((MainTableColumnModel) invocation.getArgument(0))); + when(columnFactory.createColumns()).thenAnswer(_ -> createColumns(columnPreferences.getColumns())); + + // Same order as in MainTable, so that the write-back of the table is part of every test + applier = new ColumnPreferencesApplier(table, columnFactory, mainTablePreferences); + applier.bind(); + applier.applySortOrder(); + new ColumnPreferencesRecorder(table, columnPreferences).bind(); + } + + @Test + void bindShowsConfiguredColumnsAfterMatchCategoryColumn() { + assertEquals(MainTableColumnModel.Type.MATCH_CATEGORY, modelOf(table.getColumns().getFirst()).getType()); + assertEquals(List.of(titleColumn, yearColumn), visibleColumns()); + } + + @Test + void applySortOrderPutsMatchCategoryColumnFirst() { + assertEquals(List.of(table.getColumns().getFirst(), table.getColumns().get(1)), table.getSortOrder()); + } + + @Test + void updatesDisplayedColumnsWhenColumnPreferencesChange() { + columnPreferences.setColumns(List.of(relevanceColumn, titleColumn)); + + assertEquals(List.of(relevanceColumn, titleColumn), visibleColumns()); + } + + @Test + void updatesSortOrderWhenSortOrderPreferencesChange() { + columnPreferences.setColumnSortOrder(List.of(yearColumn, titleColumn)); + + assertEquals(List.of(yearColumn, titleColumn), visibleSortOrder()); + } + + @Test + void reorderingColumnsInOneOpenTableUpdatesOtherOpenTables() { + BoundTable secondTable = newBoundTable(); + + TableColumn yearColumnInFirstTable = findColumn(table, yearColumn); + table.getColumns().remove(yearColumnInFirstTable); + table.getColumns().add(1, yearColumnInFirstTable); + + assertEquals(List.of(yearColumn, titleColumn), visibleColumns(secondTable.table())); + } + + @Test + void sortingInOneOpenTableUpdatesOtherOpenTables() { + BoundTable secondTable = newBoundTable(); + + TableColumn yearColumnInFirstTable = findColumn(table, yearColumn); + yearColumnInFirstTable.setSortType(TableColumn.SortType.DESCENDING); + table.getSortOrder().setAll(List.of(table.getColumns().getFirst(), yearColumnInFirstTable)); + + assertEquals(List.of(yearColumn), visibleSortOrder(secondTable.table())); + assertEquals(TableColumn.SortType.DESCENDING, findColumn(secondTable.table(), yearColumn).getSortType()); + } + + @Test + void keepsExistingColumnsWhenAddingAnotherConfiguredColumn() { + TableColumn originalTitleColumn = table.getColumns().get(1); + + columnPreferences.setColumns(List.of(titleColumn, yearColumn, relevanceColumn)); + + assertSame(originalTitleColumn, table.getColumns().get(1)); + } + + @Test + void keepsMatchCategoryColumnInSortOrderWhenSortedColumnIsRemoved() { + columnPreferences.setColumns(List.of(yearColumn)); + + assertEquals(List.of(table.getColumns().getFirst()), table.getSortOrder()); + } + + @Test + void ignoresReservedColumnsInPreferences() { + MainTableColumnModel reservedColumn = new MainTableColumnModel(MainTableColumnModel.Type.MATCH_CATEGORY); + + columnPreferences.setColumns(List.of(titleColumn, reservedColumn, relevanceColumn)); + + assertEquals(List.of(titleColumn, relevanceColumn), visibleColumns()); + } + + @Test + void preferencesSurviveRoundTripThroughTable() { + columnPreferences.setColumns(List.of(relevanceColumn, titleColumn)); + columnPreferences.setColumnSortOrder(List.of(relevanceColumn)); + + assertEquals(List.of(relevanceColumn, titleColumn), columnPreferences.getColumns()); + assertEquals(List.of(relevanceColumn), columnPreferences.getColumnSortOrder()); + } + + @Test + void tableSurvivesRoundTripThroughPreferences() { + TableColumn yearTableColumn = table.getColumns().get(2); + List> expectedColumns = List.of(table.getColumns().getFirst(), yearTableColumn, table.getColumns().get(1)); + + // Reordering by drag and drop, as JavaFX does it + table.getColumns().remove(yearTableColumn); + table.getColumns().add(1, yearTableColumn); + + assertEquals(expectedColumns, table.getColumns()); + assertEquals(List.of(yearColumn, titleColumn), columnPreferences.getColumns()); + } + + /// Widths are persisted only if table columns and preferences share the model instances + @Test + void columnsAddedByPreferencesUseModelInstancesOfPreferences() { + columnPreferences.setColumns(List.of(titleColumn, yearColumn, relevanceColumn)); + + assertSame(relevanceColumn, modelOf(table.getColumns().get(3))); + assertSame(relevanceColumn, columnPreferences.getColumns().get(2)); + } + + @Test + void unboundApplierStopsReactingToPreferenceChanges() { + applier.unbind(); + + columnPreferences.setColumns(List.of(relevanceColumn)); + + assertEquals(List.of(titleColumn, yearColumn), visibleColumns()); + } + + @Test + void updatesResizePolicyWhenPreferenceChanges() { + mainTablePreferences.setResizeColumnsToFit(true); + + assertEquals(TableView.CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS, table.getColumnResizePolicy()); + } + + /// Mirrors [MainTableColumnFactory#createColumns()] + private static List> createColumns(List configuredColumns) { + List> columns = new ArrayList<>(); + columns.add(new MainTableColumn<>(new MainTableColumnModel(MainTableColumnModel.Type.MATCH_CATEGORY))); + configuredColumns.stream() + .filter(MainTableColumnModel::isConfigurable) + .forEach(model -> columns.add(new MainTableColumn<>(model))); + return columns; + } + + private BoundTable newBoundTable() { + TableView additionalTable = new TableView<>(); + ColumnPreferencesApplier additionalApplier = new ColumnPreferencesApplier(additionalTable, columnFactory, mainTablePreferences); + additionalApplier.bind(); + additionalApplier.applySortOrder(); + + ColumnPreferencesRecorder additionalRecorder = new ColumnPreferencesRecorder(additionalTable, columnPreferences); + additionalRecorder.bind(); + return new BoundTable(additionalTable, additionalApplier, additionalRecorder); + } + + private static MainTableColumnModel modelOf(TableColumn column) { + return ((MainTableColumn) column).getModel(); + } + + private static TableColumn findColumn(TableView table, + MainTableColumnModel model) { + return table.getColumns().stream() + .filter(column -> column instanceof MainTableColumn mainTableColumn + && model.equals(mainTableColumn.getModel())) + .findFirst() + .orElseThrow(); + } + + private List visibleColumns() { + return visibleColumns(table); + } + + private static List visibleColumns(TableView table) { + return ColumnPreferencesRecorder.toPersistedModels(table.getColumns()); + } + + private List visibleSortOrder() { + return visibleSortOrder(table); + } + + private static List visibleSortOrder(TableView table) { + return ColumnPreferencesRecorder.toPersistedModels(table.getSortOrder()); + } + + private record BoundTable(TableView table, + ColumnPreferencesApplier applier, + ColumnPreferencesRecorder recorder) { + } +} diff --git a/jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesRecorderTest.java b/jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesRecorderTest.java new file mode 100644 index 000000000000..c06508216957 --- /dev/null +++ b/jabgui/src/test/java/org/jabref/gui/maintable/ColumnPreferencesRecorderTest.java @@ -0,0 +1,91 @@ +package org.jabref.gui.maintable; + +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.scene.control.TableView; + +import org.jabref.gui.maintable.columns.MainTableColumn; +import org.jabref.gui.testutils.JavaFxExtension; +import org.jabref.model.entry.field.SpecialField; +import org.jabref.model.entry.field.StandardField; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +@NullMarked +@ExtendWith(JavaFxExtension.class) +class ColumnPreferencesRecorderTest { + + private TableView table; + private ColumnPreferences preferences; + private MainTableColumnModel titleColumn; + private MainTableColumnModel relevanceColumn; + private ColumnPreferencesRecorder columnPreferencesRecorder; + private int columnPreferenceChanges; + + @BeforeEach + void setUp() { + titleColumn = new MainTableColumnModel(MainTableColumnModel.Type.NORMALFIELD, StandardField.TITLE.getName()); + relevanceColumn = new MainTableColumnModel(MainTableColumnModel.Type.SPECIALFIELD, SpecialField.RANKING.getName()); + + table = new TableView<>(); + table.getColumns().setAll(List.of( + new MainTableColumn<>(new MainTableColumnModel(MainTableColumnModel.Type.MATCH_CATEGORY)), + new MainTableColumn<>(titleColumn), + new MainTableColumn<>(relevanceColumn))); + + preferences = new ColumnPreferences(List.of(titleColumn, relevanceColumn), List.of(titleColumn)); + columnPreferencesRecorder = new ColumnPreferencesRecorder(table, preferences); + columnPreferencesRecorder.bind(); + preferences.getColumns().addListener((InvalidationListener) _ -> columnPreferenceChanges++); + } + + @Test + void removingColumnUpdatesPreferences() { + table.getColumns().remove(2); + + assertEquals(List.of(titleColumn), preferences.getColumns()); + } + + /// Widths are persisted only if table columns and preferences share the model instances + @Test + void storesModelInstancesOfTable() { + preferences.setColumns(List.of(MainTableColumnModel.parse(titleColumn.getName()))); + + table.getColumns().remove(2); + + assertSame(titleColumn, preferences.getColumns().getFirst()); + } + + @Test + void columnWidthChangeUpdatesPreferences() { + titleColumn.widthProperty().set(250); + + assertEquals(1, columnPreferenceChanges); + } + + @Test + void removedColumnWidthChangeDoesNotUpdatePreferences() { + table.getColumns().remove(2); + columnPreferenceChanges = 0; + + relevanceColumn.widthProperty().set(250); + + assertEquals(0, columnPreferenceChanges); + } + + @Test + void unboundTableStopsUpdatingPreferences() { + columnPreferencesRecorder.unbind(); + + table.getColumns().remove(2); + + assertEquals(List.of(titleColumn, relevanceColumn), preferences.getColumns()); + } +} diff --git a/jabgui/src/test/java/org/jabref/gui/preferences/table/TableTabViewModelTest.java b/jabgui/src/test/java/org/jabref/gui/preferences/table/TableTabViewModelTest.java new file mode 100644 index 000000000000..760bf9ff64bf --- /dev/null +++ b/jabgui/src/test/java/org/jabref/gui/preferences/table/TableTabViewModelTest.java @@ -0,0 +1,58 @@ +package org.jabref.gui.preferences.table; + +import java.util.List; + +import org.jabref.gui.DialogService; +import org.jabref.gui.frame.ExternalApplicationsPreferences; +import org.jabref.gui.maintable.ColumnPreferences; +import org.jabref.gui.maintable.MainTableColumnModel; +import org.jabref.gui.maintable.MainTablePreferences; +import org.jabref.gui.maintable.NameDisplayPreferences; +import org.jabref.gui.specialfields.SpecialFieldsPreferences; +import org.jabref.gui.testutils.JavaFxExtension; +import org.jabref.model.entry.field.StandardField; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +@NullMarked +@ExtendWith(JavaFxExtension.class) +class TableTabViewModelTest { + + @Test + void insertColumnInListIgnoresReservedColumns() { + MainTablePreferences mainTablePreferences = new MainTablePreferences(new ColumnPreferences(List.of(), List.of()), false, false); + TableTabViewModel viewModel = newTableTabViewModel(mainTablePreferences); + viewModel.addColumnProperty().set(MainTableColumnModel.parse("match_category")); + + viewModel.insertColumnInList(); + + assertEquals(List.of(), viewModel.columnsListProperty().get()); + } + + @Test + void storeSettingsFiltersReservedColumns() { + MainTablePreferences mainTablePreferences = new MainTablePreferences(new ColumnPreferences(List.of(), List.of()), false, false); + TableTabViewModel viewModel = newTableTabViewModel(mainTablePreferences); + MainTableColumnModel titleColumn = new MainTableColumnModel(MainTableColumnModel.Type.NORMALFIELD, StandardField.TITLE.getName()); + MainTableColumnModel reservedColumn = new MainTableColumnModel(MainTableColumnModel.Type.MATCH_CATEGORY); + + viewModel.columnsListProperty().addAll(titleColumn, reservedColumn); + viewModel.storeSettings(); + + assertEquals(List.of(titleColumn), mainTablePreferences.getColumnPreferences().getColumns()); + } + + private TableTabViewModel newTableTabViewModel(MainTablePreferences mainTablePreferences) { + return new TableTabViewModel( + mock(DialogService.class), + mock(SpecialFieldsPreferences.class), + mock(NameDisplayPreferences.class), + mainTablePreferences, + mock(ExternalApplicationsPreferences.class)); + } +} From 19ac0ceff10fe73b069e178ed2db3483646654a2 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sat, 19 Sep 2026 14:36:42 +0000 Subject: [PATCH 028/102] Fix JabRef not starting when Mistral AI is the AI provider (#17212) * Fix JabRef not starting when Mistral AI is the selected AI provider Declare the Mistral AI service interfaces as used by the merged langchain4j module. Co-Authored-By: Claude Opus 5 (1M context) * Add changelog entry Co-Authored-By: Claude Opus 5 (1M context) * Mark ChatModelFactoryTest as null-marked Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + ...ef.gradle.base.dependency-rules.gradle.kts | 3 +++ .../chatting/util/ChatModelFactoryTest.java | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 jablib/src/test/java/org/jabref/logic/ai/chatting/util/ChatModelFactoryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ba88c02fcc..d3f0b8868d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -236,6 +236,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where generating AI embeddings for an entry with a linked URL logged a misleading error about a missing file. [#16123](https://github.com/JabRef/jabref/issues/16123) - We fixed an issue where the basic AI preferences stayed disabled although AI was enabled. [#17128](https://github.com/JabRef/jabref/pull/17128) - We fixed an issue where the AI preferences could not be saved because the document splitter overlap size was wrongly reported as invalid. [#17119](https://github.com/JabRef/jabref/pull/17119) +- We fixed an issue where JabRef did not start when Mistral AI was selected as AI provider. [#17212](https://github.com/JabRef/jabref/pull/17212) - We fixed an issue with the LibreOffice integration where the ordering for numeric CSL styles in footnotes was broken. [#12484](https://github.com/JabRef/jabref/issues/12484) - We fixed an issue where `git push` did not report rejected remote updates. [#16367](https://github.com/JabRef/jabref/pull/16367) - We fixed formatting issues in entry preview when `.bst` styles were used. [#16314](https://github.com/JabRef/jabref/issues/16314) diff --git a/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts b/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts index 9bc14c71d27d..c0d951d6d74e 100644 --- a/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts +++ b/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts @@ -246,6 +246,9 @@ extraJavaModuleInfo { requires("java.net.http") uses("dev.langchain4j.http.client.HttpClientBuilderFactory") uses("dev.langchain4j.spi.json.ProviderJsonCodecFactory") + // Loaded by MistralAiChatModel.builder() and build(); missing uses crashes JabRef start with Mistral AI selected + uses("dev.langchain4j.model.mistralai.spi.MistralAiChatModelBuilderFactory") + uses("dev.langchain4j.model.mistralai.internal.client.MistralAiClientBuilderFactory") exportAllPackages() requireAllDefinedDependencies() } diff --git a/jablib/src/test/java/org/jabref/logic/ai/chatting/util/ChatModelFactoryTest.java b/jablib/src/test/java/org/jabref/logic/ai/chatting/util/ChatModelFactoryTest.java new file mode 100644 index 000000000000..65839bb4eed4 --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/ai/chatting/util/ChatModelFactoryTest.java @@ -0,0 +1,24 @@ +package org.jabref.logic.ai.chatting.util; + +import org.jabref.logic.ai.chatting.ChatModel; +import org.jabref.model.ai.llm.AiProvider; +import org.jabref.model.ai.tokenization.TokenEstimatorKind; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@NullMarked +class ChatModelFactoryTest { + + /// Building a model must not fail on module-path service loading (e.g., a missing `uses` for Mistral AI's builder factory) + @ParameterizedTest + @EnumSource(AiProvider.class) + void createDoesNotThrow(AiProvider provider) { + try (ChatModel model = ChatModelFactory.create(provider, "model", "key", 0.7, provider.getApiUrl(), 4096, TokenEstimatorKind.AVERAGE)) { + assertEquals("model", model.getName()); + } + } +} From 4cda967236025496856a0eb461425df70e18dd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Sat, 19 Sep 2026 20:02:11 +0000 Subject: [PATCH 029/102] Update OpenFastTrace user manual link (#17213) The user guide of OpenFastTrace is now one level deeper in a subdirectory "user_guide". Also, the more human readable variant is on the website `openfasttrace.itsallcode.org` since that is rendered with Jekyll and "Just the Docs". --- docs/requirements/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements/index.md b/docs/requirements/index.md index bf296558b6e1..4b47c5cb8877 100644 --- a/docs/requirements/index.md +++ b/docs/requirements/index.md @@ -68,6 +68,6 @@ While not a custom artifact, but we interpret these OpenFastTrace artifact types ## More Information - [General reading on traceability](https://www.sodiuswillert.com/en/blog/implementing-requirements-traceability-in-systems-software-engineering) -- [User manual of OpenFastTrace](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide.md) +- [User manual of OpenFastTrace](https://openfasttrace.itsallcode.org/user_guide/user_guide.html) We recommend using VS Code with `markdownlint` extension to edit requirement files, and not IntelliJ, as VS Code understands `markdownlintdisable` directives. From f9240b8e3d33c5bef6e356727c8c847b18574060 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 21 Sep 2026 14:28:42 +0000 Subject: [PATCH 030/102] Remove "status: stale" on push, too (#17221) Co-authored-by: Claude Opus 5 --- .github/workflows/on-pr-opened-updated.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/on-pr-opened-updated.yml b/.github/workflows/on-pr-opened-updated.yml index 6ce6febd6e3a..7634c71a33a5 100644 --- a/.github/workflows/on-pr-opened-updated.yml +++ b/.github/workflows/on-pr-opened-updated.yml @@ -29,6 +29,7 @@ jobs: # The merge commit pushed by this workflow (as jabref-machine) is exempt, as it does not change the PR's content. # Both status labels describe the state before the push and are therefore stale: whether the new # head requires changes or is ready for review is only known once CI and Qodo have run on it. + # A push is activity, so "status: stale" no longer applies either. # No label at all is the honest state for that window; "Comment on PR" sets the verdict. # Deliberately not in the concurrency group of "Comment on PR", although that would order the # removal before a still-running evaluation of the previous head: a group holds only one @@ -54,7 +55,7 @@ jobs: echo "⊘ PR moved on since this push - labels left untouched" >> $GITHUB_STEP_SUMMARY exit 0 fi - gh pr edit "$PR_NUMBER" --remove-label "automerge,status: changes-required,status: ready-for-review" + gh pr edit "$PR_NUMBER" --remove-label "automerge,status: changes-required,status: ready-for-review,status: stale" - name: Get PR info id: pr_info env: From 699f1f1e263bf8ca3e41bcbb5f5cc9c9ff6bed3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:03:51 +0000 Subject: [PATCH 031/102] Chore(deps): Bump org.xmlunit:xmlunit-matchers in /versions (#17226) Bumps [org.xmlunit:xmlunit-matchers](https://github.com/xmlunit/xmlunit) from 2.13.0 to 2.14.0. - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.13.0...v2.14.0) --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-matchers dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index d4bf6af13f0d..5eb647f95b33 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -160,8 +160,8 @@ dependencies.constraints { api("org.tinylog:slf4j-tinylog:2.8.0") api("org.tinylog:tinylog-api:2.8.0") api("org.tinylog:tinylog-impl:2.8.0") - api("org.xmlunit:xmlunit-core:2.13.0") - api("org.xmlunit:xmlunit-matchers:2.13.0") + api("org.xmlunit:xmlunit-core:2.14.0") + api("org.xmlunit:xmlunit-matchers:2.14.0") api("org.yaml:snakeyaml:2.7") api("tech.units:indriya:2.2.4") api("tools.maran:svg:1.0.0") From a091119f42adaf699833ba514e4d5da7a34b2103 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:08:50 +0000 Subject: [PATCH 032/102] Chore(deps): Bump org.xmlunit:xmlunit-core in /versions (#17228) Bumps [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit) from 2.13.0 to 2.14.0. - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.13.0...v2.14.0) --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 44bb5c0e69f075371aadc6e724b95c961772a4b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:32:57 +0000 Subject: [PATCH 033/102] Chore(deps): Bump jablib/src/main/resources/csl-styles (#17230) Bumps [jablib/src/main/resources/csl-styles](https://github.com/citation-style-language/styles) from `90e657c` to `81c3eb8`. - [Release notes](https://github.com/citation-style-language/styles/releases) - [Commits](https://github.com/citation-style-language/styles/compare/90e657c8622ec1c6f27788e041b355efac659397...81c3eb863d1c5a83aab7e296e045013d2422f2cb) --- updated-dependencies: - dependency-name: jablib/src/main/resources/csl-styles dependency-version: 81c3eb863d1c5a83aab7e296e045013d2422f2cb dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- jablib/src/main/resources/csl-styles | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/resources/csl-styles b/jablib/src/main/resources/csl-styles index 90e657c8622e..81c3eb863d1c 160000 --- a/jablib/src/main/resources/csl-styles +++ b/jablib/src/main/resources/csl-styles @@ -1 +1 @@ -Subproject commit 90e657c8622ec1c6f27788e041b355efac659397 +Subproject commit 81c3eb863d1c5a83aab7e296e045013d2422f2cb From 1b71a69b672c173c65bdf8e12714699a69144a62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:33:45 +0000 Subject: [PATCH 034/102] Chore(deps): Bump jabgui/src/main/themes.jabref.org (#17231) Bumps [jabgui/src/main/themes.jabref.org](https://github.com/JabRef/themes.jabref.org) from `82ae85e` to `25652cd`. - [Commits](https://github.com/JabRef/themes.jabref.org/compare/82ae85e86b8a348bd935ae13f72a12f160f12fdd...25652cdef6e6b5d3c50b57198e9a29c40caad82b) --- updated-dependencies: - dependency-name: jabgui/src/main/themes.jabref.org dependency-version: 25652cdef6e6b5d3c50b57198e9a29c40caad82b dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- jabgui/src/main/themes.jabref.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jabgui/src/main/themes.jabref.org b/jabgui/src/main/themes.jabref.org index 82ae85e86b8a..25652cdef6e6 160000 --- a/jabgui/src/main/themes.jabref.org +++ b/jabgui/src/main/themes.jabref.org @@ -1 +1 @@ -Subproject commit 82ae85e86b8a348bd935ae13f72a12f160f12fdd +Subproject commit 25652cdef6e6b5d3c50b57198e9a29c40caad82b From 67ab1280a00f13f9718b276442b5bee1d1b09998 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:40:14 +0000 Subject: [PATCH 035/102] chore(sbom): update CycloneDX SBOM files (#17232) Co-authored-by: Siedlerchr <320228+Siedlerchr@users.noreply.github.com> --- bom.json | 324 ++++++++++++++++++++++++++++++++++++++++--------------- bom.xml | 252 ++++++++++++++++++++++++++++--------------- 2 files changed, 398 insertions(+), 178 deletions(-) diff --git a/bom.json b/bom.json index c0deb37b5d2a..6c6dedabfdcd 100644 --- a/bom.json +++ b/bom.json @@ -1,10 +1,10 @@ { "bomFormat" : "CycloneDX", "specVersion" : "1.6", - "serialNumber" : "urn:uuid:d12530bf-59f6-4c77-b768-23f90826f040", + "serialNumber" : "urn:uuid:d16e4d89-9aeb-433f-8b1b-ec3aec7cc86d", "version" : 1, "metadata" : { - "timestamp" : "2026-09-09T17:54:53Z", + "timestamp" : "2026-09-21T17:27:21Z", "tools" : { "components" : [ { @@ -26,7 +26,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -92,7 +92,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -117,43 +117,43 @@ }, { "type" : "library", - "bom-ref" : "pkg:maven/ai.djl.huggingface/tokenizers@0.37.0?type=jar", + "bom-ref" : "pkg:maven/ai.djl.huggingface/tokenizers@0.38.0?type=jar", "group" : "ai.djl.huggingface", "name" : "tokenizers", - "version" : "0.37.0", + "version" : "0.38.0", "description" : "Deep Java Library (DJL) NLP utilities for Huggingface tokenizers", "hashes" : [ { "alg" : "MD5", - "content" : "1572ef240d905d6cb0b1a453abdf12ad" + "content" : "4015d9a531c0b0e6a035b64122ab1b0a" }, { "alg" : "SHA-1", - "content" : "332045b1db909bbfb1d387e3e13c2c04600e3e07" + "content" : "875d8c48f05165a17498215747974cde2de728bd" }, { "alg" : "SHA-256", - "content" : "7dbdd849c3c8008f6b8909df4fc65c816a6e5a173d75f4c213e8d6e25700b0e8" + "content" : "aab48c4db87ffb724d6037940aaba1d0b3d3b1de45156d7d9efc818f92f7a521" }, { "alg" : "SHA-512", - "content" : "2d13b9938ef44b08ac416e83e558263c3aa842ea95e95ed913544c293178f71dcded18fa60db7eec02a2a424dee037b9deaa7510f18a10d9e546696576be2902" + "content" : "faee99ec90854f8e29539d535005ccc99a4409d9a666a3ba285766c08c93301051984b563945c6c8757a5197c02556b8c2f24edb0b78d4d73991458417efdfa6" }, { "alg" : "SHA3-256", - "content" : "7f4c954f0f1180d00319a94ee56bb02f9b510f75111fda87f80858123c4427a5" + "content" : "d1efc883dd5f50e45582105cec280489c6d96ddd042c63ff246c3f79fe23ef82" }, { "alg" : "SHA3-512", - "content" : "b5d224d235718f44905bd17ac492244b6975d967538916887bdf15e4c98e1ca3d802e4f340ab614a64386048c86c927fd8acb9a59c66096808d2c4c6bb10cbbd" + "content" : "19e382ea4da3f7d645563d144e72185ccc849c7eeee607652a091833f07b4178b7961d8d28f70d4aba27aea3e375f74e882fd9e995d0636b58b93489a8152ac2" }, { "alg" : "SHA-384", - "content" : "a4ed22c7953b101b5e0c52d7d27feae83dfd9dd77b016a984e3265f762ca053d5fd76c905656071b6030acf99e1e5286" + "content" : "5d5f4174a22e2f6c6ae7240d730f1148aaae76e45cf71e6710789c4e5a3dd54a4a5cad653fe7bc3602086af91fb27660" }, { "alg" : "SHA3-384", - "content" : "05cfb7eec5287a020958a5ec90cec3ebc2e393383c4e71e4f1aeec4dde1cf3e63403f7d561f8e4f6f15e55a6071d8f57" + "content" : "f64ab3c8bcbbae11d88d4bf06a7bf46601b73de94a309811abd768348597888a5b04a79b366a0c91e928d52fbd366061" } ], "licenses" : [ @@ -163,7 +163,7 @@ } } ], - "purl" : "pkg:maven/ai.djl.huggingface/tokenizers@0.37.0?type=jar", + "purl" : "pkg:maven/ai.djl.huggingface/tokenizers@0.38.0?type=jar", "modified" : false, "externalReferences" : [ { @@ -180,43 +180,43 @@ }, { "type" : "library", - "bom-ref" : "pkg:maven/ai.djl.pytorch/pytorch-engine@0.37.0?type=jar", + "bom-ref" : "pkg:maven/ai.djl.pytorch/pytorch-engine@0.38.0?type=jar", "group" : "ai.djl.pytorch", "name" : "pytorch-engine", - "version" : "0.37.0", + "version" : "0.38.0", "description" : "Deep Java Library (DJL) Engine Adapter for PyTorch", "hashes" : [ { "alg" : "MD5", - "content" : "23b1d1f85c45eada96f707a41d7b7e36" + "content" : "01e3676e5c806d315ab5d226bb5822a9" }, { "alg" : "SHA-1", - "content" : "8361ec786df8d04e6229510788478982c154cd8b" + "content" : "f05d74386d0fa925cba579595374b90ba40171a0" }, { "alg" : "SHA-256", - "content" : "e4ae439c6f7b80acacbb6fc95d4686fbda705b1155c9c5bce678d946ddc69ac2" + "content" : "327801b3b90e1b0179cd7266eeb1b484834f7090c4d5003a8331b944bf8aac77" }, { "alg" : "SHA-512", - "content" : "935875bff7ff5082560f7f97c5a4efac8d34ede4ef1a7ef1feae2dad09c4c539dbbf8fbc9de8ff05c27d7b1374415e6d73fef3f90679eab7df0995e665381c70" + "content" : "509d94afadc86db35fd4b330582dfcc7935016b8a171362a81dfa6a21c311749c5f6725114d8a9ce73c8d1f5c8c27614ff088dff42cbcac3cc8f5998026ee48b" }, { "alg" : "SHA3-256", - "content" : "479268d8054ec8a5436b28bb5439b0fcf298827a09bedc618b5581460e1f7605" + "content" : "e53e194fc912afcd18dcea3d399f585133124b31b3f43c9e411b6b1fad200864" }, { "alg" : "SHA3-512", - "content" : "a21ff75a23f51a84c3905a4a7658e6e62225937a65291c6328e6484d747de2fabee5142fae23d98b873b78c27793eca3f976640c832f08312a7a5bd43d4bba7d" + "content" : "ada85253dc76ea7461d68ce4dc5486349165e97137fe72b4d1a940e9f62da1e959bcff3bb356da1fe6d4f17d77ca59c489ff4339c96cd1e0a2c797940855eae5" }, { "alg" : "SHA-384", - "content" : "c90378f0b23072a13f8659543bd8b362c8e9b6522dc9220f22d71d964a9333c5fdd965f384288da523d8dd55ba24edac" + "content" : "e18f7c0c38bac8dd443d9da8c57a9e59a43fa4dad9bbefba4df66f661a5a62766c34047a3121d5ffb28a793b81497a9d" }, { "alg" : "SHA3-384", - "content" : "49aa2cab567af598df8982bd36ee6449d1eac84662ece5d8344d0a3d30eaad1020797102bb26bb0d96ab1d06db1af9ac" + "content" : "a4b6943e1c2517efb6777b30efa94948c07e5f4eb4b0e552fdb41366d0ff2c8f8f13bcc828a2c2bc088597f49f80a1b7" } ], "licenses" : [ @@ -226,7 +226,7 @@ } } ], - "purl" : "pkg:maven/ai.djl.pytorch/pytorch-engine@0.37.0?type=jar", + "purl" : "pkg:maven/ai.djl.pytorch/pytorch-engine@0.38.0?type=jar", "modified" : false, "externalReferences" : [ { @@ -243,43 +243,43 @@ }, { "type" : "library", - "bom-ref" : "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.37.0?type=jar", + "bom-ref" : "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.38.0?type=jar", "group" : "ai.djl.pytorch", "name" : "pytorch-model-zoo", - "version" : "0.37.0", + "version" : "0.38.0", "description" : "Deep Java Library (DJL) model zoo for PyTorch", "hashes" : [ { "alg" : "MD5", - "content" : "9561ed397b19df88c89bb7c6e4f73794" + "content" : "98b1599e2dfb56e4f05ca8cbb73b621c" }, { "alg" : "SHA-1", - "content" : "b2fb396415e68bc8cc8c1e1301e558d866f09976" + "content" : "434a6ddd6991f89cca8f34bc4b786bf8b5a90008" }, { "alg" : "SHA-256", - "content" : "06656be87884d423ed6e548d6f76fead42ec1f9ad9156f5300cff48ddce9a030" + "content" : "315ad71dab4738ddd122f6b723d38f7c5fc00e93d0dc9baa445d83ab0f3a592c" }, { "alg" : "SHA-512", - "content" : "95be83d1eb3a8f1f123d07620e9b14563d02936dc4ac953cef1e90005f3cac8a5fd136d2ee03424c1bf98c174cae69ebb3275b32aac5db0a98b61a55562d9f8a" + "content" : "4ff26e98d3e4df249586e06a647aa8c35bd2a6b8d32762685e18763237e48aee403c374d52fa088d447b47d643310a243ea6817b572066ffda6fd8b7e335fac4" }, { "alg" : "SHA3-256", - "content" : "d85d4a4f6793b412e02238f78813b0766d903e46dbbe774f7dfba4c49b77ba10" + "content" : "308b7d63677ae51fbcb0fe8d9d7de4d30a2c5c8d968f779f6724705b57b4f9f5" }, { "alg" : "SHA3-512", - "content" : "63de21931bd1a6c74f877d8f69b0457216a235feafac2a415506bcc52d9eda8ce37daf20744942b8ff96d37d9f2091edd5039707620b38e33c45d001a5924e58" + "content" : "38bdbc12c78853d397e550a8732fe8cdb7d6de8185f6ca6bc5226ad2de0e30d065d5c31f40233735988c82604e85a8b1e02c97c412227f44a9b4e84effde1d1b" }, { "alg" : "SHA-384", - "content" : "ca48250cd554fc9232e10b0c490d24576f08e7c94b69b8e45948c61e19aea9312d8292924bad6f54e9b9d7a91dbef059" + "content" : "299a9ba0adfcc064ea2dde199ded401972789b65ae39e667e48dee2e10e7206ac1479b0a0f15b9617b4de61a1bdfe440" }, { "alg" : "SHA3-384", - "content" : "d61884d49b4cd460bb5ff6698418ac3753418162366b9c630510033000c71364e7a60c6fde9f94be308e938a46c38e62" + "content" : "4bb1032f08c6764c66441f015f204ab88ebca17bc28756d44508443b5bf6b7f15156452dc6d02f9c18146a0eff4dd0e3" } ], "licenses" : [ @@ -289,7 +289,7 @@ } } ], - "purl" : "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.37.0?type=jar", + "purl" : "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.38.0?type=jar", "modified" : false, "externalReferences" : [ { @@ -306,43 +306,43 @@ }, { "type" : "library", - "bom-ref" : "pkg:maven/ai.djl/api@0.37.0?type=jar", + "bom-ref" : "pkg:maven/ai.djl/api@0.38.0?type=jar", "group" : "ai.djl", "name" : "api", - "version" : "0.37.0", + "version" : "0.38.0", "description" : "Deep Java Library - api", "hashes" : [ { "alg" : "MD5", - "content" : "e9594d8bc8256a7cd54cdac82c7aeabf" + "content" : "0864990898d41fb8db92a1a5f6d6a33e" }, { "alg" : "SHA-1", - "content" : "b0f26eef62e2a7e0dfd39f1ad64611587e19c802" + "content" : "2bc351e12a0d2da3cdf74a775ee5f170c8df38b9" }, { "alg" : "SHA-256", - "content" : "3b57eb8ff74abb38785b2cecb0bfea328e0827312c9945911e217ec33b15b025" + "content" : "5a99d57d675e68febd471ca814a81180dc2be7a05d66a1b6b0eb676416a9d6bc" }, { "alg" : "SHA-512", - "content" : "b58548cd15ca4ee34017233f766c457cb2851d18c102c331bc59a0cc9c40b143583ffcf9a84edba7a968f85f6b1ddca631c01218089a31ff0803c5482832f7f3" + "content" : "7a0b4275104bbccaaebca7a99b4df6ec1abc8369b91b04629f85b89609faf789faa7877432a9554c96b545eb8e1fa8893b093dbd1bc6ffc01dc7bf1e72d8b72b" }, { "alg" : "SHA3-256", - "content" : "7c3614cb5bbd4ef0036c8aff2a7e7a707cfef79ca4b30d7c53fcdb01c01cc792" + "content" : "1d1d55a89c3cbb99cff85c5771fab3d0eebc56009745f1e6670c4cb76d2152ab" }, { "alg" : "SHA3-512", - "content" : "43db5d2b07f838b25e43599e7250a3ca76e2ce771e2d5ca3dc9bb210661b574b37b42c83773373462c9680dd46974ee3e0cbc3ad626512b3821c86dcada59980" + "content" : "a675cd49f22dc206b4a7d9a94ec2522ffde0ade1c3dfb3c9f1b9177d6e52280532d73bd6e3f7e399fbd3fd3e8864c635d014e35ee3d2853caddb847daefb0d27" }, { "alg" : "SHA-384", - "content" : "75c6e598ffee0c97f9a0e6b4369c08ecc80cb9b906b299063e31f5c54268bc62daac38d059e2d666b21827c8b1efcc99" + "content" : "a4ebc6b9e2f18ac5ef3d0cd0f6b397864785e284728c40dad163415a14b81b62b931828e6186fb8e27228baac693beef" }, { "alg" : "SHA3-384", - "content" : "ffe6b6beb579efbc1a91b584941f7b4f28f198dd09378d0bff63671e507915fdc809d5bb1b1da52d8aabc453c3bab10f" + "content" : "1265459f00961795154894acb0e372494be6edfb8fcac80398ac63898efd4494e5fa4504e1b53a29ac847c1df5f40ea0" } ], "licenses" : [ @@ -352,7 +352,7 @@ } } ], - "purl" : "pkg:maven/ai.djl/api@0.37.0?type=jar", + "purl" : "pkg:maven/ai.djl/api@0.38.0?type=jar", "modified" : false, "externalReferences" : [ { @@ -369,10 +369,10 @@ }, { "type" : "library", - "bom-ref" : "pkg:maven/ai.djl/bom@0.37.0?type=pom", + "bom-ref" : "pkg:maven/ai.djl/bom@0.38.0?type=pom", "group" : "ai.djl", "name" : "bom", - "version" : "0.37.0", + "version" : "0.38.0", "description" : "Deep Java Library (DJL) Bill of Materials (BOM)", "licenses" : [ { @@ -381,7 +381,7 @@ } } ], - "purl" : "pkg:maven/ai.djl/bom@0.37.0?type=pom", + "purl" : "pkg:maven/ai.djl/bom@0.38.0?type=pom", "modified" : false, "externalReferences" : [ { @@ -4046,35 +4046,35 @@ "hashes" : [ { "alg" : "MD5", - "content" : "e30ed842c0ce6260835a755295a9c87b" + "content" : "3be16ac8db9a8ea8dae22e8cfb524f95" }, { "alg" : "SHA-1", - "content" : "a7db774c56be114f9aef5df44404d8047ef6bec4" + "content" : "1156cfae07df2f43d0f806736d3fc0c3abc43fa8" }, { "alg" : "SHA-256", - "content" : "c884e22f5a40abbb4ab3dfc2dd9e6e69ff89e04d9f9295d6ec7085b6301e0245" + "content" : "e5c385f4e61f8e757078afceb811b1194e3d7444ab11b5691885300212d01038" }, { "alg" : "SHA-512", - "content" : "6b2967e2209e1563b4ab991b984509cc2eabdb4c998b76efcb51024db0c3d227f41bf3ea8e392e05ef9373f0cbb8498a5933376983c3391ad4ce3835960484c2" + "content" : "73dac33123beb5028c6564c6742c1ce507f73503a0d6763be0a20a37b09da02fcb34515173862852b7c7cc626cf00a97f4e54c2fa404b286472daca60b48d9a8" }, { "alg" : "SHA3-256", - "content" : "bcd707a20e9180fe91f1fcfeedf14485e41c7455007062fb77ca6b317a95bd15" + "content" : "2252fd9e303b026133cc7d43818d1cc0927629670709bf9f1a00a12a109f2a92" }, { "alg" : "SHA3-512", - "content" : "c47225b7e9f58fa44219fecbbd750dec53e724b0dd48c9e84278d0a7ff2d6aae38029e89e9b01a9bbd3c9f55826b7dcd6ac02c81bab2142285f0060dd4fd6843" + "content" : "a3f8d803cfca9404320c11f7e0be6cb8e2732cd982270f3f5df75dcfc4ca857639cd13d09c2059dced109694f7382db1ec9cc39dbc3d027fa05b6a54ea9b5b05" }, { "alg" : "SHA-384", - "content" : "d88b8b48d70209fa3c6af78aa2c08e1d585939429413afcbf66dcb1f080371711063bd263278eadeafc98eefae9b70c4" + "content" : "a9fc45b6508ab04a0555a119959554d39021c0a7b3fd82b3dde0eb97867ebbb7f4304e6c20b8fe0fcc9b7c0f522a8c0a" }, { "alg" : "SHA3-384", - "content" : "dc50137595b490b704d5ec3dd70ae25d9bc4afa55223b9722c246081c91044af938b449200aa939399dffdf2435eb22a" + "content" : "c489f48aade8e6900cb850dc899a54b96833432480c8e61429bd590e02cf2820fe59efd963044072057c0b7f2827a87d" } ], "licenses" : [ @@ -5595,43 +5595,43 @@ }, { "type" : "library", - "bom-ref" : "pkg:maven/net.bytebuddy/byte-buddy@1.18.13?type=jar", + "bom-ref" : "pkg:maven/net.bytebuddy/byte-buddy@1.18.14?type=jar", "group" : "net.bytebuddy", "name" : "byte-buddy", - "version" : "1.18.13", + "version" : "1.18.14", "description" : "Byte Buddy is a Java library for creating Java classes at run time. This artifact is a build of Byte Buddy with all ASM dependencies repackaged into its own name space.", "hashes" : [ { "alg" : "MD5", - "content" : "cdc93784981a74d6400be220967b34d5" + "content" : "d350069f3f87e8c66fa782ff91669b33" }, { "alg" : "SHA-1", - "content" : "09ed17e54e9e2f0dfa885c7b6fb9df5fde539643" + "content" : "e26b6f5163f36a5ef91f84eea1befae54e11eb76" }, { "alg" : "SHA-256", - "content" : "aa0013fbcb7e8086f4775f3dcb69a6c6833f7fd9023fe027d2d0241834cc51d5" + "content" : "b7f2b2f111c6ddc09d3d9718ca5895dde16217bb4e5f2d2a6eb571f274909a82" }, { "alg" : "SHA-512", - "content" : "c9072bebf51c20f18334674ea5c64dd8fabfde1cbb2e9fd9d5d7fbb1d68d20ad0e271f4acf6539068838fdacf502db9ae18bd77c5a01b79484a3b7400507d083" + "content" : "252f57ee0a99f81a6795484e4712966de9f0b7229a3bf8e818cf20a8ff3de6b3745f91ad6a85421696a14e6d528f6d25fa2b0550e37500c5907be3781c6504e2" }, { "alg" : "SHA3-256", - "content" : "1639b080f87db87f7b5d7b64514488a7c88da77bd9ca6ff333d256a1837205d4" + "content" : "4143f0cefbcda015e38ceaacb07bd29f608f6bbcddac717d5ac88526b1e7fadd" }, { "alg" : "SHA3-512", - "content" : "1f22c4d2dc9ef0b48c872e8ecd5c197bbfc19713d78ae80824709a8c2fdcc26c57ae852a23d500daaa90b82c10429d9fa4d519b8cef0956ee6883907a465125a" + "content" : "1ac6fe1074144c070c49f29d413e37a460538f978530fbf642eba8d69c0f3af7d785bb65b7fd77adb85ded095fa3a512be1f9d122718fc1bf8929dbd31ecd742" }, { "alg" : "SHA-384", - "content" : "fbcfcc8f2500f866e34c59bf367430000c891356b2d91b26c01d9d855b67071dec6710bcb0b2180d887d5fddad538032" + "content" : "076deee62acf162293938fdd6772477d9017156649810239f2c910d591dd79102383a3edffa21e567a920e5900808f8a" }, { "alg" : "SHA3-384", - "content" : "b685ba29b04d969dd6925fea4ceefb3d96e3aaf11615e596c080f638aef5ed177eb0c55772b743066299dd60b78c31fc" + "content" : "e015d142598754d2209157e230ed54ce769a118881df6a3fc767ddf95070336862e84d1f4abe57d3818fcf95ce52016b" } ], "licenses" : [ @@ -5641,7 +5641,7 @@ } } ], - "purl" : "pkg:maven/net.bytebuddy/byte-buddy@1.18.13?type=jar", + "purl" : "pkg:maven/net.bytebuddy/byte-buddy@1.18.14?type=jar", "modified" : false, "properties" : [ { @@ -9459,7 +9459,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -9478,7 +9478,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -9497,7 +9497,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -9706,7 +9706,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -12663,6 +12663,140 @@ } ] }, + { + "type" : "library", + "bom-ref" : "pkg:maven/tools.maran/svg-materialdesign@1.0.0?type=jar", + "group" : "tools.maran", + "name" : "svg-materialdesign", + "version" : "1.0.0", + "description" : "Collection of all Material Design SVG paths.", + "hashes" : [ + { + "alg" : "MD5", + "content" : "66d3b5a2641b0e73a2683290432880ac" + }, + { + "alg" : "SHA-1", + "content" : "42d476ce6ce3f0d3522569a42a7abe49c38dfc2d" + }, + { + "alg" : "SHA-256", + "content" : "f8cad040fad717963903b449d006441fccaafe4a6e2bec2d7319f082721c7ae8" + }, + { + "alg" : "SHA-512", + "content" : "e64f1aa4adbd055e76b5c79fe98ed282c820059571ac9408c412999f2e364f4248852bb16425ba08f5d40552b1fab062ebb37e23aff360544616f3dfd4a4396b" + }, + { + "alg" : "SHA3-256", + "content" : "2bd363ab647e3f35efe952b536ad5522bb6420c8702c5ba89cffa0ef1bd844e8" + }, + { + "alg" : "SHA3-512", + "content" : "f76e13b1b5517d93b2b99307ab3c40245787f9548e40e71c3c0caddd39d43f12ad86cebf0d72a3cb5cb68339b35e0734c5887e09f49a895fd78335519ebdbefc" + }, + { + "alg" : "SHA-384", + "content" : "48b3d1e1164c6c74c6e728c70a8f27c71b787923ac640d9813c2d66241463cfcddeb9c46f4236e2a5d7d5ee075d2a0c3" + }, + { + "alg" : "SHA3-384", + "content" : "9d00f254490f24b7d450c42168ec07ac27cdf1fd6a1f454bad8ccfce5b54544763011799364c8998d1009cf978dd8ca0" + } + ], + "licenses" : [ + { + "license" : { + "id" : "MIT", + "url" : "https://opensource.org/license/mit/" + } + }, + { + "license" : { + "id" : "Apache-2.0", + "url" : "https://www.apache.org/licenses/LICENSE-2.0" + } + } + ], + "purl" : "pkg:maven/tools.maran/svg-materialdesign@1.0.0?type=jar", + "modified" : false, + "externalReferences" : [ + { + "type" : "vcs", + "url" : "https://github.com/Maran23/svg-materialdesign" + } + ], + "properties" : [ + { + "name" : "cdx:maven:package:test", + "value" : "false" + } + ] + }, + { + "type" : "library", + "bom-ref" : "pkg:maven/tools.maran/svg@1.0.0?type=jar", + "group" : "tools.maran", + "name" : "svg", + "version" : "1.0.0", + "description" : "SVG contract as simple interface for concrete SVG implementations.", + "hashes" : [ + { + "alg" : "MD5", + "content" : "be13859b6543eaa887462c973dc54eca" + }, + { + "alg" : "SHA-1", + "content" : "11b057da59bb843e1690cc42b0eb8af154be6032" + }, + { + "alg" : "SHA-256", + "content" : "c7d275b41125c26f9f0bdb6352b7bd31fdcdaf19adb215d0951fc944829d3d27" + }, + { + "alg" : "SHA-512", + "content" : "b50d88aa57d2e95484d537b10a0c0d1b7720f386d873e44bbab813684ccdbd3b59f03f147421c755e545953eae6b4978dc966a89cefe8e4bb7d3a2af7f174e0e" + }, + { + "alg" : "SHA3-256", + "content" : "cd110cd2670c5552c7227757e42f55b0f72891059d63ce5dc43bcf186c26255f" + }, + { + "alg" : "SHA3-512", + "content" : "ef76ad2560abc73d19a777e40e306670bb446a433eaa300282f38f4ba41e20a2055b83de54b3fe9933778bc84e1abb55a5e799080f7f03df04313f85fe9d0a0a" + }, + { + "alg" : "SHA-384", + "content" : "e78271505ce8edf3bcac4a6b9605c863040454e151487f07e0c72b721c34c1c240d4b297ad701be620aea0cae02e5cf5" + }, + { + "alg" : "SHA3-384", + "content" : "e146709438aef8ffcebe4814bfc5fa56469863202607478634f0bc1eb7adcef6a28f6e56497cd530fca4885e704d0d2e" + } + ], + "licenses" : [ + { + "license" : { + "id" : "MIT", + "url" : "https://opensource.org/license/mit/" + } + } + ], + "purl" : "pkg:maven/tools.maran/svg@1.0.0?type=jar", + "modified" : false, + "externalReferences" : [ + { + "type" : "vcs", + "url" : "https://github.com/Maran23/svg" + } + ], + "properties" : [ + { + "name" : "cdx:maven:package:test", + "value" : "false" + } + ] + }, { "type" : "library", "bom-ref" : "pkg:maven/tools.maran/svgnode@2.0.0?type=jar", @@ -12738,7 +12872,7 @@ "externalReferences" : [ { "type" : "build-system", - "url" : "https://github.com/JabRef/jabref/actions/runs/34385509426" + "url" : "https://github.com/JabRef/jabref/actions/runs/35631749322" }, { "type" : "vcs", @@ -12752,9 +12886,9 @@ "ref" : "pkg:maven/JabRef/jablib@unspecified?project_path=%3Ajablib", "dependsOn" : [ "pkg:maven/JabRef/versions@unspecified?project_path=%3Aversions", - "pkg:maven/ai.djl.huggingface/tokenizers@0.37.0?type=jar", - "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.37.0?type=jar", - "pkg:maven/ai.djl/api@0.37.0?type=jar", + "pkg:maven/ai.djl.huggingface/tokenizers@0.38.0?type=jar", + "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.38.0?type=jar", + "pkg:maven/ai.djl/api@0.38.0?type=jar", "pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.22?type=jar", "pkg:maven/com.fasterxml/aalto-xml@1.4.0?type=jar", "pkg:maven/com.github.ben-manes.caffeine/caffeine@3.2.4?type=jar", @@ -12879,7 +13013,7 @@ { "ref" : "pkg:maven/JabRef/versions@unspecified?project_path=%3Aversions", "dependsOn" : [ - "pkg:maven/ai.djl/bom@0.37.0?type=pom", + "pkg:maven/ai.djl/bom@0.38.0?type=pom", "pkg:maven/dev.langchain4j/langchain4j-bom@1.20.0?type=pom", "pkg:maven/io.zonky.test.postgres/embedded-postgres-binaries-bom@18.6.0?type=pom", "pkg:maven/org.glassfish.grizzly/grizzly-bom@5.0.2?type=pom", @@ -12889,25 +13023,25 @@ ] }, { - "ref" : "pkg:maven/ai.djl.huggingface/tokenizers@0.37.0?type=jar", + "ref" : "pkg:maven/ai.djl.huggingface/tokenizers@0.38.0?type=jar", "dependsOn" : [ - "pkg:maven/ai.djl/api@0.37.0?type=jar" + "pkg:maven/ai.djl/api@0.38.0?type=jar" ] }, { - "ref" : "pkg:maven/ai.djl.pytorch/pytorch-engine@0.37.0?type=jar", + "ref" : "pkg:maven/ai.djl.pytorch/pytorch-engine@0.38.0?type=jar", "dependsOn" : [ - "pkg:maven/ai.djl/api@0.37.0?type=jar" + "pkg:maven/ai.djl/api@0.38.0?type=jar" ] }, { - "ref" : "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.37.0?type=jar", + "ref" : "pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.38.0?type=jar", "dependsOn" : [ - "pkg:maven/ai.djl.pytorch/pytorch-engine@0.37.0?type=jar" + "pkg:maven/ai.djl.pytorch/pytorch-engine@0.38.0?type=jar" ] }, { - "ref" : "pkg:maven/ai.djl/api@0.37.0?type=jar", + "ref" : "pkg:maven/ai.djl/api@0.38.0?type=jar", "dependsOn" : [ "pkg:maven/com.google.code.gson/gson@2.13.2?type=jar", "pkg:maven/net.java.dev.jna/jna@5.19.1?type=jar", @@ -12916,7 +13050,7 @@ ] }, { - "ref" : "pkg:maven/ai.djl/bom@0.37.0?type=pom", + "ref" : "pkg:maven/ai.djl/bom@0.38.0?type=pom", "dependsOn" : [ ] }, { @@ -13488,7 +13622,7 @@ "dependsOn" : [ ] }, { - "ref" : "pkg:maven/net.bytebuddy/byte-buddy@1.18.13?type=jar", + "ref" : "pkg:maven/net.bytebuddy/byte-buddy@1.18.14?type=jar", "dependsOn" : [ ] }, { @@ -13990,6 +14124,8 @@ "pkg:maven/org.tinylog/tinylog-impl@2.8.0?type=jar", "pkg:maven/tools.jackson.core/jackson-core@3.2.2?type=jar", "pkg:maven/tools.jackson.core/jackson-databind@3.2.2?type=jar", + "pkg:maven/tools.maran/svg-materialdesign@1.0.0?type=jar", + "pkg:maven/tools.maran/svg@1.0.0?type=jar", "pkg:maven/tools.maran/svgnode@2.0.0?type=jar" ] }, @@ -14094,7 +14230,7 @@ "ref" : "pkg:maven/org.mockito/mockito-core@5.23.0?type=jar", "dependsOn" : [ "pkg:maven/net.bytebuddy/byte-buddy-agent@1.17.7?type=jar", - "pkg:maven/net.bytebuddy/byte-buddy@1.18.13?type=jar", + "pkg:maven/net.bytebuddy/byte-buddy@1.18.14?type=jar", "pkg:maven/org.objenesis/objenesis@3.3?type=jar" ] }, @@ -14249,6 +14385,16 @@ "ref" : "pkg:maven/tools.jackson/jackson-bom@3.2.2?type=pom", "dependsOn" : [ ] }, + { + "ref" : "pkg:maven/tools.maran/svg-materialdesign@1.0.0?type=jar", + "dependsOn" : [ + "pkg:maven/tools.maran/svg@1.0.0?type=jar" + ] + }, + { + "ref" : "pkg:maven/tools.maran/svg@1.0.0?type=jar", + "dependsOn" : [ ] + }, { "ref" : "pkg:maven/tools.maran/svgnode@2.0.0?type=jar", "dependsOn" : [ ] diff --git a/bom.xml b/bom.xml index 91988b8e397f..40626d684fb5 100644 --- a/bom.xml +++ b/bom.xml @@ -1,7 +1,7 @@ - + - 2026-09-09T17:54:53Z + 2026-09-21T17:27:21Z @@ -19,7 +19,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -66,7 +66,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -83,27 +83,27 @@ false - + ai.djl.huggingface tokenizers - 0.37.0 + 0.38.0 Deep Java Library (DJL) NLP utilities for Huggingface tokenizers - 1572ef240d905d6cb0b1a453abdf12ad - 332045b1db909bbfb1d387e3e13c2c04600e3e07 - 7dbdd849c3c8008f6b8909df4fc65c816a6e5a173d75f4c213e8d6e25700b0e8 - 2d13b9938ef44b08ac416e83e558263c3aa842ea95e95ed913544c293178f71dcded18fa60db7eec02a2a424dee037b9deaa7510f18a10d9e546696576be2902 - 7f4c954f0f1180d00319a94ee56bb02f9b510f75111fda87f80858123c4427a5 - b5d224d235718f44905bd17ac492244b6975d967538916887bdf15e4c98e1ca3d802e4f340ab614a64386048c86c927fd8acb9a59c66096808d2c4c6bb10cbbd - a4ed22c7953b101b5e0c52d7d27feae83dfd9dd77b016a984e3265f762ca053d5fd76c905656071b6030acf99e1e5286 - 05cfb7eec5287a020958a5ec90cec3ebc2e393383c4e71e4f1aeec4dde1cf3e63403f7d561f8e4f6f15e55a6071d8f57 + 4015d9a531c0b0e6a035b64122ab1b0a + 875d8c48f05165a17498215747974cde2de728bd + aab48c4db87ffb724d6037940aaba1d0b3d3b1de45156d7d9efc818f92f7a521 + faee99ec90854f8e29539d535005ccc99a4409d9a666a3ba285766c08c93301051984b563945c6c8757a5197c02556b8c2f24edb0b78d4d73991458417efdfa6 + d1efc883dd5f50e45582105cec280489c6d96ddd042c63ff246c3f79fe23ef82 + 19e382ea4da3f7d645563d144e72185ccc849c7eeee607652a091833f07b4178b7961d8d28f70d4aba27aea3e375f74e882fd9e995d0636b58b93489a8152ac2 + 5d5f4174a22e2f6c6ae7240d730f1148aaae76e45cf71e6710789c4e5a3dd54a4a5cad653fe7bc3602086af91fb27660 + f64ab3c8bcbbae11d88d4bf06a7bf46601b73de94a309811abd768348597888a5b04a79b366a0c91e928d52fbd366061 Apache-2.0 - pkg:maven/ai.djl.huggingface/tokenizers@0.37.0?type=jar + pkg:maven/ai.djl.huggingface/tokenizers@0.38.0?type=jar false @@ -114,27 +114,27 @@ false - + ai.djl.pytorch pytorch-engine - 0.37.0 + 0.38.0 Deep Java Library (DJL) Engine Adapter for PyTorch - 23b1d1f85c45eada96f707a41d7b7e36 - 8361ec786df8d04e6229510788478982c154cd8b - e4ae439c6f7b80acacbb6fc95d4686fbda705b1155c9c5bce678d946ddc69ac2 - 935875bff7ff5082560f7f97c5a4efac8d34ede4ef1a7ef1feae2dad09c4c539dbbf8fbc9de8ff05c27d7b1374415e6d73fef3f90679eab7df0995e665381c70 - 479268d8054ec8a5436b28bb5439b0fcf298827a09bedc618b5581460e1f7605 - a21ff75a23f51a84c3905a4a7658e6e62225937a65291c6328e6484d747de2fabee5142fae23d98b873b78c27793eca3f976640c832f08312a7a5bd43d4bba7d - c90378f0b23072a13f8659543bd8b362c8e9b6522dc9220f22d71d964a9333c5fdd965f384288da523d8dd55ba24edac - 49aa2cab567af598df8982bd36ee6449d1eac84662ece5d8344d0a3d30eaad1020797102bb26bb0d96ab1d06db1af9ac + 01e3676e5c806d315ab5d226bb5822a9 + f05d74386d0fa925cba579595374b90ba40171a0 + 327801b3b90e1b0179cd7266eeb1b484834f7090c4d5003a8331b944bf8aac77 + 509d94afadc86db35fd4b330582dfcc7935016b8a171362a81dfa6a21c311749c5f6725114d8a9ce73c8d1f5c8c27614ff088dff42cbcac3cc8f5998026ee48b + e53e194fc912afcd18dcea3d399f585133124b31b3f43c9e411b6b1fad200864 + ada85253dc76ea7461d68ce4dc5486349165e97137fe72b4d1a940e9f62da1e959bcff3bb356da1fe6d4f17d77ca59c489ff4339c96cd1e0a2c797940855eae5 + e18f7c0c38bac8dd443d9da8c57a9e59a43fa4dad9bbefba4df66f661a5a62766c34047a3121d5ffb28a793b81497a9d + a4b6943e1c2517efb6777b30efa94948c07e5f4eb4b0e552fdb41366d0ff2c8f8f13bcc828a2c2bc088597f49f80a1b7 Apache-2.0 - pkg:maven/ai.djl.pytorch/pytorch-engine@0.37.0?type=jar + pkg:maven/ai.djl.pytorch/pytorch-engine@0.38.0?type=jar false @@ -145,27 +145,27 @@ false - + ai.djl.pytorch pytorch-model-zoo - 0.37.0 + 0.38.0 Deep Java Library (DJL) model zoo for PyTorch - 9561ed397b19df88c89bb7c6e4f73794 - b2fb396415e68bc8cc8c1e1301e558d866f09976 - 06656be87884d423ed6e548d6f76fead42ec1f9ad9156f5300cff48ddce9a030 - 95be83d1eb3a8f1f123d07620e9b14563d02936dc4ac953cef1e90005f3cac8a5fd136d2ee03424c1bf98c174cae69ebb3275b32aac5db0a98b61a55562d9f8a - d85d4a4f6793b412e02238f78813b0766d903e46dbbe774f7dfba4c49b77ba10 - 63de21931bd1a6c74f877d8f69b0457216a235feafac2a415506bcc52d9eda8ce37daf20744942b8ff96d37d9f2091edd5039707620b38e33c45d001a5924e58 - ca48250cd554fc9232e10b0c490d24576f08e7c94b69b8e45948c61e19aea9312d8292924bad6f54e9b9d7a91dbef059 - d61884d49b4cd460bb5ff6698418ac3753418162366b9c630510033000c71364e7a60c6fde9f94be308e938a46c38e62 + 98b1599e2dfb56e4f05ca8cbb73b621c + 434a6ddd6991f89cca8f34bc4b786bf8b5a90008 + 315ad71dab4738ddd122f6b723d38f7c5fc00e93d0dc9baa445d83ab0f3a592c + 4ff26e98d3e4df249586e06a647aa8c35bd2a6b8d32762685e18763237e48aee403c374d52fa088d447b47d643310a243ea6817b572066ffda6fd8b7e335fac4 + 308b7d63677ae51fbcb0fe8d9d7de4d30a2c5c8d968f779f6724705b57b4f9f5 + 38bdbc12c78853d397e550a8732fe8cdb7d6de8185f6ca6bc5226ad2de0e30d065d5c31f40233735988c82604e85a8b1e02c97c412227f44a9b4e84effde1d1b + 299a9ba0adfcc064ea2dde199ded401972789b65ae39e667e48dee2e10e7206ac1479b0a0f15b9617b4de61a1bdfe440 + 4bb1032f08c6764c66441f015f204ab88ebca17bc28756d44508443b5bf6b7f15156452dc6d02f9c18146a0eff4dd0e3 Apache-2.0 - pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.37.0?type=jar + pkg:maven/ai.djl.pytorch/pytorch-model-zoo@0.38.0?type=jar false @@ -176,27 +176,27 @@ false - + ai.djl api - 0.37.0 + 0.38.0 Deep Java Library - api - e9594d8bc8256a7cd54cdac82c7aeabf - b0f26eef62e2a7e0dfd39f1ad64611587e19c802 - 3b57eb8ff74abb38785b2cecb0bfea328e0827312c9945911e217ec33b15b025 - b58548cd15ca4ee34017233f766c457cb2851d18c102c331bc59a0cc9c40b143583ffcf9a84edba7a968f85f6b1ddca631c01218089a31ff0803c5482832f7f3 - 7c3614cb5bbd4ef0036c8aff2a7e7a707cfef79ca4b30d7c53fcdb01c01cc792 - 43db5d2b07f838b25e43599e7250a3ca76e2ce771e2d5ca3dc9bb210661b574b37b42c83773373462c9680dd46974ee3e0cbc3ad626512b3821c86dcada59980 - 75c6e598ffee0c97f9a0e6b4369c08ecc80cb9b906b299063e31f5c54268bc62daac38d059e2d666b21827c8b1efcc99 - ffe6b6beb579efbc1a91b584941f7b4f28f198dd09378d0bff63671e507915fdc809d5bb1b1da52d8aabc453c3bab10f + 0864990898d41fb8db92a1a5f6d6a33e + 2bc351e12a0d2da3cdf74a775ee5f170c8df38b9 + 5a99d57d675e68febd471ca814a81180dc2be7a05d66a1b6b0eb676416a9d6bc + 7a0b4275104bbccaaebca7a99b4df6ec1abc8369b91b04629f85b89609faf789faa7877432a9554c96b545eb8e1fa8893b093dbd1bc6ffc01dc7bf1e72d8b72b + 1d1d55a89c3cbb99cff85c5771fab3d0eebc56009745f1e6670c4cb76d2152ab + a675cd49f22dc206b4a7d9a94ec2522ffde0ade1c3dfb3c9f1b9177d6e52280532d73bd6e3f7e399fbd3fd3e8864c635d014e35ee3d2853caddb847daefb0d27 + a4ebc6b9e2f18ac5ef3d0cd0f6b397864785e284728c40dad163415a14b81b62b931828e6186fb8e27228baac693beef + 1265459f00961795154894acb0e372494be6edfb8fcac80398ac63898efd4494e5fa4504e1b53a29ac847c1df5f40ea0 Apache-2.0 - pkg:maven/ai.djl/api@0.37.0?type=jar + pkg:maven/ai.djl/api@0.38.0?type=jar false @@ -207,17 +207,17 @@ false - + ai.djl bom - 0.37.0 + 0.38.0 Deep Java Library (DJL) Bill of Materials (BOM) Apache-2.0 - pkg:maven/ai.djl/bom@0.37.0?type=pom + pkg:maven/ai.djl/bom@0.38.0?type=pom false @@ -1987,14 +1987,14 @@ 1.20.0 Core classes and interfaces of LangChain4j - e30ed842c0ce6260835a755295a9c87b - a7db774c56be114f9aef5df44404d8047ef6bec4 - c884e22f5a40abbb4ab3dfc2dd9e6e69ff89e04d9f9295d6ec7085b6301e0245 - 6b2967e2209e1563b4ab991b984509cc2eabdb4c998b76efcb51024db0c3d227f41bf3ea8e392e05ef9373f0cbb8498a5933376983c3391ad4ce3835960484c2 - bcd707a20e9180fe91f1fcfeedf14485e41c7455007062fb77ca6b317a95bd15 - c47225b7e9f58fa44219fecbbd750dec53e724b0dd48c9e84278d0a7ff2d6aae38029e89e9b01a9bbd3c9f55826b7dcd6ac02c81bab2142285f0060dd4fd6843 - d88b8b48d70209fa3c6af78aa2c08e1d585939429413afcbf66dcb1f080371711063bd263278eadeafc98eefae9b70c4 - dc50137595b490b704d5ec3dd70ae25d9bc4afa55223b9722c246081c91044af938b449200aa939399dffdf2435eb22a + 3be16ac8db9a8ea8dae22e8cfb524f95 + 1156cfae07df2f43d0f806736d3fc0c3abc43fa8 + e5c385f4e61f8e757078afceb811b1194e3d7444ab11b5691885300212d01038 + 73dac33123beb5028c6564c6742c1ce507f73503a0d6763be0a20a37b09da02fcb34515173862852b7c7cc626cf00a97f4e54c2fa404b286472daca60b48d9a8 + 2252fd9e303b026133cc7d43818d1cc0927629670709bf9f1a00a12a109f2a92 + a3f8d803cfca9404320c11f7e0be6cb8e2732cd982270f3f5df75dcfc4ca857639cd13d09c2059dced109694f7382db1ec9cc39dbc3d027fa05b6a54ea9b5b05 + a9fc45b6508ab04a0555a119959554d39021c0a7b3fd82b3dde0eb97867ebbb7f4304e6c20b8fe0fcc9b7c0f522a8c0a + c489f48aade8e6900cb850dc899a54b96833432480c8e61429bd590e02cf2820fe59efd963044072057c0b7f2827a87d @@ -2754,27 +2754,27 @@ false - + net.bytebuddy byte-buddy - 1.18.13 + 1.18.14 Byte Buddy is a Java library for creating Java classes at run time. This artifact is a build of Byte Buddy with all ASM dependencies repackaged into its own name space. - cdc93784981a74d6400be220967b34d5 - 09ed17e54e9e2f0dfa885c7b6fb9df5fde539643 - aa0013fbcb7e8086f4775f3dcb69a6c6833f7fd9023fe027d2d0241834cc51d5 - c9072bebf51c20f18334674ea5c64dd8fabfde1cbb2e9fd9d5d7fbb1d68d20ad0e271f4acf6539068838fdacf502db9ae18bd77c5a01b79484a3b7400507d083 - 1639b080f87db87f7b5d7b64514488a7c88da77bd9ca6ff333d256a1837205d4 - 1f22c4d2dc9ef0b48c872e8ecd5c197bbfc19713d78ae80824709a8c2fdcc26c57ae852a23d500daaa90b82c10429d9fa4d519b8cef0956ee6883907a465125a - fbcfcc8f2500f866e34c59bf367430000c891356b2d91b26c01d9d855b67071dec6710bcb0b2180d887d5fddad538032 - b685ba29b04d969dd6925fea4ceefb3d96e3aaf11615e596c080f638aef5ed177eb0c55772b743066299dd60b78c31fc + d350069f3f87e8c66fa782ff91669b33 + e26b6f5163f36a5ef91f84eea1befae54e11eb76 + b7f2b2f111c6ddc09d3d9718ca5895dde16217bb4e5f2d2a6eb571f274909a82 + 252f57ee0a99f81a6795484e4712966de9f0b7229a3bf8e818cf20a8ff3de6b3745f91ad6a85421696a14e6d528f6d25fa2b0550e37500c5907be3781c6504e2 + 4143f0cefbcda015e38ceaacb07bd29f608f6bbcddac717d5ac88526b1e7fadd + 1ac6fe1074144c070c49f29d413e37a460538f978530fbf642eba8d69c0f3af7d785bb65b7fd77adb85ded095fa3a512be1f9d122718fc1bf8929dbd31ecd742 + 076deee62acf162293938fdd6772477d9017156649810239f2c910d591dd79102383a3edffa21e567a920e5900808f8a + e015d142598754d2209157e230ed54ce769a118881df6a3fc767ddf95070336862e84d1f4abe57d3818fcf95ce52016b Apache-2.0 - pkg:maven/net.bytebuddy/byte-buddy@1.18.13?type=jar + pkg:maven/net.bytebuddy/byte-buddy@1.18.14?type=jar false false @@ -4707,7 +4707,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -4722,7 +4722,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -4737,7 +4737,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -4846,7 +4846,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -6346,6 +6346,74 @@ false + + tools.maran + svg-materialdesign + 1.0.0 + Collection of all Material Design SVG paths. + + 66d3b5a2641b0e73a2683290432880ac + 42d476ce6ce3f0d3522569a42a7abe49c38dfc2d + f8cad040fad717963903b449d006441fccaafe4a6e2bec2d7319f082721c7ae8 + e64f1aa4adbd055e76b5c79fe98ed282c820059571ac9408c412999f2e364f4248852bb16425ba08f5d40552b1fab062ebb37e23aff360544616f3dfd4a4396b + 2bd363ab647e3f35efe952b536ad5522bb6420c8702c5ba89cffa0ef1bd844e8 + f76e13b1b5517d93b2b99307ab3c40245787f9548e40e71c3c0caddd39d43f12ad86cebf0d72a3cb5cb68339b35e0734c5887e09f49a895fd78335519ebdbefc + 48b3d1e1164c6c74c6e728c70a8f27c71b787923ac640d9813c2d66241463cfcddeb9c46f4236e2a5d7d5ee075d2a0c3 + 9d00f254490f24b7d450c42168ec07ac27cdf1fd6a1f454bad8ccfce5b54544763011799364c8998d1009cf978dd8ca0 + + + + MIT + https://opensource.org/license/mit/ + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + pkg:maven/tools.maran/svg-materialdesign@1.0.0?type=jar + false + + + https://github.com/Maran23/svg-materialdesign + + + + false + + + + tools.maran + svg + 1.0.0 + SVG contract as simple interface for concrete SVG implementations. + + be13859b6543eaa887462c973dc54eca + 11b057da59bb843e1690cc42b0eb8af154be6032 + c7d275b41125c26f9f0bdb6352b7bd31fdcdaf19adb215d0951fc944829d3d27 + b50d88aa57d2e95484d537b10a0c0d1b7720f386d873e44bbab813684ccdbd3b59f03f147421c755e545953eae6b4978dc966a89cefe8e4bb7d3a2af7f174e0e + cd110cd2670c5552c7227757e42f55b0f72891059d63ce5dc43bcf186c26255f + ef76ad2560abc73d19a777e40e306670bb446a433eaa300282f38f4ba41e20a2055b83de54b3fe9933778bc84e1abb55a5e799080f7f03df04313f85fe9d0a0a + e78271505ce8edf3bcac4a6b9605c863040454e151487f07e0c72b721c34c1c240d4b297ad701be620aea0cae02e5cf5 + e146709438aef8ffcebe4814bfc5fa56469863202607478634f0bc1eb7adcef6a28f6e56497cd530fca4885e704d0d2e + + + + MIT + https://opensource.org/license/mit/ + + + pkg:maven/tools.maran/svg@1.0.0?type=jar + false + + + https://github.com/Maran23/svg + + + + false + + tools.maran svgnode @@ -6385,7 +6453,7 @@ false - https://github.com/JabRef/jabref/actions/runs/34385509426 + https://github.com/JabRef/jabref/actions/runs/35631749322 https://github.com/JabRef/jabref @@ -6396,9 +6464,9 @@ - - - + + + @@ -6511,7 +6579,7 @@ - + @@ -6519,22 +6587,22 @@ - - + + - - + + - - + + - + - + @@ -6845,7 +6913,7 @@ - + @@ -7151,6 +7219,8 @@ + + @@ -7195,7 +7265,7 @@ - + @@ -7268,6 +7338,10 @@ + + + + From 3992b122d7f1c58471b2e1a425b190514d3f7df8 Mon Sep 17 00:00:00 2001 From: Siedlerchr Date: Mon, 21 Sep 2026 20:59:04 +0200 Subject: [PATCH 036/102] Release v6.0-beta.1 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f0b8868d37..eeefd0046d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ In case there is no issue present, the pull request implementing the feature is Note that this project **does not** adhere to [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [6.0-beta.1] - 2026-09-21 ### Added @@ -2310,7 +2310,7 @@ The changelog of JabRef 4.x is available at the [v4.3.1 tag](https://github.com/ The changelog of JabRef 3.x is available at the [v3.8.2 tag](https://github.com/JabRef/jabref/blob/v3.8.2/CHANGELOG.md). The changelog of JabRef 2.11 and all previous versions is available as [text file in the v2.11.1 tag](https://github.com/JabRef/jabref/blob/v2.11.1/CHANGELOG). -[Unreleased]: https://github.com/JabRef/jabref/compare/v6.0-alpha.6...HEAD +[6.0-beta.1]: https://github.com/JabRef/jabref/compare/v6.0-alpha.6...v6.0-beta.1 [6.0-alpha.6]: https://github.com/JabRef/jabref/compare/v6.0-alpha.5...v6.0-alpha.6 [6.0-alpha.5]: https://github.com/JabRef/jabref/compare/v6.0-alpha.4...v6.0-alpha.5 [6.0-alpha.4]: https://github.com/JabRef/jabref/compare/v6.0-alpha.3...v6.0-alpha.4 From 0c49dfd44b0023166e442a557e706d34ce0e64a9 Mon Sep 17 00:00:00 2001 From: Siedlerchr Date: Mon, 21 Sep 2026 22:50:06 +0200 Subject: [PATCH 037/102] Start new development cycle +semver: minor --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/gource.yml | 6 +++--- CHANGELOG.md | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bcf0c9067d01..2e999c63c35d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,7 +14,7 @@ body: label: JabRef version options: - "5.15 (latest release)" - - "6.0-alpha.6" + - "6.0-beta.1" - Latest development branch build (please note build date below) - Other (please describe below) description: The version as shown in the about dialog. diff --git a/.github/workflows/gource.yml b/.github/workflows/gource.yml index 2c3b678f59c8..ab2774fd63bb 100644 --- a/.github/workflows/gource.yml +++ b/.github/workflows/gource.yml @@ -21,9 +21,9 @@ jobs: fail-fast: false matrix: include: - - title: 'JabRef v6.0 (since alpha.6) | more information at contribute.jabref.org' - # Last alpha release 2026-02-20, but want to have some seconds before - start: '2026-02-14' + - title: 'JabRef v6.0 (since beta.1) | more information at contribute.jabref.org' + # Last beta release 2026-09-21, but want to have some seconds before + start: '2026-09-14' end: '' file: 'jabref-v6.0-dev.mp4' # - title: 'JabRef v6.0-alpha2 | more information at contribute.jabref.org' diff --git a/CHANGELOG.md b/CHANGELOG.md index eeefd0046d49..da402b6ba247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ In case there is no issue present, the pull request implementing the feature is Note that this project **does not** adhere to [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +### Changed + +### Fixed + +### Removed + ## [6.0-beta.1] - 2026-09-21 ### Added @@ -2310,6 +2320,7 @@ The changelog of JabRef 4.x is available at the [v4.3.1 tag](https://github.com/ The changelog of JabRef 3.x is available at the [v3.8.2 tag](https://github.com/JabRef/jabref/blob/v3.8.2/CHANGELOG.md). The changelog of JabRef 2.11 and all previous versions is available as [text file in the v2.11.1 tag](https://github.com/JabRef/jabref/blob/v2.11.1/CHANGELOG). +[Unreleased]: https://github.com/JabRef/jabref/compare/v6.0-beta.1...HEAD [6.0-beta.1]: https://github.com/JabRef/jabref/compare/v6.0-alpha.6...v6.0-beta.1 [6.0-alpha.6]: https://github.com/JabRef/jabref/compare/v6.0-alpha.5...v6.0-alpha.6 [6.0-alpha.5]: https://github.com/JabRef/jabref/compare/v6.0-alpha.4...v6.0-alpha.5 From aebdd7038c7787ebbf967cc22e85b7cba5ff9a1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:07:31 +0000 Subject: [PATCH 038/102] Chore(deps): Bump org.hisp.dhis:json-tree in /versions (#17227) Bumps [org.hisp.dhis:json-tree](https://github.com/dhis2/json-tree) from 1.9.4 to 1.9.5. - [Release notes](https://github.com/dhis2/json-tree/releases) - [Changelog](https://github.com/dhis2/json-tree/blob/main/CHANGELOG.md) - [Commits](https://github.com/dhis2/json-tree/commits) --- updated-dependencies: - dependency-name: org.hisp.dhis:json-tree dependency-version: 1.9.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 5eb647f95b33..f0c850671a51 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -136,7 +136,7 @@ dependencies.constraints { api("org.glassfish.jaxb:jaxb-runtime:4.0.9") api("org.hamcrest:hamcrest:3.0") api("org.hibernate.validator:hibernate-validator:9.1.3.Final") - api("org.hisp.dhis:json-tree:1.9.4") + api("org.hisp.dhis:json-tree:1.9.5") api("org.jabref:afterburner.fx:2.0.0") api("org.jabref:easybind:2.3.0") api("org.jabref:html-to-node:0.2.0") From 1c70a9aaa4d831766a4858c76b042ffb0844916e Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 06:43:26 +0200 Subject: [PATCH 039/102] Link PR and pushed commits in the merge-main run summary (#17233) Co-authored-by: Claude Opus 5 --- .github/workflows/merge-main.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/merge-main.yml b/.github/workflows/merge-main.yml index f648818494b9..78523c587a53 100644 --- a/.github/workflows/merge-main.yml +++ b/.github/workflows/merge-main.yml @@ -59,6 +59,10 @@ jobs: ref: ${{ steps.resolve.outputs.branch }} token: ${{ secrets.GH_TOKEN_JABREF_MACHINE_PR_APPROVE || github.token }} + - name: Remember head before merge + id: before + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + # Pushes the merge commit back to the PR branch (the reason this workflow exists). # "$/" resolves the action from this workflow's own commit, not from the workspace - # the workspace holds the PR head (possibly a fork), whose copy of the action must @@ -66,7 +70,32 @@ jobs: # The PR may live in a fork ("repository" above); the base branch is always taken from # JabRef/jabref. For a stacked PR, the base is the next lower PR's head branch, not main. - name: Merge ${{ steps.resolve.outputs.base }} + id: merge uses: $/.github/actions/merge-upstream-branch with: target-branch: ${{ steps.resolve.outputs.branch }} base-branch: ${{ steps.resolve.outputs.base }} + + - name: Summary + if: always() + env: + REPO: ${{ steps.resolve.outputs.repo }} + PR_NUMBER: ${{ inputs.pr || github.event.pull_request.number }} + BEFORE: ${{ steps.before.outputs.sha }} + MERGED: ${{ steps.merge.outcome == 'success' }} + run: | + { + echo "PR: https://github.com/${{ github.repository }}/pull/$PR_NUMBER" + echo + # Only pushed commits are linked; a failed merge or push leaves nothing to link. + commits="" + if [ "$MERGED" = "true" ]; then + commits=$(git log --first-parent --format=%H "$BEFORE..HEAD") + fi + if [ -z "$commits" ]; then + echo "No commit pushed." + else + echo "Commits:" + for c in $commits; do echo "- https://github.com/$REPO/commit/$c"; done + fi + } >> "$GITHUB_STEP_SUMMARY" From b7536d36791271aa315be3fc1d0d031e2f8b556d Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 04:45:40 +0000 Subject: [PATCH 040/102] Keep library close working when cleanup throws an Error (#16810) * Keep library close working when cleanup throws an Error The cleanup steps in LibraryTab#onClosed only guarded against RuntimeException, so an Error escaped into the JavaFX event handler, aborted the close and popped the uncaught-exception dialog again on every further attempt to quit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WCr1LbJXj8TdVSPSJSpeJz * Use TODO placeholder for changelog link * Link changelog entry to PR Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WCr1LbJXj8TdVSPSJSpeJz * Catch LinkageError instead of Throwable during library cleanup Fatal errors (VirtualMachineError, ThreadDeath) must not be swallowed; the detached-classpath failure this guards against is a LinkageError. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WCr1LbJXj8TdVSPSJSpeJz * Drop CHANGELOG.md changes from this PR Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WCr1LbJXj8TdVSPSJSpeJz * Catch Throwable during library cleanup Closing a library must always succeed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WCr1LbJXj8TdVSPSJSpeJz --------- Co-authored-by: Claude Opus 5 Co-authored-by: github-actions[bot] --- jabgui/src/main/java/org/jabref/gui/LibraryTab.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java index 10901698ec59..62cff73d0cdf 100644 --- a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java +++ b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java @@ -914,6 +914,10 @@ private void onCloseRequest(Event event) { } /// Perform necessary cleanup when this Library is closed. + /// + /// Cleanup steps catch [Throwable]: anything escaping (e.g., a [NoClassDefFoundError] when the classpath + /// has vanished under a running JVM) aborts the tab close, leaving JabRef unclosable behind a recurring + /// uncaught-exception dialog. Closing must always succeed, so even fatal errors are only logged here. private void onClosed(Event event) { if (dataLoadingTask != null) { dataLoadingTask.cancel(); @@ -923,14 +927,14 @@ private void onClosed(Event event) { } try { changeMonitor.ifPresent(DatabaseChangeMonitor::unregister); - } catch (RuntimeException e) { + } catch (Throwable e) { LOGGER.error("Problem when closing change monitor", e); } try { if (searchContext != null) { searchContext.close(); } - } catch (RuntimeException e) { + } catch (Throwable e) { LOGGER.error("Problem when closing search context", e); } @@ -938,14 +942,14 @@ private void onClosed(Event event) { try { AutosaveManager.shutdown(bibDatabaseContext); - } catch (RuntimeException e) { + } catch (Throwable e) { LOGGER.error("Problem when shutting down autosave manager", e); } try { BackupManager.shutdown(bibDatabaseContext, preferences.getFilePreferences().getBackupDirectory(), preferences.getFilePreferences().shouldCreateBackup()); - } catch (RuntimeException e) { + } catch (Throwable e) { LOGGER.error("Problem when shutting down backup manager", e); } From f0af556b84724c5c149aa7f91338ae8d32c3d0b3 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 07:03:40 +0200 Subject: [PATCH 041/102] Run the fixed changelog merge driver (#17234) Rebasing re-added stale unreleased entries and dropped empty section headings after a release. Co-authored-by: Claude Opus 5 --- .github/actions/merge-upstream-branch/action.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/actions/merge-upstream-branch/action.yml b/.github/actions/merge-upstream-branch/action.yml index 2c8d6677ba22..a1084fc566a6 100644 --- a/.github/actions/merge-upstream-branch/action.yml +++ b/.github/actions/merge-upstream-branch/action.yml @@ -10,9 +10,9 @@ inputs: required: false default: "main" driver-revision: - description: "Commit of maven-flow/changelog-merge-driver to run (immutable pin; bump explicitly to pick up driver fixes)" + description: "Commit of changelog-merge-driver to run (immutable pin; bump explicitly to pick up driver fixes)" required: false - default: "4f9446a29a9e6b935b0fd710aa047f9ff03641ad" + default: "efd607a74d7dc52038e24d4a8994e3c27d6e6818" runs: using: "composite" @@ -34,6 +34,8 @@ runs: path: ~/.jbang key: jbang-changelog-merge-driver-${{ inputs.driver-revision }} + # Runs the fix of https://github.com/maven-flow/changelog-merge-driver/pull/14 from its fork; switch back + # to maven-flow once that is merged. - name: Merge upstream/${{ inputs.base-branch }} shell: bash env: @@ -45,10 +47,10 @@ runs: # The PR head may live in a fork; the base branch always comes from JabRef/jabref git remote add upstream "https://github.com/JabRef/jabref.git" || true git fetch upstream "$BASE_BRANCH" - jbang trust add https://github.com/maven-flow/changelog-merge-driver/ + jbang trust add https://github.com/koppor/changelog-merge-driver/ # Pinned to an immutable commit - never the repo's mutable HEAD/alias - since the # driver runs with a push-capable token in this job. - git config merge.changelog.driver "jbang https://github.com/maven-flow/changelog-merge-driver/blob/$DRIVER_REVISION/ChangelogMergeDriver.java %A %O %B --rebase" + git config merge.changelog.driver "jbang https://github.com/koppor/changelog-merge-driver/blob/$DRIVER_REVISION/ChangelogMergeDriver.java %A %O %B --rebase" # Overrides the checked-in "CHANGELOG.md merge=union" without dirtying the working tree echo "CHANGELOG.md merge=changelog" >> "$(git rev-parse --git-dir)/info/attributes" git config user.email "github-actions[bot]@users.noreply.github.com" From c4b085825f14efaeb53c231f8ea86e94b1a01374 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 05:02:10 +0000 Subject: [PATCH 042/102] Fix run-branch recipe name typo in justfile (#17115) Claude-Session: https://claude.ai/code/session_01FRGLfJrbbmoByCo3Srfsgb Co-authored-by: Claude Opus 5 --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index ba61a659293b..26497af3628a 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ checkout-pr pr-id: ensure-gg-cmd if command -v gh >/dev/null 2>&1; then gh pr checkout {{pr-id}}; else sh ./gg.cmd jbang https://github.com/JabRef/jabref/blob/main/.jbang/CheckoutPR.java {{pr-id}}; fi [unix] -run-brach branch: ensure-gg-cmd +run-branch branch: ensure-gg-cmd sh ./gg.cmd jbang git@jbangdev checkout {{branch}} sh ./gg.cmd jbang git@jbangdev fetch origin sh ./gg.cmd jbang git@jbangdev merge origin/main From 6679b4df7d92469dff67a2e8f1449ee2055dc47d Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 05:38:31 +0000 Subject: [PATCH 043/102] Add jabkit git merge-driver for semantic .bib merges (#16838) * Add jabkit git merge-driver for semantic .bib merges Exposes JabRef's three-way semantic merge (SemanticMergeAnalyzer + GitMergeApplier) as a Git merge driver: `jabkit git merge-driver BASE CURRENT OTHER` writes current + (other - base) into CURRENT, exits 0 on a clean merge and 1 when semantic conflicts remain. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UpFy4JVhKFAFugkbTemRoA * Link PR in CHANGELOG Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UpFy4JVhKFAFugkbTemRoA * Reject merges that would silently drop content The semantic merge plan only covers entries with a citation key, so entry type changes, @String definitions, preamble, epilogue, metadata and keyless entries from OTHER never reached CURRENT while the driver reported success. Type changes are now applied; the rest makes the driver report a conflict. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Merge entry types three-way, compare persisted metadata Entry types were only propagated when CURRENT kept BASE's type, so divergent type changes, a type change against a deletion, and additions of the same key with different types passed as clean merges. They are now reported as conflicts. Metadata is compared in its serialized form, since MetaData.equals ignores unknown metadata items. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Keep merging around conflicting entries, fix dependency scope Entries with a type conflict are removed from the merge plan instead of discarding it, so safe changes of the other entries still reach CURRENT. GitFileWriter aborts the atomic write when the encoding cannot represent the content, which otherwise replaced the file with lossy content. jabkit requires org.jspecify directly, and the merge-driver smoke test now checks that the driver stays silent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Register EventBus subscriber for the native image Applying the merge plan sets fields on entries that belong to a database, which dispatches a FieldChangedEvent to BibDatabase via Guava's EventBus. Its subscriber is invoked reflectively and thus failed in the native image. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Register KeyChangeListener subscribers for the native image Same reflective EventBus dispatch as the previous commit: applying the merge plan reaches the listener that keeps cross-references in sync. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Move the merge driver setup to the user documentation The developer documentation keeps the contract and the limits of the driver and links to the user documentation for the Git configuration. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Preserve custom entry types, refuse unparseable input Rewriting CURRENT dropped its custom entry type definitions, because the writer got a fresh BibEntryTypesManager; it now gets the types parsed from CURRENT. A file the parser reported a warning for (a duplicate @String name, for instance) cannot be written back without loss and is refused, and the mergeability check covers custom entry types and the comments of @Strings. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Link the section instead of the not-yet-published page The user documentation page is still in review, so its deep link 404s. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Escape the Git placeholders in the merge-driver help picocli formats the help text, so %O %A %B made it warn about an unknown conversion instead of printing the setup command. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Merge entry comments, guard the file prolog The comment written above an entry took part in neither the merge plan nor the conflict detection, so a comment changed in OTHER alone was dropped while the merge reported success; it is now merged like the entry type. The shared database ID and the encoding are compared as well, and a comment in front of an @Comment block makes the driver refuse the merge, because JabRef's parser drops it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Harden the comment handling of the merge driver A deletion on one side now conflicts with a comment change on the other, the guard for comments an @Comment block swallows reads the file in its own encoding instead of assuming UTF-8, and it matches the entry type the way the parser does: case-insensitively, indentation allowed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Assert the conflict output, drop restating comments The conflict tests compare the complete stderr output instead of looking for a citation key somewhere in it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Trace the merge driver as a feature Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Return the clean merge at the end of the driver Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Refuse custom entry types no entry uses The writer emits the definition of a custom entry type only for types an entry uses, so a standalone definition disappeared on rewrite. The aborted write on an encoding problem gets a regression test. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014xQwRqLx2R27m7xFYPfAdK * Move the merge logic of the Git merge driver into jablib The jabkit command now only maps Git's driver contract to exit codes and messages. BibFileMerger does the work and reports a sealed MergeOutcome, MergePreconditions names every reason a merge is refused, and EntryPropertyMerge applies the three-way rules to entry type and comment. Refuses two more lossy cases: an empty entry and a comment in front of @Preamble. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01G5wUZVmQHA3aUjgNPTNaTG * Keep only the implementation view in the code-howto The user-visible merge behavior moves to the user documentation. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01G5wUZVmQHA3aUjgNPTNaTG * Declare jspecify as compile-time-only module dependency of jabkit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01G5wUZVmQHA3aUjgNPTNaTG --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: github-actions[bot] Co-authored-by: Wanling <164749591+wanling0000@users.noreply.github.com> --- .jbang/JabKitLauncher.java | 2 + CHANGELOG.md | 2 + docs/code-howtos/git.md | 9 + docs/requirements/git.md | 9 + jabkit/src/main/java/module-info.java | 2 +- .../java/org/jabref/toolkit/commands/Git.java | 28 ++ .../toolkit/commands/GitMergeDriver.java | 128 +++++++ .../org/jabref/toolkit/commands/JabKit.java | 1 + .../toolkit/commands/GitMergeDriverTest.java | 83 ++++ jabkit/src/test/nativeimage/jabkit-offline.md | 6 + .../jabref/toolkit/commands/merge-base.bib | 11 + .../jabref/toolkit/commands/merge-current.bib | 12 + .../toolkit/commands/merge-duplicate-keys.bib | 11 + .../toolkit/commands/merge-other-conflict.bib | 12 + .../jabref/toolkit/commands/merge-other.bib | 18 + jablib/src/main/java/module-info.java | 1 + .../git/conflicts/ThreeWayEntryConflict.java | 12 + .../jabref/logic/git/io/GitFileWriter.java | 11 +- .../jabref/logic/git/merge/BibFileMerger.java | 91 +++++ .../logic/git/merge/EntryPropertyMerge.java | 95 +++++ .../jabref/logic/git/merge/MergeOutcome.java | 21 + .../logic/git/merge/MergePreconditions.java | 101 +++++ .../logic/git/merge/NonEntryContent.java | 54 +++ .../org/jabref/logic/git/merge/Refusal.java | 25 ++ .../org/jabref/logic/git/model/MergePlan.java | 12 + .../jablib/reachability-metadata.json | 26 +- .../main/resources/l10n/JabRef_en.properties | 15 + .../logic/git/merge/BibFileMergerTest.java | 361 ++++++++++++++++++ .../logic/git/util/GitFileWriterTest.java | 21 + 29 files changed, 1175 insertions(+), 5 deletions(-) create mode 100644 jabkit/src/main/java/org/jabref/toolkit/commands/Git.java create mode 100644 jabkit/src/main/java/org/jabref/toolkit/commands/GitMergeDriver.java create mode 100644 jabkit/src/test/java/org/jabref/toolkit/commands/GitMergeDriverTest.java create mode 100644 jabkit/src/test/resources/org/jabref/toolkit/commands/merge-base.bib create mode 100644 jabkit/src/test/resources/org/jabref/toolkit/commands/merge-current.bib create mode 100644 jabkit/src/test/resources/org/jabref/toolkit/commands/merge-duplicate-keys.bib create mode 100644 jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other-conflict.bib create mode 100644 jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other.bib create mode 100644 jablib/src/main/java/org/jabref/logic/git/merge/BibFileMerger.java create mode 100644 jablib/src/main/java/org/jabref/logic/git/merge/EntryPropertyMerge.java create mode 100644 jablib/src/main/java/org/jabref/logic/git/merge/MergeOutcome.java create mode 100644 jablib/src/main/java/org/jabref/logic/git/merge/MergePreconditions.java create mode 100644 jablib/src/main/java/org/jabref/logic/git/merge/NonEntryContent.java create mode 100644 jablib/src/main/java/org/jabref/logic/git/merge/Refusal.java create mode 100644 jablib/src/test/java/org/jabref/logic/git/merge/BibFileMergerTest.java diff --git a/.jbang/JabKitLauncher.java b/.jbang/JabKitLauncher.java index 00b2bed58336..12cdd8a0ab67 100755 --- a/.jbang/JabKitLauncher.java +++ b/.jbang/JabKitLauncher.java @@ -32,6 +32,8 @@ //SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/GenerateCitationKeys.java //SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/GetCitedWorks.java //SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/GetCitingWorks.java +//SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/Git.java +//SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/GitMergeDriver.java //SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/InputOption.java //SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java //SOURCES ../jabkit/src/main/java/org/jabref/toolkit/commands/Pdf.java diff --git a/CHANGELOG.md b/CHANGELOG.md index da402b6ba247..dacc0a733831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ 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) + ### Changed ### Fixed diff --git a/docs/code-howtos/git.md b/docs/code-howtos/git.md index 1393ef59196e..9b9f4f32d699 100644 --- a/docs/code-howtos/git.md +++ b/docs/code-howtos/git.md @@ -73,6 +73,15 @@ However, JabRef is able to analyze the entries and determine that: Therefore, JabRef performs an automatic merge without requiring manual conflict resolution. +## Using the semantic merge as Git merge driver + +`jabkit git merge-driver` exposes the semantic merge as a [Git merge driver](https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver). +The user-visible behavior (setup, what is merged, conflicts, refusals) is described in the user documentation, [Share](https://docs.jabref.org/collaborative-work/) → "Merging a Bib(la)TeX Library with Git". + +The command class `GitMergeDriver` (jabkit) only translates between Git's contract (`%O %A %B`, exit code) and `BibFileMerger` (jablib, package `org.jabref.logic.git.merge`), which returns a `MergeOutcome`: either `Refused` with the reasons, or `Merged` with the remaining conflicts. +`BibFileMerger` composes the existing `SemanticMergeAnalyzer` with `EntryPropertyMerge`, which applies the same three-way rules to the entry type and the comment above an entry, because the merge plan carries field values only. +`MergePreconditions` refuses the merge when writing the result would lose content; `Refusal.Reason` documents each case and the parser or writer behavior behind it. + ## Related Test Cases The semantic conflict detection and merge resolution logic is covered by: diff --git a/docs/requirements/git.md b/docs/requirements/git.md index 749e6df47226..f66ef5f7dd9c 100644 --- a/docs/requirements/git.md +++ b/docs/requirements/git.md @@ -10,6 +10,15 @@ The GitHub sharing dialog must allow users to verify that their personal access Needs: impl +## Git merge driver for `.bib` files +`feat~jabkit.git.merge-driver~1` + +`jabkit git merge-driver BASE CURRENT OTHER` must perform the semantic three-way merge and write the result to `CURRENT`, following Git's merge driver contract (`%O %A %B`). +It must exit with `0` when all changes merge cleanly and with `1` when semantic conflicts remain. +Conflicting entries must keep the `CURRENT` version and be reported on stderr by citation key. + +Needs: impl + ## Git pull with unrelated histories `req~git.pull.unrelated-histories~1` diff --git a/jabkit/src/main/java/module-info.java b/jabkit/src/main/java/module-info.java index 224f52ba206d..121a41239024 100644 --- a/jabkit/src/main/java/module-info.java +++ b/jabkit/src/main/java/module-info.java @@ -13,7 +13,7 @@ opens org.jabref.toolkit.converter; opens org.jabref.toolkit; - requires transitive org.jspecify; + requires static org.jspecify; requires java.prefs; requires java.sql; diff --git a/jabkit/src/main/java/org/jabref/toolkit/commands/Git.java b/jabkit/src/main/java/org/jabref/toolkit/commands/Git.java new file mode 100644 index 000000000000..50eee53fd3a6 --- /dev/null +++ b/jabkit/src/main/java/org/jabref/toolkit/commands/Git.java @@ -0,0 +1,28 @@ +package org.jabref.toolkit.commands; + +import org.jabref.logic.importer.ImportFormatPreferences; + +import org.jspecify.annotations.NullMarked; + +import static picocli.CommandLine.Command; +import static picocli.CommandLine.Mixin; +import static picocli.CommandLine.ParentCommand; + +/// Groups the Git subcommands. Not executable itself: picocli reports a missing subcommand. +@Command(name = "git", description = "Git integration for .bib files.", + subcommands = { + GitMergeDriver.class + }) +@NullMarked +class Git { + + @ParentCommand + private JabKit jabKit; + + @Mixin + private JabKit.SharedOptions sharedOptions; + + ImportFormatPreferences importFormatPreferences() { + return jabKit.cliPreferences.getImportFormatPreferences(); + } +} diff --git a/jabkit/src/main/java/org/jabref/toolkit/commands/GitMergeDriver.java b/jabkit/src/main/java/org/jabref/toolkit/commands/GitMergeDriver.java new file mode 100644 index 000000000000..5acb99a5d98e --- /dev/null +++ b/jabkit/src/main/java/org/jabref/toolkit/commands/GitMergeDriver.java @@ -0,0 +1,128 @@ +package org.jabref.toolkit.commands; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import org.jabref.logic.JabRefException; +import org.jabref.logic.git.conflicts.ThreeWayEntryConflict; +import org.jabref.logic.git.merge.BibFileMerger; +import org.jabref.logic.git.merge.MergeOutcome; +import org.jabref.logic.git.merge.MergeOutcome.Merged; +import org.jabref.logic.git.merge.MergeOutcome.Refused; +import org.jabref.logic.git.merge.Refusal; +import org.jabref.logic.l10n.Localization; +import org.jabref.toolkit.converter.CygWinPathConverter; + +import org.jspecify.annotations.NullMarked; +import picocli.CommandLine; + +import static picocli.CommandLine.Command; +import static picocli.CommandLine.Mixin; +import static picocli.CommandLine.Parameters; +import static picocli.CommandLine.ParentCommand; + +/// Git merge driver performing JabRef's semantic three-way merge of `.bib` files. +/// +/// Git calls the driver with the base (`%O`), current (`%A`) and other (`%B`) version of the file +/// and expects the result in the current version's file. Exit code 0 tells Git that the merge is +/// clean; any other exit code marks the file as conflicted. This class translates between that +/// contract and [BibFileMerger], which does the merging. +@Command(name = "merge-driver", + description = "Git merge driver: semantic three-way merge of .bib files. Writes the result to CURRENT.", + footer = { + "", + "Setup:", + " git config --global merge.jabref.name \"JabRef semantic .bib merge\"", + // picocli runs the help text through String.format, so the Git placeholders have to be escaped + " git config --global merge.jabref.driver \\", + " \"jabkit git merge-driver --porcelain %%O %%A %%B\"", + " echo \"*.bib merge=jabref\" >> .gitattributes", + "", + "Exit code 0: merged cleanly. 1: semantic conflicts (CURRENT keeps its version of conflicting entries)." + }) +@NullMarked +class GitMergeDriver implements Callable { + + /// Any exit code other than 0 tells Git that the file needs manual conflict resolution + private static final int CONFLICT = 1; + + @ParentCommand + private Git git; + + @Mixin + private JabKit.SharedOptions sharedOptions; + + @Parameters(index = "0", paramLabel = "BASE", converter = CygWinPathConverter.class, + description = "Common ancestor version (Git placeholder %%O)") + private Path baseFile; + + @Parameters(index = "1", paramLabel = "CURRENT", converter = CygWinPathConverter.class, + description = "Current version (Git placeholder %%A); the merge result is written here") + private Path currentFile; + + @Parameters(index = "2", paramLabel = "OTHER", converter = CygWinPathConverter.class, + description = "Other version being merged (Git placeholder %%B)") + private Path otherFile; + + @Override + // [impl->feat~jabkit.git.merge-driver~1] + public Integer call() throws JabRefException { + MergeOutcome outcome; + try { + outcome = new BibFileMerger(git.importFormatPreferences()).merge(baseFile, currentFile, otherFile); + } catch (IOException e) { + throw new JabRefException("Unable to merge " + currentFile, Localization.lang("Unable to merge %0.", currentFile), e); + } + + return switch (outcome) { + case Refused refused -> { + refused.refusals().forEach(refusal -> System.err.println(describe(refusal))); + yield CONFLICT; + } + case Merged merged when !merged.conflicts().isEmpty() -> { + System.err.println(Localization.lang("%0 entries could not be merged automatically:", merged.conflicts().size())); + merged.conflicts().forEach(conflict -> System.err.println(" " + describe(conflict))); + yield CONFLICT; + } + case Merged _ -> { + if (!sharedOptions.porcelain) { + System.out.println(Localization.lang("Merged %0 without conflicts.", currentFile)); + } + yield CommandLine.ExitCode.OK; + } + }; + } + + private static String describe(Refusal refusal) { + Path file = refusal.file(); + return switch (refusal.reason()) { + case DUPLICATE_CITATION_KEYS -> + Localization.lang("Cannot merge %0: citation keys must be unique.", file); + case PARSER_WARNINGS -> + Localization.lang("Cannot merge %0: the file was not parsed without warnings.", file); + case EMPTY_ENTRY -> + Localization.lang("Cannot merge %0: an entry without fields is not preserved.", file); + case UNUSED_CUSTOM_ENTRY_TYPE -> + Localization.lang("Cannot merge %0: a custom entry type without entries is not preserved.", file); + case UNATTACHED_COMMENT -> + Localization.lang("Cannot merge %0: a comment in front of @Comment or @Preamble is not preserved.", file); + case NON_ENTRY_CONTENT_CHANGED_IN_OTHER -> + Localization.lang("Cannot merge %0: content outside of entries with a citation key changed in OTHER.", file); + }; + } + + private static String describe(ThreeWayEntryConflict conflict) { + String citationKey = conflict.citationKey(); + if (conflict.local() == null) { + return Localization.lang("%0: deleted in CURRENT, changed in OTHER", citationKey); + } + if (conflict.remote() == null) { + return Localization.lang("%0: changed in CURRENT, deleted in OTHER", citationKey); + } + if (conflict.base() == null) { + return Localization.lang("%0: added on both sides with different content", citationKey); + } + return Localization.lang("%0: changed on both sides with different content", citationKey); + } +} diff --git a/jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java b/jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java index 7a3a36a9dd37..d605a0d81c30 100644 --- a/jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java +++ b/jabkit/src/main/java/org/jabref/toolkit/commands/JabKit.java @@ -23,6 +23,7 @@ GenerateBibFromAux.class, GetCitedWorks.class, GetCitingWorks.class, + Git.class, Pdf.class, Preferences.class, Pseudonymize.class, diff --git a/jabkit/src/test/java/org/jabref/toolkit/commands/GitMergeDriverTest.java b/jabkit/src/test/java/org/jabref/toolkit/commands/GitMergeDriverTest.java new file mode 100644 index 000000000000..e30c494f9160 --- /dev/null +++ b/jabkit/src/test/java/org/jabref/toolkit/commands/GitMergeDriverTest.java @@ -0,0 +1,83 @@ +package org.jabref.toolkit.commands; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.jabref.logic.l10n.Language; +import org.jabref.logic.l10n.Localization; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Covers the Git merge driver contract: exit codes, the output on stderr, and the file the +/// result goes to. The merge itself is tested in `BibFileMergerTest`. +class GitMergeDriverTest extends AbstractJabKitTest { + + @TempDir + private Path tempDir; + + /// The first localized message otherwise comes with a warning that the language is not set + @BeforeAll + static void setLanguage() { + Localization.setLanguage(Language.ENGLISH); + } + + @Test + void mergedFileYieldsExitCodeZero() throws IOException { + Path base = mergeFile("base", "merge-base.bib"); + Path current = mergeFile("current", "merge-current.bib"); + Path other = mergeFile("other", "merge-other.bib"); + + int exitCode = commandLine.executeToLog("git", "merge-driver", base.toString(), current.toString(), other.toString()); + + assertEquals(0, exitCode, commandLine.getErrorOutput()); + assertEquals("Merged " + current + " without conflicts.\n", commandLine.getStandardOutput()); + assertTrue(Files.readString(current).contains("Newton1999")); + } + + @Test + void conflictsAreListedOnStderrWithExitCodeOne() throws IOException { + Path base = mergeFile("base", "merge-base.bib"); + Path current = mergeFile("current", "merge-current.bib"); + Path other = mergeFile("other", "merge-other-conflict.bib"); + + int exitCode = commandLine.executeToLog("git", "merge-driver", "--porcelain", base.toString(), current.toString(), other.toString()); + + assertEquals(1, exitCode); + assertEquals(""" + 1 entries could not be merged automatically: + Smith2020: changed on both sides with different content + """, commandLine.getErrorOutput()); + assertEquals("", commandLine.getStandardOutput()); + } + + @Test + void refusedMergeLeavesCurrentUntouchedWithExitCodeOne() throws IOException { + Path base = mergeFile("base", "merge-base.bib"); + Path current = mergeFile("current", "merge-duplicate-keys.bib"); + Path other = mergeFile("other", "merge-other.bib"); + + int exitCode = commandLine.executeToLog("git", "merge-driver", "--porcelain", base.toString(), current.toString(), other.toString()); + + assertEquals(1, exitCode); + assertEquals("Cannot merge " + current + ": citation keys must be unique.\n", commandLine.getErrorOutput()); + assertEquals(Files.readString(getClassResourceAsPath("merge-duplicate-keys.bib")), Files.readString(current)); + } + + @Test + void gitWithoutSubcommandIsUsageError() { + int exitCode = commandLine.executeToLog("git"); + + assertEquals(2, exitCode); + } + + /// Git hands the driver temporary files without a `.bib` extension + private Path mergeFile(String name, String resource) throws IOException { + return Files.copy(getClassResourceAsPath(resource), tempDir.resolve(".merge_file_" + name)); + } +} diff --git a/jabkit/src/test/nativeimage/jabkit-offline.md b/jabkit/src/test/nativeimage/jabkit-offline.md index f58bcac26cbc..9ac61a0fa31f 100644 --- a/jabkit/src/test/nativeimage/jabkit-offline.md +++ b/jabkit/src/test/nativeimage/jabkit-offline.md @@ -14,6 +14,12 @@ $ "$JABKIT" pseudonymize --porcelain --force --input=src/test/resources/org/jabr 0 $ grep -c "Pseudonymizing library" build/tmp/pseudonymize.out 1 +$ cp src/test/resources/org/jabref/toolkit/commands/merge-current.bib build/tmp/merge-current.bib +$ "$JABKIT" git merge-driver --porcelain src/test/resources/org/jabref/toolkit/commands/merge-base.bib build/tmp/merge-current.bib src/test/resources/org/jabref/toolkit/commands/merge-other.bib 2>build/tmp/merge-driver.err; echo $? +0 +$ cat build/tmp/merge-driver.err +$ grep -c "Newton1999" build/tmp/merge-current.bib +1 $ "$JABKIT" convert --porcelain --input=src/test/resources/org/jabref/toolkit/commands/origin.bib --input-format=bibtex --output=build/tmp/convert.bib 2>/dev/null; echo $? 0 $ grep -c "@Book{" build/tmp/convert.bib diff --git a/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-base.bib b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-base.bib new file mode 100644 index 000000000000..335b49a880fd --- /dev/null +++ b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-base.bib @@ -0,0 +1,11 @@ +@Article{Smith2020, + author = {Smith, John}, + title = {Base Title}, + year = {2020}, +} + +@Article{Doe2021, + author = {Doe, Jane}, + title = {Another Paper}, + year = {2021}, +} diff --git a/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-current.bib b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-current.bib new file mode 100644 index 000000000000..90bc7af8eae4 --- /dev/null +++ b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-current.bib @@ -0,0 +1,12 @@ +@Article{Smith2020, + author = {Smith, John}, + title = {Current Title}, + year = {2020}, + journal = {Journal of Tests}, +} + +@Article{Doe2021, + author = {Doe, Jane}, + title = {Another Paper}, + year = {2021}, +} diff --git a/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-duplicate-keys.bib b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-duplicate-keys.bib new file mode 100644 index 000000000000..730ff84b6516 --- /dev/null +++ b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-duplicate-keys.bib @@ -0,0 +1,11 @@ +@Article{Smith2020, + author = {Smith, John}, + title = {Base Title}, + year = {2020}, +} + +@Article{Smith2020, + author = {Smith, John}, + title = {Duplicate Key}, + year = {2020}, +} diff --git a/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other-conflict.bib b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other-conflict.bib new file mode 100644 index 000000000000..167d3ea6f5f6 --- /dev/null +++ b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other-conflict.bib @@ -0,0 +1,12 @@ +@Article{Smith2020, + author = {Smith, John}, + title = {Conflicting Title}, + year = {2020}, + doi = {10.1000/xyz123}, +} + +@Article{Doe2021, + author = {Doe, Jane}, + title = {Another Paper}, + year = {2021}, +} diff --git a/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other.bib b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other.bib new file mode 100644 index 000000000000..d1a0f82f0749 --- /dev/null +++ b/jabkit/src/test/resources/org/jabref/toolkit/commands/merge-other.bib @@ -0,0 +1,18 @@ +@Article{Smith2020, + author = {Smith, John}, + title = {Base Title}, + year = {2020}, + doi = {10.1000/xyz123}, +} + +@Article{Doe2021, + author = {Doe, Jane}, + title = {Another Paper}, + year = {2021}, +} + +@Book{Newton1999, + author = {Newton, Isaac}, + title = {The Principia}, + year = {1999}, +} diff --git a/jablib/src/main/java/module-info.java b/jablib/src/main/java/module-info.java index 20b988b3642f..f52c31f7ae3d 100644 --- a/jablib/src/main/java/module-info.java +++ b/jablib/src/main/java/module-info.java @@ -126,6 +126,7 @@ exports org.jabref.logic.git.preferences; exports org.jabref.logic.icore; exports org.jabref.model.icore; + exports org.jabref.logic.git.merge; exports org.jabref.logic.git.merge.planning; exports org.jabref.logic.git.merge.execution; exports org.jabref.model.sciteTallies; diff --git a/jablib/src/main/java/org/jabref/logic/git/conflicts/ThreeWayEntryConflict.java b/jablib/src/main/java/org/jabref/logic/git/conflicts/ThreeWayEntryConflict.java index 449903a94ee7..0a8b3ba09e06 100644 --- a/jablib/src/main/java/org/jabref/logic/git/conflicts/ThreeWayEntryConflict.java +++ b/jablib/src/main/java/org/jabref/logic/git/conflicts/ThreeWayEntryConflict.java @@ -1,5 +1,8 @@ package org.jabref.logic.git.conflicts; +import java.util.Objects; +import java.util.stream.Stream; + import org.jabref.model.entry.BibEntry; import org.jspecify.annotations.NullMarked; @@ -18,4 +21,13 @@ public record ThreeWayEntryConflict( public ThreeWayEntryConflict { assert !(local == null && remote == null) : "Both local and remote are null: conflict must involve at least one side."; } + + /// The key shared by all versions of the entry; conflicts are detected for entries with a citation key only. + public String citationKey() { + return Stream.of(local, remote, base) + .filter(Objects::nonNull) + .findFirst() + .flatMap(BibEntry::getCitationKey) + .orElseThrow(); + } } diff --git a/jablib/src/main/java/org/jabref/logic/git/io/GitFileWriter.java b/jablib/src/main/java/org/jabref/logic/git/io/GitFileWriter.java index b94e539ca60b..72f2fa1af7c8 100644 --- a/jablib/src/main/java/org/jabref/logic/git/io/GitFileWriter.java +++ b/jablib/src/main/java/org/jabref/logic/git/io/GitFileWriter.java @@ -17,8 +17,13 @@ public class GitFileWriter { private static final ConcurrentHashMap FILE_LOCKS = new ConcurrentHashMap<>(); - /// @implNote this should be in sync with [org.jabref.gui.exporter.SaveDatabaseAction#saveDatabase] public static void write(Path file, BibDatabaseContext bibDatabaseContext, ImportFormatPreferences importPrefs) throws IOException { + write(file, bibDatabaseContext, importPrefs, new BibEntryTypesManager()); + } + + /// @param entryTypesManager the custom entry types of the library; without them, the `@Comment{jabref-entrytype: ...}` definitions of the file are lost + /// @implNote this should be in sync with [org.jabref.gui.exporter.SaveDatabaseAction#saveDatabase] + public static void write(Path file, BibDatabaseContext bibDatabaseContext, ImportFormatPreferences importPrefs, BibEntryTypesManager entryTypesManager) throws IOException { SelfContainedSaveConfiguration saveConfiguration = new SelfContainedSaveConfiguration(); Charset encoding = bibDatabaseContext.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8); @@ -30,11 +35,13 @@ public static void write(Path file, BibDatabaseContext bibDatabaseContext, Impor saveConfiguration, importPrefs.fieldPreferences(), importPrefs.citationKeyPatternPreferences(), - new BibEntryTypesManager() + entryTypesManager ); writer.writeDatabase(bibDatabaseContext); if (fileWriter.hasEncodingProblems()) { + // Without the abort, closing the writer would replace the target file with the lossy content + fileWriter.abort(); throw new IOException("Encoding problem detected when saving .bib file: " + fileWriter.getEncodingProblems().toString()); } diff --git a/jablib/src/main/java/org/jabref/logic/git/merge/BibFileMerger.java b/jablib/src/main/java/org/jabref/logic/git/merge/BibFileMerger.java new file mode 100644 index 000000000000..87036521e461 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/git/merge/BibFileMerger.java @@ -0,0 +1,91 @@ +package org.jabref.logic.git.merge; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jabref.logic.git.conflicts.ThreeWayEntryConflict; +import org.jabref.logic.git.io.GitFileWriter; +import org.jabref.logic.git.merge.execution.GitMergeApplier; +import org.jabref.logic.git.merge.planning.SemanticMergeAnalyzer; +import org.jabref.logic.git.model.MergeAnalysis; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.util.DummyFileUpdateMonitor; + +import org.jspecify.annotations.NullMarked; + +/// Merges three versions of a `.bib` file the way a Git merge driver needs it: the merge result +/// replaces `current`, and the caller learns whether the files were merged and which entries +/// remained in conflict. +/// +/// The result is `current + (other - base)`, computed entry by entry: [SemanticMergeAnalyzer] +/// plans the field changes of `other` that do not collide with `current`, [EntryPropertyMerge] +/// does the same for the entry type and the comment above an entry. Entries changed on both +/// sides in different ways are reported as conflicts and keep their `current` version. +/// +/// The result is written by JabRef's `.bib` writer, which reproduces only what the parser +/// understood. [MergePreconditions] therefore refuses the merge when writing would lose content, +/// and `current` is left untouched then. +@NullMarked +public final class BibFileMerger { + + private final ImportFormatPreferences importFormatPreferences; + + public BibFileMerger(ImportFormatPreferences importFormatPreferences) { + this.importFormatPreferences = importFormatPreferences; + } + + /// @param base the common ancestor of both sides (Git's `%O`) + /// @param current the version the result is written to (Git's `%A`) + /// @param other the version being merged in (Git's `%B`) + /// @throws IOException if a file cannot be read, or if the result cannot be written + public MergeOutcome merge(Path base, Path current, Path other) throws IOException { + ParserResult baseVersion = parse(base); + ParserResult currentVersion = parse(current); + ParserResult otherVersion = parse(other); + + List refusals = MergePreconditions.check(baseVersion, currentVersion, otherVersion, + importFormatPreferences.citationKeyPatternPreferences().getKeyPatterns()); + if (!refusals.isEmpty()) { + return new MergeOutcome.Refused(refusals); + } + + List conflicts = applyChangesOfOther( + baseVersion.getDatabaseContext(), + currentVersion.getDatabaseContext(), + otherVersion.getDatabaseContext()); + write(current, currentVersion); + return new MergeOutcome.Merged(conflicts); + } + + private ParserResult parse(Path file) throws IOException { + return new BibtexImporter(importFormatPreferences, new DummyFileUpdateMonitor()).importDatabase(file); + } + + /// @return the entries that keep their `current` version, because the sides changed them differently + private static List applyChangesOfOther(BibDatabaseContext base, BibDatabaseContext current, BibDatabaseContext other) { + MergeAnalysis fields = SemanticMergeAnalyzer.analyze(base, current, other); + List propertyConflicts = EntryPropertyMerge.merge(base, current, other, citationKeys(fields.conflicts())); + GitMergeApplier.applyAutoPlan(current, fields.autoPlan().without(citationKeys(propertyConflicts))); + return Stream.concat(fields.conflicts().stream(), propertyConflicts.stream()).toList(); + } + + private static Set citationKeys(List conflicts) { + return conflicts.stream().map(ThreeWayEntryConflict::citationKey).collect(Collectors.toSet()); + } + + private void write(Path file, ParserResult version) throws IOException { + BibDatabaseContext context = version.getDatabaseContext(); + // The writer emits the custom entry type definitions it knows about, not the ones the parser found + BibEntryTypesManager entryTypesManager = new BibEntryTypesManager(); + entryTypesManager.addCustomOrModifiedTypes(List.copyOf(version.getEntryTypes()), context.getMode()); + GitFileWriter.write(file, context, importFormatPreferences, entryTypesManager); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/git/merge/EntryPropertyMerge.java b/jablib/src/main/java/org/jabref/logic/git/merge/EntryPropertyMerge.java new file mode 100644 index 000000000000..77f9cd110b64 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/git/merge/EntryPropertyMerge.java @@ -0,0 +1,95 @@ +package org.jabref.logic.git.merge; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jabref.logic.git.conflicts.ThreeWayEntryConflict; +import org.jabref.logic.git.merge.planning.util.EntryTriples; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/// Three-way merge of the entry properties the field-level merge does not look at: the rules of +/// [org.jabref.logic.git.merge.planning.SemanticMergeAnalyzer] compare field maps, and a +/// [org.jabref.logic.git.model.MergePlan] carries field values only. The entry type and the +/// comment above an entry follow the same rules here: a value changed in `other` alone is taken +/// over, a value changed differently on both sides is a conflict, and so is deleting an entry +/// on one side while changing a property on the other. +@NullMarked +final class EntryPropertyMerge { + + private static final List> PROPERTIES = List.of( + new EntryProperty<>(BibEntry::getType, BibEntry::setType), + new EntryProperty<>(BibEntry::getUserComments, EntryPropertyMerge::setUserComments)); + + private EntryPropertyMerge() { + } + + /// Applies the property changes of `other` to the entries of `current`. + /// + /// @param keptAsIs citation keys of entries already known to be in conflict; they are left untouched + /// @return the conflicts found, none of them among `keptAsIs` + static List merge(BibDatabaseContext base, BibDatabaseContext current, BibDatabaseContext other, Set keptAsIs) { + EntryTriples triples = EntryTriples.from(base, current, other); + List conflicts = new ArrayList<>(); + for (String key : triples.allKeys()) { + if (keptAsIs.contains(key)) { + continue; + } + @Nullable BibEntry baseEntry = triples.baseMap.get(key); + @Nullable BibEntry currentEntry = triples.localMap.get(key); + @Nullable BibEntry otherEntry = triples.remoteMap.get(key); + if (isConflict(baseEntry, currentEntry, otherEntry)) { + conflicts.add(new ThreeWayEntryConflict(baseEntry, currentEntry, otherEntry)); + } else if ((baseEntry != null) && (currentEntry != null) && (otherEntry != null)) { + PROPERTIES.forEach(property -> property.takeChangeFromOther(baseEntry, currentEntry, otherEntry)); + } + } + return conflicts; + } + + private static boolean isConflict(@Nullable BibEntry base, @Nullable BibEntry current, @Nullable BibEntry other) { + if (base == null) { + // Added on both sides: the field-level merge unions the fields, but a type or comment cannot be unioned + return (current != null) && (other != null) && differ(current, other); + } + if (current == null) { + return (other != null) && differ(base, other); + } + if (other == null) { + return differ(base, current); + } + return PROPERTIES.stream().anyMatch(property -> property.diverged(base, current, other)); + } + + private static boolean differ(BibEntry one, BibEntry two) { + return PROPERTIES.stream().anyMatch(property -> property.differs(one, two)); + } + + private static void setUserComments(BibEntry entry, String userComments) { + entry.setCommentsBeforeEntry(userComments); + // An unchanged entry is written from its parsed serialization, which still holds the old comment + entry.setChanged(true); + } + + private record EntryProperty(Function getter, BiConsumer setter) { + boolean differs(BibEntry one, BibEntry two) { + return !getter.apply(one).equals(getter.apply(two)); + } + + boolean diverged(BibEntry base, BibEntry current, BibEntry other) { + return differs(base, current) && differs(base, other) && differs(current, other); + } + + void takeChangeFromOther(BibEntry base, BibEntry current, BibEntry other) { + if (!differs(base, current) && differs(base, other)) { + setter.accept(current, getter.apply(other)); + } + } + } +} diff --git a/jablib/src/main/java/org/jabref/logic/git/merge/MergeOutcome.java b/jablib/src/main/java/org/jabref/logic/git/merge/MergeOutcome.java new file mode 100644 index 000000000000..a4d023835fbf --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/git/merge/MergeOutcome.java @@ -0,0 +1,21 @@ +package org.jabref.logic.git.merge; + +import java.util.List; + +import org.jabref.logic.git.conflicts.ThreeWayEntryConflict; + +import org.jspecify.annotations.NullMarked; + +/// What [BibFileMerger] did with the three versions of a file. +@NullMarked +public sealed interface MergeOutcome { + + /// Nothing was written, because writing would have lost content. + record Refused(List refusals) implements MergeOutcome { + } + + /// The `current` file holds the merge result. The listed entries kept their `current` + /// version, because both sides changed them in different ways. + record Merged(List conflicts) implements MergeOutcome { + } +} diff --git a/jablib/src/main/java/org/jabref/logic/git/merge/MergePreconditions.java b/jablib/src/main/java/org/jabref/logic/git/merge/MergePreconditions.java new file mode 100644 index 000000000000..20d1919c3d1e --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/git/merge/MergePreconditions.java @@ -0,0 +1,101 @@ +package org.jabref.logic.git.merge; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; +import org.jabref.logic.git.merge.Refusal.Reason; +import org.jabref.logic.importer.ParserResult; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.types.EntryType; + +import org.jspecify.annotations.NullMarked; + +/// The checks a merge has to pass before `current` is rewritten. [Reason] documents what each +/// check protects. +@NullMarked +final class MergePreconditions { + + private static final Pattern UNATTACHED_COMMENT = Pattern.compile( + "^\\h*%.*(\\R\\s*)*\\R\\s*@(comment|preamble)\\s*[{(]", + Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); + + private MergePreconditions() { + } + + /// @param keyPatterns needed to serialize the metadata for comparison + /// @return one refusal per violated check, empty if the files can be merged + static List check(ParserResult base, ParserResult current, ParserResult other, GlobalCitationKeyPatterns keyPatterns) throws IOException { + List refusals = new ArrayList<>(); + for (ParserResult version : List.of(base, current, other)) { + refusals.addAll(lossOnWriting(version)); + } + + NonEntryContent otherContent = NonEntryContent.of(other, keyPatterns); + if (!otherContent.equals(NonEntryContent.of(base, keyPatterns)) && !otherContent.equals(NonEntryContent.of(current, keyPatterns))) { + refusals.add(new Refusal(fileOf(other), Reason.NON_ENTRY_CONTENT_CHANGED_IN_OTHER)); + } + return refusals; + } + + /// The content of the file that would not survive being parsed and written back. + private static List lossOnWriting(ParserResult version) throws IOException { + Path file = fileOf(version); + BibDatabase database = version.getDatabase(); + List refusals = new ArrayList<>(); + if (hasDuplicateCitationKeys(database)) { + refusals.add(new Refusal(file, Reason.DUPLICATE_CITATION_KEYS)); + } + if (version.hasWarnings()) { + refusals.add(new Refusal(file, Reason.PARSER_WARNINGS)); + } + if (database.getEntries().stream().anyMatch(BibEntry::isEmpty)) { + refusals.add(new Refusal(file, Reason.EMPTY_ENTRY)); + } + if (hasUnusedCustomEntryTypes(version)) { + refusals.add(new Refusal(file, Reason.UNUSED_CUSTOM_ENTRY_TYPE)); + } + if (hasUnattachedComment(file, version)) { + refusals.add(new Refusal(file, Reason.UNATTACHED_COMMENT)); + } + return refusals; + } + + private static boolean hasDuplicateCitationKeys(BibDatabase database) { + return database.getEntries().stream() + .flatMap(entry -> entry.getCitationKey().stream()) + .anyMatch(database::isDuplicateCitationKeyExisting); + } + + private static boolean hasUnusedCustomEntryTypes(ParserResult version) { + Set usedTypes = version.getDatabase().getEntries().stream() + .map(BibEntry::getType) + .collect(Collectors.toSet()); + return version.getEntryTypes().stream() + .map(BibEntryType::getType) + .anyMatch(type -> !usedTypes.contains(type)); + } + + /// The parser does not keep the comment, so the raw file has to be searched for it. + private static boolean hasUnattachedComment(Path file, ParserResult version) throws IOException { + Charset encoding = version.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8); + // Decoding through the constructor replaces unmappable bytes instead of throwing + String content = new String(Files.readAllBytes(file), encoding); + return UNATTACHED_COMMENT.matcher(content).find(); + } + + /// The versions are parsed from files, so the path is always known. + private static Path fileOf(ParserResult version) { + return version.getPath().orElseThrow(); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/git/merge/NonEntryContent.java b/jablib/src/main/java/org/jabref/logic/git/merge/NonEntryContent.java new file mode 100644 index 000000000000..f368a6dafcd1 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/git/merge/NonEntryContent.java @@ -0,0 +1,54 @@ +package org.jabref.logic.git.merge; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns; +import org.jabref.logic.exporter.MetaDataSerializer; +import org.jabref.logic.importer.ParserResult; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.BibtexString; +import org.jabref.model.metadata.MetaData; + +import org.jspecify.annotations.NullMarked; + +/// Everything written to a `.bib` file that the merge takes from `current` as it is, because +/// [org.jabref.logic.git.merge.planning.SemanticMergeAnalyzer] plans entries with a citation key +/// only. Two versions with equal non-entry content can be merged without losing any of it. +/// +/// Entries and strings compare by what the writer emits for them. Metadata is compared in its +/// serialized form, because [MetaData#equals] ignores items JabRef does not know. The shared +/// database ID and the encoding are written as file header, not as part of the metadata. +@NullMarked +record NonEntryContent( + List keylessEntries, + Map strings, + String preamble, + String epilog, + Set customEntryTypes, + String sharedDatabaseId, + Charset encoding, + boolean encodingExplicitlySupplied, + Map serializedMetaData) { + + static NonEntryContent of(ParserResult version, GlobalCitationKeyPatterns keyPatterns) { + BibDatabase database = version.getDatabase(); + MetaData metaData = version.getMetaData(); + return new NonEntryContent( + database.getEntries().stream().filter(entry -> entry.getCitationKey().isEmpty()).toList(), + database.getStringValues().stream().collect(Collectors.toMap(BibtexString::getName, string -> string)), + database.getPreamble().orElse(""), + database.getEpilog(), + version.getEntryTypes(), + database.getSharedDatabaseID().orElse(""), + metaData.getEncoding().orElse(StandardCharsets.UTF_8), + metaData.getEncodingExplicitlySupplied(), + MetaDataSerializer.getSerializedStringMap(metaData, keyPatterns)); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/git/merge/Refusal.java b/jablib/src/main/java/org/jabref/logic/git/merge/Refusal.java new file mode 100644 index 000000000000..e780f336ca00 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/git/merge/Refusal.java @@ -0,0 +1,25 @@ +package org.jabref.logic.git.merge; + +import java.nio.file.Path; + +import org.jspecify.annotations.NullMarked; + +/// Why [BibFileMerger] left the files untouched: `file` holds content that JabRef's parser or +/// writer would silently drop, or that the entry-based merge cannot represent. +@NullMarked +public record Refusal(Path file, Reason reason) { + public enum Reason { + /// The merge is planned per citation key. With duplicates, the plan would be computed from one entry and applied to another. + DUPLICATE_CITATION_KEYS, + /// The parser skipped content it could not read, so the writer cannot reproduce it. + PARSER_WARNINGS, + /// The writer drops an entry that has no fields, or automatic fields only. + EMPTY_ENTRY, + /// The writer emits the definition of a custom entry type only when an entry uses the type. + UNUSED_CUSTOM_ENTRY_TYPE, + /// The parser attaches a comment to the entry or `@String` following it, but drops one in front of `@Comment` or `@Preamble`, as does every JabRef save. + UNATTACHED_COMMENT, + /// Only entries with a citation key are merged; everything else is taken from `current` and must therefore not have changed in `other`. + NON_ENTRY_CONTENT_CHANGED_IN_OTHER + } +} diff --git a/jablib/src/main/java/org/jabref/logic/git/model/MergePlan.java b/jablib/src/main/java/org/jabref/logic/git/model/MergePlan.java index 66d96b6834ee..571f676a7fb4 100644 --- a/jablib/src/main/java/org/jabref/logic/git/model/MergePlan.java +++ b/jablib/src/main/java/org/jabref/logic/git/model/MergePlan.java @@ -1,7 +1,9 @@ package org.jabref.logic.git.model; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; @@ -27,4 +29,14 @@ public static MergePlan empty() { public boolean isEmpty() { return fieldPatches.isEmpty() && newEntries.isEmpty() && deletedEntryKeys.isEmpty(); } + + /// This plan without the entries of the given citation keys, which are then left as they are. + public MergePlan without(Set citationKeys) { + Map> remainingPatches = new LinkedHashMap<>(fieldPatches); + remainingPatches.keySet().removeAll(citationKeys); + return new MergePlan( + remainingPatches, + newEntries.stream().filter(entry -> entry.getCitationKey().filter(citationKeys::contains).isEmpty()).toList(), + deletedEntryKeys.stream().filter(key -> !citationKeys.contains(key)).toList()); + } } diff --git a/jablib/src/main/resources/META-INF/native-image/org.jabref/jablib/reachability-metadata.json b/jablib/src/main/resources/META-INF/native-image/org.jabref/jablib/reachability-metadata.json index c1fa4684bc97..5d0c50b1b776 100644 --- a/jablib/src/main/resources/META-INF/native-image/org.jabref/jablib/reachability-metadata.json +++ b/jablib/src/main/resources/META-INF/native-image/org.jabref/jablib/reachability-metadata.json @@ -267,10 +267,32 @@ "serializable": true }, { - "type": "org.jabref.model.database.BibDatabase" + "type": "org.jabref.model.database.BibDatabase", + "methods": [ + { + "name": "relayEntryChangeEvent", + "parameterTypes": [ + "org.jabref.model.entry.event.FieldChangedEvent" + ] + } + ] }, { - "type": "org.jabref.model.database.CitationKeyListener" + "type": "org.jabref.model.database.CitationKeyListener", + "methods": [ + { + "name": "listen", + "parameterTypes": [ + "org.jabref.model.entry.event.FieldChangedEvent" + ] + }, + { + "name": "listen", + "parameterTypes": [ + "org.jabref.model.database.event.EntriesRemovedEvent" + ] + } + ] }, { "type": "sun.text.resources.cldr.FormatData" diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index dd38548fac4e..63c933b56930 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -3505,6 +3505,21 @@ Pseudonymize\ the\ current\ library\ and\ generate\ a\ .bib\ and\ .csv\ file=Pse Pseudonymizing\ library\ '%0'...=Pseudonymizing library '%0'... Invalid\ output\ file\ type\ provided.=Invalid output file type provided. +# jabkit git merge-driver +Unable\ to\ merge\ %0.=Unable to merge %0. +Merged\ %0\ without\ conflicts.=Merged %0 without conflicts. +%0\ entries\ could\ not\ be\ merged\ automatically\:=%0 entries could not be merged automatically: +%0\:\ deleted\ in\ CURRENT,\ changed\ in\ OTHER=%0: deleted in CURRENT, changed in OTHER +%0\:\ changed\ in\ CURRENT,\ deleted\ in\ OTHER=%0: changed in CURRENT, deleted in OTHER +%0\:\ added\ on\ both\ sides\ with\ different\ content=%0: added on both sides with different content +%0\:\ changed\ on\ both\ sides\ with\ different\ content=%0: changed on both sides with different content +Cannot\ merge\ %0\:\ citation\ keys\ must\ be\ unique.=Cannot merge %0: citation keys must be unique. +Cannot\ merge\ %0\:\ the\ file\ was\ not\ parsed\ without\ warnings.=Cannot merge %0: the file was not parsed without warnings. +Cannot\ merge\ %0\:\ an\ entry\ without\ fields\ is\ not\ preserved.=Cannot merge %0: an entry without fields is not preserved. +Cannot\ merge\ %0\:\ a\ comment\ in\ front\ of\ @Comment\ or\ @Preamble\ is\ not\ preserved.=Cannot merge %0: a comment in front of @Comment or @Preamble is not preserved. +Cannot\ merge\ %0\:\ a\ custom\ entry\ type\ without\ entries\ is\ not\ preserved.=Cannot merge %0: a custom entry type without entries is not preserved. +Cannot\ merge\ %0\:\ content\ outside\ of\ entries\ with\ a\ citation\ key\ changed\ in\ OTHER.=Cannot merge %0: content outside of entries with a citation key changed in OTHER. + # Quick Settings Quick\ settings=Quick settings diff --git a/jablib/src/test/java/org/jabref/logic/git/merge/BibFileMergerTest.java b/jablib/src/test/java/org/jabref/logic/git/merge/BibFileMergerTest.java new file mode 100644 index 000000000000..4f599a1c7b95 --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/git/merge/BibFileMergerTest.java @@ -0,0 +1,361 @@ +package org.jabref.logic.git.merge; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import org.jabref.logic.git.conflicts.ThreeWayEntryConflict; +import org.jabref.logic.git.merge.MergeOutcome.Merged; +import org.jabref.logic.git.merge.MergeOutcome.Refused; +import org.jabref.logic.git.merge.Refusal.Reason; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.DummyFileUpdateMonitor; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class BibFileMergerTest { + + private static final String SMITH = """ + @Article{Smith2020, + author = {Smith, John}, + title = {Base Title}, + year = {2020}, + } + """; + + private static final String DOE = """ + @Article{Doe2021, + author = {Doe, Jane}, + title = {Another Paper}, + year = {2021}, + } + """; + + private static final String NEWTON = """ + @Book{Newton1999, + author = {Newton, Isaac}, + title = {The Principia}, + year = {1999}, + } + """; + + private static final String BASE = SMITH + "\n" + DOE; + + private static final BibEntry SMITH_ENTRY = new BibEntry(StandardEntryType.Article) + .withCitationKey("Smith2020") + .withField(StandardField.AUTHOR, "Smith, John") + .withField(StandardField.TITLE, "Base Title") + .withField(StandardField.YEAR, "2020"); + + private static final BibEntry DOE_ENTRY = new BibEntry(StandardEntryType.Article) + .withCitationKey("Doe2021") + .withField(StandardField.AUTHOR, "Doe, Jane") + .withField(StandardField.TITLE, "Another Paper") + .withField(StandardField.YEAR, "2021"); + + private static final BibEntry NEWTON_ENTRY = new BibEntry(StandardEntryType.Book) + .withCitationKey("Newton1999") + .withField(StandardField.AUTHOR, "Newton, Isaac") + .withField(StandardField.TITLE, "The Principia") + .withField(StandardField.YEAR, "1999"); + + @TempDir + private Path tempDir; + + private final ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); + private BibFileMerger merger; + + @BeforeEach + void setUp() { + when(importFormatPreferences.bibEntryPreferences().getKeywordSeparator()).thenReturn(','); + merger = new BibFileMerger(importFormatPreferences); + } + + @Test + void appliesNonConflictingChangesOfOtherToCurrent() throws IOException { + Path current = version("current", BASE.replace("title = {Base Title},", "title = {Current Title},\n journal = {Journal of Tests},")); + Path other = version("other", BASE.replace("year = {2020},", "year = {2020},\n doi = {10.1000/xyz123},") + "\n" + NEWTON); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, other); + + assertEquals(new Merged(List.of()), outcome); + BibEntry smith = new BibEntry(StandardEntryType.Article) + .withCitationKey("Smith2020") + .withField(StandardField.AUTHOR, "Smith, John") + .withField(StandardField.TITLE, "Current Title") + .withField(StandardField.YEAR, "2020") + .withField(StandardField.JOURNAL, "Journal of Tests") + .withField(StandardField.DOI, "10.1000/xyz123"); + assertEquals(List.of(smith, DOE_ENTRY, NEWTON_ENTRY), entriesOf(current)); + } + + @Test + void identicalVersionsMergeCleanly() throws IOException { + Path current = version("current", BASE); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, version("other", BASE)); + + assertEquals(new Merged(List.of()), outcome); + assertEquals(List.of(SMITH_ENTRY, DOE_ENTRY), entriesOf(current)); + } + + @Test + void conflictingEntryKeepsCurrentVersion() throws IOException { + Path current = version("current", BASE.replace("Base Title", "Current Title")); + Path other = version("other", BASE.replace("Base Title", "Other Title") + "\n" + NEWTON); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, other); + + assertEquals(List.of("Smith2020"), conflictingKeys(outcome)); + BibEntry smith = new BibEntry(SMITH_ENTRY).withField(StandardField.TITLE, "Current Title"); + assertEquals(List.of(smith, DOE_ENTRY, NEWTON_ENTRY), entriesOf(current)); + } + + @Test + void appliesEntryTypeChangedInOther() throws IOException { + Path current = version("current", BASE); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, version("other", BASE.replace("@Article{Smith2020", "@Book{Smith2020"))); + + assertEquals(new Merged(List.of()), outcome); + assertEquals(List.of(smithAs(StandardEntryType.Book), DOE_ENTRY), entriesOf(current)); + } + + @Test + void entryTypeChangedDifferentlyOnBothSidesIsConflict() throws IOException { + Path current = version("current", BASE.replace("@Article{Smith2020", "@Report{Smith2020")); + Path other = version("other", BASE.replace("@Article{Smith2020", "@Book{Smith2020") + "\n" + NEWTON); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, other); + + assertEquals(List.of("Smith2020"), conflictingKeys(outcome)); + assertEquals(List.of(smithAs(StandardEntryType.Report), DOE_ENTRY, NEWTON_ENTRY), entriesOf(current)); + } + + @Test + void entryTypeChangedInCurrentAndDeletedInOtherIsConflict() throws IOException { + Path current = version("current", BASE.replace("@Article{Smith2020", "@Report{Smith2020")); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, version("other", DOE)); + + assertEquals(List.of("Smith2020"), conflictingKeys(outcome)); + assertEquals(List.of(smithAs(StandardEntryType.Report), DOE_ENTRY), entriesOf(current)); + } + + @Test + void appliesEntryCommentChangedInOther() throws IOException { + Path current = version("current", "% Note about Smith\n" + BASE); + + MergeOutcome outcome = merger.merge(version("base", "% Note about Smith\n" + BASE), current, version("other", "% Changed note about Smith\n" + BASE)); + + assertEquals(new Merged(List.of()), outcome); + assertEquals("% Changed note about Smith\n", entriesOf(current).getFirst().getUserComments()); + } + + @Test + void entryCommentChangedDifferentlyOnBothSidesIsConflict() throws IOException { + Path current = version("current", "% Own note about Smith\n" + BASE); + + MergeOutcome outcome = merger.merge(version("base", "% Note about Smith\n" + BASE), current, version("other", "% Changed note about Smith\n" + BASE)); + + assertEquals(List.of("Smith2020"), conflictingKeys(outcome)); + assertEquals("% Own note about Smith\n", entriesOf(current).getFirst().getUserComments()); + } + + @Test + void entryCommentChangedInOtherAndDeletedInCurrentIsConflict() throws IOException { + Path current = version("current", DOE); + + MergeOutcome outcome = merger.merge(version("base", "% Note about Smith\n" + BASE), current, version("other", "% Changed note about Smith\n" + BASE)); + + assertEquals(List.of("Smith2020"), conflictingKeys(outcome)); + assertEquals(List.of(DOE_ENTRY), entriesOf(current)); + } + + @Test + void entryCommentChangedInCurrentAndDeletedInOtherIsConflict() throws IOException { + Path current = version("current", "% Own note about Smith\n" + BASE); + + MergeOutcome outcome = merger.merge(version("base", "% Note about Smith\n" + BASE), current, version("other", DOE)); + + assertEquals(List.of("Smith2020"), conflictingKeys(outcome)); + assertEquals(List.of(new BibEntry(SMITH_ENTRY).withUserComments("% Own note about Smith\n"), DOE_ENTRY), entriesOf(current)); + } + + @Test + void keepsCustomEntryTypeDefinition() throws IOException { + String customType = "@Comment{jabref-entrytype: mytype: req[author;title] opt[year]}\n\n" + """ + @Mytype{Smith2020, + author = {Smith, John}, + title = {Base Title}, + } + """; + Path current = version("current", customType); + + MergeOutcome outcome = merger.merge(version("base", customType), current, version("other", customType.replace("title = {Base Title},", "title = {Base Title},\n year = {2020},"))); + + assertEquals(new Merged(List.of()), outcome); + String merged = Files.readString(current); + assertTrue(merged.contains("@Comment{jabref-entrytype: mytype: req[author;title] opt[year]}"), merged); + assertTrue(merged.contains("year = {2020},"), merged); + } + + @Test + void refusesStringAddedInOther() throws IOException { + Path current = version("current", BASE); + Path other = version("other", "@String{jot = {Journal of Tests}}\n\n" + BASE); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, other); + + assertEquals(new Refused(List.of(new Refusal(other, Reason.NON_ENTRY_CONTENT_CHANGED_IN_OTHER))), outcome); + assertEquals(BASE, Files.readString(current)); + } + + @Test + void refusesSharedDatabaseIdAddedInOther() throws IOException { + Path current = version("current", BASE); + Path other = version("other", "% DBID: 6a7b8c\n\n" + BASE); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, other); + + assertEquals(new Refused(List.of(new Refusal(other, Reason.NON_ENTRY_CONTENT_CHANGED_IN_OTHER))), outcome); + assertEquals(BASE, Files.readString(current)); + } + + @Test + void refusesDuplicateCitationKeys() throws IOException { + String duplicateKeys = SMITH + "\n" + SMITH.replace("Base Title", "Duplicate Key"); + Path current = version("current", duplicateKeys); + + MergeOutcome outcome = merger.merge(version("base", BASE), current, version("other", BASE)); + + assertEquals(new Refused(List.of(new Refusal(current, Reason.DUPLICATE_CITATION_KEYS))), outcome); + assertEquals(duplicateKeys, Files.readString(current)); + } + + @Test + void refusesFileWithParserWarnings() throws IOException { + String duplicateStrings = "@String{jot = {Journal of Tests}}\n@String{jot = {Other Journal}}\n\n" + BASE; + Path current = version("current", duplicateStrings); + + MergeOutcome outcome = merger.merge(version("base", duplicateStrings), current, version("other", duplicateStrings)); + + assertEquals(List.of(Reason.PARSER_WARNINGS), reasons(outcome)); + assertEquals(duplicateStrings, Files.readString(current)); + } + + @Test + void refusesEmptyEntry() throws IOException { + String withEmptyEntry = BASE + "\n@Misc{,\n}\n"; + Path current = version("current", withEmptyEntry); + + MergeOutcome outcome = merger.merge(version("base", withEmptyEntry), current, version("other", withEmptyEntry)); + + assertEquals(List.of(Reason.EMPTY_ENTRY), reasons(outcome)); + assertEquals(withEmptyEntry, Files.readString(current)); + } + + @Test + void refusesUnusedCustomEntryType() throws IOException { + String unusedType = "@Comment{jabref-entrytype: unused: req[author] opt[year]}\n\n" + BASE; + Path current = version("current", unusedType); + + MergeOutcome outcome = merger.merge(version("base", unusedType), current, version("other", unusedType)); + + assertEquals(List.of(Reason.UNUSED_CUSTOM_ENTRY_TYPE), reasons(outcome)); + assertEquals(unusedType, Files.readString(current)); + } + + static Stream unattachedComments() { + return Stream.of( + Arguments.of("in front of a custom entry type", + "% Note about the custom type\n@Comment{jabref-entrytype: mytype: req[author;title] opt[year]}\n\n" + """ + @Mytype{Smith2020, + author = {Smith, John}, + title = {Base Title}, + } + """), + Arguments.of("in front of a lowercase @comment", + "% note\n@comment{jabref-entrytype: mytype: req[author] opt[year]}\n\n" + """ + @Mytype{Smith2020, + author = {Smith, John}, + } + """), + Arguments.of("in front of the preamble", + "% Note about the preamble\n@Preamble{\"\\newcommand{\\noopsort}[1]{}\"}\n\n" + BASE)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("unattachedComments") + void refusesUnattachedComment(String description, String content) throws IOException { + Path current = version("current", content); + + MergeOutcome outcome = merger.merge(version("base", content), current, version("other", content)); + + assertEquals(List.of(Reason.UNATTACHED_COMMENT), reasons(outcome)); + assertEquals(content, Files.readString(current)); + } + + @Test + void refusesUnattachedCommentInNonUtf8File() throws IOException { + String content = "% Encoding: ISO-8859-1\n\n% Notiz über Müller\n@Comment{jabref-entrytype: mytype: req[author] opt[year]}\n\n" + """ + @Mytype{Mueller2020, + author = {Müller, Max}, + } + """; + Path current = version("current", content, StandardCharsets.ISO_8859_1); + + MergeOutcome outcome = merger.merge(version("base", content, StandardCharsets.ISO_8859_1), current, version("other", content, StandardCharsets.ISO_8859_1)); + + assertEquals(List.of(Reason.UNATTACHED_COMMENT), reasons(outcome)); + assertEquals(content, Files.readString(current, StandardCharsets.ISO_8859_1)); + } + + /// Git hands the driver temporary files without a `.bib` extension + private Path version(String name, String content) throws IOException { + return version(name, content, StandardCharsets.UTF_8); + } + + private Path version(String name, String content, Charset encoding) throws IOException { + return Files.writeString(tempDir.resolve(".merge_file_" + name), content, encoding); + } + + private List entriesOf(Path file) throws IOException { + return new BibtexImporter(importFormatPreferences, new DummyFileUpdateMonitor()).importDatabase(file).getDatabase().getEntries(); + } + + private static BibEntry smithAs(StandardEntryType type) { + BibEntry smith = new BibEntry(SMITH_ENTRY); + smith.setType(type); + return smith; + } + + private static List conflictingKeys(MergeOutcome outcome) { + return assertInstanceOf(Merged.class, outcome).conflicts().stream().map(ThreeWayEntryConflict::citationKey).toList(); + } + + /// The refusals of the CURRENT version; identical versions are refused three times for the same reason + private static List reasons(MergeOutcome outcome) { + return assertInstanceOf(Refused.class, outcome).refusals().stream().map(Refusal::reason).distinct().toList(); + } +} diff --git a/jablib/src/test/java/org/jabref/logic/git/util/GitFileWriterTest.java b/jablib/src/test/java/org/jabref/logic/git/util/GitFileWriterTest.java index 3b7f8ac68535..b97781087e5f 100644 --- a/jablib/src/test/java/org/jabref/logic/git/util/GitFileWriterTest.java +++ b/jablib/src/test/java/org/jabref/logic/git/util/GitFileWriterTest.java @@ -1,5 +1,7 @@ package org.jabref.logic.git.util; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -14,12 +16,14 @@ import org.eclipse.jgit.util.SystemReader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.api.parallel.ResourceLock; import org.mockito.Answers; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,4 +60,21 @@ void writeThenReadBack() throws Exception { .withField(StandardField.TITLE, "Test"); assertEquals(List.of(expected), parsedContext.getDatabase().getEntries()); } + + @Test + void keepsTargetFileWhenContentCannotBeEncoded(@TempDir Path tempDir) throws Exception { + BibDatabaseContext databaseContext = BibDatabaseContext.of(""" + @article{a, + author = {Алиса}, + title = {Test}, + } + """, importFormatPreferences); + databaseContext.getMetaData().setEncoding(StandardCharsets.ISO_8859_1); + + Path target = tempDir.resolve("library.bib"); + Files.writeString(target, "previous content"); + + assertThrows(IOException.class, () -> GitFileWriter.write(target, databaseContext, importFormatPreferences)); + assertEquals("previous content", Files.readString(target)); + } } From 7b7868dc97c8ecf90fa1dfdac015f036b2833e73 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 08:16:18 +0200 Subject: [PATCH 044/102] Link pushed commits and their CHANGELOG.md diff in merge run summaries (#17235) * Link the CHANGELOG.md diff in the merge-main run summary Merge commits from main are large; the link jumps straight to the changelog changes. Co-Authored-By: Claude Opus 5 * Write the commit summary in the merge action, so conflict auto-merges get it too Co-Authored-By: Claude Opus 5 * Drop trailing blank line Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .../actions/merge-upstream-branch/action.yml | 21 +++++++++++ .github/workflows/merge-main.yml | 36 +++++-------------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/.github/actions/merge-upstream-branch/action.yml b/.github/actions/merge-upstream-branch/action.yml index a1084fc566a6..5c21e0d0a564 100644 --- a/.github/actions/merge-upstream-branch/action.yml +++ b/.github/actions/merge-upstream-branch/action.yml @@ -55,5 +55,26 @@ runs: echo "CHANGELOG.md merge=changelog" >> "$(git rev-parse --git-dir)/info/attributes" git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" + before=$(git rev-parse HEAD) git merge --no-ff --no-edit "upstream/$BASE_BRANCH" git push origin "HEAD:refs/heads/$TARGET_BRANCH" + + # Link the pushed commits in the run summary; the merge commit's full diff is large, + # so its CHANGELOG.md diff gets its own link (GitHub's file anchor is the SHA-256 of the path) + repo_url=$(git remote get-url origin) + repo_url=${repo_url%.git} + changelog_anchor=$(printf %s CHANGELOG.md | sha256sum | cut -d' ' -f1) + commits=$(git log --first-parent --format=%H "$before..HEAD") + { + if [ -z "$commits" ]; then + echo "No commit pushed." + else + echo "Commits:" + for c in $commits; do + echo "- $repo_url/commit/$c" + if ! git diff --quiet "$c^" "$c" -- CHANGELOG.md; then + echo " - [CHANGELOG.md]($repo_url/commit/$c#diff-$changelog_anchor)" + fi + done + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/merge-main.yml b/.github/workflows/merge-main.yml index 78523c587a53..1683888add92 100644 --- a/.github/workflows/merge-main.yml +++ b/.github/workflows/merge-main.yml @@ -59,9 +59,14 @@ jobs: ref: ${{ steps.resolve.outputs.branch }} token: ${{ secrets.GH_TOKEN_JABREF_MACHINE_PR_APPROVE || github.token }} - - name: Remember head before merge - id: before - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Link PR in summary + env: + PR_NUMBER: ${{ inputs.pr || github.event.pull_request.number }} + run: | + { + echo "PR: https://github.com/${{ github.repository }}/pull/$PR_NUMBER" + echo + } >> "$GITHUB_STEP_SUMMARY" # Pushes the merge commit back to the PR branch (the reason this workflow exists). # "$/" resolves the action from this workflow's own commit, not from the workspace - @@ -70,32 +75,7 @@ jobs: # The PR may live in a fork ("repository" above); the base branch is always taken from # JabRef/jabref. For a stacked PR, the base is the next lower PR's head branch, not main. - name: Merge ${{ steps.resolve.outputs.base }} - id: merge uses: $/.github/actions/merge-upstream-branch with: target-branch: ${{ steps.resolve.outputs.branch }} base-branch: ${{ steps.resolve.outputs.base }} - - - name: Summary - if: always() - env: - REPO: ${{ steps.resolve.outputs.repo }} - PR_NUMBER: ${{ inputs.pr || github.event.pull_request.number }} - BEFORE: ${{ steps.before.outputs.sha }} - MERGED: ${{ steps.merge.outcome == 'success' }} - run: | - { - echo "PR: https://github.com/${{ github.repository }}/pull/$PR_NUMBER" - echo - # Only pushed commits are linked; a failed merge or push leaves nothing to link. - commits="" - if [ "$MERGED" = "true" ]; then - commits=$(git log --first-parent --format=%H "$BEFORE..HEAD") - fi - if [ -z "$commits" ]; then - echo "No commit pushed." - else - echo "Commits:" - for c in $commits; do echo "- https://github.com/$REPO/commit/$c"; done - fi - } >> "$GITHUB_STEP_SUMMARY" From 3a4104a3590da84feecca160d68884ce9cca6f26 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 08:26:10 +0200 Subject: [PATCH 045/102] Disable OpenRewrite ForLoopIncrementInUpdate (#17184) It moves an increment into the for update clause even when a continue skips it, which changes behavior. Triggered by https://github.com/openrewrite/rewrite-static-analysis/issues/1061 Co-authored-by: Claude Opus 5 (1M context) --- rewrite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewrite.yml b/rewrite.yml index f52ab83a74be..82b455436c9c 100644 --- a/rewrite.yml +++ b/rewrite.yml @@ -151,7 +151,7 @@ recipeList: - org.openrewrite.staticanalysis.ExternalizableHasNoArgsConstructor - org.openrewrite.staticanalysis.FallThrough # - org.openrewrite.staticanalysis.ForLoopControlVariablePostfixOperators - - org.openrewrite.staticanalysis.ForLoopIncrementInUpdate +# - org.openrewrite.staticanalysis.ForLoopIncrementInUpdate # https://github.com/openrewrite/rewrite-static-analysis/issues/1061 - org.openrewrite.staticanalysis.IndexOfChecksShouldUseAStartPosition - org.openrewrite.staticanalysis.IndexOfReplaceableByContains # - org.openrewrite.staticanalysis.IndexOfShouldNotCompareGreaterThanZero From 4b50321293b85bcb364a2e8f2ffef623df6c58cf Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 08:39:43 +0200 Subject: [PATCH 046/102] Fix adding "dev: into-stable" for a newly linked bug (#17237) * Fix adding "dev: into-stable" for a newly linked bug The label step failed because the machine token may not read organization data. Co-Authored-By: Claude Opus 5 * Clarify comment Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .github/workflows/on-pr-bug-linked.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/on-pr-bug-linked.yml b/.github/workflows/on-pr-bug-linked.yml index 4f0f45481b3e..772583690520 100644 --- a/.github/workflows/on-pr-bug-linked.yml +++ b/.github/workflows/on-pr-bug-linked.yml @@ -67,7 +67,8 @@ jobs: fi # GitHub stores the type name in lower case ("bug") while the issue template writes "Bug" if [ "$labeled" = false ] && [ "${type,,}" = "bug" ]; then - gh pr edit "$PR_NUMBER" --add-label "dev: into-stable" + # `gh pr edit` queries fields that need the read:org scope, which the token lacks + gh api "repos/JabRef/jabref/issues/$PR_NUMBER/labels" -f "labels[]=dev: into-stable" > /dev/null echo "Added \`dev: into-stable\`" >> "$GITHUB_STEP_SUMMARY" labeled=true fi From d42a1ec38442e0bf58397923963581f0c1cc2389 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 06:27:15 +0000 Subject: [PATCH 047/102] Restore AI chat scroll position when switching back to an entry (#17172) * Restore AI chat scroll position when switching back to an entry Switching entries and back jumped the chat to the bottom. The scroll pane now remembers the position per chat history and restores it. Co-Authored-By: Claude Fable 5.1 * Link PR in CHANGELOG.md Co-Authored-By: Claude Fable 5.1 * Guard deferred scroll restore against a list switch Co-Authored-By: Claude Fable 5.1 * Use weak identity keys for saved scroll positions Co-Authored-By: Claude Fable 5.1 * Fix issues using OpenRewrite * Use plain if for the saved scroll position Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: github-actions[bot] --- CHANGELOG.md | 2 + .../gui/util/component/ListScrollPane.java | 31 +++++++- .../util/component/ListScrollPaneTest.java | 74 +++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 jabgui/src/test/java/org/jabref/gui/util/component/ListScrollPaneTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dacc0a733831..e2da4e5c6734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed +- We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) + ### Removed ## [6.0-beta.1] - 2026-09-21 diff --git a/jabgui/src/main/java/org/jabref/gui/util/component/ListScrollPane.java b/jabgui/src/main/java/org/jabref/gui/util/component/ListScrollPane.java index 59e7b7809515..f8b27fb09f3c 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/component/ListScrollPane.java +++ b/jabgui/src/main/java/org/jabref/gui/util/component/ListScrollPane.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; import javafx.application.Platform; @@ -21,6 +22,8 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.VBox; +import com.google.common.collect.MapMaker; + /// A container for rendering a list of items. it is not that smart as [javafx.scene.control.ListView], which can skip drawing components that are not visible, but it is flexible and supports custom spacing and auto-scroll. /// /// Mainly used in [org.jabref.gui.ai.chat.AiChatView]. @@ -36,6 +39,11 @@ public class ListScrollPane extends ScrollPane { private final ListChangeListener listContentListener = this::handleListContentChange; + /// Scroll position per list instance, so switching back to a previously shown list restores where the user was. + /// Keyed by identity: the AI chat history cache hands out one list instance per entry. + /// Weak keys let a list dropped from that cache be garbage collected. + private final Map, Double> scrollPositions = new MapMaker().weakKeys().makeMap(); + public ListScrollPane() { this.contentContainer = new VBox(); this.contentContainer.setFillWidth(true); @@ -61,6 +69,7 @@ private void setupItemReferenceListener() { if (oldList != null) { oldList.removeListener(listContentListener); + scrollPositions.put(oldList, getVvalue()); } contentContainer.getChildren().clear(); @@ -128,18 +137,32 @@ private void rebuildView() { } contentContainer.getChildren().setAll(nodes); - if (isAutoScrollToBottom()) { + Double savedPosition = scrollPositions.remove(list); + if (savedPosition != null) { + scrollTo(list, savedPosition); + } else if (isAutoScrollToBottom()) { scrollToBottom(); } } } public void scrollToBottom() { + scrollTo(getItems(), 1.0); + } + + private void scrollTo(ObservableList list, double vvalue) { // A single Platform.runLater fires before JavaFX's layout pass, so the - // content height may not yet reflect the newly added node. - // Using a double-runLater ensures we set vvalue=1.0 AFTER the layout + // content height may not yet reflect the newly added nodes. + // Using a double-runLater ensures we set the vvalue AFTER the layout // pass in the intermediate pulse has computed the final content height. - Platform.runLater(() -> Platform.runLater(() -> setVvalue(1.0))); + Platform.runLater(() -> Platform.runLater(() -> { + if (getItems() == list) { + setVvalue(vvalue); + } else { + // The list was switched before the scroll happened: keep the position for its next display + scrollPositions.put(list, vvalue); + } + })); } public final ObservableList getItems() { diff --git a/jabgui/src/test/java/org/jabref/gui/util/component/ListScrollPaneTest.java b/jabgui/src/test/java/org/jabref/gui/util/component/ListScrollPaneTest.java new file mode 100644 index 000000000000..c15febef06ee --- /dev/null +++ b/jabgui/src/test/java/org/jabref/gui/util/component/ListScrollPaneTest.java @@ -0,0 +1,74 @@ +package org.jabref.gui.util.component; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Scene; +import javafx.scene.layout.Region; +import javafx.stage.Stage; + +import org.jabref.gui.testutils.JavaFxTest; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@NullMarked +class ListScrollPaneTest extends JavaFxTest { + private ListScrollPane scrollPane; + + @Override + public void start(Stage stage) { + scrollPane = new ListScrollPane<>(); + scrollPane.setRenderer(_ -> { + Region region = new Region(); + region.setPrefHeight(100); + return region; + }); + scrollPane.setAutoScrollToBottom(true); + stage.setScene(new Scene(scrollPane, 200, 200)); + stage.show(); + } + + @Test + void switchingBackRestoresScrollPosition() { + ObservableList first = FXCollections.observableArrayList("a", "b", "c", "d", "e"); + ObservableList second = FXCollections.observableArrayList("x", "y", "z"); + + interact(() -> scrollPane.setItems(first)); + awaitEvents(); + interact(() -> scrollPane.setVvalue(0.4)); + + interact(() -> scrollPane.setItems(second)); + awaitEvents(); + assertEquals(1.0, scrollPane.getVvalue(), 0.001); + + interact(() -> scrollPane.setItems(first)); + awaitEvents(); + assertEquals(0.4, scrollPane.getVvalue(), 0.001); + } + + @Test + void switchingAgainBeforeRestoreKeepsSavedPosition() { + ObservableList first = FXCollections.observableArrayList("a", "b", "c", "d", "e"); + ObservableList second = FXCollections.observableArrayList("x", "y", "z"); + + interact(() -> scrollPane.setItems(first)); + awaitEvents(); + interact(() -> scrollPane.setVvalue(0.4)); + interact(() -> scrollPane.setItems(second)); + awaitEvents(); + + // Switch back and away again without letting the deferred restore run + interact(() -> { + scrollPane.setItems(first); + scrollPane.setItems(second); + }); + awaitEvents(); + assertEquals(1.0, scrollPane.getVvalue(), 0.001); + + interact(() -> scrollPane.setItems(first)); + awaitEvents(); + assertEquals(0.4, scrollPane.getVvalue(), 0.001); + } +} From c5d988bc2dd37801646077e7ad91a2820904d13c Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 10:23:05 +0200 Subject: [PATCH 048/102] Update html-to-node to 0.3.0 and JavaFX to 27 (#17211) * Update html-to-node to 0.3.0 Co-Authored-By: Claude Opus 5 (1M context) * Update JavaFX to 27 html-to-node 0.3.0 calls rich-text incubator APIs that exist only in JavaFX 27. Co-Authored-By: Claude Opus 5 (1M context) * Use JavaFX 27 in journal list generators Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- build-support/src/main/java/JournalListMvGenerator.java | 2 +- build-support/src/main/java/LtwaListMvGenerator.java | 2 +- versions/build.gradle.kts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build-support/src/main/java/JournalListMvGenerator.java b/build-support/src/main/java/JournalListMvGenerator.java index 30d2a39f872b..1f69e50ea11b 100644 --- a/build-support/src/main/java/JournalListMvGenerator.java +++ b/build-support/src/main/java/JournalListMvGenerator.java @@ -9,7 +9,7 @@ //DEPS org.jooq:jool:0.9.15 //DEPS org.jspecify:jspecify:1.0.0 // Keep this version in sync with versions/build.gradle.kts. -//DEPS org.openjfx:javafx-base:26.0.2 +//DEPS org.openjfx:javafx-base:27 //DEPS org.slf4j:slf4j-api:2.0.18 //DEPS org.slf4j:slf4j-simple:2.0.18 diff --git a/build-support/src/main/java/LtwaListMvGenerator.java b/build-support/src/main/java/LtwaListMvGenerator.java index 8934415f912c..13fdbba729eb 100644 --- a/build-support/src/main/java/LtwaListMvGenerator.java +++ b/build-support/src/main/java/LtwaListMvGenerator.java @@ -9,7 +9,7 @@ //DEPS org.jooq:jool:0.9.15 //DEPS org.jspecify:jspecify:1.0.0 // Keep this version in sync with versions/build.gradle.kts. -//DEPS org.openjfx:javafx-base:26.0.2 +//DEPS org.openjfx:javafx-base:27 //DEPS org.slf4j:slf4j-api:2.0.18 //DEPS org.slf4j:slf4j-simple:2.0.18 diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index f0c850671a51..5fbcfec4a6e9 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -13,7 +13,7 @@ val os = DefaultNativePlatform.getCurrentOperatingSystem() val arch = DefaultNativePlatform.getCurrentArchitecture() // Keep version in sync build-support/src/main/java/JournalListMvGenerator.java and build-support/src/main/java/LtwaListMvGenerator.java -val javafxDefault = "26.0.2" +val javafxDefault = "27" // The JavaFX version can be overridden via the gradle property `javafxVersion`. val javafx = providers.gradleProperty("javafxVersion").getOrElse(javafxDefault) @@ -139,7 +139,7 @@ dependencies.constraints { api("org.hisp.dhis:json-tree:1.9.5") api("org.jabref:afterburner.fx:2.0.0") api("org.jabref:easybind:2.3.0") - api("org.jabref:html-to-node:0.2.0") + api("org.jabref:html-to-node:0.3.0") api("org.jabref:latex-conv:0.1.0") api("org.jabref:mslinks:1.2") api("org.jetbrains:annotations:26.1.0") From f82bd2edb3ef10f6d10301bfa66c2f897eb56ec6 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 12:50:08 +0000 Subject: [PATCH 049/102] Link selected auto-found file on "+" in the File field (#16938) * Link selected auto-found file on "+" in the File field Pressing "+" with an automatically found, not yet linked file selected now links that file directly instead of opening the "Add file link" dialog. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01T5ibD7MmLqVmjTFkqipStf * Link PR in changelog Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01T5ibD7MmLqVmjTFkqipStf --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: github-actions[bot] --- CHANGELOG.md | 1 + .../org/jabref/gui/fieldeditors/LinkedFilesEditor.java | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2da4e5c6734..528930fc1712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) +- We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) ### Removed diff --git a/jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java b/jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java index f3e7f3ba1fef..cc59b9fc8208 100644 --- a/jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java +++ b/jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java @@ -1,5 +1,6 @@ package org.jabref.gui.fieldeditors; +import java.util.List; import java.util.Optional; import javafx.beans.binding.Bindings; @@ -420,6 +421,15 @@ public Parent getNode() { @FXML public void addNewFile() { + // A selected suggestion (file found in the file directory, not yet linked) is what the user wants to add, + // so "+" links it directly instead of asking for a new file link. + List selectedSuggestions = listView.getSelectionModel().getSelectedItems().stream() + .filter(LinkedFileViewModel::isAutomaticallyFound) + .toList(); + if (!selectedSuggestions.isEmpty()) { + selectedSuggestions.forEach(LinkedFileViewModel::acceptAsLinked); + return; + } dialogService.showCustomDialogAndWait(new LinkedFileEditDialog()).filter(file -> !file.isEmpty()).ifPresent(newLinkedFile -> viewModel.addNewLinkedFile(newLinkedFile)); } From fec708035ce7fd8c637daae4c9852052adefd441 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 13:19:42 +0000 Subject: [PATCH 050/102] Label new issues "status: needs-triage" and block /assign-me on them (#17239) * Label new issues "status: needs-triage" and block /assign-me on them Co-Authored-By: Claude Opus 5 * Fix YAML syntax of decline step Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/suggestion-for-improvement.md | 2 +- .github/workflows/on-issue-comment.yml | 8 ++++++++ CONTRIBUTING.md | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2e999c63c35d..e036486479b9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,7 @@ name: "Bug report" description: Create a report to help us improve type: Bug +labels: ["status: needs-triage"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/suggestion-for-improvement.md b/.github/ISSUE_TEMPLATE/suggestion-for-improvement.md index fe7db23f79be..8d89047c46fe 100644 --- a/.github/ISSUE_TEMPLATE/suggestion-for-improvement.md +++ b/.github/ISSUE_TEMPLATE/suggestion-for-improvement.md @@ -2,7 +2,7 @@ name: Suggestion for improvement about: Suggest an enhancement title: '' -labels: '' +labels: 'status: needs-triage' type: 'feature' assignees: '' diff --git a/.github/workflows/on-issue-comment.yml b/.github/workflows/on-issue-comment.yml index 100bba53aab3..dfb30906a834 100644 --- a/.github/workflows/on-issue-comment.yml +++ b/.github/workflows/on-issue-comment.yml @@ -12,8 +12,16 @@ jobs: contents: read issues: write steps: + - name: Decline self-assignment of untriaged issues + id: untriaged + if: "${{ contains(github.event.comment.body, '/assign-me') && contains(github.event.issue.labels.*.name, 'status: needs-triage') }}" + run: | + gh issue comment "${{ github.event.issue.html_url }}" --body "Hi @${{ github.event.comment.user.login }}, this issue needs triage by a maintainer first. Please wait until the \`status: needs-triage\` label is removed." + env: + GH_TOKEN: ${{ github.token }} - name: Assign the user or unassign stale assignments id: assign + if: ${{ steps.untriaged.outcome == 'skipped' }} uses: takanome-dev/assign-issue-action@fad4b83750f7df31e03c2a42e8f1880b0b4b3573 # edge with: github_token: '${{ secrets.GH_TOKEN_ASSIGN_ISSUE_ACTION || github.token }}' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 997f0adaa51c..2a8f094bcd0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,6 +83,7 @@ One can also add [callouts](https://just-the-docs.github.io/just-the-docs-tests/ Comment on the issue you want to work at with `/assign-me`. GitHub will then automatically assign you. +Issues labeled `status: needs-triage` cannot be self-assigned until a maintainer has triaged them. Before you do so, read the complete issue and its discussion. Some issues state a prerequisite - typically another pull request that has to be merged first. From 3fad03adcf1ccb3fe8286395df4cac553a51e7d3 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 13:27:01 +0000 Subject: [PATCH 051/102] Make case-sensitive search work in linked files (#17002) * Make case-sensitive search work in linked files The Lucene index for linked files only kept the lowercased, stemmed text, so `=!`, `==!` and `=~!` silently matched every spelling. Contents and annotations are now additionally indexed case-preserving, and case-sensitive comparisons are routed to those fields. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PTgPLMjHLxC6CHZAZy62jk * Fix formatting Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PTgPLMjHLxC6CHZAZy62jk * Say casing in CHANGELOG entry Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PTgPLMjHLxC6CHZAZy62jk * Stop re-exporting Lucene highlighter SearchResult no longer exposes a Highlighter in its API. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PTgPLMjHLxC6CHZAZy62jk * Update SqlBasedLibrarySearcherWithBibFilesTest.java * Update InMemoryLuceneSearchBackendTest.java --------- Co-authored-by: Claude Opus 5 Co-authored-by: github-actions[bot] Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/requirements/search-within-library.md | 10 ++++ jablib/src/main/java/module-info.java | 2 +- .../search/query/SearchToLuceneVisitor.java | 59 ++++++++++++++----- .../sqlbased/indexing/DocumentReader.java | 11 +++- .../retrieval/LinkedFilesSearcher.java | 6 +- .../model/search/LinkedFilesConstants.java | 34 ++++++++++- .../model/search/query/SearchResult.java | 34 +++++++---- .../InMemoryLuceneSearchBackendTest.java | 24 +++++++- .../SearchQueryLuceneConversionTest.java | 18 +++++- ...lBasedLibrarySearcherWithBibFilesTest.java | 10 +++- .../model/search/query/SearchResultTest.java | 46 +++++++++++++++ 12 files changed, 214 insertions(+), 41 deletions(-) create mode 100644 jablib/src/test/java/org/jabref/model/search/query/SearchResultTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 528930fc1712..d380fd1bfb60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) +- We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) ### Removed diff --git a/docs/requirements/search-within-library.md b/docs/requirements/search-within-library.md index c5e2b5c2b925..8b659623c988 100644 --- a/docs/requirements/search-within-library.md +++ b/docs/requirements/search-within-library.md @@ -59,6 +59,16 @@ When linked-file full-text indexing is enabled, users must be able to search the Needs: impl, utest +## Case-sensitive full-text search +`req~jabgui.search.fulltext.case-sensitive~1` + +Issue: [#13048](https://github.com/JabRef/jabref/issues/13048) + +The case-sensitive operators of the search syntax (`=!`, `==!`, `=~!`) must also apply to the contents and annotations of linked files. +Searching for `any ==! SEE` must not match a file that only contains `See`. + +Needs: impl, utest + ## Full-text search survives a query only Lucene rejects `req~jabgui.search.fulltext.lenient-query-parsing~1` diff --git a/jablib/src/main/java/module-info.java b/jablib/src/main/java/module-info.java index f52c31f7ae3d..b5b0d0e2c924 100644 --- a/jablib/src/main/java/module-info.java +++ b/jablib/src/main/java/module-info.java @@ -292,7 +292,7 @@ uses org.apache.lucene.codecs.lucene104.Lucene104Codec; requires org.apache.lucene.analysis.common; requires transitive org.apache.lucene.core; - requires transitive org.apache.lucene.highlighter; + requires org.apache.lucene.highlighter; requires org.apache.lucene.queryparser; // endregion diff --git a/jablib/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java b/jablib/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java index beaefbd17daf..47749e17ed10 100644 --- a/jablib/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java +++ b/jablib/src/main/java/org/jabref/logic/search/query/SearchToLuceneVisitor.java @@ -3,6 +3,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import org.jabref.model.search.LinkedFilesConstants; import org.jabref.model.search.SearchFlags; @@ -13,6 +14,7 @@ import org.apache.lucene.queryparser.classic.QueryParser; /// Tests are located in `org.jabref.logic.search.query.SearchQueryLuceneConversionTest`. +// [impl->req~jabgui.search.fulltext.case-sensitive~1] public class SearchToLuceneVisitor extends SearchBaseVisitor { private final EnumSet searchFlags; @@ -66,12 +68,10 @@ public String visitComparison(SearchParser.ComparisonContext ctx) { String term = SearchQuery.unescapeSearchValue(ctx.searchValue()); boolean isQuoted = ctx.searchValue().getStart().getType() == SearchParser.STRING_LITERAL; - // unfielded expression + // unfielded expression - the search bar flags apply if (ctx.FIELD() == null) { - if (searchFlags.contains(SearchFlags.REGULAR_EXPRESSION)) { - return "/" + term + "/"; - } - return isQuoted ? "\"" + escapeQuotes(term) + "\"" : escapeForLucene(term); + String value = buildValue(term, searchFlags.contains(SearchFlags.REGULAR_EXPRESSION), isQuoted); + return searchFlags.contains(SearchFlags.CASE_SENSITIVE) ? anyFieldExpression(value) : value; } // TODO: Here, there is no unescaping of the term (e.g., field\=thing=value does not work as expected) @@ -80,9 +80,9 @@ public String visitComparison(SearchParser.ComparisonContext ctx) { return ""; } - field = SearchFieldConstants.ANY_FIELD.equals(field) || SearchFieldConstants.ANY_FIELD_ALIAS.equals(field) ? "" : field + ":"; + boolean isAnyField = SearchFieldConstants.ANY_FIELD.equals(field) || SearchFieldConstants.ANY_FIELD_ALIAS.equals(field); int operator = ctx.operator().getStart().getType(); - return buildFieldExpression(field, term, operator, isQuoted); + return buildFieldExpression(isAnyField ? "" : field, term, operator, isQuoted); } /// A valid field is a field that is supported by the Lucene index. @@ -92,18 +92,31 @@ private boolean isValidField(String field) { return SearchFieldConstants.ANY_FIELD.equals(field) || SearchFieldConstants.ANY_FIELD_ALIAS.equals(field) || LinkedFilesConstants.PDF_FIELDS.contains(field); } + /// @param field the Lucene field to search in, or the empty string to search in all of [LinkedFilesConstants#PDF_FIELDS] private String buildFieldExpression(String field, String term, int operator, boolean isQuoted) { - boolean isRegexOp = isRegexOperator(operator); - boolean isNegationOp = isNegationOperator(operator); - - if (isRegexOp) { - String expression = field + "/" + term + "/"; - return isNegationOp ? "NOT " + expression : expression; + String value = buildValue(term, isRegexOperator(operator), isQuoted); + String expression; + if (isCaseSensitiveOperator(operator)) { + expression = field.isEmpty() ? anyFieldExpression(value) : LinkedFilesConstants.caseSensitiveFieldOf(field) + ":" + value; } else { - term = isQuoted ? "\"" + escapeQuotes(term) + "\"" : escapeForLucene(term); - String expression = field + term; - return isNegationOp ? "NOT " + expression : expression; + expression = field.isEmpty() ? value : field + ":" + value; } + return isNegationOperator(operator) ? "NOT " + expression : expression; + } + + private static String buildValue(String term, boolean isRegex, boolean isQuoted) { + if (isRegex) { + return "/" + term + "/"; + } + return isQuoted ? "\"" + escapeQuotes(term) + "\"" : escapeForLucene(term); + } + + /// The default fields of the query parser are analyzed case-insensitively, so a case-sensitive search + /// across all fields has to name the case-preserving fields explicitly. + private static String anyFieldExpression(String value) { + return LinkedFilesConstants.CASE_SENSITIVE_PDF_FIELDS.stream() + .map(field -> field + ":" + value) + .collect(Collectors.joining(" OR ", "(", ")")); } private static String escapeQuotes(String term) { @@ -130,6 +143,20 @@ private static boolean isNegationOperator(int operator) { }; } + private static boolean isCaseSensitiveOperator(int operator) { + return switch (operator) { + case SearchParser.CEQUAL, + SearchParser.CEEQUAL, + SearchParser.CREEQUAL, + SearchParser.NCEQUAL, + SearchParser.NCEEQUAL, + SearchParser.NCREEQUAL -> + true; + default -> + false; + }; + } + private static boolean isRegexOperator(int operator) { return switch (operator) { case SearchParser.REQUAL, diff --git a/jablib/src/main/java/org/jabref/logic/search/sqlbased/indexing/DocumentReader.java b/jablib/src/main/java/org/jabref/logic/search/sqlbased/indexing/DocumentReader.java index e6d8ef56e5bc..b5238749997b 100644 --- a/jablib/src/main/java/org/jabref/logic/search/sqlbased/indexing/DocumentReader.java +++ b/jablib/src/main/java/org/jabref/logic/search/sqlbased/indexing/DocumentReader.java @@ -25,7 +25,9 @@ import org.slf4j.MDC; import static org.jabref.model.search.LinkedFilesConstants.ANNOTATIONS; +import static org.jabref.model.search.LinkedFilesConstants.ANNOTATIONS_CASE_SENSITIVE; import static org.jabref.model.search.LinkedFilesConstants.CONTENT; +import static org.jabref.model.search.LinkedFilesConstants.CONTENT_CASE_SENSITIVE; import static org.jabref.model.search.LinkedFilesConstants.MODIFIED; import static org.jabref.model.search.LinkedFilesConstants.PAGE_NUMBER; import static org.jabref.model.search.LinkedFilesConstants.PATH; @@ -101,7 +103,10 @@ private void addContentIfNotEmpty(PDDocument pdfDocument, Document newDocument, try { String pdfContent = pdfTextStripper.getText(pdfDocument); if (StringUtil.isNotBlank(pdfContent)) { - newDocument.add(new TextField(CONTENT.toString(), mergeLines(pdfContent), Field.Store.YES)); + String mergedContent = mergeLines(pdfContent); + newDocument.add(new TextField(CONTENT.toString(), mergedContent, Field.Store.YES)); + // Only indexed, not stored: the stored text is read from CONTENT. + newDocument.add(new TextField(CONTENT_CASE_SENSITIVE.toString(), mergedContent, Field.Store.NO)); } // Apache PDFTextStripper is 1-based. See {@link org.apache.pdfbox.text.PDFTextStripper.processPages} @@ -113,7 +118,9 @@ private void addContentIfNotEmpty(PDDocument pdfDocument, Document newDocument, .toList(); if (!annotations.isEmpty()) { - newDocument.add(new TextField(ANNOTATIONS.toString(), String.join("\n", annotations), Field.Store.YES)); + String joinedAnnotations = String.join("\n", annotations); + newDocument.add(new TextField(ANNOTATIONS.toString(), joinedAnnotations, Field.Store.YES)); + newDocument.add(new TextField(ANNOTATIONS_CASE_SENSITIVE.toString(), joinedAnnotations, Field.Store.NO)); } } catch (IOException e) { LOGGER.warn("Could not read page {} of {}", pageNumber, resolvedPath.toAbsolutePath(), e); diff --git a/jablib/src/main/java/org/jabref/logic/search/sqlbased/retrieval/LinkedFilesSearcher.java b/jablib/src/main/java/org/jabref/logic/search/sqlbased/retrieval/LinkedFilesSearcher.java index c5782653b021..d29d06eb8969 100644 --- a/jablib/src/main/java/org/jabref/logic/search/sqlbased/retrieval/LinkedFilesSearcher.java +++ b/jablib/src/main/java/org/jabref/logic/search/sqlbased/retrieval/LinkedFilesSearcher.java @@ -30,9 +30,6 @@ import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.SearcherManager; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.search.highlight.Highlighter; -import org.apache.lucene.search.highlight.QueryScorer; -import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -116,7 +113,6 @@ private SearchResults getSearchResults(TopDocs topDocs, StoredFields storedField long startTime = System.currentTimeMillis(); Map> linkedFilesMap = getLinkedFilesMap(); - Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("", ""), new QueryScorer(searchQuery)); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document document = storedFields.document(scoreDoc.doc); @@ -130,7 +126,7 @@ private SearchResults getSearchResults(TopDocs topDocs, StoredFields storedField getFieldContents(document, LinkedFilesConstants.CONTENT), getFieldContents(document, LinkedFilesConstants.ANNOTATIONS), Integer.parseInt(getFieldContents(document, LinkedFilesConstants.PAGE_NUMBER)), - highlighter); + searchQuery); searchResults.addSearchResult(entriesWithFile, searchResult); } } diff --git a/jablib/src/main/java/org/jabref/model/search/LinkedFilesConstants.java b/jablib/src/main/java/org/jabref/model/search/LinkedFilesConstants.java index 00d041caaecd..1c15a6d01c51 100644 --- a/jablib/src/main/java/org/jabref/model/search/LinkedFilesConstants.java +++ b/jablib/src/main/java/org/jabref/model/search/LinkedFilesConstants.java @@ -1,9 +1,12 @@ package org.jabref.model.search; import java.util.List; +import java.util.Map; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.en.EnglishAnalyzer; +import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper; +import org.apache.lucene.analysis.standard.StandardTokenizer; public enum LinkedFilesConstants { /// Version number for the search index. @@ -12,21 +15,48 @@ public enum LinkedFilesConstants { /// 1. Index changes require reindexing (e.g., new/removed/renamed fields, analyzer changes) /// 2. Lucene codec changes (see module-info.java Lucene section) /// Incrementing triggers reindexing. - VERSION("6"), + VERSION("7"), PATH("path"), CONTENT("content"), ANNOTATIONS("annotations"), + /// Case-preserving copy of [#CONTENT], used for case-sensitive searches (`=!`, `==!`, `=~!`). + CONTENT_CASE_SENSITIVE("contentCaseSensitive"), + /// Case-preserving copy of [#ANNOTATIONS], used for case-sensitive searches (`=!`, `==!`, `=~!`). + ANNOTATIONS_CASE_SENSITIVE("annotationsCaseSensitive"), PAGE_NUMBER("pageNumber"), MODIFIED("modified"); - public static final Analyzer LINKED_FILES_ANALYZER = new EnglishAnalyzer(); public static final List PDF_FIELDS = List.of(CONTENT.toString(), ANNOTATIONS.toString()); + + /// The case-preserving counterpart of each entry of [#PDF_FIELDS], in the same order. + public static final List CASE_SENSITIVE_PDF_FIELDS = List.of(CONTENT_CASE_SENSITIVE.toString(), ANNOTATIONS_CASE_SENSITIVE.toString()); + + /// [EnglishAnalyzer] lowercases and stems, so the case of the indexed text is lost. + /// The case-sensitive fields are therefore only tokenized - nothing else. + private static final Analyzer CASE_PRESERVING_ANALYZER = new Analyzer() { + @Override + protected TokenStreamComponents createComponents(String fieldName) { + return new TokenStreamComponents(new StandardTokenizer()); + } + }; + + public static final Analyzer LINKED_FILES_ANALYZER = new PerFieldAnalyzerWrapper( + new EnglishAnalyzer(), + Map.of(CONTENT_CASE_SENSITIVE.toString(), CASE_PRESERVING_ANALYZER, + ANNOTATIONS_CASE_SENSITIVE.toString(), CASE_PRESERVING_ANALYZER)); + private final String field; LinkedFilesConstants(String field) { this.field = field; } + /// @return the case-preserving counterpart of `field` if there is one, `field` itself otherwise + public static String caseSensitiveFieldOf(String field) { + int index = PDF_FIELDS.indexOf(field); + return index == -1 ? field : CASE_SENSITIVE_PDF_FIELDS.get(index); + } + @Override public String toString() { return field; diff --git a/jablib/src/main/java/org/jabref/model/search/query/SearchResult.java b/jablib/src/main/java/org/jabref/model/search/query/SearchResult.java index 0e2665b651c3..e7de69400e9d 100644 --- a/jablib/src/main/java/org/jabref/model/search/query/SearchResult.java +++ b/jablib/src/main/java/org/jabref/model/search/query/SearchResult.java @@ -3,12 +3,16 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.jabref.model.search.LinkedFilesConstants; import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.search.Query; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; +import org.apache.lucene.search.highlight.QueryScorer; +import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.TextFragment; public final class SearchResult { @@ -18,7 +22,7 @@ public final class SearchResult { private final String pageContent; private final String annotation; private final int pageNumber; - private final Highlighter highlighter; + private final Query query; private List contentResultStringsHtml; private List annotationsResultStringsHtml; @@ -27,33 +31,33 @@ private SearchResult(boolean hasFulltextResults, String pageContent, String annotation, int pageNumber, - Highlighter highlighter) { + Query query) { this.hasFulltextResults = hasFulltextResults; this.path = path; this.pageContent = pageContent; this.annotation = annotation; this.pageNumber = pageNumber; - this.highlighter = highlighter; + this.query = query; } public SearchResult() { this(false, "", "", "", -1, null); } - public SearchResult(String path, String pageContent, String annotation, int pageNumber, Highlighter highlighter) { - this(true, path, pageContent, annotation, pageNumber, highlighter); + public SearchResult(String path, String pageContent, String annotation, int pageNumber, Query query) { + this(true, path, pageContent, annotation, pageNumber, query); } public List getContentResultStringsHtml() { if (contentResultStringsHtml == null) { - return contentResultStringsHtml = getHighlighterFragments(highlighter, LinkedFilesConstants.CONTENT, pageContent); + return contentResultStringsHtml = getHighlighterFragments(query, LinkedFilesConstants.CONTENT, pageContent); } return contentResultStringsHtml; } public List getAnnotationsResultStringsHtml() { if (annotationsResultStringsHtml == null) { - annotationsResultStringsHtml = getHighlighterFragments(highlighter, LinkedFilesConstants.ANNOTATIONS, annotation); + annotationsResultStringsHtml = getHighlighterFragments(query, LinkedFilesConstants.ANNOTATIONS, annotation); } return annotationsResultStringsHtml; } @@ -70,10 +74,20 @@ public int getPageNumber() { return pageNumber; } - private static List getHighlighterFragments(Highlighter highlighter, LinkedFilesConstants field, String content) { - try (TokenStream contentStream = LinkedFilesConstants.LINKED_FILES_ANALYZER.tokenStream(field.toString(), content)) { + /// Each field is highlighted only with the terms the query has for exactly that field, tokenized by that field's analyzer. + /// Otherwise, the lowercasing analyzer of `field` would also mark differently cased occurrences of a case-sensitive term. + private static List getHighlighterFragments(Query query, LinkedFilesConstants field, String content) { + return Stream.of(field.toString(), LinkedFilesConstants.caseSensitiveFieldOf(field.toString())) + .flatMap(fieldName -> getHighlighterFragments(query, fieldName, content).stream()) + .distinct() + .toList(); + } + + private static List getHighlighterFragments(Query query, String field, String content) { + Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("", ""), new QueryScorer(query, field)); + try (TokenStream contentStream = LinkedFilesConstants.LINKED_FILES_ANALYZER.tokenStream(field, content)) { TextFragment[] frags = highlighter.getBestTextFragments(contentStream, content, true, 10); - return Arrays.stream(frags).map(TextFragment::toString).toList(); + return Arrays.stream(frags).filter(frag -> frag.getScore() > 0).map(TextFragment::toString).toList(); } catch (IOException | InvalidTokenOffsetsException _) { return List.of(); } diff --git a/jablib/src/test/java/org/jabref/logic/search/inmemory/InMemoryLuceneSearchBackendTest.java b/jablib/src/test/java/org/jabref/logic/search/inmemory/InMemoryLuceneSearchBackendTest.java index 3d0e061ed529..99e47955cc42 100644 --- a/jablib/src/test/java/org/jabref/logic/search/inmemory/InMemoryLuceneSearchBackendTest.java +++ b/jablib/src/test/java/org/jabref/logic/search/inmemory/InMemoryLuceneSearchBackendTest.java @@ -8,6 +8,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jabref.logic.FilePreferences; import org.jabref.logic.importer.ImportFormatPreferences; @@ -27,6 +28,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Answers; @@ -59,7 +62,26 @@ void searchesLinkedFileContentsWithoutPostgres() throws IOException, URISyntaxEx search("comma", EnumSet.of(SearchFlags.FULLTEXT))); } - /// [Issue 9482](https://github.com/JabRef/jabref/issues/9482): a quotation mark used to make the search throw. + static Stream searchesCaseSensitively() { + return Stream.of( + Arguments.of(Set.of("minimal-sentence-case", "minimal-mixed-case"), "any =! comma"), + Arguments.of(Set.of("minimal-all-upper-case"), "any =! COMMA"), + Arguments.of(Set.of(), "any =! Comma"), + Arguments.of(Set.of("minimal-note-sentence-case"), "any ==! Hello"), + Arguments.of(Set.of("minimal-note-all-upper-case"), "any ==! HELLO"), + Arguments.of(Set.of("minimal-sentence-case", "minimal-mixed-case"), "any =~! comm."), + Arguments.of(Set.of("minimal-all-upper-case"), "any =~! COMM."), + Arguments.of(Set.of(), "any =~! Comm.") + ); + } + + // [utest->req~jabgui.search.fulltext.case-sensitive~1] + @ParameterizedTest + @MethodSource + void searchesCaseSensitively(Set expectedCitationKeys, String searchExpression) throws IOException, URISyntaxException { + assertEquals(expectedCitationKeys, search(searchExpression, EnumSet.of(SearchFlags.FULLTEXT))); + } + @Test void searchesPhraseInLinkedFileContents() throws IOException, URISyntaxException { assertEquals( diff --git a/jablib/src/test/java/org/jabref/logic/search/query/SearchQueryLuceneConversionTest.java b/jablib/src/test/java/org/jabref/logic/search/query/SearchQueryLuceneConversionTest.java index d305f38ffea4..1fbd253be00c 100644 --- a/jablib/src/test/java/org/jabref/logic/search/query/SearchQueryLuceneConversionTest.java +++ b/jablib/src/test/java/org/jabref/logic/search/query/SearchQueryLuceneConversionTest.java @@ -1,9 +1,12 @@ package org.jabref.logic.search.query; +import java.util.EnumSet; import java.util.stream.Stream; +import org.jabref.model.search.SearchFlags; import org.jabref.model.search.query.SearchQuery; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -20,9 +23,11 @@ public static Stream searchConversion() { Arguments.of("term", "any = term"), Arguments.of("term", "any CONTAINS term"), Arguments.of("term", "any MATCHES term"), - Arguments.of("term", "any =! term"), + Arguments.of("(contentCaseSensitive:term OR annotationsCaseSensitive:term)", "any =! term"), Arguments.of("term", "any == term"), - Arguments.of("term", "any ==! term"), + Arguments.of("(contentCaseSensitive:term OR annotationsCaseSensitive:term)", "any ==! term"), + Arguments.of("contentCaseSensitive:Term", "content =! Term"), + Arguments.of("annotationsCaseSensitive:Term", "annotations ==! Term"), Arguments.of("\"two terms\"", "\"two terms\""), Arguments.of("\"two terms\"", "any = \"two terms\""), @@ -37,7 +42,7 @@ public static Stream searchConversion() { Arguments.of("NOT term", "any != term"), Arguments.of("NOT term", "any !== term"), - Arguments.of("NOT term", "any !=! term"), + Arguments.of("NOT (contentCaseSensitive:term OR annotationsCaseSensitive:term)", "any !=! term"), Arguments.of("NOT \"two terms\"", "any != \"two terms\""), Arguments.of("content:image AND NOT annotations:processing", "content = image AND annotations != processing"), @@ -65,4 +70,11 @@ public static Stream searchConversion() { void searchConversion(String expected, String searchExpression) { assertEquals(expected, SearchQueryConversion.searchToLucene(new SearchQuery(searchExpression))); } + + /// The case-sensitivity toggle of the search bar applies to unfielded terms. + @Test + void caseSensitiveSearchBarFlagAppliesToUnfieldedTerm() { + assertEquals("(contentCaseSensitive:Term OR annotationsCaseSensitive:Term)", + SearchQueryConversion.searchToLucene(new SearchQuery("Term", EnumSet.of(SearchFlags.CASE_SENSITIVE)))); + } } diff --git a/jablib/src/test/java/org/jabref/logic/search/sqlbased/SqlBasedLibrarySearcherWithBibFilesTest.java b/jablib/src/test/java/org/jabref/logic/search/sqlbased/SqlBasedLibrarySearcherWithBibFilesTest.java index f19e986e6e5a..00c195e3d055 100644 --- a/jablib/src/test/java/org/jabref/logic/search/sqlbased/SqlBasedLibrarySearcherWithBibFilesTest.java +++ b/jablib/src/test/java/org/jabref/logic/search/sqlbased/SqlBasedLibrarySearcherWithBibFilesTest.java @@ -149,7 +149,15 @@ private static Stream searchLibrary() { Arguments.of(List.of(), "test-library-with-attached-files.bib", "NotExisting", true), Arguments.of(List.of(MINIMAL_NOTE_SENTENCE_CASE, MINIMAL_NOTE_ALL_UPPER_CASE, MINIMAL_NOTE_MIXED_CASE), "test-library-with-attached-files.bib", "world", true), - Arguments.of(List.of(MINIMAL_NOTE_SENTENCE_CASE, MINIMAL_NOTE_ALL_UPPER_CASE, MINIMAL_NOTE_MIXED_CASE), "test-library-with-attached-files.bib", "\"Hello World\"", true) + Arguments.of(List.of(MINIMAL_NOTE_SENTENCE_CASE, MINIMAL_NOTE_ALL_UPPER_CASE, MINIMAL_NOTE_MIXED_CASE), "test-library-with-attached-files.bib", "\"Hello World\"", true), + + // case-sensitive fulltext search + Arguments.of(List.of(MINIMAL_ALL_UPPER_CASE), "test-library-with-attached-files.bib", "any =! COMMA", true), + Arguments.of(List.of(MINIMAL_SENTENCE_CASE, MINIMAL_MIXED_CASE), "test-library-with-attached-files.bib", "any =! comma", true), + Arguments.of(List.of(MINIMAL_NOTE_ALL_UPPER_CASE), "test-library-with-attached-files.bib", "any ==! HELLO", true), + Arguments.of(List.of(MINIMAL_NOTE_SENTENCE_CASE), "test-library-with-attached-files.bib", "any ==! Hello", true), + Arguments.of(List.of(MINIMAL_ALL_UPPER_CASE), "test-library-with-attached-files.bib", "any =~! COMM.", true), + Arguments.of(List.of(MINIMAL_SENTENCE_CASE, MINIMAL_MIXED_CASE), "test-library-with-attached-files.bib", "any =~! comm.", true) ); } diff --git a/jablib/src/test/java/org/jabref/model/search/query/SearchResultTest.java b/jablib/src/test/java/org/jabref/model/search/query/SearchResultTest.java new file mode 100644 index 000000000000..2c3424993698 --- /dev/null +++ b/jablib/src/test/java/org/jabref/model/search/query/SearchResultTest.java @@ -0,0 +1,46 @@ +package org.jabref.model.search.query; + +import java.util.List; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.TermQuery; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SearchResultTest { + + private static final String CONTENT = "This sentence, comma included. THIS SENTENCE, COMMA INCLUDED."; + + @Test + void caseInsensitiveTermHighlightsAllSpellings() { + SearchResult result = new SearchResult("file.pdf", CONTENT, "", 1, new TermQuery(new Term("content", "comma"))); + assertEquals(List.of("This sentence, comma included. THIS SENTENCE, COMMA INCLUDED."), result.getContentResultStringsHtml()); + } + + @Test + void caseSensitiveTermHighlightsOnlyThatSpelling() { + SearchResult result = new SearchResult("file.pdf", CONTENT, "", 1, new TermQuery(new Term("contentCaseSensitive", "comma"))); + assertEquals(List.of("This sentence, comma included. THIS SENTENCE, COMMA INCLUDED."), result.getContentResultStringsHtml()); + } + + @Test + void compoundQueryHighlightsBothKindsOfTerms() { + BooleanQuery query = new BooleanQuery.Builder() + .add(new TermQuery(new Term("content", "sentenc")), BooleanClause.Occur.MUST) + .add(new TermQuery(new Term("contentCaseSensitive", "COMMA")), BooleanClause.Occur.MUST) + .build(); + SearchResult result = new SearchResult("file.pdf", CONTENT, "", 1, query); + assertEquals(List.of( + "This sentence, comma included. THIS SENTENCE, COMMA INCLUDED.", + "This sentence, comma included. THIS SENTENCE, COMMA INCLUDED."), result.getContentResultStringsHtml()); + } + + @Test + void caseSensitiveAnnotationTermHighlightsOnlyThatSpelling() { + SearchResult result = new SearchResult("file.pdf", "", "Hello HELLO", 1, new TermQuery(new Term("annotationsCaseSensitive", "Hello"))); + assertEquals(List.of("Hello HELLO"), result.getAnnotationsResultStringsHtml()); + } +} From bec412cf5c3f62a544e299a8bb4ae4b259185cce Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 22 Sep 2026 14:51:37 +0000 Subject: [PATCH 052/102] New Crowdin updates (#17242) * New translations jabref_en.properties for (French) * New translations jabref_en.properties for (Spanish) * New translations jabref_en.properties for (Arabic) * New translations jabref_en.properties for (Danish) * New translations jabref_en.properties for (German) * New translations jabref_en.properties for (Greek) * New translations jabref_en.properties for (Finnish) * New translations jabref_en.properties for (Italian) * New translations jabref_en.properties for (Japanese) * New translations jabref_en.properties for (Korean) * New translations jabref_en.properties for (Dutch) * New translations jabref_en.properties for (Norwegian) * New translations jabref_en.properties for (Polish) * New translations jabref_en.properties for (Portuguese) * New translations jabref_en.properties for (Russian) * New translations jabref_en.properties for (Swedish) * New translations jabref_en.properties for (Turkish) * New translations jabref_en.properties for (Ukrainian) * New translations jabref_en.properties for (Chinese Simplified) * New translations jabref_en.properties for (Chinese Traditional) * New translations jabref_en.properties for (Vietnamese) * New translations jabref_en.properties for (Portuguese, Brazilian) * New translations jabref_en.properties for (Indonesian) * New translations jabref_en.properties for (Persian) * New translations jabref_en.properties for (Tagalog) --- .../src/main/resources/l10n/JabRef_ar.properties | 2 ++ .../src/main/resources/l10n/JabRef_da.properties | 2 ++ .../src/main/resources/l10n/JabRef_de.properties | 2 ++ .../src/main/resources/l10n/JabRef_el.properties | 2 ++ .../src/main/resources/l10n/JabRef_es.properties | 2 ++ .../src/main/resources/l10n/JabRef_fa.properties | 2 ++ .../src/main/resources/l10n/JabRef_fi.properties | 2 ++ .../src/main/resources/l10n/JabRef_fr.properties | 15 +++++++++++++++ .../src/main/resources/l10n/JabRef_id.properties | 2 ++ .../src/main/resources/l10n/JabRef_it.properties | 15 +++++++++++++++ .../src/main/resources/l10n/JabRef_ja.properties | 2 ++ .../src/main/resources/l10n/JabRef_ko.properties | 2 ++ .../src/main/resources/l10n/JabRef_nl.properties | 2 ++ .../src/main/resources/l10n/JabRef_no.properties | 2 ++ .../src/main/resources/l10n/JabRef_pl.properties | 2 ++ .../src/main/resources/l10n/JabRef_pt.properties | 2 ++ .../main/resources/l10n/JabRef_pt_BR.properties | 2 ++ .../src/main/resources/l10n/JabRef_ru.properties | 2 ++ .../src/main/resources/l10n/JabRef_sv.properties | 2 ++ .../src/main/resources/l10n/JabRef_tl.properties | 2 ++ .../src/main/resources/l10n/JabRef_tr.properties | 2 ++ .../src/main/resources/l10n/JabRef_uk.properties | 2 ++ .../src/main/resources/l10n/JabRef_vi.properties | 2 ++ .../main/resources/l10n/JabRef_zh_CN.properties | 2 ++ .../main/resources/l10n/JabRef_zh_TW.properties | 2 ++ 25 files changed, 76 insertions(+) diff --git a/jablib/src/main/resources/l10n/JabRef_ar.properties b/jablib/src/main/resources/l10n/JabRef_ar.properties index 2da91418b2b6..3a9ed4705aa9 100644 --- a/jablib/src/main/resources/l10n/JabRef_ar.properties +++ b/jablib/src/main/resources/l10n/JabRef_ar.properties @@ -776,6 +776,8 @@ Unable\ to\ clear\ preferences.=تعذر إزالة الإعدادات. # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_da.properties b/jablib/src/main/resources/l10n/JabRef_da.properties index b251a12faa6e..670c5c17a17c 100644 --- a/jablib/src/main/resources/l10n/JabRef_da.properties +++ b/jablib/src/main/resources/l10n/JabRef_da.properties @@ -1022,6 +1022,8 @@ Import\ preferences=Importer indstillinger # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_de.properties b/jablib/src/main/resources/l10n/JabRef_de.properties index 9a25f9dd4690..feac66d1ef87 100644 --- a/jablib/src/main/resources/l10n/JabRef_de.properties +++ b/jablib/src/main/resources/l10n/JabRef_de.properties @@ -3473,6 +3473,8 @@ Pseudonymize\ the\ current\ library\ and\ generate\ a\ .bib\ and\ .csv\ file=Pse Pseudonymizing\ library\ '%0'...=Pseudonymisiere Bibliothek '%0'... Invalid\ output\ file\ type\ provided.=Ungültiger Ausgabedateityp angegeben. +# jabkit git merge-driver + # Quick Settings Quick\ settings=Schnelleinstellungen diff --git a/jablib/src/main/resources/l10n/JabRef_el.properties b/jablib/src/main/resources/l10n/JabRef_el.properties index 0f0b25fe1b13..8f1c4569287a 100644 --- a/jablib/src/main/resources/l10n/JabRef_el.properties +++ b/jablib/src/main/resources/l10n/JabRef_el.properties @@ -1725,6 +1725,8 @@ Related\ articles=Σχετικά άρθρα # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_es.properties b/jablib/src/main/resources/l10n/JabRef_es.properties index 3babfdf92e4d..b0d4cbb72c76 100644 --- a/jablib/src/main/resources/l10n/JabRef_es.properties +++ b/jablib/src/main/resources/l10n/JabRef_es.properties @@ -3040,6 +3040,8 @@ Updating\ PDF\ metadata.=Actualización de metadatos PDF. Pseudonymizing\ library\ '%0'...=Seudonimizando la biblioteca '%0'... Invalid\ output\ file\ type\ provided.=Se proporcionó un tipo de archivo de salida no válido. +# jabkit git merge-driver + # Quick Settings Quick\ settings=Configuraciones rápidas diff --git a/jablib/src/main/resources/l10n/JabRef_fa.properties b/jablib/src/main/resources/l10n/JabRef_fa.properties index 0a1e6a7679e0..efec3b1ff371 100644 --- a/jablib/src/main/resources/l10n/JabRef_fa.properties +++ b/jablib/src/main/resources/l10n/JabRef_fa.properties @@ -723,6 +723,8 @@ Auto\ complete\ enabled.=تکمیل خودکار غیرفعال شد. # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_fi.properties b/jablib/src/main/resources/l10n/JabRef_fi.properties index e875d986180a..f5c9c41ec3ca 100644 --- a/jablib/src/main/resources/l10n/JabRef_fi.properties +++ b/jablib/src/main/resources/l10n/JabRef_fi.properties @@ -1991,6 +1991,8 @@ Pseudonymization=Pseudonymisointi # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings Quick\ settings=Pika-asetukset diff --git a/jablib/src/main/resources/l10n/JabRef_fr.properties b/jablib/src/main/resources/l10n/JabRef_fr.properties index eec052082902..ddbd2a558285 100644 --- a/jablib/src/main/resources/l10n/JabRef_fr.properties +++ b/jablib/src/main/resources/l10n/JabRef_fr.properties @@ -3505,6 +3505,21 @@ Pseudonymize\ the\ current\ library\ and\ generate\ a\ .bib\ and\ .csv\ file=Pse Pseudonymizing\ library\ '%0'...=Pseudonymisation du fichier '%0'... Invalid\ output\ file\ type\ provided.=Type de fichier de sortie fourni invalide. +# jabkit git merge-driver +Unable\ to\ merge\ %0.=Impossible de fusionner %0. +Merged\ %0\ without\ conflicts.=%0 fusionné sans conflit. +%0\ entries\ could\ not\ be\ merged\ automatically\:=%0 entrées n'ont pas pu être fusionnées automatiquement \: +%0\:\ deleted\ in\ CURRENT,\ changed\ in\ OTHER=%0 \: supprimé de ACTUEL, changé dans AUTRE +%0\:\ changed\ in\ CURRENT,\ deleted\ in\ OTHER=%0 \: changé dans ACTUEL, supprimé de AUTRE +%0\:\ added\ on\ both\ sides\ with\ different\ content=%0 \: ajouté des deux côtés avec un contenu différent +%0\:\ changed\ on\ both\ sides\ with\ different\ content=%0 \: supprimé des deux côtés avec un contenu différent +Cannot\ merge\ %0\:\ citation\ keys\ must\ be\ unique.=Impossible de fusionner %0 \: les clés de citation doivent être uniques. +Cannot\ merge\ %0\:\ the\ file\ was\ not\ parsed\ without\ warnings.=Impossible de fusionner %0 \: le fichier n'a pas été analysé sans avertissement. +Cannot\ merge\ %0\:\ an\ entry\ without\ fields\ is\ not\ preserved.=Impossible de fusionner %0 \: une entrée sans champ n'est pas préservée. +Cannot\ merge\ %0\:\ a\ comment\ in\ front\ of\ @Comment\ or\ @Preamble\ is\ not\ preserved.=Impossible de fusionner %0 \: un commentaire devant @Comment ou @Preamble n'est pas préservé. +Cannot\ merge\ %0\:\ a\ custom\ entry\ type\ without\ entries\ is\ not\ preserved.=Impossible de fusionner %0 \: un type d'entrée personnalisé sans entrée n'est pas préservé. +Cannot\ merge\ %0\:\ content\ outside\ of\ entries\ with\ a\ citation\ key\ changed\ in\ OTHER.=Impossible de fusionner %0 \: contenu en dehors des entrées avec une clé de citation modifiée dans OTHER. + # Quick Settings Quick\ settings=Paramétrage rapide diff --git a/jablib/src/main/resources/l10n/JabRef_id.properties b/jablib/src/main/resources/l10n/JabRef_id.properties index 16b196f75c70..f7fd1e468e29 100644 --- a/jablib/src/main/resources/l10n/JabRef_id.properties +++ b/jablib/src/main/resources/l10n/JabRef_id.properties @@ -1486,6 +1486,8 @@ Related\ articles=Artikel terkait # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_it.properties b/jablib/src/main/resources/l10n/JabRef_it.properties index 6602db64fe53..3bca3898ca54 100644 --- a/jablib/src/main/resources/l10n/JabRef_it.properties +++ b/jablib/src/main/resources/l10n/JabRef_it.properties @@ -3440,6 +3440,21 @@ Pseudonymize\ the\ current\ library\ and\ generate\ a\ .bib\ and\ .csv\ file=Pse Pseudonymizing\ library\ '%0'...=Pseudonimizzazione libreria '%0'... Invalid\ output\ file\ type\ provided.=Tipo di file di output fornito non valido. +# jabkit git merge-driver +Unable\ to\ merge\ %0.=Impossibile unire %0. +Merged\ %0\ without\ conflicts.=Unito %0 senza conflitti. +%0\ entries\ could\ not\ be\ merged\ automatically\:=Le voci %0 non possono essere unite automaticamente\: +%0\:\ deleted\ in\ CURRENT,\ changed\ in\ OTHER=%0\: eliminato in CURRENT, modificato in ALTRI +%0\:\ changed\ in\ CURRENT,\ deleted\ in\ OTHER=%0\: modificato in CURRENT, eliminato in ALTRI +%0\:\ added\ on\ both\ sides\ with\ different\ content=%0\: aggiunto su entrambi i lati con contenuti diversi +%0\:\ changed\ on\ both\ sides\ with\ different\ content=%0\: modificato su entrambi i lati con contenuti diversi +Cannot\ merge\ %0\:\ citation\ keys\ must\ be\ unique.=Impossibile unire %0\: le chiavi di citazione devono essere univoche. +Cannot\ merge\ %0\:\ the\ file\ was\ not\ parsed\ without\ warnings.=Impossibile unire %0\: il file non è stato analizzato senza avvisi. +Cannot\ merge\ %0\:\ an\ entry\ without\ fields\ is\ not\ preserved.=Impossibile unire %0\: una voce senza campi non è conservata. +Cannot\ merge\ %0\:\ a\ comment\ in\ front\ of\ @Comment\ or\ @Preamble\ is\ not\ preserved.=Impossibile unire %0\: un commento di fronte a @Comment o @Preamble non è conservato. +Cannot\ merge\ %0\:\ a\ custom\ entry\ type\ without\ entries\ is\ not\ preserved.=Impossibile unire %0\: un tipo di voce personalizzata senza voci non è conservato. +Cannot\ merge\ %0\:\ content\ outside\ of\ entries\ with\ a\ citation\ key\ changed\ in\ OTHER.=Impossibile unire %0\: contenuto al di fuori delle voci con una chiave di citazione cambiata in ALTRA. + # Quick Settings Quick\ settings=Impostazioni rapide diff --git a/jablib/src/main/resources/l10n/JabRef_ja.properties b/jablib/src/main/resources/l10n/JabRef_ja.properties index 0ac48e9eb507..c375eea37c59 100644 --- a/jablib/src/main/resources/l10n/JabRef_ja.properties +++ b/jablib/src/main/resources/l10n/JabRef_ja.properties @@ -2184,6 +2184,8 @@ Related\ articles=関連文献 # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_ko.properties b/jablib/src/main/resources/l10n/JabRef_ko.properties index 9bb101ec0efa..11e9081ce8f3 100644 --- a/jablib/src/main/resources/l10n/JabRef_ko.properties +++ b/jablib/src/main/resources/l10n/JabRef_ko.properties @@ -2046,6 +2046,8 @@ Related\ articles=관련 글 # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_nl.properties b/jablib/src/main/resources/l10n/JabRef_nl.properties index ec5a29914948..413d6fac6cf3 100644 --- a/jablib/src/main/resources/l10n/JabRef_nl.properties +++ b/jablib/src/main/resources/l10n/JabRef_nl.properties @@ -2267,6 +2267,8 @@ Related\ articles=Gerelateerde artikelen # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_no.properties b/jablib/src/main/resources/l10n/JabRef_no.properties index df77e6b2d1a3..28c8d36d6884 100644 --- a/jablib/src/main/resources/l10n/JabRef_no.properties +++ b/jablib/src/main/resources/l10n/JabRef_no.properties @@ -1105,6 +1105,8 @@ Link=Lenke # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_pl.properties b/jablib/src/main/resources/l10n/JabRef_pl.properties index 2ee3471e1c25..8d089a7c9538 100644 --- a/jablib/src/main/resources/l10n/JabRef_pl.properties +++ b/jablib/src/main/resources/l10n/JabRef_pl.properties @@ -1775,6 +1775,8 @@ Exclude=Wyklucz # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_pt.properties b/jablib/src/main/resources/l10n/JabRef_pt.properties index d718d8f3bfd8..8ab858de754d 100644 --- a/jablib/src/main/resources/l10n/JabRef_pt.properties +++ b/jablib/src/main/resources/l10n/JabRef_pt.properties @@ -1323,6 +1323,8 @@ Related\ articles=Artigos relacionados # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_pt_BR.properties b/jablib/src/main/resources/l10n/JabRef_pt_BR.properties index 5aa9339b7932..abc812e075ba 100644 --- a/jablib/src/main/resources/l10n/JabRef_pt_BR.properties +++ b/jablib/src/main/resources/l10n/JabRef_pt_BR.properties @@ -3034,6 +3034,8 @@ Pseudonymize\ the\ current\ library\ and\ generate\ a\ .bib\ and\ .csv\ file=Pse Pseudonymizing\ library\ '%0'...=Pseudonimizando biblioteca '%0'... Invalid\ output\ file\ type\ provided.=Tipo de arquivo de saída fornecido inválido. +# jabkit git merge-driver + # Quick Settings Quick\ settings=Configurações rápidas diff --git a/jablib/src/main/resources/l10n/JabRef_ru.properties b/jablib/src/main/resources/l10n/JabRef_ru.properties index a6aaa859358f..576d38c6f92c 100644 --- a/jablib/src/main/resources/l10n/JabRef_ru.properties +++ b/jablib/src/main/resources/l10n/JabRef_ru.properties @@ -2797,6 +2797,8 @@ Click\ the\ "Get\ fulltext"\ button\ (second\ button\ with\ a\ download\ icon)\ # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_sv.properties b/jablib/src/main/resources/l10n/JabRef_sv.properties index 96ac9230b6d4..e00e0b96a36f 100644 --- a/jablib/src/main/resources/l10n/JabRef_sv.properties +++ b/jablib/src/main/resources/l10n/JabRef_sv.properties @@ -1461,6 +1461,8 @@ Warning\:\ The\ selected\ directory\ is\ not\ a\ valid\ directory.=Varning\: Den # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_tl.properties b/jablib/src/main/resources/l10n/JabRef_tl.properties index 234a89837c82..7f2a8c6f4fac 100644 --- a/jablib/src/main/resources/l10n/JabRef_tl.properties +++ b/jablib/src/main/resources/l10n/JabRef_tl.properties @@ -1228,6 +1228,8 @@ Related\ articles=Kaugnay na mga artikulo # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_tr.properties b/jablib/src/main/resources/l10n/JabRef_tr.properties index a32df3a58f0c..9dc3f694c37d 100644 --- a/jablib/src/main/resources/l10n/JabRef_tr.properties +++ b/jablib/src/main/resources/l10n/JabRef_tr.properties @@ -2796,6 +2796,8 @@ More\ options...=Daha fazla seçenekler... # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_uk.properties b/jablib/src/main/resources/l10n/JabRef_uk.properties index 87051f0d0cf2..4b8388a7a059 100644 --- a/jablib/src/main/resources/l10n/JabRef_uk.properties +++ b/jablib/src/main/resources/l10n/JabRef_uk.properties @@ -726,6 +726,8 @@ Could\ not\ export\ preferences=Не вдалося експортувати н # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_vi.properties b/jablib/src/main/resources/l10n/JabRef_vi.properties index 85d64e1819ce..7d3ce98969d0 100644 --- a/jablib/src/main/resources/l10n/JabRef_vi.properties +++ b/jablib/src/main/resources/l10n/JabRef_vi.properties @@ -1050,6 +1050,8 @@ This\ could\ indicate\ that\ JabRef\ did\ not\ shut\ down\ cleanly\ last\ time\ # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons diff --git a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties index 8d95c399454e..b1bccaba0ca3 100644 --- a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties @@ -3252,6 +3252,8 @@ Pseudonymize\ the\ current\ library\ and\ generate\ a\ .bib\ and\ .csv\ file=匿 Pseudonymizing\ library\ '%0'...=正在匿名化库 '%0'... Invalid\ output\ file\ type\ provided.=提供的输出文件类型无效。 +# jabkit git merge-driver + # Quick Settings Quick\ settings=快速设置 diff --git a/jablib/src/main/resources/l10n/JabRef_zh_TW.properties b/jablib/src/main/resources/l10n/JabRef_zh_TW.properties index d1ceed322040..8d5a96da3509 100644 --- a/jablib/src/main/resources/l10n/JabRef_zh_TW.properties +++ b/jablib/src/main/resources/l10n/JabRef_zh_TW.properties @@ -1058,6 +1058,8 @@ Related\ articles=相關文章 # Pseudonymizing via CLI +# jabkit git merge-driver + # Quick Settings # Quick Settings Buttons From 0a1359b2c93d0e73146cb42914c5ad58f30016c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:05:25 +0000 Subject: [PATCH 053/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace-exporter-specobject (#17248) Bumps [org.itsallcode.openfasttrace:openfasttrace-exporter-specobject](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace-exporter-specobject dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build-logic/build.gradle.kts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index d6d35888b2e0..a02c31e71ce5 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -29,8 +29,8 @@ dependencies { implementation("org.gradlex:jvm-dependency-conflict-resolution:2.5") implementation("org.gradle.toolchains:foojay-resolver:1.0.0") implementation("org.itsallcode:openfasttrace-gradle:3.2.0") - implementation("org.itsallcode.openfasttrace:openfasttrace-api:4.9.0") - implementation("org.itsallcode.openfasttrace:openfasttrace-core:4.9.0") - implementation("org.itsallcode.openfasttrace:openfasttrace-exporter-specobject:4.9.0") - implementation("org.itsallcode.openfasttrace:openfasttrace:4.9.0") + implementation("org.itsallcode.openfasttrace:openfasttrace-api:4.10.0") + implementation("org.itsallcode.openfasttrace:openfasttrace-core:4.10.0") + implementation("org.itsallcode.openfasttrace:openfasttrace-exporter-specobject:4.10.0") + implementation("org.itsallcode.openfasttrace:openfasttrace:4.10.0") } From 17d04e5d12b3d2763269d71671c7e028656d1c1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:07:54 +0000 Subject: [PATCH 054/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace-api (#17244) Bumps [org.itsallcode.openfasttrace:openfasttrace-api](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace-api dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 18e5f46f0af0076f9ff23c9095097a030ba108a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:09:07 +0000 Subject: [PATCH 055/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace (#17243) Bumps [org.itsallcode.openfasttrace:openfasttrace](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 617ad670cac22f62f5ad22553fc2edb1fa9c623e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:09:11 +0000 Subject: [PATCH 056/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace-exporter-specobject (#17245) Bumps [org.itsallcode.openfasttrace:openfasttrace-exporter-specobject](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace-exporter-specobject dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 1f4c623af7294866fda023e364d6b1c8617d10fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:14:48 +0000 Subject: [PATCH 057/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace-api (#17249) Bumps [org.itsallcode.openfasttrace:openfasttrace-api](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace-api dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 80e66fdc372e207e496f1ef2ff31cc54cf3486e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:17:52 +0000 Subject: [PATCH 058/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace (#17247) Bumps [org.itsallcode.openfasttrace:openfasttrace](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 69daa71e2904e364461744d7c45c48727b9c0f28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:20:42 +0000 Subject: [PATCH 059/102] Chore(deps): Bump org.itsallcode.openfasttrace:openfasttrace-core (#17246) Bumps [org.itsallcode.openfasttrace:openfasttrace-core](https://github.com/itsallcode/openfasttrace) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/itsallcode/openfasttrace/releases) - [Commits](https://github.com/itsallcode/openfasttrace/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: org.itsallcode.openfasttrace:openfasttrace-core dependency-version: 4.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 02e5e0200ca89a3434933474d4271ac0f56f6671 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:21:13 +0000 Subject: [PATCH 060/102] Chore(deps): Bump org.hibernate.validator:hibernate-validator (#17252) Bumps [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator) from 9.1.3.Final to 9.1.4.Final. - [Release notes](https://github.com/hibernate/hibernate-validator/releases) - [Changelog](https://github.com/hibernate/hibernate-validator/blob/9.1.4.Final/changelog.md) - [Commits](https://github.com/hibernate/hibernate-validator/compare/9.1.3.Final...9.1.4.Final) --- updated-dependencies: - dependency-name: org.hibernate.validator:hibernate-validator dependency-version: 9.1.4.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 5fbcfec4a6e9..17eeed5c6a4d 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -135,7 +135,7 @@ dependencies.constraints { api("org.glassfish.hk2:hk2-utils:4.0.2") api("org.glassfish.jaxb:jaxb-runtime:4.0.9") api("org.hamcrest:hamcrest:3.0") - api("org.hibernate.validator:hibernate-validator:9.1.3.Final") + api("org.hibernate.validator:hibernate-validator:9.1.4.Final") api("org.hisp.dhis:json-tree:1.9.5") api("org.jabref:afterburner.fx:2.0.0") api("org.jabref:easybind:2.3.0") From 3de7bc9b69125ce06c1033ebd56cde5f93e31979 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:55:17 +0000 Subject: [PATCH 061/102] Chore(deps): Bump org.itsallcode:openfasttrace-gradle from 3.2.0 to 3.3.0 in /build-logic (#17250) * Chore(deps): Bump org.itsallcode:openfasttrace-gradle in /build-logic Bumps org.itsallcode:openfasttrace-gradle from 3.2.0 to 3.3.0. --- updated-dependencies: - dependency-name: org.itsallcode:openfasttrace-gradle dependency-version: 3.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * remove workaround for openfassttrace --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christoph --- build-logic/build.gradle.kts | 2 +- ...adle.feature.requirementtracing.gradle.kts | 108 +----------------- 2 files changed, 3 insertions(+), 107 deletions(-) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index a02c31e71ce5..323bd22345c9 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { implementation("org.gradlex:java-module-testing:1.8.1") implementation("org.gradlex:jvm-dependency-conflict-resolution:2.5") implementation("org.gradle.toolchains:foojay-resolver:1.0.0") - implementation("org.itsallcode:openfasttrace-gradle:3.2.0") + implementation("org.itsallcode:openfasttrace-gradle:3.4.0") implementation("org.itsallcode.openfasttrace:openfasttrace-api:4.10.0") implementation("org.itsallcode.openfasttrace:openfasttrace-core:4.10.0") implementation("org.itsallcode.openfasttrace:openfasttrace-exporter-specobject:4.10.0") diff --git a/build-logic/src/main/kotlin/org.jabref.gradle.feature.requirementtracing.gradle.kts b/build-logic/src/main/kotlin/org.jabref.gradle.feature.requirementtracing.gradle.kts index 67446ee01a9c..67e61d4704ad 100644 --- a/build-logic/src/main/kotlin/org.jabref.gradle.feature.requirementtracing.gradle.kts +++ b/build-logic/src/main/kotlin/org.jabref.gradle.feature.requirementtracing.gradle.kts @@ -1,107 +1,3 @@ -import java.io.File -import java.util.Locale - -import org.gradle.api.Project -import org.gradle.api.plugins.ExtensionAware -import org.gradle.kotlin.dsl.getByType -import org.gradle.kotlin.dsl.register -import org.itsallcode.openfasttrace.api.core.ItemStatus -import org.itsallcode.openfasttrace.gradle.config.TagPathConfiguration -import org.itsallcode.openfasttrace.gradle.config.TracingConfig -import org.itsallcode.openfasttrace.gradle.task.CollectTask -import org.itsallcode.openfasttrace.gradle.task.TraceTask -import org.itsallcode.openfasttrace.gradle.task.config.SerializableTagPathConfig - -val taskGroupName = "trace" -val requirementConfigName = "oftRequirementConfig" - -allprojects { - val tracingConfig = extensions.create("requirementTracing", TracingConfig::class.java, project) - (tracingConfig as ExtensionAware).extensions.create("tags", TagPathConfiguration::class.java, project) -} - -gradle.projectsEvaluated { - val collectTask = tasks.register("collectRequirements") { - group = taskGroupName - description = "Collect requirements and generate specobject file" - inputDirectories.set(getAllInputDirectories(rootProject.allprojects)) - outputFile.set(rootProject.layout.buildDirectory.file("reports/requirements.xml")) - pathConfig.set(getPathConfig(rootProject.allprojects)) - } - - tasks.register("traceRequirements") { - group = taskGroupName - description = "Trace requirements and generate tracing report" - dependsOn(collectTask) - - val tracingConfig = rootProject.getTracingConfig() - failBuild.set(tracingConfig.failBuild) - requirementsFile.set(collectTask.flatMap { task -> task.outputFile }) - - if (tracingConfig.reportFile.isPresent) { - outputFile.set(tracingConfig.reportFile) - } else { - val extension = if (tracingConfig.reportFormat.get() == "html") "html" else "txt" - outputFile.set(rootProject.layout.buildDirectory.file("reports/tracing.$extension")) - } - - reportVerbosity.set(tracingConfig.reportVerbosity) - reportFormat.set(tracingConfig.reportFormat) - importedRequirements.set(getImportedRequirements(rootProject.allprojects)) - filteredArtifactTypes.set(tracingConfig.filteredArtifactTypes) - filteredTags.set(tracingConfig.filteredTags) - filterAcceptsItemsWithoutTag.set(tracingConfig.filterAcceptsItemsWithoutTag) - filterWantedStatuses.set(getWantedStatuses(tracingConfig)) - detailsSectionDisplay.set(tracingConfig.detailsSectionDisplay) - } -} - -fun Project.getTracingConfig(): TracingConfig = extensions.getByType() - -fun getAllInputDirectories(allProjects: Set): Set = allProjects - .flatMap { project -> project.getTracingConfig().inputDirectories.files } - .toSet() - -fun getImportedRequirements(allProjects: Set): Set = allProjects - .flatMap(::getImportedRequirements) - .toSet() - -fun getImportedRequirements(project: Project): Set { - val importedRequirements = project.getTracingConfig().importedRequirements.orNull.orEmpty() - if (importedRequirements.isEmpty()) { - return emptySet() - } - - val configuration = project.configurations.findByName(requirementConfigName) - ?: project.configurations.create(requirementConfigName) - - importedRequirements.forEach { dependency -> - project.dependencies.add(requirementConfigName, dependency) - } - - return configuration.files -} - -fun getPathConfig(allProjects: Set): List = allProjects - .mapNotNull(::getTagPathConfig) - -fun getTagPathConfig(project: Project): SerializableTagPathConfig? { - val tagPathConfig = project.getTracingConfig().tagPathConfig - if (tagPathConfig.pathConfig.isEmpty()) { - return null - } - return SerializableTagPathConfig(tagPathConfig) -} - -fun getWantedStatuses(tracingConfig: TracingConfig): Set = - tracingConfig.filterWantedStatuses.getOrElse(emptySet()).map(::convertStatus).toSet() - -fun convertStatus(value: String): ItemStatus = try { - ItemStatus.valueOf(value.uppercase(Locale.ROOT)) -} catch (exception: IllegalArgumentException) { - val validStatuses = ItemStatus.values().joinToString(", ") { status -> status.name } - throw IllegalArgumentException( - "Invalid status '$value'. Valid statuses are: $validStatuses", - exception, - ) +plugins { + id("org.itsallcode.openfasttrace") } From 0e6ae6ed0481265fe09594e75f73fe002ac5bfe9 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 22 Sep 2026 19:59:21 +0000 Subject: [PATCH 062/102] Update Grobid url from http to https (#17254) * Update Grobid url from http to https * fix test * fix(preferences): migrate Grobid URL Migrate the legacy endpoint during GUI and JabKit startup without changing custom URLs. * public no longer needed in main * changelog * restore public * Apply suggestion from @subhramit --------- Co-authored-by: Subhramit Basu --- CHANGELOG.md | 1 + .../migrations/PreferencesMigrations.java | 2 ++ .../org/jabref/toolkit/JabKitLauncher.java | 4 ++- .../importer/util/GrobidPreferences.java | 2 +- .../logic/importer/util/GrobidService.java | 9 ++++-- .../logic/preferences/GrobidUrlMigration.java | 19 ++++++++++++ .../pdf/PdfMergeMetadataImporterTest.java | 2 +- .../importer/util/GrobidServiceTest.java | 10 +++--- .../preferences/PreferenceMigrationTest.java | 31 +++++++++++++++++++ 9 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 jablib/src/main/java/org/jabref/logic/preferences/GrobidUrlMigration.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d380fd1bfb60..93eef956b96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed +- We re-enabled the [Grobid](https://github.com/grobidOrg/grobid) citation fetcher again. It is now reachable via https. [#16668](https://github.com/JabRef/jabref/issues/16668) - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) diff --git a/jabgui/src/main/java/org/jabref/migrations/PreferencesMigrations.java b/jabgui/src/main/java/org/jabref/migrations/PreferencesMigrations.java index d15e8063216e..11a6c61425b2 100644 --- a/jabgui/src/main/java/org/jabref/migrations/PreferencesMigrations.java +++ b/jabgui/src/main/java/org/jabref/migrations/PreferencesMigrations.java @@ -27,6 +27,7 @@ import org.jabref.logic.cleanup.CleanupPreferences; import org.jabref.logic.cleanup.FieldFormatterCleanupActions; import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.GrobidUrlMigration; import org.jabref.logic.preferences.JabRefCliPreferences; import org.jabref.logic.preview.TextBasedPreviewLayout; import org.jabref.logic.shared.security.Password; @@ -101,6 +102,7 @@ public static void runMigrations(JabRefGuiPreferences preferences) { upgradeTheme(preferences); migrateFileAnnotationsTabVisibility(preferences); upgradeEntryEditorCustomTabs(preferences); + GrobidUrlMigration.migrate(preferences); } /// Up to and including v6.0-alpha.6, custom entry editor tabs were stored in two parallel numbered diff --git a/jabkit/src/main/java/org/jabref/toolkit/JabKitLauncher.java b/jabkit/src/main/java/org/jabref/toolkit/JabKitLauncher.java index 9be6abbb68ee..749d632ab453 100644 --- a/jabkit/src/main/java/org/jabref/toolkit/JabKitLauncher.java +++ b/jabkit/src/main/java/org/jabref/toolkit/JabKitLauncher.java @@ -25,6 +25,7 @@ import org.jabref.logic.net.ssl.SSLPreferences; import org.jabref.logic.net.ssl.TrustStoreManager; import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.preferences.GrobidUrlMigration; import org.jabref.logic.preferences.JabRefCliPreferences; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.util.BuildInfo; @@ -54,7 +55,7 @@ /// It does not open any GUI. /// For the GUI application see [org.jabref.Launcher]. /// -/// Does not do any preference migrations. +/// Does not run GUI-specific preference migrations. public class JabKitLauncher { // J.U.L. bridge to SLF4J must be initialized before any logger is created, see initLogging() private static Logger LOGGER; @@ -70,6 +71,7 @@ public static void main(String[] args) { try { final JabRefCliPreferences preferences = JabRefCliPreferences.getInstance(); + GrobidUrlMigration.migrate(preferences); Injector.setModelOrService(CliPreferences.class, preferences); BuildInfo buildInfo = new BuildInfo(); diff --git a/jablib/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java b/jablib/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java index 4386cbc837c6..a9d736e9f0ee 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/importer/util/GrobidPreferences.java @@ -15,7 +15,7 @@ private GrobidPreferences() { this( false, // Grobid enabled false, // Grobid use asked - "http://grobid.jabref.org:8070" // Grobid URL + "https://grobid.jabref.org" // Grobid URL ); } diff --git a/jablib/src/main/java/org/jabref/logic/importer/util/GrobidService.java b/jablib/src/main/java/org/jabref/logic/importer/util/GrobidService.java index d2d1a1918b49..f05d5cf1994a 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/util/GrobidService.java +++ b/jablib/src/main/java/org/jabref/logic/importer/util/GrobidService.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Optional; import org.jabref.logic.importer.ImportFormatPreferences; @@ -20,8 +21,6 @@ /// Implements an API to a GROBID server, as described at /// https://grobid.readthedocs.io/en/latest/Grobid-service/#grobid-web-services /// -/// Note: Currently a custom GROBID server is used... -/// https://github.com/NikodemKch/grobid /// /// The methods are structured to match the GROBID server api. /// Each method corresponds to a GROBID service request. Only the ones already used are already implemented. @@ -67,7 +66,11 @@ public Optional processCitation(String rawCitation, ImportFormatPrefer String httpResponse = response.body(); LOGGER.debug("raw citation -> response: {}, {}", rawCitation, httpResponse); - if (httpResponse == null || "@misc{-1,\n author = {}\n}\n".equals(httpResponse) || httpResponse.equals("@misc{-1,\n author = {" + rawCitation + "}\n}\n")) { // This filters empty BibTeX entries + if (""" + @misc{-1, + author = {} + } + """.equals(httpResponse) || httpResponse.equals("@misc{%s,\n author = {%s}\n}\n".formatted(rawCitation.toLowerCase(Locale.ROOT), rawCitation))) { // This filters empty BibTeX entries, lowercase!! throw new IOException("The GROBID server response does not contain anything."); } diff --git a/jablib/src/main/java/org/jabref/logic/preferences/GrobidUrlMigration.java b/jablib/src/main/java/org/jabref/logic/preferences/GrobidUrlMigration.java new file mode 100644 index 000000000000..55b85ab66af3 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/preferences/GrobidUrlMigration.java @@ -0,0 +1,19 @@ +package org.jabref.logic.preferences; + +import org.jabref.logic.importer.util.GrobidPreferences; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class GrobidUrlMigration { + private static final String LEGACY_GROBID_URL = "http://grobid.jabref.org:8070"; + + private GrobidUrlMigration() { + } + + public static void migrate(JabRefCliPreferences preferences) { + if (LEGACY_GROBID_URL.equals(preferences.get(JabRefCliPreferences.GROBID_URL, ""))) { + preferences.put(JabRefCliPreferences.GROBID_URL, GrobidPreferences.getDefault().getGrobidURL()); + } + } +} diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/PdfMergeMetadataImporterTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/PdfMergeMetadataImporterTest.java index 494ff9449cdc..79947969d02c 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/PdfMergeMetadataImporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/pdf/PdfMergeMetadataImporterTest.java @@ -40,7 +40,7 @@ class PdfMergeMetadataImporterTest { void setUp() { GrobidPreferences grobidPreferences = mock(GrobidPreferences.class, Answers.RETURNS_DEEP_STUBS); when(grobidPreferences.isGrobidEnabled()).thenReturn(true); - when(grobidPreferences.getGrobidURL()).thenReturn("http://grobid.jabref.org:8070"); + when(grobidPreferences.getGrobidURL()).thenReturn("https://grobid.jabref.org"); ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); when(importFormatPreferences.fieldPreferences().getNonWrappableFields()).thenReturn(FXCollections.emptyObservableList()); diff --git a/jablib/src/test/java/org/jabref/logic/importer/util/GrobidServiceTest.java b/jablib/src/test/java/org/jabref/logic/importer/util/GrobidServiceTest.java index c8aee8512638..609c9a7a02ad 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/util/GrobidServiceTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/util/GrobidServiceTest.java @@ -39,15 +39,15 @@ static void setup() { GrobidPreferences grobidPreferences = new GrobidPreferences( true, false, - "http://grobid.jabref.org:8070"); + "https://grobid.jabref.org"); grobidService = new GrobidService(grobidPreferences); } @Test void processValidCitationTest() throws IOException, ParseException { BibEntry exampleBibEntry = new BibEntry(StandardEntryType.Article) - .withCitationKey("-1") - .withField(StandardField.AUTHOR, "Derwing, Tracey and Rossiter, Marian and Munro, Murray") + .withCitationKey("derwing2002teaching") + .withField(StandardField.AUTHOR, "Derwing, Tracey M. and Rossiter, Marian J. and Munro, Murray J.") .withField(StandardField.TITLE, "Teaching Native Speakers to Listen to Foreign-accented Speech") .withField(StandardField.JOURNAL, "Journal of Multilingual and Multicultural Development") .withField(StandardField.PUBLISHER, "Informa UK Limited") @@ -85,7 +85,7 @@ void failsWhenGrobidDisabled() { GrobidPreferences importSettingsWithGrobidDisabled = new GrobidPreferences( false, false, - "http://grobid.jabref.org:8070"); + "https://grobid.jabref.org"); assertThrows(UnsupportedOperationException.class, () -> new GrobidService(importSettingsWithGrobidDisabled)); } @@ -103,7 +103,7 @@ void processPdfTest() throws IOException, ParseException, URISyntaxException { @Test void extractsReferencesFromPdf() throws IOException, ParseException, URISyntaxException { BibEntry ref1 = new BibEntry(StandardEntryType.Article) - .withField(StandardField.AUTHOR, "Kopp, O") + .withField(StandardField.AUTHOR, "Kopp, O.") .withField(StandardField.ADDRESS, "Berlin; Heidelberg") .withField(StandardField.DATE, "2013") .withField(StandardField.JOURNAL, "All links were last followed on October") diff --git a/jablib/src/test/java/org/jabref/logic/preferences/PreferenceMigrationTest.java b/jablib/src/test/java/org/jabref/logic/preferences/PreferenceMigrationTest.java index 03d3e131bf24..6123f088f0ca 100644 --- a/jablib/src/test/java/org/jabref/logic/preferences/PreferenceMigrationTest.java +++ b/jablib/src/test/java/org/jabref/logic/preferences/PreferenceMigrationTest.java @@ -20,14 +20,18 @@ class PreferenceMigrationTest { private static final String AI_ANSWER_ENGINE_KIND = "aiAnswerEngineKind"; private static final String AI_RESPONSE_ENGINE_KIND = "aiResponseEngineKind"; private static final String AI_EMBEDDING_MODEL = "aiEmbeddingModel"; + private static final String LEGACY_GROBID_URL = "http://grobid.jabref.org:8070"; + private static final String HTTPS_GROBID_URL = "https://grobid.jabref.org"; private static final String UNUSED_DEFAULT_VALUE = ""; private boolean hasLegacyResponseEngineKindValue; private boolean hasResponseEngineKindValue; private boolean hasEmbeddingModelValue; + private boolean hasGrobidUrlValue; private String legacyResponseEngineKindValue; private String responseEngineKindValue; private String embeddingModelValue; + private String grobidUrlValue; @BeforeEach void setUp() { @@ -36,13 +40,16 @@ void setUp() { hasLegacyResponseEngineKindValue = preferences.hasKey(AI_ANSWER_ENGINE_KIND); hasResponseEngineKindValue = preferences.hasKey(AI_RESPONSE_ENGINE_KIND); hasEmbeddingModelValue = preferences.hasKey(AI_EMBEDDING_MODEL); + hasGrobidUrlValue = preferences.hasKey(JabRefCliPreferences.GROBID_URL); legacyResponseEngineKindValue = preferences.get(AI_ANSWER_ENGINE_KIND, UNUSED_DEFAULT_VALUE); responseEngineKindValue = preferences.get(AI_RESPONSE_ENGINE_KIND, UNUSED_DEFAULT_VALUE); embeddingModelValue = preferences.get(AI_EMBEDDING_MODEL, UNUSED_DEFAULT_VALUE); + grobidUrlValue = preferences.get(JabRefCliPreferences.GROBID_URL, UNUSED_DEFAULT_VALUE); preferences.remove(AI_ANSWER_ENGINE_KIND); preferences.remove(AI_RESPONSE_ENGINE_KIND); preferences.remove(AI_EMBEDDING_MODEL); + preferences.remove(JabRefCliPreferences.GROBID_URL); } @AfterEach @@ -52,6 +59,7 @@ void tearDown() { restorePreference(preferences, AI_ANSWER_ENGINE_KIND, hasLegacyResponseEngineKindValue, legacyResponseEngineKindValue); restorePreference(preferences, AI_RESPONSE_ENGINE_KIND, hasResponseEngineKindValue, responseEngineKindValue); restorePreference(preferences, AI_EMBEDDING_MODEL, hasEmbeddingModelValue, embeddingModelValue); + restorePreference(preferences, JabRefCliPreferences.GROBID_URL, hasGrobidUrlValue, grobidUrlValue); } @Test @@ -100,6 +108,29 @@ void getAiPreferencesMigratesOtherLegacyEmbeddingModelToDefault() { assertEquals(AiDefaultExpertSettings.EMBEDDING_MODEL, preferences.get(AI_EMBEDDING_MODEL, UNUSED_DEFAULT_VALUE)); } + @Test + void migrateGrobidUrlMigratesLegacyUrl() { + JabRefCliPreferences preferences = new JabRefCliPreferences(); + preferences.put(JabRefCliPreferences.GROBID_URL, LEGACY_GROBID_URL); + + GrobidUrlMigration.migrate(preferences); + + assertEquals(HTTPS_GROBID_URL, preferences.get(JabRefCliPreferences.GROBID_URL, UNUSED_DEFAULT_VALUE)); + assertEquals(HTTPS_GROBID_URL, preferences.getGrobidPreferences().getGrobidURL()); + } + + @Test + void migrateGrobidUrlKeepsCustomUrl() { + String customGrobidUrl = "https://grobid.example.org"; + JabRefCliPreferences preferences = new JabRefCliPreferences(); + preferences.put(JabRefCliPreferences.GROBID_URL, customGrobidUrl); + + GrobidUrlMigration.migrate(preferences); + + assertEquals(customGrobidUrl, preferences.get(JabRefCliPreferences.GROBID_URL, UNUSED_DEFAULT_VALUE)); + assertEquals(customGrobidUrl, preferences.getGrobidPreferences().getGrobidURL()); + } + private void restorePreference(JabRefCliPreferences preferences, String key, boolean hasValue, String value) { if (!hasValue) { preferences.remove(key); From 6bdb5e7dde0a3596556228e237f5bfddb20f086a Mon Sep 17 00:00:00 2001 From: InAnYan Date: Wed, 23 Sep 2026 03:39:47 +0000 Subject: [PATCH 063/102] Clarify requirements documentation (#17161) * Clarify requirements documentation * Apply batched suggestions from code review Co-authored-by: InAnYan --------- Co-authored-by: Ruslan Co-authored-by: Oliver Kopp --- docs/code-howtos/requirements.md | 218 +++++++++++++++++++++++++++++++ docs/requirements/index.md | 69 +--------- 2 files changed, 221 insertions(+), 66 deletions(-) create mode 100644 docs/code-howtos/requirements.md diff --git a/docs/code-howtos/requirements.md b/docs/code-howtos/requirements.md new file mode 100644 index 000000000000..51cd70ac647a --- /dev/null +++ b/docs/code-howtos/requirements.md @@ -0,0 +1,218 @@ +--- +parent: Code Howtos +--- +# Requirements + +In JabRef, we use [OpenFastTrace](https://github.com/itsallcode/openfasttrace) to track requirements. This is a handy tool that allows us to express our ideas and link them to the code. It supports both backward and forward tracing. For example, questions such as "How is the requirement implemented?" (forward trace) or "Which requirement led to this implementation?" (backward trace) are easy to answer. + +## Example + +Imagine you are developing Git features in JabRef and want to ensure that a PAT (Personal Access Token) is verified and has sufficient access rights. This is a very useful feature. With OFT, you can write a human-readable requirement in Markdown like this: + +```markdown +## GitHub personal access token verification +`req~git.share.personal-access-token-verification~1` + +The GitHub sharing dialog must allow users to verify that their personal access token has push access to the configured GitHub repository before sharing a library. + +Needs: impl +``` + +The power of OFT is that it actually **tracks** requirements and can report which requirements were not implemented. + +To link the requirement to the code, you write this in the code: + +```java + public void checkGitHubAccess() { + // [impl->req~git.share.personal-access-token-verification~1] + BackgroundTask + .wrap(() -> gitHubRepositoryAccessChecker.check(repositoryUrlProperty.get().trim(), gitPreferences.getUsername(), gitPreferences.getPat())) + .onSuccess(this::showGitHubAccessResult) + .onFailure(e -> { + LOGGER.debug("Could not check GitHub repository access", e); + dialogService.showErrorDialogAndWait( + Localization.lang("GitHub access"), + Localization.lang("Could not connect to GitHub. Please check your network connection and try again.")); + }) + .executeWith(taskExecutor); + } +``` + +After that, running `traceRequirements` will succeed. + +Tracing requirements is useful because it helps us: + +- link ideas and human-oriented descriptions to the code; +- keep implementation and tests (and other artifacts) aligned; +- track requirement revisions so that updates in Markdown also require updates to the relevant code; +- add context about why the code was written or why a requirement is needed, including links to issues; +- provide a useful aid for AI agents; +- prevent people from forgetting or deleting important requirements or code as the project evolves. + +## How to use OFT + +### Tooling + +OFT is available on GitHub at , where you can use the CLI. + +It is highly recommended to use the LSP server and related extensions (not official and not affiliated with OFT): (with VS Code and IntelliJ integrations). + +We also integrate it with Gradle. You can run the `traceRequirements` task, which produces `build/reports/tracing.txt` with a detailed report. + +We also have automated CI checks to verify requirement coverage; uncovered requirements are treated as an error. + +We recommend using VS Code with the `markdownlint` extension to edit requirement files rather than IntelliJ, because VS Code understands `markdownlintdisable` directives. + +### Requirement syntax + +Requirements should be written in the main JabRef repository under `docs/requirements`. They are grouped by the feature they relate to. Universal requirements can go in cross-cutting categories such as UX. + +You write a requirement identifier directly below a Markdown heading. + +Example: + +```markdown +### Example +`req~ai.example~1` +``` + +The requirement ID (`req~ai.example~1`) consists of several parts separated by a tilde: + +- an artifact type: `req`; +- the main part: `ai.example`; +- the revision: `1`. + +It is important that there is no empty line directly after the heading. + +{: note} +You need to add `` at the end of the file because the ID of the requirement must follow the heading directly. + +After adding a heading and identifier, you write the description of the requirement. At the end of the requirement, you list the artifact types it needs. + +```markdown +Needs: impl, utest +``` + +## Linking implementations + +After writing the requirement, you add a comment at the implementation site to indicate that it is covered: + +Java code: + +```java +// [impl->req~ai.example~1] +``` + +Markdown: + +```markdown + +``` + +## Conventions used in JabRef + +For requirement IDs, we follow the OFT standard artifact types, with the addition of `adr`. For the main part, we separate the path with `.`, and separate words with a hyphen, as in the example above. + +We use these artifact types: + +- `feat`: general features or ideas, primarily user-facing, unrefined requirements of varying size; +- `req`: a specific nuance, cross-cutting requirement, or bug fix; +- `impl`: a code implementation (typically Java, but can also be GitHub CI/CD code, etc.); +- `utest`: a unit test; +- `itest`: a test involving external services; +- `dsn`: a design document for specifications; +- `arch`: a high-level design requirement; +- `uman`: a user manual page or section; +- `adr`: an Architectural Decision Record. + +Throughout development, you will mainly work with `feat`, `req`, `impl`, and `utest`. Please try to use these four types, as the others are rarely needed and we do not have good examples for them. + +We would really like to use the `uman` artifact type, as new features often also need to be explained to users. At the time of writing, we do not use this type in the requirements because we do not have a cross-repository setup for OFT. + +The boundary between `feat` and `req` can sometimes be debatable, because from a software engineering perspective they refer to the same thing. However, we assign them based on the outcome: a new fetcher is a feature, while special handling of a field is a requirement. This gives us an overview of what we have in JabRef. + +## How to write a requirement + +### Requirement style + +**General ideas**: + +- `req~` = a constraint the system **must** satisfy. `feat~` = a capability offered to the user. +- Title grammar should match the tag: `req~` → **"Subject must verb"**, `feat~` → **"User can verb"**. +- One item, one requirement. If the title needs "and," split it into two items. +- The title should carry the full normative statement; the description should only add what the title could not fit. + +**Title style**: + +- Put the subject first, then the modal verb. +- For `req~`, always use **must** — never "should," "needs to," or "is required to." Use one modal verb only. +- Avoid nominalizations ("must be verified," not "verification"). +- Avoid system-as-narrator phrasing ("allows the user to," "offers to") — state what must happen. + +**Description style**: + +Use it only for what the title cannot carry: + +- the triggering condition; +- edge cases or boundary behavior; +- a brief rationale if needed; +- the GitHub issue; +- other relevant context. + +Do not repeat the subject + verb from the title, do not smuggle in a second requirement, and do not write marketing copy. + +### Example of a requirement + +**Bad**: + +```markdown +## GitHub personal access token verification +`req~git.share.personal-access-token-verification~1` + +The GitHub sharing dialog must allow users to verify that their personal +access token has push access to the configured GitHub repository before +sharing a library. +``` + +There is no modal verb in the title, and the subject-first structure is missing. The normative content is buried in the paragraph instead of the title. + +**Good**: + +```markdown +## GitHub personal access token push access must be verifiable before sharing +`req~git.share.personal-access-token-verification~1` + +Verification happens in the GitHub sharing dialog before the library is shared. +``` + +The title carries the full constraint, and the description adds only the missing detail about where it happens. + +### Linking requirements + +The syntax for linking a requirement to code was already shown above. However, you should still follow the principle of **linking the requirement to the most specific code location**. Only if there is no specific place, or if too many components are involved, can you link it at the method or class level. + +In extreme cases, you can write the linking comment and add a second comment below it explaining how the implementation works. + +All comments should be placed before the annotations. + +### Unimplemented requirements + +Sometimes you have a lot of useful ideas but no time to write or implement them. In that case, you can write the requirements and mark them as `draft` by adding: + +```markdown +Status: draft +``` + +This way they will be filtered out when running `traceRequirements`, while still preserving the ideas. + +## More information + +We use OFT mainly to trace small ideas, notes, and wishes rather than to manage the full software engineering requirements process. The concepts and terminology of software requirements engineering are still useful for structuring and writing requirements, and they help us express them clearly. However, we do not apply all of its principles in full. + +For example, INVEST is useful for writing better requirement descriptions, but we do not treat our requirements as fully negotiable items because they are more like concrete instructions or orders than open-ended, negotiable agreements. We also do not explicitly record a separate value dimension for each requirement in this workflow. + +It is better to write more small requirements than one large one. This makes coverage and tracking more detailed. + +- [General reading on traceability](https://www.sodiuswillert.com/en/blog/implementing-requirements-traceability-in-systems-software-engineering) +- [OFT User guide](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide/user_guide.md) +- [OFT artifacts](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide/user_guide.md#specification-item-artifact-type) diff --git a/docs/requirements/index.md b/docs/requirements/index.md index 4b47c5cb8877..aa430f115d6e 100644 --- a/docs/requirements/index.md +++ b/docs/requirements/index.md @@ -4,70 +4,7 @@ has_children: true --- # Requirements -Requirements capture what the JabRef should do. -Regard it as structured representation of implemented issues. +Requirements capture what JabRef should do. +Think of them as a structured representation of implemented issues. -JabRef uses [OpenFastTrace](https://github.com/itsallcode/openfasttrace) to identify each requirement and to link implementation, tests, and more to it. -This enables forward and backward tracing. -For instance, questions like: "How is the requirement implemented?" (forward trace) or "Which requirement lead to this implementation?" (backward trace). - -## Specifying requirements - -One writes directly below a Markdown heading a requirement identifier. - -Example: - -```markdown -### Example -`req~ai.example~1` -``` - -It is important that there is no empty line directly after the heading. - -{: note} -One needs to add `` to the end of the file, because the ID of the requirement needs to follow the heading directly. - -After putting a heading and an identifier, one writes down at the requirement. -Directly at the end, one writes that it requires an implementation: - -```markdown -Needs: impl -``` - -One can also state that there should be detailed design document (`dsn`). -However, typically in JabRef, we go from the requirement directly to the implementation. - -When writing a requirement, ensure that it is atomic. One requirement is related to 1 thing/feature at a time. - -## Linking implementations - -After writing the requirement, at the implementation, a comment is added that this implementation is covered: - -```java -// [impl->req~ai.example~1] -``` - -## Automated checks - -When executing the gradle task `traceRequirements`, `build/reports/tracing.txt` is generated. -It captures the links between the artifacts (requirement, implementation, ...) - -In case of a tracing error, one can inspect `build/tracing.txt` to see which requirements were not covered. - -## Custom Artifact Types - -- `pp`: means that this requirement should be guarded with a privacy policy banner. The feature should not do anything if the user does not accept the privacy policy. The specific privacy policy depends on the feature. -- `guard`: means that this action might be dangerous and should be guarded with a confirmation dialog. -- `adr`: an architectural decision record in `docs/decisions/` that code carries out. The identifier follows the ADR's title (`adr~hardcode-fieldnames~1`, without the file's number prefix, because an identifier must not start with a digit), and implementations link to it with `// [impl->adr~hardcode-fieldnames~1]`. -- `model`: means that this feature requires a model. - -While not a custom artifact, but we interpret these OpenFastTrace artifact types as follows: - -- `dsn`: means writing an ADR. - -## More Information - -- [General reading on traceability](https://www.sodiuswillert.com/en/blog/implementing-requirements-traceability-in-systems-software-engineering) -- [User manual of OpenFastTrace](https://openfasttrace.itsallcode.org/user_guide/user_guide.html) - -We recommend using VS Code with `markdownlint` extension to edit requirement files, and not IntelliJ, as VS Code understands `markdownlintdisable` directives. +For more information on how to write requirements and use OpenFastTrace, see the [Requirements code-howto](../code-howtos/requirements.md). From bb805a7db1d1aa22539a237a89ac4a7354e23ad1 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 15:10:21 +0200 Subject: [PATCH 064/102] Move Hayagriva export CHANGELOG entry to Unreleased, drop internal Removed entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f48949e991..f3f83475079f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed +- We changed the Hayagriva YAML export to keep data JabRef cannot edit (short titles, person aliases, additional identifiers) when re-exporting an imported file, to write comments, and to derive `date` from the BibTeX year/month fields. [#736](https://github.com/JabRef/jabref-koppor/pull/736) + ### Fixed - We re-enabled the [Grobid](https://github.com/grobidOrg/grobid) citation fetcher again. It is now reachable via https. [#16668](https://github.com/JabRef/jabref/issues/16668) @@ -106,7 +108,6 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed -- We changed the Hayagriva YAML export to keep data JabRef cannot edit (short titles, person aliases, additional identifiers) when re-exporting an imported file, to write comments, and to derive `date` from the BibTeX year/month fields. [#736](https://github.com/JabRef/jabref-koppor/pull/736) - We improve startup performance by load citation style sources only when used. [#15962](https://github.com/JabRef/jabref/issues/15962) - We changed the default prompts for "AI-Chat" & "Chat with Groups" to better handle etiquette, metadata, citationkeys and context separation. [#16981](https://github.com/JabRef/jabref/pull/16981) - We changed the default local embedding model to `intfloat/multilingual-e5-small` for better passage retrieval. [#17120](https://github.com/JabRef/jabref/pull/17120) @@ -321,7 +322,6 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Removed -- We removed the `HayagrivaType` layout formatter, since the Hayagriva YAML export no longer uses a layout template. [#736](https://github.com/JabRef/jabref-koppor/pull/736) - We removed the redundant "Look up BibTeX entries in all open libraries" setting from the LibreOffice panel, which is now the toggle of "Look up BibTeX entries in the currently selected library only". [#16484](https://github.com/JabRef/jabref/pull/16484) - We removed the entry editor tabs "Required fields", "Optional fields", "Optional fields 2", "Deprecated fields", "Other fields", and "Comments"; their content is part of the new "Main" tab. [#12711](https://github.com/JabRef/jabref/issues/12711) - We removed the default custom entry editor tabs "General" and "Abstract"; their content is part of the new "Main" tab. User-defined custom tabs are kept. [#12711](https://github.com/JabRef/jabref/issues/12711) From 3be0eb01933d59eb9092b97b15b5f1d73aeabd08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:44:58 +0000 Subject: [PATCH 065/102] Chore(deps): Bump JetBrains/junie-github-action from 1.7.9 to 1.7.12 (#17259) Bumps [JetBrains/junie-github-action](https://github.com/jetbrains/junie-github-action) from 1.7.9 to 1.7.12. - [Release notes](https://github.com/jetbrains/junie-github-action/releases) - [Commits](https://github.com/jetbrains/junie-github-action/compare/2f33c71d9d74cbbe1662bddeb0d1f6f59ea05b29...c49dfcdf95b75650b75a64b504673f05c89a9819) --- updated-dependencies: - dependency-name: JetBrains/junie-github-action dependency-version: 1.7.12 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/junie.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/junie.yml b/.github/workflows/junie.yml index 5629fdb9e16f..54fefcff8c76 100644 --- a/.github/workflows/junie.yml +++ b/.github/workflows/junie.yml @@ -30,6 +30,6 @@ jobs: - name: Run Junie id: junie - uses: JetBrains/junie-github-action@2f33c71d9d74cbbe1662bddeb0d1f6f59ea05b29 # v1.7.9 + uses: JetBrains/junie-github-action@c49dfcdf95b75650b75a64b504673f05c89a9819 # v1.7.12 with: junie_api_key: ${{ secrets.JUNIE_API_KEY }} From 4b4112f1cc1ed2167e0ca19f929ba5a5279898b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:52:02 +0000 Subject: [PATCH 066/102] Chore(deps): Bump ruby/setup-ruby from 1.321.0 to 1.325.0 (#17261) Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.321.0 to 1.325.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/95ef2b042f9d7a56d8268cba8559e2842e2ad01b...e8944e80fb94b20106697132f8c20c665fab29e9) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.325.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3553707516e3..e70fa3d34a8f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,7 +33,7 @@ jobs: with: show-progress: 'false' - name: Setup Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1 with: ruby-version: '3.3' # Not needed with a .ruby-version file bundler-cache: true # runs 'bundle install' and caches installed gems automatically From 24d2748764501ef0b53421e35d7c77847bb5cd1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:52:53 +0000 Subject: [PATCH 067/102] Chore(deps): Bump zauguin/install-texlive from 4.3.0 to 4.4.0 (#17262) Bumps [zauguin/install-texlive](https://github.com/zauguin/install-texlive) from 4.3.0 to 4.4.0. - [Commits](https://github.com/zauguin/install-texlive/compare/f5e7d0b117035fff797d4392ea94131747e03d60...6671d0c62046c7e349fe154d5208fe746b07e037) --- updated-dependencies: - dependency-name: zauguin/install-texlive dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/latex.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latex.yml b/.github/workflows/latex.yml index 2459aec5988c..6f49daf98e42 100644 --- a/.github/workflows/latex.yml +++ b/.github/workflows/latex.yml @@ -29,7 +29,7 @@ jobs: - name: Check out code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install TeX Live - uses: zauguin/install-texlive@f5e7d0b117035fff797d4392ea94131747e03d60 # v4 + uses: zauguin/install-texlive@6671d0c62046c7e349fe154d5208fe746b07e037 # v4.4.0 with: package_file: jablib/src/test/latex/Texlivefile check-latex: @@ -88,7 +88,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install TeX Live - uses: zauguin/install-texlive@f5e7d0b117035fff797d4392ea94131747e03d60 # v4 + uses: zauguin/install-texlive@6671d0c62046c7e349fe154d5208fe746b07e037 # v4.4.0 with: package_file: jablib/src/test/latex/Texlivefile From 65141c63c2cf645bfeed0a0bf48a0739a28082f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:54:16 +0000 Subject: [PATCH 068/102] Chore(deps): Bump anthropics/claude-code-action from 1.0.226 to 1.0.231 (#17263) Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.226 to 1.0.231. - [Release notes](https://github.com/anthropics/claude-code-action/releases) - [Commits](https://github.com/anthropics/claude-code-action/compare/7b0b255830a1fab6e602658672acad11c12d841d...cfc3eb22bfed5c26ef66e3223c982af27e4524de) --- updated-dependencies: - dependency-name: anthropics/claude-code-action dependency-version: 1.0.231 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index fb378ee26dcd..9dadbdc7e9a1 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,6 +39,6 @@ jobs: - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@7b0b255830a1fab6e602658672acad11c12d841d # v1 + uses: anthropics/claude-code-action@cfc3eb22bfed5c26ef66e3223c982af27e4524de # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} From 1deae15842b497b54fa0e306691e3753168dc7d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:55:31 +0000 Subject: [PATCH 069/102] Chore(deps): Bump jablib/src/main/resources/csl-styles (#17258) Bumps [jablib/src/main/resources/csl-styles](https://github.com/citation-style-language/styles) from `81c3eb8` to `66e929f`. - [Release notes](https://github.com/citation-style-language/styles/releases) - [Commits](https://github.com/citation-style-language/styles/compare/81c3eb863d1c5a83aab7e296e045013d2422f2cb...66e929f5618af5f59d857ee644bb3ba0adad0b94) --- updated-dependencies: - dependency-name: jablib/src/main/resources/csl-styles dependency-version: 66e929f5618af5f59d857ee644bb3ba0adad0b94 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- jablib/src/main/resources/csl-styles | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/resources/csl-styles b/jablib/src/main/resources/csl-styles index 81c3eb863d1c..66e929f5618a 160000 --- a/jablib/src/main/resources/csl-styles +++ b/jablib/src/main/resources/csl-styles @@ -1 +1 @@ -Subproject commit 81c3eb863d1c5a83aab7e296e045013d2422f2cb +Subproject commit 66e929f5618af5f59d857ee644bb3ba0adad0b94 From d91bb0454900b5b022522f743eaedca7603e7ef2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:58:08 +0000 Subject: [PATCH 070/102] Chore(deps): Bump actions/setup-java from 6.0.0 to 6.0.1 (#17260) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/dd06d9cba3e5552c54d9f8ea23572deb30010f7c...de7274f081f381c8f8158605e0321c36c376e2e6) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-comment.yml | 2 +- .github/workflows/tests-code.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index ea44334c75ca..74754d4b14c4 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -159,7 +159,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # No setup-gradle here: this job never runs Gradle, it only needs a JDK and JBang. - - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 if: ${{ github.event_name == 'workflow_dispatch' || (steps.check-label.outputs.has_label == 'false' && steps.read-pr_number.outputs.pr_number != '' && steps.isCrossRepository.outputs.isCrossRepository == 'true' && github.event.workflow_run.name != 'PR Review Activity') }} with: distribution: 'corretto' diff --git a/.github/workflows/tests-code.yml b/.github/workflows/tests-code.yml index eff6f42dd1c5..b06bd7ccc97d 100644 --- a/.github/workflows/tests-code.yml +++ b/.github/workflows/tests-code.yml @@ -149,7 +149,7 @@ jobs: repository: 'rmuller/Convert2MarkdownJavadoc' path: 'Convert2MarkdownJavadoc' - name: Setup JDK - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: java-version: '24' distribution: 'corretto' @@ -299,7 +299,7 @@ jobs: - name: Resolve //DEPS into the cached directory # JBang resolves script dependencies into ~/.m2, which is not cached here. run: echo "JBANG_REPO=$HOME/.jbang/repository" >> "$GITHUB_ENV" - - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: distribution: 'corretto' java-version: '25' @@ -636,7 +636,7 @@ jobs: submodules: 'false' show-progress: 'false' - name: Set up JDK - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: java-version: 25 distribution: 'corretto' From a8ba7a1033e8cc9294a6cca059c08af605fe2847 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:00:18 +0000 Subject: [PATCH 071/102] Chore(deps): Bump com.dlsc.gemsfx:gemsfx in /versions (#17264) Bumps [com.dlsc.gemsfx:gemsfx](https://github.com/dlsc-software-consulting-gmbh/GemsFX) from 4.4.5 to 4.5.0. - [Release notes](https://github.com/dlsc-software-consulting-gmbh/GemsFX/releases) - [Commits](https://github.com/dlsc-software-consulting-gmbh/GemsFX/compare/v4.4.5...v4.5.0) --- updated-dependencies: - dependency-name: com.dlsc.gemsfx:gemsfx dependency-version: 4.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 17eeed5c6a4d..9c0374ce9edd 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -49,7 +49,7 @@ dependencies.constraints { api("cc.jilt:jilt:1.9.2") - api("com.dlsc.gemsfx:gemsfx:4.4.5") + api("com.dlsc.gemsfx:gemsfx:4.5.0") api("com.dlsc.unitfx:unitfx:1.0.10") api("com.dlsc.pdfviewfx:pdfviewfx:3.5.0") api("com.ibm.icu:icu4j:72.0.1!!") From d91b902eda206db7f764f306c64c47f8bf0f4bc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:05:13 +0000 Subject: [PATCH 072/102] Chore(deps): Bump com.github.ben-manes.caffeine:caffeine in /versions (#17266) Bumps [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) from 3.2.4 to 3.3.0. - [Release notes](https://github.com/ben-manes/caffeine/releases) - [Commits](https://github.com/ben-manes/caffeine/compare/v3.2.4...v3.3.0) --- updated-dependencies: - dependency-name: com.github.ben-manes.caffeine:caffeine dependency-version: 3.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 9c0374ce9edd..bb1d5def083f 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -56,7 +56,7 @@ dependencies.constraints { api("com.fasterxml:aalto-xml:1.4.0") api("org.eclipse.lsp4j:org.eclipse.lsp4j:1.0.0") api("org.eclipse.lsp4j:org.eclipse.lsp4j.websocket.jakarta:1.0.0") - api("com.github.ben-manes.caffeine:caffeine:3.2.4") + api("com.github.ben-manes.caffeine:caffeine:3.3.0") api("com.github.javakeyring:java-keyring:1.0.4") api("com.github.javaparser:javaparser-core:3.28.2") api("com.github.javaparser:javaparser-symbol-solver-core:3.28.2") From ae63943833e50a2cf3c1b7733f607f894f3ff88d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:09:59 +0000 Subject: [PATCH 073/102] Chore(deps): Bump tools.jackson:jackson-bom in /versions (#17265) Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.2.2 to 3.2.3. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.2.2...jackson-bom-3.2.3) --- updated-dependencies: - dependency-name: tools.jackson:jackson-bom dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index bb1d5def083f..264dc6d52dd2 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { api(platform("org.junit:junit-bom:6.1.3")) api(platform("org.glassfish.grizzly:grizzly-bom:5.0.2")) api(platform("org.glassfish.jersey:jersey-bom:4.0.2")) - api(platform("tools.jackson:jackson-bom:3.2.2")) + api(platform("tools.jackson:jackson-bom:3.2.3")) } dependencies.constraints { From ed510692f95b95abb1ac8c0d41b7870614a7848c Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 18:14:08 +0000 Subject: [PATCH 074/102] Stale PR automation only for PRs from forks (#17238) * Mark and close stale PRs only when they come from forks Maintainers' PRs from branches in JabRef/jabref are no longer marked stale or closed. Co-Authored-By: Claude Opus 5 * Post the stale warning before applying the label Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .github/workflows/stale.yml | 56 ++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 271d7cdc056e..14a3cc4f6475 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -26,31 +26,41 @@ jobs: # "status: changes-required" was set, so exempting it would take most PRs out of the process. EXEMPT_LABELS: "📌 Pinned" steps: - # Marking only. Closing is done below, because actions/stale measures - # days-before-close against the GitHub field `updated_at`, which is bumped by any - # activity on the pull request - including activity that is not the contributor's. - # PRs whose `updated_at` keeps moving therefore never reach the close threshold. - - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11 - id: stale - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - exempt-pr-labels: ${{ env.EXEMPT_LABELS }} - - days-before-issue-stale: -1 - days-before-issue-close: -1 - - only-pr-labels: ${{ env.CHANGES_REQUIRED_LABEL }} - stale-pr-label: ${{ env.STALE_LABEL }} - remove-pr-stale-when-updated: false - - days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} - stale-pr-message: > + # Only pull requests from forks: maintainers' pull requests (branches in this repository) + # are never marked. actions/stale cannot filter by head repository, hence the script. + - name: Mark PRs from forks idle for more than ${{ env.DAYS_BEFORE_PR_STALE }} days as stale + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + DRY_RUN: ${{ github.event_name == 'pull_request' }} + MESSAGE: > The requested changes were not addressed for ${{ env.DAYS_BEFORE_PR_STALE }} days. Please follow-up in the next ${{ env.DAYS_BEFORE_PR_CLOSE }} days or your PR will be automatically closed. You can check the [contributing guidelines](https://github.com/JabRef/jabref/blob/main/CONTRIBUTING.md#pull-request-process) for hints on the pull request process. + run: | + set -euo pipefail + export CUTOFF="$(date -u -d "-${DAYS_BEFORE_PR_STALE} days" +%Y-%m-%dT%H:%M:%SZ)" + candidates="$(gh pr list --repo "$REPO" --state open --limit 1000 \ + --label "$CHANGES_REQUIRED_LABEL" \ + --json number,isCrossRepository,updatedAt,labels \ + --jq '.[] | select(.isCrossRepository and .updatedAt < env.CUTOFF) + | select([.labels[].name] - [env.STALE_LABEL] - (env.EXEMPT_LABELS | split(",")) == [.labels[].name]) + | .number')" + for pr in $candidates; do + if [ "$DRY_RUN" = "true" ]; then + echo "#$pr: would be marked stale (dry run)" + continue + fi + echo "#$pr: marking stale" + # Comment first: a labeled PR is never marked again, so its warning must not get lost. + gh pr comment "$pr" --repo "$REPO" --body "$MESSAGE" + gh issue edit "$pr" --repo "$REPO" --add-label "$STALE_LABEL" + done - days-before-pr-close: -1 - + # Closing is done here, not by actions/stale, because that measures days-before-close + # against the GitHub field `updated_at`, which is bumped by any activity on the pull + # request - including activity that is not the contributor's. + # PRs whose `updated_at` keeps moving therefore never reach the close threshold. - name: Close PRs labeled stale for more than ${{ env.DAYS_BEFORE_PR_CLOSE }} days id: close env: @@ -67,12 +77,12 @@ jobs: echo "Assessing PRs labeled '$STALE_LABEL' on or before $cutoff" closed="" - # Both labels are required, mirroring the only-pr-labels of the marking step above: + # Both labels are required, mirroring the marking step above: # a PR whose requested changes were addressed is no longer a candidate. # gh paginates internally up to --limit, so this is not a page size. candidates="$(gh pr list --repo "$REPO" --state open --limit 1000 \ --label "$STALE_LABEL" --label "$CHANGES_REQUIRED_LABEL" \ - --json number --jq '.[].number')" + --json number,isCrossRepository --jq '.[] | select(.isCrossRepository) | .number')" IFS=',' read -ra exempt_labels <<< "$EXEMPT_LABELS" From eead56f4afebf95464ec821ce06594f9f4c92756 Mon Sep 17 00:00:00 2001 From: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:18:31 +0000 Subject: [PATCH 075/102] Remove Reflection, show action descriptions as tooltips, fix tooltips not showing first time run (#17200) * Show action descriptions as tooltips in menus Menu items now show the action's long text (description plus status message) as tooltip, installed on the menu row via public JavaFX API instead of reflecting into ContextMenuContent. Tooltips repeating the label are skipped, and the redundant descriptions of "Open file(s)" and "Open folder(s)" are removed. Closes https://github.com/JabRef/jabref/issues/9615 Co-Authored-By: Claude Opus 5 * Drop opens of com.sun.javafx.scene.control ActionFactory no longer reflects into ContextMenuContent; the package is still exported for direct compile-time use. Co-Authored-By: Claude Opus 5 * Add changelog entry for first-open menu tooltip fix Co-Authored-By: Claude Opus 5 * Dispose menu tooltip setup when an item is reconfigured GuiPushToApplicationCommand reconfigures the same menu item whenever the push target changes; without disposal, listeners piled up and the previous action's tooltip could stay installed. Co-Authored-By: Claude Opus 5 * Document missing menu tooltips in the macOS system menu bar Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Carl Christian Snethlage Co-authored-by: Claude Opus 5 Co-authored-by: github-actions[bot] --- CHANGELOG.md | 2 + ...ef.gradle.base.dependency-rules.gradle.kts | 2 - jabgui/build.gradle.kts | 3 +- .../org/jabref/gui/actions/ActionFactory.java | 126 ++++++++++-------- .../jabref/gui/actions/StandardActions.java | 4 +- 5 files changed, 72 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93eef956b96a..417f9eb037fb 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) +- We added tooltips with action descriptions to menu items. [#9615](https://github.com/JabRef/jabref/issues/9615) ### Changed @@ -21,6 +22,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) +- We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) ### Removed diff --git a/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts b/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts index c0d951d6d74e..e0c43ae49166 100644 --- a/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts +++ b/build-logic/src/main/kotlin/org.jabref.gradle.base.dependency-rules.gradle.kts @@ -530,8 +530,6 @@ extraJavaModuleInfo { opens("javafx.scene.control.cell") opens("javafx.scene.control.skin") exports("com.sun.javafx.scene.control") - // required for ActionFactory#enableTooltips - opens("com.sun.javafx.scene.control") } module("org.hamcrest:hamcrest", "org.hamcrest") diff --git a/jabgui/build.gradle.kts b/jabgui/build.gradle.kts index 86f2d2ef60f5..22bcb0f22250 100644 --- a/jabgui/build.gradle.kts +++ b/jabgui/build.gradle.kts @@ -68,8 +68,7 @@ val useLibericaJdkFullJvmArgs = if (useLibericaJdkFull) listOf( "--add-opens", "javafx.controls/javafx.scene.control=org.jabref,ALL-UNNAMED", "--add-opens", "javafx.controls/javafx.scene.control.cell=org.jabref,ALL-UNNAMED", "--add-opens", "javafx.controls/javafx.scene.control.skin=org.jabref,ALL-UNNAMED", - "--add-exports", "javafx.controls/com.sun.javafx.scene.control=org.jabref,ALL-UNNAMED", - "--add-opens", "javafx.controls/com.sun.javafx.scene.control=org.jabref,ALL-UNNAMED" + "--add-exports", "javafx.controls/com.sun.javafx.scene.control=org.jabref,ALL-UNNAMED" ) else emptyList() // Compile-time counterpart of the JavaFX --add-exports above. When JavaFX is patched Maven jars, those diff --git a/jabgui/src/main/java/org/jabref/gui/actions/ActionFactory.java b/jabgui/src/main/java/org/jabref/gui/actions/ActionFactory.java index 197d0894dc4f..2a4ee82fb9d3 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/ActionFactory.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/ActionFactory.java @@ -1,33 +1,32 @@ package org.jabref.gui.actions; -import java.lang.reflect.InaccessibleObjectException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.Optional; import javafx.beans.binding.BooleanExpression; +import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; import javafx.scene.control.CheckMenuItem; -import javafx.scene.control.Label; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.Tooltip; +import javafx.stage.WindowEvent; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.logic.util.strings.StringUtil; import com.airhacks.afterburner.injection.Injector; -import com.sun.javafx.scene.control.ContextMenuContent; import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; import de.saxsys.mvvmfx.utils.commands.Command; import org.controlsfx.control.action.ActionUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /// Helper class to create and style controls according to an [Action]. public class ActionFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(ActionFactory.class); + private static final String TOOLTIP_DISPOSER_KEY = ActionFactory.class.getName() + ".tooltipDisposer"; private final KeyBindingRepository keyBindingRepository; @@ -41,60 +40,63 @@ private static void setGraphic(MenuItem node, Action action) { action.getIcon().ifPresent(icon -> node.setGraphic(icon.getGraphicNode())); } - /* - * Returns MenuItemContainer node associated with this menu item - * which can contain: - * 1. label node of type Label for displaying menu item text, - * 2. right node of type Label for displaying accelerator text, - * or an arrow if it's a Menu, - * 3. graphic node for displaying menu item icon, and - * 4. left node for displaying either radio button or check box. - * - * This is basically rewritten impl_styleableGetNode() which - * should not be used since it's marked as deprecated. - */ - private static Label getAssociatedNode(MenuItem menuItem) { - ContextMenuContent.MenuItemContainer container = (ContextMenuContent.MenuItemContainer) menuItem.getStyleableNode(); - - if (container == null) { - return null; - } else { - // We have to use reflection to get the associated label - try { - Method getLabel = ContextMenuContent.MenuItemContainer.class.getDeclaredMethod("getLabel"); - getLabel.setAccessible(true); - return (Label) getLabel.invoke(container); - } catch (InaccessibleObjectException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - LOGGER.warn("Could not get label of menu item", e); - } - } - return null; - } - public MenuItem configureMenuItem(Action action, Command command, MenuItem menuItem) { - ActionUtils.configureMenuItem(new JabRefAction(action, command, keyBindingRepository), menuItem); + JabRefAction jabRefAction = new JabRefAction(action, command, keyBindingRepository); + ActionUtils.configureMenuItem(jabRefAction, menuItem); setGraphic(menuItem, action); - enableTooltips(command, menuItem); + enableTooltip(jabRefAction, menuItem); return menuItem; } - private static void enableTooltips(Command command, MenuItem menuItem) { - if (command instanceof SimpleCommand simpleCommand) { - EasyBind.subscribe( - simpleCommand.statusMessageProperty(), - message -> { - Label label = getAssociatedNode(menuItem); - if (label != null) { - label.setMouseTransparent(false); - if (StringUtil.isBlank(message)) { - label.setTooltip(null); - } else { - label.setTooltip(new Tooltip(message)); - } - } - } - ); + /// Shows the action's long text (its description and, for a [SimpleCommand], its status message) as tooltip on the menu row. + /// + /// [MenuItem] has no tooltip property, unlike the buttons [ActionUtils] configures from the same text. + /// The row node only exists once the menu has been shown and is recreated when the menu's items change, + /// so the tooltip is (re)installed whenever the popup is shown. + /// + /// On macOS, the main menu is rendered natively by the system menu bar (see `MainMenu#setUseSystemMenuBar`). + /// Its items have no JavaFX row node, so they show no tooltip there; context menus are not affected. + private static void enableTooltip(JabRefAction jabRefAction, MenuItem menuItem) { + // An item can be configured again with another action (e.g., when the push-to-application target changes), + // so the listeners of the previous configuration have to go, or its outdated tooltip would still be installed + if (menuItem.getProperties().remove(TOOLTIP_DISPOSER_KEY) instanceof Runnable disposePreviousTooltip) { + disposePreviousTooltip.run(); } + + Tooltip tooltip = new Tooltip(); + tooltip.textProperty().bind(jabRefAction.longTextProperty()); + + Runnable updateTooltip = () -> { + // The row is not disabled itself (only styled as such), so it still receives the mouse events a tooltip needs + Optional.ofNullable(menuItem.getStyleableNode()).ifPresent(row -> { + // Installing adds the mouse handlers again, so remove any earlier installation first + Tooltip.uninstall(row, tooltip); + // Several action descriptions merely repeat the label, which is useless as tooltip + if (!StringUtil.isBlank(tooltip.getText()) && !tooltip.getText().equals(menuItem.getText())) { + Tooltip.install(row, tooltip); + } + }); + }; + EventHandler onPopupShown = _ -> updateTooltip.run(); + + Optional.ofNullable(menuItem.getParentPopup()) + .ifPresent(popup -> popup.addEventHandler(WindowEvent.WINDOW_SHOWN, onPopupShown)); + ChangeListener parentPopupListener = (_, oldPopup, newPopup) -> { + Optional.ofNullable(oldPopup).ifPresent(popup -> popup.removeEventHandler(WindowEvent.WINDOW_SHOWN, onPopupShown)); + Optional.ofNullable(newPopup).ifPresent(popup -> popup.addEventHandler(WindowEvent.WINDOW_SHOWN, onPopupShown)); + }; + menuItem.parentPopupProperty().addListener(parentPopupListener); + Subscription textSubscription = EasyBind.subscribe(tooltip.textProperty(), _ -> updateTooltip.run()); + + Runnable disposeTooltip = () -> { + textSubscription.unsubscribe(); + menuItem.parentPopupProperty().removeListener(parentPopupListener); + Optional.ofNullable(menuItem.getParentPopup()) + .ifPresent(popup -> popup.removeEventHandler(WindowEvent.WINDOW_SHOWN, onPopupShown)); + Optional.ofNullable(menuItem.getStyleableNode()).ifPresent(row -> Tooltip.uninstall(row, tooltip)); + tooltip.textProperty().unbind(); + }; + menuItem.getProperties().put(TOOLTIP_DISPOSER_KEY, disposeTooltip); } public MenuItem createMenuItem(Action action, Command command) { @@ -132,26 +134,32 @@ public MenuItem createCustomMenuItem(Action action, Command command, String text } public CheckMenuItem createCheckMenuItem(Action action, Command command, boolean selected) { - CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(new JabRefAction(action, command, keyBindingRepository)); + JabRefAction jabRefAction = new JabRefAction(action, command, keyBindingRepository); + CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(jabRefAction); checkMenuItem.setSelected(selected); setGraphic(checkMenuItem, action); + enableTooltip(jabRefAction, checkMenuItem); return checkMenuItem; } public CheckMenuItem createCheckMenuItem(Action action, Command command, BooleanExpression selectedBinding) { - CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(new JabRefAction(action, command, keyBindingRepository)); + JabRefAction jabRefAction = new JabRefAction(action, command, keyBindingRepository); + CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(jabRefAction); EasyBind.subscribe(selectedBinding, checkMenuItem::setSelected); setGraphic(checkMenuItem, action); + enableTooltip(jabRefAction, checkMenuItem); return checkMenuItem; } public Menu createMenu(Action action) { - Menu menu = ActionUtils.createMenu(new JabRefAction(action, keyBindingRepository)); + JabRefAction jabRefAction = new JabRefAction(action, keyBindingRepository); + Menu menu = ActionUtils.createMenu(jabRefAction); // For some reason the graphic is not set correctly, so let's fix this setGraphic(menu, action); + enableTooltip(jabRefAction, menu); return menu; } diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 555d4ec6ab27..4f508421e032 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -124,8 +124,8 @@ public enum StandardActions implements Action { EDIT_EXISTING_STUDY(Localization.lang("Manage study definition")), OPEN_DATABASE_FOLDER(Localization.lang("Reveal in file explorer")), - OPEN_FOLDER(Localization.lang("Open folder(s)"), Localization.lang("Open folder"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER), - OPEN_FILE(Localization.lang("Open file(s)"), Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE), + OPEN_FOLDER(Localization.lang("Open folder(s)"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER), + OPEN_FILE(Localization.lang("Open file(s)"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE), OPEN_CONSOLE(Localization.lang("Open terminal here"), Localization.lang("Open terminal here"), IconTheme.JabRefIcons.CONSOLE, KeyBinding.OPEN_CONSOLE), SORT_TABS_ALPHABETICALLY(Localization.lang("Sort tabs alphabetically")), COPY_LINKED_FILES(Localization.lang("Copy linked file(s) to folder...")), From 1d312ff0355d4aecda570dac3e7abb9b8ab012a9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 23 Sep 2026 18:53:20 +0000 Subject: [PATCH 076/102] Fix NPE when accepting backup changes that add groups (#17165) * Do not touch the active tab when reviewing a backup The library being reviewed has no tab yet, so the active tab is another library's or absent; its lookup threw and the review ended in an NPE. Co-Authored-By: Claude Fable 5.1 * Keep a library without groups groupless when accepting a metadata change A backup that adds groups to a library without groups produces a metadata change whose original group root is null; installing that root threw and the review ended in an NPE. Co-Authored-By: Claude Fable 5.1 * Link the pull request in the changelog Co-Authored-By: Claude Fable 5.1 * Forget injected test services after each backup review test Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: github-actions[bot] Co-authored-by: Christoph --- CHANGELOG.md | 1 + .../collab/metedatachange/MetadataChange.java | 7 +- .../jabref/gui/dialogs/BackupUIManager.java | 8 +- .../gui/dialogs/BackupUIManagerTest.java | 76 +++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 417f9eb037fb..703b40feca4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) +- We fixed an issue where accepting a backup that adds groups to a library without groups failed with an error. [#17165](https://github.com/JabRef/jabref/pull/17165) - We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) ### Removed diff --git a/jabgui/src/main/java/org/jabref/gui/collab/metedatachange/MetadataChange.java b/jabgui/src/main/java/org/jabref/gui/collab/metedatachange/MetadataChange.java index 58abd0ab0847..5ed16127ecc8 100644 --- a/jabgui/src/main/java/org/jabref/gui/collab/metedatachange/MetadataChange.java +++ b/jabgui/src/main/java/org/jabref/gui/collab/metedatachange/MetadataChange.java @@ -1,5 +1,7 @@ package org.jabref.gui.collab.metedatachange; +import java.util.Optional; + import org.jabref.gui.collab.DatabaseChange; import org.jabref.gui.collab.DatabaseChangeResolverFactory; import org.jabref.logic.bibtex.comparator.MetaDataDiff; @@ -22,9 +24,10 @@ public MetadataChange(MetaDataDiff metaDataDiff, BibDatabaseContext databaseCont public void applyChange(CompoundEdit undoEdit) { MetaData newMetaData = metaDataDiff.getNewMetaData(); // group change is handled by GroupChange, so the groups root keeps its original value - // to prevent any inconsistency + // to prevent any inconsistency - a library without groups stays without groups metaDataDiff.getGroupDifferences() - .ifPresent(groupDiff -> newMetaData.setGroups(groupDiff.getOriginalGroupRoot())); + .ifPresent(groupDiff -> Optional.ofNullable(groupDiff.getOriginalGroupRoot()) + .ifPresentOrElse(newMetaData::setGroups, newMetaData::clearGroups)); undoEdit.applyEdit(new UndoableMetaDataChange(databaseContext, databaseContext.getMetaData(), newMetaData)); } diff --git a/jabgui/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java b/jabgui/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java index 314fd9f80be3..224ee35ba71a 100644 --- a/jabgui/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java +++ b/jabgui/src/main/java/org/jabref/gui/dialogs/BackupUIManager.java @@ -8,7 +8,6 @@ import javafx.scene.control.ButtonType; import org.jabref.gui.DialogService; -import org.jabref.gui.LibraryTab; import org.jabref.gui.StateManager; import org.jabref.gui.autosaveandbackup.BackupManager; import org.jabref.gui.backup.BackupResolverDialog; @@ -121,13 +120,10 @@ private static Optional showReviewBackupDialog( Optional allChangesResolved = dialogService.showCustomDialogAndWait(reviewBackupDialog); if (allChangesResolved.orElse(false)) { List resolvedChanges = reviewBackupDialog.getResolvedChanges(); - LibraryTab saveState = stateManager.activeTabProperty().get().get(); + // The library is still being opened, so no tab exists for it yet - the active tab (if any) belongs to another + // library. Its change monitor is set up when the tab is created. stateManager.getUndoManager(originalDatabase).addEdit(Localization.lang("Merged external changes"), edit -> resolvedChanges.stream().filter(DatabaseChange::isAccepted).forEach(change -> change.applyChange(edit))); - if (reviewBackupDialog.areAllChangesDenied()) { - // Here the case of a backup file is handled: If no changes of the backup are merged in, the file stays the same - saveState.resetChangeMonitor(); - } // In case any change of the backup is accepted, the in-memory file differs from the file on disk (which is not the backup file) // This does NOT return the original ParserResult, but a modified version with all changes accepted or rejected diff --git a/jabgui/src/test/java/org/jabref/gui/dialogs/BackupUIManagerTest.java b/jabgui/src/test/java/org/jabref/gui/dialogs/BackupUIManagerTest.java index 498f956d3698..4b1d92349f9c 100644 --- a/jabgui/src/test/java/org/jabref/gui/dialogs/BackupUIManagerTest.java +++ b/jabgui/src/test/java/org/jabref/gui/dialogs/BackupUIManagerTest.java @@ -4,30 +4,45 @@ import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import javafx.scene.layout.StackPane; import org.jabref.gui.DialogService; +import org.jabref.gui.JabRefGuiStateManager; import org.jabref.gui.StateManager; import org.jabref.gui.backup.BackupResolverDialog; +import org.jabref.gui.clipboard.ClipBoardManager; +import org.jabref.gui.collab.DatabaseChange; +import org.jabref.gui.collab.DatabaseChangesResolverDialog; import org.jabref.gui.frame.ExternalApplicationsPreferences; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.preview.PreviewPreferences; import org.jabref.gui.testutils.JavaFxTest; import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Language; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.preview.TextBasedPreviewLayout; import org.jabref.logic.util.BackupFileType; +import org.jabref.logic.util.TaskExecutor; import org.jabref.logic.util.io.BackupFileUtil; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.entry.BibtexString; import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.groups.GroupTreeNode; import org.jabref.model.util.FileUpdateMonitor; +import com.airhacks.afterburner.injection.Injector; import org.controlsfx.control.HyperlinkLabel; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -38,6 +53,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -56,6 +72,11 @@ void setUp() { when(preferences.getExternalApplicationsPreferences()).thenReturn(mock(ExternalApplicationsPreferences.class)); } + @AfterEach + void tearDown() { + Injector.forgetAll(); + } + @Test void failedRestoreShowsBackupPathAndCause(@TempDir Path tempDir) throws IOException { Path backupDir = tempDir.resolve("backups"); @@ -82,6 +103,61 @@ void failedRestoreShowsBackupPathAndCause(@TempDir Path tempDir) throws IOExcept any(DirectoryNotEmptyException.class)); } + /// The review runs while the library is still being opened, so no tab exists for it yet and the active tab may be + /// absent altogether (welcome tab, startup). The review must not depend on it. The backup adds groups to a library + /// without groups, which produces a metadata change and a group change; accepting both has to install the groups. + @Test + void reviewBackupAcceptingAllChangesWorksWithoutAnActiveTab(@TempDir Path tempDir) throws IOException { + Path backupDir = tempDir.resolve("backups"); + when(preferences.getFilePreferences().getBackupDirectory()).thenReturn(backupDir); + when(dialogService.showCustomDialogAndWait(any(BackupResolverDialog.class))) + .thenReturn(Optional.of(BackupResolverDialog.REVIEW_BACKUP)); + when(dialogService.showCustomDialogAndWait(any(DatabaseChangesResolverDialog.class))) + .thenAnswer(invocation -> { + DatabaseChangesResolverDialog dialog = invocation.getArgument(0); + dialog.getResolvedChanges().forEach(DatabaseChange::accept); + return Optional.of(true); + }); + // Sealed, so deep stubs cannot mock it + PreviewPreferences previewPreferences = preferences.getPreviewPreferences(); + doReturn(new TextBasedPreviewLayout("", mock(LayoutFormatterPreferences.class), mock(JournalAbbreviationRepository.class))) + .when(previewPreferences).getSelectedPreviewLayout(); + Injector.setModelOrService(DialogService.class, dialogService); + Injector.setModelOrService(GuiPreferences.class, preferences); + Injector.setModelOrService(BibEntryTypesManager.class, new BibEntryTypesManager()); + Injector.setModelOrService(TaskExecutor.class, mock(TaskExecutor.class)); + Injector.setModelOrService(ClipBoardManager.class, mock(ClipBoardManager.class)); + + Path originalFile = tempDir.resolve("library.bib"); + Files.writeString(originalFile, "@Article{original, title = {Original}}"); + Path backupFile = BackupFileUtil.getPathForNewBackupFileAndCreateDirectory(originalFile, BackupFileType.BACKUP, backupDir); + Files.writeString(backupFile, """ + @Article{original, + title = {Original}, + } + @Article{added, + title = {Added}, + } + + @Comment{jabref-meta: grouping: + 0 AllEntriesGroup:; + 1 StaticGroup:TODO\\;0\\;1\\;0x8a8a8aff\\;\\;\\;; + } + """); + + // Called off the JavaFX thread, as the library loading task does + Optional result = BackupUIManager.showRestoreBackupDialog( + dialogService, + originalFile, + preferences, + mock(FileUpdateMonitor.class), + new JabRefGuiStateManager()); + + BibDatabaseContext restored = result.orElseThrow().getDatabaseContext(); + assertEquals(2, restored.getDatabase().getEntryCount()); + assertEquals(List.of("TODO"), restored.getMetaData().getGroups().orElseThrow().getChildren().stream().map(GroupTreeNode::getName).toList()); + } + @Test void backupResolverDialogShowsLibraryAndBackupSizes(@TempDir Path tempDir) throws IOException { Path originalFile = tempDir.resolve("library.bib"); From 359309f0f7be5a89403c3ff747059d4e20f4a452 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 23 Sep 2026 19:00:24 +0000 Subject: [PATCH 077/102] Add ARM64 Flatpak build channels (#17267) * Add ARM64 Flatpak build channels * Update Flatpak runtime * Restrict Flatpak stable channel to releases * add comment, sort alpha * Remove redundant Flatpak build output --- .github/workflows/binaries.yml | 89 ++++++++++++++++++++++++++++++- CHANGELOG.md | 1 + flatpak/org.jabref.jabref.desktop | 2 +- flatpak/org.jabref.jabref.json | 18 +++++-- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 255101b771f6..7690803f3300 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -566,8 +566,31 @@ jobs: echo "Link to PR: ${{ github.event.pull_request.html_url }}" >> _h5ai.header.md rsync -rt --chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r --itemize-changes --stats --rsync-path="mkdir -p /var/www/builds.jabref.org/www/${{ steps.normalize.outputs.branch }} && rsync" -e 'ssh -p 9922 -i sshkey -o StrictHostKeyChecking=no' _h5ai.header.md jrrsync@build-upload.jabref.org:/var/www/builds.jabref.org/www/${{ steps.normalize.outputs.branch }}/ + flatpak-channel: + runs-on: ubuntu-slim + outputs: + channel: ${{ steps.channel.outputs.channel }} + steps: + - name: Determine Flatpak channel + id: channel + shell: bash + run: | + channel=unsupported + + if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then + channel=nightly + elif [[ "$GITHUB_REF" == refs/tags/* ]]; then + if [[ "$GITHUB_REF_NAME" == *-beta* ]]; then + channel=beta + elif [[ "$GITHUB_REF_NAME" =~ ^v?[0-9]+(\.[0-9]+)+$ ]]; then + channel=stable + fi + fi + + echo "channel=$channel" >> "$GITHUB_OUTPUT" + build: - needs: [ compute-matrix, conditions, disk-space-check, pre-build ] + needs: [ compute-matrix, conditions, disk-space-check, flatpak-channel, pre-build ] strategy: fail-fast: false matrix: @@ -879,6 +902,17 @@ jobs: subdir: "" secretsPresent: "${{ needs.conditions.outputs.secretspresent }}" + - name: Upload portable JabRef for Flatpak packaging + if: > + needs.conditions.outputs.should-build == 'true' && + needs.flatpak-channel.outputs.channel != 'unsupported' && + startsWith(matrix.os, 'ubuntu') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: flatpak-source (${{ matrix.displayName }}) + path: jabgui/build/packages/${{ matrix.os }}/JabRef-portable_linux${{ matrix.suffix }}.tar.gz + if-no-files-found: error + - name: Upload jabgui and jabls-cli to GitHub workflow artifacts store for notarization (macOS) if: ${{ needs.conditions.outputs.should-build == 'true' && (startsWith(matrix.os, 'macos')) && (needs.conditions.outputs.should-notarize == 'true') }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -890,6 +924,59 @@ jobs: jabls-cli/build/packages/${{ matrix.os }} compression-level: 0 # no compression + flatpak: + needs: [ build, conditions, flatpak-channel ] + if: needs.conditions.outputs.should-build == 'true' && needs.flatpak-channel.outputs.channel != 'unsupported' + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + os: ubuntu-22.04 + displayName: linux-amd64 + archive: JabRef-portable_linux.tar.gz + - arch: aarch64 + os: ubuntu-22.04-arm + displayName: linux-arm + archive: JabRef-portable_linux_arm64.tar.gz + runs-on: ${{ matrix.os }} + name: ${{ matrix.displayName }} ${{ needs.flatpak-channel.outputs.channel }} Flatpak + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 + options: --privileged + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Download portable JabRef + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: flatpak-source (${{ matrix.displayName }}) + path: flatpak + + - name: Configure the manifest for the local portable archive + env: + ARCH: ${{ matrix.arch }} + ARCHIVE: ${{ matrix.archive }} + run: | + jq --arg arch "$ARCH" --arg archive "$ARCHIVE" ' + .modules[0].sources |= map( + if .["only-arches"] == [$arch] + then del(.url, .sha256) + {"path": $archive} + else . + end + ) + ' flatpak/org.jabref.jabref.json > flatpak/org.jabref.jabref.ci.json + + - name: Build and upload Flatpak + uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6 + with: + manifest-path: flatpak/org.jabref.jabref.ci.json + arch: ${{ matrix.arch }} + branch: ${{ needs.flatpak-channel.outputs.channel }} + bundle: org.jabref.jabref-${{ needs.flatpak-channel.outputs.channel }}.flatpak + artifact-name: JabRef Flatpak (${{ needs.flatpak-channel.outputs.channel }}, ${{ matrix.arch }}) + jabkit-native: needs: [ compute-matrix, conditions, disk-space-check, pre-build ] strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 703b40feca4b..46749ff4565c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added ARM64 Flatpak builds and nightly, beta, and stable channels. [#13349](https://github.com/JabRef/jabref/issues/13349) - We added `jabkit git merge-driver`, a Git merge driver that merges `.bib` files semantically. [#16838](https://github.com/JabRef/jabref/pull/16838) - We added tooltips with action descriptions to menu items. [#9615](https://github.com/JabRef/jabref/issues/9615) diff --git a/flatpak/org.jabref.jabref.desktop b/flatpak/org.jabref.jabref.desktop index 839978b0c88b..e1ff7f80f1fc 100644 --- a/flatpak/org.jabref.jabref.desktop +++ b/flatpak/org.jabref.jabref.desktop @@ -8,5 +8,5 @@ Icon=org.jabref.jabref Exec=JabRef %U Keywords=bibtex;biblatex;latex;bibliography Categories=Office; -StartupWMClass=org-jabref-JabRefMain +StartupWMClass=org.jabref.gui.JabRefGUI MimeType=text/x-bibtex; diff --git a/flatpak/org.jabref.jabref.json b/flatpak/org.jabref.jabref.json index f9d7373f9d84..0af87927370e 100644 --- a/flatpak/org.jabref.jabref.json +++ b/flatpak/org.jabref.jabref.json @@ -1,7 +1,7 @@ { "app-id" : "org.jabref.jabref", "runtime" : "org.freedesktop.Platform", - "runtime-version" : "18.08", + "runtime-version" : "26.08", "sdk" : "org.freedesktop.Sdk", "command" : "JabRef", "modules" : [ @@ -16,8 +16,20 @@ "sources" : [ { "type" : "file", - "url": "http://builds.jabref.org/master/JabRef-portable_linux.tar.gz", - "sha256": "be69feee0536fa8500dda867290dbeeeda6191c60e636a0b6e254fbaac02d471" + "url": "https://builds.jabref.org/main/linux-amd64/JabRef-portable_linux.tar.gz", + "sha256": "53e136e94beb0cb6174788352faf7983750945f5d397e7f6013b8921830b3b59", + "only-arches": [ + "x86_64" + ] + }, + { + "type" : "file", + "url": "https://builds.jabref.org/main/linux-arm/JabRef-portable_linux_arm64.tar.gz", + "sha256": "afa12af2d0e63d3e9016e3a44ea712ce98473222bffd0a94f4d302143a700e58", + "dest-filename": "JabRef-portable_linux.tar.gz", + "only-arches": [ + "aarch64" + ] }, { "type": "file", From cd4b2e7ddfe382fb786e5c215acdf609d9c926b4 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 23 Sep 2026 20:14:13 +0000 Subject: [PATCH 078/102] Avoid restoring Flatpak archive ownership (#17272) * Add ARM64 Flatpak build channels * Update Flatpak runtime * Restrict Flatpak stable channel to releases * add comment, sort alpha * Remove redundant Flatpak build output * Avoid restoring portable archive ownership --- flatpak/org.jabref.jabref.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/org.jabref.jabref.json b/flatpak/org.jabref.jabref.json index 0af87927370e..9a38c76077ad 100644 --- a/flatpak/org.jabref.jabref.json +++ b/flatpak/org.jabref.jabref.json @@ -9,7 +9,7 @@ "name" : "JabRef", "buildsystem" : "simple", "build-commands" : [ - "tar -xzf JabRef-portable_linux.tar.gz --directory=/app/ --strip-components=1", + "tar -xzf JabRef-portable_linux.tar.gz --no-same-owner --directory=/app/ --strip-components=1", "install -D -m0644 /app/lib/JabRef.png /app/share/icons/hicolor/64x64/apps/org.jabref.jabref.png", "install -D -m0644 org.jabref.jabref.desktop /app/share/applications/org.jabref.jabref.desktop" ], From 11badbdc0ba6c93bc9e9b7bad389cf7f2060a526 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:01:43 +0000 Subject: [PATCH 079/102] Chore(deps): Bump io.github.classgraph:classgraph in /versions (#17281) Bumps [io.github.classgraph:classgraph](https://github.com/classgraph/classgraph) from 4.8.195 to 4.8.196. - [Release notes](https://github.com/classgraph/classgraph/releases) - [Commits](https://github.com/classgraph/classgraph/compare/classgraph-4.8.195...classgraph-4.8.196) --- updated-dependencies: - dependency-name: io.github.classgraph:classgraph dependency-version: 4.8.196 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 264dc6d52dd2..720bc14f482c 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -88,7 +88,7 @@ dependencies.constraints { api("info.picocli:picocli-codegen:4.7.7") api("info.picocli:picocli:4.7.7") api("io.github.darvil82:terminal-text-formatter:2.3.0c") - api("io.github.classgraph:classgraph:4.8.195") + api("io.github.classgraph:classgraph:4.8.196") api("io.github.java-diff-utils:java-diff-utils:4.17") api("io.github.kusoroadeolu:veneer:1.3.2") api("io.github.stefanbratanov:jvm-openai:0.11.0") From b26ed9f3c145919a048cf7d0a70f6f3be6c302ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:02:51 +0000 Subject: [PATCH 080/102] Chore(deps): Bump org.mockito:mockito-core in /versions (#17284) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.23.0 to 5.24.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.23.0...v5.24.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.24.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 720bc14f482c..08a53c815c58 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -152,7 +152,7 @@ dependencies.constraints { api("org.kordamp.ikonli:ikonli-materialdesign2-pack:12.4.0") api("org.libreoffice:libreoffice:26.2.2") api("org.libreoffice:unoloader:26.2.2") - api("org.mockito:mockito-core:5.23.0") + api("org.mockito:mockito-core:5.24.0") api("org.ow2.asm:asm:9.10.1") api("org.postgresql:postgresql:42.7.13") api("org.slf4j:jul-to-slf4j:2.0.19") From 80f63d6fff2e486703cb34726f48e683906b5140 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:02:51 +0000 Subject: [PATCH 081/102] Chore(deps): Bump org.apache.httpcomponents.core5:httpcore5 in /versions (#17282) Bumps [org.apache.httpcomponents.core5:httpcore5](https://github.com/apache/httpcomponents-core) from 5.4.3 to 5.4.4. - [Changelog](https://github.com/apache/httpcomponents-core/blob/rel/v5.4.4/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-core/compare/rel/v5.4.3...rel/v5.4.4) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.core5:httpcore5 dependency-version: 5.4.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 08a53c815c58..6382dd314bef 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -111,7 +111,7 @@ dependencies.constraints { api("org.apache.commons:commons-csv:1.14.1") api("org.apache.commons:commons-lang3:3.20.0") api("org.apache.commons:commons-text:1.15.0") - api("org.apache.httpcomponents.core5:httpcore5:5.4.3") + api("org.apache.httpcomponents.core5:httpcore5:5.4.4") api("org.apache.httpcomponents.client5:httpclient5:5.6.4") api("org.apache.logging.log4j:log4j-to-slf4j:2.26.1") api("org.apache.lucene:lucene-analysis-common:$lucene") From 2aca86b2e1a40fbd5443c8e7270798887bfa9640 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:05:18 +0000 Subject: [PATCH 082/102] Chore(deps): Bump org.slf4j:slf4j-api from 2.0.19 to 2.0.20 in /versions (#17280) Bumps org.slf4j:slf4j-api from 2.0.19 to 2.0.20. --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-version: 2.0.20 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- versions/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index 6382dd314bef..b963ca136752 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -155,8 +155,8 @@ dependencies.constraints { api("org.mockito:mockito-core:5.24.0") api("org.ow2.asm:asm:9.10.1") api("org.postgresql:postgresql:42.7.13") - api("org.slf4j:jul-to-slf4j:2.0.19") - api("org.slf4j:slf4j-api:2.0.19") + api("org.slf4j:jul-to-slf4j:2.0.20") + api("org.slf4j:slf4j-api:2.0.20") api("org.tinylog:slf4j-tinylog:2.8.0") api("org.tinylog:tinylog-api:2.8.0") api("org.tinylog:tinylog-impl:2.8.0") From 05942d16307de0633b7a2505b33cdd51ab3b1ea0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:07:34 +0000 Subject: [PATCH 083/102] Chore(deps): Bump org.slf4j:jul-to-slf4j in /versions (#17283) Bumps org.slf4j:jul-to-slf4j from 2.0.19 to 2.0.20. --- updated-dependencies: - dependency-name: org.slf4j:jul-to-slf4j dependency-version: 2.0.20 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From f56d504b56b16ba337263a8dd609802a54a251e3 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 24 Sep 2026 17:52:26 +0000 Subject: [PATCH 084/102] Upload Flatpak bundles to builds server (#17273) * Add ARM64 Flatpak build channels * Update Flatpak runtime * Restrict Flatpak stable channel to releases * add comment, sort alpha * Remove redundant Flatpak build output * Avoid restoring portable archive ownership * Upload Flatpak bundles to builds server * Use standard Flatpak bundle upload * Wrap Flatpak upload command --- .github/workflows/binaries.yml | 44 +++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 7690803f3300..aed3b7f82a38 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -925,7 +925,7 @@ jobs: compression-level: 0 # no compression flatpak: - needs: [ build, conditions, flatpak-channel ] + needs: [ build, conditions, flatpak-channel, pre-build ] if: needs.conditions.outputs.should-build == 'true' && needs.flatpak-channel.outputs.channel != 'unsupported' strategy: fail-fast: false @@ -935,10 +935,12 @@ jobs: os: ubuntu-22.04 displayName: linux-amd64 archive: JabRef-portable_linux.tar.gz + suffix: '' - arch: aarch64 os: ubuntu-22.04-arm displayName: linux-arm archive: JabRef-portable_linux_arm64.tar.gz + suffix: _arm64 runs-on: ${{ matrix.os }} name: ${{ matrix.displayName }} ${{ needs.flatpak-channel.outputs.channel }} Flatpak container: @@ -974,8 +976,44 @@ jobs: manifest-path: flatpak/org.jabref.jabref.ci.json arch: ${{ matrix.arch }} branch: ${{ needs.flatpak-channel.outputs.channel }} - bundle: org.jabref.jabref-${{ needs.flatpak-channel.outputs.channel }}.flatpak - artifact-name: JabRef Flatpak (${{ needs.flatpak-channel.outputs.channel }}, ${{ matrix.arch }}) + bundle: JabRef-${{ needs.pre-build.outputs.Major }}.${{ needs.pre-build.outputs.Minor }}${{ matrix.suffix }}.flatpak + artifact-name: Flatpak bundle (${{ matrix.arch }}) + + upload-flatpak: + needs: [ conditions, disk-space-check, flatpak, pre-build ] + if: needs.conditions.outputs.upload-to-builds-jabref-org == 'true' && needs.disk-space-check.outputs.available == 'true' + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + displayName: linux-amd64 + suffix: '' + - arch: aarch64 + displayName: linux-arm + suffix: _arm64 + runs-on: ubuntu-22.04 + name: Upload ${{ matrix.displayName }} Flatpak to builds.jabref.org + steps: + - name: Download Flatpak bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: Flatpak bundle (${{ matrix.arch }}) + path: flatpak + + - name: Set up SSH key + run: | + echo "${{ secrets.buildJabRefPrivateKey }}" > sshkey + chmod 600 sshkey + + - name: Upload Flatpak bundle + shell: bash + run: | + rsync -rt --chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r --itemize-changes --stats \ + --rsync-path="mkdir -p /var/www/builds.jabref.org/www/${{ needs.pre-build.outputs.branch }}/${{ matrix.displayName }} && rsync" \ + -e 'ssh -p 9922 -i sshkey -o StrictHostKeyChecking=no' \ + flatpak/JabRef-${{ needs.pre-build.outputs.Major }}.${{ needs.pre-build.outputs.Minor }}${{ matrix.suffix }}.flatpak \ + jrrsync@build-upload.jabref.org:/var/www/builds.jabref.org/www/${{ needs.pre-build.outputs.branch }}/${{ matrix.displayName }}/ jabkit-native: needs: [ compute-matrix, conditions, disk-space-check, pre-build ] From bf9352eeba1021493f2997858681c4a5fbbe7238 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 24 Sep 2026 19:47:33 +0000 Subject: [PATCH 085/102] Add "Contribute to JabRef" to the Help menu (#17162) * Add "Contribute to JabRef" to the Help menu Also fixes the "Community forum" item, which opened the donation page. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01XUssZKck3iTUDqBVgLtsT3 * Link PR in CHANGELOG.md Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01XUssZKck3iTUDqBVgLtsT3 --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: github-actions[bot] --- CHANGELOG.md | 2 ++ .../src/main/java/org/jabref/gui/actions/StandardActions.java | 1 + jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java | 3 ++- jabgui/src/main/java/org/jabref/gui/icon/IconTheme.java | 1 + jabgui/src/main/java/org/jabref/gui/util/URLs.java | 1 + jablib/src/main/resources/l10n/JabRef_en.properties | 1 + 6 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46749ff4565c..b2aab5347c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added ARM64 Flatpak builds and nightly, beta, and stable channels. [#13349](https://github.com/JabRef/jabref/issues/13349) - We added `jabkit git merge-driver`, a Git merge driver that merges `.bib` files semantically. [#16838](https://github.com/JabRef/jabref/pull/16838) - We added tooltips with action descriptions to menu items. [#9615](https://github.com/JabRef/jabref/issues/9615) +- We added "Contribute to JabRef" to the Help menu, linking to [contribute.jabref.org](https://contribute.jabref.org). [#17162](https://github.com/JabRef/jabref/pull/17162) ### Changed @@ -25,6 +26,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) - We fixed an issue where accepting a backup that adds groups to a library without groups failed with an error. [#17165](https://github.com/JabRef/jabref/pull/17165) - We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) +- We fixed an issue where "Community forum" in the Help menu opened the donation page instead of the forum. [#17162](https://github.com/JabRef/jabref/pull/17162) ### Removed diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 4f508421e032..2c028b51b9b3 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -202,6 +202,7 @@ public enum StandardActions implements Action { OPEN_CHANGELOG(Localization.lang("View change log"), Localization.lang("See what has been changed in the JabRef versions")), OPEN_GITHUB("GitHub", Localization.lang("Opens JabRef's GitHub page"), IconTheme.JabRefIcons.GITHUB), DONATE(Localization.lang("Donate to JabRef"), Localization.lang("Donate to JabRef"), IconTheme.JabRefIcons.DONATE), + CONTRIBUTE(Localization.lang("Contribute to JabRef"), Localization.lang("Contribute to JabRef"), IconTheme.JabRefIcons.CONTRIBUTE), OPEN_FORUM(Localization.lang("Community forum"), Localization.lang("Community forum"), IconTheme.JabRefIcons.FORUM), ERROR_CONSOLE(Localization.lang("View event log"), Localization.lang("Display all error messages")), SEARCH_FOR_UPDATES(Localization.lang("Check for updates")), diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 9da5a1be4def..8f7a72307659 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -372,7 +372,7 @@ private void createMenu() { help.getItems().addAll( factory.createMenuItem(StandardActions.HELP, new HelpAction(HelpFile.CONTENTS, dialogService, preferences.getExternalApplicationsPreferences())), - factory.createMenuItem(StandardActions.OPEN_FORUM, new OpenBrowserAction(URLs.DONATE_URL, dialogService, preferences.getExternalApplicationsPreferences())), + factory.createMenuItem(StandardActions.OPEN_FORUM, new OpenBrowserAction(URLs.FORUM_URL, dialogService, preferences.getExternalApplicationsPreferences())), new SeparatorMenuItem(), @@ -381,6 +381,7 @@ private void createMenu() { new SeparatorMenuItem(), factory.createMenuItem(StandardActions.DONATE, new OpenBrowserAction(URLs.DONATION_URL, dialogService, preferences.getExternalApplicationsPreferences())), + factory.createMenuItem(StandardActions.CONTRIBUTE, new OpenBrowserAction(URLs.CONTRIBUTE_URL, dialogService, preferences.getExternalApplicationsPreferences())), factory.createMenuItem(StandardActions.SEARCH_FOR_UPDATES, new SearchForUpdateAction(preferences, dialogService, taskExecutor)), factory.createSubMenu(StandardActions.WEB_MENU, factory.createMenuItem(StandardActions.OPEN_WEBPAGE, new OpenBrowserAction(URLs.WEBPAGE_URL, dialogService, preferences.getExternalApplicationsPreferences())), diff --git a/jabgui/src/main/java/org/jabref/gui/icon/IconTheme.java b/jabgui/src/main/java/org/jabref/gui/icon/IconTheme.java index c69fd44dd697..bf8aab753af9 100644 --- a/jabgui/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/jabgui/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -221,6 +221,7 @@ public enum JabRefIcons implements JabRefIcon { PLAIN_TEXT_IMPORT_TODO(MaterialDesignC.CHECKBOX_BLANK_CIRCLE_OUTLINE), PLAIN_TEXT_IMPORT_DONE(MaterialDesignC.CHECKBOX_MARKED_CIRCLE_OUTLINE), DONATE(MaterialDesignG.GIFT), + CONTRIBUTE(MaterialDesignS.SOURCE_PULL), MOVE_TAB_ARROW(MaterialDesignA.ARROW_UP_BOLD), OPTIONAL(MaterialDesignL.LABEL_OUTLINE), REQUIRED(MaterialDesignL.LABEL), diff --git a/jabgui/src/main/java/org/jabref/gui/util/URLs.java b/jabgui/src/main/java/org/jabref/gui/util/URLs.java index 38a31b231966..1b5e65287c54 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/URLs.java +++ b/jabgui/src/main/java/org/jabref/gui/util/URLs.java @@ -10,6 +10,7 @@ public class URLs { public static final String GITHUB_URL = "https://github.com/JabRef/jabref"; public static final String MASTODON_URL = "https://foojay.social/@jabref"; public static final String DONATE_URL = "https://donations.jabref.org"; + public static final String CONTRIBUTE_URL = "https://contribute.jabref.org"; public static final String DEV_VERSION_LINK_URL = "https://builds.jabref.org/master/"; public static final String CHANGELOG_URL = "https://github.com/JabRef/jabref/blob/main/CHANGELOG.md"; public static final String HELP_URL = "https://docs.jabref.org/"; diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 63c933b56930..ec9796780da9 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -1406,6 +1406,7 @@ Set\ read\ status\ to\ read=Set read status to read Set\ read\ status\ to\ skimmed=Set read status to skimmed Donate\ to\ JabRef=Donate to JabRef +Contribute\ to\ JabRef=Contribute to JabRef Donation=Donation Support\ JabRef=Support JabRef Help\ us\ improve\ JabRef\ by\ donating.=Help us improve JabRef by donating. From 403c41d27fa19e64e068031bb82de1b9c87a3605 Mon Sep 17 00:00:00 2001 From: Dark Blue Date: Fri, 25 Sep 2026 09:03:14 +0000 Subject: [PATCH 086/102] Fix flatpak (#17287) --- flatpak/org.jabref.jabref.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flatpak/org.jabref.jabref.json b/flatpak/org.jabref.jabref.json index 9a38c76077ad..6ca7084c67bb 100644 --- a/flatpak/org.jabref.jabref.json +++ b/flatpak/org.jabref.jabref.json @@ -39,8 +39,7 @@ } ], "finish-args" : [ - "--socket=wayland", - "--socket=fallback-x11", + "--socket=x11", "--share=network", "--filesystem=home" ] From c9f654696b8f9a485dee6244d750b7305ce7bdb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:16:48 +0000 Subject: [PATCH 087/102] Chore(deps): Bump gradle-wrapper from 9.7.1 to 9.8.0 (#17288) * Chore(deps): Bump gradle-wrapper from 9.7.1 to 9.8.0 Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.7.1 to 9.8.0. - [Release notes](https://github.com/gradle/gradle/releases) - [Commits](https://github.com/gradle/gradle/compare/v9.7.1...v9.8.0) --- updated-dependencies: - dependency-name: gradle-wrapper dependency-version: 9.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Fix issues using OpenRewrite --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- gradle/wrapper/gradle-wrapper.jar | Bin 47505 -> 47623 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 52 ++++++++++++++++++----- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index eddabd2eef8d94a5437d6168ff9c87a78ff725b3..5097068a8d375f5bf0d15693fb5b3615c972a801 100644 GIT binary patch delta 38775 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ad7845be3062..9b7cb4af38a3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew.bat b/gradlew.bat index a51ec4f5886d..3185a43f78fa 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,33 @@ @rem Set local scope for the variables, and ensure extensions are enabled setlocal EnableExtensions +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused @@ -45,13 +72,14 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. "%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,13 +87,14 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. "%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line @@ -75,8 +104,9 @@ echo location of your Java installation. 1>&2 @rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel +@rem This label must not be changed. We rely on old scripts being able to jump to this point. :exitWithErrorLevel @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts "%COMSPEC%" /c exit %ERRORLEVEL% From c9543b11204439944e3ea2829bc8045e250510d3 Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 25 Sep 2026 15:26:55 +0000 Subject: [PATCH 088/102] New translations jabref_en.properties for (Italian) (#17289) --- jablib/src/main/resources/l10n/JabRef_it.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/jablib/src/main/resources/l10n/JabRef_it.properties b/jablib/src/main/resources/l10n/JabRef_it.properties index 3bca3898ca54..b3ce32bf29fe 100644 --- a/jablib/src/main/resources/l10n/JabRef_it.properties +++ b/jablib/src/main/resources/l10n/JabRef_it.properties @@ -1405,6 +1405,7 @@ Set\ read\ status\ to\ read=Imposta lo stato di lettura a 'letto' Set\ read\ status\ to\ skimmed=Imposta lo stato di lettura a 'scorso' Donate\ to\ JabRef=Fai una donazione a JabRef +Contribute\ to\ JabRef=Contribuisci a JabRef Donation=Donazione Support\ JabRef=Supporta JabRef Help\ us\ improve\ JabRef\ by\ donating.=Aiutaci a migliorare JabRef donando. From a1fa7c2a24aa5e8b05db9b99070eb0527e17e185 Mon Sep 17 00:00:00 2001 From: Aman Gautam Date: Fri, 25 Sep 2026 18:25:48 +0000 Subject: [PATCH 089/102] Allow Backspace in keyboard shortcuts (#17277) * Allow Backspace in keyboard shortcuts * Fix keyboard shortcut test formatting * Fix keyboard shortcut test formatting * Fix Backspace shortcut formatting and changelog * Fix keyboard shortcut test formatting --- CHANGELOG.md | 1 + .../keybindings/KeyBindingViewModel.java | 2 +- .../keybindings/KeyBindingViewModelTest.java | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2aab5347c52..5960731e569a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed +- We fixed an issue where Backspace could not be registered as a keyboard shortcut. [#17241](https://github.com/JabRef/jabref/issues/17241) - We re-enabled the [Grobid](https://github.com/grobidOrg/grobid) citation fetcher again. It is now reachable via https. [#16668](https://github.com/JabRef/jabref/issues/16668) - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModel.java index 283ceaedce83..66851552d566 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModel.java @@ -93,7 +93,7 @@ public boolean setNewBinding(KeyEvent evt) { // validate the shortcut is no modifier key KeyCode code = evt.getCode(); - if (code.isModifierKey() || (code == KeyCode.BACK_SPACE) || (code == KeyCode.SPACE) || (code == KeyCode.TAB) + if (code.isModifierKey() || (code == KeyCode.SPACE) || (code == KeyCode.TAB) || (code == KeyCode.ENTER) || (code == KeyCode.UNDEFINED)) { return false; } diff --git a/jabgui/src/test/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModelTest.java b/jabgui/src/test/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModelTest.java index 2327f5821194..e28ca08ddd63 100644 --- a/jabgui/src/test/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModelTest.java +++ b/jabgui/src/test/java/org/jabref/gui/preferences/keybindings/KeyBindingViewModelTest.java @@ -56,6 +56,43 @@ void resetToDefault() { assertFalse(keyBindingRepository.checkKeyCombinationEquality(KeyBinding.MERGE_ENTRIES, shortcutKeyEvent)); } + @Test + @EnabledOnOs(OS.MAC) + void controlBackspaceIsRecordedAsBindingOnMacOS() { + KeyBindingRepository liveRepo = new KeyBindingRepository(); + + KeyBindingsTabViewModel viewModel = + new KeyBindingsTabViewModel(liveRepo, mock(DialogService.class)); + + KeyBinding binding = KeyBinding.EDITOR_KILL_WORD_BACKWARD; + + KeyBindingViewModel selectedVM = + new KeyBindingViewModel( + viewModel.getKeyBindingRepository(), + binding, + binding.getDefaultKeyBinding()); + + viewModel.selectedKeyBindingProperty().set(Optional.of(selectedVM)); + + KeyEvent event = new KeyEvent( + KeyEvent.KEY_PRESSED, + "", + "", + KeyCode.BACK_SPACE, + false, + true, + false, + false + ); + + viewModel.setNewBindingForCurrent(event); + viewModel.storeSettings(); + + Optional saved = liveRepo.get(binding); + + assertEquals(Optional.of("ctrl+BACK_SPACE"), saved); + } + @Test @DisabledOnCIServer("locally runs fine") void verifyStoreSettingsWritesChanges() { From 050a2581a86a5d795b8bb729ea21be4d0e9c41a6 Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 25 Sep 2026 18:26:48 +0000 Subject: [PATCH 090/102] Import metadata from DNB MARC records (#17217) * fix(marc): import DNB metadata * docs: link DNB MARC PR * docs: trace DNB MARC import * fix(marc): classify DNB host items * fix(marc): normalize ISBN preference * fix indentation * test(marc): source DNB fixtures * move up, refine req * simplify to if else and early return * change comments --------- Co-authored-by: github-actions[bot] Co-authored-by: Ruslan --- CHANGELOG.md | 1 + docs/requirements/import.md | 9 ++ .../importer/fileformat/MarcXmlParser.java | 103 ++++++++++++------ .../fileformat/MarcXmlParserTest.java | 71 ++++++++++++ .../DnbMarcXmlContentDescriptionRecord.xml | 24 ++++ .../fileformat/DnbMarcXmlDoiRecord.xml | 30 +++++ .../fileformat/DnbMarcXmlFulltextRecord.xml | 25 +++++ .../DnbMarcXmlIsbnOrderingRecord.xml | 39 +++++++ .../DnbMarcXmlMonographicPartRecord.xml | 27 +++++ .../DnbMarcXmlParentJournalRecord.xml | 26 +++++ .../MarcXMLParserTestInProceedings.bib | 4 +- .../MarcXMLParserTestInProceedings.xml | 12 +- 12 files changed, 331 insertions(+), 40 deletions(-) create mode 100644 jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlContentDescriptionRecord.xml create mode 100644 jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlDoiRecord.xml create mode 100644 jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlFulltextRecord.xml create mode 100644 jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlIsbnOrderingRecord.xml create mode 100644 jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlMonographicPartRecord.xml create mode 100644 jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlParentJournalRecord.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5960731e569a..25576e2a0348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where Backspace could not be registered as a keyboard shortcut. [#17241](https://github.com/JabRef/jabref/issues/17241) - We re-enabled the [Grobid](https://github.com/grobidOrg/grobid) citation fetcher again. It is now reachable via https. [#16668](https://github.com/JabRef/jabref/issues/16668) - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) +- We fixed DNB MARC imports to preserve abstracts, DOIs, ISBN-13s, ISSNs, parent journal titles, and full-text links. [#17217](https://github.com/JabRef/jabref/pull/17217) - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) - We fixed an issue where accepting a backup that adds groups to a library without groups failed with an error. [#17165](https://github.com/JabRef/jabref/pull/17165) diff --git a/docs/requirements/import.md b/docs/requirements/import.md index 9a250e5bd3b6..5b421dbc94ae 100644 --- a/docs/requirements/import.md +++ b/docs/requirements/import.md @@ -3,6 +3,15 @@ parent: Requirements --- # Import +## Import MARC21-XML bibliographic records +`req~import.marc21-xml~1` + +JabRef supports importing MARC21 bibliographic records encoded as [MARCXML](https://www.loc.gov/standards/marcxml/) and maps recognised fields according to the [MARC 21 Format for Bibliographic Data](https://www.loc.gov/marc/bibliographic/) and the [DNB MARC21 export format](https://www.dnb.de/DE/Professionell/Metadatendienste/Exportformate/MARC21/marc21.html). + +An importer should prefer proper fulltext links (e.g. Volltext) for PDFs over other content links. + +Needs: impl, utest + ## Normalize imported BibTeX keyword delimiters `req~import.bibtex.keywords.normalize-delimiters~1` diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/MarcXmlParser.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/MarcXmlParser.java index 1ec135f496a1..9f4dc2358c7f 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/MarcXmlParser.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/MarcXmlParser.java @@ -26,6 +26,7 @@ import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.identifier.ISBN; import org.jabref.model.entry.types.StandardEntryType; import org.slf4j.Logger; @@ -36,19 +37,19 @@ import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -/// A parser for the bavarian flavour (Bibliotheksverbund Bayern) of the marc xml standard +/// A parser for the MARC21-XML standard, used by DNB and BVB /// /// See [Feldbeschreibung der Titeldaten bei der Deutschen Nationalbibliothek](https://www.dnb.de/DE/Professionell/Metadatendienste/Exportformate/MARC21/marc21_node.html) /// -/// For further information see -/// -/// - https://www.bib-bvb.de/web/kkb-online/rda-felderverzeichnis-des-b3kat-aseq -/// - https://www.loc.gov/marc/bibliographic/ for detailed documentation -/// - for modifications in B3Kat https://www.bib-bvb.de/documents/10792/9f51a033-5ca1-42e2-b2d3-a75e7f1512d4 -/// - https://www.dnb.de/DE/Professionell/Metadatendienste/Exportformate/MARC21/marc21_node.html -/// - https://www.dnb.de/SharedDocs/Downloads/DE/Professionell/Standardisierung/AGV/marc21VereinbarungDatentauschTeil1.pdf?__blob=publicationFile&v=2 -/// - about multiple books in a series https://www.dnb.de/SharedDocs/Downloads/DE/Professionell/Standardisierung/marc21FormatumstiegAbbildungBegrenzterWerke2008.pdf?__blob=publicationFile&v=2> +/// For further information see: /// +/// - https://www.bib-bvb.de/web/kkb-online/rda-felderverzeichnis-des-b3kat-aseq +/// - https://www.loc.gov/marc/bibliographic/ for detailed documentation +/// - for modifications in B3Kat https://www.bib-bvb.de/documents/10792/9f51a033-5ca1-42e2-b2d3-a75e7f1512d4 +/// - https://www.dnb.de/DE/Professionell/Metadatendienste/Exportformate/MARC21/marc21_node.html +/// - https://www.dnb.de/SharedDocs/Downloads/DE/Professionell/Standardisierung/AGV/marc21VereinbarungDatentauschTeil1.pdf?__blob=publicationFile&v=2 +/// - about multiple books in a series https://www.dnb.de/SharedDocs/Downloads/DE/Professionell/Standardisierung/marc21FormatumstiegAbbildungBegrenzterWerke2008.pdf?__blob=publicationFile&v=2> +// [impl->req~import.marc21-xml~1] public class MarcXmlParser implements Parser { private static final Logger LOGGER = LoggerFactory.getLogger(MarcXmlParser.class); private static final String DISALLOW_DOCTYPE_DECLARATION = "http://apache.org/xml/features/disallow-doctype-decl"; @@ -102,12 +103,18 @@ private BibEntry parseEntry(Element element) { BibEntry bibEntry = new BibEntry(BibEntry.DEFAULT_TYPE); List datafields = getChildren("datafield", element); + Optional hostItemBibliographicLevel = getHostItemBibliographicLevel(datafields); + for (Element datafield : datafields) { String tag = datafield.getAttribute("tag"); LOGGER.debug("tag: {}", tag); if ("020".equals(tag)) { putIsbn(bibEntry, datafield); + } else if ("022".equals(tag)) { + putIssn(bibEntry, datafield); + } else if ("024".equals(tag)) { + putOtherStandardIdentifier(bibEntry, datafield); } else if ("100".equals(tag) || "700".equals(tag) || "710".equals(tag)) { putPersonalName(bibEntry, datafield); // Author, Editor, Publisher } else if ("111".equals(tag)) { @@ -130,7 +137,7 @@ private BibEntry parseEntry(Element element) { } else if ("653".equals(tag)) { putKeywords(bibEntry, datafield); } else if ("773".equals(tag)) { - putIssue(bibEntry, datafield); + putHostItem(bibEntry, datafield, hostItemBibliographicLevel); } else if ("856".equals(tag)) { putElectronicLocation(bibEntry, datafield); } else if ("966".equals(tag)) { @@ -153,20 +160,32 @@ private void putIsbn(BibEntry bibEntry, Element datafield) { return; } - int length = isbn.length(); - if (length != 10 && length != 13) { - LOGGER.debug("Malformed ISBN received, length: {}", length); + Optional parsedIsbn = ISBN.parse(isbn); + if (parsedIsbn.isEmpty()) { + LOGGER.debug("Malformed ISBN received: {}", isbn); return; } - Optional field = bibEntry.getField(StandardField.ISBN); - if (field.isPresent()) { - // Only overwrite the field, if it's ISBN13 - if (field.get().length() == 13) { - bibEntry.setField(StandardField.ISBN, isbn); - } - } else { - bibEntry.setField(StandardField.ISBN, isbn); + ISBN validIsbn = parsedIsbn.get(); + Optional existingIsbn = bibEntry.getField(StandardField.ISBN).flatMap(ISBN::parse); + if (existingIsbn.isEmpty() || (validIsbn.isIsbn13() && existingIsbn.get().isIsbn10())) { + bibEntry.setField(StandardField.ISBN, validIsbn.asString()); + } + } + + private void putIssn(BibEntry bibEntry, Element datafield) { + String issn = getSubfield("a", datafield); + if (StringUtil.isNotBlank(issn)) { + bibEntry.setField(StandardField.ISSN, issn); + } + } + + private void putOtherStandardIdentifier(BibEntry bibEntry, Element datafield) { + String identifier = getSubfield("a", datafield); + String source = getSubfield("2", datafield); + + if (StringUtil.isNotBlank(identifier) && "doi".equalsIgnoreCase(source)) { + bibEntry.setField(StandardField.DOI, identifier); } } @@ -335,13 +354,10 @@ private void putThesisDescription(BibEntry bibEntry, Element datafield) { private void putSummary(BibEntry bibEntry, Element datafield) { String summary = getSubfield("a", datafield); - String ind1 = datafield.getAttribute("ind1"); - if (StringUtil.isNotBlank(summary) && StringUtil.isNotBlank(ind1) && "3".equals(ind1)) { // Abstract - if (bibEntry.getField(StandardField.ABSTRACT).isPresent()) { - bibEntry.setField(StandardField.ABSTRACT, bibEntry.getField(StandardField.ABSTRACT).get().concat(summary)); - } else { - bibEntry.setField(StandardField.ABSTRACT, summary); - } + if (StringUtil.isNotBlank(summary)) { + bibEntry.getField(StandardField.ABSTRACT).ifPresentOrElse( + abstractValue -> bibEntry.setField(StandardField.ABSTRACT, abstractValue.concat(summary)), + () -> bibEntry.setField(StandardField.ABSTRACT, summary)); } } @@ -358,8 +374,16 @@ private void putKeywords(BibEntry bibEntry, Element datafield) { } } - private void putIssue(BibEntry bibEntry, Element datafield) { - bibEntry.setType(StandardEntryType.Article); + private void putHostItem(BibEntry bibEntry, Element datafield, Optional hostItemBibliographicLevel) { + String hostTitle = getSubfield("t", datafield); + if (hostItemBibliographicLevel.filter(level -> level == 's').isPresent()) { + bibEntry.setType(StandardEntryType.Article); + if (StringUtil.isNotBlank(hostTitle)) { + bibEntry.setField(StandardField.JOURNAL, hostTitle); + } + } else if (hostItemBibliographicLevel.filter(level -> level == 'm').isPresent() && StringUtil.isNotBlank(hostTitle)) { + bibEntry.setField(StandardField.BOOKTITLE, hostTitle); + } List issues = getSubfields("g", datafield); @@ -389,6 +413,16 @@ private void putIssue(BibEntry bibEntry, Element datafield) { } } + private Optional getHostItemBibliographicLevel(List datafields) { + return datafields.stream() + .filter(datafield -> "773".equals(datafield.getAttribute("tag"))) + .map(datafield -> getSubfield("7", datafield)) + .filter(StringUtil::isNotBlank) + .filter(controlSubfield -> controlSubfield.length() > 3) + .map(controlSubfield -> controlSubfield.charAt(3)) + .findFirst(); + } + private void putDoi(BibEntry bibEntry, Element datafield) { String ind1 = datafield.getAttribute("ind1"); String resource = getSubfield("u", datafield); @@ -400,13 +434,10 @@ private void putDoi(BibEntry bibEntry, Element datafield) { } private void putElectronicLocation(BibEntry bibEntry, Element datafield) { - // 856 - fulltext pdf url - String ind1 = datafield.getAttribute("ind1"); - String ind2 = datafield.getAttribute("ind2"); + String fulltext = getSubfield("3", datafield); + String resource = getSubfield("u", datafield); - if ("4".equals(ind1) && "0".equals(ind2)) { - String fulltext = getSubfield("3", datafield); - String resource = getSubfield("u", datafield); + if ("Volltext".equals(fulltext) && StringUtil.isNotBlank(resource)) { handleVolltext(bibEntry, fulltext, resource, StandardField.URL); } } diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/MarcXmlParserTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/MarcXmlParserTest.java index 8661117759f5..4adf6ef71bae 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/MarcXmlParserTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/MarcXmlParserTest.java @@ -5,21 +5,26 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; import org.jabref.logic.importer.ParseException; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; import org.jabref.support.BibEntryAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +// [utest->req~import.marc21-xml~1] class MarcXmlParserTest { private static final String FILE_ENDING = ".xml"; @@ -45,6 +50,72 @@ void importEntries(String fileName) throws IOException, ParseException { doTest(fileName, bibName); } + @Test + void importsDoiFromDnbMarcXml() throws IOException, ParseException { + try (InputStream inputStream = MarcXmlParserTest.class.getResourceAsStream("DnbMarcXmlDoiRecord.xml")) { + List entries = new MarcXmlParser().parseEntries(inputStream); + + assertEquals(1, entries.size()); + assertEquals("9783031996870", entries.getFirst().getField(StandardField.ISBN).orElseThrow()); + assertEquals("10.1007/978-3-031-99687-0", entries.getFirst().getField(StandardField.DOI).orElseThrow()); + } + } + + @Test + void prefersIsbn13RegardlessOfHyphenatedIsbnOrder() throws IOException, ParseException { + try (InputStream inputStream = MarcXmlParserTest.class.getResourceAsStream("DnbMarcXmlIsbnOrderingRecord.xml")) { + List entries = new MarcXmlParser().parseEntries(inputStream); + + assertEquals(2, entries.size()); + assertEquals("9783570184783", entries.getFirst().getField(StandardField.ISBN).orElseThrow()); + assertEquals("9783570184783", entries.get(1).getField(StandardField.ISBN).orElseThrow()); + } + } + + @Test + void importsParentJournalFromDnbMarcXml() throws IOException, ParseException { + try (InputStream inputStream = MarcXmlParserTest.class.getResourceAsStream("DnbMarcXmlParentJournalRecord.xml")) { + List entries = new MarcXmlParser().parseEntries(inputStream); + + assertEquals(1, entries.size()); + assertEquals(StandardEntryType.Article, entries.getFirst().getType()); + assertEquals("Seminars in speech and language", entries.getFirst().getField(StandardField.JOURNAL).orElseThrow()); + } + } + + @Test + void doesNotChangeMonographicPartToArticle() throws IOException, ParseException { + try (InputStream inputStream = MarcXmlParserTest.class.getResourceAsStream("DnbMarcXmlMonographicPartRecord.xml")) { + List entries = new MarcXmlParser().parseEntries(inputStream); + + assertEquals(1, entries.size()); + assertEquals(StandardEntryType.Misc, entries.getFirst().getType()); + assertEquals(Optional.empty(), entries.getFirst().getField(StandardField.JOURNAL)); + } + } + + @Test + void ignoresDnbContentDescriptionUrl() throws IOException, ParseException { + try (InputStream inputStream = MarcXmlParserTest.class.getResourceAsStream("DnbMarcXmlContentDescriptionRecord.xml")) { + List entries = new MarcXmlParser().parseEntries(inputStream); + + assertEquals(1, entries.size()); + assertEquals(List.of(), entries.getFirst().getFiles()); + assertEquals(Optional.empty(), entries.getFirst().getField(StandardField.URL)); + } + } + + @Test + void importsDnbFulltextUrl() throws IOException, ParseException { + try (InputStream inputStream = MarcXmlParserTest.class.getResourceAsStream("DnbMarcXmlFulltextRecord.xml")) { + List entries = new MarcXmlParser().parseEntries(inputStream); + + assertEquals(1, entries.size()); + assertEquals("https://www.swp-berlin.org/publications/products/aktuell/2026A25_Europaeische_Staatsanwaltschaft.pdf", entries.getFirst().getFiles().getFirst().getLink()); + assertEquals(Optional.empty(), entries.getFirst().getField(StandardField.URL)); + } + } + @Test void rejectsDocumentTypeDeclarations() { String xmlWithDoctype = """ diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlContentDescriptionRecord.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlContentDescriptionRecord.xml new file mode 100644 index 000000000000..25b0c7f51b9c --- /dev/null +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlContentDescriptionRecord.xml @@ -0,0 +1,24 @@ + + + 1.1 + 1 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + 1415413312 + + X:MVB + text/html + https://services.dnb.de/plus/idn/1415413312/blurb/ + Inhaltstext + + + + 1 + + + diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlDoiRecord.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlDoiRecord.xml new file mode 100644 index 000000000000..3cbfba4ed602 --- /dev/null +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlDoiRecord.xml @@ -0,0 +1,30 @@ + + + 1.1 + 1 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + 138813876X + + 9783031996870 + 978-3-031-99687-0 + + + 10.1007/978-3-031-99687-0 + doi + + + urn + urn:nbn:de:101:1-2601280312356.532271176782 + + + + 1 + + + diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlFulltextRecord.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlFulltextRecord.xml new file mode 100644 index 000000000000..0bfc3aa2f8e0 --- /dev/null +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlFulltextRecord.xml @@ -0,0 +1,25 @@ + + + 1.1 + 1 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + 1413727956 + + application/pdf + https://www.swp-berlin.org/publications/products/aktuell/2026A25_Europaeische_Staatsanwaltschaft.pdf + Verlag + kostenfrei + Volltext + + + + 1 + + + diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlIsbnOrderingRecord.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlIsbnOrderingRecord.xml new file mode 100644 index 000000000000..633807fb8ffc --- /dev/null +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlIsbnOrderingRecord.xml @@ -0,0 +1,39 @@ + + + 1.1 + 2 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + + 3-570-18478-1 + + + 978-3-570-18478-3 + + + + 1 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + + 978-3-570-18478-3 + + + 3-570-18478-1 + + + + 2 + + + diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlMonographicPartRecord.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlMonographicPartRecord.xml new file mode 100644 index 000000000000..07bf0ac2e199 --- /dev/null +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlMonographicPartRecord.xml @@ -0,0 +1,27 @@ + + + 1.1 + 1 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + 00000nam a2200000 cc4500 + 1396020055 + + 1000 Jahre Mörschbach 1006-2006 + Teil 1 + + + 11 + (DE-101)1396790446 + + + + 1 + + + diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlParentJournalRecord.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlParentJournalRecord.xml new file mode 100644 index 000000000000..f777f1456f9f --- /dev/null +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/DnbMarcXmlParentJournalRecord.xml @@ -0,0 +1,26 @@ + + + 1.1 + 1 + + + info:srw/schema/1/marcxml-v1.1 + xml + + + 00000naa a2200000uc 4500 + 1164985388 + + |||s + + + Enthalten in + Seminars in speech and language + + + + 6 + + + diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.bib b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.bib index a36825d4bc34..2201afdc901a 100644 --- a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.bib +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.bib @@ -2,8 +2,10 @@ @Proceedings{ title = {VI International Congress on Experimental Mechanics}, year = {1988}, address = {London}, + abstract = {Summary of the conference proceedings.}, editor = {Adams, Frank}, - isbn = {1851662529}, + isbn = {9783570184783}, + issn = {1234-5679}, publisher = {{Elsevier [u.a.]}}, eventtitle = {International Congress on Experimental Mechanics}, subtitle = {[proceedings of the VI International Congress on Experimental Mechanics ... held on June 5 - 10, 1988 in Portland, Oregon, U.S.A.]}, diff --git a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.xml b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.xml index 617c406c36fe..6c5dca5494a2 100644 --- a/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.xml +++ b/jablib/src/test/resources/org/jabref/logic/importer/fileformat/MarcXMLParserTestInProceedings.xml @@ -14,13 +14,16 @@ t 230630m1988uuuu |||| 10||| eng d - 1851662529 - 1-85166-252-9 + 978-3-570-18478-3 + 978-3-570-18478-3 - 0912053208 + 0-912053-20-8 0-912053-20-8 + + 1234-5679 + DE-604 ger @@ -47,6 +50,9 @@ Elsevier [u.a.] 1988 + + Summary of the conference proceedings. + (DE-588)1071861417 Konferenzschrift From 1f4641f0ecfe18242b0ec9fc4eed6eba3fc81dc6 Mon Sep 17 00:00:00 2001 From: Ahmed Ragab Ali <166864012+A7MEDRAGAB82@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:32:04 +0000 Subject: [PATCH 091/102] Fix CrossRefTest threshold verification and live API title update (#17276) * Fix CrossRefTest threshold verification and live API title update * Refactor CrossRefTest fixture to use fluent withField --- .../logic/importer/fetcher/CrossRefTest.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/jablib/src/test/java/org/jabref/logic/importer/fetcher/CrossRefTest.java b/jablib/src/test/java/org/jabref/logic/importer/fetcher/CrossRefTest.java index 25b5c1a13854..a9c023cee41e 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fetcher/CrossRefTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fetcher/CrossRefTest.java @@ -80,7 +80,7 @@ void acceptTitleUnderThreshold() throws FetcherException { @Test void notAcceptTitleOverThreshold() throws FetcherException { BibEntry entry = new BibEntry(); - entry.setField(StandardField.TITLE, "Towards Application Portability in Platform as a Service-----"); + entry.setField(StandardField.TITLE, "Towards Application Portability in Platform as a Service xxxxx"); entry.setField(StandardField.AUTHOR, "Stefan Kolb and Guido Wirtz"); assertEquals(Optional.empty(), fetcher.findIdentifier(entry)); } @@ -121,17 +121,18 @@ void findByEntry() throws FetcherException { } @Test - void performSearchByIdFindsPaperWithoutTitle() throws FetcherException { - BibEntry entry = new BibEntry(StandardEntryType.Article); - entry.setField(StandardField.AUTHOR, "Leo Breiman"); - entry.setField(StandardField.DOI, "10.1023/a:1010933404324"); - entry.setField(StandardField.ISSN, "0885-6125"); - entry.setField(StandardField.PAGES, "5-32"); - entry.setField(StandardField.VOLUME, "45"); - entry.setField(StandardField.YEAR, "2001"); - entry.setField(StandardField.JOURNAL, "Machine Learning"); - entry.setField(StandardField.NUMBER, "1"); - entry.setField(StandardField.PUBLISHER, "Springer Science and Business Media LLC"); + void performSearchByIdFindsPaper() throws FetcherException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.TITLE, "Random Forests") + .withField(StandardField.AUTHOR, "Leo Breiman") + .withField(StandardField.DOI, "10.1023/a:1010933404324") + .withField(StandardField.ISSN, "0885-6125") + .withField(StandardField.PAGES, "5-32") + .withField(StandardField.VOLUME, "45") + .withField(StandardField.YEAR, "2001") + .withField(StandardField.JOURNAL, "Machine Learning") + .withField(StandardField.NUMBER, "1") + .withField(StandardField.PUBLISHER, "Springer Science and Business Media LLC"); assertEquals(Optional.of(entry), fetcher.performSearchById("10.1023/a:1010933404324")); } From eb78c4baf16ddd40031573e5622fd22b095f3c84 Mon Sep 17 00:00:00 2001 From: Ahmed Ragab Ali <166864012+A7MEDRAGAB82@users.noreply.github.com> Date: Sat, 26 Sep 2026 04:34:54 +0000 Subject: [PATCH 092/102] Fix: Do not treat @misc as strictly different entry type (#16578) (#17218) * Fix: Do not treat @misc as strictly different entry type (#16578) * Improve test variable naming and assert symmetry * Reformat DuplicateCheckTest according to code guidelines --------- Co-authored-by: github-actions[bot] --- CHANGELOG.md | 1 + .../jabref/logic/database/DuplicateCheck.java | 5 ++++- .../logic/database/DuplicateCheckTest.java | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25576e2a0348..40913aab02d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) - We fixed an issue where accepting a backup that adds groups to a library without groups failed with an error. [#17165](https://github.com/JabRef/jabref/pull/17165) - We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) +- We fixed an issue where entries of type Misc were penalized too strictly during duplicate detection. [#16578](https://github.com/JabRef/jabref/issues/16578) - We fixed an issue where "Community forum" in the Help menu opened the donation page instead of the forum. [#17162](https://github.com/JabRef/jabref/pull/17162) ### Removed diff --git a/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java b/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java index bc07c2fe8fe6..6ca907e5d748 100644 --- a/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java +++ b/jablib/src/main/java/org/jabref/logic/database/DuplicateCheck.java @@ -89,6 +89,9 @@ private static boolean haveSameIdentifier(final BibEntry one, final BibEntry two } private static boolean haveDifferentEntryType(final BibEntry one, final BibEntry two) { + if (one.getType().equals(StandardEntryType.Misc) || two.getType().equals(StandardEntryType.Misc)) { + return false; + } return !one.getType().equals(two.getType()); } @@ -256,7 +259,7 @@ public static double compareEntriesStrictly(BibEntry one, BibEntry two) { } totalCount++; - if (!haveDifferentEntryType(one, two)) { + if (one.getType().equals(two.getType())) { score++; } diff --git a/jablib/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java b/jablib/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java index 10f88225eb0b..4ae783d46d57 100644 --- a/jablib/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java +++ b/jablib/src/test/java/org/jabref/logic/database/DuplicateCheckTest.java @@ -647,4 +647,20 @@ void containsDuplicateReturnsEmptyForEmptyCollection() { assertEquals(Optional.empty(), duplicateChecker.containsDuplicate(List.of(), simpleArticle, BibDatabaseMode.BIBTEX)); } + + @Test + void duplicateDetectionWithMiscAndDifferentType() { + BibEntry miscEntry = new BibEntry(StandardEntryType.Misc) + .withField(StandardField.AUTHOR, "Albert Einstein") + .withField(StandardField.TITLE, "Relativity") + .withField(StandardField.YEAR, "1916"); + + BibEntry techReportEntry = new BibEntry(StandardEntryType.TechReport) + .withField(StandardField.AUTHOR, "Albert Einstein") + .withField(StandardField.TITLE, "Relativity") + .withField(StandardField.YEAR, "1916"); + + assertTrue(duplicateChecker.isDuplicate(miscEntry, techReportEntry, BibDatabaseMode.BIBTEX)); + assertTrue(duplicateChecker.isDuplicate(techReportEntry, miscEntry, BibDatabaseMode.BIBTEX)); + } } From e568b18e8a6e8aa9b6d943440ebe0e66b556a2c3 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sat, 26 Sep 2026 10:20:55 +0000 Subject: [PATCH 093/102] Update CHECKLIST.md (#17294) --- CHECKLIST.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHECKLIST.md b/CHECKLIST.md index dcfe4828cdc3..d9cae3874ed3 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -46,6 +46,7 @@ Read your own diff once, top to bottom, and confirm each point. ### Tests +- [ ] In case issue lists an example, this concrete example is covered by a test case. - [ ] Behavior changes in `org.jabref.model` / `org.jabref.logic` have added or updated tests. - [ ] Tests assert object contents (`assertEquals`), use plain JUnit asserts (not AssertJ), have no `@DisplayName`, do not catch exceptions (let them propagate so JUnit reports setup/teardown failures directly), and use `@TempDir` instead of manual temp directories. - [ ] Fetcher tests hit the live endpoints — the remote API is not mocked or stubbed (automated-review suggestions to mock it are rejected on purpose). From 32a6f24d97ab01d71ea73743c29153c96200d87f Mon Sep 17 00:00:00 2001 From: AbdoSoliman1 <183401518+AbdoSol1iman@users.noreply.github.com> Date: Sat, 26 Sep 2026 14:12:05 +0000 Subject: [PATCH 094/102] Add OCRmyPDF plugins as new engines support (EasyOCR,PaddleOCR,AppleOCR) (#16866) (#17219) * Add OCRmyPDF plugin selection support (EasyOCR) (#16866) * Add missing localization key for OCR plugin selection * fix OpenRewrite patch * add to changelog * take the mentor advice (add plugins as engines) * remove last commit trash * add other engines * update pr log in CHANGELOG.md * update CHANGELOG.md#2 --------- Co-authored-by: AbdoSol1iman Co-authored-by: github-actions[bot] --- CHANGELOG.md | 1 + .../gui/preferences/ocr/OcrTabViewModel.java | 4 ++-- .../org/jabref/logic/ocr/EngineSelection.java | 19 +++++++++++++++---- .../jabref/logic/ocr/OcrEngineFactory.java | 6 +++++- .../org/jabref/logic/ocr/OcrMyPdfEngine.java | 4 ++++ .../org/jabref/logic/ocr/OcrPreferences.java | 2 +- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40913aab02d8..f89acfb0f841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added `jabkit git merge-driver`, a Git merge driver that merges `.bib` files semantically. [#16838](https://github.com/JabRef/jabref/pull/16838) - We added tooltips with action descriptions to menu items. [#9615](https://github.com/JabRef/jabref/issues/9615) - We added "Contribute to JabRef" to the Help menu, linking to [contribute.jabref.org](https://contribute.jabref.org). [#17162](https://github.com/JabRef/jabref/pull/17162) +- We added EasyOCR, PaddleOCR, and AppleOCR as selectable OCR engines, alongside Tesseract and Docling. [#16866](https://github.com/JabRef/jabref/issues/16866) ### Changed diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java index 710bdaa0dc1a..f6a882d2dc52 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java @@ -47,7 +47,7 @@ public class OcrTabViewModel implements PreferenceTabViewModel { private static final List DEFAULT_DOCLING_PATHS = List.of( "docling" ); - private final ObjectProperty selectedEngine = new SimpleObjectProperty<>(EngineSelection.OCRMYPDF); + private final ObjectProperty selectedEngine = new SimpleObjectProperty<>(EngineSelection.TESSERACT); private final ListProperty engineOptions = new SimpleListProperty<>(FXCollections.observableArrayList(EngineSelection.values())); private final StringProperty ocrEnginePath = new SimpleStringProperty(); @@ -138,7 +138,7 @@ public void browseEnginePath() { } public Optional autoDetectDefaultEnginePath(EngineSelection engineToDetect) { - if (engineToDetect == EngineSelection.OCRMYPDF) { + if (engineToDetect != EngineSelection.DOCLING) { return DEFAULT_OCRMYPDF_PATHS.stream() .filter(this::enginePathExists) .findFirst(); diff --git a/jablib/src/main/java/org/jabref/logic/ocr/EngineSelection.java b/jablib/src/main/java/org/jabref/logic/ocr/EngineSelection.java index e127232a4390..3ab11c948ada 100644 --- a/jablib/src/main/java/org/jabref/logic/ocr/EngineSelection.java +++ b/jablib/src/main/java/org/jabref/logic/ocr/EngineSelection.java @@ -1,27 +1,38 @@ package org.jabref.logic.ocr; +import java.util.Optional; + import org.jspecify.annotations.NullMarked; @NullMarked public enum EngineSelection { - OCRMYPDF("OCRmyPDF"), - DOCLING("Docling"); + TESSERACT("Tesseract", Optional.of("tesseract")), + EASYOCR("EasyOCR", Optional.of("easyocr")), + PADDLEOCR("PaddleOCR", Optional.of("ocrmypdf_paddleocr")), + APPLEOCR("AppleOCR", Optional.of("appleocr")), + DOCLING("Docling", Optional.empty()); private final String displayName; + private final Optional identifierName; - EngineSelection(String displayName) { + EngineSelection(String displayName, Optional identifierName) { this.displayName = displayName; + this.identifierName = identifierName; } public String getDisplayName() { return displayName; } + public Optional getIdentifierName() { + return identifierName; + } + public static EngineSelection safeValueOf(String name) { try { return EngineSelection.valueOf(name); } catch (IllegalArgumentException _) { - return EngineSelection.OCRMYPDF; + return EngineSelection.TESSERACT; } } } diff --git a/jablib/src/main/java/org/jabref/logic/ocr/OcrEngineFactory.java b/jablib/src/main/java/org/jabref/logic/ocr/OcrEngineFactory.java index 71af4c855cdc..e91159fabfa0 100644 --- a/jablib/src/main/java/org/jabref/logic/ocr/OcrEngineFactory.java +++ b/jablib/src/main/java/org/jabref/logic/ocr/OcrEngineFactory.java @@ -8,11 +8,15 @@ @NullMarked public final class OcrEngineFactory { + @SuppressWarnings("checkstyle:NoWhitespaceBefore") public static OcrEngine create(OcrPreferences preferences) { return switch (preferences.getEngineSelection()) { case DOCLING -> new DoclingEngine(preferences); - case OCRMYPDF -> + case TESSERACT, + EASYOCR, + PADDLEOCR, + APPLEOCR -> new OcrMyPdfEngine(preferences); }; } diff --git a/jablib/src/main/java/org/jabref/logic/ocr/OcrMyPdfEngine.java b/jablib/src/main/java/org/jabref/logic/ocr/OcrMyPdfEngine.java index 13b133a62be9..59f885d07101 100644 --- a/jablib/src/main/java/org/jabref/logic/ocr/OcrMyPdfEngine.java +++ b/jablib/src/main/java/org/jabref/logic/ocr/OcrMyPdfEngine.java @@ -52,6 +52,10 @@ public OcrResult performOcrAndEmbedText(Path pdfPath) { List languages = ocrPreferences.getOcrLanguages().stream() .map(OcrLanguage::getCode) .toList(); + ocrPreferences.getEngineSelection().getIdentifierName().ifPresent(pluginId -> { + command.add("--ocr-engine"); + command.add(pluginId); + }); if (!languages.isEmpty()) { command.add("-l"); command.add(String.join("+", languages)); diff --git a/jablib/src/main/java/org/jabref/logic/ocr/OcrPreferences.java b/jablib/src/main/java/org/jabref/logic/ocr/OcrPreferences.java index 00f1a85274ce..6660043a6f33 100644 --- a/jablib/src/main/java/org/jabref/logic/ocr/OcrPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/ocr/OcrPreferences.java @@ -18,7 +18,7 @@ public class OcrPreferences { private final ListProperty ocrLanguages; private OcrPreferences() { - this("ocrmypdf", PagesWithTextHandling.SKIP, EngineSelection.OCRMYPDF, List.of(OcrLanguage.ENGLISH)); + this("ocrmypdf", PagesWithTextHandling.SKIP, EngineSelection.TESSERACT, List.of(OcrLanguage.ENGLISH)); } public OcrPreferences(String ocrEnginePath, PagesWithTextHandling pagesWithTextHandling, EngineSelection engineSelection, List ocrLanguages) { From c6de5d3037a31de65111fb173cbf940425d2b4d1 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 26 Sep 2026 15:35:32 +0000 Subject: [PATCH 095/102] New translations jabref_en.properties for (French) (#17297) --- jablib/src/main/resources/l10n/JabRef_fr.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/jablib/src/main/resources/l10n/JabRef_fr.properties b/jablib/src/main/resources/l10n/JabRef_fr.properties index ddbd2a558285..29f03c85eef6 100644 --- a/jablib/src/main/resources/l10n/JabRef_fr.properties +++ b/jablib/src/main/resources/l10n/JabRef_fr.properties @@ -1406,6 +1406,7 @@ Set\ read\ status\ to\ read=Mettre l'état de lecture à lu Set\ read\ status\ to\ skimmed=Met l'état de lecture à parcouru Donate\ to\ JabRef=Faire un don à JabRef +Contribute\ to\ JabRef=Contribuer à JabRef Donation=Faire un don Support\ JabRef=Soutenir JabRef Help\ us\ improve\ JabRef\ by\ donating.=Aidez-nous à améliorer JabRef en faisant un don. From b0c192005806094d363e058f6934ddbea46060bd Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 27 Sep 2026 13:37:29 +0000 Subject: [PATCH 096/102] Keep JabRef running when an LSP client sends exit (#17303) * Keep JabRef running when an LSP client sends exit Co-Authored-By: Claude Opus 5.5 * Link PR in changelog Co-Authored-By: Claude Opus 5.5 * Mark LspLauncherTest as NullMarked Co-Authored-By: Claude Opus 5.5 * Annotate createClientHandler with `@VisibleForTesting` Co-Authored-By: Claude Opus 5.5 * Drop exit() call from LspLauncherTest Co-Authored-By: Claude Opus 5.5 * Just pass standalone, drop test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + jabls/src/main/java/org/jabref/languageserver/LspLauncher.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89acfb0f841..c93230a39858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where pressing "+" in the "File" field with an automatically found file selected opened the "Add file link" dialog instead of linking the selected file. [#16938](https://github.com/JabRef/jabref/pull/16938) - We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048) - We fixed an issue where accepting a backup that adds groups to a library without groups failed with an error. [#17165](https://github.com/JabRef/jabref/pull/17165) +- We fixed an issue where JabRef closed when a client of the integrated language server disconnected. [#17303](https://github.com/JabRef/jabref/pull/17303) - We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) - We fixed an issue where entries of type Misc were penalized too strictly during duplicate detection. [#16578](https://github.com/JabRef/jabref/issues/16578) - We fixed an issue where "Community forum" in the Help menu opened the donation page instead of the forum. [#17162](https://github.com/JabRef/jabref/pull/17162) diff --git a/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java b/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java index df38aae8679a..283d888aacab 100644 --- a/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java +++ b/jabls/src/main/java/org/jabref/languageserver/LspLauncher.java @@ -83,7 +83,7 @@ public void run() { } private void handleClient(Socket socket) { - LspClientHandler clientHandler = new LspClientHandler(messageHandler, cliPreferences, abbreviationRepository, bibEntryTypesManager, true); + LspClientHandler clientHandler = new LspClientHandler(messageHandler, cliPreferences, abbreviationRepository, bibEntryTypesManager, standalone); LOGGER.debug("LSP clientHandler started."); try (socket; // socket should be closed on error InputStream in = socket.getInputStream(); From e99736e639f30534ab8f4d0a640c6e65f2c01e25 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 27 Sep 2026 17:53:08 +0000 Subject: [PATCH 097/102] Align Flatpak manifest with Flathub integration (#17298) * Align Flatpak manifest with Flathub integration * Document Flatpak host-application citation pushing * Use YAML for the Flatpak manifest --- .github/workflows/binaries.yml | 4 +-- CHANGELOG.md | 2 +- docs/requirements/distribution.md | 5 ++++ flatpak/emacsclient.sh | 3 ++ flatpak/org.jabref.jabref.json | 46 ----------------------------- flatpak/org.jabref.jabref.yaml | 48 +++++++++++++++++++++++++++++++ flatpak/soffice.sh | 3 ++ flatpak/texmaker.sh | 3 ++ flatpak/texstudio.sh | 3 ++ flatpak/vim.sh | 3 ++ 10 files changed, 71 insertions(+), 49 deletions(-) create mode 100644 flatpak/emacsclient.sh delete mode 100644 flatpak/org.jabref.jabref.json create mode 100644 flatpak/org.jabref.jabref.yaml create mode 100644 flatpak/soffice.sh create mode 100644 flatpak/texmaker.sh create mode 100644 flatpak/texstudio.sh create mode 100644 flatpak/vim.sh diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index aed3b7f82a38..1981f7ee135f 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -961,14 +961,14 @@ jobs: ARCH: ${{ matrix.arch }} ARCHIVE: ${{ matrix.archive }} run: | - jq --arg arch "$ARCH" --arg archive "$ARCHIVE" ' + flatpak-builder --show-manifest flatpak/org.jabref.jabref.yaml | jq --arg arch "$ARCH" --arg archive "$ARCHIVE" ' .modules[0].sources |= map( if .["only-arches"] == [$arch] then del(.url, .sha256) + {"path": $archive} else . end ) - ' flatpak/org.jabref.jabref.json > flatpak/org.jabref.jabref.ci.json + ' > flatpak/org.jabref.jabref.ci.json - name: Build and upload Flatpak uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index c93230a39858..d5c5ee253135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added -- We added ARM64 Flatpak builds and nightly, beta, and stable channels. [#13349](https://github.com/JabRef/jabref/issues/13349) +- We added ARM64 Flatpak builds, nightly, beta, and stable channels, and host editor integration. [#13349](https://github.com/JabRef/jabref/issues/13349) - We added `jabkit git merge-driver`, a Git merge driver that merges `.bib` files semantically. [#16838](https://github.com/JabRef/jabref/pull/16838) - We added tooltips with action descriptions to menu items. [#9615](https://github.com/JabRef/jabref/issues/9615) - We added "Contribute to JabRef" to the Help menu, linking to [contribute.jabref.org](https://contribute.jabref.org). [#17162](https://github.com/JabRef/jabref/pull/17162) diff --git a/docs/requirements/distribution.md b/docs/requirements/distribution.md index 481968b0c6b8..c4e4501af0e9 100644 --- a/docs/requirements/distribution.md +++ b/docs/requirements/distribution.md @@ -8,4 +8,9 @@ parent: Requirements The deb, rpm, and msi packages register JabRef as an application for `text/x-bibtex`, so that opening a `.bib` file from the file manager starts JabRef. +## Push citations to external applications from the Flatpak +`req~distribution.flatpak-host-editor~1` + +Users should be able to push citations from the sandboxed Flatpak to supported external applications installed on the host. The Flatpak manifest must grant session-bus access to `org.freedesktop.Flatpak` to launch those applications on the host. + diff --git a/flatpak/emacsclient.sh b/flatpak/emacsclient.sh new file mode 100644 index 000000000000..bed52387c5fe --- /dev/null +++ b/flatpak/emacsclient.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec flatpak-spawn --host emacsclient "$@" diff --git a/flatpak/org.jabref.jabref.json b/flatpak/org.jabref.jabref.json deleted file mode 100644 index 6ca7084c67bb..000000000000 --- a/flatpak/org.jabref.jabref.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "app-id" : "org.jabref.jabref", - "runtime" : "org.freedesktop.Platform", - "runtime-version" : "26.08", - "sdk" : "org.freedesktop.Sdk", - "command" : "JabRef", - "modules" : [ - { - "name" : "JabRef", - "buildsystem" : "simple", - "build-commands" : [ - "tar -xzf JabRef-portable_linux.tar.gz --no-same-owner --directory=/app/ --strip-components=1", - "install -D -m0644 /app/lib/JabRef.png /app/share/icons/hicolor/64x64/apps/org.jabref.jabref.png", - "install -D -m0644 org.jabref.jabref.desktop /app/share/applications/org.jabref.jabref.desktop" - ], - "sources" : [ - { - "type" : "file", - "url": "https://builds.jabref.org/main/linux-amd64/JabRef-portable_linux.tar.gz", - "sha256": "53e136e94beb0cb6174788352faf7983750945f5d397e7f6013b8921830b3b59", - "only-arches": [ - "x86_64" - ] - }, - { - "type" : "file", - "url": "https://builds.jabref.org/main/linux-arm/JabRef-portable_linux_arm64.tar.gz", - "sha256": "afa12af2d0e63d3e9016e3a44ea712ce98473222bffd0a94f4d302143a700e58", - "dest-filename": "JabRef-portable_linux.tar.gz", - "only-arches": [ - "aarch64" - ] - }, - { - "type": "file", - "path": "org.jabref.jabref.desktop" - } - ] - } - ], - "finish-args" : [ - "--socket=x11", - "--share=network", - "--filesystem=home" - ] -} diff --git a/flatpak/org.jabref.jabref.yaml b/flatpak/org.jabref.jabref.yaml new file mode 100644 index 000000000000..491f8e840fff --- /dev/null +++ b/flatpak/org.jabref.jabref.yaml @@ -0,0 +1,48 @@ +app-id: org.jabref.jabref +runtime: org.freedesktop.Platform +runtime-version: '26.08' +sdk: org.freedesktop.Sdk +command: JabRef +finish-args: + - --share=ipc + - --socket=x11 + - --share=network + - --filesystem=home + - --device=dri + - --talk-name=org.freedesktop.Flatpak +modules: + - name: JabRef + buildsystem: simple + build-commands: + - tar -xzf JabRef-portable_linux.tar.gz --no-same-owner --directory=/app/ --strip-components=1 + - install -D -m0644 /app/lib/JabRef.png /app/share/icons/hicolor/64x64/apps/org.jabref.jabref.png + - install -D -m0644 org.jabref.jabref.desktop /app/share/applications/org.jabref.jabref.desktop + - install -D -m0755 texstudio.sh /app/bin/texstudio + - install -D -m0755 texmaker.sh /app/bin/texmaker + - install -D -m0755 vim.sh /app/bin/vim + - install -D -m0755 emacsclient.sh /app/bin/emacsclient + - install -D -m0755 soffice.sh /app/bin/soffice + sources: + - type: file + only-arches: + - x86_64 + url: https://github.com/JabRef/jabref/releases/download/v6.0-beta.1/JabRef-portable_linux.tar.gz + sha256: ed1f771e7b3284b6e61fada14fd89b4822bb9bff2f5b2dc3f5b3034ab471f798 + - type: file + only-arches: + - aarch64 + url: https://github.com/JabRef/jabref/releases/download/v6.0-beta.1/JabRef-portable_linux_arm64.tar.gz + sha256: 551bf1b5627e17f8311b96bd3a6ef234ace601812361e5db3375067d3b70b9e6 + dest-filename: JabRef-portable_linux.tar.gz + - type: file + path: org.jabref.jabref.desktop + - type: file + path: texstudio.sh + - type: file + path: texmaker.sh + - type: file + path: vim.sh + - type: file + path: emacsclient.sh + - type: file + path: soffice.sh diff --git a/flatpak/soffice.sh b/flatpak/soffice.sh new file mode 100644 index 000000000000..f7affbe38b22 --- /dev/null +++ b/flatpak/soffice.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec flatpak-spawn --host soffice "$@" diff --git a/flatpak/texmaker.sh b/flatpak/texmaker.sh new file mode 100644 index 000000000000..70ce2bb2be87 --- /dev/null +++ b/flatpak/texmaker.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec flatpak-spawn --host texmaker "$@" diff --git a/flatpak/texstudio.sh b/flatpak/texstudio.sh new file mode 100644 index 000000000000..7df4df78cad1 --- /dev/null +++ b/flatpak/texstudio.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec flatpak-spawn --host texstudio "$@" diff --git a/flatpak/vim.sh b/flatpak/vim.sh new file mode 100644 index 000000000000..f3a9ba54d42b --- /dev/null +++ b/flatpak/vim.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec flatpak-spawn --host vim "$@" From 458e97c54335bae580712de076986cb20331e9f5 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 27 Sep 2026 19:01:58 +0000 Subject: [PATCH 098/102] Author port PRs as jabref-machine (#17305) * Author port PRs as jabref-machine Port PRs and their "created backport PR" comment appeared under a maintainer's name. github-actions[bot] approves them now, since jabref-machine cannot approve its own PRs. Co-Authored-By: Claude Opus 5.5 * Re-approve port PRs after the CHANGELOG conflict merge Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .github/workflows/automerge.yml | 3 ++- .github/workflows/on-pr-labeled.yml | 2 ++ .github/workflows/on-pr-opened-updated.yml | 4 ++++ .github/workflows/port-to-other-branch.yml | 9 ++++++--- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 86aa4156358c..75023d09bbf3 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -71,7 +71,8 @@ jobs: env: GH_TOKEN: ${{secrets.GH_TOKEN_JABREF_MACHINE_PR_APPROVE}} - name: Approve PR - if: steps.shouldrun.outputs.shouldrun == 'true' + # jabref-machine cannot approve its own PRs (ports); port-to-other-branch.yml approves those + if: steps.shouldrun.outputs.shouldrun == 'true' && github.event.pull_request.user.login != 'jabref-machine' # Guard: a push directly followed by adding the "automerge" label makes both this # workflow (live label query) and on-pr-labeled.yml approve — skip if an # un-dismissed approval by jabref-machine already exists. diff --git a/.github/workflows/on-pr-labeled.yml b/.github/workflows/on-pr-labeled.yml index 90fcacb9c40f..aefd0a0222fb 100644 --- a/.github/workflows/on-pr-labeled.yml +++ b/.github/workflows/on-pr-labeled.yml @@ -20,6 +20,8 @@ jobs: cancel-in-progress: false steps: - name: Approve PR + # jabref-machine cannot approve its own PRs (ports); port-to-other-branch.yml approves those + if: github.event.pull_request.user.login != 'jabref-machine' # Guard: automerge.yml (running for a push made just before the label was added) # queries labels live and may approve concurrently — skip if an un-dismissed # approval by jabref-machine already exists. diff --git a/.github/workflows/on-pr-opened-updated.yml b/.github/workflows/on-pr-opened-updated.yml index 7634c71a33a5..b637955f8e9d 100644 --- a/.github/workflows/on-pr-opened-updated.yml +++ b/.github/workflows/on-pr-opened-updated.yml @@ -189,6 +189,10 @@ jobs: # Remove first: the label may still be attached from the original auto-merge request, # and re-adding an attached label produces no "labeled" event. gh pr edit "$PR_NUMBER" --remove-label automerge + # "On PR labeled" skips jabref-machine's own PRs (ports), github-actions[bot] approves those + if [ "$(gh pr view "$PR_NUMBER" --json author --jq .author.login)" = "jabref-machine" ]; then + gh pr review --approve "$PR_NUMBER" + fi GH_TOKEN="$PAT" gh pr edit "$PR_NUMBER" --add-label automerge echo "✅ Approved PR with auto-merge - added label \"automerge\"" >> $GITHUB_STEP_SUMMARY fi diff --git a/.github/workflows/port-to-other-branch.yml b/.github/workflows/port-to-other-branch.yml index e23b01d287e4..2dae07a461e7 100644 --- a/.github/workflows/port-to-other-branch.yml +++ b/.github/workflows/port-to-other-branch.yml @@ -165,8 +165,8 @@ jobs: id: port uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6.0 with: - # A PAT, so that CI runs on the port PR and jabref-machine (another account) can approve it - github_token: ${{ secrets.GH_TOKEN_UPDATE_GRADLE_WRAPPER }} + # A PAT, so that CI runs on the port PR; the PR and its comment are authored by jabref-machine + github_token: ${{ secrets.GH_TOKEN_JABREF_MACHINE_PR_APPROVE }} target_branches: ${{ env.TARGET }} # The target comes from the base branch and the label above, not from "backport " labels label_pattern: ^()$ @@ -183,11 +183,14 @@ jobs: - name: Auto-merge a clean port if: steps.port.outputs.was_successful == 'true' env: - # jabref-machine's token: "On PR labeled" only fires for labels added with a PAT, and it approves as jabref-machine + # jabref-machine's token: "On PR labeled" only fires for labels added with a PAT GH_TOKEN: ${{ secrets.GH_TOKEN_JABREF_MACHINE_PR_APPROVE }} + APPROVE_TOKEN: ${{ secrets.GITHUB_TOKEN }} CREATED: ${{ steps.port.outputs.created_pull_numbers }} run: | for pr in $CREATED; do + # jabref-machine cannot approve its own PR; github-actions[bot] does it instead + GH_TOKEN="$APPROVE_TOKEN" gh pr review --approve "$pr" gh pr edit "$pr" --add-label automerge echo "✅ Port PR #$pr opened with auto-merge" >> "$GITHUB_STEP_SUMMARY" done From 0a4eea026849839cfa651d7b339282977e4de801 Mon Sep 17 00:00:00 2001 From: Subhramit Basu Date: Sun, 27 Sep 2026 19:28:27 +0000 Subject: [PATCH 099/102] Add INVEST reference to requirements (#17301) * Add INVEST reference to requirements * Apply suggestion from @InAnYan * fix strange application --------- Co-authored-by: InAnYan Co-authored-by: Ruslan --- docs/code-howtos/requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/code-howtos/requirements.md b/docs/code-howtos/requirements.md index 51cd70ac647a..ab47c1eaec63 100644 --- a/docs/code-howtos/requirements.md +++ b/docs/code-howtos/requirements.md @@ -209,7 +209,7 @@ This way they will be filtered out when running `traceRequirements`, while still We use OFT mainly to trace small ideas, notes, and wishes rather than to manage the full software engineering requirements process. The concepts and terminology of software requirements engineering are still useful for structuring and writing requirements, and they help us express them clearly. However, we do not apply all of its principles in full. -For example, INVEST is useful for writing better requirement descriptions, but we do not treat our requirements as fully negotiable items because they are more like concrete instructions or orders than open-ended, negotiable agreements. We also do not explicitly record a separate value dimension for each requirement in this workflow. +For example, [INVEST](https://en.wikipedia.org/wiki/INVEST_(mnemonic)) is useful for writing better requirement descriptions, but we do not treat our requirements as fully negotiable items because they are more like concrete instructions or orders than open-ended, negotiable agreements. We also do not explicitly record a separate value dimension for each requirement in this workflow. It is better to write more small requirements than one large one. This makes coverage and tracking more detailed. From 8248121d989acb04cb82d00fa7cb04eb9ff8d344 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 27 Sep 2026 19:48:57 +0000 Subject: [PATCH 100/102] Add typos spell check to CI and fix found typos (#17306) * Fix typos in code, documentation, and CHANGELOG Found with https://github.com/crate-ci/typos. Includes renaming ReplaceTabsBySpaceFormater to ReplaceTabsBySpaceFormatter and InterruptablePDFTextStripper to InterruptiblePDFTextStripper. The formatter key "remove_tabs" and the preference key "nameFormatterNames" are unchanged. Co-Authored-By: Claude Opus 5.5 * Fix author initials option in Simple HTML export layout The misspelled option "InititialsNoSpace" was ignored by the Authors formatter, so initials were exported with spaces ("S. A." instead of "S.A."). Co-Authored-By: Claude Opus 5.5 * Add typos spell check to CI Checks the files changed by a pull request. Data files, translations, submodules, and third-party files are excluded in _typos.toml. Co-Authored-By: Claude Opus 5.5 * Check spelling of the whole tree The tree is typo-free now, so the job no longer needs to restrict itself to the changed files. Co-Authored-By: Claude Opus 5.5 * Link PR in CHANGELOG Co-Authored-By: Claude Opus 5.5 * Fix typo in CHANGELOG Co-Authored-By: Claude Opus 5.5 * Link ritopt project page and skip SourceForge in link check SourceForge answers automated requests with 403. Co-Authored-By: Claude Opus 5.5 * Merge with textlint * Fix typo * Simplify Simple HTML export test assertion Co-Authored-By: Claude Opus 5.5 * Update jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java Co-authored-by: InAnYan * Update jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java Co-authored-by: InAnYan * Update jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java Co-authored-by: InAnYan * Update jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java Co-authored-by: InAnYan * Drop test prefix from test method names Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Co-authored-by: InAnYan --- .github/workflows/tests-code.yml | 6 +- .linkspector.yml | 2 + CHANGELOG.md | 33 ++-- _typos.toml | 142 ++++++++++++++++++ config/checkstyle/checkstyle.xml | 2 +- config/checkstyle/checkstyle_reviewdog.xml | 2 +- crowdin.yml | 2 +- docs/Gemfile | 2 +- docs/code-howtos/ai.md | 10 +- docs/code-howtos/http-server.md | 2 +- .../openoffice/code-reorganization.md | 2 +- .../openoffice/ooresult-ooerror/index.md | 4 +- .../ooresult-ooerror/ooresult-alternatives.md | 4 +- docs/code-howtos/testing.md | 4 +- docs/code-howtos/ui-recommendations.md | 2 +- .../0016-mutable-preferences-objects.md | 2 +- docs/decisions/0023-localized-preferences.md | 6 +- docs/decisions/0025-reviewdog-reviews.md | 4 +- docs/decisions/0027-synchronization.md | 2 +- .../0029-cff-export-multiple-entries.md | 6 +- .../0032-store-chats-in-local-user-folder.md | 2 +- .../0037-rag-architecture-implementation.md | 2 +- docs/decisions/0044-use-picocli-for-jabkit.md | 2 +- ...use-references-heading-in-citations-tab.md | 2 +- docs/decisions/0050-custom-code-style.md | 2 +- docs/index.md | 2 +- jabgui/build.gradle.kts | 4 +- .../main/java/org/jabref/gui/JabRefGUI.java | 2 +- .../java/org/jabref/gui/desktop/os/Linux.java | 2 +- .../duplicationFinder/DuplicateSearch.java | 2 +- .../gui/frame/JabRefFrameViewModel.java | 6 +- .../jabref/gui/groups/GroupDialogView.java | 2 +- .../gui/groups/GroupDialogViewModel.java | 2 +- .../org/jabref/gui/groups/GroupTreeView.java | 2 +- .../importer/fetcher/WebSearchPaneView.java | 2 +- .../gui/maintable/MainTableColumnModel.java | 10 +- .../columns/LinkedIdentifierColumn.java | 2 +- .../org/jabref/gui/openoffice/OOBibBase.java | 4 +- .../gui/openoffice/OpenOfficePanel.java | 2 +- .../jabref/gui/util/CustomLocalDragboard.java | 4 +- .../gui/util/FileDialogConfiguration.java | 2 +- .../org/jabref/gui/util/ZipFileChooser.java | 2 +- .../gui/walkthrough/WalkthroughAction.java | 2 +- .../DefaultAutoCompleterTest.java | 2 +- .../FieldValueSuggestionProviderTest.java | 4 +- .../PersonNameStringConverterTest.java | 2 +- .../KeywordsEditorViewModelTest.java | 12 +- .../gui/groups/GroupDialogViewModelTest.java | 6 +- .../gui/groups/GroupTreeViewModelTest.java | 2 +- .../fetcher/WebSearchPaneViewModelTest.java | 2 +- .../NumericFieldComparatorTest.java | 2 +- .../GuiPreferencesMigrationsTest.java | 2 +- .../SpecialFieldsToSeparateFieldsTest.java | 2 +- .../styletester/StyleTesterMain.java | 2 +- .../jabref/toolkit/commands/PdfUpdate.java | 18 +-- jablib/build.gradle.kts | 4 +- .../ai/chatting/util/ChatHistoryUtils.java | 2 +- .../logic/parsing/PdfContentParser.java | 4 +- .../AuxParserStatisticsProvider.java | 2 +- .../repository/BibEntrySerializer.java | 2 +- .../citationkeypattern/BracketedPattern.java | 4 +- .../org/jabref/logic/crawler/Crawler.java | 2 +- .../EscapeAmpersandsFormatter.java | 2 +- .../NormalizeWhitespaceFormatter.java | 2 +- ....java => ReplaceTabsBySpaceFormatter.java} | 2 +- .../logic/importer/fetcher/ArXivFetcher.java | 2 +- ...fComputerScienceBibliographiesFetcher.java | 4 +- .../logic/importer/fetcher/GoogleScholar.java | 4 +- .../importer/fileformat/ACMPortalParser.java | 4 +- .../fileformat/EndnoteXmlImporter.java | 4 +- .../importer/fileformat/IsiImporter.java | 2 +- .../fileformat/pdf/PdfContentImporter.java | 2 +- .../logic/importer/util/MediaTypes.java | 2 +- .../integrity/LatexIntegrityChecker.java | 2 +- .../jabref/logic/integrity/TitleChecker.java | 4 +- .../JournalAbbreviationRepository.java | 6 +- .../org/jabref/logic/l10n/Localization.java | 2 +- .../org/jabref/logic/layout/LayoutHelper.java | 2 +- .../AuthorFirstAbbrLastOxfordCommas.java | 2 +- .../format/AuthorFirstLastOxfordCommas.java | 2 +- .../AuthorLastFirstAbbrOxfordCommas.java | 2 +- .../format/AuthorLastFirstAbbreviator.java | 2 +- .../layout/format/GetOpenOfficeType.java | 2 +- .../jabref/logic/layout/format/Replace.java | 4 +- .../jabref/logic/layout/format/XMLChars.java | 2 +- .../jabref/logic/msbib/MSBibConverter.java | 4 +- .../org/jabref/logic/msbib/MSBibDatabase.java | 2 +- .../org/jabref/logic/msbib/MSBibEntry.java | 12 +- .../org/jabref/logic/net/URLDownload.java | 2 +- .../backend/NamedRangeReferenceMark.java | 2 +- .../oocsltext/CitationOOAdapterUtils.java | 10 +- .../logic/openoffice/style/OOProcess.java | 2 +- ...java => InterruptiblePDFTextStripper.java} | 4 +- .../preferences/JabRefCliPreferences.java | 8 +- .../logic/shared/security/Password.java | 2 +- .../logic/texparser/DefaultLatexParser.java | 2 +- .../logic/util/HeadlessExecutorService.java | 4 +- .../java/org/jabref/logic/util/Version.java | 4 +- .../logic/util/io/FileNameUniqueness.java | 2 +- .../strings/HTMLUnicodeConversionMaps.java | 2 +- .../org/jabref/model/entry/AuthorList.java | 14 +- .../model/entry/field/StandardField.java | 2 +- .../model/openoffice/backend/NamedRange.java | 2 +- .../model/openoffice/ootext/OOTextIntoOO.java | 2 +- .../openoffice/style/CitationGroups.java | 2 +- .../main/resources/l10n/JabRef_en.properties | 2 +- .../resources/resource/layout/docbook5.layout | 2 +- .../resource/layout/openoffice/README | 2 +- .../resource/layout/simplehtml.layout | 2 +- .../CitationStyleGeneratorTest.java | 6 +- .../logic/cleanup/MoveFilesCleanupTest.java | 2 +- .../exporter/ModsExportFormatFilesTest.java | 2 +- .../exporter/SimpleHtmlExportFormatTest.java | 54 +++++++ .../logic/formatter/FormattersTest.java | 2 +- .../NormalizeEnDashesFormatterTest.java | 2 +- ...a => ReplaceTabsBySpaceFormatterTest.java} | 6 +- .../importer/fetcher/MedlineFetcherTest.java | 2 +- .../importer/fetcher/MrDLibFetcherTest.java | 8 +- .../fetcher/SpringerNatureWebFetcherTest.java | 2 +- .../isbntobibtex/GVKIsbnFetcherTest.java | 2 +- .../transformers/SuffixTransformerTest.java | 2 +- .../importer/fileformat/BibtexParserTest.java | 4 +- .../fileformat/MedlineImporterFilesTest.java | 2 +- .../integrity/AbbreviationCheckerTest.java | 2 +- .../logic/journals/AbbreviationTest.java | 2 +- .../format/LatexToUnicodeFormatterTest.java | 10 +- .../openoffice/style/JStyleLoaderTest.java | 8 +- .../logic/openoffice/style/JStyleTest.java | 2 +- .../ProtectedTermsLoaderTest.java | 2 +- .../BibliographyConsistencyCheckTest.java | 2 +- .../org/jabref/logic/shared/DBMSTypeTest.java | 2 +- .../logic/util/io/FileNameUniquenessTest.java | 8 +- .../util/strings/StringManipulatorTest.java | 2 +- .../jabref/logic/xmp/XmpUtilReaderTest.java | 2 +- .../jabref/model/entry/AuthorListTest.java | 22 +-- .../org/jabref/model/entry/BibEntryTest.java | 10 +- .../jabref/model/entry/BibtexStringTest.java | 2 +- .../model/entry/identifier/DOITest.java | 2 +- .../model/groups/GroupTreeNodeTest.java | 8 +- .../model/openoffice/CitationEntryTest.java | 2 +- .../languageserver/util/LspRangeUtil.java | 2 +- rewrite.yml | 4 +- scripts/vms/debian-12/README.md | 2 +- scripts/vms/linux-mint-cinnamon/README.md | 2 +- scripts/vms/windows10/README.md | 2 +- scripts/vms/windows10/Vagrantfile | 2 +- scripts/vms/windows11/README.md | 2 +- scripts/vms/windows11/Vagrantfile | 2 +- .../support/CommonArchitectureTest.java | 2 +- 149 files changed, 469 insertions(+), 268 deletions(-) create mode 100644 _typos.toml rename jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/{ReplaceTabsBySpaceFormater.java => ReplaceTabsBySpaceFormatter.java} (93%) rename jablib/src/main/java/org/jabref/logic/pdf/{InterruptablePDFTextStripper.java => InterruptiblePDFTextStripper.java} (77%) create mode 100644 jablib/src/test/java/org/jabref/logic/exporter/SimpleHtmlExportFormatTest.java rename jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/{ReplaceTabsBySpaceFormaterTest.java => ReplaceTabsBySpaceFormatterTest.java} (79%) diff --git a/.github/workflows/tests-code.yml b/.github/workflows/tests-code.yml index b06bd7ccc97d..34dd72e3ff85 100644 --- a/.github/workflows/tests-code.yml +++ b/.github/workflows/tests-code.yml @@ -252,8 +252,8 @@ jobs: - name: yamllint run: pipx run yamllint==1.38.0 --strict . - textlint: - name: Textlint + spelling: + name: Spellling runs-on: ubuntu-slim steps: - name: Checkout source @@ -270,6 +270,8 @@ jobs: run: npm ci - name: textlint run: npm run textlint + - name: typos + run: pipx run typos==1.50.3 madr: name: MADR ADRs diff --git a/.linkspector.yml b/.linkspector.yml index 211b03355e26..c7e14eafe7e3 100644 --- a/.linkspector.yml +++ b/.linkspector.yml @@ -18,3 +18,5 @@ ignorePatterns: - pattern: "^https://www.pdflib.com/pdf-knowledge-base/xmp/free-xmp-validator" # Automated access blocked by Cloudflare, see issue for context: https://github.com/JabRef/jabref/issues/13404 - pattern: "onlinelibrary\\.wiley\\.com" + # Automated access blocked by SourceForge + - pattern: "sourceforge\\.net" diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c5ee253135..d96559472680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where JabRef closed when a client of the integrated language server disconnected. [#17303](https://github.com/JabRef/jabref/pull/17303) - We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) - We fixed an issue where entries of type Misc were penalized too strictly during duplicate detection. [#16578](https://github.com/JabRef/jabref/issues/16578) +- We fixed an issue where the "Simple HTML" export separated author initials with spaces. [#17306](https://github.com/JabRef/jabref/pull/17306) - We fixed an issue where "Community forum" in the Help menu opened the donation page instead of the forum. [#17162](https://github.com/JabRef/jabref/pull/17162) ### Removed @@ -40,7 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added - We added a native query field for catalogs on the SLR dialog. [#17199](https://github.com/JabRef/jabref/pull/17199) -- We made almost everything in JabRef navigatable by keyboard, including adding visiual indicators. [#17059](https://github.com/JabRef/jabref/pull/17059) +- We made almost everything in JabRef navigable by keyboard, including adding visual indicators. [#17059](https://github.com/JabRef/jabref/pull/17059) - We added a fetcher for Software Heritage identifiers (SWHID). [#16809](https://github.com/JabRef/jabref/issues/16809) - We added the community themes from [themes.jabref.org](https://themes.jabref.org/) (Everforest, Nord, Papers, Chocolate Honey, and Dino Girl's collection) as selectable themes. [#15915](https://github.com/JabRef/jabref/issues/15915) - We added a preview image to the theme selection in the preferences. [#15915](https://github.com/JabRef/jabref/issues/15915) @@ -163,7 +164,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We made the `Move file to directory` operation for Linked Files show every configured JabRef directory as possible options. [#12287](https://github.com/JabRef/jabref/issues/12287) - We reworked the entry editor tab preferences into a two-column "Tabs"/"Fields" editor supporting reordering, custom tabs, and regular expressions as field names. [#15998](https://github.com/JabRef/jabref/pull/15998). [#16594](https://github.com/JabRef/jabref/issues/16594) - We extended library pseudonymization to also pseudonymize group names, not just the entries. [#14117](https://github.com/JabRef/jabref/issues/14117) -- We introduced a leightweight search engine without fulltext search in linked files as default variant. [#15599](https://github.com/JabRef/jabref/pull/15599) +- We introduced a lightweight search engine without fulltext search in linked files as default variant. [#15599](https://github.com/JabRef/jabref/pull/15599) - We moved arXiv handling out of the DOI cleanup into the dedicated "arXiv DOI" cleanup. [#16033](https://github.com/JabRef/jabref/pull/16033) - Resolving of Math Subject Classification (MSC) needs to be activated in the references now. [#15883](https://github.com/JabRef/jabref/pull/15883) - We improved JabRef's startup performance by loading MSC codes only when activated. [#15883](https://github.com/JabRef/jabref/pull/15883) @@ -306,7 +307,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where non latin caseless author names being parsed as nameprefix instead of familyname. [#15813](https://github.com/JabRef/jabref/issues/15813) - We fixed an issue where `Quality-> Cleanup -> Rename PDF` together with `Moved linked files to file directory` would lead to an exception. [#15833](https://github.com/JabRef/jabref/issues/15833) - We fixed an issue where renaming a linked file with a very long title showed a misleading "file is being used by another process" error instead of renaming successfully. [#14771](https://github.com/JabRef/jabref/issues/14771) -- We fixed an issue where JabRef would trigger `The libray has been changed` while still saving. [#4877](https://github.com/JabRef/jabref/issues/4877) +- We fixed an issue where JabRef would trigger `The library has been changed` while still saving. [#4877](https://github.com/JabRef/jabref/issues/4877) - We fixed preview tooltip height calculation in the main table. [#16219](https://github.com/JabRef/jabref/issues/16219) - We fixed an issue where stale main table search results could remain visible after consecutive searches. [#15710](https://github.com/JabRef/jabref/issues/15710) - We fixed an issue where the button shape changed when hovering over it. [#16188](https://github.com/JabRef/jabref/issues/16188) @@ -1139,10 +1140,10 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - Clicking on the crossref and related tags in the entry editor jumps to the linked entry. [#5484](https://github.com/JabRef/jabref/issues/5484). [#9369](https://github.com/JabRef/jabref/issues/9369) - We fixed an issue where JabRef could not parse absolute file paths from Zotero exports. [#10959](https://github.com/JabRef/jabref/issues/10959) - We fixed an issue where an exception occurred when toggling between "Live" or "Locked" in the internal Document Viewer. [#10935](https://github.com/JabRef/jabref/issues/10935) -- When fetching article information fom IEEE Xplore, the em dash is now converted correctly. [JabRef/jabref-koppor#286](https://github.com/JabRef/jabref-koppor/issues/286) +- When fetching article information from IEEE Xplore, the em dash is now converted correctly. [JabRef/jabref-koppor#286](https://github.com/JabRef/jabref-koppor/issues/286) - Fixed an issue on Windows where the browser extension reported failure to send an entry to JabRef even though it was sent properly. [JabRef/JabRef-Browser-Extension#493](https://github.com/JabRef/JabRef-Browser-Extension/issues/493) - Fixed an issue on Windows where TeXworks path was not resolved if it was installed with MiKTeX. [#10977](https://github.com/JabRef/jabref/issues/10977) -- We fixed an issue with where JabRef would throw an error when using MathSciNet search, as it was unable to parse the fetched JSON coreectly. [#10996](https://github.com/JabRef/jabref/issues/10996) +- We fixed an issue with where JabRef would throw an error when using MathSciNet search, as it was unable to parse the fetched JSON correctly. [#10996](https://github.com/JabRef/jabref/issues/10996) - We fixed an issue where the "Import by ID" function would throw an error when a DOI that contains URL-encoded characters was entered. [#10648](https://github.com/JabRef/jabref/issues/10648) - We fixed an issue with handling of an "overflow" of authors at `[authIniN]`. [#11087](https://github.com/JabRef/jabref/issues/11087) - We fixed an issue where an exception occurred when selecting entries in the web search results. [#11081](https://github.com/JabRef/jabref/issues/11081) @@ -1329,7 +1330,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the "Import" -> "Library to import to" did not show the correct library name if two opened libraries had the same suffix. [#9567](https://github.com/JabRef/jabref/issues/9567) - We fixed an issue where the rpm-Version of JabRef could not be properly uninstalled and reinstalled. [#9558](https://github.com/JabRef/jabref/issues/9558). [#9603](https://github.com/JabRef/jabref/issues/9603) - We fixed an issue where the command line export using `--exportMatches` flag does not create an output bib file. [#9581](https://github.com/JabRef/jabref/issues/9581) -- We fixed an issue where custom field in the custom entry types could not be set to mulitline. [#9609](https://github.com/JabRef/jabref/issues/9609) +- We fixed an issue where custom field in the custom entry types could not be set to multiline. [#9609](https://github.com/JabRef/jabref/issues/9609) - We fixed an issue where the Office XML exporter did not resolve BibTeX-Strings when exporting entries. [forum#3741](https://discourse.jabref.org/t/exporting-bibtex-constant-strings-to-ms-office-2007-xml/3741) - We fixed an issue where the Merge Entries Toolbar configuration was not saved after hitting 'Merge Entries' button. [#9091](https://github.com/JabRef/jabref/issues/9091) - We fixed an issue where the password is stored in clear text if the user wants to use a proxy with authentication. [#8055](https://github.com/JabRef/jabref/issues/8055) @@ -1388,7 +1389,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a dropdown menu to let users change the library they want to import into during import. [#6177](https://github.com/JabRef/jabref/issues/6177) - We added the possibility to add/remove a preview style from the selected list using a double click. [#9490](https://github.com/JabRef/jabref/issues/9490) -- We added the option to define fields as "multine" directly in the custom entry types dialog. [#6448](https://github.com/JabRef/jabref/issues/6448) +- We added the option to define fields as "multiline" directly in the custom entry types dialog. [#6448](https://github.com/JabRef/jabref/issues/6448) - We changed the minWidth and the minHeight of the main window, so it won't have a width and/or a height with the value 0. [#9606](https://github.com/JabRef/jabref/issues/9606) ### Changed @@ -1547,7 +1548,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the user could not rate an entry in the main table when an entry was not yet ranked. [#5842](https://github.com/JabRef/jabref/issues/5842) - We fixed an issue that caused JabRef to sometimes open multiple instances when "Remote Operation" is enabled. [#8653](https://github.com/JabRef/jabref/issues/8653) - We fixed an issue where linked files with the filetype "application/pdf" in an entry were not shown with the correct PDF-Icon in the main table. [#8930](https://github.com/JabRef/jabref/issues/8930) -- We fixed an issue where "open folder" for linked files did not open the folder and did not select the file unter certain Linux desktop environments [#8679](https://github.com/JabRef/jabref/issues/8679). [#8849](https://github.com/JabRef/jabref/issues/8849) +- We fixed an issue where "open folder" for linked files did not open the folder and did not select the file under certain Linux desktop environments [#8679](https://github.com/JabRef/jabref/issues/8679). [#8849](https://github.com/JabRef/jabref/issues/8849) - We fixed an issue where the content of a big shared database library is not shown. [#8788](https://github.com/JabRef/jabref/issues/8788) - We fixed the unnecessary horizontal scroll bar in group panel. [#8467](https://github.com/JabRef/jabref/issues/8467) - We fixed an issue where the notification bar message, icon and actions appeared to be invisible. [#8761](https://github.com/JabRef/jabref/issues/8761) @@ -1774,10 +1775,10 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We changed the EndNote importer to import the field `label` to the corresponding bibtex field `endnote-label` [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) - The keywords added via "Manage content selectors" are now displayed in alphabetical order. [#3791](https://github.com/JabRef/jabref/issues/3791) - We improved the "Find unlinked files" dialog to show import results for each file. [#7209](https://github.com/JabRef/jabref/pull/7209) -- The content of the field `timestamp` is migrated to `creationdate`. In case one configured "udpate timestampe", it is migrated to `modificationdate`. [JabRef/jabref-koppor#130](https://github.com/JabRef/jabref-koppor/issues/130) +- The content of the field `timestamp` is migrated to `creationdate`. In case one configured "update timestamp", it is migrated to `modificationdate`. [JabRef/jabref-koppor#130](https://github.com/JabRef/jabref-koppor/issues/130) - The JabRef specific meta-data content in the main field such as priorities (prio1, prio2, ...) are migrated to their respective fields. They are removed from the keywords. [#6840](https://github.com/jabref/jabref/issues/6840) - We fixed an issue where groups generated from authors' last names did not include all entries of the authors'. [#5833](https://github.com/JabRef/jabref/issues/5833) -- The export to MS Office XML now uses the month name for the field `MonthAcessed` instead of the two digit number. [#7354](https://github.com/JabRef/jabref/issues/7354) +- The export to MS Office XML now uses the month name for the field `MonthAccessed` instead of the two digit number. [#7354](https://github.com/JabRef/jabref/issues/7354) - We included some standalone dialogs from the options menu in the main preference dialog and fixed some visual issues in the preferences dialog. [#7384](https://github.com/JabRef/jabref/pull/7384) - We improved the linking of the `python3` interpreter via the shebang to dynamically use the systems default Python. Related to. [JabRef/JabRef-Browser-Extension#177](https://github.com/JabRef/JabRef-Browser-Extension/issues/177) - Automatically found pdf files now have the linking button to the far left and uses a link icon with a plus instead of a briefcase. The file name also has lowered opacity(70%) until added. [#3607](https://github.com/JabRef/jabref/issues/3607) @@ -1795,7 +1796,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where some texts (e.g. descriptions) in dialogs could not be translated. [#7854](https://github.com/JabRef/jabref/issues/7854) - We fixed an issue where import hangs for ris files with "ER - ". [#7737](https://github.com/JabRef/jabref/issues/7737) -- We fixed an issue where getting bibliograhpic data from DOI or another identifer did not respect the library mode (BibTeX/biblatex). [#6267](https://github.com/JabRef/jabref/issues/6267) +- We fixed an issue where getting bibliographic data from DOI or another identifier did not respect the library mode (BibTeX/biblatex). [#6267](https://github.com/JabRef/jabref/issues/6267) - We fixed an issue where importing entries would not respect the library mode (BibTeX/biblatex). [#1018](https://github.com/JabRef/jabref/issues/1018) - We fixed an issue where an exception occurred when importing entries from a web search. [#7606](https://github.com/JabRef/jabref/issues/7606) - We fixed an issue where the table column sort order was not properly stored and resulted in unsorted eports. [#7524](https://github.com/JabRef/jabref/issues/7524) @@ -1852,7 +1853,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where changing the appearance of the preview tab did not trigger a restart warning. [#5464](https://github.com/JabRef/jabref/issues/5464) - We fixed an issue where editing "Custom preview style" triggers exception. [#7526](https://github.com/JabRef/jabref/issues/7526) - We fixed the [SAO/NASA Astrophysics Data System](https://docs.jabref.org/collect/import-using-online-bibliographic-database#sao-nasa-astrophysics-data-system) fetcher. [#7867](https://github.com/JabRef/jabref/pull/7867) -- We fixed an issue where a title with multiple applied formattings in EndNote was not imported correctly [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) +- We fixed an issue where a title with multiple applied formats in EndNote was not imported correctly [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) - We fixed an issue where a `report` in EndNote was imported as `article` [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) - We fixed an issue where the field `publisher` in EndNote was not imported in JabRef [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) @@ -1929,7 +1930,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the "Document Viewer" did not show the first page of the opened pdf document and did not show the correct total number of pages. [#7108](https://github.com/JabRef/jabref/issues/7108) - We fixed an issue where the context menu was not updated after a file link was changed. [#5777](https://github.com/JabRef/jabref/issues/5777) - We fixed an issue where the password for a shared SQL database was not remembered. [#6869](https://github.com/JabRef/jabref/issues/6869) -- We fixed an issue where newly added entires were not synced to a shared SQL database. [#7176](https://github.com/JabRef/jabref/issues/7176) +- We fixed an issue where newly added entries were not synced to a shared SQL database. [#7176](https://github.com/JabRef/jabref/issues/7176) - We fixed an issue where the PDF-Content importer threw an exception when no DOI number is present at the first page of the PDF document. [#7203](https://github.com/JabRef/jabref/issues/7203) - We fixed an issue where groups created from aux files did not update on file changes. [#6394](https://github.com/JabRef/jabref/issues/6394) - We fixed an issue where authors that only have last names were incorrectly identified as institutes when generating citation keys. [#7199](https://github.com/JabRef/jabref/issues/7199) @@ -2081,7 +2082,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where cleaning up entries broke web URLs, if "Make paths of linked files relative (if possible)" was enabled, which resulted in various other issues subsequently. [#5861](https://github.com/JabRef/jabref/issues/5861) - We fixed an issue where the tab "Required fields" of the entry editor did not show all required fields, if at least two of the defined required fields are linked with a logical or. [#5859](https://github.com/JabRef/jabref/issues/5859) - We fixed several issues concerning managing external file types: Now everything is usable and fully functional. Previously, there were problems with the radio buttons, with saving the settings and with loading an input field value. Furthermore, different behavior for Windows and other operating systems was given, which was unified as well. [#5846](https://github.com/JabRef/jabref/issues/5846) -- We fixed an issue where entries containing Unicode charaters were not parsed correctly. [#5899](https://github.com/JabRef/jabref/issues/5899) +- We fixed an issue where entries containing Unicode characters were not parsed correctly. [#5899](https://github.com/JabRef/jabref/issues/5899) - We fixed an issue where an entry containing an external filename with curly braces could not be saved. Curly braces are now longer allowed in filenames. [#5899](https://github.com/JabRef/jabref/issues/5899) - We fixed an issue where changing the type of an entry did not update the main table. [#5906](https://github.com/JabRef/jabref/issues/5906) - We fixed an issue in the optics of the library properties, that cropped the dialog on scaled displays. [#5969](https://github.com/JabRef/jabref/issues/5969) @@ -2265,7 +2266,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed multiple issues where entries did not show up after import if a search was active. [#1513](https://github.com/JabRef/jabref/issues/1513). [#3219](https://github.com/JabRef/jabref/issues/3219) - We fixed an issue where the group tree was not updated correctly after an entry was changed. [#3618](https://github.com/JabRef/jabref/issues/3618) - We fixed an issue where a right-click in the main table selected a wrong entry. [#3267](https://github.com/JabRef/jabref/issues/3267) -- We fixed an issue where in rare cases entries where overlayed in the main table. [#3281](https://github.com/JabRef/jabref/issues/3281) +- We fixed an issue where in rare cases entries were overlaid in the main table. [#3281](https://github.com/JabRef/jabref/issues/3281) - We fixed an issue where selecting a group messed up the focus of the main table and the entry editor. [#3367](https://github.com/JabRef/jabref/issues/3367) - We fixed an issue where composite author names were sorted incorrectly. [#2828](https://github.com/JabRef/jabref/issues/2828) - We fixed an issue where commands followed by `-` didn't work. [#3805](https://github.com/JabRef/jabref/issues/3805) @@ -2302,7 +2303,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where option 'open terminal here' with custom command was passing the wrong argument. [#4802](https://github.com/JabRef/jabref/issues/4802) - We fixed an issue where ranking an entry would generate an IllegalArgumentException. [#4754](https://github.com/JabRef/jabref/issues/4754) - We fixed an issue where special characters where removed from non-label key generation pattern parts. [#4767](https://github.com/JabRef/jabref/issues/4767) -- We fixed an issue where the RIS import would overwite the article date with the value of the acessed date. [#4816](https://github.com/JabRef/jabref/issues/4816) +- We fixed an issue where the RIS import would overwrite the article date with the value of the accessed date. [#4816](https://github.com/JabRef/jabref/issues/4816) - We fixed an issue where an NullPointer exception was thrown when a referenced entry in an Open/Libre Office document was no longer present in the library. Now an error message with the reference marker of the missing entry is shown. [#4932](https://github.com/JabRef/jabref/issues/4932) - We fixed an issue where a database exception related to a missing timezone was too big. [#4827](https://github.com/JabRef/jabref/issues/4827) - We fixed an issue where the IEEE fetcher returned an error if no keywords were present in the result from the IEEE website. [#4997](https://github.com/JabRef/jabref/issues/4997) diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 000000000000..c9456db873b0 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,142 @@ +# Spell checking with https://github.com/crate-ci/typos +[files] +extend-exclude = [ + # Submodules and data files + "jablib/src/main/abbrv.jabref.org/", + "jablib/src/main/resources/csl-styles/", + "jablib/src/main/resources/csl-locales/", + "jabgui/src/main/themes.jabref.org/", + "jablib/src/main/resources/ltwa/", + "jablib/src/main/resources/icore/", + "*.terms", + "reachability-metadata.json", + # Translations are maintained at Crowdin + "JabRef_*.properties", + "**/src/test/resources/", + "*.bib", + # Third-party files + "*.bst", + "*.xsl", + "harvard.readme", + "jablib/src/main/resources/xslt/mathml_latex/README", + "jablib/src/main/resources/resource/layout/din1505/README.txt", + "org.citationstyles.*.txt", + # Keys defined by Eclipse + "config/*Code Style*", + # Diagrams (SVG generated from BPMN) + "docs/images/", +] + +[default.extend-words] +# BibTeX field name +annote = "annote" +# "and" separators in BibTeX author lists +ands = "ands" +# .sav backup file extension +sav = "sav" +# Spelling used by external APIs (LibreOffice UNO, JAX-RS, xmpbox) +removeable = "removeable" +writeable = "writeable" +prefered = "prefered" +unparseable = "unparseable" +# Real words +afterwords = "afterwords" +bigrading = "bigrading" +# Ordinals, abbreviations, field codes and variable names +nd = "nd" +edn = "edn" +thes = "thes" +rin = "rin" +ein = "ein" +ot = "ot" +pn = "pn" +thm = "thm" +fre = "fre" +curren = "curren" +ket = "ket" +mis = "mis" +ser = "ser" +serie = "serie" +ofr = "ofr" +ois = "ois" +# Names in test data +aache = "aache" +aks = "aks" +alue = "alue" +ba = "ba" +bui = "bui" +malins = "malins" +ned = "ned" +noe = "noe" +ome = "ome" +ove = "ove" +rech = "rech" +thi = "thi" +tung = "tung" +vas = "vas" +wich = "wich" +wil = "wil" +# Word fragments in test data +allo = "allo" +caf = "caf" +ded = "ded" +fo = "fo" +hel = "hel" +ine = "ine" +lene = "lene" +lik = "lik" +lso = "lso" +mananger = "mananger" +metada = "metada" +nto = "nto" +ower = "ower" +pagent = "pagent" +perfor = "perfor" +plas = "plas" +potatoe = "potatoe" +pplication = "pplication" +prining = "prining" +quantit = "quantit" +rela = "rela" +sur = "sur" +tme = "tme" +ue = "ue" +urrent = "urrent" +wrapp = "wrapp" +# Non-English test data +abstrakt = "abstrakt" +als = "als" +apostroph = "apostroph" +archiv = "archiv" +commentaires = "commentaires" +commun = "commun" +dito = "dito" +exemple = "exemple" +funktional = "funktional" +ges = "ges" +informe = "informe" +ist = "ist" +mesure = "mesure" +nam = "nam" +oder = "oder" +philosophiae = "philosophiae" +plattform = "plattform" +problemas = "problemas" +"prozeß" = "prozeß" +publikation = "publikation" +sais = "sais" +sie = "sie" +varios = "varios" +varius = "varius" +vor = "vor" +januar = "januar" +februar = "februar" +juni = "juni" +juli = "juli" +oktober = "oktober" +dezember = "dezember" +titel = "titel" + +[default.extend-identifiers] +# Former command line parameter, mentioned in CHANGELOG.md +embeddBibfileInPdf = "embeddBibfileInPdf" diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b40b18720e28..d2cceb5c4e78 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -104,7 +104,7 @@ - + diff --git a/config/checkstyle/checkstyle_reviewdog.xml b/config/checkstyle/checkstyle_reviewdog.xml index 85f1f98811f7..62fc0d63a2a0 100644 --- a/config/checkstyle/checkstyle_reviewdog.xml +++ b/config/checkstyle/checkstyle_reviewdog.xml @@ -93,7 +93,7 @@ - + diff --git a/crowdin.yml b/crowdin.yml index 1c7507ae074a..5e69a389864f 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -18,7 +18,7 @@ "preserve_hierarchy": true "commit_message": "New translations %original_file_name% for (%language%)" -# prevent ci-skip beeing added +# prevent ci-skip being added "append_commit_message": false # diff --git a/docs/Gemfile b/docs/Gemfile index dcbf1a077c9b..170749b5d46c 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -15,7 +15,7 @@ gem 'jekyll-sitemap' gem "jekyll-remote-theme" -# Added, because defualt layout with path "**/*.md" does not work at Jekyll +# Added, because default layout with path "**/*.md" does not work at Jekyll # Source: https://jekyllrb.com/docs/configuration/front-matter-defaults/#glob-patterns-in-front-matter-defaults # Homepage: https://github.com/benbalter/jekyll-default-layout#jekyll-default-layout gem 'jekyll-default-layout' diff --git a/docs/code-howtos/ai.md b/docs/code-howtos/ai.md index 397c717f7ef8..20e0ac0d804a 100644 --- a/docs/code-howtos/ai.md +++ b/docs/code-howtos/ai.md @@ -68,7 +68,7 @@ There are 3 core models in the AI features: The code strictly follows the repository pattern, where an interface is created to access the internal storage for the purpose of abstraction. At the moment of writing, all of these models are implemented by using the [`MVStore`](https://www.h2database.com/html/mvstore.html). For the details of this decisions take a look at the [ADR 0033](./../decisions/0033-store-chats-in-mvstore.md). A helper class was made `MVStoreBase` so that it would be possible to use an in-memory `MVStore` in case there are some errors while opening on-disk storage. -A note needs to be made for embeddings: the embeddings storage is also implementing the internal LangChain4j interface for embeddings so that it could be used in LangChain4j algorithms. Additionally, there is a "fully ingested" repository, which simply contains a "list" of files that were fully ingested. This helps with checking if a file needs to be ingested or not, as there is no 1 to 1 correspondense with embeddings to file (which is many to one). +A note needs to be made for embeddings: the embeddings storage is also implementing the internal LangChain4j interface for embeddings so that it could be used in LangChain4j algorithms. Additionally, there is a "fully ingested" repository, which simply contains a "list" of files that were fully ingested. This helps with checking if a file needs to be ingested or not, as there is no 1 to 1 correspondence with embeddings to file (which is many to one). Because JabRef is not build around one global database, but rather it is a `.bib` file editor, a problem of identifying a `BibEntry` arose and it was solved in a somewhat complicated way: @@ -92,13 +92,13 @@ This section describes the standard pattern used for AI features. If should foll 1. Define the model of the artifact of your feature (for example, for summarization it is an AI summary, for chatting they are chat messages and chat history). 2. Define a repository interface (e.g. `SummaryRepository`, `ChatHistoryRepository`) and implement an `MVStore` implementation using the [org.jabref.logic.ai.util.MVStoreBase]. -3. Define a logic class in the `logic` package: either a task (e.g. `GenerateSummaryTask` or a utility class for performing an AI feature. It is recommended to make it "without side-effects" (it does not change or write anything in the system). Firstly, this will help in testing the class, and, secondly, the storage is typically hanlded in *in-memory cache* layer, that will be discussed next. -4. Make an in-memory cache storage layer for your feature that has a RAM map between a `BibEntry` (or a group, or some other object that your artifact is linked to) and your model. Sometimes this can be omitted (for example, embeddings do not have the in-memory cache and always use a repository), but generally it is made in order to always have access to the AI feature even if some precondition is not satisfied (for example, storing chat history and summmaries requires that there is a database path and a non-empty unique citation key, but in-memory layer allows to work with them as is). At the close of JabRef (or a library) the in-memory cache layer will check the preconditions and only then write the data to the repository. +3. Define a logic class in the `logic` package: either a task (e.g. `GenerateSummaryTask` or a utility class for performing an AI feature. It is recommended to make it "without side-effects" (it does not change or write anything in the system). Firstly, this will help in testing the class, and, secondly, the storage is typically handled in *in-memory cache* layer, that will be discussed next. +4. Make an in-memory cache storage layer for your feature that has a RAM map between a `BibEntry` (or a group, or some other object that your artifact is linked to) and your model. Sometimes this can be omitted (for example, embeddings do not have the in-memory cache and always use a repository), but generally it is made in order to always have access to the AI feature even if some precondition is not satisfied (for example, storing chat history and summaries requires that there is a database path and a non-empty unique citation key, but in-memory layer allows to work with them as is). At the close of JabRef (or a library) the in-memory cache layer will check the preconditions and only then write the data to the repository. 5. Make a `TaskAggregator` class. This is needed in order to be able to switch a component between entries and to deduplicate the tasks. So whenever you want to generate the artifact of your feature, you need to always communicate to the `TaskAggregator` class which will either create a new task or give you an already running one. The `TaskAggregator` also connects the results to the in-memory cache. The next points are targeted to the GUI of the feature: 1. Design a component using the MVVM pattern. You need to write the interface in the FXML, then write a controller `AiView` and a view-model `AiViewModel`. -2. A typical AI component will be a state machine: first and foremost, check if the AI features are enabled in JabRef (which equals to accepting a privacy policy of AI features). If not, then you must ensure that you component does nothing. To show the privacy policy banner, there is a dedicated component [org.jabref.gui.ai.AiPrivacyNoticeView]. The next states typically envolve checking some preconditions (for example, you can not summarize an entry, if it does not have linked files), and the final is the working state. You might find the [org.jabref.gui.util.BindingsHelper#bindEnum] useful. -3. The entry editor tabs are designed to be switchable (rebound to some other `BibEntry`), so you can have an `entryProperty` and whenver it is changed, the state machine of the component is rerun. +2. A typical AI component will be a state machine: first and foremost, check if the AI features are enabled in JabRef (which equals to accepting a privacy policy of AI features). If not, then you must ensure that you component does nothing. To show the privacy policy banner, there is a dedicated component [org.jabref.gui.ai.AiPrivacyNoticeView]. The next states typically involve checking some preconditions (for example, you can not summarize an entry, if it does not have linked files), and the final is the working state. You might find the [org.jabref.gui.util.BindingsHelper#bindEnum] useful. +3. The entry editor tabs are designed to be switchable (rebound to some other `BibEntry`), so you can have an `entryProperty` and whenever it is changed, the state machine of the component is rerun. 4. When you read an artifact for an entry (or a group, or other entity that is linked to your AI feature), the look-up should be made in 3 steps: look into the repository, look in to the in-memory cache, and only then contact the `TaskAggregator` to start a new generation task. diff --git a/docs/code-howtos/http-server.md b/docs/code-howtos/http-server.md index b8c3ce816a82..aebae17070e9 100644 --- a/docs/code-howtos/http-server.md +++ b/docs/code-howtos/http-server.md @@ -84,7 +84,7 @@ In case you want to debug on Windows, you need to choose "WSL" as the target for ## Get SSL Working When interacting with the [Microsoft Word AddIn](https://github.com/JabRef/JabRef-Word-Addin), a SSL-based connection is required. -[The Word-AddIn is currentely under development](https://github.com/JabRef/JabRef-Word-Addin/pull/568). +[The Word-AddIn is currently under development](https://github.com/JabRef/JabRef-Word-Addin/pull/568). (Based on ) diff --git a/docs/code-howtos/openoffice/code-reorganization.md b/docs/code-howtos/openoffice/code-reorganization.md index 3a6758e7e289..02c483042546 100644 --- a/docs/code-howtos/openoffice/code-reorganization.md +++ b/docs/code-howtos/openoffice/code-reorganization.md @@ -23,7 +23,7 @@ Why * `model` * `util` : general utilities * (`OOPair`, `OOTuple3`) collect two or three objects without creating a new class - * `OOResult` : while an Optional.empty can comunicate failure, it cannot provide details.\ + * `OOResult` : while an Optional.empty can communicate failure, it cannot provide details.\ `OOResult` allows an arbitrary error object to be provided in case of failure. * `OOVoidResult` : for functions returning no result on success, only diagnostics on failure. * `OOListUtil`: some utilities working on List diff --git a/docs/code-howtos/openoffice/ooresult-ooerror/index.md b/docs/code-howtos/openoffice/ooresult-ooerror/index.md index bf8641b89105..978d1aef9788 100644 --- a/docs/code-howtos/openoffice/ooresult-ooerror/index.md +++ b/docs/code-howtos/openoffice/ooresult-ooerror/index.md @@ -23,7 +23,7 @@ Since most of the code originally in `OOBibBase` was moved to `logic` and almost Note: some of the precondition checking still needs to stay in `OpenOfficePanel`: for example to provide a list of selected `BibEntry` instances, it needs to go through some steps from `frame.getCurrentLibraryTab()` to `(!entries.isEmpty() && checkThatEntriesHaveKeys(entries))` -To avoid `OOBibBase` depending on the higher level `OpenOfficePanel` message texts needed in `OOBibBase` were moved from `OpenOfficePanel` to `OOError`. (Others stayed, but could be moved if that seems worthwile) +To avoid `OOBibBase` depending on the higher level `OpenOfficePanel` message texts needed in `OOBibBase` were moved from `OpenOfficePanel` to `OOError`. (Others stayed, but could be moved if that seems worthwhile) ## OOError @@ -96,7 +96,7 @@ with an assumption that at any time exactly one of `result` and `error` is prese > `class X { boolean isOK; Object data; }` expresses this assumption more directly, (but omits the relation between the type parameters `` and the type in `data`) -* Since `OOResult` encodes the state `isOK` in `result.isPresent()` (and equivalently in `errror.isEmpty()`), we cannot allow construction of instances where both values are `isEmpty`.\ +* Since `OOResult` encodes the state `isOK` in `result.isPresent()` (and equivalently in `error.isEmpty()`), we cannot allow construction of instances where both values are `isEmpty`.\ In particular, `OOResult.ok(null)` and `OOResult.error(null)` are not allowed: it would make the state `isOK` ambiguous.\ It would also break the similarity to `Optional` to allow both `isEmpty` and `isOK` to be true. * Not allowing null, has a consequence on `OOResult`\ diff --git a/docs/code-howtos/openoffice/ooresult-ooerror/ooresult-alternatives.md b/docs/code-howtos/openoffice/ooresult-ooerror/ooresult-alternatives.md index 8fc70b091df7..38ade4a4a67c 100644 --- a/docs/code-howtos/openoffice/ooresult-ooerror/ooresult-alternatives.md +++ b/docs/code-howtos/openoffice/ooresult-ooerror/ooresult-alternatives.md @@ -89,7 +89,7 @@ In either case, the code becomes littered with exception handling code. We might push the try-catch into its own function.\ If the wrapper is called multiple times, this may reduce duplication of the catch-and-assign-message part. -We can show an error dialog here: `title` carries some information from the caller, the exeption caught brings some from below. +We can show an error dialog here: `title` carries some information from the caller, the exception caught brings some from below. We still need to notify the action handler (the caller) about failure. Since we have shown the dialog, we do not need to provide a message. @@ -366,7 +366,7 @@ Constraints: * `JabRefException` expects a localized message. Or we need to remember which `JabRefException` instances are localized and which need to be caught for localizing the message. * At the bottom we usually have very little information on higher level contexts: at a failure like `NoSuchProperty` we cannot tell which set of properties did we look in and why.\ For messages originating too deeply, we might want to override or extend the message anyway. -* for each exeption we might want to handle programmatically, we need a variant based on `JabRefException` +* for each exception we might want to handle programmatically, we need a variant based on `JabRefException` So we might end up: diff --git a/docs/code-howtos/testing.md b/docs/code-howtos/testing.md index a1521e6417fc..dfae9871425c 100644 --- a/docs/code-howtos/testing.md +++ b/docs/code-howtos/testing.md @@ -27,9 +27,9 @@ Imagine you want to test the method `format(String value)` in the class `BracesF * _Bug fixing:_ write a test case covering the bug and then fix it, leaving the test as a security that the bug will never reappear. * Do not catch exceptions in tests, instead use the `assertThrows(Exception.class, () -> doSomethingThrowsEx())` feature of [junit-jupiter](https://junit.org/junit5/docs/current/user-guide/) to the test method. -### Use `@ParamterizedTests` +### Use `@ParameterizedTest` -If possible, use `@ParamterizedTests`. +If possible, use `@ParameterizedTest`. Read more at . Example for a nicely formatted `@CsvSource` diff --git a/docs/code-howtos/ui-recommendations.md b/docs/code-howtos/ui-recommendations.md index b23ec39dd3f9..63fac48d34c0 100644 --- a/docs/code-howtos/ui-recommendations.md +++ b/docs/code-howtos/ui-recommendations.md @@ -30,6 +30,6 @@ See requirement [`req~ui.dialogs.confirmation.naming~1`](../requirements/ux.md). * The user shouldn't be able to submit the form if there are errors * However, disabling the submit button in case there are errors is also not optimal. Instead, clicking the submit button should highlight the errors. * Empty required files should not be marked as invalid until the user a) tried to submit the form or b) focused the field, deleted its contents and then left the field (see [Example](https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_required)). -* Ideally, the error message should be shown below the text field and not as a tooltip (so that users quickly understand what's the problem). For example as [in Boostrap](https://mdbootstrap.com/docs/jquery/forms/validation/?#custom-styles). +* Ideally, the error message should be shown below the text field and not as a tooltip (so that users quickly understand what's the problem). For example as [in Bootstrap](https://mdbootstrap.com/docs/jquery/forms/validation/?#custom-styles). diff --git a/docs/decisions/0016-mutable-preferences-objects.md b/docs/decisions/0016-mutable-preferences-objects.md index afeffd8f1e52..5ce8c0f1f36f 100644 --- a/docs/decisions/0016-mutable-preferences-objects.md +++ b/docs/decisions/0016-mutable-preferences-objects.md @@ -21,6 +21,6 @@ preferences. ### Consequences -* Import logic will be more hard as exising preferences objects have to be altered; and it is very hard to know which preference objects exactly are needed to be modified. +* Import logic will be more hard as existing preferences objects have to be altered; and it is very hard to know which preference objects exactly are needed to be modified. * Cached variables need to be observables, too. AKA The cache needs to be observable. * There is NO "real" factory pattern for the preferences objects, as they are mutable --> they are passed via the constructor and long-lived diff --git a/docs/decisions/0023-localized-preferences.md b/docs/decisions/0023-localized-preferences.md index aca132cf7082..8e6070769211 100644 --- a/docs/decisions/0023-localized-preferences.md +++ b/docs/decisions/0023-localized-preferences.md @@ -40,19 +40,19 @@ If user stores value, the value is stored as is. The default value is stored with `%` in front. A caller has to localize any stored value in the preferences if the string is 'escaped' by `%` as the first character. -Code: If looked-up string starts with `%`, call `Localization.lang` of the substring (strating from 2nd character). +Code: If looked-up string starts with `%`, call `Localization.lang` of the substring (starting from 2nd character). Otherwise, use string as is. * Good, because clear distinction between default value and user-supplied value. * Good, because on update of JabRef's defaults, the string is not modified. -* Good, because already used in fxml files to indicate translateable strings. +* Good, because already used in fxml files to indicate translatable strings. * Good, because consistent to FXML. ### Localize Defaults Example: `defaults.put(EMAIL_SUBJECT, Localization.lang("References"));` in `JabRefGuiPreferences`. -* Good, because it is the current implementaton +* Good, because it is the current implementation * Bad, because it does not allow for language switching ### Store the preference only when it was changed by the user diff --git a/docs/decisions/0025-reviewdog-reviews.md b/docs/decisions/0025-reviewdog-reviews.md index 0b7f76420bdb..c0c61098a3e3 100644 --- a/docs/decisions/0025-reviewdog-reviews.md +++ b/docs/decisions/0025-reviewdog-reviews.md @@ -17,7 +17,7 @@ How to quickly provide feedback to contributors that checkstyle was not matched? * Be friendly to newcomers * Provide fast feedback to contributors * Lower the workload of maintainers -* Keep maintainers focused on the "real" challanges of the code changes +* Keep maintainers focused on the "real" challenges of the code changes ## Considered Options @@ -30,4 +30,4 @@ How to quickly provide feedback to contributors that checkstyle was not matched? Chosen option: "Use Reviewdog's PullRequest review reporter", because resolves force to provide fast feedback. We do not want to use `comment-failure-action`, because [it might produce too many comments](https://github.com/quipper/comment-failure-action/issues/224). We accept that newcomers might be annoyed if quick automatic feedback by a bot is given: -We value the time of our maintainers and want to keep them focused on the real challanges of the code changes. +We value the time of our maintainers and want to keep them focused on the real challenges of the code changes. diff --git a/docs/decisions/0027-synchronization.md b/docs/decisions/0027-synchronization.md index 09fc018f38dc..3fb05155a0ef 100644 --- a/docs/decisions/0027-synchronization.md +++ b/docs/decisions/0027-synchronization.md @@ -37,7 +37,7 @@ Thus, there are additions needed to handle the local synchronization. Moreover, the optimistic offline lock does not say how a set of data is synchronized. -Both shortcomings are resolved by our algotihm. +Both shortcomings are resolved by our algorithm. This algorithm is described at [Remote JabDrive storage](../code-howtos/remote-storage-jabdrive.md). ### Algorithm based on "optimistic offline lock" diff --git a/docs/decisions/0029-cff-export-multiple-entries.md b/docs/decisions/0029-cff-export-multiple-entries.md index d0a65477d916..9abb0585ca66 100644 --- a/docs/decisions/0029-cff-export-multiple-entries.md +++ b/docs/decisions/0029-cff-export-multiple-entries.md @@ -16,8 +16,8 @@ The need for an [exporter](https://github.com/JabRef/jabref/issues/10661) to [CF ## Considered Options * When exporting: - * Export non-`software` entries with dummy topmost `sofware` and entries as `preferred-citation` - * Export non-`software` entries with dummy topmost `sofware` and entries as `references` + * Export non-`software` entries with dummy topmost `software` and entries as `preferred-citation` + * Export non-`software` entries with dummy topmost `software` and entries as `references` * Forbid exporting multiple entries at once * Forbid exporting more than one software entry at once * Export entries in several files (i.e. one / file) @@ -40,7 +40,7 @@ The decision outcome is the following. * If several entries including a `software` or `dataset` one are selected, then exporter uses this one as topmost element and the others as `references`, adding a potential `preferred-citation` for the potential `cites` element of the topmost `software` entry. * If several entries including several `software` ones are selected, then exporter uses a dummy topmost element, and selected entries are exported as `references`. The `cites` or `related` fields won't be exported in this case. * JabRef will not handle `cites` or `related` fields for non-`software` elements. -* When importing, JabRef will create several entries: one main entry for the `software` and other entries for the potential `preferred-citation` and `references` fields. JabRef will link main entry to the preferred citation using a `cites` from the main entry, and wil link main entry to the references using a `related` from the main entry. +* When importing, JabRef will create several entries: one main entry for the `software` and other entries for the potential `preferred-citation` and `references` fields. JabRef will link main entry to the preferred citation using a `cites` from the main entry, and will link main entry to the references using a `related` from the main entry. ### Positive Consequences diff --git a/docs/decisions/0032-store-chats-in-local-user-folder.md b/docs/decisions/0032-store-chats-in-local-user-folder.md index 79495bcaeccb..36b722fb20aa 100644 --- a/docs/decisions/0032-store-chats-in-local-user-folder.md +++ b/docs/decisions/0032-store-chats-in-local-user-folder.md @@ -26,7 +26,7 @@ Chats with AI should be stored somewhere. But where and how? Chosen option: "In local user folder", because it's very hard to work with a shared library, if two users will work simultaneously on one library, then AI chats file will be absolutely arbitrary -and unmergable. +and unmergeable. ## Pros and Cons of the Options diff --git a/docs/decisions/0037-rag-architecture-implementation.md b/docs/decisions/0037-rag-architecture-implementation.md index 0c7290e61f47..9ec6ee5c1729 100644 --- a/docs/decisions/0037-rag-architecture-implementation.md +++ b/docs/decisions/0037-rag-architecture-implementation.md @@ -23,7 +23,7 @@ How source data is processed: 3. **Splitting**: the string from previous step is split into parts (because LLM has fixed context window, meaning it cannot handle big documents). 4. **Embedding generation**: a vector consisting of float values is generated out of chunks. This vector represents meaning -of text and the main propety of such vectors is that chunks with similar meaning has vectors that are close to. +of text and the main property of such vectors is that chunks with similar meaning has vectors that are close to. Generation of such a vector is achieved by using a separate model called *embedding model*. 5. **Store**: chunks with relevant metadata (for example, from which document they were generated) and embedding vector are stored in a vector database. diff --git a/docs/decisions/0044-use-picocli-for-jabkit.md b/docs/decisions/0044-use-picocli-for-jabkit.md index c8fcf548916b..9b04efdfa009 100644 --- a/docs/decisions/0044-use-picocli-for-jabkit.md +++ b/docs/decisions/0044-use-picocli-for-jabkit.md @@ -81,7 +81,7 @@ Available at . ### ritopt -Availale at . +Available at . * Good, because simple interface * Good, because used in JabRef a long time ago (before 2.10) diff --git a/docs/decisions/0047-use-references-heading-in-citations-tab.md b/docs/decisions/0047-use-references-heading-in-citations-tab.md index 698981514dc3..3460cf4f27a5 100644 --- a/docs/decisions/0047-use-references-heading-in-citations-tab.md +++ b/docs/decisions/0047-use-references-heading-in-citations-tab.md @@ -17,7 +17,7 @@ Before July 2025, JabRef used "cites" and "cited by" as headings, but these were There is only one form of citation. Citation is always "backward", so there is nothing wrong with cites and cited by, except that sometimes you need a noun to refer to things that are cited or cited by, and there is only one word for that. It's "citations". -That's a problem when you want to distinguishe between citations that mean cites and citations that mean cited by. Hence the use of forward and backward. +That's a problem when you want to distinguish between citations that mean cites and citations that mean cited by. Hence the use of forward and backward. How to name the headings of these two areas? diff --git a/docs/decisions/0050-custom-code-style.md b/docs/decisions/0050-custom-code-style.md index 8ea83b06379b..f7849be6ae54 100644 --- a/docs/decisions/0050-custom-code-style.md +++ b/docs/decisions/0050-custom-code-style.md @@ -79,7 +79,7 @@ Example: * Good, because the subject is "always" in the first thing in the line. * Good, because it also formats the imports * Neutral, because optimized for code review (because of the 80 characters width): - Favors short lines (80 characters); the reading is more top-to-buttom; screens are typically wider. + Favors short lines (80 characters); the reading is more top-to-bottom; screens are typically wider. * Bad, because [prettier-action cannot be used](https://github.com/creyD/prettier_action/issues/149) * Bad, because [has line breaks at single variables](https://github.com/jhipster/prettier-java/issues/777) * Bad, because not supported by the formatter tool [jbang-fmt](https://github.com/jbangdev/jbang-fmt). diff --git a/docs/index.md b/docs/index.md index 0096c7f8fc59..0a2b8fa112c0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,7 @@ Thus, we ensure that all of JabRef's code follows the same code style. Binaries are created using [gradle](https://gradle.org). In case of an internal pull request, they are uploaded to . These binaries are created without any checks to have them available as quickly as possible, even if the localization or some fetchers are broken. -You can fnd the deployment workflow runs at: . +You can find the deployment workflow runs at: . ## Branches diff --git a/jabgui/build.gradle.kts b/jabgui/build.gradle.kts index 22bcb0f22250..0041b21b6c34 100644 --- a/jabgui/build.gradle.kts +++ b/jabgui/build.gradle.kts @@ -121,7 +121,7 @@ application { "-XX:+UseStringDeduplication" // Default garbage collector (G1) is sufficient - // More informaiton: https://learn.microsoft.com/en-us/azure/developer/java/containers/overview#understand-jvm-default-ergonomics + // More information: https://learn.microsoft.com/en-us/azure/developer/java/containers/overview#understand-jvm-default-ergonomics // "-XX:+UseZGC", "-XX:+ZUncommit" // "-XX:+UseG1GC" ) @@ -292,7 +292,7 @@ embeddedPostgresBinaryByJpackageTask.forEach { (taskName, binary) -> // Include the platform-specific Postgres binary module in the jlink runtime image. addModules.add(binary.moduleName) // Resolve the module when the packaged launcher starts so the binary resource is discoverable. - // add will siply replace the existing args! + // add will simply replace the existing args! javaOptions.set(application.applicationDefaultJvmArgs + "--add-modules=${binary.moduleName}") addModules.addAll(sharedJpackageImageModules) } diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index 934ed603e93e..d10b6f1dbace 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -206,7 +206,7 @@ public void initialize() { DefaultFileUpdateMonitor fileUpdateMonitor = new DefaultFileUpdateMonitor(); JabRefGUI.fileUpdateMonitor = fileUpdateMonitor; - HeadlessExecutorService.INSTANCE.executeInterruptableTask(fileUpdateMonitor, "FileUpdateMonitor"); + HeadlessExecutorService.INSTANCE.executeInterruptibleTask(fileUpdateMonitor, "FileUpdateMonitor"); Injector.setModelOrService(FileUpdateMonitor.class, fileUpdateMonitor); DirectoryMonitor directoryMonitor = new DirectoryMonitor(); diff --git a/jabgui/src/main/java/org/jabref/gui/desktop/os/Linux.java b/jabgui/src/main/java/org/jabref/gui/desktop/os/Linux.java index 4269ba6bbd1c..af2024e0f947 100644 --- a/jabgui/src/main/java/org/jabref/gui/desktop/os/Linux.java +++ b/jabgui/src/main/java/org/jabref/gui/desktop/os/Linux.java @@ -200,7 +200,7 @@ public Path getDefaultFileChooserDirectory() { String documentsDirectory = strings.getFirst(); Path documentsPath = Path.of(documentsDirectory); if (!Files.exists(documentsPath)) { - LoggerFactory.getLogger(Linux.class).error("xdg-user-dir returned non-existant directory {}", documentsDirectory); + LoggerFactory.getLogger(Linux.class).error("xdg-user-dir returned non-existent directory {}", documentsDirectory); return Directories.getUserDirectory(); } LoggerFactory.getLogger(Linux.class).debug("Got documents path {}", documentsPath); diff --git a/jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateSearch.java b/jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateSearch.java index ad2fae15a7fe..083400c250c6 100644 --- a/jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateSearch.java +++ b/jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateSearch.java @@ -97,7 +97,7 @@ public void execute() { duplicateCountObservable.addListener((_, _, newValue) -> UiTaskExecutor.runAndWaitInJavaFXThread(() -> duplicateTotal.set(newValue))); - duplicateSearchTask = HeadlessExecutorService.INSTANCE.executeInterruptableTask(() -> searchPossibleDuplicates(entries, database.getMode()), "DuplicateSearcher"); + duplicateSearchTask = HeadlessExecutorService.INSTANCE.executeInterruptibleTask(() -> searchPossibleDuplicates(entries, database.getMode()), "DuplicateSearcher"); BackgroundTask.wrap(this::verifyDuplicates) .onSuccess(this::handleDuplicates) .executeWith(taskExecutor); diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java index 2fac15abc6f3..38eec13f04f6 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java @@ -552,7 +552,7 @@ private void selectEntries(List entryKeys) { Set keysToSelect = new HashSet<>(entryKeys); LibraryTab firstFoundTab = null; for (LibraryTab libraryTab : sortedTabs) { - List entrysToSelectInCurrentTab = new ArrayList<>(keysToSelect.size()); + List entriesToSelectInCurrentTab = new ArrayList<>(keysToSelect.size()); for (BibEntry entry : libraryTab.getDatabase() .getEntries()) { Optional citationKeyOptional = entry.getCitationKey(); @@ -568,9 +568,9 @@ private void selectEntries(List entryKeys) { } LOGGER.debug("Found entry {} in library tab {}", citationKey, libraryTab); keysToSelect.remove(citationKey); - entrysToSelectInCurrentTab.add(entry); + entriesToSelectInCurrentTab.add(entry); } - libraryTab.clearAndSelect(entrysToSelectInCurrentTab); + libraryTab.clearAndSelect(entriesToSelectInCurrentTab); } LOGGER.trace("End of loop"); if (firstFoundTab != null) { diff --git a/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogView.java b/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogView.java index 993424991834..26adce559787 100644 --- a/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogView.java +++ b/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogView.java @@ -258,7 +258,7 @@ public void initialize() { validationVisualizer.initVisualization(viewModel.keywordRegexValidationStatus(), keywordGroupSearchTerm); validationVisualizer.initVisualization(viewModel.keywordSearchTermEmptyValidationStatus(), keywordGroupSearchTerm); validationVisualizer.initVisualization(viewModel.keywordFieldEmptyValidationStatus(), keywordGroupSearchField); - validationVisualizer.initVisualization(viewModel.texGroupFilePathValidatonStatus(), texGroupFilePath); + validationVisualizer.initVisualization(viewModel.texGroupFilePathValidationStatus(), texGroupFilePath); nameField.requestFocus(); }); diff --git a/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java b/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java index bc41696f6b33..6c7e29607c8a 100644 --- a/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java @@ -582,7 +582,7 @@ public ValidationStatus keywordSearchTermEmptyValidationStatus() { return keywordSearchTermEmptyValidator.getValidationStatus(); } - public ValidationStatus texGroupFilePathValidatonStatus() { + public ValidationStatus texGroupFilePathValidationStatus() { return texGroupFilePathValidator.getValidationStatus(); } diff --git a/jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java b/jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java index 0ea789671513..7f9c47e64991 100644 --- a/jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java +++ b/jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java @@ -317,7 +317,7 @@ protected void updateItem(GroupNodeViewModel group, boolean empty) { .withContextMenu(this::createContextMenuForGroup) .withEventFilter(MouseEvent.MOUSE_PRESSED, (_, event) -> { if (((MouseEvent) event).getButton() == MouseButton.SECONDARY && !stateManager.getSelectedEntries().isEmpty()) { - // Prevent right-click to select group whe we have selected entries + // Prevent right-click to select group when we have selected entries event.consume(); } else if (event.getTarget() instanceof StackPane pane) { if (pane.getStyleClass().contains("arrow") || pane.getStyleClass().contains("tree-disclosure-node")) { diff --git a/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java b/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java index b3ed7eb15fce..4d80d767afad 100644 --- a/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java +++ b/jabgui/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java @@ -126,7 +126,7 @@ private Button createSearchButton() { return search; } - /// Creatse help button for currently selected fetcher + /// Creates help button for currently selected fetcher private StackPane createHelpButtonContainer() { StackPane helpButtonContainer = new StackPane(); ActionFactory factory = new ActionFactory(); diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java b/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java index de975cfb2194..65a1c9d19d17 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java @@ -248,18 +248,18 @@ public static MainTableColumnModel parse(String rawColumnName) { return new MainTableColumnModel(Type.NORMALFIELD, ""); } - String[] splittedName = rawColumnName.split(COLUMNS_QUALIFIER_DELIMITER.toString()); + String[] splitName = rawColumnName.split(COLUMNS_QUALIFIER_DELIMITER.toString()); - Type type = Type.fromString(splittedName[0]); + Type type = Type.fromString(splitName[0]); String qualifier = ""; if ((type == Type.NORMALFIELD) || (type == Type.SPECIALFIELD) || (type == Type.EXTRAFILE)) { - if (splittedName.length == 1) { - qualifier = splittedName[0]; // By default the rawColumnName is parsed as NORMALFIELD + if (splitName.length == 1) { + qualifier = splitName[0]; // By default the rawColumnName is parsed as NORMALFIELD } else { - qualifier = splittedName[1]; + qualifier = splitName[1]; } } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/columns/LinkedIdentifierColumn.java b/jabgui/src/main/java/org/jabref/gui/maintable/columns/LinkedIdentifierColumn.java index be463297be4b..786c5cb7041b 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/columns/LinkedIdentifierColumn.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/columns/LinkedIdentifierColumn.java @@ -59,7 +59,7 @@ public LinkedIdentifierColumn(MainTableColumnModel model, .withTooltip(this::createIdentifierTooltip) .withMenu(this::createIdentifierMenu) .withOnMouseClickedEvent((_, linkedFiles) -> event -> { - // If we only have one identifer, open directly + // If we only have one identifier, open directly if ((linkedFiles.size() == 1) && (event.getButton() == MouseButton.PRIMARY)) { new OpenUrlAction(dialogService, stateManager, preferences).execute(); } diff --git a/jabgui/src/main/java/org/jabref/gui/openoffice/OOBibBase.java b/jabgui/src/main/java/org/jabref/gui/openoffice/OOBibBase.java index 75eb79a87e38..6ac30090fe1c 100644 --- a/jabgui/src/main/java/org/jabref/gui/openoffice/OOBibBase.java +++ b/jabgui/src/main/java/org/jabref/gui/openoffice/OOBibBase.java @@ -746,7 +746,7 @@ public OOVoidResult insertCSLCitation(List entries, /// /// @param entries The entries to cite. /// @param citationType Indicates whether it is an in-text citation, a citation in parentheses or an invisible citation. - /// @param jStyle Indicates citation formating in JStyle + /// @param jStyle Indicates citation formatting in JStyle /// @param bibDatabaseContext The database the entries belong to (all of them). Used when creating the citation mark. /// @param syncOptions Indicates whether in-text citations should be refreshed in the document. Optional.empty() indicates no refresh. Otherwise, provides options for refreshing the reference list. /// @param pageInfo A single page-info for these entries. Attributed to the last entry. @@ -1077,7 +1077,7 @@ public void guiActionUpdateDocument(List databases, OOStyle style) /// Helper method for guiActionUpdateDocument, refreshes a JStyle bibliography. /// /// @param databases Must have at least one. - /// @param jStyle Indicates citation formating in JStyle. + /// @param jStyle Indicates citation formatting in JStyle. /// @param doc Text document. /// @param frontend,fcursor Used to synchronize document. /// @param errorTitle Error message for user. diff --git a/jabgui/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java b/jabgui/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java index 99f5f13743dc..7d900d694ce0 100644 --- a/jabgui/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java +++ b/jabgui/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java @@ -524,7 +524,7 @@ protected OOBibBase call() throws BootstrapException, CreationException, IOExcep ex); } case BootstrapException bootstrapEx -> { - LOGGER.error("Exception boostrap cause", bootstrapEx.getTargetException()); + LOGGER.error("Exception bootstrap cause", bootstrapEx.getTargetException()); dialogService.showErrorDialogAndWait("Bootstrap error", bootstrapEx.getTargetException()); } case null, diff --git a/jabgui/src/main/java/org/jabref/gui/util/CustomLocalDragboard.java b/jabgui/src/main/java/org/jabref/gui/util/CustomLocalDragboard.java index 25c62ee9d1e7..955e75d99916 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/CustomLocalDragboard.java +++ b/jabgui/src/main/java/org/jabref/gui/util/CustomLocalDragboard.java @@ -50,7 +50,7 @@ public void putBibEntries(List entries) { /// Get a List of [BibEntry] from the dragboard /// - /// @return List of BibEntry or empty list if no entries are avaiable + /// @return List of BibEntry or empty list if no entries are available public List getBibEntries() { if (hasBibEntries()) { return getValue(BIB_ENTRIES); @@ -71,7 +71,7 @@ public void putPreviewLayouts(List previewLayouts) { /// Get a List of [PreviewLayout] from the dragboard /// - /// @return List of PreviewLayout or empty list if no entries are avaiable + /// @return List of PreviewLayout or empty list if no entries are available public List getPreviewLayouts() { if (hasType(DragAndDropDataFormats.PREVIEWLAYOUT_LIST_CLASS)) { return getValue(DragAndDropDataFormats.PREVIEWLAYOUT_LIST_CLASS); diff --git a/jabgui/src/main/java/org/jabref/gui/util/FileDialogConfiguration.java b/jabgui/src/main/java/org/jabref/gui/util/FileDialogConfiguration.java index 84cb52bf12ab..7d96d67f8897 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/FileDialogConfiguration.java +++ b/jabgui/src/main/java/org/jabref/gui/util/FileDialogConfiguration.java @@ -74,7 +74,7 @@ public Builder withInitialDirectory(Path directory) { directory = directory.getParent(); } // The lines above work also if the dir does not exist at all! - // NULL is accepted by the filechooser as no inital path + // NULL is accepted by the filechooser as no initial path // Explicit null check, if somehow the parent is null, as Files.exists throws an NPE otherwise if ((directory != null) && !Files.exists(directory)) { directory = null; diff --git a/jabgui/src/main/java/org/jabref/gui/util/ZipFileChooser.java b/jabgui/src/main/java/org/jabref/gui/util/ZipFileChooser.java index 4601f05b341f..f98e8791c9bd 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/ZipFileChooser.java +++ b/jabgui/src/main/java/org/jabref/gui/util/ZipFileChooser.java @@ -26,7 +26,7 @@ public class ZipFileChooser extends BaseDialog { /// New ZIP file chooser. /// - /// @param zipFile ZIP-Fle to choose from, must be readable + /// @param zipFile ZIP-File to choose from, must be readable public ZipFileChooser(FileSystem zipFile) throws IOException { setTitle(Localization.lang("Select file from ZIP-archive")); diff --git a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java index 72f1dfc590d2..19ff61766b51 100644 --- a/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java +++ b/jabgui/src/main/java/org/jabref/gui/walkthrough/WalkthroughAction.java @@ -536,7 +536,7 @@ private Walkthrough createSearchWalkthrough() { .addStep(WalkthroughStep .tooltip(Localization.lang("Type `title = covid OR title = pandemic`")) .content(new TextBlock(Localization.lang("This query finds paper with title containing either COVID or pandemics in general.")), - new InfoBlock(Localization.lang("`OR` can be satisified with either of the conditions being true."))) + new InfoBlock(Localization.lang("`OR` can be satisfied with either of the conditions being true."))) .resolver(searchFieldResolver) .trigger(Trigger.onTextMatchesRegex("(?i)title\\s*=\\s*covid\\s+OR\\s+title\\s*=\\s*pandemic|title\\s*=\\s*pandemic\\s+OR\\s+title\\s*=\\s*covid")) .position(TooltipPosition.BOTTOM) diff --git a/jabgui/src/test/java/org/jabref/gui/autocompleter/DefaultAutoCompleterTest.java b/jabgui/src/test/java/org/jabref/gui/autocompleter/DefaultAutoCompleterTest.java index 728d466be6b0..1ba2e649f0d1 100644 --- a/jabgui/src/test/java/org/jabref/gui/autocompleter/DefaultAutoCompleterTest.java +++ b/jabgui/src/test/java/org/jabref/gui/autocompleter/DefaultAutoCompleterTest.java @@ -124,7 +124,7 @@ void completeShortStringReturnsValue() { } @Test - void completeBeginnigOfSecondWordReturnsWord() { + void completeBeginningOfSecondWordReturnsWord() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "test value"); database.insertEntry(entry); diff --git a/jabgui/src/test/java/org/jabref/gui/autocompleter/FieldValueSuggestionProviderTest.java b/jabgui/src/test/java/org/jabref/gui/autocompleter/FieldValueSuggestionProviderTest.java index 22f37c18db16..b2e37449a587 100644 --- a/jabgui/src/test/java/org/jabref/gui/autocompleter/FieldValueSuggestionProviderTest.java +++ b/jabgui/src/test/java/org/jabref/gui/autocompleter/FieldValueSuggestionProviderTest.java @@ -85,7 +85,7 @@ void completeValueReturnsValue() { } @Test - void completeBeginnigOfValueReturnsValue() { + void completeBeginningOfValueReturnsValue() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "testValue"); database.insertEntry(entry); @@ -147,7 +147,7 @@ void completeShortStringReturnsFieldValue() { } @Test - void completeBeginnigOfSecondWordReturnsWholeFieldValue() { + void completeBeginningOfSecondWordReturnsWholeFieldValue() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "test value"); database.insertEntry(entry); diff --git a/jabgui/src/test/java/org/jabref/gui/autocompleter/PersonNameStringConverterTest.java b/jabgui/src/test/java/org/jabref/gui/autocompleter/PersonNameStringConverterTest.java index 9a30e20bb38d..0f5925774979 100644 --- a/jabgui/src/test/java/org/jabref/gui/autocompleter/PersonNameStringConverterTest.java +++ b/jabgui/src/test/java/org/jabref/gui/autocompleter/PersonNameStringConverterTest.java @@ -19,7 +19,7 @@ class PersonNameStringConverterTest { @BeforeEach void setUp() { - // set up auhtor's name + // set up author's name author = new Author("Joseph M.", "J. M.", "", "Reagle", "Jr."); } diff --git a/jabgui/src/test/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModelTest.java b/jabgui/src/test/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModelTest.java index dd30fc224c7c..bbc13ec45836 100644 --- a/jabgui/src/test/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModelTest.java +++ b/jabgui/src/test/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModelTest.java @@ -41,10 +41,10 @@ void getSuggestionsWithEscapedHierarchicalDelimiter() { @Test void parseKeywordWithHierarchicalKeywords() { - String hierarchichalString = "parent > node > child"; - Keyword parsedKeyword = KeywordList.parse(hierarchichalString, viewModel.getKeywordSeparator()).get(0); + String hierarchicalString = "parent > node > child"; + Keyword parsedKeyword = KeywordList.parse(hierarchicalString, viewModel.getKeywordSeparator()).get(0); - assertEquals(parsedKeyword, viewModel.parseKeyword(hierarchichalString)); + assertEquals(parsedKeyword, viewModel.parseKeyword(hierarchicalString)); } @Test @@ -57,9 +57,9 @@ void parseKeywordWithMultipleKeywords() { @Test void stringConverterToStringWithHierarchicalKeywords() { - String hierarchichalString = "parent > node > child"; - Keyword keyword = Keyword.ofHierarchical(hierarchichalString); + String hierarchicalString = "parent > node > child"; + Keyword keyword = Keyword.ofHierarchical(hierarchicalString); - assertEquals(hierarchichalString, KeywordsEditorViewModel.getStringConverter().toString(keyword)); + assertEquals(hierarchicalString, KeywordsEditorViewModel.getStringConverter().toString(keyword)); } } diff --git a/jabgui/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java b/jabgui/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java index e7595d59e15c..be92dccb0a89 100644 --- a/jabgui/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java +++ b/jabgui/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java @@ -73,14 +73,14 @@ void validateExistingAbsolutePath() throws IOException { when(metaData.getLatexFileDirectory(any(String.class))).thenReturn(Optional.of(temporaryFolder)); viewModel.texGroupFilePathProperty().setValue(anAuxFile.toString()); - assertTrue(viewModel.texGroupFilePathValidatonStatus().isValid()); + assertTrue(viewModel.texGroupFilePathValidationStatus().isValid()); } @Test void validateNonExistingAbsolutePath() { Path notAnAuxFile = temporaryFolder.resolve("notanauxfile.aux").toAbsolutePath(); viewModel.texGroupFilePathProperty().setValue(notAnAuxFile.toString()); - assertFalse(viewModel.texGroupFilePathValidatonStatus().isValid()); + assertFalse(viewModel.texGroupFilePathValidationStatus().isValid()); } @Test @@ -92,7 +92,7 @@ void validateExistingRelativePath() throws IOException { when(metaData.getLatexFileDirectory(any(String.class))).thenReturn(Optional.of(temporaryFolder)); viewModel.texGroupFilePathProperty().setValue(anAuxFile.toString()); - assertTrue(viewModel.texGroupFilePathValidatonStatus().isValid()); + assertTrue(viewModel.texGroupFilePathValidationStatus().isValid()); } @Test diff --git a/jabgui/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java b/jabgui/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java index 4ff08d3c6899..4e2d44f96294 100644 --- a/jabgui/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java +++ b/jabgui/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java @@ -280,7 +280,7 @@ void shouldShowDialogWhenKeywordDiffers() { } @Test - void shouldShowDialogWhenCaseSensitivyDiffers() { + void shouldShowDialogWhenCaseSensitivityDiffers() { AbstractGroup oldGroup = new WordKeywordGroup("group", GroupHierarchyType.INCLUDING, StandardField.KEYWORDS, "keywordTest", false, ',', true); AbstractGroup newGroup = new WordKeywordGroup("group", GroupHierarchyType.INCLUDING, StandardField.KEYWORDS, "keywordChanged", true, ',', true); diff --git a/jabgui/src/test/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModelTest.java b/jabgui/src/test/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModelTest.java index ba933f635302..1e48ce32a410 100644 --- a/jabgui/src/test/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModelTest.java +++ b/jabgui/src/test/java/org/jabref/gui/importer/fetcher/WebSearchPaneViewModelTest.java @@ -73,7 +73,7 @@ void canExtractDOIFromQueryText() { @Test void queryConsistingOfInvalidDOIIsValid() { viewModel.queryProperty().setValue("101.1007/JHEP02(2023)082"); - // There is currently no interpretation of nearly-valid identifiers, therefore, this is concidered as "regular" search term + // There is currently no interpretation of nearly-valid identifiers, therefore, this is considered as "regular" search term assertTrue(viewModel.queryValidationStatus().validProperty().getValue()); } diff --git a/jabgui/src/test/java/org/jabref/gui/util/comparator/NumericFieldComparatorTest.java b/jabgui/src/test/java/org/jabref/gui/util/comparator/NumericFieldComparatorTest.java index 9fb3048f302a..8cfebc135dae 100644 --- a/jabgui/src/test/java/org/jabref/gui/util/comparator/NumericFieldComparatorTest.java +++ b/jabgui/src/test/java/org/jabref/gui/util/comparator/NumericFieldComparatorTest.java @@ -64,7 +64,7 @@ void compareWordWithMinus() { } @Test - void compareNumericSignalWithoutNumberWithLenghtBiggerThanOne() { + void compareNumericSignalWithoutNumberWithLengthBiggerThanOne() { assertEquals(2, comparator.compare("- ", "+ ")); } diff --git a/jabgui/src/test/java/org/jabref/migrations/GuiPreferencesMigrationsTest.java b/jabgui/src/test/java/org/jabref/migrations/GuiPreferencesMigrationsTest.java index aa3cfae74b55..6ccb9393c96f 100644 --- a/jabgui/src/test/java/org/jabref/migrations/GuiPreferencesMigrationsTest.java +++ b/jabgui/src/test/java/org/jabref/migrations/GuiPreferencesMigrationsTest.java @@ -65,7 +65,7 @@ void oldStyleBibtexkeyPattern1() { @Test void arbitraryBibtexkeyPattern() { - String arbitraryPattern = "[anyUserPrividedString]"; + String arbitraryPattern = "[anyUserProvidedString]"; when(preferences.get(eq(PreferencesMigrations.V4_0_IMPORT_FILENAME_PATTERN), any())).thenReturn(arbitraryPattern); diff --git a/jabgui/src/test/java/org/jabref/migrations/SpecialFieldsToSeparateFieldsTest.java b/jabgui/src/test/java/org/jabref/migrations/SpecialFieldsToSeparateFieldsTest.java index 11dbc262077d..33682b5a5788 100644 --- a/jabgui/src/test/java/org/jabref/migrations/SpecialFieldsToSeparateFieldsTest.java +++ b/jabgui/src/test/java/org/jabref/migrations/SpecialFieldsToSeparateFieldsTest.java @@ -28,7 +28,7 @@ void migrateToCorrectField(SpecialField field, String fieldInKeyword, BibEntry e } @Test - void noKewordToMigrate() { + void noKeywordToMigrate() { BibEntry entry = new BibEntry().withField(StandardField.AUTHOR, "JabRef") .withField(StandardField.KEYWORDS, "tdd"); BibEntry expected = new BibEntry().withField(StandardField.AUTHOR, "JabRef") diff --git a/jabgui/src/test/java/org/jabref/testutils/interactive/styletester/StyleTesterMain.java b/jabgui/src/test/java/org/jabref/testutils/interactive/styletester/StyleTesterMain.java index 6a5019bc5402..eec0c2a15929 100644 --- a/jabgui/src/test/java/org/jabref/testutils/interactive/styletester/StyleTesterMain.java +++ b/jabgui/src/test/java/org/jabref/testutils/interactive/styletester/StyleTesterMain.java @@ -22,7 +22,7 @@ static void main(String[] args) { @Override public void start(Stage stage) throws JabRefException { DefaultFileUpdateMonitor fileUpdateMonitor = new DefaultFileUpdateMonitor(); - HeadlessExecutorService.INSTANCE.executeInterruptableTask(fileUpdateMonitor, "FileUpdateMonitor"); + HeadlessExecutorService.INSTANCE.executeInterruptibleTask(fileUpdateMonitor, "FileUpdateMonitor"); WorkspacePreferences workspacePreferences = WorkspacePreferences.getDefault(); ThemeManager themeManager = new ThemeManager(workspacePreferences, fileUpdateMonitor); diff --git a/jabkit/src/main/java/org/jabref/toolkit/commands/PdfUpdate.java b/jabkit/src/main/java/org/jabref/toolkit/commands/PdfUpdate.java index 9a1932b986b3..a8851991e184 100644 --- a/jabkit/src/main/java/org/jabref/toolkit/commands/PdfUpdate.java +++ b/jabkit/src/main/java/org/jabref/toolkit/commands/PdfUpdate.java @@ -49,7 +49,7 @@ class PdfUpdate implements Callable { private List formats = List.of("xmp", "bibtex-attachment"); // ToDO: default value? @Option(names = {"-k", "--citation-key"}, description = "Citation keys", required = true) - private List citationKeys = List.of(); // ToDo: check dedault value + private List citationKeys = List.of(); // ToDo: check default value @Mixin private InputOption inputOption = new InputOption(); @@ -104,7 +104,7 @@ private static void writeMetadataToPdf(List loaded, FieldPreferences fieldPreferences, JournalAbbreviationRepository abbreviationRepository, boolean writeXMP, - boolean embeddBibfile) { + boolean embedBibfile) { ParserResult pr = loaded.getLast(); BibDatabaseContext databaseContext = pr.getDatabaseContext(); pr.getPath().map(Path::toAbsolutePath).ifPresent(databaseContext::setDatabasePath); @@ -123,7 +123,7 @@ private static void writeMetadataToPdf(List loaded, embeddedBibFilePdfExporter, abbreviationRepository, writeXMP, - embeddBibfile); + embedBibfile); } return; } @@ -136,7 +136,7 @@ private static void writeMetadataToPdf(List loaded, embeddedBibFilePdfExporter, abbreviationRepository, writeXMP, - embeddBibfile); + embedBibfile); writeMetadataToPdfByFileNames( databaseContext, files, @@ -145,7 +145,7 @@ private static void writeMetadataToPdf(List loaded, embeddedBibFilePdfExporter, abbreviationRepository, writeXMP, - embeddBibfile); + embedBibfile); } private static void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext, @@ -197,7 +197,7 @@ private static void writeMetadataToPdfByCitekey(BibDatabaseContext databaseConte EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter, JournalAbbreviationRepository abbreviationRepository, boolean writeXMP, - boolean embeddBibfile) { + boolean embedBibfile) { for (String citeKey : citeKeys) { List bibEntryList = databaseContext.getDatabase().getEntriesByCitationKey(citeKey); if (bibEntryList.isEmpty()) { @@ -214,7 +214,7 @@ private static void writeMetadataToPdfByCitekey(BibDatabaseContext databaseConte embeddedBibFilePdfExporter, abbreviationRepository, writeXMP, - embeddBibfile); + embedBibfile); } } } @@ -226,7 +226,7 @@ private static void writeMetadataToPdfByFileNames(BibDatabaseContext databaseCon EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter, JournalAbbreviationRepository abbreviationRepository, boolean writeXMP, - boolean embeddBibfile) { + boolean embedBibfile) { for (Path filePath : pdfs) { if (!filePath.isAbsolute()) { filePath = FileUtil.find(filePath.toString(), databaseContext.getFileDirectories(filePreferences)).orElse( @@ -246,7 +246,7 @@ private static void writeMetadataToPdfByFileNames(BibDatabaseContext databaseCon System.out.println(Localization.lang("File %0 is not linked to any entry in library.", filePath)); } } - if (embeddBibfile) { + if (embedBibfile) { if (embeddedBibFilePdfExporter.exportToFileByPath(databaseContext, filePreferences, filePath, abbreviationRepository)) { System.out.println(Localization.lang("Successfully embedded XMP metadata of at least one entry to %0.", filePath)); } else { diff --git a/jablib/build.gradle.kts b/jablib/build.gradle.kts index 029330c2ab96..fc28928f06f9 100644 --- a/jablib/build.gradle.kts +++ b/jablib/build.gradle.kts @@ -148,7 +148,7 @@ var taskGenerateJournalListMV = tasks.register("generateJournalListMV var taskGenerateCitationStyleCatalog = tasks.register("generateCitationStyleCatalog") { group = "JabRef" description = "Generates a catalog of all available citation styles" - // The JBang gradle plugin doesn't handle parallization well - thus we enforce sequential execution + // The JBang gradle plugin doesn't handle parallelization well - thus we enforce sequential execution mustRunAfter(taskGenerateJournalListMV) val generatorScript = rootProject.layout.projectDirectory.file("build-support/src/main/java/CitationStyleCatalogGenerator.java") script = '"' + generatorScript.asFile.absolutePath + '"' @@ -163,7 +163,7 @@ var taskGenerateLtwaListMV = tasks.register("generateLtwaListMV") { group = "JabRef" description = "Converts the LTWA CSV file to a H2 MVStore" dependsOn(tasks.named("generateGrammarSource")) - // The JBang gradle plugin doesn't handle parallization well - thus we enforce sequential execution + // The JBang gradle plugin doesn't handle parallelization well - thus we enforce sequential execution mustRunAfter(taskGenerateCitationStyleCatalog) script = '"' + rootProject.layout.projectDirectory.file("build-support/src/main/java/LtwaListMvGenerator.java").asFile.absolutePath + '"' diff --git a/jablib/src/main/java/org/jabref/logic/ai/chatting/util/ChatHistoryUtils.java b/jablib/src/main/java/org/jabref/logic/ai/chatting/util/ChatHistoryUtils.java index f06467b7b597..6a08eb357ff3 100644 --- a/jablib/src/main/java/org/jabref/logic/ai/chatting/util/ChatHistoryUtils.java +++ b/jablib/src/main/java/org/jabref/logic/ai/chatting/util/ChatHistoryUtils.java @@ -74,7 +74,7 @@ public static void updateSystemMessage(List chatHistory, String new /// Finds the last user message in the chat history by iterating backwards. /// /// @param chatHistory the chat history to search - /// @return the last user messag, or empty if no user message is found + /// @return the last user message, or empty if no user message is found public static Optional getLastUserMessage(List chatHistory) { for (int i = chatHistory.size() - 1; i >= 0; i--) { if (chatHistory.get(i).role() == ChatMessage.Role.USER) { diff --git a/jablib/src/main/java/org/jabref/logic/ai/ingestion/logic/parsing/PdfContentParser.java b/jablib/src/main/java/org/jabref/logic/ai/ingestion/logic/parsing/PdfContentParser.java index 3135bd969dbc..fc4fbcdecdcd 100644 --- a/jablib/src/main/java/org/jabref/logic/ai/ingestion/logic/parsing/PdfContentParser.java +++ b/jablib/src/main/java/org/jabref/logic/ai/ingestion/logic/parsing/PdfContentParser.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.List; -import org.jabref.logic.pdf.InterruptablePDFTextStripper; +import org.jabref.logic.pdf.InterruptiblePDFTextStripper; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -25,7 +25,7 @@ public List parse(Path path) { int numberOfPages = document.getNumberOfPages(); List pages = new ArrayList<>(numberOfPages); - InterruptablePDFTextStripper stripper = new InterruptablePDFTextStripper(); + InterruptiblePDFTextStripper stripper = new InterruptiblePDFTextStripper(); for (int page = 1; page <= numberOfPages; page++) { if (Thread.currentThread().isInterrupted()) { return List.of(); diff --git a/jablib/src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java b/jablib/src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java index 6ab61c9fc83a..4668a8c3d3fc 100644 --- a/jablib/src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java +++ b/jablib/src/main/java/org/jabref/logic/auxparser/AuxParserStatisticsProvider.java @@ -14,7 +14,7 @@ public AuxParserStatisticsProvider(AuxParserResult auxParserResult) { /// Prints parsing statistics /// - /// @param includeMissingEntries shows the missing entries as text (the GUI renderes them at another place) + /// @param includeMissingEntries shows the missing entries as text (the GUI renders them at another place) public String getInformation(boolean includeMissingEntries) { String missingEntries = ""; if (includeMissingEntries && (this.auxParserResult.getUnresolvedKeysCount() > 0)) { diff --git a/jablib/src/main/java/org/jabref/logic/citation/repository/BibEntrySerializer.java b/jablib/src/main/java/org/jabref/logic/citation/repository/BibEntrySerializer.java index 934712190b0c..d1f25bf09eb6 100644 --- a/jablib/src/main/java/org/jabref/logic/citation/repository/BibEntrySerializer.java +++ b/jablib/src/main/java/org/jabref/logic/citation/repository/BibEntrySerializer.java @@ -41,7 +41,7 @@ public BibEntrySerializer(BibEntryTypesManager entryTypesManager, ImportFormatPr private String toString(BibEntry entry) { // BibEntry is not Java serializable. Thus, we need to do the serialization manually // At reading of the clipboard in JabRef, we parse the plain string in all cases, so we don't need to flag we put BibEntries here - // Furthermore, storing a string also enables other applications to work wih the data + // Furthermore, storing a string also enables other applications to work with the data BibEntryWriter writer = new BibEntryWriter(new FieldWriter(fieldPreferences), entryTypesManager); try { return writer.write(List.of(entry), BibDatabaseMode.BIBTEX); diff --git a/jablib/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java b/jablib/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java index 89a12975c639..3f9f8f9dbade 100644 --- a/jablib/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java +++ b/jablib/src/main/java/org/jabref/logic/citationkeypattern/BracketedPattern.java @@ -716,7 +716,7 @@ private static String keepLettersAndDigitsOnly(String in) { /// Gets the last name of the first author/editor /// /// @param authorList an [AuthorList] - /// @return the surname of an author/editor or the von part if no lastname is prsent or "" if no author was found or both firstname+lastname are empty + /// @return the surname of an author/editor or the von part if no lastname is present or "" if no author was found or both firstname+lastname are empty /// This method is guaranteed to never return null. private static String firstAuthor(AuthorList authorList) { return authorList.getAuthors().stream() @@ -836,7 +836,7 @@ public static String authorsAlpha(AuthorList authorList) { } /// Returns the authors according to the BibTeX LNI template - /// Examples: Examples from the tmplate + /// Examples: Examples from the template /// Also see discussion at the pull request that introduced this. /// /// @param authorList an [AuthorList] diff --git a/jablib/src/main/java/org/jabref/logic/crawler/Crawler.java b/jablib/src/main/java/org/jabref/logic/crawler/Crawler.java index cc7d6f6a0bd2..0c5221ca940e 100644 --- a/jablib/src/main/java/org/jabref/logic/crawler/Crawler.java +++ b/jablib/src/main/java/org/jabref/logic/crawler/Crawler.java @@ -15,7 +15,7 @@ import org.eclipse.jgit.api.errors.GitAPIException; -/// This class provides a service for SLR support by conducting an automated search and persistance +/// This class provides a service for SLR support by conducting an automated search and persistence /// of studies using the queries and E-Libraries specified in the provided study definition file. /// /// It composes a StudyRepository for repository management, diff --git a/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/EscapeAmpersandsFormatter.java b/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/EscapeAmpersandsFormatter.java index 11f41d2bb56b..636d35d5a6cd 100644 --- a/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/EscapeAmpersandsFormatter.java +++ b/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/EscapeAmpersandsFormatter.java @@ -50,7 +50,7 @@ public String format(@NonNull String value) { inCommandName = false; inCommand = true; } else { - // Or simply the end of this command alltogether: + // Or simply the end of this command altogether: commandName.delete(0, commandName.length()); inCommandName = false; } diff --git a/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java b/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java index df9ec92a77fb..4a73f0fc80a6 100644 --- a/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java +++ b/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/NormalizeWhitespaceFormatter.java @@ -8,7 +8,7 @@ import org.jspecify.annotations.NonNull; -/// Replaces two subsequent whitespaces (and tabs) to one space in case of single-line fields. In case of multine fields, +/// Replaces two subsequent whitespaces (and tabs) to one space in case of single-line fields. In case of multiline fields, /// the field content is kept as is. /// /// Due to the distinction between single line and multiline fields, this formatter does not implement the interface [org.jabref.logic.formatter.Formatter]. diff --git a/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormater.java b/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormatter.java similarity index 93% rename from jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormater.java rename to jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormatter.java index 676176438719..65ad3c9812c1 100644 --- a/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormater.java +++ b/jablib/src/main/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormatter.java @@ -8,7 +8,7 @@ import org.jspecify.annotations.NonNull; /// Replaces any tab with a space -public class ReplaceTabsBySpaceFormater extends Formatter { +public class ReplaceTabsBySpaceFormatter extends Formatter { private static final Pattern TAB = Pattern.compile("\t+"); diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/ArXivFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/ArXivFetcher.java index 15ef4e59d4f4..0e0715baa9c9 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/ArXivFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/ArXivFetcher.java @@ -455,7 +455,7 @@ private static String normalizeSearchQuery(String searchQuery) { /// @see ArXiv API User's Manual for a detailed /// description on how to use the API /// - /// Similar implementions: + /// Similar implementations: /// arxiv2bib which is live /// dspace-portalmec protected static class ArXiv implements FulltextFetcher, PagedSearchBasedFetcher, IdBasedFetcher, IdFetcher { diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/CollectionOfComputerScienceBibliographiesFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/CollectionOfComputerScienceBibliographiesFetcher.java index 43268355c819..289472b1422a 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/CollectionOfComputerScienceBibliographiesFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/CollectionOfComputerScienceBibliographiesFetcher.java @@ -10,7 +10,7 @@ import org.jabref.logic.formatter.bibtexfields.RemoveDigitsFormatter; import org.jabref.logic.formatter.bibtexfields.RemoveNewlinesFormatter; import org.jabref.logic.formatter.bibtexfields.RemoveRedundantSpacesFormatter; -import org.jabref.logic.formatter.bibtexfields.ReplaceTabsBySpaceFormater; +import org.jabref.logic.formatter.bibtexfields.ReplaceTabsBySpaceFormatter; import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.Parser; @@ -62,7 +62,7 @@ public Optional getHelpPage() { @Override public void doPostCleanup(BibEntry entry) { new FieldFormatterCleanup(StandardField.ABSTRACT, new RemoveNewlinesFormatter()).cleanup(entry); - new FieldFormatterCleanup(StandardField.ABSTRACT, new ReplaceTabsBySpaceFormater()).cleanup(entry); + new FieldFormatterCleanup(StandardField.ABSTRACT, new ReplaceTabsBySpaceFormatter()).cleanup(entry); new FieldFormatterCleanup(StandardField.ABSTRACT, new RemoveRedundantSpacesFormatter()).cleanup(entry); new FieldFormatterCleanup(StandardField.EDITOR, new RemoveDigitsFormatter()).cleanup(entry); // identifier fields is a key-value field diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/GoogleScholar.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/GoogleScholar.java index fe3dd26783f9..0caf262ab720 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/GoogleScholar.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/GoogleScholar.java @@ -130,7 +130,7 @@ private void addHitsFromQuery(List entryList, String queryURL) throws if (needsCaptcha(content)) { // TODO: Remove "null" - throw new FetcherException(queryURL, "Fetching from Google Scholar failed: Captacha hit." + + throw new FetcherException(queryURL, "Fetching from Google Scholar failed: Captcha hit." + Localization.lang("This might be caused by reaching the traffic limitation of Google Scholar (see 'Help' for details)."), null); } @@ -204,7 +204,7 @@ public Page performSearchPaged(BaseQueryNode queryNode, int pageNumber } } catch (IOException e) { LOGGER.info("IOException for URL {}", uriBuilder); - // if there are too much requests from the same IP adress google is answering with a 503 and redirecting to a captcha challenge + // if there are too much requests from the same IP address google is answering with a 503 and redirecting to a captcha challenge // The caught IOException looks for example like this: // java.io.IOException: Server returned HTTP response code: 503 for URL: https://ipv4.google.com/sorry/index?continue=https://scholar.google.com/scholar%3Fhl%3Den%26btnG%3DSearch%26q%3Dbpmn&hl=en&q=CGMSBI0NBDkYuqy9wAUiGQDxp4NLQCWbIEY1HjpH5zFJhv4ANPGdWj0 if (e.getMessage().contains("Server returned HTTP response code: 503 for URL")) { diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/ACMPortalParser.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/ACMPortalParser.java index eb93fa532e33..3389ccdd64f4 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/ACMPortalParser.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/ACMPortalParser.java @@ -124,8 +124,8 @@ private StandardEntryType typeStrToEnum(String typeStr) { if ("PAPER_CONFERENCE".equals(typeStr)) { type = StandardEntryType.Conference; } else { - String upperUnderscoreTyeStr = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, typeStr); - type = Enums.getIfPresent(StandardEntryType.class, upperUnderscoreTyeStr).or(StandardEntryType.Article); + String upperUnderscoreTypeStr = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, typeStr); + type = Enums.getIfPresent(StandardEntryType.class, upperUnderscoreTypeStr).or(StandardEntryType.Article); } return type; } diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/EndnoteXmlImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/EndnoteXmlImporter.java index e1a5c9df3c37..7eb4f0d38cdd 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/EndnoteXmlImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/EndnoteXmlImporter.java @@ -99,7 +99,7 @@ public EndnoteXmlImporter(ImportFormatPreferences preferences) { xmlInputFactory = XMLInputFactory.newInstance(); // prevent xxe (https://rules.sonarsource.com/java/RSPEC-2755) - // not suported by aalto-xml + // not supported by aalto-xml // xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); // required for reading Unicode characters such as ö xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, true); @@ -193,7 +193,7 @@ private BibEntry parseRecord(XMLStreamReader reader) throws XMLStreamException { parseUrls(reader, entry); case "dates" -> parseDates(reader, entry); - // TODO: Left for future work -- test files need to be adpated + // TODO: Left for future work -- test files need to be adapted // case "accession-num" -> { // String accessionNumber = parseElementContent(reader, "accession-num"); // entry.setField(new UnknownField("accession-num"), accessionNumber); diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java index 4c408950be40..a3aab8c96e53 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java @@ -76,7 +76,7 @@ public boolean isRecognizedFormat(@NonNull Reader reader) throws IOException { while (((str = bufferedReader.readLine()) != null) && (i < 50)) { /* * The following line gives false positives for RIS files, so it - * should not be uncommented. The hypen is a characteristic of the + * should not be uncommented. The hyphen is a characteristic of the * RIS format. * * str = str.replace(" - ", "") diff --git a/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/PdfContentImporter.java b/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/PdfContentImporter.java index 1e05bf20dd0b..82f1dd46bc2e 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/PdfContentImporter.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fileformat/pdf/PdfContentImporter.java @@ -527,7 +527,7 @@ Optional getEntryFromPDFContent(String firstpageContents, String lineS int edslength = "(Eds.)".length(); int posWithEditor = pos + edslength + 2; // +2 because of ":" after (Eds.) and the subsequent space if (posWithEditor > curString.length()) { - curString = curString.substring(posWithEditor - 2); // we don't have any spaces after Eds so we substract the 2 + curString = curString.substring(posWithEditor - 2); // we don't have any spaces after Eds so we subtract the 2 } else { curString = curString.substring(posWithEditor); } diff --git a/jablib/src/main/java/org/jabref/logic/importer/util/MediaTypes.java b/jablib/src/main/java/org/jabref/logic/importer/util/MediaTypes.java index 2e3421e9e76d..8bf51585bf9c 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/util/MediaTypes.java +++ b/jablib/src/main/java/org/jabref/logic/importer/util/MediaTypes.java @@ -1,6 +1,6 @@ package org.jabref.logic.importer.util; -/// Stores MediaTypes required by JabRef (which are not availble in used libraries) +/// Stores MediaTypes required by JabRef (which are not available in used libraries) public class MediaTypes { public static final String APPLICATION_BIBTEX = "application/x-bibtex"; public static final String APPLICATION_JABREF_DATA = "application/x-jabref"; diff --git a/jablib/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java b/jablib/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java index b58d6c5bd68e..fac8db40dfc1 100644 --- a/jablib/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java +++ b/jablib/src/main/java/org/jabref/logic/integrity/LatexIntegrityChecker.java @@ -33,7 +33,7 @@ /// Similar check to [HTMLCharacterChecker]. /// Here, we use SnuggleTeX, in the [HTMLCharacterChecker], it is searched for HTML characters. /// -/// Unescaped ampersands cannot be checked by SnuggleTeX, therefore the [AmpersandChecker] is available additionaly. +/// Unescaped ampersands cannot be checked by SnuggleTeX, therefore the [AmpersandChecker] is available additionally. public class LatexIntegrityChecker implements EntryChecker { private static final Logger LOGGER = LoggerFactory.getLogger(LatexIntegrityChecker.class); diff --git a/jablib/src/main/java/org/jabref/logic/integrity/TitleChecker.java b/jablib/src/main/java/org/jabref/logic/integrity/TitleChecker.java index 5ee307d646ef..cb8cddd6d2d5 100644 --- a/jablib/src/main/java/org/jabref/logic/integrity/TitleChecker.java +++ b/jablib/src/main/java/org/jabref/logic/integrity/TitleChecker.java @@ -12,7 +12,7 @@ public class TitleChecker implements ValueChecker { - private static final Pattern INSIDE_CURLY_BRAKETS = Pattern.compile("\\{[^}\\{]*\\}"); + private static final Pattern INSIDE_CURLY_BRACKETS = Pattern.compile("\\{[^}\\{]*\\}"); private static final Pattern DELIMITERS = Pattern.compile("\\.|\\!|\\?|\\;|\\:|\\["); private static final Predicate HAS_CAPITAL_LETTERS = Pattern.compile("[\\p{Lu}\\p{Lt}]").asPredicate(); @@ -41,7 +41,7 @@ public Optional checkValue(@Nullable String value) { return Optional.empty(); } - String valueOnlySpacesWithinCurlyBraces = INSIDE_CURLY_BRAKETS.matcher(value).replaceAll(""); + String valueOnlySpacesWithinCurlyBraces = INSIDE_CURLY_BRACKETS.matcher(value).replaceAll(""); String[] splitTitle = DELIMITERS.split(valueOnlySpacesWithinCurlyBraces); for (String subTitle : splitTitle) { diff --git a/jablib/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java b/jablib/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java index 9cca1c75bebc..9873ecd59563 100644 --- a/jablib/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java +++ b/jablib/src/main/java/org/jabref/logic/journals/JournalAbbreviationRepository.java @@ -40,15 +40,15 @@ public JournalAbbreviationRepository(Path journalList, LtwaRepository ltwaReposi try (MVStore store = new MVStore.Builder().readOnly().fileName(journalList.toAbsolutePath().toString()).open()) { mvFullToAbbreviationObject = store.openMap("FullToAbbreviation"); mvFullToAbbreviationObject.forEach((name, abbreviation) -> { - String abbrevationString = abbreviation.getAbbreviation(); + String abbreviationString = abbreviation.getAbbreviation(); String shortestUniqueAbbreviation = abbreviation.getShortestUniqueAbbreviation(); Abbreviation newAbbreviation = new Abbreviation( name, - abbrevationString, + abbreviationString, shortestUniqueAbbreviation ); fullToAbbreviationObject.put(name, newAbbreviation); - abbreviationToAbbreviationObject.put(abbrevationString, newAbbreviation); + abbreviationToAbbreviationObject.put(abbreviationString, newAbbreviation); dotlessToAbbreviationObject.put(newAbbreviation.getDotlessAbbreviation(), newAbbreviation); shortestUniqueToAbbreviationObject.put(shortestUniqueAbbreviation, newAbbreviation); }); diff --git a/jablib/src/main/java/org/jabref/logic/l10n/Localization.java b/jablib/src/main/java/org/jabref/logic/l10n/Localization.java index fb19d54b4eec..cd2c60b5ba25 100644 --- a/jablib/src/main/java/org/jabref/logic/l10n/Localization.java +++ b/jablib/src/main/java/org/jabref/logic/l10n/Localization.java @@ -32,7 +32,7 @@ /// /// The access to this is given by the functions [#lang(String, Object...)] and /// that developers should use whenever they use strings for the e.g. GUI that need to be translatable. -@AllowedToUseStandardStreams("Needs to have acess to System.err because it's called very early before our loggers") +@AllowedToUseStandardStreams("Needs to have access to System.err because it's called very early before our loggers") public class Localization { static final String RESOURCE_PREFIX = "l10n/JabRef"; private static Locale locale; diff --git a/jablib/src/main/java/org/jabref/logic/layout/LayoutHelper.java b/jablib/src/main/java/org/jabref/logic/layout/LayoutHelper.java index 9f78607c02fe..2e795f1c60ce 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/LayoutHelper.java +++ b/jablib/src/main/java/org/jabref/logic/layout/LayoutHelper.java @@ -189,7 +189,7 @@ private void doBracketedOptionField() throws IOException { if (start) { // changed section begin - arudert - // keep the backslash so we know wether this is a fieldname or an ordinary parameter + // keep the backslash so we know whether this is a fieldname or an ordinary parameter // if (c != '\\') { buffer.append((char) c); // } diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstAbbrLastOxfordCommas.java b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstAbbrLastOxfordCommas.java index 3d85ecb6ae89..038e427d3655 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstAbbrLastOxfordCommas.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstAbbrLastOxfordCommas.java @@ -7,7 +7,7 @@ /// - Names are given as first name, von and last name. /// - First names will be abbreviated. /// - Individual authors separated by comma. -/// - The and of a list of three or more authors is preceeded by a comma +/// - The and of a list of three or more authors is preceded by a comma /// (Oxford comma) /// public class AuthorFirstAbbrLastOxfordCommas implements LayoutFormatter { diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstLastOxfordCommas.java b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstLastOxfordCommas.java index 125fa84e778c..f56f2737253b 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstLastOxfordCommas.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorFirstLastOxfordCommas.java @@ -6,7 +6,7 @@ /// /// - Names are given as first name, von and last name. /// - Individual authors separated by comma. -/// - The and of a list of three or more authors is preceeded by a comma +/// - The and of a list of three or more authors is preceded by a comma /// (Oxford comma) /// public class AuthorFirstLastOxfordCommas implements LayoutFormatter { diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbrOxfordCommas.java b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbrOxfordCommas.java index 02fba846416c..7ec831e528ea 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbrOxfordCommas.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbrOxfordCommas.java @@ -7,7 +7,7 @@ /// - Names are given in order: von last, jr, first. /// - First names will be abbreviated. /// - Individual authors are separated by commas. -/// - The 'and' of a list of three or more authors is preceeded by a comma +/// - The 'and' of a list of three or more authors is preceded by a comma /// (Oxford comma) /// public class AuthorLastFirstAbbrOxfordCommas implements LayoutFormatter { diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbreviator.java b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbreviator.java index dc0801f5cac8..b5130f96f78a 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbreviator.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/AuthorLastFirstAbbreviator.java @@ -4,7 +4,7 @@ /// Uses as input the fields (author or editor) in the LastFirst format. /// -/// This formater enables to abbreviate the authors name in the following way: +/// This formatter enables to abbreviate the authors name in the following way: /// /// Ex: Someone, Van Something will be abbreviated as Someone, V.S. public class AuthorLastFirstAbbreviator implements LayoutFormatter { diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/GetOpenOfficeType.java b/jablib/src/main/java/org/jabref/logic/layout/format/GetOpenOfficeType.java index 3156fdd51274..bf3c8faab4fb 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/GetOpenOfficeType.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/GetOpenOfficeType.java @@ -49,7 +49,7 @@ public String format(String fieldText) { if ("Unpublished".equalsIgnoreCase(fieldText)) { return "14"; } - // Default, Miscelaneous + // Default, Miscellaneous return "10"; } } diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/Replace.java b/jablib/src/main/java/org/jabref/logic/layout/format/Replace.java index 44a9b1d57f84..f12d075a5d5b 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/Replace.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/Replace.java @@ -9,7 +9,7 @@ /// separated by a comma. To indicate the comma character, use an escape /// sequence: \, /// For inserting newlines and tabs in arguments, use \n and \t, respectively. -/// The first part is the regular expression to search for. Remember that any commma +/// The first part is the regular expression to search for. Remember that any comma /// character must be preceded by a backslash, and consequently a literal backslash must /// be written as a pair of backslashes. A description of Java regular expressions can be /// found at: @@ -18,7 +18,7 @@ /// For instance: /// /// `\format[Replace(and,&)]{\author}` -/// will output the "author" field after replacing all occurences of "and" +/// will output the "author" field after replacing all occurrences of "and" /// by "&" /// /// `\format[Replace(\s,_)]{\author}` diff --git a/jablib/src/main/java/org/jabref/logic/layout/format/XMLChars.java b/jablib/src/main/java/org/jabref/logic/layout/format/XMLChars.java index 40ff6fd0fa0d..ba630e70e9c9 100644 --- a/jablib/src/main/java/org/jabref/logic/layout/format/XMLChars.java +++ b/jablib/src/main/java/org/jabref/logic/layout/format/XMLChars.java @@ -52,7 +52,7 @@ private static String firstFormat(String s) { private String restFormat(String toFormat) { String fieldText = toFormat.replace("}", "").replace("{", ""); - // now some copy-paste problems most often occuring in abstracts when + // now some copy-paste problems most often occurring in abstracts when // copied from PDF // AND: this is accepted in the abstract of bibtex files, so are forced // to catch those cases diff --git a/jablib/src/main/java/org/jabref/logic/msbib/MSBibConverter.java b/jablib/src/main/java/org/jabref/logic/msbib/MSBibConverter.java index 41f030e9be92..363f9ee5b97b 100644 --- a/jablib/src/main/java/org/jabref/logic/msbib/MSBibConverter.java +++ b/jablib/src/main/java/org/jabref/logic/msbib/MSBibConverter.java @@ -43,9 +43,9 @@ public static MSBibEntry convert(BibEntry entry) { // Duplicate: also added as BookTitle entry.getFieldLatexFree(StandardField.BOOKTITLE).ifPresent(booktitle -> result.conferenceName = booktitle); entry.getFieldLatexFree(StandardField.PAGES).ifPresent(pages -> result.pages = new PageNumbers(pages)); - entry.getFieldLatexFree(new UnknownField(MSBIB_PREFIX + "accessed")).ifPresent(accesed -> result.dateAccessed = accesed); + entry.getFieldLatexFree(new UnknownField(MSBIB_PREFIX + "accessed")).ifPresent(accessed -> result.dateAccessed = accessed); - entry.getFieldLatexFree(StandardField.URLDATE).ifPresent(acessed -> result.dateAccessed = acessed); + entry.getFieldLatexFree(StandardField.URLDATE).ifPresent(accessed -> result.dateAccessed = accessed); // TODO: currently this can never happen if ("SoundRecording".equals(msBibType)) { diff --git a/jablib/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java b/jablib/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java index 58ed62bfdf97..482811d873a2 100644 --- a/jablib/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java +++ b/jablib/src/main/java/org/jabref/logic/msbib/MSBibDatabase.java @@ -24,7 +24,7 @@ import org.xml.sax.SAXException; /// Microsoft Word bibliography. -/// The class is uesed both for import and export +/// The class is used both for import and export /// See http://www.ecma-international.org/publications/standards/Ecma-376.htm public class MSBibDatabase { diff --git a/jablib/src/main/java/org/jabref/logic/msbib/MSBibEntry.java b/jablib/src/main/java/org/jabref/logic/msbib/MSBibEntry.java index c0621a1c483f..8a2ed876a142 100644 --- a/jablib/src/main/java/org/jabref/logic/msbib/MSBibEntry.java +++ b/jablib/src/main/java/org/jabref/logic/msbib/MSBibEntry.java @@ -156,11 +156,11 @@ private void populateFromXml(Element entry) { String dayAccessed = getXmlElementTextContent("DayAccessed", entry); String yearAccessed = getXmlElementTextContent("YearAccessed", entry); - Optional parsedDateAcessed = Date.parse(Optional.ofNullable(yearAccessed), + Optional parsedDateAccessed = Date.parse(Optional.ofNullable(yearAccessed), Optional.ofNullable(monthAccessed), Optional.ofNullable(dayAccessed)); - parsedDateAcessed.map(Date::getNormalized).ifPresent(date -> dateAccessed = date); + parsedDateAccessed.map(Date::getNormalized).ifPresent(date -> dateAccessed = date); NodeList nodeLst = entry.getElementsByTagNameNS("*", "Author"); if (nodeLst.getLength() > 0) { @@ -233,7 +233,7 @@ private List getSpecificAuthors(String type, Element authors) { /// Gets the dom representation for one entry, used for export /// /// @param document XmlDocument - /// @return XmlElement represenation of one entry + /// @return XmlElement representation of one entry public Element getEntryDom(Document document) { Element rootNode = document.createElementNS(MSBibDatabase.NAMESPACE, MSBibDatabase.PREFIX + "Source"); @@ -241,7 +241,7 @@ public Element getEntryDom(Document document) { addField(document, rootNode, entry.getKey(), entry.getValue()); } - Optional.ofNullable(dateAccessed).ifPresent(_ -> addDateAcessedFields(document, rootNode)); + Optional.ofNullable(dateAccessed).ifPresent(_ -> addDateAccessedFields(document, rootNode)); Element allAuthors = document.createElementNS(MSBibDatabase.NAMESPACE, MSBibDatabase.PREFIX + "Author"); @@ -329,7 +329,7 @@ private void addAuthor(Document document, Element allAuthors, String entryName, allAuthors.appendChild(authorTop); } - private void addDateAcessedFields(Document document, Element rootNode) { + private void addDateAccessedFields(Document document, Element rootNode) { Optional parsedDateAcesseField = Date.parse(dateAccessed); parsedDateAcesseField.flatMap(Date::getYear) .map(Object::toString) @@ -337,7 +337,7 @@ private void addDateAcessedFields(Document document, Element rootNode) { parsedDateAcesseField.flatMap(Date::getMonth) .map(Month::getFullName) - .ifPresent(monthAcessed -> addField(document, rootNode, "Month" + "Accessed", monthAcessed)); + .ifPresent(monthAccessed -> addField(document, rootNode, "Month" + "Accessed", monthAccessed)); parsedDateAcesseField.flatMap(Date::getDay) .map(Object::toString) .ifPresent(dayAccessed -> addField(document, rootNode, "Day" + "Accessed", dayAccessed)); diff --git a/jablib/src/main/java/org/jabref/logic/net/URLDownload.java b/jablib/src/main/java/org/jabref/logic/net/URLDownload.java index b3527562a503..754ee388a07c 100644 --- a/jablib/src/main/java/org/jabref/logic/net/URLDownload.java +++ b/jablib/src/main/java/org/jabref/logic/net/URLDownload.java @@ -67,7 +67,7 @@ /// /// Nothing is cached. /// -/// Implentation note: This relies on setting `followRedirects` to `true` +/// Implementation note: This relies on setting `followRedirects` to `true` /// and enabling cookie management. public class URLDownload { diff --git a/jablib/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java b/jablib/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java index 3b746f82b697..916ab98448b2 100644 --- a/jablib/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java +++ b/jablib/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java @@ -391,7 +391,7 @@ public static void checkFillCursor(XTextCursor cursor) { } } - /// Remove brackets, but if the result would become empty, leave them; if the result would be a single characer, leave the left bracket. + /// Remove brackets, but if the result would become empty, leave them; if the result would be a single character, leave the left bracket. /// /// See: cleanFillCursorForCitationGroup @Override diff --git a/jablib/src/main/java/org/jabref/logic/openoffice/oocsltext/CitationOOAdapterUtils.java b/jablib/src/main/java/org/jabref/logic/openoffice/oocsltext/CitationOOAdapterUtils.java index 29826071cbe2..b81d56790d84 100644 --- a/jablib/src/main/java/org/jabref/logic/openoffice/oocsltext/CitationOOAdapterUtils.java +++ b/jablib/src/main/java/org/jabref/logic/openoffice/oocsltext/CitationOOAdapterUtils.java @@ -11,7 +11,7 @@ private CitationOOAdapterUtils() { } static boolean hasPrecedingSpace(XTextCursor cursor) { - boolean procedingSpaceExists; + boolean precedingSpaceExists; XTextCursor checkCursor = cursor.getText().createTextCursorByRange(cursor.getStart()); // Check if we're at the start of the document - if yes we set the flag and don't insert a space @@ -20,14 +20,14 @@ static boolean hasPrecedingSpace(XTextCursor cursor) { return true; } else { // If not at the start of document, check if there is a space before - procedingSpaceExists = " ".equals(checkCursor.getString()); + precedingSpaceExists = " ".equals(checkCursor.getString()); // If not a space, check if it's a paragraph break - if (!procedingSpaceExists) { - procedingSpaceExists = checkCursor.getString().matches(LINE_BREAK); + if (!precedingSpaceExists) { + precedingSpaceExists = checkCursor.getString().matches(LINE_BREAK); } } - return procedingSpaceExists; + return precedingSpaceExists; } static boolean hasSucceedingSpace(XTextCursor cursor) { diff --git a/jablib/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java b/jablib/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java index 34647a9ba088..2058e3564ba7 100644 --- a/jablib/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java +++ b/jablib/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java @@ -51,7 +51,7 @@ public static Comparator comparatorForMulticite(JStyle style) { /// according to style. public static void produceCitationMarkers(CitationGroups citationGroups, List databases, JStyle style) { if (!citationGroups.hasGlobalOrder()) { - throw new IllegalStateException("produceCitationMarkers: globalOrder is misssing in citationGroups"); + throw new IllegalStateException("produceCitationMarkers: globalOrder is missing in citationGroups"); } citationGroups.lookupCitations(databases); diff --git a/jablib/src/main/java/org/jabref/logic/pdf/InterruptablePDFTextStripper.java b/jablib/src/main/java/org/jabref/logic/pdf/InterruptiblePDFTextStripper.java similarity index 77% rename from jablib/src/main/java/org/jabref/logic/pdf/InterruptablePDFTextStripper.java rename to jablib/src/main/java/org/jabref/logic/pdf/InterruptiblePDFTextStripper.java index 4bb9f4c3da5b..768312361d11 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/InterruptablePDFTextStripper.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/InterruptiblePDFTextStripper.java @@ -5,8 +5,8 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.text.PDFTextStripper; -public class InterruptablePDFTextStripper extends PDFTextStripper { - public InterruptablePDFTextStripper() { +public class InterruptiblePDFTextStripper extends PDFTextStripper { + public InterruptiblePDFTextStripper() { super(); } diff --git a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java index aa33ebd47f3f..4b528d320e20 100644 --- a/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java +++ b/jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java @@ -257,7 +257,7 @@ public class JabRefCliPreferences implements CliPreferences { public static final String CLEANUP_FIELD_FORMATTERS_ENABLED = "CleanUpFormattersEnabled"; public static final String CLEANUP_FIELD_FORMATTERS = "CleanUpFormatters"; public static final String NAME_FORMATTER_VALUE = "nameFormatterFormats"; - public static final String NAME_FORMATER_KEY = "nameFormatterNames"; + public static final String NAME_FORMATTER_KEY = "nameFormatterNames"; /// The OpenOffice/LibreOffice connection preferences are: OO_PATH main directory for /// OO/LO installation, used to detect location on Win/macOS when using manual @@ -312,7 +312,7 @@ public class JabRefCliPreferences implements CliPreferences { private static final String FILES_FULLTEXT_INDEX = "fulltextIndexLinkedFiles"; private static final String FILES_WORKING_DIRECTORY = "workingDirectory"; - // FixMe: Missplaced + // FixMe: Misplaced private static final String BACKUP_ENABLED = "createBackup"; private static final String BACKUP_DIRECTORY = "backupDirectory"; @@ -2322,10 +2322,10 @@ public NameFormatterPreferences getNameFormatterPreferences() { NameFormatterPreferences defaultValues = NameFormatterPreferences.getDefault(); nameFormatterPreferences = new NameFormatterPreferences( - convertStringToList(get(NAME_FORMATER_KEY, convertListToString(defaultValues.getNameFormatterKey()))), + convertStringToList(get(NAME_FORMATTER_KEY, convertListToString(defaultValues.getNameFormatterKey()))), convertStringToList(get(NAME_FORMATTER_VALUE, convertListToString(defaultValues.getNameFormatterValue())))); - bindCustomList(nameFormatterPreferences.getNameFormatterKey(), NAME_FORMATER_KEY, defaultValues.getNameFormatterKey(), + bindCustomList(nameFormatterPreferences.getNameFormatterKey(), NAME_FORMATTER_KEY, defaultValues.getNameFormatterKey(), JabRefCliPreferences::convertListToString, JabRefCliPreferences::convertStringToList); bindCustomList(nameFormatterPreferences.getNameFormatterValue(), NAME_FORMATTER_VALUE, defaultValues.getNameFormatterValue(), JabRefCliPreferences::convertListToString, JabRefCliPreferences::convertStringToList); diff --git a/jablib/src/main/java/org/jabref/logic/shared/security/Password.java b/jablib/src/main/java/org/jabref/logic/shared/security/Password.java index 34b9188387fa..787f51f09482 100644 --- a/jablib/src/main/java/org/jabref/logic/shared/security/Password.java +++ b/jablib/src/main/java/org/jabref/logic/shared/security/Password.java @@ -13,7 +13,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -/// [Password] contains methods which are useful to encrypt and decrypt passwords using symetric algorithms. +/// [Password] contains methods which are useful to encrypt and decrypt passwords using symmetric algorithms. public class Password { private final byte[] phrase; diff --git a/jablib/src/main/java/org/jabref/logic/texparser/DefaultLatexParser.java b/jablib/src/main/java/org/jabref/logic/texparser/DefaultLatexParser.java index dc5e6e6826bd..6056ae32d486 100644 --- a/jablib/src/main/java/org/jabref/logic/texparser/DefaultLatexParser.java +++ b/jablib/src/main/java/org/jabref/logic/texparser/DefaultLatexParser.java @@ -87,7 +87,7 @@ public Optional parse(Path latexFile) { // We ignore this error and just continue with parsing LOGGER.info("Parsing has been interrupted"); } catch (IOException | UncheckedIOException e) { - // Some weired error during reading + // Some weird error during reading // We ignore this error and just continue with parsing LOGGER.info("Error while parsing file {}", latexFile, e); } diff --git a/jablib/src/main/java/org/jabref/logic/util/HeadlessExecutorService.java b/jablib/src/main/java/org/jabref/logic/util/HeadlessExecutorService.java index 80559e2ebbd7..b006e451d887 100644 --- a/jablib/src/main/java/org/jabref/logic/util/HeadlessExecutorService.java +++ b/jablib/src/main/java/org/jabref/logic/util/HeadlessExecutorService.java @@ -96,11 +96,11 @@ public List> executeAll(@NonNull Collection> tasks, in } } - public Future executeInterruptableTask(final Runnable runnable, String taskName) { + public Future executeInterruptibleTask(final Runnable runnable, String taskName) { return this.lowPriorityExecutorService.submit(new NamedRunnable(taskName, runnable)); } - public void executeInterruptableTaskAndWait(@NonNull Runnable runnable) { + public void executeInterruptibleTaskAndWait(@NonNull Runnable runnable) { Future future = lowPriorityExecutorService.submit(runnable); try { future.get(); diff --git a/jablib/src/main/java/org/jabref/logic/util/Version.java b/jablib/src/main/java/org/jabref/logic/util/Version.java index 65d79670f17f..cb6b88d0cf58 100644 --- a/jablib/src/main/java/org/jabref/logic/util/Version.java +++ b/jablib/src/main/java/org/jabref/logic/util/Version.java @@ -41,8 +41,8 @@ public class Version { private Version() { } - /// Tinylog does not allow for altering existing loging configuraitons after the logger was initialized . - /// Lazy initialization to enable tinylog writing to a file (and also still enabling loggin in this class) + /// Tinylog does not allow for altering existing logging configurations after the logger was initialized . + /// Lazy initialization to enable tinylog writing to a file (and also still enabling logging in this class) private static Logger getLogger() { return LoggerFactory.getLogger(Version.class); } diff --git a/jablib/src/main/java/org/jabref/logic/util/io/FileNameUniqueness.java b/jablib/src/main/java/org/jabref/logic/util/io/FileNameUniqueness.java index c34088efddbe..87454619f7d6 100644 --- a/jablib/src/main/java/org/jabref/logic/util/io/FileNameUniqueness.java +++ b/jablib/src/main/java/org/jabref/logic/util/io/FileNameUniqueness.java @@ -48,7 +48,7 @@ public static String generateUniqueFileName(Path targetDirectory, String propose } /// This function decide whether the newly downloaded file has the same content with other files - /// It returns ture when the content is duplicate, while returns false if it is not + /// It returns true when the content is duplicate, while returns false if it is not /// /// @param directory The directory which saves the files (.pdf, for example) /// @param fileName Suggest name for the newly downloaded file diff --git a/jablib/src/main/java/org/jabref/logic/util/strings/HTMLUnicodeConversionMaps.java b/jablib/src/main/java/org/jabref/logic/util/strings/HTMLUnicodeConversionMaps.java index 1a26c2a95ef0..bf19184ad79e 100644 --- a/jablib/src/main/java/org/jabref/logic/util/strings/HTMLUnicodeConversionMaps.java +++ b/jablib/src/main/java/org/jabref/logic/util/strings/HTMLUnicodeConversionMaps.java @@ -364,7 +364,7 @@ public class HTMLUnicodeConversionMaps { // U+220F ISOamsb /* prod is NOT the same character as U+03A0 'greek capital letter pi' though the same glyph might be used for both */ - {"8721", "sum", "$\\sum$"}, // n-ary sumation, U+2211 ISOamsb + {"8721", "sum", "$\\sum$"}, // n-ary summation, U+2211 ISOamsb /* sum is NOT the same character as U+03A3 'greek capital letter sigma' though the same glyph might be used for both */ {"8722", "minus", "$-$"}, // minus sign, U+2212 ISOtech diff --git a/jablib/src/main/java/org/jabref/model/entry/AuthorList.java b/jablib/src/main/java/org/jabref/model/entry/AuthorList.java index da13a982c332..e37110dd9e73 100644 --- a/jablib/src/main/java/org/jabref/model/entry/AuthorList.java +++ b/jablib/src/main/java/org/jabref/model/entry/AuthorList.java @@ -61,7 +61,7 @@ /// fixing this problem, however, requires processing of the preabmle; /// 2. 'author names' in 'author field' are subsequences of tokens separated by /// token `and` (`and` is case-insensitive); if 'author name' is an empty -/// sequence of tokens, it is ignored; for examle, both +/// sequence of tokens, it is ignored; for example, both /// `"John Smith and Peter Black"` and /// `"and and John Smith and and Peter Black"` consists of 2 'author name's /// `"Johm Smith"` and `"Peter Black"` (in erroneous situations, this is a bit @@ -79,7 +79,7 @@ /// token, 'von-part' consists of lower-case tokens starting the first lower-case /// token and ending the lower-case token that is followed by upper-case token, /// 'last-part' consists of the rest of tokens; note that both 'first-part' and -/// 'latst-part' may be empty and 'last-part' may contain lower-case tokens; for +/// 'last-part' may be empty and 'last-part' may contain lower-case tokens; for /// example: in `"von der"`, `'first-part'='last-part'=""`, /// `'von-part'="von der"`; in /// `"Charles Louis Xavier Joseph de la Vall{\'e}e la Poussin"`, @@ -309,7 +309,7 @@ public String getAsNatbib() { /// /// @param oxfordComma Whether to put a comma before the and at the end. /// @return formatted list of authors. - /// @see serial comma for an detailed explaination about the + /// @see serial comma for an detailed explanation about the /// Oxford comma. public String getAsLastNames(boolean oxfordComma) { return andCoordinatedConjunction(getAuthors(), Author::getNamePrefixAndFamilyName, oxfordComma); @@ -325,10 +325,10 @@ public String getAsLastNames(boolean oxfordComma) { /// Neumann, John, Smith, John and Black Brown, Peter" or "von Neumann, J., /// Smith, J. and Black Brown, P.". /// - /// @param abbreviate whether to abbreivate first names. + /// @param abbreviate whether to abbreviate first names. /// @param oxfordComma Whether to put a comma before the and at the end. /// @return formatted list of authors. - /// @see serial comma for an detailed explaination about the + /// @see serial comma for an detailed explanation about the /// Oxford comma. public String getAsLastFirstNames(boolean abbreviate, boolean oxfordComma) { return andCoordinatedConjunction(getAuthors(), auth -> auth.getFamilyGiven(abbreviate), oxfordComma); @@ -385,10 +385,10 @@ public String getAsLastFirstFirstLastNamesWithAnd(boolean abbreviate) { /// von Neumann, John Smith and Peter Black Brown" or "J. von Neumann, J. /// Smith and P. Black Brown" /// - /// @param abbreviate whether to abbreivate first names. + /// @param abbreviate whether to abbreviate first names. /// @param oxfordComma Whether to put a comma before the and at the end. /// @return formatted list of authors. - /// @see serial comma for an detailed explaination about the + /// @see serial comma for an detailed explanation about the /// Oxford comma. public String getAsFirstLastNames(boolean abbreviate, boolean oxfordComma) { return andCoordinatedConjunction(getAuthors(), author -> author.getGivenFamily(abbreviate), oxfordComma); diff --git a/jablib/src/main/java/org/jabref/model/entry/field/StandardField.java b/jablib/src/main/java/org/jabref/model/entry/field/StandardField.java index ddce4ac89a2c..4a18b8f214a1 100644 --- a/jablib/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/jablib/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -149,7 +149,7 @@ public enum StandardField implements Field { ICORERANKING("icore"), OWNER("owner"), - // Timestamp-realted + // Timestamp-related CREATIONDATE("creationdate", FieldProperty.DATE), MODIFICATIONDATE("modificationdate", FieldProperty.DATE), TIMESTAMP("timestamp", FieldProperty.DATE); diff --git a/jablib/src/main/java/org/jabref/model/openoffice/backend/NamedRange.java b/jablib/src/main/java/org/jabref/model/openoffice/backend/NamedRange.java index ca705c1041c1..b31ef23dbefa 100644 --- a/jablib/src/main/java/org/jabref/model/openoffice/backend/NamedRange.java +++ b/jablib/src/main/java/org/jabref/model/openoffice/backend/NamedRange.java @@ -12,7 +12,7 @@ /// NamedRange (with NamedRangeManager) attempts to provide a common interface for working with reference mark based and bookmark based text ranges to be used as locations to fill with citation markers. LibreOffice supports name-based lookup and listing names for both (hence the name). /// -/// Note: currently only implemented for refence marks (in NamedRangeReferenceMark and NamedRangeManagerReferenceMark). +/// Note: currently only implemented for reference marks (in NamedRangeReferenceMark and NamedRangeManagerReferenceMark). public interface NamedRange { String getRangeName(); diff --git a/jablib/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java b/jablib/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java index 7aac959f2d01..785a4934cfba 100644 --- a/jablib/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java +++ b/jablib/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java @@ -163,7 +163,7 @@ public static void write(@NonNull XTextDocument doc, String tagName = isStartTag ? startTagName : endTagName; Objects.requireNonNull(tagName); - // Attibutes parsed into (name,value) pairs. + // Attributes parsed into (name,value) pairs. List> attributes = parseAttributes(attributeListPart); // Handle tags: diff --git a/jablib/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java b/jablib/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java index 28148a1ec283..dd488d846aee 100644 --- a/jablib/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java +++ b/jablib/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java @@ -103,7 +103,7 @@ public List getCitationGroupsInGlobalOrder() { return OOListUtil.map(globalOrder.get(), citationGroupsUnordered::get); } - /// Impose an order of citation groups by providing the order of their citation group idendifiers. + /// Impose an order of citation groups by providing the order of their citation group identifiers. /// /// Also set indexInGlobalOrder for each citation group. public void setGlobalOrder(@NonNull List globalOrder) { diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index ec9796780da9..569f05af57f3 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -3424,7 +3424,7 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Type `title = covid OR title = pandemic` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=This query finds paper with title containing either COVID or pandemics in general. -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true. +`OR`\ can\ be\ satisfied\ with\ either\ of\ the\ conditions\ being\ true.=`OR`\ can\ be\ satisfied\ with\ either\ of\ the\ conditions\ being\ true. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Type\ `deep\ learning\ NOT\ title\ \=\ survey` diff --git a/jablib/src/main/resources/resource/layout/docbook5.layout b/jablib/src/main/resources/resource/layout/docbook5.layout index 07280c860adf..e9f67663682f 100644 --- a/jablib/src/main/resources/resource/layout/docbook5.layout +++ b/jablib/src/main/resources/resource/layout/docbook5.layout @@ -1,4 +1,4 @@ - + diff --git a/jablib/src/main/resources/resource/layout/openoffice/README b/jablib/src/main/resources/resource/layout/openoffice/README index 1396661475bc..4c323e83a870 100644 --- a/jablib/src/main/resources/resource/layout/openoffice/README +++ b/jablib/src/main/resources/resource/layout/openoffice/README @@ -1,6 +1,6 @@ A very simple export filter that outputs a comma-separated file that can be used by Openoffice (OO) as a bibliography database. -Uses two new custom formaters: +Uses two new custom formatters: - GetOpenofficeType: converts the default bibtextype entries into the numeric codes that diff --git a/jablib/src/main/resources/resource/layout/simplehtml.layout b/jablib/src/main/resources/resource/layout/simplehtml.layout index 23a2ea0f2f24..3ab64d3c421f 100644 --- a/jablib/src/main/resources/resource/layout/simplehtml.layout +++ b/jablib/src/main/resources/resource/layout/simplehtml.layout @@ -1,5 +1,5 @@
\format[ToLowerCase]{\entrytype} \begin{citationkey}(\citationkey)\end{citationkey}
-\begin{author}
\format[Authors(LastFirst,InititialsNoSpace),HTMLChars]{\author}
\end{author}\begin{title} +\begin{author}
\format[Authors(LastFirst,InitialsNoSpace),HTMLChars]{\author}
\end{author}\begin{title}
\format[HTMLChars]{\title}
\end{title}\begin{editor}
\format[AuthorLastFirst,AuthorLastFirstAbbreviator,AuthorAndsCommaReplacer,HTMLChars]{\editor} (ed.)
\end{editor}\begin{booktitle}
\format[HTMLChars]{\booktitle}
\end{booktitle}\begin{chapter} diff --git a/jablib/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java b/jablib/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java index 5c3d91d57999..61c5dc198163 100644 --- a/jablib/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java +++ b/jablib/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java @@ -398,7 +398,7 @@ static Stream cslMapping() { .withField(StandardField.PAGES, "45--67") .withField(StandardField.TITLE, "volume + issue + pages") .withField(StandardField.VOLUME, "1") - .withField(StandardField.COMMENT, "The issue field does not exist in Bibtex standard, therefore there is no need to render it (The issue field exists in biblatex standard though.). Since, for this entry, there is no number field present and therefore no data will be overwriten, enabling the user to be able to move the data within the issue field to the number field via cleanup action is something worth pursuing.") + .withField(StandardField.COMMENT, "The issue field does not exist in Bibtex standard, therefore there is no need to render it (The issue field exists in biblatex standard though.). Since, for this entry, there is no number field present and therefore no data will be overwritten, enabling the user to be able to move the data within the issue field to the number field via cleanup action is something worth pursuing.") .withField(StandardField.ISSUE, "9"), "apa.csl"), @@ -426,7 +426,7 @@ static Stream cslMapping() { "apa.csl"), // 11. - // The issue field does not exist in bibtex standard, therefore there is no need to render it (it exists in biblatex standard though). Since, for this entry, there is no number field present and therefore no data will be overwriten, enabling the user to be able to move the data within the issue field to the number field via cleanup action is something worth pursuing. + // The issue field does not exist in bibtex standard, therefore there is no need to render it (it exists in biblatex standard though). Since, for this entry, there is no number field present and therefore no data will be overwritten, enabling the user to be able to move the data within the issue field to the number field via cleanup action is something worth pursuing. Arguments.of( "Foo, B. (n.d.). issue + pages. Bib(La)TeX Journal, (9), 45–67.\n", BibDatabaseMode.BIBTEX, @@ -480,7 +480,7 @@ static Stream cslMapping() { "apa.csl"), // 15. - // "The issue field does not exist in bibtex standard, therefore there is no need to render it (it exists in biblatex standard though). Since, for this entry, there is no number field present and therefore no data will be overwriten, enabling the user to be able to move the data within the issue field to the number field via cleanup action is something worth pursuing." + // "The issue field does not exist in bibtex standard, therefore there is no need to render it (it exists in biblatex standard though). Since, for this entry, there is no number field present and therefore no data will be overwritten, enabling the user to be able to move the data within the issue field to the number field via cleanup action is something worth pursuing." Arguments.of( "Foo, B. (n.d.). issue. Bib(La)TeX Journal, (9issue).\n", BibDatabaseMode.BIBTEX, diff --git a/jablib/src/test/java/org/jabref/logic/cleanup/MoveFilesCleanupTest.java b/jablib/src/test/java/org/jabref/logic/cleanup/MoveFilesCleanupTest.java index ee666d17ec95..4c12260390c8 100644 --- a/jablib/src/test/java/org/jabref/logic/cleanup/MoveFilesCleanupTest.java +++ b/jablib/src/test/java/org/jabref/logic/cleanup/MoveFilesCleanupTest.java @@ -81,7 +81,7 @@ void movesFile() { } @Test - void movesFileWithMulitpleLinked() { + void movesFileWithMultipleLinked() { LinkedFile fileField = new LinkedFile("", fileBefore.toAbsolutePath(), ""); entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(Arrays.asList( new LinkedFile("", Path.of(""), ""), diff --git a/jablib/src/test/java/org/jabref/logic/exporter/ModsExportFormatFilesTest.java b/jablib/src/test/java/org/jabref/logic/exporter/ModsExportFormatFilesTest.java index 6c667464148f..ec1d1e0821b2 100644 --- a/jablib/src/test/java/org/jabref/logic/exporter/ModsExportFormatFilesTest.java +++ b/jablib/src/test/java/org/jabref/logic/exporter/ModsExportFormatFilesTest.java @@ -50,7 +50,7 @@ public class ModsExportFormatFilesTest { public static Stream fileNames() throws URISyntaxException, IOException { resourceDir = Path.of(ModsExportFormatFilesTest.class.getResource("ModsExportFormatTestAllFields.bib").toURI()).getParent(); - LOGGER.debug("Mods export resouce dir {}", resourceDir); + LOGGER.debug("Mods export resource dir {}", resourceDir); try (Stream stream = Files.list(resourceDir)) { return stream.map(n -> n.getFileName().toString()).filter(n -> n.endsWith(".bib")) diff --git a/jablib/src/test/java/org/jabref/logic/exporter/SimpleHtmlExportFormatTest.java b/jablib/src/test/java/org/jabref/logic/exporter/SimpleHtmlExportFormatTest.java new file mode 100644 index 000000000000..4d8d43cd6e48 --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/exporter/SimpleHtmlExportFormatTest.java @@ -0,0 +1,54 @@ +package org.jabref.logic.exporter; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.metadata.SaveOrder; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.mockito.Answers; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@NullMarked +@Execution(ExecutionMode.SAME_THREAD) +@ResourceLock("exporter") +class SimpleHtmlExportFormatTest { + private final Exporter exporter = new TemplateExporter("Simple HTML", + "simplehtml", + "simplehtml", + "", + StandardFileType.HTML, + mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS), + SaveOrder.getDefaultSaveOrder()); + + @Test + void authorFirstNamesAreExportedAsInitials(@TempDir Path testFolder) throws IOException, SaveException, ParserConfigurationException, TransformerException { + BibEntry entry = new BibEntry() + .withCitationKey("mykey") + .withField(StandardField.AUTHOR, "Kolb, Stefan Anton") + .withField(StandardField.TITLE, "my paper title"); + Path path = testFolder.resolve("export.html"); + + exporter.export(new BibDatabaseContext(), path, List.of(entry)); + + String content = Files.readString(path); + assertTrue(content.contains("
Kolb, S.A.
"), content); + } +} diff --git a/jablib/src/test/java/org/jabref/logic/formatter/FormattersTest.java b/jablib/src/test/java/org/jabref/logic/formatter/FormattersTest.java index b9db72b510fa..f6ac69c33365 100644 --- a/jablib/src/test/java/org/jabref/logic/formatter/FormattersTest.java +++ b/jablib/src/test/java/org/jabref/logic/formatter/FormattersTest.java @@ -34,7 +34,7 @@ void allFormattersAreRegistered() { "org.jabref.logic.formatter.bibtexfields.RemoveHyphenatedNewlinesFormatter", "org.jabref.logic.formatter.bibtexfields.RemoveNewlinesFormatter", "org.jabref.logic.formatter.bibtexfields.RemoveRedundantSpacesFormatter", - "org.jabref.logic.formatter.bibtexfields.ReplaceTabsBySpaceFormater", + "org.jabref.logic.formatter.bibtexfields.ReplaceTabsBySpaceFormatter", "org.jabref.logic.formatter.bibtexfields.TrimWhitespaceFormatter")); assertEquals(expected, Formatters.getAll().stream().map(formatter -> formatter.getClass().getName()).collect(Collectors.toSet())); diff --git a/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeEnDashesFormatterTest.java b/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeEnDashesFormatterTest.java index ca848fc9b53d..53ea2d0891e7 100644 --- a/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeEnDashesFormatterTest.java +++ b/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeEnDashesFormatterTest.java @@ -31,7 +31,7 @@ void dashesWithinWordsAreKept() { } @Test - void dashesPreceededByASpaceAreKept() { + void dashesPrecededByASpaceAreKept() { assertEquals("Example -illustrative", formatter.format("Example -illustrative")); } diff --git a/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormaterTest.java b/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormatterTest.java similarity index 79% rename from jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormaterTest.java rename to jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormatterTest.java index b91a144166f7..ae63d4c433c7 100644 --- a/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormaterTest.java +++ b/jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/ReplaceTabsBySpaceFormatterTest.java @@ -5,13 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -class ReplaceTabsBySpaceFormaterTest { +class ReplaceTabsBySpaceFormatterTest { - private ReplaceTabsBySpaceFormater formatter; + private ReplaceTabsBySpaceFormatter formatter; @BeforeEach void setUp() { - formatter = new ReplaceTabsBySpaceFormater(); + formatter = new ReplaceTabsBySpaceFormatter(); } @Test diff --git a/jablib/src/test/java/org/jabref/logic/importer/fetcher/MedlineFetcherTest.java b/jablib/src/test/java/org/jabref/logic/importer/fetcher/MedlineFetcherTest.java index fbf8334daa1f..c100897f1d22 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fetcher/MedlineFetcherTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fetcher/MedlineFetcherTest.java @@ -27,7 +27,7 @@ @ExternalServicesTest class MedlineFetcherTest { - // default value is empty string in BulidInfo + // default value is empty string in BuildInfo private static final Optional API_KEY = Optional.of(new BuildInfo().medlineApiKey).filter(StringUtil::isNotBlank); private MedlineFetcher fetcher; diff --git a/jablib/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java b/jablib/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java index a2c38a318a54..5e2698142fa6 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fetcher/MrDLibFetcherTest.java @@ -33,8 +33,8 @@ void setUp() { void performSearch() throws FetcherException { BibEntry bibEntry = new BibEntry(); bibEntry.setField(StandardField.TITLE, "lernen"); - List bibEntrys = fetcher.performSearch(bibEntry); - assertFalse(bibEntrys.isEmpty()); + List bibEntries = fetcher.performSearch(bibEntry); + assertFalse(bibEntries.isEmpty()); } @Test @@ -54,8 +54,8 @@ void performSearchForHornecker2006() throws FetcherException { bibEntry.setField(StandardField.URL, "http://doi.acm.org/10.1145/1124772.1124838"); bibEntry.setField(StandardField.YEAR, "2006"); - List bibEntrys = fetcher.performSearch(bibEntry); - assertFalse(bibEntrys.isEmpty()); + List bibEntries = fetcher.performSearch(bibEntry); + assertFalse(bibEntries.isEmpty()); } @Test diff --git a/jablib/src/test/java/org/jabref/logic/importer/fetcher/SpringerNatureWebFetcherTest.java b/jablib/src/test/java/org/jabref/logic/importer/fetcher/SpringerNatureWebFetcherTest.java index ba84762abc45..07d662eb0e8b 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fetcher/SpringerNatureWebFetcherTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fetcher/SpringerNatureWebFetcherTest.java @@ -192,7 +192,7 @@ void searchByEmptyQueryFindsNothing() throws FetcherException { } @Test - @Disabled("Year search is currently broken, because the API returns mutliple years.") + @Disabled("Year search is currently broken, because the API returns multiple years.") @Override public void supportsYearSearch() { } diff --git a/jablib/src/test/java/org/jabref/logic/importer/fetcher/isbntobibtex/GVKIsbnFetcherTest.java b/jablib/src/test/java/org/jabref/logic/importer/fetcher/isbntobibtex/GVKIsbnFetcherTest.java index e4701753e420..8ebd0df8dd83 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fetcher/isbntobibtex/GVKIsbnFetcherTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fetcher/isbntobibtex/GVKIsbnFetcherTest.java @@ -101,7 +101,7 @@ void isbnNeitherAvailableOnEbookDeNorOrViaOpenLibrary() throws FetcherException } @Test - void eResourceIsbnIsReturnedAsBoook() throws FetcherException { + void eResourceIsbnIsReturnedAsBook() throws FetcherException { assertEquals(Optional.of(StandardEntryType.Book), fetcher.performSearchById("978-0-8229-4557-4").map(BibEntry::getType)); } } diff --git a/jablib/src/test/java/org/jabref/logic/importer/fetcher/transformers/SuffixTransformerTest.java b/jablib/src/test/java/org/jabref/logic/importer/fetcher/transformers/SuffixTransformerTest.java index baedaa1d5d16..e05b2a4ec138 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fetcher/transformers/SuffixTransformerTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fetcher/transformers/SuffixTransformerTest.java @@ -91,7 +91,7 @@ public void groupedOperationsSuffix() throws QueryNodeParseException { } @Test - public void notOperatorSufix() throws QueryNodeParseException { + public void notOperatorSuffix() throws QueryNodeParseException { String queryString = "!(author=\"Igor Steinmacher\" OR author=\"Christoph Treude\")"; SearchQuery searchQuery = new SearchQuery(queryString); BaseQueryNode searchQueryList = new SearchQueryVisitor(searchQuery.getSearchFlags()).visitStart(searchQuery.getContext()); diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java index 70b618cb15b6..fa3b3b431401 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java @@ -428,7 +428,7 @@ void parseRecognizesHeaderButIgnoresEncoding() throws IOException { } @Test - void parseRecognizesFormatedEntry() throws IOException { + void parseRecognizesFormattedEntry() throws IOException { ParserResult result = parser.parse( Reader.of(""" @INPROCEEDINGS{CroAnnHow05, @@ -747,7 +747,7 @@ void parseRecognizesFieldsWithQuotationMarksInBrackets() throws IOException { } @Test - void parseIgnoresAndWarnsAboutEntryWithFieldsThatAreNotSeperatedByComma() throws IOException { + void parseIgnoresAndWarnsAboutEntryWithFieldsThatAreNotSeparatedByComma() throws IOException { ParserResult result = parser .parse(Reader.of("@article{test,author={Ed von Test} year=2005}")); diff --git a/jablib/src/test/java/org/jabref/logic/importer/fileformat/MedlineImporterFilesTest.java b/jablib/src/test/java/org/jabref/logic/importer/fileformat/MedlineImporterFilesTest.java index 44398594d7d1..d0718899e1f2 100644 --- a/jablib/src/test/java/org/jabref/logic/importer/fileformat/MedlineImporterFilesTest.java +++ b/jablib/src/test/java/org/jabref/logic/importer/fileformat/MedlineImporterFilesTest.java @@ -52,7 +52,7 @@ private static Stream malformedFileNames() throws IOException { @ParameterizedTest @MethodSource("malformedFileNames") - void importMalfomedFiles(String fileName) throws IOException { + void importMalformedFiles(String fileName) throws IOException { ImporterTestEngine.testImportMalformedFiles(new MedlineImporter(), fileName); } } diff --git a/jablib/src/test/java/org/jabref/logic/integrity/AbbreviationCheckerTest.java b/jablib/src/test/java/org/jabref/logic/integrity/AbbreviationCheckerTest.java index 2409836f1568..c661a7683808 100644 --- a/jablib/src/test/java/org/jabref/logic/integrity/AbbreviationCheckerTest.java +++ b/jablib/src/test/java/org/jabref/logic/integrity/AbbreviationCheckerTest.java @@ -62,7 +62,7 @@ void checkEntryWorksForLaTeXField() { } @Test - void checkEntryWorksForLaTeXFieldStilContainingIllegalChars() { + void checkEntryWorksForLaTeXFieldStillContainingIllegalChars() { entry.setField(StandardField.BOOKTITLE, "Proceedings of the 5\\({}^{\\mbox{th}}\\) Central-European Workshop on Services and their Composition, Rostock, Germany, February 21-22, 2013"); assertEquals(List.of(), checker.check(entry)); } diff --git a/jablib/src/test/java/org/jabref/logic/journals/AbbreviationTest.java b/jablib/src/test/java/org/jabref/logic/journals/AbbreviationTest.java index 1cd3bb5b4e11..d87c98fe974e 100644 --- a/jablib/src/test/java/org/jabref/logic/journals/AbbreviationTest.java +++ b/jablib/src/test/java/org/jabref/logic/journals/AbbreviationTest.java @@ -112,7 +112,7 @@ void equals() { } @Test - void equalAbbrevationsWithFourComponentsAreAlsoCompareZero() { + void equalAbbreviationsWithFourComponentsAreAlsoCompareZero() { Abbreviation abbreviation1 = new Abbreviation("Long Name", "L. N.", "LN"); Abbreviation abbreviation2 = new Abbreviation("Long Name", "L. N.", "LN"); assertEquals(0, abbreviation1.compareTo(abbreviation2)); diff --git a/jablib/src/test/java/org/jabref/logic/layout/format/LatexToUnicodeFormatterTest.java b/jablib/src/test/java/org/jabref/logic/layout/format/LatexToUnicodeFormatterTest.java index fc5bf1b16713..7a8b2f805b00 100644 --- a/jablib/src/test/java/org/jabref/logic/layout/format/LatexToUnicodeFormatterTest.java +++ b/jablib/src/test/java/org/jabref/logic/layout/format/LatexToUnicodeFormatterTest.java @@ -27,7 +27,7 @@ void formatUmlaut() { "ı, \\i", "ı, {\\i}" }) - void smallIwithoutDot(String expected, String input) { + void smallIWithoutDot(String expected, String input) { assertEquals(expected, formatter.format(input)); } @@ -92,23 +92,23 @@ void sWithCaron() { } @Test - void iWithDiaresis() { + void iWithDiaeresis() { assertEquals("ï", formatter.format("\\\"{i}")); } @Test - void iWithDiaresisAndEscapedI() { + void iWithDiaeresisAndEscapedI() { // this might look strange in the test, but is actually a correct translation and renders identically to the above example in the UI assertEquals("ı̈", formatter.format("\\\"{\\i}")); } @Test - void iWithDiaresisAndUnnecessaryBraces() { + void iWithDiaeresisAndUnnecessaryBraces() { assertEquals("ï", formatter.format("{\\\"{i}}")); } @Test - void upperCaseIWithDiaresis() { + void upperCaseIWithDiaeresis() { assertEquals("Ï", formatter.format("\\\"{I}")); } diff --git a/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleLoaderTest.java b/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleLoaderTest.java index 8c5805b74e1c..86e8e4fd4bab 100644 --- a/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleLoaderTest.java +++ b/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleLoaderTest.java @@ -94,14 +94,14 @@ void addInvalidStyleLeadsToNoMoreStyle() { } @Test - void initalizeWithOneExternalFile() { + void initializeWithOneExternalFile() { when(preferences.getExternalJStyles()).thenReturn(FXCollections.singletonObservableList(jStyleFile.toString())); loader = new JStyleLoader(preferences, layoutPreferences, abbreviationRepository); assertEquals(NUMBER_OF_INTERNAL_STYLES + 1, loader.getStyles().size()); } @Test - void initalizeWithIncorrectExternalFile() { + void initializeWithIncorrectExternalFile() { preferences.setExternalJStyles(List.of("DefinitelyNotAValidFileNameOrWeAreExtremelyUnlucky")); loader = new JStyleLoader(preferences, layoutPreferences, abbreviationRepository); @@ -109,7 +109,7 @@ void initalizeWithIncorrectExternalFile() { } @Test - void initalizeWithOneExternalFileRemoveStyle() { + void initializeWithOneExternalFileRemoveStyle() { when(preferences.getExternalJStyles()).thenReturn(FXCollections.singletonObservableList(jStyleFile.toString())); loader = new JStyleLoader(preferences, layoutPreferences, abbreviationRepository); @@ -128,7 +128,7 @@ void initalizeWithOneExternalFileRemoveStyle() { } @Test - void initalizeWithOneExternalFileRemoveStyleUpdatesPreferences() { + void initializeWithOneExternalFileRemoveStyleUpdatesPreferences() { when(preferences.getExternalJStyles()).thenReturn(FXCollections.singletonObservableList(jStyleFile.toString())); loader = new JStyleLoader(preferences, layoutPreferences, abbreviationRepository); diff --git a/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleTest.java b/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleTest.java index 5eacaef3a571..57b211ef6a03 100644 --- a/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleTest.java +++ b/jablib/src/test/java/org/jabref/logic/openoffice/style/JStyleTest.java @@ -881,7 +881,7 @@ void getCitationMarkerJoinFirst() throws IOException { } // With pageInfo: different entries with identical non-null pageInfo: not joined. - // XY [2000a,b,c; p1] whould be confusing. + // XY [2000a,b,c; p1] would be confusing. if (true) { List citationMarkerEntries = new ArrayList<>(); CitationMarkerEntry cm1 = diff --git a/jablib/src/test/java/org/jabref/logic/protectedterms/ProtectedTermsLoaderTest.java b/jablib/src/test/java/org/jabref/logic/protectedterms/ProtectedTermsLoaderTest.java index c2a6f33adcaa..0da6f1a8d9ca 100644 --- a/jablib/src/test/java/org/jabref/logic/protectedterms/ProtectedTermsLoaderTest.java +++ b/jablib/src/test/java/org/jabref/logic/protectedterms/ProtectedTermsLoaderTest.java @@ -122,7 +122,7 @@ void newListsAreEnabled() { } @Test - void initalizedAllInternalDisabled() { + void initializedAllInternalDisabled() { ProtectedTermsLoader localLoader = new ProtectedTermsLoader(new ProtectedTermsPreferences( List.of(), List.of(), diff --git a/jablib/src/test/java/org/jabref/logic/quality/consistency/BibliographyConsistencyCheckTest.java b/jablib/src/test/java/org/jabref/logic/quality/consistency/BibliographyConsistencyCheckTest.java index a0a1ed747753..a41ba840712e 100644 --- a/jablib/src/test/java/org/jabref/logic/quality/consistency/BibliographyConsistencyCheckTest.java +++ b/jablib/src/test/java/org/jabref/logic/quality/consistency/BibliographyConsistencyCheckTest.java @@ -447,7 +447,7 @@ void nonFilteredFieldDifferenceIsReported() { } @Test - void unsetRequriedFieldsReported() { + void unsetRequiredFieldsReported() { BibEntry withDate = new BibEntry(StandardEntryType.Online) .withCitationKey("withDate") .withField(StandardField.DATE, "date") // Required in BibLaTeX diff --git a/jablib/src/test/java/org/jabref/logic/shared/DBMSTypeTest.java b/jablib/src/test/java/org/jabref/logic/shared/DBMSTypeTest.java index 1364085feadf..b217a02b5436 100644 --- a/jablib/src/test/java/org/jabref/logic/shared/DBMSTypeTest.java +++ b/jablib/src/test/java/org/jabref/logic/shared/DBMSTypeTest.java @@ -33,7 +33,7 @@ void fromStringWorksForEmptyString() { } @Test - void fromStringWorksForUnkownString() { + void fromStringWorksForUnknownString() { assertEquals(Optional.empty(), DBMSType.fromString("unknown")); } diff --git a/jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java b/jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java index a8c4154fdad0..a10e43bc90f1 100644 --- a/jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/io/FileNameUniquenessTest.java @@ -74,28 +74,28 @@ void isDuplicatedFileWithOneSimilarNames() throws IOException { } @Test - void taseDuplicateMarksReturnsOrignalFileName1() throws IOException { + void duplicateMarksReturnsOriginalFileName1() throws IOException { String fileName1 = "abc def (1)"; String fileName2 = FileNameUniqueness.eraseDuplicateMarks(fileName1); assertEquals("abc def", fileName2); } @Test - void taseDuplicateMarksReturnsOrignalFileName2() { + void duplicateMarksReturnsOriginalFileName2() { String fileName1 = "abc (def) gh (1)"; String fileName2 = FileNameUniqueness.eraseDuplicateMarks(fileName1); assertEquals("abc (def) gh", fileName2); } @Test - void taseDuplicateMarksReturnsSameName1() { + void duplicateMarksReturnsSameName1() { String fileName1 = "abc def (g)"; String fileName2 = FileNameUniqueness.eraseDuplicateMarks(fileName1); assertEquals("abc def (g)", fileName2); } @Test - void taseDuplicateMarksReturnsSameName2() { + void duplicateMarksReturnsSameName2() { String fileName1 = "abc def"; String fileName2 = FileNameUniqueness.eraseDuplicateMarks(fileName1); assertEquals("abc def", fileName2); diff --git a/jablib/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java b/jablib/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java index 26d9fd31bdc8..8c63a673d775 100644 --- a/jablib/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java @@ -80,7 +80,7 @@ void killWordTrimsTrailingWhitespace() { } @Test - void backwardsKillWordTrimsPreceedingWhitespace() { + void backwardsKillWordTrimsPrecedingWhitespace() { int caretPosition = 1; // Second space String input = " hello"; // One space should be preserved since we are deleting everything preceding the second space. diff --git a/jablib/src/test/java/org/jabref/logic/xmp/XmpUtilReaderTest.java b/jablib/src/test/java/org/jabref/logic/xmp/XmpUtilReaderTest.java index 5a76a1703377..f81439f5735b 100644 --- a/jablib/src/test/java/org/jabref/logic/xmp/XmpUtilReaderTest.java +++ b/jablib/src/test/java/org/jabref/logic/xmp/XmpUtilReaderTest.java @@ -92,7 +92,7 @@ void readArticleDublinCoreReadXmpPartialDate() throws IOException, URISyntaxExce } @Test - void readEmtpyMetadata() throws IOException, URISyntaxException { + void readEmptyMetadata() throws IOException, URISyntaxException { List entries = xmpUtilReader.readXmp(Path.of(XmpUtilShared.class.getResource("empty_metadata.pdf").toURI()), xmpPreferences); assertEquals(List.of(), entries); } diff --git a/jablib/src/test/java/org/jabref/model/entry/AuthorListTest.java b/jablib/src/test/java/org/jabref/model/entry/AuthorListTest.java index e3ea74b220e7..771f7a915b99 100644 --- a/jablib/src/test/java/org/jabref/model/entry/AuthorListTest.java +++ b/jablib/src/test/java/org/jabref/model/entry/AuthorListTest.java @@ -40,7 +40,7 @@ public class AuthorListTest { private static final AuthorList THREE_AUTHORS_WITH_LATEX = AuthorList.of(MUHAMMAD_ALKHWARIZMI, CORRADO_BOHM, KURT_GODEL); private static final AuthorList ONE_INSTITUTION_WITH_LATEX = AuthorList.of(BANU_MOSA); - private static final AuthorList ONE_INSTITUTION_WITH_STARTING_PARANTHESIS = AuthorList.of( + private static final AuthorList ONE_INSTITUTION_WITH_STARTING_PARENTHESIS = AuthorList.of( new Author( null, null, null, "{{\\L{}}ukasz Micha\\l{}}", null)); private static final AuthorList TWO_INSTITUTIONS_WITH_LATEX = AuthorList.of(BANU_MOSA, BANU_MOSA); @@ -79,7 +79,7 @@ void fixAuthorNatbib(String input, String expected) { # LaTeX-free Unicode mixed authors from LaTeX MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX , The Banū Mūsā brothers and Böhm # LaTeX-free one institution with parenthesis at start - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS , Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS , Łukasz Michał """) void getAsNatbibLatexFree(String authorListName, String expected) { AuthorList authorList = getAuthorListByName(authorListName); @@ -150,7 +150,7 @@ void fixAuthorFirstNameFirstCommas(String input, String expected, boolean abbrev # Unicode mixed authors from LaTeX abbreviate MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; true ; false ; The Banū Mūsā brothers and C. Böhm # One institution with parenthesis at start abbreviate - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; true ; false ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; true ; false ; Łukasz Michał # Empty author string for empty input EMPTY_AUTHOR ; false ; false ; '' # Unicode one author name from LaTeX @@ -166,7 +166,7 @@ void fixAuthorFirstNameFirstCommas(String input, String expected, boolean abbrev # Unicode mixed authors from LaTeX MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; false ; false ; The Banū Mūsā brothers and Corrado Böhm # One institution with parenthesis at start - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; false ; false ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; false ; false ; Łukasz Michał """) void getAsFirstLastNamesLatexFree(String authorListName, boolean abbreviate, boolean oxford, String expected) { AuthorList authorList = getAuthorListByName(authorListName); @@ -216,7 +216,7 @@ void fixAuthorLastNameFirstCommasoxford(String input, String expected, boolean a # Unicode mixed authors from LaTeX abbreviate MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; true ; false ; The Banū Mūsā brothers and Böhm, C. # One institution with parenthesis at start abbreviate - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; true ; false ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; true ; false ; Łukasz Michał # Empty author string for empty input EMPTY_AUTHOR ; false ; false ; '' # Unicode one author name from LaTeX @@ -232,7 +232,7 @@ void fixAuthorLastNameFirstCommasoxford(String input, String expected, boolean a # Unicode mixed authors from LaTeX MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; false ; false ; The Banū Mūsā brothers and Böhm, Corrado # One institution with parenthesis at start - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; false ; false ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; false ; false ; Łukasz Michał # Empty author string for empty input abbreviate Oxford comma EMPTY_AUTHOR ; true ; true ; '' # Unicode one author name from LaTeX abbreviate Oxford comma @@ -248,7 +248,7 @@ void fixAuthorLastNameFirstCommasoxford(String input, String expected, boolean a # Unicode mixed authors from LaTeX abbreviate Oxford comma MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; true ; true ; The Banū Mūsā brothers and Böhm, C. # One institution with parenthesis at start abbreviate Oxford comma - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; true ; true ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; true ; true ; Łukasz Michał # Empty author string for empty input Oxford comma EMPTY_AUTHOR ; false ; true ; '' # Unicode one author name from LaTeX Oxford comma @@ -264,7 +264,7 @@ void fixAuthorLastNameFirstCommasoxford(String input, String expected, boolean a # Unicode mixed authors from LaTeX Oxford comma MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; false ; true ; The Banū Mūsā brothers and Böhm, Corrado # One institution with parenthesis at start Oxford comma - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; false ; true ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; false ; true ; Łukasz Michał """) void getAsLastFirstNamesLatexFree(String authorListName, boolean abbreviate, boolean oxford, String expected) { AuthorList authorList = getAuthorListByName(authorListName); @@ -330,7 +330,7 @@ void fixAuthorLastNameOnlyCommas(String input, String expected, boolean oxford) # Unicode mixed authors from LaTeX MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX ; false ; The Banū Mūsā brothers and Böhm # One institution with parenthesis at start - ONE_INSTITUTION_WITH_STARTING_PARANTHESIS ; false ; Łukasz Michał + ONE_INSTITUTION_WITH_STARTING_PARENTHESIS ; false ; Łukasz Michał """) void getAsLastNamesLatexFree(String authorListName, boolean oxfordComma, String expected) { AuthorList authorList = getAuthorListByName(authorListName); @@ -903,8 +903,8 @@ private AuthorList getAuthorListByName(String name) { return TWO_INSTITUTIONS_WITH_LATEX; case "MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX": return MIXED_AUTHOR_AND_INSTITUTION_WITH_LATEX; - case "ONE_INSTITUTION_WITH_STARTING_PARANTHESIS": - return ONE_INSTITUTION_WITH_STARTING_PARANTHESIS; + case "ONE_INSTITUTION_WITH_STARTING_PARENTHESIS": + return ONE_INSTITUTION_WITH_STARTING_PARENTHESIS; default: throw new IllegalArgumentException("Unknown author list: " + name); } diff --git a/jablib/src/test/java/org/jabref/model/entry/BibEntryTest.java b/jablib/src/test/java/org/jabref/model/entry/BibEntryTest.java index cb2d5ce75938..698942d466cd 100644 --- a/jablib/src/test/java/org/jabref/model/entry/BibEntryTest.java +++ b/jablib/src/test/java/org/jabref/model/entry/BibEntryTest.java @@ -374,7 +374,7 @@ void isEmptyCiteKey() { } @Test - void identicObjectsareEqual() { + void identicalObjectsAreEqual() { BibEntry otherEntry = entry; assertEquals(entry, otherEntry); } @@ -501,27 +501,27 @@ void putKeywordsHasChanged() { } @Test - void putKeywordsPutEmpyListErasesPreviousKeywords() { + void putKeywordsPutEmptyListErasesPreviousKeywords() { entry.setField(StandardField.KEYWORDS, "Foo, Bar"); entry.putKeywords(List.of(), ','); assertTrue(entry.getKeywords(',').isEmpty()); } @Test - void putKeywordsPutEmpyListHasChanged() { + void putKeywordsPutEmptyListHasChanged() { entry.setField(StandardField.KEYWORDS, "Foo, Bar"); entry.putKeywords(List.of(), ','); assertTrue(entry.hasChanged()); } @Test - void putKeywordsPutEmpyListToEmptyBibentry() { + void putKeywordsPutEmptyListToEmptyBibentry() { entry.putKeywords(List.of(), ','); assertTrue(entry.getKeywords(',').isEmpty()); } @Test - void putKeywordsPutEmpyListToEmptyBibentryNotChanged() { + void putKeywordsPutEmptyListToEmptyBibentryNotChanged() { entry.putKeywords(List.of(), ','); assertFalse(entry.hasChanged()); } diff --git a/jablib/src/test/java/org/jabref/model/entry/BibtexStringTest.java b/jablib/src/test/java/org/jabref/model/entry/BibtexStringTest.java index 6f64b920f896..7d7e5baebe33 100644 --- a/jablib/src/test/java/org/jabref/model/entry/BibtexStringTest.java +++ b/jablib/src/test/java/org/jabref/model/entry/BibtexStringTest.java @@ -8,7 +8,7 @@ class BibtexStringTest { @Test - void initalizationWorksCorrectly() { + void initializationWorksCorrectly() { // Instantiate BibtexString bs = new BibtexString("AAA", "An alternative action"); assertEquals("AAA", bs.getName()); diff --git a/jablib/src/test/java/org/jabref/model/entry/identifier/DOITest.java b/jablib/src/test/java/org/jabref/model/entry/identifier/DOITest.java index 178ea7c6e8d2..56f92f72f6c5 100644 --- a/jablib/src/test/java/org/jabref/model/entry/identifier/DOITest.java +++ b/jablib/src/test/java/org/jabref/model/entry/identifier/DOITest.java @@ -304,7 +304,7 @@ void rejectInvalidDirectoryIndicatorInShortDoi() { } @Test - void emptyOrUndescoreOnlyReturnsEmpty() { + void emptyOrUnderscoreOnlyReturnsEmpty() { assertEquals(Optional.empty(), DOI.parse("_")); assertEquals(Optional.empty(), DOI.parse("\t_")); assertEquals(Optional.empty(), DOI.parse("___")); diff --git a/jablib/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java b/jablib/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java index e43905256853..0a984cef6ebf 100644 --- a/jablib/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java +++ b/jablib/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java @@ -59,17 +59,17 @@ public static GroupTreeNode getNodeInComplexTree(GroupTreeNode root) { .addSubgroup(new ExplicitGroup("ExplicitGrandParent", GroupHierarchyType.INDEPENDENT, ',')); root.addSubgroup(getKeywordGroup("KeywordA")); - grandParent.addSubgroup(getExplict("ExplicitB")); + grandParent.addSubgroup(getExplicit("ExplicitB")); GroupTreeNode parent = grandParent.addSubgroup(getKeywordGroup("KeywordParent")); grandParent.addSubgroup(getSearchGroup("SearchB")); grandParent.addSubgroup(getKeywordGroup("KeywordB")); GroupTreeNode node = parent.addSubgroup(getKeywordGroup("KeywordNode")); parent.addSubgroup(getSearchGroup("SearchC")); - parent.addSubgroup(getExplict("ExplicitC")); + parent.addSubgroup(getExplicit("ExplicitC")); parent.addSubgroup(getKeywordGroup("KeywordC")); - node.addSubgroup(getExplict("ExplicitChild")); + node.addSubgroup(getExplicit("ExplicitChild")); return node; } @@ -81,7 +81,7 @@ private static AbstractGroup getSearchGroup(String name) { return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchFlags.CASE_SENSITIVE)); } - private static AbstractGroup getExplict(String name) { + private static AbstractGroup getExplicit(String name) { return new ExplicitGroup(name, GroupHierarchyType.REFINING, ','); } diff --git a/jablib/src/test/java/org/jabref/model/openoffice/CitationEntryTest.java b/jablib/src/test/java/org/jabref/model/openoffice/CitationEntryTest.java index 6e003d6746b4..24f6e28a2478 100644 --- a/jablib/src/test/java/org/jabref/model/openoffice/CitationEntryTest.java +++ b/jablib/src/test/java/org/jabref/model/openoffice/CitationEntryTest.java @@ -29,7 +29,7 @@ void citationEntryOptionalInitialPageInfo() { } @Test - void citationEntryInitalPageInfoChanged() { + void citationEntryInitialPageInfoChanged() { CitationEntry citationEntry = new CitationEntry("RefMark", "Context", "Info"); assertEquals(Optional.of("Info"), citationEntry.getPageInfo()); } diff --git a/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java b/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java index 0b7f7644afb8..838653239e09 100644 --- a/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java +++ b/jabls/src/main/java/org/jabref/languageserver/util/LspRangeUtil.java @@ -5,7 +5,7 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; -/// Because only postions are supported by the lsp https://github.com/microsoft/language-server-protocol/issues/96 we need to convert back and forth +/// Because only positions are supported by the lsp https://github.com/microsoft/language-server-protocol/issues/96 we need to convert back and forth public class LspRangeUtil { public static int toOffset(String content, Position pos) { diff --git a/rewrite.yml b/rewrite.yml index 82b455436c9c..83199448004b 100644 --- a/rewrite.yml +++ b/rewrite.yml @@ -40,7 +40,7 @@ recipeList: # - org.openrewrite.staticanalysis.InlineVariable # Leads to unreadable code # - org.openrewrite.staticanalysis.LambdaBlockToExpression - # Works on case-senstive file systems only + # Works on case-sensitive file systems only # - org.openrewrite.staticanalysis.LowercasePackage # Needs manual intervention (not fixable at existing code) # - org.openrewrite.staticanalysis.MethodNameCasing @@ -60,7 +60,7 @@ recipeList: # - org.openrewrite.staticanalysis.RemoveUnusedPrivateMethods # Crashes # - org.openrewrite.staticanalysis.RenameExceptionInEmptyCatch - # Also renames private static final varialbes (e.g., LOGGER, ALL_ENTRIES_GROUP_DEFAULT_ICON, ...) + # Also renames private static final variables (e.g., LOGGER, ALL_ENTRIES_GROUP_DEFAULT_ICON, ...) # - org.openrewrite.staticanalysis.RenamePrivateFieldsToCamelCase # Does something else (deleting a field in TreeNode) # - org.openrewrite.staticanalysis.ReplaceDeprecatedRuntimeExecMethods diff --git a/scripts/vms/debian-12/README.md b/scripts/vms/debian-12/README.md index 448638e0c8ae..a3063515db2f 100644 --- a/scripts/vms/debian-12/README.md +++ b/scripts/vms/debian-12/README.md @@ -17,6 +17,6 @@ After `vagrant up`: 7. `exit` 8. In the graphical VirtualBox window, you can now login 9. Enter `startx` to see IceWM. -10. Open termminal +10. Open terminal 11. `cd jabref` 12. `./gradlew :jabgui:run` diff --git a/scripts/vms/linux-mint-cinnamon/README.md b/scripts/vms/linux-mint-cinnamon/README.md index 5518a6e1f4dd..b3b3b9040e0f 100644 --- a/scripts/vms/linux-mint-cinnamon/README.md +++ b/scripts/vms/linux-mint-cinnamon/README.md @@ -6,7 +6,7 @@ Uses Date: Sun, 27 Sep 2026 21:04:25 +0000 Subject: [PATCH 101/102] New Crowdin updates (#17311) * New translations jabref_en.properties for (French) * New translations jabref_en.properties for (Spanish) * New translations jabref_en.properties for (German) * New translations jabref_en.properties for (Finnish) * New translations jabref_en.properties for (Italian) * New translations jabref_en.properties for (Chinese Simplified) * New translations jabref_en.properties for (Portuguese, Brazilian) --- jablib/src/main/resources/l10n/JabRef_de.properties | 1 - jablib/src/main/resources/l10n/JabRef_es.properties | 1 - jablib/src/main/resources/l10n/JabRef_fi.properties | 1 - jablib/src/main/resources/l10n/JabRef_fr.properties | 1 - jablib/src/main/resources/l10n/JabRef_it.properties | 1 - jablib/src/main/resources/l10n/JabRef_pt_BR.properties | 1 - jablib/src/main/resources/l10n/JabRef_zh_CN.properties | 1 - 7 files changed, 7 deletions(-) diff --git a/jablib/src/main/resources/l10n/JabRef_de.properties b/jablib/src/main/resources/l10n/JabRef_de.properties index feac66d1ef87..3fffc9d55762 100644 --- a/jablib/src/main/resources/l10n/JabRef_de.properties +++ b/jablib/src/main/resources/l10n/JabRef_de.properties @@ -3395,7 +3395,6 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Geben Sie ein `title \= covid OR title \= pandemic` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=Diese Abfrage findet Artikel mit Titel, die entweder COVID oder Pandemien im Allgemeinen enthalten. -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=Die Bedingung `OR` ist erfüllt, wenn eine der beiden Bedingungen wahr ist. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Geben Sie `deep learning NOT title \= survey` ein diff --git a/jablib/src/main/resources/l10n/JabRef_es.properties b/jablib/src/main/resources/l10n/JabRef_es.properties index b0d4cbb72c76..ba93fedb6f32 100644 --- a/jablib/src/main/resources/l10n/JabRef_es.properties +++ b/jablib/src/main/resources/l10n/JabRef_es.properties @@ -2969,7 +2969,6 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Escriba `title\=covid O title\=pandemia` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=Esta consulta encuentra artículos cuyo título contiene COVID o pandemias en general. -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`OR` puede satisfacerse si cualquiera de las condiciones es verdadera. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Escriba `aprendizaje profundo NO título \= encuesta` diff --git a/jablib/src/main/resources/l10n/JabRef_fi.properties b/jablib/src/main/resources/l10n/JabRef_fi.properties index f5c9c41ec3ca..6e08954087ca 100644 --- a/jablib/src/main/resources/l10n/JabRef_fi.properties +++ b/jablib/src/main/resources/l10n/JabRef_fi.properties @@ -1952,7 +1952,6 @@ Type\ `shortauthor\ \=\ WHO\ AND\ date\ \=\=\ 2023`=Kirjoita `lyhyttekijä \= KU # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Kirjoita `otsikko \= covid TAI otsikko \= pandemia` -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`TAI` voi toteutua, kun jompikumpi ehdoista on tosi. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Kirjoita `syväoppiminen EI otsikko \= kysely` diff --git a/jablib/src/main/resources/l10n/JabRef_fr.properties b/jablib/src/main/resources/l10n/JabRef_fr.properties index 29f03c85eef6..9b16c3ad768c 100644 --- a/jablib/src/main/resources/l10n/JabRef_fr.properties +++ b/jablib/src/main/resources/l10n/JabRef_fr.properties @@ -3424,7 +3424,6 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Taper `title \= covid OR title \= pandemic` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=Cette requête trouve les références dont le titre contient soit COVID, soit pandémie dang General. -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`OR` est valide lorsque l'une des conditions est vraie. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Tapez `deep learning NOT title \= survey` diff --git a/jablib/src/main/resources/l10n/JabRef_it.properties b/jablib/src/main/resources/l10n/JabRef_it.properties index b3ce32bf29fe..0b9feba84078 100644 --- a/jablib/src/main/resources/l10n/JabRef_it.properties +++ b/jablib/src/main/resources/l10n/JabRef_it.properties @@ -3381,7 +3381,6 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Tipo `title \= covid OR title \= pandemic` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=Questa interrogazione trova articoli con titolo contenente COVID o pandemie in generale. -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`OR` può essere soddisfatto quando una delle condizioni è vera. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Digita `deep learning NOT title \= survey` diff --git a/jablib/src/main/resources/l10n/JabRef_pt_BR.properties b/jablib/src/main/resources/l10n/JabRef_pt_BR.properties index abc812e075ba..b3abd3f21dbd 100644 --- a/jablib/src/main/resources/l10n/JabRef_pt_BR.properties +++ b/jablib/src/main/resources/l10n/JabRef_pt_BR.properties @@ -2956,7 +2956,6 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=Digite `title \= covid OR título \= pandemic` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=Esta consulta encontra papel com título contendo COVID ou pandemias em geral. -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`OR` pode ser satisificada com uma das condições sendo verdadeira. # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=Digite `deep learning NOT title \= survey` diff --git a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties index b1bccaba0ca3..ccee6413a14d 100644 --- a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties @@ -3174,7 +3174,6 @@ This\ query\ finds\ papers\ by\ WHO\ from\ 2023.\ `AND`\ requires\ both\ conditi # Step 11 Type\ `title\ \=\ covid\ OR\ title\ \=\ pandemic`=输入 `title \= covid OR title \= pandemic` This\ query\ finds\ paper\ with\ title\ containing\ either\ COVID\ or\ pandemics\ in\ general.=此查询查找标题包含 COVID 或一般性大流行病的论文。 -`OR`\ can\ be\ satisified\ with\ either\ of\ the\ conditions\ being\ true.=`OR` 只需满足其中一个条件即可。 # Step 12 Type\ `deep\ learning\ NOT\ title\ \=\ survey`=输入 `deep learning NOT title \= survey` From d7e0302bb7c2fb101c5d7d294dc4d805e010ebcd Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 28 Sep 2026 16:29:41 +0200 Subject: [PATCH 102/102] Write braced Hayagriva authors as plain names Corporate authors like {Some company} were exported as YAML flow maps; author names are now converted from LaTeX, which drops the protecting braces. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + .../resource/layout/hayagrivayaml.layout | 2 +- .../exporter/HayagrivaYamlExporterTest.java | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96559472680..3f400c9ebc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200) - We fixed an issue where entries of type Misc were penalized too strictly during duplicate detection. [#16578](https://github.com/JabRef/jabref/issues/16578) - We fixed an issue where the "Simple HTML" export separated author initials with spaces. [#17306](https://github.com/JabRef/jabref/pull/17306) +- We fixed an issue where the Hayagriva YAML export wrote braced author names, such as company names, as invalid YAML. [#17316](https://github.com/JabRef/jabref/issues/17316) - We fixed an issue where "Community forum" in the Help menu opened the donation page instead of the forum. [#17162](https://github.com/JabRef/jabref/pull/17162) ### Removed diff --git a/jablib/src/main/resources/resource/layout/hayagrivayaml.layout b/jablib/src/main/resources/resource/layout/hayagrivayaml.layout index 053fb494068b..776c8742a845 100644 --- a/jablib/src/main/resources/resource/layout/hayagrivayaml.layout +++ b/jablib/src/main/resources/resource/layout/hayagrivayaml.layout @@ -3,7 +3,7 @@ \begin{title} title: "\title"\end{title} \begin{author} author: - - \format[Authors(LastFirst, MiddleInitial, Sep =\n - , LastSep =\n - )]{\author} + - \format[Authors(LastFirst, MiddleInitial, Sep =\n - , LastSep =\n - ),LatexToUnicode]{\author} \end{author} \begin{date} date: \date\end{date} \begin{journal} diff --git a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java index c55e90e67cd8..deaa44ee53ff 100644 --- a/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java +++ b/jablib/src/test/java/org/jabref/logic/exporter/HayagrivaYamlExporterTest.java @@ -82,6 +82,26 @@ final void exportsCorrectContent(@TempDir Path tempFile) throws IOException, Sav assertEquals(expected, Files.readAllLines(file)); } + @Test + final void exportsCorporateAuthorWithoutBraces(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("examplekey") + .withField(StandardField.AUTHOR, "{Some company} and M{\\\"u}ller, Hans"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + hayagrivaYamlExporter.export(databaseContext, file, List.of(entry)); + + List expected = List.of( + "examplekey:", + " type: article", + " author:", + " - Some company", + " - Müller, Hans"); + + assertEquals(expected, Files.readAllLines(file)); + } + @Test final void exportsCorrectMultipleAuthors(@TempDir Path tempFile) throws IOException, SaveException, ParserConfigurationException, TransformerException { BibEntry entry = new BibEntry(StandardEntryType.Article)