@rohal12/spindle 0.59.0 → 0.59.1

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.
@@ -11,7 +11,6 @@ import {
11
11
  } from './js-lexer';
12
12
  import {
13
13
  argPieces,
14
- holdsCode,
15
14
  parseOrError,
16
15
  withParseCache,
17
16
  type ParametersOf,
@@ -53,7 +52,6 @@ function declarationRegex(sigil: string): RegExp {
53
52
  const VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
54
53
  /** Quoted first argument of an input macro naming a story variable. */
55
54
  const QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
56
- const FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
57
55
 
58
56
  const VALID_VAR_TYPES = new Set<string>(['number', 'string', 'boolean']);
59
57
 
@@ -131,22 +129,6 @@ export function parseStoryVariables(
131
129
  return schema;
132
130
  }
133
131
 
134
- /**
135
- * Extract for-loop local variable names from passage content.
136
- * `{for @item of ...}` → "item"
137
- * `{for @index, @item of ...}` → "index", "item"
138
- */
139
- function extractForLocals(content: string): Set<string> {
140
- const locals = new Set<string>();
141
- let match: RegExpExecArray | null;
142
- FOR_LOCAL_RE.lastIndex = 0;
143
- while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
144
- locals.add(match[1]!);
145
- if (match[2]) locals.add(match[2]!);
146
- }
147
- return locals;
148
- }
149
-
150
132
  /**
151
133
  * Validate a single variable reference path (e.g. "player.health") against
152
134
  * the schema. Returns an error message or null if valid.
@@ -154,7 +136,6 @@ function extractForLocals(content: string): Set<string> {
154
136
  function validateRef(
155
137
  ref: string,
156
138
  schema: Map<string, VariableSchema>,
157
- forLocals: Set<string>,
158
139
  ): string | null {
159
140
  const parts = ref.split('.');
160
141
  const rootName = parts[0]!;
@@ -162,9 +143,6 @@ function validateRef(
162
143
  const nameError = variableNameError(rootName, '$' + rootName);
163
144
  if (nameError) return nameError;
164
145
 
165
- // Skip for-loop locals
166
- if (forLocals.has(rootName)) return null;
167
-
168
146
  const rootSchema = schema.get(rootName);
169
147
  if (!rootSchema) {
170
148
  return `Undeclared variable: $${ref}`;
@@ -252,8 +230,9 @@ function scanInterpolations(
252
230
  * Report `$var` references in JavaScript code, lexed as the expression
253
231
  * engine lexes it (`lexJs`): references in code, but none inside string or
254
232
  * regex literals or comments, nor property names (`a.$b`, `{ $b: 1 }`).
255
- * The text of string and template literals is scanned for the markup that
256
- * labels evaluate (`{$…}`); the `${…}` parts of template literals are code.
233
+ * The text of string and template literals is not scanned: a string in code
234
+ * is literal text unless a macro argument that holds markup is read as such
235
+ * (see scanArgs); the `${…}` parts of template literals are code.
257
236
  */
258
237
  function scanCode(
259
238
  code: string,
@@ -270,7 +249,6 @@ function scanCode(
270
249
  VAR_PATH_RE.lastIndex = ref.start;
271
250
  onRef(VAR_PATH_RE.exec(code)![1]!);
272
251
  }
273
- for (const text of parsed.strings) scanInterpolations(text, onRef);
274
252
  }
275
253
 
276
254
  /** `scanCode` for code acorn can't parse: lexed leniently. */
@@ -279,8 +257,6 @@ function scanCodeLeniently(
279
257
  onRef: RefCallback,
280
258
  goal: JsGoal,
281
259
  ): void {
282
- /** Open template literals: their text so far, null in an interpolation. */
283
- const templates: { nesting: number; text: string | null }[] = [];
284
260
  lexJs(
285
261
  code,
286
262
  {
@@ -289,32 +265,11 @@ function scanCodeLeniently(
289
265
  VAR_PATH_RE.lastIndex = index;
290
266
  onRef(VAR_PATH_RE.exec(code)![1]!);
291
267
  },
292
- literal(text, _index, nesting) {
293
- const template = templates[templates.length - 1];
294
- if (template?.nesting === nesting) {
295
- if (template.text === null) {
296
- // The `}` ending an interpolation: back to the template's text
297
- template.text = '';
298
- } else if (text === '`' || text === '${') {
299
- scanInterpolations(template.text, onRef);
300
- if (text === '`') templates.pop();
301
- else template.text = null;
302
- } else {
303
- template.text += text;
304
- }
305
- } else if (text === '`') {
306
- templates.push({ nesting, text: '' });
307
- } else if (text[0] === '"' || text[0] === "'") {
308
- const { closed } = scanStringLiteral(text, 0);
309
- scanInterpolations(text.slice(1, closed ? -1 : undefined), onRef);
310
- }
311
- // Regex literals and comments hold no references
312
- },
268
+ // Strings, regex literals and comments hold no references
269
+ literal() {},
313
270
  },
314
271
  goal,
315
272
  );
316
- // An unterminated template literal's text
317
- for (const t of templates) if (t.text) scanInterpolations(t.text, onRef);
318
273
  }
319
274
 
320
275
  /**
@@ -341,22 +296,29 @@ function collectPassageRefs(
341
296
  }
342
297
 
343
298
  /**
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.
299
+ * Report the references in the arguments of a macro by what its parameters
300
+ * say each holds (see ParameterDef.holds): code is scanned as code and
301
+ * markup as markup, but literal text (a placeholder, a passage name) holds
302
+ * none. A macro without declared parameters, or arguments that do not
303
+ * parse, are scanned as code.
348
304
  */
349
- function scanStringCode(
305
+ function scanArgs(
350
306
  token: Extract<Token, { type: 'macro' }>,
351
307
  params: readonly ParameterDef[] | undefined,
352
308
  onRef: RefCallback,
353
309
  ): 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);
310
+ if (!token.rawArgs) return;
311
+ if (params) {
312
+ const pieces = [...argPieces(token.rawArgs, 0, params, token.name, '')];
313
+ if (!pieces.some((piece) => piece.kind === 'argument-error')) {
314
+ for (const piece of pieces) {
315
+ if (piece.kind === 'code') scanCode(piece.code, onRef, piece.goal);
316
+ else if (piece.kind === 'text') scanInterpolations(piece.text, onRef);
317
+ }
318
+ return;
358
319
  }
359
320
  }
321
+ scanCode(token.rawArgs, onRef);
360
322
  }
361
323
 
362
324
  /** Report the `$var` references in the tokens of `content`. */
@@ -379,13 +341,14 @@ function collectTokenRefs(
379
341
  else scanInterpolations(value, onRef, storeVarMacros);
380
342
  }
381
343
  } else if (token.type === 'macro' && !token.isClose) {
382
- scanCode(token.rawArgs, onRef);
383
- scanStringCode(token, parametersOf(token.name), onRef);
344
+ scanArgs(token, parametersOf(token.name), onRef);
384
345
 
385
346
  if (storeVarMacros.has(token.name.toLowerCase())) {
386
347
  const first = token.rawArgs.trim().split(/\s+/)[0] ?? '';
387
348
  const quoted = QUOTED_VAR_ARG_RE.exec(first);
388
349
  if (quoted) onRef(quoted[1]!);
350
+ // Unquoted, the variable is an argument of no declared role
351
+ else if (!/^["'`]/.test(first)) scanCode(first, onRef);
389
352
  }
390
353
 
391
354
  if (token.name === 'do') {
@@ -430,12 +393,11 @@ export function validatePassages(
430
393
  // Don't validate the declarations or the SaveTitle code themselves
431
394
  if (NOT_MARKUP.has(name)) continue;
432
395
 
433
- const forLocals = extractForLocals(passage.content);
434
396
  collectPassageRefs(
435
397
  passage.content,
436
398
  storeVarSet,
437
399
  (ref) => {
438
- const error = validateRef(ref, schema, forLocals);
400
+ const error = validateRef(ref, schema);
439
401
  if (error) {
440
402
  errors.push(`Passage "${name}": ${error}`);
441
403
  }
package/src/structural.ts CHANGED
@@ -36,6 +36,12 @@ export interface DeepCloneOptions {
36
36
  * plain object, which would lose their prototype and identity.
37
37
  */
38
38
  keepUnregistered?: boolean;
39
+ /**
40
+ * The copies made so far, by original. Values copied with one map share
41
+ * their references: an object met again, in the same value or another,
42
+ * is the copy already made. The map is filled as values are copied.
43
+ */
44
+ seen?: Map<object, object>;
39
45
  }
40
46
 
41
47
  type TypedArrayCtor = new (
@@ -45,7 +51,7 @@ type TypedArrayCtor = new (
45
51
  ) => ArrayBufferView;
46
52
 
47
53
  export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
48
- const seen = new Map<object, object>();
54
+ const seen = options.seen ?? new Map<object, object>();
49
55
 
50
56
  /** Record `copy` as the copy of `obj` before its contents are copied. */
51
57
  const keep = <C extends object>(obj: object, copy: C): C => {
@@ -53,11 +59,20 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
53
59
  return copy;
54
60
  };
55
61
 
56
- /** Copy the own (enumerable) keys of `obj` into `copy`. */
62
+ /**
63
+ * Copy the own (enumerable) keys of `obj` into `copy`, symbol keys too: a
64
+ * save refuses them, which a copy must not hide.
65
+ */
57
66
  function copyKeys(obj: object, copy: object): object {
58
67
  for (const key of Object.keys(obj)) {
59
68
  setOwn(copy, key, clone((obj as Record<string, unknown>)[key]));
60
69
  }
70
+ for (const sym of Object.getOwnPropertySymbols(obj)) {
71
+ if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) continue;
72
+ (copy as Record<symbol, unknown>)[sym] = clone(
73
+ (obj as Record<symbol, unknown>)[sym],
74
+ );
75
+ }
61
76
  return copy;
62
77
  }
63
78
 
@@ -195,20 +210,25 @@ function loosePairs(): Pairs {
195
210
  /**
196
211
  * Pairs that also require the same reference structure: each object of one
197
212
  * value pairs with exactly one of the other, so shared references and
198
- * cycles are shared at the same places.
213
+ * cycles are shared at the same places. `ab` and `ba` hold the pairs (and
214
+ * may hold some already); those made are listed in `added`.
199
215
  */
200
- function strictPairs(): Pairs {
201
- const ab = new Map<object, object>();
202
- const ba = new Map<object, object>();
203
- const pairs: Pairs = (a, b) => {
216
+ function strictPairs(
217
+ ab = new Map<object, object>(),
218
+ ba = new Map<object, object>(),
219
+ ): Pairs & { added: [object, object][] } {
220
+ const added: [object, object][] = [];
221
+ const pairs = ((a, b) => {
204
222
  const known = ab.get(a);
205
223
  if (known !== undefined) return known === b;
206
224
  if (ba.has(b)) return false;
207
225
  ab.set(a, b);
208
226
  ba.set(b, a);
227
+ added.push([a, b]);
209
228
  return undefined;
210
- };
229
+ }) as Pairs & { added: [object, object][] };
211
230
  pairs.strict = true;
231
+ pairs.added = added;
212
232
  return pairs;
213
233
  }
214
234
 
@@ -225,28 +245,55 @@ export function deepEqualStrict(a: unknown, b: unknown): boolean {
225
245
  * `prev` at the same place replaced by `prev`'s: the history moments of a
226
246
  * save then share what did not change between them, which a save stores
227
247
  * once. Containers on the way to a change are copied; neither value is
228
- * changed. Shared references and cycles within `curr` stay so.
248
+ * changed. The result has the reference structure of `curr`: what `curr`
249
+ * shares or cycles through stays so, and what it does not share stays
250
+ * apart, whichever parts are reused.
229
251
  */
230
252
  export function shareEqual<T>(prev: unknown, curr: T): T {
231
- const done = new Map<object, unknown>();
232
- function share(p: unknown, c: unknown): unknown {
233
- if (p === c || !isObjectValue(c) || !isObjectValue(p)) return c;
234
- if (done.has(c)) return done.get(c);
235
- if (deepEqualStrict(p, c)) {
236
- done.set(c, p);
237
- return p;
253
+ if (!isObjectValue(prev) || !isObjectValue(curr)) return curr;
254
+ /** The object of the result standing for each object of `curr` met. */
255
+ const done = new Map<object, object>();
256
+ /** The object of `curr` each object of `prev` in the result stands for. */
257
+ const used = new Map<object, object>();
258
+
259
+ /** `value` for the result: copied, as far as it is not met already. */
260
+ const fresh = <V>(value: V): V =>
261
+ isObjectValue(value)
262
+ ? deepClone(value, { keepUnregistered: true, seen: done })
263
+ : value;
264
+
265
+ /**
266
+ * Whether `p` and `c` are equal and so can stand for each other, which
267
+ * needs each object in them to stand for one object only, in the whole
268
+ * result. Then every pair of objects they hold is placed in the result.
269
+ */
270
+ function reuse(p: object, c: object): boolean {
271
+ const pairs = strictPairs(used, done);
272
+ if (equal(p, c, pairs)) return true;
273
+ for (const [a, b] of pairs.added) {
274
+ used.delete(a);
275
+ done.delete(b);
238
276
  }
277
+ return false;
278
+ }
279
+
280
+ function share(p: unknown, c: unknown): unknown {
281
+ if (!isObjectValue(c)) return c;
282
+ const placed = done.get(c);
283
+ if (placed !== undefined) return placed;
284
+ if (!isObjectValue(p)) return fresh(c);
285
+ if (reuse(p, c)) return p;
239
286
  if (c instanceof Map && p instanceof Map) {
240
287
  const out = new Map();
241
288
  done.set(c, out);
242
- for (const [k, v] of c) out.set(k, p.has(k) ? share(p.get(k), v) : v);
289
+ for (const [k, v] of c) out.set(fresh(k), share(p.get(k), v));
243
290
  return out;
244
291
  }
245
292
  if (Array.isArray(c) && Array.isArray(p)) {
246
293
  const out = new Array(c.length) as unknown[];
247
294
  done.set(c, out);
248
295
  for (let i = 0; i < c.length; i++) {
249
- if (i in c) out[i] = i < p.length ? share(p[i], c[i]) : c[i];
296
+ if (i in c) out[i] = share(p[i], c[i]);
250
297
  }
251
298
  return out;
252
299
  }
@@ -255,12 +302,11 @@ export function shareEqual<T>(prev: unknown, curr: T): T {
255
302
  done.set(c, out);
256
303
  const cr = c as Record<string, unknown>;
257
304
  for (const key of Object.keys(cr)) {
258
- setOwn(out, key, hasOwn(p, key) ? share(p[key], cr[key]) : cr[key]);
305
+ setOwn(out, key, share(hasOwn(p, key) ? p[key] : undefined, cr[key]));
259
306
  }
260
307
  return out;
261
308
  }
262
- done.set(c, c);
263
- return c;
309
+ return fresh(c);
264
310
  }
265
311
  return share(prev, curr) as T;
266
312
  }