├── .github └── CODEOWNERS ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── android ├── .gitignore ├── README.md ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── contentful │ │ │ └── rich │ │ │ └── android │ │ │ ├── AndroidContext.java │ │ │ ├── AndroidProcessor.java │ │ │ ├── AndroidRenderer.java │ │ │ └── renderer │ │ │ ├── chars │ │ │ ├── BlockRenderer.java │ │ │ ├── CharSequenceRendererProvider.java │ │ │ ├── EmbeddedLinkRenderer.java │ │ │ ├── HeadingRenderer.java │ │ │ ├── HorizontalRuleRenderer.java │ │ │ ├── HyperLinkRenderer.java │ │ │ ├── ListRenderer.java │ │ │ ├── QuoteRenderer.java │ │ │ ├── TextRenderer.java │ │ │ └── span │ │ │ │ └── QuoteSpan.java │ │ │ ├── listdecorator │ │ │ ├── BulletDecorator.java │ │ │ ├── Decorator.java │ │ │ ├── LowerCaseCharacterDecorator.java │ │ │ ├── LowerCaseRomanNumeralsDecorator.java │ │ │ ├── NumbersDecorator.java │ │ │ ├── UpperCaseCharacterDecorator.java │ │ │ └── UpperCaseRomanNumeralsDecorator.java │ │ │ └── views │ │ │ ├── BlockRenderer.java │ │ │ ├── EmbeddedLinkRenderer.java │ │ │ ├── HorizontalRuleRenderer.java │ │ │ ├── HyperLinkRenderer.java │ │ │ ├── ListRenderer.java │ │ │ ├── NativeViewsRendererProvider.java │ │ │ ├── QuoteRenderer.java │ │ │ ├── TableRenderer.java │ │ │ └── TextRenderer.java │ └── res │ │ ├── drawable-night │ │ └── table_header_cell_border.xml │ │ ├── drawable │ │ ├── quote_background.xml │ │ ├── table_border.xml │ │ ├── table_cell_border.xml │ │ ├── table_divider.xml │ │ └── table_header_cell_border.xml │ │ ├── layout │ │ ├── rich_block_layout.xml │ │ ├── rich_embedded_layout.xml │ │ ├── rich_horizontal_rule_layout.xml │ │ ├── rich_hyperlink_layout.xml │ │ ├── rich_indention_layout.xml │ │ ├── rich_list_layout.xml │ │ ├── rich_quote_layout.xml │ │ ├── rich_table_cell_layout.xml │ │ ├── rich_table_layout.xml │ │ └── rich_text_layout.xml │ │ └── values │ │ └── ids.xml │ └── test │ └── java │ ├── chars │ ├── AllTheThingsTest.java │ ├── HeadingTest.java │ ├── LinkTest.java │ ├── ListDecoratorTest.java │ ├── ListTest.java │ ├── MarksTextTest.java │ └── QuoteTest.java │ ├── lib │ └── ContentfulCreator.java │ └── views │ ├── AllTheThingsTest.java │ ├── HeadingTest.java │ ├── LinkTest.java │ ├── ListTest.java │ ├── MarksTextTest.java │ └── QuoteTest.java ├── android_sample ├── .gitignore ├── README.md ├── build.gradle ├── proguard-rules.pro ├── screenshots │ ├── headings.png │ ├── links.png │ ├── lists.png │ ├── native-headings.png │ ├── native-links.png │ ├── native-lists.png │ ├── native-quotes.png │ ├── native-textmarks.png │ ├── quotes.png │ └── textmarks.png ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── contentful │ │ └── rich │ │ └── android │ │ └── sample │ │ ├── MainActivity.kt │ │ ├── Page.kt │ │ ├── PageAdapter.kt │ │ ├── PageFragment.kt │ │ ├── RichCharSequenceAdapter.kt │ │ └── RichNativeViewAdapter.kt │ └── res │ ├── layout │ ├── activity_main.xml │ ├── appbar_item.xml │ ├── dropdown_item.xml │ ├── fragment_page.xml │ └── sample_item.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── values-night │ ├── colors.xml │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── catalog-info.yaml ├── core ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── contentful │ │ └── rich │ │ └── core │ │ ├── Context.java │ │ ├── Processor.java │ │ ├── RenderabilityChecker.java │ │ ├── Renderer.java │ │ └── simple │ │ ├── RemoveEmpties.java │ │ ├── RemoveToDeepNesting.java │ │ ├── Simplification.java │ │ └── Simplifier.java │ └── test │ └── java │ ├── ComplexProcessorTest.java │ ├── ProcessorTest.java │ ├── RendererOrderTest.java │ ├── SimplifierTest.java │ └── lib │ ├── ContentfulCreator.java │ └── DescendingTextRendererProvider.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── html ├── README.md ├── build.gradle ├── out │ └── production │ │ └── classes │ │ └── META-INF │ │ └── html_main.kotlin_module └── src │ ├── main │ └── java │ │ └── com │ │ └── contentful │ │ └── rich │ │ └── html │ │ ├── HtmlContext.java │ │ ├── HtmlProcessor.java │ │ ├── HtmlRendererProvider.java │ │ └── renderer │ │ ├── DynamicTagRenderer.java │ │ ├── TagRenderer.java │ │ ├── TagWithArgumentsRenderer.java │ │ └── TextRenderer.java │ └── test │ └── java │ ├── AllTheThingsTest.java │ ├── CustomRendererTest.java │ ├── EncodedRichTextTest.java │ ├── HeadingTest.java │ ├── LinksTest.java │ ├── ListsTest.java │ ├── MarksTextTest.java │ ├── e2e │ ├── E2EResources.java │ └── E2ETest.java │ └── lib │ └── ContentfulCreator.java ├── jitpack.yml └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @contentful/team-tundra @contentful/team-developer-experience 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .idea/ 26 | .gradle 27 | **.iml 28 | 29 | **build/ 30 | local.properties 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "interactive" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![](https://jitpack.io/v/contentful/rich-text-renderer-java.svg)](https://jitpack.io/#contentful/rich-text-renderer-java) 3 | 4 | Contentful Rich Text Rendering Library for Java 5 | =============================================== 6 | 7 | > Java library for [Rich Text API](https://www.contentful.com/developers/docs/tutorials/general/rich-text-field-type-alpha/) . It helps in easily rendering rich text stored in Contentful using Java. 8 | 9 | 10 | What is Contentful? 11 | ------------------- 12 | [Contentful](https://www.contentful.com) provides a content infrastructure for digital teams to power content in websites, apps, and devices. Contentful, unlike any other CMS, is built to integrate with the modern software stack. It offers a central hub for rich content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship digital products faster. 13 | 14 |
15 | Table of contents 16 | 17 | 18 | - [Core Features](#core-features) 19 | - [Html](#html) 20 | - [Android](#android) 21 | - [Extensions](#extensions) 22 | 23 |
24 | 25 | 26 | 27 | Core Features 28 | ============= 29 | 30 | Take rich text elements from the [base library](https://github.com/contentful/contentful.java) and render them in a representation easy to use and understand and even easier to use in own projects, due to these submodules: 31 | 32 | Html 33 | ==== 34 | 35 | For rendering rich text to html, please take a look at the modules [README.md](html/README.md) 36 | 37 | Android 38 | ======= 39 | 40 | For further understanding on how to use rich text in Android, please take a look at the modules [README.md](android/README.md) 41 | 42 | 43 | Extensions 44 | ========== 45 | 46 | Extending the core functionality can be achieved by adding a new `Renderer` to the `Processor`. A Processor is the structure containing several renderer. Every renderer knows exactly how to render one part of the rich text api. By adding a renderer to the processor the functionality of either one of the core processors can be extended. For adding a new renderer calling a processors `.addRenderer()` method is needed. If existing renderer should be overwritten use the `.overrideRenderer(…)`- method, otherwise the default renderer added will take over and the custom renderer gets ignored. 47 | 48 | > Extending is especially needed, if you are planing on using some of the `embedded` or `hyperlink` Rich Nodes: Those cannot generate a generic representation, since these libraries cannot know how to render specific representations. In those cases it is advised to use custom renderers. 49 | 50 | > We are always looking for feedback, so if you have some, feel free to create an [issue for us](https://github.com/contentful/rich-text-renderer.java/issues/new). 51 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("maven-publish") 4 | } 5 | 6 | repositories { 7 | maven { url 'https://jitpack.io' } 8 | } 9 | 10 | android { 11 | compileSdk 33 12 | buildToolsVersion "30.0.3" 13 | 14 | defaultConfig { 15 | minSdkVersion 21 16 | targetSdkVersion 33 17 | versionCode 1 18 | versionName "1.0" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_11 30 | targetCompatibility JavaVersion.VERSION_11 31 | } 32 | 33 | testOptions { 34 | unitTests { 35 | includeAndroidResources = true 36 | returnDefaultValues = true 37 | } 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation project(path: ':core') 43 | 44 | implementation 'androidx.appcompat:appcompat:1.6.0' 45 | implementation "com.contentful.java:java-sdk:${project.contentful_version}" 46 | implementation 'com.google.code.findbugs:jsr305:3.0.2' 47 | implementation 'androidx.cardview:cardview:1.0.0' 48 | 49 | testImplementation 'org.robolectric:robolectric:4.9' 50 | testImplementation 'com.google.truth:truth:0.42' 51 | testImplementation 'junit:junit:4.13.2' 52 | } 53 | 54 | afterEvaluate { 55 | publishing { 56 | publications { 57 | mavenJava(MavenPublication) { 58 | from components.release 59 | groupId = project.group 60 | artifactId = 'android' 61 | version = project.version 62 | } 63 | } 64 | } 65 | } 66 | 67 | if (android.productFlavors.size() > 0) { 68 | android.libraryVariants.all { variant -> 69 | if (variant.name.toLowerCase().contains("debug")) { 70 | return 71 | } 72 | 73 | def bundleTask = tasks["bundle${variant.name.capitalize()}"] 74 | 75 | artifacts { 76 | archives(bundleTask.archivePath) { 77 | archiveClassifier = variant.flavorName 78 | builtBy bundleTask 79 | name = project.name 80 | } 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /android/src/main/java/com/contentful/rich/android/AndroidContext.java: -------------------------------------------------------------------------------- 1 | package com.contentful.rich.android; 2 | 3 | import android.view.LayoutInflater; 4 | 5 | import com.contentful.java.cda.rich.CDARichBlock; 6 | import com.contentful.java.cda.rich.CDARichList; 7 | import com.contentful.java.cda.rich.CDARichNode; 8 | import com.contentful.rich.core.Context; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import javax.annotation.Nonnull; 14 | import javax.annotation.Nullable; 15 | 16 | /** 17 | * This context stores all encountered path parents and the android context. 18 | */ 19 | public class AndroidContext extends Context> { 20 | 21 | private final android.content.Context androidContext; 22 | private final LayoutInflater inflater; 23 | private final List path = new ArrayList<>(); 24 | 25 | /** 26 | * Initialize this rich text rendering context by the help of an android context 27 | * 28 | * @param androidContext this android context will be used for android specific tasks renderers are executing. 29 | */ 30 | public AndroidContext(android.content.Context androidContext) { 31 | this.androidContext = androidContext; 32 | this.inflater = LayoutInflater.from(androidContext); 33 | } 34 | 35 | /** 36 | * @return the Android context provided. 37 | */ 38 | public android.content.Context getAndroidContext() { 39 | return androidContext; 40 | } 41 | 42 | /** 43 | * @return layout inflater for inflating layouts. 44 | */ 45 | public LayoutInflater getInflater() { 46 | return inflater; 47 | } 48 | 49 | /** 50 | * This method is called once a block of rich text is encountered. 51 | * 52 | * @param block encountered block. 53 | */ 54 | @Override public void onBlockEntered(@Nonnull CDARichBlock block) { 55 | super.onBlockEntered(block); 56 | 57 | path.add(block); 58 | } 59 | 60 | /** 61 | * Once an encountered block is exited, this method gets called. 62 | * 63 | * @param block exited block. 64 | */ 65 | @Override public void onBlockExited(@Nonnull CDARichBlock block) { 66 | super.onBlockExited(block); 67 | 68 | path.remove(path.size() - 1); 69 | } 70 | 71 | /** 72 | * @return the path of nodes up to the root node. 73 | */ 74 | @Nullable @Override public List 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 title = newHashMap(); 49 | title.put("en-US", "Title"); 50 | 51 | final HashMap localized = newHashMap(); 52 | localized.put("title", title); 53 | 54 | final HashMap file = newHashMap(); 55 | file.put("en-US", newHashMap()); 56 | localized.put("file", file); 57 | 58 | fields.set(entry, localized); 59 | } catch (IllegalAccessException e) { 60 | } 61 | 62 | fields.setAccessible(false); 63 | } 64 | 65 | if (defaultLocale != null) { 66 | defaultLocale.setAccessible(true); 67 | 68 | try { 69 | defaultLocale.set(entry, "en-US"); 70 | } catch (IllegalAccessException e) { 71 | } 72 | 73 | defaultLocale.setAccessible(false); 74 | } 75 | 76 | entry.attrs().put("id", "fake_id"); 77 | entry.attrs().put("contentType", "fake_type"); 78 | 79 | entry.setField("en-US", "title", "Title"); 80 | return entry; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /android/src/test/java/views/AllTheThingsTest.java: -------------------------------------------------------------------------------- 1 | package views; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | 6 | import com.contentful.java.cda.rich.CDARichDocument; 7 | import com.contentful.java.cda.rich.CDARichEmbeddedBlock; 8 | import com.contentful.java.cda.rich.CDARichHeading; 9 | import com.contentful.java.cda.rich.CDARichHorizontalRule; 10 | import com.contentful.java.cda.rich.CDARichHyperLink; 11 | import com.contentful.java.cda.rich.CDARichListItem; 12 | import com.contentful.java.cda.rich.CDARichMark; 13 | import com.contentful.java.cda.rich.CDARichNode; 14 | import com.contentful.java.cda.rich.CDARichOrderedList; 15 | import com.contentful.java.cda.rich.CDARichParagraph; 16 | import com.contentful.java.cda.rich.CDARichQuote; 17 | import com.contentful.java.cda.rich.CDARichText; 18 | import com.contentful.java.cda.rich.CDARichUnorderedList; 19 | import com.contentful.rich.android.AndroidContext; 20 | import com.contentful.rich.android.AndroidProcessor; 21 | import com.contentful.rich.android.R; 22 | 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.robolectric.Robolectric; 27 | import org.robolectric.RobolectricTestRunner; 28 | 29 | import java.util.ArrayList; 30 | 31 | import static com.google.common.collect.Lists.newArrayList; 32 | import static com.google.common.truth.Truth.assertThat; 33 | import static lib.ContentfulCreator.mockCDAEntry; 34 | 35 | @RunWith(RobolectricTestRunner.class) 36 | public class AllTheThingsTest { 37 | private Activity activity; 38 | 39 | @Before 40 | public void setup() { 41 | activity = Robolectric.setupActivity(Activity.class); 42 | } 43 | 44 | @Test 45 | public void allTheNodes() { 46 | final AndroidProcessor processor = AndroidProcessor.creatingNativeViews(); 47 | final AndroidContext context = new AndroidContext(activity); 48 | 49 | final View result = processor.process(context, createAllNode()); 50 | 51 | assertThat(result).isNotNull(); 52 | final View content = result.findViewById(R.id.rich_content); 53 | assertThat(content).isNotNull(); 54 | 55 | ArrayList views = new ArrayList<>(); 56 | content.findViewsWithText(views, "heading - 1", View.FIND_VIEWS_WITH_TEXT); 57 | assertThat(views).hasSize(1); 58 | 59 | content.findViewsWithText(views, "heading - 6", View.FIND_VIEWS_WITH_TEXT); 60 | assertThat(views).hasSize(2); 61 | 62 | content.findViewsWithText(views, "some list item content", View.FIND_VIEWS_WITH_TEXT); 63 | assertThat(views).hasSize(9); 64 | } 65 | 66 | private CDARichNode createAllNode() { 67 | final CDARichDocument result = new CDARichDocument(); 68 | 69 | for (int i = 1; i < 7; ++i) { 70 | final CDARichHeading heading = new CDARichHeading(i); 71 | heading.getContent().add(new CDARichText("heading - " + i, new ArrayList<>())); 72 | result.getContent().add(heading); 73 | } 74 | 75 | final CDARichHorizontalRule horizontalRule = new CDARichHorizontalRule(); 76 | result.getContent().add(horizontalRule); 77 | 78 | final CDARichEmbeddedBlock embeddedLink = new CDARichEmbeddedBlock(mockCDAEntry()); 79 | result.getContent().add(embeddedLink); 80 | 81 | final CDARichHyperLink hyperLink = new CDARichHyperLink("https://contentful.com/"); 82 | hyperLink.getContent().add(new CDARichText("Hyper hyper", new ArrayList<>())); 83 | result.getContent().add(hyperLink); 84 | 85 | final CDARichText allTheMarks = new CDARichText( 86 | "ALL THE TEXT MARKS!", 87 | newArrayList( 88 | new CDARichMark.CDARichMarkUnderline(), 89 | new CDARichMark.CDARichMarkBold(), 90 | new CDARichMark.CDARichMarkItalic(), 91 | new CDARichMark.CDARichMarkCode(), 92 | new CDARichMark.CDARichMarkCustom("top") 93 | ) 94 | ); 95 | result.getContent().add(allTheMarks); 96 | 97 | final CDARichOrderedList orderedList = new CDARichOrderedList(); 98 | final CDARichListItem listItem = new CDARichListItem(); 99 | listItem.getContent().add(new CDARichText("some list item content", new ArrayList<>())); 100 | orderedList.getContent().add(listItem); 101 | orderedList.getContent().add(listItem); 102 | orderedList.getContent().add(listItem); 103 | result.getContent().add(orderedList); 104 | 105 | final CDARichUnorderedList unorderedList = new CDARichUnorderedList(); 106 | unorderedList.getContent().add(listItem); 107 | unorderedList.getContent().add(listItem); 108 | unorderedList.getContent().add(listItem); 109 | unorderedList.getContent().add(listItem); 110 | result.getContent().add(unorderedList); 111 | 112 | final CDARichParagraph paragraph = new CDARichParagraph(); 113 | paragraph.getContent().add(new CDARichText("Paragraph", new ArrayList<>())); 114 | result.getContent().add(paragraph); 115 | 116 | final CDARichQuote quote = new CDARichQuote(); 117 | quote.getContent().add(new CDARichText("Famous quote", new ArrayList<>())); 118 | result.getContent().add(quote); 119 | 120 | final CDARichNode node = new CDARichNode(); 121 | result.getContent().add(node); 122 | 123 | return result; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /android/src/test/java/views/HeadingTest.java: -------------------------------------------------------------------------------- 1 | package views; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | import android.widget.TextView; 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 | import com.contentful.rich.android.R; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.robolectric.Robolectric; 17 | import org.robolectric.RobolectricTestRunner; 18 | 19 | import java.util.ArrayList; 20 | 21 | import static com.google.common.truth.Truth.assertThat; 22 | 23 | @RunWith(RobolectricTestRunner.class) 24 | public class HeadingTest { 25 | private Activity activity; 26 | 27 | @Before 28 | public void setup() { 29 | activity = Robolectric.setupActivity(Activity.class); 30 | } 31 | 32 | @Test 33 | public void firstHeadingParsesContentTest() { 34 | final AndroidProcessor processor = AndroidProcessor.creatingNativeViews(); 35 | final AndroidContext context = new AndroidContext(activity); 36 | 37 | final CDARichHeading heading = new CDARichHeading(1); 38 | heading.getContent().add(new CDARichText("heading 1", new ArrayList<>())); 39 | 40 | final View result = processor.process(context, heading); 41 | 42 | assertThat(result).isNotNull(); 43 | final View content = result.findViewById(R.id.rich_content); 44 | assertThat(content).isNotNull(); 45 | 46 | ArrayList views = new ArrayList<>(); 47 | content.findViewsWithText(views, "heading 1", View.FIND_VIEWS_WITH_TEXT); 48 | assertThat(views).hasSize(1); 49 | assertThat(views.get(0)).isInstanceOf(TextView.class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/src/test/java/views/LinkTest.java: -------------------------------------------------------------------------------- 1 | package views; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | import android.widget.TextView; 6 | 7 | import com.contentful.java.cda.rich.CDARichEmbeddedBlock; 8 | import com.contentful.java.cda.rich.CDARichHyperLink; 9 | import com.contentful.java.cda.rich.CDARichText; 10 | import com.contentful.rich.android.AndroidContext; 11 | import com.contentful.rich.android.AndroidProcessor; 12 | import com.contentful.rich.android.R; 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.creatingNativeViews(); 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 View result = processor.process(context, link); 44 | 45 | assertThat(result).isNotNull(); 46 | final View content = result.findViewById(R.id.rich_content); 47 | assertThat(content).isNotNull(); 48 | 49 | ArrayList views = new ArrayList<>(); 50 | content.findViewsWithText(views, "Some link text", View.FIND_VIEWS_WITH_TEXT); 51 | assertThat(views).hasSize(1); 52 | assertThat(views.get(0)).isInstanceOf(TextView.class); 53 | } 54 | 55 | @Test 56 | public void createUnsanitzedStrings() { 57 | final AndroidProcessor processor = AndroidProcessor.creatingNativeViews(); 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 View result = processor.process(context, link); 64 | 65 | assertThat(result).isNotNull(); 66 | final View content = result.findViewById(R.id.rich_content); 67 | assertThat(content).isNotNull(); 68 | 69 | ArrayList views = new ArrayList<>(); 70 | content.findViewsWithText(views, "Some link text", View.FIND_VIEWS_WITH_TEXT); 71 | assertThat(views).hasSize(1); 72 | assertThat(views.get(0)).isInstanceOf(TextView.class); 73 | } 74 | 75 | @Test 76 | public void createEmbeddedLink() { 77 | final AndroidProcessor processor = AndroidProcessor.creatingNativeViews(); 78 | final AndroidContext context = new AndroidContext(activity); 79 | 80 | final CDARichHyperLink link = new CDARichEmbeddedBlock(ContentfulCreator.mockCDAEntry()); 81 | link.getContent().add(new CDARichText("My embedded entry", new ArrayList<>())); 82 | 83 | final View result = processor.process(context, link); 84 | 85 | assertThat(result).isNotNull(); 86 | final View content = result.findViewById(R.id.rich_content); 87 | assertThat(content).isNotNull(); 88 | 89 | ArrayList views = new ArrayList<>(); 90 | content.findViewsWithText(views, "My embedded entry", View.FIND_VIEWS_WITH_TEXT); 91 | assertThat(views).hasSize(1); 92 | assertThat(views.get(0)).isInstanceOf(TextView.class); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /android/src/test/java/views/QuoteTest.java: -------------------------------------------------------------------------------- 1 | package views; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 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 | import com.contentful.rich.android.R; 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 QuoteTest { 24 | private Activity activity; 25 | 26 | @Before 27 | public void setup() { 28 | activity = Robolectric.setupActivity(Activity.class); 29 | } 30 | 31 | @Test 32 | public void quoteTest() { 33 | final AndroidProcessor processor = AndroidProcessor.creatingNativeViews(); 34 | final AndroidContext context = new AndroidContext(activity); 35 | final CDARichQuote quote = new CDARichQuote(); 36 | 37 | quote.getContent().add(new CDARichText("Edel sei der Mensch,\n" + 38 | "Hilfreich und gut! — Johann Wolfgang von Goethe", new ArrayList<>())); 39 | 40 | final View result = processor.process(context, quote); 41 | 42 | assertThat(result).isNotNull(); 43 | final View content = result.findViewById(R.id.rich_content); 44 | assertThat(content).isNotNull(); 45 | 46 | final ArrayList views = new ArrayList<>(); 47 | content.findViewsWithText(views, "Edel sei der Mensch", View.FIND_VIEWS_WITH_TEXT); 48 | 49 | assertThat(views).isNotNull(); 50 | assertThat(views).hasSize(1); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /android_sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android_sample/README.md: -------------------------------------------------------------------------------- 1 | Rendering Rich Text Sample Android APP 2 | ============================================ 3 | 4 | This app demonstrates how to use a predefined Rich Text object into textviews and other native Android views. 5 | -------------------------------------------------------------------------------- /android_sample/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id "org.jetbrains.kotlin.android" version "1.8.0" 4 | } 5 | 6 | repositories { 7 | maven { url 'https://jitpack.io' } 8 | } 9 | 10 | android { 11 | compileSdk 33 12 | buildToolsVersion "30.0.3" 13 | 14 | 15 | defaultConfig { 16 | applicationId "com.contentful.rich.android.sample" 17 | minSdkVersion 21 18 | targetSdkVersion 33 19 | versionCode 1 20 | versionName "1.0" 21 | 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | 24 | } 25 | 26 | buildFeatures { 27 | viewBinding = true 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_11 32 | targetCompatibility JavaVersion.VERSION_11 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = "11" 37 | } 38 | 39 | buildTypes { 40 | configureEach { 41 | buildConfigField("String", "SPACE_ID", "\"${System.getenv("RICH_TEXT_SPACE_ID")}\"") 42 | buildConfigField("String", "DELIVERY_TOKEN", "\"${System.getenv("RICH_TEXT_DELIVERY_TOKEN")}\"") 43 | buildConfigField("String", "ENVIRONMENT_ID", "\"${System.getenv("RICH_TEXT_ENVIRONMENT_ID")}\"") 44 | } 45 | 46 | release { 47 | minifyEnabled false 48 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 49 | } 50 | } 51 | 52 | } 53 | 54 | dependencies { 55 | implementation project(path: ':android') 56 | implementation project(path: ':core') 57 | 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0" 59 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" 60 | implementation 'androidx.appcompat:appcompat:1.6.0' 61 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" // Use the latest version available 62 | 63 | 64 | implementation "com.contentful.java:java-sdk:${project.contentful_version}" 65 | implementation 'com.google.code.findbugs:jsr305:3.0.2' 66 | 67 | implementation 'com.google.android.material:material:1.8.0' 68 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 69 | } 70 | -------------------------------------------------------------------------------- /android_sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android_sample/screenshots/headings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/headings.png -------------------------------------------------------------------------------- /android_sample/screenshots/links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/links.png -------------------------------------------------------------------------------- /android_sample/screenshots/lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/lists.png -------------------------------------------------------------------------------- /android_sample/screenshots/native-headings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/native-headings.png -------------------------------------------------------------------------------- /android_sample/screenshots/native-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/native-links.png -------------------------------------------------------------------------------- /android_sample/screenshots/native-lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/native-lists.png -------------------------------------------------------------------------------- /android_sample/screenshots/native-quotes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/native-quotes.png -------------------------------------------------------------------------------- /android_sample/screenshots/native-textmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/native-textmarks.png -------------------------------------------------------------------------------- /android_sample/screenshots/quotes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/quotes.png -------------------------------------------------------------------------------- /android_sample/screenshots/textmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/screenshots/textmarks.png -------------------------------------------------------------------------------- /android_sample/settings.gradle: -------------------------------------------------------------------------------- 1 | project.name = "sample" 2 | pluginManagement { 3 | repositories { 4 | gradlePluginPortal() 5 | jcenter() 6 | google() 7 | } 8 | resolutionStrategy { 9 | eachPlugin { 10 | if (requested.id.id == "com.android.application") { 11 | useModule("com.android.tools.build:gradle:${requested.version}") 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android_sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /android_sample/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /android_sample/src/main/java/com/contentful/rich/android/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.contentful.rich.android.sample 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.AdapterView 6 | import android.widget.AdapterView.OnItemSelectedListener 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.contentful.java.cda.CDAClient 9 | import com.contentful.java.cda.CDAEntry 10 | import com.contentful.java.cda.QueryOperation.Matches 11 | import com.contentful.java.cda.rich.CDARichDocument 12 | import com.contentful.java.cda.rich.CDARichParagraph 13 | import com.contentful.rich.android.sample.databinding.ActivityMainBinding 14 | import com.contentful.rich.core.simple.RemoveEmpties 15 | import com.contentful.rich.core.simple.RemoveToDeepNesting 16 | import com.contentful.rich.core.simple.Simplifier 17 | import kotlinx.coroutines.GlobalScope 18 | import kotlinx.coroutines.launch 19 | 20 | private const val SPACE_ID = "i1ppeoxgdpvt" 21 | private const val TOKEN = "ec6NU4tHJupJ_SLf-sAq5QRDbJvJkRcBTMnha9XFYuI" 22 | private const val ENVIRONMENT = "master" 23 | 24 | class MainActivity( 25 | private val client: CDAClient = CDAClient.builder() 26 | .setSpace(SPACE_ID) 27 | .setToken(TOKEN) 28 | .setEnvironment(ENVIRONMENT) 29 | .build() 30 | ) : AppCompatActivity() { 31 | 32 | private lateinit var binding: ActivityMainBinding 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | binding = ActivityMainBinding.inflate(layoutInflater) 37 | val view = binding.root 38 | setContentView(view) 39 | 40 | setSupportActionBar(binding.toolbar) 41 | supportActionBar?.setDisplayShowTitleEnabled(false) 42 | 43 | if (PAGES.last().name != "Contentful") { 44 | binding.spinner.prompt = "⌛ Loading from Contentful ⏳" 45 | loadPageFromContentful() 46 | } 47 | 48 | binding.spinner.onItemSelectedListener = object : OnItemSelectedListener { 49 | override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { 50 | supportFragmentManager.beginTransaction() 51 | .replace(R.id.container, PageFragment.newInstance(position)) 52 | .commit() 53 | } 54 | 55 | override fun onNothingSelected(parent: AdapterView<*>) {} 56 | } 57 | } 58 | 59 | private fun loadPageFromContentful() { 60 | GlobalScope.launch { 61 | val document = client 62 | .fetch(CDAEntry::class.java) 63 | .withContentType("rich") 64 | .where("fields.name", Matches, "simple") 65 | .orderBy("fields.name") 66 | .all() 67 | .entries() 68 | .values 69 | .map { entry -> 70 | if(entry.getField("en", "rich") != null) { 71 | entry.getField("en", "rich") as CDARichDocument 72 | } else { 73 | CDARichDocument() 74 | } 75 | }.reduce { a, b -> 76 | val p = CDARichParagraph() 77 | p.content.addAll(b.content) 78 | a.content.add(p) 79 | a 80 | } 81 | 82 | PAGES.add( 83 | Page("Contentful", 84 | Simplifier( 85 | RemoveToDeepNesting(10), 86 | ).simplify(document) as CDARichDocument) 87 | ) 88 | 89 | runOnUiThread { 90 | binding.spinner.prompt = PAGES[0].name 91 | binding.spinner.adapter = PageAdapter( 92 | this@MainActivity, 93 | PAGES.toTypedArray() 94 | ) 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /android_sample/src/main/java/com/contentful/rich/android/sample/PageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.contentful.rich.android.sample 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.ArrayAdapter 9 | import androidx.appcompat.widget.ThemedSpinnerAdapter 10 | import com.contentful.rich.android.sample.databinding.AppbarItemBinding 11 | import com.contentful.rich.android.sample.databinding.DropdownItemBinding 12 | 13 | class PageAdapter(context: Context, objects: Array) : 14 | ArrayAdapter(context, R.layout.appbar_item, objects), ThemedSpinnerAdapter { 15 | 16 | private val mDropDownHelper: ThemedSpinnerAdapter.Helper = ThemedSpinnerAdapter.Helper(context) 17 | 18 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 19 | val binding: AppbarItemBinding = 20 | if (convertView != null) AppbarItemBinding.bind(convertView) 21 | else AppbarItemBinding.inflate(LayoutInflater.from(context), parent, false) 22 | 23 | binding.listItemText.text = getItem(position)?.name ?: "null" 24 | 25 | return binding.root 26 | } 27 | 28 | override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { 29 | val binding: DropdownItemBinding = 30 | if (convertView != null) DropdownItemBinding.bind(convertView) 31 | else DropdownItemBinding.inflate(LayoutInflater.from(context), parent, false) 32 | 33 | binding.listItemText.text = getItem(position)?.name ?: "null" 34 | 35 | return binding.root 36 | } 37 | 38 | override fun getDropDownViewTheme(): Resources.Theme? { 39 | return mDropDownHelper.dropDownViewTheme 40 | } 41 | 42 | override fun setDropDownViewTheme(theme: Resources.Theme?) { 43 | mDropDownHelper.dropDownViewTheme = theme 44 | } 45 | } -------------------------------------------------------------------------------- /android_sample/src/main/java/com/contentful/rich/android/sample/PageFragment.kt: -------------------------------------------------------------------------------- 1 | package com.contentful.rich.android.sample 2 | 3 | import android.os.Bundle 4 | import android.text.method.LinkMovementMethod 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.contentful.rich.android.sample.databinding.FragmentPageBinding 12 | import com.contentful.rich.android.sample.databinding.SampleItemBinding 13 | 14 | class PageFragment : Fragment() { 15 | class Holder(private val binding: SampleItemBinding) : RecyclerView.ViewHolder(binding.root) { 16 | fun bind(text: String) { 17 | binding.sampleItemText.text = text 18 | binding.sampleItemText.movementMethod = LinkMovementMethod.getInstance() 19 | } 20 | fun removeViews(view: View) { 21 | binding.sampleItemCard.removeAllViews() 22 | binding.sampleItemCard.addView(view) 23 | } 24 | } 25 | 26 | private var _binding: FragmentPageBinding? = null 27 | private val binding get() = _binding!! 28 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 29 | savedInstanceState: Bundle?): View? { 30 | _binding = FragmentPageBinding.inflate(inflater, container, false) 31 | binding.pageRecycler.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) 32 | binding.pageRecycler.adapter = 33 | RichCharSequenceAdapter(arguments?.getInt(ARG_PAGE_INDEX, 0) 34 | ?: 0, requireContext()) 35 | 36 | binding.mainNativeCheck.setOnCheckedChangeListener { _, isChecked -> 37 | if (isChecked) { 38 | onNativeViewProcessingSelected() 39 | } else { 40 | onCharSequenceProcessingSelected() 41 | } 42 | } 43 | 44 | return binding.root 45 | } 46 | 47 | private fun onNativeViewProcessingSelected() { 48 | binding.pageRecycler.adapter = 49 | RichNativeViewAdapter( 50 | arguments?.getInt(ARG_PAGE_INDEX, 0) ?: 0, 51 | requireContext()) 52 | 53 | } 54 | 55 | private fun onCharSequenceProcessingSelected() { 56 | binding.pageRecycler.adapter = 57 | RichCharSequenceAdapter( 58 | arguments?.getInt(ARG_PAGE_INDEX, 0) ?: 0, 59 | requireContext()) 60 | } 61 | 62 | companion object { 63 | private const val ARG_PAGE_INDEX = "page_index" 64 | 65 | fun newInstance(pageIndex: Int): PageFragment { 66 | val fragment = PageFragment() 67 | val args = Bundle() 68 | args.putInt(ARG_PAGE_INDEX, pageIndex) 69 | fragment.arguments = args 70 | return fragment 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /android_sample/src/main/java/com/contentful/rich/android/sample/RichCharSequenceAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.contentful.rich.android.sample 2 | 3 | import android.content.Context 4 | import android.text.Spannable 5 | import android.text.SpannableStringBuilder 6 | import android.text.style.ClickableSpan 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.contentful.java.cda.CDAAsset 12 | import com.contentful.java.cda.CDAEntry 13 | import com.contentful.java.cda.rich.CDARichHyperLink 14 | import com.contentful.rich.android.AndroidContext 15 | import com.contentful.rich.android.AndroidProcessor 16 | import com.contentful.rich.android.sample.databinding.SampleItemBinding 17 | 18 | class RichCharSequenceAdapter(pageIndex: Int, private val androidContext: Context) : RecyclerView.Adapter() { 19 | private val page: Page = PAGES[pageIndex] 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageFragment.Holder { 22 | val itemBinding = SampleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 23 | return PageFragment.Holder(itemBinding) 24 | } 25 | override fun onBindViewHolder(holder: PageFragment.Holder, position: Int) { 26 | val item = page.document.content[position] 27 | val context = AndroidContext(androidContext) 28 | val processor = AndroidProcessor.creatingCharSequences() 29 | processor.overrideRenderer( 30 | { _, node -> node is CDARichHyperLink && node.data is CDAEntry }, 31 | { _, node -> 32 | val data = (node as CDARichHyperLink).data as CDAEntry 33 | val text = androidContext.resources.getString(R.string.hyperlink_entry_inline, data.id()) 34 | val message = androidContext.resources.getString(R.string.hyperlink_entry_inline_clicked, data.id()) 35 | 36 | SpannableStringBuilder(text).apply { 37 | setSpan( 38 | object : ClickableSpan() { 39 | override fun onClick(widget: View) { 40 | androidContext.toast(message) 41 | } 42 | }, 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 43 | ) 44 | }.toString() 45 | } 46 | ) 47 | 48 | processor.overrideRenderer( 49 | { _, node -> node is CDARichHyperLink && node.data is CDAAsset }, 50 | { _, node -> 51 | val data = (node as CDARichHyperLink).data as CDAAsset 52 | val text = androidContext.resources.getString(R.string.hyperlink_asset_inline, data.id()) 53 | val message = androidContext.resources.getString(R.string.hyperlink_asset_inline_clicked, data.id()) 54 | 55 | SpannableStringBuilder(text).apply { 56 | setSpan( 57 | object : ClickableSpan() { 58 | override fun onClick(widget: View) { 59 | androidContext.toast(message) 60 | } 61 | }, 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 62 | ) 63 | }.toString() 64 | } 65 | ) 66 | 67 | val text = processor.process(context, item) ?: "???" 68 | holder.bind(text.toString()) 69 | } 70 | 71 | override fun getItemCount(): Int = page.document.content.size 72 | } -------------------------------------------------------------------------------- /android_sample/src/main/java/com/contentful/rich/android/sample/RichNativeViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.contentful.rich.android.sample 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import android.widget.Toast 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.contentful.java.cda.CDAAsset 10 | import com.contentful.java.cda.CDAEntry 11 | import com.contentful.java.cda.rich.CDARichHyperLink 12 | import com.contentful.rich.android.AndroidContext 13 | import com.contentful.rich.android.AndroidProcessor 14 | import com.contentful.rich.android.sample.databinding.SampleItemBinding 15 | 16 | class RichNativeViewAdapter(pageIndex: Int, private val androidContext: Context) : RecyclerView.Adapter() { 17 | private val page: Page = PAGES[pageIndex] 18 | private val inflater: LayoutInflater = LayoutInflater.from(androidContext) 19 | 20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageFragment.Holder { 21 | val itemBinding = SampleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 22 | return PageFragment.Holder(itemBinding) 23 | } 24 | 25 | override fun onBindViewHolder(holder: PageFragment.Holder, position: Int) { 26 | val item = page.document.content[position] 27 | val context = AndroidContext(androidContext) 28 | 29 | val processor = AndroidProcessor.creatingNativeViews() 30 | processor.overrideRenderer( 31 | { _, node -> node is CDARichHyperLink && node.data is CDAEntry }, 32 | { _, node -> 33 | val data = (node as CDARichHyperLink).data as CDAEntry 34 | TextView(androidContext).apply { 35 | text = resources.getString(R.string.hyperlink_entry_inline, data.id()) 36 | setOnClickListener { 37 | androidContext.toast(resources.getString(R.string.hyperlink_entry_inline_clicked, data.id())) 38 | } 39 | } 40 | } 41 | ) 42 | 43 | processor.overrideRenderer( 44 | { _, node -> node is CDARichHyperLink && node.data is CDAAsset }, 45 | { _, node -> 46 | val data = (node as CDARichHyperLink).data as CDAAsset 47 | TextView(androidContext).apply { 48 | text = resources.getString(R.string.hyperlink_asset_inline, data.id()) 49 | setOnClickListener { 50 | androidContext.toast(resources.getString(R.string.hyperlink_asset_inline_clicked, data.id())) 51 | } 52 | } 53 | } 54 | ) 55 | 56 | val view = processor.process(context, item) 57 | ?: TextView(this.androidContext).apply { setText(R.string.error_no_view) } 58 | 59 | holder.removeViews(view) 60 | } 61 | 62 | override fun getItemCount(): Int = page.document.content.size 63 | 64 | } 65 | 66 | fun Context.toast(message: CharSequence) { 67 | Toast.makeText(this, message, Toast.LENGTH_LONG).show() 68 | } 69 | -------------------------------------------------------------------------------- /android_sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 32 | 33 | 40 | 41 | 42 | 43 | 44 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /android_sample/src/main/res/layout/appbar_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /android_sample/src/main/res/layout/dropdown_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /android_sample/src/main/res/layout/fragment_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /android_sample/src/main/res/layout/sample_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android_sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android_sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/rich-text-renderer-java/a1502f736dce6c1a1c2f0efae42d278eefc82304/android_sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android_sample/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #00BFA5 4 | #00897B 5 | #FF4081 6 | #121212 7 | #FFFFFF 8 | #B3FFFFFF 9 | -------------------------------------------------------------------------------- /android_sample/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 17 | 18 | 13 | 14 | 18 | 19 |