@@ -18,16 +18,31 @@ export default class Parser {
1818 */
1919 caseSensitive : boolean ;
2020
21- constructor ( supportedTagNames : string [ ] , caseSensitive ?: boolean ) {
21+ /**
22+ * Is more lenient about closing tags and mismatched tags. Instead of throwing an error, it will turn the entire node
23+ * into a {@link TextNode} with the text of the entire node.
24+ */
25+ lenient : boolean ;
26+
27+ constructor (
28+ supportedTagNames : string [ ] ,
29+ caseSensitive ?: boolean ,
30+ lenient ?: boolean
31+ ) {
2232 this . supportedTagNames = supportedTagNames ;
2333 this . caseSensitive = caseSensitive ?? false ;
34+ this . lenient = lenient ?? false ;
2435 if ( ! this . caseSensitive ) {
2536 this . supportedTagNames = this . supportedTagNames . map ( ( tag ) =>
2637 tag . toLowerCase ( )
2738 ) ;
2839 }
2940 }
3041
42+ private getNameRespectingSensitivity ( name : string ) : string {
43+ return this . caseSensitive ? name : name . toLowerCase ( ) ;
44+ }
45+
3146 /**
3247 * Convert a chunk of BBCode to a {@link RootNode}.
3348 * @param text The chunk of BBCode to convert.
@@ -115,11 +130,9 @@ export default class Parser {
115130 // First, we determine if it is a valid tag name.
116131 if (
117132 this . supportedTagNames . includes (
118- this . caseSensitive
119- ? currentTagName
120- : currentTagName . toLowerCase ( )
133+ this . getNameRespectingSensitivity ( currentTagName )
121134 ) &&
122- ( ! buildingCode || currentTagName === "code" )
135+ ( ! buildingCode || currentTagName . toLowerCase ( ) === "code" )
123136 ) {
124137 // The tag name is valid.
125138 if ( nextCharacter === "]" ) {
@@ -133,7 +146,7 @@ export default class Parser {
133146 } else if ( buildingClosingTag ) {
134147 // We're making the closing tag. Now that we've completed, we want to remove the last element from the stack and add it to the children of the element prior.
135148 let lastElement = currentStack . pop ( ) ! ;
136- if ( currentTagName === "list" ) {
149+ if ( currentTagName . toLowerCase ( ) === "list" ) {
137150 // List tag. If the last element is a list item, we need to add it to the previous element.
138151 if ( lastElement . name === "*" ) {
139152 const previousElement = currentStack . pop ( ) ! ;
@@ -142,21 +155,48 @@ export default class Parser {
142155 }
143156 }
144157
145- if ( lastElement . name !== currentTagName ) {
146- throw new Error (
147- `Expected closing tag for '${ currentTagName } ', found '${ lastElement . name } '.`
148- ) ;
149- } else {
150- currentStack [ currentStack . length - 1 ] . addChild ( lastElement ) ;
151- buildingText = true ;
152- buildingClosingTag = false ;
153- buildingTagName = false ;
154- if ( currentTagName === "code" ) {
155- buildingCode = false ;
158+ if (
159+ this . getNameRespectingSensitivity ( lastElement . name ) !==
160+ this . getNameRespectingSensitivity ( currentTagName )
161+ ) {
162+ if ( ! this . lenient ) {
163+ throw new Error (
164+ `Expected closing tag for '${ currentTagName } ', found '${ lastElement . name } '.`
165+ ) ;
166+ } else {
167+ // Let's just put the last element back in the stack so that we know how to chain it.
168+ currentStack . push ( lastElement ) ;
169+ // We could have multiple misplaced tags, so we need to go through the entire stack in reverse order until we find the matching node.
170+ for ( let i = currentStack . length - 1 ; i >= 0 ; i -- ) {
171+ if (
172+ this . getNameRespectingSensitivity (
173+ currentStack [ i ] . name
174+ ) ===
175+ this . getNameRespectingSensitivity ( currentTagName )
176+ ) {
177+ lastElement = currentStack . pop ( ) ! ;
178+ break ;
179+ } else {
180+ const node = currentStack . pop ( ) ! ;
181+ let nodeText = ( node as Node ) . makeOpeningTag ( ) ;
182+ node . children . forEach ( ( child ) => {
183+ nodeText += child . toString ( ) ;
184+ } ) ;
185+ currentStack [ i - 1 ] . addChild ( new TextNode ( nodeText ) ) ;
186+ }
187+ }
156188 }
189+ }
157190
158- currentTagName = "" ;
191+ currentStack [ currentStack . length - 1 ] . addChild ( lastElement ) ;
192+ buildingText = true ;
193+ buildingClosingTag = false ;
194+ buildingTagName = false ;
195+ if ( currentTagName . toLowerCase ( ) === "code" ) {
196+ buildingCode = false ;
159197 }
198+
199+ currentTagName = "" ;
160200 } else {
161201 // Simple tag, there are no attributes or values. We push a tag to the stack and continue.
162202 const currentTag = new Node ( { name : currentTagName } ) ;
@@ -296,13 +336,24 @@ export default class Parser {
296336
297337 if ( currentStack . length > 1 ) {
298338 // We didn't close all tags.
299- throw new Error (
300- `Expected all tags to be closed. Found ${
301- currentStack . length - 1
302- } unclosed tags, most recently unclosed tag is "${
303- currentStack [ currentStack . length - 1 ] . name
304- } ".`
305- ) ;
339+ if ( ! this . lenient ) {
340+ throw new Error (
341+ `Expected all tags to be closed. Found ${
342+ currentStack . length - 1
343+ } unclosed tags, most recently unclosed tag is "${
344+ currentStack [ currentStack . length - 1 ] . name
345+ } ".`
346+ ) ;
347+ } else {
348+ for ( let i = currentStack . length - 1 ; i >= 1 ; i -- ) {
349+ const node = currentStack . pop ( ) ! ;
350+ let nodeText = ( node as Node ) . makeOpeningTag ( ) ;
351+ node . children . forEach ( ( child ) => {
352+ nodeText += child . toString ( ) ;
353+ } ) ;
354+ currentStack [ i - 1 ] . addChild ( new TextNode ( nodeText ) ) ;
355+ }
356+ }
306357 }
307358
308359 return rootNode ;
0 commit comments