@rohal12/spindle 0.59.10 → 0.59.11
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 +522 -342
- package/dist/pkg/macro-registry.json +1 -0
- package/dist/pkg/story-variables.js +480 -319
- package/package.json +1 -1
- package/src/components/macros/If.tsx +2 -7
- package/src/components/macros/SaveManager.tsx +5 -2
- package/src/components/macros/Switch.tsx +4 -2
- package/src/components/macros/WidgetInvocation.tsx +10 -13
- package/src/components/macros/branches.ts +20 -0
- package/src/hooks/use-modal-focus.ts +6 -1
- package/src/js-lexer.ts +18 -1
- package/src/markup/render.tsx +29 -10
- package/src/markup/spindle.peggy +11 -0
- package/src/styles.css +5 -0
- package/src/triggers.ts +4 -2
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { MacroError } from './MacroError';
|
|
3
3
|
import type { Branch } from '../../markup/ast';
|
|
4
|
-
import { selectBranch } from './branches';
|
|
5
|
-
import { wrapContent } from './display';
|
|
4
|
+
import { selectBranch, renderBranch } from './branches';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* The {if}/{elseif}/{else} branch whose condition holds, else the {else}
|
|
@@ -37,11 +36,7 @@ defineMacro({
|
|
|
37
36
|
);
|
|
38
37
|
}
|
|
39
38
|
if (!branch) return null;
|
|
40
|
-
return
|
|
41
|
-
ctx.resolve!(branch.className),
|
|
42
|
-
ctx.resolve!(branch.id),
|
|
43
|
-
ctx.renderNodes(branch.children),
|
|
44
|
-
);
|
|
39
|
+
return renderBranch(branch, ctx);
|
|
45
40
|
},
|
|
46
41
|
text({ branches = [] }, ctx) {
|
|
47
42
|
const branch = selectIfBranch(branches, ctx.evaluate);
|
|
@@ -345,12 +345,15 @@ export function SaveManagerContent() {
|
|
|
345
345
|
class="playthrough-group"
|
|
346
346
|
key={group.playthrough.id}
|
|
347
347
|
>
|
|
348
|
-
<
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
349
350
|
class="playthrough-header"
|
|
351
|
+
aria-expanded={!isCollapsed}
|
|
350
352
|
onClick={() => toggleCollapse(group.playthrough.id)}
|
|
351
353
|
>
|
|
352
354
|
<span
|
|
353
355
|
class={`playthrough-chevron ${isCollapsed ? '' : 'open'}`}
|
|
356
|
+
aria-hidden="true"
|
|
354
357
|
>
|
|
355
358
|
▶
|
|
356
359
|
</span>
|
|
@@ -361,7 +364,7 @@ export function SaveManagerContent() {
|
|
|
361
364
|
<span class="playthrough-date">
|
|
362
365
|
{formatDate(group.playthrough.createdAt)}
|
|
363
366
|
</span>
|
|
364
|
-
</
|
|
367
|
+
</button>
|
|
365
368
|
|
|
366
369
|
{!isCollapsed && (
|
|
367
370
|
<div class="playthrough-saves">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { MacroError } from './MacroError';
|
|
3
3
|
import type { Branch } from '../../markup/ast';
|
|
4
|
-
import { selectBranch } from './branches';
|
|
4
|
+
import { selectBranch, renderBranch } from './branches';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The first {case} branch whose value equals `value`, else the {default}
|
|
@@ -20,6 +20,7 @@ defineMacro({
|
|
|
20
20
|
name: 'switch',
|
|
21
21
|
subMacros: ['case', 'default'],
|
|
22
22
|
parameters: [{ name: 'expression', type: 'expression', required: true }],
|
|
23
|
+
interpolate: true,
|
|
23
24
|
merged: true,
|
|
24
25
|
render({ rawArgs, branches = [] }, ctx) {
|
|
25
26
|
let switchValue: unknown;
|
|
@@ -46,7 +47,8 @@ defineMacro({
|
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
if (!branch) return null;
|
|
51
|
+
return renderBranch(branch, ctx);
|
|
50
52
|
},
|
|
51
53
|
text({ rawArgs, branches = [] }, ctx) {
|
|
52
54
|
const branch = selectCase(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
@@ -7,6 +7,8 @@ import { splitArgs } from './arg-utils';
|
|
|
7
7
|
import { createNamespace } from '../../utils/namespace';
|
|
8
8
|
import { useRenderOptions } from '../../hooks/use-render-options';
|
|
9
9
|
import { LocalsScope } from './locals-scope';
|
|
10
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
11
|
+
import { wrapContent } from './display';
|
|
10
12
|
|
|
11
13
|
export { splitArgs };
|
|
12
14
|
|
|
@@ -20,17 +22,14 @@ interface WidgetInvocationProps {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function WidgetInvocation(props: WidgetInvocationProps) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
>
|
|
32
|
-
{content}
|
|
33
|
-
</span>
|
|
25
|
+
const resolve = useInterpolate();
|
|
26
|
+
// Invocation selectors (`{.badge#hero Stats}`) need an element to land on;
|
|
27
|
+
// dynamic ones (`{.{$theme} Stats}`) follow the state like a built-in
|
|
28
|
+
// macro's.
|
|
29
|
+
return wrapContent(
|
|
30
|
+
resolve(props.className),
|
|
31
|
+
resolve(props.id),
|
|
32
|
+
<WidgetContent {...props} />,
|
|
34
33
|
);
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -39,8 +38,6 @@ function WidgetContent({
|
|
|
39
38
|
params,
|
|
40
39
|
rawArgs,
|
|
41
40
|
invocationChildren,
|
|
42
|
-
className,
|
|
43
|
-
id,
|
|
44
41
|
}: WidgetInvocationProps) {
|
|
45
42
|
const renderOptions = useRenderOptions();
|
|
46
43
|
const parentValues = renderOptions.locals;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { ComponentChildren, VNode } from 'preact';
|
|
1
2
|
import type { Branch } from '../../markup/ast';
|
|
3
|
+
import type { MacroContext } from '../../define-macro';
|
|
4
|
+
import { wrapContent } from './display';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* The branch a conditional block ({if}, {switch}) shows: the first of
|
|
@@ -16,3 +19,20 @@ export function selectBranch(
|
|
|
16
19
|
}
|
|
17
20
|
return fallback;
|
|
18
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A selected branch's content, in a wrapper if the branch has CSS selectors
|
|
25
|
+
* (`{.hot#id case 1}`). The macro must be defined with `interpolate`.
|
|
26
|
+
*/
|
|
27
|
+
export function renderBranch(
|
|
28
|
+
branch: Branch,
|
|
29
|
+
ctx: Pick<MacroContext, 'resolve'> & {
|
|
30
|
+
renderNodes: (nodes: Branch['children']) => ComponentChildren;
|
|
31
|
+
},
|
|
32
|
+
): VNode<any> {
|
|
33
|
+
return wrapContent(
|
|
34
|
+
ctx.resolve!(branch.className),
|
|
35
|
+
ctx.resolve!(branch.id),
|
|
36
|
+
ctx.renderNodes(branch.children),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -29,7 +29,12 @@ function isRendered(el: HTMLElement): boolean {
|
|
|
29
29
|
|
|
30
30
|
function focusables(root: HTMLElement): HTMLElement[] {
|
|
31
31
|
return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
|
|
32
|
-
(el) =>
|
|
32
|
+
(el) =>
|
|
33
|
+
!el.hidden &&
|
|
34
|
+
!el.closest('[hidden], [inert]') &&
|
|
35
|
+
// Also covers controls disabled by a <fieldset disabled> ancestor
|
|
36
|
+
!el.matches(':disabled') &&
|
|
37
|
+
isRendered(el),
|
|
33
38
|
);
|
|
34
39
|
}
|
|
35
40
|
|
package/src/js-lexer.ts
CHANGED
|
@@ -128,7 +128,7 @@ const OPERAND_ENDS: ReadonlySet<TokenType> = new Set([
|
|
|
128
128
|
* whether a `{` opens a block or an object literal, which decides whether a
|
|
129
129
|
* `/` after its `}` opens a regex and a `%` a transient: after the `:` of a
|
|
130
130
|
* conditional it opens an object literal (`a ? b : {} / 2`), not a block as
|
|
131
|
-
* after a label; after a block's `}` it opens another block (`{}{} %n = 1`),
|
|
131
|
+
* after a label, and a `function` or `class` there is an expression; after a block's `}` it opens another block (`{}{} %n = 1`),
|
|
132
132
|
* and so it does on a new line after a statement (`p⏎{} %n = 1`).
|
|
133
133
|
*/
|
|
134
134
|
const SigilParser = class extends (Parser as unknown as Base) {
|
|
@@ -146,6 +146,8 @@ const SigilParser = class extends (Parser as unknown as Base) {
|
|
|
146
146
|
closedBlock = false;
|
|
147
147
|
/** The value of the token before the one being finished. */
|
|
148
148
|
prevValue: unknown;
|
|
149
|
+
/** The type of the token before the last one. */
|
|
150
|
+
typeBeforeLast: TokenType = tt.eof;
|
|
149
151
|
/** Look ahead after a `%name` starting a line (off in look-aheads). */
|
|
150
152
|
lookahead = true;
|
|
151
153
|
/**
|
|
@@ -222,6 +224,21 @@ const SigilParser = class extends (Parser as unknown as Base) {
|
|
|
222
224
|
(prevType === tt.dot || prevType === tt.questionDot)) ||
|
|
223
225
|
((type === tt._function || type === tt._class) &&
|
|
224
226
|
notABody(self.input, self.pos));
|
|
227
|
+
// acorn takes a `function` or `class` for a statement's (whose `}` a
|
|
228
|
+
// regex may follow) after the `:` of a conditional, and `async function`
|
|
229
|
+
// always. Both are expressions where an operand is (`a ? b : class {} / 2`,
|
|
230
|
+
// `x = async function () {} / 2`): read them as after what comes before.
|
|
231
|
+
if (!property && (type === tt._function || type === tt._class)) {
|
|
232
|
+
const afterAsync =
|
|
233
|
+
type === tt._function &&
|
|
234
|
+
prevType === tt.name &&
|
|
235
|
+
this.prevValue === 'async' &&
|
|
236
|
+
!this.breakHere;
|
|
237
|
+
const before = afterAsync ? this.typeBeforeLast : prevType;
|
|
238
|
+
self.type =
|
|
239
|
+
before === tt.colon && this.colonEndsTernary ? tt.parenL : before;
|
|
240
|
+
}
|
|
241
|
+
this.typeBeforeLast = prevType;
|
|
225
242
|
// @ts-expect-error acorn internals
|
|
226
243
|
super.finishToken(property ? tt.name : type, value);
|
|
227
244
|
// A variable named `of` (`const of of list`): acorn takes it for the
|
package/src/markup/render.tsx
CHANGED
|
@@ -524,25 +524,43 @@ function useDirectAttributes(
|
|
|
524
524
|
return direct.length > 0 ? elementRef : undefined;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
const decodedText = new Map<string, string>();
|
|
527
528
|
const decodedAttributeText = new Map<string, string>();
|
|
528
529
|
|
|
529
530
|
/**
|
|
530
|
-
* Decode character references (`&`, `{`) in
|
|
531
|
-
*
|
|
531
|
+
* Decode character references (`&`, `{`) in `text` the way the HTML
|
|
532
|
+
* parser decodes them, by letting it parse them: in an attribute value, or
|
|
533
|
+
* else in element text.
|
|
532
534
|
*/
|
|
533
|
-
function
|
|
535
|
+
function decodeReferences(
|
|
536
|
+
text: string,
|
|
537
|
+
cache: Map<string, string>,
|
|
538
|
+
parse: (text: string) => string | null,
|
|
539
|
+
): string {
|
|
534
540
|
if (!text.includes('&')) return text;
|
|
535
|
-
let decoded =
|
|
541
|
+
let decoded = cache.get(text);
|
|
536
542
|
if (decoded === undefined) {
|
|
537
|
-
|
|
538
|
-
decoded
|
|
539
|
-
(parseHtmlInert(html).firstChild as Element).getAttribute('title') ??
|
|
540
|
-
text;
|
|
541
|
-
decodedAttributeText.set(text, decoded);
|
|
543
|
+
decoded = parse(text) ?? text;
|
|
544
|
+
cache.set(text, decoded);
|
|
542
545
|
}
|
|
543
546
|
return decoded;
|
|
544
547
|
}
|
|
545
548
|
|
|
549
|
+
function decodeAttributeText(text: string): string {
|
|
550
|
+
return decodeReferences(text, decodedAttributeText, (t) => {
|
|
551
|
+
const html = `<i title="${t.replace(/"/g, '"')}"></i>`;
|
|
552
|
+
return (parseHtmlInert(html).firstChild as Element).getAttribute('title');
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Literal text outside markdown (preformatted and SVG elements). */
|
|
557
|
+
function decodeText(text: string): string {
|
|
558
|
+
return decodeReferences(text, decodedText, (t) => {
|
|
559
|
+
const html = `<i>${t.replace(/</g, '<')}</i>`;
|
|
560
|
+
return parseHtmlInert(html).firstChild!.textContent;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
546
564
|
/** A `{` and a sigil starting a reference, in a code attribute. */
|
|
547
565
|
const SIGIL_REFERENCE = /\{[$_@%]\w/;
|
|
548
566
|
|
|
@@ -725,7 +743,8 @@ function renderMacro(node: MacroNode, key: string) {
|
|
|
725
743
|
* Render a non-text AST node to a Preact element.
|
|
726
744
|
*/
|
|
727
745
|
function renderSingleNode(node: ASTNode): preact.ComponentChildren {
|
|
728
|
-
|
|
746
|
+
// Authored text outside markdown still has its character references decoded
|
|
747
|
+
if (node.type === 'text') return decodeText(node.value);
|
|
729
748
|
const key = nodeKey(node);
|
|
730
749
|
switch (node.type) {
|
|
731
750
|
case 'variable':
|
package/src/markup/spindle.peggy
CHANGED
|
@@ -233,6 +233,7 @@ Content
|
|
|
233
233
|
Item
|
|
234
234
|
= TextRun
|
|
235
235
|
/ Escape
|
|
236
|
+
/ Comment
|
|
236
237
|
/ LinkLexeme
|
|
237
238
|
/ HtmlElement
|
|
238
239
|
/ VoidCloser
|
|
@@ -291,6 +292,7 @@ Tokens
|
|
|
291
292
|
TokenItem
|
|
292
293
|
= TextRun
|
|
293
294
|
/ Escape
|
|
295
|
+
/ Comment
|
|
294
296
|
/ LinkLexeme
|
|
295
297
|
/ &{ return !textMode; } tag:HtmlOpen { return tag; }
|
|
296
298
|
/ &{ return !textMode; } tag:HtmlClose { return VOID_TAGS.has(tag.lower) ? null : tag; }
|
|
@@ -317,6 +319,15 @@ Here
|
|
|
317
319
|
TextRun
|
|
318
320
|
= value:$[^\\{[<]+ { return lexeme('text', location(), { value }); }
|
|
319
321
|
|
|
322
|
+
/**
|
|
323
|
+
* A closed HTML comment, opaque: its tags and macros are not markup, and
|
|
324
|
+
* rendering drops it. (An unclosed `<!--` is ordinary text.)
|
|
325
|
+
*/
|
|
326
|
+
Comment
|
|
327
|
+
= value:$("<!--" (">" / "->" / (!"-->" .)* "-->")) {
|
|
328
|
+
return lexeme('text', location(), { value });
|
|
329
|
+
}
|
|
330
|
+
|
|
320
331
|
/**
|
|
321
332
|
* A backslash run. Before a brace, an odd run escapes it; in text mode the
|
|
322
333
|
* run is also paired up (`\\` is one backslash), in passage text markdown
|
package/src/styles.css
CHANGED
package/src/triggers.ts
CHANGED
|
@@ -251,7 +251,7 @@ export function checkTriggersOnNavigation(): void {
|
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
253
|
* Re-evaluate watchers whenever a namespace their conditions can read
|
|
254
|
-
* ($variables, _temporary, %transient)
|
|
254
|
+
* ($variables, _temporary, %transient) or the render counts change. Navigation is left to the
|
|
255
255
|
* store: navigate() checks watchers once the new moment is complete
|
|
256
256
|
* (checkTriggersOnNavigation), while history traversal and loads
|
|
257
257
|
* reinitialize watcher state instead of firing.
|
|
@@ -269,7 +269,9 @@ export function connectTriggersToStore(): () => void {
|
|
|
269
269
|
if (
|
|
270
270
|
state.variables === before.variables &&
|
|
271
271
|
state.temporary === before.temporary &&
|
|
272
|
-
state.transient === before.transient
|
|
272
|
+
state.transient === before.transient &&
|
|
273
|
+
// hasRendered() / rendered() read the counts an {include} bumps
|
|
274
|
+
state.renderCounts === before.renderCounts
|
|
273
275
|
) {
|
|
274
276
|
return;
|
|
275
277
|
}
|