support www links without protocol in preview mode (resolve #949)

This commit is contained in:
Ferdinand Mütsch 2020-10-03 13:04:30 +02:00 committed by Niedermann IT-Dienstleistungen
parent 2d1e4e5b60
commit 7767776835
9 changed files with 230 additions and 58 deletions

View file

@ -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;
}
}

View file

@ -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 `[<link-text>](<note-file-id>)`
* 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<String> existingNoteRemoteIds) {
Pattern noteLinkCandidates = Pattern.compile(linksThatLookLikeNoteLinksRegEx);
Matcher matcher = noteLinkCandidates.matcher(markdown);
Set<String> 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, ""));
}
}

View file

@ -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<String> existingNoteRemoteIds;
public NoteLinksProcessor(Set<String> existingNoteRemoteIds) {
this.existingNoteRemoteIds = existingNoteRemoteIds;
}
/**
* Replaces all links to other notes of the form `[<link-text>](<note-file-id>)`
* 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<String> existingNoteRemoteIds) {
Pattern noteLinkCandidates = Pattern.compile(linksThatLookLikeNoteLinksRegEx);
Matcher matcher = noteLinkCandidates.matcher(markdown);
Set<String> 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));
}
}

View file

@ -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);
}

View file

@ -0,0 +1,12 @@
package it.niedermann.owncloud.notes.shared.util.text;
import java.util.LinkedList;
public class TextProcessorChain extends LinkedList<TextProcessor> {
public String apply(String s) {
for (TextProcessor textProcessor : this) {
s = textProcessor.process(s);
}
return s;
}
}

View file

@ -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));
}
}

View file

@ -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<String> 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(

View file

@ -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<String> 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();
}
}
}

View file

@ -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);
}
}