diff --git a/.jbang/JabKitLauncher.java b/.jbang/JabKitLauncher.java index d980f268703..0c861e2260c 100755 --- a/.jbang/JabKitLauncher.java +++ b/.jbang/JabKitLauncher.java @@ -9,6 +9,7 @@ //REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/,raw=https://raw.githubusercontent.com/JabRef/jabref/refs/heads/main/jablib/lib/ //DEPS org.jabref:jablib:6.0-SNAPSHOT +// see https://github.com/gradlex-org/extra-java-module-info/issues/237 why we include e-adr here //DEPS io.github.adr:e-adr:2.0.0 // requirements needed by jabkit projecxt need to be listed; requirements by jablib are loaded transitively diff --git a/.jbang/JabLsLauncher.java b/.jbang/JabLsLauncher.java index ff4cedaed07..a210272dc23 100755 --- a/.jbang/JabLsLauncher.java +++ b/.jbang/JabLsLauncher.java @@ -9,6 +9,8 @@ //REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/,raw=https://raw.githubusercontent.com/JabRef/jabref/refs/heads/main/jablib/lib/ //DEPS org.jabref:jablib:6.0-SNAPSHOT +// see https://github.com/gradlex-org/extra-java-module-info/issues/237 why we include e-adr here +//DEPS io.github.adr:e-adr:2.0.0 // from jabls-cli //DEPS info.picocli:picocli:4.7.7 diff --git a/.jbang/JabSrvLauncher.java b/.jbang/JabSrvLauncher.java index b7f08259f7b..73a5fa44145 100755 --- a/.jbang/JabSrvLauncher.java +++ b/.jbang/JabSrvLauncher.java @@ -9,6 +9,8 @@ //REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/,raw=https://raw.githubusercontent.com/JabRef/jabref/refs/heads/main/jablib/lib/ //DEPS org.jabref:jablib:6.0-SNAPSHOT +// see https://github.com/gradlex-org/extra-java-module-info/issues/237 why we include e-adr here +//DEPS io.github.adr:e-adr:2.0.0 // from jabsrv-cli //DEPS info.picocli:picocli:4.7.7 diff --git a/jabgui/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java b/jabgui/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java index 87e989d8f8f..b8c3a574b21 100644 --- a/jabgui/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java +++ b/jabgui/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java @@ -22,7 +22,6 @@ import org.jabref.gui.LibraryTab; import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.columns.MainTableColumn; -import org.jabref.logic.bibtex.InvalidFieldValueException; import org.jabref.logic.exporter.AtomicFileWriter; import org.jabref.logic.exporter.BibDatabaseWriter; import org.jabref.logic.exporter.BibWriter; @@ -272,7 +271,7 @@ void performBackup(Path backupPath) { // Thus, we currently do not need any new backup this.needsBackup = false; } catch (IOException e) { - logIfCritical(backupPath, e); + LOGGER.error("Error while saving to file {}", backupPath, e); } } @@ -293,19 +292,6 @@ public void discardBackup(Path backupDir) { } } - private void logIfCritical(Path backupPath, IOException e) { - Throwable innermostCause = e; - while (innermostCause.getCause() != null) { - innermostCause = innermostCause.getCause(); - } - boolean isErrorInField = innermostCause instanceof InvalidFieldValueException; - - // do not print errors in field values into the log during autosave - if (!isErrorInField) { - LOGGER.error("Error while saving to file {}", backupPath, e); - } - } - @Subscribe public synchronized void listen(@SuppressWarnings("unused") BibDatabaseContextChangedEvent event) { if (!event.isFilteredOut()) { diff --git a/jabgui/src/main/java/org/jabref/gui/collab/entrychange/PreviewWithSourceTab.java b/jabgui/src/main/java/org/jabref/gui/collab/entrychange/PreviewWithSourceTab.java index 43b7f201dbd..069068f9f6f 100644 --- a/jabgui/src/main/java/org/jabref/gui/collab/entrychange/PreviewWithSourceTab.java +++ b/jabgui/src/main/java/org/jabref/gui/collab/entrychange/PreviewWithSourceTab.java @@ -78,7 +78,7 @@ private String getSourceString(BibEntry entry, BibEntryTypesManager entryTypesManager) throws IOException { StringWriter writer = new StringWriter(); BibWriter bibWriter = new BibWriter(writer, OS.NEWLINE); - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); + FieldWriter fieldWriter = new FieldWriter(fieldPreferences); new BibEntryWriter(fieldWriter, entryTypesManager).write(entry, bibWriter, type); return writer.toString(); } diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/SourceTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index 7e80aaeeca9..e0ad2c577d4 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -38,7 +38,6 @@ import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.bibtex.InvalidFieldValueException; import org.jabref.logic.exporter.BibWriter; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ParserResult; @@ -161,7 +160,7 @@ private void highlightField(Field field, String searchPattern) { private String getSourceString(BibEntry entry, BibDatabaseMode type, FieldPreferences fieldPreferences) throws IOException { try (StringWriter writer = new StringWriter()) { BibWriter bibWriter = new BibWriter(writer, "\n"); // JavaFX works with LF only - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); + FieldWriter fieldWriter = new FieldWriter(fieldPreferences); BibEntryWriter bibEntryWriter = new BibEntryWriter(fieldWriter, entryTypesManager); bibEntryWriter.write(entry, bibWriter, type, true); fieldPositions = bibEntryWriter.getFieldPositions(); @@ -276,95 +275,102 @@ private void storeSource(BibEntry outOfFocusEntry, String text) { } BibtexParser bibtexParser = new BibtexParser(importFormatPreferences, fileMonitor); + ParserResult parserResult; try { - ParserResult parserResult = bibtexParser.parse(Reader.of(text)); - BibDatabase database = parserResult.getDatabase(); - - if (database.getEntryCount() > 1) { - LOGGER.error("More than one entry found."); - // We use the error dialog as the notification is hidden - dialogService.showWarningDialogAndWait( - Localization.lang("Problem with parsing entry"), - Localization.lang("Parsing failed because more than one entry was found. Please check your BibTeX syntax.") - ); - return; - } - - if (!database.hasEntries()) { - if (parserResult.hasWarnings()) { - LOGGER.warn("Could not store entry", parserResult.warnings()); - String errors = parserResult.getErrorMessage(); - dialogService.showErrorDialogAndWait(errors); - validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", errors))); - return; - } else { - LOGGER.warn("No entries found."); - String errors = Localization.lang("No entries available"); - dialogService.showErrorDialogAndWait(errors); - validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", errors))); - return; - } - } + parserResult = bibtexParser.parse(Reader.of(text)); + } catch (IOException ex) { + validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", ex.getMessage()))); + LOGGER.debug("Incorrect source", ex); + return; + } + BibDatabase database = parserResult.getDatabase(); + + if (database.getEntryCount() > 1) { + LOGGER.error("More than one entry found."); + // We use the error dialog as the notification is hidden + dialogService.showWarningDialogAndWait( + Localization.lang("Problem with parsing entry"), + Localization.lang("Parsing failed because more than one entry was found. Please check your BibTeX syntax.") + ); + return; + } + if (!database.hasEntries()) { if (parserResult.hasWarnings()) { - LOGGER.warn("Failed to parse Bib(La)TeX", parserResult.warnings()); + LOGGER.warn("Could not store entry: {}", parserResult.warnings()); String errors = parserResult.getErrorMessage(); dialogService.showErrorDialogAndWait(errors); validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", errors))); + return; + } else { + LOGGER.warn("No entries found."); + String errors = Localization.lang("No entries available"); + dialogService.showErrorDialogAndWait(errors); + validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", errors))); + return; } + } - NamedCompoundEdit compound = new NamedCompoundEdit(Localization.lang("source edit")); - BibEntry newEntry = database.getEntries().getFirst(); - newEntry.getCitationKey() - .ifPresentOrElse( - outOfFocusEntry::setCitationKey, - () -> outOfFocusEntry.clearCiteKey()); - - // First, remove fields that the user has removed. - for (Map.Entry field : outOfFocusEntry.getFieldMap().entrySet()) { - Field fieldName = field.getKey(); - String fieldValue = field.getValue(); - - if (!newEntry.hasField(fieldName)) { - compound.addEdit(new UndoableFieldChange(outOfFocusEntry, fieldName, fieldValue, null)); - outOfFocusEntry.clearField(fieldName); - } + if (parserResult.hasWarnings()) { + LOGGER.warn("Failed to parse Bib(La)TeX: {}", parserResult.warnings()); + String errors = parserResult.getErrorMessage(); + dialogService.showErrorDialogAndWait(errors); + validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", errors))); + } + + NamedCompoundEdit compound = new NamedCompoundEdit(Localization.lang("source edit")); + BibEntry newEntry = database.getEntries().getFirst(); + newEntry.getCitationKey() + .ifPresentOrElse( + outOfFocusEntry::setCitationKey, + outOfFocusEntry::clearCiteKey); + + // First, remove fields that the user has removed. + for (Map.Entry field : outOfFocusEntry.getFieldMap().entrySet()) { + Field fieldName = field.getKey(); + String fieldValue = field.getValue(); + + if (!newEntry.hasField(fieldName)) { + compound.addEdit(new UndoableFieldChange(outOfFocusEntry, fieldName, fieldValue, null)); + outOfFocusEntry.clearField(fieldName); } + } - // Then set all fields that have been set by the user. - for (Map.Entry field : newEntry.getFieldMap().entrySet()) { - Field fieldName = field.getKey(); - String oldValue = outOfFocusEntry.getField(fieldName).orElse(null); - String newValue = field.getValue(); - if (!Objects.equals(oldValue, newValue)) { - // Test if the field is legally set. - new FieldWriter(fieldPreferences).write(fieldName, newValue); - - compound.addEdit(new UndoableFieldChange(outOfFocusEntry, fieldName, oldValue, newValue)); - outOfFocusEntry.setField(fieldName, newValue); + // Then set all fields that have been set by the user. + for (Map.Entry field : newEntry.getFieldMap().entrySet()) { + Field fieldName = field.getKey(); + String oldValue = outOfFocusEntry.getField(fieldName).orElse(null); + String newValue = field.getValue(); + if (!Objects.equals(oldValue, newValue)) { + // Test if the field is legally set. + List errors = FieldWriter.checkBalancedBraces(newValue); + if (!errors.isEmpty()) { + validationMessage.setValue(ValidationMessage.error( + Localization.lang("Failed to parse Bib(La)TeX: %0", String.join("\n", errors)))); + return; } - } - // See if the user has changed the entry type: - if (!Objects.equals(newEntry.getType(), outOfFocusEntry.getType())) { - compound.addEdit(new UndoableChangeType(outOfFocusEntry, outOfFocusEntry.getType(), newEntry.getType())); - outOfFocusEntry.setType(newEntry.getType()); - } - compound.end(); - undoManager.addEdit(compound); - - ObservableList selectedEntries = stateManager.getSelectedEntries(); - if (selectedEntries == null || selectedEntries.isEmpty()) { - stateManager.activeTabProperty().get().ifPresent(libraryTab -> - libraryTab.getMainTable().clearAndSelect(outOfFocusEntry) - ); + compound.addEdit(new UndoableFieldChange(outOfFocusEntry, fieldName, oldValue, newValue)); + outOfFocusEntry.setField(fieldName, newValue); } + } - validationMessage.setValue(null); - } catch (InvalidFieldValueException | IOException ex) { - validationMessage.setValue(ValidationMessage.error(Localization.lang("Failed to parse Bib(La)TeX: %0", ex.getMessage()))); - LOGGER.debug("Incorrect source", ex); + // See if the user has changed the entry type: + if (!Objects.equals(newEntry.getType(), outOfFocusEntry.getType())) { + compound.addEdit(new UndoableChangeType(outOfFocusEntry, outOfFocusEntry.getType(), newEntry.getType())); + outOfFocusEntry.setType(newEntry.getType()); + } + compound.end(); + undoManager.addEdit(compound); + + ObservableList selectedEntries = stateManager.getSelectedEntries(); + if (selectedEntries == null || selectedEntries.isEmpty()) { + stateManager.activeTabProperty().get().ifPresent(libraryTab -> + libraryTab.getMainTable().clearAndSelect(outOfFocusEntry) + ); } + + validationMessage.setValue(null); } private void listenForSaveKeybinding(KeyEvent event) { diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index ba2bfb2fbe0..b728bbf007c 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -630,7 +630,7 @@ private void jumpToEntry(CitationRelationItem entry) { private String getSourceString(BibEntry entry, BibDatabaseMode type, FieldPreferences fieldPreferences, BibEntryTypesManager entryTypesManager) throws IOException { StringWriter writer = new StringWriter(); BibWriter bibWriter = new BibWriter(writer, OS.NEWLINE); - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); + FieldWriter fieldWriter = new FieldWriter(fieldPreferences); new BibEntryWriter(fieldWriter, entryTypesManager).write(entry, bibWriter, type); return writer.toString(); } diff --git a/jabgui/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java b/jabgui/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java index 8e9a277d6c2..0671dbde28b 100644 --- a/jabgui/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java @@ -169,7 +169,7 @@ public boolean hasDuplicate(BibEntry entry) { public String getSourceString(BibEntry entry) { StringWriter writer = new StringWriter(); BibWriter bibWriter = new BibWriter(writer, OS.NEWLINE); - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(preferences.getFieldPreferences()); + FieldWriter fieldWriter = new FieldWriter(preferences.getFieldPreferences()); try { // Force reformatting so the displayed BibTeX is consistently formatted new BibEntryWriter(fieldWriter, entryTypesManager).write(entry, bibWriter, selectedDb.getValue().getMode(), true); diff --git a/jabgui/src/main/java/org/jabref/gui/undo/UndoableFieldChange.java b/jabgui/src/main/java/org/jabref/gui/undo/UndoableFieldChange.java index 37c389d18c7..09aa09bfa60 100644 --- a/jabgui/src/main/java/org/jabref/gui/undo/UndoableFieldChange.java +++ b/jabgui/src/main/java/org/jabref/gui/undo/UndoableFieldChange.java @@ -7,21 +7,24 @@ import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldTextMapper; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /// This class represents a change in any field value. The relevant /// information is the BibEntry, the field name, the old and the /// new value. Old/new values can be null. +@NullMarked public class UndoableFieldChange extends AbstractUndoableJabRefEdit { private static final Logger LOGGER = LoggerFactory.getLogger(UndoableFieldChange.class); private final BibEntry entry; private final Field field; - private final String oldValue; - private final String newValue; + @Nullable private final String oldValue; + @Nullable private final String newValue; - public UndoableFieldChange(BibEntry entry, Field field, String oldValue, String newValue) { + public UndoableFieldChange(BibEntry entry, Field field, @Nullable String oldValue, @Nullable String newValue) { this.entry = entry; this.field = field; this.oldValue = oldValue; diff --git a/jablib-examples/jbang/doi_to_bibtex.java b/jablib-examples/jbang/doi_to_bibtex.java index 1a955e9c62b..42506dd6d8f 100644 --- a/jablib-examples/jbang/doi_to_bibtex.java +++ b/jablib-examples/jbang/doi_to_bibtex.java @@ -14,6 +14,8 @@ //JAVA 25+ //RUNTIME_OPTIONS --enable-native-access=ALL-UNNAMED //FILES tinylog.properties=tinylog.properties +// see https://github.com/gradlex-org/extra-java-module-info/issues/237 why we include e-adr here +//DEPS io.github.adr:e-adr:2.0.0 //REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/ //DEPS org.jabref:jablib:6.0-SNAPSHOT diff --git a/jablib-examples/jbang/ieee_pdf_references_to_bibtex.java b/jablib-examples/jbang/ieee_pdf_references_to_bibtex.java index 3e5493c07e9..2a2e7a58cc6 100644 --- a/jablib-examples/jbang/ieee_pdf_references_to_bibtex.java +++ b/jablib-examples/jbang/ieee_pdf_references_to_bibtex.java @@ -16,6 +16,8 @@ //REPOS mavencentral,mavencentralsnapshots=https://central.sonatype.com/repository/maven-snapshots/ //DEPS org.jabref:jablib:6.0-SNAPSHOT +// see https://github.com/gradlex-org/extra-java-module-info/issues/237 why we include e-adr here +//DEPS io.github.adr:e-adr:2.0.0 // JabRef relies on PR https://github.com/unicode-org/icu/pull/2127; for experiments the release version is OK. //DEPS com.ibm.icu:icu4j:78.1 //DEPS com.fasterxml.jackson.core:jackson-annotations:2.20 diff --git a/jablib/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java b/jablib/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java index dd9df5e52da..56e4de2f9ca 100644 --- a/jablib/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java +++ b/jablib/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java @@ -170,15 +170,10 @@ private void writeField(BibEntry entry, BibWriter out, Field field, int indent) if (value.isPresent() && !value.get().trim().isEmpty()) { out.write(" "); out.write(getFormattedFieldName(field, indent)); - try { - int start = out.getCurrentPosition(); - out.write(fieldWriter.write(field, value.get())); - int end = out.getCurrentPosition(); - fieldPositions.put(field, new Range(start, end)); - } catch (InvalidFieldValueException ex) { - LOGGER.warn("Invalid field value {} of field {} of entry {}", value.get(), field, entry.getCitationKey().orElse(""), ex); - throw new IOException("Error in field '" + field + " of entry " + entry.getCitationKey().orElse("") + "': " + ex.getMessage(), ex); - } + int start = out.getCurrentPosition(); + out.write(fieldWriter.write(field, value.get())); + int end = out.getCurrentPosition(); + fieldPositions.put(field, new Range(start, end)); out.writeLine(","); } } @@ -200,7 +195,7 @@ static int getLengthOfLongestFieldName(BibEntry entry) { @ADR(49) static String getFormattedFieldName(Field field, int indent) { String fieldName = field.getName(); - return fieldName + StringUtil.repeatSpaces(indent - fieldName.length()) + " = "; + return fieldName + " ".repeat(Math.max(0, indent - fieldName.length())) + " = "; } public Map getFieldPositions() { diff --git a/jablib/src/main/java/org/jabref/logic/bibtex/FieldWriter.java b/jablib/src/main/java/org/jabref/logic/bibtex/FieldWriter.java index 4b4bd2fd403..b7120e85db5 100644 --- a/jablib/src/main/java/org/jabref/logic/bibtex/FieldWriter.java +++ b/jablib/src/main/java/org/jabref/logic/bibtex/FieldWriter.java @@ -1,9 +1,11 @@ package org.jabref.logic.bibtex; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; -import java.util.StringJoiner; +import java.util.List; +import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.InternalField; @@ -21,28 +23,25 @@ public class FieldWriter { private static final char FIELD_START = '{'; private static final char FIELD_END = '}'; - private final boolean neverFailOnHashes; private final FieldPreferences preferences; - public FieldWriter(FieldPreferences preferences) { - this(true, preferences); + private record BracePosition(int index, int line, int column) { } - private FieldWriter(boolean neverFailOnHashes, FieldPreferences preferences) { - this.neverFailOnHashes = neverFailOnHashes; + public FieldWriter(FieldPreferences preferences) { this.preferences = preferences; } - public static FieldWriter buildIgnoreHashes(FieldPreferences prefs) { - return new FieldWriter(true, prefs); - } + /// Checks text for balanced braces and returns a list of unbalanced + /// + /// @return list of balancing errors + public static List checkBalancedBraces(String text) { + Deque openBraces = new ArrayDeque<>(); + List errors = new ArrayList<>(); - private static void checkBraces(String text) throws InvalidFieldValueException { - Deque queue = new ArrayDeque<>(); int line = 0; int lastLineIndex = 0; - // First we collect all occurrences: for (int i = 0; i < text.length(); i++) { char item = text.charAt(i); if (item == '\n') { @@ -53,31 +52,79 @@ private static void checkBraces(String text) throws InvalidFieldValueException { if (!isEscaped(text, i)) { if (item == '{') { - queue.add("Line %d, column %d (index %d): in '%s'".formatted( - line + 1, i - lastLineIndex + 1, i, getErrorContextSnippet(text, i))); + openBraces.push(new FieldWriter.BracePosition(i, line, i - lastLineIndex + 1)); } else if (item == '}') { - if (queue.pollLast() == null) { - String errorMessage = "Unescaped '}' without matching opening bracket found at line %d, column %d (index %d): in '%s'".formatted( - line + 1, i - lastLineIndex + 1, i, getErrorContextSnippet(text, i)); - LOGGER.error(errorMessage); - throw new InvalidFieldValueException(errorMessage); + if (openBraces.isEmpty()) { + errors.add(Localization.lang("Unbalanced '}' at line %0, column %1 (index %2): in '%3'.", + line + 1, + i - lastLineIndex + 1, + i, + getContextSnippet(text, i))); + } else { + openBraces.pop(); + } + } + } + } + + while (!openBraces.isEmpty()) { + FieldWriter.BracePosition lastOpenBrace = openBraces.pop(); + errors.add(Localization.lang("Unbalanced '{' at line %0, column %1 (index %2): in '%3'.", + lastOpenBrace.line() + 1, + lastOpenBrace.column(), + lastOpenBrace.index(), + getContextSnippet(text, lastOpenBrace.index()))); + } + + return errors; + } + + /// Escapes the last unbalanced curly brace in the given text. + /// + /// @param text the text to sanitize + /// @return sanitized text + public static String sanitizeUnbalancedBraces(String text) { + int length = text.length(); + Deque lastOpenBrace = new ArrayDeque<>(length); + boolean[] toEscape = new boolean[length]; + + // Match braces, mark orphan '}' + for (int i = 0; i < length; i++) { + char currentChar = text.charAt(i); + if (!isEscaped(text, i)) { + if (currentChar == '{') { + // Remember the last open brace + lastOpenBrace.push(i); + } else if (currentChar == '}') { + if (!lastOpenBrace.isEmpty()) { + // Match with the last open brace + lastOpenBrace.pop(); + } else { + // No related opening brace + toEscape[i] = true; } } } } - if (!queue.isEmpty()) { - StringJoiner joiner = new StringJoiner("\n"); - for (String error : queue) { - joiner.add(error); + // Remaining open braces + while (!lastOpenBrace.isEmpty()) { + toEscape[lastOpenBrace.pop()] = true; + } + + // Create the result with escaped unbalanced braces + StringBuilder sanitized = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (toEscape[i]) { + sanitized.append('\\'); } - String errorMessage = "The following unescaped '{' do not have matching closing bracket:\n%s".formatted(joiner); - LOGGER.error(errorMessage); - throw new InvalidFieldValueException(errorMessage); + sanitized.append(text.charAt(i)); } + + return sanitized.toString(); } - private static String getErrorContextSnippet(String text, int index) { + private static String getContextSnippet(String text, int index) { int neighbourSize = 5; return text.substring(Math.max(0, index - neighbourSize), index) + "*" + text.charAt(index) + "*" @@ -90,7 +137,7 @@ private static String getErrorContextSnippet(String text, int index) { /// @param text the input string to check for escaped characters /// @param index the index of the character in the text to check for escaping /// @return true if the character at the specified index is escaped, false otherwise - private static boolean isEscaped(String text, int index) { + public static boolean isEscaped(String text, int index) { int indexCounter = 0; for (int i = index - 1; i >= 0; i--) { if (text.charAt(i) == '\\') { @@ -107,25 +154,25 @@ private static boolean isEscaped(String text, int index) { /// @param field the name of the field - used to trigger different serializations, e.g., turning off resolution for some strings /// @param content the content of the field /// @return a formatted string suitable for output - /// @throws InvalidFieldValueException if content is not a correct bibtex string, e.g., because of improperly balanced braces or using # not paired - public String write(Field field, String content) throws InvalidFieldValueException { + public String write(Field field, String content) { if (content == null) { return FIELD_START + "" + FIELD_END; } + // Always sanitize the braces + String sanitized = sanitizeUnbalancedBraces(content); + if (!shouldResolveStrings(field) || field.equals(InternalField.BIBTEX_STRING)) { - return formatWithoutResolvingStrings(content); + return formatWithoutResolvingStrings(sanitized); } - return formatAndResolveStrings(content); + return formatAndResolveStrings(sanitized); } /// This method handles # in the field content to get valid bibtex strings /// /// For instance, `#jan# - #feb#` gets `jan #{ - } # feb` (see @link{org.jabref.logic.bibtex.LatexFieldFormatterTests#makeHashEnclosedWordsRealStringsInMonthField()}) - private String formatAndResolveStrings(String content) throws InvalidFieldValueException { - checkBraces(content); - + private String formatAndResolveStrings(String content) { content = content.replace("##", ""); StringBuilder stringBuilder = new StringBuilder(); @@ -147,20 +194,13 @@ private String formatAndResolveStrings(String content) throws InvalidFieldValueE } if (pos2 == -1) { - if (neverFailOnHashes) { - pos1 = content.length(); // just write out the rest of the text, and throw no exception - } else { - LOGGER.error("The character {} is not allowed in BibTeX strings unless escaped as in '\\\\{}'. " - + "In JabRef, use pairs of # characters to indicate a string. " - + "Note that the entry causing the problem has been selected. Field value: {}", - BIBTEX_STRING_START_END_SYMBOL, - BIBTEX_STRING_START_END_SYMBOL, - content); - throw new InvalidFieldValueException( - "The character " + BIBTEX_STRING_START_END_SYMBOL + " is not allowed in BibTeX strings unless escaped as in '\\" + BIBTEX_STRING_START_END_SYMBOL + "'.\n" - + "In JabRef, use pairs of # characters to indicate a string.\n" - + "Note that the entry causing the problem has been selected. Field value: " + content); - } + pos1 = content.length(); + LOGGER.warn("The character {} is not allowed in BibTeX strings unless escaped as in '\\\\{}'. " + + "In JabRef, use pairs of # characters to indicate a string. " + + "Field value: {}", + BIBTEX_STRING_START_END_SYMBOL, + BIBTEX_STRING_START_END_SYMBOL, + content); } if (pos1 > pivot) { @@ -208,8 +248,7 @@ private boolean shouldResolveStrings(Field field) { return false; } - private String formatWithoutResolvingStrings(String content) throws InvalidFieldValueException { - checkBraces(content); + private String formatWithoutResolvingStrings(String content) { return FIELD_START + content + FIELD_END; } diff --git a/jablib/src/main/java/org/jabref/logic/bibtex/InvalidFieldValueException.java b/jablib/src/main/java/org/jabref/logic/bibtex/InvalidFieldValueException.java deleted file mode 100644 index 1efac074be9..00000000000 --- a/jablib/src/main/java/org/jabref/logic/bibtex/InvalidFieldValueException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.jabref.logic.bibtex; - -/// @deprecated Use only if you know what you are doing. -/// Otherwise, you should implement your functionality as {@link org.jabref.logic.integrity.IntegrityCheck} instead. -/// The JabRef team leaves the `@deprecated` annotation to have IntelliJ listing this method with a strike-through. -@Deprecated -public class InvalidFieldValueException extends Exception { - - public InvalidFieldValueException(String message) { - super(message); - } -} diff --git a/jablib/src/main/java/org/jabref/logic/bst/BstVMVisitor.java b/jablib/src/main/java/org/jabref/logic/bst/BstVMVisitor.java index 6af2b6ca870..0b4f2d80a32 100644 --- a/jablib/src/main/java/org/jabref/logic/bst/BstVMVisitor.java +++ b/jablib/src/main/java/org/jabref/logic/bst/BstVMVisitor.java @@ -7,7 +7,6 @@ import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.bibtex.InvalidFieldValueException; import org.jabref.model.entry.Month; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; @@ -80,24 +79,21 @@ public Integer visitReadCommand(BstParser.ReadCommandContext ctx) { Field field = FieldFactory.parseField(mEntry.getKey()); String fieldValue = e.entry.getResolvedFieldOrAlias(field, bstVMContext.bibDatabase()) .map(content -> { - try { - String result = fieldWriter.write(field, content); - if (result.startsWith("{")) { - // Strip enclosing {} from the output - return result.substring(1, result.length() - 1); - } - if (field == StandardField.MONTH) { - // We don't have the internal BibTeX strings at hand. - // Thus, we look up the full month name in the generic table. - return Month.parse(result) - .map(Month::getFullName) - .orElse(result); - } - return result; - } catch (InvalidFieldValueException invalidFieldValueException) { - // in case there is something wrong with the content, just return the content itself - return content; + String result = fieldWriter.write(field, content); + if (result.startsWith("{") + && result.endsWith("}") + && !FieldWriter.isEscaped(result, result.length() - 1)) { + // Strip enclosing {} from the output + return result.substring(1, result.length() - 1); } + if (field == StandardField.MONTH) { + // We don't have the internal BibTeX strings at hand. + // Thus, we look up the full month name in the generic table. + return Month.parse(result) + .map(Month::getFullName) + .orElse(result); + } + return result; }) .orElse(null); mEntry.setValue(fieldValue); diff --git a/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java index bbea4760167..cced366b611 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java @@ -19,7 +19,6 @@ import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.bibtex.InvalidFieldValueException; import org.jabref.logic.bibtex.comparator.BibtexStringComparator; import org.jabref.logic.bibtex.comparator.CrossRefEntryComparator; import org.jabref.logic.bibtex.comparator.FieldComparator; @@ -372,18 +371,14 @@ protected void writeString(BibtexString bibtexString, int maxKeyLength) throws I bibWriter.writeLine(userComments); } - bibWriter.write(STRING_PREFIX + "{" + bibtexString.getName() + StringUtil - .repeatSpaces(maxKeyLength - bibtexString.getName().length()) + " = "); + bibWriter.write(STRING_PREFIX + "{" + bibtexString.getName() + + " ".repeat(maxKeyLength - bibtexString.getName().length()) + " = "); if (bibtexString.getContent().isEmpty()) { bibWriter.write("{}"); } else { - try { - String formatted = new FieldWriter(fieldPreferences) - .write(InternalField.BIBTEX_STRING, bibtexString.getContent()); - bibWriter.write(formatted); - } catch (InvalidFieldValueException ex) { - throw new IOException(ex); - } + String formatted = new FieldWriter(fieldPreferences) + .write(InternalField.BIBTEX_STRING, bibtexString.getContent()); + bibWriter.write(formatted); } bibWriter.writeLine("}"); diff --git a/jablib/src/main/java/org/jabref/logic/exporter/EmbeddedBibFilePdfExporter.java b/jablib/src/main/java/org/jabref/logic/exporter/EmbeddedBibFilePdfExporter.java index 9bc56dccc29..aac7891ed76 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/EmbeddedBibFilePdfExporter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/EmbeddedBibFilePdfExporter.java @@ -152,7 +152,7 @@ private void embedBibTex(String bibTeX, Path path) throws IOException { private String getBibString(List entries) throws IOException { StringWriter stringWriter = new StringWriter(); BibWriter bibWriter = new BibWriter(stringWriter, OS.NEWLINE); - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); + FieldWriter fieldWriter = new FieldWriter(fieldPreferences); BibEntryWriter bibEntryWriter = new BibEntryWriter(fieldWriter, bibEntryTypesManager); for (BibEntry entry : entries) { bibEntryWriter.write(entry, bibWriter, bibDatabaseMode); diff --git a/jablib/src/main/java/org/jabref/logic/integrity/BracketChecker.java b/jablib/src/main/java/org/jabref/logic/integrity/BracketChecker.java index 70ba1f65bf0..26188506689 100644 --- a/jablib/src/main/java/org/jabref/logic/integrity/BracketChecker.java +++ b/jablib/src/main/java/org/jabref/logic/integrity/BracketChecker.java @@ -1,34 +1,17 @@ package org.jabref.logic.integrity; +import java.util.List; import java.util.Optional; -import org.jabref.logic.l10n.Localization; -import org.jabref.logic.util.strings.StringUtil; +import org.jabref.logic.bibtex.FieldWriter; public class BracketChecker implements ValueChecker { @Override public Optional checkValue(String value) { - if (StringUtil.isBlank(value)) { - return Optional.empty(); - } - - // metaphor: integer-based stack (push + / pop -) - int counter = 0; - for (char a : value.trim().toCharArray()) { - if (a == '{') { - counter++; - } else if (a == '}') { - if (counter == 0) { - return Optional.of(Localization.lang("unexpected closing curly bracket")); - } else { - counter--; - } - } - } - - if (counter > 0) { - return Optional.of(Localization.lang("unexpected opening curly bracket")); + List errors = FieldWriter.checkBalancedBraces(value); + if (!errors.isEmpty()) { + return Optional.of(String.join("\n", errors)); } return Optional.empty(); diff --git a/jablib/src/main/java/org/jabref/logic/util/strings/StringUtil.java b/jablib/src/main/java/org/jabref/logic/util/strings/StringUtil.java index db54a63652b..b71288a0213 100644 --- a/jablib/src/main/java/org/jabref/logic/util/strings/StringUtil.java +++ b/jablib/src/main/java/org/jabref/logic/util/strings/StringUtil.java @@ -569,29 +569,6 @@ public static String replaceSpecialCharacters(String s) { return result; } - /// Return a String with n spaces - /// - /// @param n Number of spaces - /// @return String with n spaces - public static String repeatSpaces(int n) { - return repeat(Math.max(0, n), ' '); - } - - /// Return a String with n copies of the char c - /// - /// @param n Number of copies - /// @param c char to copy - /// @return String with n copies of c - public static String repeat(int n, char c) { - StringBuilder resultSB = new StringBuilder(n); - - for (int i = 0; i < n; i++) { - resultSB.append(c); - } - - return resultSB.toString(); - } - public static boolean isNullOrEmpty(@Nullable String toTest) { return (toTest == null) || toTest.isEmpty(); } @@ -747,7 +724,7 @@ public static String alignStringTable(List> table) { sb.append(WRAPPED_LINE_PREFIX); sb.append(pair.getKey()); - sb.append(StringUtil.repeatSpaces(padding)); + sb.append(" ".repeat(padding)); sb.append(STRING_TABLE_DELIMITER); sb.append(pair.getValue()); diff --git a/jablib/src/main/java/org/jabref/model/entry/BibEntry.java b/jablib/src/main/java/org/jabref/model/entry/BibEntry.java index ce6a878b17b..1d6796f9992 100644 --- a/jablib/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/jablib/src/main/java/org/jabref/model/entry/BibEntry.java @@ -680,7 +680,7 @@ public String toString() { FieldPreferences fieldPreferences) { try (StringWriter writer = new StringWriter()) { BibWriter bibWriter = new BibWriter(writer, "\n"); - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); + FieldWriter fieldWriter = new FieldWriter(fieldPreferences); BibEntryWriter bibEntryWriter = new BibEntryWriter(fieldWriter, entryTypesManager); bibEntryWriter.write(entry, bibWriter, type, true); return writer.toString(); diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 697fd806c6e..005a7c14dd1 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -1416,8 +1416,6 @@ DOI\ %0\ is\ invalid=DOI %0 is invalid Same\ DOI\ used\ in\ multiple\ entries=Same DOI used in multiple entries should\ start\ with\ a\ name=should start with a name should\ end\ with\ a\ name=should end with a name -unexpected\ closing\ curly\ bracket=unexpected closing curly bracket -unexpected\ opening\ curly\ bracket=unexpected opening curly bracket capital\ letters\ are\ not\ masked\ using\ curly\ brackets\ {}=capital letters are not masked using curly brackets {} should\ contain\ a\ four\ digit\ number=should contain a four digit number should\ contain\ a\ valid\ page\ number\ range=should contain a valid page number range @@ -1565,6 +1563,8 @@ Remove\ word\ enclosing\ braces=Remove word enclosing braces Removes\ braces\ encapsulating\ a\ complete\ word\ and\ the\ complete\ field\ content.=Removes braces encapsulating a complete word and the complete field content. Removes\ braces\ encapsulating\ the\ complete\ field\ content.=Removes braces encapsulating the complete field content. Removes\ all\ balanced\ {}\ braces\ around\ words.=Removes all balanced {} braces around words. +Unbalanced\ '{'\ at\ line\ %0,\ column\ %1\ (index\ %2)\:\ in\ '%3'.=Unbalanced '{' at line %0, column %1 (index %2): in '%3'. +Unbalanced\ '}'\ at\ line\ %0,\ column\ %1\ (index\ %2)\:\ in\ '%3'.=Unbalanced '}' at line %0, column %1 (index %2): in '%3'. Shorten\ %0=Shorten %0 Shorten\ DOI=Shorten DOI Shortened\ DOI\ to\:\ %0=Shortened DOI to: %0 diff --git a/jablib/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java b/jablib/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java index dd9f1e6ff00..82dec4f4bdc 100644 --- a/jablib/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java +++ b/jablib/src/test/java/org/jabref/logic/bibtex/BibEntryWriterTest.java @@ -33,7 +33,6 @@ import org.mockito.Answers; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -656,15 +655,6 @@ void doNotWriteEmptyFields() throws IOException { assertEquals(expected, stringWriter.toString()); } - @Test - void writeThrowsErrorIfFieldContainsUnbalancedBraces() { - BibEntry entry = new BibEntry(StandardEntryType.Article) - .withField(StandardField.NOTE, "some text with unbalanced { braces") - .withChanged(true); - - assertThrows(IOException.class, () -> bibEntryWriter.write(entry, bibWriter, BibDatabaseMode.BIBTEX)); - } - @Test void roundTripWithPrecedingCommentTest() throws Exception { String bibtexEntry = """ diff --git a/jablib/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java b/jablib/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java index 57a6927949b..487f50cd323 100644 --- a/jablib/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java +++ b/jablib/src/test/java/org/jabref/logic/bibtex/FieldWriterTest.java @@ -12,10 +12,12 @@ 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.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotEquals; class FieldWriterTest { @@ -56,14 +58,14 @@ void setUp() { @ParameterizedTest @MethodSource - void keepHashSignInComment(String text) throws InvalidFieldValueException { + void keepHashSignInComment(String text) { String writeResult = writer.write(StandardField.COMMENT, text); String resultWithLfAsNewLineSeparator = StringUtil.unifyLineBreaks(writeResult, "\n"); assertEquals("{" + text + "}", resultWithLfAsNewLineSeparator); } @Test - void noNormalizationOfNewlinesInAbstractField() throws InvalidFieldValueException { + void noNormalizationOfNewlinesInAbstractField() { String text = "lorem" + OS.NEWLINE + " ipsum lorem ipsum\nlorem ipsum \rlorem ipsum\r\ntest"; String result = writer.write(StandardField.ABSTRACT, text); // The normalization is done at org.jabref.logic.exporter.BibWriter, so no need to normalize here @@ -72,7 +74,7 @@ void noNormalizationOfNewlinesInAbstractField() throws InvalidFieldValueExceptio } @Test - void preserveNewlineInAbstractField() throws InvalidFieldValueException { + void preserveNewlineInAbstractField() { String text = "lorem ipsum lorem ipsum" + OS.NEWLINE + "lorem ipsum lorem ipsum"; String result = writer.write(StandardField.ABSTRACT, text); @@ -82,7 +84,7 @@ void preserveNewlineInAbstractField() throws InvalidFieldValueException { } @Test - void preserveMultipleNewlinesInAbstractField() throws InvalidFieldValueException { + void preserveMultipleNewlinesInAbstractField() { String text = "lorem ipsum lorem ipsum" + OS.NEWLINE + OS.NEWLINE + "lorem ipsum lorem ipsum"; String result = writer.write(StandardField.ABSTRACT, text); @@ -92,7 +94,7 @@ void preserveMultipleNewlinesInAbstractField() throws InvalidFieldValueException } @Test - void preserveNewlineInReviewField() throws InvalidFieldValueException { + void preserveNewlineInReviewField() { String text = "lorem ipsum lorem ipsum" + OS.NEWLINE + "lorem ipsum lorem ipsum"; String result = writer.write(StandardField.REVIEW, text); @@ -102,7 +104,7 @@ void preserveNewlineInReviewField() throws InvalidFieldValueException { } @Test - void whitespaceFromNonMultiLineFieldsKept() throws InvalidFieldValueException { + void whitespaceFromNonMultiLineFieldsKept() { // This was a decision on 2024-06-15 when fixing https://github.com/JabRef/jabref/issues/4877 // We want to have a clean architecture for reading and writing // Normalizing is done during write (and not during read) @@ -118,92 +120,91 @@ void whitespaceFromNonMultiLineFieldsKept() throws InvalidFieldValueException { assertEquals(expected, any); } - @Test - void reportUnbalancedBracing() { - String unbalanced = "{"; - - assertThrows(InvalidFieldValueException.class, () -> writer.write(new UnknownField("anyfield"), unbalanced)); - } - - @Test - void reportUnbalancedBracingWithEscapedClosingBraces() { - String unbalanced = "{\\}"; - - assertThrows(InvalidFieldValueException.class, () -> writer.write(new UnknownField("anyfield"), unbalanced)); - } - - @Test - void reportUnbalancedBracingWithEscapedOpeningBraces() { - String unbalanced = "\\{}"; - - assertThrows(InvalidFieldValueException.class, () -> writer.write(new UnknownField("anyfield"), unbalanced)); + @ParameterizedTest + @CsvSource({ + "'\\{', '{'", + "'\\}', '}'", + "'\\{\\{', '{{'", + "'\\{\\}', '{\\}'", + "'\\{\\}', '\\{}'", + "'Incorporating evolutionary {Measures into Conservation Prioritization}', 'Incorporating evolutionary {Measures into Conservation Prioritization}'", + "'Incorporating {\\O}evolutionary {Measures into Conservation Prioritization}', 'Incorporating {\\O}evolutionary {Measures into Conservation Prioritization}'", + }) + void unbalancedBracesSanitized(String expected, String input) { + assertEquals("{" + expected + "}", writer.write(StandardField.COMMENT, input)); } - @Test - void tolerateBalancedBrace() throws InvalidFieldValueException { - String text = "Incorporating evolutionary {Measures into Conservation Prioritization}"; - - assertEquals("{" + text + "}", writer.write(new UnknownField("anyfield"), text)); + @ParameterizedTest + @ValueSource(strings = { + "{", + "}", + "{{", + "\\{}"}) + void checkUnbalancedBraces(String input) { + assertNotEquals(List.of(), FieldWriter.checkBalancedBraces(input)); } - @Test - void tolerateEscapeCharacters() throws InvalidFieldValueException { - String text = "Incorporating {\\O}evolutionary {Measures into Conservation Prioritization}"; - - assertEquals("{" + text + "}", writer.write(new UnknownField("anyfield"), text)); + @ParameterizedTest + @ValueSource(strings = { + "\\{\\}", + "Incorporating evolutionary {Measures into Conservation Prioritization}", + "Incorporating {\\O}evolutionary {Measures into Conservation Prioritization}", + "{Measures into Conservation Prioritization}"}) + void checkBalancedBraces(String input) { + assertEquals(List.of(), FieldWriter.checkBalancedBraces(input)); } @Test - void hashEnclosedWordsGetRealStringsInMonthField() throws InvalidFieldValueException { + void hashEnclosedWordsGetRealStringsInMonthField() { String text = "#jan# - #feb#"; assertEquals("jan # { - } # feb", writer.write(StandardField.MONTH, text)); } @Test - void hashWorksSimple() throws InvalidFieldValueException { + void hashWorksSimple() { String text = "#text"; assertEquals("{#text}", writer.write(StandardField.MONTH, text)); } @Test - void escapedHashWorksSimple() throws InvalidFieldValueException { + void escapedHashWorksSimple() { String text = "\\#text"; assertEquals("{\\#text}", writer.write(StandardField.MONTH, text)); } @Test - void doubleHashesRemoved() throws InvalidFieldValueException { + void doubleHashesRemoved() { String text = "te##xt"; assertEquals("{text}", writer.write(StandardField.MONTH, text)); } @Test - void multipleSpacesNotShrunkOnSingleLineField() throws InvalidFieldValueException { + void multipleSpacesNotShrunkOnSingleLineField() { String text = "t w o"; assertEquals("{t w o}", writer.write(StandardField.MONTH, text)); } @Test - void doubleSpacesAreKept() throws InvalidFieldValueException { + void doubleSpacesAreKept() { String text = " text "; assertEquals("{ text }", writer.write(StandardField.MONTH, text)); } @Test - void spacesAreNotTrimmedAtMultilineField() throws InvalidFieldValueException { + void spacesAreNotTrimmedAtMultilineField() { String text = " text "; assertEquals("{ text }", writer.write(StandardField.COMMENT, text)); // Note: Spaces are trimmed at BibDatabaseWriter#applySaveActions } @Test - void multipleSpacesKeptOnMultiLineField() throws InvalidFieldValueException { + void multipleSpacesKeptOnMultiLineField() { String text = "t w o"; assertEquals("{t w o}", writer.write(StandardField.COMMENT, text)); } @Test - void finalNewLineIsKeptAtMultilineField() throws InvalidFieldValueException { + void finalNewLineIsKeptAtMultilineField() { String text = " text " + OS.NEWLINE; assertEquals("{" + text + "}", writer.write(StandardField.COMMENT, text)); // Note: Spaces are trimmed at BibDatabaseWriter#applySaveActions diff --git a/jablib/src/test/java/org/jabref/logic/util/strings/StringUtilTest.java b/jablib/src/test/java/org/jabref/logic/util/strings/StringUtilTest.java index f2a356bcf5b..d1981945232 100644 --- a/jablib/src/test/java/org/jabref/logic/util/strings/StringUtilTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/strings/StringUtilTest.java @@ -388,31 +388,6 @@ void replaceSpecialCharactersWithNonNormalizedUnicode() { assertEquals("Modele", StringUtil.replaceSpecialCharacters("Modè€le")); } - static Stream testRepeatSpacesData() { - return Stream.of( - Arguments.of("", -1), - Arguments.of("", 0), - Arguments.of(" ", 1), - Arguments.of(" ", 7) - ); - } - - @ParameterizedTest - @MethodSource("testRepeatSpacesData") - void repeatSpaces(String result, int count) { - assertEquals(result, StringUtil.repeatSpaces(count)); - } - - @ParameterizedTest - @CsvSource(textBlock = """ - '', 0, a - a, 1, a - aaaaaaa, 7, a - """) - void repeat(String expected, int count, char character) { - assertEquals(expected, StringUtil.repeat(count, character)); - } - @Test void boldHTML() { assertEquals("AA", StringUtil.boldHTML("AA"));