@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.
- package/LICENSE +20 -0
- package/README.md +407 -0
- package/lib/module/HtmlRenderer.js +183 -0
- package/lib/module/HtmlRenderer.js.map +1 -0
- package/lib/module/context/index.js +32 -0
- package/lib/module/context/index.js.map +1 -0
- package/lib/module/hooks/index.js +6 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useContentWidth.js +12 -0
- package/lib/module/hooks/useContentWidth.js.map +1 -0
- package/lib/module/hooks/useHtmlParser.js +16 -0
- package/lib/module/hooks/useHtmlParser.js.map +1 -0
- package/lib/module/hooks/useTagStyle.js +26 -0
- package/lib/module/hooks/useTagStyle.js.map +1 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/parser/index.js +62 -0
- package/lib/module/parser/index.js.map +1 -0
- package/lib/module/renderer/ErrorBoundary.js +66 -0
- package/lib/module/renderer/ErrorBoundary.js.map +1 -0
- package/lib/module/renderer/NodeRenderer.js +279 -0
- package/lib/module/renderer/NodeRenderer.js.map +1 -0
- package/lib/module/renderer/index.js +5 -0
- package/lib/module/renderer/index.js.map +1 -0
- package/lib/module/renderer/tags/BlockTags.js +28 -0
- package/lib/module/renderer/tags/BlockTags.js.map +1 -0
- package/lib/module/renderer/tags/FormTags.js +129 -0
- package/lib/module/renderer/tags/FormTags.js.map +1 -0
- package/lib/module/renderer/tags/ImageTag.js +163 -0
- package/lib/module/renderer/tags/ImageTag.js.map +1 -0
- package/lib/module/renderer/tags/LinkTag.js +50 -0
- package/lib/module/renderer/tags/LinkTag.js.map +1 -0
- package/lib/module/renderer/tags/ListTags.js +96 -0
- package/lib/module/renderer/tags/ListTags.js.map +1 -0
- package/lib/module/renderer/tags/MediaTags.js +69 -0
- package/lib/module/renderer/tags/MediaTags.js.map +1 -0
- package/lib/module/renderer/tags/TableTags.js +48 -0
- package/lib/module/renderer/tags/TableTags.js.map +1 -0
- package/lib/module/renderer/tags/TextTags.js +87 -0
- package/lib/module/renderer/tags/TextTags.js.map +1 -0
- package/lib/module/renderer/tags/index.js +11 -0
- package/lib/module/renderer/tags/index.js.map +1 -0
- package/lib/module/styles/cssToRn.js +34 -0
- package/lib/module/styles/cssToRn.js.map +1 -0
- package/lib/module/styles/darkModeStyles.js +81 -0
- package/lib/module/styles/darkModeStyles.js.map +1 -0
- package/lib/module/styles/defaultStyles.js +218 -0
- package/lib/module/styles/defaultStyles.js.map +1 -0
- package/lib/module/styles/index.js +7 -0
- package/lib/module/styles/index.js.map +1 -0
- package/lib/module/styles/mergeStyles.js +47 -0
- package/lib/module/styles/mergeStyles.js.map +1 -0
- package/lib/module/types/index.js +4 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/accessibility.js +108 -0
- package/lib/module/utils/accessibility.js.map +1 -0
- package/lib/module/utils/cache.js +69 -0
- package/lib/module/utils/cache.js.map +1 -0
- package/lib/module/utils/index.js +95 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/sanitize.js +102 -0
- package/lib/module/utils/sanitize.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/HtmlRenderer.d.ts +15 -0
- package/lib/typescript/src/HtmlRenderer.d.ts.map +1 -0
- package/lib/typescript/src/context/index.d.ts +5 -0
- package/lib/typescript/src/context/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +4 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useContentWidth.d.ts +6 -0
- package/lib/typescript/src/hooks/useContentWidth.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useHtmlParser.d.ts +11 -0
- package/lib/typescript/src/hooks/useHtmlParser.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useTagStyle.d.ts +11 -0
- package/lib/typescript/src/hooks/useTagStyle.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/parser/index.d.ts +10 -0
- package/lib/typescript/src/parser/index.d.ts.map +1 -0
- package/lib/typescript/src/renderer/ErrorBoundary.d.ts +22 -0
- package/lib/typescript/src/renderer/ErrorBoundary.d.ts.map +1 -0
- package/lib/typescript/src/renderer/NodeRenderer.d.ts +7 -0
- package/lib/typescript/src/renderer/NodeRenderer.d.ts.map +1 -0
- package/lib/typescript/src/renderer/index.d.ts +3 -0
- package/lib/typescript/src/renderer/index.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/BlockTags.d.ts +18 -0
- package/lib/typescript/src/renderer/tags/BlockTags.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/FormTags.d.ts +16 -0
- package/lib/typescript/src/renderer/tags/FormTags.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/ImageTag.d.ts +18 -0
- package/lib/typescript/src/renderer/tags/ImageTag.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/LinkTag.d.ts +19 -0
- package/lib/typescript/src/renderer/tags/LinkTag.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/ListTags.d.ts +15 -0
- package/lib/typescript/src/renderer/tags/ListTags.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/MediaTags.d.ts +14 -0
- package/lib/typescript/src/renderer/tags/MediaTags.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/TableTags.d.ts +15 -0
- package/lib/typescript/src/renderer/tags/TableTags.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/TextTags.d.ts +22 -0
- package/lib/typescript/src/renderer/tags/TextTags.d.ts.map +1 -0
- package/lib/typescript/src/renderer/tags/index.d.ts +9 -0
- package/lib/typescript/src/renderer/tags/index.d.ts.map +1 -0
- package/lib/typescript/src/styles/cssToRn.d.ts +11 -0
- package/lib/typescript/src/styles/cssToRn.d.ts.map +1 -0
- package/lib/typescript/src/styles/darkModeStyles.d.ts +7 -0
- package/lib/typescript/src/styles/darkModeStyles.d.ts.map +1 -0
- package/lib/typescript/src/styles/defaultStyles.d.ts +8 -0
- package/lib/typescript/src/styles/defaultStyles.d.ts.map +1 -0
- package/lib/typescript/src/styles/index.d.ts +5 -0
- package/lib/typescript/src/styles/index.d.ts.map +1 -0
- package/lib/typescript/src/styles/mergeStyles.d.ts +10 -0
- package/lib/typescript/src/styles/mergeStyles.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +158 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/accessibility.d.ts +32 -0
- package/lib/typescript/src/utils/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/utils/cache.d.ts +24 -0
- package/lib/typescript/src/utils/cache.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +33 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/sanitize.d.ts +11 -0
- package/lib/typescript/src/utils/sanitize.d.ts.map +1 -0
- package/package.json +171 -0
- package/src/HtmlRenderer.tsx +216 -0
- package/src/context/index.tsx +30 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useContentWidth.ts +9 -0
- package/src/hooks/useHtmlParser.ts +18 -0
- package/src/hooks/useTagStyle.ts +23 -0
- package/src/index.tsx +39 -0
- package/src/parser/index.ts +80 -0
- package/src/renderer/ErrorBoundary.tsx +80 -0
- package/src/renderer/NodeRenderer.tsx +345 -0
- package/src/renderer/index.tsx +2 -0
- package/src/renderer/tags/BlockTags.tsx +49 -0
- package/src/renderer/tags/FormTags.tsx +169 -0
- package/src/renderer/tags/ImageTag.tsx +215 -0
- package/src/renderer/tags/LinkTag.tsx +76 -0
- package/src/renderer/tags/ListTags.tsx +148 -0
- package/src/renderer/tags/MediaTags.tsx +81 -0
- package/src/renderer/tags/TableTags.tsx +94 -0
- package/src/renderer/tags/TextTags.tsx +139 -0
- package/src/renderer/tags/index.ts +8 -0
- package/src/styles/cssToRn.ts +45 -0
- package/src/styles/darkModeStyles.ts +80 -0
- package/src/styles/defaultStyles.ts +176 -0
- package/src/styles/index.ts +4 -0
- package/src/styles/mergeStyles.ts +59 -0
- package/src/types/index.ts +229 -0
- package/src/utils/accessibility.ts +132 -0
- package/src/utils/cache.ts +83 -0
- package/src/utils/index.ts +151 -0
- 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,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
|
+
};
|