From 513330eea9265838e1e7e0b7dc4ca21a14f4678f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 16 Mar 2019 11:40:06 +0100 Subject: [PATCH] Integrated BBCode parser --- .../client/ui/adapters/PostsAdapter.java | 16 +- .../client/ui/views/ContentTextView.java | 22 +- .../bbcodeparser/BBCodeParser.java | 293 ++++++++++++++++++ 3 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/communiquons/bbcodeparser/BBCodeParser.java diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/adapters/PostsAdapter.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/adapters/PostsAdapter.java index 993fe55..e8e8ee1 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/adapters/PostsAdapter.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/adapters/PostsAdapter.java @@ -35,8 +35,10 @@ import org.communiquons.android.comunic.client.ui.views.SurveyView; import org.communiquons.android.comunic.client.ui.views.WebLinkView; import org.communiquons.android.comunic.client.ui.views.WebUserAccountImage; import org.communiquons.android.comunic.client.ui.views.YouTubeVideoView; +import org.communiquons.bbcodeparser.BBCodeParser; import java.util.ArrayList; +import java.util.Objects; /** * Posts adapter @@ -265,8 +267,8 @@ public class PostsAdapter extends BaseRecyclerViewAdapter { else if(post.getPage_type() == PageType.USER_PAGE && mList.getUsersInfo().containsKey(post.getPage_id())){ - mTargetPageName.setText(mList.getUsersInfo().get(post.getPage_id()) - .getDisplayFullName()); + mTargetPageName.setText(Objects.requireNonNull( + mList.getUsersInfo().get(post.getPage_id())).getDisplayFullName()); } @@ -274,8 +276,8 @@ public class PostsAdapter extends BaseRecyclerViewAdapter { else if(post.getPage_type() == PageType.GROUP_PAGE && mList.getGroupsInfo().containsKey(post.getPage_id())){ - mTargetPageName.setText(mList.getGroupsInfo().get(post.getPage_id()) - .getDisplayName()); + mTargetPageName.setText(Objects.requireNonNull( + mList.getGroupsInfo().get(post.getPage_id())).getDisplayName()); } @@ -320,11 +322,7 @@ public class PostsAdapter extends BaseRecyclerViewAdapter { //Set post content - mPostContent.setParsedText( - StringsUtils.RemoveBBCode( - UiUtils.prepareStringTextView(post.getContent()) - ) - ); + mPostContent.setParsedText(new BBCodeParser().parse(post.getContent())); //Post likes diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/views/ContentTextView.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/views/ContentTextView.java index e5dd878..fb75865 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/views/ContentTextView.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/views/ContentTextView.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.net.Uri; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -63,17 +64,22 @@ public class ContentTextView extends android.support.v7.widget.AppCompatTextView } /** - * Set na new text, with links parsed + * Set a new text, with links parsed * * @param text The text to parse */ public void setParsedText(String text) { - super.setText(text); + this.setParsedText(new SpannableStringBuilder(text)); + } - //Parse text - SpannableStringBuilder ssb = new SpannableStringBuilder(text); + /** + * Set a new text, with links parsed + * + * @param ssb Builder to use + */ + public void setParsedText(SpannableStringBuilder ssb){ - String[] parts = text.split("\\s+"); + String[] parts = ssb.toString().split("\\s+"); int pos = 0; for (String part : parts) { @@ -106,7 +112,7 @@ public class ContentTextView extends android.support.v7.widget.AppCompatTextView private abstract class BaseClickableSpan extends ClickableSpan { @Override - public void updateDrawState(TextPaint ds) { + public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); ds.setColor(mLinksColor); @@ -126,7 +132,7 @@ public class ContentTextView extends android.support.v7.widget.AppCompatTextView } @Override - public void onClick(View widget) { + public void onClick(@NonNull View widget) { getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(mURL))); } } @@ -143,7 +149,7 @@ public class ContentTextView extends android.support.v7.widget.AppCompatTextView } @Override - public void onClick(View widget) { + public void onClick(@NonNull View widget) { MainActivity.FollowTag(getActivity(), mTag); } } diff --git a/app/src/main/java/org/communiquons/bbcodeparser/BBCodeParser.java b/app/src/main/java/org/communiquons/bbcodeparser/BBCodeParser.java new file mode 100644 index 0000000..c0b0867 --- /dev/null +++ b/app/src/main/java/org/communiquons/bbcodeparser/BBCodeParser.java @@ -0,0 +1,293 @@ +package org.communiquons.bbcodeparser; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.AlignmentSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.UnderlineSpan; +import android.util.Log; +import android.widget.TextView; + +import java.util.Objects; + +/** + * BBCode parser + * + * @author Pierre HUBERT + */ +public class BBCodeParser { + + /** + * Debug tag + */ + private static final String TAG = BBCodeParser.class.getSimpleName(); + + /** + * Recursion limit, default set to 10 + */ + private int mRecursionLimit = 10; + + private char[] text; + + /** + * Construct new instance of BBCodeParser + */ + public BBCodeParser(){ + + } + + public int getRecursionLimit() { + return mRecursionLimit; + } + + public BBCodeParser setRecursionLimit(int recursionLimit) { + this.mRecursionLimit = recursionLimit; + return this; + } + + + /** + * Parse a BBCode string for a {@link TextView} + * + * @param text The text to parse + * @return Parsed string + */ + public SpannableStringBuilder parse(@NonNull String text){ + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + + this.text = text.toCharArray(); + + try { + parseInternal(null, 0, 0, ssb); + } catch (Exception e){ + Log.e(TAG, "Unable to parse text!"); + e.printStackTrace(); + return new SpannableStringBuilder(text); + } + + return ssb; + + } + + private int parseInternal(@Nullable String parentTag, int level, int pos, SpannableStringBuilder ssb){ + + int new_pos = pos; + int childNumber = 0; + while(new_pos < text.length) { + + boolean stop = false; + while (new_pos < text.length && !stop) { + for (; new_pos < text.length && text[new_pos] != '['; new_pos++) + ssb.append(text[new_pos]); + new_pos++; + + if(new_pos > text.length) new_pos = text.length; + + + + if(level < mRecursionLimit) + for (int i = new_pos; i < text.length && !stop && text[i] != '[' && text[i] != ' '; i++) + if (text[i] == ']') + stop = true; + + if (!stop && text[new_pos-1] == '[') + ssb.append('['); + + } + + if(new_pos >= text.length) + return new_pos; + + int closeTagPos = findChar(new_pos, ']'); + + //Process the new tag + //Check if we have to exit current tag + if(text[new_pos] == '/') + return closeTagPos; + + + //Determine tag + String tag = String.copyValueOf(text, new_pos, closeTagPos - new_pos); + + int length_before = ssb.length(); + + //Pre decoding + preDecodeTag(childNumber, parentTag, tag, ssb, length_before); + + //Parse tag content + int end_pos = parseInternal(tag,level+1, closeTagPos + 1, ssb); + int length_after = ssb.length(); + + //Post decoding + postDecodeTag(tag, ssb, length_before , length_after); + + + new_pos = end_pos + 1; + childNumber++; + } + + return new_pos; + } + + + private void preDecodeTag(int childNumber, @Nullable String parentTag, + String tag, SpannableStringBuilder ssb, int begin){ + + + switch (tag){ + + + case "ul": + case "ol": + ssb.append(System.getProperty("line.separator")); + break; + + + case "li": + + if (Objects.equals(parentTag, "ol")) { + ssb.append(String.valueOf(childNumber + 1)).append(". "); + } + else + ssb.append("\u2022 "); + break; + + + case "quote": + case "code": + ssb.append(System.getProperty("line.separator")); + ssb.append(System.getProperty("line.separator")); + break; + + } + } + + + private void postDecodeTag(String tag, SpannableStringBuilder ssb, int begin, int end) { + + Object span = null; + Object span2 = null; + + String[] args = null; + + if(tag.contains("=")){ + args = tag.split("="); + tag = args[0]; + } + + + switch (tag) { + + //Decode italic and bold + case "i": + case "b": + int style = tag.equals("i") ? Typeface.ITALIC : Typeface.BOLD; + span = new StyleSpan(style); + break; + + //Underline text + case "u": + span = new UnderlineSpan(); + break; + + //Strikethrough + case "s": + span = new StrikethroughSpan(); + break; + + + //Superscript + case "sup": + span = new SuperscriptSpan(); + span2 = new RelativeSizeSpan(0.6f); + break; + + //Subscript + case "sub": + span = new SubscriptSpan(); + span2 = new RelativeSizeSpan(0.6f); + break; + + //Color + case "color": + if(args == null){ + Log.e(TAG, "Invalid color!"); + break; + } + span = new ForegroundColorSpan(Color.parseColor(args[1])); + break; + + + //Lists - Lists are aligned to the left by default + case "ul": + case "ol": + + + //Left alignment + case "left": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + span = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_LEFT); + } + break; + + //Center alignment + case "center": + span = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER); + break; + + //Right alignment + case "right": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + span = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_RIGHT); + } + break; + + //Line break for list items + case "li": + ssb.append(System.getProperty("line.separator")); + break; + + + //Quotes + case "quote": + case "code": + postDecodeTag("left", ssb, begin, end); + postDecodeTag("i", ssb, begin, end); + ssb.append(System.getProperty("line.separator")); + ssb.append(System.getProperty("line.separator")); + ssb.append(System.getProperty("line.separator")); + break; + + default: + Log.e(TAG, tag + " not recognized: " + tag); + break; + } + + if(span != null) + ssb.setSpan(span, begin, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if(span2 != null) + ssb.setSpan(span2, begin, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + + private int findChar(int start, char c){ + for (int i = start; i < text.length; i++) { + if(text[i] == c) + return i; + } + + return text.length; + } +}