@rohal12/spindle 0.57.0 → 0.59.0
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 +773 -493
- package/dist/pkg/macro-registry.json +29 -11
- package/dist/pkg/story-variables.js +804 -268
- package/dist/pkg/tooling.js +39 -2
- package/package.json +1 -1
- package/src/code-check.ts +215 -51
- package/src/components/macros/Button.tsx +3 -1
- package/src/components/macros/Checkbox.tsx +4 -1
- package/src/components/macros/Dialog.tsx +1 -1
- package/src/components/macros/Include.tsx +2 -6
- package/src/components/macros/MacroLink.tsx +4 -1
- package/src/components/macros/Meter.tsx +1 -1
- package/src/components/macros/Radiobutton.tsx +2 -2
- package/src/components/macros/Watch.tsx +11 -6
- package/src/components/macros/include-args.ts +23 -0
- package/src/components/macros/input-macro.ts +4 -1
- package/src/components/macros/macro-args.ts +119 -46
- package/src/components/macros/option-utils.ts +10 -4
- package/src/expression.ts +4 -54
- package/src/index.tsx +2 -4
- package/src/js-lexer.ts +19 -0
- package/src/markup/grammar-shared.ts +8 -0
- package/src/markup/parse.ts +107 -2
- package/src/markup/spindle.d.peggy.ts +5 -2
- package/src/markup/spindle.peggy +27 -16
- package/src/markup/tokens.ts +3 -0
- package/src/markup/validate.ts +22 -12
- package/src/registry.ts +70 -6
- package/src/story-variables.ts +52 -10
- package/src/tooling.ts +63 -5
- package/src/transform.ts +65 -0
- package/src/widgets/widget-def.ts +2 -2
- package/types/index.d.ts +23 -0
- package/types/tooling.d.ts +337 -0
package/src/markup/spindle.peggy
CHANGED
|
@@ -18,41 +18,48 @@
|
|
|
18
18
|
// isBlock(name), isRaw(name) whether a macro takes a body / a raw body
|
|
19
19
|
// isCodeAttribute(name) whether an attribute value is code (onclick)
|
|
20
20
|
//
|
|
21
|
+
// SIGIL_SCOPES and withSelectors come from tokens.ts (see grammar-shared.ts)
|
|
21
22
|
// Note: Peggy's code blocks count braces without reading JavaScript strings,
|
|
22
23
|
// so the code here writes literal braces as LB and RB.
|
|
23
24
|
|
|
24
25
|
{{
|
|
25
26
|
const LB = String.fromCharCode(123);
|
|
26
27
|
const RB = String.fromCharCode(125);
|
|
27
|
-
const
|
|
28
|
+
const { SIGIL_SCOPES, withSelectors } = shared;
|
|
28
29
|
const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
29
30
|
const BRANCH_PARENT = { elseif: 'if', else: 'if', case: 'switch', default: 'switch', next: 'timed' };
|
|
30
31
|
const BRANCHING = new Set(['if', 'switch', 'timed']);
|
|
31
32
|
const LINK_SEPARATORS = [['|', false], ['->', false], ['<-', true]];
|
|
32
33
|
const NO_SELECTORS = {};
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* A link's display text and target, from its text `inner` (at `at` in the
|
|
37
|
+
* input), with where the target is written.
|
|
38
|
+
*/
|
|
39
|
+
function parseLink(inner, at) {
|
|
40
|
+
/** The trimmed text from `start` to `end` of inner, and where it is. */
|
|
41
|
+
const part = (start, end) => {
|
|
42
|
+
const raw = inner.slice(start, end);
|
|
43
|
+
const value = raw.trim();
|
|
44
|
+
const from = at + start + (value ? raw.indexOf(value) : 0);
|
|
45
|
+
return { value, start: from, end: from + value.length };
|
|
46
|
+
};
|
|
35
47
|
for (const [separator, targetFirst] of LINK_SEPARATORS) {
|
|
36
48
|
const idx = inner.indexOf(separator);
|
|
37
49
|
if (idx === -1) continue;
|
|
38
|
-
const before =
|
|
39
|
-
const after =
|
|
40
|
-
|
|
50
|
+
const before = part(0, idx);
|
|
51
|
+
const after = part(idx + separator.length, inner.length);
|
|
52
|
+
const [display, target] = targetFirst ? [after, before] : [before, after];
|
|
53
|
+
return { display: display.value, target: target.value, targetStart: target.start, targetEnd: target.end };
|
|
41
54
|
}
|
|
42
|
-
const
|
|
43
|
-
return { display:
|
|
55
|
+
const whole = part(0, inner.length);
|
|
56
|
+
return { display: whole.value, target: whole.value, targetStart: whole.start, targetEnd: whole.end };
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
function quoteArg(value) {
|
|
47
60
|
return '"' + value.replace(/[\\"]/g, '\\$&') + '"';
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
function withSelectors(target, from) {
|
|
51
|
-
if (from.className) target.className = from.className;
|
|
52
|
-
if (from.id) target.id = from.id;
|
|
53
|
-
return target;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
63
|
function selectorsOf(parts) {
|
|
57
64
|
const classes = [];
|
|
58
65
|
let id = '';
|
|
@@ -111,7 +118,7 @@ function tokenOf(x) {
|
|
|
111
118
|
case 'text':
|
|
112
119
|
return { type: 'text', value: x.value, ...span };
|
|
113
120
|
case 'link':
|
|
114
|
-
return withSelectors({ type: 'link', display: x.display, target: x.target, ...span }, x);
|
|
121
|
+
return withSelectors({ type: 'link', display: x.display, target: x.target, targetStart: x.targetStart, targetEnd: x.targetEnd, ...span }, x);
|
|
115
122
|
case 'variable':
|
|
116
123
|
return withSelectors({ type: 'variable', name: x.name, scope: x.scope, ...span }, x);
|
|
117
124
|
case 'expression':
|
|
@@ -341,8 +348,8 @@ Literal
|
|
|
341
348
|
/ "[" { return lexeme('text', location(), { value: '[' }); }
|
|
342
349
|
|
|
343
350
|
LinkLexeme
|
|
344
|
-
= &{ return !textMode; } "[[" s:Selectors inner:$LinkText "]]" {
|
|
345
|
-
return withSelectors(lexeme('link', location(), parseLink(inner)), s);
|
|
351
|
+
= &{ return !textMode; } "[[" s:Selectors at:Here inner:$LinkText "]]" {
|
|
352
|
+
return withSelectors(lexeme('link', location(), parseLink(inner, at)), s);
|
|
346
353
|
}
|
|
347
354
|
/ &{ return !textMode; } "[[" { fail('Unclosed link: [[ without ]]', location()); }
|
|
348
355
|
|
|
@@ -359,6 +366,10 @@ Selectors
|
|
|
359
366
|
= parts:SelectorPart+ " "? { return selectorsOf(parts); }
|
|
360
367
|
/ "" { return NO_SELECTORS; }
|
|
361
368
|
|
|
369
|
+
/** The selectors at the start of the input, and where they end. */
|
|
370
|
+
SelectorsPrefix
|
|
371
|
+
= s:Selectors end:Here .* { return { ...s, end }; }
|
|
372
|
+
|
|
362
373
|
SelectorPart
|
|
363
374
|
= prefix:[.#] name:$SelectorChar* { return [prefix, name]; }
|
|
364
375
|
|
package/src/markup/tokens.ts
CHANGED
|
@@ -58,6 +58,9 @@ export interface LinkToken extends Span, Selectors {
|
|
|
58
58
|
type: 'link';
|
|
59
59
|
display: string;
|
|
60
60
|
target: string;
|
|
61
|
+
/** Where the target is written, from `targetStart` to `targetEnd`. */
|
|
62
|
+
targetStart: number;
|
|
63
|
+
targetEnd: number;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export interface MacroToken extends Span, Selectors {
|
package/src/markup/validate.ts
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
import { lineColumn, MarkupError, parseMarkup, tokenizeMarkup } from './parse';
|
|
9
9
|
import type { Token } from './tokens';
|
|
10
10
|
import { parseWidgetDef } from '../widgets/widget-def';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
codeAndText,
|
|
13
|
+
parseOrError,
|
|
14
|
+
withParseCache,
|
|
15
|
+
type ParametersOf,
|
|
16
|
+
} from '../code-check';
|
|
12
17
|
import { CodeSyntaxError } from '../js-lexer';
|
|
13
|
-
import type { ParameterDef } from '../registry';
|
|
14
|
-
import { SUB_MACRO_PARAMETERS } from '../components/macros/option-utils';
|
|
15
18
|
|
|
16
19
|
/** A passage to validate. */
|
|
17
20
|
export interface MarkupPassage {
|
|
@@ -59,16 +62,24 @@ export interface MarkupValidationOptions {
|
|
|
59
62
|
*/
|
|
60
63
|
checkPassageNames?: boolean;
|
|
61
64
|
/**
|
|
62
|
-
* The declared parameters of a macro
|
|
63
|
-
*
|
|
64
|
-
* as
|
|
65
|
-
* `{do}`, branch conditions
|
|
65
|
+
* The declared parameters of a macro (see parameterLookup in
|
|
66
|
+
* code-check.ts), whose `expression` and `statements` arguments are
|
|
67
|
+
* checked as code and whose `text` and `string` arguments as what they
|
|
68
|
+
* hold. Without them only the code of `{$…}`, `{do}`, branch conditions
|
|
69
|
+
* and attributes is checked.
|
|
66
70
|
*/
|
|
67
|
-
parametersOf
|
|
71
|
+
parametersOf?: ParametersOf;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Passages that hold no markup: declarations, and the JavaScript function
|
|
76
|
+
* body of SaveTitle.
|
|
77
|
+
*/
|
|
78
|
+
export const NOT_MARKUP = new Set([
|
|
79
|
+
'StoryVariables',
|
|
80
|
+
'StoryTransients',
|
|
81
|
+
'SaveTitle',
|
|
82
|
+
]);
|
|
72
83
|
const NOT_MARKUP_TAGS = ['script', 'stylesheet'];
|
|
73
84
|
|
|
74
85
|
/** Branch macros, which their parent macro renders. */
|
|
@@ -221,8 +232,7 @@ export function validateMarkup(
|
|
|
221
232
|
}
|
|
222
233
|
};
|
|
223
234
|
|
|
224
|
-
const parametersOf = (
|
|
225
|
-
options.parametersOf?.(name) ?? SUB_MACRO_PARAMETERS[name.toLowerCase()];
|
|
235
|
+
const parametersOf: ParametersOf = options.parametersOf ?? (() => undefined);
|
|
226
236
|
|
|
227
237
|
/**
|
|
228
238
|
* Check `tokens`, the tokens of `src`, which starts at `base` in the
|
package/src/registry.ts
CHANGED
|
@@ -104,11 +104,36 @@ export const PARAMETER_TYPES = [
|
|
|
104
104
|
|
|
105
105
|
export type ParameterType = (typeof PARAMETER_TYPES)[number];
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* What the value of a `string` or `text` argument holds, for the check at
|
|
109
|
+
* story start and for tooling (see code-check.ts):
|
|
110
|
+
* - `markup`: markup the macro renders (`{button}`'s label): its markup
|
|
111
|
+
* is checked.
|
|
112
|
+
* - `text`: plain text the macro uses as written (`{checkbox}`'s label).
|
|
113
|
+
* - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
|
|
114
|
+
* - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
|
|
115
|
+
* is checked as code, and its variable references against the schema.
|
|
116
|
+
*
|
|
117
|
+
* Without it, the argument of a macro with `interpolate` holds `markup`,
|
|
118
|
+
* any other `text`.
|
|
119
|
+
*/
|
|
120
|
+
export const STRING_HOLDS = [
|
|
121
|
+
'markup',
|
|
122
|
+
'text',
|
|
123
|
+
'passage',
|
|
124
|
+
'expression',
|
|
125
|
+
'statements',
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
128
|
+
export type StringHolds = (typeof STRING_HOLDS)[number];
|
|
129
|
+
|
|
107
130
|
export interface ParameterDef {
|
|
108
131
|
name: string;
|
|
109
132
|
required?: boolean;
|
|
110
133
|
description?: string;
|
|
111
134
|
type: ParameterType;
|
|
135
|
+
/** What a `string` or `text` argument holds (see StringHolds). */
|
|
136
|
+
holds?: StringHolds;
|
|
112
137
|
/** The options of an `options` parameter. */
|
|
113
138
|
parameters?: readonly ParameterDef[];
|
|
114
139
|
}
|
|
@@ -116,30 +141,62 @@ export interface ParameterDef {
|
|
|
116
141
|
/**
|
|
117
142
|
* Throw if a parameter `macro` declares (or an option of one) has no type,
|
|
118
143
|
* or one that isn't a ParameterType: arguments are read by their type, and
|
|
119
|
-
* there is no default.
|
|
144
|
+
* there is no default. Throw too if it declares what it `holds` wrongly.
|
|
120
145
|
*/
|
|
121
146
|
export function checkParameterTypes(
|
|
122
147
|
macro: string,
|
|
123
148
|
parameters: readonly ParameterDef[],
|
|
124
149
|
): void {
|
|
150
|
+
const holdsFix =
|
|
151
|
+
`Give a \`string\` or \`text\` parameter one of ${STRING_HOLDS.join(', ')} ` +
|
|
152
|
+
'to hold (see docs/custom-macros.md#what-a-string-holds).';
|
|
125
153
|
for (const param of parameters) {
|
|
126
154
|
const type: unknown = param.type;
|
|
127
155
|
if (!PARAMETER_TYPES.includes(type as ParameterType)) {
|
|
128
|
-
|
|
156
|
+
throw parameterError(
|
|
157
|
+
macro,
|
|
158
|
+
param,
|
|
129
159
|
type === undefined
|
|
130
160
|
? 'has no type'
|
|
131
|
-
: `has the unknown type ${JSON.stringify(type)}
|
|
132
|
-
|
|
133
|
-
`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ` +
|
|
134
|
-
`Give it one of the types ${PARAMETER_TYPES.join(', ')} ` +
|
|
161
|
+
: `has the unknown type ${JSON.stringify(type)}`,
|
|
162
|
+
`Give it one of the types ${PARAMETER_TYPES.join(', ')} ` +
|
|
135
163
|
'(see docs/custom-macros.md#parameter-types), ' +
|
|
136
164
|
'or declare no parameters and read props.rawArgs.',
|
|
137
165
|
);
|
|
138
166
|
}
|
|
167
|
+
const holds: unknown = param.holds;
|
|
168
|
+
if (holds !== undefined && type !== 'string' && type !== 'text') {
|
|
169
|
+
throw parameterError(
|
|
170
|
+
macro,
|
|
171
|
+
param,
|
|
172
|
+
`is of the type ${type}, which holds what it is`,
|
|
173
|
+
holdsFix,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (holds !== undefined && !STRING_HOLDS.includes(holds as StringHolds)) {
|
|
177
|
+
throw parameterError(
|
|
178
|
+
macro,
|
|
179
|
+
param,
|
|
180
|
+
`holds the unknown ${JSON.stringify(holds)}`,
|
|
181
|
+
holdsFix,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
139
184
|
if (param.parameters) checkParameterTypes(macro, param.parameters);
|
|
140
185
|
}
|
|
141
186
|
}
|
|
142
187
|
|
|
188
|
+
/** The error for the parameter `param` of `macro`: what is wrong, and the fix. */
|
|
189
|
+
function parameterError(
|
|
190
|
+
macro: string,
|
|
191
|
+
param: ParameterDef,
|
|
192
|
+
problem: string,
|
|
193
|
+
fix: string,
|
|
194
|
+
): Error {
|
|
195
|
+
return new Error(
|
|
196
|
+
`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ${fix}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
143
200
|
type ArgValue<T, D> = T extends 'flag' | 'separator'
|
|
144
201
|
? boolean
|
|
145
202
|
: T extends 'names'
|
|
@@ -178,6 +235,13 @@ export function registerMacroMetadata(
|
|
|
178
235
|
metadataRegistry.set(name, metadata);
|
|
179
236
|
}
|
|
180
237
|
|
|
238
|
+
/** The declared parameters of the macro `name`, if it declares any. */
|
|
239
|
+
export function getMacroParameters(
|
|
240
|
+
name: string,
|
|
241
|
+
): readonly ParameterDef[] | undefined {
|
|
242
|
+
return metadataRegistry.get(name)?.parameters;
|
|
243
|
+
}
|
|
244
|
+
|
|
181
245
|
export function getMacroRegistry(): MacroMetadata[] {
|
|
182
246
|
return Array.from(metadataRegistry.values());
|
|
183
247
|
}
|
package/src/story-variables.ts
CHANGED
|
@@ -9,7 +9,15 @@ import {
|
|
|
9
9
|
scanStringLiteral,
|
|
10
10
|
type JsGoal,
|
|
11
11
|
} from './js-lexer';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
argPieces,
|
|
14
|
+
holdsCode,
|
|
15
|
+
parseOrError,
|
|
16
|
+
withParseCache,
|
|
17
|
+
type ParametersOf,
|
|
18
|
+
} from './code-check';
|
|
19
|
+
import { getMacroParameters, type ParameterDef } from './registry';
|
|
20
|
+
import { NOT_MARKUP } from './markup/validate';
|
|
13
21
|
import { createNamespace, variableNameError } from './utils/namespace';
|
|
14
22
|
|
|
15
23
|
/**
|
|
@@ -319,18 +327,45 @@ function collectPassageRefs(
|
|
|
319
327
|
content: string,
|
|
320
328
|
storeVarMacros: ReadonlySet<string>,
|
|
321
329
|
onRef: RefCallback,
|
|
330
|
+
parametersOf: ParametersOf,
|
|
322
331
|
): void {
|
|
323
332
|
withParseCache(() =>
|
|
324
|
-
collectTokenRefs(
|
|
333
|
+
collectTokenRefs(
|
|
334
|
+
content,
|
|
335
|
+
tokensOf(content, false),
|
|
336
|
+
storeVarMacros,
|
|
337
|
+
onRef,
|
|
338
|
+
parametersOf,
|
|
339
|
+
),
|
|
325
340
|
);
|
|
326
341
|
}
|
|
327
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Report the references in the code that a macro's quoted arguments hold
|
|
345
|
+
* (see ParameterDef.holds), the condition and `run` action of `{watch}`:
|
|
346
|
+
* the argument scan sees only the strings. Only the macros that declare
|
|
347
|
+
* code in a string have their arguments read.
|
|
348
|
+
*/
|
|
349
|
+
function scanStringCode(
|
|
350
|
+
token: Extract<Token, { type: 'macro' }>,
|
|
351
|
+
params: readonly ParameterDef[] | undefined,
|
|
352
|
+
onRef: RefCallback,
|
|
353
|
+
): void {
|
|
354
|
+
if (!params || !token.rawArgs || !holdsCode(params)) return;
|
|
355
|
+
for (const piece of argPieces(token.rawArgs, 0, params, token.name, '')) {
|
|
356
|
+
if (piece.kind === 'code' && piece.inString) {
|
|
357
|
+
scanCode(piece.code, onRef, piece.goal);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
328
362
|
/** Report the `$var` references in the tokens of `content`. */
|
|
329
363
|
function collectTokenRefs(
|
|
330
364
|
content: string,
|
|
331
365
|
tokens: Token[],
|
|
332
366
|
storeVarMacros: ReadonlySet<string>,
|
|
333
367
|
onRef: RefCallback,
|
|
368
|
+
parametersOf: ParametersOf = () => undefined,
|
|
334
369
|
): void {
|
|
335
370
|
for (let t = 0; t < tokens.length; t++) {
|
|
336
371
|
const token = tokens[t]!;
|
|
@@ -345,6 +380,7 @@ function collectTokenRefs(
|
|
|
345
380
|
}
|
|
346
381
|
} else if (token.type === 'macro' && !token.isClose) {
|
|
347
382
|
scanCode(token.rawArgs, onRef);
|
|
383
|
+
scanStringCode(token, parametersOf(token.name), onRef);
|
|
348
384
|
|
|
349
385
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
350
386
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? '';
|
|
@@ -383,6 +419,7 @@ export function validatePassages(
|
|
|
383
419
|
passages: Map<string, Passage>,
|
|
384
420
|
schema: Map<string, VariableSchema>,
|
|
385
421
|
storeVarMacros: Iterable<string> = BUILTIN_STORE_VAR_MACROS,
|
|
422
|
+
parametersOf: ParametersOf = getMacroParameters,
|
|
386
423
|
): string[] {
|
|
387
424
|
const errors: string[] = [];
|
|
388
425
|
const storeVarSet = new Set(
|
|
@@ -390,16 +427,21 @@ export function validatePassages(
|
|
|
390
427
|
);
|
|
391
428
|
|
|
392
429
|
for (const [name, passage] of passages) {
|
|
393
|
-
// Don't validate the
|
|
394
|
-
if (name
|
|
430
|
+
// Don't validate the declarations or the SaveTitle code themselves
|
|
431
|
+
if (NOT_MARKUP.has(name)) continue;
|
|
395
432
|
|
|
396
433
|
const forLocals = extractForLocals(passage.content);
|
|
397
|
-
collectPassageRefs(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
434
|
+
collectPassageRefs(
|
|
435
|
+
passage.content,
|
|
436
|
+
storeVarSet,
|
|
437
|
+
(ref) => {
|
|
438
|
+
const error = validateRef(ref, schema, forLocals);
|
|
439
|
+
if (error) {
|
|
440
|
+
errors.push(`Passage "${name}": ${error}`);
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
parametersOf,
|
|
444
|
+
);
|
|
403
445
|
}
|
|
404
446
|
|
|
405
447
|
return errors;
|
package/src/tooling.ts
CHANGED
|
@@ -10,12 +10,53 @@ import {
|
|
|
10
10
|
type MarkupPassage,
|
|
11
11
|
} from './markup/validate';
|
|
12
12
|
import { isBlockMacro } from './markup/ast';
|
|
13
|
+
import { tokenizeMarkupTolerant } from './markup/parse';
|
|
14
|
+
import {
|
|
15
|
+
collectPassageReferences,
|
|
16
|
+
parameterLookup,
|
|
17
|
+
type PassageReference,
|
|
18
|
+
} from './code-check';
|
|
13
19
|
import { blockWidgetNames } from './widgets/widget-def';
|
|
14
20
|
import type { ParameterDef } from './registry';
|
|
15
21
|
|
|
16
22
|
export { parseStoryVariables } from './story-variables';
|
|
17
23
|
export { checkParameterTypes } from './registry';
|
|
18
24
|
export { formatDiagnostic } from './markup/validate';
|
|
25
|
+
|
|
26
|
+
// The parsing rules (see "Tooling API" in docs/tooling.md): the leaf rules
|
|
27
|
+
// the runtime parses with, so editor tooling needn't mirror them.
|
|
28
|
+
export { findCodeEnd, lexJs, lexTemplate, scanStringLiteral } from './js-lexer';
|
|
29
|
+
export type {
|
|
30
|
+
FindCodeEndOptions,
|
|
31
|
+
JsGoal,
|
|
32
|
+
JsLexHandlers,
|
|
33
|
+
Sigil,
|
|
34
|
+
} from './js-lexer';
|
|
35
|
+
export {
|
|
36
|
+
MarkupError,
|
|
37
|
+
parseSelectors,
|
|
38
|
+
tokenizeMarkup,
|
|
39
|
+
tokenizeMarkupTolerant,
|
|
40
|
+
} from './markup/parse';
|
|
41
|
+
export type { ParseMarkupOptions, TolerantTokens } from './markup/parse';
|
|
42
|
+
export { SIGIL_SCOPES, isSigil } from './markup/tokens';
|
|
43
|
+
export type { Selectors, Token, VariableScope } from './markup/tokens';
|
|
44
|
+
export {
|
|
45
|
+
endsWithOperator,
|
|
46
|
+
readQuoted,
|
|
47
|
+
splitArgs,
|
|
48
|
+
splitTopLevel,
|
|
49
|
+
stripLooseQuotes,
|
|
50
|
+
unescapeQuoted,
|
|
51
|
+
} from './components/macros/arg-utils';
|
|
52
|
+
export { splitIncludeFlag } from './components/macros/include-args';
|
|
53
|
+
export {
|
|
54
|
+
evaluatePassageName,
|
|
55
|
+
passageTarget,
|
|
56
|
+
} from './components/macros/macro-args';
|
|
57
|
+
export type { PassageTarget } from './components/macros/macro-args';
|
|
58
|
+
export { transform } from './transform';
|
|
59
|
+
export type { PassageReference } from './code-check';
|
|
19
60
|
export type { MarkupDiagnostic, MarkupPassage } from './markup/validate';
|
|
20
61
|
|
|
21
62
|
/** What tooling knows about a macro (see MacroMetadata). */
|
|
@@ -33,8 +74,13 @@ export interface ToolingMacro {
|
|
|
33
74
|
name: string;
|
|
34
75
|
block: boolean;
|
|
35
76
|
subMacros: string[];
|
|
36
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Its declared parameters: their types tell which arguments are code, and
|
|
79
|
+
* what a `string` or `text` argument holds (see ParameterDef.holds).
|
|
80
|
+
*/
|
|
37
81
|
parameters?: readonly ParameterDef[];
|
|
82
|
+
/** Whether it resolves markup: its strings hold markup by default. */
|
|
83
|
+
interpolate?: boolean;
|
|
38
84
|
}
|
|
39
85
|
|
|
40
86
|
/**
|
|
@@ -49,22 +95,34 @@ export function validateStoryMarkup(
|
|
|
49
95
|
options: ValidateMarkupOptions = {},
|
|
50
96
|
): MarkupDiagnostic[] {
|
|
51
97
|
const list = [...passages];
|
|
98
|
+
const all = [...macros];
|
|
52
99
|
const known = new Set<string>();
|
|
53
|
-
const parameters = new Map<string, readonly ParameterDef[] | undefined>();
|
|
54
100
|
const blocks = new Set(blockWidgetNames(list).map((n) => n.toLowerCase()));
|
|
55
|
-
for (const macro of
|
|
101
|
+
for (const macro of all) {
|
|
56
102
|
const name = macro.name.toLowerCase();
|
|
57
103
|
known.add(name);
|
|
58
|
-
parameters.set(name, macro.parameters);
|
|
59
104
|
for (const sub of macro.subMacros) known.add(sub.toLowerCase());
|
|
60
105
|
if (macro.block) blocks.add(name);
|
|
61
106
|
}
|
|
62
107
|
return validateMarkup(list, {
|
|
63
108
|
isKnownMacro: (name) => known.has(name),
|
|
64
109
|
macroNames: known,
|
|
65
|
-
parametersOf: (
|
|
110
|
+
parametersOf: parameterLookup(all),
|
|
66
111
|
isBlockMacro: (name) =>
|
|
67
112
|
blocks.has(name.toLowerCase()) || isBlockMacro(name),
|
|
68
113
|
checkPassageNames: options.checkPassageNames,
|
|
69
114
|
});
|
|
70
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The passages the markup of `source` names (see PassageReference), against
|
|
119
|
+
* the given macros: what the story-start check looks up. Malformed tags are
|
|
120
|
+
* skipped, so it reads half-typed markup.
|
|
121
|
+
*/
|
|
122
|
+
export function collectStoryPassageReferences(
|
|
123
|
+
source: string,
|
|
124
|
+
macros: Iterable<ToolingMacro>,
|
|
125
|
+
): PassageReference[] {
|
|
126
|
+
const { tokens } = tokenizeMarkupTolerant(source);
|
|
127
|
+
return collectPassageReferences(source, tokens, parameterLookup(macros));
|
|
128
|
+
}
|
package/src/transform.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sigil transform: the story's code with its variable references
|
|
3
|
+
* (`$var`, `_var`, `@var`, `%var`) turned into the namespace lookups the
|
|
4
|
+
* expression engine runs. Pure string-to-string, so tooling can use it
|
|
5
|
+
* (see src/tooling.ts).
|
|
6
|
+
*/
|
|
7
|
+
import { parseCode, type JsGoal, type Sigil } from './js-lexer';
|
|
8
|
+
import { variableNameError } from './utils/namespace';
|
|
9
|
+
|
|
10
|
+
const TRANSFORM_CACHE_MAX = 500;
|
|
11
|
+
|
|
12
|
+
const NAMESPACES: Record<Sigil, string> = {
|
|
13
|
+
$: 'variables',
|
|
14
|
+
_: 'temporary',
|
|
15
|
+
'@': 'locals',
|
|
16
|
+
'%': 'transient',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Ends with an identifier character. */
|
|
20
|
+
const IDENT_END_RE = /[\p{ID_Continue}$\u200c\u200d]$/u;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Transform expression: $var → variables["var"], _var → temporary["var"],
|
|
24
|
+
* @var → locals["var"], %var → transient["var"].
|
|
25
|
+
* The lexer (`lexJs`) finds the references: only in code (string, template
|
|
26
|
+
* and regex literal text and comments are left untouched), only where an
|
|
27
|
+
* identifier starts (not in `a$b`, nor as the property name in `obj._x`),
|
|
28
|
+
* and `%var` only where an operand is expected, since `%` is also the
|
|
29
|
+
* modulo operator. `goal` tells whether `expr` is an expression or a list of
|
|
30
|
+
* statements, as that decides whether a leading `{` opens an object literal
|
|
31
|
+
* or a block. A reference to a variable named `__proto__` throws a
|
|
32
|
+
* SyntaxError: no namespace can hold one (see utils/namespace.ts).
|
|
33
|
+
* */
|
|
34
|
+
export function transform(expr: string, goal: JsGoal = 'expression'): string {
|
|
35
|
+
const key = goal === 'statements' ? 's' + expr : 'e' + expr;
|
|
36
|
+
const cached = transformCache.get(key);
|
|
37
|
+
if (cached !== undefined) return cached;
|
|
38
|
+
const { refs } = parseCode(expr, goal);
|
|
39
|
+
let result = '';
|
|
40
|
+
let at = 0;
|
|
41
|
+
for (const ref of refs) {
|
|
42
|
+
const error = variableNameError(ref.name, ref.sigil + ref.name);
|
|
43
|
+
if (error) throw new SyntaxError(`spindle: ${error}`);
|
|
44
|
+
result += expr.slice(at, ref.start);
|
|
45
|
+
if (ref.shorthand) result += `${ref.name}: `;
|
|
46
|
+
// `typeof%x` needs a space once `%x` turns into an identifier.
|
|
47
|
+
else if (IDENT_END_RE.test(result.slice(-2))) result += ' ';
|
|
48
|
+
result += `${NAMESPACES[ref.sigil]}["${ref.name}"]`;
|
|
49
|
+
at = ref.end;
|
|
50
|
+
}
|
|
51
|
+
result += expr.slice(at);
|
|
52
|
+
if (transformCache.size >= TRANSFORM_CACHE_MAX) {
|
|
53
|
+
transformCache.delete(transformCache.keys().next().value!);
|
|
54
|
+
}
|
|
55
|
+
transformCache.set(key, result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Transformed code by goal and source. */
|
|
60
|
+
const transformCache = new Map<string, string>();
|
|
61
|
+
|
|
62
|
+
/** Clear the transformed code. Useful for testing and HMR. */
|
|
63
|
+
export function clearTransformCache(): void {
|
|
64
|
+
transformCache.clear();
|
|
65
|
+
}
|
|
@@ -7,8 +7,8 @@ import type { MacroArgs } from '../registry';
|
|
|
7
7
|
|
|
8
8
|
/** A {widget} definition's name, then its `@` parameters. */
|
|
9
9
|
export const WIDGET_PARAMETERS = [
|
|
10
|
-
{ name: 'name', type: 'text', required: true },
|
|
11
|
-
{ name: 'parameters', type: 'text' },
|
|
10
|
+
{ name: 'name', type: 'text', holds: 'text', required: true },
|
|
11
|
+
{ name: 'parameters', type: 'text', holds: 'text' },
|
|
12
12
|
] as const;
|
|
13
13
|
|
|
14
14
|
export interface WidgetDef {
|
package/types/index.d.ts
CHANGED
|
@@ -272,6 +272,27 @@ export type ParameterType =
|
|
|
272
272
|
| 'separator'
|
|
273
273
|
| 'options';
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* What the value of a `string` or `text` argument holds, for the check at
|
|
277
|
+
* story start and for tooling:
|
|
278
|
+
* - `markup`: markup the macro renders (`{button}`'s label): its markup
|
|
279
|
+
* is checked.
|
|
280
|
+
* - `text`: plain text the macro uses as written (`{checkbox}`'s label).
|
|
281
|
+
* - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
|
|
282
|
+
* - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
|
|
283
|
+
* is checked as code, and its variable references against the schema.
|
|
284
|
+
*
|
|
285
|
+
* Without it, the argument of a macro with `interpolate` holds `markup`,
|
|
286
|
+
* any other `text`.
|
|
287
|
+
* @see {@link ../../src/registry.ts} for the implementation.
|
|
288
|
+
*/
|
|
289
|
+
export type StringHolds =
|
|
290
|
+
| 'markup'
|
|
291
|
+
| 'text'
|
|
292
|
+
| 'passage'
|
|
293
|
+
| 'expression'
|
|
294
|
+
| 'statements';
|
|
295
|
+
|
|
275
296
|
/**
|
|
276
297
|
* Parameter metadata for a macro definition.
|
|
277
298
|
* @see {@link ../../src/registry.ts} for the implementation.
|
|
@@ -282,6 +303,8 @@ export interface ParameterDef {
|
|
|
282
303
|
description?: string;
|
|
283
304
|
/** How the argument is read: required, there is no default. */
|
|
284
305
|
type: ParameterType;
|
|
306
|
+
/** What a `string` or `text` argument holds (see StringHolds). */
|
|
307
|
+
holds?: StringHolds;
|
|
285
308
|
/** The options of an `options` parameter. */
|
|
286
309
|
parameters?: readonly ParameterDef[];
|
|
287
310
|
}
|