react-native-marked 7.1.0 → 8.0.0

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 (51) hide show
  1. package/README.md +72 -0
  2. package/dist/commonjs/hooks/useMarkdownWithComponents.js +74 -0
  3. package/dist/commonjs/hooks/useMarkdownWithComponents.js.map +1 -0
  4. package/dist/commonjs/index.js +14 -0
  5. package/dist/commonjs/index.js.map +1 -1
  6. package/dist/commonjs/lib/Parser.js +5 -5
  7. package/dist/commonjs/lib/Parser.js.map +1 -1
  8. package/dist/commonjs/lib/ReactComponentRegistry.js +30 -0
  9. package/dist/commonjs/lib/ReactComponentRegistry.js.map +1 -0
  10. package/dist/commonjs/lib/ReactComponentTokenizer.js +106 -0
  11. package/dist/commonjs/lib/ReactComponentTokenizer.js.map +1 -0
  12. package/dist/commonjs/lib/Renderer.js +6 -5
  13. package/dist/commonjs/lib/Renderer.js.map +1 -1
  14. package/dist/commonjs/utils/svg.js +2 -2
  15. package/dist/commonjs/utils/svg.js.map +1 -1
  16. package/dist/module/hooks/useMarkdownWithComponents.js +66 -0
  17. package/dist/module/hooks/useMarkdownWithComponents.js.map +1 -0
  18. package/dist/module/index.js +3 -1
  19. package/dist/module/index.js.map +1 -1
  20. package/dist/module/lib/Parser.js +5 -5
  21. package/dist/module/lib/Parser.js.map +1 -1
  22. package/dist/module/lib/ReactComponentRegistry.js +23 -0
  23. package/dist/module/lib/ReactComponentRegistry.js.map +1 -0
  24. package/dist/module/lib/ReactComponentTokenizer.js +100 -0
  25. package/dist/module/lib/ReactComponentTokenizer.js.map +1 -0
  26. package/dist/module/lib/Renderer.js +6 -5
  27. package/dist/module/lib/Renderer.js.map +1 -1
  28. package/dist/module/utils/svg.js +2 -2
  29. package/dist/module/utils/svg.js.map +1 -1
  30. package/dist/typescript/hooks/useMarkdownWithComponents.d.ts +7 -0
  31. package/dist/typescript/hooks/useMarkdownWithComponents.d.ts.map +1 -0
  32. package/dist/typescript/index.d.ts +5 -2
  33. package/dist/typescript/index.d.ts.map +1 -1
  34. package/dist/typescript/lib/Parser.d.ts.map +1 -1
  35. package/dist/typescript/lib/ReactComponentRegistry.d.ts +22 -0
  36. package/dist/typescript/lib/ReactComponentRegistry.d.ts.map +1 -0
  37. package/dist/typescript/lib/ReactComponentTokenizer.d.ts +15 -0
  38. package/dist/typescript/lib/ReactComponentTokenizer.d.ts.map +1 -0
  39. package/dist/typescript/lib/Renderer.d.ts +3 -3
  40. package/dist/typescript/lib/Renderer.d.ts.map +1 -1
  41. package/dist/typescript/lib/types.d.ts +3 -3
  42. package/dist/typescript/lib/types.d.ts.map +1 -1
  43. package/package.json +18 -18
  44. package/src/hooks/useMarkdownWithComponents.tsx +89 -0
  45. package/src/index.ts +13 -1
  46. package/src/lib/Parser.tsx +6 -4
  47. package/src/lib/ReactComponentRegistry.tsx +52 -0
  48. package/src/lib/ReactComponentTokenizer.ts +149 -0
  49. package/src/lib/Renderer.tsx +11 -3
  50. package/src/lib/types.ts +8 -1
  51. package/src/utils/svg.ts +2 -2
@@ -0,0 +1,89 @@
1
+ import type { ReactNode } from "react";
2
+ import React, { useMemo } from "react";
3
+ import { useReactComponentRegistry } from "../lib/ReactComponentRegistry";
4
+ import type { ReactComponentToken } from "../lib/ReactComponentTokenizer";
5
+ import {
6
+ isReactComponentToken,
7
+ ReactComponentTokenizer,
8
+ } from "../lib/ReactComponentTokenizer";
9
+ import Renderer from "../lib/Renderer";
10
+ import type { RendererInterface } from "../lib/types";
11
+ import type { useMarkdownHookOptions } from "./useMarkdown";
12
+ import useMarkdown from "./useMarkdown";
13
+
14
+ export interface useMarkdownWithComponentsOptions
15
+ extends Omit<useMarkdownHookOptions, "tokenizer"> {}
16
+
17
+ interface ComponentData {
18
+ token: ReactComponentToken;
19
+ id: string;
20
+ }
21
+
22
+ export function useMarkdownWithComponents(
23
+ value: string,
24
+ options?: useMarkdownWithComponentsOptions,
25
+ ): ReactNode[] {
26
+ const registry = useReactComponentRegistry();
27
+
28
+ const { tokenizer, componentMap } = useMemo(() => {
29
+ const map = new Map<string, ComponentData>();
30
+
31
+ const customTokenizer = new ReactComponentTokenizer();
32
+ const originalHtml = customTokenizer.html.bind(customTokenizer);
33
+
34
+ let componentCounter = 0;
35
+
36
+ customTokenizer.html = (src: string) => {
37
+ const token = originalHtml(src);
38
+ if (token && isReactComponentToken(token)) {
39
+ const id = `${token.componentName}-${componentCounter++}`;
40
+ map.set(token.raw, { token, id });
41
+ }
42
+ return token;
43
+ };
44
+
45
+ return { tokenizer: customTokenizer, componentMap: map };
46
+ }, []);
47
+
48
+ const baseRenderer = useMemo(
49
+ () => options?.renderer ?? new Renderer(),
50
+ [options?.renderer],
51
+ );
52
+
53
+ const renderer = useMemo<RendererInterface>(() => {
54
+ const wrappedRenderer = Object.create(baseRenderer) as typeof baseRenderer;
55
+
56
+ wrappedRenderer.html = (text: string | ReactNode[], styles): ReactNode => {
57
+ if (typeof text === "string") {
58
+ const data = componentMap.get(text);
59
+ if (data) {
60
+ if (registry?.hasComponent(data.token.componentName)) {
61
+ const Component = registry.getComponent(data.token.componentName);
62
+ if (Component) {
63
+ return (
64
+ <Component key={data.id} props={data.token.componentProps}>
65
+ {data.token.componentChildren || undefined}
66
+ </Component>
67
+ );
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ }
73
+
74
+ return baseRenderer.html(text, styles);
75
+ };
76
+
77
+ return wrappedRenderer;
78
+ }, [baseRenderer, componentMap, registry]);
79
+
80
+ const elements = useMarkdown(value, {
81
+ ...options,
82
+ tokenizer,
83
+ renderer,
84
+ });
85
+
86
+ return elements;
87
+ }
88
+
89
+ export default useMarkdownWithComponents;
package/src/index.ts CHANGED
@@ -5,7 +5,10 @@ import {
5
5
  marked,
6
6
  } from "marked";
7
7
  import useMarkdown, { type useMarkdownHookOptions } from "./hooks/useMarkdown";
8
+ import useMarkdownWithComponents from "./hooks/useMarkdownWithComponents";
8
9
  import Markdown from "./lib/Markdown";
10
+ import type { ReactComponentRegistry } from "./lib/ReactComponentRegistry";
11
+ import { ReactComponentRegistryProvider } from "./lib/ReactComponentRegistry";
9
12
  import Renderer from "./lib/Renderer";
10
13
  import type {
11
14
  MarkdownProps,
@@ -24,8 +27,17 @@ export type {
24
27
  useMarkdownHookOptions,
25
28
  Token,
26
29
  Tokens,
30
+ ReactComponentRegistry,
27
31
  };
28
32
 
29
- export { useMarkdown, MarkedLexer, Renderer, MarkedTokenizer, MarkedHooks };
33
+ export {
34
+ useMarkdown,
35
+ useMarkdownWithComponents,
36
+ MarkedLexer,
37
+ Renderer,
38
+ MarkedTokenizer,
39
+ MarkedHooks,
40
+ ReactComponentRegistryProvider,
41
+ };
30
42
 
31
43
  export default Markdown;
@@ -82,7 +82,7 @@ class Parser {
82
82
  return this.renderer.hr(this.styles.hr);
83
83
  }
84
84
  case "list": {
85
- let startIndex = Number.parseInt(token.start.toString());
85
+ let startIndex = Number.parseInt(token.start.toString(), 10);
86
86
  if (Number.isNaN(startIndex)) {
87
87
  startIndex = 1;
88
88
  }
@@ -139,17 +139,18 @@ class Parser {
139
139
  const href = getValidURL(this.baseUrl, token.href);
140
140
 
141
141
  if (this.hasDuplicateTextChildToken(token)) {
142
- return this.renderer.link(token.text, href, linkStyle);
142
+ return this.renderer.link(token.text, href, linkStyle, token.title);
143
143
  }
144
144
 
145
145
  const children = this._parse(token.tokens, linkStyle);
146
- return this.renderer.link(children, href, linkStyle);
146
+ return this.renderer.link(children, href, linkStyle, token.title);
147
147
  }
148
148
  case "image": {
149
149
  return this.renderer.image(
150
150
  token.href,
151
- token.text || token.title,
151
+ token.text,
152
152
  this.styles.image,
153
+ token.title,
153
154
  );
154
155
  }
155
156
  case "strong": {
@@ -277,6 +278,7 @@ class Parser {
277
278
  imageToken.href,
278
279
  imageToken.text ?? imageToken.title ?? "",
279
280
  this.styles.image,
281
+ imageToken.title,
280
282
  ),
281
283
  );
282
284
  }
@@ -0,0 +1,52 @@
1
+ import type { ReactNode } from "react";
2
+ import React, { createContext, useContext, useMemo } from "react";
3
+ import type { ReactComponentProps } from "./ReactComponentTokenizer";
4
+
5
+ export interface ReactComponentRendererProps {
6
+ props: ReactComponentProps;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export type ReactComponentRenderer = (
11
+ props: ReactComponentRendererProps,
12
+ ) => ReactNode;
13
+
14
+ export type ReactComponentRegistry = Record<string, ReactComponentRenderer>;
15
+
16
+ interface ReactComponentRegistryContextValue {
17
+ components: ReactComponentRegistry;
18
+ getComponent: (name: string) => ReactComponentRenderer | undefined;
19
+ hasComponent: (name: string) => boolean;
20
+ }
21
+
22
+ const ReactComponentRegistryContext =
23
+ createContext<ReactComponentRegistryContextValue | null>(null);
24
+
25
+ interface ReactComponentRegistryProviderProps {
26
+ components: ReactComponentRegistry;
27
+ children: ReactNode;
28
+ }
29
+
30
+ export function ReactComponentRegistryProvider({
31
+ components,
32
+ children,
33
+ }: ReactComponentRegistryProviderProps) {
34
+ const contextValue = useMemo<ReactComponentRegistryContextValue>(
35
+ () => ({
36
+ components,
37
+ getComponent: (name: string) => components[name],
38
+ hasComponent: (name: string) => name in components,
39
+ }),
40
+ [components],
41
+ );
42
+
43
+ return (
44
+ <ReactComponentRegistryContext.Provider value={contextValue}>
45
+ {children}
46
+ </ReactComponentRegistryContext.Provider>
47
+ );
48
+ }
49
+
50
+ export function useReactComponentRegistry(): ReactComponentRegistryContextValue | null {
51
+ return useContext(ReactComponentRegistryContext);
52
+ }
@@ -0,0 +1,149 @@
1
+ import type { Tokens } from "marked";
2
+ import { Tokenizer } from "marked";
3
+
4
+ const SELF_CLOSING_REGEX = /^<([A-Z][a-zA-Z0-9]*)([^>]*?)\s*\/>/;
5
+ const OPENING_TAG_REGEX = /^<([A-Z][a-zA-Z0-9]*)([^>]*)>/;
6
+ const PROP_REGEX = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}))?/g;
7
+
8
+ export type ReactComponentProps = Record<string, string | boolean | number>;
9
+
10
+ export interface ReactComponentToken extends Tokens.HTML {
11
+ componentName: string;
12
+ componentProps: ReactComponentProps;
13
+ componentChildren: string;
14
+ }
15
+
16
+ function parseProps(propsString: string): ReactComponentProps {
17
+ const props: ReactComponentProps = {};
18
+ if (!propsString) return props;
19
+
20
+ let match: RegExpExecArray | null;
21
+
22
+ match = PROP_REGEX.exec(propsString);
23
+ while (match !== null) {
24
+ const [, propName, doubleQuoted, singleQuoted, braced] = match;
25
+ if (propName) {
26
+ if (doubleQuoted !== undefined) {
27
+ props[propName] = doubleQuoted;
28
+ } else if (singleQuoted !== undefined) {
29
+ props[propName] = singleQuoted;
30
+ } else if (braced !== undefined) {
31
+ const trimmed = braced.trim();
32
+ if (trimmed === "true") {
33
+ props[propName] = true;
34
+ } else if (trimmed === "false") {
35
+ props[propName] = false;
36
+ } else if (!Number.isNaN(Number(trimmed))) {
37
+ props[propName] = Number(trimmed);
38
+ } else {
39
+ props[propName] = trimmed;
40
+ }
41
+ } else {
42
+ props[propName] = true;
43
+ }
44
+ }
45
+ match = PROP_REGEX.exec(propsString);
46
+ }
47
+
48
+ PROP_REGEX.lastIndex = 0;
49
+ return props;
50
+ }
51
+
52
+ function findClosingTag(
53
+ src: string,
54
+ componentName: string,
55
+ startIndex: number,
56
+ ): { endIndex: number; children: string } | null {
57
+ const openingTag = new RegExp(`<${componentName}(?:\\s[^>]*)?>`, "g");
58
+ const closingTag = `</${componentName}>`;
59
+
60
+ let depth = 1;
61
+ let currentIndex = startIndex;
62
+
63
+ while (depth > 0 && currentIndex < src.length) {
64
+ const closingIndex = src.indexOf(closingTag, currentIndex);
65
+ if (closingIndex === -1) return null;
66
+
67
+ openingTag.lastIndex = currentIndex;
68
+ let nestedMatch: RegExpExecArray | null;
69
+ nestedMatch = openingTag.exec(src);
70
+ while (nestedMatch !== null && nestedMatch.index < closingIndex) {
71
+ depth++;
72
+ nestedMatch = openingTag.exec(src);
73
+ }
74
+
75
+ depth--;
76
+ if (depth === 0) {
77
+ return {
78
+ endIndex: closingIndex + closingTag.length,
79
+ children: src.slice(startIndex, closingIndex),
80
+ };
81
+ }
82
+ currentIndex = closingIndex + closingTag.length;
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ export class ReactComponentTokenizer extends Tokenizer {
89
+ override html(src: string): Tokens.HTML | undefined {
90
+ const selfClosingMatch = SELF_CLOSING_REGEX.exec(src);
91
+ if (selfClosingMatch) {
92
+ const [raw, componentName, propsString] = selfClosingMatch;
93
+ return this.createReactComponentToken(
94
+ raw,
95
+ componentName ?? "",
96
+ propsString ?? "",
97
+ "",
98
+ );
99
+ }
100
+
101
+ const openingMatch = OPENING_TAG_REGEX.exec(src);
102
+ if (openingMatch) {
103
+ const [openingTag, componentName, propsString] = openingMatch;
104
+ const result = findClosingTag(
105
+ src,
106
+ componentName ?? "",
107
+ openingTag?.length ?? 0,
108
+ );
109
+
110
+ if (result) {
111
+ const raw = src.slice(0, result.endIndex);
112
+ return this.createReactComponentToken(
113
+ raw,
114
+ componentName ?? "",
115
+ propsString ?? "",
116
+ result.children,
117
+ );
118
+ }
119
+ }
120
+
121
+ return super.html(src);
122
+ }
123
+
124
+ private createReactComponentToken(
125
+ raw: string,
126
+ componentName: string,
127
+ propsString: string,
128
+ children: string,
129
+ ): ReactComponentToken {
130
+ return {
131
+ type: "html",
132
+ raw,
133
+ text: raw,
134
+ block: true,
135
+ pre: false,
136
+ componentName,
137
+ componentProps: parseProps(propsString),
138
+ componentChildren: children.trim(),
139
+ };
140
+ }
141
+ }
142
+
143
+ export function isReactComponentToken(
144
+ token: Tokens.HTML,
145
+ ): token is ReactComponentToken {
146
+ return "componentName" in token && typeof token.componentName === "string";
147
+ }
148
+
149
+ export default ReactComponentTokenizer;
@@ -97,12 +97,14 @@ class Renderer implements RendererInterface {
97
97
  children: string | ReactNode[],
98
98
  href: string,
99
99
  styles?: TextStyle,
100
+ title?: string,
100
101
  ): ReactNode {
101
102
  return (
102
103
  <Text
103
104
  selectable
104
105
  accessibilityRole="link"
105
106
  accessibilityHint="Opens in a new window"
107
+ accessibilityLabel={title || "Link"}
106
108
  key={this.getKey()}
107
109
  onPress={onLinkPress(href)}
108
110
  style={styles}
@@ -112,12 +114,17 @@ class Renderer implements RendererInterface {
112
114
  );
113
115
  }
114
116
 
115
- image(uri: string, alt?: string, style?: ImageStyle): ReactNode {
117
+ image(
118
+ uri: string,
119
+ alt?: string,
120
+ style?: ImageStyle,
121
+ title?: string,
122
+ ): ReactNode {
116
123
  const key = this.getKey();
117
124
  if (uri.endsWith(".svg")) {
118
125
  return <MDSvg uri={uri} key={key} />;
119
126
  }
120
- return <MDImage key={key} uri={uri} alt={alt} style={style} />;
127
+ return <MDImage key={key} uri={uri} alt={alt || title} style={style} />;
121
128
  }
122
129
 
123
130
  strong(children: string | ReactNode[], styles?: TextStyle): ReactNode {
@@ -153,8 +160,9 @@ class Renderer implements RendererInterface {
153
160
  imageUrl: string,
154
161
  alt?: string,
155
162
  style?: ImageStyle,
163
+ title?: string,
156
164
  ): ReactNode {
157
- const imageNode = this.image(imageUrl, alt, style);
165
+ const imageNode = this.image(imageUrl, alt, style, title);
158
166
  return (
159
167
  <TouchableHighlight
160
168
  accessibilityRole="link"
package/src/lib/types.ts CHANGED
@@ -55,8 +55,14 @@ export interface RendererInterface {
55
55
  children: string | ReactNode[],
56
56
  href: string,
57
57
  styles?: TextStyle,
58
+ title?: string,
59
+ ): ReactNode;
60
+ image(
61
+ uri: string,
62
+ alt?: string,
63
+ style?: ImageStyle,
64
+ title?: string,
58
65
  ): ReactNode;
59
- image(uri: string, alt?: string, style?: ImageStyle): ReactNode;
60
66
  strong(children: string | ReactNode[], styles?: TextStyle): ReactNode;
61
67
  em(children: string | ReactNode[], styles?: TextStyle): ReactNode;
62
68
  codespan(text: string, styles?: TextStyle): ReactNode;
@@ -69,6 +75,7 @@ export interface RendererInterface {
69
75
  imageUrl: string,
70
76
  alt?: string,
71
77
  style?: ImageStyle,
78
+ title?: string | null,
72
79
  ): ReactNode;
73
80
  table(
74
81
  header: ReactNode[][],
package/src/utils/svg.ts CHANGED
@@ -7,8 +7,8 @@ export const getSvgDimensions = (svg: string) => {
7
7
  const rootChild = parsed.children[0] as ElementNode;
8
8
 
9
9
  return {
10
- width: Number.parseInt(String(rootChild.properties?.width ?? "0")),
11
- height: Number.parseInt(String(rootChild.properties?.height ?? "0")),
10
+ width: Number.parseInt(String(rootChild.properties?.width ?? "0"), 10),
11
+ height: Number.parseInt(String(rootChild.properties?.height ?? "0"), 10),
12
12
  viewBox: String(rootChild.properties?.viewBox ?? ""),
13
13
  };
14
14
  };