@@ -68,6 +68,7 @@ object Parser {
6868
6969 private val emptyLazyArray = new Array [Lazy ](0 )
7070 private val isSpaceOrTab : Char => Boolean = c => c == ' ' || c == '\t '
71+
7172}
7273
7374class Parser (
@@ -91,6 +92,10 @@ class Parser(
9192 }
9293 }
9394
95+ private def failParse (msg : String , offset : Int ): Nothing = {
96+ throw new ParseError (msg, offset = offset)
97+ }
98+
9499 def Pos [$ : P ]: P [Position ] = Index .map(offset => new Position (fileScope, offset))
95100
96101 def id [$ : P ]: P [String ] = P (
@@ -284,7 +289,8 @@ class Parser(
284289
285290 def maybeChompedTripleBarString [$ : P ]: P [Seq [String ]] = tripleBarString.map {
286291 case (true , lines) =>
287- lines.dropRight(1 ) ++ Seq (lines.last.stripLineEnd)
292+ val last = lines.length - 1
293+ lines.updated(last, lines(last).stripLineEnd)
288294 case (false , lines) =>
289295 lines
290296 }
@@ -314,14 +320,67 @@ class Parser(
314320 }
315321 )
316322
323+ private def describeWhitespace (whitespace : String ): String = {
324+ var spaces = 0
325+ var tabs = 0
326+ for (c <- whitespace) {
327+ if (c == ' ' ) spaces += 1
328+ else if (c == '\t ' ) tabs += 1
329+ }
330+ if (spaces > 0 && tabs > 0 ) {
331+ s " ${spaces} ${if (spaces == 1 ) " space" else " spaces" } and ${tabs} ${
332+ if (tabs == 1 ) " tab" else " tabs"
333+ }"
334+ } else if (spaces > 0 ) {
335+ s " ${spaces} ${if (spaces == 1 ) " space" else " spaces" }"
336+ } else if (tabs > 0 ) {
337+ s " ${tabs} ${if (tabs == 1 ) " tab" else " tabs" }"
338+ } else {
339+ " no indentation"
340+ }
341+ }
342+
317343 def tripleBarStringBody [$ : P ](indent : String , sep : String ): P [Seq [String ]] = P (
318- // Because we already parsed the indentation, we special case the first line and only look for the content .
344+ // First line: indentation already consumed, just capture content up to the line separator .
319345 (CharsWhile (! sep.contains(_), 0 ) ~~ sep).! .flatMapX { firstLine =>
320- // That's the core of the parsing. Either we have an empty line or we have indentation + content + new line separator .
321- (sep.! | (indent. ! ~~ ( CharsWhile ( ! sep.contains(_), 0 ) ~~ sep). ! ).map(_._2 ))
346+ // Subsequent lines: empty line (just separator) | indented line with whitespace check .
347+ (sep.! | tripleBarStringIndentedLine (indent, sep))
322348 .opaque(" |||-block line must either be an empty line or start with at least one whitespace" )
323349 .repX
324- .map(Seq (firstLine) ++ _)
350+ .map(firstLine +: _)
351+ }
352+ )
353+
354+ private def tripleBarStringIndentedLine [$ : P ](indent : String , sep : String ): P [String ] = P (
355+ // Parse whitespace once, then check if it matches the expected indentation.
356+ (CharsWhile (isSpaceOrTab, 0 ).! ~~ Index ).flatMapX { case (whitespace, wsEndOffset) =>
357+ if (whitespace.isEmpty) {
358+ // No whitespace at all — not an indented line (e.g. ||| terminator without leading spaces).
359+ Fail .opaque(" expected indentation for text block line" )
360+ } else if (whitespace.startsWith(indent)) {
361+ // Indentation matches — parse the rest of the line content after the indent.
362+ (CharsWhile (! sep.contains(_), 0 ) ~~ sep).! .map { content =>
363+ whitespace.substring(indent.length) + content
364+ }
365+ } else {
366+ val isTerminator = P .current.input.isReachable(wsEndOffset + 2 ) &&
367+ P .current.input(wsEndOffset) == '|' &&
368+ P .current.input(wsEndOffset + 1 ) == '|' &&
369+ P .current.input(wsEndOffset + 2 ) == '|'
370+ if (isTerminator) {
371+ // This is the ||| terminator line — fail to let the outer parser match |||.
372+ Fail .opaque(" text block terminator |||" )
373+ } else {
374+ // Indentation mismatch — emit a detailed error.
375+ val expectedDescription = describeWhitespace(indent)
376+ val actualDescription = describeWhitespace(whitespace)
377+ failParse(
378+ " text block indentation mismatch: expected at least " +
379+ expectedDescription + " , found " + actualDescription,
380+ wsEndOffset
381+ )
382+ }
383+ }
325384 }
326385 )
327386
0 commit comments