├── .npmignore
├── .babelrc
├── .gitignore
├── doc
└── images
│ ├── ios-1.png
│ ├── ios-10.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
│ ├── android-1.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
│ ├── android-10.png
│ └── style-example.png
├── .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
├── .github
└── workflows
│ ├── checks.yml
│ └── npm-publish.yml
├── .eslintrc.js
├── LICENSE
├── package.json
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | /example
2 | /.idea
3 | /bin
4 | /docs
5 | /doc
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "module:metro-react-native-babel-preset"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.idea
3 | /.DS_Store
4 | yarn-error.log
5 | yarn.lock
6 | .eslintcache
7 |
--------------------------------------------------------------------------------
/doc/images/ios-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-1.png
--------------------------------------------------------------------------------
/doc/images/ios-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-10.png
--------------------------------------------------------------------------------
/doc/images/ios-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-2.png
--------------------------------------------------------------------------------
/doc/images/ios-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-3.png
--------------------------------------------------------------------------------
/doc/images/ios-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-4.png
--------------------------------------------------------------------------------
/doc/images/ios-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-5.png
--------------------------------------------------------------------------------
/doc/images/ios-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-6.png
--------------------------------------------------------------------------------
/doc/images/ios-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-7.png
--------------------------------------------------------------------------------
/doc/images/ios-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-8.png
--------------------------------------------------------------------------------
/doc/images/ios-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/ios-9.png
--------------------------------------------------------------------------------
/doc/images/android-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-1.png
--------------------------------------------------------------------------------
/doc/images/android-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-2.png
--------------------------------------------------------------------------------
/doc/images/android-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-3.png
--------------------------------------------------------------------------------
/doc/images/android-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-4.png
--------------------------------------------------------------------------------
/doc/images/android-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-5.png
--------------------------------------------------------------------------------
/doc/images/android-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-6.png
--------------------------------------------------------------------------------
/doc/images/android-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-7.png
--------------------------------------------------------------------------------
/doc/images/android-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-8.png
--------------------------------------------------------------------------------
/doc/images/android-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-9.png
--------------------------------------------------------------------------------
/doc/images/android-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/android-10.png
--------------------------------------------------------------------------------
/doc/images/style-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RonRadtke/react-native-markdown-display/HEAD/doc/images/style-example.png
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: false,
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | useTabs: false,
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/checks.yml:
--------------------------------------------------------------------------------
1 | name: Code checks
2 |
3 | on:
4 | push:
5 | branches: [ "master", "main" ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ "master", "main" ]
9 |
10 | jobs:
11 | checks:
12 | name: Code checks
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 |
18 | - name: Install dependencies
19 | run: npm install --ci
20 |
21 | - name: Run ESLint
22 | run: npm run lint
23 |
24 | - name: Run Prettier
25 | run: npm run format-check
26 |
27 | # TODO: type checks...
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | '@react-native-community',
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/eslint-recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:import/errors',
8 | 'plugin:import/warnings',
9 | 'plugin:import/typescript',
10 | 'plugin:react/recommended',
11 | 'plugin:react-hooks/recommended',
12 | 'prettier',
13 | ],
14 | plugins: ['@typescript-eslint'],
15 | parserOptions: {
16 | ecmaVersion: 6,
17 | sourceType: 'module',
18 | ecmaFeatures: {
19 | jsx: true,
20 | },
21 | },
22 | settings: {
23 | react: {
24 | version: require('./package.json').peerDependencies.react,
25 | },
26 | 'import/ignore': ['node_modules'],
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will publish a package to npm when a release is created.
2 | # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | # TODO: add tests or leave them out, add build steps...?
5 |
6 | name: npm publish
7 |
8 | on:
9 | release:
10 | types: [created]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: 16
20 | - run: npm ci
21 | # - run: npm test
22 |
23 | publish-npm:
24 | needs: build
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v3
28 | - uses: actions/setup-node@v3
29 | with:
30 | node-version: 16
31 | registry-url: https://registry.npmjs.org/
32 | - run: npm ci
33 | # - run: npm run build
34 | - run: npm publish
35 | env:
36 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ronradtke/react-native-markdown-display",
3 | "version": "8.1.0",
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 | "format": "prettier -w src",
9 | "format-check": "prettier --check src",
10 | "lint": "eslint --cache ./src",
11 | "lint-fix": "eslint --fix --cache ./src"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/RonRadtke/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": "Michael Lohmann, Daniel Maslowski, Ron Radtke and maintainers of the original library",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/RonRadtke/react-native-markdown-display/issues"
29 | },
30 | "homepage": "https://github.com/RonRadtke/react-native-markdown-display",
31 | "dependencies": {
32 | "css-to-react-native": "^3.2.0",
33 | "markdown-it": "^13.0.1",
34 | "prop-types": "^15.7.2",
35 | "react-native-fit-image": "^1.5.5"
36 | },
37 | "peerDependencies": {
38 | "react": ">=16.2.0",
39 | "react-native": ">=0.50.4"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.21.0",
43 | "@babel/runtime": "^7.21.0",
44 | "@react-native-community/eslint-config": "^3.2.0",
45 | "@types/markdown-it": "^12.2.3",
46 | "@types/react-native": ">=0.50.0",
47 | "@typescript-eslint/eslint-plugin": "^5.54.0",
48 | "@typescript-eslint/parser": "^5.53.0",
49 | "eslint": "^8.26.0",
50 | "eslint-config-defaults": "^9.0.0",
51 | "eslint-config-prettier": "^8.7.0",
52 | "eslint-plugin-import": "^2.27.5",
53 | "eslint-plugin-react": "^7.32.2",
54 | "eslint-plugin-react-hooks": "^4.6.0",
55 | "eslint-plugin-react-native": "^4.0.0",
56 | "metro-react-native-babel-preset": "^0.76.0",
57 | "prettier": "^2.8.4",
58 | "typescript": "^4.9.5"
59 | },
60 | "directories": {
61 | "doc": "doc"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/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 |
9 | export function openUrl(url: string): void;
10 |
11 | export function hasParents(parents: any[], type: string): boolean;
12 |
13 | export type RenderFunction = (
14 | node: ASTNode,
15 | children: ReactNode[],
16 | parentNodes: ASTNode[],
17 | styles: any,
18 | styleObj?: any,
19 | // must have this so that we can have fixed overrides with more arguments
20 | ...args: any
21 | ) => ReactNode;
22 |
23 | export type RenderLinkFunction = (
24 | node: ASTNode,
25 | children: ReactNode[],
26 | parentNodes: ASTNode[],
27 | styles: any,
28 | onLinkPress?: (url: string) => boolean,
29 | ) => ReactNode;
30 |
31 | export type RenderImageFunction = (
32 | node: ASTNode,
33 | children: ReactNode[],
34 | parentNodes: ASTNode[],
35 | styles: any,
36 | allowedImageHandlers: string[],
37 | defaultImageHandler: string,
38 | ) => ReactNode;
39 |
40 | export interface RenderRules {
41 | [name: string]: RenderFunction | undefined;
42 |
43 | link?: RenderLinkFunction;
44 | blocklink?: RenderLinkFunction;
45 | image?: RenderImageFunction;
46 | }
47 |
48 | export const renderRules: RenderRules;
49 |
50 | export interface MarkdownParser {
51 | parse: (value: string, options: any) => Token[];
52 | }
53 |
54 | export interface ASTNode {
55 | type: string;
56 | sourceType: string; // original source token name
57 | sourceInfo: string;
58 | sourceMeta: any;
59 | key: string;
60 | content: string;
61 | markup: string;
62 | tokenIndex: number;
63 | index: number;
64 | attributes: Record
204 |
205 | ```
206 | # h1 Heading 8-)
207 | ## h2 Heading
208 | ### h3 Heading
209 | #### h4 Heading
210 | ##### h5 Heading
211 | ###### h6 Heading
212 | ```
213 |
214 | | iOS | Android
215 | | --- | ---
216 | |
224 |
225 | ```
226 | Some text above
227 | ___
228 |
229 | Some text in the middle
230 |
231 | ---
232 |
233 | Some text below
234 | ```
235 |
236 | | iOS | Android
237 | | --- | ---
238 | |
248 |
249 | ```
250 | **This is bold text**
251 |
252 | __This is bold text__
253 |
254 | *This is italic text*
255 |
256 | _This is italic text_
257 |
258 | ~~Strikethrough~~
259 | ```
260 |
261 | | iOS | Android
262 | | --- | ---
263 | |
271 |
272 | ```
273 | > Blockquotes can also be nested...
274 | >> ...by using additional greater-than signs right next to each other...
275 | > > > ...or with spaces between arrows.
276 | ```
277 |
278 | | iOS | Android
279 | | --- | ---
280 | |
288 |
289 | ```
290 | Unordered
291 |
292 | + Create a list by starting a line with `+`, `-`, or `*`
293 | + Sub-lists are made by indenting 2 spaces:
294 | - Marker character change forces new list start:
295 | * Ac tristique libero volutpat at
296 | + Facilisis in pretium nisl aliquet. This is a very long list item that will surely wrap onto the next line.
297 | - Nulla volutpat aliquam velit
298 | + Very easy!
299 |
300 | Ordered
301 |
302 | 1. Lorem ipsum dolor sit amet
303 | 2. Consectetur adipiscing elit. This is a very long list item that will surely wrap onto the next line.
304 | 3. Integer molestie lorem at massa
305 |
306 | Start numbering with offset:
307 |
308 | 57. foo
309 | 58. bar
310 | ```
311 |
312 | | iOS | Android
313 | | --- | ---
314 | |
322 |
323 | ```
324 | Inline \`code\`
325 |
326 | Indented code
327 |
328 | // Some comments
329 | line 1 of code
330 | line 2 of code
331 | line 3 of code
332 |
333 |
334 | Block code "fences"
335 |
336 | \`\`\`
337 | Sample text here...
338 | \`\`\`
339 |
340 | Syntax highlighting
341 |
342 | \`\`\` js
343 | var foo = function (bar) {
344 | return bar++;
345 | };
346 |
347 | console.log(foo(5));
348 | \`\`\`
349 | ```
350 |
351 | | iOS | Android
352 | | --- | ---
353 | |
361 |
362 | ```
363 | | Option | Description |
364 | | ------ | ----------- |
365 | | data | path to data files to supply the data that will be passed into templates. |
366 | | engine | engine to be used for processing templates. Handlebars is the default. |
367 | | ext | extension to be used for dest files. |
368 |
369 | Right aligned columns
370 |
371 | | Option | Description |
372 | | ------:| -----------:|
373 | | data | path to data files to supply the data that will be passed into templates. |
374 | | engine | engine to be used for processing templates. Handlebars is the default. |
375 | | ext | extension to be used for dest files. |
376 | ```
377 |
378 | | iOS | Android
379 | | --- | ---
380 | |
387 |
388 | ```
389 | [link text](https://www.google.com)
390 |
391 | [link with title](https://www.google.com "title text!")
392 |
393 | Autoconverted link https://www.google.com (enable linkify to see)
394 | ```
395 |
396 | | iOS | Android
397 | | --- | ---
398 | |
405 |
406 | ```
407 | 
408 | 
409 |
410 | Like links, Images also have a footnote style syntax
411 |
412 | ![Alt text][id]
413 |
414 | With a reference later in the document defining the URL location:
415 |
416 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
417 | ```
418 |
419 | | iOS | Android
420 | | --- | ---
421 | |
429 |
430 | ```
431 | Enable typographer option to see result.
432 |
433 | (c) (C) (r) (R) (tm) (TM) (p) (P) +-
434 |
435 | test.. test... test..... test?..... test!....
436 |
437 | !!!!!! ???? ,, -- ---
438 |
439 | "Smartypants, double quotes" and 'single quotes'
440 | ```
441 |
442 | | iOS | Android
443 | | --- | ---
444 | |
452 |
453 | 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:
454 |
455 |
456 | #### Step 1
457 |
458 | 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:
459 |
460 |
461 | ```jsx
462 | import React from 'react';
463 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
464 |
465 | import Markdown, { MarkdownIt }from '@ronradtke/react-native-markdown-display';
466 | import blockEmbedPlugin from 'markdown-it-block-embed';
467 |
468 | const markdownItInstance =
469 | MarkdownIt({typographer: true})
470 | .use(blockEmbedPlugin, {
471 | containerClassName: "video-embed"
472 | });
473 |
474 | const copy = `
475 | # Some header
476 |
477 | @[youtube](lJIrF4YjHfQ)
478 | `;
479 |
480 | const App: () => React$Node = () => {
481 | return (
482 | <>
483 | Headings
203 |
|
217 |
218 | Horizontal Rules
223 |
|
239 |
240 |
241 | Emphasis
247 |
|
264 |
265 | Blockquotes
270 |
|
281 |
282 | Lists
287 |
|
315 |
316 | Code
321 |
|
354 |
355 | Tables
360 |
|
381 |
382 | Links
386 |
|
399 |
400 | Images
404 |
|
422 |
423 | Typographic Replacements
428 |
|
445 |
446 | Plugins and Extensions
451 | Some header
663 |
664 | ```
665 |
666 |
667 |
673 | 674 | This is all of the markdown in one place for testing that your applied styles work in all cases 675 | 676 | ``` 677 | Headings 678 | 679 | # h1 Heading 8-) 680 | ## h2 Heading 681 | ### h3 Heading 682 | #### h4 Heading 683 | ##### h5 Heading 684 | ###### h6 Heading 685 | 686 | 687 | Horizontal Rules 688 | 689 | Some text above 690 | ___ 691 | 692 | Some text in the middle 693 | 694 | --- 695 | 696 | Some text below 697 | 698 | 699 | Emphasis 700 | 701 | **This is bold text** 702 | 703 | __This is bold text__ 704 | 705 | *This is italic text* 706 | 707 | _This is italic text_ 708 | 709 | ~~Strikethrough~~ 710 | 711 | 712 | Blockquotes 713 | 714 | > Blockquotes can also be nested... 715 | >> ...by using additional greater-than signs right next to each other... 716 | > > > ...or with spaces between arrows. 717 | 718 | 719 | Lists 720 | 721 | Unordered 722 | 723 | + Create a list by starting a line with `+`, `-`, or `*` 724 | + Sub-lists are made by indenting 2 spaces: 725 | - Marker character change forces new list start: 726 | * Ac tristique libero volutpat at 727 | + Facilisis in pretium nisl aliquet. This is a very long list item that will surely wrap onto the next line. 728 | - Nulla volutpat aliquam velit 729 | + Very easy! 730 | 731 | Ordered 732 | 733 | 1. Lorem ipsum dolor sit amet 734 | 2. Consectetur adipiscing elit. This is a very long list item that will surely wrap onto the next line. 735 | 3. Integer molestie lorem at massa 736 | 737 | Start numbering with offset: 738 | 739 | 57. foo 740 | 58. bar 741 | 742 | 743 | Code 744 | 745 | Inline \`code\` 746 | 747 | Indented code 748 | 749 | // Some comments 750 | line 1 of code 751 | line 2 of code 752 | line 3 of code 753 | 754 | 755 | Block code "fences" 756 | 757 | \`\`\` 758 | Sample text here... 759 | \`\`\` 760 | 761 | Syntax highlighting 762 | 763 | \`\`\` js 764 | var foo = function (bar) { 765 | return bar++; 766 | }; 767 | 768 | console.log(foo(5)); 769 | \`\`\` 770 | 771 | 772 | Tables 773 | 774 | | Option | Description | 775 | | ------ | ----------- | 776 | | data | path to data files to supply the data that will be passed into templates. | 777 | | engine | engine to be used for processing templates. Handlebars is the default. | 778 | | ext | extension to be used for dest files. | 779 | 780 | Right aligned columns 781 | 782 | | Option | Description | 783 | | ------:| -----------:| 784 | | data | path to data files to supply the data that will be passed into templates. | 785 | | engine | engine to be used for processing templates. Handlebars is the default. | 786 | | ext | extension to be used for dest files. | 787 | 788 | 789 | Links 790 | 791 | [link text](https://www.google.com) 792 | 793 | [link with title](https://www.google.com "title text!") 794 | 795 | Autoconverted link https://www.google.com (enable linkify to see) 796 | 797 | 798 | Images 799 | 800 |  801 |  802 | 803 | Like links, Images also have a footnote style syntax 804 | 805 | ![Alt text][id] 806 | 807 | With a reference later in the document defining the URL location: 808 | 809 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 810 | 811 | 812 | Typographic Replacements 813 | 814 | Enable typographer option to see result. 815 | 816 | (c) (C) (r) (R) (tm) (TM) (p) (P) +- 817 | 818 | test.. test... test..... test?..... test!.... 819 | 820 | !!!!!! ???? ,, -- --- 821 | 822 | "Smartypants, double quotes" and 'single quotes' 823 | 824 | ``` 825 | 826 |
827 |
843 |
844 |
845 |
846 | ```jsx
847 | import React from 'react';
848 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
849 |
850 | import Markdown from '@ronradtke/react-native-markdown-display';
851 |
852 | const copy = `
853 | This is some text which is red because of the body style, which is also really small!
854 |
855 | \`\`\`
856 | //This is a code block woooo
857 |
858 | const cool = () => {
859 | console.log('????');
860 | };
861 | \`\`\`
862 |
863 | and some more small text
864 |
865 | # This is a h1
866 | ## this is a h2
867 | ### this is a h3
868 | `;
869 |
870 | const App: () => React$Node = () => {
871 | return (
872 | <>
873 |
908 |
909 | ```jsx
910 | import React from 'react';
911 | import { SafeAreaView, ScrollView, StatusBar, StyleSheet } from 'react-native';
912 |
913 | import Markdown from '@ronradtke/react-native-markdown-display';
914 |
915 | const styles = StyleSheet.create({
916 | heading1: {
917 | fontSize: 32,
918 | backgroundColor: '#000000',
919 | color: '#FFFFFF',
920 | },
921 | heading2: {
922 | fontSize: 24,
923 | },
924 | heading3: {
925 | fontSize: 18,
926 | },
927 | heading4: {
928 | fontSize: 16,
929 | },
930 | heading5: {
931 | fontSize: 13,
932 | },
933 | heading6: {
934 | fontSize: 11,
935 | }
936 | });
937 |
938 | const copy = `
939 | # h1 Heading 8-)
940 | ## h2 Heading 8-)
941 | ### h3 Heading 8-)
942 |
943 | | Option | Description |
944 | | ------ | ----------- |
945 | | data | path to data files to supply the data that will be passed into templates. |
946 | | engine | engine to be used for processing templates. Handlebars is the default. |
947 | | ext | extension to be used for dest files. |
948 | `;
949 |
950 | const App: () => React$Node = () => {
951 | return (
952 | <>
953 |
982 |
983 | ```jsx
984 | import React from 'react';
985 | import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
986 |
987 | import Markdown from '@ronradtke/react-native-markdown-display';
988 |
989 | const rules = {
990 | heading1: (node, children, parent, styles) =>
991 |
1091 |
1092 | ```jsx
1093 | import React from 'react';
1094 | import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
1095 |
1096 | import Markdown from '@ronradtke/react-native-markdown-display';
1097 |
1098 | const copy = `[This is a link!](https://github.com/iamacup/react-native-markdown-display/)`;
1099 |
1100 | const onLinkPress = (url) => {
1101 | if (url) {
1102 | // some custom logic
1103 | return false;
1104 | }
1105 |
1106 | // return true to open with `Linking.openURL
1107 | // return false to handle it yourself
1108 | return true
1109 | }
1110 |
1111 | const App: () => React$Node = () => {
1112 | return (
1113 | <>
1114 |
1139 |
1140 | 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)
1141 |
1142 | Something like this with `yourCustomHandlerFunctionOrLogicHere`:
1143 |
1144 | ```jsx
1145 | import React from 'react';
1146 | import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
1147 |
1148 | import Markdown from '@ronradtke/react-native-markdown-display';
1149 |
1150 | const copy = `[This is a link!](https://github.com/iamacup/react-native-markdown-display/)`;
1151 |
1152 | const rules = {
1153 | link: (node, children, parent, styles) => {
1154 | return (
1155 |