mirror of
https://github.com/apple/pkl.git
synced 2026-06-12 16:44:33 +02:00
SPICE-0028: Add support for multi-line string line continuations (#1507)
SPICE: https://github.com/apple/pkl-evolution/pull/31
This commit is contained in:
@@ -255,8 +255,7 @@ String literals are enclosed in double quotes:
|
|||||||
"Hello, World!"
|
"Hello, World!"
|
||||||
----
|
----
|
||||||
|
|
||||||
TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences,
|
TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences and stricter rules for line indentation in multiline strings.],
|
||||||
have stricter rules for line indentation in multiline strings, and do not have a line continuation character.],
|
|
||||||
String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them!
|
String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them!
|
||||||
|
|
||||||
Inside a string literal, the following character escape sequences have special meaning:
|
Inside a string literal, the following character escape sequences have special meaning:
|
||||||
@@ -362,6 +361,23 @@ str = """
|
|||||||
"""
|
"""
|
||||||
----
|
----
|
||||||
|
|
||||||
|
To prevent line breaks from becoming part of the string's value, use a backslash (`\`) to end those lines.
|
||||||
|
|
||||||
|
[source%tested,{pkl}]
|
||||||
|
----
|
||||||
|
str = """
|
||||||
|
Although the Dodo is extinct, \
|
||||||
|
the species will be remembered.
|
||||||
|
"""
|
||||||
|
----
|
||||||
|
|
||||||
|
This multiline string is equivalent to the following single-line string:
|
||||||
|
|
||||||
|
[source%parsed,{pkl-expr}]
|
||||||
|
----
|
||||||
|
"Although the Dodo is extinct, the species will be remembered."
|
||||||
|
----
|
||||||
|
|
||||||
[[custom-string-delimiters]]
|
[[custom-string-delimiters]]
|
||||||
=== Custom String Delimiters
|
=== Custom String Delimiters
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package org.pkl.core.util;
|
|||||||
import static org.pkl.parser.Token.FALSE;
|
import static org.pkl.parser.Token.FALSE;
|
||||||
import static org.pkl.parser.Token.NULL;
|
import static org.pkl.parser.Token.NULL;
|
||||||
import static org.pkl.parser.Token.STRING_ESCAPE_BACKSLASH;
|
import static org.pkl.parser.Token.STRING_ESCAPE_BACKSLASH;
|
||||||
|
import static org.pkl.parser.Token.STRING_ESCAPE_CONTINUATION;
|
||||||
import static org.pkl.parser.Token.STRING_ESCAPE_NEWLINE;
|
import static org.pkl.parser.Token.STRING_ESCAPE_NEWLINE;
|
||||||
import static org.pkl.parser.Token.STRING_ESCAPE_QUOTE;
|
import static org.pkl.parser.Token.STRING_ESCAPE_QUOTE;
|
||||||
import static org.pkl.parser.Token.STRING_ESCAPE_RETURN;
|
import static org.pkl.parser.Token.STRING_ESCAPE_RETURN;
|
||||||
@@ -41,7 +42,8 @@ public final class SyntaxHighlighter {
|
|||||||
STRING_ESCAPE_RETURN,
|
STRING_ESCAPE_RETURN,
|
||||||
STRING_ESCAPE_QUOTE,
|
STRING_ESCAPE_QUOTE,
|
||||||
STRING_ESCAPE_BACKSLASH,
|
STRING_ESCAPE_BACKSLASH,
|
||||||
STRING_ESCAPE_UNICODE);
|
STRING_ESCAPE_UNICODE,
|
||||||
|
STRING_ESCAPE_CONTINUATION);
|
||||||
|
|
||||||
private static final EnumSet<Token> constant = EnumSet.of(TRUE, FALSE, NULL);
|
private static final EnumSet<Token> constant = EnumSet.of(TRUE, FALSE, NULL);
|
||||||
|
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
amends "../snippetTest.pkl"
|
||||||
|
|
||||||
|
examples {
|
||||||
|
["string continuation"] {
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
|
||||||
|
#"""
|
||||||
|
hello \#
|
||||||
|
world
|
||||||
|
"""#
|
||||||
|
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
\
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
res1 = "xxx\ xxx"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
foo = """
|
||||||
|
hello \
|
||||||
|
\
|
||||||
|
world
|
||||||
|
"""
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
foo = "hello \
|
||||||
|
world"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
foo =
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
world
|
||||||
|
"""
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
examples {
|
||||||
|
["string continuation"] {
|
||||||
|
"hello world"
|
||||||
|
"hello world"
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
–– Pkl Error ––
|
–– Pkl Error ––
|
||||||
Invalid character escape sequence `\a`.
|
Invalid character escape sequence `\a`.
|
||||||
|
|
||||||
Valid character escape sequences are: \n \r \t \" \\
|
Valid character escape sequences are: \n \r \t \" \\ \<newline>
|
||||||
|
|
||||||
x | res1 = "xxx\axxx"
|
x | res1 = "xxx\axxx"
|
||||||
^^
|
^^
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
–– Pkl Error ––
|
||||||
|
Invalid character escape sequence `\ `.
|
||||||
|
|
||||||
|
Valid character escape sequences are: \n \r \t \" \\ \<newline>
|
||||||
|
|
||||||
|
x | res1 = "xxx\ xxx"
|
||||||
|
^^
|
||||||
|
at invalidCharacterEscape2 (file:///$snippetsDir/input/errors/invalidCharacterEscape2.pkl)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
–– Pkl Error ––
|
||||||
|
Line must match or exceed indentation of the String's last line.
|
||||||
|
|
||||||
|
x | \
|
||||||
|
^
|
||||||
|
at parser19 (file:///$snippetsDir/input/errors/parser19.pkl)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
–– Pkl Error ––
|
||||||
|
Invalid line continuation escape sequence.
|
||||||
|
|
||||||
|
Line continuations are only allowed in multi-line strings.
|
||||||
|
|
||||||
|
x | foo = "hello \
|
||||||
|
^^
|
||||||
|
at parser20 (file:///$snippetsDir/input/errors/parser20.pkl)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
–– Pkl Error ––
|
||||||
|
Invalid line continuation escape sequence.
|
||||||
|
|
||||||
|
Whitespace between the continuation escape and following newline is not allowed.
|
||||||
|
|
||||||
|
x | hello \
|
||||||
|
^^^^^
|
||||||
|
at parser21 (file:///$snippetsDir/input/errors/parser21.pkl)
|
||||||
@@ -75,6 +75,11 @@ final class Builder {
|
|||||||
OPERATOR ->
|
OPERATOR ->
|
||||||
new Text(node.text(source));
|
new Text(node.text(source));
|
||||||
case STRING_NEWLINE -> mustForceLine();
|
case STRING_NEWLINE -> mustForceLine();
|
||||||
|
case STRING_CONTINUATION -> {
|
||||||
|
var escape = node.text(source);
|
||||||
|
yield new Nodes(
|
||||||
|
List.of(new Text(escape.substring(0, escape.length() - 1)), mustForceLine()));
|
||||||
|
}
|
||||||
case MODULE_DECLARATION -> formatModuleDeclaration(node);
|
case MODULE_DECLARATION -> formatModuleDeclaration(node);
|
||||||
case MODULE_DEFINITION -> formatModuleDefinition(node);
|
case MODULE_DEFINITION -> formatModuleDefinition(node);
|
||||||
case SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node);
|
case SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node);
|
||||||
@@ -922,7 +927,9 @@ final class Builder {
|
|||||||
if (elem.type == NodeType.TERMINAL && text(elem).endsWith("(")) {
|
if (elem.type == NodeType.TERMINAL && text(elem).endsWith("(")) {
|
||||||
isInStringInterpolation = true;
|
isInStringInterpolation = true;
|
||||||
}
|
}
|
||||||
result.add(format(elem));
|
var formatted = format(elem);
|
||||||
|
if (formatted instanceof Nodes formattedNodes) result.addAll(formattedNodes.nodes());
|
||||||
|
else result.add(formatted);
|
||||||
prev = elem;
|
prev = elem;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+20
@@ -48,6 +48,26 @@ quux {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// line continuations
|
||||||
|
corge {
|
||||||
|
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
|
||||||
|
#"""
|
||||||
|
hello \#
|
||||||
|
world
|
||||||
|
"""#
|
||||||
|
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
\
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
obj {
|
obj {
|
||||||
data {
|
data {
|
||||||
["bar"] = """
|
["bar"] = """
|
||||||
|
|||||||
+19
@@ -52,6 +52,25 @@ quux {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// line continuations
|
||||||
|
corge {
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
|
||||||
|
#"""
|
||||||
|
hello \#
|
||||||
|
world
|
||||||
|
"""#
|
||||||
|
|
||||||
|
"""
|
||||||
|
hello \
|
||||||
|
\
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
obj {
|
obj {
|
||||||
data {
|
data {
|
||||||
["bar"] =
|
["bar"] =
|
||||||
|
|||||||
@@ -978,6 +978,8 @@ class GenericParserImpl {
|
|||||||
STRING_ESCAPE_RETURN,
|
STRING_ESCAPE_RETURN,
|
||||||
STRING_ESCAPE_UNICODE ->
|
STRING_ESCAPE_UNICODE ->
|
||||||
children.add(make(NodeType.STRING_ESCAPE, next().span));
|
children.add(make(NodeType.STRING_ESCAPE, next().span));
|
||||||
|
case STRING_ESCAPE_CONTINUATION ->
|
||||||
|
throw parserError("invalidLineContinuationEscapeSequence");
|
||||||
case INTERPOLATION_START -> {
|
case INTERPOLATION_START -> {
|
||||||
children.add(makeTerminal(next()));
|
children.add(makeTerminal(next()));
|
||||||
ff(children);
|
ff(children);
|
||||||
@@ -1011,6 +1013,8 @@ class GenericParserImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case STRING_NEWLINE -> children.add(make(NodeType.STRING_NEWLINE, next().span));
|
case STRING_NEWLINE -> children.add(make(NodeType.STRING_NEWLINE, next().span));
|
||||||
|
case STRING_ESCAPE_CONTINUATION ->
|
||||||
|
children.add(make(NodeType.STRING_CONTINUATION, next().span));
|
||||||
case STRING_ESCAPE_NEWLINE,
|
case STRING_ESCAPE_NEWLINE,
|
||||||
STRING_ESCAPE_TAB,
|
STRING_ESCAPE_TAB,
|
||||||
STRING_ESCAPE_QUOTE,
|
STRING_ESCAPE_QUOTE,
|
||||||
@@ -1060,7 +1064,8 @@ class GenericParserImpl {
|
|||||||
throw parserError(ErrorMessages.create("stringIndentationMustMatchLastLine"), child.span);
|
throw parserError(ErrorMessages.create("stringIndentationMustMatchLastLine"), child.span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousNewline = child.type == NodeType.STRING_NEWLINE;
|
previousNewline =
|
||||||
|
child.type == NodeType.STRING_NEWLINE || child.type == NodeType.STRING_CONTINUATION;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -460,6 +460,22 @@ public final class Lexer {
|
|||||||
yield Token.INTERPOLATION_START;
|
yield Token.INTERPOLATION_START;
|
||||||
}
|
}
|
||||||
case 'u' -> lexUnicodeEscape();
|
case 'u' -> lexUnicodeEscape();
|
||||||
|
case '\n' -> Token.STRING_ESCAPE_CONTINUATION;
|
||||||
|
case ' ', '\t' -> {
|
||||||
|
var c = cursor;
|
||||||
|
var next = nextChar();
|
||||||
|
while (next == ' ' || next == '\t') next = nextChar();
|
||||||
|
if (next == '\n')
|
||||||
|
throw lexError(
|
||||||
|
ErrorMessages.create("invalidLineContinuationEscapeSequenceWhitespace"),
|
||||||
|
c - 2,
|
||||||
|
cursor - c + 2);
|
||||||
|
|
||||||
|
throw lexError(
|
||||||
|
ErrorMessages.create("invalidCharacterEscapeSequence", "\\" + (char) ch, "\\"),
|
||||||
|
c - 2,
|
||||||
|
2);
|
||||||
|
}
|
||||||
default ->
|
default ->
|
||||||
throw lexError(
|
throw lexError(
|
||||||
ErrorMessages.create("invalidCharacterEscapeSequence", "\\" + (char) ch, "\\"),
|
ErrorMessages.create("invalidCharacterEscapeSequence", "\\" + (char) ch, "\\"),
|
||||||
|
|||||||
@@ -1131,6 +1131,8 @@ final class ParserImpl {
|
|||||||
end = tk.span;
|
end = tk.span;
|
||||||
builder.append(parseUnicodeEscape(tk));
|
builder.append(parseUnicodeEscape(tk));
|
||||||
}
|
}
|
||||||
|
case STRING_ESCAPE_CONTINUATION ->
|
||||||
|
throw parserError("invalidLineContinuationEscapeSequence");
|
||||||
case INTERPOLATION_START -> {
|
case INTERPOLATION_START -> {
|
||||||
var istart = next().span;
|
var istart = next().span;
|
||||||
if (!builder.isEmpty()) {
|
if (!builder.isEmpty()) {
|
||||||
@@ -1167,7 +1169,8 @@ final class ParserImpl {
|
|||||||
STRING_ESCAPE_QUOTE,
|
STRING_ESCAPE_QUOTE,
|
||||||
STRING_ESCAPE_BACKSLASH,
|
STRING_ESCAPE_BACKSLASH,
|
||||||
STRING_ESCAPE_RETURN,
|
STRING_ESCAPE_RETURN,
|
||||||
STRING_ESCAPE_UNICODE ->
|
STRING_ESCAPE_UNICODE,
|
||||||
|
STRING_ESCAPE_CONTINUATION ->
|
||||||
stringTokens.add(new TempNode(next(), null));
|
stringTokens.add(new TempNode(next(), null));
|
||||||
case INTERPOLATION_START -> {
|
case INTERPOLATION_START -> {
|
||||||
var istart = next();
|
var istart = next();
|
||||||
@@ -1236,6 +1239,7 @@ final class ParserImpl {
|
|||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
isNewLine = true;
|
isNewLine = true;
|
||||||
}
|
}
|
||||||
|
case STRING_ESCAPE_CONTINUATION -> isNewLine = true;
|
||||||
case STRING_PART -> {
|
case STRING_PART -> {
|
||||||
var text = token.text(lexer);
|
var text = token.text(lexer);
|
||||||
if (isNewLine) {
|
if (isNewLine) {
|
||||||
@@ -1642,6 +1646,7 @@ final class ParserImpl {
|
|||||||
case STRING_ESCAPE_BACKSLASH -> "\\";
|
case STRING_ESCAPE_BACKSLASH -> "\\";
|
||||||
case STRING_ESCAPE_TAB -> "\t";
|
case STRING_ESCAPE_TAB -> "\t";
|
||||||
case STRING_ESCAPE_RETURN -> "\r";
|
case STRING_ESCAPE_RETURN -> "\r";
|
||||||
|
case STRING_ESCAPE_CONTINUATION -> "";
|
||||||
case STRING_ESCAPE_UNICODE -> parseUnicodeEscape(tk);
|
case STRING_ESCAPE_UNICODE -> parseUnicodeEscape(tk);
|
||||||
default -> throw new RuntimeException("Unreacheable code");
|
default -> throw new RuntimeException("Unreacheable code");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ public enum Token {
|
|||||||
STRING_ESCAPE_QUOTE,
|
STRING_ESCAPE_QUOTE,
|
||||||
STRING_ESCAPE_BACKSLASH,
|
STRING_ESCAPE_BACKSLASH,
|
||||||
STRING_ESCAPE_UNICODE,
|
STRING_ESCAPE_UNICODE,
|
||||||
|
STRING_ESCAPE_CONTINUATION,
|
||||||
STRING_END,
|
STRING_END,
|
||||||
STRING_PART;
|
STRING_PART;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -70,6 +70,7 @@ public enum NodeType {
|
|||||||
STRING_CHARS,
|
STRING_CHARS,
|
||||||
OPERATOR,
|
OPERATOR,
|
||||||
STRING_NEWLINE,
|
STRING_NEWLINE,
|
||||||
|
STRING_CONTINUATION,
|
||||||
STRING_ESCAPE,
|
STRING_ESCAPE,
|
||||||
|
|
||||||
// members
|
// members
|
||||||
|
|||||||
@@ -44,13 +44,23 @@ The separator character (`_`) cannot follow `0x`, `0b`, `.`, `e`, or 'E' in a nu
|
|||||||
invalidCharacterEscapeSequence=\
|
invalidCharacterEscapeSequence=\
|
||||||
Invalid character escape sequence `{0}`.\n\
|
Invalid character escape sequence `{0}`.\n\
|
||||||
\n\
|
\n\
|
||||||
Valid character escape sequences are: {1}n {1}r {1}t {1}" {1}\\
|
Valid character escape sequences are: {1}n {1}r {1}t {1}" {1}\\ {1}<newline>
|
||||||
|
|
||||||
invalidUnicodeEscapeSequence=\
|
invalidUnicodeEscapeSequence=\
|
||||||
Invalid Unicode escape sequence `{0}`.\n\
|
Invalid Unicode escape sequence `{0}`.\n\
|
||||||
\n\
|
\n\
|
||||||
Valid Unicode escape sequences are {1}'{'0'}' to {1}'{'10FFFF'}' (1-6 hexadecimal characters).
|
Valid Unicode escape sequences are {1}'{'0'}' to {1}'{'10FFFF'}' (1-6 hexadecimal characters).
|
||||||
|
|
||||||
|
invalidLineContinuationEscapeSequence=\
|
||||||
|
Invalid line continuation escape sequence.\n\
|
||||||
|
\n\
|
||||||
|
Line continuations are only allowed in multi-line strings.
|
||||||
|
|
||||||
|
invalidLineContinuationEscapeSequenceWhitespace=\
|
||||||
|
Invalid line continuation escape sequence.\n\
|
||||||
|
\n\
|
||||||
|
Whitespace between the continuation escape and following newline is not allowed.
|
||||||
|
|
||||||
missingDelimiter=\
|
missingDelimiter=\
|
||||||
Missing `{0}` delimiter.
|
Missing `{0}` delimiter.
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ class GenericSexpRenderer(code: String) {
|
|||||||
NodeType.TERMINAL,
|
NodeType.TERMINAL,
|
||||||
NodeType.OPERATOR,
|
NodeType.OPERATOR,
|
||||||
NodeType.STRING_NEWLINE,
|
NodeType.STRING_NEWLINE,
|
||||||
|
NodeType.STRING_CONTINUATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val UNPACK_CHILDREN =
|
private val UNPACK_CHILDREN =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -97,6 +97,7 @@ class ParserComparisonTest {
|
|||||||
"errors/parser18.pkl",
|
"errors/parser18.pkl",
|
||||||
"errors/nested1.pkl",
|
"errors/nested1.pkl",
|
||||||
"errors/invalidCharacterEscape.pkl",
|
"errors/invalidCharacterEscape.pkl",
|
||||||
|
"errors/invalidCharacterEscape2.pkl",
|
||||||
"errors/invalidUnicodeEscape.pkl",
|
"errors/invalidUnicodeEscape.pkl",
|
||||||
"errors/unterminatedUnicodeEscape.pkl",
|
"errors/unterminatedUnicodeEscape.pkl",
|
||||||
"errors/keywordNotAllowedHere1.pkl",
|
"errors/keywordNotAllowedHere1.pkl",
|
||||||
|
|||||||
Reference in New Issue
Block a user