Skip to content

Commit bec4388

Browse files
committed
feat(og): add option to use class or data-tw attribute as tw prop when using html-to-react
[bump]
1 parent da38cf9 commit bec4388

File tree

2 files changed

+86
-36
lines changed

2 files changed

+86
-36
lines changed

.changeset/fruity-nights-retire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cf-wasm/og": patch
3+
---
4+
5+
feat: add option to use `class` or `data-tw` attribute as `tw` prop when using `html-to-react`

packages/og/src/html-to-react.ts

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,94 @@
1-
import { convertHtmlToReact, type ParserOptions } from '@hedgedoc/html-to-react';
2-
import { createElement, Fragment, type ReactElement } from 'react';
1+
import { convertHtmlToReact, type ParserOptions as HTMLToReactParserOptions } from '@hedgedoc/html-to-react';
2+
import { createElement, Fragment, isValidElement, type ReactElement, type ReactNode } from 'react';
33

4-
type ReactChild = ReactElement | string | null;
4+
export interface ParserOptions extends HTMLToReactParserOptions {
5+
tailwind?: boolean | 'data' | 'class';
6+
}
57

6-
function props(input: { children?: ReactChild | ReactChild[] }) {
7-
const props = { ...input };
8+
class Transformer {
9+
private options: ParserOptions & { tailwind: boolean | 'data' | 'class' };
810

9-
if ('children' in props) {
10-
const { children } = props;
11-
if (typeof children === 'undefined' || children === null) {
12-
delete props.children;
13-
} else if (typeof children === 'string') {
14-
props.children = children;
15-
} else if (Array.isArray(children)) {
16-
const filtered = children
17-
.filter((child) => typeof child === 'string' || Boolean(child))
18-
.map((child) => (typeof child === 'object' && child ? element(child) : child));
11+
constructor(options: ParserOptions = {}) {
12+
this.options = { tailwind: false, ...options };
13+
}
1914

20-
if (filtered.length === 0) {
15+
transform(html: string): ReactElement {
16+
return this.wrapper(convertHtmlToReact(html));
17+
}
18+
19+
wrapper(children: (ReactElement | string | null)[]): ReactElement {
20+
if (children.length === 1 && isValidElement(children[0])) {
21+
return this.element(children[0]);
22+
}
23+
24+
return this.fragment(children);
25+
}
26+
27+
element(element: ReactElement) {
28+
return createElement(element.type, typeof element.props === 'object' && element.props ? this.props(element.props) : {});
29+
}
30+
31+
fragment(children: ReactNode): ReactElement {
32+
const transformed = this.children(children);
33+
return createElement(Fragment, typeof transformed !== 'undefined' ? { children: transformed } : {});
34+
}
35+
36+
props(input: { children?: ReactNode; className?: unknown; tw?: unknown; 'data-tw'?: unknown }) {
37+
const props = { ...input };
38+
39+
if ('children' in props) {
40+
const transformed = this.children(props.children);
41+
if (typeof transformed === 'undefined') {
2142
delete props.children;
22-
} else if (filtered.length === 1 && typeof filtered[0] === 'string') {
23-
props.children = filtered[0];
2443
} else {
25-
props.children = filtered;
44+
props.children = transformed;
2645
}
2746
}
28-
}
2947

30-
return props;
31-
}
48+
if (this.options.tailwind === true || this.options.tailwind === 'class') {
49+
if ('className' in props && typeof props.className === 'string') {
50+
props.tw = props.className;
51+
}
52+
} else if (this.options.tailwind === 'data') {
53+
if ('data-tw' in props && typeof props['data-tw'] === 'string') {
54+
props.tw = props['data-tw'];
55+
}
56+
}
3257

33-
function element(element: ReactElement) {
34-
return createElement(element.type, typeof element.props === 'object' && element.props ? props(element.props) : {});
35-
}
58+
return props;
59+
}
3660

37-
function fragment(children: ReactChild[]): ReactElement {
38-
return createElement(Fragment, { children });
39-
}
61+
children(children: ReactNode): ReactNode {
62+
if (children === null || typeof children === 'undefined' || typeof children === 'boolean') {
63+
return undefined;
64+
}
65+
if (isValidElement(children)) {
66+
return this.element(children);
67+
}
68+
if (Array.isArray(children)) {
69+
const filtered: ReactNode[] = [];
4070

41-
function wrapper(children: ReactChild[]): ReactElement {
42-
if (children.length === 1 && typeof children[0] === 'object' && children[0]) {
43-
return element({ ...children[0], key: null });
44-
}
71+
for (const child of children) {
72+
if (child === null || typeof child === 'undefined' || typeof child === 'boolean') {
73+
continue;
74+
}
75+
if (isValidElement(child)) {
76+
filtered.push(this.element(child));
77+
} else {
78+
filtered.push(child);
79+
}
80+
}
4581

46-
return element(fragment(children));
82+
if (filtered.length === 0) {
83+
return undefined;
84+
}
85+
if (filtered.length === 1) {
86+
return filtered[0];
87+
}
88+
return filtered;
89+
}
90+
return children;
91+
}
4792
}
4893

4994
/**
@@ -54,15 +99,15 @@ function wrapper(children: ReactChild[]): ReactElement {
5499
*
55100
* @returns The {@link ReactElement}
56101
*/
57-
export const htmlToReact = (html: string, options?: ParserOptions): ReactElement => {
102+
export const htmlToReact = (html: string, options: ParserOptions = {}): ReactElement => {
58103
if (typeof html !== 'string') {
59104
throw new TypeError('Argument 1 must be of type string');
60105
}
61106
if (html.trim().length === 0) {
62107
throw new TypeError('Blank html string cannot be parsed');
63108
}
64109

65-
return wrapper(convertHtmlToReact(html, options));
110+
return new Transformer(options).transform(html);
66111
};
67112

68-
export { htmlToReact as t, type ParserOptions };
113+
export { htmlToReact as t };

0 commit comments

Comments
 (0)