Skip to content

Commit eabf3be

Browse files
committed
Handle HTML block elements in splitMarkdownBlocks
1 parent 327e252 commit eabf3be

2 files changed

Lines changed: 99 additions & 7 deletions

File tree

src/lib/services/contents/fields/rich-text/helper.js

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,41 @@ const hashString = (str) => {
5454
return hash.toString(16);
5555
};
5656

57+
/**
58+
* HTML void elements that cannot have children or a closing tag. Used to avoid incorrectly
59+
* entering HTML block mode when a void element starts a line.
60+
* @see https://developer.mozilla.org/en-US/docs/Glossary/Void_element
61+
*/
62+
const VOID_ELEMENTS = new Set([
63+
'area',
64+
'base',
65+
'br',
66+
'col',
67+
'embed',
68+
'hr',
69+
'img',
70+
'input',
71+
'link',
72+
'meta',
73+
'param',
74+
'source',
75+
'track',
76+
'wbr',
77+
]);
78+
79+
/**
80+
* Regex to detect the opening line of a fenced code block (backtick or tilde fence).
81+
*/
82+
const FENCE_OPEN_REGEX = /^[ ]{0,3}(`{3,}|~{3,})/;
83+
/**
84+
* Regex to detect a line that opens an HTML block element, capturing the tag name.
85+
*/
86+
const HTML_OPEN_TAG_REGEX = /^<([a-zA-Z][a-zA-Z0-9]*)(?:[\s>])/;
87+
5788
/**
5889
* Split a Markdown string into logical blocks at blank lines, keeping fenced code blocks (backtick
59-
* or tilde fences) intact even when they contain blank lines.
90+
* or tilde fences) and HTML block elements (e.g. `<div>`) intact even when they contain blank
91+
* lines.
6092
* @param {string} markdown The full Markdown string.
6193
* @returns {string[]} Array of non-empty block strings.
6294
*/
@@ -69,6 +101,8 @@ export const splitMarkdownBlocks = (markdown) => {
69101
const current = [];
70102
/** @type {{ char: string, length: number } | null} */
71103
let fence = null;
104+
/** @type {{ tag: string, depth: number } | null} */
105+
let htmlBlock = null;
72106

73107
markdown.split('\n').forEach((line) => {
74108
if (fence) {
@@ -84,18 +118,47 @@ export const splitMarkdownBlocks = (markdown) => {
84118
) {
85119
fence = null;
86120
}
121+
} else if (htmlBlock) {
122+
current.push(line);
123+
124+
const openRe = new RegExp(`<${htmlBlock.tag}(?:[\\s>])`, 'gi');
125+
const closeRe = new RegExp(`<\\/${htmlBlock.tag}>`, 'gi');
126+
127+
htmlBlock.depth += [...line.matchAll(openRe)].length - [...line.matchAll(closeRe)].length;
128+
129+
if (htmlBlock.depth <= 0) {
130+
htmlBlock = null;
131+
}
87132
} else {
88-
const fenceMatch = /^[ ]{0,3}(`{3,}|~{3,})/.exec(line);
133+
const fenceMatch = FENCE_OPEN_REGEX.exec(line);
89134

90135
if (fenceMatch) {
91136
current.push(line);
92137
fence = { char: fenceMatch[1][0], length: fenceMatch[1].length };
93-
} else if (line === '') {
94-
if (current.length) {
95-
blocks.push(current.splice(0).join('\n'));
96-
}
97138
} else {
98-
current.push(line);
139+
const htmlOpenMatch = HTML_OPEN_TAG_REGEX.exec(line);
140+
141+
if (htmlOpenMatch) {
142+
const tag = htmlOpenMatch[1].toLowerCase();
143+
144+
current.push(line);
145+
146+
if (!VOID_ELEMENTS.has(tag)) {
147+
const openRe = new RegExp(`<${tag}(?:[\\s>])`, 'gi');
148+
const closeRe = new RegExp(`<\\/${tag}>`, 'gi');
149+
const depth = [...line.matchAll(openRe)].length - [...line.matchAll(closeRe)].length;
150+
151+
if (depth > 0) {
152+
htmlBlock = { tag, depth };
153+
}
154+
}
155+
} else if (line === '') {
156+
if (current.length) {
157+
blocks.push(current.splice(0).join('\n'));
158+
}
159+
} else {
160+
current.push(line);
161+
}
99162
}
100163
}
101164
});

src/lib/services/contents/fields/rich-text/helper.test.js

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)