├── .npmignore ├── doc └── images │ ├── ios-1.png │ ├── ios-2.png │ ├── ios-3.png │ ├── ios-4.png │ ├── ios-5.png │ ├── ios-6.png │ ├── ios-7.png │ ├── ios-8.png │ ├── ios-9.png │ ├── ios-10.png │ ├── android-1.png │ ├── android-10.png │ ├── android-2.png │ ├── android-3.png │ ├── android-4.png │ ├── android-5.png │ ├── android-6.png │ ├── android-7.png │ ├── android-8.png │ ├── android-9.png │ └── style-example.png ├── .gitignore ├── .prettierrc.js ├── src ├── lib │ ├── util │ │ ├── getUniqueID.js │ │ ├── Token.js │ │ ├── hasParents.js │ │ ├── stringToTokens.js │ │ ├── openUrl.js │ │ ├── splitTextNonTextNodes.js │ │ ├── removeTextStyleProps.js │ │ ├── renderInlineAsText.js │ │ ├── flattenInlineTokens.js │ │ ├── convertAdditionalStyles.js │ │ ├── groupTextTokens.js │ │ ├── getTokenTypeByToken.js │ │ ├── omitListItemParagraph.js │ │ ├── cleanupTokens.js │ │ └── tokensToAST.js │ ├── data │ │ └── textStyleProps.js │ ├── parser.js │ ├── styles.js │ ├── AstRenderer.js │ └── renderRules.js ├── index.d.ts └── index.js ├── .eslintrc.js ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | /example 2 | /.idea 3 | /bin 4 | /docs 5 | /doc 6 | -------------------------------------------------------------------------------- /doc/images/ios-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-1.png -------------------------------------------------------------------------------- /doc/images/ios-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-2.png -------------------------------------------------------------------------------- /doc/images/ios-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-3.png -------------------------------------------------------------------------------- /doc/images/ios-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-4.png -------------------------------------------------------------------------------- /doc/images/ios-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-5.png -------------------------------------------------------------------------------- /doc/images/ios-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-6.png -------------------------------------------------------------------------------- /doc/images/ios-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-7.png -------------------------------------------------------------------------------- /doc/images/ios-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-8.png -------------------------------------------------------------------------------- /doc/images/ios-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-9.png -------------------------------------------------------------------------------- /doc/images/ios-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/ios-10.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | package-lock.json 4 | /.DS_Store 5 | yarn-error.log 6 | yarn.lock 7 | .eslintcache 8 | -------------------------------------------------------------------------------- /doc/images/android-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-1.png -------------------------------------------------------------------------------- /doc/images/android-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-10.png -------------------------------------------------------------------------------- /doc/images/android-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-2.png -------------------------------------------------------------------------------- /doc/images/android-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-3.png -------------------------------------------------------------------------------- /doc/images/android-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-4.png -------------------------------------------------------------------------------- /doc/images/android-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-5.png -------------------------------------------------------------------------------- /doc/images/android-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-6.png -------------------------------------------------------------------------------- /doc/images/android-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-7.png -------------------------------------------------------------------------------- /doc/images/android-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-8.png -------------------------------------------------------------------------------- /doc/images/android-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/android-9.png -------------------------------------------------------------------------------- /doc/images/style-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasmerlin/react-native-markdown-display/HEAD/doc/images/style-example.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/util/getUniqueID.js: -------------------------------------------------------------------------------- 1 | let uuid = new Date().getTime(); 2 | 3 | export default function getUniqueID() { 4 | uuid++; 5 | return `rnmr_${uuid.toString(16)}`; 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | settings: { 5 | react: { 6 | version: require('./package.json').peerDependencies.react, 7 | }, 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/util/Token.js: -------------------------------------------------------------------------------- 1 | export default class Token { 2 | constructor(type, nesting = 0, children = null, block = false) { 3 | this.type = type; 4 | this.nesting = nesting; 5 | this.children = children; 6 | this.block = block; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/util/hasParents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {Array} parents 4 | * @param {string} type 5 | * @return {boolean} 6 | */ 7 | export default function hasParents(parents, type) { 8 | return parents.findIndex((el) => el.type === type) > -1; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/util/stringToTokens.js: -------------------------------------------------------------------------------- 1 | export function stringToTokens(source, markdownIt) { 2 | let result = []; 3 | try { 4 | result = markdownIt.parse(source, {}); 5 | } catch (err) { 6 | console.warn(err); 7 | } 8 | 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/util/openUrl.js: -------------------------------------------------------------------------------- 1 | import {Linking} from 'react-native'; 2 | 3 | export default function openUrl(url, customCallback) { 4 | if (customCallback) { 5 | const result = customCallback(url); 6 | if (url && result && typeof result === 'boolean') { 7 | Linking.openURL(url); 8 | } 9 | } else if (url) { 10 | Linking.openURL(url); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/util/splitTextNonTextNodes.js: -------------------------------------------------------------------------------- 1 | export default function splitTextNonTextNodes(children) { 2 | return children.reduce( 3 | (acc, curr) => { 4 | if (curr.type.displayName === 'Text') { 5 | acc.textNodes.push(curr); 6 | } else { 7 | acc.nonTextNodes.push(curr); 8 | } 9 | 10 | return acc; 11 | }, 12 | {textNodes: [], nonTextNodes: []}, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/util/removeTextStyleProps.js: -------------------------------------------------------------------------------- 1 | import textStyleProps from '../data/textStyleProps'; 2 | 3 | export default function removeTextStyleProps(style) { 4 | const intersection = textStyleProps.filter((value) => 5 | Object.keys(style).includes(value), 6 | ); 7 | 8 | const obj = {...style}; 9 | 10 | intersection.forEach((value) => { 11 | delete obj[value]; 12 | }); 13 | 14 | return obj; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/util/renderInlineAsText.js: -------------------------------------------------------------------------------- 1 | export default function renderInlineAsText(tokens) { 2 | var result = ''; 3 | 4 | for (var i = 0, len = tokens.length; i < len; i++) { 5 | if (tokens[i].type === 'text') { 6 | result += tokens[i].content; 7 | } else if (tokens[i].type === 'image') { 8 | result += renderInlineAsText(tokens[i].children); 9 | } 10 | } 11 | 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/util/flattenInlineTokens.js: -------------------------------------------------------------------------------- 1 | export default function flattenTokens(tokens) { 2 | return tokens.reduce((acc, curr) => { 3 | if (curr.type === 'inline' && curr.children && curr.children.length > 0) { 4 | const children = flattenTokens(curr.children); 5 | while (children.length) { 6 | acc.push(children.shift()); 7 | } 8 | } else { 9 | acc.push(curr); 10 | } 11 | 12 | return acc; 13 | }, []); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/data/textStyleProps.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'textShadowOffset', 3 | 'color', 4 | 'fontSize', 5 | 'fontStyle', 6 | 'fontWeight', 7 | 'lineHeight', 8 | 'textAlign', 9 | 'textDecorationLine', 10 | 'textShadowColor', 11 | 'fontFamily', 12 | 'textShadowRadius', 13 | 'includeFontPadding', 14 | 'textAlignVertical', 15 | 'fontVariant', 16 | 'letterSpacing', 17 | 'textDecorationColor', 18 | 'textDecorationStyle', 19 | 'textTransform', 20 | 'writingDirection', 21 | ]; 22 | -------------------------------------------------------------------------------- /src/lib/util/convertAdditionalStyles.js: -------------------------------------------------------------------------------- 1 | import cssToReactNative from 'css-to-react-native'; 2 | 3 | export default function convertAdditionalStyles(style) { 4 | const rules = style.split(';'); 5 | 6 | const tuples = rules 7 | .map((rule) => { 8 | let [key, value] = rule.split(':'); 9 | 10 | if (key && value) { 11 | key = key.trim(); 12 | value = value.trim(); 13 | return [key, value]; 14 | } else { 15 | return null; 16 | } 17 | }) 18 | .filter((x) => { 19 | return x != null; 20 | }); 21 | 22 | const conv = cssToReactNative(tuples); 23 | 24 | return conv; 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/util/groupTextTokens.js: -------------------------------------------------------------------------------- 1 | import Token from './Token'; 2 | 3 | export default function groupTextTokens(tokens) { 4 | const result = []; 5 | 6 | let hasGroup = false; 7 | 8 | tokens.forEach((token, index) => { 9 | if (!token.block && !hasGroup) { 10 | hasGroup = true; 11 | result.push(new Token('textgroup', 1)); 12 | result.push(token); 13 | } else if (!token.block && hasGroup) { 14 | result.push(token); 15 | } else if (token.block && hasGroup) { 16 | hasGroup = false; 17 | result.push(new Token('textgroup', -1)); 18 | result.push(token); 19 | } else { 20 | result.push(token); 21 | } 22 | }); 23 | 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/parser.js: -------------------------------------------------------------------------------- 1 | import tokensToAST from './util/tokensToAST'; 2 | import {stringToTokens} from './util/stringToTokens'; 3 | import {cleanupTokens} from './util/cleanupTokens'; 4 | import groupTextTokens from './util/groupTextTokens'; 5 | import omitListItemParagraph from './util/omitListItemParagraph'; 6 | 7 | /** 8 | * 9 | * @param {string} source 10 | * @param {function} [renderer] 11 | * @param {AstRenderer} [markdownIt] 12 | * @return {View} 13 | */ 14 | export default function parser(source, renderer, markdownIt) { 15 | if (Array.isArray(source)) { 16 | return renderer(source); 17 | } 18 | 19 | let tokens = stringToTokens(source, markdownIt); 20 | tokens = cleanupTokens(tokens); 21 | tokens = groupTextTokens(tokens); 22 | tokens = omitListItemParagraph(tokens); 23 | 24 | const astTree = tokensToAST(tokens); 25 | 26 | return renderer(astTree); 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/util/getTokenTypeByToken.js: -------------------------------------------------------------------------------- 1 | const regSelectOpenClose = /_open|_close/g; 2 | 3 | /** 4 | * 5 | * @example { 6 | "type": "heading_open", 7 | "tag": "h1", 8 | "attrs": null, 9 | "map": [ 10 | 1, 11 | 2 12 | ], 13 | "nesting": 1, 14 | "level": 0, 15 | "children": null, 16 | "content": "", 17 | "markup": "#", 18 | "info": "", 19 | "meta": null, 20 | "block": true, 21 | "hidden": false 22 | } 23 | * @param token 24 | * @return {String} 25 | */ 26 | export default function getTokenTypeByToken(token) { 27 | let cleanedType = 'unknown'; 28 | 29 | if (token.type) { 30 | cleanedType = token.type.replace(regSelectOpenClose, ''); 31 | } 32 | 33 | switch (cleanedType) { 34 | case 'heading': { 35 | cleanedType = `${cleanedType}${token.tag.substr(1)}`; 36 | break; 37 | } 38 | default: { 39 | break; 40 | } 41 | } 42 | 43 | return cleanedType; 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 - 2019 Mient-jan Stelling and Tom Pickard 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 | -------------------------------------------------------------------------------- /src/lib/util/omitListItemParagraph.js: -------------------------------------------------------------------------------- 1 | export default function omitListItemParagraph(tokens) { 2 | // used to ensure that we remove the correct ending paragraph token 3 | let depth = null; 4 | return tokens.filter((token, index) => { 5 | // update depth if we've already removed a starting paragraph token 6 | if (depth !== null) { 7 | depth = depth + token.nesting; 8 | } 9 | 10 | // check for a list_item token followed by paragraph token (to remove) 11 | if (token.type === 'list_item' && token.nesting === 1 && depth === null) { 12 | const next = index + 1 in tokens ? tokens[index + 1] : null; 13 | if (next && next.type === 'paragraph' && next.nesting === 1) { 14 | depth = 0; 15 | return true; 16 | } 17 | } else if (token.type === 'paragraph') { 18 | if (token.nesting === 1 && depth === 1) { 19 | // remove the paragraph token immediately after the list_item token 20 | return false; 21 | } else if (token.nesting === -1 && depth === 0) { 22 | // remove the ending paragraph token; reset depth 23 | depth = null; 24 | return false; 25 | } 26 | } 27 | return true; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/util/cleanupTokens.js: -------------------------------------------------------------------------------- 1 | import getTokenTypeByToken from './getTokenTypeByToken'; 2 | import flattenInlineTokens from './flattenInlineTokens'; 3 | import renderInlineAsText from './renderInlineAsText'; 4 | 5 | export function cleanupTokens(tokens) { 6 | tokens = flattenInlineTokens(tokens); 7 | tokens.forEach((token) => { 8 | token.type = getTokenTypeByToken(token); 9 | 10 | // set image and hardbreak to block elements 11 | if (token.type === 'image' || token.type === 'hardbreak') { 12 | token.block = true; 13 | } 14 | 15 | // Set img alt text 16 | if (token.type === 'image') { 17 | token.attrs[token.attrIndex('alt')][1] = renderInlineAsText( 18 | token.children, 19 | ); 20 | } 21 | }); 22 | 23 | /** 24 | * changing a link token to a blocklink to fix issue where link tokens with 25 | * nested non text tokens breaks component 26 | */ 27 | const stack = []; 28 | tokens = tokens.reduce((acc, token, index) => { 29 | if (token.type === 'link' && token.nesting === 1) { 30 | stack.push(token); 31 | } else if ( 32 | stack.length > 0 && 33 | token.type === 'link' && 34 | token.nesting === -1 35 | ) { 36 | if (stack.some((stackToken) => stackToken.block)) { 37 | stack[0].type = 'blocklink'; 38 | stack[0].block = true; 39 | token.type = 'blocklink'; 40 | token.block = true; 41 | } 42 | 43 | stack.push(token); 44 | 45 | while (stack.length) { 46 | acc.push(stack.shift()); 47 | } 48 | } else if (stack.length > 0) { 49 | stack.push(token); 50 | } else { 51 | acc.push(token); 52 | } 53 | 54 | return acc; 55 | }, []); 56 | 57 | return tokens; 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jonasmerlin/react-native-markdown-display", 3 | "version": "1.0.2", 4 | "description": "Markdown renderer for react-native, with CommonMark spec support + adds syntax extensions & sugar (URL autolinking, typographer), originally created by Mient-jan Stelling as react-native-markdown-renderer", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "scripts": { 8 | "lint": "eslint --fix --cache ./src" 9 | }, 10 | "pre-commit": [ 11 | "lint" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/jonasmerlin/react-native-markdown-display.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "react-native", 20 | "native", 21 | "markdown", 22 | "commonmark", 23 | "markdown-it" 24 | ], 25 | "author": "Mient-jan Stelling and Tom Pickard + others from the community", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/jonasmerlin/react-native-markdown-display/issues" 29 | }, 30 | "homepage": "https://github.com/jonasmerlin/react-native-markdown-display/", 31 | "dependencies": { 32 | "css-to-react-native": "^3.0.0", 33 | "markdown-it": "^13.0.1", 34 | "prop-types": "^15.8.1", 35 | "react-native-fit-image": "^1.5.5" 36 | }, 37 | "peerDependencies": { 38 | "react": "^16.2.0 || ^18.0.0", 39 | "react-native": ">=0.50.4" 40 | }, 41 | "devDependencies": { 42 | "@types/markdown-it": "^12.2.3", 43 | "@types/react-native": ">=0.67.7", 44 | "@babel/core": "^7.17.10", 45 | "@babel/runtime": "^7.17.9", 46 | "@react-native-community/eslint-config": "^3.0.2", 47 | "@typescript-eslint/parser": "^5.23.0", 48 | "eslint": "^8.15.0", 49 | "json-schema": "^0.4.0", 50 | "pre-commit": "1.2.2", 51 | "typescript": "^4.6.4" 52 | }, 53 | "directories": { 54 | "doc": "doc" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/util/tokensToAST.js: -------------------------------------------------------------------------------- 1 | import getUniqueID from './getUniqueID'; 2 | import getTokenTypeByToken from './getTokenTypeByToken'; 3 | 4 | /** 5 | * 6 | * @param {{type: string, tag:string, content: string, children: *, attrs: Array, meta, info, block: boolean}} token 7 | * @param {number} tokenIndex 8 | * @return {{type: string, content, tokenIndex: *, index: number, attributes: {}, children: *}} 9 | */ 10 | function createNode(token, tokenIndex) { 11 | const type = getTokenTypeByToken(token); 12 | const content = token.content; 13 | 14 | let attributes = {}; 15 | 16 | if (token.attrs) { 17 | attributes = token.attrs.reduce((prev, curr) => { 18 | const [name, value] = curr; 19 | return {...prev, [name]: value}; 20 | }, {}); 21 | } 22 | 23 | return { 24 | type, 25 | sourceType: token.type, 26 | sourceInfo: token.info, 27 | sourceMeta: token.meta, 28 | block: token.block, 29 | markup: token.markup, 30 | key: getUniqueID() + '_' + type, 31 | content, 32 | tokenIndex, 33 | index: 0, 34 | attributes, 35 | children: tokensToAST(token.children), 36 | }; 37 | } 38 | 39 | /** 40 | * 41 | * @param {Array<{type: string, tag:string, content: string, children: *, attrs: Array}>}tokens 42 | * @return {Array} 43 | */ 44 | export default function tokensToAST(tokens) { 45 | let stack = []; 46 | let children = []; 47 | 48 | if (!tokens || tokens.length === 0) { 49 | return []; 50 | } 51 | 52 | for (let i = 0; i < tokens.length; i++) { 53 | const token = tokens[i]; 54 | const astNode = createNode(token, i); 55 | 56 | if ( 57 | !( 58 | astNode.type === 'text' && 59 | astNode.children.length === 0 && 60 | astNode.content === '' 61 | ) 62 | ) { 63 | astNode.index = children.length; 64 | 65 | if (token.nesting === 1) { 66 | children.push(astNode); 67 | stack.push(children); 68 | children = astNode.children; 69 | } else if (token.nesting === -1) { 70 | children = stack.pop(); 71 | } else if (token.nesting === 0) { 72 | children.push(astNode); 73 | } 74 | } 75 | } 76 | 77 | return children; 78 | } 79 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import MarkdownIt from 'markdown-it'; 3 | import Token from 'markdown-it/lib/token'; 4 | import {ComponentType, ReactNode} from 'react'; 5 | import {StyleSheet, View} from 'react-native'; 6 | 7 | export function getUniqueID(): string; 8 | export function openUrl(url: string): void; 9 | 10 | export function hasParents(parents: any[], type: string): boolean; 11 | 12 | export type RenderFunction = ( 13 | node: ASTNode, 14 | children: ReactNode[], 15 | parentNodes: ASTNode[], 16 | styles: any, 17 | styleObj?: any, 18 | // must have this so that we can have fixed overrides with more arguments 19 | ...args: any 20 | ) => ReactNode; 21 | 22 | export type RenderLinkFunction = ( 23 | node: ASTNode, 24 | children: ReactNode[], 25 | parentNodes: ASTNode[], 26 | styles: any, 27 | onLinkPress?: (url: string) => boolean, 28 | ) => ReactNode; 29 | 30 | export type RenderImageFunction = ( 31 | node: ASTNode, 32 | children: ReactNode[], 33 | parentNodes: ASTNode[], 34 | styles: any, 35 | allowedImageHandlers: string[], 36 | defaultImageHandler: string, 37 | ) => ReactNode; 38 | 39 | export interface RenderRules { 40 | [name: string]: RenderFunction | undefined; 41 | link?: RenderLinkFunction; 42 | blocklink?: RenderLinkFunction; 43 | image?: RenderImageFunction; 44 | } 45 | 46 | export const renderRules: RenderRules; 47 | 48 | export interface MarkdownParser { 49 | parse: (value: string, options: any) => Token[]; 50 | } 51 | 52 | export interface ASTNode { 53 | type: string; 54 | sourceType: string; // original source token name 55 | key: string; 56 | content: string; 57 | markup: string; 58 | tokenIndex: number; 59 | index: number; 60 | attributes: Record; 61 | children: ASTNode[]; 62 | } 63 | 64 | export class AstRenderer { 65 | constructor(renderRules: RenderRules, style?: any); 66 | getRenderFunction(type: string): RenderFunction; 67 | renderNode(node: any, parentNodes: ReadonlyArray): ReactNode; 68 | render(nodes: ReadonlyArray): View; 69 | } 70 | 71 | export function parser( 72 | source: string, 73 | renderer: (node: ASTNode) => View, 74 | parser: MarkdownParser, 75 | ): any; 76 | 77 | export function stringToTokens( 78 | source: string, 79 | markdownIt: MarkdownParser, 80 | ): Token[]; 81 | 82 | export function tokensToAST(tokens: ReadonlyArray): ASTNode[]; 83 | 84 | export interface MarkdownProps { 85 | children?: ReactNode; 86 | rules?: RenderRules; 87 | style?: StyleSheet.NamedStyles; 88 | renderer?: AstRenderer; 89 | markdownit?: MarkdownIt; 90 | mergeStyle?: boolean; 91 | debugPrintTree?: boolean; 92 | onLinkPress?: (url: string) => boolean; 93 | } 94 | 95 | type MarkdownStatic = ComponentType; 96 | export const Markdown: MarkdownStatic; 97 | export type Markdown = MarkdownStatic; 98 | export {MarkdownIt}; 99 | export default Markdown; 100 | -------------------------------------------------------------------------------- /src/lib/styles.js: -------------------------------------------------------------------------------- 1 | import {Platform} from 'react-native'; 2 | 3 | // this is converted to a stylesheet internally at run time with StyleSheet.create( 4 | export const styles = { 5 | // The main container 6 | body: {}, 7 | 8 | // Headings 9 | heading1: { 10 | flexDirection: 'row', 11 | fontSize: 32, 12 | }, 13 | heading2: { 14 | flexDirection: 'row', 15 | fontSize: 24, 16 | }, 17 | heading3: { 18 | flexDirection: 'row', 19 | fontSize: 18, 20 | }, 21 | heading4: { 22 | flexDirection: 'row', 23 | fontSize: 16, 24 | }, 25 | heading5: { 26 | flexDirection: 'row', 27 | fontSize: 13, 28 | }, 29 | heading6: { 30 | flexDirection: 'row', 31 | fontSize: 11, 32 | }, 33 | 34 | // Horizontal Rule 35 | hr: { 36 | backgroundColor: '#000000', 37 | height: 1, 38 | }, 39 | 40 | // Emphasis 41 | strong: { 42 | fontWeight: 'bold', 43 | }, 44 | em: { 45 | fontStyle: 'italic', 46 | }, 47 | s: { 48 | textDecorationLine: 'line-through', 49 | }, 50 | 51 | // Blockquotes 52 | blockquote: { 53 | backgroundColor: '#F5F5F5', 54 | borderColor: '#CCC', 55 | borderLeftWidth: 4, 56 | marginLeft: 5, 57 | paddingHorizontal: 5, 58 | }, 59 | 60 | // Lists 61 | bullet_list: {}, 62 | ordered_list: {}, 63 | list_item: { 64 | flexDirection: 'row', 65 | justifyContent: 'flex-start', 66 | }, 67 | // @pseudo class, does not have a unique render rule 68 | bullet_list_icon: { 69 | marginLeft: 10, 70 | marginRight: 10, 71 | }, 72 | // @pseudo class, does not have a unique render rule 73 | bullet_list_content: { 74 | flex: 1, 75 | }, 76 | // @pseudo class, does not have a unique render rule 77 | ordered_list_icon: { 78 | marginLeft: 10, 79 | marginRight: 10, 80 | }, 81 | // @pseudo class, does not have a unique render rule 82 | ordered_list_content: { 83 | flex: 1, 84 | }, 85 | 86 | // Code 87 | code_inline: { 88 | borderWidth: 1, 89 | borderColor: '#CCCCCC', 90 | backgroundColor: '#f5f5f5', 91 | padding: 10, 92 | borderRadius: 4, 93 | ...Platform.select({ 94 | ['ios']: { 95 | fontFamily: 'Courier', 96 | }, 97 | ['android']: { 98 | fontFamily: 'monospace', 99 | }, 100 | }), 101 | }, 102 | code_block: { 103 | borderWidth: 1, 104 | borderColor: '#CCCCCC', 105 | backgroundColor: '#f5f5f5', 106 | padding: 10, 107 | borderRadius: 4, 108 | ...Platform.select({ 109 | ['ios']: { 110 | fontFamily: 'Courier', 111 | }, 112 | ['android']: { 113 | fontFamily: 'monospace', 114 | }, 115 | }), 116 | }, 117 | fence: { 118 | borderWidth: 1, 119 | borderColor: '#CCCCCC', 120 | backgroundColor: '#f5f5f5', 121 | padding: 10, 122 | borderRadius: 4, 123 | ...Platform.select({ 124 | ['ios']: { 125 | fontFamily: 'Courier', 126 | }, 127 | ['android']: { 128 | fontFamily: 'monospace', 129 | }, 130 | }), 131 | }, 132 | 133 | // Tables 134 | table: { 135 | borderWidth: 1, 136 | borderColor: '#000000', 137 | borderRadius: 3, 138 | }, 139 | thead: {}, 140 | tbody: {}, 141 | th: { 142 | flex: 1, 143 | padding: 5, 144 | }, 145 | tr: { 146 | borderBottomWidth: 1, 147 | borderColor: '#000000', 148 | flexDirection: 'row', 149 | }, 150 | td: { 151 | flex: 1, 152 | padding: 5, 153 | }, 154 | 155 | // Links 156 | link: { 157 | textDecorationLine: 'underline', 158 | }, 159 | blocklink: { 160 | flex: 1, 161 | borderColor: '#000000', 162 | borderBottomWidth: 1, 163 | }, 164 | 165 | // Images 166 | image: { 167 | flex: 1, 168 | }, 169 | 170 | // Text Output 171 | text: {}, 172 | textgroup: {}, 173 | paragraph: { 174 | marginTop: 10, 175 | marginBottom: 10, 176 | flexWrap: 'wrap', 177 | flexDirection: 'row', 178 | alignItems: 'flex-start', 179 | justifyContent: 'flex-start', 180 | width: '100%', 181 | }, 182 | hardbreak: { 183 | width: '100%', 184 | height: 1, 185 | }, 186 | softbreak: {}, 187 | 188 | // Believe these are never used but retained for completeness 189 | pre: {}, 190 | inline: {}, 191 | span: {}, 192 | }; 193 | -------------------------------------------------------------------------------- /src/lib/AstRenderer.js: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | import getUniqueID from './util/getUniqueID'; 4 | import convertAdditionalStyles from './util/convertAdditionalStyles'; 5 | 6 | import textStyleProps from './data/textStyleProps'; 7 | 8 | export default class AstRenderer { 9 | /** 10 | * 11 | * @param {Object.} renderRules 12 | * @param {any} style 13 | */ 14 | constructor( 15 | renderRules, 16 | style, 17 | onLinkPress, 18 | maxTopLevelChildren, 19 | topLevelMaxExceededItem, 20 | allowedImageHandlers, 21 | defaultImageHandler, 22 | debugPrintTree, 23 | ) { 24 | this._renderRules = renderRules; 25 | this._style = style; 26 | this._onLinkPress = onLinkPress; 27 | this._maxTopLevelChildren = maxTopLevelChildren; 28 | this._topLevelMaxExceededItem = topLevelMaxExceededItem; 29 | this._allowedImageHandlers = allowedImageHandlers; 30 | this._defaultImageHandler = defaultImageHandler; 31 | this._debugPrintTree = debugPrintTree; 32 | } 33 | 34 | /** 35 | * 36 | * @param {string} type 37 | * @return {string} 38 | */ 39 | getRenderFunction = (type) => { 40 | const renderFunction = this._renderRules[type]; 41 | 42 | if (!renderFunction) { 43 | console.warn( 44 | `Warning, unknown render rule encountered: ${type}. 'unknown' render rule used (by default, returns null - nothing rendered)`, 45 | ); 46 | return this._renderRules.unknown; 47 | } 48 | 49 | return renderFunction; 50 | }; 51 | 52 | /** 53 | * 54 | * @param node 55 | * @param parentNodes 56 | * @return {*} 57 | */ 58 | renderNode = (node, parentNodes, isRoot = false) => { 59 | const renderFunction = this.getRenderFunction(node.type); 60 | const parents = [...parentNodes]; 61 | 62 | if (this._debugPrintTree === true) { 63 | let str = ''; 64 | 65 | for (let a = 0; a < parents.length; a++) { 66 | str = str + '-'; 67 | } 68 | 69 | console.log(`${str}${node.type}`); 70 | } 71 | 72 | parents.unshift(node); 73 | 74 | // calculate the children first 75 | let children = node.children.map((value) => { 76 | return this.renderNode(value, parents); 77 | }); 78 | 79 | // render any special types of nodes that have different renderRule function signatures 80 | 81 | if (node.type === 'link' || node.type === 'blocklink') { 82 | return renderFunction( 83 | node, 84 | children, 85 | parentNodes, 86 | this._style, 87 | this._onLinkPress, 88 | ); 89 | } 90 | 91 | if (node.type === 'image') { 92 | return renderFunction( 93 | node, 94 | children, 95 | parentNodes, 96 | this._style, 97 | this._allowedImageHandlers, 98 | this._defaultImageHandler, 99 | ); 100 | } 101 | 102 | // We are at the bottom of some tree - grab all the parent styles 103 | // this effectively grabs the styles from parents and 104 | // applies them in order of priority parent (least) to child (most) 105 | // to allow styling global, then lower down things individually 106 | 107 | // we have to handle list_item seperately here because they have some child 108 | // pseudo classes that need the additional style props from parents passed down to them 109 | if (children.length === 0 || node.type === 'list_item') { 110 | const styleObj = {}; 111 | 112 | for (let a = parentNodes.length - 1; a > -1; a--) { 113 | // grab and additional attributes specified by markdown-it 114 | let refStyle = {}; 115 | 116 | if ( 117 | parentNodes[a].attributes && 118 | parentNodes[a].attributes.style && 119 | typeof parentNodes[a].attributes.style === 'string' 120 | ) { 121 | refStyle = convertAdditionalStyles(parentNodes[a].attributes.style); 122 | } 123 | 124 | // combine in specific styles for the object 125 | if (this._style[parentNodes[a].type]) { 126 | refStyle = { 127 | ...refStyle, 128 | ...StyleSheet.flatten(this._style[parentNodes[a].type]), 129 | }; 130 | 131 | // workaround for list_items and their content cascading down the tree 132 | if (parentNodes[a].type === 'list_item') { 133 | let contentStyle = {}; 134 | 135 | if (parentNodes[a + 1].type === 'bullet_list') { 136 | contentStyle = this._style.bullet_list_content; 137 | } else if (parentNodes[a + 1].type === 'ordered_list') { 138 | contentStyle = this._style.ordered_list_content; 139 | } 140 | 141 | refStyle = { 142 | ...refStyle, 143 | ...StyleSheet.flatten(contentStyle), 144 | }; 145 | } 146 | } 147 | 148 | // then work out if any of them are text styles that should be used in the end. 149 | const arr = Object.keys(refStyle); 150 | 151 | for (let b = 0; b < arr.length; b++) { 152 | if (textStyleProps.includes(arr[b])) { 153 | styleObj[arr[b]] = refStyle[arr[b]]; 154 | } 155 | } 156 | } 157 | 158 | return renderFunction(node, children, parentNodes, this._style, styleObj); 159 | } 160 | 161 | // cull top level children 162 | 163 | if ( 164 | isRoot === true && 165 | this._maxTopLevelChildren && 166 | children.length > this._maxTopLevelChildren 167 | ) { 168 | children = children.slice(0, this._maxTopLevelChildren); 169 | children.push(this._topLevelMaxExceededItem); 170 | } 171 | 172 | // render anythign else that has a normal signature 173 | 174 | return renderFunction(node, children, parentNodes, this._style); 175 | }; 176 | 177 | /** 178 | * 179 | * @param nodes 180 | * @return {*} 181 | */ 182 | render = (nodes) => { 183 | const root = {type: 'body', key: getUniqueID(), children: nodes}; 184 | return this.renderNode(root, [], true); 185 | }; 186 | } 187 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base Markdown component 3 | * @author Mient-jan Stelling + contributors 4 | */ 5 | 6 | import React, {useMemo} from 'react'; 7 | import {Text, StyleSheet} from 'react-native'; 8 | import PropTypes from 'prop-types'; 9 | import parser from './lib/parser'; 10 | import getUniqueID from './lib/util/getUniqueID'; 11 | import hasParents from './lib/util/hasParents'; 12 | import openUrl from './lib/util/openUrl'; 13 | import tokensToAST from './lib/util/tokensToAST'; 14 | import renderRules from './lib/renderRules'; 15 | import AstRenderer from './lib/AstRenderer'; 16 | import MarkdownIt from 'markdown-it'; 17 | import removeTextStyleProps from './lib/util/removeTextStyleProps'; 18 | import {styles} from './lib/styles'; 19 | import {stringToTokens} from './lib/util/stringToTokens'; 20 | import FitImage from 'react-native-fit-image'; 21 | import textStyleProps from './lib/data/textStyleProps'; 22 | 23 | export { 24 | getUniqueID, 25 | openUrl, 26 | hasParents, 27 | renderRules, 28 | AstRenderer, 29 | parser, 30 | stringToTokens, 31 | tokensToAST, 32 | MarkdownIt, 33 | styles, 34 | removeTextStyleProps, 35 | FitImage, 36 | textStyleProps, 37 | }; 38 | 39 | // we use StyleSheet.flatten here to make sure we have an object, in case someone 40 | // passes in a StyleSheet.create result to the style prop 41 | const getStyle = (mergeStyle, style) => { 42 | let useStyles = {}; 43 | 44 | if (mergeStyle === true && style !== null) { 45 | // make sure we get anything user defuned 46 | Object.keys(style).forEach((value) => { 47 | useStyles[value] = { 48 | ...StyleSheet.flatten(style[value]), 49 | }; 50 | }); 51 | 52 | // combine any existing styles 53 | Object.keys(styles).forEach((value) => { 54 | useStyles[value] = { 55 | ...styles[value], 56 | ...StyleSheet.flatten(style[value]), 57 | }; 58 | }); 59 | } else { 60 | useStyles = { 61 | ...styles, 62 | }; 63 | 64 | if (style !== null) { 65 | Object.keys(style).forEach((value) => { 66 | useStyles[value] = { 67 | ...StyleSheet.flatten(style[value]), 68 | }; 69 | }); 70 | } 71 | } 72 | 73 | Object.keys(useStyles).forEach((value) => { 74 | useStyles['_VIEW_SAFE_' + value] = removeTextStyleProps(useStyles[value]); 75 | }); 76 | 77 | return StyleSheet.create(useStyles); 78 | }; 79 | 80 | const getRenderer = ( 81 | renderer, 82 | rules, 83 | style, 84 | mergeStyle, 85 | onLinkPress, 86 | maxTopLevelChildren, 87 | topLevelMaxExceededItem, 88 | allowedImageHandlers, 89 | defaultImageHandler, 90 | debugPrintTree, 91 | ) => { 92 | if (renderer && rules) { 93 | console.warn( 94 | 'react-native-markdown-display you are using renderer and rules at the same time. This is not possible, props.rules is ignored', 95 | ); 96 | } 97 | 98 | if (renderer && style) { 99 | console.warn( 100 | 'react-native-markdown-display you are using renderer and style at the same time. This is not possible, props.style is ignored', 101 | ); 102 | } 103 | 104 | // these checks are here to prevent extra overhead. 105 | if (renderer) { 106 | if (!(typeof renderer === 'function') || renderer instanceof AstRenderer) { 107 | return renderer; 108 | } else { 109 | throw new Error( 110 | 'Provided renderer is not compatible with function or AstRenderer. please change', 111 | ); 112 | } 113 | } else { 114 | let useStyles = getStyle(mergeStyle, style); 115 | 116 | return new AstRenderer( 117 | { 118 | ...renderRules, 119 | ...(rules || {}), 120 | }, 121 | useStyles, 122 | onLinkPress, 123 | maxTopLevelChildren, 124 | topLevelMaxExceededItem, 125 | allowedImageHandlers, 126 | defaultImageHandler, 127 | debugPrintTree, 128 | ); 129 | } 130 | }; 131 | 132 | const Markdown = React.memo( 133 | ({ 134 | children, 135 | renderer = null, 136 | rules = null, 137 | style = null, 138 | mergeStyle = true, 139 | markdownit = MarkdownIt({ 140 | typographer: true, 141 | }), 142 | onLinkPress, 143 | maxTopLevelChildren = null, 144 | topLevelMaxExceededItem = ..., 145 | allowedImageHandlers = [ 146 | 'data:image/png;base64', 147 | 'data:image/gif;base64', 148 | 'data:image/jpeg;base64', 149 | 'https://', 150 | 'http://', 151 | ], 152 | defaultImageHandler = 'https://', 153 | debugPrintTree = false, 154 | }) => { 155 | const momoizedRenderer = useMemo( 156 | () => 157 | getRenderer( 158 | renderer, 159 | rules, 160 | style, 161 | mergeStyle, 162 | onLinkPress, 163 | maxTopLevelChildren, 164 | topLevelMaxExceededItem, 165 | allowedImageHandlers, 166 | defaultImageHandler, 167 | debugPrintTree, 168 | ), 169 | [ 170 | maxTopLevelChildren, 171 | onLinkPress, 172 | renderer, 173 | rules, 174 | style, 175 | mergeStyle, 176 | topLevelMaxExceededItem, 177 | allowedImageHandlers, 178 | defaultImageHandler, 179 | debugPrintTree, 180 | ], 181 | ); 182 | 183 | const momoizedParser = useMemo(() => markdownit, [markdownit]); 184 | 185 | return parser(children, momoizedRenderer.render, momoizedParser); 186 | }, 187 | ); 188 | 189 | Markdown.propTypes = { 190 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.array]).isRequired, 191 | renderer: PropTypes.oneOfType([ 192 | PropTypes.func, 193 | PropTypes.instanceOf(AstRenderer), 194 | ]), 195 | onLinkPress: PropTypes.func, 196 | maxTopLevelChildren: PropTypes.number, 197 | topLevelMaxExceededItem: PropTypes.any, 198 | rules: (props, propName, componentName) => { 199 | let invalidProps = []; 200 | const prop = props[propName]; 201 | 202 | if (!prop) { 203 | return; 204 | } 205 | 206 | if (typeof prop === 'object') { 207 | invalidProps = Object.keys(prop).filter( 208 | (key) => typeof prop[key] !== 'function', 209 | ); 210 | } 211 | 212 | if (typeof prop !== 'object') { 213 | return new Error( 214 | `Invalid prop \`${propName}\` supplied to \`${componentName}\`. Must be of shape {[index:string]:function} `, 215 | ); 216 | } else if (invalidProps.length > 0) { 217 | return new Error( 218 | `Invalid prop \`${propName}\` supplied to \`${componentName}\`. These ` + 219 | `props are not of type function \`${invalidProps.join(', ')}\` `, 220 | ); 221 | } 222 | }, 223 | markdownit: PropTypes.instanceOf(MarkdownIt), 224 | style: PropTypes.any, 225 | mergeStyle: PropTypes.bool, 226 | allowedImageHandlers: PropTypes.arrayOf(PropTypes.string), 227 | defaultImageHandler: PropTypes.string, 228 | debugPrintTree: PropTypes.bool, 229 | }; 230 | 231 | export default Markdown; 232 | -------------------------------------------------------------------------------- /src/lib/renderRules.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Text, 4 | TouchableWithoutFeedback, 5 | View, 6 | Platform, 7 | StyleSheet, 8 | } from 'react-native'; 9 | import FitImage from 'react-native-fit-image'; 10 | 11 | import openUrl from './util/openUrl'; 12 | import hasParents from './util/hasParents'; 13 | 14 | import textStyleProps from './data/textStyleProps'; 15 | 16 | const renderRules = { 17 | // when unknown elements are introduced, so it wont break 18 | unknown: (node, children, parent, styles) => null, 19 | 20 | // The main container 21 | body: (node, children, parent, styles) => ( 22 | 23 | {children} 24 | 25 | ), 26 | 27 | // Headings 28 | heading1: (node, children, parent, styles) => ( 29 | 30 | {children} 31 | 32 | ), 33 | heading2: (node, children, parent, styles) => ( 34 | 35 | {children} 36 | 37 | ), 38 | heading3: (node, children, parent, styles) => ( 39 | 40 | {children} 41 | 42 | ), 43 | heading4: (node, children, parent, styles) => ( 44 | 45 | {children} 46 | 47 | ), 48 | heading5: (node, children, parent, styles) => ( 49 | 50 | {children} 51 | 52 | ), 53 | heading6: (node, children, parent, styles) => ( 54 | 55 | {children} 56 | 57 | ), 58 | 59 | // Horizontal Rule 60 | hr: (node, children, parent, styles) => ( 61 | 62 | ), 63 | 64 | // Emphasis 65 | strong: (node, children, parent, styles) => ( 66 | 67 | {children} 68 | 69 | ), 70 | em: (node, children, parent, styles) => ( 71 | 72 | {children} 73 | 74 | ), 75 | s: (node, children, parent, styles) => ( 76 | 77 | {children} 78 | 79 | ), 80 | 81 | // Blockquotes 82 | blockquote: (node, children, parent, styles) => ( 83 | 84 | {children} 85 | 86 | ), 87 | 88 | // Lists 89 | bullet_list: (node, children, parent, styles) => ( 90 | 91 | {children} 92 | 93 | ), 94 | ordered_list: (node, children, parent, styles) => ( 95 | 96 | {children} 97 | 98 | ), 99 | // this is a unique and quite annoying render rule because it has 100 | // child items that can be styled (the list icon and the list content) 101 | // outside of the AST tree so there are some work arounds in the 102 | // AST renderer specifically to get the styling right here 103 | list_item: (node, children, parent, styles, inheritedStyles = {}) => { 104 | // we need to grab any text specific stuff here that is applied on the list_item style 105 | // and apply it onto bullet_list_icon. the AST renderer has some workaround code to make 106 | // the content classes apply correctly to the child AST tree items as well 107 | // as code that forces the creation of the inheritedStyles object for list_items 108 | const refStyle = { 109 | ...inheritedStyles, 110 | ...StyleSheet.flatten(styles.list_item), 111 | }; 112 | 113 | const arr = Object.keys(refStyle); 114 | 115 | const modifiedInheritedStylesObj = {}; 116 | 117 | for (let b = 0; b < arr.length; b++) { 118 | if (textStyleProps.includes(arr[b])) { 119 | modifiedInheritedStylesObj[arr[b]] = refStyle[arr[b]]; 120 | } 121 | } 122 | 123 | if (hasParents(parent, 'bullet_list')) { 124 | return ( 125 | 126 | 129 | {Platform.select({ 130 | android: '\u2022', 131 | ios: '\u00B7', 132 | default: '\u2022', 133 | })} 134 | 135 | {children} 136 | 137 | ); 138 | } 139 | 140 | if (hasParents(parent, 'ordered_list')) { 141 | const orderedListIndex = parent.findIndex( 142 | (el) => el.type === 'ordered_list', 143 | ); 144 | 145 | const orderedList = parent[orderedListIndex]; 146 | let listItemNumber; 147 | 148 | if (orderedList.attributes && orderedList.attributes.start) { 149 | listItemNumber = orderedList.attributes.start + node.index; 150 | } else { 151 | listItemNumber = node.index + 1; 152 | } 153 | 154 | return ( 155 | 156 | 157 | {listItemNumber} 158 | {node.markup} 159 | 160 | {children} 161 | 162 | ); 163 | } 164 | 165 | // we should not need this, but just in case 166 | return ( 167 | 168 | {children} 169 | 170 | ); 171 | }, 172 | 173 | // Code 174 | code_inline: (node, children, parent, styles, inheritedStyles = {}) => ( 175 | 176 | {node.content} 177 | 178 | ), 179 | code_block: (node, children, parent, styles, inheritedStyles = {}) => { 180 | // we trim new lines off the end of code blocks because the parser sends an extra one. 181 | let {content} = node; 182 | 183 | if ( 184 | typeof node.content === 'string' && 185 | node.content.charAt(node.content.length - 1) === '\n' 186 | ) { 187 | content = node.content.substring(0, node.content.length - 1); 188 | } 189 | 190 | return ( 191 | 192 | {content} 193 | 194 | ); 195 | }, 196 | fence: (node, children, parent, styles, inheritedStyles = {}) => { 197 | // we trim new lines off the end of code blocks because the parser sends an extra one. 198 | let {content} = node; 199 | 200 | if ( 201 | typeof node.content === 'string' && 202 | node.content.charAt(node.content.length - 1) === '\n' 203 | ) { 204 | content = node.content.substring(0, node.content.length - 1); 205 | } 206 | 207 | return ( 208 | 209 | {content} 210 | 211 | ); 212 | }, 213 | 214 | // Tables 215 | table: (node, children, parent, styles) => ( 216 | 217 | {children} 218 | 219 | ), 220 | thead: (node, children, parent, styles) => ( 221 | 222 | {children} 223 | 224 | ), 225 | tbody: (node, children, parent, styles) => ( 226 | 227 | {children} 228 | 229 | ), 230 | th: (node, children, parent, styles) => ( 231 | 232 | {children} 233 | 234 | ), 235 | tr: (node, children, parent, styles) => ( 236 | 237 | {children} 238 | 239 | ), 240 | td: (node, children, parent, styles) => ( 241 | 242 | {children} 243 | 244 | ), 245 | 246 | // Links 247 | link: (node, children, parent, styles, onLinkPress) => ( 248 | openUrl(node.attributes.href, onLinkPress)}> 252 | {children} 253 | 254 | ), 255 | blocklink: (node, children, parent, styles, onLinkPress) => ( 256 | openUrl(node.attributes.href, onLinkPress)} 259 | style={styles.blocklink}> 260 | {children} 261 | 262 | ), 263 | 264 | // Images 265 | image: ( 266 | node, 267 | children, 268 | parent, 269 | styles, 270 | allowedImageHandlers, 271 | defaultImageHandler, 272 | ) => { 273 | const {src, alt} = node.attributes; 274 | 275 | // we check that the source starts with at least one of the elements in allowedImageHandlers 276 | const show = 277 | allowedImageHandlers.filter((value) => { 278 | return src.toLowerCase().startsWith(value.toLowerCase()); 279 | }).length > 0; 280 | 281 | if (show === false && defaultImageHandler === null) { 282 | return null; 283 | } 284 | 285 | const imageProps = { 286 | indicator: true, 287 | key: node.key, 288 | style: styles._VIEW_SAFE_image, 289 | source: { 290 | uri: show === true ? src : `${defaultImageHandler}${src}`, 291 | }, 292 | }; 293 | 294 | if (alt) { 295 | imageProps.accessible = true; 296 | imageProps.accessibilityLabel = alt; 297 | } 298 | 299 | return ; 300 | }, 301 | 302 | // Text Output 303 | text: (node, children, parent, styles, inheritedStyles = {}) => ( 304 | 305 | {node.content} 306 | 307 | ), 308 | textgroup: (node, children, parent, styles) => ( 309 | 310 | {children} 311 | 312 | ), 313 | paragraph: (node, children, parent, styles) => ( 314 | 315 | {children} 316 | 317 | ), 318 | hardbreak: (node, children, parent, styles) => ( 319 | 320 | {'\n'} 321 | 322 | ), 323 | softbreak: (node, children, parent, styles) => ( 324 | 325 | {'\n'} 326 | 327 | ), 328 | 329 | // Believe these are never used but retained for completeness 330 | pre: (node, children, parent, styles) => ( 331 | 332 | {children} 333 | 334 | ), 335 | inline: (node, children, parent, styles) => ( 336 | 337 | {children} 338 | 339 | ), 340 | span: (node, children, parent, styles) => ( 341 | 342 | {children} 343 | 344 | ), 345 | }; 346 | 347 | export default renderRules; 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Markdown Display [![npm version](https://badge.fury.io/js/react-native-markdown-display.svg)](https://badge.fury.io/js/react-native-markdown-display) [![Known Vulnerabilities](https://snyk.io/test/github/iamacup/react-native-markdown-display/badge.svg)](https://snyk.io/test/github/iamacup/react-native-markdown-display) 2 | 3 | **This is a fork of [iamacup/react-native-markdown-display](https://github.com/iamacup/react-native-markdown-display) that increases the depended upon versions of react to 18 and react-native to v0.68. This makes it compatible with Expo SDK 46** 4 | 5 | It a 100% compatible CommonMark renderer, a react-native markdown renderer done right. This is __not__ a web-view markdown renderer but a renderer that uses native components for all its elements. These components can be overwritten and styled as needed. 6 | 7 | ### Compatibility with react-native-markdown-renderer 8 | 9 | This is intended to be a replacement for react-native-markdown-renderer, with a variety of bug fixes and enhancements - **Due to how the new style rules work, there may be some tweaking needed**, [see how to style stuff section below](#How-to-style-stuff) 10 | 11 | ### Install 12 | 13 | #### Yarn 14 | ```npm 15 | yarn add react-native-markdown-display 16 | ``` 17 | 18 | #### NPM 19 | ```npm 20 | npm install -S react-native-markdown-display 21 | ``` 22 | 23 | ### Get Started 24 | 25 | ```jsx 26 | import React from 'react'; 27 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native'; 28 | 29 | import Markdown from 'react-native-markdown-display'; 30 | 31 | const copy = `# h1 Heading 8-) 32 | 33 | **This is some bold text!** 34 | 35 | This is normal text 36 | `; 37 | 38 | const App: () => React$Node = () => { 39 | return ( 40 | <> 41 | 42 | 43 | 47 | 48 | {copy} 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default App; 57 | ``` 58 | 59 | 60 | ### Props and Functions 61 | 62 | The `` object takes the following common props: 63 | 64 | | Property | Default | Required | Description 65 | | --- | --- | --- | --- 66 | | `children` | N/A | `true` | The markdown string to render, or the [pre-processed tree](#pre-processing) 67 | | `style` | [source](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/styles.js) | `false` | An object to override the styling for the various rules, [see style section below](#rules-and-styles) for more info 68 | | `mergeStyle` | `true` | `false` | If true, when a style is supplied, the individual items are merged with the default styles instead of overwriting them 69 | | `rules` | [source](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/renderRules.js) | `false` | An object of rules that specify how to render each markdown item, [see rules section below](#rules) for more info 70 | | `onLinkPress` | `import { Linking } from 'react-native';` and `Linking.openURL(url);` | `false` | A handler function to change click behaviour, [see handling links section below](#handling-links) for more info 71 | | `debugPrintTree` | `false` | `false` | Will print the AST tree to the console to help you see what the markdown is being translated to 72 | 73 | 74 | And some additional, less used options: 75 | 76 | | Property | Default | Required | Description 77 | | --- | --- | --- | --- 78 | | `renderer` | `instanceOf(AstRenderer)` | `false` | Used to specify a custom renderer, you can not use the rules or styles props with a custom renderer. 79 | | `markdownit` | `instanceOf(MarkdownIt)` | `false` | A custom markdownit instance with your configuration, default is `MarkdownIt({typographer: true})` 80 | | `maxTopLevelChildren` | `null` | `false` | If defined as a number will only render out first `n` many top level children, then will try to render out `topLevelMaxExceededItem` 81 | | `topLevelMaxExceededItem` | `...` | `false` | Will render when `maxTopLevelChildren` is hit. Make sure to give it a key! 82 | | `allowedImageHandlers` | `['data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://']` | `false` | Any image that does not start with one of these will have the `defaultImageHandler` value prepended to it (unless `defaultImageHandler` is null in which case it won't try to render anything) 83 | | `defaultImageHandler` | `http://` | `false` | Will be prepended to an image url if it does not start with something in the `allowedImageHandlers` array, if this is set to null, it won't try to recover but will just not render anything instead. 84 | 85 | 86 | # Syntax Support 87 | 88 |
Headings 89 |

90 | 91 | ``` 92 | # h1 Heading 8-) 93 | ## h2 Heading 94 | ### h3 Heading 95 | #### h4 Heading 96 | ##### h5 Heading 97 | ###### h6 Heading 98 | ``` 99 | 100 | | iOS | Android 101 | | --- | --- 102 | | | 103 | 104 |

105 |
106 | 107 | 108 |
Horizontal Rules 109 |

110 | 111 | ``` 112 | Some text above 113 | ___ 114 | 115 | Some text in the middle 116 | 117 | --- 118 | 119 | Some text below 120 | ``` 121 | 122 | | iOS | Android 123 | | --- | --- 124 | | | 125 | 126 | 127 |

128 |
129 | 130 | 131 | 132 |
Emphasis 133 |

134 | 135 | ``` 136 | **This is bold text** 137 | 138 | __This is bold text__ 139 | 140 | *This is italic text* 141 | 142 | _This is italic text_ 143 | 144 | ~~Strikethrough~~ 145 | ``` 146 | 147 | | iOS | Android 148 | | --- | --- 149 | | | 150 | 151 |

152 |
153 | 154 | 155 |
Blockquotes 156 |

157 | 158 | ``` 159 | > Blockquotes can also be nested... 160 | >> ...by using additional greater-than signs right next to each other... 161 | > > > ...or with spaces between arrows. 162 | ``` 163 | 164 | | iOS | Android 165 | | --- | --- 166 | | | 167 | 168 |

169 |
170 | 171 | 172 |
Lists 173 |

174 | 175 | ``` 176 | Unordered 177 | 178 | + Create a list by starting a line with `+`, `-`, or `*` 179 | + Sub-lists are made by indenting 2 spaces: 180 | - Marker character change forces new list start: 181 | * Ac tristique libero volutpat at 182 | + Facilisis in pretium nisl aliquet. This is a very long list item that will surely wrap onto the next line. 183 | - Nulla volutpat aliquam velit 184 | + Very easy! 185 | 186 | Ordered 187 | 188 | 1. Lorem ipsum dolor sit amet 189 | 2. Consectetur adipiscing elit. This is a very long list item that will surely wrap onto the next line. 190 | 3. Integer molestie lorem at massa 191 | 192 | Start numbering with offset: 193 | 194 | 57. foo 195 | 58. bar 196 | ``` 197 | 198 | | iOS | Android 199 | | --- | --- 200 | | | 201 | 202 |

203 |
204 | 205 | 206 |
Code 207 |

208 | 209 | ``` 210 | Inline \`code\` 211 | 212 | Indented code 213 | 214 | // Some comments 215 | line 1 of code 216 | line 2 of code 217 | line 3 of code 218 | 219 | 220 | Block code "fences" 221 | 222 | \`\`\` 223 | Sample text here... 224 | \`\`\` 225 | 226 | Syntax highlighting 227 | 228 | \`\`\` js 229 | var foo = function (bar) { 230 | return bar++; 231 | }; 232 | 233 | console.log(foo(5)); 234 | \`\`\` 235 | ``` 236 | 237 | | iOS | Android 238 | | --- | --- 239 | | | 240 | 241 |

242 |
243 | 244 | 245 |
Tables 246 |

247 | 248 | ``` 249 | | Option | Description | 250 | | ------ | ----------- | 251 | | data | path to data files to supply the data that will be passed into templates. | 252 | | engine | engine to be used for processing templates. Handlebars is the default. | 253 | | ext | extension to be used for dest files. | 254 | 255 | Right aligned columns 256 | 257 | | Option | Description | 258 | | ------:| -----------:| 259 | | data | path to data files to supply the data that will be passed into templates. | 260 | | engine | engine to be used for processing templates. Handlebars is the default. | 261 | | ext | extension to be used for dest files. | 262 | ``` 263 | 264 | | iOS | Android 265 | | --- | --- 266 | | | 267 | 268 |

269 |
270 | 271 |
Links 272 |

273 | 274 | ``` 275 | [link text](https://www.google.com) 276 | 277 | [link with title](https://www.google.com "title text!") 278 | 279 | Autoconverted link https://www.google.com (enable linkify to see) 280 | ``` 281 | 282 | | iOS | Android 283 | | --- | --- 284 | | | 285 | 286 |

287 |
288 | 289 |
Images 290 |

291 | 292 | ``` 293 | ![Minion](https://octodex.github.com/images/minion.png) 294 | ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") 295 | 296 | Like links, Images also have a footnote style syntax 297 | 298 | ![Alt text][id] 299 | 300 | With a reference later in the document defining the URL location: 301 | 302 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 303 | ``` 304 | 305 | | iOS | Android 306 | | --- | --- 307 | | | 308 | 309 |

310 |
311 | 312 | 313 |
Typographic Replacements 314 |

315 | 316 | ``` 317 | Enable typographer option to see result. 318 | 319 | (c) (C) (r) (R) (tm) (TM) (p) (P) +- 320 | 321 | test.. test... test..... test?..... test!.... 322 | 323 | !!!!!! ???? ,, -- --- 324 | 325 | "Smartypants, double quotes" and 'single quotes' 326 | ``` 327 | 328 | | iOS | Android 329 | | --- | --- 330 | | | 331 | 332 |

333 |
334 | 335 | 336 |
Plugins and Extensions 337 |

338 | 339 | Plugins for **extra** syntax support can be added using any markdown-it compatible plugins - [see plugins](https://www.npmjs.com/browse/keyword/markdown-it-plugin) for documentation from markdown-it. An example for integration follows: 340 | 341 | 342 | #### Step 1 343 | 344 | Identify the new components and integrate the plugin with a rendered component. We can use the `debugPrintTree` property to see what rules we are rendering: 345 | 346 | 347 | ```jsx 348 | import React from 'react'; 349 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native'; 350 | 351 | import Markdown, { MarkdownIt } from 'react-native-markdown-display'; 352 | import blockEmbedPlugin from 'markdown-it-block-embed'; 353 | 354 | const markdownItInstance = 355 | MarkdownIt({typographer: true}) 356 | .use(blockEmbedPlugin, { 357 | containerClassName: "video-embed" 358 | }); 359 | 360 | const copy = ` 361 | # Some header 362 | 363 | @[youtube](lJIrF4YjHfQ) 364 | `; 365 | 366 | const App: () => React$Node = () => { 367 | return ( 368 | <> 369 | 370 | 371 | 375 | 379 | {copy} 380 | 381 | 382 | 383 | 384 | ); 385 | }; 386 | 387 | export default App; 388 | 389 | ``` 390 | 391 | In the console, we will see the following rendered tree: 392 | 393 | ``` 394 | body 395 | -heading1 396 | --textgroup 397 | ---text 398 | -video 399 | ``` 400 | 401 | With the following error message: 402 | 403 | ``` 404 | Warning, unknown render rule encountered: video. 'unknown' render rule used (by default, returns null - nothing rendered) 405 | ``` 406 | 407 | 408 | #### Step 2 409 | 410 | We need to create the **render rules** and **styles** to handle this new **'video'** component 411 | 412 | 413 | ```jsx 414 | import React from 'react'; 415 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native'; 416 | 417 | import Markdown, { MarkdownIt } from 'react-native-markdown-display'; 418 | import blockEmbedPlugin from 'markdown-it-block-embed'; 419 | 420 | const markdownItInstance = 421 | MarkdownIt({typographer: true}) 422 | .use(blockEmbedPlugin, { 423 | containerClassName: "video-embed" 424 | }); 425 | 426 | const copy = ` 427 | # Some header 428 | 429 | @[youtube](lJIrF4YjHfQ) 430 | `; 431 | 432 | const App: () => React$Node = () => { 433 | return ( 434 | <> 435 | 436 | 437 | 441 | { 451 | // examine the node properties to see what video we need to render 452 | console.log(node); // expected output of this is in readme.md below this code snip 453 | 454 | return ( 455 | Return a video component instead of this text component! 456 | ); 457 | } 458 | 459 | }} 460 | > 461 | {copy} 462 | 463 | 464 | 465 | 466 | ); 467 | }; 468 | 469 | export default App; 470 | ``` 471 | 472 | And all of the video properties needed to render something meaningful are on the node, like this: 473 | 474 | ``` 475 | {type: "video", sourceType: "video", sourceInfo: {…}, sourceMeta: null, block: true, …} 476 | attributes: {} 477 | block: true 478 | children: [] 479 | content: "" 480 | index: 1 481 | key: "rnmr_1720a98f540_video" 482 | markup: "@[youtube](lJIrF4YjHfQ)" 483 | sourceInfo: 484 | service: YouTubeService 485 | env: PluginEnvironment {md: MarkdownIt, options: {…}, services: {…}} 486 | name: "youtube" 487 | options: 488 | height: 390 489 | width: 640 490 | serviceName: "youtube" 491 | videoID: "lJIrF4YjHfQ" 492 | videoReference: "lJIrF4YjHfQ" 493 | sourceMeta: null 494 | sourceType: "video" 495 | tokenIndex: 5 496 | type: "video" 497 | ``` 498 | 499 | #### Other Debugging 500 | 501 | You can do some additional debugging of what the markdown instance is spitting out like this: 502 | 503 | ```jsx 504 | import Markdown, { MarkdownIt } from 'react-native-markdown-display'; 505 | import blockEmbedPlugin from 'markdown-it-block-embed'; 506 | 507 | const markdownItInstance = 508 | MarkdownIt({typographer: true}) 509 | .use(blockEmbedPlugin, { 510 | containerClassName: "video-embed" 511 | }); 512 | 513 | const copy = ` 514 | # Some header 515 | 516 | @[youtube](lJIrF4YjHfQ) 517 | `; 518 | 519 | // this shows you the tree that is used by the react-native-markdown-display 520 | const astTree = markdownItInstance.parse(copy, {}); 521 | console.log(astTree); 522 | 523 | //this contains the html that would be generated - not used by react-native-markdown-display but useful for reference 524 | const html = markdownItInstance.render(copy); 525 | console.log(html); 526 | 527 | ``` 528 | 529 | The above code will output something like this: 530 | 531 | ``` 532 | astTree: 533 | 534 | (4) [Token, Token, Token, Token] 535 | 536 | 0: Token {type: "heading_open", tag: "h1", attrs: null, map: Array(2), nesting: 1, …} 537 | 1: Token {type: "inline", tag: "", attrs: null, map: Array(2), nesting: 0, …} 538 | 2: Token {type: "heading_close", tag: "h1", attrs: null, map: null, nesting: -1, …} 539 | 3: Token {type: "video", tag: "div", attrs: null, map: Array(2), nesting: 0, …} 540 | 541 | length: 4 542 | ``` 543 | 544 | ``` 545 | html: 546 | 547 | 548 |

Some header

549 |
550 | ``` 551 | 552 | 553 |

554 |
555 | 556 | 557 |
All Markdown for Testing 558 |

559 | 560 | This is all of the markdown in one place for testing that your applied styles work in all cases 561 | 562 | ``` 563 | Headings 564 | 565 | # h1 Heading 8-) 566 | ## h2 Heading 567 | ### h3 Heading 568 | #### h4 Heading 569 | ##### h5 Heading 570 | ###### h6 Heading 571 | 572 | 573 | Horizontal Rules 574 | 575 | Some text above 576 | ___ 577 | 578 | Some text in the middle 579 | 580 | --- 581 | 582 | Some text below 583 | 584 | 585 | Emphasis 586 | 587 | **This is bold text** 588 | 589 | __This is bold text__ 590 | 591 | *This is italic text* 592 | 593 | _This is italic text_ 594 | 595 | ~~Strikethrough~~ 596 | 597 | 598 | Blockquotes 599 | 600 | > Blockquotes can also be nested... 601 | >> ...by using additional greater-than signs right next to each other... 602 | > > > ...or with spaces between arrows. 603 | 604 | 605 | Lists 606 | 607 | Unordered 608 | 609 | + Create a list by starting a line with `+`, `-`, or `*` 610 | + Sub-lists are made by indenting 2 spaces: 611 | - Marker character change forces new list start: 612 | * Ac tristique libero volutpat at 613 | + Facilisis in pretium nisl aliquet. This is a very long list item that will surely wrap onto the next line. 614 | - Nulla volutpat aliquam velit 615 | + Very easy! 616 | 617 | Ordered 618 | 619 | 1. Lorem ipsum dolor sit amet 620 | 2. Consectetur adipiscing elit. This is a very long list item that will surely wrap onto the next line. 621 | 3. Integer molestie lorem at massa 622 | 623 | Start numbering with offset: 624 | 625 | 57. foo 626 | 58. bar 627 | 628 | 629 | Code 630 | 631 | Inline \`code\` 632 | 633 | Indented code 634 | 635 | // Some comments 636 | line 1 of code 637 | line 2 of code 638 | line 3 of code 639 | 640 | 641 | Block code "fences" 642 | 643 | \`\`\` 644 | Sample text here... 645 | \`\`\` 646 | 647 | Syntax highlighting 648 | 649 | \`\`\` js 650 | var foo = function (bar) { 651 | return bar++; 652 | }; 653 | 654 | console.log(foo(5)); 655 | \`\`\` 656 | 657 | 658 | Tables 659 | 660 | | Option | Description | 661 | | ------ | ----------- | 662 | | data | path to data files to supply the data that will be passed into templates. | 663 | | engine | engine to be used for processing templates. Handlebars is the default. | 664 | | ext | extension to be used for dest files. | 665 | 666 | Right aligned columns 667 | 668 | | Option | Description | 669 | | ------:| -----------:| 670 | | data | path to data files to supply the data that will be passed into templates. | 671 | | engine | engine to be used for processing templates. Handlebars is the default. | 672 | | ext | extension to be used for dest files. | 673 | 674 | 675 | Links 676 | 677 | [link text](https://www.google.com) 678 | 679 | [link with title](https://www.google.com "title text!") 680 | 681 | Autoconverted link https://www.google.com (enable linkify to see) 682 | 683 | 684 | Images 685 | 686 | ![Minion](https://octodex.github.com/images/minion.png) 687 | ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") 688 | 689 | Like links, Images also have a footnote style syntax 690 | 691 | ![Alt text][id] 692 | 693 | With a reference later in the document defining the URL location: 694 | 695 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 696 | 697 | 698 | Typographic Replacements 699 | 700 | Enable typographer option to see result. 701 | 702 | (c) (C) (r) (R) (tm) (TM) (p) (P) +- 703 | 704 | test.. test... test..... test?..... test!.... 705 | 706 | !!!!!! ???? ,, -- --- 707 | 708 | "Smartypants, double quotes" and 'single quotes' 709 | 710 | ``` 711 | 712 |

713 |
714 | 715 | 716 | # Rules and Styles 717 | 718 | ### How to style stuff 719 | 720 | Text styles are applied in a way that makes it much more convenient to manage changes to global styles while also allowing fine tuning of individual elements. 721 | 722 | Think of the implementation like applying styles in CSS. changes to the `body` effect everything, but can be overwritten further down the style / component tree. 723 | 724 | **Be careful when styling 'text':** the text rule is not applied to all rendered text, most notably list bullet points. If you want to, for instance, color all text, change the `body` style. 725 | 726 | 727 |
Example 728 |

729 | 730 | 731 | 732 | ```jsx 733 | import React from 'react'; 734 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native'; 735 | 736 | import Markdown from 'react-native-markdown-display'; 737 | 738 | const copy = ` 739 | This is some text which is red because of the body style, which is also really small! 740 | 741 | \`\`\` 742 | //This is a code block woooo 743 | 744 | const cool = () => { 745 | console.log('????'); 746 | }; 747 | \`\`\` 748 | 749 | and some more small text 750 | 751 | # This is a h1 752 | ## this is a h2 753 | ### this is a h3 754 | `; 755 | 756 | const App: () => React$Node = () => { 757 | return ( 758 | <> 759 | 760 | 761 | 765 | 772 | {copy} 773 | 774 | 775 | 776 | 777 | ); 778 | }; 779 | 780 | export default App; 781 | ``` 782 | 783 |

784 |
785 | 786 | ### Styles 787 | 788 | Styles are used to override how certain rules are styled. The existing implementation is [here](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/styles.js) 789 | 790 | **NOTE:** By default styles are merged with the existing implementation, to change this, see the `mergeStyle` prop 791 | 792 |
Example Implementation 793 |

794 | 795 | ```jsx 796 | import React from 'react'; 797 | import { SafeAreaView, ScrollView, StatusBar, StyleSheet } from 'react-native'; 798 | 799 | import Markdown from 'react-native-markdown-display'; 800 | 801 | const styles = StyleSheet.create({ 802 | heading1: { 803 | fontSize: 32, 804 | backgroundColor: '#000000', 805 | color: '#FFFFFF', 806 | }, 807 | heading2: { 808 | fontSize: 24, 809 | }, 810 | heading3: { 811 | fontSize: 18, 812 | }, 813 | heading4: { 814 | fontSize: 16, 815 | }, 816 | heading5: { 817 | fontSize: 13, 818 | }, 819 | heading6: { 820 | fontSize: 11, 821 | } 822 | }); 823 | 824 | const copy = ` 825 | # h1 Heading 8-) 826 | ## h2 Heading 8-) 827 | ### h3 Heading 8-) 828 | 829 | | Option | Description | 830 | | ------ | ----------- | 831 | | data | path to data files to supply the data that will be passed into templates. | 832 | | engine | engine to be used for processing templates. Handlebars is the default. | 833 | | ext | extension to be used for dest files. | 834 | `; 835 | 836 | const App: () => React$Node = () => { 837 | return ( 838 | <> 839 | 840 | 841 | 845 | 848 | {copy} 849 | 850 | 851 | 852 | 853 | ); 854 | }; 855 | 856 | export default App; 857 | ``` 858 | 859 |

860 |
861 | 862 | ### Rules 863 | 864 | Rules are used to specify how you want certain elements to be displayed. The existing implementation is [here](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/renderRules.js) 865 | 866 |
Example Implementation 867 |

868 | 869 | ```jsx 870 | import React from 'react'; 871 | import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native'; 872 | 873 | import Markdown from 'react-native-markdown-display'; 874 | 875 | const rules = { 876 | heading1: (node, children, parent, styles) => 877 | 878 | >> H1 TEXT HERE >> "{children}" 879 | , 880 | heading2: (node, children, parent, styles) => 881 | 882 | >> H2 TEXT HERE >> "{children}" 883 | , 884 | heading3: (node, children, parent, styles) => 885 | 886 | >> H3 TEXT HERE >> "{children}" 887 | , 888 | }; 889 | 890 | const copy = ` 891 | # h1 Heading 8-) 892 | ## h2 Heading 8-) 893 | ### h3 Heading 8-) 894 | 895 | | Option | Description | 896 | | ------ | ----------- | 897 | | data | path to data files to supply the data that will be passed into templates. | 898 | | engine | engine to be used for processing templates. Handlebars is the default. | 899 | | ext | extension to be used for dest files. | 900 | `; 901 | 902 | const App: () => React$Node = () => { 903 | return ( 904 | <> 905 | 906 | 907 | 911 | 914 | {copy} 915 | 916 | 917 | 918 | 919 | ); 920 | }; 921 | 922 | export default App; 923 | ``` 924 | 925 |

926 |
927 | 928 | 929 | ### All rules and their associated styles: 930 | 931 | | Render Rule | Style(s) | 932 | | ------ | ----------- | 933 | | `body` | `body` | 934 | | `heading1` | `heading1` | 935 | | `heading2` | `heading2` | 936 | | `heading3` | `heading3` | 937 | | `heading4` | `heading4` | 938 | | `heading5` | `heading5` | 939 | | `heading6` | `heading6` | 940 | | `hr` | `hr` | 941 | | `strong` | `strong` | 942 | | `em` | `em` | 943 | | `s` | `s` | 944 | | `blockquote` | `blockquote` | 945 | | `bullet_list` | `bullet_list` | 946 | | `ordered_list` | `ordered_list` | 947 | | `list_item` | `list_item` - This is a special case that contains a set of pseudo classes that don't align to the render rule: `ordered_list_icon`, `ordered_list_content`, `bullet_list_icon`, `bullet_list_content` | 948 | | `code_inline` | `code_inline` | 949 | | `code_block` | `code_block` | 950 | | `fence` | `fence` | 951 | | `table` | `table` | 952 | | `thead` | `thead` | 953 | | `tbody` | `tbody` | 954 | | `th` | `th` | 955 | | `tr` | `tr` | 956 | | `td` | `td` | 957 | | `link` | `link` | 958 | | `blocklink` | `blocklink` | 959 | | `image` | `image` | 960 | | `text` | `text` | 961 | | `textgroup` | `textgroup` | 962 | | `paragraph` | `paragraph` | 963 | | `hardbreak` | `hardbreak` | 964 | | `softbreak` | `softbreak` | 965 | | `pre` | `pre` | 966 | | `inline` | `inline` | 967 | | `span` | `span` | 968 | 969 | # Handling Links 970 | 971 | Links, by default, will be handled with the `import { Linking } from 'react-native';` import and `Linking.openURL(url);` call. 972 | 973 | It is possible to overwrite this behaviour in one of two ways: 974 | 975 |
onLinkPress Callback 976 |

977 | 978 | ```jsx 979 | import React from 'react'; 980 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native'; 981 | 982 | import Markdown from 'react-native-markdown-display'; 983 | 984 | const copy = `[This is a link!](https://github.com/iamacup/react-native-markdown-display/)`; 985 | 986 | const onLinkPress = (url) => { 987 | if (url) { 988 | // some custom logic 989 | return false; 990 | } 991 | 992 | // return true to open with `Linking.openURL 993 | // return false to handle it yourself 994 | return true 995 | } 996 | 997 | const App: () => React$Node = () => { 998 | return ( 999 | <> 1000 | 1001 | 1002 | 1006 | 1009 | {copy} 1010 | 1011 | 1012 | 1013 | 1014 | ); 1015 | }; 1016 | 1017 | export default App; 1018 | ``` 1019 | 1020 |

1021 |
1022 | 1023 |
Using a Custom Rule 1024 |

1025 | 1026 | You will need to overwrite one or both of `link` and `blocklink`, the original defenitions can be [found here](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/renderRules.js) 1027 | 1028 | Something like this with `yourCustomHandlerFunctionOrLogicHere`: 1029 | 1030 | ```jsx 1031 | import React from 'react'; 1032 | import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native'; 1033 | 1034 | import Markdown from 'react-native-markdown-display'; 1035 | 1036 | const copy = `[This is a link!](https://github.com/iamacup/react-native-markdown-display/)`; 1037 | 1038 | const rules = { 1039 | link: (node, children, parent, styles) => { 1040 | return ( 1041 | yourCustomHandlerFunctionOrLogicHere(node.attributes.href) }> 1042 | {children} 1043 | 1044 | ); 1045 | }, 1046 | }; 1047 | 1048 | const App: () => React$Node = () => { 1049 | return ( 1050 | <> 1051 | 1052 | 1053 | 1057 | 1060 | {copy} 1061 | 1062 | 1063 | 1064 | 1065 | ); 1066 | }; 1067 | 1068 | export default App; 1069 | ``` 1070 | 1071 |

1072 |
1073 | 1074 | 1075 | # Disabling Specific Types of Markdown 1076 | 1077 | You can dissable any type of markdown you want, which is very useful in a mobile environment, by passing the markdownit property like below. Note that for convenience we also export the `MarkdownIt` instance so you don't have to include it as a project dependency directly just to remove some types of markdown. 1078 | 1079 | This example will stop images and links. 1080 | 1081 | ```jsx 1082 | import React from 'react'; 1083 | import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native'; 1084 | 1085 | import Markdown, { MarkdownIt } from 'react-native-markdown-display'; 1086 | 1087 | const copy = ` 1088 | # This heading will show with formatting 1089 | 1090 | [but this link will just](be displayed as this text) 1091 | `; 1092 | 1093 | const App: () => React$Node = () => { 1094 | return ( 1095 | <> 1096 | 1097 | 1098 | 1102 | 1107 | {copy} 1108 | 1109 | 1110 | 1111 | 1112 | ); 1113 | }; 1114 | 1115 | export default App; 1116 | ``` 1117 | 1118 | A full list of things you can turn off is [here](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/commonmark.js) 1119 | 1120 | 1121 | ### Pre Processing 1122 | 1123 | It is possible to need to pre-process the data outside of this library ([related discussion here](https://github.com/iamacup/react-native-markdown-display/issues/79)). As a result, you can pass an AST tree directly as the children like this: 1124 | 1125 | ```jsx 1126 | import React from 'react'; 1127 | import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native'; 1128 | 1129 | import Markdown, { MarkdownIt, tokensToAST, stringToTokens } from 'react-native-markdown-display'; 1130 | 1131 | const markdownItInstance = MarkdownIt({typographer: true}); 1132 | 1133 | const copy = ` 1134 | # Hello this is a title 1135 | 1136 | This is some text with **BOLD!** 1137 | `; 1138 | 1139 | const ast = tokensToAST(stringToTokens(copy, markdownItInstance)) 1140 | 1141 | const App: () => React$Node = () => { 1142 | return ( 1143 | <> 1144 | 1145 | 1146 | 1150 | 1152 | {ast} 1153 | 1154 | 1155 | 1156 | 1157 | ); 1158 | }; 1159 | 1160 | export default App; 1161 | ``` 1162 | 1163 | 1164 | ### Other Notes 1165 | 1166 | This is a fork of [react-native-markdown-renderer](https://github.com/mientjan/react-native-markdown-renderer), a library that unfortunately has not been updated for some time so i took all of the outstanding pull requests from that library and tested + merged as necessary. 1167 | --------------------------------------------------------------------------------