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`
### Code block
// todo syntax higlight
```

View File

@ -55,8 +55,8 @@ class AppModule {
@Singleton
@Provides
UrlProvider urlProvider() {
return new UrlProviderImpl();
UriProcessor uriProcessor() {
return new UriProcessorImpl();
}
@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
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

View File

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

View File

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

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

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

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")
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 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) {

View File

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

View File

@ -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 = "<a ";
private final Map<String, SpanProvider> customTags;
private final Set<String> 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);
}