Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Docs

# Builds the Docusaurus site in website/ and publishes it to GitHub Pages.
# Pages must be enabled once in the repository settings with
# "Source: GitHub Actions" before the deploy job can succeed.

on:
push:
branches: [main]
paths:
- 'website/**'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'website/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: website/package-lock.json

- name: Install
run: npm ci
working-directory: website

- name: Build
run: npm run build
working-directory: website

- name: Upload Pages artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: website/build

deploy:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![CI](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
[![Open in Snack](https://img.shields.io/badge/Open%20in-Snack-4630EB?logo=expo&logoColor=white)](https://snack.expo.dev/?platform=web&sdkVersion=56.0.0&name=html-renderer%20example&dependencies=%40nikpnevmatikos%2Fhtml-renderer%2C%40nikpnevmatikos%2Fhtml-renderer-video%2C%40nikpnevmatikos%2Fhtml-renderer-video%2Fexpo%2Cexpo-video%2Cexpo-status-bar&files=%7B%22App.tsx%22%3A%7B%22type%22%3A%22CODE%22%2C%22url%22%3A%22https%3A%2F%2Fraw.githubusercontent.com%2FNikPnevmatikos%2FHtml-Renderer%2Fmain%2Fexample%2FApp.tsx%22%7D%7D)
[![Docs](https://img.shields.io/badge/docs-nikpnevmatikos.github.io-1a73e8)](https://nikpnevmatikos.github.io/Html-Renderer/)

A modern React Native HTML renderer, written in TypeScript with **zero native modules**. Built from scratch as a lightweight alternative to `react-native-render-html` and its successor `@native-html/render`.

Expand Down
1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![CI](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/NikPnevmatikos/Html-Renderer/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
[![Open in Snack](https://img.shields.io/badge/Open%20in-Snack-4630EB?logo=expo&logoColor=white)](https://snack.expo.dev/?platform=web&sdkVersion=56.0.0&name=html-renderer%20example&dependencies=%40nikpnevmatikos%2Fhtml-renderer%2C%40nikpnevmatikos%2Fhtml-renderer-video%2C%40nikpnevmatikos%2Fhtml-renderer-video%2Fexpo%2Cexpo-video%2Cexpo-status-bar&files=%7B%22App.tsx%22%3A%7B%22type%22%3A%22CODE%22%2C%22url%22%3A%22https%3A%2F%2Fraw.githubusercontent.com%2FNikPnevmatikos%2FHtml-Renderer%2Fmain%2Fexample%2FApp.tsx%22%7D%7D)
[![Docs](https://img.shields.io/badge/docs-nikpnevmatikos.github.io-1a73e8)](https://nikpnevmatikos.github.io/Html-Renderer/)

A modern React Native HTML renderer, written in TypeScript with **zero native modules**. Built from scratch as a lightweight alternative to `react-native-render-html` and its successor `@native-html/render`.

Expand Down
20 changes: 20 additions & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Dependencies
/node_modules

# Production
/build

# Generated files
.docusaurus
.cache-loader

# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
20 changes: 20 additions & 0 deletions website/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Documentation site

Docusaurus site for `@nikpnevmatikos/html-renderer`, published to
https://nikpnevmatikos.github.io/Html-Renderer/ by `.github/workflows/docs.yml`
on every push to `main` that touches `website/`.

This folder is **not** an npm workspace member. It has its own `package.json`
and lockfile so Docusaurus dependencies never mix with the React Native ones.

```bash
cd website
npm install
npm start # dev server with live reload at http://localhost:3000/Html-Renderer/
npm run build # static build into website/build
```

- Pages live in `docs/`. Sidebar order comes from each page's `sidebar_position`.
- The landing page is `src/pages/index.tsx`. It embeds the repository's
`example/App.tsx` in Expo Snack through `src/components/SnackEmbed.tsx`.
- Broken internal links and anchors fail the build on purpose.
121 changes: 121 additions & 0 deletions website/docs/custom-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
sidebar_position: 5
title: Custom rendering
description: Replace renderers, define your own tags, rewrite the DOM, handle links, size images and configure built-in renderers.
---

# Custom rendering

Every extension point uses the same small API surface. Plugins such as the [video plugin](./plugins.md) are built on exactly these props.

## Custom renderers

`customRenderers` replaces or wraps the renderer for a tag. The renderer receives the node and a `defaultRender` function, so wrapping the built-in output is one line:

```tsx
import { type CustomRenderer } from '@nikpnevmatikos/html-renderer';

const customRenderers: Record<string, CustomRenderer> = {
h1: (node, defaultRender) => (
<View style={{ borderBottomWidth: 2, borderBottomColor: 'blue' }}>
{defaultRender()}
</View>
),
};

<HtmlRenderer html={html} customRenderers={customRenderers} />;
```

Custom renderers are plain function calls, not components. If your renderer needs hooks, return a real component element from it and put the hooks inside that component.

## Custom HTML element models

`customHTMLElementModels` defines tags of your own that behave like real HTML: block or inline display, default styles, and void elements that ignore their children.

```tsx
import { type HTMLElementModel } from '@nikpnevmatikos/html-renderer';

const customHTMLElementModels: Record<string, HTMLElementModel> = {
'my-card': {
display: 'block',
tagDefaultStyle: { backgroundColor: '#eef', padding: 12 },
},
'x-spacer': {
display: 'block',
isVoid: true,
tagDefaultStyle: { height: 20 },
},
};

<HtmlRenderer
html="<my-card>hello</my-card>"
customHTMLElementModels={customHTMLElementModels}
/>;
```

## DOM transform hook

`transformDom` runs after parsing and before the render tree is built. It receives the DOM as an array of nodes and returns the DOM to render. Use it for sanitization, tag rewrites or content substitution:

```tsx
import { type TransformDom, type DomNode } from '@nikpnevmatikos/html-renderer';

const transformDom: TransformDom = (dom) => rewriteText(dom);

function rewriteText(nodes: DomNode[]): DomNode[] {
return nodes.map((n) => {
if (n.type === 'text') {
return { ...n, data: n.data.replace(/REPLACE_ME/g, 'replaced-by-hook') };
}
return { ...n, children: rewriteText(n.children) };
});
}

<HtmlRenderer html={html} transformDom={transformDom} />;
```

## Link handling

By default links open with `Linking.openURL`. `onLinkPress` receives the `href` and the element's attributes, so you can route in-app instead:

```tsx
import { type OnLinkPress } from '@nikpnevmatikos/html-renderer';

const onLinkPress: OnLinkPress = (href, attribs) => {
if (attribs.target === '_blank') {
void Linking.openURL(href);
} else {
navigation.navigate('InAppBrowser', { url: href });
}
};

<HtmlRenderer html={html} onLinkPress={onLinkPress} />;
```

## Auto-fit images

Pass `contentWidth` and images wider than it scale down proportionally:

```tsx
import { Dimensions } from 'react-native';

const contentWidth = Dimensions.get('window').width - 32;

<HtmlRenderer html={html} contentWidth={contentWidth} />;
```

## Renderer props

`renderersProps` passes configuration to individual renderers, built-in or custom:

```tsx
<HtmlRenderer
html={html}
renderersProps={{
ol: { startIndex: 5, markerTextStyle: { color: '#888' } },
ul: { markerTextStyle: { color: 'red' } },
img: { initialDimensions: { width: 300, height: 200 } },
details: { initialOpen: true },
}}
/>
```
60 changes: 60 additions & 0 deletions website/docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
sidebar_position: 1
title: Getting started
description: Install @nikpnevmatikos/html-renderer and render your first HTML in React Native.
---

# Getting started

`@nikpnevmatikos/html-renderer` renders HTML in React Native. It is written in TypeScript, ships **zero native modules**, and gives you real CSS: a `stylesheet` prop that accepts selectors and resolves them with proper specificity.

## Why this renderer

- **Zero native code.** Works on iOS, Android and web (via `react-native-web`), and in Expo Go without a dev build.
- **New Architecture compatible** out of the box. There is nothing native to migrate.
- **Real CSS stylesheets.** Type, class, id, descendant and child selectors, with specificity and source order per the spec.
- **A transient render tree.** HTML is parsed to a DOM, resolved into a styled render tree, and emitted as `<Text>`, `<View>` and `<Image>` elements.
- **Full style inheritance and cascade**, plus the box-model basics.
- **Extensible.** Custom renderers, custom element models, DOM transform hooks and per-renderer config. Plugin packages use the same API.
- **Entity-encoded input handled.** HTML that arrives as `&lt;p&gt;hello&lt;/p&gt;` from a CMS or API, even double-encoded, is detected and rendered as HTML.
- **Typed end-to-end** and covered by 130+ unit tests.

## Install

```bash
npm install @nikpnevmatikos/html-renderer
```

Peer dependencies: `react >= 18` and `react-native >= 0.73`.

## Quick start

```tsx
import { HtmlRenderer } from '@nikpnevmatikos/html-renderer';

export default function Screen() {
return (
<HtmlRenderer
html={`<h1>Hello</h1><p>This is <strong>bold</strong> and <a href="https://x.dev">a link</a>.</p>`}
/>
);
}
```

That is the whole integration. Everything else on this site is optional: styling, custom rendering, plugins.

## Try it without installing

The repository's example app runs in Expo Snack. Open it, edit the HTML or the stylesheet, and watch the preview update:

[Open the example in Snack](https://snack.expo.dev/?platform=web&sdkVersion=56.0.0&name=html-renderer%20example&dependencies=%40nikpnevmatikos%2Fhtml-renderer%2C%40nikpnevmatikos%2Fhtml-renderer-video%2C%40nikpnevmatikos%2Fhtml-renderer-video%2Fexpo%2Cexpo-video%2Cexpo-status-bar&files=%7B%22App.tsx%22%3A%7B%22type%22%3A%22CODE%22%2C%22url%22%3A%22https%3A%2F%2Fraw.githubusercontent.com%2FNikPnevmatikos%2FHtml-Renderer%2Fmain%2Fexample%2FApp.tsx%22%7D%7D)

Snack must be on Expo SDK 56 or newer for the `<video>` section, because older Snack runtimes do not ship `expo-video`.

## Where next

- [Supported HTML](./supported-html.md) for the tag list.
- [Styling and cascade](./styling.md) for the `stylesheet` prop, the style maps and the cascade order.
- [Props](./props.md) for the full component API.
- [Custom rendering](./custom-rendering.md) to replace renderers, define tags and rewrite the DOM.
- [Plugins](./plugins.md) for `<video>` support and what is planned.
42 changes: 42 additions & 0 deletions website/docs/how-it-works.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
sidebar_position: 7
title: How it works
description: The parse, build and render pipeline, and where styles are resolved.
---

# How it works

```text
HTML string
└─ parseHtml (htmlparser2) → DOM tree
└─ transformDom? (optional) → DOM tree
└─ buildRenderTree → Render tree
└─ resolveStyles (full cascade per element)
└─ hoistBlocks (fragment inline-wrapping-block)
└─ collapseWhitespace (CSS whitespace rules)
└─ Renderer → <View> / <Text> / <Image>
```

Styles resolve at build time into a single `ResolvedStyle` per element. At render time, `splitStyle` partitions each style into the half that applies to `<View>` and the half that applies to `<Text>`, and applies them to the correct component.

The render tree is transient: it is built for the given `html` and props, rendered, and discarded. There is no persistent DOM to keep in sync.

## Working on the repository

The repository is an npm workspaces monorepo: `packages/core` is the renderer, `packages/video` is the video plugin, `example` is an Expo app used for manual testing, and `website` is this site.

```bash
npm install # installs all workspace deps
npm run dev # tsc --watch on core
npm test # jest, 130+ tests
npm run typecheck # tsc --noEmit on core
npm run build # build core to dist

# live example app
cd example && npm start

# this documentation site
cd website && npm install && npm start
```

Bug reports and pull requests are welcome on [GitHub](https://github.com/NikPnevmatikos/Html-Renderer). See the repository's `CONTRIBUTING.md` for the workflow.
18 changes: 18 additions & 0 deletions website/docs/limitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
sidebar_position: 8
title: Limitations
description: What the renderer does not do yet, and what is on the roadmap.
---

# Limitations

Actively on the roadmap:

- **No `rowspan` on tables.** `colspan` works.
- **CSS units beyond `px`.** `em` and `rem` are not resolved yet. `%` works only on `width` and `height`.
- **Forms.** `<input>`, `<textarea>`, `<button>` and `<select>` are not rendered yet. Planned for the core, pure JS via React Native's `TextInput` and `Pressable`.
- **Stylesheet features.** Pseudo-classes such as `:first-child` and `:nth-child`, attribute selectors such as `[type="text"]`, and `@media` queries are not supported yet.
- **Advanced CSS.** Transforms, and flex or grid layout of HTML content.
- **Table column sizing.** Cell `width` attributes are honored: a row where every cell has a percent width distributes columns proportionally, and sized cells in a mixed row keep their width while the rest share the remainder. Columns without any width render equal-width, and `width` on `<col>` is ignored.

Progress is tracked in the repository's [changelog](https://github.com/NikPnevmatikos/Html-Renderer/blob/main/CHANGELOG.md) and [issues](https://github.com/NikPnevmatikos/Html-Renderer/issues).
Loading
Loading