getPath() {
75 | return path;
76 | }
77 |
78 | /**
79 | * @return the first rich list of the path.
80 | */
81 | @Nullable public CDARichList getTopListOfPath() {
82 | for (int i = path.size() - 2; i >= 0; --i) {
83 | final CDARichNode node = path.get(i);
84 | if (node instanceof CDARichList) {
85 | return (CDARichList) node;
86 | }
87 | }
88 | return null;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/AndroidProcessor.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android;
2 |
3 | import android.view.View;
4 |
5 | import com.contentful.rich.android.renderer.chars.CharSequenceRendererProvider;
6 | import com.contentful.rich.android.renderer.views.NativeViewsRendererProvider;
7 | import com.contentful.rich.core.Processor;
8 |
9 | import javax.annotation.Nonnull;
10 |
11 | /**
12 | * Processor processing rich text to create Android output.
13 | *
14 | * Use one of the factory methods to create an Android processor creating your desired output.
15 | *
16 | * @see AndroidProcessor#creatingCharSequences()
17 | */
18 | public class AndroidProcessor extends Processor {
19 |
20 | /**
21 | * Hide constructor to force creation through factory methods.
22 | */
23 | private AndroidProcessor() {
24 | }
25 |
26 | /**
27 | * Create an Android Processor capable of generating char sequences from rich text input nodes.
28 | *
29 | * @return AndroidProcessor to create char sequences
30 | * @see android.text.Spannable
31 | */
32 | public static AndroidProcessor creatingCharSequences() {
33 | final AndroidProcessor processor = new AndroidProcessor<>();
34 | new CharSequenceRendererProvider().provide(processor);
35 |
36 | return processor;
37 | }
38 |
39 | public static AndroidProcessor creatingNativeViews() {
40 | final AndroidProcessor processor = new AndroidProcessor<>();
41 | new NativeViewsRendererProvider().provide(processor);
42 |
43 | return processor;
44 | }
45 |
46 | /**
47 | * Add a renderer to the processor using only one class.
48 | *
49 | * @param renderer the combined Android renderer
50 | * @return this instance for chaining
51 | * @see com.contentful.rich.core.RenderabilityChecker
52 | * @see com.contentful.rich.core.Renderer
53 | */
54 | @Nonnull
55 | public Processor addRenderer(@Nonnull AndroidRenderer renderer) {
56 | return super.addRenderer(renderer, renderer);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/AndroidRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android;
2 |
3 | import com.contentful.java.cda.rich.CDARichNode;
4 | import com.contentful.rich.core.RenderabilityChecker;
5 | import com.contentful.rich.core.Context;
6 | import com.contentful.rich.core.Renderer;
7 |
8 | import javax.annotation.Nonnull;
9 | import javax.annotation.Nullable;
10 |
11 | /**
12 | * This is a combination interface for android renderer
13 | *
14 | * @param context to be applied to this renderer.
15 | * @param result of rendering a node.
16 | */
17 | public abstract class AndroidRenderer implements RenderabilityChecker, Renderer {
18 | protected final AndroidProcessor processor;
19 |
20 | /**
21 | * Create an Android Renderer, containing its processor for child node rendering.
22 | *
23 | * @param processor save the processor for child processing.
24 | */
25 | public AndroidRenderer(@Nonnull AndroidProcessor processor) {
26 | this.processor = processor;
27 | }
28 |
29 | /**
30 | * Overwritten to check on whether this renderer can be used to render this node in the given context.
31 | *
32 | * @param context context this check should be performed in
33 | * @param node node to be checked
34 | * @return true if the node can be rendered in the context.
35 | */
36 | @Override abstract public boolean canRender(@Nullable C context, @Nonnull CDARichNode node);
37 |
38 | /**
39 | * Performs the rendering resulting in type R.
40 | *
41 | * @param context the generic context this node should be rendered in.
42 | * @param node the node to be rendered.
43 | * @return the rendered result.
44 | */
45 | @Nullable @Override abstract public R render(@Nonnull C context, @Nonnull CDARichNode node);
46 | }
47 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/BlockRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import android.text.SpannableStringBuilder;
4 | import android.text.style.LeadingMarginSpan;
5 |
6 | import com.contentful.java.cda.rich.CDARichBlock;
7 | import com.contentful.java.cda.rich.CDARichList;
8 | import com.contentful.java.cda.rich.CDARichNode;
9 | import com.contentful.rich.android.AndroidContext;
10 | import com.contentful.rich.android.AndroidProcessor;
11 | import com.contentful.rich.android.AndroidRenderer;
12 |
13 | import java.util.List;
14 |
15 | import javax.annotation.Nonnull;
16 | import javax.annotation.Nullable;
17 |
18 | import androidx.annotation.NonNull;
19 |
20 | import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
21 |
22 | /**
23 | * Renders the given block and all of it's child nodes.
24 | */
25 | public class BlockRenderer extends AndroidRenderer {
26 | /**
27 | * Create a new BlockRenderer.
28 | *
29 | * @param processor to be used for child processing.
30 | */
31 | public BlockRenderer(@Nonnull AndroidProcessor processor) {
32 | super(processor);
33 | }
34 |
35 | /**
36 | * Can the given node be rendered with this renderer and the given context?
37 | *
38 | * @param context context this check should be performed in
39 | * @param node node to be checked
40 | * @return true if the node can be rendered.
41 | */
42 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
43 | if (context != null) {
44 | if (node instanceof CDARichBlock) {
45 | return true;
46 | }
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Perform rendering of the given block into the a character sequence.
54 | *
55 | * @param context the generic context this node should be rendered in.
56 | * @param node the node to be rendered.
57 | * @return all child nodes and this block rendered into one character sequence.
58 | */
59 | @Nullable @Override public CharSequence render(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
60 | final CDARichBlock block = (CDARichBlock) node;
61 |
62 | final SpannableStringBuilder result = new SpannableStringBuilder();
63 |
64 | if (block.getContent() != null) {
65 | for (final CDARichNode childNode : block.getContent()) {
66 | if (childNode != null) {
67 | final CharSequence childResult = processor.process(context, childNode);
68 | if (childResult != null) {
69 | result.append(childResult);
70 | }
71 | }
72 | }
73 | }
74 | childWithNewline(result);
75 |
76 | return wrap(node, indent(context, node, decorate(context, node, result)));
77 | }
78 |
79 | /**
80 | * Ensures child ends on exactly one newline.
81 | *
82 | * @param builder the current rendered spannable builder.
83 | */
84 | @Nonnull
85 | protected void childWithNewline(@Nonnull SpannableStringBuilder builder) {
86 | if (builder != null) {
87 | String text = builder.toString();
88 | if (text != null) {
89 | while (text.endsWith("\n")) {
90 | text = text.substring(0, text.length() - 1);
91 | }
92 | builder.clear();
93 | builder.append(text);
94 | }
95 | }
96 | }
97 |
98 | /**
99 | * Prefix given builder by a custom decoration.
100 | *
101 | * @param context the context this decoration should be applied to.
102 | * @param node the actual node, rendered into the builder.
103 | * @param builder a builder for generating the desired output.
104 | * @return builder for convenience.
105 | */
106 | @Nonnull
107 | protected SpannableStringBuilder decorate(@Nonnull AndroidContext context, @Nonnull CDARichNode node, @Nonnull SpannableStringBuilder builder) {
108 | return builder;
109 | }
110 |
111 | /**
112 | * Surround the given builder, if needed
113 | *
114 | * @param node the node to be rendered.
115 | * @param builder the result of the rendering.
116 | * @return the rendered result plus a optional wrap.
117 | */
118 | @NonNull
119 | public SpannableStringBuilder wrap(@Nonnull CDARichNode node, @Nonnull SpannableStringBuilder builder) {
120 | return builder;
121 | }
122 |
123 | /**
124 | * Add inline spannables to the first character to ensure indentation as needed.
125 | *
126 | * @param context of the node to be indented.
127 | * @param node the node to be indented.
128 | * @param builder the result of the node's children to be indented.
129 | * @return an indented spannable.
130 | */
131 | @Nonnull SpannableStringBuilder indent(
132 | @Nonnull AndroidContext context,
133 | @Nonnull CDARichNode node,
134 | @Nonnull SpannableStringBuilder builder) {
135 | int lists = 0;
136 | final List path = context.getPath();
137 | if (path != null) {
138 | for (final CDARichNode pathElement : path) {
139 | if (pathElement instanceof CDARichList) {
140 | lists++;
141 | }
142 | }
143 | }
144 |
145 | if (lists > 1) {
146 | final CDARichBlock block = (CDARichBlock) node;
147 | if (block.getContent().size() > 0) {
148 | if (block.getContent().get(0) instanceof CDARichBlock) {
149 | builder.append("\n");
150 | }
151 | }
152 | builder.setSpan(new LeadingMarginSpan.Standard(lists * 10 /*px*/), 0, builder.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
153 | }
154 |
155 | return builder;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/CharSequenceRendererProvider.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import com.contentful.rich.android.AndroidProcessor;
4 | import com.contentful.rich.android.renderer.listdecorator.BulletDecorator;
5 | import com.contentful.rich.android.renderer.listdecorator.LowerCaseCharacterDecorator;
6 | import com.contentful.rich.android.renderer.listdecorator.LowerCaseRomanNumeralsDecorator;
7 | import com.contentful.rich.android.renderer.listdecorator.NumbersDecorator;
8 | import com.contentful.rich.android.renderer.listdecorator.UpperCaseCharacterDecorator;
9 | import com.contentful.rich.android.renderer.listdecorator.UpperCaseRomanNumeralsDecorator;
10 |
11 | import javax.annotation.Nonnull;
12 |
13 | /**
14 | * This provider will be used to fill the processor with renderer used in the text rendering for android.
15 | */
16 | public class CharSequenceRendererProvider {
17 |
18 | /**
19 | * Add all relevant renderers to the AndroidProcessor.
20 | *
21 | * @param processor to be modified.
22 | */
23 | public void provide(@Nonnull AndroidProcessor processor) {
24 | processor.addRenderer(new HorizontalRuleRenderer(processor));
25 | processor.addRenderer(new TextRenderer(processor));
26 | processor.addRenderer(new HeadingRenderer(processor));
27 | processor.addRenderer(new ListRenderer(processor, new BulletDecorator()));
28 | processor.addRenderer(new ListRenderer(processor,
29 | new NumbersDecorator(),
30 | new UpperCaseCharacterDecorator(),
31 | new LowerCaseRomanNumeralsDecorator(),
32 | new LowerCaseCharacterDecorator(),
33 | new LowerCaseCharacterDecorator(),
34 | new UpperCaseRomanNumeralsDecorator()
35 | ));
36 | processor.addRenderer(new HyperLinkRenderer(processor));
37 | processor.addRenderer(new QuoteRenderer(processor));
38 | processor.addRenderer(new BlockRenderer(processor));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/HeadingRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import android.text.SpannableStringBuilder;
4 | import android.text.Spanned;
5 | import android.text.style.AbsoluteSizeSpan;
6 | import com.contentful.java.cda.rich.CDARichHeading;
7 | import com.contentful.java.cda.rich.CDARichNode;
8 | import com.contentful.rich.android.AndroidContext;
9 | import com.contentful.rich.android.AndroidProcessor;
10 |
11 | import javax.annotation.Nonnull;
12 | import javax.annotation.Nullable;
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | /**
17 | * Render all available headings into a spannable changing it's vertical size.
18 | */
19 | public class HeadingRenderer extends BlockRenderer {
20 |
21 | static private final List SIZE_MAP = new ArrayList<>();
22 |
23 | static {
24 | SIZE_MAP.add(72); // Level 1
25 | SIZE_MAP.add(60); // Level 2
26 | SIZE_MAP.add(52); // Level 3
27 | SIZE_MAP.add(44); // Level 4
28 | SIZE_MAP.add(36); // Level 5
29 | SIZE_MAP.add(28); // Level 6
30 | }
31 |
32 | public HeadingRenderer(@Nonnull AndroidProcessor processor) {
33 | super(processor);
34 | }
35 |
36 | /**
37 | * Is the given node a CDARichHeading?
38 | *
39 | * @param context context this check should be performed in
40 | * @param node node to be checked
41 | * @return true if node is a rich CDARichHeading.
42 | */
43 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
44 | if (node instanceof CDARichHeading) {
45 | final CDARichHeading heading = (CDARichHeading) node;
46 | if (heading.getLevel() > 0 && ((CDARichHeading) node).getLevel() < 7) {
47 | return true;
48 | }
49 | }
50 |
51 | return false;
52 | }
53 |
54 | /**
55 | * Add a size changing span to the rendered children.
56 | *
57 | * @param node the node to be rendered.
58 | * @param builder the result of the rendering.
59 | * @return the builder, enhanced by a size altering span.
60 | */
61 | @Nonnull @Override
62 | public SpannableStringBuilder wrap(@Nonnull CDARichNode node, @Nonnull SpannableStringBuilder builder) {
63 | final CDARichHeading heading = (CDARichHeading) node;
64 |
65 | final AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(SIZE_MAP.get(heading.getLevel() - 1), true);
66 | builder.setSpan(sizeSpan, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
67 |
68 | return builder;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/HorizontalRuleRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import android.text.Spannable;
4 | import android.text.SpannableStringBuilder;
5 | import android.text.style.StrikethroughSpan;
6 |
7 | import com.contentful.java.cda.rich.CDARichHorizontalRule;
8 | import com.contentful.java.cda.rich.CDARichNode;
9 | import com.contentful.rich.android.AndroidContext;
10 | import com.contentful.rich.android.AndroidProcessor;
11 | import com.contentful.rich.android.AndroidRenderer;
12 |
13 | import javax.annotation.Nonnull;
14 | import javax.annotation.Nullable;
15 |
16 | import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
17 |
18 | /**
19 | * Renderer for a horizontal rule.
20 | */
21 | public class HorizontalRuleRenderer extends AndroidRenderer {
22 | /**
23 | * Create the renderer, referencing its processor.
24 | *
25 | * This element does not have any children.
26 | *
27 | * @param processor the unused processor for this renderer.
28 | */
29 | public HorizontalRuleRenderer(@Nonnull AndroidProcessor processor) {
30 | super(processor);
31 | }
32 |
33 | /**
34 | * Check if the given node is a horizontal rule.
35 | *
36 | * @param context context this check should be performed in
37 | * @param node node to be checked
38 | * @return true if the given node is a rule
39 | */
40 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
41 | return node instanceof CDARichHorizontalRule;
42 | }
43 |
44 | /**
45 | * Render a horizontal line.
46 | *
47 | * @param context the generic context this node should be rendered in.
48 | * @param node the node to be rendered.
49 | * @return a span containing only a line.
50 | */
51 | @Nullable @Override
52 | public CharSequence render(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
53 | final Spannable result = new SpannableStringBuilder(" ");
54 | result.setSpan(new StrikethroughSpan(), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
55 | return result;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/HyperLinkRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import android.text.SpannableStringBuilder;
4 | import android.text.Spanned;
5 | import android.text.method.MovementMethod;
6 | import android.text.style.URLSpan;
7 | import androidx.annotation.NonNull;
8 | import com.contentful.java.cda.rich.CDARichHyperLink;
9 | import com.contentful.java.cda.rich.CDARichNode;
10 | import com.contentful.rich.android.AndroidContext;
11 | import com.contentful.rich.android.AndroidProcessor;
12 |
13 | import javax.annotation.Nonnull;
14 | import javax.annotation.Nullable;
15 | import java.util.Map;
16 |
17 | /**
18 | * Render the children into a hyperlink.
19 | *
20 | * Please remember to use {@link android.widget.TextView#setMovementMethod(MovementMethod)}
21 | * with {@link android.text.method.LinkMovementMethod#}
22 | */
23 | public class HyperLinkRenderer extends BlockRenderer {
24 |
25 | /**
26 | * Create the processor preserving hyperlink renderer.
27 | *
28 | * @param processor the processor to be used for child rendering.
29 | */
30 | public HyperLinkRenderer(@Nonnull AndroidProcessor processor) {
31 | super(processor);
32 | }
33 |
34 | /**
35 | * Checks if the giben node is a hyperlink.
36 | *
37 | * @param context context this check should be performed in
38 | * @param node node to be checked
39 | * @return true if the given node is a hyperlink.
40 | */
41 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
42 | if (!(node instanceof CDARichHyperLink)) {
43 | return false;
44 | }
45 | Object data = ((CDARichHyperLink)node).getData();
46 | return data instanceof String || (data instanceof Map && ((Map, ?>) data).containsKey("uri"));
47 | }
48 |
49 | /**
50 | * Decorate the rendered children to either be using a clickable span.
51 | *
52 | * @param context the context this decoration should be applied to.
53 | * @param node the actual node, rendered into the builder.
54 | * @param builder a builder for generating the desired output.
55 | * @return the enhanced children, rendered as a hyperlink.
56 | */
57 | @NonNull @Override protected SpannableStringBuilder decorate(
58 | @Nonnull AndroidContext context,
59 | @Nonnull CDARichNode node,
60 | @Nonnull SpannableStringBuilder builder) {
61 |
62 | final CDARichHyperLink link = (CDARichHyperLink) node;
63 | final Object data = link.getData();
64 | final String uri;
65 |
66 | if (data instanceof String) {
67 | uri = (String) data;
68 | } else if (data instanceof Map) {
69 | uri = (String) ((Map, ?>) data).get("uri");
70 | } else {
71 | return builder; // Return unchanged if data is neither String nor Map
72 | }
73 |
74 | final URLSpan span = new URLSpan(uri);
75 | builder.setSpan(span, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
76 | return builder;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/QuoteRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import android.text.SpannableStringBuilder;
4 | import android.text.Spanned;
5 |
6 | import com.contentful.java.cda.rich.CDARichNode;
7 | import com.contentful.java.cda.rich.CDARichQuote;
8 | import com.contentful.rich.android.AndroidContext;
9 | import com.contentful.rich.android.AndroidProcessor;
10 | import com.contentful.rich.android.renderer.chars.span.QuoteSpan;
11 |
12 | import javax.annotation.Nonnull;
13 | import javax.annotation.Nullable;
14 |
15 | import androidx.annotation.NonNull;
16 |
17 | /**
18 | * Add a fancy bar next to the children, to indicate this text is a quote.
19 | */
20 | public class QuoteRenderer extends BlockRenderer {
21 |
22 | /**
23 | * Create a new quote renderer, saving its processor for child node processing.
24 | *
25 | * @param processor
26 | */
27 | public QuoteRenderer(@Nonnull AndroidProcessor processor) {
28 | super(processor);
29 | }
30 |
31 | /**
32 | * Is the given node a quote?
33 | *
34 | * @param context context this check should be performed in
35 | * @param node node to be checked
36 | * @return true or false. Depending on whether the node given is a CDARichQuote.
37 | */
38 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
39 | return node instanceof CDARichQuote;
40 | }
41 |
42 | /**
43 | * Decorate the given children by adding a start-aligned bar.
44 | *
45 | * @param context the context this decoration should be applied to.
46 | * @param node the actual node, rendered into the builder.
47 | * @param renderedChildren the rendered child nodes as a builder.
48 | * @return the builder, enhanced by vertical bar.
49 | */
50 | @NonNull @Override protected SpannableStringBuilder decorate(
51 | @Nonnull AndroidContext context,
52 | @Nonnull CDARichNode node,
53 | @Nonnull SpannableStringBuilder renderedChildren) {
54 |
55 | final QuoteSpan span = new QuoteSpan(0x80808080, 30, 20);
56 | renderedChildren.setSpan(span, 0, renderedChildren.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
57 | return renderedChildren;
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/TextRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars;
2 |
3 | import android.graphics.Typeface;
4 | import android.text.Spannable;
5 | import android.text.SpannableStringBuilder;
6 | import android.text.style.BackgroundColorSpan;
7 | import android.text.style.StyleSpan;
8 | import android.text.style.SubscriptSpan;
9 | import android.text.style.SuperscriptSpan;
10 | import android.text.style.TextAppearanceSpan;
11 | import android.text.style.UnderlineSpan;
12 | import com.contentful.java.cda.rich.CDARichMark;
13 | import com.contentful.java.cda.rich.CDARichMark.CDARichMarkBold;
14 | import com.contentful.java.cda.rich.CDARichMark.CDARichMarkItalic;
15 | import com.contentful.java.cda.rich.CDARichNode;
16 | import com.contentful.java.cda.rich.CDARichText;
17 | import com.contentful.rich.android.AndroidContext;
18 | import com.contentful.rich.android.AndroidProcessor;
19 | import com.contentful.rich.android.AndroidRenderer;
20 |
21 | import javax.annotation.Nonnull;
22 | import javax.annotation.Nullable;
23 |
24 | import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
25 | import static com.contentful.java.cda.rich.CDARichMark.CDARichMarkCode;
26 | import static com.contentful.java.cda.rich.CDARichMark.CDARichMarkCustom;
27 | import static com.contentful.java.cda.rich.CDARichMark.CDARichMarkUnderline;
28 |
29 | /**
30 | * This renderer will render a rich text node into a spannable, respecting it's marks.
31 | */
32 | public class TextRenderer extends AndroidRenderer {
33 | /**
34 | * Constructor taking a processor for child processing.
35 | *
36 | * Since CDARichText do not have children, the parameter will be ignored.
37 | *
38 | * @param processor used for subrendering of children.
39 | */
40 | public TextRenderer(@Nonnull AndroidProcessor processor) {
41 | super(processor);
42 | }
43 |
44 | /**
45 | * Is the incoming node a rich text?
46 | *
47 | * @param context context this check should be performed in
48 | * @param node node to be checked
49 | * @return true if the node is a rich text node.
50 | */
51 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
52 | return node instanceof CDARichText;
53 | }
54 |
55 | /**
56 | * Converts the incoming rich text into a string and adds spans according to its markers.
57 | *
58 | * @param context the generic context this node should be rendered in.
59 | * @param node the node to be rendered.
60 | * @return a spannable containing the text content of the rich text and decorations based on its markers.
61 | */
62 | @Nullable @Override public CharSequence render(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
63 | final CDARichText richText = (CDARichText) node;
64 |
65 | final Spannable result = new SpannableStringBuilder(richText.getText());
66 |
67 | for (final CDARichMark mark : richText.getMarks()) {
68 | if (mark instanceof CDARichMarkUnderline) {
69 | result.setSpan(new UnderlineSpan(), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
70 | }
71 | if (mark instanceof CDARichMarkBold) {
72 | result.setSpan(new StyleSpan(Typeface.BOLD), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
73 | }
74 | if (mark instanceof CDARichMark.CDARichMarkSubscript) {
75 | result.setSpan(new SubscriptSpan(), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
76 | }
77 | if (mark instanceof CDARichMark.CDARichMarkSuperscript) {
78 | result.setSpan(new SuperscriptSpan(), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
79 | }
80 | if (mark instanceof CDARichMarkItalic) {
81 | result.setSpan(new StyleSpan(Typeface.ITALIC), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
82 | }
83 | if (mark instanceof CDARichMarkCode) {
84 | result.setSpan(new TextAppearanceSpan("monospace", 0, 0, null, null), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
85 | }
86 | if (mark instanceof CDARichMarkCustom) {
87 | result.setSpan(new BackgroundColorSpan(0x80ffff00), 0, result.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
88 | }
89 | }
90 |
91 | return result;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/chars/span/QuoteSpan.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.chars.span;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 | import android.os.Parcel;
6 | import android.text.Layout;
7 | import android.text.ParcelableSpan;
8 | import android.text.style.LeadingMarginSpan;
9 |
10 | import androidx.annotation.ColorInt;
11 | import androidx.annotation.IntRange;
12 | import androidx.annotation.NonNull;
13 | import androidx.annotation.Px;
14 |
15 | /**
16 | * This is a custom span to decorate the children with a margin.
17 | */
18 | public class QuoteSpan implements LeadingMarginSpan, ParcelableSpan {
19 | @ColorInt
20 | private final int mColor;
21 | @Px
22 | private final int mStripeWidth;
23 | @Px
24 | private final int mGapWidth;
25 |
26 | /**
27 | * Creates a {@link android.text.style.QuoteSpan} based on a color, a stripe width and the width of the gap
28 | * between the stripe and the text.
29 | *
30 | * @param color the color of the quote stripe.
31 | * @param stripeWidth the width of the stripe.
32 | * @param gapWidth the width of the gap between the stripe and the text.
33 | */
34 | public QuoteSpan(@ColorInt int color, @IntRange(from = 0) int stripeWidth,
35 | @IntRange(from = 0) int gapWidth) {
36 | mColor = color;
37 | mStripeWidth = stripeWidth;
38 | mGapWidth = gapWidth;
39 | }
40 |
41 |
42 | @Override
43 | public int describeContents() {
44 | return 0;
45 | }
46 |
47 | @Override
48 | public void writeToParcel(Parcel dest, int flags) {
49 | writeToParcelInternal(dest, flags);
50 | }
51 |
52 | public void writeToParcelInternal(Parcel dest, int flags) {
53 | dest.writeInt(mColor);
54 | dest.writeInt(mStripeWidth);
55 | dest.writeInt(mGapWidth);
56 | }
57 |
58 | @Override
59 | public int getLeadingMargin(boolean first) {
60 | return mStripeWidth + mGapWidth;
61 | }
62 |
63 | @Override
64 | public void drawLeadingMargin(@NonNull Canvas c, @NonNull Paint p, int x, int dir,
65 | int top, int baseline, int bottom,
66 | @NonNull CharSequence text, int start, int end,
67 | boolean first, @NonNull Layout layout) {
68 | Paint.Style style = p.getStyle();
69 | int color = p.getColor();
70 |
71 | p.setStyle(Paint.Style.FILL);
72 | p.setColor(mColor);
73 |
74 | c.drawRect(x, top, x + dir * mStripeWidth, bottom, p);
75 |
76 | p.setStyle(style);
77 | p.setColor(color);
78 | }
79 |
80 | @Override public int getSpanTypeId() {
81 | return 1000;
82 | }
83 |
84 | public int getSpanTypeIdInternal() {
85 | return getSpanTypeId();
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/BulletDecorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import android.text.SpannableString;
4 | import android.text.Spanned;
5 |
6 | import javax.annotation.Nonnull;
7 |
8 | public class BulletDecorator extends Decorator{
9 | @Nonnull @Override public CharSequence getSymbol() {
10 | return "*";
11 | }
12 |
13 | public @Nonnull CharSequence decorate(int position) {
14 | final SpannableString spannable = new SpannableString("• ");
15 | spannable.setSpan(this, 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
16 | return spannable;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/Decorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import javax.annotation.Nonnull;
4 |
5 | public class Decorator {
6 | @Nonnull public CharSequence getSymbol() {
7 | return "";
8 | }
9 |
10 | @Nonnull public CharSequence decorate(int position) {
11 | return "";
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/LowerCaseCharacterDecorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import javax.annotation.Nonnull;
4 |
5 | public class LowerCaseCharacterDecorator extends UpperCaseCharacterDecorator {
6 | @Nonnull @Override public CharSequence getSymbol() {
7 | return "a";
8 | }
9 |
10 | public @Nonnull CharSequence decorate(int position) {
11 | return super.decorate(position).toString().toLowerCase();
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/LowerCaseRomanNumeralsDecorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import javax.annotation.Nonnull;
4 |
5 | public class LowerCaseRomanNumeralsDecorator extends UpperCaseRomanNumeralsDecorator {
6 | @Nonnull @Override public CharSequence getSymbol() {
7 | return "i";
8 | }
9 |
10 | @Nonnull @Override
11 | public CharSequence decorate(int position) {
12 | return super.decorate(position).toString().toLowerCase();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/NumbersDecorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import android.text.SpannableString;
4 | import android.text.Spanned;
5 |
6 | import javax.annotation.Nonnull;
7 |
8 | public class NumbersDecorator extends Decorator {
9 | @Nonnull @Override public CharSequence getSymbol() {
10 | return "1";
11 | }
12 |
13 | public @Nonnull CharSequence decorate(int position) {
14 | final SpannableString spannable = new SpannableString(position + ". ");
15 | spannable.setSpan(this, 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
16 | return spannable;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/UpperCaseCharacterDecorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import android.text.SpannableString;
4 | import android.text.Spanned;
5 |
6 | import javax.annotation.Nonnull;
7 |
8 | public class UpperCaseCharacterDecorator extends Decorator {
9 | @Nonnull @Override public CharSequence getSymbol() {
10 | return "A";
11 | }
12 |
13 | public @Nonnull CharSequence decorate(int position) {
14 | final SpannableString spannable = new SpannableString(getColumnDecoration(position - 1) + ". ");
15 | spannable.setSpan(this, 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
16 | return spannable;
17 | }
18 |
19 | private CharSequence getColumnDecoration(int index) {
20 | if (index < 26) {
21 | return Character.toString((char) ('A' + index));
22 | } else {
23 | return getColumnDecoration((index / 26) - 1).toString() + getColumnDecoration(index % 26);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/listdecorator/UpperCaseRomanNumeralsDecorator.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.listdecorator;
2 |
3 | import android.text.SpannableString;
4 | import android.text.Spanned;
5 |
6 | import java.util.TreeMap;
7 |
8 | import javax.annotation.Nonnull;
9 |
10 | public class UpperCaseRomanNumeralsDecorator extends Decorator {
11 | private final TreeMap map = new TreeMap<>();
12 |
13 | public UpperCaseRomanNumeralsDecorator() {
14 | map.put(1000, "M");
15 | map.put(900, "CM");
16 | map.put(500, "D");
17 | map.put(400, "CD");
18 | map.put(100, "C");
19 | map.put(90, "XC");
20 | map.put(50, "L");
21 | map.put(40, "XL");
22 | map.put(10, "X");
23 | map.put(9, "IX");
24 | map.put(5, "V");
25 | map.put(4, "IV");
26 | map.put(3, "III");
27 | map.put(2, "II");
28 | map.put(1, "I");
29 | }
30 |
31 | @Nonnull @Override public CharSequence getSymbol() {
32 | return "I";
33 | }
34 |
35 | @Nonnull @Override
36 | public CharSequence decorate(int position) {
37 | final SpannableString spannable = new SpannableString(getRomanDecoration(position) + ". ");
38 | spannable.setSpan(this, 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
39 | return spannable;
40 | }
41 |
42 | public CharSequence getRomanDecoration(int index) {
43 | try {
44 | int l = map.floorKey(index);
45 | if (index == l) {
46 | return map.get(index);
47 | }
48 | return map.get(l) + getRomanDecoration(index - l);
49 | } catch (NullPointerException e) {
50 | return " ";
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/views/BlockRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.views;
2 |
3 | import android.text.SpannableStringBuilder;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.TextView;
7 | import android.widget.LinearLayout;
8 |
9 | import com.contentful.java.cda.rich.CDARichBlock;
10 | import com.contentful.java.cda.rich.CDARichNode;
11 | import com.contentful.rich.android.AndroidContext;
12 | import com.contentful.rich.android.AndroidProcessor;
13 | import com.contentful.rich.android.AndroidRenderer;
14 | import com.contentful.rich.android.R;
15 |
16 | import javax.annotation.Nonnull;
17 | import javax.annotation.Nullable;
18 |
19 | public class BlockRenderer extends AndroidRenderer {
20 | public BlockRenderer(@Nonnull AndroidProcessor processor) {
21 | super(processor);
22 | }
23 |
24 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
25 | return node instanceof CDARichBlock;
26 | }
27 |
28 | @Nullable @Override public View render(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
29 | final CDARichBlock block = (CDARichBlock) node;
30 | final View result = inflateRichLayout(context, node);
31 | final ViewGroup content = result.findViewById(R.id.rich_content);
32 |
33 | TextView lastTextView = null;
34 | for (final CDARichNode childNode : block.getContent()) {
35 | final View childView = processor.process(context, childNode);
36 |
37 | if (childView != null) {
38 | if (childView instanceof TextView) {
39 | final TextView childTextView = (TextView) childView;
40 | if (lastTextView != null) {
41 | CharSequence lastText = lastTextView.getText();
42 | CharSequence childText = childTextView.getText();
43 | if (lastText == null) {
44 | lastText = "";
45 | }
46 | if (childText == null) {
47 | childText = "";
48 | }
49 | lastTextView.setText(new SpannableStringBuilder(lastText).append(childText));
50 | if(childTextView.getMovementMethod() != null) {
51 | lastTextView.setMovementMethod(childTextView.getMovementMethod());
52 | }
53 | } else {
54 | lastTextView = childTextView;
55 | content.addView(childView);
56 | }
57 | } else {
58 | if (context.getPath() != null && context.getPath().size() > 1) {
59 | final View indented = context.getInflater().inflate(R.layout.rich_indention_layout, null, false);
60 | ((ViewGroup) indented.findViewById(R.id.rich_content)).addView(childView);
61 | content.addView(indented);
62 | } else {
63 | // Add margin to the child view
64 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
65 | LinearLayout.LayoutParams.MATCH_PARENT,
66 | LinearLayout.LayoutParams.WRAP_CONTENT
67 | );
68 | params.topMargin = 16; // Add 16dp margin at the top
69 | childView.setLayoutParams(params);
70 | content.addView(childView);
71 | }
72 | }
73 | }
74 | }
75 |
76 | return result;
77 | }
78 |
79 | protected View inflateRichLayout(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
80 | return context.getInflater().inflate(R.layout.rich_block_layout, null, false);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/views/EmbeddedLinkRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.views;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 | import android.widget.ImageView;
6 | import android.widget.TextView;
7 |
8 | import com.contentful.java.cda.CDAAsset;
9 | import com.contentful.java.cda.CDAEntry;
10 | import com.contentful.java.cda.rich.CDARichEmbeddedInline;
11 | import com.contentful.java.cda.rich.CDARichNode;
12 | import com.contentful.rich.android.AndroidContext;
13 | import com.contentful.rich.android.AndroidProcessor;
14 | import com.contentful.rich.android.R;
15 | import com.contentful.rich.android.renderer.chars.EmbeddedLinkRenderer.BitmapProvider;
16 |
17 | import javax.annotation.Nonnull;
18 | import javax.annotation.Nullable;
19 |
20 | import static com.contentful.rich.android.renderer.chars.EmbeddedLinkRenderer.defaultBitmapProvider;
21 |
22 | public class EmbeddedLinkRenderer extends BlockRenderer {
23 | private final BitmapProvider provider;
24 |
25 | public EmbeddedLinkRenderer(@Nonnull AndroidProcessor processor) {
26 | this(processor, defaultBitmapProvider);
27 | }
28 |
29 | public EmbeddedLinkRenderer(@Nonnull AndroidProcessor processor, BitmapProvider provider) {
30 | super(processor);
31 | this.provider = provider;
32 | }
33 |
34 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
35 | return node instanceof CDARichEmbeddedInline;
36 | }
37 |
38 | @Override protected View inflateRichLayout(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
39 | final View embedded = context.getInflater().inflate(R.layout.rich_embedded_layout, null, false);
40 | final ViewGroup content = embedded.findViewById(R.id.rich_content);
41 |
42 | final CDARichEmbeddedInline link = (CDARichEmbeddedInline) node;
43 | final Object data = link.getData();
44 |
45 | final View toBeEmbedded;
46 | if (data instanceof CDAEntry) {
47 | final CDAEntry entry = (CDAEntry) data;
48 |
49 | final TextView textView = new TextView(context.getAndroidContext());
50 | textView.setText(entry.getField("title"));
51 |
52 | toBeEmbedded = textView;
53 | } else if (data instanceof CDAAsset) {
54 | final CDAAsset asset = (CDAAsset) data;
55 | final ImageView image = new ImageView(context.getAndroidContext());
56 | image.setImageBitmap(provider.provide(context.getAndroidContext(), asset));
57 |
58 | toBeEmbedded = image;
59 | } else {
60 | final TextView textView = new TextView(context.getAndroidContext());
61 | textView.setText("⚠️");
62 |
63 | toBeEmbedded = textView;
64 | }
65 |
66 | content.addView(toBeEmbedded, 0);
67 | return embedded;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/views/HorizontalRuleRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.views;
2 |
3 | import android.view.View;
4 |
5 | import com.contentful.java.cda.rich.CDARichHorizontalRule;
6 | import com.contentful.java.cda.rich.CDARichNode;
7 | import com.contentful.rich.android.AndroidContext;
8 | import com.contentful.rich.android.AndroidProcessor;
9 | import com.contentful.rich.android.AndroidRenderer;
10 | import com.contentful.rich.android.R;
11 |
12 | import javax.annotation.Nonnull;
13 | import javax.annotation.Nullable;
14 |
15 | /**
16 | * This renderer will render a rich text node into a TextView, respecting it's marks.
17 | */
18 | public class HorizontalRuleRenderer extends AndroidRenderer {
19 | /**
20 | * Constructor taking a processor for child processing.
21 | *
22 | * Since CDARichText do not have children, the parameter will be ignored.
23 | *
24 | * @param processor used for subrendering of children.
25 | */
26 | public HorizontalRuleRenderer(@Nonnull AndroidProcessor processor) {
27 | super(processor);
28 | }
29 |
30 | /**
31 | * Is the incoming node a horizontal rule?
32 | *
33 | * @param context context this check should be performed in
34 | * @param node node to be checked
35 | * @return true if the node is a rich rule node.
36 | */
37 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
38 | return node instanceof CDARichHorizontalRule;
39 | }
40 |
41 | /**
42 | * Creates a horizontal line.
43 | *
44 | * @param context the generic context this node should be rendered in.
45 | * @param node the node to be rendered.
46 | * @return a view representing a horizontal line.
47 | */
48 | @Nullable @Override public View render(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
49 | return context.getInflater().inflate(R.layout.rich_horizontal_rule_layout, null);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/views/ListRenderer.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.views;
2 |
3 | import android.text.SpannableStringBuilder;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.TextView;
7 | import com.contentful.java.cda.rich.CDARichBlock;
8 | import com.contentful.java.cda.rich.CDARichList;
9 | import com.contentful.java.cda.rich.CDARichListItem;
10 | import com.contentful.java.cda.rich.CDARichNode;
11 | import com.contentful.rich.android.AndroidContext;
12 | import com.contentful.rich.android.AndroidProcessor;
13 | import com.contentful.rich.android.R;
14 | import com.contentful.rich.android.renderer.listdecorator.Decorator;
15 |
16 | import javax.annotation.Nonnull;
17 | import javax.annotation.Nullable;
18 | import java.util.ArrayList;
19 | import java.util.Arrays;
20 | import java.util.HashMap;
21 | import java.util.List;
22 | import java.util.Map;
23 |
24 | public class ListRenderer extends BlockRenderer {
25 |
26 | private final Map decoratorBySymbolMap = new HashMap<>();
27 | private final List decorators = new ArrayList<>();
28 |
29 | public ListRenderer(@Nonnull AndroidProcessor processor, @Nonnull Decorator... decorators) {
30 | super(processor);
31 |
32 | this.decorators.addAll(Arrays.asList(decorators));
33 |
34 | for (final Decorator decorator : decorators) {
35 | this.decoratorBySymbolMap.put(decorator.getSymbol().toString(), decorator);
36 | }
37 | }
38 |
39 | @Override public boolean canRender(@Nullable AndroidContext context, @Nonnull CDARichNode node) {
40 | if (context != null && node instanceof CDARichListItem) {
41 | final CDARichList list = context.getTopListOfPath();
42 | if (list != null) {
43 | return decoratorBySymbolMap.containsKey(list.getDecoration().toString());
44 | }
45 | }
46 |
47 | return false;
48 | }
49 |
50 | @Nullable @Override public View render(@Nonnull AndroidContext context, @Nonnull CDARichNode node) {
51 | final CDARichBlock block = (CDARichBlock) node;
52 | final ViewGroup result = (ViewGroup) context.getInflater().inflate(R.layout.rich_list_layout, null, false);
53 | provideDecoration(context, result, node);
54 |
55 | final ViewGroup content = result.findViewById(R.id.rich_content);
56 |
57 | TextView lastTextView = null;
58 | for (final CDARichNode childNode : block.getContent()) {
59 | final View childView = processor.process(context, childNode);
60 |
61 | if (childView != null) {
62 | if (childView instanceof TextView) {
63 | final TextView childTextView = (TextView) childView;
64 | if (lastTextView != null) {
65 | lastTextView.setText(
66 | new SpannableStringBuilder(lastTextView.getText()).append(childTextView.getText())
67 | );
68 | if(childTextView.getMovementMethod() != null) {
69 | lastTextView.setMovementMethod(childTextView.getMovementMethod());
70 | }
71 | } else {
72 | lastTextView = childTextView;
73 | content.addView(childView);
74 | }
75 | } else {
76 | content.addView(childView);
77 | }
78 | }
79 | }
80 |
81 | return result;
82 | }
83 |
84 | protected void provideDecoration(@Nonnull AndroidContext context, @Nonnull ViewGroup group, @Nonnull CDARichNode node) {
85 | final TextView decoration = group.findViewById(R.id.rich_list_decoration);
86 |
87 | final List path = context.getPath();
88 | CDARichList list = context.getTopListOfPath();
89 | final Decorator currentDecorator;
90 | final int childIndex;
91 | if (list == null) {
92 | list = (CDARichList) node;
93 | childIndex = 0;
94 | currentDecorator = decoratorBySymbolMap.get(list.getDecoration());
95 | } else {
96 | final int listIndex = path.indexOf(list);
97 | final int listItemIndexOnPath = listIndex + 1;
98 | childIndex = list.getContent().indexOf(path.get(listItemIndexOnPath));
99 |
100 | final int nestedListCount = (int) (getListOfTypeCount(context, list)) % Integer.MAX_VALUE;
101 |
102 | final Decorator initialDecorator = decoratorBySymbolMap.get(list.getDecoration().toString());
103 | final int initialDecoratorIndex = decorators.indexOf(initialDecorator);
104 | int currentPosition = ((initialDecoratorIndex + nestedListCount) % decorators.size()) - 1;
105 | if(currentPosition < 0) {
106 | currentPosition = 0;
107 | }
108 |
109 | currentDecorator = decorators.get(currentPosition);
110 | }
111 |
112 |
113 | decoration.setText(currentDecorator.decorate(childIndex + 1));
114 | }
115 |
116 | /**
117 | * Count lists on the path.
118 | *
119 | * @param context where is the path stored in? The context!
120 | * @param list the list to be listed.
121 | * @return the number of lists of the supported type.
122 | */
123 | private long getListOfTypeCount(@Nonnull AndroidContext context, CDARichList list) {
124 | if (context.getPath() == null) {
125 | return 0;
126 | }
127 | int count = 0;
128 | for (CDARichNode node: context.getPath()) {
129 | if (node instanceof CDARichList && ((CDARichList) node).getDecoration().equals(list.getDecoration())) {
130 | count++;
131 | }
132 | }
133 | return count;
134 | }
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/android/src/main/java/com/contentful/rich/android/renderer/views/NativeViewsRendererProvider.java:
--------------------------------------------------------------------------------
1 | package com.contentful.rich.android.renderer.views;
2 |
3 | import android.view.View;
4 |
5 | import com.contentful.rich.android.AndroidProcessor;
6 | import com.contentful.rich.android.renderer.listdecorator.BulletDecorator;
7 | import com.contentful.rich.android.renderer.listdecorator.LowerCaseCharacterDecorator;
8 | import com.contentful.rich.android.renderer.listdecorator.LowerCaseRomanNumeralsDecorator;
9 | import com.contentful.rich.android.renderer.listdecorator.NumbersDecorator;
10 | import com.contentful.rich.android.renderer.listdecorator.UpperCaseCharacterDecorator;
11 | import com.contentful.rich.android.renderer.listdecorator.UpperCaseRomanNumeralsDecorator;
12 |
13 | import javax.annotation.Nonnull;
14 |
15 | /**
16 | * Provider for all native renderers.
17 | */
18 | public class NativeViewsRendererProvider {
19 | /**
20 | * Fill up the given processor with the default renderers supporting native views.
21 | *
22 | * @param processor the processor to be augmented.
23 | */
24 | public void provide(@Nonnull AndroidProcessor processor) {
25 | processor.addRenderer(new TextRenderer(processor));
26 | processor.addRenderer(new HorizontalRuleRenderer(processor));
27 | processor.addRenderer(new ListRenderer(processor, new BulletDecorator()));
28 | processor.addRenderer(new ListRenderer(processor,
29 | new NumbersDecorator(),
30 | new UpperCaseCharacterDecorator(),
31 | new LowerCaseRomanNumeralsDecorator(),
32 | new LowerCaseCharacterDecorator(),
33 | new LowerCaseCharacterDecorator(),
34 | new UpperCaseRomanNumeralsDecorator()
35 | ));
36 | processor.addRenderer(new HyperLinkRenderer(processor));
37 | processor.addRenderer(new QuoteRenderer(processor));
38 | processor.addRenderer(new TableRenderer(processor));
39 | processor.addRenderer(new BlockRenderer(processor));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable-night/table_header_cell_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 | -
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/quote_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
14 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/table_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/table_cell_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/table_divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/table_header_cell_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 | -
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_block_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_embedded_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
28 |
29 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_horizontal_rule_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_hyperlink_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
27 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_indention_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_list_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
28 |
29 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_quote_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_table_cell_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
23 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_table_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/rich_text_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/android/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/src/test/java/chars/AllTheThingsTest.java:
--------------------------------------------------------------------------------
1 | package chars;
2 |
3 | import android.app.Activity;
4 |
5 | import com.contentful.java.cda.rich.CDARichDocument;
6 | import com.contentful.java.cda.rich.CDARichEmbeddedBlock;
7 | import com.contentful.java.cda.rich.CDARichHeading;
8 | import com.contentful.java.cda.rich.CDARichHorizontalRule;
9 | import com.contentful.java.cda.rich.CDARichHyperLink;
10 | import com.contentful.java.cda.rich.CDARichListItem;
11 | import com.contentful.java.cda.rich.CDARichMark;
12 | import com.contentful.java.cda.rich.CDARichNode;
13 | import com.contentful.java.cda.rich.CDARichOrderedList;
14 | import com.contentful.java.cda.rich.CDARichParagraph;
15 | import com.contentful.java.cda.rich.CDARichQuote;
16 | import com.contentful.java.cda.rich.CDARichText;
17 | import com.contentful.java.cda.rich.CDARichUnorderedList;
18 | import com.contentful.rich.android.AndroidContext;
19 | import com.contentful.rich.android.AndroidProcessor;
20 |
21 | import org.junit.Before;
22 | import org.junit.Test;
23 | import org.junit.runner.RunWith;
24 | import org.robolectric.Robolectric;
25 | import org.robolectric.RobolectricTestRunner;
26 |
27 | import java.util.ArrayList;
28 |
29 | import static com.google.common.collect.Lists.newArrayList;
30 | import static com.google.common.truth.Truth.assertThat;
31 | import static lib.ContentfulCreator.mockCDAEntry;
32 |
33 | @RunWith(RobolectricTestRunner.class)
34 | public class AllTheThingsTest {
35 | private Activity activity;
36 |
37 | @Before
38 | public void setup() {
39 | activity = Robolectric.setupActivity(Activity.class);
40 | }
41 |
42 | @Test
43 | public void allTheNodes() {
44 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
45 | final AndroidContext context = new AndroidContext(activity);
46 |
47 | final CharSequence result = processor.process(context, createAllNode());
48 |
49 | assertThat(result.toString()).isEqualTo("" +
50 | "heading - 1" +
51 | "heading - 2" +
52 | "heading - 3" +
53 | "heading - 4" +
54 | "heading - 5" +
55 | "heading - 6" +
56 | " Hyper hyperALL THE TEXT MARKS!" +
57 | "1. some list item content\n" +
58 | "1. some list item content\n" +
59 | "1. some list item content• some list item content\n" +
60 | "• some list item content\n" +
61 | "• some list item content\n" +
62 | "• some list item contentParagraphFamous quote");
63 | }
64 |
65 | private CDARichNode createAllNode() {
66 | final CDARichDocument result = new CDARichDocument();
67 |
68 | for (int i = 1; i < 7; ++i) {
69 | final CDARichHeading heading = new CDARichHeading(i);
70 | heading.getContent().add(new CDARichText("heading - " + i, new ArrayList<>()));
71 | result.getContent().add(heading);
72 | }
73 |
74 | final CDARichHorizontalRule horizontalRule = new CDARichHorizontalRule();
75 | result.getContent().add(horizontalRule);
76 |
77 | final CDARichEmbeddedBlock embeddedLink = new CDARichEmbeddedBlock(mockCDAEntry());
78 | result.getContent().add(embeddedLink);
79 |
80 | final CDARichHyperLink hyperLink = new CDARichHyperLink("https://contentful.com/");
81 | hyperLink.getContent().add(new CDARichText("Hyper hyper", new ArrayList<>()));
82 | result.getContent().add(hyperLink);
83 |
84 | final CDARichText allTheMarks = new CDARichText(
85 | "ALL THE TEXT MARKS!",
86 | newArrayList(
87 | new CDARichMark.CDARichMarkUnderline(),
88 | new CDARichMark.CDARichMarkBold(),
89 | new CDARichMark.CDARichMarkItalic(),
90 | new CDARichMark.CDARichMarkCode(),
91 | new CDARichMark.CDARichMarkCustom("top")
92 | )
93 | );
94 | result.getContent().add(allTheMarks);
95 |
96 | final CDARichOrderedList orderedList = new CDARichOrderedList();
97 | final CDARichListItem listItem = new CDARichListItem();
98 | listItem.getContent().add(new CDARichText("some list item content", new ArrayList<>()));
99 | orderedList.getContent().add(listItem);
100 | orderedList.getContent().add(listItem);
101 | orderedList.getContent().add(listItem);
102 | result.getContent().add(orderedList);
103 |
104 | final CDARichUnorderedList unorderedList = new CDARichUnorderedList();
105 | unorderedList.getContent().add(listItem);
106 | unorderedList.getContent().add(listItem);
107 | unorderedList.getContent().add(listItem);
108 | unorderedList.getContent().add(listItem);
109 | result.getContent().add(unorderedList);
110 |
111 | final CDARichParagraph paragraph = new CDARichParagraph();
112 | paragraph.getContent().add(new CDARichText("Paragraph", new ArrayList<>()));
113 | result.getContent().add(paragraph);
114 |
115 | final CDARichQuote quote = new CDARichQuote();
116 | quote.getContent().add(new CDARichText("Famous quote", new ArrayList<>()));
117 | result.getContent().add(quote);
118 |
119 | final CDARichNode node = new CDARichNode();
120 | result.getContent().add(node);
121 |
122 | return result;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/android/src/test/java/chars/HeadingTest.java:
--------------------------------------------------------------------------------
1 | package chars;
2 |
3 | import android.app.Activity;
4 | import android.text.Spannable;
5 | import android.text.style.AbsoluteSizeSpan;
6 |
7 | import com.contentful.java.cda.rich.CDARichHeading;
8 | import com.contentful.java.cda.rich.CDARichText;
9 | import com.contentful.rich.android.AndroidContext;
10 | import com.contentful.rich.android.AndroidProcessor;
11 |
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.junit.runner.RunWith;
15 | import org.robolectric.Robolectric;
16 | import org.robolectric.RobolectricTestRunner;
17 |
18 | import java.util.ArrayList;
19 |
20 | import static com.google.common.truth.Truth.assertThat;
21 |
22 | @RunWith(RobolectricTestRunner.class)
23 | public class HeadingTest {
24 | private Activity activity;
25 |
26 | @Before
27 | public void setup() {
28 | activity = Robolectric.setupActivity(Activity.class);
29 | }
30 |
31 | @Test
32 | public void firstHeadingParsesContentTest() {
33 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
34 | final AndroidContext context = new AndroidContext(activity);
35 |
36 | final CDARichHeading heading = new CDARichHeading(1);
37 | heading.getContent().add(new CDARichText("heading 1", new ArrayList<>()));
38 |
39 | final CharSequence result = processor.process(context, heading);
40 |
41 | assertThat(result.toString()).isEqualTo("heading 1");
42 |
43 | assertThat(result).isInstanceOf(Spannable.class);
44 | final Spannable spannable = (Spannable) result;
45 |
46 | final Object[] spans = spannable.getSpans(0, result.length(), Object.class);
47 | assertThat(spans.length).isEqualTo(1);
48 | assertThat(spans[0]).isInstanceOf(AbsoluteSizeSpan.class);
49 | assertThat(((AbsoluteSizeSpan) spans[0]).getSize()).isEqualTo(60);
50 | }
51 |
52 | @Test
53 | public void notSupportedHeadingWillRenderDefaultText() {
54 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
55 | final AndroidContext context = new AndroidContext(activity);
56 |
57 | final CDARichHeading heading = new CDARichHeading(-1);
58 | heading.getContent().add(new CDARichText("illegal", new ArrayList<>()));
59 |
60 | final CharSequence result = processor.process(context, heading);
61 |
62 | assertThat(((Spannable) result).getSpans(0, result.length(), Object.class)).hasLength(0);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/android/src/test/java/chars/LinkTest.java:
--------------------------------------------------------------------------------
1 | package chars;
2 |
3 | import android.app.Activity;
4 | import android.text.Spannable;
5 | import android.text.style.ForegroundColorSpan;
6 | import android.text.style.URLSpan;
7 |
8 | import com.contentful.java.cda.rich.CDARichEmbeddedBlock;
9 | import com.contentful.java.cda.rich.CDARichHyperLink;
10 | import com.contentful.java.cda.rich.CDARichText;
11 | import com.contentful.rich.android.AndroidContext;
12 | import com.contentful.rich.android.AndroidProcessor;
13 |
14 | import org.junit.Before;
15 | import org.junit.Test;
16 | import org.junit.runner.RunWith;
17 | import org.robolectric.Robolectric;
18 | import org.robolectric.RobolectricTestRunner;
19 |
20 | import java.util.ArrayList;
21 |
22 | import lib.ContentfulCreator;
23 |
24 | import static com.google.common.truth.Truth.assertThat;
25 |
26 | @RunWith(RobolectricTestRunner.class)
27 | public class LinkTest {
28 | private Activity activity;
29 |
30 | @Before
31 | public void setup() {
32 | activity = Robolectric.setupActivity(Activity.class);
33 | }
34 |
35 | @Test
36 | public void hyperlinkTest() {
37 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
38 | final AndroidContext context = new AndroidContext(activity);
39 |
40 | final CDARichHyperLink link = new CDARichHyperLink("https://contentful.com");
41 | link.getContent().add(new CDARichText("Some link text", new ArrayList<>()));
42 |
43 | final CharSequence result = processor.process(context, link);
44 |
45 | assertThat(result).isInstanceOf(Spannable.class);
46 | assertThat(result.toString()).isEqualTo("Some link text");
47 |
48 | assertThat(((Spannable) result).getSpans(0, result.length(), Object.class)).hasLength(1);
49 | assertThat(((Spannable) result).getSpans(0, result.length(), Object.class)[0]).isInstanceOf(URLSpan.class);
50 |
51 | final URLSpan span = (URLSpan) ((Spannable) result).getSpans(0, result.length(), Object.class)[0];
52 | assertThat(span.getURL()).isEqualTo("https://contentful.com");
53 | }
54 |
55 | @Test
56 | public void createUnsanitzedStrings() {
57 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
58 | final AndroidContext context = new AndroidContext(activity);
59 |
60 | final CDARichHyperLink link = new CDARichHyperLink("https://contentful.com");
61 | link.getContent().add(new CDARichText("Some link text", new ArrayList<>()));
62 |
63 | final CharSequence result = processor.process(context, link);
64 |
65 | assertThat(result).isInstanceOf(Spannable.class);
66 | assertThat(result.toString()).isEqualTo("Some link text");
67 |
68 | final Object[] spans = ((Spannable) result).getSpans(0, result.length(), Object.class);
69 | assertThat(spans).hasLength(1);
70 | assertThat(spans[0]).isInstanceOf(URLSpan.class);
71 |
72 | final URLSpan span = (URLSpan) ((Spannable) result).getSpans(0, result.length(), Object.class)[0];
73 | assertThat(span.getURL()).isEqualTo("https://contentful.com");
74 | }
75 |
76 | @Test
77 | public void createEmbeddedLink() {
78 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
79 | final AndroidContext context = new AndroidContext(activity);
80 |
81 | final CDARichHyperLink link = new CDARichEmbeddedBlock(ContentfulCreator.mockCDAEntry());
82 | link.getContent().add(new CDARichText("My embedded entry", new ArrayList<>()));
83 |
84 | final CharSequence result = processor.process(context, link);
85 |
86 | assertThat(result).isInstanceOf(Spannable.class);
87 | assertThat(result.toString()).isEqualTo("My embedded entry");
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/android/src/test/java/chars/QuoteTest.java:
--------------------------------------------------------------------------------
1 | package chars;
2 |
3 | import android.app.Activity;
4 | import android.text.Spannable;
5 |
6 | import com.contentful.java.cda.rich.CDARichQuote;
7 | import com.contentful.java.cda.rich.CDARichText;
8 | import com.contentful.rich.android.AndroidContext;
9 | import com.contentful.rich.android.AndroidProcessor;
10 |
11 | import org.junit.Before;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 | import org.robolectric.Robolectric;
15 | import org.robolectric.RobolectricTestRunner;
16 |
17 | import java.util.ArrayList;
18 |
19 | import static com.google.common.truth.Truth.assertThat;
20 |
21 | @RunWith(RobolectricTestRunner.class)
22 | public class QuoteTest {
23 | private Activity activity;
24 |
25 | @Before
26 | public void setup() {
27 | activity = Robolectric.setupActivity(Activity.class);
28 | }
29 |
30 | @Test
31 | public void quoteTest() {
32 | final AndroidProcessor processor = AndroidProcessor.creatingCharSequences();
33 | final AndroidContext context = new AndroidContext(activity);
34 | final CDARichQuote quote = new CDARichQuote();
35 |
36 | quote.getContent().add(new CDARichText("Edel sei der Mensch,\n" +
37 | "Hilfreich und gut! — Johann Wolfgang von Goethe", new ArrayList<>()));
38 |
39 | final CharSequence result = processor.process(context, quote);
40 |
41 | assertThat(result.toString()).isEqualTo("Edel sei der Mensch,\n" +
42 | "Hilfreich und gut! — Johann Wolfgang von Goethe");
43 | assertThat(((Spannable) result).getSpans(0, result.length(), Object.class)).hasLength(1);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/android/src/test/java/lib/ContentfulCreator.java:
--------------------------------------------------------------------------------
1 | package lib;
2 |
3 | import com.contentful.java.cda.CDAEntry;
4 | import com.contentful.java.cda.CDAResource;
5 | import com.contentful.java.cda.LocalizedResource;
6 |
7 | import java.lang.reflect.Field;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | import static com.google.common.collect.Maps.newHashMap;
12 |
13 | public class ContentfulCreator {
14 | public static CDAResource mockCDAEntry() {
15 | final CDAEntry entry = new CDAEntry();
16 | Field attrs = null;
17 | for (final Field field : CDAResource.class.getDeclaredFields()) {
18 | if ("attrs".equals(field.getName())) {
19 | attrs = field;
20 | }
21 | }
22 |
23 | if (attrs != null) {
24 | attrs.setAccessible(true);
25 | try {
26 | Map map = newHashMap();
27 | map.put("title", newHashMap());
28 | attrs.set(entry, map);
29 | } catch (IllegalAccessException e) {
30 | }
31 | attrs.setAccessible(false);
32 | }
33 |
34 | Field fields = null;
35 | Field defaultLocale = null;
36 | for (final Field field : LocalizedResource.class.getDeclaredFields()) {
37 | if ("fields".equals(field.getName())) {
38 | fields = field;
39 | } else if ("defaultLocale".equals(field.getName())) {
40 | defaultLocale = field;
41 | }
42 | }
43 |
44 | if (fields != null) {
45 | fields.setAccessible(true);
46 |
47 | try {
48 | final HashMap