@nanogiants/react-native-render-html 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +263 -0
- package/dist/index.d.mts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +1093 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1068 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/src/HTMLValidator.spec.ts +157 -0
- package/src/HTMLValidator.ts +35 -0
- package/src/RenderHTML.spec.tsx +435 -0
- package/src/RenderHTML.tsx +47 -0
- package/src/__snapshots__/RenderHTML.spec.tsx.snap +2795 -0
- package/src/assets.ts +5 -0
- package/src/context/AlignedWidthItem.tsx +25 -0
- package/src/context/AlignedWidthProvider.tsx +52 -0
- package/src/context/HtmlProvider.tsx +564 -0
- package/src/index.ts +4 -0
- package/src/renderers/ATagRenderer.tsx +60 -0
- package/src/renderers/ImgTagRenderer.tsx +64 -0
- package/src/renderers/LiTagRenderer.tsx +73 -0
- package/src/renderers/PTagRenderer.tsx +31 -0
- package/src/renderers/_DefaultBlockRenderer.tsx +82 -0
- package/src/renderers/_DefaultTextRenderer.tsx +26 -0
- package/src/renderers/_NodeRenderer.tsx +119 -0
- package/src/renderers/_NodesRenderer.tsx +12 -0
- package/src/types.ts +162 -0
- package/src/utils.ts +43 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot tests for the RenderHTML renderer.
|
|
3
|
+
*
|
|
4
|
+
* Each test renders a specific HTML fragment via React Native Testing Library
|
|
5
|
+
* and records the component tree as a snapshot. When you refactor the renderers
|
|
6
|
+
* the snapshots will tell you exactly what changed, making regressions
|
|
7
|
+
* immediately visible.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - One focused HTML snippet per test (easy to understand diffs).
|
|
11
|
+
* - `renderImage` is a simple stub that renders nothing — we test structure,
|
|
12
|
+
* not image library integration.
|
|
13
|
+
* - `renderAsync` is used (React 19 ready) so all async state updates from
|
|
14
|
+
* Image.getSize are properly flushed inside act().
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { renderAsync, screen } from '@testing-library/react-native';
|
|
18
|
+
|
|
19
|
+
import { RenderHTML } from './RenderHTML';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const renderImage = () => null;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Snapshots
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
describe('RenderHTML snapshots', () => {
|
|
32
|
+
// ── empty / trivial ───────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
it('renders empty string without crashing', async () => {
|
|
35
|
+
await renderAsync(<RenderHTML html="" renderImage={renderImage} />);
|
|
36
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders a plain text node', async () => {
|
|
40
|
+
await renderAsync(<RenderHTML html="Hello world" renderImage={renderImage} />);
|
|
41
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── headings ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
it('renders h1', async () => {
|
|
47
|
+
await renderAsync(<RenderHTML html="<h1>Heading 1</h1>" renderImage={renderImage} />);
|
|
48
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders h2', async () => {
|
|
52
|
+
await renderAsync(<RenderHTML html="<h2>Heading 2</h2>" renderImage={renderImage} />);
|
|
53
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders h3', async () => {
|
|
57
|
+
await renderAsync(<RenderHTML html="<h3>Heading 3</h3>" renderImage={renderImage} />);
|
|
58
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders h4', async () => {
|
|
62
|
+
await renderAsync(<RenderHTML html="<h4>Heading 4</h4>" renderImage={renderImage} />);
|
|
63
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders h5', async () => {
|
|
67
|
+
await renderAsync(<RenderHTML html="<h5>Heading 5</h5>" renderImage={renderImage} />);
|
|
68
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('renders h6', async () => {
|
|
72
|
+
await renderAsync(<RenderHTML html="<h6>Heading 6</h6>" renderImage={renderImage} />);
|
|
73
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── paragraph & inline formatting ────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
it('renders a paragraph', async () => {
|
|
79
|
+
await renderAsync(<RenderHTML html="<p>A simple paragraph.</p>" renderImage={renderImage} />);
|
|
80
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('renders bold text', async () => {
|
|
84
|
+
await renderAsync(
|
|
85
|
+
<RenderHTML
|
|
86
|
+
html="<p><b>Bold</b> and <strong>strong</strong></p>"
|
|
87
|
+
renderImage={renderImage}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders italic text', async () => {
|
|
94
|
+
await renderAsync(
|
|
95
|
+
<RenderHTML html="<p><i>Italic</i> and <em>emphasis</em></p>" renderImage={renderImage} />,
|
|
96
|
+
);
|
|
97
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders underline text', async () => {
|
|
101
|
+
await renderAsync(<RenderHTML html="<p><u>Underlined</u></p>" renderImage={renderImage} />);
|
|
102
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('renders strikethrough text', async () => {
|
|
106
|
+
await renderAsync(
|
|
107
|
+
<RenderHTML html="<p><s>Strike</s> and <del>deleted</del></p>" renderImage={renderImage} />,
|
|
108
|
+
);
|
|
109
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('renders mark (highlight)', async () => {
|
|
113
|
+
await renderAsync(
|
|
114
|
+
<RenderHTML html="<p><mark>Highlighted</mark></p>" renderImage={renderImage} />,
|
|
115
|
+
);
|
|
116
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders small text', async () => {
|
|
120
|
+
await renderAsync(
|
|
121
|
+
<RenderHTML html="<p><small>Tiny text</small></p>" renderImage={renderImage} />,
|
|
122
|
+
);
|
|
123
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders superscript and subscript', async () => {
|
|
127
|
+
await renderAsync(
|
|
128
|
+
<RenderHTML html="<p>H<sub>2</sub>O and E=mc<sup>2</sup></p>" renderImage={renderImage} />,
|
|
129
|
+
);
|
|
130
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('renders span', async () => {
|
|
134
|
+
await renderAsync(
|
|
135
|
+
<RenderHTML html="<p>Normal <span>span</span> text</p>" renderImage={renderImage} />,
|
|
136
|
+
);
|
|
137
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── links ─────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
it('renders an internal link', async () => {
|
|
143
|
+
await renderAsync(
|
|
144
|
+
<RenderHTML html='<a href="/about">About us</a>' renderImage={renderImage} />,
|
|
145
|
+
);
|
|
146
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('renders an external link', async () => {
|
|
150
|
+
await renderAsync(
|
|
151
|
+
<RenderHTML html='<a href="https://example.com">External</a>' renderImage={renderImage} />,
|
|
152
|
+
);
|
|
153
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders a link with no href', async () => {
|
|
157
|
+
await renderAsync(<RenderHTML html="<a>No href</a>" renderImage={renderImage} />);
|
|
158
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── div ───────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
it('renders a div with mixed content', async () => {
|
|
164
|
+
await renderAsync(
|
|
165
|
+
<RenderHTML
|
|
166
|
+
html="<div><p>Paragraph</p><span>Inline</span></div>"
|
|
167
|
+
renderImage={renderImage}
|
|
168
|
+
/>,
|
|
169
|
+
);
|
|
170
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── semantic sectioning ───────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
it('renders main', async () => {
|
|
176
|
+
await renderAsync(
|
|
177
|
+
<RenderHTML html="<main><p>Main content</p></main>" renderImage={renderImage} />,
|
|
178
|
+
);
|
|
179
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('renders section', async () => {
|
|
183
|
+
await renderAsync(
|
|
184
|
+
<RenderHTML html="<section><p>Section content</p></section>" renderImage={renderImage} />,
|
|
185
|
+
);
|
|
186
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('renders article', async () => {
|
|
190
|
+
await renderAsync(
|
|
191
|
+
<RenderHTML html="<article><p>Article content</p></article>" renderImage={renderImage} />,
|
|
192
|
+
);
|
|
193
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('renders aside', async () => {
|
|
197
|
+
await renderAsync(
|
|
198
|
+
<RenderHTML html="<aside><p>Aside content</p></aside>" renderImage={renderImage} />,
|
|
199
|
+
);
|
|
200
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('renders nav', async () => {
|
|
204
|
+
await renderAsync(
|
|
205
|
+
<RenderHTML
|
|
206
|
+
html='<nav><a href="/home">Home</a><a href="/about">About</a></nav>'
|
|
207
|
+
renderImage={renderImage}
|
|
208
|
+
/>,
|
|
209
|
+
);
|
|
210
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('renders header', async () => {
|
|
214
|
+
await renderAsync(
|
|
215
|
+
<RenderHTML html="<header><h1>Site Title</h1></header>" renderImage={renderImage} />,
|
|
216
|
+
);
|
|
217
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('renders footer', async () => {
|
|
221
|
+
await renderAsync(
|
|
222
|
+
<RenderHTML html="<footer><p>Copyright 2026</p></footer>" renderImage={renderImage} />,
|
|
223
|
+
);
|
|
224
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('applies tagStyles to section', async () => {
|
|
228
|
+
await renderAsync(
|
|
229
|
+
<RenderHTML
|
|
230
|
+
html="<section><p>Styled section</p></section>"
|
|
231
|
+
renderImage={renderImage}
|
|
232
|
+
tagStyles={{ section: { block: { backgroundColor: 'lightyellow', padding: 12 } } }}
|
|
233
|
+
/>,
|
|
234
|
+
);
|
|
235
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ── blockquote ────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
it('renders a blockquote', async () => {
|
|
241
|
+
await renderAsync(
|
|
242
|
+
<RenderHTML html="<blockquote><p>Quoted text</p></blockquote>" renderImage={renderImage} />,
|
|
243
|
+
);
|
|
244
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ── horizontal rule & line break ──────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
it('renders hr', async () => {
|
|
250
|
+
await renderAsync(
|
|
251
|
+
<RenderHTML html="<p>Before</p><hr /><p>After</p>" renderImage={renderImage} />,
|
|
252
|
+
);
|
|
253
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('renders br between text nodes', async () => {
|
|
257
|
+
await renderAsync(
|
|
258
|
+
<RenderHTML html="<p>Line one<br />Line two</p>" renderImage={renderImage} />,
|
|
259
|
+
);
|
|
260
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ── code / pre ────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
it('renders inline code', async () => {
|
|
266
|
+
await renderAsync(
|
|
267
|
+
<RenderHTML html="<p>Run <code>npm install</code> first</p>" renderImage={renderImage} />,
|
|
268
|
+
);
|
|
269
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('renders a pre block', async () => {
|
|
273
|
+
await renderAsync(<RenderHTML html="<pre>const x = 1;</pre>" renderImage={renderImage} />);
|
|
274
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ── lists ─────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
it('renders an unordered list', async () => {
|
|
280
|
+
await renderAsync(
|
|
281
|
+
<RenderHTML
|
|
282
|
+
html="<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>"
|
|
283
|
+
renderImage={renderImage}
|
|
284
|
+
/>,
|
|
285
|
+
);
|
|
286
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('renders an ordered list', async () => {
|
|
290
|
+
await renderAsync(
|
|
291
|
+
<RenderHTML
|
|
292
|
+
html="<ol><li>First</li><li>Second</li><li>Third</li></ol>"
|
|
293
|
+
renderImage={renderImage}
|
|
294
|
+
/>,
|
|
295
|
+
);
|
|
296
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('renders a nested list', async () => {
|
|
300
|
+
await renderAsync(
|
|
301
|
+
<RenderHTML
|
|
302
|
+
html="<ul><li>Item 1<ul><li>Sub A</li><li>Sub B</li></ul></li><li>Item 2</li></ul>"
|
|
303
|
+
renderImage={renderImage}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ── definition list ───────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
it('renders a definition list', async () => {
|
|
312
|
+
await renderAsync(
|
|
313
|
+
<RenderHTML
|
|
314
|
+
html="<dl><dt>Term</dt><dd>Definition of the term</dd></dl>"
|
|
315
|
+
renderImage={renderImage}
|
|
316
|
+
/>,
|
|
317
|
+
);
|
|
318
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── table ─────────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
it('renders a simple table', async () => {
|
|
324
|
+
await renderAsync(
|
|
325
|
+
<RenderHTML
|
|
326
|
+
html={
|
|
327
|
+
'<table>' +
|
|
328
|
+
'<thead><tr><th>Name</th><th>Age</th></tr></thead>' +
|
|
329
|
+
'<tbody><tr><td>Alice</td><td>30</td></tr><tr><td>Bob</td><td>25</td></tr></tbody>' +
|
|
330
|
+
'<tfoot><tr><td>Total</td><td>2</td></tr></tfoot>' +
|
|
331
|
+
'</table>'
|
|
332
|
+
}
|
|
333
|
+
renderImage={renderImage}
|
|
334
|
+
/>,
|
|
335
|
+
);
|
|
336
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ── image ─────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
it('renders img (after Image.getSize resolves)', async () => {
|
|
342
|
+
// renderAsync flushes all async state updates (including Image.getSize
|
|
343
|
+
// callbacks) inside act(), so we get the stable post-load snapshot.
|
|
344
|
+
await renderAsync(
|
|
345
|
+
<RenderHTML
|
|
346
|
+
html='<img src="https://example.com/photo.jpg" alt="Photo" />'
|
|
347
|
+
renderImage={renderImage}
|
|
348
|
+
/>,
|
|
349
|
+
);
|
|
350
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ── style overrides ───────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
it('applies baseStyle to text', async () => {
|
|
356
|
+
await renderAsync(
|
|
357
|
+
<RenderHTML
|
|
358
|
+
html="<p>Styled text</p>"
|
|
359
|
+
renderImage={renderImage}
|
|
360
|
+
baseStyle={{ fontSize: 18, color: 'navy' }}
|
|
361
|
+
/>,
|
|
362
|
+
);
|
|
363
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('applies tagStyles overrides', async () => {
|
|
367
|
+
await renderAsync(
|
|
368
|
+
<RenderHTML
|
|
369
|
+
html="<h1>Custom heading</h1>"
|
|
370
|
+
renderImage={renderImage}
|
|
371
|
+
tagStyles={{ h1: { text: { color: 'red', fontSize: 40 }, block: { marginTop: 0 } } }}
|
|
372
|
+
/>,
|
|
373
|
+
);
|
|
374
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('applies classesStyles', async () => {
|
|
378
|
+
await renderAsync(
|
|
379
|
+
<RenderHTML
|
|
380
|
+
html='<p class="highlight">Highlighted paragraph</p>'
|
|
381
|
+
renderImage={renderImage}
|
|
382
|
+
classesStyles={{
|
|
383
|
+
highlight: { text: { color: 'green' }, block: { backgroundColor: 'yellow' } },
|
|
384
|
+
}}
|
|
385
|
+
/>,
|
|
386
|
+
);
|
|
387
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('applies listGap', async () => {
|
|
391
|
+
await renderAsync(
|
|
392
|
+
<RenderHTML html="<ul><li>A</li><li>B</li></ul>" renderImage={renderImage} listGap={8} />,
|
|
393
|
+
);
|
|
394
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ── unsupported / ignored tags ────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
it('ignores script tags', async () => {
|
|
400
|
+
await renderAsync(
|
|
401
|
+
<RenderHTML
|
|
402
|
+
html="<p>Visible</p><script>alert(1)</script><p>Also visible</p>"
|
|
403
|
+
renderImage={renderImage}
|
|
404
|
+
/>,
|
|
405
|
+
);
|
|
406
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('ignores style tags', async () => {
|
|
410
|
+
await renderAsync(
|
|
411
|
+
<RenderHTML html="<style>body{color:red}</style><p>Text</p>" renderImage={renderImage} />,
|
|
412
|
+
);
|
|
413
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ── complex document ──────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
it('renders a realistic article fragment', async () => {
|
|
419
|
+
await renderAsync(
|
|
420
|
+
<RenderHTML
|
|
421
|
+
html={
|
|
422
|
+
'<h1>Article Title</h1>' +
|
|
423
|
+
'<p>An introductory paragraph with <b>bold</b>, <i>italic</i> and an ' +
|
|
424
|
+
'<a href="https://example.com">external link</a>.</p>' +
|
|
425
|
+
'<h2>Section</h2>' +
|
|
426
|
+
'<ul><li>Point one</li><li>Point two</li></ul>' +
|
|
427
|
+
'<blockquote><p>A notable quote.</p></blockquote>' +
|
|
428
|
+
'<pre>code block</pre>'
|
|
429
|
+
}
|
|
430
|
+
renderImage={renderImage}
|
|
431
|
+
/>,
|
|
432
|
+
);
|
|
433
|
+
expect(screen.toJSON()).toMatchSnapshot();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseDocument } from 'htmlparser2';
|
|
2
|
+
import { type ComponentType, type FunctionComponent, type ReactNode, useMemo } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
type ColorValue,
|
|
5
|
+
type ImageProps,
|
|
6
|
+
type TextProps,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { HtmlProvider, type OnHTMLLinkPress } from './context/HtmlProvider';
|
|
12
|
+
import { HTMLValidator } from './HTMLValidator';
|
|
13
|
+
import { NodesRenderer } from './renderers/_NodesRenderer';
|
|
14
|
+
import type { CommonProps, HtmlStyle, TagStyles } from './types';
|
|
15
|
+
|
|
16
|
+
export interface RenderHTMLProps extends CommonProps {
|
|
17
|
+
html: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const RenderHTML: FunctionComponent<RenderHTMLProps> = (props) => {
|
|
21
|
+
const nodes = useMemo(() => {
|
|
22
|
+
if (!props.html) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const cleaned = props.html.replace(/\n/g, '');
|
|
26
|
+
const n = parseDocument(cleaned);
|
|
27
|
+
return new HTMLValidator(n.children).cleanup();
|
|
28
|
+
}, [props.html]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View>
|
|
32
|
+
<HtmlProvider
|
|
33
|
+
tagStyles={props.tagStyles}
|
|
34
|
+
baseStyle={props.baseStyle}
|
|
35
|
+
classesStyles={props.classesStyles}
|
|
36
|
+
listGap={props.listGap}
|
|
37
|
+
overrideExternalLinkTintColor={props.overrideExternalLinkTintColor}
|
|
38
|
+
markerColor={props.markerColor}
|
|
39
|
+
onLinkPress={props.onLinkPress}
|
|
40
|
+
renderImage={props.renderImage}
|
|
41
|
+
renderTextComponent={props.renderTextComponent}
|
|
42
|
+
>
|
|
43
|
+
<NodesRenderer nodes={nodes} />
|
|
44
|
+
</HtmlProvider>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
};
|