mirror of
https://github.com/apple/pkl.git
synced 2026-05-05 14:43:28 +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!"
|
||||
----
|
||||
|
||||
TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences,
|
||||
have stricter rules for line indentation in multiline strings, and do not have a line continuation character.],
|
||||
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.],
|
||||
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:
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.pkl.core.util;
|
||||
import static org.pkl.parser.Token.FALSE;
|
||||
import static org.pkl.parser.Token.NULL;
|
||||
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_QUOTE;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_RETURN;
|
||||
@@ -41,7 +42,8 @@ public final class SyntaxHighlighter {
|
||||
STRING_ESCAPE_RETURN,
|
||||
STRING_ESCAPE_QUOTE,
|
||||
STRING_ESCAPE_BACKSLASH,
|
||||
STRING_ESCAPE_UNICODE);
|
||||
STRING_ESCAPE_UNICODE,
|
||||
STRING_ESCAPE_CONTINUATION);
|
||||
|
||||
private static final EnumSet<Token> constant = EnumSet.of(TRUE, FALSE, NULL);
|
||||
|
||||
|
||||
21
pkl-core/src/test/files/LanguageSnippetTests/input/basic/stringMultilineContinuation.pkl
vendored
Normal file
21
pkl-core/src/test/files/LanguageSnippetTests/input/basic/stringMultilineContinuation.pkl
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
amends "../snippetTest.pkl"
|
||||
|
||||
examples {
|
||||
["string continuation"] {
|
||||
"""
|
||||
hello \
|
||||
world
|
||||
"""
|
||||
|
||||
#"""
|
||||
hello \#
|
||||
world
|
||||
"""#
|
||||
|
||||
"""
|
||||
hello \
|
||||
\
|
||||
world
|
||||
"""
|
||||
}
|
||||
}
|
||||
1
pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidCharacterEscape2.pkl
vendored
Normal file
1
pkl-core/src/test/files/LanguageSnippetTests/input/errors/invalidCharacterEscape2.pkl
vendored
Normal file
@@ -0,0 +1 @@
|
||||
res1 = "xxx\ xxx"
|
||||
5
pkl-core/src/test/files/LanguageSnippetTests/input/errors/parser19.pkl
vendored
Normal file
5
pkl-core/src/test/files/LanguageSnippetTests/input/errors/parser19.pkl
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
foo = """
|
||||
hello \
|
||||
\
|
||||
world
|
||||
"""
|
||||
2
pkl-core/src/test/files/LanguageSnippetTests/input/errors/parser20.pkl
vendored
Normal file
2
pkl-core/src/test/files/LanguageSnippetTests/input/errors/parser20.pkl
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
foo = "hello \
|
||||
world"
|
||||
5
pkl-core/src/test/files/LanguageSnippetTests/input/errors/parser21.pkl
vendored
Normal file
5
pkl-core/src/test/files/LanguageSnippetTests/input/errors/parser21.pkl
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
foo =
|
||||
"""
|
||||
hello \
|
||||
world
|
||||
"""
|
||||
7
pkl-core/src/test/files/LanguageSnippetTests/output/basic/stringMultilineContinuation.pcf
vendored
Normal file
7
pkl-core/src/test/files/LanguageSnippetTests/output/basic/stringMultilineContinuation.pcf
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
examples {
|
||||
["string continuation"] {
|
||||
"hello world"
|
||||
"hello world"
|
||||
"hello world"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
–– Pkl Error ––
|
||||
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"
|
||||
^^
|
||||
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidCharacterEscape2.err
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidCharacterEscape2.err
vendored
Normal file
@@ -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)
|
||||
6
pkl-core/src/test/files/LanguageSnippetTests/output/errors/parser19.err
vendored
Normal file
6
pkl-core/src/test/files/LanguageSnippetTests/output/errors/parser19.err
vendored
Normal file
@@ -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)
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/output/errors/parser20.err
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/output/errors/parser20.err
vendored
Normal file
@@ -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)
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/output/errors/parser21.err
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/output/errors/parser21.err
vendored
Normal file
@@ -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 ->
|
||||
new Text(node.text(source));
|
||||
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_DEFINITION -> formatModuleDefinition(node);
|
||||
case SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node);
|
||||
@@ -922,7 +927,9 @@ final class Builder {
|
||||
if (elem.type == NodeType.TERMINAL && text(elem).endsWith("(")) {
|
||||
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;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -48,6 +48,26 @@ quux {
|
||||
"""
|
||||
}
|
||||
|
||||
// line continuations
|
||||
corge {
|
||||
|
||||
"""
|
||||
hello \
|
||||
world
|
||||
"""
|
||||
|
||||
#"""
|
||||
hello \#
|
||||
world
|
||||
"""#
|
||||
|
||||
"""
|
||||
hello \
|
||||
\
|
||||
world
|
||||
"""
|
||||
}
|
||||
|
||||
obj {
|
||||
data {
|
||||
["bar"] = """
|
||||
|
||||
@@ -52,6 +52,25 @@ quux {
|
||||
"""
|
||||
}
|
||||
|
||||
// line continuations
|
||||
corge {
|
||||
"""
|
||||
hello \
|
||||
world
|
||||
"""
|
||||
|
||||
#"""
|
||||
hello \#
|
||||
world
|
||||
"""#
|
||||
|
||||
"""
|
||||
hello \
|
||||
\
|
||||
world
|
||||
"""
|
||||
}
|
||||
|
||||
obj {
|
||||
data {
|
||||
["bar"] =
|
||||
|
||||
@@ -978,6 +978,8 @@ class GenericParserImpl {
|
||||
STRING_ESCAPE_RETURN,
|
||||
STRING_ESCAPE_UNICODE ->
|
||||
children.add(make(NodeType.STRING_ESCAPE, next().span));
|
||||
case STRING_ESCAPE_CONTINUATION ->
|
||||
throw parserError("invalidLineContinuationEscapeSequence");
|
||||
case INTERPOLATION_START -> {
|
||||
children.add(makeTerminal(next()));
|
||||
ff(children);
|
||||
@@ -1011,6 +1013,8 @@ class GenericParserImpl {
|
||||
}
|
||||
}
|
||||
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,
|
||||
STRING_ESCAPE_TAB,
|
||||
STRING_ESCAPE_QUOTE,
|
||||
@@ -1060,7 +1064,8 @@ class GenericParserImpl {
|
||||
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;
|
||||
}
|
||||
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 ->
|
||||
throw lexError(
|
||||
ErrorMessages.create("invalidCharacterEscapeSequence", "\\" + (char) ch, "\\"),
|
||||
|
||||
@@ -1131,6 +1131,8 @@ final class ParserImpl {
|
||||
end = tk.span;
|
||||
builder.append(parseUnicodeEscape(tk));
|
||||
}
|
||||
case STRING_ESCAPE_CONTINUATION ->
|
||||
throw parserError("invalidLineContinuationEscapeSequence");
|
||||
case INTERPOLATION_START -> {
|
||||
var istart = next().span;
|
||||
if (!builder.isEmpty()) {
|
||||
@@ -1167,7 +1169,8 @@ final class ParserImpl {
|
||||
STRING_ESCAPE_QUOTE,
|
||||
STRING_ESCAPE_BACKSLASH,
|
||||
STRING_ESCAPE_RETURN,
|
||||
STRING_ESCAPE_UNICODE ->
|
||||
STRING_ESCAPE_UNICODE,
|
||||
STRING_ESCAPE_CONTINUATION ->
|
||||
stringTokens.add(new TempNode(next(), null));
|
||||
case INTERPOLATION_START -> {
|
||||
var istart = next();
|
||||
@@ -1236,6 +1239,7 @@ final class ParserImpl {
|
||||
builder.append('\n');
|
||||
isNewLine = true;
|
||||
}
|
||||
case STRING_ESCAPE_CONTINUATION -> isNewLine = true;
|
||||
case STRING_PART -> {
|
||||
var text = token.text(lexer);
|
||||
if (isNewLine) {
|
||||
@@ -1642,6 +1646,7 @@ final class ParserImpl {
|
||||
case STRING_ESCAPE_BACKSLASH -> "\\";
|
||||
case STRING_ESCAPE_TAB -> "\t";
|
||||
case STRING_ESCAPE_RETURN -> "\r";
|
||||
case STRING_ESCAPE_CONTINUATION -> "";
|
||||
case STRING_ESCAPE_UNICODE -> parseUnicodeEscape(tk);
|
||||
default -> throw new RuntimeException("Unreacheable code");
|
||||
};
|
||||
|
||||
@@ -129,6 +129,7 @@ public enum Token {
|
||||
STRING_ESCAPE_QUOTE,
|
||||
STRING_ESCAPE_BACKSLASH,
|
||||
STRING_ESCAPE_UNICODE,
|
||||
STRING_ESCAPE_CONTINUATION,
|
||||
STRING_END,
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -70,6 +70,7 @@ public enum NodeType {
|
||||
STRING_CHARS,
|
||||
OPERATOR,
|
||||
STRING_NEWLINE,
|
||||
STRING_CONTINUATION,
|
||||
STRING_ESCAPE,
|
||||
|
||||
// members
|
||||
|
||||
@@ -44,13 +44,23 @@ The separator character (`_`) cannot follow `0x`, `0b`, `.`, `e`, or 'E' in a nu
|
||||
invalidCharacterEscapeSequence=\
|
||||
Invalid character escape sequence `{0}`.\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=\
|
||||
Invalid Unicode escape sequence `{0}`.\n\
|
||||
\n\
|
||||
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=\
|
||||
Missing `{0}` delimiter.
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ class GenericSexpRenderer(code: String) {
|
||||
NodeType.TERMINAL,
|
||||
NodeType.OPERATOR,
|
||||
NodeType.STRING_NEWLINE,
|
||||
NodeType.STRING_CONTINUATION,
|
||||
)
|
||||
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -97,6 +97,7 @@ class ParserComparisonTest {
|
||||
"errors/parser18.pkl",
|
||||
"errors/nested1.pkl",
|
||||
"errors/invalidCharacterEscape.pkl",
|
||||
"errors/invalidCharacterEscape2.pkl",
|
||||
"errors/invalidUnicodeEscape.pkl",
|
||||
"errors/unterminatedUnicodeEscape.pkl",
|
||||
"errors/keywordNotAllowedHere1.pkl",
|
||||
|
||||
Reference in New Issue
Block a user