@neeleshyadav/react-native-html-renderer 1.1.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 (155) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +407 -0
  3. package/lib/module/HtmlRenderer.js +183 -0
  4. package/lib/module/HtmlRenderer.js.map +1 -0
  5. package/lib/module/context/index.js +32 -0
  6. package/lib/module/context/index.js.map +1 -0
  7. package/lib/module/hooks/index.js +6 -0
  8. package/lib/module/hooks/index.js.map +1 -0
  9. package/lib/module/hooks/useContentWidth.js +12 -0
  10. package/lib/module/hooks/useContentWidth.js.map +1 -0
  11. package/lib/module/hooks/useHtmlParser.js +16 -0
  12. package/lib/module/hooks/useHtmlParser.js.map +1 -0
  13. package/lib/module/hooks/useTagStyle.js +26 -0
  14. package/lib/module/hooks/useTagStyle.js.map +1 -0
  15. package/lib/module/index.js +23 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/module/parser/index.js +62 -0
  19. package/lib/module/parser/index.js.map +1 -0
  20. package/lib/module/renderer/ErrorBoundary.js +66 -0
  21. package/lib/module/renderer/ErrorBoundary.js.map +1 -0
  22. package/lib/module/renderer/NodeRenderer.js +279 -0
  23. package/lib/module/renderer/NodeRenderer.js.map +1 -0
  24. package/lib/module/renderer/index.js +5 -0
  25. package/lib/module/renderer/index.js.map +1 -0
  26. package/lib/module/renderer/tags/BlockTags.js +28 -0
  27. package/lib/module/renderer/tags/BlockTags.js.map +1 -0
  28. package/lib/module/renderer/tags/FormTags.js +129 -0
  29. package/lib/module/renderer/tags/FormTags.js.map +1 -0
  30. package/lib/module/renderer/tags/ImageTag.js +163 -0
  31. package/lib/module/renderer/tags/ImageTag.js.map +1 -0
  32. package/lib/module/renderer/tags/LinkTag.js +50 -0
  33. package/lib/module/renderer/tags/LinkTag.js.map +1 -0
  34. package/lib/module/renderer/tags/ListTags.js +96 -0
  35. package/lib/module/renderer/tags/ListTags.js.map +1 -0
  36. package/lib/module/renderer/tags/MediaTags.js +69 -0
  37. package/lib/module/renderer/tags/MediaTags.js.map +1 -0
  38. package/lib/module/renderer/tags/TableTags.js +48 -0
  39. package/lib/module/renderer/tags/TableTags.js.map +1 -0
  40. package/lib/module/renderer/tags/TextTags.js +87 -0
  41. package/lib/module/renderer/tags/TextTags.js.map +1 -0
  42. package/lib/module/renderer/tags/index.js +11 -0
  43. package/lib/module/renderer/tags/index.js.map +1 -0
  44. package/lib/module/styles/cssToRn.js +34 -0
  45. package/lib/module/styles/cssToRn.js.map +1 -0
  46. package/lib/module/styles/darkModeStyles.js +81 -0
  47. package/lib/module/styles/darkModeStyles.js.map +1 -0
  48. package/lib/module/styles/defaultStyles.js +218 -0
  49. package/lib/module/styles/defaultStyles.js.map +1 -0
  50. package/lib/module/styles/index.js +7 -0
  51. package/lib/module/styles/index.js.map +1 -0
  52. package/lib/module/styles/mergeStyles.js +47 -0
  53. package/lib/module/styles/mergeStyles.js.map +1 -0
  54. package/lib/module/types/index.js +4 -0
  55. package/lib/module/types/index.js.map +1 -0
  56. package/lib/module/utils/accessibility.js +108 -0
  57. package/lib/module/utils/accessibility.js.map +1 -0
  58. package/lib/module/utils/cache.js +69 -0
  59. package/lib/module/utils/cache.js.map +1 -0
  60. package/lib/module/utils/index.js +95 -0
  61. package/lib/module/utils/index.js.map +1 -0
  62. package/lib/module/utils/sanitize.js +102 -0
  63. package/lib/module/utils/sanitize.js.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/HtmlRenderer.d.ts +15 -0
  66. package/lib/typescript/src/HtmlRenderer.d.ts.map +1 -0
  67. package/lib/typescript/src/context/index.d.ts +5 -0
  68. package/lib/typescript/src/context/index.d.ts.map +1 -0
  69. package/lib/typescript/src/hooks/index.d.ts +4 -0
  70. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  71. package/lib/typescript/src/hooks/useContentWidth.d.ts +6 -0
  72. package/lib/typescript/src/hooks/useContentWidth.d.ts.map +1 -0
  73. package/lib/typescript/src/hooks/useHtmlParser.d.ts +11 -0
  74. package/lib/typescript/src/hooks/useHtmlParser.d.ts.map +1 -0
  75. package/lib/typescript/src/hooks/useTagStyle.d.ts +11 -0
  76. package/lib/typescript/src/hooks/useTagStyle.d.ts.map +1 -0
  77. package/lib/typescript/src/index.d.ts +9 -0
  78. package/lib/typescript/src/index.d.ts.map +1 -0
  79. package/lib/typescript/src/parser/index.d.ts +10 -0
  80. package/lib/typescript/src/parser/index.d.ts.map +1 -0
  81. package/lib/typescript/src/renderer/ErrorBoundary.d.ts +22 -0
  82. package/lib/typescript/src/renderer/ErrorBoundary.d.ts.map +1 -0
  83. package/lib/typescript/src/renderer/NodeRenderer.d.ts +7 -0
  84. package/lib/typescript/src/renderer/NodeRenderer.d.ts.map +1 -0
  85. package/lib/typescript/src/renderer/index.d.ts +3 -0
  86. package/lib/typescript/src/renderer/index.d.ts.map +1 -0
  87. package/lib/typescript/src/renderer/tags/BlockTags.d.ts +18 -0
  88. package/lib/typescript/src/renderer/tags/BlockTags.d.ts.map +1 -0
  89. package/lib/typescript/src/renderer/tags/FormTags.d.ts +16 -0
  90. package/lib/typescript/src/renderer/tags/FormTags.d.ts.map +1 -0
  91. package/lib/typescript/src/renderer/tags/ImageTag.d.ts +18 -0
  92. package/lib/typescript/src/renderer/tags/ImageTag.d.ts.map +1 -0
  93. package/lib/typescript/src/renderer/tags/LinkTag.d.ts +19 -0
  94. package/lib/typescript/src/renderer/tags/LinkTag.d.ts.map +1 -0
  95. package/lib/typescript/src/renderer/tags/ListTags.d.ts +15 -0
  96. package/lib/typescript/src/renderer/tags/ListTags.d.ts.map +1 -0
  97. package/lib/typescript/src/renderer/tags/MediaTags.d.ts +14 -0
  98. package/lib/typescript/src/renderer/tags/MediaTags.d.ts.map +1 -0
  99. package/lib/typescript/src/renderer/tags/TableTags.d.ts +15 -0
  100. package/lib/typescript/src/renderer/tags/TableTags.d.ts.map +1 -0
  101. package/lib/typescript/src/renderer/tags/TextTags.d.ts +22 -0
  102. package/lib/typescript/src/renderer/tags/TextTags.d.ts.map +1 -0
  103. package/lib/typescript/src/renderer/tags/index.d.ts +9 -0
  104. package/lib/typescript/src/renderer/tags/index.d.ts.map +1 -0
  105. package/lib/typescript/src/styles/cssToRn.d.ts +11 -0
  106. package/lib/typescript/src/styles/cssToRn.d.ts.map +1 -0
  107. package/lib/typescript/src/styles/darkModeStyles.d.ts +7 -0
  108. package/lib/typescript/src/styles/darkModeStyles.d.ts.map +1 -0
  109. package/lib/typescript/src/styles/defaultStyles.d.ts +8 -0
  110. package/lib/typescript/src/styles/defaultStyles.d.ts.map +1 -0
  111. package/lib/typescript/src/styles/index.d.ts +5 -0
  112. package/lib/typescript/src/styles/index.d.ts.map +1 -0
  113. package/lib/typescript/src/styles/mergeStyles.d.ts +10 -0
  114. package/lib/typescript/src/styles/mergeStyles.d.ts.map +1 -0
  115. package/lib/typescript/src/types/index.d.ts +158 -0
  116. package/lib/typescript/src/types/index.d.ts.map +1 -0
  117. package/lib/typescript/src/utils/accessibility.d.ts +32 -0
  118. package/lib/typescript/src/utils/accessibility.d.ts.map +1 -0
  119. package/lib/typescript/src/utils/cache.d.ts +24 -0
  120. package/lib/typescript/src/utils/cache.d.ts.map +1 -0
  121. package/lib/typescript/src/utils/index.d.ts +33 -0
  122. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  123. package/lib/typescript/src/utils/sanitize.d.ts +11 -0
  124. package/lib/typescript/src/utils/sanitize.d.ts.map +1 -0
  125. package/package.json +171 -0
  126. package/src/HtmlRenderer.tsx +216 -0
  127. package/src/context/index.tsx +30 -0
  128. package/src/hooks/index.ts +3 -0
  129. package/src/hooks/useContentWidth.ts +9 -0
  130. package/src/hooks/useHtmlParser.ts +18 -0
  131. package/src/hooks/useTagStyle.ts +23 -0
  132. package/src/index.tsx +39 -0
  133. package/src/parser/index.ts +80 -0
  134. package/src/renderer/ErrorBoundary.tsx +80 -0
  135. package/src/renderer/NodeRenderer.tsx +345 -0
  136. package/src/renderer/index.tsx +2 -0
  137. package/src/renderer/tags/BlockTags.tsx +49 -0
  138. package/src/renderer/tags/FormTags.tsx +169 -0
  139. package/src/renderer/tags/ImageTag.tsx +215 -0
  140. package/src/renderer/tags/LinkTag.tsx +76 -0
  141. package/src/renderer/tags/ListTags.tsx +148 -0
  142. package/src/renderer/tags/MediaTags.tsx +81 -0
  143. package/src/renderer/tags/TableTags.tsx +94 -0
  144. package/src/renderer/tags/TextTags.tsx +139 -0
  145. package/src/renderer/tags/index.ts +8 -0
  146. package/src/styles/cssToRn.ts +45 -0
  147. package/src/styles/darkModeStyles.ts +80 -0
  148. package/src/styles/defaultStyles.ts +176 -0
  149. package/src/styles/index.ts +4 -0
  150. package/src/styles/mergeStyles.ts +59 -0
  151. package/src/types/index.ts +229 -0
  152. package/src/utils/accessibility.ts +132 -0
  153. package/src/utils/cache.ts +83 -0
  154. package/src/utils/index.ts +151 -0
  155. package/src/utils/sanitize.ts +149 -0
@@ -0,0 +1,216 @@
1
+ import { useMemo } from 'react';
2
+ import { View, useColorScheme } from 'react-native';
3
+ import type {
4
+ HtmlRendererProps,
5
+ HtmlRendererContextValue,
6
+ TagsStyles,
7
+ } from './types';
8
+ import { HtmlRendererContext } from './context';
9
+ import { parseHTML } from './parser';
10
+ import { getDefaultDarkModeStyles } from './styles/darkModeStyles';
11
+ import { renderNodes } from './renderer';
12
+ import { ErrorBoundary } from './renderer/ErrorBoundary';
13
+ import { sanitizeDOM } from './utils/sanitize';
14
+ import { getCachedDOM, setCachedDOM, buildDOMCacheKey } from './utils/cache';
15
+
16
+ /**
17
+ * Renders an HTML string into native React Native components.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <HtmlRenderer
22
+ * html="<h1>Hello</h1><p>World</p>"
23
+ * contentWidth={350}
24
+ * onLinkPress={(href) => Linking.openURL(href)}
25
+ * />
26
+ * ```
27
+ */
28
+ export function HtmlRenderer({
29
+ html,
30
+ contentWidth,
31
+ baseStyle,
32
+ tagsStyles,
33
+ classesStyles,
34
+ idsStyles,
35
+ customRenderers,
36
+ onLinkPress,
37
+ onImagePress,
38
+ onError,
39
+ fallback,
40
+ ignoredTags,
41
+ ignoredStyles,
42
+ allowedStyles,
43
+ defaultTextProps,
44
+ defaultViewProps,
45
+ renderersProps,
46
+ maxImagesWidth,
47
+ imagesInitialDimensions,
48
+ listsPrefixesRenderers,
49
+ emSize = 14,
50
+ systemFonts,
51
+ fallbackFonts,
52
+ debug = false,
53
+ allowDangerousHtml = false,
54
+ darkModeStyles,
55
+ colorScheme: colorSchemeProp,
56
+ allowFontScaling = true,
57
+ maxFontSizeMultiplier,
58
+ }: HtmlRendererProps) {
59
+ // --- Color scheme ---
60
+ const systemScheme = useColorScheme();
61
+ const resolvedScheme = colorSchemeProp ?? systemScheme;
62
+ const isDark = resolvedScheme === 'dark';
63
+
64
+ // --- Memoize set conversions ---
65
+ const ignoredTagsSet = useMemo(
66
+ () => new Set(ignoredTags ?? []),
67
+ [ignoredTags]
68
+ );
69
+ const ignoredStylesSet = useMemo(
70
+ () => new Set(ignoredStyles ?? []),
71
+ [ignoredStyles]
72
+ );
73
+ const allowedStylesSet = useMemo(
74
+ () => (allowedStyles ? new Set(allowedStyles) : null),
75
+ [allowedStyles]
76
+ );
77
+
78
+ // --- Parse DOM (with cache, key includes sanitization params to prevent poisoning) ---
79
+ const nodes = useMemo(() => {
80
+ try {
81
+ const cacheKey = buildDOMCacheKey(
82
+ html,
83
+ allowDangerousHtml,
84
+ ignoredTagsSet
85
+ );
86
+ const cached = getCachedDOM(cacheKey);
87
+ if (cached) return cached;
88
+
89
+ let parsed = parseHTML(html, ignoredTagsSet);
90
+
91
+ // Sanitize if not explicitly opted out
92
+ if (!allowDangerousHtml) {
93
+ parsed = sanitizeDOM(parsed);
94
+ }
95
+
96
+ setCachedDOM(cacheKey, parsed);
97
+ return parsed;
98
+ } catch (e) {
99
+ const error = e instanceof Error ? e : new Error('Failed to parse HTML');
100
+ if (debug) {
101
+ console.error('[HtmlRenderer] Parse error:', error);
102
+ }
103
+ onError?.(error);
104
+ return [];
105
+ }
106
+ }, [html, ignoredTagsSet, allowDangerousHtml, debug, onError]);
107
+
108
+ // --- Merge dark mode styles ---
109
+ const effectiveTagsStyles: TagsStyles = useMemo(() => {
110
+ const base = tagsStyles ?? {};
111
+ if (!isDark) return base;
112
+
113
+ const defaultDark = getDefaultDarkModeStyles();
114
+ const userDark = darkModeStyles ?? {};
115
+
116
+ // Merge: base tagsStyles + default dark overrides + user dark overrides
117
+ const merged: TagsStyles = { ...base };
118
+ for (const tag of Object.keys(defaultDark)) {
119
+ merged[tag] = { ...(merged[tag] ?? {}), ...defaultDark[tag] };
120
+ }
121
+ for (const tag of Object.keys(userDark)) {
122
+ merged[tag] = { ...(merged[tag] ?? {}), ...userDark[tag] };
123
+ }
124
+ return merged;
125
+ }, [tagsStyles, isDark, darkModeStyles]);
126
+
127
+ // --- Build context value ---
128
+ const ctx: HtmlRendererContextValue = useMemo(
129
+ () => ({
130
+ contentWidth,
131
+ tagsStyles: effectiveTagsStyles,
132
+ classesStyles: classesStyles ?? {},
133
+ idsStyles: idsStyles ?? {},
134
+ customRenderers: customRenderers ?? {},
135
+ onLinkPress,
136
+ onImagePress,
137
+ renderersProps: renderersProps ?? {},
138
+ emSize,
139
+ debug,
140
+ ignoredTags: ignoredTagsSet,
141
+ ignoredStyles: ignoredStylesSet,
142
+ allowedStyles: allowedStylesSet,
143
+ defaultTextProps,
144
+ defaultViewProps,
145
+ maxImagesWidth,
146
+ imagesInitialDimensions: imagesInitialDimensions ?? {
147
+ width: 100,
148
+ height: 100,
149
+ },
150
+ listsPrefixesRenderers,
151
+ systemFonts,
152
+ fallbackFonts,
153
+ nestLevel: 0,
154
+ colorScheme: resolvedScheme,
155
+ darkModeStyles: darkModeStyles ?? {},
156
+ allowFontScaling,
157
+ maxFontSizeMultiplier,
158
+ }),
159
+ [
160
+ contentWidth,
161
+ effectiveTagsStyles,
162
+ classesStyles,
163
+ idsStyles,
164
+ customRenderers,
165
+ onLinkPress,
166
+ onImagePress,
167
+ renderersProps,
168
+ emSize,
169
+ debug,
170
+ ignoredTagsSet,
171
+ ignoredStylesSet,
172
+ allowedStylesSet,
173
+ defaultTextProps,
174
+ defaultViewProps,
175
+ maxImagesWidth,
176
+ imagesInitialDimensions,
177
+ listsPrefixesRenderers,
178
+ systemFonts,
179
+ fallbackFonts,
180
+ resolvedScheme,
181
+ darkModeStyles,
182
+ allowFontScaling,
183
+ maxFontSizeMultiplier,
184
+ ]
185
+ );
186
+
187
+ // --- Debug log ---
188
+ if (debug) {
189
+ console.log('[HtmlRenderer] Parsed DOM:', JSON.stringify(nodes, null, 2));
190
+ console.log('[HtmlRenderer] Color scheme:', resolvedScheme);
191
+ }
192
+
193
+ // --- Render ---
194
+ const rendered = useMemo(() => {
195
+ try {
196
+ return renderNodes(nodes, ctx, 'rn');
197
+ } catch (e) {
198
+ const error = e instanceof Error ? e : new Error('Failed to render HTML');
199
+ if (debug) {
200
+ console.error('[HtmlRenderer] Render error:', error);
201
+ }
202
+ onError?.(error);
203
+ return [];
204
+ }
205
+ }, [nodes, ctx, debug, onError]);
206
+
207
+ return (
208
+ <ErrorBoundary onError={onError} fallback={fallback}>
209
+ <HtmlRendererContext.Provider value={ctx}>
210
+ <View style={[containerStyle, baseStyle]}>{rendered}</View>
211
+ </HtmlRendererContext.Provider>
212
+ </ErrorBoundary>
213
+ );
214
+ }
215
+
216
+ const containerStyle = { flexShrink: 1 as const };
@@ -0,0 +1,30 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { HtmlRendererContextValue } from '../types';
3
+
4
+ const DEFAULT_CONTEXT: HtmlRendererContextValue = {
5
+ contentWidth: 300,
6
+ tagsStyles: {},
7
+ classesStyles: {},
8
+ idsStyles: {},
9
+ customRenderers: {},
10
+ renderersProps: {},
11
+ emSize: 14,
12
+ debug: false,
13
+ ignoredTags: new Set(),
14
+ ignoredStyles: new Set(),
15
+ allowedStyles: null,
16
+ imagesInitialDimensions: { width: 100, height: 100 },
17
+ nestLevel: 0,
18
+ colorScheme: 'light',
19
+ darkModeStyles: {},
20
+ allowFontScaling: true,
21
+ maxFontSizeMultiplier: undefined,
22
+ };
23
+
24
+ export const HtmlRendererContext =
25
+ createContext<HtmlRendererContextValue>(DEFAULT_CONTEXT);
26
+
27
+ /** Access the HtmlRenderer context from inside the render tree. */
28
+ export function useHtmlRendererContext(): HtmlRendererContextValue {
29
+ return useContext(HtmlRendererContext);
30
+ }
@@ -0,0 +1,3 @@
1
+ export { useHtmlParser } from './useHtmlParser';
2
+ export { useContentWidth } from './useContentWidth';
3
+ export { useTagStyle } from './useTagStyle';
@@ -0,0 +1,9 @@
1
+ import { useHtmlRendererContext } from '../context';
2
+
3
+ /**
4
+ * Returns the current `contentWidth` from the HtmlRenderer context.
5
+ * Useful inside custom renderers that need to know the available layout width.
6
+ */
7
+ export function useContentWidth(): number {
8
+ return useHtmlRendererContext().contentWidth;
9
+ }
@@ -0,0 +1,18 @@
1
+ import { useMemo } from 'react';
2
+ import { parseHTML } from '../parser';
3
+ import type { DOMNode } from '../types';
4
+
5
+ /**
6
+ * Hook that parses an HTML string into a DOM tree.
7
+ * The result is memoized and only recomputed when `html` or `ignoredTags` change.
8
+ *
9
+ * @param html - Raw HTML string.
10
+ * @param ignoredTags - Optional set of tag names to skip.
11
+ * @returns Array of parsed DOMNode objects.
12
+ */
13
+ export function useHtmlParser(
14
+ html: string,
15
+ ignoredTags?: Set<string>
16
+ ): DOMNode[] {
17
+ return useMemo(() => parseHTML(html, ignoredTags), [html, ignoredTags]);
18
+ }
@@ -0,0 +1,23 @@
1
+ import { useMemo } from 'react';
2
+ import { useHtmlRendererContext } from '../context';
3
+ import { getDefaultTagStyles } from '../styles';
4
+ import type { RNStyle } from '../types';
5
+
6
+ /**
7
+ * Returns the fully merged style for a given HTML tag, combining:
8
+ * default tag styles → user `tagsStyles` override.
9
+ *
10
+ * Useful inside custom renderers that want access to the computed base style.
11
+ *
12
+ * @param tag - The HTML tag name (e.g. `'p'`, `'h1'`).
13
+ */
14
+ export function useTagStyle(tag: string): RNStyle {
15
+ const ctx = useHtmlRendererContext();
16
+
17
+ return useMemo(() => {
18
+ const defaults = getDefaultTagStyles(ctx.emSize);
19
+ const base = defaults[tag] ?? {};
20
+ const override = ctx.tagsStyles[tag] ?? {};
21
+ return { ...base, ...override } as RNStyle;
22
+ }, [tag, ctx.emSize, ctx.tagsStyles]);
23
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,39 @@
1
+ // Main component
2
+ export { HtmlRenderer } from './HtmlRenderer';
3
+
4
+ // Parser
5
+ export { parseHTML } from './parser';
6
+
7
+ // Styles
8
+ export {
9
+ parseInlineStyle,
10
+ getDefaultTagStyles,
11
+ getDefaultDarkModeStyles,
12
+ } from './styles';
13
+
14
+ // Hooks
15
+ export { useHtmlParser, useContentWidth, useTagStyle } from './hooks';
16
+
17
+ // Context (for advanced use inside custom renderers)
18
+ export { useHtmlRendererContext } from './context';
19
+
20
+ // Utilities
21
+ export { sanitizeDOM } from './utils/sanitize';
22
+ export { clearImageDimensionCache, clearDOMCache } from './utils/cache';
23
+
24
+ // Types
25
+ export type {
26
+ HtmlRendererProps,
27
+ DOMNode,
28
+ DOMElement,
29
+ DOMText,
30
+ RNStyle,
31
+ CustomRenderer,
32
+ CustomRendererProps,
33
+ TagsStyles,
34
+ ClassesStyles,
35
+ IdsStyles,
36
+ HtmlRendererContextValue,
37
+ ListPrefixRendererProps,
38
+ ListsPrefixesRenderers,
39
+ } from './types';
@@ -0,0 +1,80 @@
1
+ import { parseDocument } from 'htmlparser2';
2
+ import type { DOMNode, DOMElement, DOMText } from '../types';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Internal htmlparser2 node shape (loosely typed to avoid coupling)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ interface HP2Node {
9
+ type: string;
10
+ name?: string;
11
+ data?: string;
12
+ attribs?: Record<string, string>;
13
+ children?: HP2Node[];
14
+ }
15
+
16
+ // Tags whose content should never appear in the rendered output.
17
+ const IGNORED_TAGS = new Set(['script', 'style', 'head', 'meta', 'link']);
18
+
19
+ /** Max depth to prevent stack overflow from deeply nested HTML. */
20
+ const MAX_DEPTH = 100;
21
+
22
+ function convertNode(
23
+ node: HP2Node,
24
+ ignoredTags: Set<string>,
25
+ depth: number
26
+ ): DOMNode | null {
27
+ if (depth > MAX_DEPTH) return null;
28
+
29
+ if (node.type === 'text') {
30
+ const data = node.data ?? '';
31
+ if (data.trim().length === 0) return null;
32
+ return { type: 'text', data } satisfies DOMText;
33
+ }
34
+
35
+ if (node.type === 'tag' || node.type === 'script' || node.type === 'style') {
36
+ const tag = (node.name ?? '').toLowerCase();
37
+
38
+ // Skip built-in ignored tags AND user-supplied ignored tags
39
+ if (IGNORED_TAGS.has(tag) || ignoredTags.has(tag)) {
40
+ return null;
41
+ }
42
+
43
+ const children: DOMNode[] = [];
44
+ for (const child of node.children ?? []) {
45
+ const converted = convertNode(child, ignoredTags, depth + 1);
46
+ if (converted) children.push(converted);
47
+ }
48
+
49
+ return {
50
+ type: 'element',
51
+ tag,
52
+ attributes: node.attribs ?? {},
53
+ children,
54
+ } satisfies DOMElement;
55
+ }
56
+
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Parse an HTML string into an array of `DOMNode` objects.
62
+ *
63
+ * @param html - Raw HTML string.
64
+ * @param ignoredTags - Optional set of tag names to skip entirely.
65
+ * @returns Array of parsed DOM nodes.
66
+ */
67
+ export function parseHTML(
68
+ html: string,
69
+ ignoredTags: Set<string> = new Set()
70
+ ): DOMNode[] {
71
+ const doc = parseDocument(html);
72
+ const nodes: DOMNode[] = [];
73
+
74
+ for (const child of (doc.children ?? []) as HP2Node[]) {
75
+ const converted = convertNode(child, ignoredTags, 0);
76
+ if (converted) nodes.push(converted);
77
+ }
78
+
79
+ return nodes;
80
+ }
@@ -0,0 +1,80 @@
1
+ import { Component, type ReactNode, type ErrorInfo } from 'react';
2
+ import { View, Text, type ViewStyle, type TextStyle } from 'react-native';
3
+
4
+ interface ErrorBoundaryProps {
5
+ onError?: (error: Error) => void;
6
+ fallback?: ReactNode;
7
+ children: ReactNode;
8
+ }
9
+
10
+ interface ErrorBoundaryState {
11
+ hasError: boolean;
12
+ error: Error | null;
13
+ }
14
+
15
+ /**
16
+ * Catches rendering errors in the HTML renderer tree and
17
+ * displays a fallback UI instead of crashing the host app.
18
+ */
19
+ export class ErrorBoundary extends Component<
20
+ ErrorBoundaryProps,
21
+ ErrorBoundaryState
22
+ > {
23
+ constructor(props: ErrorBoundaryProps) {
24
+ super(props);
25
+ this.state = { hasError: false, error: null };
26
+ }
27
+
28
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
29
+ return { hasError: true, error };
30
+ }
31
+
32
+ override componentDidCatch(error: Error, info: ErrorInfo): void {
33
+ if (__DEV__) {
34
+ console.error(
35
+ '[HtmlRenderer] Render error caught by ErrorBoundary:',
36
+ error,
37
+ info.componentStack
38
+ );
39
+ }
40
+ this.props.onError?.(error);
41
+ }
42
+
43
+ override render(): ReactNode {
44
+ if (this.state.hasError) {
45
+ if (this.props.fallback !== undefined) {
46
+ return this.props.fallback;
47
+ }
48
+
49
+ return (
50
+ <View style={fallbackContainer}>
51
+ <Text style={fallbackTitle}>Unable to render HTML content</Text>
52
+ {__DEV__ && this.state.error && (
53
+ <Text style={fallbackDetail}>{this.state.error.message}</Text>
54
+ )}
55
+ </View>
56
+ );
57
+ }
58
+ return this.props.children;
59
+ }
60
+ }
61
+
62
+ const fallbackContainer: ViewStyle = {
63
+ padding: 16,
64
+ backgroundColor: '#fef2f2',
65
+ borderRadius: 8,
66
+ borderWidth: 1,
67
+ borderColor: '#fecaca',
68
+ };
69
+
70
+ const fallbackTitle: TextStyle = {
71
+ color: '#991b1b',
72
+ fontSize: 14,
73
+ fontWeight: '600',
74
+ };
75
+
76
+ const fallbackDetail: TextStyle = {
77
+ color: '#b91c1c',
78
+ fontSize: 12,
79
+ marginTop: 4,
80
+ };