@nikpnevmatikos/html-renderer 0.1.0-alpha.3 → 0.2.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Html-Renderer contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Html-Renderer contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,279 +1,297 @@
1
- # Html-Renderer
2
-
3
- [![npm version](https://img.shields.io/npm/v/@nikpnevmatikos/html-renderer?color=blue&label=npm)](https://www.npmjs.com/package/@nikpnevmatikos/html-renderer)
4
- [![CI](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
-
7
- A modern React Native HTML renderer, written in TypeScript with **zero native modules**. Built from scratch as a maintained alternative to the abandoned `react-native-render-html`.
8
-
9
- > **Status:** currently in alpha (`0.1.0-alpha.2`). Install with `@alpha` tag see below.
10
-
11
- - **Zero native code** — works on iOS, Android, Web (via `react-native-web`), and Expo Go without a dev build.
12
- - **Fabric (new architecture) compatible** out of the box.
13
- - **Real CSS stylesheet support** a `stylesheet` prop that accepts actual CSS with selectors and specificity.
14
- - Transient render tree model: HTML DOM resolved render tree → `<Text>` / `<View>` / `<Image>`.
15
- - Full style inheritance, CSS cascade, and the box-model basics.
16
- - Extensible via custom renderers, custom element models, DOM transform hooks, and per-renderer config.
17
- - 100+ unit tests, typed end-to-end.
18
-
19
- ## Install
20
-
21
- ```bash
22
- npm install @nikpnevmatikos/html-renderer@alpha
23
- ```
24
-
25
- Peer dependencies: `react >= 18`, `react-native >= 0.73`.
26
-
27
- ## Quick start
28
-
29
- ```tsx
30
- import { HtmlRenderer } from '@nikpnevmatikos/html-renderer';
31
-
32
- export default function Screen() {
33
- return (
34
- <HtmlRenderer
35
- html={`<h1>Hello</h1><p>This is <strong>bold</strong> and <a href="https://x.dev">a link</a>.</p>`}
36
- />
37
- );
38
- }
39
- ```
40
-
41
- ## Props
42
-
43
- | Prop | Type | Description |
44
- |---|---|---|
45
- | `html` | `string` | The HTML source to render. |
46
- | `baseStyle` | `ResolvedStyle` | Root style inherited by all content. |
47
- | `stylesheet` | `string` | A CSS stylesheet with real selectors (type, class, id, descendant, child). |
48
- | `tagsStyles` | `Record<tag, StyleInput>` | Per-tag style override (`{ h1: {...} }`). |
49
- | `classesStyles` | `Record<class, StyleInput>` | Style by `class` attribute. |
50
- | `idsStyles` | `Record<id, StyleInput>` | Style by `id` attribute. |
51
- | `customRenderers` | `Record<tag, CustomRenderer>` | Replace or wrap the renderer for any tag. |
52
- | `customHTMLElementModels` | `Record<tag, HTMLElementModel>` | Define new tags with custom block/inline semantics and default styles. |
53
- | `renderersProps` | `Record<tag, Record<string, unknown>>` | Per-renderer config. Built-in consumers: `ol.startIndex`, `ul/ol.markerTextStyle`, `img.initialDimensions`. |
54
- | `contentWidth` | `number` | Max render width. Images wider than this scale down proportionally. |
55
- | `transformDom` | `(dom: DomNode[]) => DomNode[]` | Runs after parse, before build. Use for sanitization or tag rewrites. |
56
- | `onLinkPress` | `(href, attribs) => void` | Override the default `Linking.openURL` link handler. |
57
- | `ignoredDomTags` | `string[]` | Tags to drop during parse (subtree removed). |
58
- | `ignoredStyles` | `string[]` | CSS properties to drop. Accepts kebab-case (`background-color`) or camelCase (`backgroundColor`). |
59
- | `defaultTextProps` | `TextProps` | Spread onto every `<Text>`. |
60
- | `defaultViewProps` | `ViewProps` | Spread onto every `<View>`. |
61
- | `textSelectable` | `boolean` | Shortcut for `defaultTextProps.selectable = true`. |
62
-
63
- `StyleInput` is `ResolvedStyle | string` — every style map accepts either an RN-style object *or* a CSS declarations string:
64
-
65
- ```tsx
66
- tagsStyles={{
67
- h1: { color: 'red', fontSize: 24 },
68
- h2: 'color: blue; font-size: 20px', // CSS string works too
69
- }}
70
- ```
71
-
72
- ## Supported tags
73
-
74
- **Block:** `p`, `div`, `h1–h6`, `ul`, `ol`, `li`, `pre`, `blockquote`, `hr`, `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`, `caption`.
75
-
76
- **Inline:** `span`, `strong`/`b`, `em`/`i`, `u`, `s`/`del`/`strike`, `ins`, `mark`, `small`, `code`, `a`, `br`, `img`.
77
-
78
- ## Supported CSS
79
-
80
- - Typography: `color`, `font-size` (px), `font-family`, `font-weight`, `font-style`, `text-align`, `text-decoration`, `line-height` (px)
81
- - Box model: `margin` (shorthand + individual sides), `padding` (shorthand + individual sides), `background-color`
82
- - Colors: hex, `rgb()`, `rgba()`, `hsl()`, named (passed through to RN's color system)
83
- - Units: **`px` only** for now `em`/`rem`/`%` are not resolved yet
84
-
85
- ## Examples
86
-
87
- ### `stylesheet` — real CSS with selectors
88
-
89
- ```tsx
90
- const css = `
91
- article.card {
92
- background-color: #fafbfc;
93
- padding: 12px;
94
- }
95
- article.card h3 {
96
- color: #1a73e8;
97
- }
98
- .highlight {
99
- background-color: #fff3a3;
100
- }
101
- h1 > span {
102
- font-weight: bold;
103
- }
104
- `;
105
-
106
- <HtmlRenderer html={html} stylesheet={css} />;
107
- ```
108
-
109
- Supports: type (`h1`), class (`.foo`), id (`#bar`), universal (`*`), compound (`h1.big#hero`), descendant (`article span`), child (`h1 > span`), selector lists (`h1, h2`). Specificity and source order work per the CSS spec.
110
-
111
- Not supported: pseudo-classes, pseudo-elements, attribute selectors, sibling combinators (`+`, `~`), `@media` queries.
112
-
113
- ### Custom renderer
114
-
115
- ```tsx
116
- import { type CustomRenderer } from '@nikpnevmatikos/html-renderer';
117
-
118
- const customRenderers: Record<string, CustomRenderer> = {
119
- h1: (node, defaultRender) => (
120
- <View style={{ borderBottomWidth: 2, borderBottomColor: 'blue' }}>
121
- {defaultRender()}
122
- </View>
123
- ),
124
- };
125
-
126
- <HtmlRenderer html={html} customRenderers={customRenderers} />;
127
- ```
128
-
129
- ### Custom HTML element models
130
-
131
- Define your own tags that behave like real HTML:
132
-
133
- ```tsx
134
- import { type HTMLElementModel } from '@nikpnevmatikos/html-renderer';
135
-
136
- const customHTMLElementModels: Record<string, HTMLElementModel> = {
137
- 'my-card': {
138
- display: 'block',
139
- tagDefaultStyle: { backgroundColor: '#eef', padding: 12 },
140
- },
141
- 'x-spacer': {
142
- display: 'block',
143
- isVoid: true, // ignore any children
144
- tagDefaultStyle: { height: 20 },
145
- },
146
- };
147
-
148
- <HtmlRenderer
149
- html="<my-card>hello</my-card>"
150
- customHTMLElementModels={customHTMLElementModels}
151
- />;
152
- ```
153
-
154
- ### DOM transform hook
155
-
156
- ```tsx
157
- import { type TransformDom } from '@nikpnevmatikos/html-renderer';
158
-
159
- const sanitize: TransformDom = (dom) => walk(dom);
160
-
161
- function walk(nodes) {
162
- return nodes.map((n) => {
163
- if (n.type === 'element') {
164
- return { ...n, attribs: { ...n.attribs, onclick: '' }, children: walk(n.children) };
165
- }
166
- return n;
167
- });
168
- }
169
-
170
- <HtmlRenderer html={html} transformDom={sanitize} />;
171
- ```
172
-
173
- ### Link handling
174
-
175
- ```tsx
176
- import { type OnLinkPress } from '@nikpnevmatikos/html-renderer';
177
-
178
- const onLinkPress: OnLinkPress = (href, attribs) => {
179
- if (attribs.target === '_blank') {
180
- void Linking.openURL(href);
181
- } else {
182
- navigation.navigate('InAppBrowser', { url: href });
183
- }
184
- };
185
-
186
- <HtmlRenderer html={html} onLinkPress={onLinkPress} />;
187
- ```
188
-
189
- ### Auto-fit images
190
-
191
- ```tsx
192
- import { Dimensions } from 'react-native';
193
-
194
- const contentWidth = Dimensions.get('window').width - 32;
195
-
196
- <HtmlRenderer html={html} contentWidth={contentWidth} />;
197
- ```
198
-
199
- ### Renderers props (per-renderer config)
200
-
201
- ```tsx
202
- <HtmlRenderer
203
- html={html}
204
- renderersProps={{
205
- ol: { startIndex: 5, markerTextStyle: { color: '#888' } },
206
- ul: { markerTextStyle: { color: 'red' } },
207
- img: { initialDimensions: { width: 300, height: 200 } },
208
- }}
209
- />
210
- ```
211
-
212
- ## Cascade order
213
-
214
- From lowest to highest priority:
215
-
216
- ```
217
- 1. baseStyle (HtmlRenderer prop — root defaults)
218
- 2. Built-in tag defaults (h1 bold, strong bold, etc.)
219
- 3. stylesheet matches (by selector specificity + source order)
220
- 4. tagsStyles (per-tag programmatic override)
221
- 5. classesStyles (by matched class)
222
- 6. idsStyles (by matched id)
223
- 7. Inline style="..." (highest — HTML inline always wins)
224
- ```
225
-
226
- ## How it works
227
-
228
- ```
229
- HTML string
230
- └─ parseHtml (htmlparser2) → DOM tree
231
- └─ transformDom? (optional) → DOM tree
232
- └─ buildRenderTree → Render tree
233
- └─ resolveStyles (full cascade per element)
234
- └─ hoistBlocks (fragment inline-wrapping-block)
235
- └─ collapseWhitespace (CSS whitespace rules)
236
- └─ Renderer → <View> / <Text> / <Image>
237
- ```
238
-
239
- Styles resolve at build time into a single `ResolvedStyle` per element. At render time, `splitStyle` partitions each style into View-applicable and Text-applicable halves and applies them to the correct component.
240
-
241
- ## Current limitations
242
-
243
- Actively on the roadmap:
244
-
245
- - **No `rowspan`** on tables (colspan works).
246
- - **CSS units** beyond `px` — `em`, `rem`, `%` not resolved yet.
247
- - **Forms** — `<input>`, `<textarea>`, `<button>`, `<select>` not yet rendered (planned for core, pure-JS via RN's `TextInput` / `Pressable`).
248
- - **Stylesheet features** — pseudo-classes (`:first-child`, `:nth-child`), attribute selectors (`[type="text"]`), and `@media` queries not yet supported.
249
- - **Advanced CSS** transforms, opacity, individual `border-*` sides, `border-radius`, flex/grid display modes.
250
- - **Intrinsic table column widths** — columns render equal-width (`flex: 1`); `width` on `<col>` / cells is ignored.
251
-
252
- ## Plugin packages (planned)
253
-
254
- Features that need native dependencies ship as separate packages so the core stays zero-native-modules. Each uses the core's `customRenderers` and `customHTMLElementModels` APIs — no core changes needed to add them.
255
-
256
- | Tags | Planned package | Native peer dep |
257
- |---|---|---|
258
- | `<iframe>` | `@nikpnevmatikos/html-renderer-webview` | `react-native-webview` |
259
- | `<video>`, `<audio>` | `@nikpnevmatikos/html-renderer-video` | `expo-video` or `react-native-video` |
260
- | `<svg>` | `@nikpnevmatikos/html-renderer-svg` | `react-native-svg` |
261
-
262
- Until these ship, you can wire any of these tags yourself via `customRenderers` — the same API the plugins will use. See the "Custom renderer" and "Custom HTML element models" examples above.
263
-
264
- ## Development
265
-
266
- ```bash
267
- npm install # installs all workspace deps
268
- npm run dev # tsc --watch on core
269
- npm test # jest — 100+ tests
270
- npm run typecheck # tsc --noEmit on core
271
- npm run build # build core to dist
272
-
273
- # live example app
274
- cd example && npm start
275
- ```
276
-
277
- ## License
278
-
279
- MIT — see [LICENSE](./LICENSE).
1
+ # Html-Renderer
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@nikpnevmatikos/html-renderer?color=blue&label=npm)](https://www.npmjs.com/package/@nikpnevmatikos/html-renderer)
4
+ [![CI](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
+
7
+ A modern React Native HTML renderer, written in TypeScript with **zero native modules**. Built from scratch as a maintained alternative to the abandoned `react-native-render-html`.
8
+
9
+ - **Zero native code** works on iOS, Android, Web (via `react-native-web`), and Expo Go without a dev build.
10
+ - **Fabric (new architecture) compatible** out of the box.
11
+ - **Real CSS stylesheet support** — a `stylesheet` prop that accepts actual CSS with selectors and specificity.
12
+ - Transient render tree model: HTML DOM → resolved render tree → `<Text>` / `<View>` / `<Image>`.
13
+ - Full style inheritance, CSS cascade, and the box-model basics.
14
+ - Extensible via custom renderers, custom element models, DOM transform hooks, and per-renderer config.
15
+ - **Entity-encoded HTML** (`&lt;p&gt;hello&lt;/p&gt;` from CMS/API backends, even double-encoded) is auto-detected and rendered as HTML — no pre-decoding needed.
16
+ - 130+ unit tests, typed end-to-end.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @nikpnevmatikos/html-renderer
22
+ ```
23
+
24
+ Peer dependencies: `react >= 18`, `react-native >= 0.73`.
25
+
26
+ ## Quick start
27
+
28
+ ```tsx
29
+ import { HtmlRenderer } from '@nikpnevmatikos/html-renderer';
30
+
31
+ export default function Screen() {
32
+ return (
33
+ <HtmlRenderer
34
+ html={`<h1>Hello</h1><p>This is <strong>bold</strong> and <a href="https://x.dev">a link</a>.</p>`}
35
+ />
36
+ );
37
+ }
38
+ ```
39
+
40
+ ## Props
41
+
42
+ | Prop | Type | Description |
43
+ |---|---|---|
44
+ | `html` | `string` | The HTML source to render. |
45
+ | `baseStyle` | `ResolvedStyle` | Root style. Inherited text props cascade to all content; box props (background, padding, …) style the root container. |
46
+ | `stylesheet` | `string` | A CSS stylesheet with real selectors (type, class, id, descendant, child). |
47
+ | `tagsStyles` | `Record<tag, StyleInput>` | Per-tag style override (`{ h1: {...} }`). `body` is special: it styles the document root even when the HTML is a fragment with no `<body>` tag see below. |
48
+ | `classesStyles` | `Record<class, StyleInput>` | Style by `class` attribute. |
49
+ | `idsStyles` | `Record<id, StyleInput>` | Style by `id` attribute. |
50
+ | `customRenderers` | `Record<tag, CustomRenderer>` | Replace or wrap the renderer for any tag. |
51
+ | `customHTMLElementModels` | `Record<tag, HTMLElementModel>` | Define new tags with custom block/inline semantics and default styles. |
52
+ | `renderersProps` | `Record<tag, Record<string, unknown>>` | Per-renderer config. Built-in consumers: `ol.startIndex`, `ul/ol.markerTextStyle`, `img.initialDimensions`. |
53
+ | `contentWidth` | `number` | Max render width. Images wider than this scale down proportionally. |
54
+ | `transformDom` | `(dom: DomNode[]) => DomNode[]` | Runs after parse, before build. Use for sanitization or tag rewrites. |
55
+ | `onLinkPress` | `(href, attribs) => void` | Override the default `Linking.openURL` link handler. |
56
+ | `ignoredDomTags` | `string[]` | Tags to drop during parse (subtree removed). Merged with the built-in defaults: `head`, `title`, `style`, `script`, `link`, `meta`, `base`. |
57
+ | `ignoredStyles` | `string[]` | CSS properties to drop. Accepts kebab-case (`background-color`) or camelCase (`backgroundColor`). |
58
+ | `defaultTextProps` | `TextProps` | Spread onto every `<Text>`. |
59
+ | `defaultViewProps` | `ViewProps` | Spread onto every `<View>`. |
60
+ | `textSelectable` | `boolean` | Shortcut for `defaultTextProps.selectable = true`. |
61
+
62
+ `StyleInput` is `ResolvedStyle | string` — every style map accepts either an RN-style object *or* a CSS declarations string:
63
+
64
+ ```tsx
65
+ tagsStyles={{
66
+ h1: { color: 'red', fontSize: 24 },
67
+ h2: 'color: blue; font-size: 20px', // CSS string works too
68
+ }}
69
+ ```
70
+
71
+ ## Supported tags
72
+
73
+ **Block:** `p`, `div`, `h1–h6`, `ul`, `ol`, `li`, `pre`, `blockquote`, `hr`, `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`, `caption`.
74
+
75
+ **Inline:** `span`, `strong`/`b`, `em`/`i`, `u`, `s`/`del`/`strike`, `ins`, `mark`, `small`, `code`, `a`, `br`, `img`.
76
+
77
+ **Documents:** full-document HTML works too — literal `<html>`/`<body>` render as plain block containers, and `<head>`, `<title>`, `<style>`, `<script>`, `<link>`, `<meta>`, `<base>` are ignored by default.
78
+
79
+ ## Supported CSS
80
+
81
+ - Typography: `color`, `font-size` (px), `font-family`, `font-weight`, `font-style`, `text-align`, `text-decoration`, `text-transform`, `letter-spacing`, `line-height` (px)
82
+ - Box model: `margin` / `padding` (shorthands + individual sides), `background-color`, the `border` family (shorthands, per-side widths/colors, `border-style`, `border-radius` including per-corner), `width`/`height` with `min-`/`max-` variants
83
+ - Other: `opacity`, `display` (`flex` and `none`)
84
+ - Colors: hex, `rgb()`, `rgba()`, `hsl()`, named (passed through to RN's color system)
85
+ - Units: **`px` only** for now — `em`/`rem` are not resolved yet, and `%` works only on `width`/`height`
86
+
87
+ ## Examples
88
+
89
+ ### Root styles — `baseStyle` and `tagsStyles.body`
90
+
91
+ `tagsStyles.body` styles the document root even when the source HTML is a fragment with no `<body>` element — as if the content were wrapped in a synthetic body. Inherited text properties cascade into all content; box properties are applied once to the root container. This makes it the natural way to set the root text color:
92
+
93
+ ```tsx
94
+ <HtmlRenderer
95
+ html="<p>Hello</p>"
96
+ tagsStyles={{ body: { color: 'white' } }} // text renders white
97
+ />
98
+ ```
99
+
100
+ `baseStyle` does the same thing one rung lower in the cascade (`tagsStyles.body` wins where both set a property). If the HTML contains a literal `<body>` tag, `tagsStyles.body` is applied on that element instead, so box props are never applied twice.
101
+
102
+ ### `stylesheet` — real CSS with selectors
103
+
104
+ ```tsx
105
+ const css = `
106
+ article.card {
107
+ background-color: #fafbfc;
108
+ padding: 12px;
109
+ }
110
+ article.card h3 {
111
+ color: #1a73e8;
112
+ }
113
+ .highlight {
114
+ background-color: #fff3a3;
115
+ }
116
+ h1 > span {
117
+ font-weight: bold;
118
+ }
119
+ `;
120
+
121
+ <HtmlRenderer html={html} stylesheet={css} />;
122
+ ```
123
+
124
+ Supports: type (`h1`), class (`.foo`), id (`#bar`), universal (`*`), compound (`h1.big#hero`), descendant (`article span`), child (`h1 > span`), selector lists (`h1, h2`). Specificity and source order work per the CSS spec.
125
+
126
+ Not supported: pseudo-classes, pseudo-elements, attribute selectors, sibling combinators (`+`, `~`), `@media` queries.
127
+
128
+ ### Custom renderer
129
+
130
+ ```tsx
131
+ import { type CustomRenderer } from '@nikpnevmatikos/html-renderer';
132
+
133
+ const customRenderers: Record<string, CustomRenderer> = {
134
+ h1: (node, defaultRender) => (
135
+ <View style={{ borderBottomWidth: 2, borderBottomColor: 'blue' }}>
136
+ {defaultRender()}
137
+ </View>
138
+ ),
139
+ };
140
+
141
+ <HtmlRenderer html={html} customRenderers={customRenderers} />;
142
+ ```
143
+
144
+ ### Custom HTML element models
145
+
146
+ Define your own tags that behave like real HTML:
147
+
148
+ ```tsx
149
+ import { type HTMLElementModel } from '@nikpnevmatikos/html-renderer';
150
+
151
+ const customHTMLElementModels: Record<string, HTMLElementModel> = {
152
+ 'my-card': {
153
+ display: 'block',
154
+ tagDefaultStyle: { backgroundColor: '#eef', padding: 12 },
155
+ },
156
+ 'x-spacer': {
157
+ display: 'block',
158
+ isVoid: true, // ignore any children
159
+ tagDefaultStyle: { height: 20 },
160
+ },
161
+ };
162
+
163
+ <HtmlRenderer
164
+ html="<my-card>hello</my-card>"
165
+ customHTMLElementModels={customHTMLElementModels}
166
+ />;
167
+ ```
168
+
169
+ ### DOM transform hook
170
+
171
+ ```tsx
172
+ import { type TransformDom } from '@nikpnevmatikos/html-renderer';
173
+
174
+ const sanitize: TransformDom = (dom) => walk(dom);
175
+
176
+ function walk(nodes) {
177
+ return nodes.map((n) => {
178
+ if (n.type === 'element') {
179
+ return { ...n, attribs: { ...n.attribs, onclick: '' }, children: walk(n.children) };
180
+ }
181
+ return n;
182
+ });
183
+ }
184
+
185
+ <HtmlRenderer html={html} transformDom={sanitize} />;
186
+ ```
187
+
188
+ ### Link handling
189
+
190
+ ```tsx
191
+ import { type OnLinkPress } from '@nikpnevmatikos/html-renderer';
192
+
193
+ const onLinkPress: OnLinkPress = (href, attribs) => {
194
+ if (attribs.target === '_blank') {
195
+ void Linking.openURL(href);
196
+ } else {
197
+ navigation.navigate('InAppBrowser', { url: href });
198
+ }
199
+ };
200
+
201
+ <HtmlRenderer html={html} onLinkPress={onLinkPress} />;
202
+ ```
203
+
204
+ ### Auto-fit images
205
+
206
+ ```tsx
207
+ import { Dimensions } from 'react-native';
208
+
209
+ const contentWidth = Dimensions.get('window').width - 32;
210
+
211
+ <HtmlRenderer html={html} contentWidth={contentWidth} />;
212
+ ```
213
+
214
+ ### Renderers props (per-renderer config)
215
+
216
+ ```tsx
217
+ <HtmlRenderer
218
+ html={html}
219
+ renderersProps={{
220
+ ol: { startIndex: 5, markerTextStyle: { color: '#888' } },
221
+ ul: { markerTextStyle: { color: 'red' } },
222
+ img: { initialDimensions: { width: 300, height: 200 } },
223
+ }}
224
+ />
225
+ ```
226
+
227
+ ## Cascade order
228
+
229
+ From lowest to highest priority:
230
+
231
+ ```
232
+ 1. baseStyle (HtmlRenderer prop — root defaults)
233
+ 2. tagsStyles.body (document root applies even without a <body> tag)
234
+ 3. Built-in tag defaults (h1 bold, strong bold, etc.)
235
+ 4. stylesheet matches (by selector specificity + source order)
236
+ 5. tagsStyles (per-tag programmatic override)
237
+ 6. classesStyles (by matched class)
238
+ 7. idsStyles (by matched id)
239
+ 8. Inline style="..." (highest HTML inline always wins)
240
+ ```
241
+
242
+ Root styles (1–2) reach descendants through inheritance, so only inherited text properties cascade down — and any element-level match (3–8) overrides them.
243
+
244
+ ## How it works
245
+
246
+ ```
247
+ HTML string
248
+ └─ parseHtml (htmlparser2) DOM tree
249
+ └─ transformDom? (optional) → DOM tree
250
+ └─ buildRenderTree → Render tree
251
+ └─ resolveStyles (full cascade per element)
252
+ └─ hoistBlocks (fragment inline-wrapping-block)
253
+ └─ collapseWhitespace (CSS whitespace rules)
254
+ └─ Renderer → <View> / <Text> / <Image>
255
+ ```
256
+
257
+ Styles resolve at build time into a single `ResolvedStyle` per element. At render time, `splitStyle` partitions each style into View-applicable and Text-applicable halves and applies them to the correct component.
258
+
259
+ ## Current limitations
260
+
261
+ Actively on the roadmap:
262
+
263
+ - **No `rowspan`** on tables (colspan works).
264
+ - **CSS units** beyond `px` — `em`, `rem` not resolved yet; `%` only on `width`/`height`.
265
+ - **Forms** — `<input>`, `<textarea>`, `<button>`, `<select>` not yet rendered (planned for core, pure-JS via RN's `TextInput` / `Pressable`).
266
+ - **Stylesheet features** — pseudo-classes (`:first-child`, `:nth-child`), attribute selectors (`[type="text"]`), and `@media` queries not yet supported.
267
+ - **Advanced CSS** transforms, flex/grid layout of HTML content.
268
+ - **Intrinsic table column widths** — columns render equal-width (`flex: 1`); `width` on `<col>` / cells is ignored.
269
+
270
+ ## Plugin packages (planned)
271
+
272
+ Features that need native dependencies ship as separate packages so the core stays zero-native-modules. Each uses the core's `customRenderers` and `customHTMLElementModels` APIs — no core changes needed to add them.
273
+
274
+ | Tags | Planned package | Native peer dep |
275
+ |---|---|---|
276
+ | `<iframe>` | `@nikpnevmatikos/html-renderer-webview` | `react-native-webview` |
277
+ | `<video>`, `<audio>` | `@nikpnevmatikos/html-renderer-video` | `expo-video` or `react-native-video` |
278
+ | `<svg>` | `@nikpnevmatikos/html-renderer-svg` | `react-native-svg` |
279
+
280
+ Until these ship, you can wire any of these tags yourself via `customRenderers` — the same API the plugins will use. See the "Custom renderer" and "Custom HTML element models" examples above.
281
+
282
+ ## Development
283
+
284
+ ```bash
285
+ npm install # installs all workspace deps
286
+ npm run dev # tsc --watch on core
287
+ npm test # jest — 130+ tests
288
+ npm run typecheck # tsc --noEmit on core
289
+ npm run build # build core to dist
290
+
291
+ # live example app
292
+ cd example && npm start
293
+ ```
294
+
295
+ ## License
296
+
297
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.d.ts CHANGED
@@ -4,4 +4,6 @@ export type { DomNode, DomElement, DomText, HTMLElementModel, RenderNode, Render
4
4
  export { parseHtml } from './parser/parse';
5
5
  export { buildRenderTree } from './render-tree/build';
6
6
  export { parseInlineStyle } from './styles/parse-inline';
7
+ export { resolveRootStyle, ROOT_DEFAULT_STYLE } from './styles/root';
8
+ export type { RootStyleOptions } from './styles/root';
7
9
  //# sourceMappingURL=index.d.ts.map