@@ -421,58 +421,26 @@ export default class ExpensiMark {
421
421
// block quotes naturally appear on their own line. Blockquotes should not appear in code fences or
422
422
// inline code blocks. A single prepending space should be stripped if it exists
423
423
process : ( textToProcess , replacement , shouldKeepRawInput = false ) => {
424
- const regex = / ^ (?: & g t ; ) + + (? ! ) (? ! [ ^ < ] * (?: < \/ p r e > | < \/ c o d e > | < \/ v i d e o > ) ) ( [ ^ \v \n \r ] + ) / gm;
424
+ const regex = / ^ (?: & g t ; ) + + (? ! ) (? ! [ ^ < ] * (?: < \/ p r e > | < \/ c o d e > | < \/ v i d e o > ) ) ( [ ^ \v \n \r ] * ) / gm;
425
+
426
+ let replacedText = this . replaceTextWithExtras ( textToProcess , regex , EXTRAS_DEFAULT , replacement ) ;
425
427
if ( shouldKeepRawInput ) {
426
- const rawInputRegex = / ^ (?: & g t ; ) + + (? ! ) (? ! [ ^ < ] * (?: < \/ p r e > | < \/ c o d e > | < \/ v i d e o > ) ) ( [ ^ \v \n \r ] * ) / gm;
427
- return this . replaceTextWithExtras ( textToProcess , rawInputRegex , EXTRAS_DEFAULT , replacement ) ;
428
+ return replacedText ;
429
+ }
430
+
431
+ for ( let i = this . maxQuoteDepth ; i > 0 ; i -- ) {
432
+ replacedText = replacedText . replaceAll ( `${ '</blockquote>' . repeat ( i ) } \n${ '<blockquote>' . repeat ( i ) } ` , '\n' ) ;
428
433
}
429
- return this . modifyTextForQuote ( regex , textToProcess , replacement as ReplacementFn ) ;
434
+ replacedText = replacedText . replaceAll ( '</blockquote>\n' , '</blockquote>' ) ;
435
+ return replacedText ;
430
436
} ,
431
437
replacement : ( _extras , g1 ) => {
432
- // We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading".
433
- // To do this we need to parse body of the quote without first space
434
- const handleMatch = ( match : string ) => match ;
435
- const textToReplace = g1 . replace ( / ^ & g t ; ( ) ? / gm, handleMatch ) ;
436
- const filterRules = [ 'heading1' ] ;
437
-
438
- // if we don't reach the max quote depth we allow the recursive call to process possible quote
439
- if ( this . currentQuoteDepth < this . maxQuoteDepth - 1 ) {
440
- filterRules . push ( 'quote' ) ;
441
- this . currentQuoteDepth ++ ;
442
- }
443
-
444
- const replacedText = this . replace ( textToReplace , {
445
- filterRules,
446
- shouldEscapeText : false ,
447
- shouldKeepRawInput : false ,
448
- } ) ;
449
- this . currentQuoteDepth = 0 ;
450
- return `<blockquote>${ replacedText } </blockquote>` ;
438
+ const { replacedText} = this . replaceQuoteText ( g1 , false ) ;
439
+ return `<blockquote>${ replacedText || ' ' } </blockquote>` ;
451
440
} ,
452
441
rawInputReplacement : ( _extras , g1 ) => {
453
- // We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading".
454
- // To do this we need to parse body of the quote without first space
455
- let isStartingWithSpace = false ;
456
- const handleMatch = ( _match : string , g2 : string ) => {
457
- isStartingWithSpace = ! ! g2 ;
458
- return '' ;
459
- } ;
460
- const textToReplace = g1 . replace ( / ^ & g t ; ( ) ? / gm, handleMatch ) ;
461
- const filterRules = [ 'heading1' ] ;
462
-
463
- // if we don't reach the max quote depth we allow the recursive call to process possible quote
464
- if ( this . currentQuoteDepth < this . maxQuoteDepth - 1 || isStartingWithSpace ) {
465
- filterRules . push ( 'quote' ) ;
466
- this . currentQuoteDepth ++ ;
467
- }
468
-
469
- const replacedText = this . replace ( textToReplace , {
470
- filterRules,
471
- shouldEscapeText : false ,
472
- shouldKeepRawInput : true ,
473
- } ) ;
474
- this . currentQuoteDepth = 0 ;
475
- return `<blockquote>${ isStartingWithSpace ? ' ' : '' } ${ replacedText } </blockquote>` ;
442
+ const { replacedText, shouldAddSpace} = this . replaceQuoteText ( g1 , true ) ;
443
+ return `<blockquote>${ shouldAddSpace ? ' ' : '' } ${ replacedText } </blockquote>` ;
476
444
} ,
477
445
} ,
478
446
/**
@@ -1129,8 +1097,9 @@ export default class ExpensiMark {
1129
1097
return ;
1130
1098
}
1131
1099
1100
+ const nextItem = splitText ?. [ index + 1 ] ;
1132
1101
// Insert '\n' unless it ends with '\n' or '>' or it's the last element, or if it's a header ('# ') with a space.
1133
- if ( text . match ( / [ \n | > ] [ > ] ? [ \s ] ? $ / ) || index === splitText . length - 1 || text === '# ' ) {
1102
+ if ( ( nextItem && text . match ( / > [ \s ] ? $ / ) && ! nextItem . startsWith ( '> ' ) ) || text . match ( / \n [ \s ] ? $ / ) || index === splitText . length - 1 || text === '# ' ) {
1134
1103
joinedText += text ;
1135
1104
} else {
1136
1105
joinedText += `${ text } \n` ;
@@ -1142,6 +1111,65 @@ export default class ExpensiMark {
1142
1111
return joinedText ;
1143
1112
}
1144
1113
1114
+ /**
1115
+ * Unpacks nested quote HTML tags that have been packed by the 'quote' rule in this.rules for shouldKeepRawInput = false
1116
+ *
1117
+ * For example, it parses the following HTML:
1118
+ * <blockquote>
1119
+ * quote 1
1120
+ * <blockquote>
1121
+ * quote 2
1122
+ * </blockquote>
1123
+ * quote 3
1124
+ * </blockquote>
1125
+ *
1126
+ * into:
1127
+ * <blockquote> quote 1</blockquote>
1128
+ * <blockquote><blockquote> quote 2</blockquote>
1129
+ * <blockquote> quote 3</blockquote>
1130
+ *
1131
+ * Note that there will always be only a single closing tag, even if multiple opening tags exist.
1132
+ * Only one closing tag is needed to detect if a nested quote has ended.
1133
+ */
1134
+ unpackNestedQuotes ( text : string ) : string {
1135
+ let parsedText = text . replace ( / ( ( < \/ b l o c k q u o t e > ) + ( < b r \/ > ) ? ) | ( < b r \/ > ) / g, ( match ) => {
1136
+ return `${ match } </split>` ;
1137
+ } ) ;
1138
+ const splittedText = parsedText . split ( '</split>' ) ;
1139
+ if ( splittedText . length > 0 && splittedText [ splittedText . length - 1 ] === '' ) {
1140
+ splittedText . pop ( ) ;
1141
+ }
1142
+
1143
+ let count = 0 ;
1144
+ parsedText = splittedText
1145
+ . map ( ( line ) => {
1146
+ const hasBR = line . endsWith ( '<br />' ) ;
1147
+ if ( line === '' && count === 0 ) {
1148
+ return '' ;
1149
+ }
1150
+
1151
+ const textLine = line . replace ( / ( < b r \/ > ) $ / g, '' ) ;
1152
+ if ( textLine . startsWith ( '<blockquote>' ) ) {
1153
+ count += ( textLine . match ( / < b l o c k q u o t e > / g) || [ ] ) . length ;
1154
+ }
1155
+ if ( textLine . endsWith ( '</blockquote>' ) ) {
1156
+ count -= ( textLine . match ( / < \/ b l o c k q u o t e > / g) || [ ] ) . length ;
1157
+ if ( count > 0 ) {
1158
+ return `${ textLine } ${ '<blockquote>' . repeat ( count ) } ` ;
1159
+ }
1160
+ }
1161
+
1162
+ if ( count > 0 ) {
1163
+ return `${ textLine } ${ '</blockquote>' } ${ '<blockquote>' . repeat ( count ) } ` ;
1164
+ }
1165
+
1166
+ return textLine + ( hasBR ? '<br />' : '' ) ;
1167
+ } )
1168
+ . join ( '' ) ;
1169
+
1170
+ return parsedText ;
1171
+ }
1172
+
1145
1173
/**
1146
1174
* Replaces HTML with markdown
1147
1175
*/
@@ -1154,6 +1182,7 @@ export default class ExpensiMark {
1154
1182
if ( parseBodyTag ) {
1155
1183
generatedMarkdown = parseBodyTag [ 2 ] ;
1156
1184
}
1185
+ generatedMarkdown = this . unpackNestedQuotes ( generatedMarkdown ) ;
1157
1186
1158
1187
const processRule = ( rule : RuleWithRegex ) => {
1159
1188
// Pre-processes input HTML before applying regex
@@ -1186,91 +1215,31 @@ export default class ExpensiMark {
1186
1215
}
1187
1216
1188
1217
/**
1189
- * Modify text for Quotes replacing chevrons with html elements
1218
+ * Main text to html 'quote' parsing logic.
1219
+ * Removes >( ) from text and recursively calls replace function to process nested quotes and build blockquote HTML result.
1220
+ * @param shouldKeepRawInput determines if the raw input should be kept for nested quotes.
1190
1221
*/
1191
- modifyTextForQuote ( regex : RegExp , textToCheck : string , replacement : ReplacementFn ) : string {
1192
- let replacedText = '' ;
1193
- let textToFormat = '' ;
1194
- const match = textToCheck . match ( regex ) ;
1195
-
1196
- // If there's matches we need to modify the quotes
1197
- if ( match !== null ) {
1198
- let insideCodefence = false ;
1199
-
1200
- // Split the textToCheck in lines
1201
- const textSplitted = textToCheck . split ( '\n' ) ;
1202
-
1203
- for ( let i = 0 ; i < textSplitted . length ; i ++ ) {
1204
- if ( ! insideCodefence ) {
1205
- // We need to know when there is a start of codefence so we dont quote
1206
- insideCodefence = Str . contains ( textSplitted [ i ] , '<pre>' ) ;
1207
- }
1208
-
1209
- // Since the last space will be trimmed and would incorrectly disable a condition we check it manually
1210
- const isLastBlockquote = textSplitted [ i ] === '>' && i === textSplitted . length - 1 ;
1211
-
1212
- // We only want to modify lines starting with "> " that is not codefence
1213
- if ( ( Str . startsWith ( textSplitted [ i ] , '> ' ) || isLastBlockquote ) && ! insideCodefence ) {
1214
- if ( textSplitted [ i ] === '>' ) {
1215
- textToFormat += `${ textSplitted [ i ] } \n` ;
1216
- insideCodefence = true ;
1217
- } else {
1218
- textToFormat += `${ textSplitted [ i ] } \n` ;
1219
- }
1220
- } else {
1221
- // Make sure we will only modify if we have Text needed to be formatted for quote
1222
- if ( textToFormat !== '' ) {
1223
- replacedText += this . formatTextForQuote ( regex , textToFormat , replacement ) ;
1224
- textToFormat = '' ;
1225
- }
1226
-
1227
- // We dont want a \n after the textSplitted if it is the last row
1228
- if ( i === textSplitted . length - 1 ) {
1229
- replacedText += `${ textSplitted [ i ] } ` ;
1230
- } else {
1231
- replacedText += `${ textSplitted [ i ] } \n` ;
1232
- }
1233
-
1234
- // We need to know when we are not inside codefence anymore
1235
- if ( insideCodefence ) {
1236
- insideCodefence = ! Str . contains ( textSplitted [ i ] , '</pre>' ) ;
1237
- }
1238
- }
1239
- }
1240
-
1241
- // When loop ends we need the last quote to be formatted if we have quotes at last rows
1242
- if ( textToFormat !== '' ) {
1243
- replacedText += this . formatTextForQuote ( regex , textToFormat , replacement ) ;
1244
- }
1245
- } else {
1246
- // If we doesn't have matches make sure the function will return the same textToCheck
1247
- replacedText = textToCheck ;
1222
+ replaceQuoteText ( text : string , shouldKeepRawInput : boolean ) : { replacedText : string ; shouldAddSpace : boolean } {
1223
+ let isStartingWithSpace = false ;
1224
+ const handleMatch = ( _match : string , g2 : string ) => {
1225
+ isStartingWithSpace = ! ! g2 ;
1226
+ return '' ;
1227
+ } ;
1228
+ const textToReplace = text . replace ( / ^ & g t ; ( ) ? / gm, handleMatch ) ;
1229
+ const filterRules = [ 'heading1' ] ;
1230
+ // If we don't reach the max quote depth, we allow the recursive call to process other possible quotes
1231
+ if ( this . currentQuoteDepth < this . maxQuoteDepth - 1 && ! isStartingWithSpace ) {
1232
+ filterRules . push ( 'quote' ) ;
1233
+ this . currentQuoteDepth ++ ;
1248
1234
}
1249
- return replacedText ;
1250
- }
1251
-
1252
- /**
1253
- * Format the content of blockquote if the text matches the regex or else just return the original text
1254
- */
1255
- formatTextForQuote ( regex : RegExp , textToCheck : string , replacement : ReplacementFn ) : string {
1256
- if ( textToCheck . match ( regex ) ) {
1257
- // Remove '>' and trim the spaces between nested quotes
1258
- const formatRow = ( row : string ) => {
1259
- let quoteContent = row [ 4 ] === ' ' ? row . substr ( 5 ) : row . substr ( 4 ) ;
1260
- if ( row === '> ' ) quoteContent = row . substr ( 4 ) ;
1261
-
1262
- if ( quoteContent . trimStart ( ) . startsWith ( '>' ) ) {
1263
- return quoteContent . trimStart ( ) ;
1264
- }
1265
- return quoteContent ;
1266
- } ;
1267
- let textToFormat = textToCheck . split ( '\n' ) . map ( formatRow ) . join ( '\n' ) ;
1235
+ const replacedText = this . replace ( textToReplace , {
1236
+ filterRules,
1237
+ shouldEscapeText : false ,
1238
+ shouldKeepRawInput,
1239
+ } ) ;
1240
+ this . currentQuoteDepth = 0 ;
1268
1241
1269
- // Remove leading and trailing line breaks
1270
- textToFormat = textToFormat . replace ( / ^ \n + | \n + $ / g, '' ) ;
1271
- return replacement ( EXTRAS_DEFAULT , textToFormat ) ;
1272
- }
1273
- return textToCheck ;
1242
+ return { replacedText, shouldAddSpace : isStartingWithSpace } ;
1274
1243
}
1275
1244
1276
1245
/**
0 commit comments