├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── App.tsx ├── LICENSE ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── EditorJs Native 2.svg ├── presentation.jpg └── presentation_2.jpg ├── src ├── Components │ ├── Bold │ │ ├── index.tsx │ │ └── types.ts │ ├── Code │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Delimiter │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── FallbackBlock │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── FullWidthImage │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Header │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── ImageFrame │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Italic │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── LinkTool │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── List │ │ ├── ListItem │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Mark │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Paragraph │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Personality │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Quote │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── SimpleImage │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Underline │ │ ├── index.tsx │ │ └── types.ts │ ├── __test__ │ │ ├── components │ │ │ └── Header.spec.tsx │ │ └── mock │ │ │ └── editorJsOutputData.ts │ └── index.tsx ├── config │ ├── createEditorJsViewer.tsx │ └── index.ts ├── constants │ └── sizes.ts ├── hooks │ └── useParseHtmlTags.tsx ├── index.tsx └── types │ ├── createEditorJsViewerProps.ts │ ├── editorJsDataProps.ts │ ├── editorJsViwerNative.ts │ └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "react", 19 | "@typescript-eslint" 20 | ], 21 | "rules": { 22 | "indent": [ 23 | "error", 24 | 2, 25 | { "SwitchCase": 1 } 26 | ], 27 | "linebreak-style": [ 28 | "error", 29 | "windows" 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "always" 38 | ], 39 | "react/react-in-jsx-scope": "off" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Expo 2 | # 3 | .expo/ 4 | dist/ 5 | npm-debug.* 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | 14 | # OSX 15 | # 16 | .DS_Store 17 | 18 | # Xcode 19 | # 20 | build/ 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | xcuserdata 30 | *.xccheckout 31 | *.moved-aside 32 | DerivedData 33 | *.hmap 34 | *.ipa 35 | *.xcuserstate 36 | project.xcworkspace 37 | 38 | # Android/IntelliJ 39 | # 40 | build/ 41 | .idea 42 | .gradle 43 | local.properties 44 | *.iml 45 | 46 | # node.js 47 | # 48 | node_modules/ 49 | npm-debug.log 50 | yarn-error.log 51 | 52 | # BUCK 53 | buck-out/ 54 | \.buckd/ 55 | *.keystore 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/ 63 | 64 | */fastlane/report.xml 65 | */fastlane/Preview.html 66 | */fastlane/screenshots 67 | 68 | # Bundle artifacts 69 | *.jsbundle 70 | 71 | # CocoaPods 72 | /ios/Pods/ 73 | 74 | # Packages 75 | lib/ 76 | dist 77 | 78 | # Personal files 79 | # 80 | data.json 81 | tmp/ 82 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, ScrollView, View, Text } from 'react-native'; 3 | 4 | import { createEditorJsViewer } from './src/config/createEditorJsViewer'; 5 | import { IComponentBlockProps } from './src/types/createEditorJsViewerProps'; 6 | import data from './data.json'; 7 | 8 | interface IPopcornBlockData { 9 | text: string; 10 | emoji: string; 11 | } 12 | 13 | const PopcornBlock = ({ block, containerStyle }: IComponentBlockProps) => { 14 | return ( 15 | 16 | {block.data.emoji} 17 | {block.data.text} 18 | 19 | ); 20 | }; 21 | 22 | interface IRandomColeredTextData { 23 | text: string; 24 | } 25 | 26 | const RandomColeredTextBlock = ({ block, containerStyle }: IComponentBlockProps) => { 27 | const randomColor = `#${Math.floor(Math.random()*16777215).toString(16)}`; 28 | return ( 29 | {block.data.text} 30 | ); 31 | }; 32 | 33 | const EditorJsViewerNative = createEditorJsViewer({ 34 | customTools: { 35 | // popcorn: { // This can be any name 36 | // Component: PopcornBlock 37 | // }, 38 | // randomColeredText: { // This can be any name 39 | // Component: RandomColeredTextBlock 40 | // }, 41 | } 42 | }); 43 | 44 | export default function App() { 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | flex: 1, 57 | marginTop: 50, 58 | paddingHorizontal: 16, 59 | alignItems: 'center', 60 | justifyContent: 'center', 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexandre Hideki Siroma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | EditorJs native viewer presentation image 3 |

4 | 5 | [![npm version](https://img.shields.io/npm/v/editorjs-viewer-native?style=flat-square)](https://badge.fury.io/js/editorjs-viewer-native) 6 | [![npm downloads](https://img.shields.io/npm/dm/editorjs-viewer-native.svg?style=flat-square)](https://npm-stat.com/charts.html?package=editorjs-viewer-native) 7 | 8 | # About 9 | This lib provide a component to render in a React Native a JSON generated by [`Editor.js`](https://editorjs.io/)! 10 | 11 | ## Installation 12 | Version 1.0.0 is now available! 13 | ```cmd 14 | npm i editorjs-viewer-native 15 | ``` 16 | or 17 | ```cmd 18 | yarn add editorjs-viewer-native 19 | ``` 20 | 21 | ## Current support for editorJs's plugins: 22 | - [`Code`](https://github.com/editor-js/code) 23 | - [`Delimiter`](https://github.com/editor-js/delimiter) 24 | - [`Header`](https://github.com/editor-js/header) 25 | - [`Image`](https://github.com/editor-js/image) 26 | - [`Link`](https://github.com/editor-js/link) 27 | - [`List`](https://github.com/editor-js/list) 28 | - [`Marker`](https://github.com/editor-js/marker) 29 | - [`Paragraph`](https://github.com/editor-js/paragraph) 30 | - [`Personality`](https://github.com/editor-js/personality) 31 | - [`Quote`](https://github.com/editor-js/quote) 32 | - [`Simple Image`](https://github.com/editor-js/simple-image) 33 | - [`Underline`](https://github.com/editor-js/underline) 34 | - [`Custom block type`](#support-for-custom-blocks) 35 | 36 | See the[` update plans`](#updates-plans) 37 | ## Usage 38 | Create a component using function `createEditorJsViewer`. 39 | 40 | ```tsx 41 | import { ScrollView } from 'react-native'; 42 | import { createEditorJsViewer } from "editorjs-viewer-native"; 43 | 44 | import dataFromEditorJs from "./data.json"; 45 | 46 | const EditorJsViewerNative = createEditorJsViewer(); 47 | 48 | export default function App() { 49 | return ( 50 | 51 | 52 | 53 | ); 54 | } 55 | ``` 56 | 57 | ## Custom component for supported plugins 58 | If you want to use your custom component to render **any** supported plugin block, you can define a Component in `createEditorJsViewer` config object. 59 | ```tsx 60 | import { ScrollView, Text } from 'react-native'; 61 | import { createEditorJsViewer, IHeaderProps } from "editorjs-viewer-native"; 62 | import dataFromEditorJs from "./data.json"; 63 | 64 | const MyHeader = ({ data }: IHeaderProps) => { 65 | return {data.text} 66 | } 67 | 68 | const EditorJsViewerNative = createEditorJsViewer({ 69 | tools: { // Updated to "tools" in v1 (before was toolsParser) 70 | header: { 71 | Component: MyHeader // Updated to "Component" in v1 (before was CustomComponent) 72 | } 73 | } 74 | }) 75 | 76 | export default function App() { 77 | return ( 78 | 79 | 80 | 81 | ); 82 | } 83 | ``` 84 | Now the component `MyHeader` will render all data of type **header**. 85 | 86 | ## Support for custom blocks 87 | If you want to render a custom block type, you can define a `customTools` in `createEditorJsViewer` config object. 88 | ```json 89 | // outputData.json 90 | { 91 | "blocks": [ 92 | { 93 | "id": "customBlock_id2", 94 | "type" : "randomColeredText", 95 | "data" : { 96 | "text" : "The color of this text is generated randomic" 97 | } 98 | } 99 | ] 100 | } 101 | ``` 102 | ```tsx 103 | import { ScrollView, Text } from 'react-native'; 104 | import { createEditorJsViewer, IHeaderProps, IComponentBlockProps } from "editorjs-viewer-native"; 105 | import dataFromEditorJs from "./data.json"; 106 | 107 | interface IRandomColeredTextData { 108 | text: string; 109 | } 110 | 111 | // * Any component will receive "data" and "containerStyle" 112 | // * "data" contain the data of block 113 | // * "containerStyle" is a simple style to prevent margin top on first element or margin bottom on last component 114 | const RandomColeredTextBlock = ({ block, containerStyle }: IComponentBlockProps) => { 115 | const randomColor = `#${Math.floor(Math.random()*16777215).toString(16)}`; 116 | return ( 117 | {block.data.text} 118 | ); 119 | }; 120 | 121 | const EditorJsViewerNative = createEditorJsViewer({ 122 | customTools: { 123 | randomColeredText: { // Name of your custom block type 124 | Component: RandomColeredTextBlock 125 | } 126 | } 127 | }) 128 | 129 | export default function App() { 130 | return ( 131 | 132 | 133 | 134 | ); 135 | } 136 | ``` 137 | Now the component `MyHeader` will render all data of type **header**. 138 | 139 | ## Fallback for unsupported block types 140 | If you want to show a message for unsupported blocks (good for test, bad for production) set `showBlockFallback` as true inside `createEditorJsViewer` config object. 141 | 142 | ```tsx 143 | const EditorJsViewerNative = createEditorJsViewer({ 144 | showBlockFallback: true // Update to "showBlockFallback" (before was unknownBlockFallback) 145 | }) 146 | ``` 147 | 148 | ## Updates plans 149 | ### Support for: 150 | - [`Attaches`](https://github.com/editor-js/attaches) 151 | - [`Checklist`](https://github.com/editor-js/checklist) 152 | - [`Raw HTML`](https://github.com/editor-js/raw) 153 | - [`Table`](https://github.com/editor-js/table) 154 | 155 | ## Open source 156 | Feel free to clone/fork this project! 157 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "editorjs-viewer-native", 4 | "slug": "editorjs-viewer-native", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hidekih/editorjs-viewer-native/4bb04110addfb2f4b993b1fdd864cd249fe68611/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hidekih/editorjs-viewer-native/4bb04110addfb2f4b993b1fdd864cd249fe68611/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hidekih/editorjs-viewer-native/4bb04110addfb2f4b993b1fdd864cd249fe68611/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hidekih/editorjs-viewer-native/4bb04110addfb2f4b993b1fdd864cd249fe68611/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset', 'babel-preset-expo'], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "editorjs-viewer-native", 3 | "version": "1.0.1", 4 | "description": "A React Native viewer for JSON created by EditorJs", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "sideEffects": false, 9 | "files": [ 10 | "dist/**/*" 11 | ], 12 | "keywords": [ 13 | "react-native", 14 | "ios", 15 | "android", 16 | "editorjs", 17 | "mobile" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Hidekih/editorjs-viewer-native.git" 22 | }, 23 | "author": "Alexandre Hideki Siroma", 24 | "license": "MIT", 25 | "scripts": { 26 | "dev": "npm set init.main \"node_modules/expo/AppEntry.js\" && expo start", 27 | "test": "jest --no-cache", 28 | "build": "rm -rf dist && tsup src/index.tsx --format esm,cjs --dts --minify --external react,react-native", 29 | "update:readme": "npm publish --update-readme", 30 | "pub": "jest && tsup src/index.tsx --format esm,cjs --dts --minify --external react,react-native && npm publish --access public" 31 | }, 32 | "dependencies": { 33 | "html-entities": "^2.3.3" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.12.9", 37 | "@testing-library/jest-native": "^5.4.1", 38 | "@testing-library/react-native": "^11.5.0", 39 | "@types/jest": "^29.2.6", 40 | "@types/react": "^18.0.26", 41 | "@types/react-native": "^0.70.8", 42 | "@typescript-eslint/eslint-plugin": "^5.48.0", 43 | "@typescript-eslint/parser": "^5.48.0", 44 | "babel-plugin-module-resolver": "^5.0.0", 45 | "eslint": "^8.31.0", 46 | "eslint-plugin-react": "^7.31.11", 47 | "expo": "^47.0.13", 48 | "jest": "^29.3.1", 49 | "metro-react-native-babel-preset": "^0.74.1", 50 | "react": "^18.2.0", 51 | "react-dom": "^18.2.0", 52 | "react-native": "^0.71.1", 53 | "react-native-web": "^0.18.12", 54 | "react-test-renderer": "^18.2.0", 55 | "tsup": "^6.5.0", 56 | "typescript": "^4.9.4" 57 | }, 58 | "peerDependencies": { 59 | "react": "*", 60 | "react-native": "*" 61 | }, 62 | "bugs": { 63 | "url": "https://github.com/Hidekih/editorjs-viewer-native/issues" 64 | }, 65 | "homepage": "https://github.com/Hidekih/editorjs-viewer-native#readme", 66 | "jest": { 67 | "preset": "react-native", 68 | "testEnvironment": "node", 69 | "clearMocks": true, 70 | "modulePathIgnorePatterns": [ 71 | "/dist/" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/EditorJs Native 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/presentation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hidekih/editorjs-viewer-native/4bb04110addfb2f4b993b1fdd864cd249fe68611/public/presentation.jpg -------------------------------------------------------------------------------- /public/presentation_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hidekih/editorjs-viewer-native/4bb04110addfb2f4b993b1fdd864cd249fe68611/public/presentation_2.jpg -------------------------------------------------------------------------------- /src/Components/Bold/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | 3 | import { IBoldProps } from './types'; 4 | 5 | const Bold = ({ fontFamily, children }: IBoldProps) => { 6 | return ( 7 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export { Bold }; 20 | -------------------------------------------------------------------------------- /src/Components/Bold/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type IBoldProps = { 4 | fontFamily?: string; 5 | } & TextProps; 6 | -------------------------------------------------------------------------------- /src/Components/Code/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | 3 | import { styles } from './styles'; 4 | import { ICodeProps } from './types'; 5 | 6 | const Code = ({ children }: ICodeProps) => { 7 | return ( 8 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { Code }; 18 | -------------------------------------------------------------------------------- /src/Components/Code/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | code: { 5 | color: '#C44437', 6 | backgroundColor: '#f2f2f2' 7 | } 8 | }); 9 | 10 | export default styles; 11 | -------------------------------------------------------------------------------- /src/Components/Code/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type ICodeProps = TextProps; 4 | -------------------------------------------------------------------------------- /src/Components/Delimiter/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import { decode } from 'html-entities'; 3 | 4 | import { styles } from './styles'; 5 | import { IDelimiterProps } from './types'; 6 | 7 | /** Reference https://github.com/editor-js/delimiter */ 8 | const Delimiter = ({ containerStyle, ...rest }: IDelimiterProps) => { 9 | return ( 10 | 15 | {decode('*')} 16 | {decode('*')} 17 | {decode('*')} 18 | 19 | ); 20 | }; 21 | 22 | export { Delimiter }; 23 | -------------------------------------------------------------------------------- /src/Components/Delimiter/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | flexDirection: 'row', 6 | alignItems: 'center', 7 | justifyContent: 'center', 8 | width: '100%', 9 | marginVertical: 8, 10 | paddingVertical: 16, 11 | }, 12 | delimiter: { 13 | marginHorizontal: 6, 14 | fontSize: 24, 15 | color: '#181819', 16 | }, 17 | }); 18 | 19 | export default styles; 20 | -------------------------------------------------------------------------------- /src/Components/Delimiter/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, ViewProps, ViewStyle } from 'react-native'; 2 | 3 | /** Reference https://github.com/editor-js/delimiter */ 4 | export type IDelimiterProps = { 5 | data: Record; 6 | containerStyle?: StyleProp; 7 | } & Omit; 8 | -------------------------------------------------------------------------------- /src/Components/FallbackBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | 3 | import { IFallbackBlockProps } from './types'; 4 | import { styles } from './styles'; 5 | 6 | const FallbackBlock = ({ blockType, containerStyle, ...rest }: IFallbackBlockProps) => { 7 | return ( 8 | 13 | 17 | Type "{blockType}" is yet not supported :c 18 | 19 | 20 | ); 21 | }; 22 | 23 | export { FallbackBlock }; 24 | -------------------------------------------------------------------------------- /src/Components/FallbackBlock/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | display: 'flex', 6 | alignItem: 'center', 7 | justifyContent: 'center', 8 | marginVertical: 8, 9 | width: '100%', 10 | padding: 16, 11 | backgroundColor: '#F4EDED', 12 | borderColor: '#E88285', 13 | borderStyle: 'solid', 14 | borderWidth: 1, 15 | borderRadius: 4, 16 | }, 17 | alertText: { 18 | fontSize: 16, 19 | color: '#E44045', 20 | textAlign: 'center', 21 | } 22 | }); 23 | 24 | export default styles; 25 | -------------------------------------------------------------------------------- /src/Components/FallbackBlock/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, ViewProps, ViewStyle } from 'react-native'; 2 | 3 | export type IFallbackBlockProps = { 4 | blockType: string; 5 | containerStyle?: StyleProp; 6 | } & Omit; 7 | 8 | -------------------------------------------------------------------------------- /src/Components/FullWidthImage/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { Image, LayoutChangeEvent, View } from 'react-native'; 3 | 4 | import style from './styles'; 5 | import { IFullWidthImageProps } from './types'; 6 | 7 | const FullWidthImage = ({ uri, ...props }: IFullWidthImageProps) => { 8 | const [width, setWidth] = useState(0); 9 | const [height, setHeight] = useState(0); 10 | const [hasErrorOnSize, setHasErrorOnSize] = useState(false); 11 | 12 | const onLayout = useCallback((event: LayoutChangeEvent) => { 13 | const containerWidth = event.nativeEvent.layout.width; 14 | 15 | Image.getSize(uri, (w, h) => { 16 | setWidth(containerWidth); 17 | setHeight(containerWidth * h / w); 18 | }, () => { 19 | setHasErrorOnSize(true); 20 | }); 21 | }, [uri]); 22 | 23 | return ( 24 | 25 | {hasErrorOnSize ? ( 26 | 30 | ) : ( 31 | 35 | )} 36 | 37 | ); 38 | }; 39 | 40 | export { FullWidthImage }; 41 | -------------------------------------------------------------------------------- /src/Components/FullWidthImage/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import { MAX_IMAGE_HEIGHT } from '../../constants/sizes'; 4 | 5 | export const styles = StyleSheet.create({ 6 | container: { 7 | width: '100%' 8 | }, 9 | defaultImage: { 10 | height: MAX_IMAGE_HEIGHT, 11 | width: '100%', 12 | resizeMode: 'contain', 13 | } 14 | }); 15 | 16 | export default styles; 17 | -------------------------------------------------------------------------------- /src/Components/FullWidthImage/types.ts: -------------------------------------------------------------------------------- 1 | import { ImageProps } from 'react-native'; 2 | 3 | export type IFullWidthImageProps = { 4 | uri: string; 5 | } & ImageProps; 6 | -------------------------------------------------------------------------------- /src/Components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | import { useMemo } from 'react'; 3 | 4 | import { styles } from './styles'; 5 | import { IHeaderProps } from './types'; 6 | 7 | const Header = ({ data, fontFamily, style, ...rest }: IHeaderProps) => { 8 | const headingStyleByLevel = useMemo(() => styles[`h${data.level}`], []); 9 | 10 | return ( 11 | 23 | {data.text} 24 | 25 | ); 26 | }; 27 | 28 | export { Header }; 29 | -------------------------------------------------------------------------------- /src/Components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | marginVertical: 8, 6 | color: '#181819', 7 | }, 8 | h1: { 9 | fontWeight: '700', 10 | fontSize: 28 11 | }, 12 | h2: { 13 | fontWeight: '700', 14 | fontSize: 24 15 | }, 16 | h3: { 17 | fontWeight: '600', 18 | fontSize: 20 19 | }, 20 | h4: { 21 | fontWeight: '600', 22 | fontSize: 18 23 | }, 24 | h5: { 25 | fontWeight: '600', 26 | fontSize: 16 27 | }, 28 | h6: { 29 | fontWeight: '500', 30 | fontSize: 16 31 | }, 32 | }); 33 | 34 | export default styles; 35 | -------------------------------------------------------------------------------- /src/Components/Header/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type IHeaderProps = { 4 | data: { 5 | level: 1 | 2 | 3 | 4 | 5 | 6; 6 | text: string; 7 | }; 8 | fontFamily?: string; 9 | } & TextProps; 10 | -------------------------------------------------------------------------------- /src/Components/ImageFrame/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | 3 | import { FullWidthImage } from '../FullWidthImage'; 4 | import { IImageFrameProps } from './types'; 5 | import { styles } from './styles'; 6 | 7 | const ImageFrame = ({ data, captionFontFamily, containerStyle, ...rest }: IImageFrameProps) => { 8 | return ( 9 | 10 | 17 | 18 | {data.caption && ( 19 | 20 | {data.caption} 21 | 22 | ) } 23 | 24 | ); 25 | }; 26 | 27 | export { ImageFrame }; 28 | -------------------------------------------------------------------------------- /src/Components/ImageFrame/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | width: '100%', 6 | marginVertical: 8, 7 | alignItems: 'center' 8 | }, 9 | caption: { 10 | width: '100%', 11 | marginTop: 4, 12 | fontSize: 12, 13 | color: '#292929', 14 | textAlign: 'center' 15 | } 16 | }); 17 | 18 | export default styles; 19 | -------------------------------------------------------------------------------- /src/Components/ImageFrame/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, ViewProps, ViewStyle } from 'react-native'; 2 | 3 | export type IImageFrameProps = { 4 | data: { 5 | file: { 6 | url : string 7 | }, 8 | caption?: string; 9 | withBorder?: boolean; 10 | withBackground?: boolean; 11 | stretched?: boolean; 12 | }; 13 | captionFontFamily?: string; 14 | containerStyle?: StyleProp; 15 | } & Omit; 16 | -------------------------------------------------------------------------------- /src/Components/Italic/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | 3 | import { styles } from './styles'; 4 | import { IItalicProps } from './types'; 5 | 6 | const Italic = ({ children, fontFamily }: IItalicProps) => { 7 | return ( 8 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export { Italic }; 21 | -------------------------------------------------------------------------------- /src/Components/Italic/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | italic: { 5 | fontStyle: 'italic' 6 | } 7 | }); 8 | 9 | export default styles; 10 | -------------------------------------------------------------------------------- /src/Components/Italic/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type IItalicProps = { 4 | fontFamily?: string; 5 | } & TextProps; 6 | -------------------------------------------------------------------------------- /src/Components/LinkTool/index.tsx: -------------------------------------------------------------------------------- 1 | import { TouchableOpacity, Linking, View, Text, Image, Alert } from 'react-native'; 2 | import { useCallback } from 'react'; 3 | 4 | import { styles } from './styles'; 5 | import { ILinkToolProps } from './types'; 6 | 7 | const LinkTool = ({ data, containerStyle, ...rest }: ILinkToolProps) => { 8 | const { link, meta } = data; 9 | 10 | const handleOpenLink = useCallback(async (link: string) => { 11 | if (!link) { 12 | Alert.alert('Missing link'); 13 | return; 14 | } 15 | 16 | try { 17 | await Linking.openURL(link); 18 | } catch { 19 | Alert.alert(`Don't know how to open this URL: ${link}`); 20 | } 21 | }, []); 22 | 23 | return ( 24 | handleOpenLink(link)} 32 | {...rest} 33 | > 34 | 35 | 36 | { (meta.title || meta.site_name) && ( 37 | 42 | {meta.title || meta.site_name } 43 | 44 | )} 45 | 46 | { meta.description && ( 47 | 52 | {meta.description} 53 | 54 | )} 55 | 56 | { link && ( 57 | 62 | {link} 63 | 64 | )} 65 | 66 | 67 | 68 | { meta?.image?.url && ( 69 | 75 | )} 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | export { LinkTool }; 83 | -------------------------------------------------------------------------------- /src/Components/LinkTool/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | // Size of dataContainer padding + childs height 4 | const MAX_IMAGE_SIZE = 104; 5 | 6 | export const styles = StyleSheet.create({ 7 | wrapper: { 8 | width: '100%', 9 | marginVertical: 8, 10 | }, 11 | container: { 12 | flexDirection: 'row', 13 | width: '100%', 14 | borderRadius: 4, 15 | borderColor: '#DFDFDE', 16 | borderStyle: 'solid', 17 | borderWidth: 1, 18 | }, 19 | dataContainer: { 20 | flex: 1, 21 | paddingTop: 12, 22 | paddingBottom: 12, 23 | paddingLeft: 12, 24 | }, 25 | title: { 26 | fontWeight: '600', 27 | fontSize: 16, 28 | height: 18 29 | }, 30 | description: { 31 | color: '#7D7C78', 32 | marginTop: 4, 33 | fontSize: 14, 34 | height: 36 35 | }, 36 | link: { 37 | marginTop: 6, 38 | fontSize: 14, 39 | height: 16 40 | }, 41 | imageContainer: { 42 | width: MAX_IMAGE_SIZE, 43 | height: MAX_IMAGE_SIZE, 44 | marginLeft: 12, 45 | alignItems: 'center' 46 | }, 47 | image: { 48 | width: '100%', 49 | height: MAX_IMAGE_SIZE, 50 | resizeMode: 'cover', 51 | }, 52 | }); 53 | 54 | export default styles; 55 | -------------------------------------------------------------------------------- /src/Components/LinkTool/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, TouchableOpacityProps, ViewStyle } from 'react-native'; 2 | 3 | export type ILinkToolProps = { 4 | data: { 5 | link: string; 6 | meta: { 7 | title?: string; 8 | site_name?: string; 9 | description?: string; 10 | image?: { 11 | url?: string; 12 | } 13 | } 14 | }; 15 | containerStyle?: StyleProp; 16 | } & Omit; 17 | 18 | -------------------------------------------------------------------------------- /src/Components/List/ListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Text, View } from 'react-native'; 3 | 4 | import { useParseHtmlTags } from '../../../hooks/useParseHtmlTags'; 5 | import { styles } from './styles'; 6 | import { IItemIListProps } from './types'; 7 | 8 | const ListItem = ({ value, listStyle, index, fontFamily }: IItemIListProps) => { 9 | const { parseHtmlTag, defaultListTags } = useParseHtmlTags(); 10 | 11 | const parsedText = useMemo(() => parseHtmlTag(defaultListTags, value), []); 12 | 13 | return ( 14 | 15 | { 16 | { 17 | 'ordered': , 18 | 'unordered': {index + 1} 19 | }[listStyle] 20 | } 21 | 22 | 31 | {parsedText} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export { ListItem }; 38 | -------------------------------------------------------------------------------- /src/Components/List/ListItem/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | flexDirection: 'row', 6 | alignContent: 'center', 7 | paddingLeft: 12 8 | }, 9 | listItem: { 10 | fontSize: 16, 11 | color: '#292929', 12 | }, 13 | listStyleNumber: { 14 | fontSize: 16, 15 | color: '#292929', 16 | marginRight: 16, 17 | }, 18 | listStyleDot: { 19 | height: 6, 20 | maxHeight: 6, 21 | width: 6, 22 | maxWidth: 6, 23 | marginRight: 12, 24 | borderRadius: 6, 25 | backgroundColor: '#292929', 26 | } 27 | }); 28 | 29 | export default styles; 30 | -------------------------------------------------------------------------------- /src/Components/List/ListItem/types.ts: -------------------------------------------------------------------------------- 1 | import { IListProps } from '../types'; 2 | 3 | export type IItemIListProps = { 4 | value: string; 5 | index: number; 6 | listStyle: IListProps['data']['style']; 7 | fontFamily?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/Components/List/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { SectionList } from 'react-native'; 3 | 4 | import { IListProps } from './types'; 5 | import { styles } from './styles'; 6 | import { ListItem } from './ListItem'; 7 | 8 | const List = ({ data, fontFamily, containerStyle }: IListProps) => { 9 | const sections = useMemo(() => { 10 | return [{ data: data.items }]; 11 | }, []); 12 | 13 | return ( 14 | item + index} 18 | renderItem={({ item, index }) => ( 19 | 25 | )} 26 | /> 27 | ); 28 | }; 29 | 30 | export { List }; 31 | -------------------------------------------------------------------------------- /src/Components/List/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | list: { 5 | marginVertical: 8, 6 | }, 7 | }); 8 | 9 | export default styles; 10 | -------------------------------------------------------------------------------- /src/Components/List/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, ViewProps, ViewStyle } from 'react-native'; 2 | 3 | export type IListProps = { 4 | data: { 5 | items: Array; 6 | style: 'ordered' | 'unordered'; 7 | }; 8 | fontFamily?: string; 9 | containerStyle?: StyleProp; 10 | } & Omit; 11 | -------------------------------------------------------------------------------- /src/Components/Mark/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | 3 | import { styles } from './styles'; 4 | import { IMarkProps } from './types'; 5 | 6 | const Mark = ({ children }: IMarkProps) => { 7 | return ( 8 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { Mark }; 18 | -------------------------------------------------------------------------------- /src/Components/Mark/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | mark: { 5 | backgroundColor: '#FFFBCB' 6 | } 7 | }); 8 | 9 | export default styles; 10 | -------------------------------------------------------------------------------- /src/Components/Mark/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type IMarkProps = TextProps; 4 | 5 | -------------------------------------------------------------------------------- /src/Components/Paragraph/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | import { useParseHtmlTags } from '../../hooks/useParseHtmlTags'; 5 | import { IParagraphProps } from './types'; 6 | import { styles } from './styles'; 7 | 8 | const Paragraph = ({ data, fontFamily, style, ...rest }: IParagraphProps) => { 9 | const { parseHtmlTag, defaultListTags } = useParseHtmlTags(); 10 | 11 | const parsedText = useMemo(() => parseHtmlTag(defaultListTags, data.text), []); 12 | 13 | return ( 14 | 21 | {parsedText} 22 | 23 | ); 24 | }; 25 | 26 | export { Paragraph }; 27 | -------------------------------------------------------------------------------- /src/Components/Paragraph/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | paragraph: { 5 | marginVertical: 8, 6 | fontSize: 16, 7 | color: '#292929', 8 | } 9 | }); 10 | 11 | export default styles; 12 | -------------------------------------------------------------------------------- /src/Components/Paragraph/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type IParagraphProps = { 4 | data: { 5 | text: string; 6 | }; 7 | fontFamily?: string; 8 | } & TextProps; 9 | -------------------------------------------------------------------------------- /src/Components/Personality/index.tsx: -------------------------------------------------------------------------------- 1 | import { TouchableOpacity, Linking, View, Text, Image, Alert } from 'react-native'; 2 | import { useCallback } from 'react'; 3 | 4 | import { styles } from './styles'; 5 | import { IPersonalityProps } from './types'; 6 | 7 | const Personality = ({ data, containerStyle, ...rest }: IPersonalityProps) => { 8 | const { link, description, name, photo } = data; 9 | 10 | const handleOpenLink = useCallback(async (link: string) => { 11 | if (!link) { 12 | Alert.alert('Missing link'); 13 | return; 14 | } 15 | 16 | try { 17 | await Linking.openURL(link); 18 | } catch { 19 | Alert.alert(`Don't know how to open this URL: ${link}`); 20 | } 21 | }, []); 22 | 23 | return ( 24 | handleOpenLink(link)} 32 | {...rest} 33 | > 34 | 35 | 36 | {name && ( 37 | 42 | {name} 43 | 44 | )} 45 | 46 | { description && ( 47 | 52 | {description} 53 | 54 | )} 55 | 56 | { link && ( 57 | 62 | {link} 63 | 64 | )} 65 | 66 | 67 | 68 | { photo && ( 69 | 73 | )} 74 | 75 | 76 | 77 | ); 78 | }; 79 | 80 | export { Personality }; 81 | -------------------------------------------------------------------------------- /src/Components/Personality/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | // Size of dataContainer padding + childs height 4 | const MAX_IMAGE_SIZE = 80; 5 | 6 | export const styles = StyleSheet.create({ 7 | wrapper: { 8 | width: '100%', 9 | marginVertical: 8, 10 | }, 11 | container: { 12 | flexDirection: 'row', 13 | width: '100%', 14 | borderRadius: 4, 15 | borderColor: '#DFDFDE', 16 | borderStyle: 'solid', 17 | borderWidth: 1, 18 | }, 19 | dataContainer: { 20 | flex: 1, 21 | padding: 12, 22 | }, 23 | title: { 24 | fontWeight: '600', 25 | fontSize: 16, 26 | height: 18 27 | }, 28 | description: { 29 | color: '#7D7C78', 30 | marginTop: 4, 31 | fontSize: 14, 32 | height: 36 33 | }, 34 | link: { 35 | marginTop: 6, 36 | fontSize: 14, 37 | height: 16 38 | }, 39 | imageContainer: { 40 | width: MAX_IMAGE_SIZE, 41 | height: MAX_IMAGE_SIZE, 42 | margin: 12, 43 | alignItems: 'center' 44 | }, 45 | image: { 46 | width: MAX_IMAGE_SIZE, 47 | height: MAX_IMAGE_SIZE, 48 | resizeMode: 'cover', 49 | borderRadius: 4, 50 | }, 51 | }); 52 | 53 | export default styles; 54 | -------------------------------------------------------------------------------- /src/Components/Personality/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, TouchableOpacityProps, ViewStyle } from 'react-native'; 2 | 3 | export type IPersonalityProps = { 4 | data: { 5 | name?: string; 6 | description?: string; 7 | link: string; 8 | photo?: string; 9 | }; 10 | containerStyle?: StyleProp; 11 | } & Omit; 12 | 13 | -------------------------------------------------------------------------------- /src/Components/Quote/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | 3 | import { useParseHtmlTags } from '../../hooks/useParseHtmlTags'; 4 | import { IQuoteProps } from './types'; 5 | import { styles } from './styles'; 6 | import { useMemo } from 'react'; 7 | 8 | const Quote = ({ data, quoteFontFamily, captionFontFamily, containerStyle, ...rest }: IQuoteProps) => { 9 | const { parseHtmlTag, defaultListTags } = useParseHtmlTags(); 10 | 11 | const parsedText = useMemo(() => parseHtmlTag(defaultListTags, data.text), []); 12 | 13 | return ( 14 | 15 | 27 | {parsedText} 28 | 29 | 30 | {data.caption && ( 31 | 38 | -{data.caption} 39 | 40 | )} 41 | 42 | ); 43 | }; 44 | 45 | export { Quote }; 46 | -------------------------------------------------------------------------------- /src/Components/Quote/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | width: '100%', 6 | marginVertical: 8, 7 | paddingHorizontal: 12, 8 | paddingVertical: 8, 9 | borderLeftWidth: 4, 10 | borderLeftColor: '#292929', 11 | borderStyle: 'solid', 12 | }, 13 | quoteText: { 14 | fontSize: 16, 15 | color: '#292929', 16 | }, 17 | caption: { 18 | marginTop: 12, 19 | fontSize: 14, 20 | color: '#292929', 21 | }, 22 | }); 23 | 24 | export default styles; 25 | -------------------------------------------------------------------------------- /src/Components/Quote/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, ViewProps, ViewStyle } from 'react-native'; 2 | 3 | export type IQuoteProps = { 4 | data: { 5 | text: string, 6 | caption?: string, 7 | alignment?: 'left' | 'center' 8 | }; 9 | quoteFontFamily?: string; 10 | captionFontFamily?: string; 11 | containerStyle?: StyleProp; 12 | } & Omit; 13 | -------------------------------------------------------------------------------- /src/Components/SimpleImage/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | 3 | import { ISimpleImageProps } from './types'; 4 | import { FullWidthImage } from '../FullWidthImage'; 5 | import { styles } from './styles'; 6 | 7 | const SimpleImage = ({ data, captionFontFamily, containerStyle, ...rest }: ISimpleImageProps) => { 8 | return ( 9 | 13 | 20 | 21 | {data.caption && ( 22 | 23 | {data.caption} 24 | 25 | )} 26 | 27 | ); 28 | }; 29 | 30 | export { SimpleImage }; 31 | -------------------------------------------------------------------------------- /src/Components/SimpleImage/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | width: '100%', 6 | marginVertical: 8, 7 | alignItems: 'center' 8 | }, 9 | caption: { 10 | width: '100%', 11 | marginTop: 4, 12 | fontSize: 12, 13 | color: '#292929', 14 | textAlign: 'center' 15 | } 16 | }); 17 | 18 | export default styles; 19 | -------------------------------------------------------------------------------- /src/Components/SimpleImage/types.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, TextProps, ViewStyle } from 'react-native'; 2 | 3 | export type ISimpleImageProps = { 4 | data: { 5 | url: string; 6 | caption?: string; 7 | withBorder?: boolean; 8 | withBackground?: boolean; 9 | stretched?: boolean; 10 | }; 11 | captionFontFamily?: string; 12 | containerStyle?: StyleProp; 13 | } & Omit; 14 | -------------------------------------------------------------------------------- /src/Components/Underline/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | 3 | import { IUnderlineProps } from './types'; 4 | 5 | const Underline = ({ fontFamily, children }: IUnderlineProps) => { 6 | return ( 7 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export { Underline }; 20 | -------------------------------------------------------------------------------- /src/Components/Underline/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type IUnderlineProps = { 4 | fontFamily?: string; 5 | } & TextProps; 6 | -------------------------------------------------------------------------------- /src/Components/__test__/components/Header.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react-native'; 2 | 3 | import { Header } from '../../Header/index'; 4 | import { headerMock } from '../mock/editorJsOutputData'; 5 | 6 | describe('Header', () => { 7 | it('Should be able to render a Header with value', async () => { 8 | const component = ( 9 |
10 | ); 11 | 12 | render(component); 13 | 14 | const header = await screen.findByText(headerMock.data.text); 15 | 16 | expect(!!header).toBeTruthy(); 17 | }); 18 | 19 | it('Should to have accessibilityRole as header', async () => { 20 | const component = ( 21 |
22 | ); 23 | 24 | render(component); 25 | 26 | expect(await screen.findAllByRole('header')).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/Components/__test__/mock/editorJsOutputData.ts: -------------------------------------------------------------------------------- 1 | import { IHeaderProps } from '../..'; 2 | 3 | export const headerMock: IHeaderProps = { data: { level: 1, text: 'Test header title'} }; 4 | -------------------------------------------------------------------------------- /src/Components/index.tsx: -------------------------------------------------------------------------------- 1 | export { Bold } from './Bold'; 2 | export type { IBoldProps } from './Bold/types'; 3 | 4 | export { Code } from './Code'; 5 | export type { ICodeProps } from './Code/types'; 6 | 7 | export { Delimiter } from './Delimiter'; 8 | export type { IDelimiterProps } from './Delimiter/types'; 9 | 10 | export { FallbackBlock } from './FallbackBlock'; 11 | export type { IFallbackBlockProps } from './FallbackBlock/types'; 12 | 13 | export { Header } from './Header'; 14 | export type { IHeaderProps } from './Header/types'; 15 | 16 | export { ImageFrame } from './ImageFrame'; 17 | export type { IImageFrameProps } from './ImageFrame/types'; 18 | 19 | export { Italic } from './Italic'; 20 | export type { IItalicProps } from './Italic/types'; 21 | 22 | export { LinkTool } from './LinkTool'; 23 | export type { ILinkToolProps } from './LinkTool/types'; 24 | 25 | export { List } from './List'; 26 | export type { IListProps } from './List/types'; 27 | 28 | export { Mark } from './Mark'; 29 | export type { IMarkProps } from './Mark/types'; 30 | 31 | export { Paragraph } from './Paragraph'; 32 | export type { IParagraphProps } from './Paragraph/types'; 33 | 34 | export { Personality } from './Personality'; 35 | export type { IPersonalityProps } from './Personality/types'; 36 | 37 | export { Quote } from './Quote'; 38 | export type { IQuoteProps } from './Quote/types'; 39 | 40 | export { SimpleImage } from './SimpleImage'; 41 | export type { ISimpleImageProps } from './SimpleImage/types'; 42 | 43 | export { Underline } from './Underline'; 44 | export type { IUnderlineProps } from './Underline/types'; 45 | -------------------------------------------------------------------------------- /src/config/createEditorJsViewer.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import { memo, useEffect, useState } from 'react'; 3 | import { View } from 'react-native'; 4 | 5 | import type { IEditorJsViwerNativeProps, ICreateEditorJsViewerProps, IComponentObject, IComponentBlockProps } from '../types'; 6 | import { Delimiter } from '../components/Delimiter'; 7 | import { FallbackBlock } from '../components/FallbackBlock'; 8 | import { Header } from '../components/Header'; 9 | import { ImageFrame } from '../components/ImageFrame'; 10 | import { LinkTool } from '../components/LinkTool'; 11 | import { List } from '../components/List'; 12 | import { Paragraph } from '../components/Paragraph'; 13 | import { Personality } from '../components/Personality'; 14 | import { Quote } from '../components/Quote'; 15 | import { SimpleImage } from '../components/SimpleImage'; 16 | 17 | export const createEditorJsViewer = (props?: ICreateEditorJsViewerProps) => { 18 | const { tools, customTools, showBlockFallback } = props ?? {}; 19 | 20 | return memo(({ data, ...rest }: IEditorJsViwerNativeProps) => { 21 | const [blockComponents, setBlockComponents] = useState(null); 22 | 23 | useEffect(() => { 24 | const componentToolsObj: IComponentObject = { 25 | delimiter: ({ block, containerStyle }: IComponentBlockProps) => { 26 | const CustomHeader = tools?.delimiter?.Component; 27 | return CustomHeader ? ( 28 | 29 | ) : ( 30 | 34 | ); 35 | }, 36 | header: ({ block, containerStyle }: IComponentBlockProps) => { 37 | const CustomHeader = tools?.header?.Component; 38 | return (CustomHeader ? ( 39 | 40 | ) : ( 41 |
46 | )); 47 | }, 48 | image: ({ block, containerStyle }: IComponentBlockProps) => { 49 | const CustomImage = tools?.image?.Component; 50 | return CustomImage ? ( 51 | 52 | ) : ( 53 | 58 | ); 59 | }, 60 | linkTool: ({ block, containerStyle }: IComponentBlockProps) => { 61 | const CustomLinkTool = tools?.linkTool?.Component; 62 | return CustomLinkTool ? ( 63 | 64 | ) : ( 65 | 69 | ); 70 | }, 71 | list: ({ block, containerStyle }: IComponentBlockProps) => { 72 | const CustomList = tools?.list?.Component; 73 | return CustomList ? ( 74 | 75 | ) : ( 76 | 81 | ); 82 | }, 83 | paragraph: ({ block, containerStyle }: IComponentBlockProps) => { 84 | const CustomParagraph = tools?.paragraph?.Component; 85 | return CustomParagraph ? ( 86 | 87 | ) : ( 88 | 93 | ); 94 | }, 95 | personality: ({ block, containerStyle }: IComponentBlockProps) => { 96 | const CustomPersonality = tools?.personality?.Component; 97 | return CustomPersonality ? ( 98 | 99 | ) : ( 100 | 104 | ); 105 | }, 106 | simpleImage: ({ block, containerStyle }: IComponentBlockProps) => { 107 | const CustomSimpleImage = tools?.simpleImage?.Component; 108 | return CustomSimpleImage ? ( 109 | 110 | ) : ( 111 | 116 | ); 117 | }, 118 | quote: ({ block, containerStyle }: IComponentBlockProps) => { 119 | const CustomQuote = tools?.quote?.Component ?? Quote; 120 | 121 | return CustomQuote ? ( 122 | 123 | ) : ( 124 | 130 | ); 131 | }, 132 | }; 133 | 134 | if (customTools) { 135 | Object.entries(customTools).forEach(([key, value]) => { 136 | if (value.Component) { 137 | componentToolsObj[key] = value.Component; 138 | } 139 | }); 140 | } 141 | 142 | setBlockComponents(componentToolsObj); 143 | }, [customTools]); 144 | 145 | if (!blockComponents) return null; 146 | 147 | return ( 148 | 149 | {data.blocks.map((block, index) => { 150 | // Some simpleImage type can be named as `image` but has a difference betwen 'normal' image 151 | if (block.type == 'image' && block.data?.file == null) { 152 | block.type = 'simpleImage'; 153 | } 154 | 155 | const isFirstBlock = index == 0; 156 | const isLastBlock = index == data.blocks.length - 1; 157 | 158 | // Removing default margin top/bottom from first/last element 159 | const overrideMarginIfIsFirstOrLastElement = { 160 | marginTop: isFirstBlock ? 0 : undefined, 161 | marginBottom: isLastBlock ? 0 : undefined 162 | }; 163 | 164 | const ComponentByType = blockComponents[block.type]; 165 | 166 | if (ComponentByType) { 167 | return ( 168 | 173 | ); 174 | } 175 | 176 | return showBlockFallback ? ( 177 | 182 | ) : ( 183 | null 184 | ); 185 | })} 186 | 187 | ); 188 | }); 189 | }; 190 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { createEditorJsViewer } from './createEditorJsViewer'; 2 | -------------------------------------------------------------------------------- /src/constants/sizes.ts: -------------------------------------------------------------------------------- 1 | export const MAX_IMAGE_HEIGHT = 180; 2 | -------------------------------------------------------------------------------- /src/hooks/useParseHtmlTags.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, ReactNode, Fragment, useCallback } from 'react'; 2 | import { Text } from 'react-native'; 3 | import { decode } from 'html-entities'; 4 | 5 | import { Bold } from '../components/Bold'; 6 | import { Code } from '../components/Code'; 7 | import { Italic } from '../components/Italic'; 8 | import { Mark } from '../components/Mark'; 9 | import { Underline } from '../components/Underline'; 10 | 11 | // ? This default list should not be responsibility of this hook ? 12 | const defaultListTags = [ 13 | 'b', 14 | 'code', 15 | 'i', 16 | 'mark', 17 | 'u', 18 | ]; 19 | 20 | export const useParseHtmlTags = () => { 21 | const getTagName = useCallback((value: string) => { 22 | const startTagName = value.trim().substring(1); 23 | const indexOfEndTagName = startTagName.match(/ (.*?)>/)?.index ?? 1; 24 | 25 | return startTagName.substring(0, Number(indexOfEndTagName) ?? 1); 26 | }, []); 27 | 28 | const getComponentByName = useCallback((name: string) => { 29 | switch (name) { 30 | case 'b': return Bold; 31 | case 'code': return Code; 32 | case 'i': return Italic; 33 | case 'mark': return Mark; 34 | case 'u': return Underline; 35 | 36 | default: return Text; 37 | } 38 | }, []); 39 | 40 | const createElementByTagName = 41 | useCallback((tagChildren: string | ReactNode, tagName: string): ReactNode => { 42 | return createElement( 43 | getComponentByName(tagName), 44 | null, 45 | tagChildren 46 | ); 47 | }, [getComponentByName] 48 | ); 49 | 50 | const matchTagInList = 51 | useCallback((listOfTags: Array, text: string): RegExpMatchArray | null => { 52 | const { match } = listOfTags.reduce((acum, tag) => { 53 | const currentMatch = text.match(new RegExp(`<${tag}(.*?)>`)); 54 | 55 | if (!currentMatch) return acum; 56 | 57 | if (acum.match == null) { 58 | acum.match = currentMatch; 59 | return acum; 60 | } 61 | 62 | const currentMatchIsPreviousAcumMatch = 63 | currentMatch.index != null && 64 | acum.match.index != null && 65 | currentMatch.index < acum.match.index; 66 | 67 | if (currentMatchIsPreviousAcumMatch) { 68 | acum.match = currentMatch; 69 | return acum; 70 | } 71 | 72 | return acum; 73 | }, { match: null } as { match: RegExpMatchArray | null }); 74 | 75 | return match; 76 | }, [] 77 | ); 78 | 79 | const parseHtmlTag = 80 | useCallback((listOfTags: Array, value: string): ReactNode => { 81 | const firstMatchTag = matchTagInList(listOfTags, value); 82 | 83 | if (firstMatchTag == null || firstMatchTag?.index == null) { 84 | return {decode(value)}; 85 | } 86 | 87 | const stringAfterTargetTag = 88 | value.substring(firstMatchTag.index + firstMatchTag[0].length); 89 | 90 | const tagName = getTagName(firstMatchTag[0]); 91 | 92 | const closeTag = stringAfterTargetTag.match(new RegExp(``)); 93 | 94 | const nextOpenTag = matchTagInList(listOfTags, stringAfterTargetTag); 95 | 96 | if ( 97 | closeTag?.index == null || 98 | (closeTag.index == null && nextOpenTag?.index == null) 99 | ) { 100 | return ( 101 | {decode(value)} 102 | ); 103 | } 104 | 105 | // If does not exists anyone open tag 106 | if ( 107 | nextOpenTag?.index == null || 108 | (closeTag.index < nextOpenTag.index) 109 | ) { 110 | const textBeforeTag = value.substring(0, firstMatchTag.index); 111 | const tagText = stringAfterTargetTag.substring(0, closeTag.index); 112 | const textAfterTag = stringAfterTargetTag.substring(closeTag.index + closeTag[0].length); 113 | 114 | return ( 115 | 116 | {textBeforeTag && decode(textBeforeTag)} 117 | {createElementByTagName(decode(tagText), tagName)} 118 | {textAfterTag && parseHtmlTag(listOfTags, textAfterTag)} 119 | 120 | ); 121 | } 122 | 123 | const textBeforeTag = value.substring(0, firstMatchTag.index); 124 | const tagText = value.substring(firstMatchTag.index + firstMatchTag[0].length); 125 | const textAfterTag = stringAfterTargetTag.substring(closeTag.index + closeTag[0].length); 126 | 127 | return ( 128 | 129 | {textBeforeTag && decode(textBeforeTag)} 130 | {createElementByTagName( 131 | parseHtmlTag(listOfTags, tagText.substring(0, closeTag.index)), 132 | tagName 133 | )} 134 | {textAfterTag && parseHtmlTag(listOfTags, textAfterTag)} 135 | 136 | ); 137 | }, [matchTagInList, getTagName, createElementByTagName ] 138 | ); 139 | 140 | return { 141 | defaultListTags, 142 | parseHtmlTag, 143 | }; 144 | }; 145 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './config'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/types/createEditorJsViewerProps.ts: -------------------------------------------------------------------------------- 1 | import { StyleProp, ViewStyle } from 'react-native'; 2 | import type { 3 | IDelimiterProps, 4 | IHeaderProps, 5 | IImageFrameProps, 6 | ILinkToolProps, 7 | IListProps, 8 | IParagraphProps, 9 | IPersonalityProps, 10 | ISimpleImageProps, 11 | IQuoteProps 12 | } from '../components'; 13 | import { OutputBlockData } from './editorJsDataProps'; 14 | 15 | export interface IComponentBlockProps { 16 | block: OutputBlockData; 17 | containerStyle: StyleProp; 18 | } 19 | 20 | export interface IComponentObject { 21 | [key: string]: (param: IComponentBlockProps) => JSX.Element; 22 | } 23 | 24 | export interface ICreateEditorJsViewerProps { 25 | tools?: IToolsParser; 26 | customTools?: ICustomToolsParser; 27 | /** Show a fallback Component when type of block is `unknown`. Default is `false`*/ 28 | showBlockFallback?: boolean; 29 | } 30 | 31 | export interface IToolsParser { 32 | delimiter?: { 33 | /** 34 | * A component with type {@link IDelimiterProps} or any for your custom header tool 35 | */ 36 | Component?: (props: IDelimiterProps | any ) => JSX.Element; 37 | }, 38 | header?: { 39 | /** 40 | * A component with type {@link IHeaderProps} or any for your custom header tool 41 | */ 42 | Component?: (props: IHeaderProps | any ) => JSX.Element; 43 | /** 44 | * This prop will be ignored if you use a CustomComponent 45 | */ 46 | fontFamily?: string; 47 | } 48 | image?: { 49 | /** 50 | * A component with type {@link IImageFrameProps} or any for your custom image tool 51 | */ 52 | Component: (props: IImageFrameProps | any ) => JSX.Element; 53 | /** 54 | * This prop will be ignored if you use a CustomComponent 55 | */ 56 | captionFontFamily?: string; 57 | } 58 | linkTool?: { 59 | /** 60 | * A component with type {@link ILinkToolProps} or any for your custom linkTool tool 61 | */ 62 | Component?: (props: ILinkToolProps | any ) => JSX.Element; 63 | } 64 | /** 65 | * Is not recommended to replace this component because he need a html parser to RN JSX 66 | */ 67 | list?: { 68 | /** 69 | * A component with type {@link IListProps} or any for your custom list tool 70 | */ 71 | Component?: (prop: IListProps | any ) => JSX.Element; 72 | /** 73 | * This prop will be ignored if you use a CustomComponent 74 | */ 75 | fontFamily?: string; 76 | } 77 | /** 78 | * Is not recommended to replace this component because he need a html parser to RN JSX 79 | */ 80 | paragraph?: { 81 | /** 82 | * A component with type {@link IParagraphProps} or any for your custom paragraph tool 83 | */ 84 | Component?: (props: IParagraphProps | any ) => JSX.Element; 85 | /** 86 | * This prop will be ignored if you use a CustomComponent 87 | */ 88 | fontFamily?: string; 89 | } 90 | personality?: { 91 | /** 92 | * A component with type {@link IPersonalityProps} or any for your custom paragraph tool 93 | */ 94 | Component?: (props: IPersonalityProps | any ) => JSX.Element; 95 | } 96 | simpleImage?: { 97 | /** 98 | * A component with type {@link ISimpleImageProps } or any for your custom simpleImage tool 99 | */ 100 | Component?: (props: ISimpleImageProps | any ) => JSX.Element; 101 | /** 102 | * This prop will be ignored if you use a CustomComponent 103 | */ 104 | captionFontFamily?: string; 105 | } 106 | quote?: { 107 | /** 108 | * A component with type {@link IQuoteProps } or any for your custom simpleImage tool 109 | */ 110 | Component?: (props: IQuoteProps | any ) => JSX.Element; 111 | /** 112 | * This prop will be ignored if you use a CustomComponent 113 | */ 114 | quoteFontFamily?: string; 115 | /** 116 | * This prop will be ignored if you use a CustomComponent 117 | */ 118 | captionFontFamily?: string; 119 | } 120 | } 121 | 122 | export interface ICustomToolsParser { 123 | [key: string]: { 124 | Component?: (param: IComponentBlockProps) => JSX.Element; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/types/editorJsDataProps.ts: -------------------------------------------------------------------------------- 1 | // Interfaces from EditorJs v2.26.4 2 | // Copy of OutputData 3 | export interface IEditorJsData { 4 | /** 5 | * Editor's version 6 | */ 7 | version?: string; 8 | 9 | /** 10 | * Timestamp of saving in milliseconds 11 | */ 12 | time?: number; 13 | 14 | /** 15 | * Saved Blocks 16 | */ 17 | blocks: OutputBlockData[]; 18 | } 19 | 20 | export interface OutputBlockData< 21 | Type extends string = string, 22 | Data extends object = any 23 | > { 24 | /** 25 | * Unique Id of the block 26 | */ 27 | id?: string; 28 | /** 29 | * Tool type 30 | */ 31 | type: Type; 32 | /** 33 | * Saved Block data 34 | */ 35 | data: BlockToolData; 36 | 37 | /** 38 | * Block Tunes data 39 | */ 40 | tunes?: { [name: string]: BlockTuneData }; 41 | } 42 | 43 | export type BlockToolData = T; 44 | 45 | export type BlockTuneData = any; 46 | -------------------------------------------------------------------------------- /src/types/editorJsViwerNative.ts: -------------------------------------------------------------------------------- 1 | import { ViewProps } from 'react-native'; 2 | 3 | import { IEditorJsData } from './editorJsDataProps'; 4 | 5 | export type IEditorJsViwerNativeProps = { 6 | data: IEditorJsData; 7 | } & Pick; 8 | 9 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { ICreateEditorJsViewerProps, IToolsParser, IComponentBlockProps, IComponentObject, ICustomToolsParser } from './createEditorJsViewerProps'; 2 | export type { IEditorJsData } from './editorJsDataProps'; 3 | export type { IEditorJsViwerNativeProps } from './editorJsViwerNative'; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "jsx": "react-native", 9 | "skipLibCheck": true, 10 | "moduleResolution": "node" 11 | }, 12 | "include": [ 13 | "src" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "**/__tests__/*" 18 | ], 19 | "extends": "expo/tsconfig.base" 20 | } 21 | --------------------------------------------------------------------------------