diff --git a/CHANGELOG.md b/CHANGELOG.md index 149ea8d8bc7..3fa07b2d99c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We added support for searching ShortScience for an entry through the user's browser. [#6018](https://github.com/JabRef/jabref/pull/6018) - We added tooltips for most fields in the entry editor containing a short description. [#5847](https://github.com/JabRef/jabref/issues/5847) +- We added support for basic markdown in custom formatted previews [#6194](https://github.com/JabRef/jabref/issues/6194) ### Changed diff --git a/build.gradle b/build.gradle index b0953b82aae..48afce41006 100644 --- a/build.gradle +++ b/build.gradle @@ -195,6 +195,7 @@ dependencies { exclude module: "log4j-core" } + implementation 'com.vladsch.flexmark:flexmark-all:0.60.2' testImplementation 'io.github.classgraph:classgraph:4.8.65' testImplementation 'org.junit.jupiter:junit-jupiter:5.6.1' diff --git a/external-libraries.txt b/external-libraries.txt index f67e09af7fd..05e11258f7d 100644 --- a/external-libraries.txt +++ b/external-libraries.txt @@ -334,6 +334,11 @@ Project: OpenOffice.org URL: http://www.openoffice.org/api/SDK License: Apache-2.0 +Id: com.vladsch.flexmark:flexmark-all +Project: flexmark-java +URL: https://github.com/vsch/flexmark-java +License: BSD-2-Clause + ## Sorted list of runtime dependencies output by gradle ```text diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 91b721e8a4a..d04c03e13f2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -82,4 +82,11 @@ requires org.antlr.antlr4.runtime; requires flowless; requires org.apache.tika.core; + + requires flexmark; + requires flexmark.ext.gfm.strikethrough; + requires flexmark.ext.gfm.tasklist; + requires flexmark.util.ast; + requires flexmark.util.data; + requires annotations; } diff --git a/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/src/main/java/org/jabref/logic/layout/LayoutEntry.java index ec7d6067a7e..ee6632a7046 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -57,6 +57,7 @@ import org.jabref.logic.layout.format.JournalAbbreviator; import org.jabref.logic.layout.format.LastPage; import org.jabref.logic.layout.format.LatexToUnicodeFormatter; +import org.jabref.logic.layout.format.MarkdownFormatter; import org.jabref.logic.layout.format.NameFormatter; import org.jabref.logic.layout.format.NoSpaceBetweenAbbreviations; import org.jabref.logic.layout.format.NotFoundFormatter; @@ -536,6 +537,8 @@ private LayoutFormatter getLayoutFormatterByName(String name) { return new WrapContent(); case "WrapFileLinks": return new WrapFileLinks(prefs.getFileLinkPreferences()); + case "Markdown": + return new MarkdownFormatter(); default: return null; } diff --git a/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java b/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java new file mode 100644 index 00000000000..4aeff2a55ad --- /dev/null +++ b/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java @@ -0,0 +1,42 @@ +package org.jabref.logic.layout.format; + +import java.util.List; +import java.util.Objects; + +import org.jabref.logic.layout.LayoutFormatter; + +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; +import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.data.MutableDataSet; + +public class MarkdownFormatter implements LayoutFormatter { + + private final Parser parser; + private final HtmlRenderer renderer; + + public MarkdownFormatter() { + MutableDataSet options = new MutableDataSet(); + options.set(Parser.EXTENSIONS, List.of( + StrikethroughExtension.create(), + TaskListExtension.create() + )); + options.set(HtmlRenderer.NO_P_TAGS_USE_BR, true); + + parser = Parser.builder(options).build(); + renderer = HtmlRenderer.builder(options).build(); + } + + @Override + public String format(final String fieldText) { + Objects.requireNonNull(fieldText, "Field Text should not be null, when handed to formatter"); + + Node document = parser.parse(fieldText); + String html = renderer.render(document); + + // workaround HTMLChars transforming "\n" into
by returning a one liner + return html.replaceAll("\\r\\n|\\r|\\n", " ").trim(); + } +} diff --git a/src/main/java/org/jabref/migrations/PreferencesMigrations.java b/src/main/java/org/jabref/migrations/PreferencesMigrations.java index d841a6c0c08..d1fcd36b57a 100644 --- a/src/main/java/org/jabref/migrations/PreferencesMigrations.java +++ b/src/main/java/org/jabref/migrations/PreferencesMigrations.java @@ -49,6 +49,7 @@ public static void runMigrations() { addCrossRefRelatedFieldsForAutoComplete(Globals.prefs); upgradePreviewStyleFromReviewToComment(Globals.prefs); upgradeColumnPreferences(Globals.prefs); + upgradePreviewStyleAllowMarkdown(Globals.prefs); } /** @@ -301,6 +302,12 @@ static void upgradePreviewStyleFromReviewToComment(JabRefPreferences prefs) { prefs.setPreviewStyle(migratedStyle); } + static void upgradePreviewStyleAllowMarkdown(JabRefPreferences prefs) { + String currentPreviewStyle = prefs.getPreviewStyle(); + String migratedStyle = currentPreviewStyle.replace("\\begin{comment}

Comment: \\format[HTMLChars]{\\comment} \\end{comment}", "\\begin{comment}

Comment: \\format[Markdown,HTMLChars]{\\comment} \\end{comment}"); + prefs.setPreviewStyle(migratedStyle); + } + /** * The former preferences default of columns was a simple list of strings ("author;title;year;..."). Since 5.0 * the preferences store the type of the column too, so that the formerly hardwired columns like the graphic groups diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 096396915f3..5dddcd3b233 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -674,7 +674,7 @@ private JabRefPreferences() { + "\\begin{year}\\year\\end{year}\\begin{volume}, \\volume\\end{volume}" + "\\begin{pages}, \\format[FormatPagesForHTML]{\\pages} \\end{pages}__NEWLINE__" + "\\begin{abstract}

Abstract: \\format[HTMLChars]{\\abstract} \\end{abstract}__NEWLINE__" - + "\\begin{comment}

Comment: \\format[HTMLChars]{\\comment} \\end{comment}" + + "\\begin{comment}

Comment: \\format[Markdown,HTMLChars]{\\comment} \\end{comment}" + "__NEWLINE__

"); // set default theme diff --git a/src/test/java/org/jabref/logic/layout/format/MarkdownFormatterTest.java b/src/test/java/org/jabref/logic/layout/format/MarkdownFormatterTest.java new file mode 100644 index 00000000000..a09bfe97c65 --- /dev/null +++ b/src/test/java/org/jabref/logic/layout/format/MarkdownFormatterTest.java @@ -0,0 +1,60 @@ +package org.jabref.logic.layout.format; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class MarkdownFormatterTest { + + private MarkdownFormatter markdownFormatter; + + @BeforeEach + void setUp() { + markdownFormatter = new MarkdownFormatter(); + } + + @Test + void formatWhenFormattingPlainTextThenReturnsTextWrappedInParagraph() { + assertThat(markdownFormatter.format("Hello World")).isEqualTo("

Hello World

"); + } + + @Test + void formatWhenFormattingComplexMarkupThenReturnsOnlyOneLine() { + String source = "Markup\n\n* list item one\n* list item 2\n\n rest"; + assertThat(markdownFormatter.format(source)) + .contains("Markup
") + .contains("
  • list item one
  • ") + .contains("
  • list item 2
  • ") + .contains("> rest") + .doesNotContain("\n"); + } + + @Test + void formatWhenFormattingEmptyStringThenReturnsEmptyString() { + assertThat(markdownFormatter.format("")).isEqualTo(""); + } + + @Test + void formatWhenFormattingNullThenThrowsException() { + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> markdownFormatter.format(null)) + .withMessageContaining("Field Text should not be null, when handed to formatter") + .withNoCause(); + } + + @Test + void formatWhenMarkupContainingStrikethroughThenContainsMatchingDel() { + assertThat(markdownFormatter.format("a ~~b~~ b")).contains("b"); + } + + @Test + void formatWhenMarkupContainingTaskListThenContainsFormattedTaskList() { + assertThat(markdownFormatter.format("Some text\n" + + "* [ ] open task\n" + + "* [x] closed task\n\n" + + "some other text")) + .contains("
  •  open task
  • ") + .contains("
  •  closed task
  • "); + } +} diff --git a/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java b/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java index 950dc132d47..4ec51549da8 100644 --- a/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java +++ b/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java @@ -22,7 +22,7 @@ class PreferencesMigrationsTest { private final String[] oldStylePatterns = new String[]{"\\bibtexkey", "\\bibtexkey\\begin{title} - \\format[RemoveBrackets]{\\title}\\end{title}"}; private final String[] newStylePatterns = new String[]{"[bibtexkey]", - "[bibtexkey] - [title]"}; + "[bibtexkey] - [title]"}; @BeforeEach void setUp() { @@ -118,6 +118,56 @@ void testPreviewStyle() { verify(prefs).setPreviewStyle(newPreviewStyle); } + @Test + void upgradePreviewStyleAllowMarkup() { + String oldPreviewStyle = "" + + "\\bibtextype\\begin{bibtexkey} (\\bibtexkey)" + + "\\end{bibtexkey}
    __NEWLINE__" + + "\\begin{author} \\format[Authors(LastFirst,Initials,Semicolon,Amp),HTMLChars]{\\author}
    \\end{author}__NEWLINE__" + + "\\begin{editor} \\format[Authors(LastFirst,Initials,Semicolon,Amp),HTMLChars]{\\editor} " + + "(\\format[IfPlural(Eds.,Ed.)]{\\editor})
    \\end{editor}__NEWLINE__" + + "\\begin{title} \\format[HTMLChars]{\\title} \\end{title}
    __NEWLINE__" + + "\\begin{chapter} \\format[HTMLChars]{\\chapter}
    \\end{chapter}__NEWLINE__" + + "\\begin{journal} \\format[HTMLChars]{\\journal}, \\end{journal}__NEWLINE__" + // Include the booktitle field for @inproceedings, @proceedings, etc. + + "\\begin{booktitle} \\format[HTMLChars]{\\booktitle}, \\end{booktitle}__NEWLINE__" + + "\\begin{school} \\format[HTMLChars]{\\school}, \\end{school}__NEWLINE__" + + "\\begin{institution} \\format[HTMLChars]{\\institution}, \\end{institution}__NEWLINE__" + + "\\begin{publisher} \\format[HTMLChars]{\\publisher}, \\end{publisher}__NEWLINE__" + + "\\begin{year}\\year\\end{year}\\begin{volume}, \\volume\\end{volume}" + + "\\begin{pages}, \\format[FormatPagesForHTML]{\\pages} \\end{pages}__NEWLINE__" + + "\\begin{abstract}

    Abstract: \\format[HTMLChars]{\\abstract} \\end{abstract}__NEWLINE__" + + "\\begin{comment}

    Comment: \\format[HTMLChars]{\\comment} \\end{comment}" + + "__NEWLINE__

    "; + + String newPreviewStyle = "" + + "\\bibtextype\\begin{bibtexkey} (\\bibtexkey)" + + "\\end{bibtexkey}
    __NEWLINE__" + + "\\begin{author} \\format[Authors(LastFirst,Initials,Semicolon,Amp),HTMLChars]{\\author}
    \\end{author}__NEWLINE__" + + "\\begin{editor} \\format[Authors(LastFirst,Initials,Semicolon,Amp),HTMLChars]{\\editor} " + + "(\\format[IfPlural(Eds.,Ed.)]{\\editor})
    \\end{editor}__NEWLINE__" + + "\\begin{title} \\format[HTMLChars]{\\title} \\end{title}
    __NEWLINE__" + + "\\begin{chapter} \\format[HTMLChars]{\\chapter}
    \\end{chapter}__NEWLINE__" + + "\\begin{journal} \\format[HTMLChars]{\\journal}, \\end{journal}__NEWLINE__" + // Include the booktitle field for @inproceedings, @proceedings, etc. + + "\\begin{booktitle} \\format[HTMLChars]{\\booktitle}, \\end{booktitle}__NEWLINE__" + + "\\begin{school} \\format[HTMLChars]{\\school}, \\end{school}__NEWLINE__" + + "\\begin{institution} \\format[HTMLChars]{\\institution}, \\end{institution}__NEWLINE__" + + "\\begin{publisher} \\format[HTMLChars]{\\publisher}, \\end{publisher}__NEWLINE__" + + "\\begin{year}\\year\\end{year}\\begin{volume}, \\volume\\end{volume}" + + "\\begin{pages}, \\format[FormatPagesForHTML]{\\pages} \\end{pages}__NEWLINE__" + + "\\begin{abstract}

    Abstract: \\format[HTMLChars]{\\abstract} \\end{abstract}__NEWLINE__" + + "\\begin{comment}

    Comment: \\format[Markdown,HTMLChars]{\\comment} \\end{comment}" + + "__NEWLINE__

    "; + + prefs.setPreviewStyle(oldPreviewStyle); + when(prefs.getPreviewStyle()).thenReturn(oldPreviewStyle); + + PreferencesMigrations.upgradePreviewStyleAllowMarkdown(prefs); + + verify(prefs).setPreviewStyle(newPreviewStyle); + } + @Test void testUpgradeColumnPreferencesAlreadyMigrated() { List columnNames = Arrays.asList("entrytype", "author/editor", "title", "year", "journal/booktitle", "bibtexkey", "printed"); @@ -138,7 +188,7 @@ void testUpgradeColumnPreferencesFromWithoutTypes() { List columnWidths = Arrays.asList("75", "300", "470", "60", "130", "100", "30"); List updatedNames = Arrays.asList("groups", "files", "linked_id", "field:entrytype", "field:author/editor", "field:title", "field:year", "field:journal/booktitle", "field:bibtexkey", "special:printed"); List updatedWidths = Arrays.asList("28", "28", "28", "75", "300", "470", "60", "130", "100", "30"); - List newSortTypes = Arrays.asList("ASCENDING","ASCENDING","ASCENDING","ASCENDING","ASCENDING","ASCENDING","ASCENDING","ASCENDING","ASCENDING","ASCENDING"); + List newSortTypes = Arrays.asList("ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING", "ASCENDING"); when(prefs.getStringList(JabRefPreferences.COLUMN_NAMES)).thenReturn(columnNames); when(prefs.getStringList(JabRefPreferences.COLUMN_WIDTHS)).thenReturn(columnWidths);