mirror of https://codeberg.org/gitnex/GitNex.git
Improve md link opening (#1023)
* Open issue/mention links direclty instead of using `DeepLinksActivity` * open in custom tabs if enabled * improve code Co-authored-by: qwerty287 <ndev@web.de> Co-authored-by: M M Arif <mmarif@noreply.codeberg.org> Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1023 Reviewed-by: M M Arif <mmarif@noreply.codeberg.org> Co-authored-by: qwerty287 <qwerty287@noreply.codeberg.org> Co-committed-by: qwerty287 <qwerty287@noreply.codeberg.org>
This commit is contained in:
parent
fd94f8b0b8
commit
e444f8f729
|
@ -1,6 +1,7 @@
|
|||
package org.mian.gitnex.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
@ -9,12 +10,18 @@ import androidx.core.content.res.ResourcesCompat;
|
|||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
import org.commonmark.node.AbstractVisitor;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.Image;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.parser.InlineParserFactory;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.parser.PostProcessor;
|
||||
import org.mian.gitnex.R;
|
||||
import org.mian.gitnex.activities.IssueDetailActivity;
|
||||
import org.mian.gitnex.activities.ProfileActivity;
|
||||
import org.mian.gitnex.clients.PicassoService;
|
||||
import org.mian.gitnex.core.MainGrammarLocator;
|
||||
import java.util.Objects;
|
||||
|
@ -26,6 +33,7 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonConfiguration;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.core.CorePlugin;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
|
@ -68,8 +76,8 @@ public class Markdown {
|
|||
|
||||
private static final Timeout timeout = new Timeout(MAX_CLAIM_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
private static final ExecutorService executorService =
|
||||
new ThreadPoolExecutor(MAX_POOL_SIZE / 2, MAX_POOL_SIZE, MAX_THREAD_KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>());
|
||||
private static final ExecutorService executorService = new ThreadPoolExecutor(MAX_POOL_SIZE / 2, MAX_POOL_SIZE, MAX_THREAD_KEEP_ALIVE_SECONDS,
|
||||
TimeUnit.SECONDS, new SynchronousQueue<>());
|
||||
|
||||
private static final Pool<Renderer> rendererPool;
|
||||
private static final Pool<RecyclerViewRenderer> rvRendererPool;
|
||||
|
@ -84,11 +92,15 @@ public class Markdown {
|
|||
config.setAllocator(new Allocator<Renderer>() {
|
||||
|
||||
@Override
|
||||
public Renderer allocate(Slot slot) throws Exception {
|
||||
public Renderer allocate(Slot slot) {
|
||||
|
||||
return new Renderer(slot);
|
||||
}
|
||||
|
||||
@Override public void deallocate(Renderer poolable) throws Exception {}
|
||||
@Override
|
||||
public void deallocate(Renderer poolable) {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
@ -103,10 +115,14 @@ public class Markdown {
|
|||
|
||||
@Override
|
||||
public RecyclerViewRenderer allocate(Slot slot) {
|
||||
|
||||
return new RecyclerViewRenderer(slot);
|
||||
}
|
||||
|
||||
@Override public void deallocate(RecyclerViewRenderer poolable) {}
|
||||
@Override
|
||||
public void deallocate(RecyclerViewRenderer poolable) {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
@ -123,7 +139,9 @@ public class Markdown {
|
|||
renderer.setParameters(context, markdown, textView);
|
||||
executorService.execute(renderer);
|
||||
}
|
||||
} catch(InterruptedException ignored) {}
|
||||
}
|
||||
catch(InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static void render(Context context, String markdown, RecyclerView recyclerView) {
|
||||
|
@ -135,7 +153,9 @@ public class Markdown {
|
|||
renderer.setParameters(context, markdown, recyclerView);
|
||||
executorService.execute(renderer);
|
||||
}
|
||||
} catch(InterruptedException ignored) {}
|
||||
}
|
||||
catch(InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static class Renderer implements Runnable, Poolable {
|
||||
|
@ -149,31 +169,27 @@ public class Markdown {
|
|||
private TextView textView;
|
||||
|
||||
public Renderer(Slot slot) {
|
||||
|
||||
this.slot = slot;
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
|
||||
Prism4jTheme prism4jTheme = TinyDB.getInstance(context).getString("currentTheme").equals("dark") ?
|
||||
Prism4jThemeDarkula.create() :
|
||||
Prism4jThemeDefault.create();
|
||||
Prism4jTheme prism4jTheme =
|
||||
TinyDB.getInstance(context).getString("currentTheme").equals("dark") ? Prism4jThemeDarkula.create() : Prism4jThemeDefault.create();
|
||||
|
||||
Markwon.Builder builder = Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(LinkifyPlugin.create(true))
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(TablePlugin.create(context))
|
||||
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get()))
|
||||
.usePlugin(SyntaxHighlightPlugin.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
|
||||
Markwon.Builder builder = Markwon.builder(context).usePlugin(CorePlugin.create()).usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(LinkifyPlugin.create(true)).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(TablePlugin.create(context))
|
||||
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create())).usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create()).usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get())).usePlugin(
|
||||
SyntaxHighlightPlugin
|
||||
.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
|
||||
private Typeface tf;
|
||||
|
||||
private void setupTf(Context context) {
|
||||
|
||||
switch(TinyDB.getInstance(context).getInt("customFontId", -1)) {
|
||||
case 0:
|
||||
tf = Typeface.createFromAsset(context.getAssets(), "fonts/roboto.ttf");
|
||||
|
@ -189,13 +205,17 @@ public class Markdown {
|
|||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
if(tf == null) setupTf(textView.getContext());
|
||||
|
||||
if(tf == null) {
|
||||
setupTf(textView.getContext());
|
||||
}
|
||||
textView.setTypeface(tf);
|
||||
super.beforeSetText(textView, markdown);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
|
||||
builder.codeBlockTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
|
||||
builder.codeBlockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
|
||||
builder.blockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
|
||||
|
@ -203,7 +223,9 @@ public class Markdown {
|
|||
builder.codeTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
|
||||
builder.linkColor(ResourcesCompat.getColor(context.getResources(), R.color.lightBlue, null));
|
||||
|
||||
if(tf == null) setupTf(context);
|
||||
if(tf == null) {
|
||||
setupTf(context);
|
||||
}
|
||||
builder.headingTypeface(tf);
|
||||
}
|
||||
});
|
||||
|
@ -225,7 +247,9 @@ public class Markdown {
|
|||
Objects.requireNonNull(markdown);
|
||||
Objects.requireNonNull(textView);
|
||||
|
||||
if(markwon == null) setup();
|
||||
if(markwon == null) {
|
||||
setup();
|
||||
}
|
||||
|
||||
Spanned processedMarkdown = markwon.toMarkdown(markdown);
|
||||
|
||||
|
@ -248,8 +272,10 @@ public class Markdown {
|
|||
}
|
||||
|
||||
public void expire() {
|
||||
|
||||
slot.expire(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class RecyclerViewRenderer implements Runnable, Poolable {
|
||||
|
@ -264,6 +290,7 @@ public class Markdown {
|
|||
private MarkwonAdapter adapter;
|
||||
|
||||
public RecyclerViewRenderer(Slot slot) {
|
||||
|
||||
this.slot = slot;
|
||||
}
|
||||
|
||||
|
@ -271,31 +298,26 @@ public class Markdown {
|
|||
|
||||
Objects.requireNonNull(context);
|
||||
|
||||
Prism4jTheme prism4jTheme = TinyDB.getInstance(context).getString("currentTheme").equals("dark") ?
|
||||
Prism4jThemeDarkula.create() :
|
||||
Prism4jThemeDefault.create();
|
||||
Prism4jTheme prism4jTheme =
|
||||
TinyDB.getInstance(context).getString("currentTheme").equals("dark") ? Prism4jThemeDarkula.create() : Prism4jThemeDefault.create();
|
||||
|
||||
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||
.addInlineProcessor(new IssueInlineProcessor(context))
|
||||
.addInlineProcessor(new UserInlineProcessor(context))
|
||||
.build();
|
||||
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder().addInlineProcessor(new IssueInlineProcessor())
|
||||
.addInlineProcessor(new UserInlineProcessor()).build();
|
||||
|
||||
Markwon.Builder builder = Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
Markwon.Builder builder = Markwon.builder(context).usePlugin(CorePlugin.create()).usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(LinkifyPlugin.create(true)) // TODO not working
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(TableEntryPlugin.create(context))
|
||||
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get()))
|
||||
.usePlugin(SyntaxHighlightPlugin.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(TableEntryPlugin.create(context))
|
||||
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create())).usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create()).usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get())).usePlugin(
|
||||
SyntaxHighlightPlugin
|
||||
.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
|
||||
private final Context context = RecyclerViewRenderer.this.context;
|
||||
private Typeface tf;
|
||||
|
||||
private void setupTf(Context context) {
|
||||
|
||||
switch(TinyDB.getInstance(context).getInt("customFontId", -1)) {
|
||||
case 0:
|
||||
tf = Typeface.createFromAsset(context.getAssets(), "fonts/roboto.ttf");
|
||||
|
@ -311,18 +333,24 @@ public class Markdown {
|
|||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
if(tf == null) setupTf(textView.getContext());
|
||||
|
||||
if(tf == null) {
|
||||
setupTf(textView.getContext());
|
||||
}
|
||||
textView.setTypeface(tf);
|
||||
super.beforeSetText(textView, markdown);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
|
||||
builder.inlineParserFactory(inlineParserFactory);
|
||||
builder.postProcessor(new LinkPostProcessor(TinyDB.getInstance(context), context.getString(R.string.commentButtonText)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
|
||||
builder.codeBlockTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
|
||||
builder.codeBlockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
|
||||
builder.blockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
|
||||
|
@ -330,63 +358,73 @@ public class Markdown {
|
|||
builder.codeTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
|
||||
builder.linkColor(ResourcesCompat.getColor(context.getResources(), R.color.lightBlue, null));
|
||||
|
||||
if(tf == null) setupTf(context);
|
||||
if(tf == null) {
|
||||
setupTf(context);
|
||||
}
|
||||
builder.headingTypeface(Typeface.create(tf, Typeface.BOLD));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
|
||||
builder.linkResolver((view, link) -> {
|
||||
if(link.startsWith("gitnexuser://")) {
|
||||
Intent i = new Intent(view.getContext(), ProfileActivity.class);
|
||||
i.putExtra("username", link.substring(13));
|
||||
view.getContext().startActivity(i);
|
||||
}
|
||||
else if(link.startsWith("gitnexissue://")) {
|
||||
link = link.substring(14); // remove gitnexissue://
|
||||
Intent i = new Intent(view.getContext(), IssueDetailActivity.class);
|
||||
String index;
|
||||
TinyDB tinyDB = TinyDB.getInstance(context);
|
||||
if(link.contains("/")) {
|
||||
index = link.split("#")[1];
|
||||
tinyDB.putString("repoFullName", link.split("#")[0]);
|
||||
i.putExtra("openedFromLink", "true");
|
||||
}
|
||||
else {
|
||||
index = link.substring(1);
|
||||
}
|
||||
|
||||
tinyDB.putString("issueNumber", index);
|
||||
i.putExtra("issueNumber", index);
|
||||
view.getContext().startActivity(i);
|
||||
}
|
||||
else if(link.startsWith("gitnexcommit://")) {
|
||||
// this is not supported by GitNex itself right now, so let's open the browser
|
||||
TinyDB tinyDB = TinyDB.getInstance(context);
|
||||
String instanceUrl = tinyDB.getString("instanceUrl");
|
||||
instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/"));
|
||||
link = link.substring(15);
|
||||
if(link.contains("/")) {
|
||||
AppUtil.openUrlInBrowser(context,
|
||||
instanceUrl + link.substring(0, link.lastIndexOf("/")) + "/commit/" + link.split("/")[2]);
|
||||
}
|
||||
else {
|
||||
AppUtil.openUrlInBrowser(context, instanceUrl + tinyDB.getString("repoFullName") + "/commit/" + link);
|
||||
}
|
||||
}
|
||||
else {
|
||||
AppUtil.openUrlInBrowser(view.getContext(), link);
|
||||
}
|
||||
});
|
||||
super.configureConfiguration(builder);
|
||||
}
|
||||
});
|
||||
|
||||
markwon = builder.build();
|
||||
}
|
||||
|
||||
private void setupAdapter() {
|
||||
adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.custom_markdown_adapter)
|
||||
.include(TableBlock.class, TableEntry.create(builder2 -> builder2
|
||||
.tableLayout(R.layout.custom_markdown_table, R.id.table_layout)
|
||||
|
||||
adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.custom_markdown_adapter).include(TableBlock.class, TableEntry.create(
|
||||
builder2 -> builder2.tableLayout(R.layout.custom_markdown_table, R.id.table_layout)
|
||||
.textLayoutIsRoot(R.layout.custom_markdown_adapter)))
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.custom_markdown_code_block, R.id.textCodeBlock))
|
||||
.build();
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.custom_markdown_code_block, R.id.textCodeBlock)).build();
|
||||
}
|
||||
|
||||
public void setParameters(Context context, String markdown, RecyclerView recyclerView) {
|
||||
TinyDB tinyDB = TinyDB.getInstance(context);
|
||||
String instanceUrl = tinyDB.getString("instanceUrl");
|
||||
instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/")).replaceAll("\\.", "\\.");
|
||||
|
||||
// first step: replace comment urls with {url without comment} (comment)
|
||||
final Pattern patternComment = Pattern.compile("((?<!]\\(|`)" + instanceUrl + "[^/]+/[^/]+/(?:issues|pulls)/\\d+)(?:/#|#)issuecomment-(\\d+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherComment = patternComment.matcher(markdown);
|
||||
markdown = matcherComment.replaceAll("$1 ([" + context.getString(R.string.commentButtonText) + "]($1#issuecomment-$2))");
|
||||
|
||||
// second step: remove links to issue descriptions
|
||||
final Pattern patternIssueDesc = Pattern.compile("((?<!]\\(|`)" + instanceUrl + "[^/]+/[^/]+/(?:issues|pulls)/\\d+)(?:/#|#)issue-(\\d+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherIssueDesc = patternIssueDesc.matcher(markdown);
|
||||
markdown = matcherIssueDesc.replaceAll("$1");
|
||||
|
||||
// third step: replace issue links from the same repo
|
||||
final Pattern pattern = Pattern.compile("(?<!]\\(|`)" + instanceUrl + tinyDB.getString("repoFullName") + "/(?:issues|pulls)/(\\d+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcher = pattern.matcher(markdown);
|
||||
markdown = matcher.replaceAll("#$1");
|
||||
|
||||
// fourth step: replace issue links from other repos
|
||||
String substOtherRepo =
|
||||
"[$2/$3#$4](" + instanceUrl.replace("http://", "gitnex://").replace("http://", "gitnex://") + "$1)";
|
||||
final Pattern patternOtherRepo = Pattern.compile("(?<!]\\(|`)" + instanceUrl + "(([^/]+)/([^/]+)/(?:issues|pulls)/(\\d+))(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherOtherRepo = patternOtherRepo.matcher(markdown);
|
||||
markdown = matcherOtherRepo.replaceAll(substOtherRepo);
|
||||
|
||||
// fifth step: render commit links
|
||||
String substCommit =
|
||||
"[$2](" + instanceUrl.replace("http://", "gitnex://").replace("http://", "gitnex://") + "$1)";
|
||||
final Pattern patternCommit = Pattern.compile("(?<!]\\(|`)" + instanceUrl + "([^/]+/[^/]+/commit/([a-z0-9_]+))(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherCommit = patternCommit.matcher(markdown);
|
||||
markdown = matcherCommit.replaceAll(substCommit);
|
||||
|
||||
// sixth step: replace relative attachment links
|
||||
String substAttachments =
|
||||
instanceUrl + tinyDB.getString("repoFullName") + "/$1";
|
||||
final Pattern patternAttachments = Pattern.compile("(?<=\\()/(attachments/\\S+)(?=\\))", Pattern.MULTILINE); // TODO code block ``
|
||||
final Matcher matcherAttachments = patternAttachments.matcher(markdown);
|
||||
markdown = matcherAttachments.replaceAll(substAttachments);
|
||||
|
||||
this.context = context;
|
||||
this.markdown = markdown;
|
||||
|
@ -400,7 +438,9 @@ public class Markdown {
|
|||
Objects.requireNonNull(markdown);
|
||||
Objects.requireNonNull(recyclerView);
|
||||
|
||||
if(markwon == null) setup();
|
||||
if(markwon == null) {
|
||||
setup();
|
||||
}
|
||||
|
||||
setupAdapter();
|
||||
|
||||
|
@ -409,8 +449,10 @@ public class Markdown {
|
|||
MarkwonAdapter localAdapter = adapter;
|
||||
localReference.post(() -> {
|
||||
localReference.setLayoutManager(new LinearLayoutManager(context) {
|
||||
|
||||
@Override
|
||||
public boolean canScrollVertically() {
|
||||
|
||||
return false; // disable RecyclerView scrolling, handeled by seperate ScrollViews
|
||||
}
|
||||
});
|
||||
|
@ -437,81 +479,232 @@ public class Markdown {
|
|||
}
|
||||
|
||||
public void expire() {
|
||||
|
||||
slot.expire(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class IssueInlineProcessor extends InlineProcessor {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public IssueInlineProcessor(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private static final Pattern RE = Pattern.compile("(?<=#)\\d+");
|
||||
private static final Pattern RE = Pattern.compile("(?<!\\w)#\\d+");
|
||||
|
||||
@Override
|
||||
public char specialCharacter() {
|
||||
|
||||
return '#';
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node parse() {
|
||||
|
||||
final String id = match(RE);
|
||||
if (id != null) {
|
||||
final Link link = new Link(createIssueOrPullRequestLinkDestination(id, context), null);
|
||||
link.appendChild(text("#" + id));
|
||||
if(id != null) {
|
||||
Link link = new Link("gitnexissue://" + id, null);
|
||||
link.appendChild(text(id));
|
||||
return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String createIssueOrPullRequestLinkDestination(@NonNull String id, Context context) {
|
||||
String instanceUrl = TinyDB.getInstance(context).getString("instanceUrl");
|
||||
instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/"));
|
||||
instanceUrl = instanceUrl.replace("http://", "gitnex://");
|
||||
instanceUrl = instanceUrl.replace("https://", "gitnex://");
|
||||
|
||||
return instanceUrl + TinyDB.getInstance(context).getString("repoFullName") + "/issues/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UserInlineProcessor extends InlineProcessor {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public UserInlineProcessor(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private static final Pattern RE = Pattern.compile("(?<!\\S)(?<=@)\\w+");
|
||||
private static final Pattern RE = Pattern.compile("(?<!\\w)@\\w+");
|
||||
|
||||
@Override
|
||||
public char specialCharacter() {
|
||||
|
||||
return '@';
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node parse() {
|
||||
|
||||
final String user = match(RE);
|
||||
if (user != null) {
|
||||
final Link link = new Link(createUserLinkDestination(user, context), null);
|
||||
link.appendChild(text("@" + user));
|
||||
if(user != null) {
|
||||
final Link link = new Link("gitnexuser://" + user.substring(1 /* remove @ */), null);
|
||||
link.appendChild(text(user));
|
||||
return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String createUserLinkDestination(@NonNull String user, Context context) {
|
||||
String instanceUrl = TinyDB.getInstance(context).getString("instanceUrl");
|
||||
instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/"));
|
||||
instanceUrl = instanceUrl.replace("http://", "gitnex://");
|
||||
instanceUrl = instanceUrl.replace("https://", "gitnex://");
|
||||
|
||||
return instanceUrl + user;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LinkPostProcessor implements PostProcessor {
|
||||
|
||||
private final String commentText;
|
||||
private final TinyDB tinyDB;
|
||||
private String instanceUrl;
|
||||
private String fullRepoName;
|
||||
|
||||
public LinkPostProcessor(TinyDB tinyDB, String commentText) {
|
||||
|
||||
this.commentText = commentText;
|
||||
this.tinyDB = tinyDB;
|
||||
init();
|
||||
}
|
||||
|
||||
private static Node insertNode(Node node, Node insertAfterNode) {
|
||||
|
||||
insertAfterNode.insertAfter(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
|
||||
String instanceUrl = tinyDB.getString("instanceUrl");
|
||||
instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/")).replaceAll("\\.", "\\.");
|
||||
this.instanceUrl = instanceUrl;
|
||||
fullRepoName = tinyDB.getString("repoFullName");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node process(Node node) {
|
||||
|
||||
init();
|
||||
AutolinkVisitor autolinkVisitor = new AutolinkVisitor();
|
||||
node.accept(autolinkVisitor);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void link(Text textNode) {
|
||||
|
||||
String literal = textNode.getLiteral();
|
||||
|
||||
Node lastNode = textNode;
|
||||
boolean foundAny = false;
|
||||
|
||||
final Pattern patternIssue = Pattern
|
||||
.compile(instanceUrl + "([^/]+/[^/]+)/(?:issues|pulls)/(\\d+)(?:(?:/#|#)(issue-\\d+|issuecomment-\\d+)|)", Pattern.MULTILINE);
|
||||
final Matcher matcherIssue = patternIssue.matcher(literal);
|
||||
|
||||
final Pattern patternCommit = Pattern.compile(instanceUrl + "([^/]+/[^/]+)/commit/([a-z0-9_]+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherCommit = patternCommit.matcher(literal);
|
||||
|
||||
int foundAt = 0;
|
||||
for(int i = 0; i < literal.length(); i++) {
|
||||
int issueStart = literal.length();
|
||||
if(matcherIssue.find(i)) {
|
||||
issueStart = matcherIssue.start();
|
||||
foundAny = true;
|
||||
}
|
||||
|
||||
int commitStart = literal.length();
|
||||
if(matcherCommit.find(i)) {
|
||||
commitStart = matcherCommit.start();
|
||||
foundAny = true;
|
||||
}
|
||||
|
||||
if(commitStart < issueStart) {
|
||||
// next one is a commit
|
||||
if(matcherCommit.start() > i) {
|
||||
lastNode = insertNode(new Text(literal.substring(foundAt, matcherCommit.start())), lastNode);
|
||||
}
|
||||
String shortSha = matcherCommit.group(2);
|
||||
if(shortSha == null) {
|
||||
return;
|
||||
}
|
||||
if(shortSha.length() > 10) {
|
||||
shortSha = shortSha.substring(0, 10);
|
||||
}
|
||||
String text;
|
||||
if(matcherCommit.group(1).equals(fullRepoName)) {
|
||||
text = shortSha;
|
||||
}
|
||||
else {
|
||||
text = matcherCommit.group(1) + "/" + shortSha;
|
||||
}
|
||||
Text contentNode = new Text(text);
|
||||
Link linkNode = new Link("gitnexcommit://" + text, null);
|
||||
linkNode.appendChild(contentNode);
|
||||
lastNode = insertNode(linkNode, lastNode);
|
||||
|
||||
i = matcherCommit.start();
|
||||
}
|
||||
else if(issueStart < literal.length()) {
|
||||
// next one is an issue/comment
|
||||
if(matcherIssue.start() > i) {
|
||||
lastNode = insertNode(new Text(literal.substring(i, matcherIssue.start())), lastNode);
|
||||
}
|
||||
|
||||
String text;
|
||||
if(matcherIssue.group(1).equals(fullRepoName)) {
|
||||
text = "#" + matcherIssue.group(2);
|
||||
}
|
||||
else {
|
||||
text = matcherIssue.group(1) + "#" + matcherIssue.group(2);
|
||||
}
|
||||
Text contentNode = new Text(text);
|
||||
Link linkNode = new Link("gitnexissue://" + text, null);
|
||||
linkNode.appendChild(contentNode);
|
||||
lastNode = insertNode(linkNode, lastNode);
|
||||
|
||||
String anchor = matcherIssue.group(3);
|
||||
if(anchor != null && anchor.startsWith("issuecomment-")) {
|
||||
// comment
|
||||
|
||||
// insert space
|
||||
lastNode = insertNode(new Text(" "), lastNode);
|
||||
|
||||
Text commentNode = new Text("(" + commentText + ")");
|
||||
Link linkCommentNode = new Link(matcherIssue.group(), null);
|
||||
linkCommentNode.appendChild(commentNode);
|
||||
lastNode = insertNode(linkCommentNode, lastNode);
|
||||
}
|
||||
|
||||
i = matcherIssue.end();
|
||||
}
|
||||
|
||||
// reset every time to make it usable in a "pure" state
|
||||
matcherCommit.reset();
|
||||
matcherIssue.reset();
|
||||
}
|
||||
|
||||
if(foundAny) {
|
||||
textNode.unlink();
|
||||
}
|
||||
}
|
||||
|
||||
private void linkifyImage(Image node) {
|
||||
|
||||
final Matcher patternAttachments = Pattern.compile("(/attachments/\\S+)", Pattern.MULTILINE).matcher(node.getDestination());
|
||||
if(patternAttachments.matches()) {
|
||||
node.setDestination(instanceUrl + fullRepoName + patternAttachments.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
private class AutolinkVisitor extends AbstractVisitor {
|
||||
|
||||
int inLink = 0;
|
||||
|
||||
@Override
|
||||
public void visit(Link link) {
|
||||
|
||||
inLink++;
|
||||
super.visit(link);
|
||||
inLink--;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Image image) {
|
||||
|
||||
super.visit(image);
|
||||
linkifyImage(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Text text) {
|
||||
|
||||
if(inLink == 0) {
|
||||
link(text);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue