@rohal12/spindle 0.10.0 → 0.11.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.
- package/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/PassageLink.tsx +4 -0
- package/src/components/macros/Button.tsx +4 -0
- package/src/components/macros/Computed.tsx +55 -24
- package/src/components/macros/For.tsx +4 -0
- package/src/components/macros/If.tsx +7 -3
- package/src/components/macros/Include.tsx +4 -0
- package/src/components/macros/MacroLink.tsx +4 -0
- package/src/components/macros/Meter.tsx +4 -0
- package/src/components/macros/PassageDisplay.tsx +4 -0
- package/src/components/macros/Print.tsx +4 -0
- package/src/components/macros/Repeat.tsx +19 -3
- package/src/components/macros/Set.tsx +6 -4
- package/src/components/macros/Timed.tsx +4 -0
- package/src/components/macros/Type.tsx +4 -0
- package/src/components/macros/Unset.tsx +6 -3
- package/src/components/macros/VarDisplay.tsx +4 -0
- package/src/components/macros/option-utils.ts +4 -1
- package/src/expression.ts +106 -2
- package/src/hooks/use-interpolate.ts +17 -0
- package/src/index.tsx +4 -1
- package/src/interpolation.ts +44 -0
- package/src/markup/render.tsx +55 -9
- package/src/markup/tokenizer.ts +35 -4
package/src/markup/render.tsx
CHANGED
|
@@ -40,7 +40,9 @@ import { getWidget } from '../widgets/widget-registry';
|
|
|
40
40
|
import { getMacro } from '../registry';
|
|
41
41
|
import { markdownToHtml } from './markdown';
|
|
42
42
|
import { h } from 'preact';
|
|
43
|
-
import type { ASTNode, Branch, MacroNode } from './ast';
|
|
43
|
+
import type { ASTNode, Branch, HtmlNode, MacroNode, VariableNode } from './ast';
|
|
44
|
+
import { useStoryStore } from '../store';
|
|
45
|
+
import { useInterpolate } from '../hooks/use-interpolate';
|
|
44
46
|
|
|
45
47
|
export interface LocalsScope {
|
|
46
48
|
values: Record<string, unknown>;
|
|
@@ -106,6 +108,19 @@ function convertDomNode(
|
|
|
106
108
|
return null;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
112
|
+
const resolve = useInterpolate();
|
|
113
|
+
const attrs: Record<string, unknown> = {};
|
|
114
|
+
for (const [k, v] of Object.entries(node.attributes)) {
|
|
115
|
+
attrs[k] = resolve(v) ?? v;
|
|
116
|
+
}
|
|
117
|
+
return h(
|
|
118
|
+
node.tag,
|
|
119
|
+
attrs,
|
|
120
|
+
node.children.length > 0 ? renderNodes(node.children) : undefined,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
function renderMacro(node: MacroNode, key: number) {
|
|
110
125
|
switch (node.name) {
|
|
111
126
|
case 'set':
|
|
@@ -381,17 +396,19 @@ function renderMacro(node: MacroNode, key: number) {
|
|
|
381
396
|
/>
|
|
382
397
|
);
|
|
383
398
|
|
|
384
|
-
case 'timed':
|
|
399
|
+
case 'timed': {
|
|
400
|
+
const firstBranch = node.branches?.[0];
|
|
385
401
|
return (
|
|
386
402
|
<Timed
|
|
387
403
|
key={key}
|
|
388
404
|
rawArgs={node.rawArgs}
|
|
389
405
|
children={node.children}
|
|
390
406
|
branches={node.branches ?? EMPTY_BRANCHES}
|
|
391
|
-
className={node.className}
|
|
392
|
-
id={node.id}
|
|
407
|
+
className={node.className ?? firstBranch?.className}
|
|
408
|
+
id={node.id ?? firstBranch?.id}
|
|
393
409
|
/>
|
|
394
410
|
);
|
|
411
|
+
}
|
|
395
412
|
|
|
396
413
|
case 'repeat':
|
|
397
414
|
return (
|
|
@@ -513,10 +530,11 @@ function renderSingleNode(
|
|
|
513
530
|
return renderMacro(node, key);
|
|
514
531
|
|
|
515
532
|
case 'html':
|
|
516
|
-
return
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
533
|
+
return (
|
|
534
|
+
<HtmlNodeRenderer
|
|
535
|
+
key={key}
|
|
536
|
+
node={node}
|
|
537
|
+
/>
|
|
520
538
|
);
|
|
521
539
|
|
|
522
540
|
default: {
|
|
@@ -536,6 +554,31 @@ export function renderInlineNodes(nodes: ASTNode[]): preact.ComponentChildren {
|
|
|
536
554
|
return nodes.map((node, i) => renderSingleNode(node, i));
|
|
537
555
|
}
|
|
538
556
|
|
|
557
|
+
function hasUnclosedBacktick(s: string): boolean {
|
|
558
|
+
let count = 0;
|
|
559
|
+
for (let i = 0; i < s.length; i++) {
|
|
560
|
+
if (s[i] === '`') count++;
|
|
561
|
+
}
|
|
562
|
+
return count % 2 === 1;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function getVariableTextValue(node: VariableNode): string {
|
|
566
|
+
const state = useStoryStore.getState();
|
|
567
|
+
const parts = node.name.split('.');
|
|
568
|
+
const root = parts[0]!;
|
|
569
|
+
|
|
570
|
+
let value: unknown;
|
|
571
|
+
if (node.scope === 'variable') value = state.variables[root];
|
|
572
|
+
else if (node.scope === 'temporary') value = state.temporary[root];
|
|
573
|
+
|
|
574
|
+
for (let i = 1; i < parts.length; i++) {
|
|
575
|
+
if (value == null || typeof value !== 'object') return '';
|
|
576
|
+
value = (value as Record<string, unknown>)[parts[i]!];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return value == null ? '' : String(value);
|
|
580
|
+
}
|
|
581
|
+
|
|
539
582
|
/**
|
|
540
583
|
* Render AST nodes with full CommonMark markdown support.
|
|
541
584
|
*
|
|
@@ -563,7 +606,10 @@ export function renderNodes(nodes: ASTNode[]): preact.ComponentChildren {
|
|
|
563
606
|
for (let i = 0; i < nodes.length; i++) {
|
|
564
607
|
const node = nodes[i]!;
|
|
565
608
|
if (node.type === 'text') {
|
|
566
|
-
combined += node.value;
|
|
609
|
+
combined += node.value.replace(/^[ \t]{4,}/gm, ' ');
|
|
610
|
+
} else if (node.type === 'variable' && hasUnclosedBacktick(combined)) {
|
|
611
|
+
// Inline variable value to avoid placeholder inside code span
|
|
612
|
+
combined += getVariableTextValue(node);
|
|
567
613
|
} else {
|
|
568
614
|
const phIdx = components.length;
|
|
569
615
|
components.push(renderSingleNode(node, i));
|
package/src/markup/tokenizer.ts
CHANGED
|
@@ -198,10 +198,32 @@ function parseSelectors(
|
|
|
198
198
|
while (i < input.length && (input[i] === '.' || input[i] === '#')) {
|
|
199
199
|
const prefix = input[i]!;
|
|
200
200
|
i++; // skip the . or #
|
|
201
|
-
|
|
202
|
-
while (i < input.length
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
let name = '';
|
|
202
|
+
while (i < input.length) {
|
|
203
|
+
if (/[a-zA-Z0-9_-]/.test(input[i]!)) {
|
|
204
|
+
name += input[i];
|
|
205
|
+
i++;
|
|
206
|
+
} else if (
|
|
207
|
+
input[i] === '{' &&
|
|
208
|
+
(input[i + 1] === '$' || input[i + 1] === '_' || input[i + 1] === '@')
|
|
209
|
+
) {
|
|
210
|
+
// Consume interpolation: {$var}, {_var}, {@var} (with optional dot paths)
|
|
211
|
+
const braceStart = i;
|
|
212
|
+
i += 2; // skip { and prefix
|
|
213
|
+
while (i < input.length && /[\w.]/.test(input[i]!)) i++;
|
|
214
|
+
if (i < input.length && input[i] === '}') {
|
|
215
|
+
i++; // skip }
|
|
216
|
+
name += input.slice(braceStart, i);
|
|
217
|
+
} else {
|
|
218
|
+
// Not a valid interpolation — stop
|
|
219
|
+
i = braceStart;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (name) {
|
|
205
227
|
if (prefix === '.') {
|
|
206
228
|
classes.push(name);
|
|
207
229
|
} else {
|
|
@@ -286,6 +308,15 @@ export function tokenize(input: string): Token[] {
|
|
|
286
308
|
}
|
|
287
309
|
|
|
288
310
|
while (i < input.length) {
|
|
311
|
+
// Handle escaped braces: \{ and \}
|
|
312
|
+
if (input[i] === '\\' && (input[i + 1] === '{' || input[i + 1] === '}')) {
|
|
313
|
+
flushText(i);
|
|
314
|
+
tokens.push({ type: 'text', value: input[i + 1]!, start: i, end: i + 2 });
|
|
315
|
+
i += 2;
|
|
316
|
+
textStart = i;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
289
320
|
// Check for [[ link
|
|
290
321
|
if (input[i] === '[' && input[i + 1] === '[') {
|
|
291
322
|
flushText(i);
|