From d4e46b099faaab5787c6df89898014a1ec34a61c Mon Sep 17 00:00:00 2001 From: AlexanderGirgis <11259344+AlexanderGirgis@users.noreply.github.com> Date: Sat, 4 Apr 2020 00:17:27 +0200 Subject: [PATCH] Allow basic markup syntax custom previews (#6232) Add MarkdownFormatter using https://github.com/vsch/flexmark-java/ to format markdown. To configure Markdown in custom previews add the "Markdown" formatter. Markdown is enabled by default for the comment field as requested in https://github.com/JabRef/jabref/issues/6194 --- CHANGELOG.md | 1 + build.gradle | 1 + external-libraries.txt | 5 ++ src/main/java/module-info.java | 6 ++ .../org/jabref/logic/layout/LayoutEntry.java | 3 + .../layout/format/MarkdownFormatter.java | 41 ++++++++++ .../migrations/PreferencesMigrations.java | 7 ++ .../jabref/preferences/JabRefPreferences.java | 2 +- .../layout/format/MarkdownFormatterTest.java | 57 ++++++++++++++ .../migrations/PreferencesMigrationsTest.java | 74 ++++++++++++++++++- 10 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java create mode 100644 src/test/java/org/jabref/logic/layout/format/MarkdownFormatterTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aaaa4312fc..d713bed9d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,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 updated EditionChecker to permit edition to start with a number. [#6144](https://github.com/JabRef/jabref/issues/6144) - 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 0e0031b434e..ebb001d5086 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.66' 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..6bbf87db090 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -82,4 +82,10 @@ 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; } 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..e8eadf2bded --- /dev/null +++ b/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java @@ -0,0 +1,41 @@ +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() + )); + + 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..27f6c67fed3 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("\\format[HTMLChars]{\\comment}", "\\format[Markdown,HTMLChars]{\\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 c3cc1adef51..45326331cba 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -672,7 +672,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..595bc6fcc1d --- /dev/null +++ b/src/test/java/org/jabref/logic/layout/format/MarkdownFormatterTest.java @@ -0,0 +1,57 @@ +package org.jabref.logic.layout.format; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MarkdownFormatterTest { + + private MarkdownFormatter markdownFormatter; + + @BeforeEach + void setUp() { + markdownFormatter = new MarkdownFormatter(); + } + + @Test + void formatWhenFormattingPlainTextThenReturnsTextWrappedInParagraph() { + assertEquals("

Hello World

", markdownFormatter.format("Hello World")); + } + + @Test + void formatWhenFormattingComplexMarkupThenReturnsOnlyOneLine() { + assertFalse(markdownFormatter.format("Markup\n\n* list item one\n* list item 2\n\n rest").contains("\n")); + } + + @Test + void formatWhenFormattingEmptyStringThenReturnsEmptyString() { + assertEquals("", markdownFormatter.format("")); + } + + @Test + void formatWhenFormattingNullThenThrowsException() { + Exception exception = assertThrows(NullPointerException.class, () -> markdownFormatter.format(null)); + assertEquals("Field Text should not be null, when handed to formatter", exception.getMessage()); + } + + @Test + void formatWhenMarkupContainingStrikethroughThenContainsMatchingDel() { + // Only test strikethrough extension + assertTrue(markdownFormatter.format("a ~~b~~ b").contains("b")); + } + + @Test + void formatWhenMarkupContainingTaskListThenContainsFormattedTaskList() { + String actual = markdownFormatter.format("Some text\n" + + "* [ ] open task\n" + + "* [x] closed task\n\n" + + "some other text"); + // Only test list items + assertTrue(actual.contains("
  •  open task
  • ")); + assertTrue(actual.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..140aa4d3cae 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,76 @@ void testPreviewStyle() { verify(prefs).setPreviewStyle(newPreviewStyle); } + @Test + void upgradePreviewStyleAllowMarkupDefault() { + 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 upgradePreviewStyleAllowMarkupCustomized() { + String oldPreviewStyle = "" + + "My highly customized format only using comments:
    " + + "\\begin{comment} Something: \\format[HTMLChars]{\\comment} special \\end{comment}" + + "__NEWLINE__

    "; + + String newPreviewStyle = "" + + "My highly customized format only using comments:
    " + + "\\begin{comment} Something: \\format[Markdown,HTMLChars]{\\comment} special \\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 +208,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);