@rohal12/spindle 0.51.4 → 0.52.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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +4512 -1702
- package/dist/pkg/macro-registry.json +7 -7
- package/dist/pkg/story-variables.js +1636 -177
- package/package.json +5 -2
- package/src/action-registry.ts +70 -18
- package/src/automation/runner.ts +2 -1
- package/src/class-registry.ts +214 -103
- package/src/components/Passage.tsx +2 -2
- package/src/components/PassageDialog.tsx +2 -5
- package/src/components/StoryInterface.tsx +2 -4
- package/src/components/macros/Button.tsx +5 -31
- package/src/components/macros/Checkbox.tsx +7 -4
- package/src/components/macros/Computed.tsx +28 -14
- package/src/components/macros/For.tsx +29 -3
- package/src/components/macros/If.tsx +8 -0
- package/src/components/macros/Include.tsx +7 -6
- package/src/components/macros/MacroError.tsx +2 -1
- package/src/components/macros/MacroLink.tsx +12 -45
- package/src/components/macros/Meter.tsx +11 -3
- package/src/components/macros/Nobr.tsx +1 -0
- package/src/components/macros/PassageDisplay.tsx +3 -0
- package/src/components/macros/Print.tsx +4 -0
- package/src/components/macros/Radiobutton.tsx +5 -2
- package/src/components/macros/SaveManager.tsx +25 -8
- package/src/components/macros/Set.tsx +5 -4
- package/src/components/macros/Span.tsx +1 -0
- package/src/components/macros/StoryTitle.tsx +1 -0
- package/src/components/macros/Switch.tsx +13 -0
- package/src/components/macros/Unset.tsx +31 -10
- package/src/components/macros/VarDisplay.tsx +21 -4
- package/src/components/macros/Widget.tsx +20 -1
- package/src/components/macros/WidgetInvocation.tsx +17 -75
- package/src/components/macros/arg-utils.ts +107 -1
- package/src/components/macros/detached-body.tsx +68 -0
- package/src/components/macros/option-utils.ts +3 -2
- package/src/define-macro.ts +32 -5
- package/src/execute-mutation.ts +271 -68
- package/src/expression.ts +91 -59
- package/src/hooks/use-action.ts +24 -3
- package/src/hooks/use-interpolate.ts +36 -5
- package/src/index.tsx +12 -2
- package/src/interpolation.ts +394 -96
- package/src/js-lexer.ts +1231 -97
- package/src/markup/ast.ts +7 -2
- package/src/markup/code-attributes.ts +64 -0
- package/src/markup/markdown.ts +188 -9
- package/src/markup/render.tsx +430 -49
- package/src/markup/tokenizer.ts +578 -110
- package/src/prng.ts +41 -8
- package/src/registry.ts +35 -0
- package/src/saves/save-manager.ts +346 -160
- package/src/saves/storage.ts +16 -7
- package/src/saves/types.ts +2 -1
- package/src/settings.ts +12 -9
- package/src/store.ts +640 -186
- package/src/story-api.ts +38 -73
- package/src/story-init.ts +1 -1
- package/src/story-variables.ts +98 -102
- package/src/triggers.ts +10 -12
- package/src/utils/counts.ts +45 -0
- package/src/utils/error-message.ts +12 -0
- package/src/utils/live-locals.ts +10 -3
- package/src/utils/namespace.ts +71 -0
- package/src/utils/object-path.ts +99 -14
- package/src/utils/stable-key.ts +14 -9
- package/src/widgets/widget-registry.ts +9 -0
- package/types/index.d.ts +43 -7
- package/types/tooling.d.ts +1 -0
package/src/markup/ast.ts
CHANGED
|
@@ -242,8 +242,13 @@ export function buildAST(tokens: Token[]): ASTNode[] {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// Handle branch macros (elseif/else, case/default, next)
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
const expectedParent = Object.prototype.hasOwnProperty.call(
|
|
246
|
+
BRANCH_PARENT,
|
|
247
|
+
name,
|
|
248
|
+
)
|
|
249
|
+
? BRANCH_PARENT[name]
|
|
250
|
+
: undefined;
|
|
251
|
+
if (expectedParent) {
|
|
247
252
|
const topNode =
|
|
248
253
|
stack.length > 0 ? stack[stack.length - 1]!.node : null;
|
|
249
254
|
if (
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createScanMemo, scanBalancedBrace } from './tokenizer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attributes whose value is code with braces of its own, not text: event
|
|
5
|
+
* handlers (`on…`, JavaScript), `pattern` (a regular expression, as in
|
|
6
|
+
* `\p{L}{2,3}`) and `srcdoc` (an HTML document, with its own scripts and
|
|
7
|
+
* styles). Their braces are code, so they are not read as markup: only
|
|
8
|
+
* sigil references (`{$x}`, `{_x}`, `{@x}`, `{%x}` and expressions starting
|
|
9
|
+
* with one) are resolved, and any other `{` is literal.
|
|
10
|
+
*/
|
|
11
|
+
export function isCodeAttribute(name: string): boolean {
|
|
12
|
+
const lower = name.toLowerCase();
|
|
13
|
+
return lower.startsWith('on') || lower === 'pattern' || lower === 'srcdoc';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Literal text, the source of one `{…}` reference (without braces), or a
|
|
17
|
+
* `{…}` block that is kept exactly as written. */
|
|
18
|
+
export type SigilPart =
|
|
19
|
+
| { text: string }
|
|
20
|
+
| { expr: string }
|
|
21
|
+
| { verbatim: string };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Split a code attribute value into literal text and its `{…}` blocks that
|
|
25
|
+
* start with a sigil. Braces inside strings don't close a block; an
|
|
26
|
+
* unclosed block, any other brace and backslashes are text. A block whose
|
|
27
|
+
* sigil is not followed by a word character (`{$(…)}`) is code, kept
|
|
28
|
+
* verbatim: not resolved, and not decoded like text.
|
|
29
|
+
*/
|
|
30
|
+
export function splitSigilTemplate(template: string): SigilPart[] {
|
|
31
|
+
const parts: SigilPart[] = [];
|
|
32
|
+
const memo = createScanMemo();
|
|
33
|
+
let text = '';
|
|
34
|
+
let i = 0;
|
|
35
|
+
|
|
36
|
+
while (i < template.length) {
|
|
37
|
+
const brace = template.indexOf('{', i);
|
|
38
|
+
if (brace === -1) {
|
|
39
|
+
text += template.slice(i);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
text += template.slice(i, brace);
|
|
43
|
+
i = brace + 1;
|
|
44
|
+
const sigil = template[i];
|
|
45
|
+
const end =
|
|
46
|
+
sigil === '$' || sigil === '_' || sigil === '@' || sigil === '%'
|
|
47
|
+
? scanBalancedBrace(template, i, memo)
|
|
48
|
+
: -1;
|
|
49
|
+
if (end === -1) {
|
|
50
|
+
text += '{';
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (text) parts.push({ text });
|
|
54
|
+
text = '';
|
|
55
|
+
const block = template.slice(i, end);
|
|
56
|
+
parts.push(
|
|
57
|
+
/^.\w/.test(block) ? { expr: block } : { verbatim: `{${block}}` },
|
|
58
|
+
);
|
|
59
|
+
i = end + 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (text) parts.push({ text });
|
|
63
|
+
return parts;
|
|
64
|
+
}
|
package/src/markup/markdown.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { micromark } from 'micromark';
|
|
2
|
+
import type {
|
|
3
|
+
Code,
|
|
4
|
+
Construct,
|
|
5
|
+
Extension,
|
|
6
|
+
Options,
|
|
7
|
+
Resolver,
|
|
8
|
+
} from 'micromark-util-types';
|
|
2
9
|
import { gfmTable, gfmTableHtml } from 'micromark-extension-gfm-table';
|
|
3
10
|
import {
|
|
4
11
|
gfmStrikethrough,
|
|
@@ -6,17 +13,15 @@ import {
|
|
|
6
13
|
} from 'micromark-extension-gfm-strikethrough';
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
16
|
+
* micromark options for spindle's markdown: CommonMark with GFM tables and
|
|
17
|
+
* strikethrough, raw HTML passed through, and no indented code.
|
|
11
18
|
*
|
|
12
19
|
* When `inline` is true, block-level constructs (lists, headings, blockquotes,
|
|
13
|
-
* thematic breaks) are disabled. This is used when
|
|
14
|
-
* inline HTML elements like `<span>` where
|
|
20
|
+
* thematic breaks, fenced code, tables) are disabled. This is used when
|
|
21
|
+
* rendering content inside inline HTML elements like `<span>` where
|
|
22
|
+
* block-level output is invalid. Fences then read as code spans.
|
|
15
23
|
*/
|
|
16
|
-
export function
|
|
17
|
-
text: string,
|
|
18
|
-
options?: { inline?: boolean },
|
|
19
|
-
): string {
|
|
24
|
+
export function markdownOptions(options?: { inline?: boolean }): Options {
|
|
20
25
|
const disabled: string[] = ['codeIndented'];
|
|
21
26
|
if (options?.inline) {
|
|
22
27
|
disabled.push(
|
|
@@ -25,9 +30,11 @@ export function markdownToHtml(
|
|
|
25
30
|
'setextUnderline',
|
|
26
31
|
'thematicBreak',
|
|
27
32
|
'blockQuote',
|
|
33
|
+
'codeFenced',
|
|
34
|
+
'table',
|
|
28
35
|
);
|
|
29
36
|
}
|
|
30
|
-
return
|
|
37
|
+
return {
|
|
31
38
|
allowDangerousHtml: true,
|
|
32
39
|
extensions: [
|
|
33
40
|
gfmTable(),
|
|
@@ -35,5 +42,177 @@ export function markdownToHtml(
|
|
|
35
42
|
{ disable: { null: disabled } },
|
|
36
43
|
],
|
|
37
44
|
htmlExtensions: [gfmTableHtml(), gfmStrikethroughHtml()],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a text string as CommonMark markdown and return an HTML string, as
|
|
50
|
+
* micromark does with `markdownOptions`, but in about linear time (see
|
|
51
|
+
* `unclosedHtml` and `textData`).
|
|
52
|
+
*/
|
|
53
|
+
export function markdownToHtml(
|
|
54
|
+
text: string,
|
|
55
|
+
options?: { inline?: boolean },
|
|
56
|
+
): string {
|
|
57
|
+
const base = markdownOptions(options);
|
|
58
|
+
return micromark(text, {
|
|
59
|
+
...base,
|
|
60
|
+
extensions: [...(base.extensions ?? []), unclosedHtml(text), textData],
|
|
38
61
|
});
|
|
39
62
|
}
|
|
63
|
+
|
|
64
|
+
/** Join a data token just read to a data token right before it. */
|
|
65
|
+
const joinData: Resolver = (events) => {
|
|
66
|
+
const at = events.length - 2;
|
|
67
|
+
const before = events[at - 1];
|
|
68
|
+
if (before && before[0] === 'exit' && before[1].type === 'data') {
|
|
69
|
+
before[1].end = events[at]![1].end;
|
|
70
|
+
events.length = at;
|
|
71
|
+
}
|
|
72
|
+
return events;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Text that no construct takes, read as data joined to the data before it.
|
|
77
|
+
*
|
|
78
|
+
* Where every text construct fails at a character (a `!` without `[`, a `]`
|
|
79
|
+
* without a label, a `\` before a letter, an unclosed `<!--`), micromark
|
|
80
|
+
* reads it, on to the next character a construct may start at, as a new data
|
|
81
|
+
* token. Runs of data tokens are joined only after the paragraph is read,
|
|
82
|
+
* each run by splicing the paragraph's whole list of events: time quadratic
|
|
83
|
+
* in the number of runs where other tokens part them (`\]a!` repeated: an
|
|
84
|
+
* escape, data `a`, data `!`), and erratically so, as the cost of moving
|
|
85
|
+
* the list's elements depends on where the garbage collector keeps it.
|
|
86
|
+
*
|
|
87
|
+
* Tried where micromark's text tries constructs, after all of them (as a
|
|
88
|
+
* construct of every character), this reads that data as micromark would
|
|
89
|
+
* and joins it to the data before it at once, at the end of the list.
|
|
90
|
+
*/
|
|
91
|
+
const textData: Extension = {
|
|
92
|
+
text: {
|
|
93
|
+
null: {
|
|
94
|
+
name: 'textData',
|
|
95
|
+
tokenize(effects, ok) {
|
|
96
|
+
const self = this;
|
|
97
|
+
const constructs = this.parser.constructs.text;
|
|
98
|
+
|
|
99
|
+
/** Whether a construct may start at `code`, as micromark's text asks. */
|
|
100
|
+
function atBreak(code: Code): boolean {
|
|
101
|
+
if (code === null) return true;
|
|
102
|
+
const list = constructs[code] ?? [];
|
|
103
|
+
return (Array.isArray(list) ? list : [list]).some(
|
|
104
|
+
(item) => !item.previous || item.previous.call(self, self.previous),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function data(code: Code) {
|
|
109
|
+
if (atBreak(code)) {
|
|
110
|
+
effects.exit('data');
|
|
111
|
+
return ok(code);
|
|
112
|
+
}
|
|
113
|
+
effects.consume(code);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (code) => {
|
|
118
|
+
effects.enter('data');
|
|
119
|
+
effects.consume(code);
|
|
120
|
+
return data;
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
resolveTo: joinData,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Characters of an email autolink's local part (micromark's `asciiAtext`). */
|
|
129
|
+
const ATEXT = /[#-'*+\--9=?A-Z^-~]/;
|
|
130
|
+
const ALPHANUMERIC = /[0-9A-Za-z]/;
|
|
131
|
+
const ALPHA = /[A-Za-z]/;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Inline raw HTML that can't be closed in `text`, read as the text it is.
|
|
135
|
+
*
|
|
136
|
+
* At a `<` opening a comment (`<!--`), a processing instruction (`<?`), a
|
|
137
|
+
* CDATA section (`<![CDATA[`) or a declaration (`<!` and a letter), micromark
|
|
138
|
+
* reads on for the closer (`-->`, `?>`, `]]>`, `>`) to the end of the
|
|
139
|
+
* paragraph, and without one the `<` is text. Many such openers took time
|
|
140
|
+
* quadratic in their number. Where no closer comes later in the whole text,
|
|
141
|
+
* this construct, tried at each `<` before micromark's own, takes the `<`
|
|
142
|
+
* as text at once, as micromark would end up doing.
|
|
143
|
+
*
|
|
144
|
+
* It is tried exactly where micromark tries raw HTML: not in code, link
|
|
145
|
+
* destinations or HTML blocks (which start at a line start and run to their
|
|
146
|
+
* end condition or the end of the document), whose output stays micromark's.
|
|
147
|
+
* A `<?` may begin an email autolink (`<?a@b.c>`, tried before raw HTML),
|
|
148
|
+
* which it then leaves to micromark.
|
|
149
|
+
*/
|
|
150
|
+
function unclosedHtml(text: string): Extension {
|
|
151
|
+
// micromark drops a byte order mark, which offsets don't count
|
|
152
|
+
const shift = text.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
153
|
+
const comment = text.lastIndexOf('-->');
|
|
154
|
+
const instruction = text.lastIndexOf('?>');
|
|
155
|
+
const cdata = text.lastIndexOf(']]>');
|
|
156
|
+
const declaration = text.lastIndexOf('>');
|
|
157
|
+
|
|
158
|
+
/** Whether the raw HTML opening at `p` can't be closed. */
|
|
159
|
+
function unclosed(p: number): boolean {
|
|
160
|
+
if (text[p] !== '<') return false;
|
|
161
|
+
// A comment closes with a `-->` from just past `<!`: `<!-->` is one
|
|
162
|
+
if (text.startsWith('<!--', p)) return comment < p + 2;
|
|
163
|
+
if (text.startsWith('<![CDATA[', p)) return cdata < p + 9;
|
|
164
|
+
if (text[p + 1] === '!' && ALPHA.test(text[p + 2] ?? '')) {
|
|
165
|
+
return declaration < p + 3;
|
|
166
|
+
}
|
|
167
|
+
if (text[p + 1] === '?') return instruction < p + 2 && !emailAutolink(p);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Whether an email autolink opens at `p`, as micromark reads one. */
|
|
172
|
+
function emailAutolink(p: number): boolean {
|
|
173
|
+
let i = p + 1;
|
|
174
|
+
while (ATEXT.test(text[i] ?? '')) i++;
|
|
175
|
+
if (text[i] !== '@') return false;
|
|
176
|
+
i++;
|
|
177
|
+
let size = 0;
|
|
178
|
+
let state: 'atSignOrDot' | 'label' | 'value' = 'atSignOrDot';
|
|
179
|
+
for (;;) {
|
|
180
|
+
const c = text[i] ?? '';
|
|
181
|
+
if (state === 'atSignOrDot') {
|
|
182
|
+
if (!ALPHANUMERIC.test(c)) return false;
|
|
183
|
+
state = 'label';
|
|
184
|
+
} else if (state === 'label') {
|
|
185
|
+
if (c === '>') return true;
|
|
186
|
+
if (c === '.') {
|
|
187
|
+
i++;
|
|
188
|
+
size = 0;
|
|
189
|
+
state = 'atSignOrDot';
|
|
190
|
+
} else {
|
|
191
|
+
state = 'value';
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
if (!((c === '-' || ALPHANUMERIC.test(c)) && size++ < 63)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
i++;
|
|
198
|
+
state = c === '-' ? 'value' : 'label';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const construct: Construct = {
|
|
204
|
+
name: 'unclosedHtml',
|
|
205
|
+
resolveTo: joinData,
|
|
206
|
+
tokenize(effects, ok, nok) {
|
|
207
|
+
const at = this.now().offset + shift;
|
|
208
|
+
return (code) => {
|
|
209
|
+
if (code !== 60 || !unclosed(at)) return nok(code);
|
|
210
|
+
effects.enter('data');
|
|
211
|
+
effects.consume(code);
|
|
212
|
+
effects.exit('data');
|
|
213
|
+
return ok;
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
return { text: { 60: construct } };
|
|
218
|
+
}
|