@@ -239,6 +239,9 @@ trait JavaScanners extends ast.parser.ScannersCommon {
239
239
*/
240
240
protected def putChar (c : Char ): Unit = { cbuf.append(c) }
241
241
242
+ /** Remove the last N characters from the buffer */
243
+ private def popNChars (n : Int ): Unit = if (n > 0 ) cbuf.setLength(cbuf.length - n)
244
+
242
245
/** Clear buffer and set name */
243
246
private def setName (): Unit = {
244
247
name = newTermName(cbuf.toString())
@@ -322,15 +325,26 @@ trait JavaScanners extends ast.parser.ScannersCommon {
322
325
323
326
case '\" ' =>
324
327
in.next()
325
- while (in.ch != '\" ' && (in.isUnicode || in.ch != CR && in.ch != LF && in.ch != SU )) {
326
- getlitch()
327
- }
328
- if (in.ch == '\" ' ) {
329
- token = STRINGLIT
330
- setName()
331
- in.next()
328
+ if (in.ch != '\" ' ) { // "..." non-empty string literal
329
+ while (in.ch != '\" ' && (in.isUnicode || in.ch != CR && in.ch != LF && in.ch != SU )) {
330
+ getlitch()
331
+ }
332
+ if (in.ch == '\" ' ) {
333
+ token = STRINGLIT
334
+ setName()
335
+ in.next()
336
+ } else {
337
+ syntaxError(" unclosed string literal" )
338
+ }
332
339
} else {
333
- syntaxError(" unclosed string literal" )
340
+ in.next()
341
+ if (in.ch != '\" ' ) { // "" empty string literal
342
+ token = STRINGLIT
343
+ setName()
344
+ } else {
345
+ in.next()
346
+ getTextBlock()
347
+ }
334
348
}
335
349
return
336
350
@@ -691,6 +705,8 @@ trait JavaScanners extends ast.parser.ScannersCommon {
691
705
case '\" ' => putChar('\" ' )
692
706
case '\' ' => putChar('\' ' )
693
707
case '\\ ' => putChar('\\ ' )
708
+ case 's' => putChar(' ' ) // specific to text blocks
709
+ case CR | LF => // specific to text blocks
694
710
case _ =>
695
711
syntaxError(in.cpos - 1 , " invalid escape character" )
696
712
putChar(in.ch)
@@ -702,6 +718,120 @@ trait JavaScanners extends ast.parser.ScannersCommon {
702
718
in.next()
703
719
}
704
720
721
+ /** read a triple-quote delimited text block, starting after the first three
722
+ * double quotes
723
+ */
724
+ private def getTextBlock (): Unit = {
725
+ // Open delimiter is followed by optional space, then a newline
726
+ while (in.ch == ' ' || in.ch == '\t ' || in.ch == FF ) {
727
+ in.next()
728
+ }
729
+ if (in.ch != LF && in.ch != CR ) { // CR-LF is already normalized into LF by `JavaCharArrayReader`
730
+ syntaxError(" illegal text block open delimiter sequence, missing line terminator" )
731
+ return
732
+ }
733
+ in.next()
734
+
735
+ /* Do a lookahead scan over the full text block to:
736
+ * - compute common white space prefix
737
+ * - find the offset where the text block ends
738
+ */
739
+ var commonWhiteSpacePrefix = Int .MaxValue
740
+ var blockEndOffset = 0
741
+ val backtrackTo = in.copy
742
+ var blockClosed = false
743
+ var lineWhiteSpacePrefix = 0
744
+ var lineIsOnlyWhitespace = true
745
+ while (! blockClosed && (in.isUnicode || in.ch != SU )) {
746
+ if (in.ch == '\" ' ) { // Potential end of the block
747
+ in.next()
748
+ if (in.ch == '\" ' ) {
749
+ in.next()
750
+ if (in.ch == '\" ' ) {
751
+ blockClosed = true
752
+ commonWhiteSpacePrefix = commonWhiteSpacePrefix min lineWhiteSpacePrefix
753
+ blockEndOffset = in.cpos - 2
754
+ }
755
+ }
756
+
757
+ // Not the end of the block - just a single or double " character
758
+ if (! blockClosed) {
759
+ lineIsOnlyWhitespace = false
760
+ }
761
+ } else if (in.ch == CR || in.ch == LF ) { // new line in the block
762
+ in.next()
763
+ if (! lineIsOnlyWhitespace) {
764
+ commonWhiteSpacePrefix = commonWhiteSpacePrefix min lineWhiteSpacePrefix
765
+ }
766
+ lineWhiteSpacePrefix = 0
767
+ lineIsOnlyWhitespace = true
768
+ } else if (lineIsOnlyWhitespace &&
769
+ (in.ch == ' ' || in.ch == '\t ' || in.ch == FF )) { // extend white space prefix
770
+ in.next()
771
+ lineWhiteSpacePrefix += 1
772
+ } else {
773
+ lineIsOnlyWhitespace = false
774
+ getlitch()
775
+ }
776
+ }
777
+ setName() // clear the literal buffer
778
+
779
+ // Bail out if the block never did have an end
780
+ if (! blockClosed) {
781
+ syntaxError(" unclosed text block" )
782
+ return
783
+ }
784
+
785
+ // Second pass: construct the literal string value this time
786
+ in = backtrackTo
787
+ while (in.cpos < blockEndOffset) {
788
+ // Drop the line's leading whitespace
789
+ var remainingPrefix = commonWhiteSpacePrefix
790
+ while (remainingPrefix > 0 && in.ch != CR && in.ch != LF && in.cpos < blockEndOffset) {
791
+ in.next()
792
+ remainingPrefix -= 1
793
+ }
794
+
795
+ var trailingWhitespaceLength = 0
796
+ var escapedNewline = false // Does the line end with `\`?
797
+ while (in.ch != CR && in.ch != LF && in.cpos < blockEndOffset && ! escapedNewline) {
798
+ if (isWhitespace(in.ch)) {
799
+ trailingWhitespaceLength += 1
800
+ } else {
801
+ trailingWhitespaceLength = 0
802
+ }
803
+
804
+ // Detect if the line is about to end with `\`
805
+ if (in.ch == '\\ ' && {
806
+ val lookahead = in.copy
807
+ lookahead.next()
808
+ lookahead.ch == CR || lookahead.ch == LF
809
+ }) {
810
+ escapedNewline = true
811
+ }
812
+
813
+ getlitch()
814
+ }
815
+
816
+ // Drop the line's trailing whitespace
817
+ popNChars(trailingWhitespaceLength)
818
+
819
+ // Normalize line terminators
820
+ if ((in.ch == CR || in.ch == LF ) && ! escapedNewline) {
821
+ in.next()
822
+ putChar('\n ' )
823
+ }
824
+ }
825
+
826
+ token = STRINGLIT
827
+ setName()
828
+
829
+ // Trailing """
830
+ in.next()
831
+ in.next()
832
+ in.next()
833
+ }
834
+
705
835
/** read fractional part and exponent of floating point number
706
836
* if one is present.
707
837
*/
0 commit comments