react-native-nitro-markdown 0.4.1 → 0.4.3

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 (155) hide show
  1. package/README.md +329 -322
  2. package/lib/commonjs/MarkdownContext.js +2 -1
  3. package/lib/commonjs/MarkdownContext.js.map +1 -1
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/markdown-stream.js +3 -1
  6. package/lib/commonjs/markdown-stream.js.map +1 -1
  7. package/lib/commonjs/markdown.js +51 -35
  8. package/lib/commonjs/markdown.js.map +1 -1
  9. package/lib/commonjs/renderers/code.js +3 -3
  10. package/lib/commonjs/renderers/code.js.map +1 -1
  11. package/lib/commonjs/renderers/heading.js +1 -1
  12. package/lib/commonjs/renderers/heading.js.map +1 -1
  13. package/lib/commonjs/renderers/image.js +7 -5
  14. package/lib/commonjs/renderers/image.js.map +1 -1
  15. package/lib/commonjs/renderers/link.js +15 -3
  16. package/lib/commonjs/renderers/link.js.map +1 -1
  17. package/lib/commonjs/renderers/list.js +2 -2
  18. package/lib/commonjs/renderers/list.js.map +1 -1
  19. package/lib/commonjs/renderers/table.js +32 -15
  20. package/lib/commonjs/renderers/table.js.map +1 -1
  21. package/lib/commonjs/use-markdown-stream.js +16 -14
  22. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  23. package/lib/commonjs/utils/link-security.js +21 -0
  24. package/lib/commonjs/utils/link-security.js.map +1 -0
  25. package/lib/commonjs/utils/stream-timeline.js +62 -0
  26. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  27. package/lib/module/MarkdownContext.js +2 -1
  28. package/lib/module/MarkdownContext.js.map +1 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/markdown-stream.js +3 -1
  31. package/lib/module/markdown-stream.js.map +1 -1
  32. package/lib/module/markdown.js +52 -36
  33. package/lib/module/markdown.js.map +1 -1
  34. package/lib/module/renderers/blockquote.js.map +1 -1
  35. package/lib/module/renderers/code.js +3 -3
  36. package/lib/module/renderers/code.js.map +1 -1
  37. package/lib/module/renderers/heading.js +1 -1
  38. package/lib/module/renderers/heading.js.map +1 -1
  39. package/lib/module/renderers/image.js +7 -5
  40. package/lib/module/renderers/image.js.map +1 -1
  41. package/lib/module/renderers/link.js +15 -3
  42. package/lib/module/renderers/link.js.map +1 -1
  43. package/lib/module/renderers/list.js +2 -2
  44. package/lib/module/renderers/list.js.map +1 -1
  45. package/lib/module/renderers/paragraph.js.map +1 -1
  46. package/lib/module/renderers/table.js +33 -16
  47. package/lib/module/renderers/table.js.map +1 -1
  48. package/lib/module/use-markdown-stream.js +16 -14
  49. package/lib/module/use-markdown-stream.js.map +1 -1
  50. package/lib/module/utils/link-security.js +15 -0
  51. package/lib/module/utils/link-security.js.map +1 -0
  52. package/lib/module/utils/stream-timeline.js +56 -0
  53. package/lib/module/utils/stream-timeline.js.map +1 -0
  54. package/lib/typescript/commonjs/Markdown.nitro.d.ts +3 -3
  55. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  57. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/headless.d.ts +2 -2
  59. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/index.d.ts +1 -1
  61. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/markdown-stream.d.ts +2 -2
  63. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/markdown.d.ts +9 -4
  65. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  67. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  69. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  71. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  73. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  75. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  77. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  79. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  81. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  85. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/theme.d.ts +2 -2
  87. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  90. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  92. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  93. package/lib/typescript/module/Markdown.nitro.d.ts +3 -3
  94. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  95. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  96. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  97. package/lib/typescript/module/headless.d.ts +2 -2
  98. package/lib/typescript/module/headless.d.ts.map +1 -1
  99. package/lib/typescript/module/index.d.ts +1 -1
  100. package/lib/typescript/module/index.d.ts.map +1 -1
  101. package/lib/typescript/module/markdown-stream.d.ts +2 -2
  102. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/module/markdown.d.ts +9 -4
  104. package/lib/typescript/module/markdown.d.ts.map +1 -1
  105. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  106. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  107. package/lib/typescript/module/renderers/code.d.ts +5 -5
  108. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  109. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  110. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  111. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  112. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  113. package/lib/typescript/module/renderers/image.d.ts +2 -2
  114. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  115. package/lib/typescript/module/renderers/link.d.ts +3 -3
  116. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  117. package/lib/typescript/module/renderers/list.d.ts +7 -7
  118. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  119. package/lib/typescript/module/renderers/math.d.ts +4 -4
  120. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  122. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/table.d.ts +3 -3
  124. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  125. package/lib/typescript/module/theme.d.ts +2 -2
  126. package/lib/typescript/module/theme.d.ts.map +1 -1
  127. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  128. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  129. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  130. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  131. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  132. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  133. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +0 -1
  134. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +0 -1
  135. package/package.json +4 -3
  136. package/src/Markdown.nitro.ts +5 -3
  137. package/src/MarkdownContext.ts +31 -25
  138. package/src/headless.ts +2 -2
  139. package/src/index.ts +1 -0
  140. package/src/markdown-stream.tsx +6 -10
  141. package/src/markdown.tsx +86 -45
  142. package/src/renderers/blockquote.tsx +4 -4
  143. package/src/renderers/code.tsx +11 -9
  144. package/src/renderers/heading.tsx +4 -4
  145. package/src/renderers/horizontal-rule.tsx +3 -3
  146. package/src/renderers/image.tsx +11 -9
  147. package/src/renderers/link.tsx +25 -7
  148. package/src/renderers/list.tsx +9 -12
  149. package/src/renderers/math.tsx +4 -4
  150. package/src/renderers/paragraph.tsx +3 -3
  151. package/src/renderers/table.tsx +74 -46
  152. package/src/theme.ts +3 -3
  153. package/src/use-markdown-stream.ts +22 -16
  154. package/src/utils/link-security.ts +22 -0
  155. package/src/utils/stream-timeline.ts +72 -0
@@ -4,28 +4,28 @@ import {
4
4
  type ReactNode,
5
5
  type ComponentType,
6
6
  } from "react";
7
+ import type { MarkdownNode } from "./headless";
7
8
  import {
8
9
  defaultMarkdownTheme,
9
10
  type MarkdownTheme,
10
11
  type NodeStyleOverrides,
11
12
  type StylingStrategy,
12
13
  } from "./theme";
13
- import type { MarkdownNode } from "./headless";
14
14
 
15
- export interface NodeRendererProps {
15
+ export type NodeRendererProps = {
16
16
  node: MarkdownNode;
17
17
  depth: number;
18
18
  inListItem: boolean;
19
19
  parentIsText?: boolean;
20
- }
20
+ };
21
21
 
22
- export interface BaseCustomRendererProps {
22
+ export type BaseCustomRendererProps = {
23
23
  node: MarkdownNode;
24
24
  children: ReactNode;
25
25
  Renderer: ComponentType<NodeRendererProps>;
26
- }
26
+ };
27
27
 
28
- export interface EnhancedRendererProps extends BaseCustomRendererProps {
28
+ export type EnhancedRendererProps = {
29
29
  level?: 1 | 2 | 3 | 4 | 5 | 6;
30
30
  href?: string;
31
31
  title?: string;
@@ -36,63 +36,69 @@ export interface EnhancedRendererProps extends BaseCustomRendererProps {
36
36
  ordered?: boolean;
37
37
  start?: number;
38
38
  checked?: boolean;
39
- }
39
+ } & BaseCustomRendererProps;
40
40
 
41
- export interface HeadingRendererProps extends BaseCustomRendererProps {
41
+ export type HeadingRendererProps = {
42
42
  level: 1 | 2 | 3 | 4 | 5 | 6;
43
- }
43
+ } & BaseCustomRendererProps;
44
44
 
45
- export interface LinkRendererProps extends BaseCustomRendererProps {
45
+ export type LinkRendererProps = {
46
46
  href: string;
47
47
  title?: string;
48
- }
48
+ } & BaseCustomRendererProps;
49
49
 
50
- export interface ImageRendererProps extends BaseCustomRendererProps {
50
+ export type ImageRendererProps = {
51
51
  url: string;
52
52
  alt?: string;
53
53
  title?: string;
54
- }
54
+ } & BaseCustomRendererProps;
55
55
 
56
- export interface CodeBlockRendererProps extends BaseCustomRendererProps {
56
+ export type CodeBlockRendererProps = {
57
57
  content: string;
58
58
  language?: string;
59
- }
59
+ } & BaseCustomRendererProps;
60
60
 
61
- export interface InlineCodeRendererProps extends BaseCustomRendererProps {
61
+ export type InlineCodeRendererProps = {
62
62
  content: string;
63
- }
63
+ } & BaseCustomRendererProps;
64
64
 
65
- export interface ListRendererProps extends BaseCustomRendererProps {
65
+ export type ListRendererProps = {
66
66
  ordered: boolean;
67
67
  start?: number;
68
- }
68
+ } & BaseCustomRendererProps;
69
69
 
70
- export interface TaskListItemRendererProps extends BaseCustomRendererProps {
70
+ export type TaskListItemRendererProps = {
71
71
  checked: boolean;
72
- }
72
+ } & BaseCustomRendererProps;
73
+
74
+ export type CustomRendererProps = {} & EnhancedRendererProps;
73
75
 
74
- export interface CustomRendererProps extends EnhancedRendererProps {}
76
+ export type LinkPressHandler = (
77
+ href: string,
78
+ ) => void | boolean | Promise<void | boolean>;
75
79
 
76
80
  export type CustomRenderer = (
77
- props: EnhancedRendererProps
81
+ props: EnhancedRendererProps,
78
82
  ) => ReactNode | undefined;
79
83
 
80
84
  export type CustomRenderers = Partial<
81
85
  Record<MarkdownNode["type"], CustomRenderer>
82
86
  >;
83
87
 
84
- export interface MarkdownContextValue {
88
+ export type MarkdownContextValue = {
85
89
  renderers: CustomRenderers;
86
90
  theme: MarkdownTheme;
87
91
  styles?: NodeStyleOverrides;
88
92
  stylingStrategy: StylingStrategy;
89
- }
93
+ onLinkPress?: LinkPressHandler;
94
+ };
90
95
 
91
96
  export const MarkdownContext = createContext<MarkdownContextValue>({
92
97
  renderers: {},
93
98
  theme: defaultMarkdownTheme,
94
99
  styles: undefined,
95
100
  stylingStrategy: "opinionated",
101
+ onLinkPress: undefined,
96
102
  });
97
103
 
98
104
  export const useMarkdownContext = () => useContext(MarkdownContext);
package/src/headless.ts CHANGED
@@ -19,7 +19,7 @@ export type { ParserOptions } from "./Markdown.nitro";
19
19
  * Represents a node in the Markdown AST (Abstract Syntax Tree).
20
20
  * Each node has a type and optional properties depending on the node type.
21
21
  */
22
- export interface MarkdownNode {
22
+ export type MarkdownNode = {
23
23
  /** The type of markdown element this node represents. Used to decide how to render the node. */
24
24
  type:
25
25
  | "document"
@@ -73,7 +73,7 @@ export interface MarkdownNode {
73
73
  align?: string;
74
74
  /** Nested child nodes for hierarchical elements like paragraphs, lists, and tables. */
75
75
  children?: MarkdownNode[];
76
- }
76
+ };
77
77
 
78
78
  export const MarkdownParserModule =
79
79
  NitroModules.createHybridObject<MarkdownParser>("MarkdownParser");
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export type {
18
18
  InlineCodeRendererProps,
19
19
  ListRendererProps,
20
20
  TaskListItemRendererProps,
21
+ LinkPressHandler,
21
22
  MarkdownContextValue,
22
23
  } from "./MarkdownContext";
23
24
 
@@ -1,14 +1,8 @@
1
- import {
2
- useState,
3
- useEffect,
4
- useRef,
5
- startTransition,
6
- type FC,
7
- } from "react";
1
+ import { useState, useEffect, useRef, startTransition, type FC } from "react";
8
2
  import { Markdown, type MarkdownProps } from "./markdown";
9
3
  import type { MarkdownSession } from "./specs/MarkdownSession.nitro";
10
4
 
11
- export interface MarkdownStreamProps extends Omit<MarkdownProps, "children"> {
5
+ export type MarkdownStreamProps = {
12
6
  /**
13
7
  * The active MarkdownSession to stream content from.
14
8
  */
@@ -29,7 +23,7 @@ export interface MarkdownStreamProps extends Omit<MarkdownProps, "children"> {
29
23
  * Useful when you want to prioritize user interactions over stream renders.
30
24
  */
31
25
  useTransitionUpdates?: boolean;
32
- }
26
+ } & Omit<MarkdownProps, "children">;
33
27
 
34
28
  /**
35
29
  * A component that renders streaming Markdown from a MarkdownSession.
@@ -68,7 +62,9 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
68
62
  lastEmittedRef.current = latest;
69
63
 
70
64
  if (useTransitionUpdates) {
71
- startTransition(() => setText(latest));
65
+ startTransition(() => {
66
+ setText(latest);
67
+ });
72
68
  } else {
73
69
  setText(latest);
74
70
  }
package/src/markdown.tsx CHANGED
@@ -1,13 +1,11 @@
1
1
  import {
2
- defaultMarkdownTheme,
3
- minimalMarkdownTheme,
4
- mergeThemes,
5
- type MarkdownTheme,
6
- type PartialMarkdownTheme,
7
- type NodeStyleOverrides,
8
- type StylingStrategy,
9
- } from "./theme";
10
- import { useMemo, type ReactNode, type FC, Fragment } from "react";
2
+ useEffect,
3
+ useMemo,
4
+ type FC,
5
+ Fragment,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ } from "react";
11
9
  import {
12
10
  StyleSheet,
13
11
  View,
@@ -28,21 +26,32 @@ import {
28
26
  MarkdownContext,
29
27
  useMarkdownContext,
30
28
  type CustomRenderers,
29
+ type LinkPressHandler,
31
30
  type NodeRendererProps,
32
31
  } from "./MarkdownContext";
33
-
34
- import { Heading } from "./renderers/heading";
35
- import { Paragraph } from "./renderers/paragraph";
36
- import { Link } from "./renderers/link";
37
32
  import { Blockquote } from "./renderers/blockquote";
38
- import { HorizontalRule } from "./renderers/horizontal-rule";
39
33
  import { CodeBlock, InlineCode } from "./renderers/code";
40
- import { List, ListItem, TaskListItem } from "./renderers/list";
41
- import { TableRenderer } from "./renderers/table";
34
+ import { Heading } from "./renderers/heading";
35
+ import { HorizontalRule } from "./renderers/horizontal-rule";
42
36
  import { Image } from "./renderers/image";
37
+ import { Link } from "./renderers/link";
38
+ import { List, ListItem, TaskListItem } from "./renderers/list";
43
39
  import { MathInline, MathBlock } from "./renderers/math";
40
+ import { Paragraph } from "./renderers/paragraph";
41
+ import { TableRenderer } from "./renderers/table";
42
+ import {
43
+ defaultMarkdownTheme,
44
+ minimalMarkdownTheme,
45
+ mergeThemes,
46
+ type MarkdownTheme,
47
+ type PartialMarkdownTheme,
48
+ type NodeStyleOverrides,
49
+ type StylingStrategy,
50
+ } from "./theme";
44
51
 
45
- export interface MarkdownProps {
52
+ const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
53
+
54
+ export type MarkdownProps = {
46
55
  /**
47
56
  * The markdown string to parse and render.
48
57
  */
@@ -93,7 +102,12 @@ export interface MarkdownProps {
93
102
  * Optional style for the container view.
94
103
  */
95
104
  style?: StyleProp<ViewStyle>;
96
- }
105
+ /**
106
+ * Optional link press handler.
107
+ * Return false to prevent the default openURL behavior.
108
+ */
109
+ onLinkPress?: LinkPressHandler;
110
+ };
97
111
 
98
112
  export const Markdown: FC<MarkdownProps> = ({
99
113
  children,
@@ -105,34 +119,42 @@ export const Markdown: FC<MarkdownProps> = ({
105
119
  style,
106
120
  onParsingInProgress,
107
121
  onParseComplete,
122
+ onLinkPress,
108
123
  }) => {
109
- const ast = useMemo(() => {
124
+ const parseResult = useMemo(() => {
110
125
  try {
111
- if (onParsingInProgress) {
112
- onParsingInProgress();
113
- }
114
-
115
- let result: MarkdownNode;
126
+ let ast: MarkdownNode;
116
127
  if (options) {
117
- result = parseMarkdownWithOptions(children, options);
128
+ ast = parseMarkdownWithOptions(children, options);
118
129
  } else {
119
- result = parseMarkdown(children);
130
+ ast = parseMarkdown(children);
120
131
  }
121
132
 
122
- if (onParseComplete) {
123
- onParseComplete({
124
- raw: children,
125
- ast: result,
126
- text: getFlattenedText(result),
127
- });
128
- }
129
-
130
- return result;
131
- } catch (error) {
132
- console.error("Failed to parse markdown:", error);
133
- return null;
133
+ return {
134
+ ast,
135
+ text: getFlattenedText(ast),
136
+ };
137
+ } catch {
138
+ return {
139
+ ast: null,
140
+ text: "",
141
+ };
134
142
  }
135
- }, [children, options, onParsingInProgress, onParseComplete]);
143
+ }, [children, options]);
144
+
145
+ useEffect(() => {
146
+ onParsingInProgress?.();
147
+ }, [children, options, onParsingInProgress]);
148
+
149
+ useEffect(() => {
150
+ if (!parseResult.ast) return;
151
+
152
+ onParseComplete?.({
153
+ raw: children,
154
+ ast: parseResult.ast,
155
+ text: parseResult.text,
156
+ });
157
+ }, [children, onParseComplete, parseResult.ast, parseResult.text]);
136
158
 
137
159
  const theme = useMemo(() => {
138
160
  const base =
@@ -142,9 +164,9 @@ export const Markdown: FC<MarkdownProps> = ({
142
164
  return mergeThemes(base, userTheme);
143
165
  }, [userTheme, stylingStrategy]);
144
166
 
145
- const baseStyles = useMemo(() => createBaseStyles(theme), [theme]);
167
+ const baseStyles = getBaseStyles(theme);
146
168
 
147
- if (!ast) {
169
+ if (!parseResult.ast) {
148
170
  return (
149
171
  <View style={[baseStyles.container, style]}>
150
172
  <Text style={baseStyles.errorText}>Error parsing markdown</Text>
@@ -154,10 +176,16 @@ export const Markdown: FC<MarkdownProps> = ({
154
176
 
155
177
  return (
156
178
  <MarkdownContext.Provider
157
- value={{ renderers, theme, styles: nodeStyles, stylingStrategy }}
179
+ value={{
180
+ renderers,
181
+ theme,
182
+ styles: nodeStyles,
183
+ stylingStrategy,
184
+ onLinkPress,
185
+ }}
158
186
  >
159
187
  <View style={[baseStyles.container, style]}>
160
- <NodeRenderer node={ast} depth={0} inListItem={false} />
188
+ <NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
161
189
  </View>
162
190
  </MarkdownContext.Provider>
163
191
  );
@@ -185,7 +213,7 @@ const NodeRenderer: FC<NodeRendererProps> = ({
185
213
  parentIsText = false,
186
214
  }) => {
187
215
  const { renderers, theme, styles: nodeStyles } = useMarkdownContext();
188
- const baseStyles = useMemo(() => createBaseStyles(theme), [theme]);
216
+ const baseStyles = getBaseStyles(theme);
189
217
 
190
218
  const renderChildren = (
191
219
  children?: MarkdownNode[],
@@ -308,7 +336,9 @@ const NodeRenderer: FC<NodeRendererProps> = ({
308
336
  };
309
337
 
310
338
  const result = customRenderer(enhancedProps);
311
- if (result !== undefined) return <>{result}</>;
339
+ if (result !== undefined) {
340
+ return result as ReactElement | null;
341
+ }
312
342
  }
313
343
 
314
344
  const nodeStyleOverride = nodeStyles?.[node.type];
@@ -491,6 +521,17 @@ const NodeRenderer: FC<NodeRendererProps> = ({
491
521
  }
492
522
  };
493
523
 
524
+ type BaseStyles = ReturnType<typeof createBaseStyles>;
525
+
526
+ const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
527
+ const cached = baseStylesCache.get(theme);
528
+ if (cached) return cached;
529
+
530
+ const created = createBaseStyles(theme);
531
+ baseStylesCache.set(theme, created);
532
+ return created;
533
+ };
534
+
494
535
  const createBaseStyles = (theme: MarkdownTheme) =>
495
536
  StyleSheet.create({
496
537
  container: {
@@ -1,11 +1,11 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import { View, StyleSheet, type ViewStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface BlockquoteProps {
5
+ type BlockquoteProps = {
6
6
  children: ReactNode;
7
7
  style?: ViewStyle;
8
- }
8
+ };
9
9
 
10
10
  export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
11
11
  const { theme } = useMarkdownContext();
@@ -23,7 +23,7 @@ export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
23
23
  borderRadius: theme.borderRadius.s,
24
24
  },
25
25
  }),
26
- [theme]
26
+ [theme],
27
27
  );
28
28
 
29
29
  return <View style={[styles.blockquote, style]}>{children}</View>;
@@ -1,4 +1,4 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -8,16 +8,16 @@ import {
8
8
  type ViewStyle,
9
9
  type TextStyle,
10
10
  } from "react-native";
11
+ import { getTextContent } from "../headless";
11
12
  import { useMarkdownContext } from "../MarkdownContext";
12
13
  import type { MarkdownNode } from "../headless";
13
- import { getTextContent } from "../headless";
14
14
 
15
- interface CodeBlockProps {
15
+ type CodeBlockProps = {
16
16
  language?: string;
17
17
  content?: string;
18
18
  node?: MarkdownNode;
19
19
  style?: ViewStyle;
20
- }
20
+ };
21
21
 
22
22
  export const CodeBlock: FC<CodeBlockProps> = ({
23
23
  language,
@@ -60,14 +60,16 @@ export const CodeBlock: FC<CodeBlockProps> = ({
60
60
  ...(Platform.OS === "android" && { includeFontPadding: false }),
61
61
  },
62
62
  }),
63
- [theme]
63
+ [theme],
64
64
  );
65
65
 
66
66
  const showLanguage = theme.showCodeLanguage && language;
67
67
 
68
68
  return (
69
69
  <View style={[styles.codeBlock, style]}>
70
- {showLanguage && <Text style={styles.codeLanguage}>{language}</Text>}
70
+ {showLanguage ? (
71
+ <Text style={styles.codeLanguage}>{language}</Text>
72
+ ) : null}
71
73
  <ScrollView horizontal showsHorizontalScrollIndicator={false}>
72
74
  <Text style={styles.codeBlockText}>{displayContent}</Text>
73
75
  </ScrollView>
@@ -75,12 +77,12 @@ export const CodeBlock: FC<CodeBlockProps> = ({
75
77
  );
76
78
  };
77
79
 
78
- interface InlineCodeProps {
80
+ type InlineCodeProps = {
79
81
  content?: string;
80
82
  node?: MarkdownNode;
81
83
  children?: ReactNode;
82
84
  style?: TextStyle;
83
- }
85
+ };
84
86
 
85
87
  export const InlineCode: FC<InlineCodeProps> = ({
86
88
  content,
@@ -109,7 +111,7 @@ export const InlineCode: FC<InlineCodeProps> = ({
109
111
  ...(Platform.OS === "android" && { includeFontPadding: false }),
110
112
  },
111
113
  }),
112
- [theme]
114
+ [theme],
113
115
  );
114
116
  return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
115
117
  };
@@ -1,12 +1,12 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import { Text, StyleSheet, Platform, type TextStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface HeadingProps {
5
+ type HeadingProps = {
6
6
  level: number;
7
7
  children: ReactNode;
8
8
  style?: TextStyle;
9
- }
9
+ };
10
10
 
11
11
  const ANDROID_SYSTEM_FONTS = new Set([
12
12
  "sans-serif",
@@ -71,7 +71,7 @@ export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
71
71
  color: theme.colors.textMuted,
72
72
  },
73
73
  }),
74
- [theme]
74
+ [headingWeight, theme],
75
75
  );
76
76
 
77
77
  const headingStyles = [
@@ -2,9 +2,9 @@ import { useMemo, type FC } from "react";
2
2
  import { View, StyleSheet, type ViewStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface HorizontalRuleProps {
5
+ type HorizontalRuleProps = {
6
6
  style?: ViewStyle;
7
- }
7
+ };
8
8
 
9
9
  export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
10
10
  const { theme } = useMarkdownContext();
@@ -17,7 +17,7 @@ export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
17
17
  marginVertical: theme.spacing.xl,
18
18
  },
19
19
  }),
20
- [theme]
20
+ [theme],
21
21
  );
22
22
  return <View style={[styles.horizontalRule, style]} />;
23
23
  };
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  useState,
3
- useEffect,
4
3
  useLayoutEffect,
5
4
  useMemo,
6
5
  type ReactNode,
@@ -15,10 +14,9 @@ import {
15
14
  Platform,
16
15
  type ViewStyle,
17
16
  } from "react-native";
18
-
19
17
  import { parseMarkdownWithOptions, type MarkdownNode } from "../headless";
20
- import type { NodeRendererProps } from "../MarkdownContext";
21
18
  import { useMarkdownContext } from "../MarkdownContext";
19
+ import type { NodeRendererProps } from "../MarkdownContext";
22
20
 
23
21
  const renderInlineContent = (
24
22
  node: MarkdownNode,
@@ -36,13 +34,13 @@ const renderInlineContent = (
36
34
  return null;
37
35
  };
38
36
 
39
- interface ImageProps {
37
+ type ImageProps = {
40
38
  url: string;
41
39
  title?: string;
42
40
  alt?: string;
43
41
  Renderer?: ComponentType<NodeRendererProps>;
44
42
  style?: ViewStyle;
45
- }
43
+ };
46
44
 
47
45
  export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
48
46
  const [loading, setLoading] = useState(true);
@@ -187,22 +185,26 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
187
185
 
188
186
  return (
189
187
  <View style={[styles.imageContainer, style]}>
190
- {loading && !aspectRatio && (
188
+ {loading && !aspectRatio ? (
191
189
  <View style={styles.imageLoading}>
192
190
  <Text style={styles.imageLoadingText}>Loading image...</Text>
193
191
  </View>
194
- )}
192
+ ) : null}
195
193
  <RNImage
196
194
  source={{ uri: url }}
197
195
  style={[styles.image, loading && !aspectRatio && styles.imageHidden]}
198
196
  resizeMode="contain"
199
- onLoad={() => setLoading(false)}
197
+ onLoad={() => {
198
+ setLoading(false);
199
+ }}
200
200
  onError={() => {
201
201
  setLoading(false);
202
202
  setError(true);
203
203
  }}
204
204
  />
205
- {title && !loading && <Text style={styles.imageCaption}>{title}</Text>}
205
+ {title && !loading ? (
206
+ <Text style={styles.imageCaption}>{title}</Text>
207
+ ) : null}
206
208
  </View>
207
209
  );
208
210
  };
@@ -1,4 +1,4 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import {
3
3
  Text,
4
4
  StyleSheet,
@@ -7,15 +7,19 @@ import {
7
7
  type TextStyle,
8
8
  } from "react-native";
9
9
  import { useMarkdownContext } from "../MarkdownContext";
10
+ import {
11
+ getAllowedExternalHref,
12
+ normalizeLinkHref,
13
+ } from "../utils/link-security";
10
14
 
11
- interface LinkProps {
15
+ type LinkProps = {
12
16
  href: string;
13
17
  children: ReactNode;
14
18
  style?: TextStyle;
15
- }
19
+ };
16
20
 
17
21
  export const Link: FC<LinkProps> = ({ href, children, style }) => {
18
- const { theme } = useMarkdownContext();
22
+ const { theme, onLinkPress } = useMarkdownContext();
19
23
  const styles = useMemo(
20
24
  () =>
21
25
  StyleSheet.create({
@@ -27,11 +31,25 @@ export const Link: FC<LinkProps> = ({ href, children, style }) => {
27
31
  ...(Platform.OS === "android" && { includeFontPadding: false }),
28
32
  },
29
33
  }),
30
- [theme]
34
+ [theme],
31
35
  );
32
36
 
33
- const handlePress = () => {
34
- if (href) Linking.openURL(href);
37
+ const handlePress = async () => {
38
+ const normalizedHref = normalizeLinkHref(href);
39
+ if (!normalizedHref) return;
40
+
41
+ try {
42
+ const shouldOpen = (await onLinkPress?.(normalizedHref)) !== false;
43
+ if (!shouldOpen) return;
44
+
45
+ const allowedExternalHref = getAllowedExternalHref(normalizedHref);
46
+ if (!allowedExternalHref) return;
47
+
48
+ const canOpen = await Linking.canOpenURL(allowedExternalHref);
49
+ if (!canOpen) return;
50
+
51
+ await Linking.openURL(allowedExternalHref);
52
+ } catch {}
35
53
  };
36
54
 
37
55
  return (