@rohal12/spindle 0.52.3 → 0.52.4
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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +267 -624
- package/dist/pkg/story-variables.js +155 -469
- package/package.json +1 -1
- package/src/components/macros/Widget.tsx +18 -7
- package/src/index.tsx +5 -8
- package/src/interpolation.ts +16 -22
- package/src/js-lexer.ts +20 -9
- package/src/markup/ast.ts +108 -147
- package/src/markup/code-attributes.ts +4 -6
- package/src/markup/tokenizer.ts +218 -574
package/package.json
CHANGED
|
@@ -5,12 +5,20 @@ import { defineMacro } from '../../define-macro';
|
|
|
5
5
|
import { checkVariableName } from '../../utils/namespace';
|
|
6
6
|
import { MacroError } from './MacroError';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Read the arguments of a {widget} definition: its name (quotes dropped)
|
|
10
|
+
* and its parameters, the words after it that start with one of the
|
|
11
|
+
* characters of `sigils`.
|
|
12
|
+
*/
|
|
13
|
+
export function parseWidgetDef(
|
|
14
|
+
rawArgs: string,
|
|
15
|
+
sigils = '@',
|
|
16
|
+
): { name: string; params: string[] } {
|
|
17
|
+
const [first, ...rest] = rawArgs.trim().split(/\s+/);
|
|
18
|
+
return {
|
|
19
|
+
name: first!.replace(/["']/g, ''),
|
|
20
|
+
params: rest.filter((word) => sigils.includes(word[0]!)),
|
|
21
|
+
};
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
defineMacro({
|
|
@@ -20,7 +28,10 @@ defineMacro({
|
|
|
20
28
|
let parsed: ReturnType<typeof parseWidgetDef> | undefined;
|
|
21
29
|
let error: unknown;
|
|
22
30
|
try {
|
|
23
|
-
|
|
31
|
+
// Refuse `@` parameters that no namespace can hold
|
|
32
|
+
const def = parseWidgetDef(rawArgs);
|
|
33
|
+
for (const param of def.params) checkVariableName(param.slice(1), param);
|
|
34
|
+
parsed = def;
|
|
24
35
|
} catch (err) {
|
|
25
36
|
error = err;
|
|
26
37
|
}
|
package/src/index.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import { tokenize } from './markup/tokenizer';
|
|
|
21
21
|
import { buildAST, registerBlockMacro } from './markup/ast';
|
|
22
22
|
import { registerWidget } from './widgets/widget-registry';
|
|
23
23
|
import { astContainsChildren } from './widgets/ast-scanner';
|
|
24
|
+
import { parseWidgetDef } from './components/macros/Widget';
|
|
24
25
|
import { errorMessage } from './utils/error-message';
|
|
25
26
|
import type { ASTNode } from './markup/ast';
|
|
26
27
|
import './macros/register-builtins';
|
|
@@ -173,14 +174,10 @@ export function boot() {
|
|
|
173
174
|
const widgetAST = buildAST(widgetTokens);
|
|
174
175
|
for (const node of widgetAST) {
|
|
175
176
|
if (node.type === 'macro' && node.name === 'widget' && node.rawArgs) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.filter(
|
|
181
|
-
(t) =>
|
|
182
|
-
t.startsWith('$') || t.startsWith('_') || t.startsWith('@'),
|
|
183
|
-
);
|
|
177
|
+
const { name: widgetName, params } = parseWidgetDef(
|
|
178
|
+
node.rawArgs,
|
|
179
|
+
'$_@',
|
|
180
|
+
);
|
|
184
181
|
const children = node.children as ASTNode[];
|
|
185
182
|
const isBlock = astContainsChildren(children);
|
|
186
183
|
try {
|
package/src/interpolation.ts
CHANGED
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
* string, so it is reported instead of being run or shown as source.
|
|
15
15
|
*/
|
|
16
16
|
import { evaluate } from './expression';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
SCOPE_SIGILS,
|
|
19
|
+
SIGIL_SCOPES,
|
|
20
|
+
tokenize,
|
|
21
|
+
type VariableScope,
|
|
22
|
+
} from './markup/tokenizer';
|
|
23
|
+
import type { Sigil } from './js-lexer';
|
|
18
24
|
import { buildAST } from './markup/ast';
|
|
19
25
|
import type { ASTNode, MacroNode, TextNode } from './markup/ast';
|
|
20
26
|
import { getMacro, getMacroText, isSubMacro } from './registry';
|
|
@@ -116,15 +122,6 @@ function display(value: unknown): string {
|
|
|
116
122
|
return value == null ? '' : String(value);
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
type VariableKind = 'variable' | 'temporary' | 'local' | 'transient';
|
|
120
|
-
|
|
121
|
-
const KIND_SIGILS = {
|
|
122
|
-
variable: '$',
|
|
123
|
-
temporary: '_',
|
|
124
|
-
local: '@',
|
|
125
|
-
transient: '%',
|
|
126
|
-
} as const;
|
|
127
|
-
|
|
128
125
|
/**
|
|
129
126
|
* The value of a variable reference (`name` without its sigil, with an
|
|
130
127
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -133,12 +130,12 @@ const KIND_SIGILS = {
|
|
|
133
130
|
*/
|
|
134
131
|
function resolveVariable(
|
|
135
132
|
scope: TextScope,
|
|
136
|
-
kind:
|
|
133
|
+
kind: VariableScope,
|
|
137
134
|
name: string,
|
|
138
135
|
): unknown {
|
|
139
136
|
const parts = name.split('.');
|
|
140
137
|
const root = parts[0]!;
|
|
141
|
-
checkVariableName(root,
|
|
138
|
+
checkVariableName(root, SCOPE_SIGILS[kind] + root);
|
|
142
139
|
const store =
|
|
143
140
|
kind === 'variable'
|
|
144
141
|
? scope.variables
|
|
@@ -210,12 +207,16 @@ class TextEvaluator {
|
|
|
210
207
|
}
|
|
211
208
|
|
|
212
209
|
/** A variable reference, or '' and an error if it can't be one. */
|
|
213
|
-
private variable(
|
|
210
|
+
private variable(
|
|
211
|
+
kind: VariableScope,
|
|
212
|
+
name: string,
|
|
213
|
+
scope: TextScope,
|
|
214
|
+
): string {
|
|
214
215
|
try {
|
|
215
216
|
return display(resolveVariable(scope, kind, name));
|
|
216
217
|
} catch (error) {
|
|
217
218
|
// Reported as passage text reports it: `{$__proto__ error: …}`
|
|
218
|
-
this.errors.push({ macro:
|
|
219
|
+
this.errors.push({ macro: SCOPE_SIGILS[kind] + name, error });
|
|
219
220
|
return '';
|
|
220
221
|
}
|
|
221
222
|
}
|
|
@@ -341,13 +342,6 @@ export function interpolateText(
|
|
|
341
342
|
return renderText(parsed.nodes, scope);
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
const SIGIL_KINDS = {
|
|
345
|
-
$: 'variable',
|
|
346
|
-
_: 'temporary',
|
|
347
|
-
'@': 'local',
|
|
348
|
-
'%': 'transient',
|
|
349
|
-
} as const;
|
|
350
|
-
|
|
351
345
|
/**
|
|
352
346
|
* Resolve the sigil references in the value of a code attribute (see
|
|
353
347
|
* isCodeAttribute): `{$x}`, `{_x.y}`, `{$n + 1}`. Everything else, other
|
|
@@ -367,7 +361,7 @@ export function interpolateCode(
|
|
|
367
361
|
} else if ('verbatim' in part) {
|
|
368
362
|
text += part.verbatim;
|
|
369
363
|
} else if (/^[$_@%][\w.]+$/.test(part.expr)) {
|
|
370
|
-
const kind =
|
|
364
|
+
const kind = SIGIL_SCOPES[part.expr[0] as Sigil];
|
|
371
365
|
try {
|
|
372
366
|
text += display(resolveVariable(scope, kind, part.expr.slice(1)));
|
|
373
367
|
} catch (error) {
|
package/src/js-lexer.ts
CHANGED
|
@@ -145,15 +145,14 @@ function scanRegex(
|
|
|
145
145
|
};
|
|
146
146
|
let i = start + 1;
|
|
147
147
|
while (i < src.length) {
|
|
148
|
+
// Unterminated at the end of the line: leave the rest to the parser
|
|
149
|
+
const lineEnd = regexLineEnd(src, i);
|
|
150
|
+
if (lineEnd !== -1) return done(lineEnd, false);
|
|
148
151
|
const c = src.charAt(i);
|
|
149
152
|
if (c === '\\') {
|
|
150
|
-
// An escaped line break ends the line, and the regex, too
|
|
151
|
-
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return done(i + 1, false);
|
|
152
153
|
i += 2;
|
|
153
154
|
continue;
|
|
154
155
|
}
|
|
155
|
-
// Unterminated at the end of the line: leave the rest to the parser
|
|
156
|
-
if (LINE_TERMINATOR_RE.test(c)) return done(i, false);
|
|
157
156
|
if (c === '[') {
|
|
158
157
|
// A class, where `/` does not close
|
|
159
158
|
i = scanRegexClass(src, i, cache);
|
|
@@ -191,16 +190,17 @@ function scanRegexClass(
|
|
|
191
190
|
const opens = [open];
|
|
192
191
|
let i = open + 1;
|
|
193
192
|
while (i < src.length) {
|
|
193
|
+
const lineEnd = regexLineEnd(src, i);
|
|
194
|
+
if (lineEnd !== -1) {
|
|
195
|
+
i = lineEnd;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
194
198
|
const c = src.charAt(i);
|
|
195
199
|
if (c === '\\') {
|
|
196
|
-
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) {
|
|
197
|
-
i++;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
200
|
i += 2;
|
|
201
201
|
continue;
|
|
202
202
|
}
|
|
203
|
-
if (c === ']'
|
|
203
|
+
if (c === ']') break;
|
|
204
204
|
if (c === '[' && cache) opens.push(i);
|
|
205
205
|
i++;
|
|
206
206
|
}
|
|
@@ -209,6 +209,17 @@ function scanRegexClass(
|
|
|
209
209
|
return i;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Index of the line break at `i`, escaped (`\` then a line break) or not,
|
|
214
|
+
* where a regex literal or class ends unterminated; -1 if there is none.
|
|
215
|
+
*/
|
|
216
|
+
function regexLineEnd(src: string, i: number): number {
|
|
217
|
+
const c = src.charAt(i);
|
|
218
|
+
if (LINE_TERMINATOR_RE.test(c)) return i;
|
|
219
|
+
if (c === '\\' && LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return i + 1;
|
|
220
|
+
return -1;
|
|
221
|
+
}
|
|
222
|
+
|
|
212
223
|
/**
|
|
213
224
|
* The next index from `from` on where `find` matches (-1 for none), for
|
|
214
225
|
* searches from increasing positions: a search from within the stretch the
|
package/src/markup/ast.ts
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
withSelectors,
|
|
3
|
+
type HtmlToken,
|
|
4
|
+
type MacroToken,
|
|
5
|
+
type Selectors,
|
|
6
|
+
type Token,
|
|
7
|
+
type VariableScope,
|
|
8
|
+
} from './tokenizer';
|
|
2
9
|
|
|
3
10
|
export interface TextNode {
|
|
4
11
|
type: 'text';
|
|
5
12
|
value: string;
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
export interface VariableNode {
|
|
15
|
+
export interface VariableNode extends Selectors {
|
|
9
16
|
type: 'variable';
|
|
10
17
|
name: string;
|
|
11
|
-
scope:
|
|
12
|
-
className?: string;
|
|
13
|
-
id?: string;
|
|
18
|
+
scope: VariableScope;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
export interface ExpressionNode {
|
|
21
|
+
export interface ExpressionNode extends Selectors {
|
|
17
22
|
type: 'expression';
|
|
18
23
|
expression: string;
|
|
19
|
-
className?: string;
|
|
20
|
-
id?: string;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
export interface Branch {
|
|
26
|
+
export interface Branch extends Selectors {
|
|
24
27
|
rawArgs: string;
|
|
25
|
-
className?: string;
|
|
26
|
-
id?: string;
|
|
27
28
|
children: ASTNode[];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export interface MacroNode {
|
|
31
|
+
export interface MacroNode extends Selectors {
|
|
31
32
|
type: 'macro';
|
|
32
33
|
name: string;
|
|
33
34
|
rawArgs: string;
|
|
34
35
|
children: ASTNode[];
|
|
35
36
|
branches?: Branch[];
|
|
36
|
-
className?: string;
|
|
37
|
-
id?: string;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
export interface HtmlNode {
|
|
@@ -99,6 +98,24 @@ function quoteArg(value: string): string {
|
|
|
99
98
|
return `"${value.replace(/[\\"]/g, '\\$&')}"`;
|
|
100
99
|
}
|
|
101
100
|
|
|
101
|
+
/** A new branch for the macro token opening it, with its selectors. */
|
|
102
|
+
function newBranch(token: MacroToken): Branch {
|
|
103
|
+
return withSelectors<Branch>({ rawArgs: token.rawArgs, children: [] }, token);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A macro or HTML element, or a token opening or closing one. */
|
|
107
|
+
type Tagged = MacroNode | HtmlNode | MacroToken | HtmlToken;
|
|
108
|
+
|
|
109
|
+
/** The name of a macro or HTML element. */
|
|
110
|
+
function tagName(tagged: Tagged): string {
|
|
111
|
+
return tagged.type === 'html' ? tagged.tag : tagged.name;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The tag closing a macro or HTML element, as written in markup. */
|
|
115
|
+
function closerOf(tagged: Tagged): string {
|
|
116
|
+
return tagged.type === 'html' ? `</${tagged.tag}>` : `{/${tagged.name}}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/**
|
|
103
120
|
* Build an AST from a token array. Block macros are nested into trees
|
|
104
121
|
* using a stack. Throws on unclosed or mismatched macros.
|
|
@@ -119,6 +136,32 @@ export function buildAST(tokens: Token[]): ASTNode[] {
|
|
|
119
136
|
return top.children;
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Pop the node on top of the stack, which the closing `token` (`</b>`,
|
|
141
|
+
* `{/if}`) must close. Names match ignoring case.
|
|
142
|
+
*/
|
|
143
|
+
function close(token: MacroToken | HtmlToken) {
|
|
144
|
+
const found = closerOf(token);
|
|
145
|
+
if (stack.length === 0) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unexpected closing ${found} (at character ${token.start})`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const top = stack[stack.length - 1]!;
|
|
152
|
+
if (
|
|
153
|
+
top.node.type !== token.type ||
|
|
154
|
+
tagName(top.node).toLowerCase() !== tagName(token).toLowerCase()
|
|
155
|
+
) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Expected ${closerOf(top.node)} but found ${found} (at character ${token.start})`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stack.pop();
|
|
162
|
+
current().push(top.node);
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
for (const token of tokens) {
|
|
123
166
|
switch (token.type) {
|
|
124
167
|
case 'text':
|
|
@@ -127,120 +170,64 @@ export function buildAST(tokens: Token[]): ASTNode[] {
|
|
|
127
170
|
|
|
128
171
|
case 'link': {
|
|
129
172
|
const rawArgs = `${quoteArg(token.display)} ${quoteArg(token.target)}`;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (token.className) macroNode.className = token.className;
|
|
137
|
-
if (token.id) macroNode.id = token.id;
|
|
138
|
-
current().push(macroNode);
|
|
173
|
+
current().push(
|
|
174
|
+
withSelectors<MacroNode>(
|
|
175
|
+
{ type: 'macro', name: 'link', rawArgs, children: [] },
|
|
176
|
+
token,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
139
179
|
break;
|
|
140
180
|
}
|
|
141
181
|
|
|
142
|
-
case 'variable':
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (token.id) varNode.id = token.id;
|
|
150
|
-
current().push(varNode);
|
|
182
|
+
case 'variable':
|
|
183
|
+
current().push(
|
|
184
|
+
withSelectors<VariableNode>(
|
|
185
|
+
{ type: 'variable', name: token.name, scope: token.scope },
|
|
186
|
+
token,
|
|
187
|
+
),
|
|
188
|
+
);
|
|
151
189
|
break;
|
|
152
|
-
}
|
|
153
190
|
|
|
154
|
-
case 'expression':
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
current().push(exprNode);
|
|
191
|
+
case 'expression':
|
|
192
|
+
current().push(
|
|
193
|
+
withSelectors<ExpressionNode>(
|
|
194
|
+
{ type: 'expression', expression: token.expression },
|
|
195
|
+
token,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
162
198
|
break;
|
|
163
|
-
}
|
|
164
199
|
|
|
165
200
|
case 'html': {
|
|
166
|
-
if (token.isSelfClose) {
|
|
167
|
-
// Self-closing HTML tag (br, hr, img, etc.)
|
|
168
|
-
current().push({
|
|
169
|
-
type: 'html',
|
|
170
|
-
tag: token.tag,
|
|
171
|
-
attributes: token.attributes,
|
|
172
|
-
children: [],
|
|
173
|
-
});
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (token.isClose) {
|
|
178
|
-
// Closing HTML tag — pop from stack
|
|
179
|
-
if (stack.length === 0) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`Unexpected closing </${token.tag}> (at character ${token.start})`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const top = stack[stack.length - 1]!;
|
|
186
|
-
if (
|
|
187
|
-
top.node.type !== 'html' ||
|
|
188
|
-
top.node.tag.toLowerCase() !== token.tag.toLowerCase()
|
|
189
|
-
) {
|
|
190
|
-
const expected =
|
|
191
|
-
top.node.type === 'html'
|
|
192
|
-
? `</${top.node.tag}>`
|
|
193
|
-
: `{/${top.node.name}}`;
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Expected ${expected} but found </${token.tag}> (at character ${token.start})`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
stack.pop();
|
|
200
|
-
current().push(top.node);
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Opening HTML tag — push onto stack
|
|
205
201
|
const htmlNode: HtmlNode = {
|
|
206
202
|
type: 'html',
|
|
207
203
|
tag: token.tag,
|
|
208
204
|
attributes: token.attributes,
|
|
209
205
|
children: [],
|
|
210
206
|
};
|
|
211
|
-
|
|
207
|
+
if (token.isSelfClose) {
|
|
208
|
+
// Self-closing HTML tag (br, hr, img, etc.)
|
|
209
|
+
current().push(htmlNode);
|
|
210
|
+
} else if (token.isClose) {
|
|
211
|
+
// Closing HTML tag — pop from stack
|
|
212
|
+
close(token);
|
|
213
|
+
} else {
|
|
214
|
+
// Opening HTML tag — push onto stack
|
|
215
|
+
stack.push({ node: htmlNode, start: token.start });
|
|
216
|
+
}
|
|
212
217
|
break;
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
case 'macro': {
|
|
216
|
-
// Normalize macro name to lowercase — registration is lowercase,
|
|
217
|
-
// but the tokenizer preserves original casing from passage markup.
|
|
218
|
-
const name = token.name.toLowerCase();
|
|
219
|
-
|
|
220
221
|
if (token.isClose) {
|
|
221
222
|
// Closing tag — pop from stack
|
|
222
|
-
|
|
223
|
-
throw new Error(
|
|
224
|
-
`Unexpected closing {/${token.name}} (at character ${token.start})`,
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const top = stack[stack.length - 1]!;
|
|
229
|
-
if (top.node.type !== 'macro' || top.node.name !== name) {
|
|
230
|
-
const expected =
|
|
231
|
-
top.node.type === 'macro'
|
|
232
|
-
? `{/${top.node.name}}`
|
|
233
|
-
: `</${top.node.tag}>`;
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Expected ${expected} but found {/${token.name}} (at character ${token.start})`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
stack.pop();
|
|
240
|
-
current().push(top.node);
|
|
223
|
+
close(token);
|
|
241
224
|
break;
|
|
242
225
|
}
|
|
243
226
|
|
|
227
|
+
// Normalize macro name to lowercase — registration is lowercase,
|
|
228
|
+
// but the tokenizer preserves original casing from passage markup.
|
|
229
|
+
const name = token.name.toLowerCase();
|
|
230
|
+
|
|
244
231
|
// Handle branch macros (elseif/else, case/default, next)
|
|
245
232
|
const expectedParent = Object.prototype.hasOwnProperty.call(
|
|
246
233
|
BRANCH_PARENT,
|
|
@@ -261,55 +248,29 @@ export function buildAST(tokens: Token[]): ASTNode[] {
|
|
|
261
248
|
);
|
|
262
249
|
}
|
|
263
250
|
|
|
264
|
-
|
|
265
|
-
rawArgs: token.rawArgs,
|
|
266
|
-
children: [],
|
|
267
|
-
};
|
|
268
|
-
if (token.className) branch.className = token.className;
|
|
269
|
-
if (token.id) branch.id = token.id;
|
|
270
|
-
topNode.branches!.push(branch);
|
|
251
|
+
topNode.branches!.push(newBranch(token));
|
|
271
252
|
break;
|
|
272
253
|
}
|
|
273
254
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
children: [],
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// Branching blocks: className/id goes on the first branch, not the node
|
|
284
|
-
if (BRANCHING_BLOCK_MACROS.has(name)) {
|
|
285
|
-
const firstBranch: Branch = {
|
|
286
|
-
rawArgs: token.rawArgs,
|
|
287
|
-
children: [],
|
|
288
|
-
};
|
|
289
|
-
if (token.className) firstBranch.className = token.className;
|
|
290
|
-
if (token.id) firstBranch.id = token.id;
|
|
291
|
-
node.branches = [firstBranch];
|
|
292
|
-
} else {
|
|
293
|
-
if (token.className) node.className = token.className;
|
|
294
|
-
if (token.id) node.id = token.id;
|
|
295
|
-
}
|
|
255
|
+
const node: MacroNode = {
|
|
256
|
+
type: 'macro',
|
|
257
|
+
name,
|
|
258
|
+
rawArgs: token.rawArgs,
|
|
259
|
+
children: [],
|
|
260
|
+
};
|
|
296
261
|
|
|
297
|
-
|
|
262
|
+
if (!BLOCK_MACROS.has(name)) {
|
|
263
|
+
// Self-closing macro (set, print, etc.)
|
|
264
|
+
current().push(withSelectors(node, token));
|
|
298
265
|
break;
|
|
299
266
|
}
|
|
300
267
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
children: [],
|
|
308
|
-
};
|
|
309
|
-
if (token.className) macroNode.className = token.className;
|
|
310
|
-
if (token.id) macroNode.id = token.id;
|
|
311
|
-
current().push(macroNode);
|
|
312
|
-
}
|
|
268
|
+
// Block macro — push onto stack. Branching blocks: className/id
|
|
269
|
+
// goes on the first branch, not the node
|
|
270
|
+
if (BRANCHING_BLOCK_MACROS.has(name))
|
|
271
|
+
node.branches = [newBranch(token)];
|
|
272
|
+
else withSelectors(node, token);
|
|
273
|
+
stack.push({ node, start: token.start });
|
|
313
274
|
break;
|
|
314
275
|
}
|
|
315
276
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createScanMemo, scanBalancedBrace } from './tokenizer';
|
|
1
|
+
import { createScanMemo, isSigil, scanBalancedBrace } from './tokenizer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Attributes whose value is code with braces of its own, not text: event
|
|
@@ -41,11 +41,9 @@ export function splitSigilTemplate(template: string): SigilPart[] {
|
|
|
41
41
|
}
|
|
42
42
|
text += template.slice(i, brace);
|
|
43
43
|
i = brace + 1;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
? scanBalancedBrace(template, i, memo)
|
|
48
|
-
: -1;
|
|
44
|
+
const end = isSigil(template[i])
|
|
45
|
+
? scanBalancedBrace(template, i, memo)
|
|
46
|
+
: -1;
|
|
49
47
|
if (end === -1) {
|
|
50
48
|
text += '{';
|
|
51
49
|
continue;
|