@weborigami/language 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +18 -0
- package/main.js +1 -1
- package/package.json +3 -3
- package/src/compiler/compile.js +13 -7
- package/src/compiler/origami.pegjs +150 -68
- package/src/compiler/parse.d.ts +3 -0
- package/src/compiler/parse.js +961 -647
- package/src/compiler/parserHelpers.js +15 -1
- package/src/runtime/concatTreeValues.js +6 -0
- package/src/runtime/evaluate.js +40 -30
- package/src/runtime/expressionFunction.js +4 -4
- package/src/runtime/formatError.js +52 -0
- package/src/runtime/internal.js +0 -2
- package/src/runtime/ops.js +43 -10
- package/test/compiler/parse.test.js +127 -48
- package/test/runtime/evaluate.test.js +8 -0
- package/test/runtime/ops.test.js +17 -4
- package/src/compiler/code.d.ts +0 -3
- package/src/runtime/format.js +0 -126
- package/test/runtime/format.test.js +0 -66
package/index.ts
CHANGED
|
@@ -3,6 +3,15 @@ import { StringLike } from "../async-tree/index.js";
|
|
|
3
3
|
|
|
4
4
|
export * from "./main.js";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A chunk of compiled Origami code. This is just an Array with an additional
|
|
8
|
+
* `source` property.
|
|
9
|
+
*/
|
|
10
|
+
interface ArrayWithSource extends Array<any> {
|
|
11
|
+
source?: Source;
|
|
12
|
+
}
|
|
13
|
+
export type Code = ArrayWithSource;
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* A class constructor is an object with a `new` method that returns an
|
|
8
17
|
* instance of the indicated type.
|
|
@@ -32,3 +41,12 @@ export type FileUnpackFunction = (
|
|
|
32
41
|
export type Mixin<MixinMembers> = <T>(
|
|
33
42
|
Base: Constructor<T>
|
|
34
43
|
) => Constructor<T & MixinMembers>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Source code representation used by the parser.
|
|
47
|
+
*/
|
|
48
|
+
export type Source = {
|
|
49
|
+
name: string;
|
|
50
|
+
text: string;
|
|
51
|
+
url: URL;
|
|
52
|
+
}
|
package/main.js
CHANGED
|
@@ -17,5 +17,5 @@ export { default as concatTreeValues } from "./src/runtime/concatTreeValues.js";
|
|
|
17
17
|
export { default as evaluate } from "./src/runtime/evaluate.js";
|
|
18
18
|
export * as expressionFunction from "./src/runtime/expressionFunction.js";
|
|
19
19
|
export { default as extname } from "./src/runtime/extname.js";
|
|
20
|
-
export { default as
|
|
20
|
+
export { default as formatError } from "./src/runtime/formatError.js";
|
|
21
21
|
export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"typescript": "5.3.3"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@weborigami/async-tree": "
|
|
14
|
-
"@weborigami/types": "0.0.
|
|
13
|
+
"@weborigami/async-tree": "https://gitpkg.now.sh/WebOrigami/origami/async-tree?e08c2451",
|
|
14
|
+
"@weborigami/types": "0.0.42",
|
|
15
15
|
"peggy": "3.0.2",
|
|
16
16
|
"watcher": "2.3.0"
|
|
17
17
|
},
|
package/src/compiler/compile.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { createExpressionFunction } from "../runtime/expressionFunction.js";
|
|
2
2
|
import { parse } from "./parse.js";
|
|
3
3
|
|
|
4
|
-
function compile(
|
|
4
|
+
function compile(source, startRule) {
|
|
5
|
+
if (typeof source === "string") {
|
|
6
|
+
source = { text: source };
|
|
7
|
+
}
|
|
5
8
|
// Trim whitespace from template blocks before we begin lexing, as our
|
|
6
9
|
// heuristics are non-local and hard to implement in our parser.
|
|
7
|
-
const preprocessed = trimTemplateWhitespace(text);
|
|
8
|
-
const code = parse(preprocessed, {
|
|
10
|
+
const preprocessed = trimTemplateWhitespace(source.text);
|
|
11
|
+
const code = parse(preprocessed, {
|
|
12
|
+
grammarSource: source,
|
|
13
|
+
startRule,
|
|
14
|
+
});
|
|
9
15
|
const fn = createExpressionFunction(code);
|
|
10
16
|
return fn;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
export function expression(
|
|
14
|
-
return compile(
|
|
19
|
+
export function expression(source) {
|
|
20
|
+
return compile(source, "expression");
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
export function templateDocument(
|
|
18
|
-
return compile(
|
|
23
|
+
export function templateDocument(source) {
|
|
24
|
+
return compile(source, "templateDocument");
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
// Trim the whitespace around and in substitution blocks in a template. There's
|
|
@@ -7,7 +7,17 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
import * as ops from "../runtime/ops.js";
|
|
10
|
-
import { makeFunctionCall, makeTemplate } from "./parserHelpers.js";
|
|
10
|
+
import { makeFunctionCall, makePipeline, makeTemplate } from "./parserHelpers.js";
|
|
11
|
+
|
|
12
|
+
// If a parse result is an object that will be evaluated at runtime, attach the
|
|
13
|
+
// location of the source code that produced it for debugging and error messages.
|
|
14
|
+
function annotate(parseResult, location) {
|
|
15
|
+
if (typeof parseResult === "object" && parseResult !== null) {
|
|
16
|
+
parseResult.location = location;
|
|
17
|
+
}
|
|
18
|
+
return parseResult;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
}}
|
|
12
22
|
|
|
13
23
|
// A block of optional whitespace
|
|
@@ -16,11 +26,20 @@ __
|
|
|
16
26
|
|
|
17
27
|
// A filesystem path that begins with a slash: `/foo/bar`
|
|
18
28
|
absoluteFilePath "absolute file path"
|
|
19
|
-
= path:leadingSlashPath {
|
|
29
|
+
= path:leadingSlashPath {
|
|
30
|
+
return annotate([[ops.filesRoot], ...path], location());
|
|
31
|
+
}
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
args "function arguments"
|
|
34
|
+
= parensArgs
|
|
35
|
+
/ path:leadingSlashPath {
|
|
36
|
+
return annotate([ops.traverse, ...path], location());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
array "array"
|
|
40
|
+
= "[" __ list:list? __ closingBracket {
|
|
41
|
+
return annotate([ops.array, ...(list ?? [])], location());
|
|
42
|
+
}
|
|
24
43
|
|
|
25
44
|
// An assignment statement: `foo = 1`
|
|
26
45
|
assignment "tree assignment"
|
|
@@ -28,10 +47,9 @@ assignment "tree assignment"
|
|
|
28
47
|
|
|
29
48
|
assignmentOrShorthand
|
|
30
49
|
= assignment
|
|
31
|
-
/ key:identifier {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
= "[" __ list:list? "]" { return [ops.array, ...(list ?? [])]; }
|
|
50
|
+
/ key:identifier {
|
|
51
|
+
return annotate([key, [ops.inherited, key]], location());
|
|
52
|
+
}
|
|
35
53
|
|
|
36
54
|
// Something that can be called. This is more restrictive than the `expr`
|
|
37
55
|
// parser; it doesn't accept regular function calls.
|
|
@@ -41,10 +59,36 @@ callTarget "function call"
|
|
|
41
59
|
/ object
|
|
42
60
|
/ tree
|
|
43
61
|
/ lambda
|
|
62
|
+
/ parameterizedLambda
|
|
44
63
|
/ protocolCall
|
|
45
64
|
/ group
|
|
46
65
|
/ scopeReference
|
|
47
66
|
|
|
67
|
+
// Required closing curly brace. We use this for the `tree` term: it's the last
|
|
68
|
+
// term in the `step` parser that starts with a curly brace, so if that parser
|
|
69
|
+
// sees a left curly brace, here we must see a right curly brace.
|
|
70
|
+
closingBrace
|
|
71
|
+
= "}"
|
|
72
|
+
/ .? {
|
|
73
|
+
error("Expected right curly brace");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Required closing bracket
|
|
77
|
+
closingBracket
|
|
78
|
+
= "]"
|
|
79
|
+
/ .? {
|
|
80
|
+
error("Expected right bracket");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Required closing parenthesis. We use this for the `group` term: it's the last
|
|
84
|
+
// term in the `step` parser that starts with a parenthesis, so if that parser
|
|
85
|
+
// sees a left parenthesis, here we must see a right parenthesis.
|
|
86
|
+
closingParen
|
|
87
|
+
= ")"
|
|
88
|
+
/ .? {
|
|
89
|
+
error("Expected right parenthesis");
|
|
90
|
+
}
|
|
91
|
+
|
|
48
92
|
// A single line comment
|
|
49
93
|
comment "comment"
|
|
50
94
|
= "#" [^\n\r]*
|
|
@@ -52,6 +96,8 @@ comment "comment"
|
|
|
52
96
|
digits
|
|
53
97
|
= @[0-9]+
|
|
54
98
|
|
|
99
|
+
doubleArrow = "⇒" / "=>"
|
|
100
|
+
|
|
55
101
|
doubleQuoteString "double quote string"
|
|
56
102
|
= '"' chars:doubleQuoteStringChar* '"' { return chars.join(""); }
|
|
57
103
|
|
|
@@ -62,26 +108,8 @@ escapedChar "backslash-escaped character"
|
|
|
62
108
|
= "\\" @.
|
|
63
109
|
|
|
64
110
|
// An Origami expression, no leading/trailing whitespace
|
|
65
|
-
expr
|
|
66
|
-
|
|
67
|
-
// follow (array, object, etc.); we want to parse the largest thing first.
|
|
68
|
-
= implicitParensCall
|
|
69
|
-
/ functionComposition
|
|
70
|
-
// Then try parsers that look for a distinctive token at the start: an opening
|
|
71
|
-
// slash, bracket, curly brace, etc.
|
|
72
|
-
/ absoluteFilePath
|
|
73
|
-
/ array
|
|
74
|
-
/ object
|
|
75
|
-
/ tree
|
|
76
|
-
/ lambda
|
|
77
|
-
/ templateLiteral
|
|
78
|
-
/ group
|
|
79
|
-
/ string
|
|
80
|
-
/ number
|
|
81
|
-
// Protocol calls are distinguished by a colon, but it's not at the start.
|
|
82
|
-
/ protocolCall
|
|
83
|
-
// Least distinctive option is a simple scope reference, so it comes last.
|
|
84
|
-
/ scopeReference
|
|
111
|
+
expr
|
|
112
|
+
= pipeline
|
|
85
113
|
|
|
86
114
|
// Top-level Origami expression, possible leading/trailing whitepsace.
|
|
87
115
|
expression "Origami expression"
|
|
@@ -95,30 +123,36 @@ float "floating-point number"
|
|
|
95
123
|
// Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
|
|
96
124
|
// of function calls, like `fn(arg1)(arg2)(arg3)`.
|
|
97
125
|
functionComposition "function composition"
|
|
98
|
-
= target:callTarget chain:
|
|
126
|
+
= target:callTarget chain:args* end:implicitParensArgs? {
|
|
127
|
+
if (end) {
|
|
128
|
+
chain.push(end);
|
|
129
|
+
}
|
|
130
|
+
return annotate(makeFunctionCall(target, chain), location());
|
|
131
|
+
}
|
|
99
132
|
|
|
100
133
|
// An expression in parentheses: `(foo)`
|
|
101
134
|
group "parenthetical group"
|
|
102
|
-
= "(" __ @expr __
|
|
135
|
+
= "(" __ @expr __ closingParen
|
|
136
|
+
|
|
137
|
+
// A host identifier that may include a colon and port number: `example.com:80`.
|
|
138
|
+
// This is used as a special case at the head of a path, where we want to
|
|
139
|
+
// interpret a colon as part of a text identifier.
|
|
140
|
+
host "HTTP/HTTPS host"
|
|
141
|
+
= identifier (":" number)? { return text(); }
|
|
103
142
|
|
|
104
143
|
identifier "identifier"
|
|
105
144
|
= chars:identifierChar+ { return chars.join(""); }
|
|
106
145
|
|
|
107
146
|
identifierChar
|
|
108
|
-
= [^(){}\[\]
|
|
147
|
+
= [^(){}\[\]<>\-=,/:\`"'\\# →⇒\t\n\r] // No unescaped whitespace or special chars
|
|
148
|
+
/ @'-' !'>' // Accept a hyphen but not in a single arrow combination
|
|
109
149
|
/ escapedChar
|
|
110
150
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
= target:(functionComposition / callTarget) inlineSpace+ args:list {
|
|
114
|
-
return [target, ...args];
|
|
115
|
-
}
|
|
151
|
+
identifierList
|
|
152
|
+
= @identifier|1.., separator| separator?
|
|
116
153
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// interpret a colon as part of a text identifier.
|
|
120
|
-
host "HTTP/HTTPS host"
|
|
121
|
-
= identifier (":" number)? { return text(); }
|
|
154
|
+
implicitParensArgs "arguments with implicit parentheses"
|
|
155
|
+
= inlineSpace+ @list
|
|
122
156
|
|
|
123
157
|
inlineSpace
|
|
124
158
|
= [ \t]
|
|
@@ -130,16 +164,18 @@ integer "integer"
|
|
|
130
164
|
|
|
131
165
|
// A lambda expression: `=foo()`
|
|
132
166
|
lambda "lambda function"
|
|
133
|
-
= "=" __ expr:expr {
|
|
167
|
+
= "=" __ expr:expr {
|
|
168
|
+
return annotate([ops.lambda, null, expr], location());
|
|
169
|
+
}
|
|
134
170
|
|
|
135
171
|
// A path that begins with a slash: `/foo/bar`
|
|
136
172
|
leadingSlashPath "path with a leading slash"
|
|
137
173
|
= "/" @path
|
|
138
|
-
/ "/" { return [""]; }
|
|
174
|
+
/ "/" { return annotate([""], location()); }
|
|
139
175
|
|
|
140
176
|
// A separated list of expressions
|
|
141
177
|
list "list"
|
|
142
|
-
=
|
|
178
|
+
= @expr|1.., separator| separator?
|
|
143
179
|
|
|
144
180
|
newLine
|
|
145
181
|
= "\n"
|
|
@@ -156,13 +192,13 @@ number "number"
|
|
|
156
192
|
// TODO: Use Object.fromEntries with array of key/value pairs
|
|
157
193
|
//
|
|
158
194
|
object "object literal"
|
|
159
|
-
= "{" __ properties:objectProperties? "}" {
|
|
195
|
+
= "{" __ properties:objectProperties? __ "}" {
|
|
196
|
+
return annotate([ops.object, ...(properties ?? [])], location());
|
|
197
|
+
}
|
|
160
198
|
|
|
161
199
|
// A separated list of object properties or shorthands
|
|
162
200
|
objectProperties
|
|
163
|
-
=
|
|
164
|
-
return [head].concat(tail);
|
|
165
|
-
}
|
|
201
|
+
= @objectPropertyOrShorthand|1.., separator| separator?
|
|
166
202
|
|
|
167
203
|
// A single object property with key and value: `x: 1`
|
|
168
204
|
objectProperty "object property"
|
|
@@ -170,23 +206,29 @@ objectProperty "object property"
|
|
|
170
206
|
|
|
171
207
|
objectPropertyOrShorthand
|
|
172
208
|
= objectProperty
|
|
173
|
-
/ key:identifier {
|
|
209
|
+
/ key:identifier {
|
|
210
|
+
return annotate([key, [ops.scope, key]], location());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
parameterizedLambda
|
|
214
|
+
= "(" __ parameters:identifierList? __ ")" __ doubleArrow __ expr:expr {
|
|
215
|
+
return annotate([ops.lambda, parameters ?? [], expr], location());
|
|
216
|
+
}
|
|
174
217
|
|
|
175
218
|
// Function arguments in parentheses
|
|
176
219
|
parensArgs "function arguments in parentheses"
|
|
177
|
-
= "(" __ list:list? ")" {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
= __ "," __
|
|
181
|
-
/ whitespaceWithNewLine
|
|
220
|
+
= "(" __ list:list? __ ")" {
|
|
221
|
+
return list ?? annotate([undefined], location());
|
|
222
|
+
}
|
|
182
223
|
|
|
183
|
-
|
|
184
|
-
=
|
|
224
|
+
pipeline
|
|
225
|
+
= steps:(@step|1.., __ singleArrow __ |) {
|
|
226
|
+
return annotate(makePipeline(steps), location());
|
|
227
|
+
}
|
|
185
228
|
|
|
186
229
|
// A slash-separated path of keys
|
|
187
230
|
path "slash-separated path"
|
|
188
|
-
=
|
|
189
|
-
/ key:pathKey { return [key]; }
|
|
231
|
+
= pathKey|1.., "/"|
|
|
190
232
|
|
|
191
233
|
// A single key in a slash-separated path
|
|
192
234
|
pathKey "path element"
|
|
@@ -196,7 +238,7 @@ pathKey "path element"
|
|
|
196
238
|
// There can be zero, one, or two slashes after the colon.
|
|
197
239
|
protocolCall "function call using protocol: syntax"
|
|
198
240
|
= protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
|
|
199
|
-
return [protocol, host, ...(path ?? [])];
|
|
241
|
+
return annotate([protocol, host, ...(path ?? [])], location());
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
protocol "protocol"
|
|
@@ -212,7 +254,18 @@ reservedProtocol "reserved protocol"
|
|
|
212
254
|
/ "tree" { return ops.treeHttps; } // Alias
|
|
213
255
|
|
|
214
256
|
scopeReference "scope reference"
|
|
215
|
-
= key:identifier {
|
|
257
|
+
= key:identifier {
|
|
258
|
+
return annotate([ops.scope, key], location());
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
separator
|
|
262
|
+
= __ "," __
|
|
263
|
+
/ whitespaceWithNewLine
|
|
264
|
+
|
|
265
|
+
sign
|
|
266
|
+
= [+\-]
|
|
267
|
+
|
|
268
|
+
singleArrow = "→" / "->"
|
|
216
269
|
|
|
217
270
|
singleQuoteString "single quote string"
|
|
218
271
|
= "'" chars:singleQuoteStringChar* "'" { return chars.join(""); }
|
|
@@ -220,6 +273,29 @@ singleQuoteString "single quote string"
|
|
|
220
273
|
singleQuoteStringChar
|
|
221
274
|
= !("'" / newLine) @textChar
|
|
222
275
|
|
|
276
|
+
// A single step in a pipeline, or a top-level expression
|
|
277
|
+
step
|
|
278
|
+
// Literals that can't start a function call
|
|
279
|
+
= number
|
|
280
|
+
// Try functions next; they can start with expression types that follow
|
|
281
|
+
// (array, object, etc.), and we want to parse the larger thing first.
|
|
282
|
+
/ functionComposition
|
|
283
|
+
// Then try parsers that look for a distinctive token at the start: an opening
|
|
284
|
+
// slash, bracket, curly brace, etc.
|
|
285
|
+
/ absoluteFilePath
|
|
286
|
+
/ array
|
|
287
|
+
/ object
|
|
288
|
+
/ tree
|
|
289
|
+
/ lambda
|
|
290
|
+
/ parameterizedLambda
|
|
291
|
+
/ templateLiteral
|
|
292
|
+
/ string
|
|
293
|
+
/ group
|
|
294
|
+
// Protocol calls are distinguished by a colon, but it's not at the start.
|
|
295
|
+
/ protocolCall
|
|
296
|
+
// Least distinctive option is a simple scope reference, so it comes last.
|
|
297
|
+
/ scopeReference
|
|
298
|
+
|
|
223
299
|
start
|
|
224
300
|
= number
|
|
225
301
|
|
|
@@ -230,7 +306,9 @@ string "string"
|
|
|
230
306
|
// A top-level document defining a template. This is the same as a template
|
|
231
307
|
// literal, but can contain backticks at the top level.
|
|
232
308
|
templateDocument "template"
|
|
233
|
-
= contents:templateDocumentContents {
|
|
309
|
+
= contents:templateDocumentContents {
|
|
310
|
+
return annotate([ops.lambda, null, contents], location());
|
|
311
|
+
}
|
|
234
312
|
|
|
235
313
|
// Template documents can contain backticks at the top level.
|
|
236
314
|
templateDocumentChar
|
|
@@ -238,7 +316,9 @@ templateDocumentChar
|
|
|
238
316
|
|
|
239
317
|
// The contents of a template document containing plain text and substitutions
|
|
240
318
|
templateDocumentContents
|
|
241
|
-
= parts:(templateDocumentText / templateSubstitution)* {
|
|
319
|
+
= parts:(templateDocumentText / templateSubstitution)* {
|
|
320
|
+
return annotate(makeTemplate(parts), location());
|
|
321
|
+
}
|
|
242
322
|
|
|
243
323
|
templateDocumentText "template text"
|
|
244
324
|
= chars:templateDocumentChar+ { return chars.join(""); }
|
|
@@ -252,7 +332,9 @@ templateLiteralChar
|
|
|
252
332
|
|
|
253
333
|
// The contents of a template literal containing plain text and substitutions
|
|
254
334
|
templateLiteralContents
|
|
255
|
-
= parts:(templateLiteralText / templateSubstitution)* {
|
|
335
|
+
= parts:(templateLiteralText / templateSubstitution)* {
|
|
336
|
+
return annotate(makeTemplate(parts), location());
|
|
337
|
+
}
|
|
256
338
|
|
|
257
339
|
// Plain text in a template literal
|
|
258
340
|
templateLiteralText
|
|
@@ -267,13 +349,13 @@ textChar
|
|
|
267
349
|
|
|
268
350
|
// A tree literal: `{ index.html = "Hello" }`
|
|
269
351
|
tree "tree literal"
|
|
270
|
-
= "{" __ assignments:treeAssignments?
|
|
352
|
+
= "{" __ assignments:treeAssignments? __ closingBrace {
|
|
353
|
+
return annotate([ops.tree, ...(assignments ?? [])], location());
|
|
354
|
+
}
|
|
271
355
|
|
|
272
356
|
// A separated list of assignments or shorthands
|
|
273
357
|
treeAssignments
|
|
274
|
-
=
|
|
275
|
-
return [head].concat(tail);
|
|
276
|
-
}
|
|
358
|
+
= @assignmentOrShorthand|1.., separator| separator?
|
|
277
359
|
|
|
278
360
|
whitespaceWithNewLine
|
|
279
361
|
= inlineSpace* comment? newLine __
|