@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,345 @@
1
+ import { Fragment, type ReactNode } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ } from 'react-native';
9
+ import type {
10
+ DOMNode,
11
+ DOMElement,
12
+ HtmlRendererContextValue,
13
+ RNStyle,
14
+ } from '../types';
15
+ import { getDefaultTagStyles, mergeStylesForElement } from '../styles';
16
+ import { INLINE_TAGS, TEXT_BLOCK_TAGS, isInlineContent } from '../utils';
17
+ import { getAccessibilityProps } from '../utils/accessibility';
18
+ import {
19
+ TextBlock,
20
+ renderInlineNodes,
21
+ ImageTag,
22
+ LinkTag,
23
+ ListTag,
24
+ TableTag,
25
+ BlockTag,
26
+ InputTag,
27
+ TextareaTag,
28
+ ButtonTag,
29
+ SelectTag,
30
+ VideoTag,
31
+ AudioTag,
32
+ } from './tags';
33
+
34
+ /**
35
+ * Recursively render an array of DOMNodes into React Native components.
36
+ */
37
+ export function renderNodes(
38
+ nodes: DOMNode[],
39
+ ctx: HtmlRendererContextValue,
40
+ keyPrefix: string
41
+ ): ReactNode[] {
42
+ const defaults = getDefaultTagStyles(ctx.emSize);
43
+
44
+ return nodes
45
+ .map((node, index) => {
46
+ const key = `${keyPrefix}_${index}`;
47
+
48
+ // --- Text node ---
49
+ if (node.type === 'text') {
50
+ return (
51
+ <Text
52
+ key={key}
53
+ allowFontScaling={ctx.allowFontScaling}
54
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
55
+ {...ctx.defaultTextProps}
56
+ >
57
+ {node.data}
58
+ </Text>
59
+ );
60
+ }
61
+
62
+ if (node.type !== 'element') return null;
63
+
64
+ // --- Ignored tags ---
65
+ if (ctx.ignoredTags.has(node.tag)) return null;
66
+
67
+ // --- Compute merged style ---
68
+ const style = mergeStylesForElement(
69
+ node,
70
+ defaults,
71
+ ctx.tagsStyles,
72
+ ctx.classesStyles,
73
+ ctx.idsStyles,
74
+ ctx.ignoredStyles.size > 0 ? ctx.ignoredStyles : undefined,
75
+ ctx.allowedStyles
76
+ );
77
+
78
+ // --- Debug logging ---
79
+ if (ctx.debug) {
80
+ console.log(`[HtmlRenderer] <${node.tag}>`, {
81
+ attributes: node.attributes,
82
+ style,
83
+ });
84
+ }
85
+
86
+ // --- Custom renderer ---
87
+ const customRenderer = ctx.customRenderers[node.tag];
88
+ if (customRenderer) {
89
+ try {
90
+ const passProps = ctx.renderersProps[node.tag] ?? {};
91
+ const result = customRenderer({
92
+ node,
93
+ children: renderNodes(node.children, ctx, key),
94
+ style,
95
+ attributes: node.attributes,
96
+ passProps,
97
+ renderChildren: (childNodes) => renderNodes(childNodes, ctx, key),
98
+ contentWidth: ctx.contentWidth,
99
+ });
100
+ return <Fragment key={key}>{result}</Fragment>;
101
+ } catch (e) {
102
+ if (ctx.debug) {
103
+ console.error(
104
+ `[HtmlRenderer] Custom renderer for <${node.tag}> threw:`,
105
+ e
106
+ );
107
+ }
108
+ return null;
109
+ }
110
+ }
111
+
112
+ // --- Built-in tag handling ---
113
+ try {
114
+ return renderElement(node, style, key, ctx);
115
+ } catch (e) {
116
+ if (ctx.debug) {
117
+ console.error(`[HtmlRenderer] Error rendering <${node.tag}>:`, e);
118
+ }
119
+ return null;
120
+ }
121
+ })
122
+ .filter((n) => n != null);
123
+ }
124
+
125
+ function renderElement(
126
+ node: DOMElement,
127
+ style: RNStyle,
128
+ key: string,
129
+ ctx: HtmlRendererContextValue
130
+ ): ReactNode {
131
+ const { tag } = node;
132
+ const a11y = getAccessibilityProps(node);
133
+
134
+ // Debug: red border around every node
135
+ const debugStyle: ViewStyle | undefined = ctx.debug
136
+ ? { borderWidth: 1, borderColor: 'red' }
137
+ : undefined;
138
+
139
+ // Self-closing tags
140
+ if (tag === 'br') {
141
+ return (
142
+ <Text
143
+ key={key}
144
+ allowFontScaling={ctx.allowFontScaling}
145
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
146
+ {...ctx.defaultTextProps}
147
+ >
148
+ {'\n'}
149
+ </Text>
150
+ );
151
+ }
152
+
153
+ if (tag === 'hr') {
154
+ return (
155
+ <View
156
+ key={key}
157
+ style={[hrBaseStyle, style as ViewStyle, debugStyle]}
158
+ {...a11y}
159
+ />
160
+ );
161
+ }
162
+
163
+ // Image
164
+ if (tag === 'img') {
165
+ return (
166
+ <ImageTag key={key} node={node} style={style} nodeKey={key} ctx={ctx} />
167
+ );
168
+ }
169
+
170
+ // Link (block-level — inline links handled in renderInlineNodes)
171
+ if (tag === 'a') {
172
+ return (
173
+ <LinkTag
174
+ key={key}
175
+ node={node}
176
+ style={style}
177
+ nodeKey={key}
178
+ ctx={ctx}
179
+ renderNodes={renderNodes}
180
+ />
181
+ );
182
+ }
183
+
184
+ // Lists
185
+ if (tag === 'ul' || tag === 'ol') {
186
+ return (
187
+ <ListTag
188
+ key={key}
189
+ node={node}
190
+ style={style}
191
+ nodeKey={key}
192
+ ctx={ctx}
193
+ renderNodes={renderNodes}
194
+ />
195
+ );
196
+ }
197
+
198
+ // Table
199
+ if (tag === 'table') {
200
+ return (
201
+ <TableTag
202
+ key={key}
203
+ node={node}
204
+ style={style}
205
+ nodeKey={key}
206
+ ctx={ctx}
207
+ renderNodes={renderNodes}
208
+ />
209
+ );
210
+ }
211
+
212
+ // Table row
213
+ if (tag === 'tr') {
214
+ return (
215
+ <View
216
+ key={key}
217
+ style={[style as ViewStyle, { flexDirection: 'row' }]}
218
+ {...a11y}
219
+ >
220
+ {renderNodes(node.children, ctx, key)}
221
+ </View>
222
+ );
223
+ }
224
+
225
+ // Form elements (read-only)
226
+ if (tag === 'input') {
227
+ return (
228
+ <InputTag key={key} node={node} style={style} nodeKey={key} ctx={ctx} />
229
+ );
230
+ }
231
+ if (tag === 'textarea') {
232
+ return (
233
+ <TextareaTag
234
+ key={key}
235
+ node={node}
236
+ style={style}
237
+ nodeKey={key}
238
+ ctx={ctx}
239
+ />
240
+ );
241
+ }
242
+ if (tag === 'button') {
243
+ return (
244
+ <ButtonTag key={key} node={node} style={style} nodeKey={key} ctx={ctx} />
245
+ );
246
+ }
247
+ if (tag === 'select') {
248
+ return (
249
+ <SelectTag key={key} node={node} style={style} nodeKey={key} ctx={ctx} />
250
+ );
251
+ }
252
+
253
+ // Media placeholders
254
+ if (tag === 'video') {
255
+ return (
256
+ <VideoTag key={key} node={node} style={style} nodeKey={key} ctx={ctx} />
257
+ );
258
+ }
259
+ if (tag === 'audio') {
260
+ return (
261
+ <AudioTag key={key} node={node} style={style} nodeKey={key} ctx={ctx} />
262
+ );
263
+ }
264
+
265
+ // Inline tags at top level — wrap in Text
266
+ if (INLINE_TAGS.has(tag)) {
267
+ return (
268
+ <Text
269
+ key={key}
270
+ style={
271
+ debugStyle ? [style as TextStyle, debugStyle] : (style as TextStyle)
272
+ }
273
+ allowFontScaling={ctx.allowFontScaling}
274
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
275
+ {...a11y}
276
+ {...ctx.defaultTextProps}
277
+ >
278
+ {renderInlineNodes(node.children, ctx, key, renderNodes)}
279
+ </Text>
280
+ );
281
+ }
282
+
283
+ // Text-block tags (p, h1-h6, li, td, th, pre, label)
284
+ if (TEXT_BLOCK_TAGS.has(tag)) {
285
+ return (
286
+ <TextBlock
287
+ key={key}
288
+ tag={tag}
289
+ node={node}
290
+ style={style}
291
+ children={node.children}
292
+ nodeKey={key}
293
+ ctx={ctx}
294
+ renderNodes={renderNodes}
295
+ />
296
+ );
297
+ }
298
+
299
+ // Unknown tag — log warning in debug mode
300
+ if (ctx.debug) {
301
+ console.warn(`[HtmlRenderer] Unknown tag <${tag}>, rendering children.`);
302
+ }
303
+
304
+ // Unknown / generic block elements — render children, never crash
305
+ if (node.children.length === 0) {
306
+ return null;
307
+ }
308
+
309
+ // If all children are inline, render as text
310
+ if (isInlineContent(node.children)) {
311
+ return (
312
+ <Text
313
+ key={key}
314
+ style={
315
+ debugStyle ? [style as TextStyle, debugStyle] : (style as TextStyle)
316
+ }
317
+ allowFontScaling={ctx.allowFontScaling}
318
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
319
+ {...a11y}
320
+ {...ctx.defaultTextProps}
321
+ >
322
+ {renderInlineNodes(node.children, ctx, key, renderNodes)}
323
+ </Text>
324
+ );
325
+ }
326
+
327
+ // Otherwise render as a block container
328
+ return (
329
+ <BlockTag
330
+ key={key}
331
+ node={node}
332
+ style={debugStyle ? ({ ...style, ...debugStyle } as RNStyle) : style}
333
+ children={node.children}
334
+ nodeKey={key}
335
+ ctx={ctx}
336
+ renderNodes={renderNodes}
337
+ />
338
+ );
339
+ }
340
+
341
+ const hrBaseStyle: ViewStyle = {
342
+ width: '100%',
343
+ height: StyleSheet.hairlineWidth,
344
+ backgroundColor: '#ccc',
345
+ };
@@ -0,0 +1,2 @@
1
+ export { renderNodes } from './NodeRenderer';
2
+ export { ErrorBoundary } from './ErrorBoundary';
@@ -0,0 +1,49 @@
1
+ import { memo, type ReactNode } from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+ import type {
4
+ DOMNode,
5
+ DOMElement,
6
+ RNStyle,
7
+ HtmlRendererContextValue,
8
+ } from '../../types';
9
+ import { getAccessibilityProps } from '../../utils/accessibility';
10
+
11
+ interface BlockTagProps {
12
+ node: DOMElement;
13
+ style: RNStyle;
14
+ children: DOMNode[];
15
+ nodeKey: string;
16
+ ctx: HtmlRendererContextValue;
17
+ renderNodes: (
18
+ nodes: DOMNode[],
19
+ ctx: HtmlRendererContextValue,
20
+ keyPrefix: string
21
+ ) => ReactNode[];
22
+ }
23
+
24
+ /**
25
+ * Generic block-level tag renderer.
26
+ * Used for `<div>`, `<section>`, `<article>`, `<header>`, `<footer>`,
27
+ * `<main>`, `<nav>`, `<aside>`, `<blockquote>`, `<figure>`, etc.
28
+ */
29
+ export const BlockTag = memo(function BlockTag({
30
+ node,
31
+ style,
32
+ children,
33
+ nodeKey,
34
+ ctx,
35
+ renderNodes,
36
+ }: BlockTagProps) {
37
+ const a11y = getAccessibilityProps(node);
38
+
39
+ return (
40
+ <View
41
+ key={nodeKey}
42
+ style={style as ViewStyle}
43
+ {...a11y}
44
+ {...ctx.defaultViewProps}
45
+ >
46
+ {renderNodes(children, ctx, nodeKey)}
47
+ </View>
48
+ );
49
+ });
@@ -0,0 +1,169 @@
1
+ import { memo } from 'react';
2
+ import { View, Text, type ViewStyle, type TextStyle } from 'react-native';
3
+ import type {
4
+ DOMElement,
5
+ RNStyle,
6
+ HtmlRendererContextValue,
7
+ } from '../../types';
8
+ import { extractTextContent } from '../../utils';
9
+ import { getAccessibilityProps } from '../../utils/accessibility';
10
+
11
+ interface FormTagProps {
12
+ node: DOMElement;
13
+ style: RNStyle;
14
+ nodeKey: string;
15
+ ctx: HtmlRendererContextValue;
16
+ }
17
+
18
+ /**
19
+ * Read-only renderers for form elements.
20
+ */
21
+ export const InputTag = memo(function InputTag({
22
+ node,
23
+ style,
24
+ nodeKey,
25
+ ctx,
26
+ }: FormTagProps) {
27
+ const type = node.attributes.type ?? 'text';
28
+ const value = node.attributes.value ?? node.attributes.placeholder ?? '';
29
+ const a11y = getAccessibilityProps(node);
30
+
31
+ if (type === 'hidden') return null;
32
+
33
+ if (type === 'checkbox' || type === 'radio') {
34
+ const checked = 'checked' in node.attributes;
35
+ return (
36
+ <Text
37
+ key={nodeKey}
38
+ style={style as TextStyle}
39
+ accessibilityRole={type === 'checkbox' ? 'checkbox' : 'radio'}
40
+ accessibilityState={{ checked }}
41
+ allowFontScaling={ctx.allowFontScaling}
42
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
43
+ {...ctx.defaultTextProps}
44
+ >
45
+ {type === 'checkbox'
46
+ ? checked
47
+ ? '\u2611 '
48
+ : '\u2610 '
49
+ : checked
50
+ ? '\u25C9 '
51
+ : '\u25CB '}
52
+ </Text>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <View key={nodeKey} style={style as ViewStyle} {...a11y}>
58
+ <Text
59
+ style={inputText}
60
+ allowFontScaling={ctx.allowFontScaling}
61
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
62
+ {...ctx.defaultTextProps}
63
+ >
64
+ {value}
65
+ </Text>
66
+ </View>
67
+ );
68
+ });
69
+
70
+ export const TextareaTag = memo(function TextareaTag({
71
+ node,
72
+ style,
73
+ nodeKey,
74
+ ctx,
75
+ }: FormTagProps) {
76
+ const text =
77
+ extractTextContent(node.children) || node.attributes.placeholder || '';
78
+ const a11y = getAccessibilityProps(node);
79
+
80
+ return (
81
+ <View key={nodeKey} style={style as ViewStyle} {...a11y}>
82
+ <Text
83
+ style={inputText}
84
+ allowFontScaling={ctx.allowFontScaling}
85
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
86
+ {...ctx.defaultTextProps}
87
+ >
88
+ {text}
89
+ </Text>
90
+ </View>
91
+ );
92
+ });
93
+
94
+ export const ButtonTag = memo(function ButtonTag({
95
+ node,
96
+ style,
97
+ nodeKey,
98
+ ctx,
99
+ }: FormTagProps) {
100
+ const label =
101
+ extractTextContent(node.children) || node.attributes.value || 'Button';
102
+
103
+ return (
104
+ <View
105
+ key={nodeKey}
106
+ style={style as ViewStyle}
107
+ accessibilityRole="button"
108
+ accessibilityLabel={node.attributes['aria-label'] ?? label}
109
+ >
110
+ <Text
111
+ style={buttonText}
112
+ allowFontScaling={ctx.allowFontScaling}
113
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
114
+ {...ctx.defaultTextProps}
115
+ >
116
+ {label}
117
+ </Text>
118
+ </View>
119
+ );
120
+ });
121
+
122
+ export const SelectTag = memo(function SelectTag({
123
+ node,
124
+ style,
125
+ nodeKey,
126
+ ctx,
127
+ }: FormTagProps) {
128
+ const options = node.children.filter(
129
+ (c) => c.type === 'element' && c.tag === 'option'
130
+ );
131
+ let label = '';
132
+ for (const opt of options) {
133
+ if (opt.type === 'element') {
134
+ if ('selected' in opt.attributes) {
135
+ label = extractTextContent(opt.children);
136
+ break;
137
+ }
138
+ if (!label) {
139
+ label = extractTextContent(opt.children);
140
+ }
141
+ }
142
+ }
143
+ const a11y = getAccessibilityProps(node);
144
+
145
+ return (
146
+ <View key={nodeKey} style={style as ViewStyle} {...a11y}>
147
+ <Text
148
+ style={inputText}
149
+ allowFontScaling={ctx.allowFontScaling}
150
+ maxFontSizeMultiplier={ctx.maxFontSizeMultiplier}
151
+ {...ctx.defaultTextProps}
152
+ >
153
+ {label || '\u2014'}
154
+ </Text>
155
+ </View>
156
+ );
157
+ });
158
+
159
+ const inputText: TextStyle = {
160
+ fontSize: 14,
161
+ color: '#333',
162
+ };
163
+
164
+ const buttonText: TextStyle = {
165
+ fontSize: 14,
166
+ color: '#fff',
167
+ fontWeight: '600',
168
+ textAlign: 'center',
169
+ };