@nikpnevmatikos/html-renderer 0.1.0-alpha.3 → 0.1.0-alpha.4

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,282 @@
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
+ > **Status:** currently in alpha (`0.1.0-alpha.4`). 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
+ - **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.
18
+ - 100+ unit tests, typed end-to-end.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @nikpnevmatikos/html-renderer@alpha
24
+ ```
25
+
26
+ Peer dependencies: `react >= 18`, `react-native >= 0.73`.
27
+
28
+ > The `@alpha` tag is needed while the package is in prerelease. Once `0.1.0` stable is published, plain `npm install @nikpnevmatikos/html-renderer` will work.
29
+
30
+ ## Quick start
31
+
32
+ ```tsx
33
+ import { HtmlRenderer } from '@nikpnevmatikos/html-renderer';
34
+
35
+ export default function Screen() {
36
+ return (
37
+ <HtmlRenderer
38
+ html={`<h1>Hello</h1><p>This is <strong>bold</strong> and <a href="https://x.dev">a link</a>.</p>`}
39
+ />
40
+ );
41
+ }
42
+ ```
43
+
44
+ ## Props
45
+
46
+ | Prop | Type | Description |
47
+ |---|---|---|
48
+ | `html` | `string` | The HTML source to render. |
49
+ | `baseStyle` | `ResolvedStyle` | Root style inherited by all content. |
50
+ | `stylesheet` | `string` | A CSS stylesheet with real selectors (type, class, id, descendant, child). |
51
+ | `tagsStyles` | `Record<tag, StyleInput>` | Per-tag style override (`{ h1: {...} }`). |
52
+ | `classesStyles` | `Record<class, StyleInput>` | Style by `class` attribute. |
53
+ | `idsStyles` | `Record<id, StyleInput>` | Style by `id` attribute. |
54
+ | `customRenderers` | `Record<tag, CustomRenderer>` | Replace or wrap the renderer for any tag. |
55
+ | `customHTMLElementModels` | `Record<tag, HTMLElementModel>` | Define new tags with custom block/inline semantics and default styles. |
56
+ | `renderersProps` | `Record<tag, Record<string, unknown>>` | Per-renderer config. Built-in consumers: `ol.startIndex`, `ul/ol.markerTextStyle`, `img.initialDimensions`. |
57
+ | `contentWidth` | `number` | Max render width. Images wider than this scale down proportionally. |
58
+ | `transformDom` | `(dom: DomNode[]) => DomNode[]` | Runs after parse, before build. Use for sanitization or tag rewrites. |
59
+ | `onLinkPress` | `(href, attribs) => void` | Override the default `Linking.openURL` link handler. |
60
+ | `ignoredDomTags` | `string[]` | Tags to drop during parse (subtree removed). |
61
+ | `ignoredStyles` | `string[]` | CSS properties to drop. Accepts kebab-case (`background-color`) or camelCase (`backgroundColor`). |
62
+ | `defaultTextProps` | `TextProps` | Spread onto every `<Text>`. |
63
+ | `defaultViewProps` | `ViewProps` | Spread onto every `<View>`. |
64
+ | `textSelectable` | `boolean` | Shortcut for `defaultTextProps.selectable = true`. |
65
+
66
+ `StyleInput` is `ResolvedStyle | string` — every style map accepts either an RN-style object *or* a CSS declarations string:
67
+
68
+ ```tsx
69
+ tagsStyles={{
70
+ h1: { color: 'red', fontSize: 24 },
71
+ h2: 'color: blue; font-size: 20px', // CSS string works too
72
+ }}
73
+ ```
74
+
75
+ ## Supported tags
76
+
77
+ **Block:** `p`, `div`, `h1–h6`, `ul`, `ol`, `li`, `pre`, `blockquote`, `hr`, `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`, `caption`.
78
+
79
+ **Inline:** `span`, `strong`/`b`, `em`/`i`, `u`, `s`/`del`/`strike`, `ins`, `mark`, `small`, `code`, `a`, `br`, `img`.
80
+
81
+ ## Supported CSS
82
+
83
+ - Typography: `color`, `font-size` (px), `font-family`, `font-weight`, `font-style`, `text-align`, `text-decoration`, `line-height` (px)
84
+ - Box model: `margin` (shorthand + individual sides), `padding` (shorthand + individual sides), `background-color`
85
+ - Colors: hex, `rgb()`, `rgba()`, `hsl()`, named (passed through to RN's color system)
86
+ - Units: **`px` only** for now — `em`/`rem`/`%` are not resolved yet
87
+
88
+ ## Examples
89
+
90
+ ### `stylesheet` real CSS with selectors
91
+
92
+ ```tsx
93
+ const css = `
94
+ article.card {
95
+ background-color: #fafbfc;
96
+ padding: 12px;
97
+ }
98
+ article.card h3 {
99
+ color: #1a73e8;
100
+ }
101
+ .highlight {
102
+ background-color: #fff3a3;
103
+ }
104
+ h1 > span {
105
+ font-weight: bold;
106
+ }
107
+ `;
108
+
109
+ <HtmlRenderer html={html} stylesheet={css} />;
110
+ ```
111
+
112
+ 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.
113
+
114
+ Not supported: pseudo-classes, pseudo-elements, attribute selectors, sibling combinators (`+`, `~`), `@media` queries.
115
+
116
+ ### Custom renderer
117
+
118
+ ```tsx
119
+ import { type CustomRenderer } from '@nikpnevmatikos/html-renderer';
120
+
121
+ const customRenderers: Record<string, CustomRenderer> = {
122
+ h1: (node, defaultRender) => (
123
+ <View style={{ borderBottomWidth: 2, borderBottomColor: 'blue' }}>
124
+ {defaultRender()}
125
+ </View>
126
+ ),
127
+ };
128
+
129
+ <HtmlRenderer html={html} customRenderers={customRenderers} />;
130
+ ```
131
+
132
+ ### Custom HTML element models
133
+
134
+ Define your own tags that behave like real HTML:
135
+
136
+ ```tsx
137
+ import { type HTMLElementModel } from '@nikpnevmatikos/html-renderer';
138
+
139
+ const customHTMLElementModels: Record<string, HTMLElementModel> = {
140
+ 'my-card': {
141
+ display: 'block',
142
+ tagDefaultStyle: { backgroundColor: '#eef', padding: 12 },
143
+ },
144
+ 'x-spacer': {
145
+ display: 'block',
146
+ isVoid: true, // ignore any children
147
+ tagDefaultStyle: { height: 20 },
148
+ },
149
+ };
150
+
151
+ <HtmlRenderer
152
+ html="<my-card>hello</my-card>"
153
+ customHTMLElementModels={customHTMLElementModels}
154
+ />;
155
+ ```
156
+
157
+ ### DOM transform hook
158
+
159
+ ```tsx
160
+ import { type TransformDom } from '@nikpnevmatikos/html-renderer';
161
+
162
+ const sanitize: TransformDom = (dom) => walk(dom);
163
+
164
+ function walk(nodes) {
165
+ return nodes.map((n) => {
166
+ if (n.type === 'element') {
167
+ return { ...n, attribs: { ...n.attribs, onclick: '' }, children: walk(n.children) };
168
+ }
169
+ return n;
170
+ });
171
+ }
172
+
173
+ <HtmlRenderer html={html} transformDom={sanitize} />;
174
+ ```
175
+
176
+ ### Link handling
177
+
178
+ ```tsx
179
+ import { type OnLinkPress } from '@nikpnevmatikos/html-renderer';
180
+
181
+ const onLinkPress: OnLinkPress = (href, attribs) => {
182
+ if (attribs.target === '_blank') {
183
+ void Linking.openURL(href);
184
+ } else {
185
+ navigation.navigate('InAppBrowser', { url: href });
186
+ }
187
+ };
188
+
189
+ <HtmlRenderer html={html} onLinkPress={onLinkPress} />;
190
+ ```
191
+
192
+ ### Auto-fit images
193
+
194
+ ```tsx
195
+ import { Dimensions } from 'react-native';
196
+
197
+ const contentWidth = Dimensions.get('window').width - 32;
198
+
199
+ <HtmlRenderer html={html} contentWidth={contentWidth} />;
200
+ ```
201
+
202
+ ### Renderers props (per-renderer config)
203
+
204
+ ```tsx
205
+ <HtmlRenderer
206
+ html={html}
207
+ renderersProps={{
208
+ ol: { startIndex: 5, markerTextStyle: { color: '#888' } },
209
+ ul: { markerTextStyle: { color: 'red' } },
210
+ img: { initialDimensions: { width: 300, height: 200 } },
211
+ }}
212
+ />
213
+ ```
214
+
215
+ ## Cascade order
216
+
217
+ From lowest to highest priority:
218
+
219
+ ```
220
+ 1. baseStyle (HtmlRenderer prop — root defaults)
221
+ 2. Built-in tag defaults (h1 bold, strong bold, etc.)
222
+ 3. stylesheet matches (by selector specificity + source order)
223
+ 4. tagsStyles (per-tag programmatic override)
224
+ 5. classesStyles (by matched class)
225
+ 6. idsStyles (by matched id)
226
+ 7. Inline style="..." (highest — HTML inline always wins)
227
+ ```
228
+
229
+ ## How it works
230
+
231
+ ```
232
+ HTML string
233
+ └─ parseHtml (htmlparser2) → DOM tree
234
+ └─ transformDom? (optional) → DOM tree
235
+ └─ buildRenderTree → Render tree
236
+ └─ resolveStyles (full cascade per element)
237
+ └─ hoistBlocks (fragment inline-wrapping-block)
238
+ └─ collapseWhitespace (CSS whitespace rules)
239
+ └─ Renderer → <View> / <Text> / <Image>
240
+ ```
241
+
242
+ 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.
243
+
244
+ ## Current limitations
245
+
246
+ Actively on the roadmap:
247
+
248
+ - **No `rowspan`** on tables (colspan works).
249
+ - **CSS units** beyond `px` `em`, `rem`, `%` not resolved yet.
250
+ - **Forms** `<input>`, `<textarea>`, `<button>`, `<select>` not yet rendered (planned for core, pure-JS via RN's `TextInput` / `Pressable`).
251
+ - **Stylesheet features** — pseudo-classes (`:first-child`, `:nth-child`), attribute selectors (`[type="text"]`), and `@media` queries not yet supported.
252
+ - **Advanced CSS** — transforms, opacity, individual `border-*` sides, `border-radius`, flex/grid display modes.
253
+ - **Intrinsic table column widths** — columns render equal-width (`flex: 1`); `width` on `<col>` / cells is ignored.
254
+
255
+ ## Plugin packages (planned)
256
+
257
+ 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.
258
+
259
+ | Tags | Planned package | Native peer dep |
260
+ |---|---|---|
261
+ | `<iframe>` | `@nikpnevmatikos/html-renderer-webview` | `react-native-webview` |
262
+ | `<video>`, `<audio>` | `@nikpnevmatikos/html-renderer-video` | `expo-video` or `react-native-video` |
263
+ | `<svg>` | `@nikpnevmatikos/html-renderer-svg` | `react-native-svg` |
264
+
265
+ 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.
266
+
267
+ ## Development
268
+
269
+ ```bash
270
+ npm install # installs all workspace deps
271
+ npm run dev # tsc --watch on core
272
+ npm test # jest — 100+ tests
273
+ npm run typecheck # tsc --noEmit on core
274
+ npm run build # build core to dist
275
+
276
+ # live example app
277
+ cd example && npm start
278
+ ```
279
+
280
+ ## License
281
+
282
+ MIT — see [LICENSE](./LICENSE).
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/parser/parse.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAKjD"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/parser/parse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAUxC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAKjD"}
@@ -1,10 +1,31 @@
1
1
  import { parseDocument } from 'htmlparser2';
2
+ import { decodeHTML } from 'entities';
3
+ // An entity-encoded tag opening, e.g. "&lt;p", "&lt;/span", "&#60;div".
4
+ // "(?:amp;)*" also catches double-encoded input such as "&amp;lt;p".
5
+ const ENCODED_TAG = /&(?:amp;)*(?:lt|#0*60|#x0*3c);[a-zA-Z!/]/i;
6
+ // A real tag opening, e.g. "<p" or "</span".
7
+ const REAL_TAG = /<[a-zA-Z!/]/;
8
+ // Bound on decode passes so nested encoding resolves without looping forever.
9
+ const MAX_DECODE_PASSES = 3;
2
10
  export function parseHtml(html) {
3
- const doc = parseDocument(html, { decodeEntities: true });
11
+ const doc = parseDocument(decodeIfEncoded(html), { decodeEntities: true });
4
12
  return doc.children
5
13
  .map(toDomNode)
6
14
  .filter((n) => n !== null);
7
15
  }
16
+ /**
17
+ * Some backends deliver HTML entity-escaped ("&lt;p&gt;hello&lt;/p&gt;").
18
+ * Parsed directly that yields a single text node of literal markup, so when
19
+ * the input contains encoded tags but no real ones, decode it first. Input
20
+ * with any real tag is left untouched — there "&lt;" is intentional text.
21
+ */
22
+ function decodeIfEncoded(html) {
23
+ let out = html;
24
+ for (let pass = 0; pass < MAX_DECODE_PASSES && !REAL_TAG.test(out) && ENCODED_TAG.test(out); pass++) {
25
+ out = decodeHTML(out);
26
+ }
27
+ return out;
28
+ }
8
29
  function toDomNode(n) {
9
30
  if (n.type === 'text') {
10
31
  const t = n;
@@ -1 +1 @@
1
- {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/parser/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,OAAO,GAAG,CAAC,QAAQ;SAChB,GAAG,CAAC,SAAS,CAAC;SACd,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,SAAS,CAAC,CAAO;IACxB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,CAAS,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACrB,MAAM,EAAE,GAAG,CAAY,CAAC;QACxB,OAAO;YACL,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;YAC3B,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE;YAC1B,QAAQ,EAAE,EAAE,CAAC,QAAQ;iBAClB,GAAG,CAAC,SAAS,CAAC;iBACd,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;SAC3C,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/parser/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAItC,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,WAAW,GAAG,2CAA2C,CAAC;AAChE,6CAA6C;AAC7C,MAAM,QAAQ,GAAG,aAAa,CAAC;AAC/B,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAE5B,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,GAAG,GAAG,aAAa,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,OAAO,GAAG,CAAC,QAAQ;SAChB,GAAG,CAAC,SAAS,CAAC;SACd,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KACE,IAAI,IAAI,GAAG,CAAC,EACZ,IAAI,GAAG,iBAAiB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EACxE,IAAI,EAAE,EACN,CAAC;QACD,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAAC,CAAO;IACxB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,CAAS,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACrB,MAAM,EAAE,GAAG,CAAY,CAAC;QACxB,OAAO;YACL,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;YAC3B,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE;YAC1B,QAAQ,EAAE,EAAE,CAAC,QAAQ;iBAClB,GAAG,CAAC,SAAS,CAAC;iBACd,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;SAC3C,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -30,8 +30,8 @@ export function RenderedImage({ node, contentWidth, initialDimensions, }) {
30
30
  return <View style={styles.placeholder}/>;
31
31
  }
32
32
  if (resolvedSize.w === 0 || resolvedSize.h === 0) {
33
- return (<View style={styles.broken}>
34
- <Text style={styles.brokenLabel}>{node.alt ?? '[broken image]'}</Text>
33
+ return (<View style={styles.broken}>
34
+ <Text style={styles.brokenLabel}>{node.alt ?? '[broken image]'}</Text>
35
35
  </View>);
36
36
  }
37
37
  const fitted = fit(resolvedSize.w, resolvedSize.h, contentWidth);
@@ -52,8 +52,8 @@ export function HtmlRenderer({ html, baseStyle, tagsStyles, classesStyles, idsSt
52
52
  defaultViewProps: mergedViewProps,
53
53
  };
54
54
  const parentStyle = { ...ROOT_STYLE, ...(baseStyle ?? {}) };
55
- return (<View {...mergedViewProps}>
56
- {renderBlockChildren(tree, parentStyle, ctx)}
55
+ return (<View {...mergedViewProps}>
56
+ {renderBlockChildren(tree, parentStyle, ctx)}
57
57
  </View>);
58
58
  }
59
59
  function groupBlockChildren(children) {
@@ -95,8 +95,8 @@ function renderSegment(seg, parentStyle, key, ctx) {
95
95
  return renderBlockElement(seg.node, key, ctx);
96
96
  case 'run': {
97
97
  const { text: tStyle } = splitStyle(parentStyle);
98
- return (<Text key={key} {...ctx.defaultTextProps} style={tStyle}>
99
- {seg.nodes.map((n, i) => renderInline(n, i, ctx))}
98
+ return (<Text key={key} {...ctx.defaultTextProps} style={tStyle}>
99
+ {seg.nodes.map((n, i) => renderInline(n, i, ctx))}
100
100
  </Text>);
101
101
  }
102
102
  }
@@ -108,8 +108,8 @@ function renderBlockElement(el, key, ctx) {
108
108
  renderersProps: ctx.renderersProps,
109
109
  contentWidth: ctx.contentWidth,
110
110
  };
111
- return (<React.Fragment key={key}>
112
- {custom(el, () => renderBlockDefault(el, undefined, ctx), info)}
111
+ return (<React.Fragment key={key}>
112
+ {custom(el, () => renderBlockDefault(el, undefined, ctx), info)}
113
113
  </React.Fragment>);
114
114
  }
115
115
  return renderBlockDefault(el, key, ctx);
@@ -120,41 +120,41 @@ function renderBlockDefault(el, key, ctx) {
120
120
  return (<View key={key} {...ctx.defaultViewProps} style={[styles.hr, vStyle]}/>);
121
121
  }
122
122
  if (el.tag === 'ul' || el.tag === 'ol') {
123
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, styles.list, vStyle]}>
124
- {renderBlockChildren(el.children, el.style, ctx)}
123
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, styles.list, vStyle]}>
124
+ {renderBlockChildren(el.children, el.style, ctx)}
125
125
  </View>);
126
126
  }
127
127
  if (el.tag === 'li' && el.listMarker !== undefined) {
128
128
  const listKey = el.listOrdered ? 'ol' : 'ul';
129
129
  const listProps = ctx.renderersProps[listKey];
130
130
  const markerStyleOverride = listProps?.markerTextStyle;
131
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.listItem, vStyle]}>
132
- <Text {...ctx.defaultTextProps} style={[tStyle, styles.listMarker, markerStyleOverride]}>
133
- {el.listMarker}
134
- </Text>
135
- <View {...ctx.defaultViewProps} style={styles.listItemContent}>
136
- {renderBlockChildren(el.children, el.style, ctx)}
137
- </View>
131
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.listItem, vStyle]}>
132
+ <Text {...ctx.defaultTextProps} style={[tStyle, styles.listMarker, markerStyleOverride]}>
133
+ {el.listMarker}
134
+ </Text>
135
+ <View {...ctx.defaultViewProps} style={styles.listItemContent}>
136
+ {renderBlockChildren(el.children, el.style, ctx)}
137
+ </View>
138
138
  </View>);
139
139
  }
140
140
  if (el.tag === 'pre') {
141
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, styles.preBlock, vStyle]}>
142
- {renderBlockChildren(el.children, el.style, ctx)}
141
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, styles.preBlock, vStyle]}>
142
+ {renderBlockChildren(el.children, el.style, ctx)}
143
143
  </View>);
144
144
  }
145
145
  if (el.tag === 'blockquote') {
146
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, styles.blockquote, vStyle]}>
147
- {renderBlockChildren(el.children, el.style, ctx)}
146
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, styles.blockquote, vStyle]}>
147
+ {renderBlockChildren(el.children, el.style, ctx)}
148
148
  </View>);
149
149
  }
150
150
  if (el.tag === 'table') {
151
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.table, vStyle]}>
152
- {renderTableChildren(el.children, ctx)}
151
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.table, vStyle]}>
152
+ {renderTableChildren(el.children, ctx)}
153
153
  </View>);
154
154
  }
155
155
  if (el.tag === 'thead' || el.tag === 'tbody' || el.tag === 'tfoot') {
156
- return (<View key={key} {...ctx.defaultViewProps}>
157
- {renderTableChildren(el.children, ctx)}
156
+ return (<View key={key} {...ctx.defaultViewProps}>
157
+ {renderTableChildren(el.children, ctx)}
158
158
  </View>);
159
159
  }
160
160
  if (el.tag === 'tr') {
@@ -164,14 +164,14 @@ function renderBlockDefault(el, key, ctx) {
164
164
  return renderTableCell(el, key, ctx);
165
165
  }
166
166
  if (el.tag === 'caption') {
167
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.tableCaption, vStyle]}>
168
- {renderBlockChildren(el.children, el.style, ctx)}
167
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.tableCaption, vStyle]}>
168
+ {renderBlockChildren(el.children, el.style, ctx)}
169
169
  </View>);
170
170
  }
171
171
  const a11yRole = blockA11yRole(el.tag);
172
172
  const a11yLevel = HEADING_LEVEL[el.tag];
173
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, vStyle]} accessibilityRole={a11yRole} {...(a11yLevel !== undefined ? { 'aria-level': a11yLevel } : {})}>
174
- {renderBlockChildren(el.children, el.style, ctx)}
173
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.block, vStyle]} accessibilityRole={a11yRole} {...(a11yLevel !== undefined ? { 'aria-level': a11yLevel } : {})}>
174
+ {renderBlockChildren(el.children, el.style, ctx)}
175
175
  </View>);
176
176
  }
177
177
  function blockA11yRole(tag) {
@@ -195,22 +195,22 @@ function renderTableRow(tr, key, ctx) {
195
195
  cells.push(c);
196
196
  }
197
197
  }
198
- return (<View key={key} {...ctx.defaultViewProps} style={styles.tr}>
199
- {cells.map((cell, i) => renderTableCell(cell, i, ctx))}
198
+ return (<View key={key} {...ctx.defaultViewProps} style={styles.tr}>
199
+ {cells.map((cell, i) => renderTableCell(cell, i, ctx))}
200
200
  </View>);
201
201
  }
202
202
  function renderTableCell(cell, key, ctx) {
203
203
  const { view: vStyle } = splitStyle(cell.style);
204
204
  const flex = cell.colSpan ?? 1;
205
- return (<View key={key} {...ctx.defaultViewProps} style={[styles.tableCell, { flex }, vStyle]}>
206
- {renderBlockChildren(cell.children, cell.style, ctx)}
205
+ return (<View key={key} {...ctx.defaultViewProps} style={[styles.tableCell, { flex }, vStyle]}>
206
+ {renderBlockChildren(cell.children, cell.style, ctx)}
207
207
  </View>);
208
208
  }
209
209
  function renderInline(node, key, ctx) {
210
210
  if (node.kind === 'text') {
211
211
  const { text: tStyle } = splitStyle(node.style);
212
- return (<Text key={key} {...ctx.defaultTextProps} style={tStyle}>
213
- {node.text}
212
+ return (<Text key={key} {...ctx.defaultTextProps} style={tStyle}>
213
+ {node.text}
214
214
  </Text>);
215
215
  }
216
216
  if (node.kind === 'image') {
@@ -225,8 +225,8 @@ function renderInlineElement(el, key, ctx) {
225
225
  renderersProps: ctx.renderersProps,
226
226
  contentWidth: ctx.contentWidth,
227
227
  };
228
- return (<React.Fragment key={key}>
229
- {custom(el, () => renderInlineDefault(el, undefined, ctx), info)}
228
+ return (<React.Fragment key={key}>
229
+ {custom(el, () => renderInlineDefault(el, undefined, ctx), info)}
230
230
  </React.Fragment>);
231
231
  }
232
232
  return renderInlineDefault(el, key, ctx);
@@ -244,12 +244,12 @@ function renderInlineDefault(el, key, ctx) {
244
244
  else {
245
245
  void Linking.openURL(href);
246
246
  }
247
- }}>
248
- {children}
247
+ }}>
248
+ {children}
249
249
  </Text>);
250
250
  }
251
- return (<Text key={key} {...ctx.defaultTextProps} style={tStyle}>
252
- {children}
251
+ return (<Text key={key} {...ctx.defaultTextProps} style={tStyle}>
252
+ {children}
253
253
  </Text>);
254
254
  }
255
255
  const styles = StyleSheet.create({
package/package.json CHANGED
@@ -1,74 +1,75 @@
1
- {
2
- "name": "@nikpnevmatikos/html-renderer",
3
- "version": "0.1.0-alpha.3",
4
- "description": "React Native HTML renderer in TypeScript — zero native modules, Fabric/Expo compatible. Supports tagsStyles, stylesheet with CSS selectors, custom renderers, and more.",
5
- "author": "NikPnevmatikos",
6
- "license": "MIT",
7
- "homepage": "https://github.com/NikPnevmatikos/Html-Renderer#readme",
8
- "bugs": {
9
- "url": "https://github.com/NikPnevmatikos/Html-Renderer/issues"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/NikPnevmatikos/Html-Renderer.git",
14
- "directory": "packages/core"
15
- },
16
- "main": "./dist/index.js",
17
- "module": "./dist/index.js",
18
- "types": "./dist/index.d.ts",
19
- "react-native": "./dist/index.js",
20
- "files": [
21
- "dist",
22
- "README.md",
23
- "LICENSE"
24
- ],
25
- "publishConfig": {
26
- "access": "public"
27
- },
28
- "engines": {
29
- "node": ">=20"
30
- },
31
- "sideEffects": false,
32
- "scripts": {
33
- "build": "tsc",
34
- "dev": "tsc --watch --preserveWatchOutput",
35
- "typecheck": "tsc --noEmit",
36
- "test": "jest",
37
- "clean": "rimraf dist"
38
- },
39
- "jest": {
40
- "preset": "ts-jest",
41
- "testEnvironment": "node",
42
- "testMatch": [
43
- "<rootDir>/src/**/*.test.ts"
44
- ]
45
- },
46
- "keywords": [
47
- "react-native",
48
- "html",
49
- "renderer",
50
- "expo",
51
- "fabric",
52
- "typescript",
53
- "stylesheet",
54
- "css",
55
- "react-native-web"
56
- ],
57
- "peerDependencies": {
58
- "react": ">=18",
59
- "react-native": ">=0.73"
60
- },
61
- "dependencies": {
62
- "domhandler": "^5.0.3",
63
- "htmlparser2": "^10.0.0"
64
- },
65
- "devDependencies": {
66
- "@types/jest": "^29.5.14",
67
- "@types/react": "~19.1.0",
68
- "jest": "^29.7.0",
69
- "react": "19.1.0",
70
- "react-native": "0.81.5",
71
- "ts-jest": "^29.4.9",
72
- "typescript": "~5.9.2"
73
- }
74
- }
1
+ {
2
+ "name": "@nikpnevmatikos/html-renderer",
3
+ "version": "0.1.0-alpha.4",
4
+ "description": "React Native HTML renderer in TypeScript — zero native modules, Fabric/Expo compatible. Supports tagsStyles, stylesheet with CSS selectors, custom renderers, and more.",
5
+ "author": "NikPnevmatikos",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/NikPnevmatikos/Html-Renderer#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/NikPnevmatikos/Html-Renderer/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/NikPnevmatikos/Html-Renderer.git",
14
+ "directory": "packages/core"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "react-native": "./dist/index.js",
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "sideEffects": false,
32
+ "scripts": {
33
+ "build": "tsc",
34
+ "dev": "tsc --watch --preserveWatchOutput",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "jest",
37
+ "clean": "rimraf dist"
38
+ },
39
+ "jest": {
40
+ "preset": "ts-jest",
41
+ "testEnvironment": "node",
42
+ "testMatch": [
43
+ "<rootDir>/src/**/*.test.ts"
44
+ ]
45
+ },
46
+ "keywords": [
47
+ "react-native",
48
+ "html",
49
+ "renderer",
50
+ "expo",
51
+ "fabric",
52
+ "typescript",
53
+ "stylesheet",
54
+ "css",
55
+ "react-native-web"
56
+ ],
57
+ "peerDependencies": {
58
+ "react": ">=18",
59
+ "react-native": ">=0.73"
60
+ },
61
+ "dependencies": {
62
+ "domhandler": "^5.0.3",
63
+ "entities": "^7.0.1",
64
+ "htmlparser2": "^10.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/jest": "^29.5.14",
68
+ "@types/react": "~19.1.0",
69
+ "jest": "^29.7.0",
70
+ "react": "19.1.0",
71
+ "react-native": "0.81.5",
72
+ "ts-jest": "^29.4.9",
73
+ "typescript": "~5.9.2"
74
+ }
75
+ }