@rohal12/spindle 0.57.0 → 0.58.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 +584 -426
- package/dist/pkg/story-variables.js +567 -154
- package/dist/pkg/tooling.js +39 -2
- package/package.json +1 -1
- package/src/code-check.ts +107 -17
- package/src/components/macros/Include.tsx +2 -6
- package/src/components/macros/MacroLink.tsx +3 -0
- package/src/components/macros/arg-utils.ts +37 -0
- package/src/components/macros/include-args.ts +23 -0
- package/src/components/macros/macro-args.ts +35 -3
- package/src/components/macros/option-utils.ts +9 -3
- package/src/expression.ts +4 -54
- package/src/markup/grammar-shared.ts +8 -0
- package/src/markup/parse.ts +89 -2
- package/src/markup/spindle.d.peggy.ts +5 -2
- package/src/markup/spindle.peggy +6 -7
- package/src/markup/validate.ts +11 -4
- package/src/registry.ts +7 -0
- package/src/story-variables.ts +48 -10
- package/src/tooling.ts +59 -0
- package/src/transform.ts +65 -0
- package/types/tooling.d.ts +311 -0
|
@@ -13,12 +13,15 @@ export declare class SyntaxError extends globalThis.SyntaxError {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface ParseOptions {
|
|
16
|
-
startRule: 'Markup' | 'Tokens';
|
|
16
|
+
startRule: 'Markup' | 'Tokens' | 'SelectorsPrefix';
|
|
17
17
|
/** Text mode (attribute values, labels). */
|
|
18
18
|
text: boolean;
|
|
19
19
|
/** What the grammar leaves to code (see parse.ts MarkupHooks). */
|
|
20
20
|
hooks: unknown;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Parse markup: the AST for `Markup`, the tokens for `Tokens`, the selectors
|
|
25
|
+
* and where they end for `SelectorsPrefix`.
|
|
26
|
+
*/
|
|
24
27
|
export declare function parse(input: string, options: ParseOptions): unknown;
|
package/src/markup/spindle.peggy
CHANGED
|
@@ -18,13 +18,14 @@
|
|
|
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']);
|
|
@@ -47,12 +48,6 @@ function quoteArg(value) {
|
|
|
47
48
|
return '"' + value.replace(/[\\"]/g, '\\$&') + '"';
|
|
48
49
|
}
|
|
49
50
|
|
|
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
51
|
function selectorsOf(parts) {
|
|
57
52
|
const classes = [];
|
|
58
53
|
let id = '';
|
|
@@ -359,6 +354,10 @@ Selectors
|
|
|
359
354
|
= parts:SelectorPart+ " "? { return selectorsOf(parts); }
|
|
360
355
|
/ "" { return NO_SELECTORS; }
|
|
361
356
|
|
|
357
|
+
/** The selectors at the start of the input, and where they end. */
|
|
358
|
+
SelectorsPrefix
|
|
359
|
+
= s:Selectors end:Here .* { return { ...s, end }; }
|
|
360
|
+
|
|
362
361
|
SelectorPart
|
|
363
362
|
= prefix:[.#] name:$SelectorChar* { return [prefix, name]; }
|
|
364
363
|
|
package/src/markup/validate.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { parseWidgetDef } from '../widgets/widget-def';
|
|
|
11
11
|
import { codeAndText, parseOrError, withParseCache } from '../code-check';
|
|
12
12
|
import { CodeSyntaxError } from '../js-lexer';
|
|
13
13
|
import type { ParameterDef } from '../registry';
|
|
14
|
-
import {
|
|
14
|
+
import { subMacroParameters } from '../components/macros/option-utils';
|
|
15
15
|
|
|
16
16
|
/** A passage to validate. */
|
|
17
17
|
export interface MarkupPassage {
|
|
@@ -67,8 +67,15 @@ export interface MarkupValidationOptions {
|
|
|
67
67
|
parametersOf?(name: string): readonly ParameterDef[] | undefined;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Passages that hold no markup: declarations, and the JavaScript function
|
|
72
|
+
* body of SaveTitle.
|
|
73
|
+
*/
|
|
74
|
+
export const NOT_MARKUP = new Set([
|
|
75
|
+
'StoryVariables',
|
|
76
|
+
'StoryTransients',
|
|
77
|
+
'SaveTitle',
|
|
78
|
+
]);
|
|
72
79
|
const NOT_MARKUP_TAGS = ['script', 'stylesheet'];
|
|
73
80
|
|
|
74
81
|
/** Branch macros, which their parent macro renders. */
|
|
@@ -222,7 +229,7 @@ export function validateMarkup(
|
|
|
222
229
|
};
|
|
223
230
|
|
|
224
231
|
const parametersOf = (name: string) =>
|
|
225
|
-
options.parametersOf?.(name) ??
|
|
232
|
+
options.parametersOf?.(name) ?? subMacroParameters(name);
|
|
226
233
|
|
|
227
234
|
/**
|
|
228
235
|
* Check `tokens`, the tokens of `src`, which starts at `base` in the
|
package/src/registry.ts
CHANGED
|
@@ -178,6 +178,13 @@ export function registerMacroMetadata(
|
|
|
178
178
|
metadataRegistry.set(name, metadata);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/** The declared parameters of the macro `name`, if it declares any. */
|
|
182
|
+
export function getMacroParameters(
|
|
183
|
+
name: string,
|
|
184
|
+
): readonly ParameterDef[] | undefined {
|
|
185
|
+
return metadataRegistry.get(name)?.parameters;
|
|
186
|
+
}
|
|
187
|
+
|
|
181
188
|
export function getMacroRegistry(): MacroMetadata[] {
|
|
182
189
|
return Array.from(metadataRegistry.values());
|
|
183
190
|
}
|
package/src/story-variables.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
scanStringLiteral,
|
|
10
10
|
type JsGoal,
|
|
11
11
|
} from './js-lexer';
|
|
12
|
-
import { parseOrError, withParseCache } from './code-check';
|
|
12
|
+
import { argPieces, parseOrError, withParseCache } from './code-check';
|
|
13
|
+
import { getMacroParameters, type ParameterDef } from './registry';
|
|
14
|
+
import { NOT_MARKUP } from './markup/validate';
|
|
13
15
|
import { createNamespace, variableNameError } from './utils/namespace';
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -319,18 +321,47 @@ function collectPassageRefs(
|
|
|
319
321
|
content: string,
|
|
320
322
|
storeVarMacros: ReadonlySet<string>,
|
|
321
323
|
onRef: RefCallback,
|
|
324
|
+
parametersOf: ParametersOf,
|
|
322
325
|
): void {
|
|
323
326
|
withParseCache(() =>
|
|
324
|
-
collectTokenRefs(
|
|
327
|
+
collectTokenRefs(
|
|
328
|
+
content,
|
|
329
|
+
tokensOf(content, false),
|
|
330
|
+
storeVarMacros,
|
|
331
|
+
onRef,
|
|
332
|
+
parametersOf,
|
|
333
|
+
),
|
|
325
334
|
);
|
|
326
335
|
}
|
|
327
336
|
|
|
337
|
+
/** The declared parameters of a macro, if it has any. */
|
|
338
|
+
type ParametersOf = (macro: string) => readonly ParameterDef[] | undefined;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Report the references in the code that a macro's quoted arguments hold,
|
|
342
|
+
* the condition and `run` action of `{watch}`: the argument scan sees only
|
|
343
|
+
* the strings.
|
|
344
|
+
*/
|
|
345
|
+
function scanStringCode(
|
|
346
|
+
token: Extract<Token, { type: 'macro' }>,
|
|
347
|
+
params: readonly ParameterDef[] | undefined,
|
|
348
|
+
onRef: RefCallback,
|
|
349
|
+
): void {
|
|
350
|
+
if (!params || !token.rawArgs) return;
|
|
351
|
+
for (const piece of argPieces(token.rawArgs, 0, params, token.name, '')) {
|
|
352
|
+
if (piece.kind === 'code' && piece.inString) {
|
|
353
|
+
scanCode(piece.code, onRef, piece.goal);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
328
358
|
/** Report the `$var` references in the tokens of `content`. */
|
|
329
359
|
function collectTokenRefs(
|
|
330
360
|
content: string,
|
|
331
361
|
tokens: Token[],
|
|
332
362
|
storeVarMacros: ReadonlySet<string>,
|
|
333
363
|
onRef: RefCallback,
|
|
364
|
+
parametersOf: ParametersOf = () => undefined,
|
|
334
365
|
): void {
|
|
335
366
|
for (let t = 0; t < tokens.length; t++) {
|
|
336
367
|
const token = tokens[t]!;
|
|
@@ -345,6 +376,7 @@ function collectTokenRefs(
|
|
|
345
376
|
}
|
|
346
377
|
} else if (token.type === 'macro' && !token.isClose) {
|
|
347
378
|
scanCode(token.rawArgs, onRef);
|
|
379
|
+
scanStringCode(token, parametersOf(token.name), onRef);
|
|
348
380
|
|
|
349
381
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
350
382
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? '';
|
|
@@ -383,6 +415,7 @@ export function validatePassages(
|
|
|
383
415
|
passages: Map<string, Passage>,
|
|
384
416
|
schema: Map<string, VariableSchema>,
|
|
385
417
|
storeVarMacros: Iterable<string> = BUILTIN_STORE_VAR_MACROS,
|
|
418
|
+
parametersOf: ParametersOf = getMacroParameters,
|
|
386
419
|
): string[] {
|
|
387
420
|
const errors: string[] = [];
|
|
388
421
|
const storeVarSet = new Set(
|
|
@@ -390,16 +423,21 @@ export function validatePassages(
|
|
|
390
423
|
);
|
|
391
424
|
|
|
392
425
|
for (const [name, passage] of passages) {
|
|
393
|
-
// Don't validate the
|
|
394
|
-
if (name
|
|
426
|
+
// Don't validate the declarations or the SaveTitle code themselves
|
|
427
|
+
if (NOT_MARKUP.has(name)) continue;
|
|
395
428
|
|
|
396
429
|
const forLocals = extractForLocals(passage.content);
|
|
397
|
-
collectPassageRefs(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
430
|
+
collectPassageRefs(
|
|
431
|
+
passage.content,
|
|
432
|
+
storeVarSet,
|
|
433
|
+
(ref) => {
|
|
434
|
+
const error = validateRef(ref, schema, forLocals);
|
|
435
|
+
if (error) {
|
|
436
|
+
errors.push(`Passage "${name}": ${error}`);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
parametersOf,
|
|
440
|
+
);
|
|
403
441
|
}
|
|
404
442
|
|
|
405
443
|
return errors;
|
package/src/tooling.ts
CHANGED
|
@@ -10,12 +10,50 @@ 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 { collectPassageReferences, type PassageReference } from './code-check';
|
|
15
|
+
import { subMacroParameters } from './components/macros/option-utils';
|
|
13
16
|
import { blockWidgetNames } from './widgets/widget-def';
|
|
14
17
|
import type { ParameterDef } from './registry';
|
|
15
18
|
|
|
16
19
|
export { parseStoryVariables } from './story-variables';
|
|
17
20
|
export { checkParameterTypes } from './registry';
|
|
18
21
|
export { formatDiagnostic } from './markup/validate';
|
|
22
|
+
|
|
23
|
+
// The parsing rules (see "Tooling API" in docs/tooling.md): the leaf rules
|
|
24
|
+
// the runtime parses with, so editor tooling needn't mirror them.
|
|
25
|
+
export { findCodeEnd, lexJs, lexTemplate, scanStringLiteral } from './js-lexer';
|
|
26
|
+
export type {
|
|
27
|
+
FindCodeEndOptions,
|
|
28
|
+
JsGoal,
|
|
29
|
+
JsLexHandlers,
|
|
30
|
+
Sigil,
|
|
31
|
+
} from './js-lexer';
|
|
32
|
+
export {
|
|
33
|
+
MarkupError,
|
|
34
|
+
parseSelectors,
|
|
35
|
+
tokenizeMarkup,
|
|
36
|
+
tokenizeMarkupTolerant,
|
|
37
|
+
} from './markup/parse';
|
|
38
|
+
export type { ParseMarkupOptions, TolerantTokens } from './markup/parse';
|
|
39
|
+
export { SIGIL_SCOPES, isSigil } from './markup/tokens';
|
|
40
|
+
export type { Selectors, Token, VariableScope } from './markup/tokens';
|
|
41
|
+
export {
|
|
42
|
+
endsWithOperator,
|
|
43
|
+
readQuoted,
|
|
44
|
+
splitArgs,
|
|
45
|
+
splitTopLevel,
|
|
46
|
+
stripLooseQuotes,
|
|
47
|
+
unescapeQuoted,
|
|
48
|
+
} from './components/macros/arg-utils';
|
|
49
|
+
export { splitIncludeFlag } from './components/macros/include-args';
|
|
50
|
+
export {
|
|
51
|
+
evaluatePassageName,
|
|
52
|
+
passageTarget,
|
|
53
|
+
} from './components/macros/macro-args';
|
|
54
|
+
export type { PassageTarget } from './components/macros/macro-args';
|
|
55
|
+
export { transform } from './transform';
|
|
56
|
+
export type { PassageReference } from './code-check';
|
|
19
57
|
export type { MarkupDiagnostic, MarkupPassage } from './markup/validate';
|
|
20
58
|
|
|
21
59
|
/** What tooling knows about a macro (see MacroMetadata). */
|
|
@@ -68,3 +106,24 @@ export function validateStoryMarkup(
|
|
|
68
106
|
checkPassageNames: options.checkPassageNames,
|
|
69
107
|
});
|
|
70
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The passages the markup of `source` names (see PassageReference), against
|
|
112
|
+
* the given macros: what the story-start check looks up. Malformed tags are
|
|
113
|
+
* skipped, so it reads half-typed markup.
|
|
114
|
+
*/
|
|
115
|
+
export function collectStoryPassageReferences(
|
|
116
|
+
source: string,
|
|
117
|
+
macros: Iterable<ToolingMacro>,
|
|
118
|
+
): PassageReference[] {
|
|
119
|
+
const parameters = new Map<string, readonly ParameterDef[] | undefined>();
|
|
120
|
+
for (const macro of macros) {
|
|
121
|
+
parameters.set(macro.name.toLowerCase(), macro.parameters);
|
|
122
|
+
}
|
|
123
|
+
const { tokens } = tokenizeMarkupTolerant(source);
|
|
124
|
+
return collectPassageReferences(
|
|
125
|
+
source,
|
|
126
|
+
tokens,
|
|
127
|
+
(name) => parameters.get(name.toLowerCase()) ?? subMacroParameters(name),
|
|
128
|
+
);
|
|
129
|
+
}
|
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
|
+
}
|
package/types/tooling.d.ts
CHANGED
|
@@ -160,3 +160,314 @@ export interface ValidateMarkupOptions {
|
|
|
160
160
|
* `Passage "Start", line 3, column 5 (story.twee:12): Unclosed {if}: …`.
|
|
161
161
|
*/
|
|
162
162
|
export declare function formatDiagnostic(diagnostic: MarkupDiagnostic): string;
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Parsing rules
|
|
166
|
+
//
|
|
167
|
+
// The leaf rules Spindle parses passages with, so that editor tooling can use
|
|
168
|
+
// them instead of mirroring them (see docs/tooling.md). They are pure
|
|
169
|
+
// string-to-data functions that do not load the runtime.
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/** The sigil of a variable reference: story, temporary, local, transient. */
|
|
173
|
+
export type Sigil = '$' | '_' | '@' | '%';
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* What a piece of code is: one expression (a macro argument) or a list of
|
|
177
|
+
* statements (a `{do}` body).
|
|
178
|
+
*/
|
|
179
|
+
export type JsGoal = 'expression' | 'statements';
|
|
180
|
+
|
|
181
|
+
/** What `lexJs` reports, in source order, every character exactly once. */
|
|
182
|
+
export interface JsLexHandlers {
|
|
183
|
+
/**
|
|
184
|
+
* One character of code, outside literals and comments. `nesting` is the
|
|
185
|
+
* number of template-literal `${…}` interpolations around it.
|
|
186
|
+
*/
|
|
187
|
+
code?(ch: string, index: number, nesting: number): void;
|
|
188
|
+
/**
|
|
189
|
+
* Literal text passed through verbatim: a string or regex literal, a
|
|
190
|
+
* comment, or a piece of a template literal.
|
|
191
|
+
*/
|
|
192
|
+
literal?(text: string, index: number, nesting: number): void;
|
|
193
|
+
/** A sigil variable reference (`$name`), covering the sigil and name. */
|
|
194
|
+
variable?(sigil: Sigil, name: string, index: number, nesting: number): void;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Walk `src` as JavaScript, reporting code characters, literal text and
|
|
199
|
+
* variable references to `handlers`. Text that is no JavaScript is read
|
|
200
|
+
* leniently: an unterminated literal runs to the end. Returns `src.length`.
|
|
201
|
+
*/
|
|
202
|
+
export declare function lexJs(
|
|
203
|
+
src: string,
|
|
204
|
+
handlers: JsLexHandlers,
|
|
205
|
+
goal?: JsGoal,
|
|
206
|
+
): number;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Lex the template literal opening at `start` (a backtick) as `lexJs` does,
|
|
210
|
+
* its interpolations one nesting level deeper. Returns the index just past
|
|
211
|
+
* its closing backtick, or `src.length` if it is unterminated.
|
|
212
|
+
*/
|
|
213
|
+
export declare function lexTemplate(
|
|
214
|
+
src: string,
|
|
215
|
+
start: number,
|
|
216
|
+
handlers?: JsLexHandlers,
|
|
217
|
+
nesting?: number,
|
|
218
|
+
): number;
|
|
219
|
+
|
|
220
|
+
export interface FindCodeEndOptions {
|
|
221
|
+
/** What the code is (default `expression`). */
|
|
222
|
+
goal?: JsGoal;
|
|
223
|
+
/** End the code at a `{` in code, at any depth, for which this holds. */
|
|
224
|
+
stop?: (index: number) => boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Where the code starting at `start` ends: the index of the `}` that closes
|
|
229
|
+
* the `{…}` around it (or, with `stop`, of the first `{` for which it holds).
|
|
230
|
+
* Braces and quotes in literals and comments don't count. `-1` when there is
|
|
231
|
+
* no such end, or the code before it can't be JavaScript.
|
|
232
|
+
*/
|
|
233
|
+
export declare function findCodeEnd(
|
|
234
|
+
src: string,
|
|
235
|
+
start: number,
|
|
236
|
+
options?: FindCodeEndOptions,
|
|
237
|
+
): number;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Scan the `"…"` or `'…'` string literal opening at `start`: `end` is just
|
|
241
|
+
* past its closing quote, or `src.length` when unterminated (`closed` false).
|
|
242
|
+
*/
|
|
243
|
+
export declare function scanStringLiteral(
|
|
244
|
+
src: string,
|
|
245
|
+
start: number,
|
|
246
|
+
): { end: number; closed: boolean };
|
|
247
|
+
|
|
248
|
+
/** The namespace a variable reference reads. */
|
|
249
|
+
export type VariableScope = 'variable' | 'temporary' | 'local' | 'transient';
|
|
250
|
+
|
|
251
|
+
/** The scope each variable sigil names. */
|
|
252
|
+
export declare const SIGIL_SCOPES: Readonly<Record<Sigil, VariableScope>>;
|
|
253
|
+
|
|
254
|
+
/** Whether `c` is a variable sigil. */
|
|
255
|
+
export declare function isSigil(c: string | undefined): c is Sigil;
|
|
256
|
+
|
|
257
|
+
/** The `.class#id` selectors written before a link, variable or macro. */
|
|
258
|
+
export interface Selectors {
|
|
259
|
+
className?: string;
|
|
260
|
+
id?: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The selectors that start `source` at `at`, and `end`, the index just past
|
|
265
|
+
* them and the one space that may follow (`at` if there are none). A name
|
|
266
|
+
* may hold `{$name}` interpolations.
|
|
267
|
+
*/
|
|
268
|
+
export declare function parseSelectors(
|
|
269
|
+
source: string,
|
|
270
|
+
at?: number,
|
|
271
|
+
): Selectors & { end: number };
|
|
272
|
+
|
|
273
|
+
/** Where a token is in the input: from `start` up to `end`. */
|
|
274
|
+
interface TokenSpan {
|
|
275
|
+
start: number;
|
|
276
|
+
end: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface TextToken extends TokenSpan {
|
|
280
|
+
type: 'text';
|
|
281
|
+
value: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface LinkToken extends TokenSpan, Selectors {
|
|
285
|
+
type: 'link';
|
|
286
|
+
display: string;
|
|
287
|
+
target: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface MacroToken extends TokenSpan, Selectors {
|
|
291
|
+
type: 'macro';
|
|
292
|
+
name: string;
|
|
293
|
+
rawArgs: string;
|
|
294
|
+
isClose: boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface VariableToken extends TokenSpan, Selectors {
|
|
298
|
+
type: 'variable';
|
|
299
|
+
name: string;
|
|
300
|
+
scope: VariableScope;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface ExpressionToken extends TokenSpan, Selectors {
|
|
304
|
+
type: 'expression';
|
|
305
|
+
expression: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface HtmlToken extends TokenSpan {
|
|
309
|
+
type: 'html';
|
|
310
|
+
tag: string;
|
|
311
|
+
attributes: Record<string, string>;
|
|
312
|
+
isClose: boolean;
|
|
313
|
+
isSelfClose: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** A flat markup token, with its offsets (UTF-16 code units) in the input. */
|
|
317
|
+
export type Token =
|
|
318
|
+
| TextToken
|
|
319
|
+
| LinkToken
|
|
320
|
+
| MacroToken
|
|
321
|
+
| VariableToken
|
|
322
|
+
| ExpressionToken
|
|
323
|
+
| HtmlToken;
|
|
324
|
+
|
|
325
|
+
/** Malformed markup, with where it starts (0-based offset, 1-based line and column). */
|
|
326
|
+
export declare class MarkupError extends Error {
|
|
327
|
+
reason: string;
|
|
328
|
+
offset: number;
|
|
329
|
+
line: number;
|
|
330
|
+
column: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface ParseMarkupOptions {
|
|
334
|
+
/**
|
|
335
|
+
* Text mode, for markup that becomes a string (HTML attribute values,
|
|
336
|
+
* macro labels): only `{…}` markup and brace escapes are recognized.
|
|
337
|
+
*/
|
|
338
|
+
text?: boolean;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The flat tokens of markup, without nesting, so unclosed or mismatched
|
|
343
|
+
* macros and elements are no error here. Throws a `MarkupError` for a
|
|
344
|
+
* malformed tag, such as an unclosed `{`, `[[` or attribute value.
|
|
345
|
+
*/
|
|
346
|
+
export declare function tokenizeMarkup(
|
|
347
|
+
source: string,
|
|
348
|
+
options?: ParseMarkupOptions,
|
|
349
|
+
): Token[];
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Split macro arguments at top-level commas (outside strings, templates and
|
|
353
|
+
* brackets); with no comma, adjacent standalone values separated by
|
|
354
|
+
* whitespace (`"Label" "target"`).
|
|
355
|
+
*/
|
|
356
|
+
export declare function splitArgs(raw: string): string[];
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Split `src` at every top-level character for which `isSeparator` holds
|
|
360
|
+
* (code outside literals, comments and brackets). Segments are untrimmed and
|
|
361
|
+
* empty ones are kept.
|
|
362
|
+
*/
|
|
363
|
+
export declare function splitTopLevel(
|
|
364
|
+
src: string,
|
|
365
|
+
isSeparator: (ch: string) => boolean,
|
|
366
|
+
): string[];
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Read the `"…"` or `'…'` string whose opening quote is at `start`: its
|
|
370
|
+
* unescaped value (`\"`, `\'` and `\\` only) and the index just past it, or
|
|
371
|
+
* `null` if there is none or it is unterminated.
|
|
372
|
+
*/
|
|
373
|
+
export declare function readQuoted(
|
|
374
|
+
src: string,
|
|
375
|
+
start: number,
|
|
376
|
+
): { value: string; end: number } | null;
|
|
377
|
+
|
|
378
|
+
/** Undo `\"`, `\'` and `\\` in the body of a quoted macro argument. */
|
|
379
|
+
export declare function unescapeQuoted(body: string): string;
|
|
380
|
+
|
|
381
|
+
/** Strip an optional quote from each end of a loosely quoted argument. */
|
|
382
|
+
export declare function stripLooseQuotes(src: string): string;
|
|
383
|
+
|
|
384
|
+
/** Whether code ends with an operator that still needs an operand. */
|
|
385
|
+
export declare function endsWithOperator(src: string): boolean;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The arguments of `{include}`: whether it has the standalone `inline` flag
|
|
389
|
+
* (first or last word, outside quotes and brackets), and the passage
|
|
390
|
+
* expression that is left.
|
|
391
|
+
*/
|
|
392
|
+
export declare function splitIncludeFlag(rawArgs: string): {
|
|
393
|
+
inline: boolean;
|
|
394
|
+
passage: string | undefined;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/** The tokens of markup that may be malformed, and its errors. */
|
|
398
|
+
export interface TolerantTokens {
|
|
399
|
+
tokens: Token[];
|
|
400
|
+
/** One for each malformed tag, in source order, with offsets in the source. */
|
|
401
|
+
errors: MarkupError[];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* `tokenizeMarkup` for half-typed markup: the tokens it can read, and every
|
|
406
|
+
* error, instead of throwing at the first. At a malformed tag the tokens
|
|
407
|
+
* before it are kept, its first character is read as text, and tokenizing
|
|
408
|
+
* resumes after it. Offsets are in `source`. For well-formed markup the
|
|
409
|
+
* tokens are those of `tokenizeMarkup` and there are no errors.
|
|
410
|
+
*/
|
|
411
|
+
export declare function tokenizeMarkupTolerant(
|
|
412
|
+
source: string,
|
|
413
|
+
options?: ParseMarkupOptions,
|
|
414
|
+
): TolerantTokens;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The code Spindle runs for `expr`: its variable references (`$var`, `_var`,
|
|
418
|
+
* `@var`, `%var`) turned into namespace lookups (`variables["var"]`).
|
|
419
|
+
* String, template and regex literals and comments are untouched. `goal` is
|
|
420
|
+
* `'statements'` for a `{do}` body. Throws a `SyntaxError` for code that is
|
|
421
|
+
* not well-formed, and for a reference to a variable named `__proto__`.
|
|
422
|
+
*/
|
|
423
|
+
export declare function transform(expr: string, goal?: JsGoal): string;
|
|
424
|
+
|
|
425
|
+
/** What a `passage` argument is: a quoted name, or an expression. */
|
|
426
|
+
export type PassageTarget =
|
|
427
|
+
| { kind: 'name'; name: string }
|
|
428
|
+
| { kind: 'expression'; expression: string };
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Read a `passage` argument (`{goto "Hall"}`, `{include $room}`) as written:
|
|
432
|
+
* a quoted string is the name its JavaScript literal has (`"Hall"` is
|
|
433
|
+
* `Hall`), anything else an expression whose value, as a string, is the name
|
|
434
|
+
* when it runs.
|
|
435
|
+
*/
|
|
436
|
+
export declare function passageTarget(arg: string): PassageTarget;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The passage a `passage` argument names, as `{goto}`, `{include}` and
|
|
440
|
+
* `{link}` find it: `String(evaluate(expr))`. Throws what `evaluate` throws,
|
|
441
|
+
* and an error naming the current passage if the story has no passage of
|
|
442
|
+
* that name.
|
|
443
|
+
*/
|
|
444
|
+
export declare function evaluatePassageName(
|
|
445
|
+
expr: string | undefined,
|
|
446
|
+
evaluate: (expr: string) => unknown,
|
|
447
|
+
state: {
|
|
448
|
+
storyData: { passages: { has(name: string): boolean } } | null;
|
|
449
|
+
currentPassage: string;
|
|
450
|
+
},
|
|
451
|
+
): string;
|
|
452
|
+
|
|
453
|
+
/** A passage that markup names, and where. */
|
|
454
|
+
export interface PassageReference {
|
|
455
|
+
/** The macro it is the argument of; `link` for `[[…]]` links. */
|
|
456
|
+
macro: string;
|
|
457
|
+
/** The name written out, or the expression whose value is the name. */
|
|
458
|
+
target: PassageTarget;
|
|
459
|
+
/** Where it is written, as written (quotes included): offsets in `source`. */
|
|
460
|
+
start: number;
|
|
461
|
+
end: number;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The passages the markup of `source` names, in source order: `[[…]]` links,
|
|
466
|
+
* the passage of `{goto}`, `{include}` and `{link}` (and of macros that
|
|
467
|
+
* declare a `passage` argument), the `goto` and `dialog` actions of
|
|
468
|
+
* `{watch}` and the body of `{dialog}`. Malformed tags are skipped, so
|
|
469
|
+
* half-typed markup reads.
|
|
470
|
+
*/
|
|
471
|
+
export declare function collectPassageReferences(
|
|
472
|
+
source: string,
|
|
473
|
+
): PassageReference[];
|