Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

### Fixed

- Fixed issues with escaping keywords in "Keywords Editor" [#14780](https://github.com/JabRef/jabref/issues/14780)
- We fixed an issue where institutional authors in braces {Institutional Author} triggered a "Names are not in the standard format" warning. [#15157](https://github.com/JabRef/jabref/issues/15157)

### Removed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import java.net.URL;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.swing.undo.UndoManager;

Expand Down Expand Up @@ -113,7 +115,7 @@ public KeywordsEditor(Field field,
keywordTagsField.setMatcher((keyword, searchText) -> keyword.get().toLowerCase().startsWith(searchText.toLowerCase()));
keywordTagsField.setComparator(Comparator.comparing(Keyword::get));

keywordTagsField.setNewItemProducer(searchText -> KeywordsEditorViewModel.getStringConverter().fromString(searchText));
keywordTagsField.setNewItemProducer(searchText -> viewModel.parseKeyword(searchText));

keywordTagsField.setShowSearchIcon(false);
keywordTagsField.setOnMouseClicked(_ -> keywordTagsField.getEditor().requestFocus());
Expand All @@ -124,7 +126,11 @@ public KeywordsEditor(Field field,
String keywordSeparator = String.valueOf(viewModel.getKeywordSeparator());
keywordTagsField.getEditor().setOnKeyReleased(event -> {
if (event.getText().equals(keywordSeparator)) {
keywordTagsField.commit();
String editorText = keywordTagsField.getEditor().getText();

if (isSeparatedKeyword(editorText, keywordSeparator)) {
keywordTagsField.commit();
}
event.consume();
}
});
Expand Down Expand Up @@ -155,6 +161,22 @@ public KeywordsEditor(Field field,
Bindings.bindContentBidirectional(keywordTagsField.getTags(), viewModel.keywordListProperty());
}

private boolean isSeparatedKeyword(String keywordString, String keywordSeparator) {
int separatorLastOccurrence = keywordString.lastIndexOf(keywordSeparator);
if (separatorLastOccurrence == -1) {
return false;
}

int separatorFirstOccurrence = keywordString.lastIndexOf(keywordSeparator);
String substringWithSeparator = new StringBuilder(keywordString.substring(0, separatorFirstOccurrence)).reverse().toString();

AtomicBoolean isSeparatedKeyword = new AtomicBoolean(true);
substringWithSeparator.chars().takeWhile(symbol -> symbol == Keyword.DEFAULT_ESCAPE_SYMBOL)
.forEachOrdered(_ -> isSeparatedKeyword.set(!isSeparatedKeyword.get()));

return isSeparatedKeyword.get();
}

private Node createTag(Keyword keyword) {
Label tagLabel = new Label();
tagLabel.setText(keywordTagsField.getConverter().toString(keyword));
Expand All @@ -172,7 +194,7 @@ private Node createTag(Keyword keyword) {
tagLabel.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) {
keywordTagsField.removeTags(keyword);
keywordTagsField.getEditor().setText(keyword.get());
keywordTagsField.getEditor().setText(KeywordList.serialize(List.of(keyword), viewModel.getKeywordSeparator()));
keywordTagsField.getEditor().positionCaret(keyword.get().length());
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.jabref.model.entry.KeywordList;
import org.jabref.model.entry.field.Field;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -86,14 +87,19 @@ public List<Keyword> getSuggestions(String request) {
.distinct()
.collect(Collectors.toList());

Keyword requestedKeyword = new Keyword(request);
Keyword requestedKeyword = parseKeyword(request);
if (!suggestions.contains(requestedKeyword)) {
suggestions.addFirst(requestedKeyword);
}

return suggestions;
}

@Nullable
public Keyword parseKeyword(String keyword) {
return KeywordList.parse(keyword, keywordSeparator).stream().findFirst().orElse(null);
}

public Character getKeywordSeparator() {
return keywordSeparator;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,70 @@
package org.jabref.gui.fieldeditors;

import java.util.List;

import javax.swing.undo.UndoManager;

import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.model.entry.BibEntryPreferences;
import org.jabref.model.entry.Keyword;
import org.jabref.model.entry.KeywordList;
import org.jabref.model.entry.field.Field;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class KeywordsEditorViewModelTest {
private KeywordsEditorViewModel viewModel;

@BeforeEach
void setUp() {
SuggestionProvider<String> suggestionProvider = mock(SuggestionProvider.class);
CliPreferences cliPreferences = mock(CliPreferences.class);

BibEntryPreferences bibEntryPreferences = mock(BibEntryPreferences.class);

when(cliPreferences.getBibEntryPreferences()).thenReturn(bibEntryPreferences);
when(bibEntryPreferences.getKeywordSeparator()).thenReturn(',');
when(suggestionProvider.getPossibleSuggestions()).thenReturn(List.of("value", "key\\,\\\\", "parent > node > child", "father \\> inheritor"));
viewModel = new KeywordsEditorViewModel(mock(Field.class), suggestionProvider, mock(FieldCheckers.class), cliPreferences, mock(UndoManager.class));
}

@Test
void getSuggestionsWithEscapedSeparator() {
String request = "key";
assertEquals(List.of(new Keyword(request), new Keyword("key\\,\\\\")), viewModel.getSuggestions(request));
}

@Test
void getSuggestionsWithEscapedHierarchicalDelimiter() {
String request = "father";
assertEquals(List.of(new Keyword(request), new Keyword("father \\> inheritor")), viewModel.getSuggestions(request));
}

@Test
void parseKeywordWithHierarchicalKeywords() {
String hierarchichalString = "parent > node > child";
Keyword parsedKeyword = KeywordList.parse(hierarchichalString, viewModel.getKeywordSeparator()).get(0);

assertEquals(parsedKeyword, viewModel.parseKeyword(hierarchichalString));
}

@Test
void parseKeywordWithMultipleKeywords() {
String multipleKeysStr = "key1, key2";
Keyword firstParsedKeyword = KeywordList.parse(multipleKeysStr, viewModel.getKeywordSeparator()).get(0);

assertEquals(firstParsedKeyword, viewModel.parseKeyword(multipleKeysStr));
}

@Test
void stringConverterWithHierarchicalKeywords() {
void stringConverterToStringWithHierarchicalKeywords() {
String hierarchichalString = "parent > node > child";
Keyword keyword = Keyword.ofHierarchical(hierarchichalString);

Expand Down
1 change: 1 addition & 0 deletions jablib/src/main/java/org/jabref/model/entry/Keyword.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
public class Keyword extends ChainNode<Keyword> implements Comparable<Keyword> {

public static Character DEFAULT_HIERARCHICAL_DELIMITER = '>';
public static final Character DEFAULT_ESCAPE_SYMBOL = '\\';
private final String keyword;

public Keyword(@NonNull String keyword) {
Expand Down
9 changes: 5 additions & 4 deletions jablib/src/main/java/org/jabref/model/entry/KeywordList.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public static KeywordList parse(@NonNull String keywordString, @NonNull Characte
if (isEscaping.get()) {
currentToken.append(currentChar);
isEscaping.set(false);
} else if (currentChar == '\\') {
} else if (currentChar == Keyword.DEFAULT_ESCAPE_SYMBOL) {
isEscaping.set(true);
} else if (currentChar == Keyword.DEFAULT_HIERARCHICAL_DELIMITER) {
hierarchy.add(currentToken.toString().trim());
Expand All @@ -90,15 +90,16 @@ public static KeywordList parse(@NonNull String keywordString, @NonNull Characte

public static String serialize(List<Keyword> keywords, Character delimiter) {
String delimiterStr = delimiter.toString();
String escapedDelimiter = "\\" + delimiterStr;
String escapeSequenceStr = Keyword.DEFAULT_ESCAPE_SYMBOL.toString();
String escapedDelimiter = escapeSequenceStr + delimiterStr;
String hierarchicalDelimiterStr = Keyword.DEFAULT_HIERARCHICAL_DELIMITER.toString();
String escapedHierarchicalDelimiter = "\\" + hierarchicalDelimiterStr;
String escapedHierarchicalDelimiter = escapeSequenceStr + hierarchicalDelimiterStr;
String hierarchicalSeparator = " " + hierarchicalDelimiterStr + " ";

return keywords.stream()
.map(keyword -> keyword.flatten().stream()
.map(Keyword::get)
.map(nodeKeyword -> nodeKeyword.replace("\\", "\\\\"))
.map(nodeKeyword -> nodeKeyword.replace(escapeSequenceStr, escapeSequenceStr + escapeSequenceStr))
.map(nodeKeyword -> nodeKeyword.replace(delimiterStr, escapedDelimiter))
.map(nodeKeyword -> nodeKeyword.replace(hierarchicalDelimiterStr, escapedHierarchicalDelimiter))
.collect(Collectors.joining(hierarchicalSeparator)))
Expand Down
Loading