@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.
@@ -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
- /** Parse markup: the AST for `Markup`, the tokens for `Tokens`. */
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;
@@ -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 SIGIL_SCOPES = { $: 'variable', _: 'temporary', '@': 'local', '%': 'transient' };
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
 
@@ -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 { SUB_MACRO_PARAMETERS } from '../components/macros/option-utils';
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
- /** Passages that hold no markup. */
71
- const NOT_MARKUP = new Set(['StoryVariables', 'StoryTransients']);
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) ?? SUB_MACRO_PARAMETERS[name.toLowerCase()];
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
  }
@@ -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(content, tokensOf(content, false), storeVarMacros, onRef),
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 StoryVariables/StoryTransients passages themselves
394
- if (name === 'StoryVariables' || name === 'StoryTransients') continue;
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(passage.content, storeVarSet, (ref) => {
398
- const error = validateRef(ref, schema, forLocals);
399
- if (error) {
400
- errors.push(`Passage "${name}": ${error}`);
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
+ }
@@ -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
+ }
@@ -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[];