From 77677768350da954391ddf3860d42d3a29223416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Sat, 3 Oct 2020 13:04:30 +0200 Subject: [PATCH] support www links without protocol in preview mode (resolve #949) --- .../notes/edit/NotePreviewFragment.java | 21 +++++-- .../notes/shared/util/NoteLinksUtils.java | 52 ++--------------- .../shared/util/text/NoteLinksProcessor.java | 57 +++++++++++++++++++ .../notes/shared/util/text/TextProcessor.java | 10 ++++ .../shared/util/text/TextProcessorChain.java | 12 ++++ .../shared/util/text/WwwLinksProcessor.java | 28 +++++++++ .../NoteLinksProcessorTest.java} | 24 +++++--- .../util/text/TextProcessorChainTest.java | 35 ++++++++++++ .../util/text/WwwLinksProcessorTest.java | 49 ++++++++++++++++ 9 files changed, 230 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessor.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessor.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChain.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessor.java rename app/src/test/java/it/niedermann/owncloud/notes/shared/util/{NoteLinksUtilsTest.java => text/NoteLinksProcessorTest.java} (71%) create mode 100644 app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChainTest.java create mode 100644 app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessorTest.java diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java index 1eb90c43..9013438d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java @@ -35,9 +35,13 @@ import com.yydcdut.markdown.syntax.text.TextFactory; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding; import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.model.DBNote; import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; import it.niedermann.owncloud.notes.shared.util.NoteLinksUtils; import it.niedermann.owncloud.notes.shared.util.SSOUtil; +import it.niedermann.owncloud.notes.shared.util.text.NoteLinksProcessor; +import it.niedermann.owncloud.notes.shared.util.text.TextProcessorChain; +import it.niedermann.owncloud.notes.shared.util.text.WwwLinksProcessor; import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.searchAndColor; import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_CHECKED_MINUS; @@ -46,7 +50,6 @@ import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNC import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR; import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.parseCompat; import static it.niedermann.owncloud.notes.shared.util.NoteLinksUtils.extractNoteRemoteId; -import static it.niedermann.owncloud.notes.shared.util.NoteLinksUtils.replaceNoteLinksWithDummyUrls; import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; public class NotePreviewFragment extends SearchableBaseNoteFragment implements OnRefreshListener { @@ -165,11 +168,13 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O } }) .build()); + + TextProcessorChain chain = defaultTextProcessorChain(note); try { - binding.singleNoteContent.setText(parseCompat(markdownProcessor, replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId())))); + binding.singleNoteContent.setText(parseCompat(markdownProcessor, chain.apply(note.getContent()))); } catch (StringIndexOutOfBoundsException e) { // Workaround for RxMarkdown: https://github.com/stefan-niedermann/nextcloud-notes/issues/668 - binding.singleNoteContent.setText(replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId()))); + binding.singleNoteContent.setText(chain.apply(note.getContent())); Toast.makeText(binding.singleNoteContent.getContext(), R.string.could_not_load_preview_two_digit_numbered_list, Toast.LENGTH_LONG).show(); e.printStackTrace(); } @@ -205,11 +210,12 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O if (db.getNoteServerSyncHelper().isSyncPossible() && SSOUtil.isConfigured(getContext())) { binding.swiperefreshlayout.setRefreshing(true); try { + TextProcessorChain chain = defaultTextProcessorChain(note); SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()); db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, () -> { note = db.getNote(note.getAccountId(), note.getId()); changedText = note.getContent(); - binding.singleNoteContent.setText(parseCompat(markdownProcessor, replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId())))); + binding.singleNoteContent.setText(parseCompat(markdownProcessor, chain.apply(note.getContent()))); binding.swiperefreshlayout.setRefreshing(false); }); db.getNoteServerSyncHelper().scheduleSync(ssoAccount, false); @@ -227,4 +233,11 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O super.applyBrand(mainColor, textColor); binding.singleNoteContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); } + + private TextProcessorChain defaultTextProcessorChain(DBNote note) { + TextProcessorChain chain = new TextProcessorChain(); + chain.add(new NoteLinksProcessor(db.getRemoteIds(note.getAccountId()))); + chain.add(new WwwLinksProcessor()); + return chain; + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java index 389e2bef..668d2746 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java @@ -1,66 +1,26 @@ package it.niedermann.owncloud.notes.shared.util; -import android.text.TextUtils; - -import androidx.annotation.VisibleForTesting; - -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import it.niedermann.owncloud.notes.shared.util.text.NoteLinksProcessor; public class NoteLinksUtils { - @VisibleForTesting - static final String RELATIVE_LINK_WORKAROUND_PREFIX = "https://nextcloudnotes/notes/"; - - private static final String linksThatLookLikeNoteLinksRegEx = "\\[[^]]*]\\((\\d+)\\)"; - private static final String replaceNoteRemoteIdsRegEx = "\\[([^\\]]*)\\]\\((%s)\\)"; - /** - * Replaces all links to other notes of the form `[]()` - * in the markdown string with links to a dummy url. - * - * Why is this needed? - * See discussion in issue #623 - * - * @return Markdown with all note-links replaced with dummy-url-links - */ - public static String replaceNoteLinksWithDummyUrls(String markdown, Set existingNoteRemoteIds) { - Pattern noteLinkCandidates = Pattern.compile(linksThatLookLikeNoteLinksRegEx); - Matcher matcher = noteLinkCandidates.matcher(markdown); - - Set noteRemoteIdsToReplace = new HashSet<>(); - while (matcher.find()) { - String presumedNoteId = matcher.group(1); - if (existingNoteRemoteIds.contains(presumedNoteId)) { - noteRemoteIdsToReplace.add(presumedNoteId); - } - } - - String noteRemoteIdsCondition = TextUtils.join("|", noteRemoteIdsToReplace); - Pattern replacePattern = Pattern.compile(String.format(replaceNoteRemoteIdsRegEx, noteRemoteIdsCondition)); - Matcher replaceMatcher = replacePattern.matcher(markdown); - return replaceMatcher.replaceAll(String.format("[$1](%s$2)", RELATIVE_LINK_WORKAROUND_PREFIX)); - } - - /** - * Tests if the given link is a note-link (which was transformed in {@link #replaceNoteLinksWithDummyUrls}) or not + * Tests if the given link is a note-link (which was transformed in {@link it.niedermann.owncloud.notes.shared.util.text.NoteLinksProcessor}) or not * * @param link Link under test * @return true if the link is a note-link */ public static boolean isNoteLink(String link) { - return link.startsWith(RELATIVE_LINK_WORKAROUND_PREFIX); + return link.startsWith(NoteLinksProcessor.RELATIVE_LINK_WORKAROUND_PREFIX); } /** - * Extracts the remoteId back from links that were transformed in {@link #replaceNoteLinksWithDummyUrls}. + * Extracts the remoteId back from links that were transformed in {@link it.niedermann.owncloud.notes.shared.util.text.NoteLinksProcessor}. * - * @param link Link that was transformed in {@link #replaceNoteLinksWithDummyUrls} + * @param link Link that was transformed in {@link it.niedermann.owncloud.notes.shared.util.text.NoteLinksProcessor} * @return the remoteId of the linked note */ public static long extractNoteRemoteId(String link) { - return Long.parseLong(link.replace(RELATIVE_LINK_WORKAROUND_PREFIX, "")); + return Long.parseLong(link.replace(NoteLinksProcessor.RELATIVE_LINK_WORKAROUND_PREFIX, "")); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessor.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessor.java new file mode 100644 index 00000000..81f138a1 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessor.java @@ -0,0 +1,57 @@ +package it.niedermann.owncloud.notes.shared.util.text; + +import android.text.TextUtils; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import androidx.annotation.VisibleForTesting; + +public class NoteLinksProcessor extends TextProcessor { + + public static final String RELATIVE_LINK_WORKAROUND_PREFIX = "https://nextcloudnotes/notes/"; + + @VisibleForTesting + private static final String linksThatLookLikeNoteLinksRegEx = "\\[[^]]*]\\((\\d+)\\)"; + private static final String replaceNoteRemoteIdsRegEx = "\\[([^\\]]*)\\]\\((%s)\\)"; + + private Set existingNoteRemoteIds; + + public NoteLinksProcessor(Set existingNoteRemoteIds) { + this.existingNoteRemoteIds = existingNoteRemoteIds; + } + + /** + * Replaces all links to other notes of the form `[]()` + * in the markdown string with links to a dummy url. + * + * Why is this needed? + * See discussion in issue #623 + * + * @return Markdown with all note-links replaced with dummy-url-links + */ + @Override + public String process(String s) { + return replaceNoteLinksWithDummyUrls(s, existingNoteRemoteIds); + } + + private static String replaceNoteLinksWithDummyUrls(String markdown, Set existingNoteRemoteIds) { + Pattern noteLinkCandidates = Pattern.compile(linksThatLookLikeNoteLinksRegEx); + Matcher matcher = noteLinkCandidates.matcher(markdown); + + Set noteRemoteIdsToReplace = new HashSet<>(); + while (matcher.find()) { + String presumedNoteId = matcher.group(1); + if (existingNoteRemoteIds.contains(presumedNoteId)) { + noteRemoteIdsToReplace.add(presumedNoteId); + } + } + + String noteRemoteIdsCondition = TextUtils.join("|", noteRemoteIdsToReplace); + Pattern replacePattern = Pattern.compile(String.format(replaceNoteRemoteIdsRegEx, noteRemoteIdsCondition)); + Matcher replaceMatcher = replacePattern.matcher(markdown); + return replaceMatcher.replaceAll(String.format("[$1](%s$2)", RELATIVE_LINK_WORKAROUND_PREFIX)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessor.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessor.java new file mode 100644 index 00000000..4eb4e4f7 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessor.java @@ -0,0 +1,10 @@ +package it.niedermann.owncloud.notes.shared.util.text; + +abstract public class TextProcessor { + /** + * Applies a specified transformation on a text string and returns the updated string. + * @param s Text to transform + * @return Transformed text + */ + abstract public String process(String s); +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChain.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChain.java new file mode 100644 index 00000000..70af737a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChain.java @@ -0,0 +1,12 @@ +package it.niedermann.owncloud.notes.shared.util.text; + +import java.util.LinkedList; + +public class TextProcessorChain extends LinkedList { + public String apply(String s) { + for (TextProcessor textProcessor : this) { + s = textProcessor.process(s); + } + return s; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessor.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessor.java new file mode 100644 index 00000000..24baa016 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessor.java @@ -0,0 +1,28 @@ +package it.niedermann.owncloud.notes.shared.util.text; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WwwLinksProcessor extends TextProcessor { + + public static final String WWW_URLS_PROTOCOL_PREFIX = "http://"; + private static final String replaceWwwUrlsRegEx = "\\[([^]]*)]\\((www\\..+)\\)"; + + /** + * Prefixes all links, that not not start with a protocol identifier, but with "www." with http:// + * + * See https://github.com/stefan-niedermann/nextcloud-notes/issues/949 + * + * @return Markdown with all pseudo-links replaced through actual HTTP-links + */ + @Override + public String process(String s) { + return replaceWwwUrls(s); + } + + private static String replaceWwwUrls(String markdown) { + Pattern replacePattern = Pattern.compile(replaceWwwUrlsRegEx); + Matcher replaceMatcher = replacePattern.matcher(markdown); + return replaceMatcher.replaceAll(String.format("[$1](%s$2)", WWW_URLS_PROTOCOL_PREFIX)); + } +} diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtilsTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessorTest.java similarity index 71% rename from app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtilsTest.java rename to app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessorTest.java index b4d5bb98..8b96a207 100644 --- a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtilsTest.java +++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/NoteLinksProcessorTest.java @@ -1,4 +1,4 @@ -package it.niedermann.owncloud.notes.shared.util; +package it.niedermann.owncloud.notes.shared.util.text; import junit.framework.TestCase; @@ -8,18 +8,21 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import static it.niedermann.owncloud.notes.shared.util.NoteLinksUtils.RELATIVE_LINK_WORKAROUND_PREFIX; +import static it.niedermann.owncloud.notes.shared.util.text.NoteLinksProcessor.RELATIVE_LINK_WORKAROUND_PREFIX; - -public class NoteLinksUtilsTest extends TestCase { +public class NoteLinksProcessorTest extends TestCase { public void testEmptyString() { + TextProcessor sut = new NoteLinksProcessor(Collections.emptySet()); + String markdown = ""; - String result = NoteLinksUtils.replaceNoteLinksWithDummyUrls(markdown, Collections.emptySet()); + String result = sut.process(markdown); Assert.assertEquals("", result); } public void testDoNotChangeOtherMarkdownElements() { + TextProcessor sut = new NoteLinksProcessor(Collections.emptySet()); + //language=md String markdown = "\n" + "# heading \n" + @@ -37,14 +40,16 @@ public class NoteLinksUtilsTest extends TestCase { "**Everything** else could be in here.\n" + "\n"; - Assert.assertEquals(markdown, NoteLinksUtils.replaceNoteLinksWithDummyUrls(markdown, Collections.emptySet())); + Assert.assertEquals(markdown, sut.process(markdown)); } @SuppressWarnings("MarkdownUnresolvedFileReference") public void testDoNotReplaceNormalLinks() { + TextProcessor sut = new NoteLinksProcessor(Collections.singleton("123456")); + //language=md String markdown = "[normal link](https://example.com) and another [note link](123456)"; - String result = NoteLinksUtils.replaceNoteLinksWithDummyUrls(markdown, Collections.singleton("123456")); + String result = sut.process(markdown); Assert.assertEquals(String.format("[normal link](https://example.com) and another [note link](%s123456)", RELATIVE_LINK_WORKAROUND_PREFIX), result); } @@ -52,9 +57,12 @@ public class NoteLinksUtilsTest extends TestCase { Set remoteIdsOfExistingNotes = new HashSet<>(); remoteIdsOfExistingNotes.add("123456"); remoteIdsOfExistingNotes.add("321456"); + + TextProcessor sut = new NoteLinksProcessor(remoteIdsOfExistingNotes); + String markdown = "[link to real note](123456) and another [link to non-existing note](654321) and one more [another link to real note](321456)"; - String result = NoteLinksUtils.replaceNoteLinksWithDummyUrls(markdown, remoteIdsOfExistingNotes); + String result = sut.process(markdown); String expected = String.format("[link to real note](%s123456) and another [link to non-existing note](654321) and one more [another link to real note](%s321456)", RELATIVE_LINK_WORKAROUND_PREFIX, RELATIVE_LINK_WORKAROUND_PREFIX); Assert.assertEquals( diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChainTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChainTest.java new file mode 100644 index 00000000..6565223e --- /dev/null +++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/TextProcessorChainTest.java @@ -0,0 +1,35 @@ +package it.niedermann.owncloud.notes.shared.util.text; + +import junit.framework.TestCase; + +import org.junit.Assert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class TextProcessorChainTest extends TestCase { + + public void testApplyAllInOrder() { + TextProcessorChain chain = new TextProcessorChain(); + chain.add(new SelfIdentifyingProcessor(1)); + chain.add(new SelfIdentifyingProcessor(2)); + + Assert.assertEquals("SelfIdentifyingProcessor 1\nSelfIdentifyingProcessor 2", chain.apply("")); + } + + class SelfIdentifyingProcessor extends TextProcessor { + private int id; + + public SelfIdentifyingProcessor(int id) { + this.id = id; + } + + @Override + public String process(String s) { + List parts = new ArrayList<>(Arrays.asList(s.split("\n"))); + parts.add(String.format("%s %d", getClass().getSimpleName(), id)); + return String.join("\n", parts.toArray(new String[]{})).trim(); + } + } +} \ No newline at end of file diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessorTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessorTest.java new file mode 100644 index 00000000..1c31025c --- /dev/null +++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/text/WwwLinksProcessorTest.java @@ -0,0 +1,49 @@ +package it.niedermann.owncloud.notes.shared.util.text; + +import junit.framework.TestCase; + +import org.junit.Assert; + +public class WwwLinksProcessorTest extends TestCase { + + public void testEmptyString() { + TextProcessor sut = new WwwLinksProcessor(); + + String markdown = ""; + String result = sut.process(markdown); + Assert.assertEquals("", result); + } + + public void testDoNotChangeOtherMarkdownElements() { + TextProcessor sut = new WwwLinksProcessor(); + + //language=md + String markdown = "\n" + + "# heading \n" + + " \n" + + "This is a _markdown_ document. \n" + + " \n" + + "But\n" + + " - there \n" + + " - are \n" + + " - no \n" + + "\n" + + "link elements.\n" + + "\n" + + "----\n" + + "**Everything** else could be in here.\n" + + "\n"; + + Assert.assertEquals(markdown, sut.process(markdown)); + } + + @SuppressWarnings("MarkdownUnresolvedFileReference") + public void testDoNotReplaceNormalLinks() { + TextProcessor sut = new WwwLinksProcessor(); + + //language=md + String markdown = "[normal link](https://example.com) and another [www link](www.example.com) and one more [normal link](https://www.example.com)"; + String result = sut.process(markdown); + Assert.assertEquals("[normal link](https://example.com) and another [www link](http://www.example.com) and one more [normal link](https://www.example.com)", result); + } +} \ No newline at end of file