From 44a229b1ad61e0d6217ffab9a73fd3d4ad4a1122 Mon Sep 17 00:00:00 2001 From: antalk2 Date: Mon, 2 Aug 2021 08:42:00 +0200 Subject: [PATCH] Oobranch d : apply style (#7789) * step0 : start model/openoffice, logic/openoffice/style * correction: import order * add general utilities * add UNO utilities, move CreationException, NoDocumentException * Xlint:unchecked model/openoffice/util * add ootext * add rangesort * add compareStartsUnsafe, compareStartsThenEndsUnsafe * add Tuple3 * add ootext * add rangesort * delNamesArray size correction * rangeSort update * cleanup * style additions * checkstyle on tests * add missing message * ootext changes from improve-reversibility-rebased-03 * rangesort changes from improve-reversibility-rebased-03 * rangesort update from improve-reversibility-rebased-03 add comment on RangeSet.add costs use UnoTextRange.compareXXXUnsafe * use longer lines in comments * propagate changes from improve-reversibility-rebased-03 * deleted src/main/java/org/jabref/model/openoffice/rangesort/RangeSet.java * deleted src/main/java/org/jabref/model/openoffice/rangesort/RangeSet.java * use StringUtil.isNullOrEmpty * no natural sort for ComparableMark * in response to review https://github.com/JabRef/jabref/pull/7788#pullrequestreview-698494039 - more use of StringUtil.isNullOrEmpty - private final XTextRangeCompare cmp; - List partition = partitions.computeIfAbsent(partitionKey, _key -> new ArrayList<>()); - visualSort does not throw WrappedTargetException, NoDocumentException - set renamed to comparableMarks * use {@code }, PMD suggestions * update model/style from improve-reversibility-rebased-03 * update logic/style from improve-reversibility-rebased-03 * replaced single-character names in OOBibStyle.java (in changed part) * some longer names in OOBibStyleGetCitationMarker.java * drop normalizePageInfos, use 'preferred' and 'fallback' in getAuthorLastSeparatorInTextWithFallBack * checkstyle * use putIfAbsent * use "{}" with LOGGER * use Objects.hash and Objects.equals in CitationLookupResult * simplified CitedKey.getBibEntry * more use of "{}" in LOGGER * Citation.lookup: use streams * Citation.lookup: Optional::get before findFirst --- .../logic/openoffice/style/OOBibStyle.java | 441 +++++++++- .../style/OOBibStyleGetCitationMarker.java | 774 ++++++++++++++++++ .../style/OOBibStyleGetNumCitationMarker.java | 281 +++++++ .../style/OOFormatBibliography.java | 200 +++++ .../logic/openoffice/style/OOProcess.java | 76 ++ .../style/OOProcessAuthorYearMarkers.java | 159 ++++ .../style/OOProcessCitationKeyMarkers.java | 35 + .../style/OOProcessNumericMarkers.java | 47 ++ .../logic/openoffice/style/StyleLoader.java | 16 +- .../model/openoffice/style/Citation.java | 146 ++++ .../model/openoffice/style/CitationGroup.java | 156 ++++ .../openoffice/style/CitationGroupId.java | 18 + .../openoffice/style/CitationGroups.java | 295 +++++++ .../style/CitationLookupResult.java | 49 ++ .../openoffice/style/CitationMarkerEntry.java | 28 + .../style/CitationMarkerNormEntry.java | 21 + .../style/CitationMarkerNumericBibEntry.java | 19 + .../style/CitationMarkerNumericEntry.java | 20 + .../model/openoffice/style/CitationPath.java | 17 + .../model/openoffice/style/CitationType.java | 24 + .../model/openoffice/style/CitedKey.java | 138 ++++ .../model/openoffice/style/CitedKeys.java | 84 ++ .../openoffice/style/ComparableCitation.java | 13 + .../openoffice/style/ComparableCitedKey.java | 16 + .../openoffice/style/CompareCitation.java | 30 + .../openoffice/style/CompareCitedKey.java | 39 + .../style/NonUniqueCitationMarker.java | 15 + .../model/openoffice/style/OODataModel.java | 34 + .../model/openoffice/style/PageInfo.java | 48 ++ src/main/resources/l10n/JabRef_en.properties | 1 + .../openoffice/style/OOBibStyleTest.java | 392 +++++++++ .../style/OOBibStyleTestHelper.java | 336 ++++++++ 32 files changed, 3942 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetNumCitationMarker.java create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOFormatBibliography.java create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOProcess.java create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOProcessAuthorYearMarkers.java create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOProcessCitationKeyMarkers.java create mode 100644 src/main/java/org/jabref/logic/openoffice/style/OOProcessNumericMarkers.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/Citation.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationGroup.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationGroupId.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationGroups.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationLookupResult.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationMarkerEntry.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationMarkerNormEntry.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericBibEntry.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericEntry.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationPath.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitationType.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitedKey.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CitedKeys.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/ComparableCitation.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/ComparableCitedKey.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CompareCitation.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/CompareCitedKey.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/NonUniqueCitationMarker.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/OODataModel.java create mode 100644 src/main/java/org/jabref/model/openoffice/style/PageInfo.java create mode 100644 src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOBibStyle.java b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyle.java index 561099afe96..082d94a7fdf 100644 --- a/src/main/java/org/jabref/logic/openoffice/style/OOBibStyle.java +++ b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyle.java @@ -31,9 +31,17 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.OrFields; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.EntryType; import org.jabref.model.entry.types.EntryTypeFactory; +import org.jabref.model.openoffice.ootext.OOFormat; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.CitationMarkerEntry; +import org.jabref.model.openoffice.style.CitationMarkerNormEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericBibEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericEntry; +import org.jabref.model.openoffice.style.NonUniqueCitationMarker; import org.jabref.model.strings.StringUtil; import org.slf4j.Logger; @@ -84,6 +92,22 @@ public class OOBibStyle implements Comparable { private static final String CITATION_CHARACTER_FORMAT = "CitationCharacterFormat"; private static final String FORMAT_CITATIONS = "FormatCitations"; private static final String GROUPED_NUMBERS_SEPARATOR = "GroupedNumbersSeparator"; + + // These two can do what ItalicCitations, BoldCitations, + // SuperscriptCitations and SubscriptCitations were supposed to do, + // as well as underline smallcaps and strikeout. + private static final String CITATION_GROUP_MARKUP_BEFORE = "CitationGroupMarkupBefore"; + private static final String CITATION_GROUP_MARKUP_AFTER = "CitationGroupMarkupAfter"; + + private static final String AUTHORS_PART_MARKUP_BEFORE = "AuthorsPartMarkupBefore"; + private static final String AUTHORS_PART_MARKUP_AFTER = "AuthorsPartMarkupAfter"; + + private static final String AUTHOR_NAMES_LIST_MARKUP_BEFORE = "AuthorNamesListMarkupBefore"; + private static final String AUTHOR_NAMES_LIST_MARKUP_AFTER = "AuthorNamesListMarkupAfter"; + + private static final String AUTHOR_NAME_MARKUP_BEFORE = "AuthorNameMarkupBefore"; + private static final String AUTHOR_NAME_MARKUP_AFTER = "AuthorNameMarkupAfter"; + private static final String PAGE_INFO_SEPARATOR = "PageInfoSeparator"; private static final String CITATION_SEPARATOR = "CitationSeparator"; private static final String IN_TEXT_YEAR_SEPARATOR = "InTextYearSeparator"; @@ -157,11 +181,24 @@ private void setDefaultProperties() { properties.put(IS_NUMBER_ENTRIES, Boolean.FALSE); properties.put(BRACKET_BEFORE, "["); properties.put(BRACKET_AFTER, "]"); - properties.put(REFERENCE_PARAGRAPH_FORMAT, "Default"); + properties.put(REFERENCE_PARAGRAPH_FORMAT, "Standard"); properties.put(REFERENCE_HEADER_PARAGRAPH_FORMAT, "Heading 1"); // Set default properties for the citation marker: citProperties.put(AUTHOR_FIELD, FieldFactory.serializeOrFields(StandardField.AUTHOR, StandardField.EDITOR)); + + citProperties.put(CITATION_GROUP_MARKUP_BEFORE, ""); + citProperties.put(CITATION_GROUP_MARKUP_AFTER, ""); + + citProperties.put(AUTHORS_PART_MARKUP_BEFORE, ""); + citProperties.put(AUTHORS_PART_MARKUP_AFTER, ""); + + citProperties.put(AUTHOR_NAMES_LIST_MARKUP_BEFORE, ""); + citProperties.put(AUTHOR_NAMES_LIST_MARKUP_AFTER, ""); + + citProperties.put(AUTHOR_NAME_MARKUP_BEFORE, ""); + citProperties.put(AUTHOR_NAME_MARKUP_AFTER, ""); + citProperties.put(YEAR_FIELD, StandardField.YEAR.getName()); citProperties.put(MAX_AUTHORS, 3); citProperties.put(MAX_AUTHORS_FIRST, -1); @@ -178,7 +215,7 @@ private void setDefaultProperties() { citProperties.put(GROUPED_NUMBERS_SEPARATOR, "-"); citProperties.put(MINIMUM_GROUPING_COUNT, 3); citProperties.put(FORMAT_CITATIONS, Boolean.FALSE); - citProperties.put(CITATION_CHARACTER_FORMAT, "Default"); + citProperties.put(CITATION_CHARACTER_FORMAT, "Standard"); citProperties.put(ITALIC_CITATIONS, Boolean.FALSE); citProperties.put(BOLD_CITATIONS, Boolean.FALSE); citProperties.put(SUPERSCRIPT_CITATIONS, Boolean.FALSE); @@ -379,8 +416,8 @@ private void handlePropertiesLine(String line, Map map) { value = value.trim().substring(1, value.trim().length() - 1); } Object toSet = value; - if (NUM_PATTERN.matcher(value).matches()) { - toSet = Integer.parseInt(value); + if (NUM_PATTERN.matcher(value.trim()).matches()) { + toSet = Integer.parseInt(value.trim()); } else if ("true".equalsIgnoreCase(value.trim())) { toSet = Boolean.TRUE; } else if ("false".equalsIgnoreCase(value.trim())) { @@ -408,6 +445,7 @@ public Layout getReferenceFormat(EntryType type) { } } + /* begin_old */ /** * Format a number-based citation marker for the given number. * @@ -474,21 +512,9 @@ public String getNumCitationMarker(List number, int minGroupingCount, b sb.append(bracketAfter); return sb.toString(); } + /* end_old */ - /** - * Format the marker for the in-text citation according to this BIB style. Uniquefier letters are added as - * provided by the uniquefiers argument. If successive entries within the citation are uniquefied from each other, - * this method will perform a grouping of these entries. - * - * @param entries The list of JabRef BibEntry providing the data. - * @param database A map of BibEntry-BibDatabase pairs. - * @param inParenthesis Signals whether a parenthesized citation or an in-text citation is wanted. - * @param uniquefiers Strings to add behind the year for each entry in case it's needed to separate similar - * entries. - * @param unlimAuthors Boolean for each entry. If true, we should not use "et al" formatting regardless - * of the number of authors. Can be null to indicate that no entries should have unlimited names. - * @return The formatted citation. - */ + /* begin_old */ public String getCitationMarker(List entries, Map database, boolean inParenthesis, String[] uniquefiers, int[] unlimAuthors) { // Look for groups of uniquefied entries that should be combined in the output. @@ -552,7 +578,9 @@ public String getCitationMarker(List entries, Map entries, String[] uniquefiers, int from, int t } uniquefiers[from] = sb.toString(); } + /* end_old */ + /* begin_old */ /** * This method produces (Author, year) style citation strings in many different forms. * @@ -623,7 +653,9 @@ private String getAuthorYearParenthesisMarker(List entries, Map entries, Map getBibLayout() { + return bibLayout; + } + + protected Map getProperties() { + return properties; + } + + protected Map getCitProperties() { + return citProperties; + } + + protected void addJournal(String journalName) { + journals.add(journalName); + } + + protected void setLocalCopy(String contentsOfJstyleFile) { + localCopy = contentsOfJstyleFile; + } + + protected void setName(String nameOfTheStyle) { + name = nameOfTheStyle; + } + + protected boolean getIsDefaultLayoutPresent() { + return isDefaultLayoutPresent; + } + + protected void setIsDefaultLayoutPresent(boolean isPresent) { + isDefaultLayoutPresent = isPresent; + } + + protected void setValid(boolean isValid) { + valid = isValid; + } + + protected LayoutFormatterPreferences getPrefs() { + return prefs; + } + + protected void setDefaultBibLayout(Layout layout) { + defaultBibLayout = layout; + } + + /** + * Format a number-based citation marker for the given entries. + * + * @return The text for the citation. + */ + public OOText getNumCitationMarker2(List entries) { + final int minGroupingCount = this.getMinimumGroupingCount(); + return OOBibStyleGetNumCitationMarker.getNumCitationMarker2(this, + entries, + minGroupingCount); + } + + /** + * For some tests we need to override minGroupingCount. + */ + public OOText getNumCitationMarker2(List entries, + int minGroupingCount) { + return OOBibStyleGetNumCitationMarker.getNumCitationMarker2(this, + entries, + minGroupingCount); + } + + /** + * Format a number-based bibliography label for the given number. + */ + public OOText getNumCitationMarkerForBibliography(CitationMarkerNumericBibEntry entry) { + return OOBibStyleGetNumCitationMarker.getNumCitationMarkerForBibliography(this, entry); + } + + public OOText getNormalizedCitationMarker(CitationMarkerNormEntry ce) { + return OOBibStyleGetCitationMarker.getNormalizedCitationMarker(this, ce, Optional.empty()); + } + + /** + * Format the marker for the in-text citation according to this + * BIB style. Uniquefier letters are added as provided by the + * citationMarkerEntries argument. If successive entries within + * the citation are uniquefied from each other, this method will + * perform a grouping of these entries. + * + * If successive entries within the citation are uniquefied from + * each other, this method will perform a grouping of these + * entries. + * + * @param citationMarkerEntries The list of entries providing the + * data. + * + * @param inParenthesis Signals whether a parenthesized citation + * or an in-text citation is wanted. + * + * @param nonUniqueCitationMarkerHandling + * + * THROWS : Should throw if finds that uniqueLetters + * provided do not make the entries unique. + * + * FORGIVEN : is needed to allow preliminary markers + * for freshly inserted citations without + * going throw the uniquefication process. + * + * @return The formatted citation. The result does not include + * the standard wrappers: + * OOFormat.setLocaleNone() and OOFormat.setCharStyle(). + * These are added by decorateCitationMarker() + */ + public OOText createCitationMarker(List citationMarkerEntries, + boolean inParenthesis, + NonUniqueCitationMarker nonUniqueCitationMarkerHandling) { + return OOBibStyleGetCitationMarker.createCitationMarker(this, + citationMarkerEntries, + inParenthesis, + nonUniqueCitationMarkerHandling); + } + + /** + * Add setLocaleNone and optionally setCharStyle(CitationCharacterFormat) around + * citationText. Called in fillCitationMarkInCursor, so these are + * also applied to "Unresolved()" entries and numeric styles. + */ + public OOText decorateCitationMarker(OOText citationText) { + OOBibStyle style = this; + OOText citationText2 = OOFormat.setLocaleNone(citationText); + if (style.isFormatCitations()) { + String charStyle = style.getCitationCharacterFormat(); + citationText2 = OOFormat.setCharStyle(citationText2, charStyle); + } + return citationText2; + } + + /* + * + * Property getters + * + */ + + /** + * Minimal number of consecutive citation numbers needed to start + * replacing with an range like "10-13". + */ + public int getMinimumGroupingCount() { + return getIntCitProperty(OOBibStyle.MINIMUM_GROUPING_COUNT); + } + + /** + * Used in number ranges like "10-13" in numbered citations. + */ + public String getGroupedNumbersSeparator() { + return getStringCitProperty(OOBibStyle.GROUPED_NUMBERS_SEPARATOR); + } + + private boolean getBooleanProperty(String propName) { + return (Boolean) properties.get(propName); + } + + private String getStringProperty(String propName) { + return (String) properties.get(propName); + } + + /** + * Should citation markers be italicized? + * + */ + public String getCitationGroupMarkupBefore() { + return getStringCitProperty(CITATION_GROUP_MARKUP_BEFORE); + } + + public String getCitationGroupMarkupAfter() { + return getStringCitProperty(CITATION_GROUP_MARKUP_AFTER); + } + + /** Author list, including " et al." */ + public String getAuthorsPartMarkupBefore() { + return getStringCitProperty(AUTHORS_PART_MARKUP_BEFORE); + } + + public String getAuthorsPartMarkupAfter() { + return getStringCitProperty(AUTHORS_PART_MARKUP_AFTER); + } + + /** Author list, excluding " et al." */ + public String getAuthorNamesListMarkupBefore() { + return getStringCitProperty(AUTHOR_NAMES_LIST_MARKUP_BEFORE); + } + + public String getAuthorNamesListMarkupAfter() { + return getStringCitProperty(AUTHOR_NAMES_LIST_MARKUP_AFTER); + } + + /** Author names. Excludes Author separators */ + public String getAuthorNameMarkupBefore() { + return getStringCitProperty(AUTHOR_NAME_MARKUP_BEFORE); + } + + public String getAuthorNameMarkupAfter() { + return getStringCitProperty(AUTHOR_NAME_MARKUP_AFTER); + } + + public boolean getMultiCiteChronological() { + // "MultiCiteChronological" + return this.getBooleanCitProperty(OOBibStyle.MULTI_CITE_CHRONOLOGICAL); + } + + // Probably obsolete, now we can use " et al." instead in EtAlString + public boolean getItalicEtAl() { + // "ItalicEtAl" + return this.getBooleanCitProperty(OOBibStyle.ITALIC_ET_AL); + } + + /** + * @return Names of fields containing authors: the first + * non-empty field will be used. + */ + protected OrFields getAuthorFieldNames() { + String authorFieldNamesString = this.getStringCitProperty(OOBibStyle.AUTHOR_FIELD); + return FieldFactory.parseOrFields(authorFieldNamesString); + } + + /** + * @return Field containing year, with fallback fields. + */ + protected OrFields getYearFieldNames() { + String yearFieldNamesString = this.getStringCitProperty(OOBibStyle.YEAR_FIELD); + return FieldFactory.parseOrFields(yearFieldNamesString); + } + + /* The String to add between the two last author names, e.g. " & ". */ + protected String getAuthorLastSeparator() { + return getStringCitProperty(OOBibStyle.AUTHOR_LAST_SEPARATOR); + } + + /* As getAuthorLastSeparator, for in-text citation. */ + protected String getAuthorLastSeparatorInTextWithFallBack() { + String preferred = getStringCitProperty(OOBibStyle.AUTHOR_LAST_SEPARATOR_IN_TEXT); + String fallback = getStringCitProperty(OOBibStyle.AUTHOR_LAST_SEPARATOR); + return Objects.requireNonNullElse(preferred, fallback); + } + + protected String getPageInfoSeparator() { + return getStringCitProperty(OOBibStyle.PAGE_INFO_SEPARATOR); + } + + protected String getUniquefierSeparator() { + return getStringCitProperty(OOBibStyle.UNIQUEFIER_SEPARATOR); + } + + protected String getCitationSeparator() { + return getStringCitProperty(OOBibStyle.CITATION_SEPARATOR); + } + + protected String getYearSeparator() { + return getStringCitProperty(OOBibStyle.YEAR_SEPARATOR); + } + + protected String getYearSeparatorInText() { + return getStringCitProperty(OOBibStyle.IN_TEXT_YEAR_SEPARATOR); + } + + /** The maximum number of authors to write out in full without + * using "et al." Set to -1 to always write out all authors. + */ + protected int getMaxAuthors() { + return getIntCitProperty(OOBibStyle.MAX_AUTHORS); + } + + public int getMaxAuthorsFirst() { + return getIntCitProperty(OOBibStyle.MAX_AUTHORS_FIRST); + } + + /** Opening parenthesis before citation (or year, for in-text) */ + protected String getBracketBefore() { + return getStringCitProperty(OOBibStyle.BRACKET_BEFORE); + } + + /** Closing parenthesis after citation */ + protected String getBracketAfter() { + return getStringCitProperty(OOBibStyle.BRACKET_AFTER); + } + + /** Opening parenthesis before citation marker in the bibliography. */ + private String getBracketBeforeInList() { + return getStringCitProperty(OOBibStyle.BRACKET_BEFORE_IN_LIST); + } + + public String getBracketBeforeInListWithFallBack() { + return Objects.requireNonNullElse(getBracketBeforeInList(), getBracketBefore()); + } + + /** Closing parenthesis after citation marker in the bibliography */ + private String getBracketAfterInList() { + return getStringCitProperty(OOBibStyle.BRACKET_AFTER_IN_LIST); + } + + String getBracketAfterInListWithFallBack() { + return Objects.requireNonNullElse(getBracketAfterInList(), getBracketAfter()); + } + + public OOText getFormattedBibliographyTitle() { + OOBibStyle style = this; + OOText title = style.getReferenceHeaderText(); + String parStyle = style.getReferenceHeaderParagraphFormat(); + if (parStyle != null) { + title = OOFormat.paragraph(title, parStyle); + } else { + title = OOFormat.paragraph(title); + } + return title; + } + } diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java new file mode 100644 index 00000000000..deb1a306913 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java @@ -0,0 +1,774 @@ +package org.jabref.logic.openoffice.style; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.Author; +import org.jabref.model.entry.AuthorList; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.OrFields; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.CitationLookupResult; +import org.jabref.model.openoffice.style.CitationMarkerEntry; +import org.jabref.model.openoffice.style.CitationMarkerNormEntry; +import org.jabref.model.openoffice.style.NonUniqueCitationMarker; +import org.jabref.model.openoffice.style.PageInfo; +import org.jabref.model.strings.StringUtil; + +class OOBibStyleGetCitationMarker { + + private OOBibStyleGetCitationMarker() { + /**/ + } + + /** + * Look up the nth author and return the "proper" last name for + * citation markers. + * + * Note: "proper" in the sense that it includes the "von" part + * of the name (followed by a space) if there is one. + * + * @param authorList The author list. + * @param number The number of the author to return. + * @return The author name, or an empty String if inapplicable. + */ + private static String getAuthorLastName(AuthorList authorList, int number) { + StringBuilder sb = new StringBuilder(); + + if (authorList.getNumberOfAuthors() > number) { + Author author = authorList.getAuthor(number); + // "von " if von exists + Optional von = author.getVon(); + if (von.isPresent() && !von.get().isEmpty()) { + sb.append(von.get()); + sb.append(' '); + } + // last name if it exists + sb.append(author.getLast().orElse("")); + } + + return sb.toString(); + } + + private static String markupAuthorName(OOBibStyle style, String name) { + return (style.getAuthorNameMarkupBefore() + + name + + style.getAuthorNameMarkupAfter()); + } + + /** + * @param authorList Parsed list of authors. + * + * @param maxAuthors The maximum number of authors to write out. + * If there are more authors, then ET_AL_STRING is emitted + * to mark their omission. + * Set to -1 to write out all authors. + * + * maxAuthors=0 is pointless, now throws IllegalArgumentException + * (Earlier it behaved as maxAuthors=1) + * + * maxAuthors less than -1 : throw IllegalArgumentException + * + * @param andString For "A, B[ and ]C" + * + * @return "Au[AS]Bu[AS]Cu[OXFORD_COMMA][andString]Du[yearSep]" + * or "Au[etAlString][yearSep]" + * + * where AS = AUTHOR_SEPARATOR + * Au, Bu, Cu, Du are last names of authors. + * + * Note: + * - The "Au[AS]Bu[AS]Cu" (or the "Au") part may be empty (maxAuthors==0 or nAuthors==0). + * - OXFORD_COMMA is only emitted if nAuthors is at least 3. + * - andString is only emitted if nAuthors is at least 2. + */ + private static String formatAuthorList(OOBibStyle style, + AuthorList authorList, + int maxAuthors, + String andString) { + + Objects.requireNonNull(authorList); + + // Apparently maxAuthorsBeforeEtAl is always 1 for in-text citations. + // In reference lists can be for example 7, + // (https://www.chicagomanualofstyle.org/turabian/turabian-author-date-citation-quick-guide.html) + // but those are handled elsewhere. + // + // There is also + // https://apastyle.apa.org/style-grammar-guidelines/ ... + // ... citations/basic-principles/same-year-first-author + // suggesting the to avoid ambiguity, we may need more than one name + // before "et al.". We do not currently do this kind of disambiguation, + // but we might, one day. + // + final int maxAuthorsBeforeEtAl = 1; + + // The String to represent authors that are not mentioned, + // e.g. " et al." + String etAlString = style.getEtAlString(); + + // getItalicEtAl is not necessary now, since etAlString could + // itself contain the markup. + // This is for backward compatibility. + if (style.getItalicEtAl()) { + etAlString = "" + etAlString + ""; + } + + // The String to add between author names except the last two, + // e.g. ", ". + String authorSep = style.getAuthorSeparator(); + + // The String to put after the second to last author in case + // of three or more authors: (A, B[,] and C) + String oxfordComma = style.getOxfordComma(); + + StringBuilder sb = new StringBuilder(); + + final int nAuthors = authorList.getNumberOfAuthors(); + + // To reduce ambiguity, throw on unexpected values of maxAuthors + if (maxAuthors == 0 && nAuthors != 0) { + throw new IllegalArgumentException("maxAuthors = 0 in formatAuthorList"); + } + if (maxAuthors < -1) { + throw new IllegalArgumentException("maxAuthors < -1 in formatAuthorList"); + } + + // emitAllAuthors == false means use "et al." + boolean emitAllAuthors = ((nAuthors <= maxAuthors) || (maxAuthors == -1)); + + int nAuthorsToEmit = (emitAllAuthors + ? nAuthors + // If we use "et al." maxAuthorsBeforeEtAl also limits the + // number of authors emitted. + : Math.min(maxAuthorsBeforeEtAl, nAuthors)); + + if (nAuthorsToEmit >= 1) { + sb.append(style.getAuthorsPartMarkupBefore()); + sb.append(style.getAuthorNamesListMarkupBefore()); + // The first author + String name = getAuthorLastName(authorList, 0); + sb.append(markupAuthorName(style, name)); + } + + if (nAuthors >= 2) { + + if (emitAllAuthors) { + // Emit last names, except for the last author + int j = 1; + while (j < (nAuthors - 1)) { + sb.append(authorSep); + String name = getAuthorLastName(authorList, j); + sb.append(markupAuthorName(style, name)); + j++; + } + // oxfordComma if at least 3 authors + if (nAuthors >= 3) { + sb.append(oxfordComma); + } + // Emit " and "+"LastAuthor" + sb.append(andString); + String name = getAuthorLastName(authorList, nAuthors - 1); + sb.append(markupAuthorName(style, name)); + + } else { + // Emit last names up to nAuthorsToEmit. + // + // The (maxAuthorsBeforeEtAl > 1) test is intended to + // make sure the compiler eliminates this block as + // long as maxAuthorsBeforeEtAl is fixed to 1. + if (maxAuthorsBeforeEtAl > 1) { + int j = 1; + while (j < nAuthorsToEmit) { + sb.append(authorSep); + String name = getAuthorLastName(authorList, j); + sb.append(markupAuthorName(style, name)); + j++; + } + } + } + } + + if (nAuthorsToEmit >= 1) { + sb.append(style.getAuthorNamesListMarkupAfter()); + } + + if (nAuthors >= 2 && !emitAllAuthors) { + sb.append(etAlString); + } + + sb.append(style.getAuthorsPartMarkupAfter()); + return sb.toString(); + } + + /** + * On success, getRawCitationMarkerField returns content, + * but we also need to know which field matched, because + * for some fields (actually: for author names) we need to + * reproduce the surrounding braces to inform AuthorList.parse + * not to split up the content. + */ + private static class FieldAndContent { + Field field; + String content; + FieldAndContent(Field field, String content) { + this.field = field; + this.content = content; + } + } + + /** + * @return the field and the content of the first nonempty (after trimming) + * field (or alias) from {@code fields} found in {@code entry}. + * Return {@code Optional.empty()} if found nothing. + */ + private static Optional getRawCitationMarkerField(BibEntry entry, + BibDatabase database, + OrFields fields) { + Objects.requireNonNull(entry, "Entry cannot be null"); + Objects.requireNonNull(database, "database cannot be null"); + + for (Field field : fields /* FieldFactory.parseOrFields(fields)*/) { + Optional optionalContent = entry.getResolvedFieldOrAlias(field, database); + final boolean foundSomething = (optionalContent.isPresent() + && !optionalContent.get().trim().isEmpty()); + if (foundSomething) { + return Optional.of(new FieldAndContent(field, optionalContent.get())); + } + } + return Optional.empty(); + } + + /** + * This method looks up a field for an entry in a database. + * + * Any number of backup fields can be used if the primary field is + * empty. + * + * @param fields A list of fields, to look up, using first nonempty hit. + * + * If backup fields are needed, separate field + * names by /. + * + * E.g. to use "author" with "editor" as backup, + * specify + * FieldFactory.serializeOrFields(StandardField.AUTHOR, + * StandardField.EDITOR) + * + * @return The resolved field content, or an empty string if the + * field(s) were empty. + * + * + * + */ + private static String getCitationMarkerField(OOBibStyle style, + CitationLookupResult db, + OrFields fields) { + Objects.requireNonNull(db); + + Optional optionalFieldAndContent = + getRawCitationMarkerField(db.entry, db.database, fields); + + if (optionalFieldAndContent.isEmpty()) { + // No luck? Return an empty string: + return ""; + } + + FieldAndContent fc = optionalFieldAndContent.get(); + String result = style.getFieldFormatter().format(fc.content); + + // If the field we found is mentioned in authorFieldNames and + // content has a pair of braces around it, we add a pair of + // braces around the result, so that AuthorList.parse does not split + // the content. + final OrFields fieldsToRebrace = style.getAuthorFieldNames(); + if (fieldsToRebrace.contains(fc.field) && StringUtil.isInCurlyBrackets(fc.content)) { + result = "{" + result + "}"; + } + return result; + } + + private static AuthorList getAuthorList(OOBibStyle style, CitationLookupResult db) { + + // The bibtex fields providing author names, e.g. "author" or + // "editor". + OrFields authorFieldNames = style.getAuthorFieldNames(); + + String authorListAsString = getCitationMarkerField(style, db, authorFieldNames); + return AuthorList.parse(authorListAsString); + } + + private enum AuthorYearMarkerPurpose { + IN_PARENTHESIS, + IN_TEXT, + NORMALIZED + } + + /** + * How many authors would be emitted for entry, considering + * style and entry.getIsFirstAppearanceOfSource() + * + * If entry is unresolved, return 0. + */ + private static int calculateNAuthorsToEmit(OOBibStyle style, CitationMarkerEntry entry) { + + if (entry.getLookupResult().isEmpty()) { + // unresolved + return 0; + } + + int maxAuthors = (entry.getIsFirstAppearanceOfSource() + ? style.getMaxAuthorsFirst() + : style.getMaxAuthors()); + + AuthorList authorList = getAuthorList(style, entry.getLookupResult().get()); + int nAuthors = authorList.getNumberOfAuthors(); + + if (maxAuthors == -1) { + return nAuthors; + } else { + return Integer.min(nAuthors, maxAuthors); + } + } + + /** + * Produce (Author, year) or "Author (year)" style citation strings. + * + * @param purpose IN_PARENTHESIS and NORMALIZED puts parentheses around the whole, + * IN_TEXT around each (year,uniqueLetter,pageInfo) part. + * + * NORMALIZED omits uniqueLetter and pageInfo, + * ignores isFirstAppearanceOfSource (always + * style.getMaxAuthors, not getMaxAuthorsFirst) + * + * @param entries The list of CitationMarkerEntry values to process. + * + * Here we do not check for duplicate entries: those + * are handled by {@code getCitationMarker} by + * omitting them from the list. + * + * Unresolved citations recognized by + * entry.getBibEntry() and/or + * entry.getDatabase() returning empty, and + * emitted as "Unresolved${citationKey}". + * + * Neither uniqueLetter nor pageInfo are emitted + * for unresolved citations. + * + * @param startsNewGroup Should have the same length as {@code entries}, and + * contain true for entries starting a new group, + * false for those that only add a uniqueLetter to + * the grouped presentation. + * + * @param maxAuthorsOverride If not empty, always show this number of authors. + * Added to allow NORMALIZED to use maxAuthors value that differs from + * style.getMaxAuthors() + * + * @return The formatted citation. + * + */ + private static OOText getAuthorYearParenthesisMarker2(OOBibStyle style, + AuthorYearMarkerPurpose purpose, + List entries, + boolean[] startsNewGroup, + Optional maxAuthorsOverride) { + + boolean inParenthesis = (purpose == AuthorYearMarkerPurpose.IN_PARENTHESIS + || purpose == AuthorYearMarkerPurpose.NORMALIZED); + + // The String to separate authors from year, e.g. "; ". + String yearSep = (inParenthesis + ? style.getYearSeparator() + : style.getYearSeparatorInText()); + + // The opening parenthesis. + String startBrace = style.getBracketBefore(); + + // The closing parenthesis. + String endBrace = style.getBracketAfter(); + + // The String to separate citations from each other. + String citationSeparator = style.getCitationSeparator(); + + // The bibtex field providing the year, e.g. "year". + OrFields yearFieldNames = style.getYearFieldNames(); + + // The String to add between the two last author names, e.g. " & ". + String andString = (inParenthesis + ? style.getAuthorLastSeparator() + : style.getAuthorLastSeparatorInTextWithFallBack()); + + String pageInfoSeparator = style.getPageInfoSeparator(); + String uniquefierSeparator = style.getUniquefierSeparator(); + + StringBuilder sb = new StringBuilder(); + sb.append(style.getCitationGroupMarkupBefore()); + + if (inParenthesis) { + sb.append(startBrace); // shared parenthesis + } + + for (int j = 0; j < entries.size(); j++) { + CitationMarkerEntry entry = entries.get(j); + boolean startingNewGroup = startsNewGroup[j]; + boolean endingAGroup = (j + 1 == entries.size()) || startsNewGroup[j + 1]; + + if (!startingNewGroup) { + // Just add our uniqueLetter + String uniqueLetter = entry.getUniqueLetter().orElse(null); + if (uniqueLetter != null) { + sb.append(uniquefierSeparator); + sb.append(uniqueLetter); + } + + // And close the brace, if we are the last in the group. + if (!inParenthesis && endingAGroup) { + sb.append(endBrace); + } + continue; + } + + if (j > 0) { + sb.append(citationSeparator); + } + + StringBuilder pageInfoPart = new StringBuilder(""); + if (purpose != AuthorYearMarkerPurpose.NORMALIZED) { + Optional pageInfo = + PageInfo.normalizePageInfo(entry.getPageInfo()); + if (pageInfo.isPresent()) { + pageInfoPart.append(pageInfoSeparator); + pageInfoPart.append(OOText.toString(pageInfo.get())); + } + } + + final boolean isUnresolved = entry.getLookupResult().isEmpty(); + if (isUnresolved) { + sb.append(String.format("Unresolved(%s)", entry.getCitationKey())); + if (purpose != AuthorYearMarkerPurpose.NORMALIZED) { + sb.append(pageInfoPart); + } + } else { + + CitationLookupResult db = entry.getLookupResult().get(); + + int maxAuthors = (purpose == AuthorYearMarkerPurpose.NORMALIZED + ? style.getMaxAuthors() + : calculateNAuthorsToEmit(style, entry)); + + if (maxAuthorsOverride.isPresent()) { + maxAuthors = maxAuthorsOverride.get(); + } + + AuthorList authorList = getAuthorList(style, db); + String authorString = formatAuthorList(style, authorList, maxAuthors, andString); + sb.append(authorString); + sb.append(yearSep); + + if (!inParenthesis) { + sb.append(startBrace); // parenthesis before year + } + + String year = getCitationMarkerField(style, db, yearFieldNames); + if (year != null) { + sb.append(year); + } + + if (purpose != AuthorYearMarkerPurpose.NORMALIZED) { + String uniqueLetter = entry.getUniqueLetter().orElse(null); + if (uniqueLetter != null) { + sb.append(uniqueLetter); + } + } + + if (purpose != AuthorYearMarkerPurpose.NORMALIZED) { + sb.append(pageInfoPart); + } + + if (!inParenthesis && endingAGroup) { + sb.append(endBrace); // parenthesis after year + } + } + } // for j + + if (inParenthesis) { + sb.append(endBrace); // shared parenthesis + } + sb.append(style.getCitationGroupMarkupAfter()); + return OOText.fromString(sb.toString()); + } + + /** + * Add / override methods for the purpose of creating a normalized citation marker. + */ + private static class CitationMarkerNormEntryWrap implements CitationMarkerEntry { + + CitationMarkerNormEntry inner; + + CitationMarkerNormEntryWrap(CitationMarkerNormEntry inner) { + this.inner = inner; + } + + @Override + public String getCitationKey() { + return inner.getCitationKey(); + } + + @Override + public Optional getLookupResult() { + return inner.getLookupResult(); + } + + @Override + public Optional getUniqueLetter() { + return Optional.empty(); + } + + @Override + public Optional getPageInfo() { + return Optional.empty(); + } + + @Override + public boolean getIsFirstAppearanceOfSource() { + return false; + } + } + + /** + * @param normEntry A citation to process. + * + * @return A normalized citation marker for deciding which + * citations need uniqueLetters. + * + * For details of what "normalized" means: {@see getAuthorYearParenthesisMarker2} + * + * Note: now includes some markup. + */ + static OOText getNormalizedCitationMarker(OOBibStyle style, + CitationMarkerNormEntry normEntry, + Optional maxAuthorsOverride) { + boolean[] startsNewGroup = {true}; + CitationMarkerEntry entry = new CitationMarkerNormEntryWrap(normEntry); + return getAuthorYearParenthesisMarker2(style, + AuthorYearMarkerPurpose.NORMALIZED, + Collections.singletonList(entry), + startsNewGroup, + maxAuthorsOverride); + } + + private static List + getNormalizedCitationMarkers(OOBibStyle style, + List citationMarkerEntries, + Optional maxAuthorsOverride) { + + List normalizedMarkers = new ArrayList<>(citationMarkerEntries.size()); + for (CitationMarkerEntry citationMarkerEntry : citationMarkerEntries) { + OOText normalized = getNormalizedCitationMarker(style, + citationMarkerEntry, + maxAuthorsOverride); + normalizedMarkers.add(normalized); + } + return normalizedMarkers; + } + + /** + * Produce citation marker for a citation group. + * + * Attempts to join consecutive citations: if normalized citations + * markers match and no pageInfo is present, the second entry + * can be presented by appending its uniqueLetter to the + * previous. + * + * If either entry has pageInfo, join is inhibited. + * If the previous entry has more names than we need + * we check with extended normalizedMarkers if they match. + * + * For consecutive identical entries, the second one is omitted. + * Identical requires same pageInfo here, we do not try to merge them. + * Note: notifying the user about them would be nice. + * + * @param citationMarkerEntries A group of citations to process. + * + * @param inParenthesis If true, put parenthesis around the whole group, + * otherwise around each (year,uniqueLetter,pageInfo) part. + * + * @param nonUniqueCitationMarkerHandling What should happen if we + * stumble upon citations with identical normalized + * citation markers which cite different sources and + * are not distinguished by uniqueLetters. + * + * Note: only consecutive citations are checked. + * + */ + public static OOText + createCitationMarker(OOBibStyle style, + List citationMarkerEntries, + boolean inParenthesis, + NonUniqueCitationMarker nonUniqueCitationMarkerHandling) { + + final int nEntries = citationMarkerEntries.size(); + + // Original: + // + // Look for groups of uniquefied entries that should be combined in the output. + // E.g. (Olsen, 2005a, b) should be output instead of (Olsen, 2005a; Olsen, 2005b). + // + // Now: + // - handle pageInfos + // - allow duplicate entries with same or different pageInfos. + // + // We assume entries are already sorted, all we need is to + // group consecutive entries if we can. + // + // We also assume, that identical entries have the same uniqueLetters. + // + + List normalizedMarkers = getNormalizedCitationMarkers(style, + citationMarkerEntries, + Optional.empty()); + + // How many authors would be emitted without grouping. + int[] nAuthorsToEmit = new int[nEntries]; + int[] nAuthorsToEmitRevised = new int[nEntries]; + for (int i = 0; i < nEntries; i++) { + CitationMarkerEntry entry = citationMarkerEntries.get(i); + int n = calculateNAuthorsToEmit(style, entry); + nAuthorsToEmit[i] = n; + nAuthorsToEmitRevised[i] = n; + } + + boolean[] startsNewGroup = new boolean[nEntries]; + List filteredCitationMarkerEntries = new ArrayList<>(nEntries); + int i_out = 0; + + if (nEntries > 0) { + filteredCitationMarkerEntries.add(citationMarkerEntries.get(0)); + startsNewGroup[i_out] = true; + i_out++; + } + + for (int i = 1; i < nEntries; i++) { + final CitationMarkerEntry ce1 = citationMarkerEntries.get(i - 1); + final CitationMarkerEntry ce2 = citationMarkerEntries.get(i); + + final String nm1 = OOText.toString(normalizedMarkers.get(i - 1)); + final String nm2 = OOText.toString(normalizedMarkers.get(i)); + + final boolean isUnresolved1 = ce1.getLookupResult().isEmpty(); + final boolean isUnresolved2 = ce2.getLookupResult().isEmpty(); + + boolean startingNewGroup; + boolean sameAsPrev; /* true indicates ce2 may be omitted from output */ + if (isUnresolved2) { + startingNewGroup = true; + sameAsPrev = false; // keep it visible + } else { + // Does the number of authors to be shown differ? + // Since we compared normalizedMarkers, the difference + // between maxAuthors and maxAuthorsFirst may invalidate + // our expectation that adding uniqueLetter is valid. + + boolean nAuthorsShownInhibitsJoin; + if (isUnresolved1) { + nAuthorsShownInhibitsJoin = true; // no join for unresolved + } else { + final boolean isFirst1 = ce1.getIsFirstAppearanceOfSource(); + final boolean isFirst2 = ce2.getIsFirstAppearanceOfSource(); + + // nAuthorsToEmitRevised[i-1] may have been indirectly increased, + // we have to check that too. + if (!isFirst1 && + !isFirst2 && + (nAuthorsToEmitRevised[i - 1] == nAuthorsToEmit[i - 1])) { + // we can rely on normalizedMarkers + nAuthorsShownInhibitsJoin = false; + } else if (style.getMaxAuthors() == style.getMaxAuthorsFirst()) { + // we can rely on normalizedMarkers + nAuthorsShownInhibitsJoin = false; + } else { + final int prevShown = nAuthorsToEmitRevised[i - 1]; + final int need = nAuthorsToEmit[i]; + + if (prevShown < need) { + // We do not retrospectively change the number of authors shown + // at the previous entry, take that as decided. + nAuthorsShownInhibitsJoin = true; + } else { + // prevShown >= need + // Check with extended normalizedMarkers. + OOText nmx1 = + getNormalizedCitationMarker(style, ce1, Optional.of(prevShown)); + OOText nmx2 = + getNormalizedCitationMarker(style, ce2, Optional.of(prevShown)); + boolean extendedMarkersDiffer = !nmx2.equals(nmx1); + nAuthorsShownInhibitsJoin = extendedMarkersDiffer; + } + } + } + + final boolean citationKeysDiffer = !ce2.getCitationKey().equals(ce1.getCitationKey()); + final boolean normalizedMarkersDiffer = !nm2.equals(nm1); + + Optional pageInfo2 = PageInfo.normalizePageInfo(ce2.getPageInfo()); + Optional pageInfo1 = PageInfo.normalizePageInfo(ce1.getPageInfo()); + final boolean bothPageInfosAreEmpty = pageInfo2.isEmpty() && pageInfo1.isEmpty(); + final boolean pageInfosDiffer = !pageInfo2.equals(pageInfo1); + + Optional ul2 = ce2.getUniqueLetter(); + Optional ul1 = ce1.getUniqueLetter(); + final boolean uniqueLetterPresenceChanged = (ul2.isPresent() != ul1.isPresent()); + final boolean uniqueLettersDiffer = !ul2.equals(ul1); + + final boolean uniqueLetterDoesNotMakeUnique = (citationKeysDiffer + && !normalizedMarkersDiffer + && !uniqueLettersDiffer); + + if (uniqueLetterDoesNotMakeUnique && + nonUniqueCitationMarkerHandling.equals(NonUniqueCitationMarker.THROWS)) { + throw new IllegalArgumentException("different citation keys," + + " but same normalizedMarker and uniqueLetter"); + } + + final boolean pageInfoInhibitsJoin = (bothPageInfosAreEmpty + ? false + : (citationKeysDiffer || pageInfosDiffer)); + + startingNewGroup = (normalizedMarkersDiffer + || nAuthorsShownInhibitsJoin + || pageInfoInhibitsJoin + || uniqueLetterPresenceChanged + || uniqueLetterDoesNotMakeUnique); + + if (!startingNewGroup) { + // inherit from first of group. Used at next i. + nAuthorsToEmitRevised[i] = nAuthorsToEmitRevised[i - 1]; + } + + sameAsPrev = (!startingNewGroup + && !uniqueLettersDiffer + && !citationKeysDiffer + && !pageInfosDiffer); + } + + if (!sameAsPrev) { + filteredCitationMarkerEntries.add(ce2); + startsNewGroup[i_out] = startingNewGroup; + i_out++; + } + } + + return getAuthorYearParenthesisMarker2(style, + (inParenthesis + ? AuthorYearMarkerPurpose.IN_PARENTHESIS + : AuthorYearMarkerPurpose.IN_TEXT), + filteredCitationMarkerEntries, + startsNewGroup, + Optional.empty()); + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetNumCitationMarker.java b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetNumCitationMarker.java new file mode 100644 index 00000000000..5dce4e8caa1 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetNumCitationMarker.java @@ -0,0 +1,281 @@ +package org.jabref.logic.openoffice.style; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.CitationMarkerNumericBibEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericEntry; +import org.jabref.model.openoffice.style.PageInfo; +import org.jabref.model.openoffice.util.OOListUtil; + +class OOBibStyleGetNumCitationMarker { + + /* + * The number encoding "this entry is unresolved" + */ + public final static int UNRESOLVED_ENTRY_NUMBER = 0; + + private OOBibStyleGetNumCitationMarker() { + /**/ + } + + /** + * Defines sort order for CitationMarkerNumericEntry. + */ + private static int compareCitationMarkerNumericEntry(CitationMarkerNumericEntry a, + CitationMarkerNumericEntry b) { + int na = a.getNumber().orElse(UNRESOLVED_ENTRY_NUMBER); + int nb = b.getNumber().orElse(UNRESOLVED_ENTRY_NUMBER); + int res = Integer.compare(na, nb); + if (res == 0) { + res = PageInfo.comparePageInfo(a.getPageInfo(), b.getPageInfo()); + } + return res; + } + + /** + * Create a numeric marker for use in the bibliography as label for the entry. + * + * To support for example numbers in superscript without brackets for the text, + * but "[1]" form for the bibliography, the style can provide + * the optional "BracketBeforeInList" and "BracketAfterInList" strings + * to be used in the bibliography instead of "BracketBefore" and "BracketAfter" + * + * @return "[${number}]" where + * "[" stands for BRACKET_BEFORE_IN_LIST (with fallback BRACKET_BEFORE) + * "]" stands for BRACKET_AFTER_IN_LIST (with fallback BRACKET_AFTER) + * "${number}" stands for the formatted number. + */ + public static OOText getNumCitationMarkerForBibliography(OOBibStyle style, + CitationMarkerNumericBibEntry entry) { + // prefer BRACKET_BEFORE_IN_LIST and BRACKET_AFTER_IN_LIST + String bracketBefore = style.getBracketBeforeInListWithFallBack(); + String bracketAfter = style.getBracketAfterInListWithFallBack(); + StringBuilder sb = new StringBuilder(); + sb.append(style.getCitationGroupMarkupBefore()); + sb.append(bracketBefore); + final Optional current = entry.getNumber(); + sb.append(current.isPresent() + ? String.valueOf(current.get()) + : (OOBibStyle.UNDEFINED_CITATION_MARKER + entry.getCitationKey())); + sb.append(bracketAfter); + sb.append(style.getCitationGroupMarkupAfter()); + return OOText.fromString(sb.toString()); + } + + /* + * emitBlock : a helper for getNumCitationMarker2 + * + * Given a block containing either a single entry or two or more + * entries that are joinable into an "i-j" form, append to {@code sb} the + * formatted text. + * + * Assumes: + * + * - block is not empty + * + * - For a block with a single element the element may have + * pageInfo and its num part may be Optional.empty() + * + * - For a block with two or more elements + * + * - The elements do not have pageInfo and their number part is + * not empty. + * + * - The elements number parts are consecutive positive integers, + * without repetition. + * + */ + private static void emitBlock(List block, + OOBibStyle style, + int minGroupingCount, + StringBuilder sb) { + + final int blockSize = block.size(); + if (blockSize == 0) { + throw new IllegalArgumentException("The block is empty"); + } + + if (blockSize == 1) { + // Add single entry: + CitationMarkerNumericEntry entry = block.get(0); + final Optional num = entry.getNumber(); + sb.append(num.isEmpty() + ? (OOBibStyle.UNDEFINED_CITATION_MARKER + entry.getCitationKey()) + : String.valueOf(num.get())); + // Emit pageInfo + Optional pageInfo = entry.getPageInfo(); + if (pageInfo.isPresent()) { + sb.append(style.getPageInfoSeparator()); + sb.append(OOText.toString(pageInfo.get())); + } + return; + } + + if (blockSize >= 2) { + + /* + * Check assumptions + */ + + if (block.stream().anyMatch(x -> x.getPageInfo().isPresent())) { + throw new IllegalArgumentException("Found pageInfo in a block with more than one elements"); + } + + if (block.stream().anyMatch(x -> x.getNumber().isEmpty())) { + throw new IllegalArgumentException("Found unresolved entry in a block with more than one elements"); + } + + for (int j = 1; j < blockSize; j++) { + if ((block.get(j).getNumber().get() - block.get(j - 1).getNumber().get()) != 1) { + throw new IllegalArgumentException("Numbers are not consecutive"); + } + } + + /* + * Do the actual work + */ + + if (blockSize >= minGroupingCount) { + int first = block.get(0).getNumber().get(); + int last = block.get(blockSize - 1).getNumber().get(); + if (last != (first + blockSize - 1)) { + throw new IllegalArgumentException("blockSize and length of num range differ"); + } + + // Emit: "first-last" + sb.append(first); + sb.append(style.getGroupedNumbersSeparator()); + sb.append(last); + } else { + + // Emit: first, first+1,..., last + for (int j = 0; j < blockSize; j++) { + if (j > 0) { + sb.append(style.getCitationSeparator()); + } + sb.append(block.get(j).getNumber().get()); + } + } + return; + } + } + + /** + * Format a number-based citation marker for the given number or numbers. + * + * @param entries Provide the citation numbers. + * + * An Optional.empty() number means: could not look this up + * in the databases. Positive integers are the valid numbers. + * + * Duplicate citation numbers are allowed: + * + * - If their pageInfos are identical, only a + * single instance is emitted. + * + * - If their pageInfos differ, the number is emitted with each + * distinct pageInfo. + * + * pageInfos are expected to be normalized + * + * @param minGroupingCount Zero and negative means never group. + * Only used by tests to override the value in style. + * + * @return The text for the citation. + * + */ + public static OOText getNumCitationMarker2(OOBibStyle style, + List entries, + int minGroupingCount) { + + final boolean joinIsDisabled = (minGroupingCount <= 0); + final int nCitations = entries.size(); + + String bracketBefore = style.getBracketBefore(); + String bracketAfter = style.getBracketAfter(); + + // Sort a copy of entries + List sorted = OOListUtil.map(entries, e -> e); + sorted.sort(OOBibStyleGetNumCitationMarker::compareCitationMarkerNumericEntry); + + // "[" + StringBuilder sb = new StringBuilder(bracketBefore); + + /* + * Original: + * [2,3,4] -> [2-4] + * [0,1,2] -> [??,1,2] + * [0,1,2,3] -> [??,1-3] + * + * Now we have to consider: duplicate numbers and pageInfos + * [1,1] -> [1] + * [1,1 "pp nn"] -> keep separate if pageInfo differs + * [1 "pp nn",1 "pp nn"] -> [1 "pp nn"] + */ + + boolean blocksEmitted = false; + List currentBlock = new ArrayList<>(); + List nextBlock = new ArrayList<>(); + + for (int i = 0; i < nCitations; i++) { + + final CitationMarkerNumericEntry current = sorted.get(i); + if (current.getNumber().isPresent() && current.getNumber().get() < 0) { + throw new IllegalArgumentException("getNumCitationMarker2: found negative number"); + } + + if (currentBlock.isEmpty()) { + currentBlock.add(current); + } else { + CitationMarkerNumericEntry prev = currentBlock.get(currentBlock.size() - 1); + if (current.getNumber().isEmpty() || prev.getNumber().isEmpty()) { + nextBlock.add(current); // do not join if not found + } else if (joinIsDisabled) { + nextBlock.add(current); // join disabled + } else if (compareCitationMarkerNumericEntry(current, prev) == 0) { + // Same as prev, just forget it. + } else if ((current.getNumber().get() == (prev.getNumber().get() + 1)) + && (prev.getPageInfo().isEmpty()) + && (current.getPageInfo().isEmpty())) { + // Just two consecutive numbers without pageInfo: join + currentBlock.add(current); + } else { + // do not join + nextBlock.add(current); + } + } + + if (nextBlock.size() > 0) { + // emit current block + if (blocksEmitted) { + sb.append(style.getCitationSeparator()); + } + emitBlock(currentBlock, style, minGroupingCount, sb); + blocksEmitted = true; + currentBlock = nextBlock; + nextBlock = new ArrayList<>(); + } + + } + + if (nextBlock.size() != 0) { + throw new IllegalStateException("impossible: (nextBlock.size() != 0) after loop"); + } + + if (currentBlock.size() > 0) { + // We are emitting a block + if (blocksEmitted) { + sb.append(style.getCitationSeparator()); + } + emitBlock(currentBlock, style, minGroupingCount, sb); + } + + // Emit: "]" + sb.append(bracketAfter); + return OOText.fromString(sb.toString()); + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOFormatBibliography.java b/src/main/java/org/jabref/logic/openoffice/style/OOFormatBibliography.java new file mode 100644 index 00000000000..6c22c8abb09 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOFormatBibliography.java @@ -0,0 +1,200 @@ +package org.jabref.logic.openoffice.style; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.layout.Layout; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.openoffice.ootext.OOFormat; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.CitationGroup; +import org.jabref.model.openoffice.style.CitationGroupId; +import org.jabref.model.openoffice.style.CitationGroups; +import org.jabref.model.openoffice.style.CitationPath; +import org.jabref.model.openoffice.style.CitedKey; +import org.jabref.model.openoffice.style.CitedKeys; + +public class OOFormatBibliography { + private static final OOPreFormatter POSTFORMATTER = new OOPreFormatter(); + private static final Field UNIQUEFIER_FIELD = new UnknownField("uniq"); + + private OOFormatBibliography() { + } + + /** + * @return The formatted bibliography, including its title. + */ + public static OOText formatBibliography(CitationGroups cgs, + CitedKeys bibliography, + OOBibStyle style, + boolean alwaysAddCitedOnPages) { + + OOText title = style.getFormattedBibliographyTitle(); + OOText body = formatBibliographyBody(cgs, bibliography, style, alwaysAddCitedOnPages); + return OOText.fromString(title.toString() + body.toString()); + } + + /** + * @return Formatted body of the bibliography. Excludes the title. + */ + public static OOText formatBibliographyBody(CitationGroups cgs, + CitedKeys bibliography, + OOBibStyle style, + boolean alwaysAddCitedOnPages) { + + StringBuilder stringBuilder = new StringBuilder(); + + for (CitedKey ck : bibliography.values()) { + OOText entryText = formatBibliographyEntry(cgs, ck, style, alwaysAddCitedOnPages); + stringBuilder.append(entryText.toString()); + } + + return OOText.fromString(stringBuilder.toString()); + } + + /** + * @return A paragraph. Includes label and "Cited on pages". + */ + public static OOText formatBibliographyEntry(CitationGroups cgs, + CitedKey ck, + OOBibStyle style, + boolean alwaysAddCitedOnPages) { + StringBuilder sb = new StringBuilder(); + + // insert marker "[1]" + if (style.isNumberEntries()) { + sb.append(style.getNumCitationMarkerForBibliography(ck).toString()); + } else { + // !style.isNumberEntries() : emit no prefix + // Note: We might want [citationKey] prefix for style.isCitationKeyCiteMarkers(); + } + + // Add entry body + sb.append(formatBibliographyEntryBody(ck, style).toString()); + + // Add "Cited on pages" + if (ck.getLookupResult().isEmpty() || alwaysAddCitedOnPages) { + sb.append(formatCitedOnPages(cgs, ck).toString()); + } + + // Add paragraph + OOText entryText = OOText.fromString(sb.toString()); + String parStyle = style.getReferenceParagraphFormat(); + return OOFormat.paragraph(entryText, parStyle); + } + + /** + * @return just the body of a bibliography entry. No label, "Cited on pages" or paragraph. + */ + public static OOText formatBibliographyEntryBody(CitedKey ck, OOBibStyle style) { + if (ck.getLookupResult().isEmpty()) { + // Unresolved entry + return OOText.fromString(String.format("Unresolved(%s)", ck.citationKey)); + } else { + // Resolved entry, use the layout engine + BibEntry bibentry = ck.getLookupResult().get().entry; + Layout layout = style.getReferenceFormat(bibentry.getType()); + layout.setPostFormatter(POSTFORMATTER); + + return formatFullReferenceOfBibEntry(layout, + bibentry, + ck.getLookupResult().get().database, + ck.getUniqueLetter().orElse(null)); + } + } + + /** + * Format the reference part of a bibliography entry using a Layout. + * + * @param layout The Layout to format the reference with. + * @param entry The entry to insert. + * @param database The database the entry belongs to. + * @param uniquefier Uniqiefier letter, if any, to append to the entry's year. + * + * @return OOText The reference part of a bibliography entry formatted as OOText + */ + private static OOText formatFullReferenceOfBibEntry(Layout layout, + BibEntry entry, + BibDatabase database, + String uniquefier) { + + // Backup the value of the uniq field, just in case the entry already has it: + Optional oldUniqVal = entry.getField(UNIQUEFIER_FIELD); + + // Set the uniq field with the supplied uniquefier: + if (uniquefier == null) { + entry.clearField(UNIQUEFIER_FIELD); + } else { + entry.setField(UNIQUEFIER_FIELD, uniquefier); + } + + // Do the layout for this entry: + OOText formattedText = OOText.fromString(layout.doLayout(entry, database)); + + // Afterwards, reset the old value: + if (oldUniqVal.isPresent()) { + entry.setField(UNIQUEFIER_FIELD, oldUniqVal.get()); + } else { + entry.clearField(UNIQUEFIER_FIELD); + } + + return formattedText; + } + + /** + * Format links to citations of the source (ck). + * + * Requires reference marks for the citation groups. + * + * - The links are created as references that show page numbers of the reference marks. + * - We do not control the text shown, that is provided by OpenOffice. + */ + private static OOText formatCitedOnPages(CitationGroups cgs, CitedKey ck) { + + if (!cgs.citationGroupsProvideReferenceMarkNameForLinking()) { + return OOText.fromString(""); + } + + StringBuilder sb = new StringBuilder(); + + String prefix = String.format(" (%s: ", Localization.lang("Cited on pages")); + String suffix = ")"; + sb.append(prefix); + + List citationGroups = new ArrayList<>(); + for (CitationPath p : ck.getCitationPaths()) { + CitationGroupId cgid = p.group; + Optional cg = cgs.getCitationGroup(cgid); + if (cg.isEmpty()) { + throw new IllegalStateException(); + } + citationGroups.add(cg.get()); + } + + // sort the citationGroups according to their indexInGlobalOrder + citationGroups.sort((a, b) -> { + Integer aa = a.getIndexInGlobalOrder().orElseThrow(IllegalStateException::new); + Integer bb = b.getIndexInGlobalOrder().orElseThrow(IllegalStateException::new); + return (aa.compareTo(bb)); + }); + + int i = 0; + for (CitationGroup cg : citationGroups) { + if (i > 0) { + sb.append(", "); + } + String markName = cg.getReferenceMarkNameForLinking().orElseThrow(IllegalStateException::new); + OOText xref = OOFormat.formatReferenceToPageNumberOfReferenceMark(markName); + sb.append(xref.toString()); + i++; + } + sb.append(suffix); + return OOText.fromString(sb.toString()); + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java b/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java new file mode 100644 index 00000000000..f8f7de6c7b7 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java @@ -0,0 +1,76 @@ +package org.jabref.logic.openoffice.style; + +import java.util.Comparator; +import java.util.List; + +import org.jabref.logic.bibtex.comparator.FieldComparator; +import org.jabref.logic.bibtex.comparator.FieldComparatorStack; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.openoffice.style.CitationGroups; + +public class OOProcess { + + static final Comparator AUTHOR_YEAR_TITLE_COMPARATOR = makeAuthorYearTitleComparator(); + static final Comparator YEAR_AUTHOR_TITLE_COMPARATOR = makeYearAuthorTitleComparator(); + + private OOProcess() { + /**/ + } + + private static Comparator makeAuthorYearTitleComparator() { + List> ayt = List.of(new FieldComparator(StandardField.AUTHOR), + new FieldComparator(StandardField.YEAR), + new FieldComparator(StandardField.TITLE)); + return new FieldComparatorStack<>(ayt); + } + + private static Comparator makeYearAuthorTitleComparator() { + List> yat = List.of(new FieldComparator(StandardField.YEAR), + new FieldComparator(StandardField.AUTHOR), + new FieldComparator(StandardField.TITLE)); + return new FieldComparatorStack<>(yat); + } + + /** + * The comparator used to sort within a group of merged + * citations. + * + * The term used here is "multicite". The option controlling the + * order is "MultiCiteChronological" in style files. + * + * Yes, they are always sorted one way or another. + */ + public static Comparator comparatorForMulticite(OOBibStyle style) { + if (style.getMultiCiteChronological()) { + return OOProcess.YEAR_AUTHOR_TITLE_COMPARATOR; + } else { + return OOProcess.AUTHOR_YEAR_TITLE_COMPARATOR; + } + } + + /** + * Fill cgs.bibliography and cgs.citationGroupsUnordered//CitationMarker + * according to style. + */ + public static void produceCitationMarkers(CitationGroups cgs, List databases, OOBibStyle style) { + + if (!cgs.hasGlobalOrder()) { + throw new IllegalStateException("produceCitationMarkers: globalOrder is misssing in cgs"); + } + + cgs.lookupCitations(databases); + cgs.imposeLocalOrder(comparatorForMulticite(style)); + + // fill CitationGroup.citationMarker + if (style.isCitationKeyCiteMarkers()) { + OOProcessCitationKeyMarkers.produceCitationMarkers(cgs, style); + } else if (style.isNumberEntries()) { + OOProcessNumericMarkers.produceCitationMarkers(cgs, style); + } else { + OOProcessAuthorYearMarkers.produceCitationMarkers(cgs, style); + } + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOProcessAuthorYearMarkers.java b/src/main/java/org/jabref/logic/openoffice/style/OOProcessAuthorYearMarkers.java new file mode 100644 index 00000000000..896e747913f --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOProcessAuthorYearMarkers.java @@ -0,0 +1,159 @@ +package org.jabref.logic.openoffice.style; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.Citation; +import org.jabref.model.openoffice.style.CitationGroup; +import org.jabref.model.openoffice.style.CitationGroups; +import org.jabref.model.openoffice.style.CitationMarkerEntry; +import org.jabref.model.openoffice.style.CitationType; +import org.jabref.model.openoffice.style.CitedKey; +import org.jabref.model.openoffice.style.CitedKeys; +import org.jabref.model.openoffice.style.NonUniqueCitationMarker; +import org.jabref.model.openoffice.util.OOListUtil; + +class OOProcessAuthorYearMarkers { + + private OOProcessAuthorYearMarkers() { + /**/ + } + + /** + * Fills {@code sortedCitedKeys//normCitMarker} + */ + private static void createNormalizedCitationMarkers(CitedKeys sortedCitedKeys, OOBibStyle style) { + + for (CitedKey ck : sortedCitedKeys.values()) { + ck.setNormalizedCitationMarker(Optional.of(style.getNormalizedCitationMarker(ck))); + } + } + + /** + * For each cited source make the citation keys unique by setting + * the uniqueLetter fields to letters ("a", "b") or Optional.empty() + * + * precondition: sortedCitedKeys already has normalized citation markers. + * precondition: sortedCitedKeys is sorted (according to the order we want the letters to be assigned) + * + * Expects to see data for all cited sources here. + * Clears uniqueLetters before filling. + * + * On return: Each citedKey in sortedCitedKeys has uniqueLetter set as needed. + * The same values are copied to the corresponding citations in cgs. + * + * Depends on: style, citations and their order. + */ + private static void createUniqueLetters(CitedKeys sortedCitedKeys, CitationGroups cgs) { + + // The entries in the clashingKeys lists preserve + // firstAppearance order from sortedCitedKeys.values(). + // + // The index of the citationKey in this order will decide + // which unique letter it receives. + // + Map> normCitMarkerToClachingKeys = new HashMap<>(); + for (CitedKey citedKey : sortedCitedKeys.values()) { + String normCitMarker = OOText.toString(citedKey.getNormalizedCitationMarker().get()); + String citationKey = citedKey.citationKey; + + List clashingKeys = normCitMarkerToClachingKeys.putIfAbsent(normCitMarker, new ArrayList<>(1)); + if (!clashingKeys.contains(citationKey)) { + // First appearance of citationKey, add to list. + clashingKeys.add(citationKey); + } + } + + // Clear old uniqueLetter values. + for (CitedKey citedKey : sortedCitedKeys.values()) { + citedKey.setUniqueLetter(Optional.empty()); + } + + // For sets of citation keys figthing for a normCitMarker + // add unique letters to the year. + for (List clashingKeys : normCitMarkerToClachingKeys.values()) { + if (clashingKeys.size() <= 1) { + continue; // No fight, no letters. + } + // Multiple citation keys: they get their letters + // according to their order in clashingKeys. + int nextUniqueLetter = 'a'; + for (String citationKey : clashingKeys) { + String ul = String.valueOf((char) nextUniqueLetter); + sortedCitedKeys.get(citationKey).setUniqueLetter(Optional.of(ul)); + nextUniqueLetter++; + } + } + sortedCitedKeys.distributeUniqueLetters(cgs); + } + + /* *************************************** + * + * Calculate presentation of citation groups + * (create citMarkers) + * + * **************************************/ + + /** + * Set isFirstAppearanceOfSource in each citation. + * + * Preconditions: globalOrder, localOrder + */ + private static void setIsFirstAppearanceOfSourceInCitations(CitationGroups cgs) { + Set seenBefore = new HashSet<>(); + for (CitationGroup cg : cgs.getCitationGroupsInGlobalOrder()) { + for (Citation cit : cg.getCitationsInLocalOrder()) { + String currentKey = cit.citationKey; + if (!seenBefore.contains(currentKey)) { + cit.setIsFirstAppearanceOfSource(true); + seenBefore.add(currentKey); + } else { + cit.setIsFirstAppearanceOfSource(false); + } + } + } + } + + /** + * Produce citMarkers for normal + * (!isCitationKeyCiteMarkers && !isNumberEntries) styles. + * + * @param cgs + * @param style Bibliography style. + */ + static void produceCitationMarkers(CitationGroups cgs, OOBibStyle style) { + + assert !style.isCitationKeyCiteMarkers(); + assert !style.isNumberEntries(); + // Citations in (Au1, Au2 2000) form + + CitedKeys citedKeys = cgs.getCitedKeysSortedInOrderOfAppearance(); + + createNormalizedCitationMarkers(citedKeys, style); + createUniqueLetters(citedKeys, cgs); + cgs.createPlainBibliographySortedByComparator(OOProcess.AUTHOR_YEAR_TITLE_COMPARATOR); + + // Mark first appearance of each citationKey + setIsFirstAppearanceOfSourceInCitations(cgs); + + for (CitationGroup cg : cgs.getCitationGroupsInGlobalOrder()) { + + final boolean inParenthesis = (cg.citationType == CitationType.AUTHORYEAR_PAR); + final NonUniqueCitationMarker strictlyUnique = NonUniqueCitationMarker.THROWS; + + List cits = cg.getCitationsInLocalOrder(); + List citationMarkerEntries = OOListUtil.map(cits, e -> e); + OOText citMarker = style.createCitationMarker(citationMarkerEntries, + inParenthesis, + strictlyUnique); + cg.setCitationMarker(Optional.of(citMarker)); + } + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOProcessCitationKeyMarkers.java b/src/main/java/org/jabref/logic/openoffice/style/OOProcessCitationKeyMarkers.java new file mode 100644 index 00000000000..9789d6c6caa --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOProcessCitationKeyMarkers.java @@ -0,0 +1,35 @@ +package org.jabref.logic.openoffice.style; + +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.Citation; +import org.jabref.model.openoffice.style.CitationGroup; +import org.jabref.model.openoffice.style.CitationGroups; +import org.jabref.model.openoffice.util.OOListUtil; + +class OOProcessCitationKeyMarkers { + + private OOProcessCitationKeyMarkers() { + /**/ + } + + /** + * Produce citation markers for the case when the citation + * markers are the citation keys themselves, separated by commas. + */ + static void produceCitationMarkers(CitationGroups cgs, OOBibStyle style) { + + assert style.isCitationKeyCiteMarkers(); + + cgs.createPlainBibliographySortedByComparator(OOProcess.AUTHOR_YEAR_TITLE_COMPARATOR); + + for (CitationGroup cg : cgs.getCitationGroupsInGlobalOrder()) { + String citMarker = + style.getCitationGroupMarkupBefore() + + String.join(",", OOListUtil.map(cg.getCitationsInLocalOrder(), Citation::getCitationKey)) + + style.getCitationGroupMarkupAfter(); + cg.setCitationMarker(Optional.of(OOText.fromString(citMarker))); + } + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/OOProcessNumericMarkers.java b/src/main/java/org/jabref/logic/openoffice/style/OOProcessNumericMarkers.java new file mode 100644 index 00000000000..589ebfb7a48 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/style/OOProcessNumericMarkers.java @@ -0,0 +1,47 @@ +package org.jabref.logic.openoffice.style; + +import java.util.List; +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.CitationGroup; +import org.jabref.model.openoffice.style.CitationGroups; +import org.jabref.model.openoffice.style.CitationMarkerNumericEntry; +import org.jabref.model.openoffice.util.OOListUtil; + +class OOProcessNumericMarkers { + + private OOProcessNumericMarkers() { + /**/ + } + + /** + * Produce citation markers for the case of numbered citations + * with bibliography sorted by first appearance in the text. + * + * Numbered citation markers for each CitationGroup. + * Numbering is according to first appearance. + * Assumes global order and local order are already applied. + * + * @param cgs + * @param style + * + */ + static void produceCitationMarkers(CitationGroups cgs, OOBibStyle style) { + + assert style.isNumberEntries(); + + if (style.isSortByPosition()) { + cgs.createNumberedBibliographySortedInOrderOfAppearance(); + } else { + cgs.createNumberedBibliographySortedByComparator(OOProcess.AUTHOR_YEAR_TITLE_COMPARATOR); + } + + for (CitationGroup cg : cgs.getCitationGroupsInGlobalOrder()) { + List cits = OOListUtil.map(cg.getCitationsInLocalOrder(), e -> e); + OOText citMarker = style.getNumCitationMarker2(cits); + cg.setCitationMarker(Optional.of(citMarker)); + } + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/style/StyleLoader.java b/src/main/java/org/jabref/logic/openoffice/style/StyleLoader.java index 6d7651a24de..3098e06233c 100644 --- a/src/main/java/org/jabref/logic/openoffice/style/StyleLoader.java +++ b/src/main/java/org/jabref/logic/openoffice/style/StyleLoader.java @@ -61,19 +61,19 @@ public boolean addStyleIfValid(String filename) { try { OOBibStyle newStyle = new OOBibStyle(new File(filename), layoutFormatterPreferences, encoding); if (externalStyles.contains(newStyle)) { - LOGGER.info("External style file " + filename + " already existing."); + LOGGER.info("External style file {} already existing.", filename); } else if (newStyle.isValid()) { externalStyles.add(newStyle); storeExternalStyles(); return true; } else { - LOGGER.error(String.format("Style with filename %s is invalid", filename)); + LOGGER.error("Style with filename {} is invalid", filename); } } catch (FileNotFoundException e) { // The file couldn't be found... should we tell anyone? - LOGGER.info("Cannot find external style file " + filename, e); + LOGGER.info("Cannot find external style file {}", filename, e); } catch (IOException e) { - LOGGER.info("Problem reading external style file " + filename, e); + LOGGER.info("Problem reading external style file {}", filename, e); } return false; } @@ -88,13 +88,13 @@ private void loadExternalStyles() { if (style.isValid()) { // Problem! externalStyles.add(style); } else { - LOGGER.error(String.format("Style with filename %s is invalid", filename)); + LOGGER.error("Style with filename {} is invalid", filename); } } catch (FileNotFoundException e) { // The file couldn't be found... should we tell anyone? - LOGGER.info("Cannot find external style file " + filename, e); + LOGGER.info("Cannot find external style file {}", filename); } catch (IOException e) { - LOGGER.info("Problem reading external style file " + filename, e); + LOGGER.info("Problem reading external style file {}", filename, e); } } } @@ -105,7 +105,7 @@ private void loadInternalStyles() { try { internalStyles.add(new OOBibStyle(filename, layoutFormatterPreferences)); } catch (IOException e) { - LOGGER.info("Problem reading internal style file " + filename, e); + LOGGER.info("Problem reading internal style file {}", filename, e); } } } diff --git a/src/main/java/org/jabref/model/openoffice/style/Citation.java b/src/main/java/org/jabref/model/openoffice/style/Citation.java new file mode 100644 index 00000000000..7bae4fd2548 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/Citation.java @@ -0,0 +1,146 @@ +package org.jabref.model.openoffice.style; + +import java.util.List; +import java.util.Optional; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.util.OOPair; + +public class Citation implements ComparableCitation, CitationMarkerEntry, CitationMarkerNumericEntry { + + /** key in database */ + public final String citationKey; + + /** Result from database lookup. Optional.empty() if not found. */ + private Optional db; + + /** The number used for numbered citation styles . */ + private Optional number; + + /** Letter that makes the in-text citation unique. */ + private Optional uniqueLetter; + + /** pageInfo */ + private Optional pageInfo; + + /** isFirstAppearanceOfSource */ + private boolean isFirstAppearanceOfSource; + + /** + * + */ + public Citation(String citationKey) { + this.citationKey = citationKey; + this.db = Optional.empty(); + this.number = Optional.empty(); + this.uniqueLetter = Optional.empty(); + this.pageInfo = Optional.empty(); + this.isFirstAppearanceOfSource = false; + } + + @Override + public String getCitationKey() { + return citationKey; + } + + @Override + public Optional getPageInfo() { + return pageInfo; + } + + @Override + public boolean getIsFirstAppearanceOfSource() { + return isFirstAppearanceOfSource; + } + + @Override + public Optional getBibEntry() { + return (db.isPresent() + ? Optional.of(db.get().entry) + : Optional.empty()); + } + + public static Optional lookup(BibDatabase database, String key) { + return (database + .getEntryByCitationKey(key) + .map(bibEntry -> new CitationLookupResult(bibEntry, database))); + } + + public static Optional lookup(List databases, String key) { + return (databases.stream() + .map(database -> Citation.lookup(database, key)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst()); + } + + public void lookupInDatabases(List databases) { + db = Citation.lookup(databases, citationKey); + } + + public Optional getLookupResult() { + return db; + } + + public void setLookupResult(Optional db) { + this.db = db; + } + + public boolean isUnresolved() { + return db.isEmpty(); + } + + @Override + public Optional getNumber() { + return number; + } + + public void setNumber(Optional number) { + this.number = number; + } + + public int getNumberOrThrow() { + return number.get(); + } + + public Optional getUniqueLetter() { + return uniqueLetter; + } + + public void setUniqueLetter(Optional uniqueLetter) { + this.uniqueLetter = uniqueLetter; + } + + public void setPageInfo(Optional pageInfo) { + Optional normalizedPageInfo = PageInfo.normalizePageInfo(pageInfo); + if (!normalizedPageInfo.equals(pageInfo)) { + throw new IllegalArgumentException("setPageInfo argument is not normalized"); + } + this.pageInfo = normalizedPageInfo; + } + + public void setIsFirstAppearanceOfSource(boolean value) { + isFirstAppearanceOfSource = value; + } + + /* + * Setters for CitationGroups.distribute() + */ + public static void setLookupResult(OOPair> pair) { + Citation cit = pair.a; + cit.db = pair.b; + } + + public static void setNumber(OOPair> pair) { + Citation cit = pair.a; + cit.number = pair.b; + } + + public static void setUniqueLetter(OOPair> pair) { + Citation cit = pair.a; + cit.uniqueLetter = pair.b; + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationGroup.java b/src/main/java/org/jabref/model/openoffice/style/CitationGroup.java new file mode 100644 index 00000000000..3b9021eb73f --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationGroup.java @@ -0,0 +1,156 @@ +package org.jabref.model.openoffice.style; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.util.OOListUtil; + +/** + * A CitationGroup describes a group of citations. + */ +public class CitationGroup { + + public final OODataModel dataModel; + + /* + * Identifies this citation group. + */ + public final CitationGroupId cgid; + + /* + * The core data, stored in the document: + * The type of citation and citations in storage order. + */ + public final CitationType citationType; + public final List citationsInStorageOrder; + + /* + * Extra data + */ + + /* + * A name of a reference mark to link to by formatCitedOnPages. + * May be initially empty, if backend does not use reference marks. + * + * produceCitationMarkers might want fill it to support cross-references to citation groups from + * the bibliography. + */ + private Optional referenceMarkNameForLinking; + + /* + * Indices into citations: citations[localOrder[i]] provides ith citation according to the + * currently imposed local order for presentation. + * + * Initialized to (0..(nCitations-1)) in the constructor. + */ + private List localOrder; + + /* + * "Cited on pages" uses this to sort the cross-references. + */ + private Optional indexInGlobalOrder; + + /* + * Citation marker. + */ + private Optional citationMarker; + + public CitationGroup(OODataModel dataModel, + CitationGroupId cgid, + CitationType citationType, + List citationsInStorageOrder, + Optional referenceMarkNameForLinking) { + this.dataModel = dataModel; + this.cgid = cgid; + this.citationType = citationType; + this.citationsInStorageOrder = Collections.unmodifiableList(citationsInStorageOrder); + this.localOrder = OOListUtil.makeIndices(citationsInStorageOrder.size()); + this.referenceMarkNameForLinking = referenceMarkNameForLinking; + this.indexInGlobalOrder = Optional.empty(); + this.citationMarker = Optional.empty(); + } + + public int numberOfCitations() { + return citationsInStorageOrder.size(); + } + + /* + * localOrder + */ + + /** + * Sort citations for presentation within a CitationGroup. + */ + void imposeLocalOrder(Comparator entryComparator) { + + // For JabRef52 the single pageInfo is always in the last-in-localorder citation. + // We adjust here accordingly by taking it out and adding it back after sorting. + final int last = this.numberOfCitations() - 1; + Optional lastPageInfo = Optional.empty(); + if (dataModel == OODataModel.JabRef52) { + Citation lastCitation = getCitationsInLocalOrder().get(last); + lastPageInfo = lastCitation.getPageInfo(); + lastCitation.setPageInfo(Optional.empty()); + } + + this.localOrder = OOListUtil.order(citationsInStorageOrder, + new CompareCitation(entryComparator, true)); + + if (dataModel == OODataModel.JabRef52) { + getCitationsInLocalOrder().get(last).setPageInfo(lastPageInfo); + } + } + + public List getLocalOrder() { + return Collections.unmodifiableList(localOrder); + } + + /* + * citations + */ + + public List getCitationsInLocalOrder() { + return OOListUtil.map(localOrder, i -> citationsInStorageOrder.get(i)); + } + + /* + * indexInGlobalOrder + */ + + public void setIndexInGlobalOrder(Optional indexInGlobalOrder) { + this.indexInGlobalOrder = indexInGlobalOrder; + } + + public Optional getIndexInGlobalOrder() { + return this.indexInGlobalOrder; + } + + /* + * referenceMarkNameForLinking + */ + + public Optional getReferenceMarkNameForLinking() { + return referenceMarkNameForLinking; + } + + public void setReferenceMarkNameForLinking(Optional referenceMarkNameForLinking) { + this.referenceMarkNameForLinking = referenceMarkNameForLinking; + } + + /* + * citationMarker + */ + + public void setCitationMarker(Optional citationMarker) { + this.citationMarker = citationMarker; + } + + public Optional getCitationMarker() { + return this.citationMarker; + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationGroupId.java b/src/main/java/org/jabref/model/openoffice/style/CitationGroupId.java new file mode 100644 index 00000000000..6ba6b760cb6 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationGroupId.java @@ -0,0 +1,18 @@ +package org.jabref.model.openoffice.style; + +/** + * Identifies a citation group in a document. + */ +public class CitationGroupId { + String id; + public CitationGroupId(String id) { + this.id = id; + } + + /** + * CitationEntry needs some string identifying the group that it can pass back later. + */ + public String citationGroupIdAsString() { + return id; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java b/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java new file mode 100644 index 00000000000..0bfe4a832d0 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationGroups.java @@ -0,0 +1,295 @@ +package org.jabref.model.openoffice.style; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.util.OOListUtil; +import org.jabref.model.openoffice.util.OOPair; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CitationGroups : the set of citation groups in the document. + * + * This is the main input (as well as output) for creating citation markers and bibliography. + * + */ +public class CitationGroups { + + private static final Logger LOGGER = LoggerFactory.getLogger(CitationGroups.class); + + private Map citationGroupsUnordered; + + /** + * Provides order of appearance for the citation groups. + */ + private Optional> globalOrder; + + /** + * This is going to be the bibliography + */ + private Optional bibliography; + + /** + * Constructor + */ + public CitationGroups(Map citationGroups) { + + this.citationGroupsUnordered = citationGroups; + + this.globalOrder = Optional.empty(); + this.bibliography = Optional.empty(); + } + + public int numberOfCitationGroups() { + return citationGroupsUnordered.size(); + } + + /** + * For each citation in {@code where} call {@code fun.accept(new Pair(citation, value));} + */ + public void distributeToCitations(List where, + Consumer> fun, + T value) { + + for (CitationPath p : where) { + CitationGroup cg = citationGroupsUnordered.get(p.group); + if (cg == null) { + LOGGER.warn("CitationGroups.distributeToCitations: group missing"); + continue; + } + Citation cit = cg.citationsInStorageOrder.get(p.storageIndexInGroup); + fun.accept(new OOPair<>(cit, value)); + } + } + + /* + * Look up each Citation in databases. + */ + public void lookupCitations(List databases) { + /* + * It is not clear which of the two solutions below is better. + */ + + // (1) collect-lookup-distribute + // + // CitationDatabaseLookupResult for the same citation key is the same object. Until we + // insert a new citation from the GUI. + CitedKeys cks = getCitedKeysUnordered(); + cks.lookupInDatabases(databases); + cks.distributeLookupResults(this); + + // (2) lookup each citation directly + // + // CitationDatabaseLookupResult for the same citation key may be a different object: + // CitedKey.addPath has to use equals, so CitationDatabaseLookupResult has to override + // Object.equals, which depends on BibEntry.equals and BibDatabase.equals doing the + // right thing. Seems to work. But what we gained from avoiding collect-and-distribute + // may be lost in more complicated consistency checking in addPath. + // + /// for (CitationGroup cg : getCitationGroupsUnordered()) { + /// for (Citation cit : cg.citationsInStorageOrder) { + /// cit.lookupInDatabases(databases); + /// } + /// } + } + + public List getCitationGroupsUnordered() { + return new ArrayList<>(citationGroupsUnordered.values()); + } + + /** + * Citation groups in {@code globalOrder} + */ + public List getCitationGroupsInGlobalOrder() { + if (globalOrder.isEmpty()) { + throw new IllegalStateException("getCitationGroupsInGlobalOrder: not ordered yet"); + } + return OOListUtil.map(globalOrder.get(), cgid -> citationGroupsUnordered.get(cgid)); + } + + /** + * Impose an order of citation groups by providing the order of their citation group + * idendifiers. + * + * Also set indexInGlobalOrder for each citation group. + */ + public void setGlobalOrder(List globalOrder) { + Objects.requireNonNull(globalOrder); + if (globalOrder.size() != numberOfCitationGroups()) { + throw new IllegalStateException("setGlobalOrder: globalOrder.size() != numberOfCitationGroups()"); + } + this.globalOrder = Optional.of(globalOrder); + + // Propagate to each CitationGroup + int i = 0; + for (CitationGroupId cgid : globalOrder) { + citationGroupsUnordered.get(cgid).setIndexInGlobalOrder(Optional.of(i)); + i++; + } + } + + public boolean hasGlobalOrder() { + return globalOrder.isPresent(); + } + + /** + * Impose an order for citations within each group. + */ + public void imposeLocalOrder(Comparator entryComparator) { + for (CitationGroup cg : citationGroupsUnordered.values()) { + cg.imposeLocalOrder(entryComparator); + } + } + + /** + * Collect citations into a list of cited sources using neither CitationGroup.globalOrder or + * Citation.localOrder + */ + public CitedKeys getCitedKeysUnordered() { + LinkedHashMap res = new LinkedHashMap<>(); + for (CitationGroup cg : citationGroupsUnordered.values()) { + int storageIndexInGroup = 0; + for (Citation cit : cg.citationsInStorageOrder) { + String key = cit.citationKey; + CitationPath path = new CitationPath(cg.cgid, storageIndexInGroup); + if (res.containsKey(key)) { + res.get(key).addPath(path, cit); + } else { + res.put(key, new CitedKey(key, path, cit)); + } + storageIndexInGroup++; + } + } + return new CitedKeys(res); + } + + /** + * CitedKeys created iterating citations in (globalOrder,localOrder) + */ + public CitedKeys getCitedKeysSortedInOrderOfAppearance() { + if (!hasGlobalOrder()) { + throw new IllegalStateException("getSortedCitedKeys: no globalOrder"); + } + LinkedHashMap res = new LinkedHashMap<>(); + for (CitationGroup cg : getCitationGroupsInGlobalOrder()) { + for (int i : cg.getLocalOrder()) { + Citation cit = cg.citationsInStorageOrder.get(i); + String citationKey = cit.citationKey; + CitationPath path = new CitationPath(cg.cgid, i); + if (res.containsKey(citationKey)) { + res.get(citationKey).addPath(path, cit); + } else { + res.put(citationKey, new CitedKey(citationKey, path, cit)); + } + } + } + return new CitedKeys(res); + } + + public Optional getBibliography() { + return bibliography; + } + + /** + * @return Citation keys where lookupCitations() failed. + */ + public List getUnresolvedKeys() { + + CitedKeys bib = getBibliography().orElse(getCitedKeysUnordered()); + + List unresolvedKeys = new ArrayList<>(); + for (CitedKey ck : bib.values()) { + if (ck.getLookupResult().isEmpty()) { + unresolvedKeys.add(ck.citationKey); + } + } + return unresolvedKeys; + } + + public void createNumberedBibliographySortedInOrderOfAppearance() { + if (!bibliography.isEmpty()) { + throw new IllegalStateException("createNumberedBibliographySortedInOrderOfAppearance:" + + " already have a bibliography"); + } + CitedKeys citedKeys = getCitedKeysSortedInOrderOfAppearance(); + citedKeys.numberCitedKeysInCurrentOrder(); + citedKeys.distributeNumbers(this); + bibliography = Optional.of(citedKeys); + } + + /** + * precondition: database lookup already performed (otherwise we just sort citation keys) + */ + public void createPlainBibliographySortedByComparator(Comparator entryComparator) { + if (!bibliography.isEmpty()) { + throw new IllegalStateException("createPlainBibliographySortedByComparator: already have a bibliography"); + } + CitedKeys citedKeys = getCitedKeysUnordered(); + citedKeys.sortByComparator(entryComparator); + bibliography = Optional.of(citedKeys); + } + + /** + * precondition: database lookup already performed (otherwise we just sort citation keys) + */ + public void createNumberedBibliographySortedByComparator(Comparator entryComparator) { + if (!bibliography.isEmpty()) { + throw new IllegalStateException("createNumberedBibliographySortedByComparator: already have a bibliography"); + } + CitedKeys citedKeys = getCitedKeysUnordered(); + citedKeys.sortByComparator(entryComparator); + citedKeys.numberCitedKeysInCurrentOrder(); + citedKeys.distributeNumbers(this); + bibliography = Optional.of(citedKeys); + } + + /* + * Query by CitationGroupId + */ + + public Optional getCitationGroup(CitationGroupId cgid) { + CitationGroup cg = citationGroupsUnordered.get(cgid); + return Optional.ofNullable(cg); + } + + /* + * @return true if all citation groups have referenceMarkNameForLinking + */ + public boolean citationGroupsProvideReferenceMarkNameForLinking() { + for (CitationGroup cg : citationGroupsUnordered.values()) { + if (cg.getReferenceMarkNameForLinking().isEmpty()) { + return false; + } + } + return true; + } + + /* + * Callbacks. + */ + + public void afterCreateCitationGroup(CitationGroup cg) { + citationGroupsUnordered.put(cg.cgid, cg); + + globalOrder = Optional.empty(); + bibliography = Optional.empty(); + } + + public void afterRemoveCitationGroup(CitationGroup cg) { + citationGroupsUnordered.remove(cg.cgid); + globalOrder.map(l -> l.remove(cg.cgid)); + + bibliography = Optional.empty(); + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationLookupResult.java b/src/main/java/org/jabref/model/openoffice/style/CitationLookupResult.java new file mode 100644 index 00000000000..bfe6dc1debe --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationLookupResult.java @@ -0,0 +1,49 @@ +package org.jabref.model.openoffice.style; + +import java.util.Objects; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; + +public class CitationLookupResult { + + public final BibEntry entry; + public final BibDatabase database; + + public CitationLookupResult(BibEntry entry, BibDatabase database) { + Objects.requireNonNull(entry); + Objects.requireNonNull(database); + this.entry = entry; + this.database = database; + } + + /** + * Note: BibEntry overrides Object.equals, but BibDatabase does not. + * + * Consequently, {@code this.database.equals(that.database)} below + * is equivalent to {@code this.database == that.database}. + * + * Since within each GUI call we use a fixed list of databases, it is OK. + * + * CitationLookupResult.equals is used in CitedKey.addPath to check the added Citation + * refers to the same source as the others. As long as we look up each citation key + * only once (in CitationGroups.lookupCitations), the default implementation for equals + * would be sufficient (and could also omit hashCode below). + */ + @Override + public boolean equals(Object otherObject) { + if (otherObject == this) { + return true; + } + if (!(otherObject instanceof CitationLookupResult)) { + return false; + } + CitationLookupResult that = (CitationLookupResult) otherObject; + return Objects.equals(this.entry, that.entry) && Objects.equals(this.database, that.database); + } + + @Override + public int hashCode() { + return Objects.hash(entry, database); + } +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationMarkerEntry.java b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerEntry.java new file mode 100644 index 00000000000..5fcb9bcc1a8 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerEntry.java @@ -0,0 +1,28 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; + +/** + * This is what we need for createCitationMarker to produce author-year citation markers. + */ +public interface CitationMarkerEntry extends CitationMarkerNormEntry { + + /** + * uniqueLetter or Optional.empty() if not needed. + */ + Optional getUniqueLetter(); + + /** + * pageInfo for this citation, provided by the user. + * May be empty, for none. + */ + Optional getPageInfo(); + + /** + * @return true if this citation is the first appearance of the source cited. Some styles use + * different limit on the number of authors shown in this case. + */ + boolean getIsFirstAppearanceOfSource(); +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNormEntry.java b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNormEntry.java new file mode 100644 index 00000000000..a2581b38b60 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNormEntry.java @@ -0,0 +1,21 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +/** + * This is what we need to produce normalized author-year citation markers. + */ +public interface CitationMarkerNormEntry { + + /** Citation key. This is what we usually get from the document. + * + * Used if getLookupResult() returns empty, which indicates failure to lookup in the databases. + */ + String getCitationKey(); + + /** Result of looking up citation key in databases. + * + * Optional.empty() indicates unresolved citation. + */ + Optional getLookupResult(); +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericBibEntry.java b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericBibEntry.java new file mode 100644 index 00000000000..398282113b8 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericBibEntry.java @@ -0,0 +1,19 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +/** + * This is for the numeric bibliography labels. + */ +public interface CitationMarkerNumericBibEntry { + + /** + * For unresolved citation we show the citation key. + */ + String getCitationKey(); + + /** + * @return Optional.empty() for unresolved + */ + Optional getNumber(); +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericEntry.java b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericEntry.java new file mode 100644 index 00000000000..9c0c6081489 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationMarkerNumericEntry.java @@ -0,0 +1,20 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; + +/** + * This is what we need for numeric citation markers. + */ +public interface CitationMarkerNumericEntry { + + String getCitationKey(); + + /** + * @return Optional.empty() for unresolved + */ + Optional getNumber(); + + Optional getPageInfo(); +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationPath.java b/src/main/java/org/jabref/model/openoffice/style/CitationPath.java new file mode 100644 index 00000000000..0920ea20feb --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationPath.java @@ -0,0 +1,17 @@ +package org.jabref.model.openoffice.style; + +/** + * Identifies a citation with the identifier of the citation group containing it and its storage + * index within. + */ +public class CitationPath { + + public final CitationGroupId group; + + public final int storageIndexInGroup; + + CitationPath(CitationGroupId group, int storageIndexInGroup) { + this.group = group; + this.storageIndexInGroup = storageIndexInGroup; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitationType.java b/src/main/java/org/jabref/model/openoffice/style/CitationType.java new file mode 100644 index 00000000000..14189e903e3 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitationType.java @@ -0,0 +1,24 @@ +package org.jabref.model.openoffice.style; + +/* + * Presentation types of citation groups. + */ +public enum CitationType { + + AUTHORYEAR_PAR, + AUTHORYEAR_INTEXT, + INVISIBLE_CIT; + + public boolean inParenthesis() { + return switch (this) { + case AUTHORYEAR_PAR, INVISIBLE_CIT -> true; + case AUTHORYEAR_INTEXT -> false; + }; + } + + public boolean withText() { + return (this != INVISIBLE_CIT); + } +} + + diff --git a/src/main/java/org/jabref/model/openoffice/style/CitedKey.java b/src/main/java/org/jabref/model/openoffice/style/CitedKey.java new file mode 100644 index 00000000000..5331204bd75 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitedKey.java @@ -0,0 +1,138 @@ +package org.jabref.model.openoffice.style; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.ootext.OOText; + +/** + * Cited keys are collected from the citations in citation groups. + * + * They contain backreferences to the corresponding citations in {@code where}. This allows the + * extra information generated using CitedKeys to be distributed back to the in-text citations. + */ +public class CitedKey implements + ComparableCitedKey, + CitationMarkerNormEntry, + CitationMarkerNumericBibEntry { + + public final String citationKey; + private final List where; + + private Optional db; + private Optional number; // For Numbered citation styles. + private Optional uniqueLetter; // For AuthorYear citation styles. + private Optional normCitMarker; // For AuthorYear citation styles. + + CitedKey(String citationKey, CitationPath path, Citation cit) { + + this.citationKey = citationKey; + this.where = new ArrayList<>(); // remember order + this.where.add(path); + + // synchronized with Citation + this.db = cit.getLookupResult(); + this.number = cit.getNumber(); + this.uniqueLetter = cit.getUniqueLetter(); + + // CitedKey only + this.normCitMarker = Optional.empty(); + } + + /* + * Implement ComparableCitedKey + */ + @Override + public String getCitationKey() { + return citationKey; + } + + @Override + public Optional getBibEntry() { + return db.map(e -> e.entry); + } + + /* + * Implement CitationMarkerNormEntry + */ + @Override + public Optional getLookupResult() { + return db; + } + + /* + * Implement CitationMarkerNumericBibEntry + */ + @Override + public Optional getNumber() { + return number; + } + + public void setNumber(Optional number) { + this.number = number; + } + + public List getCitationPaths() { + return new ArrayList<>(where); + } + + public Optional getUniqueLetter() { + return uniqueLetter; + } + + public void setUniqueLetter(Optional uniqueLetter) { + this.uniqueLetter = uniqueLetter; + } + + public Optional getNormalizedCitationMarker() { + return normCitMarker; + } + + public void setNormalizedCitationMarker(Optional normCitMarker) { + this.normCitMarker = normCitMarker; + } + + /** + * Appends to end of {@code where} + */ + void addPath(CitationPath path, Citation cit) { + this.where.add(path); + + // Check consistency + if (!cit.getLookupResult().equals(this.db)) { + throw new IllegalStateException("CitedKey.addPath: mismatch on cit.db"); + } + if (!cit.getNumber().equals(this.number)) { + throw new IllegalStateException("CitedKey.addPath: mismatch on cit.number"); + } + if (!cit.getUniqueLetter().equals(this.uniqueLetter)) { + throw new IllegalStateException("CitedKey.addPath: mismatch on cit.uniqueLetter"); + } + } + + /* + * Lookup + */ + void lookupInDatabases(List databases) { + this.db = Citation.lookup(databases, this.citationKey); + } + + void distributeLookupResult(CitationGroups cgs) { + cgs.distributeToCitations(where, Citation::setLookupResult, db); + } + + /* + * Make unique using a letter or by numbering + */ + + void distributeNumber(CitationGroups cgs) { + cgs.distributeToCitations(where, Citation::setNumber, number); + } + + void distributeUniqueLetter(CitationGroups cgs) { + cgs.distributeToCitations(where, Citation::setUniqueLetter, uniqueLetter); + } +} diff --git a/src/main/java/org/jabref/model/openoffice/style/CitedKeys.java b/src/main/java/org/jabref/model/openoffice/style/CitedKeys.java new file mode 100644 index 00000000000..898fadbebe9 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CitedKeys.java @@ -0,0 +1,84 @@ +package org.jabref.model.openoffice.style; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; + +public class CitedKeys { + + /** + * Order-preserving map from citation keys to associated data. + */ + private LinkedHashMap data; + + CitedKeys(LinkedHashMap data) { + this.data = data; + } + + /** + * The cited keys in their current order. + */ + public List values() { + return new ArrayList<>(data.values()); + } + + public CitedKey get(String citationKey) { + return data.get(citationKey); + } + + /** + * Sort entries for the bibliography. + */ + void sortByComparator(Comparator entryComparator) { + List cks = new ArrayList<>(data.values()); + cks.sort(new CompareCitedKey(entryComparator, true)); + LinkedHashMap newData = new LinkedHashMap<>(); + for (CitedKey ck : cks) { + newData.put(ck.citationKey, ck); + } + data = newData; + } + + void numberCitedKeysInCurrentOrder() { + int i = 1; + for (CitedKey ck : data.values()) { + if (ck.getLookupResult().isPresent()) { + ck.setNumber(Optional.of(i)); + i++; + } else { + // Unresolved citations do not get a number. + ck.setNumber(Optional.empty()); + } + } + } + + public void lookupInDatabases(List databases) { + for (CitedKey ck : this.data.values()) { + ck.lookupInDatabases(databases); + } + } + + void distributeLookupResults(CitationGroups cgs) { + for (CitedKey ck : this.data.values()) { + ck.distributeLookupResult(cgs); + } + } + + void distributeNumbers(CitationGroups cgs) { + for (CitedKey ck : this.data.values()) { + ck.distributeNumber(cgs); + } + } + + public void distributeUniqueLetters(CitationGroups cgs) { + for (CitedKey ck : this.data.values()) { + ck.distributeUniqueLetter(cgs); + } + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/style/ComparableCitation.java b/src/main/java/org/jabref/model/openoffice/style/ComparableCitation.java new file mode 100644 index 00000000000..81120c09b2a --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/ComparableCitation.java @@ -0,0 +1,13 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; + +/** + * When sorting citations (in a group), we also consider pageInfo. + * Otherwise we sort citations as cited keys. + */ +public interface ComparableCitation extends ComparableCitedKey { + Optional getPageInfo(); +} diff --git a/src/main/java/org/jabref/model/openoffice/style/ComparableCitedKey.java b/src/main/java/org/jabref/model/openoffice/style/ComparableCitedKey.java new file mode 100644 index 00000000000..23b3ffaba78 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/ComparableCitedKey.java @@ -0,0 +1,16 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +import org.jabref.model.entry.BibEntry; + +/** + * This is what we need to sort bibliography entries. + */ +public interface ComparableCitedKey { + + String getCitationKey(); + + Optional getBibEntry(); +} + diff --git a/src/main/java/org/jabref/model/openoffice/style/CompareCitation.java b/src/main/java/org/jabref/model/openoffice/style/CompareCitation.java new file mode 100644 index 00000000000..966715b11a8 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CompareCitation.java @@ -0,0 +1,30 @@ +package org.jabref.model.openoffice.style; + +import java.util.Comparator; + +import org.jabref.model.entry.BibEntry; + +/* + * Given a Comparator provide a Comparator that can handle unresolved + * citation keys and takes pageInfo into account. + */ +public class CompareCitation implements Comparator { + + private CompareCitedKey citedKeyComparator; + + CompareCitation(Comparator entryComparator, boolean unresolvedComesFirst) { + this.citedKeyComparator = new CompareCitedKey(entryComparator, unresolvedComesFirst); + } + + public int compare(ComparableCitation a, ComparableCitation b) { + int res = citedKeyComparator.compare(a, b); + + // Also consider pageInfo + if (res == 0) { + res = PageInfo.comparePageInfo(a.getPageInfo(), b.getPageInfo()); + } + return res; + } +} + + diff --git a/src/main/java/org/jabref/model/openoffice/style/CompareCitedKey.java b/src/main/java/org/jabref/model/openoffice/style/CompareCitedKey.java new file mode 100644 index 00000000000..7f6d3adfab0 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/CompareCitedKey.java @@ -0,0 +1,39 @@ +package org.jabref.model.openoffice.style; + +import java.util.Comparator; +import java.util.Optional; + +import org.jabref.model.entry.BibEntry; + +/* + * Given a Comparator provide a Comparator that also handles + * unresolved citation keys. + */ +public class CompareCitedKey implements Comparator { + + Comparator entryComparator; + boolean unresolvedComesFirst; + + CompareCitedKey(Comparator entryComparator, boolean unresolvedComesFirst) { + this.entryComparator = entryComparator; + this.unresolvedComesFirst = unresolvedComesFirst; + } + + public int compare(ComparableCitedKey a, ComparableCitedKey b) { + Optional aBibEntry = a.getBibEntry(); + Optional bBibEntry = b.getBibEntry(); + final int mul = unresolvedComesFirst ? (+1) : (-1); + + if (aBibEntry.isEmpty() && bBibEntry.isEmpty()) { + // Both are unresolved: compare them by citation key. + return a.getCitationKey().compareTo(b.getCitationKey()); + } else if (aBibEntry.isEmpty()) { + return -mul; + } else if (bBibEntry.isEmpty()) { + return mul; + } else { + // Proper comparison of entries + return entryComparator.compare(aBibEntry.get(), bBibEntry.get()); + } + } +} diff --git a/src/main/java/org/jabref/model/openoffice/style/NonUniqueCitationMarker.java b/src/main/java/org/jabref/model/openoffice/style/NonUniqueCitationMarker.java new file mode 100644 index 00000000000..34300734d2f --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/NonUniqueCitationMarker.java @@ -0,0 +1,15 @@ +package org.jabref.model.openoffice.style; + +/** + * What should createCitationMarker do if it discovers that uniqueLetters provided are not + * sufficient for unique presentation? + */ +public enum NonUniqueCitationMarker { + + /** Give an insufficient representation anyway. */ + FORGIVEN, + + /** Throw an exception */ + THROWS +} + diff --git a/src/main/java/org/jabref/model/openoffice/style/OODataModel.java b/src/main/java/org/jabref/model/openoffice/style/OODataModel.java new file mode 100644 index 00000000000..835715dfca5 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/OODataModel.java @@ -0,0 +1,34 @@ +package org.jabref.model.openoffice.style; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; + +/** What is the data stored? */ +public enum OODataModel { + + /** JabRef52: pageInfo belongs to CitationGroup, not Citation. */ + JabRef52, + + /** JabRef60: pageInfo belongs to Citation. */ + JabRef60; + + /** + * @param pageInfo Nullable. + * @return JabRef60 style pageInfo list with pageInfo in the last slot. + */ + public static List> fakePageInfos(String pageInfo, int nCitations) { + List> pageInfos = new ArrayList<>(nCitations); + for (int i = 0; i < nCitations; i++) { + pageInfos.add(Optional.empty()); + } + if (pageInfo != null) { + final int last = nCitations - 1; + Optional optionalPageInfo = Optional.ofNullable(OOText.fromString(pageInfo)); + pageInfos.set(last, PageInfo.normalizePageInfo(optionalPageInfo)); + } + return pageInfos; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/style/PageInfo.java b/src/main/java/org/jabref/model/openoffice/style/PageInfo.java new file mode 100644 index 00000000000..ec3a8436dcf --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/style/PageInfo.java @@ -0,0 +1,48 @@ +package org.jabref.model.openoffice.style; + +import java.util.Optional; + +import org.jabref.model.openoffice.ootext.OOText; + +public class PageInfo { + + private PageInfo() { + // hide public constructor + } + + /* + * pageInfo normalization + */ + public static Optional normalizePageInfo(Optional optionalText) { + if (optionalText == null || optionalText.isEmpty() || "".equals(OOText.toString(optionalText.get()))) { + return Optional.empty(); + } + String str = OOText.toString(optionalText.get()); + String trimmed = str.trim(); + if (trimmed.equals("")) { + return Optional.empty(); + } + return Optional.of(OOText.fromString(trimmed)); + } + + /** + * Defines sort order for pageInfo strings. + * + * Optional.empty comes before non-empty. + */ + public static int comparePageInfo(Optional a, Optional b) { + + Optional aa = PageInfo.normalizePageInfo(a); + Optional bb = PageInfo.normalizePageInfo(b); + if (aa.isEmpty() && bb.isEmpty()) { + return 0; + } + if (aa.isEmpty()) { + return -1; + } + if (bb.isEmpty()) { + return +1; + } + return aa.get().toString().compareTo(bb.get().toString()); + } +} diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 4c39b711bb8..147380113f6 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2315,6 +2315,7 @@ Custom\ DOI\ URI=Custom DOI URI Customization=Customization Use\ custom\ DOI\ base\ URI\ for\ article\ access=Use custom DOI base URI for article access +Cited\ on\ pages=Cited on pages Unable\ to\ find\ valid\ certification\ path\ to\ requested\ target(%0),\ download\ anyway?=Unable to find valid certification path to requested target(%0), download anyway? Download\ operation\ canceled.=Download operation canceled. diff --git a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java index 786e678324c..82d7888afd8 100644 --- a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java +++ b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTest.java @@ -11,7 +11,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jabref.logic.layout.Layout; import org.jabref.logic.layout.LayoutFormatterPreferences; @@ -20,6 +23,11 @@ import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.entry.types.UnknownEntryType; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.CitationMarkerEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericBibEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericEntry; +import org.jabref.model.openoffice.style.NonUniqueCitationMarker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -80,13 +88,85 @@ void testNumerical() throws IOException { assertTrue(style.isSortByPosition()); } + /* + * begin helpers + */ + static String runGetNumCitationMarker2a(OOBibStyle style, + List num, int minGroupingCount, boolean inList) { + return OOBibStyleTestHelper.runGetNumCitationMarker2a(style, num, minGroupingCount, inList); + } + + static CitationMarkerNumericEntry numEntry(String key, int num, String pageInfoOrNull) { + return OOBibStyleTestHelper.numEntry(key, num, pageInfoOrNull); + } + + static CitationMarkerNumericBibEntry numBibEntry(String key, Optional num) { + return OOBibStyleTestHelper.numBibEntry(key, num); + } + + static String runGetNumCitationMarker2b(OOBibStyle style, + int minGroupingCount, + CitationMarkerNumericEntry... s) { + List input = Stream.of(s).collect(Collectors.toList()); + OOText res = style.getNumCitationMarker2(input, minGroupingCount); + return res.toString(); + } + + static CitationMarkerEntry makeCitationMarkerEntry(BibEntry entry, + BibDatabase database, + String uniqueLetterQ, + String pageInfoQ, + boolean isFirstAppearanceOfSource) { + return OOBibStyleTestHelper.makeCitationMarkerEntry(entry, + database, + uniqueLetterQ, + pageInfoQ, + isFirstAppearanceOfSource); + } + + /* + * Similar to old API. pageInfo is new, and unlimAuthors is + * replaced with isFirstAppearanceOfSource + */ + static String getCitationMarker2(OOBibStyle style, + List entries, + Map entryDBMap, + boolean inParenthesis, + String[] uniquefiers, + Boolean[] isFirstAppearanceOfSource, + String[] pageInfo) { + return OOBibStyleTestHelper.getCitationMarker2(style, + entries, + entryDBMap, + inParenthesis, + uniquefiers, + isFirstAppearanceOfSource, + pageInfo); + } + + /* + * end helpers + */ + @Test void testGetNumCitationMarker() throws IOException { OOBibStyle style = new OOBibStyle(StyleLoader.DEFAULT_NUMERICAL_STYLE_PATH, layoutFormatterPreferences); assertEquals("[1] ", style.getNumCitationMarker(Arrays.asList(1), -1, true)); + assertEquals("[1] ", runGetNumCitationMarker2a(style, Arrays.asList(1), -1, true)); + assertEquals("[1]", style.getNumCitationMarker(Arrays.asList(1), -1, false)); + assertEquals("[1]", runGetNumCitationMarker2a(style, Arrays.asList(1), -1, false)); + assertEquals("[1]", runGetNumCitationMarker2b(style, -1, numEntry("key", 1, null))); + assertEquals("[1] ", style.getNumCitationMarker(Arrays.asList(1), 0, true)); + assertEquals("[1] ", runGetNumCitationMarker2a(style, Arrays.asList(1), 0, true)); + + /* + * The following tests as for a numeric label for a + * bibliography entry containing more than one numbers. + * We do not need this, not reproduced. + */ assertEquals("[1-3] ", style.getNumCitationMarker(Arrays.asList(1, 2, 3), 1, true)); assertEquals("[1; 2; 3] ", style.getNumCitationMarker(Arrays.asList(1, 2, 3), 5, true)); assertEquals("[1; 2; 3] ", style.getNumCitationMarker(Arrays.asList(1, 2, 3), -1, true)); @@ -95,12 +175,24 @@ void testGetNumCitationMarker() throws IOException { String citation = style.getNumCitationMarker(Arrays.asList(1), -1, false); assertEquals("[1; pp. 55-56]", style.insertPageInfo(citation, "pp. 55-56")); + + CitationMarkerNumericEntry e2 = numEntry("key", 1, "pp. 55-56"); + assertEquals(true, e2.getPageInfo().isPresent()); + assertEquals("pp. 55-56", e2.getPageInfo().get().toString()); + citation = runGetNumCitationMarker2b(style, -1, e2); + assertEquals("[1; pp. 55-56]", citation); + + OOBibStyleTestHelper.testGetNumCitationMarkerExtra(style); } @Test void testGetNumCitationMarkerUndefined() throws IOException { OOBibStyle style = new OOBibStyle(StyleLoader.DEFAULT_NUMERICAL_STYLE_PATH, layoutFormatterPreferences); + /* + * Testing bibliography labels with multiple numbers again. + * Not reproduced. + */ assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "; 2-4] ", style.getNumCitationMarker(Arrays.asList(4, 2, 3, 0), 1, true)); @@ -113,6 +205,52 @@ void testGetNumCitationMarkerUndefined() throws IOException { assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "; " + OOBibStyle.UNDEFINED_CITATION_MARKER + "; " + OOBibStyle.UNDEFINED_CITATION_MARKER + "] ", style.getNumCitationMarker(Arrays.asList(0, 0, 0), 1, true)); + + /* + * We have these instead: + */ + + // unresolved citations look like [??key] + assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "key" + "]", + runGetNumCitationMarker2b(style, 1, + numEntry("key", 0, null))); + + // pageInfo is shown for unresolved citations + assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "key" + "; p1]", + runGetNumCitationMarker2b(style, 1, + numEntry("key", 0, "p1"))); + + // unresolved citations sorted to the front + assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "key" + "; 2-4]", + runGetNumCitationMarker2b(style, 1, + numEntry("x4", 4, ""), + numEntry("x2", 2, ""), + numEntry("x3", 3, ""), + numEntry("key", 0, ""))); + + assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "key" + "; 1-3]", + runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, ""), + numEntry("x2", 2, ""), + numEntry("y3", 3, ""), + numEntry("key", 0, ""))); + + // multiple unresolved citations are not collapsed + assertEquals("[" + + OOBibStyle.UNDEFINED_CITATION_MARKER + "x1" + "; " + + OOBibStyle.UNDEFINED_CITATION_MARKER + "x2" + "; " + + OOBibStyle.UNDEFINED_CITATION_MARKER + "x3" + "]", + runGetNumCitationMarker2b(style, 1, + numEntry("x1", 0, ""), + numEntry("x2", 0, ""), + numEntry("x3", 0, ""))); + + /* + * BIBLIOGRAPHY + */ + CitationMarkerNumericBibEntry x = numBibEntry("key", Optional.empty()); + assertEquals("[" + OOBibStyle.UNDEFINED_CITATION_MARKER + "key" + "] ", + style.getNumCitationMarkerForBibliography(x).toString()); } @Test @@ -120,8 +258,14 @@ void testGetCitProperty() throws IOException { OOBibStyle style = new OOBibStyle(StyleLoader.DEFAULT_NUMERICAL_STYLE_PATH, layoutFormatterPreferences); assertEquals(", ", style.getStringCitProperty("AuthorSeparator")); + + // old assertEquals(3, style.getIntCitProperty("MaxAuthors")); assertTrue(style.getBooleanCitProperty(OOBibStyle.MULTI_CITE_CHRONOLOGICAL)); + // new + assertEquals(3, style.getMaxAuthors()); + assertTrue(style.getMultiCiteChronological()); + assertEquals("Default", style.getCitationCharacterFormat()); assertEquals("Default [number] style file.", style.getName()); Set journals = style.getJournals(); @@ -138,17 +282,39 @@ void testGetCitationMarker() throws IOException { .withField(StandardField.PUBLISHER, "ACM") .withField(StandardField.TITLE, "Extending XP practices to support security requirements engineering") .withField(StandardField.PAGES, "11--18"); + entry.setCitationKey("Bostrom2006"); // citation key is not optional now BibDatabase database = new BibDatabase(); database.insertEntry(entry); Map entryDBMap = new HashMap<>(); entryDBMap.put(entry, database); + // Check what unlimAuthors values correspond to isFirstAppearanceOfSource false/true + assertEquals(3, style.getMaxAuthors()); + assertEquals(-1, style.getMaxAuthorsFirst()); + assertEquals("[Boström et al., 2006]", style.getCitationMarker(Collections.singletonList(entry), entryDBMap, true, null, null)); + assertEquals("[Boström et al., 2006]", + getCitationMarker2(style, + Collections.singletonList(entry), entryDBMap, + true, null, null, null)); + assertEquals("Boström et al. [2006]", style.getCitationMarker(Collections.singletonList(entry), entryDBMap, false, null, new int[]{3})); + assertEquals("Boström et al. [2006]", + getCitationMarker2(style, + Collections.singletonList(entry), entryDBMap, + false, null, new Boolean[]{false}, null)); + assertEquals("[Boström, Wäyrynen, Bodén, Beznosov & Kruchten, 2006]", style.getCitationMarker(Collections.singletonList(entry), entryDBMap, true, null, new int[]{5})); + assertEquals("[Boström, Wäyrynen, Bodén, Beznosov & Kruchten, 2006]", + getCitationMarker2(style, + Collections.singletonList(entry), entryDBMap, + true, + null, + new Boolean[]{true} /* corresponds to -1, not 5 */, + null)); } @Test @@ -225,6 +391,7 @@ void testInstitutionAuthorMarker() throws IOException { BibDatabase database = new BibDatabase(); BibEntry entry = new BibEntry(); + entry.setCitationKey("JabRef2016"); entry.setType(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "{JabRef Development Team}"); entry.setField(StandardField.TITLE, "JabRef Manual"); @@ -233,6 +400,10 @@ void testInstitutionAuthorMarker() throws IOException { entries.add(entry); entryDBMap.put(entry, database); assertEquals("[JabRef Development Team, 2016]", style.getCitationMarker(entries, entryDBMap, true, null, null)); + + assertEquals("[JabRef Development Team, 2016]", + getCitationMarker2(style, + entries, entryDBMap, true, null, null, null)); } @Test @@ -513,4 +684,225 @@ void testIsValidWithDefaultSectionAtTheStart() throws Exception { OOBibStyle style = new OOBibStyle("testWithDefaultAtFirstLIne.jstyle", layoutFormatterPreferences); assertTrue(style.isValid()); } + + @Test + void testGetCitationMarkerJoinFirst() throws IOException { + OOBibStyle style = new OOBibStyle(StyleLoader.DEFAULT_NUMERICAL_STYLE_PATH, + layoutFormatterPreferences); + + // Question: What should happen if some of the sources is + // marked as isFirstAppearanceOfSource? + // This test documents what is happening now. + + // Two entries with identical normalizedMarkers and many authors. + BibEntry entry1 = new BibEntry() + .withField(StandardField.AUTHOR, + "Gustav Bostr\\\"{o}m" + + " and Jaana W\\\"{a}yrynen" + + " and Marine Bod\\'{e}n" + + " and Konstantin Beznosov" + + " and Philippe Kruchten") + .withField(StandardField.YEAR, "2006") + .withField(StandardField.BOOKTITLE, "A book 1") + .withField(StandardField.PUBLISHER, "ACM") + .withField(StandardField.TITLE, "Title 1") + .withField(StandardField.PAGES, "11--18"); + entry1.setCitationKey("b1"); + + BibEntry entry2 = new BibEntry() + .withField(StandardField.AUTHOR, + "Gustav Bostr\\\"{o}m" + + " and Jaana W\\\"{a}yrynen" + + " and Marine Bod\\'{e}n" + + " and Konstantin Beznosov" + + " and Philippe Kruchten") + .withField(StandardField.YEAR, "2006") + .withField(StandardField.BOOKTITLE, "A book 2") + .withField(StandardField.PUBLISHER, "ACM") + .withField(StandardField.TITLE, "title2") + .withField(StandardField.PAGES, "11--18"); + entry2.setCitationKey("b2"); + + // Last Author differs. + BibEntry entry3 = new BibEntry() + .withField(StandardField.AUTHOR, + "Gustav Bostr\\\"{o}m" + + " and Jaana W\\\"{a}yrynen" + + " and Marine Bod\\'{e}n" + + " and Konstantin Beznosov" + + " and Philippe NotKruchten") + .withField(StandardField.YEAR, "2006") + .withField(StandardField.BOOKTITLE, "A book 3") + .withField(StandardField.PUBLISHER, "ACM") + .withField(StandardField.TITLE, "title3") + .withField(StandardField.PAGES, "11--18"); + entry3.setCitationKey("b3"); + + BibDatabase database = new BibDatabase(); + database.insertEntry(entry1); + database.insertEntry(entry2); + database.insertEntry(entry3); + + // Without pageInfo, two isFirstAppearanceOfSource may be joined. + // The third is NotKruchten, should not be joined. + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", null, true); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry2, database, "b", null, true); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry3, database, "c", null, true); + citationMarkerEntries.add(cm3); + + assertEquals("[Boström, Wäyrynen, Bodén, Beznosov & Kruchten, 2006a,b" + + "; Boström, Wäyrynen, Bodén, Beznosov & NotKruchten, 2006c]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + + assertEquals("Boström, Wäyrynen, Bodén, Beznosov & Kruchten [2006a,b]" + + "; Boström, Wäyrynen, Bodén, Beznosov & NotKruchten [2006c]", + style.createCitationMarker(citationMarkerEntries, + false, + NonUniqueCitationMarker.THROWS).toString()); + } + + // Without pageInfo, only the first is isFirstAppearanceOfSource. + // The second may be joined, based on expanded normalizedMarkers. + // The third is NotKruchten, should not be joined. + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", null, true); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry2, database, "b", null, false); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry3, database, "c", null, false); + citationMarkerEntries.add(cm3); + + assertEquals("[Boström, Wäyrynen, Bodén, Beznosov & Kruchten, 2006a,b" + + "; Boström et al., 2006c]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + + } + // Without pageInfo, only the second is isFirstAppearanceOfSource. + // The second is not joined, because it is a first appearance, thus + // requires more names to be shown. + // The third is NotKruchten, should not be joined. + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", null, false); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry2, database, "b", null, true); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry3, database, "c", null, false); + citationMarkerEntries.add(cm3); + + assertEquals("[Boström et al., 2006a" + + "; Boström, Wäyrynen, Bodén, Beznosov & Kruchten, 2006b" + + "; Boström et al., 2006c]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + } + + // Without pageInfo, neither is isFirstAppearanceOfSource. + // The second is joined. + // The third is NotKruchten, but is joined because NotKruchten is not among the names shown. + // Is this the correct behaviour? + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", null, false); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry2, database, "b", null, false); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry3, database, "c", null, false); + citationMarkerEntries.add(cm3); + + assertEquals("[Boström et al., 2006a,b,c]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + } + + // With pageInfo: different entries with identical non-null pageInfo: not joined. + // XY [2000a,b,c; p1] whould be confusing. + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", "p1", false); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry2, database, "b", "p1", false); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry3, database, "c", "p1", false); + citationMarkerEntries.add(cm3); + + assertEquals("[Boström et al., 2006a; p1" + + "; Boström et al., 2006b; p1" + + "; Boström et al., 2006c; p1]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + } + + // With pageInfo: same entries with identical non-null pageInfo: collapsed. + // Note: "same" here looks at the visible parts and citation key only, + // but ignores the rest. Normally the citation key should distinguish. + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", "p1", false); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry1, database, "a", "p1", false); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry1, database, "a", "p1", false); + citationMarkerEntries.add(cm3); + + assertEquals("[Boström et al., 2006a; p1]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + } + // With pageInfo: same entries with different pageInfo: kept separate. + // Empty ("") and missing pageInfos considered equal, thus collapsed. + if (true) { + List citationMarkerEntries = new ArrayList<>(); + CitationMarkerEntry cm1 = + makeCitationMarkerEntry(entry1, database, "a", "p1", false); + citationMarkerEntries.add(cm1); + CitationMarkerEntry cm2 = + makeCitationMarkerEntry(entry1, database, "a", "p2", false); + citationMarkerEntries.add(cm2); + CitationMarkerEntry cm3 = + makeCitationMarkerEntry(entry1, database, "a", "", false); + citationMarkerEntries.add(cm3); + CitationMarkerEntry cm4 = + makeCitationMarkerEntry(entry1, database, "a", null, false); + citationMarkerEntries.add(cm4); + + assertEquals("[Boström et al., 2006a; p1" + + "; Boström et al., 2006a; p2" + + "; Boström et al., 2006a]", + style.createCitationMarker(citationMarkerEntries, + true, + NonUniqueCitationMarker.THROWS).toString()); + } + } } diff --git a/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java new file mode 100644 index 00000000000..ae5e83abd2f --- /dev/null +++ b/src/test/java/org/jabref/logic/openoffice/style/OOBibStyleTestHelper.java @@ -0,0 +1,336 @@ +package org.jabref.logic.openoffice.style; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.Citation; +import org.jabref.model.openoffice.style.CitationLookupResult; +import org.jabref.model.openoffice.style.CitationMarkerEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericBibEntry; +import org.jabref.model.openoffice.style.CitationMarkerNumericEntry; +import org.jabref.model.openoffice.style.NonUniqueCitationMarker; +import org.jabref.model.openoffice.style.PageInfo; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OOBibStyleTestHelper { + /* + * begin Helpers for testing style.getNumCitationMarker2 + */ + + /* + * Minimal implementation for CitationMarkerNumericEntry + */ + static class CitationMarkerNumericEntryImpl implements CitationMarkerNumericEntry { + + /* + * The number encoding "this entry is unresolved" for the constructor. + */ + public final static int UNRESOLVED_ENTRY_NUMBER = 0; + + private String citationKey; + private Optional num; + private Optional pageInfo; + + public CitationMarkerNumericEntryImpl(String citationKey, int num, Optional pageInfo) { + this.citationKey = citationKey; + this.num = (num == UNRESOLVED_ENTRY_NUMBER + ? Optional.empty() + : Optional.of(num)); + this.pageInfo = PageInfo.normalizePageInfo(pageInfo); + } + + @Override + public String getCitationKey() { + return citationKey; + } + + @Override + public Optional getNumber() { + return num; + } + + @Override + public Optional getPageInfo() { + return pageInfo; + } + } + + static class CitationMarkerNumericBibEntryImpl implements CitationMarkerNumericBibEntry { + String key; + Optional number; + + public CitationMarkerNumericBibEntryImpl(String key, Optional number) { + this.key = key; + this.number = number; + } + + @Override + public String getCitationKey() { + return key; + } + + @Override + public Optional getNumber() { + return number; + } + } + + static CitationMarkerNumericBibEntry numBibEntry(String key, Optional number) { + return new CitationMarkerNumericBibEntryImpl(key, number); + } + + /** + * Reproduce old method + * + * @param inList true means label for the bibliography + */ + static String runGetNumCitationMarker2a(OOBibStyle style, + List num, int minGroupingCount, boolean inList) { + if (inList) { + if (num.size() != 1) { + throw new IllegalArgumentException("Numeric label for the bibliography with " + + String.valueOf(num.size()) + " numbers?"); + } + int n = num.get(0); + CitationMarkerNumericBibEntryImpl x = + new CitationMarkerNumericBibEntryImpl("key", + (n == 0) ? Optional.empty() : Optional.of(n)); + return style.getNumCitationMarkerForBibliography(x).toString(); + } else { + List input = + num.stream() + .map(n -> + new CitationMarkerNumericEntryImpl("key" + String.valueOf(n), + n, + Optional.empty())) + .collect(Collectors.toList()); + return style.getNumCitationMarker2(input, minGroupingCount).toString(); + } + } + + /* + * Unlike getNumCitationMarker, getNumCitationMarker2 can handle pageInfo. + */ + static CitationMarkerNumericEntry numEntry(String key, int num, String pageInfoOrNull) { + Optional pageInfo = Optional.ofNullable(OOText.fromString(pageInfoOrNull)); + return new CitationMarkerNumericEntryImpl(key, num, pageInfo); + } + + static String runGetNumCitationMarker2b(OOBibStyle style, + int minGroupingCount, + CitationMarkerNumericEntry... s) { + List input = Stream.of(s).collect(Collectors.toList()); + OOText res = style.getNumCitationMarker2(input, minGroupingCount); + return res.toString(); + } + + /* + * end Helpers for testing style.getNumCitationMarker2 + */ + + /* + * begin helper + */ + static CitationMarkerEntry makeCitationMarkerEntry(BibEntry entry, + BibDatabase database, + String uniqueLetterQ, + String pageInfoQ, + boolean isFirstAppearanceOfSource) { + if (entry.getCitationKey().isEmpty()) { + throw new IllegalArgumentException("entry.getCitationKey() is empty"); + } + String citationKey = entry.getCitationKey().get(); + Citation result = new Citation(citationKey); + result.setLookupResult(Optional.of(new CitationLookupResult(entry, database))); + result.setUniqueLetter(Optional.ofNullable(uniqueLetterQ)); + Optional pageInfo = Optional.ofNullable(OOText.fromString(pageInfoQ)); + result.setPageInfo(PageInfo.normalizePageInfo(pageInfo)); + result.setIsFirstAppearanceOfSource(isFirstAppearanceOfSource); + return result; + } + + /* + * Similar to old API. pageInfo is new, and unlimAuthors is + * replaced with isFirstAppearanceOfSource + */ + static String getCitationMarker2(OOBibStyle style, + List entries, + Map entryDBMap, + boolean inParenthesis, + String[] uniquefiers, + Boolean[] isFirstAppearanceOfSource, + String[] pageInfo) { + if (uniquefiers == null) { + uniquefiers = new String[entries.size()]; + Arrays.fill(uniquefiers, null); + } + if (pageInfo == null) { + pageInfo = new String[entries.size()]; + Arrays.fill(pageInfo, null); + } + if (isFirstAppearanceOfSource == null) { + isFirstAppearanceOfSource = new Boolean[entries.size()]; + Arrays.fill(isFirstAppearanceOfSource, false); + } + List citationMarkerEntries = new ArrayList<>(); + for (int i = 0; i < entries.size(); i++) { + BibEntry entry = entries.get(i); + CitationMarkerEntry e = makeCitationMarkerEntry(entry, + entryDBMap.get(entry), + uniquefiers[i], + pageInfo[i], + isFirstAppearanceOfSource[i]); + citationMarkerEntries.add(e); + } + return style.createCitationMarker(citationMarkerEntries, + inParenthesis, + NonUniqueCitationMarker.THROWS).toString(); + } + + /* + * end helper + */ + + static void testGetNumCitationMarkerExtra(OOBibStyle style) throws IOException { + // Identical numeric entries are joined. + assertEquals("[1; 2]", runGetNumCitationMarker2b(style, 3, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x1", 2, null), + numEntry("x2", 1, null))); + + // ... unless minGroupingCount <= 0 + assertEquals("[1; 1; 2; 2]", runGetNumCitationMarker2b(style, 0, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x1", 2, null), + numEntry("x2", 1, null))); + + // ... or have different pageInfos + assertEquals("[1; p1a; 1; p1b; 2; p2; 3]", runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, "p1a"), + numEntry("x1", 1, "p1b"), + numEntry("x2", 2, "p2"), + numEntry("x2", 2, "p2"), + numEntry("x3", 3, null), + numEntry("x3", 3, null))); + + // Consecutive numbers can become a range ... + assertEquals("[1-3]", runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + // ... unless minGroupingCount is too high + assertEquals("[1; 2; 3]", runGetNumCitationMarker2b(style, 4, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + // ... or if minGroupingCount <= 0 + assertEquals("[1; 2; 3]", runGetNumCitationMarker2b(style, 0, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + // ... a pageInfo needs to be emitted + assertEquals("[1; p1; 2-3]", runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, "p1"), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + // null and "" pageInfos are taken as equal. + // Due to trimming, " " is the same as well. + assertEquals("[1]", runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, ""), + numEntry("x1", 1, null), + numEntry("x1", 1, " "))); + + // pageInfos are trimmed + assertEquals("[1; p1]", runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, "p1"), + numEntry("x1", 1, " p1"), + numEntry("x1", 1, "p1 "))); + + // The citation numbers come out sorted + assertEquals("[3-5; 7; 10-12]", runGetNumCitationMarker2b(style, 1, + numEntry("x12", 12, null), + numEntry("x7", 7, null), + numEntry("x3", 3, null), + numEntry("x4", 4, null), + numEntry("x11", 11, null), + numEntry("x10", 10, null), + numEntry("x5", 5, null))); + + // pageInfos are sorted together with the numbers + // (but they inhibit ranges where they are, even if they are identical, + // but not empty-or-null) + assertEquals("[3; p3; 4; p4; 5; p5; 7; p7; 10; px; 11; px; 12; px]", + runGetNumCitationMarker2b(style, 1, + numEntry("x12", 12, "px"), + numEntry("x7", 7, "p7"), + numEntry("x3", 3, "p3"), + numEntry("x4", 4, "p4"), + numEntry("x11", 11, "px"), + numEntry("x10", 10, "px"), + numEntry("x5", 5, "p5"))); + + // pageInfo sorting (for the same number) + assertEquals("[1; 1; a; 1; b]", + runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, ""), + numEntry("x1", 1, "b"), + numEntry("x1", 1, "a"))); + + // pageInfo sorting (for the same number) is not numeric. + assertEquals("[1; p100; 1; p20; 1; p9]", + runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, "p20"), + numEntry("x1", 1, "p9"), + numEntry("x1", 1, "p100"))); + + assertEquals("[1-3]", + runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + assertEquals("[1; 2; 3]", + runGetNumCitationMarker2b(style, 5, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + assertEquals("[1; 2; 3]", + runGetNumCitationMarker2b(style, -1, + numEntry("x1", 1, null), + numEntry("x2", 2, null), + numEntry("x3", 3, null))); + + assertEquals("[1; 3; 12]", + runGetNumCitationMarker2b(style, 1, + numEntry("x1", 1, null), + numEntry("x12", 12, null), + numEntry("x3", 3, null))); + + assertEquals("[3-5; 7; 10-12]", + runGetNumCitationMarker2b(style, 1, + numEntry("x12", 12, ""), + numEntry("x7", 7, ""), + numEntry("x3", 3, ""), + numEntry("x4", 4, ""), + numEntry("x11", 11, ""), + numEntry("x10", 10, ""), + numEntry("x5", 5, ""))); + } +}