@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.
@@ -30,12 +30,34 @@ export type ParameterType =
30
30
  | 'separator'
31
31
  | 'options';
32
32
 
33
+ /**
34
+ * What the value of a `string` or `text` argument holds, for the check at
35
+ * story start and for tooling:
36
+ * - `markup`: markup the macro renders (`{button}`'s label): its markup
37
+ * is checked.
38
+ * - `text`: plain text the macro uses as written (`{checkbox}`'s label).
39
+ * - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
40
+ * - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
41
+ * is checked as code, and its variable references against the schema.
42
+ *
43
+ * Without it, the argument of a macro with `interpolate` holds `markup`,
44
+ * any other `text`.
45
+ */
46
+ export type StringHolds =
47
+ | 'markup'
48
+ | 'text'
49
+ | 'passage'
50
+ | 'expression'
51
+ | 'statements';
52
+
33
53
  export interface ParameterDef {
34
54
  name: string;
35
55
  required?: boolean;
36
56
  description?: string;
37
57
  /** How the argument is read: required, there is no default. */
38
58
  type: ParameterType;
59
+ /** What a `string` or `text` argument holds (see StringHolds). */
60
+ holds?: StringHolds;
39
61
  /** The options of an `options` parameter. */
40
62
  parameters?: readonly ParameterDef[];
41
63
  }
@@ -160,3 +182,318 @@ export interface ValidateMarkupOptions {
160
182
  * `Passage "Start", line 3, column 5 (story.twee:12): Unclosed {if}: …`.
161
183
  */
162
184
  export declare function formatDiagnostic(diagnostic: MarkupDiagnostic): string;
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Parsing rules
188
+ //
189
+ // The leaf rules Spindle parses passages with, so that editor tooling can use
190
+ // them instead of mirroring them (see docs/tooling.md). They are pure
191
+ // string-to-data functions that do not load the runtime.
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /** The sigil of a variable reference: story, temporary, local, transient. */
195
+ export type Sigil = '$' | '_' | '@' | '%';
196
+
197
+ /**
198
+ * What a piece of code is: one expression (a macro argument) or a list of
199
+ * statements (a `{do}` body).
200
+ */
201
+ export type JsGoal = 'expression' | 'statements';
202
+
203
+ /** What `lexJs` reports, in source order, every character exactly once. */
204
+ export interface JsLexHandlers {
205
+ /**
206
+ * One character of code, outside literals and comments. `nesting` is the
207
+ * number of template-literal `${…}` interpolations around it.
208
+ */
209
+ code?(ch: string, index: number, nesting: number): void;
210
+ /**
211
+ * Literal text passed through verbatim: a string or regex literal, a
212
+ * comment, or a piece of a template literal.
213
+ */
214
+ literal?(text: string, index: number, nesting: number): void;
215
+ /** A sigil variable reference (`$name`), covering the sigil and name. */
216
+ variable?(sigil: Sigil, name: string, index: number, nesting: number): void;
217
+ }
218
+
219
+ /**
220
+ * Walk `src` as JavaScript, reporting code characters, literal text and
221
+ * variable references to `handlers`. Text that is no JavaScript is read
222
+ * leniently: an unterminated literal runs to the end. Returns `src.length`.
223
+ */
224
+ export declare function lexJs(
225
+ src: string,
226
+ handlers: JsLexHandlers,
227
+ goal?: JsGoal,
228
+ ): number;
229
+
230
+ /**
231
+ * Lex the template literal opening at `start` (a backtick) as `lexJs` does,
232
+ * its interpolations one nesting level deeper. Returns the index just past
233
+ * its closing backtick, or `src.length` if it is unterminated.
234
+ */
235
+ export declare function lexTemplate(
236
+ src: string,
237
+ start: number,
238
+ handlers?: JsLexHandlers,
239
+ nesting?: number,
240
+ ): number;
241
+
242
+ export interface FindCodeEndOptions {
243
+ /** What the code is (default `expression`). */
244
+ goal?: JsGoal;
245
+ /** End the code at a `{` in code, at any depth, for which this holds. */
246
+ stop?: (index: number) => boolean;
247
+ }
248
+
249
+ /**
250
+ * Where the code starting at `start` ends: the index of the `}` that closes
251
+ * the `{…}` around it (or, with `stop`, of the first `{` for which it holds).
252
+ * Braces and quotes in literals and comments don't count. `-1` when there is
253
+ * no such end, or the code before it can't be JavaScript.
254
+ */
255
+ export declare function findCodeEnd(
256
+ src: string,
257
+ start: number,
258
+ options?: FindCodeEndOptions,
259
+ ): number;
260
+
261
+ /**
262
+ * Scan the `"…"` or `'…'` string literal opening at `start`: `end` is just
263
+ * past its closing quote, or `src.length` when unterminated (`closed` false).
264
+ */
265
+ export declare function scanStringLiteral(
266
+ src: string,
267
+ start: number,
268
+ ): { end: number; closed: boolean };
269
+
270
+ /** The namespace a variable reference reads. */
271
+ export type VariableScope = 'variable' | 'temporary' | 'local' | 'transient';
272
+
273
+ /** The scope each variable sigil names. */
274
+ export declare const SIGIL_SCOPES: Readonly<Record<Sigil, VariableScope>>;
275
+
276
+ /** Whether `c` is a variable sigil. */
277
+ export declare function isSigil(c: string | undefined): c is Sigil;
278
+
279
+ /** The `.class#id` selectors written before a link, variable or macro. */
280
+ export interface Selectors {
281
+ className?: string;
282
+ id?: string;
283
+ }
284
+
285
+ /**
286
+ * The selectors that start `source` at `at`, and `end`, the index just past
287
+ * them and the one space that may follow (`at` if there are none). A name
288
+ * may hold `{$name}` interpolations.
289
+ */
290
+ export declare function parseSelectors(
291
+ source: string,
292
+ at?: number,
293
+ ): Selectors & { end: number };
294
+
295
+ /** Where a token is in the input: from `start` up to `end`. */
296
+ interface TokenSpan {
297
+ start: number;
298
+ end: number;
299
+ }
300
+
301
+ export interface TextToken extends TokenSpan {
302
+ type: 'text';
303
+ value: string;
304
+ }
305
+
306
+ export interface LinkToken extends TokenSpan, Selectors {
307
+ type: 'link';
308
+ display: string;
309
+ target: string;
310
+ /** Where the target is written, from `targetStart` to `targetEnd`. */
311
+ targetStart: number;
312
+ targetEnd: number;
313
+ }
314
+
315
+ export interface MacroToken extends TokenSpan, Selectors {
316
+ type: 'macro';
317
+ name: string;
318
+ rawArgs: string;
319
+ isClose: boolean;
320
+ }
321
+
322
+ export interface VariableToken extends TokenSpan, Selectors {
323
+ type: 'variable';
324
+ name: string;
325
+ scope: VariableScope;
326
+ }
327
+
328
+ export interface ExpressionToken extends TokenSpan, Selectors {
329
+ type: 'expression';
330
+ expression: string;
331
+ }
332
+
333
+ export interface HtmlToken extends TokenSpan {
334
+ type: 'html';
335
+ tag: string;
336
+ attributes: Record<string, string>;
337
+ isClose: boolean;
338
+ isSelfClose: boolean;
339
+ }
340
+
341
+ /** A flat markup token, with its offsets (UTF-16 code units) in the input. */
342
+ export type Token =
343
+ | TextToken
344
+ | LinkToken
345
+ | MacroToken
346
+ | VariableToken
347
+ | ExpressionToken
348
+ | HtmlToken;
349
+
350
+ /** Malformed markup, with where it starts (0-based offset, 1-based line and column). */
351
+ export declare class MarkupError extends Error {
352
+ reason: string;
353
+ offset: number;
354
+ line: number;
355
+ column: number;
356
+ }
357
+
358
+ export interface ParseMarkupOptions {
359
+ /**
360
+ * Text mode, for markup that becomes a string (HTML attribute values,
361
+ * macro labels): only `{…}` markup and brace escapes are recognized.
362
+ */
363
+ text?: boolean;
364
+ }
365
+
366
+ /**
367
+ * The flat tokens of markup, without nesting, so unclosed or mismatched
368
+ * macros and elements are no error here. Throws a `MarkupError` for a
369
+ * malformed tag, such as an unclosed `{`, `[[` or attribute value.
370
+ */
371
+ export declare function tokenizeMarkup(
372
+ source: string,
373
+ options?: ParseMarkupOptions,
374
+ ): Token[];
375
+
376
+ /**
377
+ * Split macro arguments at top-level commas (outside strings, templates and
378
+ * brackets); with no comma, adjacent standalone values separated by
379
+ * whitespace (`"Label" "target"`).
380
+ */
381
+ export declare function splitArgs(raw: string): string[];
382
+
383
+ /**
384
+ * Split `src` at every top-level character for which `isSeparator` holds
385
+ * (code outside literals, comments and brackets). Segments are untrimmed and
386
+ * empty ones are kept.
387
+ */
388
+ export declare function splitTopLevel(
389
+ src: string,
390
+ isSeparator: (ch: string) => boolean,
391
+ ): string[];
392
+
393
+ /**
394
+ * Read the `"…"` or `'…'` string whose opening quote is at `start`: its
395
+ * unescaped value (`\"`, `\'` and `\\` only) and the index just past it, or
396
+ * `null` if there is none or it is unterminated.
397
+ */
398
+ export declare function readQuoted(
399
+ src: string,
400
+ start: number,
401
+ ): { value: string; end: number } | null;
402
+
403
+ /** Undo `\"`, `\'` and `\\` in the body of a quoted macro argument. */
404
+ export declare function unescapeQuoted(body: string): string;
405
+
406
+ /** Strip an optional quote from each end of a loosely quoted argument. */
407
+ export declare function stripLooseQuotes(src: string): string;
408
+
409
+ /** Whether code ends with an operator that still needs an operand. */
410
+ export declare function endsWithOperator(src: string): boolean;
411
+
412
+ /**
413
+ * The arguments of `{include}`: whether it has the standalone `inline` flag
414
+ * (first or last word, outside quotes and brackets), and the passage
415
+ * expression that is left.
416
+ */
417
+ export declare function splitIncludeFlag(rawArgs: string): {
418
+ inline: boolean;
419
+ passage: string | undefined;
420
+ };
421
+
422
+ /** The tokens of markup that may be malformed, and its errors. */
423
+ export interface TolerantTokens {
424
+ tokens: Token[];
425
+ /** One for each malformed tag, in source order, with offsets in the source. */
426
+ errors: MarkupError[];
427
+ }
428
+
429
+ /**
430
+ * `tokenizeMarkup` for half-typed markup: the tokens it can read, and every
431
+ * error, instead of throwing at the first. At a malformed tag the tokens
432
+ * before it are kept, its first character is read as text, and tokenizing
433
+ * resumes after it. Offsets are in `source`. For well-formed markup the
434
+ * tokens are those of `tokenizeMarkup` and there are no errors.
435
+ */
436
+ export declare function tokenizeMarkupTolerant(
437
+ source: string,
438
+ options?: ParseMarkupOptions,
439
+ ): TolerantTokens;
440
+
441
+ /**
442
+ * The code Spindle runs for `expr`: its variable references (`$var`, `_var`,
443
+ * `@var`, `%var`) turned into namespace lookups (`variables["var"]`).
444
+ * String, template and regex literals and comments are untouched. `goal` is
445
+ * `'statements'` for a `{do}` body. Throws a `SyntaxError` for code that is
446
+ * not well-formed, and for a reference to a variable named `__proto__`.
447
+ */
448
+ export declare function transform(expr: string, goal?: JsGoal): string;
449
+
450
+ /** What a `passage` argument is: a quoted name, or an expression. */
451
+ export type PassageTarget =
452
+ | { kind: 'name'; name: string }
453
+ | { kind: 'expression'; expression: string };
454
+
455
+ /**
456
+ * Read a `passage` argument (`{goto "Hall"}`, `{include $room}`) as written:
457
+ * a quoted string is the name its JavaScript literal has (`"Hall"` is
458
+ * `Hall`), anything else an expression whose value, as a string, is the name
459
+ * when it runs.
460
+ */
461
+ export declare function passageTarget(arg: string): PassageTarget;
462
+
463
+ /**
464
+ * The passage a `passage` argument names, as `{goto}`, `{include}` and
465
+ * `{link}` find it: `String(evaluate(expr))`. Throws what `evaluate` throws,
466
+ * and an error naming the current passage if the story has no passage of
467
+ * that name.
468
+ */
469
+ export declare function evaluatePassageName(
470
+ expr: string | undefined,
471
+ evaluate: (expr: string) => unknown,
472
+ state: {
473
+ storyData: { passages: { has(name: string): boolean } } | null;
474
+ currentPassage: string;
475
+ },
476
+ ): string;
477
+
478
+ /** A passage that markup names, and where. */
479
+ export interface PassageReference {
480
+ /** The macro it is the argument of; `link` for `[[…]]` links. */
481
+ macro: string;
482
+ /** The name written out, or the expression whose value is the name. */
483
+ target: PassageTarget;
484
+ /** Where it is written, as written (quotes included): offsets in `source`. */
485
+ start: number;
486
+ end: number;
487
+ }
488
+
489
+ /**
490
+ * The passages the markup of `source` names, in source order: `[[…]]` links,
491
+ * the passage of `{goto}`, `{include}` and `{link}` (and of macros that
492
+ * declare a `passage` argument), the `goto` and `dialog` actions of
493
+ * `{watch}` (and the `string` and `text` arguments of macros that declare
494
+ * they hold a passage name) and the body of `{dialog}`. Malformed tags are
495
+ * skipped, so half-typed markup reads.
496
+ */
497
+ export declare function collectPassageReferences(
498
+ source: string,
499
+ ): PassageReference[];