From 250dd7677dff39436131acd71a70b5d87c52a199 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 22 May 2017 12:53:21 +0300 Subject: [PATCH] Working with realtive urls --- README.md | 2 + .../java/ru/noties/markwon/AppModule.java | 4 +- .../ru/noties/markwon/CollectionUtils.java | 12 ++++++ .../java/ru/noties/markwon/MainActivity.java | 18 ++++++-- .../ru/noties/markwon/MarkdownLoader.java | 7 +-- .../ru/noties/markwon/MarkdownRenderer.java | 35 ++++++++++++++- .../java/ru/noties/markwon/UriProcessor.java | 9 ++++ ...roviderImpl.java => UriProcessorImpl.java} | 24 ++++++++--- .../java/ru/noties/markwon/UrlProvider.java | 8 ---- .../markwon/SpannableConfiguration.java | 17 +++++++- .../java/ru/noties/markwon/UrlProcessor.java | 8 ++++ .../ru/noties/markwon/UrlProcessorNoOp.java | 11 +++++ .../UrlProcessorRelativeToAbsolute.java | 43 +++++++++++++++++++ .../renderer/SpannableMarkdownVisitor.java | 13 +++--- .../renderer/html/HtmlImageGetter.java | 14 +++++- .../renderer/html/SpannableHtmlParser.java | 32 +++++++++++--- 16 files changed, 217 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/ru/noties/markwon/CollectionUtils.java create mode 100644 app/src/main/java/ru/noties/markwon/UriProcessor.java rename app/src/main/java/ru/noties/markwon/{UrlProviderImpl.java => UriProcessorImpl.java} (72%) delete mode 100644 app/src/main/java/ru/noties/markwon/UrlProvider.java create mode 100644 library-renderer/src/main/java/ru/noties/markwon/UrlProcessor.java create mode 100644 library-renderer/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java create mode 100644 library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java diff --git a/README.md b/README.md index 4fd73bc0..382ec771 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ Lorem ipsum `dolor` sit amet Lorem ipsum dolor `sit` amet Lorem ipsum dolor sit `amet` +`Lorem ipsum dolor sit amet` + ### Code block // todo syntax higlight ``` diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java index d9868304..9a8f5a86 100644 --- a/app/src/main/java/ru/noties/markwon/AppModule.java +++ b/app/src/main/java/ru/noties/markwon/AppModule.java @@ -55,8 +55,8 @@ class AppModule { @Singleton @Provides - UrlProvider urlProvider() { - return new UrlProviderImpl(); + UriProcessor uriProcessor() { + return new UriProcessorImpl(); } @Provides diff --git a/app/src/main/java/ru/noties/markwon/CollectionUtils.java b/app/src/main/java/ru/noties/markwon/CollectionUtils.java new file mode 100644 index 00000000..7e3cad92 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/CollectionUtils.java @@ -0,0 +1,12 @@ +package ru.noties.markwon; + +import java.util.Collection; + +public abstract class CollectionUtils { + + public static boolean isEmpty(Collection collection) { + return collection == null || collection.size() == 0; + } + + private CollectionUtils() {} +} diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index bda7c876..30f1995d 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -27,6 +27,9 @@ public class MainActivity extends Activity { @Inject Themes themes; + @Inject + UriProcessor uriProcessor; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -38,9 +41,13 @@ public class MainActivity extends Activity { themes.apply(this); // how can we obtain SpannableConfiguration after theme was applied? + // as we inject `themes` we won't be able to inject configuration, as it requires theme set setContentView(R.layout.activity_main); + // we process additionally github urls, as if url has in path `blob`, we won't receive + // desired file, but instead rendered html + checkUri(); final AppBarItem.Renderer appBarRenderer = new AppBarItem.Renderer(findViewById(R.id.app_bar), new View.OnClickListener() { @@ -59,7 +66,7 @@ public class MainActivity extends Activity { markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(String text) { - markdownRenderer.render(MainActivity.this, text, new MarkdownRenderer.MarkdownReadyListener() { + markdownRenderer.render(MainActivity.this, uri(), text, new MarkdownRenderer.MarkdownReadyListener() { @Override public void onMarkdownReady(CharSequence markdown) { Markwon.setText(textView, markdown); @@ -80,8 +87,6 @@ public class MainActivity extends Activity { final Uri uri = uri(); - Debug.i(uri); - if (uri != null) { title = uri.getLastPathSegment(); subtitle = uri.toString(); @@ -93,6 +98,13 @@ public class MainActivity extends Activity { return new AppBarItem.State(title, subtitle); } + private void checkUri() { + final Uri uri = uri(); + if (uri != null) { + getIntent().setData(uriProcessor.process(uri)); + } + } + private Uri uri() { final Intent intent = getIntent(); return intent != null diff --git a/app/src/main/java/ru/noties/markwon/MarkdownLoader.java b/app/src/main/java/ru/noties/markwon/MarkdownLoader.java index 0c9f10db..0e186765 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownLoader.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownLoader.java @@ -45,9 +45,6 @@ public class MarkdownLoader { @Inject OkHttpClient client; - @Inject - UrlProvider urlProvider; - private Future task; @Inject @@ -130,10 +127,8 @@ public class MarkdownLoader { private String loadExternalUrl(@NonNull Uri uri) { - final String url = urlProvider.provide(uri); - final Request request = new Request.Builder() - .url(url) + .url(uri.toString()) .build(); Response response = null; diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index b6f51e67..002719e1 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -1,14 +1,23 @@ package ru.noties.markwon; import android.content.Context; +import android.net.Uri; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; + +import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import javax.inject.Inject; +import ru.noties.markwon.renderer.SpannableRenderer; + @ActivityScope public class MarkdownRenderer { @@ -31,15 +40,37 @@ public class MarkdownRenderer { MarkdownRenderer() { } - public void render(@NonNull final Context context, @NonNull final String markdown, @NonNull final MarkdownReadyListener listener) { + public void render( + @NonNull final Context context, + @Nullable final Uri uri, + @NonNull final String markdown, + @NonNull final MarkdownReadyListener listener) { cancel(); task = service.submit(new Runnable() { @Override public void run() { + + final UrlProcessor urlProcessor; + if (uri == null) { + urlProcessor = null; + } else { + urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString()); + } + final SpannableConfiguration configuration = SpannableConfiguration.builder(context) .asyncDrawableLoader(loader) + .urlProcessor(urlProcessor) .build(); - final CharSequence text = Markwon.markdown(configuration, markdown); + + final Parser parser = Parser.builder() + .extensions(Collections.singleton(StrikethroughExtension.create())) + .build(); + + final Node node = parser.parse(markdown); + final SpannableRenderer renderer = new SpannableRenderer(); + final CharSequence text = renderer.render(configuration, node); + +// final CharSequence text = Markwon.markdown(configuration, markdown); handler.post(new Runnable() { @Override public void run() { diff --git a/app/src/main/java/ru/noties/markwon/UriProcessor.java b/app/src/main/java/ru/noties/markwon/UriProcessor.java new file mode 100644 index 00000000..a4fb5b85 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/UriProcessor.java @@ -0,0 +1,9 @@ +package ru.noties.markwon; + +import android.net.Uri; +import android.support.annotation.NonNull; + +@SuppressWarnings("WeakerAccess") +public interface UriProcessor { + Uri process(@NonNull Uri uri); +} diff --git a/app/src/main/java/ru/noties/markwon/UrlProviderImpl.java b/app/src/main/java/ru/noties/markwon/UriProcessorImpl.java similarity index 72% rename from app/src/main/java/ru/noties/markwon/UrlProviderImpl.java rename to app/src/main/java/ru/noties/markwon/UriProcessorImpl.java index d935454e..03d29353 100644 --- a/app/src/main/java/ru/noties/markwon/UrlProviderImpl.java +++ b/app/src/main/java/ru/noties/markwon/UriProcessorImpl.java @@ -5,25 +5,33 @@ import android.support.annotation.NonNull; import java.util.List; -class UrlProviderImpl implements UrlProvider { +class UriProcessorImpl implements UriProcessor { private static final String GITHUB = "github.com"; @Override - public String provide(@NonNull Uri uri) { + public Uri process(@NonNull final Uri uri) { // hm... github, even having a README.md in path will return rendered HTML + final Uri out; + if (GITHUB.equals(uri.getAuthority())) { + final List segments = uri.getPathSegments(); - if (segments != null - && segments.contains("blob")) { + final int size = segments != null + ? segments.size() + : 0; + + if (size > 0) { + // we need to modify the final uri final Uri.Builder builder = new Uri.Builder() .scheme(uri.getScheme()) .authority(uri.getAuthority()) .fragment(uri.getFragment()) .query(uri.getQuery()); + for (String segment: segments) { final String part; if ("blob".equals(segment)) { @@ -33,10 +41,14 @@ class UrlProviderImpl implements UrlProvider { } builder.appendPath(part); } - uri = builder.build(); + out = builder.build(); + } else { + out = uri; } + } else { + out = uri; } - return uri.toString(); + return out; } } diff --git a/app/src/main/java/ru/noties/markwon/UrlProvider.java b/app/src/main/java/ru/noties/markwon/UrlProvider.java deleted file mode 100644 index 82dc87c2..00000000 --- a/app/src/main/java/ru/noties/markwon/UrlProvider.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.noties.markwon; - -import android.net.Uri; -import android.support.annotation.NonNull; - -public interface UrlProvider { - String provide(@NonNull Uri uri); -} diff --git a/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java index 835cc948..024c8fa0 100644 --- a/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ b/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java @@ -24,6 +24,7 @@ public class SpannableConfiguration { private final AsyncDrawable.Loader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkSpan.Resolver linkResolver; + private final UrlProcessor urlProcessor; private final SpannableHtmlParser htmlParser; private SpannableConfiguration(Builder builder) { @@ -31,6 +32,7 @@ public class SpannableConfiguration { this.asyncDrawableLoader = builder.asyncDrawableLoader; this.syntaxHighlight = builder.syntaxHighlight; this.linkResolver = builder.linkResolver; + this.urlProcessor = builder.urlProcessor; this.htmlParser = builder.htmlParser; } @@ -50,6 +52,10 @@ public class SpannableConfiguration { return linkResolver; } + public UrlProcessor urlProcessor() { + return urlProcessor; + } + public SpannableHtmlParser htmlParser() { return htmlParser; } @@ -61,6 +67,7 @@ public class SpannableConfiguration { private AsyncDrawable.Loader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkSpan.Resolver linkResolver; + private UrlProcessor urlProcessor; private SpannableHtmlParser htmlParser; public Builder(Context context) { @@ -87,6 +94,11 @@ public class SpannableConfiguration { return this; } + public Builder urlProcessor(UrlProcessor urlProcessor) { + this.urlProcessor = urlProcessor; + return this; + } + public Builder htmlParser(SpannableHtmlParser htmlParser) { this.htmlParser = htmlParser; return this; @@ -105,8 +117,11 @@ public class SpannableConfiguration { if (linkResolver == null) { linkResolver = new LinkResolverDef(); } + if (urlProcessor == null) { + urlProcessor = new UrlProcessorNoOp(); + } if (htmlParser == null) { - htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader); + htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor); } return new SpannableConfiguration(this); } diff --git a/library-renderer/src/main/java/ru/noties/markwon/UrlProcessor.java b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessor.java new file mode 100644 index 00000000..b190d110 --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessor.java @@ -0,0 +1,8 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +public interface UrlProcessor { + @NonNull + String process(@NonNull String destination); +} diff --git a/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java new file mode 100644 index 00000000..d23af279 --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java @@ -0,0 +1,11 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +public class UrlProcessorNoOp implements UrlProcessor { + @NonNull + @Override + public String process(@NonNull String destination) { + return destination; + } +} diff --git a/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java new file mode 100644 index 00000000..bae6fcb6 --- /dev/null +++ b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java @@ -0,0 +1,43 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.net.MalformedURLException; +import java.net.URL; + +public class UrlProcessorRelativeToAbsolute implements UrlProcessor { + + private final URL base; + + public UrlProcessorRelativeToAbsolute(@NonNull String base) { + this.base = obtain(base); + } + + @NonNull + @Override + public String process(@NonNull String destination) { + + String out = destination; + + if (base != null) { + try { + final URL u = new URL(base, destination); + out = u.toString(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + return out; + } + + @Nullable + private static URL obtain(String base) { + try { + return new URL(base); + } catch (MalformedURLException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index effe20fd..656f1087 100644 --- a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -51,7 +51,7 @@ import ru.noties.markwon.spans.ThematicBreakSpan; @SuppressWarnings("WeakerAccess") public class SpannableMarkdownVisitor extends AbstractVisitor { - private static final String HTML_CONTENT = "<%1$s>%2$s"; + private static final String HTML_CONTENT = "<%1$s>%2$s"; private final SpannableConfiguration configuration; private final SpannableStringBuilder builder; @@ -253,7 +253,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { @Override public void visit(SoftLineBreak softLineBreak) { - newLine(); + // at first here was a new line, but here should be a space char + builder.append(' '); } @Override @@ -306,13 +307,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final Node parent = image.getParent(); final boolean link = parent != null && parent instanceof Link; + final String destination = configuration.urlProcessor().process(image.getDestination()); setSpan( length, new AsyncDrawableSpan( configuration.theme(), new AsyncDrawable( - image.getDestination(), + destination, configuration.asyncDrawableLoader() ), AsyncDrawableSpan.ALIGN_BOTTOM, @@ -351,7 +353,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { setSpan(item.start, span); } else { final String content = builder.subSequence(start, builder.length()).toString(); - final String html = String.format(HTML_CONTENT, item.tag, content); + final String html = String.format(HTML_CONTENT, item.tag, content, tag.name()); final Object[] spans = htmlParser.htmlSpans(html); final int length = spans != null ? spans.length @@ -382,7 +384,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { public void visit(Link link) { final int length = builder.length(); visitChildren(link); - setSpan(length, new LinkSpan(configuration.theme(), link.getDestination(), configuration.linkResolver())); + final String destination = configuration.urlProcessor().process(link.getDestination()); + setSpan(length, new LinkSpan(configuration.theme(), destination, configuration.linkResolver())); } private void setSpan(int start, @NonNull Object span) { diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java index 1f0a2893..1469a4f1 100644 --- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java @@ -2,20 +2,30 @@ package ru.noties.markwon.renderer.html; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Html; +import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.spans.AsyncDrawable; class HtmlImageGetter implements Html.ImageGetter { private final AsyncDrawable.Loader loader; + private final UrlProcessor urlProcessor; - HtmlImageGetter(@NonNull AsyncDrawable.Loader loader) { + HtmlImageGetter(@NonNull AsyncDrawable.Loader loader, @Nullable UrlProcessor urlProcessor) { this.loader = loader; + this.urlProcessor = urlProcessor; } @Override public Drawable getDrawable(String source) { - return new AsyncDrawable(source, loader); + final String destination; + if (urlProcessor == null) { + destination = source; + } else { + destination = urlProcessor.process(source); + } + return new AsyncDrawable(destination, loader); } } diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java index b919b703..6a6e577a 100644 --- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java +++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java @@ -13,6 +13,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import ru.noties.debug.Debug; +import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.SpannableTheme; @@ -22,8 +24,20 @@ public class SpannableHtmlParser { // we need to handle images independently (in order to parse alt, width, height, etc) // creates default parser - public static SpannableHtmlParser create(@NonNull SpannableTheme theme, @NonNull AsyncDrawable.Loader loader) { - return builderWithDefaults(theme, loader) + public static SpannableHtmlParser create( + @NonNull SpannableTheme theme, + @NonNull AsyncDrawable.Loader loader + ) { + return builderWithDefaults(theme, loader, null) + .build(); + } + + public static SpannableHtmlParser create( + @NonNull SpannableTheme theme, + @NonNull AsyncDrawable.Loader loader, + @NonNull UrlProcessor urlProcessor + ) { + return builderWithDefaults(theme, loader, urlProcessor) .build(); } @@ -33,7 +47,8 @@ public class SpannableHtmlParser { public static Builder builderWithDefaults( @NonNull SpannableTheme theme, - @Nullable AsyncDrawable.Loader asyncDrawableLoader + @Nullable AsyncDrawable.Loader asyncDrawableLoader, + @Nullable UrlProcessor urlProcessor ) { final BoldProvider boldProvider = new BoldProvider(); @@ -42,7 +57,7 @@ public class SpannableHtmlParser { final HtmlParser parser; if (asyncDrawableLoader != null) { - parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader), null); + parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader, urlProcessor), null); } else { parser = DefaultHtmlParser.create(null, null); } @@ -71,9 +86,12 @@ public class SpannableHtmlParser { public interface HtmlParser { Object[] getSpans(@NonNull String html); + Spanned parse(@NonNull String html); } + private static final String LINK_START = " customTags; private final Set voidTags; private final HtmlParser parser; @@ -97,7 +115,7 @@ public class SpannableHtmlParser { if (length < 3) { tag = null; } else { - // okay, we will consider a tag a void one if it's in our void list tag or if it ends with `/>` + // okay, we will consider a tag a void one if it's in our void list tag final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1); final boolean voidTag; if (closing) { @@ -144,10 +162,14 @@ public class SpannableHtmlParser { @Nullable public Object[] htmlSpans(String html) { // todo, additional handling of: image & link + Debug.i("html: %s", html); return parser.getSpans(html); } + // this is called when we encounter `void` tag + // `img` is a void tag public Spanned html(String html) { + Debug.i("html: %s", html); return parser.parse(html); }