Skip to content

Commit 90024ea

Browse files
authored
feat: fix Tree component to render custom tag children (#12)
* feat: fix Tree component to render custom tag children The Tree component now properly renders child nodes for custom tags instead of only passing attributes. This enables custom tag handlers to receive and render nested content. Changes: - Render child nodes before passing to CustomTag component - Add block prop to CustomTag for access to block data - Use trim() for robust whitespace handling in innerBlocks detection - Remove outdated self-closing elements comment Supporting improvements: - Migrate to React 19 automatic JSX runtime - Update WordPress parser dependency to v5.38.0 - Add comprehensive block render test suite (30+ tests) - Remove redundant parser tests * ci: add GitHub Actions workflow and ESLint configuration - Add GitHub Actions CI workflow (.github/workflows/ci.yml) - Runs on Node 18.x and 20.x - Lints, builds, and tests on push and pull requests - Add ESLint flat config (eslint.config.js) - Configured for ES modules and JSX - Includes globals for test suite - Add npm scripts: lint and lint:fix - Add "type": "module" to package.json - Add ESLint dependencies: @eslint/js, eslint, eslint-plugin-react - Add eslint-disable comments for JSX dynamic components * ci: update to test only on Node 20.x * fix: configure Babel to output ESM and add file extensions - Update Babel preset-env to not transpile modules (modules: false) - Add .js file extensions to all import paths for ESM compatibility - Update build test script to run 'npm run build' before 'node --test' This allows the package to work properly as an ES module with explicit file extensions required by Node.js ESM resolution. All 30 block rendering tests now pass. * docs: add comprehensive README - Project overview and features - Installation instructions - Basic and advanced usage examples - Custom block and tag handlers - Complete API documentation - Supported core blocks list - Development guide (build, test, lint) - CI/CD information - Browser support and metadata * chore: publish via GitHub Packages * chore: add auto release notes
1 parent cc8d7bb commit 90024ea

File tree

25 files changed

+4333
-669
lines changed

25 files changed

+4333
-669
lines changed

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main, develop]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
node-version: [20.x]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Use Node.js ${{ matrix.node-version }}
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: ${{ matrix.node-version }}
24+
cache: 'npm'
25+
26+
- name: Install dependencies
27+
run: npm ci
28+
29+
- name: Lint
30+
run: npm run lint
31+
32+
- name: Build
33+
run: npm run build
34+
35+
- name: Test
36+
run: npm test

.github/workflows/release.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*.*.*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
packages: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Use Node.js 20.x
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 20.x
21+
registry-url: 'https://npm.pkg.github.com'
22+
scope: '@frontkom'
23+
cache: 'npm'
24+
25+
- name: Install dependencies
26+
run: npm ci
27+
28+
- name: Lint
29+
run: npm run lint
30+
31+
- name: Test
32+
run: npm test
33+
34+
- name: Publish to GitHub Packages
35+
env:
36+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37+
run: npm publish
38+
39+
- name: Create GitHub Release with changelog
40+
uses: actions/create-release@v1
41+
env:
42+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
with:
44+
tag_name: ${{ github.ref }}
45+
release_name: ${{ github.ref_name }}
46+
generate_release_notes: true

README.md

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# @frontkom/block-react-parser
2+
3+
A Gutenberg-generated HTML to React parser. Converts WordPress block editor (Gutenberg) markup into React components with full support for custom tag and block handlers.
4+
5+
## Features
6+
7+
- **Parses Gutenberg blocks** from WordPress HTML markup
8+
- **Custom handlers** for block types and HTML tags
9+
- **Dynamic component rendering** with proper prop passing
10+
- **React 19 support** with automatic JSX runtime
11+
- **Comprehensive test coverage** (30+ tests)
12+
- **ESLint validated** code quality
13+
- **CI/CD ready** with GitHub Actions
14+
15+
## Installation
16+
17+
Add GitHub Packages to your `.npmrc` (requires a GitHub token with `read:packages` for private repos, or the default `GITHUB_TOKEN` in CI):
18+
19+
```bash
20+
@frontkom:registry=https://npm.pkg.github.com
21+
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
22+
```
23+
24+
Then install:
25+
26+
```bash
27+
npm install @frontkom/block-react-parser
28+
```
29+
30+
## Usage
31+
32+
### Basic Usage
33+
34+
```jsx
35+
import { parseBlocks, Provider, customBlocks, customTags } from '@frontkom/block-react-parser';
36+
37+
function MyComponent({ html }) {
38+
const blocks = parseBlocks(html);
39+
40+
return (
41+
<Provider value={{ CustomBlocks: customBlocks(), CustomTags: customTags() }}>
42+
{blocks}
43+
</Provider>
44+
);
45+
}
46+
```
47+
48+
### Custom Block Handlers
49+
50+
Create custom handlers for specific Gutenberg blocks:
51+
52+
```jsx
53+
import { customBlocks, Provider } from '@frontkom/block-react-parser';
54+
55+
const CustomParagraph = ({ block }) => (
56+
<p className="custom-paragraph">
57+
{block.innerHTML}
58+
</p>
59+
);
60+
61+
const handlers = customBlocks({
62+
'core/paragraph': CustomParagraph
63+
});
64+
65+
function App({ html, blocks }) {
66+
return (
67+
<Provider value={{ CustomBlocks: handlers }}>
68+
{blocks}
69+
</Provider>
70+
);
71+
}
72+
```
73+
74+
### Custom Tag Handlers
75+
76+
Create custom handlers for specific HTML tags:
77+
78+
```jsx
79+
import { customTags, Provider } from '@frontkom/block-react-parser';
80+
81+
const CustomImage = ({ attribs, node }) => (
82+
<figure className="image-wrapper">
83+
<img {...attribs} />
84+
{node}
85+
</figure>
86+
);
87+
88+
const tags = customTags({
89+
img: CustomImage
90+
});
91+
92+
function App({ blocks }) {
93+
return (
94+
<Provider value={{ CustomTags: tags }}>
95+
{blocks}
96+
</Provider>
97+
);
98+
}
99+
```
100+
101+
## API
102+
103+
### `parseBlocks(html: string): JSX.Element[]`
104+
105+
Parses Gutenberg HTML markup and returns an array of React components.
106+
107+
```jsx
108+
const blocks = parseBlocks(gutenbergHtml);
109+
```
110+
111+
### `Block`
112+
113+
The main block rendering component. Used internally but can be imported for custom implementations.
114+
115+
```jsx
116+
import { Block } from '@frontkom/block-react-parser';
117+
118+
<Block block={blockObject} />
119+
```
120+
121+
### `Tree`
122+
123+
The tree rendering component for nested HTML structures. Used internally for rendering block content.
124+
125+
### `Provider`
126+
127+
React Context Provider for passing custom block and tag handlers.
128+
129+
```jsx
130+
<Provider value={{ CustomBlocks: handlers, CustomTags: tags }}>
131+
{blocks}
132+
</Provider>
133+
```
134+
135+
### `customBlocks(handlers?: Record<string, Component>): Record<string, Component>`
136+
137+
Helper function to create block handlers, merging with core block defaults.
138+
139+
```jsx
140+
const handlers = customBlocks({
141+
'core/image': MyImageComponent,
142+
'core/gallery': MyGalleryComponent
143+
});
144+
```
145+
146+
### `customTags(handlers?: Record<string, Component>): Record<string, Component>`
147+
148+
Helper function to create tag handlers, merging with core tag defaults (img, br, hr).
149+
150+
```jsx
151+
const tags = customTags({
152+
img: MyImageComponent,
153+
video: MyVideoComponent
154+
});
155+
```
156+
157+
### `attribsProps(attributes: Record<string, any>): Record<string, any>`
158+
159+
Utility function to convert HTML attributes object to React props.
160+
161+
### `innerNode(innerBlocks: Block[], innerContent: string[]): Node`
162+
163+
Utility function to construct a node tree from WordPress inner blocks and content.
164+
165+
## Supported Core Blocks
166+
167+
The parser includes default handlers for most core Gutenberg blocks:
168+
169+
- Image & Gallery
170+
- Paragraphs & Headings
171+
- Lists (ordered and unordered)
172+
- Quotes & Pullquotes
173+
- Code & Preformatted
174+
- Buttons
175+
- Separators & Spacers
176+
- Tables
177+
- Video & Audio
178+
- Groups & Columns
179+
- Cover & Embed
180+
- Media & Text
181+
182+
## Development
183+
184+
### Installation
185+
186+
```bash
187+
npm install
188+
```
189+
190+
### Build
191+
192+
```bash
193+
npm run build
194+
```
195+
196+
Compiles source files to `dist/` using Babel.
197+
198+
### Testing
199+
200+
```bash
201+
npm test
202+
```
203+
204+
Runs 30+ tests covering:
205+
- All major block types
206+
- Custom handler overrides
207+
- Edge cases and optional attributes
208+
- Proper component rendering
209+
210+
### Linting
211+
212+
```bash
213+
npm run lint # Check code quality
214+
npm run lint:fix # Auto-fix issues
215+
```
216+
217+
## CI/CD
218+
219+
This project uses GitHub Actions for continuous integration:
220+
221+
- **Linting**: ESLint validation on every commit
222+
- **Building**: Babel compilation verification
223+
- **Testing**: Full test suite on Node 20.x
224+
- **Triggers**: Push to main/develop and all pull requests
225+
226+
See [.github/workflows/ci.yml](.github/workflows/ci.yml) for workflow details.
227+
228+
## Browser Support
229+
230+
Targets > 0.25% market share and excludes dead browsers via browserslist.
231+
232+
## License
233+
234+
ISC
235+
236+
## Author
237+
238+
Roberto Ornelas <rob@frontkom.com>

dist/components/Block.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
"use strict";
1+
// eslint-disable-next-line no-unused-vars
2+
import Tree from "./Tree.js";
3+
import innerNode from "../utils/innerNode.js";
4+
import { useBlockComponent } from "./Context.js";
25

3-
Object.defineProperty(exports, "__esModule", {
4-
value: true
5-
});
6-
exports.default = Block;
7-
var _Tree = _interopRequireDefault(require("./Tree"));
8-
var _innerNode = _interopRequireDefault(require("../utils/innerNode"));
9-
var _Context = require("./Context");
10-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
116
/**
127
* Block element.
138
*
149
* @param {object} componentProps - properties that includes the block object.
1510
* @returns {JSX.Element | null | undefined}
1611
*/
17-
function Block(_ref) {
12+
import { jsx as _jsx } from "react/jsx-runtime";
13+
export default function Block(_ref) {
1814
let {
1915
block
2016
} = _ref;
@@ -23,9 +19,9 @@ function Block(_ref) {
2319
innerContent,
2420
innerBlocks
2521
} = block;
26-
const CustomBlock = (0, _Context.useBlockComponent)(blockName);
22+
const CustomBlock = useBlockComponent(blockName);
2723
if (CustomBlock) {
28-
return /*#__PURE__*/React.createElement(CustomBlock, {
24+
return /*#__PURE__*/_jsx(CustomBlock, {
2925
block: block
3026
});
3127
}
@@ -38,9 +34,9 @@ function Block(_ref) {
3834
if (innerContent.length === 1 && (innerContent[0] === "\n" || innerContent[0].substring(0, 2) === "</")) {
3935
return null;
4036
}
41-
const node = (0, _innerNode.default)(innerBlocks, innerContent);
37+
const node = innerNode(innerBlocks, innerContent);
4238
if (node) {
43-
return /*#__PURE__*/React.createElement(_Tree.default, {
39+
return /*#__PURE__*/_jsx(Tree, {
4440
node: node,
4541
block: block
4642
});

0 commit comments

Comments
 (0)