Working with realtive urls

This commit is contained in:
Dimitry Ivanov 2017-05-22 12:53:21 +03:00
parent 9fbf5ff1b1
commit 250dd7677d
16 changed files with 217 additions and 40 deletions

View File

@ -97,6 +97,8 @@ Lorem ipsum `dolor` sit amet
Lorem ipsum dolor `sit` amet Lorem ipsum dolor `sit` amet
Lorem ipsum dolor sit `amet` Lorem ipsum dolor sit `amet`
`Lorem ipsum dolor sit amet`
### Code block ### Code block
// todo syntax higlight // todo syntax higlight
``` ```

View File

@ -55,8 +55,8 @@ class AppModule {
@Singleton @Singleton
@Provides @Provides
UrlProvider urlProvider() { UriProcessor uriProcessor() {
return new UrlProviderImpl(); return new UriProcessorImpl();
} }
@Provides @Provides

View File

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

View File

@ -27,6 +27,9 @@ public class MainActivity extends Activity {
@Inject @Inject
Themes themes; Themes themes;
@Inject
UriProcessor uriProcessor;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -38,9 +41,13 @@ public class MainActivity extends Activity {
themes.apply(this); themes.apply(this);
// how can we obtain SpannableConfiguration after theme was applied? // 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); 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 final AppBarItem.Renderer appBarRenderer
= new AppBarItem.Renderer(findViewById(R.id.app_bar), new View.OnClickListener() { = 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() { markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
@Override @Override
public void apply(String text) { public void apply(String text) {
markdownRenderer.render(MainActivity.this, text, new MarkdownRenderer.MarkdownReadyListener() { markdownRenderer.render(MainActivity.this, uri(), text, new MarkdownRenderer.MarkdownReadyListener() {
@Override @Override
public void onMarkdownReady(CharSequence markdown) { public void onMarkdownReady(CharSequence markdown) {
Markwon.setText(textView, markdown); Markwon.setText(textView, markdown);
@ -80,8 +87,6 @@ public class MainActivity extends Activity {
final Uri uri = uri(); final Uri uri = uri();
Debug.i(uri);
if (uri != null) { if (uri != null) {
title = uri.getLastPathSegment(); title = uri.getLastPathSegment();
subtitle = uri.toString(); subtitle = uri.toString();
@ -93,6 +98,13 @@ public class MainActivity extends Activity {
return new AppBarItem.State(title, subtitle); return new AppBarItem.State(title, subtitle);
} }
private void checkUri() {
final Uri uri = uri();
if (uri != null) {
getIntent().setData(uriProcessor.process(uri));
}
}
private Uri uri() { private Uri uri() {
final Intent intent = getIntent(); final Intent intent = getIntent();
return intent != null return intent != null

View File

@ -45,9 +45,6 @@ public class MarkdownLoader {
@Inject @Inject
OkHttpClient client; OkHttpClient client;
@Inject
UrlProvider urlProvider;
private Future<?> task; private Future<?> task;
@Inject @Inject
@ -130,10 +127,8 @@ public class MarkdownLoader {
private String loadExternalUrl(@NonNull Uri uri) { private String loadExternalUrl(@NonNull Uri uri) {
final String url = urlProvider.provide(uri);
final Request request = new Request.Builder() final Request request = new Request.Builder()
.url(url) .url(uri.toString())
.build(); .build();
Response response = null; Response response = null;

View File

@ -1,14 +1,23 @@
package ru.noties.markwon; package ru.noties.markwon;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; 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.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import javax.inject.Inject; import javax.inject.Inject;
import ru.noties.markwon.renderer.SpannableRenderer;
@ActivityScope @ActivityScope
public class MarkdownRenderer { public class MarkdownRenderer {
@ -31,15 +40,37 @@ public class MarkdownRenderer {
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(); cancel();
task = service.submit(new Runnable() { task = service.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
final UrlProcessor urlProcessor;
if (uri == null) {
urlProcessor = null;
} else {
urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString());
}
final SpannableConfiguration configuration = SpannableConfiguration.builder(context) final SpannableConfiguration configuration = SpannableConfiguration.builder(context)
.asyncDrawableLoader(loader) .asyncDrawableLoader(loader)
.urlProcessor(urlProcessor)
.build(); .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() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {

View File

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

View File

@ -5,25 +5,33 @@ import android.support.annotation.NonNull;
import java.util.List; import java.util.List;
class UrlProviderImpl implements UrlProvider { class UriProcessorImpl implements UriProcessor {
private static final String GITHUB = "github.com"; private static final String GITHUB = "github.com";
@Override @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 // hm... github, even having a README.md in path will return rendered HTML
final Uri out;
if (GITHUB.equals(uri.getAuthority())) { if (GITHUB.equals(uri.getAuthority())) {
final List<String> segments = uri.getPathSegments(); final List<String> segments = uri.getPathSegments();
if (segments != null final int size = segments != null
&& segments.contains("blob")) { ? segments.size()
: 0;
if (size > 0) {
// we need to modify the final uri // we need to modify the final uri
final Uri.Builder builder = new Uri.Builder() final Uri.Builder builder = new Uri.Builder()
.scheme(uri.getScheme()) .scheme(uri.getScheme())
.authority(uri.getAuthority()) .authority(uri.getAuthority())
.fragment(uri.getFragment()) .fragment(uri.getFragment())
.query(uri.getQuery()); .query(uri.getQuery());
for (String segment: segments) { for (String segment: segments) {
final String part; final String part;
if ("blob".equals(segment)) { if ("blob".equals(segment)) {
@ -33,10 +41,14 @@ class UrlProviderImpl implements UrlProvider {
} }
builder.appendPath(part); builder.appendPath(part);
} }
uri = builder.build(); out = builder.build();
} else {
out = uri;
} }
} else {
out = uri;
} }
return uri.toString(); return out;
} }
} }

View File

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

View File

@ -24,6 +24,7 @@ public class SpannableConfiguration {
private final AsyncDrawable.Loader asyncDrawableLoader; private final AsyncDrawable.Loader asyncDrawableLoader;
private final SyntaxHighlight syntaxHighlight; private final SyntaxHighlight syntaxHighlight;
private final LinkSpan.Resolver linkResolver; private final LinkSpan.Resolver linkResolver;
private final UrlProcessor urlProcessor;
private final SpannableHtmlParser htmlParser; private final SpannableHtmlParser htmlParser;
private SpannableConfiguration(Builder builder) { private SpannableConfiguration(Builder builder) {
@ -31,6 +32,7 @@ public class SpannableConfiguration {
this.asyncDrawableLoader = builder.asyncDrawableLoader; this.asyncDrawableLoader = builder.asyncDrawableLoader;
this.syntaxHighlight = builder.syntaxHighlight; this.syntaxHighlight = builder.syntaxHighlight;
this.linkResolver = builder.linkResolver; this.linkResolver = builder.linkResolver;
this.urlProcessor = builder.urlProcessor;
this.htmlParser = builder.htmlParser; this.htmlParser = builder.htmlParser;
} }
@ -50,6 +52,10 @@ public class SpannableConfiguration {
return linkResolver; return linkResolver;
} }
public UrlProcessor urlProcessor() {
return urlProcessor;
}
public SpannableHtmlParser htmlParser() { public SpannableHtmlParser htmlParser() {
return htmlParser; return htmlParser;
} }
@ -61,6 +67,7 @@ public class SpannableConfiguration {
private AsyncDrawable.Loader asyncDrawableLoader; private AsyncDrawable.Loader asyncDrawableLoader;
private SyntaxHighlight syntaxHighlight; private SyntaxHighlight syntaxHighlight;
private LinkSpan.Resolver linkResolver; private LinkSpan.Resolver linkResolver;
private UrlProcessor urlProcessor;
private SpannableHtmlParser htmlParser; private SpannableHtmlParser htmlParser;
public Builder(Context context) { public Builder(Context context) {
@ -87,6 +94,11 @@ public class SpannableConfiguration {
return this; return this;
} }
public Builder urlProcessor(UrlProcessor urlProcessor) {
this.urlProcessor = urlProcessor;
return this;
}
public Builder htmlParser(SpannableHtmlParser htmlParser) { public Builder htmlParser(SpannableHtmlParser htmlParser) {
this.htmlParser = htmlParser; this.htmlParser = htmlParser;
return this; return this;
@ -105,8 +117,11 @@ public class SpannableConfiguration {
if (linkResolver == null) { if (linkResolver == null) {
linkResolver = new LinkResolverDef(); linkResolver = new LinkResolverDef();
} }
if (urlProcessor == null) {
urlProcessor = new UrlProcessorNoOp();
}
if (htmlParser == null) { if (htmlParser == null) {
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader); htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor);
} }
return new SpannableConfiguration(this); return new SpannableConfiguration(this);
} }

View File

@ -0,0 +1,8 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
public interface UrlProcessor {
@NonNull
String process(@NonNull String destination);
}

View File

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

View File

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

View File

@ -51,7 +51,7 @@ import ru.noties.markwon.spans.ThematicBreakSpan;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class SpannableMarkdownVisitor extends AbstractVisitor { public class SpannableMarkdownVisitor extends AbstractVisitor {
private static final String HTML_CONTENT = "<%1$s>%2$s</%1$s>"; private static final String HTML_CONTENT = "<%1$s>%2$s</%3$s>";
private final SpannableConfiguration configuration; private final SpannableConfiguration configuration;
private final SpannableStringBuilder builder; private final SpannableStringBuilder builder;
@ -253,7 +253,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
@Override @Override
public void visit(SoftLineBreak softLineBreak) { public void visit(SoftLineBreak softLineBreak) {
newLine(); // at first here was a new line, but here should be a space char
builder.append(' ');
} }
@Override @Override
@ -306,13 +307,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final Node parent = image.getParent(); final Node parent = image.getParent();
final boolean link = parent != null && parent instanceof Link; final boolean link = parent != null && parent instanceof Link;
final String destination = configuration.urlProcessor().process(image.getDestination());
setSpan( setSpan(
length, length,
new AsyncDrawableSpan( new AsyncDrawableSpan(
configuration.theme(), configuration.theme(),
new AsyncDrawable( new AsyncDrawable(
image.getDestination(), destination,
configuration.asyncDrawableLoader() configuration.asyncDrawableLoader()
), ),
AsyncDrawableSpan.ALIGN_BOTTOM, AsyncDrawableSpan.ALIGN_BOTTOM,
@ -351,7 +353,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
setSpan(item.start, span); setSpan(item.start, span);
} else { } else {
final String content = builder.subSequence(start, builder.length()).toString(); 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 Object[] spans = htmlParser.htmlSpans(html);
final int length = spans != null final int length = spans != null
? spans.length ? spans.length
@ -382,7 +384,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
public void visit(Link link) { public void visit(Link link) {
final int length = builder.length(); final int length = builder.length();
visitChildren(link); 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) { private void setSpan(int start, @NonNull Object span) {

View File

@ -2,20 +2,30 @@ package ru.noties.markwon.renderer.html;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Html; import android.text.Html;
import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
class HtmlImageGetter implements Html.ImageGetter { class HtmlImageGetter implements Html.ImageGetter {
private final AsyncDrawable.Loader loader; 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.loader = loader;
this.urlProcessor = urlProcessor;
} }
@Override @Override
public Drawable getDrawable(String source) { 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);
} }
} }

View File

@ -13,6 +13,8 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.AsyncDrawable;
import ru.noties.markwon.spans.SpannableTheme; 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) // we need to handle images independently (in order to parse alt, width, height, etc)
// creates default parser // creates default parser
public static SpannableHtmlParser create(@NonNull SpannableTheme theme, @NonNull AsyncDrawable.Loader loader) { public static SpannableHtmlParser create(
return builderWithDefaults(theme, loader) @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(); .build();
} }
@ -33,7 +47,8 @@ public class SpannableHtmlParser {
public static Builder builderWithDefaults( public static Builder builderWithDefaults(
@NonNull SpannableTheme theme, @NonNull SpannableTheme theme,
@Nullable AsyncDrawable.Loader asyncDrawableLoader @Nullable AsyncDrawable.Loader asyncDrawableLoader,
@Nullable UrlProcessor urlProcessor
) { ) {
final BoldProvider boldProvider = new BoldProvider(); final BoldProvider boldProvider = new BoldProvider();
@ -42,7 +57,7 @@ public class SpannableHtmlParser {
final HtmlParser parser; final HtmlParser parser;
if (asyncDrawableLoader != null) { if (asyncDrawableLoader != null) {
parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader), null); parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader, urlProcessor), null);
} else { } else {
parser = DefaultHtmlParser.create(null, null); parser = DefaultHtmlParser.create(null, null);
} }
@ -71,9 +86,12 @@ public class SpannableHtmlParser {
public interface HtmlParser { public interface HtmlParser {
Object[] getSpans(@NonNull String html); Object[] getSpans(@NonNull String html);
Spanned parse(@NonNull String html); Spanned parse(@NonNull String html);
} }
private static final String LINK_START = "<a ";
private final Map<String, SpanProvider> customTags; private final Map<String, SpanProvider> customTags;
private final Set<String> voidTags; private final Set<String> voidTags;
private final HtmlParser parser; private final HtmlParser parser;
@ -97,7 +115,7 @@ public class SpannableHtmlParser {
if (length < 3) { if (length < 3) {
tag = null; tag = null;
} else { } 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 closing = '<' == html.charAt(0) && '/' == html.charAt(1);
final boolean voidTag; final boolean voidTag;
if (closing) { if (closing) {
@ -144,10 +162,14 @@ public class SpannableHtmlParser {
@Nullable @Nullable
public Object[] htmlSpans(String html) { public Object[] htmlSpans(String html) {
// todo, additional handling of: image & link // todo, additional handling of: image & link
Debug.i("html: %s", html);
return parser.getSpans(html); return parser.getSpans(html);
} }
// this is called when we encounter `void` tag
// `img` is a void tag
public Spanned html(String html) { public Spanned html(String html) {
Debug.i("html: %s", html);
return parser.parse(html); return parser.parse(html);
} }