@rohal12/spindle 0.51.3 → 0.51.4

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.
@@ -118,8 +118,11 @@ function parseHtmlAttributes(input, j) {
118
118
  while (j < input.length && /[a-zA-Z0-9_\-:@]/.test(input[j])) j++;
119
119
  const attrName = input.slice(attrStart, j);
120
120
  if (!attrName) break;
121
- if (input[j] === "=") {
122
- j++;
121
+ let eqIdx = j;
122
+ while (eqIdx < input.length && /\s/.test(input[eqIdx])) eqIdx++;
123
+ if (input[eqIdx] === "=") {
124
+ j = eqIdx + 1;
125
+ while (j < input.length && /\s/.test(input[j])) j++;
123
126
  if (input[j] === "\"" || input[j] === "'") {
124
127
  const quote = input[j];
125
128
  j++;
@@ -260,16 +263,23 @@ function tokenize(input) {
260
263
  textStart = closeEnd;
261
264
  }
262
265
  while (i < input.length) {
263
- if (input[i] === "\\" && (input[i + 1] === "{" || input[i + 1] === "}")) {
264
- flushText(i);
265
- tokens.push({
266
- type: "text",
267
- value: input[i + 1],
268
- start: i,
269
- end: i + 2
270
- });
271
- i += 2;
272
- textStart = i;
266
+ if (input[i] === "\\") {
267
+ let k = i + 1;
268
+ while (input[k] === "\\") k++;
269
+ const next = input[k];
270
+ if ((next === "{" || next === "}") && (k - i) % 2 === 1) {
271
+ flushText(k - 1);
272
+ tokens.push({
273
+ type: "text",
274
+ value: next,
275
+ start: k - 1,
276
+ end: k + 1
277
+ });
278
+ i = k + 1;
279
+ textStart = i;
280
+ continue;
281
+ }
282
+ i = k;
273
283
  continue;
274
284
  }
275
285
  if (input[i] === "[" && input[i + 1] === "[") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.51.3",
3
+ "version": "0.51.4",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -313,3 +313,79 @@ export function deserialize<T>(value: T): T {
313
313
 
314
314
  return deser(value) as T;
315
315
  }
316
+
317
+ // --- Validate ---
318
+
319
+ function isDataRecord(value: unknown): value is Record<string, unknown> {
320
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
321
+ }
322
+
323
+ /**
324
+ * Whether `value` is serialized data that deserialize() can restore: every
325
+ * tagged value has the shape serialize() writes for its tag (a valid ISO
326
+ * date, a compilable RegExp, Map entries as `[key, value]` pairs, Set
327
+ * entries as an array, class data as an object), at any depth. Use it on
328
+ * data from outside the running story, such as an imported save, before
329
+ * storing it. Tags of unregistered classes pass; they load as plain objects.
330
+ */
331
+ export function isDeserializable(value: unknown): boolean {
332
+ // Objects on the current path; JSON can't hold cycles, and deserialize()
333
+ // can't restore them
334
+ const path = new Set<object>();
335
+
336
+ function check(val: unknown): boolean {
337
+ if (val === null || typeof val !== 'object') return true;
338
+ if (path.has(val)) return false;
339
+ path.add(val);
340
+ const ok = checkObject(val);
341
+ path.delete(val);
342
+ return ok;
343
+ }
344
+
345
+ function checkObject(val: object): boolean {
346
+ if (Array.isArray(val)) return val.every(check);
347
+
348
+ const obj = val as Record<string, unknown>;
349
+ if (!(CLASS_TAG in obj && DATA_TAG in obj)) {
350
+ return Object.keys(obj).every((key) => check(obj[key]));
351
+ }
352
+
353
+ const name = obj[CLASS_TAG];
354
+ const data = obj[DATA_TAG];
355
+ if (typeof name !== 'string' || !isDataRecord(data)) return false;
356
+
357
+ switch (name) {
358
+ case '__Date__':
359
+ return (
360
+ typeof data.iso === 'string' &&
361
+ !Number.isNaN(new Date(data.iso).getTime())
362
+ );
363
+ case '__RegExp__':
364
+ if (typeof data.source !== 'string' || typeof data.flags !== 'string')
365
+ return false;
366
+ try {
367
+ new RegExp(data.source, data.flags);
368
+ return true;
369
+ } catch {
370
+ return false;
371
+ }
372
+ case '__Map__':
373
+ return (
374
+ Array.isArray(data.entries) &&
375
+ data.entries.every(
376
+ (entry: unknown) =>
377
+ Array.isArray(entry) &&
378
+ entry.length === 2 &&
379
+ check(entry[0]) &&
380
+ check(entry[1]),
381
+ )
382
+ );
383
+ case '__Set__':
384
+ return Array.isArray(data.entries) && data.entries.every(check);
385
+ default:
386
+ return Object.keys(data).every((key) => check(data[key]));
387
+ }
388
+ }
389
+
390
+ return check(value);
391
+ }
@@ -1,6 +1,7 @@
1
1
  import { h, render } from 'preact';
2
2
  import { useContext } from 'preact/hooks';
3
3
  import { defineMacro } from '../../define-macro';
4
+ import { readWholeQuoted } from './arg-utils';
4
5
  import {
5
6
  renderNodes,
6
7
  LocalsUpdateContext,
@@ -14,7 +15,9 @@ defineMacro({
14
15
  block: true,
15
16
  interpolate: true,
16
17
  render({ rawArgs, children = [] }, ctx) {
17
- const label = ctx.resolve?.(rawArgs.replace(/^["']|["']$/g, '')) ?? rawArgs;
18
+ const text =
19
+ readWholeQuoted(rawArgs.trim()) ?? rawArgs.replace(/^["']|["']$/g, '');
20
+ const label = ctx.resolve?.(text) ?? rawArgs;
18
21
  const nobr = useContext(NobrContext);
19
22
 
20
23
  const handleClick = () => {
@@ -1,8 +1,11 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import { readWholeQuoted, stripLooseQuotes } from './arg-utils';
2
3
 
3
4
  function parseLabel(rawArgs: string): string {
4
- const match = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+["']?(.+?)["']?\s*$/);
5
- return match?.[1] ?? '';
5
+ const match = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+(.+?)\s*$/);
6
+ if (!match) return '';
7
+ const rest = match[1]!;
8
+ return readWholeQuoted(rest) ?? stripLooseQuotes(rest);
6
9
  }
7
10
 
8
11
  defineMacro({
@@ -1,5 +1,6 @@
1
1
  import { defineMacro } from '../../define-macro';
2
2
  import { PassageDialog } from '../PassageDialog';
3
+ import { readWholeQuoted } from './arg-utils';
3
4
 
4
5
  defineMacro({
5
6
  name: 'dialog',
@@ -11,7 +12,9 @@ defineMacro({
11
12
  const noclose = /\bnoclose\s*$/.test(rawArgs);
12
13
  const labelRaw = rawArgs.replace(/\bnoclose\s*$/, '').trim();
13
14
  const label =
14
- ctx.resolve?.(labelRaw.replace(/^["']|["']$/g, '')) ?? labelRaw;
15
+ ctx.resolve?.(
16
+ readWholeQuoted(labelRaw) ?? labelRaw.replace(/^["']|["']$/g, ''),
17
+ ) ?? labelRaw;
15
18
  const passageName = ctx
16
19
  .collectText(children)
17
20
  .trim()
@@ -9,10 +9,12 @@ import {
9
9
  LocalsValuesContext,
10
10
  LocalsUpdateContext,
11
11
  NobrContext,
12
+ InlineContext,
12
13
  renderNodes,
13
14
  } from '../../markup/render';
14
15
  import { defineMacro } from '../../define-macro';
15
16
  import { MacroError } from './MacroError';
17
+ import { stableKey } from '../../utils/stable-key';
16
18
  import type { ASTNode } from '../../markup/ast';
17
19
 
18
20
  /**
@@ -97,11 +99,12 @@ function ForIteration({
97
99
  const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
98
100
 
99
101
  const nobr = useContext(NobrContext);
102
+ const inline = useContext(InlineContext);
100
103
 
101
104
  return (
102
105
  <LocalsUpdateContext.Provider value={updater}>
103
106
  <LocalsValuesContext.Provider value={localState}>
104
- {renderNodes(children, { nobr, locals: localState })}
107
+ {renderNodes(children, { nobr, inline, locals: localState })}
105
108
  </LocalsValuesContext.Provider>
106
109
  </LocalsUpdateContext.Provider>
107
110
  );
@@ -151,7 +154,7 @@ defineMacro({
151
154
 
152
155
  const content = list.map((item, i) => (
153
156
  <ForIteration
154
- key={`${i}-${JSON.stringify(item)}`}
157
+ key={`${i}-${stableKey(item)}`}
155
158
  parentValues={parentValues}
156
159
  itemVar={itemVar}
157
160
  itemValue={item}
@@ -3,6 +3,7 @@ import { tokenize } from '../../markup/tokenizer';
3
3
  import { buildAST } from '../../markup/ast';
4
4
  import { NobrContext } from '../../markup/render';
5
5
  import { defineMacro } from '../../define-macro';
6
+ import { isWhitespace, topLevelIndices } from './arg-utils';
6
7
 
7
8
  const FLAG = 'inline';
8
9
 
@@ -19,27 +20,16 @@ function parseIncludeArgs(rawArgs: string): {
19
20
  const trimmed = rawArgs.trim();
20
21
 
21
22
  // First and last whitespace runs at depth 0, as [start, end) offsets.
23
+ const spaces = topLevelIndices(trimmed, isWhitespace);
22
24
  let first: [number, number] | null = null;
23
25
  let last: [number, number] | null = null;
24
- let depth = 0;
25
- let inString: string | null = null;
26
- for (let i = 0; i < trimmed.length; i++) {
27
- const ch = trimmed[i]!;
28
- if (inString) {
29
- if (ch === '\\') i++;
30
- else if (ch === inString) inString = null;
31
- continue;
32
- }
33
- if (ch === '"' || ch === "'" || ch === '`') inString = ch;
34
- else if (ch === '(' || ch === '[' || ch === '{') depth++;
35
- else if (ch === ')' || ch === ']' || ch === '}') depth--;
36
- else if (depth === 0 && /\s/.test(ch)) {
37
- let end = i + 1;
38
- while (end < trimmed.length && /\s/.test(trimmed[end]!)) end++;
39
- last = [i, end];
40
- first ??= last;
41
- i = end - 1;
42
- }
26
+ if (spaces.length > 0) {
27
+ let s = 0;
28
+ while (s + 1 < spaces.length && spaces[s + 1] === spaces[s]! + 1) s++;
29
+ first = [spaces[0]!, spaces[s]! + 1];
30
+ let e = spaces.length - 1;
31
+ while (e > 0 && spaces[e - 1] === spaces[e]! - 1) e--;
32
+ last = [spaces[e]!, spaces[spaces.length - 1]! + 1];
43
33
  }
44
34
 
45
35
  // Next to a binary operator the word is an operand (`"a" + inline`).
@@ -9,6 +9,7 @@ import {
9
9
  } from '../../markup/render';
10
10
  import { defineMacro } from '../../define-macro';
11
11
  import { liveLocalsView } from '../../utils/live-locals';
12
+ import { unescapeQuoted } from './arg-utils';
12
13
 
13
14
  function parseArgs(rawArgs: string): {
14
15
  display: string;
@@ -20,7 +21,7 @@ function parseArgs(rawArgs: string): {
20
21
  const re = /(["'])((?:\\[^]|(?!\1)[^\\])*)\1/g;
21
22
  let m;
22
23
  while ((m = re.exec(rawArgs)) !== null) {
23
- parts.push(m[2]!.replace(/\\(["'\\])/g, '$1'));
24
+ parts.push(unescapeQuoted(m[2]!));
24
25
  }
25
26
  if (parts.length >= 2) {
26
27
  return { display: parts[0]!, passage: parts[1]! };
@@ -1,63 +1,41 @@
1
1
  import { defineMacro } from '../../define-macro';
2
2
  import { MacroError } from './MacroError';
3
+ import { isWhitespace, readWholeQuoted, splitTopLevel } from './arg-utils';
3
4
 
4
- function parseArgs(rawArgs: string): {
5
+ /**
6
+ * Parse `{meter currentExpr maxExpr ["label"]}`. Arguments are separated by
7
+ * whitespace outside string and template literals and bracket pairs; a
8
+ * trailing standalone `"…"` or `'…'` string is the label mode, with `\"`,
9
+ * `\'` and `\\` escapes.
10
+ */
11
+ export function parseMeterArgs(rawArgs: string): {
5
12
  currentExpr: string;
6
13
  maxExpr: string;
7
14
  labelMode: string;
8
15
  } {
9
- const trimmed = rawArgs.trim();
16
+ const tokens = splitTopLevel(rawArgs.trim(), isWhitespace).filter(Boolean);
10
17
 
11
- // Extract quoted label mode from the end if present
12
18
  let labelMode = '';
13
- let rest = trimmed;
14
- const quoteMatch = rest.match(/\s+(?:"([^"]*)"|'([^']*)')$/);
15
- if (quoteMatch) {
16
- labelMode = quoteMatch[1] ?? quoteMatch[2] ?? '';
17
- rest = rest.slice(0, quoteMatch.index ?? rest.length).trim();
19
+ const label =
20
+ tokens.length >= 2 ? readWholeQuoted(tokens[tokens.length - 1]!) : null;
21
+ if (label !== null) {
22
+ labelMode = label;
23
+ tokens.pop();
18
24
  }
19
25
 
20
- // Split remaining into two expressions.
21
- const exprs = splitExpressions(rest);
22
- if (exprs.length < 2) {
26
+ if (tokens.length < 2) {
23
27
  throw new Error(
24
28
  'meter requires two arguments: {meter currentExpr maxExpr}',
25
29
  );
26
30
  }
27
31
 
28
32
  return {
29
- currentExpr: exprs[0]!,
30
- maxExpr: exprs.slice(1).join(' '),
33
+ currentExpr: tokens[0]!,
34
+ maxExpr: tokens.slice(1).join(' '),
31
35
  labelMode,
32
36
  };
33
37
  }
34
38
 
35
- function splitExpressions(input: string): string[] {
36
- const result: string[] = [];
37
- let current = '';
38
- let depth = 0;
39
-
40
- for (let i = 0; i < input.length; i++) {
41
- const ch = input[i]!;
42
- if (ch === '(' || ch === '[') {
43
- depth++;
44
- current += ch;
45
- } else if (ch === ')' || ch === ']') {
46
- depth--;
47
- current += ch;
48
- } else if (/\s/.test(ch) && depth === 0 && current.length > 0) {
49
- result.push(current);
50
- current = '';
51
- // Skip additional whitespace
52
- while (i + 1 < input.length && /\s/.test(input[i + 1]!)) i++;
53
- } else {
54
- current += ch;
55
- }
56
- }
57
- if (current.length > 0) result.push(current);
58
- return result;
59
- }
60
-
61
39
  function formatLabel(
62
40
  current: number,
63
41
  max: number,
@@ -76,7 +54,7 @@ defineMacro({
76
54
  merged: true,
77
55
  render({ rawArgs }, ctx) {
78
56
  try {
79
- const { currentExpr, maxExpr, labelMode } = parseArgs(rawArgs);
57
+ const { currentExpr, maxExpr, labelMode } = parseMeterArgs(rawArgs);
80
58
  const current = Number(ctx.evaluate!(currentExpr));
81
59
  const max = Number(ctx.evaluate!(maxExpr));
82
60
  const pct =
@@ -1,6 +1,28 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import {
3
+ isWhitespace,
4
+ readQuoted,
5
+ readWholeQuoted,
6
+ stripLooseQuotes,
7
+ } from './arg-utils';
2
8
 
3
9
  function parseRadioArgs(rawArgs: string): { value: string; label: string } {
10
+ // $var "value" label: a quoted value accepts \" \' and \\ escapes and
11
+ // may contain the other quote kind.
12
+ const head = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+/);
13
+ if (head) {
14
+ const rest = rawArgs.slice(head[0].length).trim();
15
+ const value = readQuoted(rest, 0);
16
+ if (
17
+ value &&
18
+ (value.end === rest.length || isWhitespace(rest[value.end]!))
19
+ ) {
20
+ const labelRaw = rest.slice(value.end).trim();
21
+ const label = readWholeQuoted(labelRaw) ?? stripLooseQuotes(labelRaw);
22
+ return { value: value.value, label };
23
+ }
24
+ }
25
+
4
26
  const match = rawArgs.match(
5
27
  /^\s*["']?\$?[\w.]+["']?\s+["'](.+?)["']\s+["']?(.+?)["']?\s*$/,
6
28
  );
@@ -5,7 +5,7 @@ import {
5
5
  useRef,
6
6
  useContext,
7
7
  } from 'preact/hooks';
8
- import { useStoryStore } from '../../store';
8
+ import { useStoryStore, resolvePlaythroughId } from '../../store';
9
9
  import { isSaveExport, type SaveRecord } from '../../saves/types';
10
10
  import {
11
11
  getSavesGrouped,
@@ -106,11 +106,16 @@ export function SaveManagerContent() {
106
106
 
107
107
  // Dialog saves are not slot saves, so the save hooks get no slot.
108
108
  const handleNewSave = async () => {
109
- if (!ifid || !playthroughId) return;
109
+ if (!ifid) return;
110
+ // The playthrough current at the click, which a restart may have
111
+ // switched since this render
112
+ const playthrough = resolvePlaythroughId();
110
113
  try {
111
- await saveWithHooks(undefined, undefined, beginSave, (payload) =>
112
- createSave(ifid, playthroughId, payload),
113
- );
114
+ await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
115
+ const ptId = await playthrough;
116
+ if (!ptId) throw new Error('No playthrough');
117
+ return createSave(ifid, ptId, payload);
118
+ });
114
119
  showStatus('Save created');
115
120
  await refresh();
116
121
  } catch {
@@ -119,9 +124,12 @@ export function SaveManagerContent() {
119
124
  };
120
125
 
121
126
  const handleOverwrite = async (saveId: string) => {
127
+ const playthrough = resolvePlaythroughId();
122
128
  try {
123
129
  await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
124
- if (!(await overwriteSave(saveId, payload))) {
130
+ // Only the current playthrough's saves offer "Save Here"
131
+ const ptId = (await playthrough) || undefined;
132
+ if (!(await overwriteSave(saveId, payload, undefined, ptId))) {
125
133
  throw new Error('Save not found');
126
134
  }
127
135
  });
@@ -1,51 +1,100 @@
1
1
  import { defineMacro } from '../../define-macro';
2
2
  import { addMacroTrigger, removeTrigger } from '../../triggers';
3
3
  import type { WatchOptions } from '../../triggers';
4
+ import { isWhitespace, readQuoted, readWholeQuoted } from './arg-utils';
4
5
 
5
- defineMacro({
6
- name: 'watch',
7
- render(props, ctx) {
8
- const { hooks } = ctx;
6
+ const WORD_RE = /\w+/y;
7
+ const DIGITS_RE = /\d+/y;
9
8
 
10
- // Parse: condition string + keyword options
11
- const raw = props.rawArgs.trim();
12
- // First quoted string is the condition
13
- const condMatch = raw.match(/^(['"])(.*?)\1\s*/);
14
- if (!condMatch) return null;
9
+ /** Match the sticky regex `re` at `pos`, returning the matched text. */
10
+ function matchAt(re: RegExp, src: string, pos: number): string | null {
11
+ re.lastIndex = pos;
12
+ return re.exec(src)?.[0] ?? null;
13
+ }
15
14
 
16
- const condition = condMatch[2]!;
17
- const rest = raw.slice(condMatch[0].length);
15
+ /**
16
+ * Parse `{watch "condition" keyword value …}`: a quoted condition followed
17
+ * by keyword options (`goto "X"`, `dialog "X"`, `run "X"`, `name "X"`,
18
+ * `priority N`, `once`). Quoted strings accept `\"`, `\'` and `\\` escapes.
19
+ * Returns `null` when the arguments do not start with a quoted condition.
20
+ */
21
+ export function parseWatchArgs(
22
+ rawArgs: string,
23
+ ): { condition: string; options: WatchOptions } | null {
24
+ const raw = rawArgs.trim();
25
+ const cond = readQuoted(raw, 0);
26
+ if (!cond) return null;
18
27
 
19
- const options: WatchOptions = {};
20
- // Parse keyword args: goto "X", dialog "X", run "X", once, name "X", priority N
21
- const kwRe = /(\w+)\s+(?:"([^"]*?)"|'([^']*?)'|(\d+))|(\w+)/g;
22
- let m: RegExpExecArray | null;
23
- while ((m = kwRe.exec(rest)) !== null) {
24
- const key = m[1] ?? m[5];
25
- const val = m[2] ?? m[3] ?? m[4];
26
- if (!key) continue;
27
- switch (key) {
28
- case 'goto':
29
- options.goto = val;
30
- break;
31
- case 'dialog':
32
- options.dialog = val;
33
- break;
34
- case 'run':
35
- options.run = val;
36
- break;
37
- case 'name':
38
- options.name = val;
39
- break;
40
- case 'priority':
41
- options.priority = val ? Number(val) : 0;
42
- break;
43
- case 'once':
44
- options.once = true;
45
- break;
28
+ const options: WatchOptions = {};
29
+ let i = cond.end;
30
+ while (i < raw.length) {
31
+ const key = matchAt(WORD_RE, raw, i);
32
+ if (!key) {
33
+ // Skip whitespace and stray characters, or a stray quoted string.
34
+ i = readQuoted(raw, i)?.end ?? i + 1;
35
+ continue;
36
+ }
37
+ i += key.length;
38
+
39
+ // A value is a quoted string or a digit run, after whitespace.
40
+ let val: string | undefined;
41
+ let j = i;
42
+ while (j < raw.length && isWhitespace(raw[j]!)) j++;
43
+ if (j > i) {
44
+ const quoted = readQuoted(raw, j);
45
+ const digits = quoted ? null : matchAt(DIGITS_RE, raw, j);
46
+ if (quoted) {
47
+ val = quoted.value;
48
+ i = quoted.end;
49
+ } else if (digits) {
50
+ val = digits;
51
+ i = j + digits.length;
46
52
  }
47
53
  }
48
54
 
55
+ switch (key) {
56
+ case 'goto':
57
+ options.goto = val;
58
+ break;
59
+ case 'dialog':
60
+ options.dialog = val;
61
+ break;
62
+ case 'run':
63
+ options.run = val;
64
+ break;
65
+ case 'name':
66
+ options.name = val;
67
+ break;
68
+ case 'priority':
69
+ options.priority = val ? Number(val) : 0;
70
+ break;
71
+ case 'once':
72
+ options.once = true;
73
+ break;
74
+ }
75
+ }
76
+
77
+ return { condition: cond.value, options };
78
+ }
79
+
80
+ /**
81
+ * Parse the watcher name of `{unwatch "name"}`: a quoted string, with the
82
+ * same escapes as `{watch}`, or a bare name.
83
+ */
84
+ export function parseUnwatchName(rawArgs: string): string {
85
+ const raw = rawArgs.trim();
86
+ return readWholeQuoted(raw) ?? raw.replace(/^['"]|['"]$/g, '');
87
+ }
88
+
89
+ defineMacro({
90
+ name: 'watch',
91
+ render(props, ctx) {
92
+ const { hooks } = ctx;
93
+
94
+ const parsed = parseWatchArgs(props.rawArgs);
95
+ if (!parsed) return null;
96
+ const { condition, options } = parsed;
97
+
49
98
  // Register during render (like {set}) — triggers survive navigation
50
99
  // and are cleaned up via resetTriggers() on restart or {unwatch}.
51
100
  // Remounts (revisits, re-rendered branches) keep the registered watcher.
@@ -62,8 +111,7 @@ defineMacro({
62
111
  defineMacro({
63
112
  name: 'unwatch',
64
113
  render(props, ctx) {
65
- const raw = props.rawArgs.trim();
66
- const name = raw.replace(/^['"]|['"]$/g, '');
114
+ const name = parseUnwatchName(props.rawArgs);
67
115
 
68
116
  const ran = ctx.hooks.useRef(false);
69
117
  if (!ran.current) {