1- // Taken from https://github.com/creeperyang/html-parser-lite
2- // and slightly modified. Original also under MIT license. Thanks.
3-
4- // attribute, like href="javascript:void(0)"
5- // 1. start with name (not empty and not =)
6- // 2. and then \s*=\s*
7- // 3. and value can be "value" | 'value' | value
8- // 4. 2 and 3 are optional
9- const attrRe = / ( [ ^ = \s ] + ) ( \s * = \s * ( ( " ( [ ^ " ] * ) " ) | ( ' ( [ ^ ' ] * ) ' ) | [ ^ > \s ] + ) ) ? / g
10- const endTagRe = / ^ < \/ ( [ ^ > \s ] + ) [ ^ > ] * > / m
11- // start tag, like <a href="link"> <img/>
12- // 1. must start with <tagName
13- // 2. optional attrbutes
14- // 3. /> or >
15- const startTagRe = / ^ < ( [ ^ > \s / ] + ) ( ( \s + [ ^ = > \s ] + ( \s * = \s * ( ( " [ ^ " ] * " ) | ( ' [ ^ ' ] * ' ) | [ ^ > \s ] + ) ) ? ) * ) \s * (?: \/ \s * ) ? > / m
16- const selfCloseTagRe = / \s * \/ \s * > \s * $ / m
1+ export interface HtmlParserScanner {
2+ characters : ( text : string ) => void
3+ comment : ( text : string ) => void
4+ startElement : ( tagName : string , attrs : Record < string , any > , isSelfColse : boolean , raw : string ) => void
5+ endElement : ( tagName : string ) => void
6+ }
177
188/**
199 * This is a simple html parser. Will read and parse html string.
2010 *
2111 * Original code by Erik Arvidsson, Mozilla Public License
2212 * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
2313 */
24- export class HtmlParser {
25- scanner : any
26- options : any
27- attrRe = attrRe
28- endTagRe = endTagRe
29- startTagRe = startTagRe
30- defaults = { ignoreWhitespaceText : false }
31-
32- constructor ( options : {
33- scanner ?: any
34- ignoreWhitespaceText ?: boolean
35- } = { } ) {
36- this . scanner = options . scanner
37- // Faster object merge for simple case
38- this . options = options . ignoreWhitespaceText !== undefined
39- ? options
40- : { ...this . defaults , ...options }
41- }
14+ export function createHtmlParser ( scanner : HtmlParserScanner ) {
15+ const attrRe = / ( [ ^ = \s ] + ) ( \s * = \s * ( ( " ( [ ^ " ] * ) " ) | ( ' ( [ ^ ' ] * ) ' ) | [ ^ > \s ] + ) ) ? / g
16+ const endTagRe = / ^ < \/ ( [ ^ > \s ] + ) [ ^ > ] * > / m
17+ const startTagRe = / ^ < ( [ ^ > \s / ] + ) ( ( \s + [ ^ = > \s ] + ( \s * = \s * ( ( " [ ^ " ] * " ) | ( ' [ ^ ' ] * ' ) | [ ^ > \s ] + ) ) ? ) * ) \s * (?: \/ \s * ) ? > / m
18+ const selfCloseTagRe = / \s * \/ \s * > \s * $ / m
4219
43- parse ( html : string ) {
20+ function parse ( html : string ) {
4421 let treatAsChars = false
4522 let index , match , characters
46- // Precompile regex for script/style end tags to avoid repeated creation
4723 let scriptEndRe : RegExp | null = null
4824 let styleEndRe : RegExp | null = null
4925
5026 while ( html . length ) {
51- treatAsChars = true // Set default early
27+ treatAsChars = true
5228
53- // comment - use startsWith for faster string comparison
5429 if ( html . startsWith ( '<!--' ) ) {
5530 index = html . indexOf ( '-->' )
5631 if ( index !== - 1 ) {
57- this . scanner . comment ( html . substring ( 4 , index ) )
32+ scanner . comment ( html . substring ( 4 , index ) )
5833 html = html . slice ( index + 3 )
5934 treatAsChars = false
6035 }
6136 }
62- // end tag - use startsWith and avoid deprecated RegExp globals
6337 else if ( html . startsWith ( '</' ) ) {
64- match = html . match ( this . endTagRe )
38+ match = html . match ( endTagRe )
6539 if ( match ) {
6640 html = html . slice ( match [ 0 ] . length )
6741 treatAsChars = false
68- this . parseEndTag ( match [ 0 ] , match [ 1 ] )
42+ scanner . endElement ( match [ 1 ] )
6943 }
7044 }
71- // start tag - avoid charAt for better performance
7245 else if ( html [ 0 ] === '<' ) {
73- match = html . match ( this . startTagRe )
46+ match = html . match ( startTagRe )
7447 if ( match ) {
7548 html = html . slice ( match [ 0 ] . length )
7649 treatAsChars = false
77- const tagName = this . parseStartTag ( match [ 0 ] , match [ 1 ] , match )
78- // Optimize script/style handling with precompiled regex
50+ const tagName = parseStartTag ( match [ 0 ] , match [ 1 ] , match )
7951 if ( tagName === 'script' ) {
8052 if ( ! scriptEndRe )
8153 scriptEndRe = / < \/ s c r i p t / i
8254 index = html . search ( scriptEndRe )
8355 if ( index !== - 1 ) {
84- this . scanner . characters ( html . slice ( 0 , index ) )
56+ scanner . characters ( html . slice ( 0 , index ) )
8557 html = html . slice ( index )
8658 treatAsChars = false
8759 }
@@ -91,7 +63,7 @@ export class HtmlParser {
9163 styleEndRe = / < \/ s t y l e / i
9264 index = html . search ( styleEndRe )
9365 if ( index !== - 1 ) {
94- this . scanner . characters ( html . slice ( 0 , index ) )
66+ scanner . characters ( html . slice ( 0 , index ) )
9567 html = html . slice ( index )
9668 treatAsChars = false
9769 }
@@ -103,9 +75,8 @@ export class HtmlParser {
10375 index = html . indexOf ( '<' )
10476
10577 if ( index === 0 ) {
106- // Skip the first '<' and find the next one
10778 index = html . indexOf ( '<' , 1 )
108- characters = html [ 0 ] // Just the '<' character
79+ characters = html [ 0 ]
10980 html = html . slice ( 1 )
11081 }
11182 else if ( index === - 1 ) {
@@ -117,57 +88,59 @@ export class HtmlParser {
11788 html = html . slice ( index )
11889 }
11990
120- // Faster whitespace check - avoid regex for empty strings
121- if ( characters && ( ! this . options . ignoreWhitespaceText || / [ ^ \s ] / . test ( characters ) ) )
122- this . scanner . characters ( characters )
91+ if ( characters )
92+ scanner . characters ( characters )
12393 }
12494
125- match = null // Clear match for next iteration
95+ match = null
12696 }
12797 }
12898
129- private parseStartTag ( input : string , tagName : string , match : any ) {
99+ function parseStartTag ( input : string , tagName : string , match : any ) {
130100 const isSelfColse = selfCloseTagRe . test ( input )
131101 let attrInput = match [ 2 ]
132102 if ( isSelfColse )
133103 attrInput = attrInput . replace ( / \s * \/ \s * $ / , '' )
134- const attrs = this . parseAttributes ( tagName , attrInput )
135- this . scanner . startElement ( tagName , attrs , isSelfColse , match [ 0 ] )
104+ const attrs = parseAttributes ( attrInput )
105+ scanner . startElement ( tagName , attrs , isSelfColse , match [ 0 ] )
136106 return tagName . toLocaleLowerCase ( )
137107 }
138108
139- private parseEndTag ( input : string , tagName : string ) {
140- this . scanner . endElement ( tagName )
141- }
142-
143- private parseAttributes ( tagName : string , input : string ) {
109+ function parseAttributes ( input : string ) {
144110 const attrs : Record < string , any > = { }
111+
145112 if ( ! input || ! input . trim ( ) )
146113 return attrs
147114
148- // Fast path for simple attributes without quotes
115+ // If there are no quotes in the input, split by whitespace and parse attributes simply
149116 if ( ! / [ " ' ] / . test ( input ) ) {
150117 const parts = input . trim ( ) . split ( / \s + / )
151118 for ( const part of parts ) {
152119 const eqIndex = part . indexOf ( '=' )
153120 if ( eqIndex === - 1 ) {
121+ // Attribute without value (boolean attribute)
154122 attrs [ part ] = true
155123 }
156124 else {
125+ // Attribute with value (unquoted)
157126 attrs [ part . slice ( 0 , eqIndex ) ] = part . slice ( eqIndex + 1 )
158127 }
159128 }
160129 return attrs
161130 }
162131
163- // Fallback to regex for complex attributes
164- this . attrRe . lastIndex = 0
132+ // Otherwise, use regex to extract attributes with quoted values
133+ attrRe . lastIndex = 0
165134 let match
166135 // eslint-disable-next-line no-cond-assign
167- while ( ( match = this . attrRe . exec ( input ) ) !== null ) {
136+ while ( ( match = attrRe . exec ( input ) ) !== null ) {
137+ // Destructure the match to get attribute name and value (quoted or unquoted)
168138 const [ , name , , value , , valueInQuote , , valueInSingleQuote ] = match
139+ // Prefer single-quoted, then double-quoted, then unquoted, then true (boolean attribute)
169140 attrs [ name ] = valueInSingleQuote ?? valueInQuote ?? value ?? true
170141 }
171142 return attrs
172143 }
144+
145+ return parse
173146}
0 commit comments