react-native-marked 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +29 -0
  3. package/dist/commonjs/components/MDImage.js +55 -0
  4. package/dist/commonjs/components/MDImage.js.map +1 -0
  5. package/dist/commonjs/index.js +14 -0
  6. package/dist/commonjs/index.js.map +1 -0
  7. package/dist/commonjs/lib/Markdown.js +57 -0
  8. package/dist/commonjs/lib/Markdown.js.map +1 -0
  9. package/dist/commonjs/lib/Parser.js +264 -0
  10. package/dist/commonjs/lib/Parser.js.map +1 -0
  11. package/dist/commonjs/lib/Renderer.js +86 -0
  12. package/dist/commonjs/lib/Renderer.js.map +1 -0
  13. package/dist/commonjs/types/types.js +6 -0
  14. package/dist/commonjs/types/types.js.map +1 -0
  15. package/dist/commonjs/utils/string.js +17 -0
  16. package/dist/commonjs/utils/string.js.map +1 -0
  17. package/dist/module/components/MDImage.js +37 -0
  18. package/dist/module/components/MDImage.js.map +1 -0
  19. package/dist/module/index.js +3 -0
  20. package/dist/module/index.js.map +1 -0
  21. package/dist/module/lib/Markdown.js +38 -0
  22. package/dist/module/lib/Markdown.js.map +1 -0
  23. package/dist/module/lib/Parser.js +252 -0
  24. package/dist/module/lib/Parser.js.map +1 -0
  25. package/dist/module/lib/Renderer.js +70 -0
  26. package/dist/module/lib/Renderer.js.map +1 -0
  27. package/dist/module/types/types.js +2 -0
  28. package/dist/module/types/types.js.map +1 -0
  29. package/dist/module/utils/string.js +8 -0
  30. package/dist/module/utils/string.js.map +1 -0
  31. package/dist/typescript/components/MDImage.d.ts +7 -0
  32. package/dist/typescript/index.d.ts +2 -0
  33. package/dist/typescript/lib/Markdown.d.ts +9 -0
  34. package/dist/typescript/lib/Parser.d.ts +14 -0
  35. package/dist/typescript/lib/Renderer.d.ts +14 -0
  36. package/dist/typescript/types/types.d.ts +23 -0
  37. package/dist/typescript/utils/string.d.ts +5 -0
  38. package/package.json +179 -0
  39. package/src/components/MDImage.tsx +40 -0
  40. package/src/index.ts +3 -0
  41. package/src/lib/Markdown.tsx +39 -0
  42. package/src/lib/Parser.ts +256 -0
  43. package/src/lib/Renderer.tsx +75 -0
  44. package/src/types/types.ts +25 -0
  45. package/src/utils/string.ts +7 -0
package/package.json ADDED
@@ -0,0 +1,179 @@
1
+ {
2
+ "name": "react-native-marked",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "test",
5
+ "main": "dist/commonjs/index",
6
+ "module": "dist/module/index",
7
+ "types": "dist/typescript/index.d.ts",
8
+ "react-native": "src/index",
9
+ "source": "src/index",
10
+ "files": [
11
+ "src",
12
+ "dist",
13
+ "android",
14
+ "ios",
15
+ "cpp",
16
+ "react-native-marked.podspec",
17
+ "!dist/typescript/example",
18
+ "!android/build",
19
+ "!ios/build",
20
+ "!**/__tests__",
21
+ "!**/__fixtures__",
22
+ "!**/__mocks__"
23
+ ],
24
+ "scripts": {
25
+ "test": "jest",
26
+ "typescript": "tsc --noEmit",
27
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
28
+ "build": "bob build",
29
+ "prepare": "npm run build",
30
+ "release": "release-it --preRelease=alpha"
31
+ },
32
+ "keywords": [
33
+ "react-native",
34
+ "ios",
35
+ "android"
36
+ ],
37
+ "repository": "https://github.com/gmsgowtham/react-native-marked",
38
+ "author": "Gowtham G <webappsbygowtham@gmail.com> (https://github.com/gmsgowtham)",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/gmsgowtham/react-native-marked/issues"
42
+ },
43
+ "homepage": "https://github.com/gmsgowtham/react-native-marked#readme",
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org/"
46
+ },
47
+ "devDependencies": {
48
+ "@arkweid/lefthook": "^0.7.7",
49
+ "@babel/eslint-parser": "^7.18.2",
50
+ "@commitlint/config-conventional": "^17.0.2",
51
+ "@react-native-community/eslint-config": "^3.0.2",
52
+ "@release-it/conventional-changelog": "^5.0.0",
53
+ "@types/jest": "^28.1.2",
54
+ "@types/marked": "^4.0.3",
55
+ "@types/react": "~17.0.21",
56
+ "@types/react-native": "0.68.0",
57
+ "@typescript-eslint/eslint-plugin": "^5.34.0",
58
+ "@typescript-eslint/parser": "^5.34.0",
59
+ "commitlint": "^17.0.2",
60
+ "eslint": "^8.4.1",
61
+ "eslint-config-prettier": "^8.5.0",
62
+ "eslint-plugin-prettier": "^4.0.0",
63
+ "jest": "^28.1.1",
64
+ "pod-install": "^0.1.0",
65
+ "prettier": "^2.0.5",
66
+ "react": "17.0.2",
67
+ "react-native": "0.68.2",
68
+ "react-native-builder-bob": "^0.18.3",
69
+ "release-it": "^15.0.0",
70
+ "typescript": "^4.5.2"
71
+ },
72
+ "resolutions": {
73
+ "@types/react": "17.0.21"
74
+ },
75
+ "peerDependencies": {
76
+ "react": "^16.8.6 || ^17.0.0 || ^18.0.0",
77
+ "react-native": ">=0.60.0"
78
+ },
79
+ "jest": {
80
+ "preset": "react-native",
81
+ "modulePathIgnorePatterns": [
82
+ "<rootDir>/examples/*/node_modules",
83
+ "<rootDir>/dist/"
84
+ ]
85
+ },
86
+ "commitlint": {
87
+ "extends": [
88
+ "@commitlint/config-conventional"
89
+ ],
90
+ "rules": {
91
+ "type-enum": [
92
+ 2,
93
+ "always",
94
+ [
95
+ "build",
96
+ "chore",
97
+ "ci",
98
+ "docs",
99
+ "feat",
100
+ "fix",
101
+ "perf",
102
+ "refactor",
103
+ "revert",
104
+ "style",
105
+ "test",
106
+ "todo"
107
+ ]
108
+ ]
109
+ }
110
+ },
111
+ "release-it": {
112
+ "git": {
113
+ "commitMessage": "chore: release ${version}",
114
+ "tagName": "v${version}"
115
+ },
116
+ "npm": {
117
+ "publish": true
118
+ },
119
+ "github": {
120
+ "release": true
121
+ },
122
+ "plugins": {
123
+ "@release-it/conventional-changelog": {
124
+ "preset": "angular"
125
+ }
126
+ }
127
+ },
128
+ "eslintConfig": {
129
+ "root": true,
130
+ "parser": "@typescript-eslint/parser",
131
+ "extends": [
132
+ "@react-native-community",
133
+ "prettier",
134
+ "plugin:@typescript-eslint/recommended"
135
+ ],
136
+ "rules": {
137
+ "prettier/prettier": [
138
+ "error",
139
+ {
140
+ "quoteProps": "consistent",
141
+ "singleQuote": true,
142
+ "tabWidth": 2,
143
+ "trailingComma": "es5",
144
+ "useTabs": false
145
+ }
146
+ ]
147
+ }
148
+ },
149
+ "eslintIgnore": [
150
+ "node_modules/",
151
+ "dist/",
152
+ "examples/*/node_modules"
153
+ ],
154
+ "prettier": {
155
+ "quoteProps": "consistent",
156
+ "singleQuote": true,
157
+ "tabWidth": 2,
158
+ "trailingComma": "es5",
159
+ "useTabs": false
160
+ },
161
+ "react-native-builder-bob": {
162
+ "source": "src",
163
+ "output": "dist",
164
+ "targets": [
165
+ "commonjs",
166
+ "module",
167
+ [
168
+ "typescript",
169
+ {
170
+ "project": "tsconfig.build.json"
171
+ }
172
+ ]
173
+ ]
174
+ },
175
+ "dependencies": {
176
+ "marked": "^4.0.18",
177
+ "react-native-fast-image": "^8.5.11"
178
+ }
179
+ }
@@ -0,0 +1,40 @@
1
+ import React, { memo, useEffect, useState } from 'react';
2
+ import { Image } from 'react-native';
3
+ import FastImage from 'react-native-fast-image';
4
+
5
+ interface MDImageProps {
6
+ uri: string;
7
+ width: number;
8
+ }
9
+
10
+ const MDImage = ({ uri, width }: MDImageProps) => {
11
+ const [height, setHeight] = useState<number>(0);
12
+
13
+ useEffect(() => {
14
+ Image.prefetch(uri);
15
+ Image.getSize(uri, (iWidth, iHeight) => {
16
+ const heightCalc = width * (iHeight / iWidth);
17
+ setHeight(heightCalc);
18
+ });
19
+ }, [uri, width, height]);
20
+
21
+ if (height < 1) {
22
+ return null;
23
+ }
24
+
25
+ return (
26
+ <FastImage
27
+ fallback
28
+ source={{ uri }}
29
+ style={[
30
+ {
31
+ width: width,
32
+ height: height,
33
+ },
34
+ ]}
35
+ resizeMode={FastImage.resizeMode.cover}
36
+ />
37
+ );
38
+ };
39
+
40
+ export default memo(MDImage);
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import Markdown from './lib/Markdown';
2
+
3
+ export default Markdown;
@@ -0,0 +1,39 @@
1
+ import React, { ReactNode, useCallback } from 'react';
2
+ import { marked } from 'marked';
3
+ import Parser from './Parser';
4
+ import { FlatList, FlatListProps } from 'react-native';
5
+
6
+ interface MarkdownProps {
7
+ value: string;
8
+ contentWidth: number;
9
+ listProps?: Omit<
10
+ FlatListProps<ReactNode>,
11
+ 'data' | 'renderItem' | 'horizontal'
12
+ >;
13
+ }
14
+
15
+ const Markdown = ({ value, contentWidth, listProps }: MarkdownProps) => {
16
+ const tokens = marked.lexer(value.trim());
17
+ const parser = new Parser({ contentWidth });
18
+ const rnElements = parser.parse(tokens);
19
+
20
+ const renderItem = useCallback(({ item }: { item: ReactNode }) => {
21
+ return <>{item}</>;
22
+ }, []);
23
+
24
+ const keyExtractor = useCallback((_, index) => index.toString(), []);
25
+
26
+ return (
27
+ <FlatList
28
+ keyExtractor={keyExtractor}
29
+ maxToRenderPerBatch={8}
30
+ initialNumToRender={8}
31
+ removeClippedSubviews
32
+ {...listProps}
33
+ data={rnElements}
34
+ renderItem={renderItem}
35
+ />
36
+ );
37
+ };
38
+
39
+ export default Markdown;
@@ -0,0 +1,256 @@
1
+ import type { marked } from 'marked';
2
+ import {
3
+ ImageStyle,
4
+ StyleProp,
5
+ StyleSheet,
6
+ TextStyle,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import Renderer from './Renderer';
10
+ import type { MarkedStyles, ParserOptions } from '../types/types';
11
+
12
+ class Parser {
13
+ private renderer;
14
+ private styles: MarkedStyles;
15
+ private options: ParserOptions;
16
+ constructor(options: ParserOptions) {
17
+ this.renderer = new Renderer();
18
+ this.options = options;
19
+ this.styles = {
20
+ ...defaultStyles,
21
+ ...options.styles,
22
+ };
23
+ }
24
+
25
+ parse(tokens: marked.Token[]) {
26
+ return this.parseBlocks(tokens);
27
+ }
28
+
29
+ parseBlocks(
30
+ tokens: marked.Token[],
31
+ styleObj: StyleProp<ViewStyle | TextStyle | ImageStyle> = {}
32
+ ) {
33
+ const elements: React.ReactNode[] = tokens.map((token) => {
34
+ switch (token.type) {
35
+ case 'space': {
36
+ return null;
37
+ }
38
+ case 'paragraph': {
39
+ let tempTokens: marked.Token[] = [];
40
+ const paragraphChildren: React.ReactNode[] = [];
41
+ token.tokens.forEach((t) => {
42
+ // TODO: move type checking value to const
43
+ if (t.type === 'image') {
44
+ paragraphChildren.push(
45
+ this.renderer.getTextNode(
46
+ this.parseInline(tempTokens),
47
+ styleObj
48
+ )
49
+ );
50
+ paragraphChildren.push(this.parseInline([t]));
51
+ tempTokens = [];
52
+ return;
53
+ }
54
+ tempTokens = [...tempTokens, t];
55
+ });
56
+
57
+ if (tempTokens.length > 0) {
58
+ paragraphChildren.push(
59
+ this.renderer.getTextNode(this.parseInline(tempTokens), styleObj)
60
+ );
61
+ }
62
+
63
+ return this.renderer.getParagraph(paragraphChildren, [
64
+ styleObj,
65
+ this.styles.paragraph,
66
+ ]);
67
+ }
68
+ case 'blockquote': {
69
+ return this.renderer.getBlockquote(
70
+ this.parseBlocks(token.tokens, this.styles.blockquoteText),
71
+ this.styles.blockquoteContainer
72
+ );
73
+ }
74
+ case 'heading': {
75
+ switch (token.depth) {
76
+ case 1:
77
+ return this.renderer.getHeading(token.text, this.styles.h1);
78
+ case 2:
79
+ return this.renderer.getHeading(token.text, this.styles.h2);
80
+ case 3:
81
+ return this.renderer.getHeading(token.text, this.styles.h3);
82
+ case 4:
83
+ return this.renderer.getHeading(token.text, this.styles.h4);
84
+ case 5:
85
+ return this.renderer.getHeading(token.text, this.styles.h5);
86
+ case 6:
87
+ return this.renderer.getHeading(token.text, this.styles.h6);
88
+ default:
89
+ return null;
90
+ }
91
+ }
92
+ case 'code': {
93
+ return this.renderer.getCodeBlock(
94
+ token.text,
95
+ this.styles.code,
96
+ this.styles.em
97
+ );
98
+ }
99
+ default:
100
+ return null;
101
+ }
102
+ });
103
+ return elements;
104
+ }
105
+
106
+ parseInline(
107
+ tokens: marked.Token[],
108
+ styleObj: StyleProp<ViewStyle | TextStyle | ImageStyle> = {}
109
+ ) {
110
+ const elements: React.ReactNode[] = tokens.map((token) => {
111
+ if (!token) return null;
112
+
113
+ switch (token.type) {
114
+ case 'escape': {
115
+ return this.renderer.getTextNode(token.text, [
116
+ styleObj,
117
+ this.styles.text,
118
+ ]);
119
+ }
120
+ case 'html': {
121
+ console.log('html', token);
122
+ return null;
123
+ }
124
+ case 'link': {
125
+ return this.renderer.getLinkNode(token.text, token.href, [
126
+ styleObj,
127
+ this.styles.link,
128
+ ]);
129
+ }
130
+ case 'image': {
131
+ return this.renderer.getImage(token.href, this.options.contentWidth);
132
+ }
133
+ case 'strong': {
134
+ return this.renderer.getTextNode(
135
+ this.parseInline(token.tokens, this.styles.strong),
136
+ [styleObj, this.styles.strong]
137
+ );
138
+ }
139
+ case 'em': {
140
+ return this.renderer.getTextNode(
141
+ this.parseInline(token.tokens, this.styles.em),
142
+ [styleObj, this.styles.em]
143
+ );
144
+ }
145
+ case 'codespan': {
146
+ return this.renderer.getTextNode(
147
+ ` ${token.text} `,
148
+ this.styles.codespan
149
+ );
150
+ }
151
+ case 'br': {
152
+ return this.renderer.getTextNode('\n', {});
153
+ }
154
+ case 'del': {
155
+ return null;
156
+ }
157
+ case 'text': {
158
+ return this.renderer.getTextNode(token.raw, [
159
+ styleObj,
160
+ this.styles.text,
161
+ ]);
162
+ }
163
+ default: {
164
+ const errMsg = 'Token with "' + token.type + '" type was not found.';
165
+ console.error(errMsg);
166
+ return null;
167
+ }
168
+ }
169
+ });
170
+ return elements;
171
+ }
172
+ }
173
+
174
+ // TODO: material typography for font size
175
+ const defaultStyles = StyleSheet.create<MarkedStyles>({
176
+ em: {
177
+ fontSize: 16,
178
+ lineHeight: 24,
179
+ fontStyle: 'italic',
180
+ },
181
+ strong: {
182
+ fontSize: 16,
183
+ lineHeight: 24,
184
+ fontWeight: 'bold',
185
+ },
186
+ text: {
187
+ fontSize: 16,
188
+ lineHeight: 24,
189
+ },
190
+ paragraph: {
191
+ fontSize: 16,
192
+ lineHeight: 24,
193
+ paddingVertical: 8,
194
+ },
195
+ link: {
196
+ fontStyle: 'italic',
197
+ fontSize: 16,
198
+ lineHeight: 24,
199
+ color: '#0074cc',
200
+ },
201
+ blockquoteText: {
202
+ color: '#6a737d',
203
+ },
204
+ blockquoteContainer: {
205
+ borderLeftColor: '#dfe2e5',
206
+ paddingLeft: 16,
207
+ borderLeftWidth: 5,
208
+ },
209
+ h1: {
210
+ fontSize: 32,
211
+ lineHeight: 40,
212
+ fontWeight: 'bold',
213
+ paddingVertical: 8,
214
+ letterSpacing: 0,
215
+ },
216
+ h2: {
217
+ fontSize: 28,
218
+ lineHeight: 36,
219
+ fontWeight: '500',
220
+ paddingVertical: 8,
221
+ },
222
+ h3: {
223
+ fontSize: 36,
224
+ lineHeight: 44,
225
+ fontWeight: '500',
226
+ },
227
+ h4: {
228
+ fontSize: 22,
229
+ lineHeight: 28,
230
+ fontWeight: '500',
231
+ },
232
+ h5: {
233
+ fontSize: 16,
234
+ lineHeight: 24,
235
+ fontWeight: '500',
236
+ },
237
+ h6: {
238
+ fontSize: 14,
239
+ lineHeight: 20,
240
+ fontWeight: '500',
241
+ },
242
+ codespan: {
243
+ fontSize: 16,
244
+ lineHeight: 24,
245
+ fontStyle: 'italic',
246
+ backgroundColor: '#afb8c133',
247
+ fontWeight: '300',
248
+ },
249
+ code: {
250
+ padding: 16,
251
+ backgroundColor: '#afb8c133',
252
+ minWidth: '100%',
253
+ },
254
+ });
255
+
256
+ export default Parser;
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { Linking, ScrollView, View, ViewStyle } from 'react-native';
3
+ import type { StyleProp, TextStyle } from 'react-native';
4
+ import { Text } from 'react-native';
5
+ import MDImage from './../components/MDImage';
6
+ import { generateRandomString } from '../utils/string';
7
+
8
+ class Renderer {
9
+ private onLinkPress = (href: string) => () => Linking.openURL(href);
10
+
11
+ getTextNode = (
12
+ children: string | React.ReactNode[],
13
+ styles: StyleProp<TextStyle>
14
+ ) => {
15
+ return (
16
+ <Text key={generateRandomString()} style={styles}>
17
+ {children}
18
+ </Text>
19
+ );
20
+ };
21
+
22
+ getLinkNode = (text: string, href: string, styles: StyleProp<TextStyle>) => {
23
+ return (
24
+ <Text
25
+ key={generateRandomString()}
26
+ onPress={this.onLinkPress(href)}
27
+ style={styles}
28
+ >
29
+ {text}
30
+ </Text>
31
+ );
32
+ };
33
+
34
+ getParagraph(children: React.ReactNode[], styles: StyleProp<ViewStyle>) {
35
+ return (
36
+ <View key={generateRandomString()} style={styles}>
37
+ {children}
38
+ </View>
39
+ );
40
+ }
41
+
42
+ getCodeBlock(
43
+ text: string,
44
+ containerStyle: StyleProp<ViewStyle>,
45
+ textStyle: StyleProp<TextStyle>
46
+ ) {
47
+ return (
48
+ <ScrollView contentContainerStyle={containerStyle} horizontal>
49
+ <Text style={textStyle}>{text}</Text>
50
+ </ScrollView>
51
+ );
52
+ }
53
+
54
+ getBlockquote(children: React.ReactNode[], styles: StyleProp<ViewStyle>) {
55
+ return (
56
+ <View key={generateRandomString()} style={styles}>
57
+ {children}
58
+ </View>
59
+ );
60
+ }
61
+
62
+ getHeading(text: string, styles: StyleProp<TextStyle>) {
63
+ return (
64
+ <Text key={generateRandomString()} style={styles}>
65
+ {text}
66
+ </Text>
67
+ );
68
+ }
69
+
70
+ getImage(uri: string, width: number) {
71
+ return <MDImage key={generateRandomString()} uri={uri} width={width} />;
72
+ }
73
+ }
74
+
75
+ export default Renderer;
@@ -0,0 +1,25 @@
1
+ import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
2
+
3
+ export interface MarkedStyles {
4
+ em?: TextStyle;
5
+ strong?: TextStyle;
6
+ text?: TextStyle;
7
+ paragraph?: ViewStyle;
8
+ link?: TextStyle;
9
+ blockquoteText?: TextStyle;
10
+ blockquoteContainer?: ViewStyle;
11
+ h1?: TextStyle;
12
+ h2?: TextStyle;
13
+ h3?: TextStyle;
14
+ h4?: TextStyle;
15
+ h5?: TextStyle;
16
+ h6?: TextStyle;
17
+ image?: ImageStyle;
18
+ codespan?: TextStyle;
19
+ code?: ViewStyle;
20
+ }
21
+
22
+ export interface ParserOptions {
23
+ contentWidth: number;
24
+ styles?: MarkedStyles;
25
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generates a random string
3
+ * @returns string
4
+ */
5
+ export const generateRandomString = () => {
6
+ return (Math.random() + 1).toString(36).substring(7);
7
+ };