@@ -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 - z A - Z ] [ a - z A - Z 0 - 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 } ) ;
0 commit comments