Skip to content

Commit 8b9b373

Browse files
committed
withEmptyBlockPlaceholder
1 parent 2b31a85 commit 8b9b373

4 files changed

Lines changed: 47 additions & 6 deletions

File tree

demo/react-source-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ export const serializeReactNode = (node: React.ReactNode, depth = 0): string =>
4949

5050
const openTag = `<${tag}${props.length ? ` ${props.join(' ')}` : ''}>`;
5151
const closeTag = `</${tag}>`;
52+
const selfClosingTag = `<${tag}${props.length ? ` ${props.join(' ')}` : ''} />`;
5253
const children = serializeReactNode(node.props?.children, depth + 1);
5354

5455
if (!children) {
55-
return `${' '.repeat(depth)}${openTag}${closeTag}`;
56+
return `${' '.repeat(depth)}${selfClosingTag}`;
5657
}
5758

5859
return `${' '.repeat(depth)}${openTag}\n${children}\n${' '.repeat(depth)}${closeTag}`;

src/renderers/react/__tests__/blocks.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ describe('ReactRenderer – blocks', () => {
2222
expect(html).toBe(`<div><h${level}>Heading ${level}</h${level}></div>`);
2323
}
2424
});
25+
26+
it('should render empty header with <br/>', () => {
27+
const html = renderDelta(d({ insert: '\n', attributes: { header: 2 } }));
28+
expect(html).toBe('<div><h2><br/></h2></div>');
29+
});
2530
});
2631

2732
describe('blockquotes', () => {
@@ -31,6 +36,11 @@ describe('ReactRenderer – blocks', () => {
3136
);
3237
expect(html).toBe('<div><blockquote>A quote</blockquote></div>');
3338
});
39+
40+
it('should render empty blockquote with <br/>', () => {
41+
const html = renderDelta(d({ insert: '\n', attributes: { blockquote: true } }));
42+
expect(html).toBe('<div><blockquote><br/></blockquote></div>');
43+
});
3444
});
3545

3646
describe('code blocks', () => {
@@ -63,6 +73,11 @@ describe('ReactRenderer – blocks', () => {
6373
'<div><pre><code class="ql-syntax language-javascript">const x = 1;</code></pre></div>',
6474
);
6575
});
76+
77+
it('should render an empty code block container', () => {
78+
const html = renderDelta(d({ insert: '\n', attributes: { 'code-block': true } }));
79+
expect(html).toBe('<div><pre><code class="ql-syntax"></code></pre></div>');
80+
});
6681
});
6782

6883
describe('paragraphs', () => {
@@ -77,5 +92,10 @@ describe('ReactRenderer – blocks', () => {
7792
);
7893
expect(html).toBe('<div><p>First paragraph</p><p>Second paragraph</p></div>');
7994
});
95+
96+
it('should render empty paragraph with <br/>', () => {
97+
const html = renderDelta(d({ insert: '\n' }));
98+
expect(html).toBe('<div><p><br/></p></div>');
99+
});
80100
});
81101
});

src/renderers/react/__tests__/lists.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ describe('ReactRenderer – lists', () => {
3939
);
4040
});
4141

42+
it('should render empty checklist item with <br/>', () => {
43+
const html = renderDelta(d({ insert: '\n', attributes: { list: 'unchecked' } }));
44+
expect(html).toBe('<div><ul><li data-checked="false"><br/></li></ul></div>');
45+
});
46+
4247
it('should render nested bullet lists', () => {
4348
const html = renderDelta(
4449
d(

src/renderers/react/functions/build-renderer-config.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ function sanitizeUrl(url: string, cfg: ResolvedReactConfig): string | undefined
102102
return url;
103103
}
104104

105+
/**
106+
* React equivalent of HTML renderers' `children || '<br/>'` behavior.
107+
* Keeps empty block lines visible and structurally consistent.
108+
*/
109+
function withEmptyBlockPlaceholder(children: ReactNode): ReactNode {
110+
if (children === null || children === undefined || children === false || children === '') {
111+
return createElement('br');
112+
}
113+
return children;
114+
}
115+
105116
// ─── Node Override Helpers ─────────────────────────────────────────────────
106117

107118
function renderCodeBlockContainer(node: TNode, cfg: ResolvedReactConfig): ReactNode {
@@ -148,18 +159,18 @@ export function buildRendererConfig(
148159
blocks: {
149160
paragraph: withCustomComponent(cfg, 'paragraph', (node, children) => {
150161
const tag = resolveTag(cfg, 'paragraph', node, 'p');
151-
return createElement(tag, null, children || null);
162+
return createElement(tag, null, withEmptyBlockPlaceholder(children));
152163
}),
153164

154165
header: withCustomComponent(cfg, 'header', (node, children) => {
155166
const level = getHeaderLevel(node);
156167
const tag = resolveTag(cfg, 'header', node, `h${level}`);
157-
return createElement(tag, null, children || null);
168+
return createElement(tag, null, withEmptyBlockPlaceholder(children));
158169
}),
159170

160171
blockquote: withCustomComponent(cfg, 'blockquote', (node, children) => {
161172
const tag = resolveTag(cfg, 'blockquote', node, 'blockquote');
162-
return createElement(tag, null, children || null);
173+
return createElement(tag, null, withEmptyBlockPlaceholder(children));
163174
}),
164175

165176
'code-block': withCustomComponent(cfg, 'code-block', {
@@ -168,7 +179,7 @@ export function buildRendererConfig(
168179
const props: Record<string, unknown> = { className: meta.className };
169180
if (meta.language) props['data-language'] = meta.language;
170181
const tag = resolveTag(cfg, 'code-block', node, 'pre');
171-
return createElement(tag, props, children || null);
182+
return createElement(tag, props, withEmptyBlockPlaceholder(children));
172183
},
173184
toProps: (meta) => {
174185
const props: Record<string, unknown> = { className: meta.className };
@@ -183,7 +194,11 @@ export function buildRendererConfig(
183194
const props: Record<string, unknown> = {};
184195
if (checked !== undefined) props['data-checked'] = checked;
185196
const tag = resolveTag(cfg, 'list-item', node, 'li');
186-
return createElement(tag, Object.keys(props).length > 0 ? props : null, children || null);
197+
return createElement(
198+
tag,
199+
Object.keys(props).length > 0 ? props : null,
200+
withEmptyBlockPlaceholder(children),
201+
);
187202
},
188203
toProps: (checked) => {
189204
if (checked !== undefined) return { 'data-checked': checked };

0 commit comments

Comments
 (0)