From f6ea6a9ed8ec1bf8b45a0fb51d70d215e8dd9b15 Mon Sep 17 00:00:00 2001 From: Subhramit Basu Bhowmick Date: Sat, 24 Aug 2024 18:19:49 +0530 Subject: [PATCH] CSL4LibreOffice - D [GSoC '24] (#11636) * Refactor CitationStyleGeneratorTest * Add test: [StAX] Parse title, isNumericStyle * Add CSLFormatUtils * Refactor CSLCitationOOAdapter and add JavaDoc * Add test for citeproc DIN 1505-2 * Better method names, more javadoc * Add tests for CSLFormatUtils * Add javadoc for adapter * Fix locales * Fix submodules for styles * OpenRewrite * Fix submodules for styles * Fix locales * Rename test method * Change order of methods in mark manager * Fix submodules for styles * Fix locales * Disable test * Review actions - I * Better javadoc for disabled test * Remove "public" as per best practices * Review changes [3] * Swap arguments for CitationStyleTest * Add comment to @Disabled --- .../logic/citationstyle/CitationStyle.java | 4 +- .../oocsltext/CSLCitationOOAdapter.java | 240 ++---- .../openoffice/oocsltext/CSLFormatUtils.java | 162 ++++ .../oocsltext/CSLReferenceMarkManager.java | 34 +- .../CitationStyleGeneratorTest.java | 53 +- .../citationstyle/CitationStyleTest.java | 63 +- .../oocsltext/CSLFormatUtilsTest.java | 712 ++++++++++++++++++ 7 files changed, 1068 insertions(+), 200 deletions(-) create mode 100644 src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java create mode 100644 src/test/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtilsTest.java diff --git a/src/main/java/org/jabref/logic/citationstyle/CitationStyle.java b/src/main/java/org/jabref/logic/citationstyle/CitationStyle.java index 61d15761158..2da81dd1d54 100644 --- a/src/main/java/org/jabref/logic/citationstyle/CitationStyle.java +++ b/src/main/java/org/jabref/logic/citationstyle/CitationStyle.java @@ -26,6 +26,7 @@ import org.jabref.logic.openoffice.style.OOStyle; import org.jabref.logic.util.StandardFileType; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,7 +77,8 @@ private static Optional createCitationStyleFromSource(final Input public record StyleInfo(String title, boolean isNumericStyle) { } - private static Optional parseStyleInfo(String filename, String content) { + @VisibleForTesting + static Optional parseStyleInfo(String filename, String content) { FACTORY.setProperty(XMLInputFactory.IS_COALESCING, true); try { diff --git a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java index 4f4385763ff..11b2f9d0053 100644 --- a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java +++ b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLCitationOOAdapter.java @@ -4,14 +4,12 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; -import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jabref.logic.citationkeypattern.BracketedPattern; import org.jabref.logic.citationstyle.CitationStyle; import org.jabref.logic.citationstyle.CitationStyleGenerator; -import org.jabref.logic.citationstyle.CitationStyleOutputFormat; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.AuthorList; import org.jabref.model.entry.BibEntry; @@ -27,17 +25,14 @@ import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextDocument; import com.sun.star.uno.Exception; -import org.apache.commons.text.StringEscapeUtils; +/** + * This class processes CSL citations in JabRef and interacts directly with LibreOffice using an XTextDocument instance. + * It is tightly coupled with {@link CSLReferenceMarkManager} for management of reference marks tied to the CSL citations. + * Any method in this class is NOT supposed to be moved. + */ public class CSLCitationOOAdapter { - // TODO: These are static final fields right now, should add the functionality to let user select these and store them in preferences. - public static final String BIBLIOGRAPHY_TITLE = "References"; - public static final String BIBLIOGRAPHY_HEADER_PARAGRAPH_FORMAT = "Heading 2"; - private static final int MAX_ALPHA_AUTHORS = 4; - - private static final Pattern YEAR_IN_CITATION_PATTERN = Pattern.compile("(.)(.*), (\\d{4}.*)"); - private final CitationStyleOutputFormat format = CitationStyleOutputFormat.HTML; private final XTextDocument document; private final CSLReferenceMarkManager markManager; @@ -50,42 +45,10 @@ public void readExistingMarks() throws WrappedTargetException, NoSuchElementExce markManager.readExistingMarks(); } - public void insertBibliography(XTextCursor cursor, CitationStyle selectedStyle, List entries, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager bibEntryTypesManager) - throws WrappedTargetException, CreationException { - - OOText title = OOFormat.paragraph(OOText.fromString(BIBLIOGRAPHY_TITLE), BIBLIOGRAPHY_HEADER_PARAGRAPH_FORMAT); - OOTextIntoOO.write(document, cursor, OOText.fromString(title.toString())); - OOText ooBreak = OOFormat.paragraph(OOText.fromString(""), "Body Text"); - OOTextIntoOO.write(document, cursor, ooBreak); - - String style = selectedStyle.getSource(); - - // Sort entries based on their order of appearance in the document - entries.sort(Comparator.comparingInt(entry -> markManager.getCitationNumber(entry.getCitationKey().orElse("")))); - for (BibEntry entry : entries) { - String citation = CitationStyleGenerator.generateCitation(List.of(entry), style, format, bibDatabaseContext, bibEntryTypesManager).getFirst(); - String citationKey = entry.getCitationKey().orElse(""); - int currentNumber = markManager.getCitationNumber(citationKey); - - String formattedCitation; - if (selectedStyle.isNumericStyle()) { - formattedCitation = updateSingleCitation(transformHtml(citation), currentNumber); - } else { - formattedCitation = transformHtml(citation); - } - OOText ooText = OOFormat.setLocaleNone(OOText.fromString(formattedCitation)); - - OOTextIntoOO.write(document, cursor, ooText); - if (selectedStyle.isNumericStyle()) { - // Select the paragraph break - cursor.goLeft((short) 1, true); - - // Delete the selected content (paragraph break) - cursor.setString(""); - } - } - } - + /** + * Inserts a citation for a group of entries. + * Comparable to LaTeX's \cite command. + */ public void insertCitation(XTextCursor cursor, CitationStyle selectedStyle, List entries, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager bibEntryTypesManager) throws CreationException, IOException, Exception { String style = selectedStyle.getSource(); @@ -93,15 +56,15 @@ public void insertCitation(XTextCursor cursor, CitationStyle selectedStyle, List String inTextCitation; if (isAlphanumeric) { - inTextCitation = generateAlphanumericCitation(entries, bibDatabaseContext); + inTextCitation = CSLFormatUtils.generateAlphanumericCitation(entries, bibDatabaseContext); } else { - inTextCitation = CitationStyleGenerator.generateInText(entries, style, format, bibDatabaseContext, bibEntryTypesManager).getText(); + inTextCitation = CitationStyleGenerator.generateInText(entries, style, CSLFormatUtils.OUTPUT_FORMAT, bibDatabaseContext, bibEntryTypesManager).getText(); } - String formattedCitation = transformHtml(inTextCitation); + String formattedCitation = CSLFormatUtils.transformHTML(inTextCitation); if (selectedStyle.isNumericStyle()) { - formattedCitation = updateMultipleCitations(formattedCitation, entries); + formattedCitation = updateSingleOrMultipleCitationNumbers(formattedCitation, entries); } OOText ooText = OOFormat.setLocaleNone(OOText.fromString(formattedCitation)); @@ -110,7 +73,7 @@ public void insertCitation(XTextCursor cursor, CitationStyle selectedStyle, List } /** - * Inserts the in-text citation for a group of entries. + * Inserts in-text citations for a group of entries. * Comparable to LaTeX's \citet command. * * @implNote Very similar to the {@link #insertCitation(XTextCursor, CitationStyle, List, BibDatabaseContext, BibEntryTypesManager)} method.insertInText method @@ -126,7 +89,7 @@ public void insertInTextCitation(XTextCursor cursor, CitationStyle selectedStyle String inTextCitation; if (isAlphanumeric) { // Generate the alphanumeric citation - inTextCitation = generateAlphanumericCitation(List.of(currentEntry), bibDatabaseContext); + inTextCitation = CSLFormatUtils.generateAlphanumericCitation(List.of(currentEntry), bibDatabaseContext); // Get the author's name String authorName = currentEntry.getResolvedFieldOrAlias(StandardField.AUTHOR, bibDatabaseContext.getDatabase()) .map(AuthorList::parse) @@ -135,12 +98,12 @@ public void insertInTextCitation(XTextCursor cursor, CitationStyle selectedStyle // Combine author name with the citation inTextCitation = authorName + " " + inTextCitation; } else { - inTextCitation = CitationStyleGenerator.generateInText(List.of(currentEntry), style, format, bibDatabaseContext, bibEntryTypesManager).getText(); + inTextCitation = CitationStyleGenerator.generateInText(List.of(currentEntry), style, CSLFormatUtils.OUTPUT_FORMAT, bibDatabaseContext, bibEntryTypesManager).getText(); } - String formattedCitation = transformHtml(inTextCitation); + String formattedCitation = CSLFormatUtils.transformHTML(inTextCitation); String finalText; if (selectedStyle.isNumericStyle()) { - formattedCitation = updateMultipleCitations(formattedCitation, List.of(currentEntry)); + formattedCitation = updateSingleOrMultipleCitationNumbers(formattedCitation, List.of(currentEntry)); String prefix = currentEntry.getResolvedFieldOrAlias(StandardField.AUTHOR, bibDatabaseContext.getDatabase()) .map(AuthorList::parse) .map(list -> BracketedPattern.joinAuthorsOnLastName(list, 1, "", " et al.") + " ") @@ -149,7 +112,7 @@ public void insertInTextCitation(XTextCursor cursor, CitationStyle selectedStyle } else if (isAlphanumeric) { finalText = formattedCitation; } else { - finalText = changeToInText(formattedCitation); + finalText = CSLFormatUtils.changeToInText(formattedCitation); } if (iterator.hasNext()) { finalText += ","; @@ -160,14 +123,10 @@ public void insertInTextCitation(XTextCursor cursor, CitationStyle selectedStyle } } - private String changeToInText(String formattedCitation) { - Matcher matcher = YEAR_IN_CITATION_PATTERN.matcher(formattedCitation); - if (matcher.find()) { - return matcher.group(2) + " " + matcher.group(1) + matcher.group(3); - } - return formattedCitation; - } - + /** + * Inserts "empty" citations for a list of entries at the cursor to the document. + * Adds the entries to the list for which bibliography is to be generated. + */ public void insertEmpty(XTextCursor cursor, List entries) throws CreationException, Exception { for (BibEntry entry : entries) { @@ -180,6 +139,54 @@ public void insertEmpty(XTextCursor cursor, List entries) cursor.collapseToEnd(); } + /** + * Creates a "Bibliography" section in the document and inserts a list of references. + * The list is generated based on the existing citations, in-text citations and empty citations in the document. + */ + public void insertBibliography(XTextCursor cursor, CitationStyle selectedStyle, List entries, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager bibEntryTypesManager) + throws WrappedTargetException, CreationException { + + OOText title = OOFormat.paragraph(OOText.fromString(CSLFormatUtils.DEFAULT_BIBLIOGRAPHY_TITLE), CSLFormatUtils.DEFAULT_BIBLIOGRAPHY_HEADER_PARAGRAPH_FORMAT); + OOTextIntoOO.write(document, cursor, OOText.fromString(title.toString())); + OOText ooBreak = OOFormat.paragraph(OOText.fromString(""), "Body Text"); + OOTextIntoOO.write(document, cursor, ooBreak); + + String style = selectedStyle.getSource(); + + // Sort entries based on their order of appearance in the document + entries.sort(Comparator.comparingInt(entry -> markManager.getCitationNumber(entry.getCitationKey().orElse("")))); + for (BibEntry entry : entries) { + String citation = CitationStyleGenerator.generateCitation(List.of(entry), style, CSLFormatUtils.OUTPUT_FORMAT, bibDatabaseContext, bibEntryTypesManager).getFirst(); + String citationKey = entry.getCitationKey().orElse(""); + int currentNumber = markManager.getCitationNumber(citationKey); + + String formattedCitation; + if (selectedStyle.isNumericStyle()) { + formattedCitation = CSLFormatUtils.updateSingleBibliographyNumber(CSLFormatUtils.transformHTML(citation), currentNumber); + } else { + formattedCitation = CSLFormatUtils.transformHTML(citation); + } + OOText ooText = OOFormat.setLocaleNone(OOText.fromString(formattedCitation)); + + OOTextIntoOO.write(document, cursor, ooText); + if (selectedStyle.isNumericStyle()) { + // Select the paragraph break + cursor.goLeft((short) 1, true); + + // Delete the selected content (paragraph break) + cursor.setString(""); + } + } + } + + /** + * Inserts multiple references and also adds a space before the citation if not already present ("smart space"). + * + * @implNote It is difficult to "segment" a single citation generated for a group of entries into distinct parts based on the entries such that each entry can be draped with its corresponding reference mark. + * This is because of the sheer variety in the styles of citations and the separators between them (when grouped) in case of Citation Style Language. + * Furthermore, it is also difficult to generate a "single" reference mark for a group of entries. + * Thus, in case of citations for a group of entries, we first insert the citation (text), then insert the invisible reference marks for each entry separately after it. + */ private void insertMultipleReferenceMarks(XTextCursor cursor, List entries, OOText ooText) throws CreationException, Exception { boolean preceedingSpaceExists; @@ -218,9 +225,9 @@ private void insertMultipleReferenceMarks(XTextCursor cursor, List ent } /** - * Transforms the numbers in the citation to globally-unique numbers + * Transforms the numbers in the citation to globally-unique (and thus, reusable) numbers. */ - private String updateMultipleCitations(String citation, List entries) { + private String updateSingleOrMultipleCitationNumbers(String citation, List entries) { Pattern pattern = Pattern.compile("(\\D*)(\\d+)(\\D*)"); Matcher matcher = pattern.matcher(citation); StringBuilder sb = new StringBuilder(); @@ -238,107 +245,20 @@ private String updateMultipleCitations(String citation, List entries) return sb.toString(); } - public static String updateSingleCitation(String citation, int currentNumber) { - Pattern pattern = Pattern.compile("(\\[|\\()?(\\d+)(\\]|\\))?(\\.)?\\s*"); - Matcher matcher = pattern.matcher(citation); - StringBuilder sb = new StringBuilder(); - boolean numberReplaced = false; - - while (matcher.find()) { - if (!numberReplaced) { - String prefix = matcher.group(1) != null ? matcher.group(1) : ""; - String suffix = matcher.group(3) != null ? matcher.group(3) : ""; - String dot = matcher.group(4) != null ? "." : ""; - String space = matcher.group().endsWith(" ") ? " " : ""; - - String replacement = prefix + currentNumber + suffix + dot + space; - - matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); - numberReplaced = true; - } else { - matcher.appendReplacement(sb, matcher.group()); - } - } - matcher.appendTail(sb); - return sb.toString(); - } - /** - * Transforms provided HTML into a format that can be fully parsed by OOTextIntoOO.write(...) - * The transformed HTML can be used for inserting into a LibreOffice document - * Context: The HTML produced by CitationStyleGenerator.generateCitation(...) is not directly (completely) parsable by OOTextIntoOO.write(...) - * For more details, read the documentation of the write(...) method in the {@link OOTextIntoOO} class. - * Additional Information. - * - * @param html The HTML string to be transformed into OO-write ready HTML. - * @return The formatted html string + * Checks if an entry has already been cited before in the document. + * Required for consistent numbering of numeric citations - if present, the number is to be reused, else a new number is to be assigned. */ - private String transformHtml(String html) { - // Initial clean up of escaped characters - html = StringEscapeUtils.unescapeHtml4(html); - - // Handle margins (spaces between citation number and text) - html = html.replaceAll("
(.*?)
(.*?)
", "$1 $2"); - - // Remove unsupported tags - html = html.replaceAll("]*>", ""); - html = html.replace("", ""); - - // Remove unsupported links - html = html.replaceAll("]*>", ""); - html = html.replace("", ""); - - // Replace span tags with inline styles for bold - html = html.replaceAll("(.*?)", "$1"); - - // Replace span tags with inline styles for italic - html = html.replaceAll("(.*?)", "$1"); - - // Replace span tags with inline styles for underline - html = html.replaceAll("(.*?)", "$1"); - - html = html.replaceAll("(.*?)", "$1"); - - // Clean up any remaining span tags - html = html.replaceAll("]*>", ""); - - return html; - } - public boolean isCitedEntry(BibEntry entry) { String citationKey = entry.getCitationKey().orElse(""); return markManager.hasCitationForKey(citationKey); } - private String generateAlphanumericCitation(List entries, BibDatabaseContext bibDatabaseContext) { - StringBuilder citation = new StringBuilder("["); - for (int i = 0; i < entries.size(); i++) { - BibEntry entry = entries.get(i); - Optional author = entry.getResolvedFieldOrAlias(StandardField.AUTHOR, bibDatabaseContext.getDatabase()); - Optional year = entry.getResolvedFieldOrAlias(StandardField.YEAR, bibDatabaseContext.getDatabase()); - - if (author.isPresent() && year.isPresent()) { - AuthorList authorList = AuthorList.parse(author.get()); - String alphaKey = BracketedPattern.authorsAlpha(authorList); - - // Extract last two digits of the year - String shortYear = year.get().length() >= 2 ? - year.get().substring(year.get().length() - 2) : - year.get(); - - citation.append(alphaKey).append(shortYear); - } else { - citation.append(entry.getCitationKey().orElse("")); - } - - if (i < entries.size() - 1) { - citation.append("; "); - } - } - citation.append("]"); - return citation.toString(); - } - + /** + * Currently, we have support for one alphanumeric CSL style. + * There is no tag or field in .csl style files that can be parsed to determine if it is an alphanumeric style. + * Thus, we currently hardcode the check for "DIN 1505-2". + */ private boolean isAlphanumericStyle(CitationStyle style) { return "DIN 1505-2 (alphanumeric, Deutsch) - standard superseded by ISO-690".equals(style.getTitle()); } diff --git a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java new file mode 100644 index 00000000000..0666a411cf7 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtils.java @@ -0,0 +1,162 @@ +package org.jabref.logic.openoffice.oocsltext; + +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jabref.logic.citationkeypattern.BracketedPattern; +import org.jabref.logic.citationstyle.CitationStyleOutputFormat; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.AuthorList; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.ootext.OOTextIntoOO; + +import com.sun.star.text.XTextCursor; +import com.sun.star.text.XTextDocument; +import org.apache.commons.text.StringEscapeUtils; + +/** + * Contains utility constants and methods for processing of CSL citations as generated by methods of citeproc-java ({@link org.jabref.logic.citationstyle.CitationStyleGenerator}). + *

These methods are used in {@link CSLCitationOOAdapter} which inserts CSL citation text into an OO document.

+ */ +public class CSLFormatUtils { + + // TODO: These are static final fields right now, should add the functionality to let user select these and store them in preferences. + public static final String DEFAULT_BIBLIOGRAPHY_TITLE = "References"; + public static final String DEFAULT_BIBLIOGRAPHY_HEADER_PARAGRAPH_FORMAT = "Heading 2"; + + public static final CitationStyleOutputFormat OUTPUT_FORMAT = CitationStyleOutputFormat.HTML; + private static final Pattern YEAR_IN_CITATION_PATTERN = Pattern.compile("(.)(.*), (\\d{4}.*)"); + + /** + * Transforms provided HTML into a format that can be fully parsed and inserted into an OO document. + * Context: The HTML produced by {@link org.jabref.logic.citationstyle.CitationStyleGenerator#generateCitation(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateCitation} or {@link org.jabref.logic.citationstyle.CitationStyleGenerator#generateInText(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateInText} is not directly (completely) parsable by by {@link OOTextIntoOO#write(XTextDocument, XTextCursor, OOText) write}. + * For more details, read the documentation for the {@link OOTextIntoOO} class. + * Additional Information. + * + * @param html The HTML string to be transformed into OO-write ready HTML. + * @return The formatted html string. + */ + public static String transformHTML(String html) { + // Initial clean up of escaped characters + html = StringEscapeUtils.unescapeHtml4(html); + + // Handle margins (spaces between citation number and text) + html = html.replaceAll("
(.*?)
(.*?)
", "$1 $2"); + + // Remove unsupported tags + html = html.replaceAll("]*>", ""); + html = html.replace("", ""); + + // Remove unsupported links + html = html.replaceAll("]*>", ""); + html = html.replace("", ""); + + // Replace span tags with inline styles for bold + html = html.replaceAll("(.*?)", "$1"); + + // Replace span tags with inline styles for italic + html = html.replaceAll("(.*?)", "$1"); + + // Replace span tags with inline styles for underline + html = html.replaceAll("(.*?)", "$1"); + + html = html.replaceAll("(.*?)", "$1"); + + // Clean up any remaining span tags + html = html.replaceAll("]*>", ""); + + return html; + } + + /** + * Alphanumeric citations are not natively supported by citeproc-java (see {@link org.jabref.logic.citationstyle.CitationStyleGenerator#generateInText(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateInText}). + * Thus, we manually format a citation to produce its alphanumeric form. + * + * @param entries the list of entries for which the alphanumeric citation is to be generated. + * @return the alphanumeric citation (for a single entry or a group of entries). + */ + public static String generateAlphanumericCitation(List entries, BibDatabaseContext bibDatabaseContext) { + StringBuilder citation = new StringBuilder("["); + for (int i = 0; i < entries.size(); i++) { + BibEntry entry = entries.get(i); + Optional author = entry.getResolvedFieldOrAlias(StandardField.AUTHOR, bibDatabaseContext.getDatabase()); + Optional year = entry.getResolvedFieldOrAlias(StandardField.YEAR, bibDatabaseContext.getDatabase()); + + if (author.isPresent() && year.isPresent()) { + AuthorList authorList = AuthorList.parse(author.get()); + String alphaKey = BracketedPattern.authorsAlpha(authorList); + + // Extract last two digits of the year + String shortYear = year.get().length() >= 2 ? + year.get().substring(year.get().length() - 2) : + year.get(); + + citation.append(alphaKey).append(shortYear); + } else { + citation.append(entry.getCitationKey().orElse("")); + } + + if (i < entries.size() - 1) { + citation.append("; "); + } + } + citation.append("]"); + return citation.toString(); + } + + /** + * Method to update citation number of a bibliographic entry (to be inserted in the list of references). + * By default, citeproc-java ({@link org.jabref.logic.citationstyle.CitationStyleGenerator#generateCitation(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateCitation} always start the numbering of a list of citations with "1". + * If a citation doesn't correspond to the first cited entry, the number should be changed to the relevant current citation number. + * If an entries has been cited before, the colder number should be reused. + * The number can be enclosed in different formats, such as "1", "1.", "1)", "(1)" or "[1]". + *

+ * Precondition: Use ONLY with numeric citation styles.

+ * + * @param citation the numeric citation with an unresolved number. + * @param currentNumber the correct number to update the citation with. + * @return the bibliographic citation with resolved number. + */ + public static String updateSingleBibliographyNumber(String citation, int currentNumber) { + Pattern pattern = Pattern.compile("(\\[|\\()?(\\d+)(\\]|\\))?(\\.)?\\s*"); + Matcher matcher = pattern.matcher(citation); + StringBuilder sb = new StringBuilder(); + boolean numberReplaced = false; + + while (matcher.find()) { + if (!numberReplaced) { + String prefix = matcher.group(1) != null ? matcher.group(1) : ""; + String suffix = matcher.group(3) != null ? matcher.group(3) : ""; + String dot = matcher.group(4) != null ? "." : ""; + String space = matcher.group().endsWith(" ") ? " " : ""; + + String replacement = prefix + currentNumber + suffix + dot + space; + + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + numberReplaced = true; + } else { + matcher.appendReplacement(sb, matcher.group()); + } + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * Extracts year from a citation having single or multiple entries, for the purpose of using in in-text citations. + * + * @param formattedCitation the citation cleaned up and formatted using {@link CSLFormatUtils#transformHTML transformHTML}. + */ + public static String changeToInText(String formattedCitation) { + Matcher matcher = YEAR_IN_CITATION_PATTERN.matcher(formattedCitation); + if (matcher.find()) { + return matcher.group(2) + " " + matcher.group(1) + matcher.group(3); + } + return formattedCitation; + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLReferenceMarkManager.java b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLReferenceMarkManager.java index 027b6662c23..42440572166 100644 --- a/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLReferenceMarkManager.java +++ b/src/main/java/org/jabref/logic/openoffice/oocsltext/CSLReferenceMarkManager.java @@ -43,6 +43,21 @@ public CSLReferenceMarkManager(XTextDocument document) { this.citationKeyToNumber = new HashMap<>(); } + public CSLReferenceMark createReferenceMark(BibEntry entry) throws Exception { + String citationKey = entry.getCitationKey().orElse(CUID.randomCUID2(8).toString()); + int citationNumber = getCitationNumber(citationKey); + CSLReferenceMark referenceMark = CSLReferenceMark.of(citationKey, citationNumber, factory); + addMark(referenceMark); + return referenceMark; + } + + public void addMark(CSLReferenceMark mark) { + marksByName.put(mark.getName(), mark); + idsByMark.put(mark, marksByID.size()); + marksByID.add(mark); + updateCitationInfo(mark.getName()); + } + public void readExistingMarks() throws WrappedTargetException, NoSuchElementException { XReferenceMarksSupplier supplier = UnoRuntime.queryInterface(XReferenceMarksSupplier.class, document); XNameAccess marks = supplier.getReferenceMarks(); @@ -72,26 +87,11 @@ private void updateCitationInfo(String name) { } } - public void addMark(CSLReferenceMark mark) { - marksByName.put(mark.getName(), mark); - idsByMark.put(mark, marksByID.size()); - marksByID.add(mark); - updateCitationInfo(mark.getName()); + public boolean hasCitationForKey(String citationKey) { + return citationKeyToNumber.containsKey(citationKey); } public int getCitationNumber(String citationKey) { return citationKeyToNumber.computeIfAbsent(citationKey, k -> ++highestCitationNumber); } - - public CSLReferenceMark createReferenceMark(BibEntry entry) throws Exception { - String citationKey = entry.getCitationKey().orElse(CUID.randomCUID2(8).toString()); - int citationNumber = getCitationNumber(citationKey); - CSLReferenceMark referenceMark = CSLReferenceMark.of(citationKey, citationNumber, factory); - addMark(referenceMark); - return referenceMark; - } - - public boolean hasCitationForKey(String citationKey) { - return citationKeyToNumber.containsKey(citationKey); - } } diff --git a/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java b/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java index df3571b3ca1..3ee3a339fc0 100644 --- a/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java +++ b/src/test/java/org/jabref/logic/citationstyle/CitationStyleGeneratorTest.java @@ -1,5 +1,6 @@ package org.jabref.logic.citationstyle; +import java.io.IOException; import java.util.List; import java.util.stream.Stream; @@ -13,6 +14,8 @@ import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; +import de.undercouch.citeproc.output.Citation; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -22,15 +25,16 @@ class CitationStyleGeneratorTest { + private final BibEntry testEntry = TestEntry.getTestEntry(); + private final BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(List.of(testEntry))); private final BibEntryTypesManager bibEntryTypesManager = new BibEntryTypesManager(); + private final List styleList = CitationStyle.discoverCitationStyles(); @Test void aCMCitation() { - BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(List.of(TestEntry.getTestEntry()))); context.setMode(BibDatabaseMode.BIBLATEX); - List styleList = CitationStyle.discoverCitationStyles(); - CitationStyle style = styleList.stream().filter(e -> "ACM SIGGRAPH".equals(e.getTitle())).findAny().orElse(null); - String citation = CitationStyleGenerator.generateCitation(List.of(TestEntry.getTestEntry()), style.getSource(), CitationStyleOutputFormat.HTML, context, new BibEntryTypesManager()).getFirst(); + CitationStyle style = styleList.stream().filter(e -> "ACM SIGGRAPH".equals(e.getTitle())).findAny().get(); + String citation = CitationStyleGenerator.generateCitation(List.of(testEntry), style.getSource(), CitationStyleOutputFormat.HTML, context, bibEntryTypesManager).getFirst(); // if the acm-siggraph.csl citation style changes this has to be modified String expected = "
" @@ -43,11 +47,9 @@ void aCMCitation() { @Test void aPACitation() { - BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(List.of(TestEntry.getTestEntry()))); context.setMode(BibDatabaseMode.BIBLATEX); - List styleList = CitationStyle.discoverCitationStyles(); - CitationStyle style = styleList.stream().filter(e -> "American Psychological Association 7th edition".equals(e.getTitle())).findAny().orElse(null); - String citation = CitationStyleGenerator.generateCitation(List.of(TestEntry.getTestEntry()), style.getSource(), CitationStyleOutputFormat.HTML, context, new BibEntryTypesManager()).getFirst(); + CitationStyle style = styleList.stream().filter(e -> "American Psychological Association 7th edition".equals(e.getTitle())).findAny().get(); + String citation = CitationStyleGenerator.generateCitation(List.of(testEntry), style.getSource(), CitationStyleOutputFormat.HTML, context, bibEntryTypesManager).getFirst(); // if the apa-7th-citation.csl citation style changes this has to be modified String expected = "
" @@ -58,6 +60,21 @@ void aPACitation() { assertEquals(expected, citation); } + /** + * Fails due to citeproc-java ({@link CitationStyleGenerator#generateInText(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateInText}) returning an empty citation. + * Alphanumeric citations are thus, currently manually generated by formatting (see {@link org.jabref.logic.openoffice.oocsltext.CSLFormatUtils#generateAlphanumericCitation(List, BibDatabaseContext) generateAlphaNumericCitation}). + */ + @Test + @Disabled("Till alphanumeric citations are supported by citeproc-java") + void din1502AlphanumericInTextCitation() throws IOException { + context.setMode(BibDatabaseMode.BIBLATEX); + CitationStyle style = styleList.stream().filter(e -> "DIN 1505-2 (alphanumeric, Deutsch) - standard superseded by ISO-690".equals(e.getTitle())).findAny().get(); + Citation citation = CitationStyleGenerator.generateInText(List.of(testEntry), style.getSource(), CitationStyleOutputFormat.HTML, context, bibEntryTypesManager); + String inTextCitationText = citation.getText(); + + assertEquals("[Smit2016]", inTextCitationText); + } + @Test void ignoreNewLine() { BibEntry entry = new BibEntry(); @@ -97,11 +114,10 @@ void htmlFormat() { "
[1]
B. Smith, B. Jones, and J. Williams, “Title of the test entry,” BibTeX Journal, vol. 34, no. 3, pp. 45–67, Jul. 2016, doi: 10.1001/bla.blubb.
\n" + "
\n"; - BibEntry entry = TestEntry.getTestEntry(); String style = CitationStyle.getDefault().getSource(); CitationStyleOutputFormat format = CitationStyleOutputFormat.HTML; - String actualCitation = CitationStyleGenerator.generateCitation(List.of(entry), style, format, new BibDatabaseContext(), bibEntryTypesManager).getFirst(); + String actualCitation = CitationStyleGenerator.generateCitation(List.of(testEntry), style, format, context, bibEntryTypesManager).getFirst(); assertEquals(expectedCitation, actualCitation); } @@ -109,11 +125,10 @@ void htmlFormat() { void textFormat() { String expectedCitation = "[1]B. Smith, B. Jones, and J. Williams, “Title of the test entry,” BibTeX Journal, vol. 34, no. 3, pp. 45–67, Jul. 2016, doi: 10.1001/bla.blubb.\n"; - BibEntry entry = TestEntry.getTestEntry(); String style = CitationStyle.getDefault().getSource(); CitationStyleOutputFormat format = CitationStyleOutputFormat.TEXT; - String actualCitation = CitationStyleGenerator.generateCitation(List.of(entry), style, format, new BibDatabaseContext(new BibDatabase(List.of(entry))), bibEntryTypesManager).getFirst(); + String actualCitation = CitationStyleGenerator.generateCitation(List.of(testEntry), style, format, context, bibEntryTypesManager).getFirst(); assertEquals(expectedCitation, actualCitation); } @@ -134,12 +149,11 @@ void handleDiacritics() { @Test void handleAmpersand() { String expectedCitation = "[1]B. Smith, B. Jones, and J. Williams, “Famous quote: “&TitleTest&” - that is it,” BibTeX Journal, vol. 34, no. 3, pp. 45–67, Jul. 2016, doi: 10.1001/bla.blubb.\n"; - BibEntry entry = TestEntry.getTestEntry(); - entry.setField(StandardField.TITLE, "Famous quote: “&TitleTest&” - that is it"); + testEntry.setField(StandardField.TITLE, "Famous quote: “&TitleTest&” - that is it"); String style = CitationStyle.getDefault().getSource(); CitationStyleOutputFormat format = CitationStyleOutputFormat.TEXT; - String actualCitation = CitationStyleGenerator.generateCitation(List.of(entry), style, format, new BibDatabaseContext(), bibEntryTypesManager).getFirst(); + String actualCitation = CitationStyleGenerator.generateCitation(List.of(testEntry), style, format, context, bibEntryTypesManager).getFirst(); assertEquals(expectedCitation, actualCitation); } @@ -208,7 +222,7 @@ static Stream cslMapping() { .withField(StandardField.ISSUE, "7") .withField(StandardField.EID, "e0270533"), "ieee.csl"), - Arguments.of( + Arguments.of( "[1]F. Last and J. Doe, no. 33, pp. 7–8.\n", BibDatabaseMode.BIBLATEX, new BibEntry(StandardEntryType.Article) @@ -574,15 +588,14 @@ static Stream cslMapping() { @ParameterizedTest @MethodSource - void cslMapping(String expected, BibDatabaseMode mode, BibEntry entry, String cslFileName) throws Exception { - BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(new BibDatabase(List.of(entry))); - bibDatabaseContext.setMode(mode); + void cslMapping(String expected, BibDatabaseMode mode, BibEntry entry, String cslFileName) { + context.setMode(mode); String citation = CitationStyleGenerator.generateCitation( List.of(entry), CitationStyle.createCitationStyleFromFile(cslFileName).orElseThrow().getSource(), CitationStyleOutputFormat.TEXT, - bibDatabaseContext, + context, bibEntryTypesManager).getFirst(); assertEquals(expected, citation); } diff --git a/src/test/java/org/jabref/logic/citationstyle/CitationStyleTest.java b/src/test/java/org/jabref/logic/citationstyle/CitationStyleTest.java index 421a941f35d..8423e534659 100644 --- a/src/test/java/org/jabref/logic/citationstyle/CitationStyleTest.java +++ b/src/test/java/org/jabref/logic/citationstyle/CitationStyleTest.java @@ -1,6 +1,8 @@ package org.jabref.logic.citationstyle; import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; import org.jabref.logic.util.TestEntry; import org.jabref.model.database.BibDatabase; @@ -9,14 +11,18 @@ import org.jabref.model.entry.BibEntryTypesManager; 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; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class CitationStyleTest { @Test - void getDefault() throws Exception { + void getDefault() { assertNotNull(CitationStyle.getDefault()); } @@ -37,8 +43,61 @@ void defaultCitation() { } @Test - void discoverCitationStylesNotNull() throws Exception { + void discoverCitationStylesNotNull() { List styleList = CitationStyle.discoverCitationStyles(); assertNotNull(styleList); } + + @ParameterizedTest + @MethodSource + void citationStylePresent(String cslFileName) { + Optional citationStyle = CitationStyle.createCitationStyleFromFile(cslFileName); + assertTrue(citationStyle.isPresent()); + } + + static Stream citationStylePresent() { + return Stream.of( + Arguments.of("ieee.csl"), + Arguments.of("apa.csl"), + Arguments.of("vancouver.csl"), + Arguments.of("chicago-author-date.csl"), + Arguments.of("nature.csl") + ); + } + + @ParameterizedTest + @MethodSource + void titleMatches(String expectedTitle, String cslFileName) { + Optional citationStyle = CitationStyle.createCitationStyleFromFile(cslFileName); + CitationStyle.StyleInfo styleInfo = new CitationStyle.StyleInfo(citationStyle.get().getTitle(), citationStyle.get().isNumericStyle()); + assertEquals(expectedTitle, styleInfo.title()); + } + + static Stream titleMatches() { + return Stream.of( + Arguments.of("IEEE", "ieee.csl"), + Arguments.of("American Psychological Association 7th edition", "apa.csl"), + Arguments.of("Vancouver", "vancouver.csl"), + Arguments.of("Chicago Manual of Style 17th edition (author-date)", "chicago-author-date.csl"), + Arguments.of("Nature", "nature.csl") + ); + } + + @ParameterizedTest + @MethodSource + void numericPropertyMatches(boolean expectedNumericNature, String cslFileName) { + Optional citationStyle = CitationStyle.createCitationStyleFromFile(cslFileName); + CitationStyle.StyleInfo styleInfo = new CitationStyle.StyleInfo(citationStyle.get().getTitle(), citationStyle.get().isNumericStyle()); + assertEquals(expectedNumericNature, styleInfo.isNumericStyle()); + } + + private static Stream numericPropertyMatches() { + return Stream.of( + Arguments.of(true, "ieee.csl"), + Arguments.of(false, "apa.csl"), + Arguments.of(true, "vancouver.csl"), + Arguments.of(false, "chicago-author-date.csl"), + Arguments.of(true, "nature.csl") + ); + } } diff --git a/src/test/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtilsTest.java b/src/test/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtilsTest.java new file mode 100644 index 00000000000..ada5ac67c42 --- /dev/null +++ b/src/test/java/org/jabref/logic/openoffice/oocsltext/CSLFormatUtilsTest.java @@ -0,0 +1,712 @@ +package org.jabref.logic.openoffice.oocsltext; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Stream; + +import org.jabref.logic.citationstyle.CitationStyle; +import org.jabref.logic.citationstyle.CitationStyleGenerator; +import org.jabref.logic.citationstyle.CitationStyleOutputFormat; +import org.jabref.logic.util.TestEntry; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabaseMode; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.openoffice.ootext.OOText; + +import de.undercouch.citeproc.output.Citation; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.jabref.logic.openoffice.oocsltext.CSLFormatUtils.generateAlphanumericCitation; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CSLFormatUtilsTest { + + private static final List STYLE_LIST = CitationStyle.discoverCitationStyles(); + + private final BibEntry testEntry = TestEntry.getTestEntry(); + private final BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(List.of(TestEntry.getTestEntry()))); + private final BibEntryTypesManager bibEntryTypesManager = new BibEntryTypesManager(); + + /** + * Test to check transformation of raw, unsupported HTML into OO-ready HTML. + */ + @ParameterizedTest + @MethodSource + void ooHTMLTransformFromRawHTML(String expected, String rawHtml) { + String actual = CSLFormatUtils.transformHTML(rawHtml); + assertEquals(expected, actual); + } + + static Stream ooHTMLTransformFromRawHTML() { + return Stream.of( + + // region: general test cases for unescaping HTML entities + + // Ampersand (& entity) + Arguments.of( + "Smith & Jones", + "Smith & Jones" + ), + + // Non-breaking space (  entity) + Arguments.of( + "Text with non-breaking spaces", + "Text with non-breaking spaces" + ), + + // Bold formatting, less than, greater than symbols (<, > entities) + Arguments.of( + "Bold Text", + "<b>Bold Text</b>" + ), + + // endregion + + // Handling margins + Arguments.of( + "[1] Citation text", + "
[1]
Citation text
" + ), + + // Removing unsupported div tags + Arguments.of( + "Aligned text", + "
Aligned text
" + ), + + // Removing unsupported links + Arguments.of( + "Text with link", + "Text with link" + ), + + // Replacing span tags with inline styles for bold + Arguments.of( + "Text with bold", + "Text with bold" + ), + + // Replacing span tags with inline styles for italic + Arguments.of( + "Text with italic", + "Text with italic" + ), + + // Replacing span tags with inline styles for underline + Arguments.of( + "Text with underline", + "Text with underline" + ), + + // Replacing span tags with inline styles for small-caps + Arguments.of( + "Text with small caps", + "Text with small caps" + ), + + // Test case for cleaning up remaining span tags + Arguments.of( + "Text with unnecessary span", + "Text with unnecessary span" + ), + + // Test case combining multiple transformations + Arguments.of( + "[1] Author, \"Title,\" Journal, vol. 1, no. 1, pp. 1-10, 2023.", + "
[1]
Author, "Title," Journal, vol. 1, no. 1, pp. 1-10, 2023.
" + ), + + // Comprehensive test + Arguments.of( + "[1] Smith & Jones, " + + "\"Analysis of in HTML,\" " + + "Journal of Web Development, " + + "vol. 1, no. 1, pp. 1-10, 2023. " + + "https://doi.org/10.1000/example", + + "
[1]
" + + "Smith & Jones, " + + ""Analysis of <code> in HTML," " + + "Journal of Web Development, " + + "vol. 1, no. 1, pp. 1-10, 2023. " + + "https://doi.org/10.1000/example
" + ) + ); + } + + /** + * Test to check correct transformation of raw CSL bibliography generated by citeproc-java methods into OO-ready text. + *

+ * Precondition: This test assumes that {@link CitationStyleGenerator#generateCitation(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateCitation} works as expected. + *

+ */ + @ParameterizedTest + @MethodSource + void ooHTMLTransformFromRawBibliography(String expected, CitationStyle style) { + String citation = CitationStyleGenerator.generateCitation(List.of(testEntry), style.getSource(), CSLFormatUtils.OUTPUT_FORMAT, context, bibEntryTypesManager).getFirst(); + String actual = CSLFormatUtils.transformHTML(citation); + assertEquals(expected, actual); + } + + static Stream ooHTMLTransformFromRawBibliography() { + return Stream.of( + + // Non-numeric, parentheses, commas, full stops, slashes, hyphens, colons, italics + Arguments.of( + " Smith, B., Jones, B., & Williams, J. (2016). Title of the test entry. BibTeX Journal, 34(3), 45–67. https://doi.org/10.1001/bla.blubb\n", + STYLE_LIST.stream().filter(e -> "American Psychological Association 7th edition".equals(e.getTitle())).findAny().get() + ), + + // Numeric type "[1]", brackets, newlines + Arguments.of( + " \n" + + " [1] B. Smith, B. Jones, and J. Williams, “Title of the test entry,” BibTeX Journal, vol. 34, no. 3, pp. 45–67, Jul. 2016, doi: 10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "IEEE".equals(e.getTitle())).findAny().get() + ), + + // Numeric type "1." + Arguments.of( + " \n" + + " 1. Smith, B., Jones, B., Williams, J.: Title of the test entry. BibTeX Journal. 34, 45–67 (2016). https://doi.org/10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Springer - Lecture Notes in Computer Science".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + " Smith, Bill, Bob Jones, and Jeff Williams. 2016. “Title of the Test Entry.” Edited by Phil Taylor. BibTeX Journal 34 (3): 45–67. https://doi.org/10.1001/bla.blubb.\n", + STYLE_LIST.stream().filter(e -> "Chicago Manual of Style 17th edition (author-date)".equals(e.getTitle())).findAny().get() + ), + + // Semicolons + Arguments.of( + " \n" + + " 1. Smith B, Jones B, Williams J. Title of the test entry. Taylor P, editor. BibTeX Journal [Internet]. 2016 Jul;34(3):45–67. Available from: https://github.com/JabRef\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Vancouver".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + " \n" + + " 1. Smith, B., Jones, B. & Williams, J. Title of the test entry. BibTeX Journal 34, 45–67 (2016).\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Nature".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + " \n" + + " 1. Smith B, Jones B, Williams J. Title of the test entry. Taylor P, ed. BibTeX Journal. 2016;34(3):45-67. doi:10.1001/bla.blubb\n" + + " \n", + STYLE_LIST.stream().filter(e -> "American Medical Association 11th edition".equals(e.getTitle())).findAny().get() + ), + + // Small-caps + Arguments.of( + " Smith, B., Jones, B., Williams, J. (2016) Title of the test entry Taylor, P. (ed.). BibTeX Journal, 34(3), pp. 45–67.\n", + STYLE_LIST.stream().filter(e -> "De Montfort University - Harvard".equals(e.getTitle())).findAny().get() + ), + + // Underlines + Arguments.of( + " Smith, Bill, Bob Jones, and Jeff Williams. “Title of the test entry.” Ed. Phil Taylor. BibTeX Journal 34.3 (2016): 45–67. .\n", + STYLE_LIST.stream().filter(e -> "Modern Language Association 7th edition (underline)".equals(e.getTitle())).findAny().get() + ), + + // Non-breaking spaces + Arguments.of( + " Smith, Bill, Bob Jones, & Jeff Williams, “Title of the test entry,” BibTeX Journal, 2016, vol. 34, no. 3, pp. 45–67.\n", + STYLE_LIST.stream().filter(e -> "Histoire & Mesure (Français)".equals(e.getTitle())).findAny().get() + ), + + // Numeric with a full stop - "1." + Arguments.of( + " \n" + + " 1. Smith, B., Jones, B. and Williams, J. 2016. Title of the test entry. BibTeX Journal. 34: 45–67.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "The Journal of Veterinary Medical Science".equals(e.getTitle())).findAny().get() + ), + + // Bold text, bold numeric with a full stop - "1." + Arguments.of( + " \n" + + " 1. Smith B, Jones B, Williams J. Title of the test entry. BibTeX Journal 2016 ; 34 : 45–67.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Acta Orthopædica Belgica".equals(e.getTitle())).findAny().get() + ), + + // Naked numeric - "1" + Arguments.of( + " \n" + + " 1 Smith Bill, Jones Bob, Williams Jeff. Title of the test entry. BibTeX Journal 2016;34(3):45–67. Doi: 10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Acta Anaesthesiologica Taiwanica".equals(e.getTitle())).findAny().get() + ), + + // Numeric in parentheses - "(1)" + Arguments.of( + " \n" + + " (1) Smith, B.; Jones, B.; Williams, J. Title of the Test Entry. BibTeX Journal 2016, 34 (3), 45–67. https://doi.org/10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "American Chemical Society".equals(e.getTitle())).findAny().get() + ), + + // Numeric with right parenthesis - "1)" + Arguments.of( + " \n" + + " 1) Smith B., Jones B., Williams J., BibTeX Journal, 34, 45–67 (2016).\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Chemical and Pharmaceutical Bulletin".equals(e.getTitle())).findAny().get() + ), + + // Numeric in superscript - "1" + Arguments.of( + " 1 B. Smith, B. Jones, and J. Williams, “Title of the test entry,” BibTeX Journal 34(3), 45–67 (2016).\n", + STYLE_LIST.stream().filter(e -> "American Institute of Physics 4th edition".equals(e.getTitle())).findAny().get() + ) + ); + } + + /** + * Test to check correct transformation of raw CSL citation with a single entry generated by citeproc-java methods into OO-ready text. + *

+ * Precondition: This test assumes that {@link CitationStyleGenerator#generateInText(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateInText} works as expected. + *

+ */ + @ParameterizedTest + @MethodSource + void ooHTMLTransformFromCitationWithSingleEntry(String expected, CitationStyle style) throws IOException { + Citation citation = CitationStyleGenerator.generateInText(List.of(testEntry), style.getSource(), CSLFormatUtils.OUTPUT_FORMAT, context, bibEntryTypesManager); + String inTextCitationText = citation.getText(); + String actual = CSLFormatUtils.transformHTML(inTextCitationText); + OOText ooText = OOText.fromString(actual); + assertEquals(OOText.fromString(expected), ooText); + } + + static Stream ooHTMLTransformFromCitationWithSingleEntry() { + return Stream.of( + + Arguments.of( + "(Smith et al., 2016)", + STYLE_LIST.stream().filter(e -> "American Psychological Association 7th edition".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "[1]", + STYLE_LIST.stream().filter(e -> "IEEE".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "[1]", + STYLE_LIST.stream().filter(e -> "Springer - Lecture Notes in Computer Science".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(Smith, Jones, and Williams 2016)", + STYLE_LIST.stream().filter(e -> "Chicago Manual of Style 17th edition (author-date)".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(1)", + STYLE_LIST.stream().filter(e -> "Vancouver".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1", + STYLE_LIST.stream().filter(e -> "Nature".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1", + STYLE_LIST.stream().filter(e -> "American Medical Association 11th edition".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(Smith, Jones, Williams, 2016)", + STYLE_LIST.stream().filter(e -> "De Montfort University - Harvard".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(Smith, Jones, & Williams)", + STYLE_LIST.stream().filter(e -> "Modern Language Association 7th edition (underline)".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "Smith, B., B. Jones, and J. Williams, 2016.", + STYLE_LIST.stream().filter(e -> "Histoire & Mesure (Français)".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "[1]", + STYLE_LIST.stream().filter(e -> "The Journal of Veterinary Medical Science".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(1)", + STYLE_LIST.stream().filter(e -> "Acta Orthopædica Belgica".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1", + STYLE_LIST.stream().filter(e -> "Acta Anaesthesiologica Taiwanica".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1", + STYLE_LIST.stream().filter(e -> "American Chemical Society".equals(e.getTitle())).findAny().get() + ), + + // Note: not sure if the right parenthesis outside the superscript is correct, but that's how citeproc-java generates it in raw form as well. + Arguments.of( + "1)", + STYLE_LIST.stream().filter(e -> "Chemical and Pharmaceutical Bulletin".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1", + STYLE_LIST.stream().filter(e -> "American Institute of Physics 4th edition".equals(e.getTitle())).findAny().get() + ) + ); + } + + /** + * Test to check correct transformation of raw CSL citations with multiple entries generated by citeproc-java methods into OO-ready text. + *

+ * Precondition: This test assumes that {@link CitationStyleGenerator#generateInText(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateInText} works as expected. + *

+ */ + @ParameterizedTest + @MethodSource + void ooHTMLTransformFromCitationWithMultipleEntries(String expected, CitationStyle style) throws IOException { + BibEntry entry1 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Garcia, Maria and Lee, David") + .withField(StandardField.JOURNAL, "International Review of Physics") + .withField(StandardField.NUMBER, "6") + .withField(StandardField.PAGES, "789--810") + .withField(StandardField.TITLE, "Quantum Entanglement in Superconductors") + .withField(StandardField.VOLUME, "28") + .withField(StandardField.ISSUE, "3") + .withField(StandardField.YEAR, "2021") + .withCitationKey("Garcia_2021"); + + BibEntry entry2 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Smith, John and Johnson, Emily") + .withField(StandardField.JOURNAL, "Journal of Computer Science") + .withField(StandardField.NUMBER, "4") + .withField(StandardField.PAGES, "101--120") + .withField(StandardField.TITLE, "A Study on Machine Learning Algorithms") + .withField(StandardField.VOLUME, "15") + .withField(StandardField.ISSUE, "2") + .withField(StandardField.YEAR, "2020") + .withCitationKey("Smith_2020"); + + List entries = List.of(entry1, entry2); + BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(entries)); + context.setMode(BibDatabaseMode.BIBLATEX); + Citation citation = CitationStyleGenerator.generateInText(entries, style.getSource(), CSLFormatUtils.OUTPUT_FORMAT, context, bibEntryTypesManager); + String inTextCitationText = citation.getText(); + String actual = CSLFormatUtils.transformHTML(inTextCitationText); + assertEquals(expected, actual); + } + + static Stream ooHTMLTransformFromCitationWithMultipleEntries() { + return Stream.of( + Arguments.of( + "(Garcia & Lee, 2021; Smith & Johnson, 2020)", + STYLE_LIST.stream().filter(e -> "American Psychological Association 7th edition".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "[1], [2]", + STYLE_LIST.stream().filter(e -> "IEEE".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "[1, 2]", + STYLE_LIST.stream().filter(e -> "Springer - Lecture Notes in Computer Science".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(Garcia and Lee 2021; Smith and Johnson 2020)", + STYLE_LIST.stream().filter(e -> "Chicago Manual of Style 17th edition (author-date)".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(1,2)", + STYLE_LIST.stream().filter(e -> "Vancouver".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1,2", + STYLE_LIST.stream().filter(e -> "Nature".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1,2", + STYLE_LIST.stream().filter(e -> "American Medical Association 11th edition".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(Garcia, Lee, 2021; Smith, Johnson, 2020)", + STYLE_LIST.stream().filter(e -> "De Montfort University - Harvard".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(Garcia & Lee; Smith & Johnson)", + STYLE_LIST.stream().filter(e -> "Modern Language Association 7th edition (underline)".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "Garcia, M. and D. Lee, 2021 ; Smith, J. and E. Johnson, 2020.", + STYLE_LIST.stream().filter(e -> "Histoire & Mesure (Français)".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "[1, 2]", + STYLE_LIST.stream().filter(e -> "The Journal of Veterinary Medical Science".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "(1,2)", + STYLE_LIST.stream().filter(e -> "Acta Orthopædica Belgica".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1,2", + STYLE_LIST.stream().filter(e -> "Acta Anaesthesiologica Taiwanica".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1,2", + STYLE_LIST.stream().filter(e -> "American Chemical Society".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1,2)", + STYLE_LIST.stream().filter(e -> "Chemical and Pharmaceutical Bulletin".equals(e.getTitle())).findAny().get() + ), + + Arguments.of( + "1,2", + STYLE_LIST.stream().filter(e -> "American Institute of Physics 4th edition".equals(e.getTitle())).findAny().get() + ) + ); + } + + /** + * Test for modifying the number (index) of a numeric citation. + * The numeric index should change to the provided "current number". + * The rest of the citation should stay as it is (other numbers in the body shouldn't be affected). + *

+ * Precondition 1: This test assumes that {@link CitationStyleGenerator#generateCitation(List, String, CitationStyleOutputFormat, BibDatabaseContext, BibEntryTypesManager) generateCitation} works as expected.
+ * Precondition 2: This test assumes that the method {@link CSLFormatUtils#transformHTML(String) transformHTML} works as expected.
+ * Precondition 3: Run this test ONLY on numeric Citation Styles.

+ */ + @ParameterizedTest + @MethodSource + void updateSingleNumericCitation(String expected, CitationStyle style) { + String citation = CitationStyleGenerator.generateCitation(List.of(testEntry), style.getSource(), CSLFormatUtils.OUTPUT_FORMAT, context, bibEntryTypesManager).getFirst(); + String transformedCitation = CSLFormatUtils.transformHTML(citation); + String actual = CSLFormatUtils.updateSingleBibliographyNumber(transformedCitation, 3); + assertEquals(expected, actual); + } + + static Stream updateSingleNumericCitation() { + return Stream.of( + + // Type: "[1]" + Arguments.of( + " \n" + + " [3] B. Smith, B. Jones, and J. Williams, “Title of the test entry,” BibTeX Journal, vol. 34, no. 3, pp. 45–67, Jul. 2016, doi: 10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "IEEE".equals(e.getTitle())).findAny().get() + ), + + // Type: "1." + Arguments.of( + " \n" + + " 3. Smith, B., Jones, B. and Williams, J. 2016. Title of the test entry. BibTeX Journal. 34: 45–67.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "The Journal of Veterinary Medical Science".equals(e.getTitle())).findAny().get() + ), + + // Type:"1." + Arguments.of( + " \n" + + " 3. Smith B, Jones B, Williams J. Title of the test entry. BibTeX Journal 2016 ; 34 : 45–67.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Acta Orthopædica Belgica".equals(e.getTitle())).findAny().get() + ), + + // Type: "1" + Arguments.of( + " \n" + + " 3 Smith Bill, Jones Bob, Williams Jeff. Title of the test entry. BibTeX Journal 2016;34(3):45–67. Doi: 10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Acta Anaesthesiologica Taiwanica".equals(e.getTitle())).findAny().get() + ), + + // Type: "(1)" + Arguments.of( + " \n" + + " (3) Smith, B.; Jones, B.; Williams, J. Title of the Test Entry. BibTeX Journal 2016, 34 (3), 45–67. https://doi.org/10.1001/bla.blubb.\n" + + " \n", + STYLE_LIST.stream().filter(e -> "American Chemical Society".equals(e.getTitle())).findAny().get() + ), + + // Type: "1)" + Arguments.of( + " \n" + + " 3) Smith B., Jones B., Williams J., BibTeX Journal, 34, 45–67 (2016).\n" + + " \n", + STYLE_LIST.stream().filter(e -> "Chemical and Pharmaceutical Bulletin".equals(e.getTitle())).findAny().get() + ), + + // Type: "1" + Arguments.of( + " 3 B. Smith, B. Jones, and J. Williams, “Title of the test entry,” BibTeX Journal 34(3), 45–67 (2016).\n", + STYLE_LIST.stream().filter(e -> "American Institute of Physics 4th edition".equals(e.getTitle())).findAny().get() + ) + ); + } + + /** + * Tests if a citation (LaTeX "\cite") is converted into an in-text citation (LaTeX "\citet") as expected. + */ + @ParameterizedTest + @MethodSource + void ChangeToInText(String expected, String input) { + String actual = CSLFormatUtils.changeToInText(input); + assertEquals(expected, actual); + } + + private static Stream ChangeToInText() { + return Stream.of( + + // APA Style + Arguments.of("Smith (2020)", "(Smith, 2020)"), + Arguments.of("Johnson & Brown (2018)", "(Johnson & Brown, 2018)"), + Arguments.of("Williams et al. (2019)", "(Williams et al., 2019)"), + + // MLA Style + Arguments.of("(Smith 20)", "(Smith 20)"), + Arguments.of("(Johnson and Brown 18)", "(Johnson and Brown 18)"), + Arguments.of("(Williams et al. 19)", "(Williams et al. 19)"), + + // Chicago Style (Author-Date) + Arguments.of("(Smith 2020)", "(Smith 2020)"), + Arguments.of("(Johnson and Brown 2018)", "(Johnson and Brown 2018)"), + Arguments.of("(Williams et al. 2019)", "(Williams et al. 2019)"), + + // Harvard Style + Arguments.of("Smith (2020)", "(Smith, 2020)"), + Arguments.of("Johnson and Brown (2018)", "(Johnson and Brown, 2018)"), + Arguments.of("Williams et al. (2019)", "(Williams et al., 2019)"), + + // IEEE Style + Arguments.of("[1]", "[1]"), + Arguments.of("[2], [3]", "[2], [3]"), + + // Vancouver Style + Arguments.of("(1)", "(1)"), + Arguments.of("(1,2)", "(1,2)"), + + // Nature Style + Arguments.of("1", "1"), + Arguments.of("1,2", "1,2") + + ); + } + + /** + * Test for proper generation of alphanumeric citations (currently supported: DIN 1505-2). + *

+ * Precondition: This test assumes that the method {@link org.jabref.logic.citationkeypattern.BracketedPattern#authorsAlpha authorsAlpha} works as expected.

+ */ + @ParameterizedTest + @MethodSource + void generateAlphanumericCitationTest(String expected, List entries) { + BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(entries)); + String actual = generateAlphanumericCitation(entries, context); + assertEquals(expected, actual); + } + + private static Stream generateAlphanumericCitationTest() { + BibEntry entry1 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Garcia, Maria") + .withField(StandardField.TITLE, "Quantum Entanglement in Superconductors") + .withField(StandardField.JOURNAL, "International Review of Physics") + .withField(StandardField.VOLUME, "28") + .withField(StandardField.NUMBER, "6") + .withField(StandardField.PAGES, "789--810") + .withField(StandardField.YEAR, "2021") + .withCitationKey("Garcia_2021"); + + BibEntry entry2 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Smith, John and Johnson, Emily") + .withField(StandardField.TITLE, "A Study on Machine Learning Algorithms") + .withField(StandardField.JOURNAL, "Journal of Computer Science") + .withField(StandardField.VOLUME, "15") + .withField(StandardField.NUMBER, "4") + .withField(StandardField.PAGES, "101--120") + .withField(StandardField.YEAR, "2020") + .withCitationKey("Smith_2020"); + + BibEntry entry3 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Johnson, Emily; Williams, Jessica; Lee, David") + .withField(StandardField.TITLE, "Trends in Artificial Intelligence") + .withField(StandardField.JOURNAL, "AI Magazine") + .withField(StandardField.VOLUME, "41") + .withField(StandardField.NUMBER, "2") + .withField(StandardField.PAGES, "45--60") + .withField(StandardField.YEAR, "2019") + .withCitationKey("Johnson_2019"); + + BibEntry entry4 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Smith, John; Johnson, Emily; Lee, David; Williams, Jessica") + .withField(StandardField.TITLE, "Big Data Analytics in Healthcare") + .withField(StandardField.JOURNAL, "Journal of Medical Informatics") + .withField(StandardField.VOLUME, "23") + .withField(StandardField.NUMBER, "1") + .withField(StandardField.PAGES, "11--25") + .withField(StandardField.YEAR, "2018") + .withCitationKey("Smith_2018"); + + BibEntry entry5 = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "Garcia, Maria; Smith, John; Johnson, Emily; Lee, David; Williams, Jessica") + .withField(StandardField.TITLE, "Advances in Renewable Energy Technologies") + .withField(StandardField.JOURNAL, "Energy Policy") + .withField(StandardField.VOLUME, "52") + .withField(StandardField.NUMBER, "3") + .withField(StandardField.PAGES, "120--135") + .withField(StandardField.YEAR, "2017") + .withCitationKey("Garcia_2017"); + + return Stream.of( + + // Entry with single author + Arguments.of("[Ga21]", List.of(entry1)), + + // Entry with two authors + Arguments.of("[SJ20]", List.of(entry2)), + + // Entry with three authors + Arguments.of("[JWL19]", List.of(entry3)), + + // Entry with four authors + Arguments.of("[SJLW18]", List.of(entry4)), + + // Entry with five authors + Arguments.of("[GSJL17]", List.of(entry5)), + + // Multiple entries with varying number of authors + Arguments.of("[Ga21; SJ20; JWL19; SJLW18; GSJL17]", List.of(entry1, entry2, entry3, entry4, entry5)) + + ); + } +}