@rohal12/spindle 0.45.1 → 0.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -88,7 +88,7 @@ export interface MacroDefinition {
88
88
  storeVar?: boolean;
89
89
  description?: string;
90
90
  parameters?: ParameterDef[];
91
- render: (props: MacroProps, ctx: MacroContext) => VNode | null;
91
+ render: (props: MacroProps, ctx: MacroContext) => ComponentChildren;
92
92
  }
93
93
 
94
94
  const sharedHooks = {
@@ -23,3 +23,71 @@ import type { parseStoryVariables as PublishedParse } from '../types/tooling';
23
23
  const _parseSourceToPublished: typeof PublishedParse = {} as typeof SourceParse;
24
24
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
25
  const _parsePublishedToSource: typeof SourceParse = {} as typeof PublishedParse;
26
+
27
+ // Custom macro API: the published MacroContext/MacroDefinition must match the
28
+ // ones defineMacro() actually passes and accepts.
29
+ import type {
30
+ MacroContext as SourceMacroContext,
31
+ MacroDefinition as SourceMacroDefinition,
32
+ } from './define-macro';
33
+ import type { MacroProps as SourceMacroProps } from './registry';
34
+ import type { ASTNode as SourceASTNode } from './markup/ast';
35
+ import type {
36
+ MacroContext as PublishedMacroContext,
37
+ MacroDefinition as PublishedMacroDefinition,
38
+ MacroProps as PublishedMacroProps,
39
+ ASTNode as PublishedASTNode,
40
+ } from '../types/index';
41
+
42
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
43
+ const _ctxSourceToPublished: PublishedMacroContext = {} as SourceMacroContext;
44
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
45
+ const _ctxPublishedToSource: SourceMacroContext = {} as PublishedMacroContext;
46
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
47
+ const _defSourceToPublished: PublishedMacroDefinition =
48
+ {} as SourceMacroDefinition;
49
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
+ const _defPublishedToSource: SourceMacroDefinition =
51
+ {} as PublishedMacroDefinition;
52
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
53
+ const _propsSourceToPublished: PublishedMacroProps = {} as SourceMacroProps;
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
+ const _propsPublishedToSource: SourceMacroProps = {} as PublishedMacroProps;
56
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
57
+ const _astSourceToPublished: PublishedASTNode = {} as SourceASTNode;
58
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
59
+ const _astPublishedToSource: SourceASTNode = {} as PublishedASTNode;
60
+
61
+ // A typical custom macro written against the published types must type-check,
62
+ // and misuse of the hooks must not (no `any` leaking through).
63
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
64
+ const _exampleMacro: PublishedMacroDefinition = {
65
+ name: 'counter',
66
+ block: true,
67
+ render(props, ctx) {
68
+ const [count, setCount] = ctx.hooks.useState(0);
69
+ const countIsNumber: number = count;
70
+ setCount((n) => n + 1);
71
+ // @ts-expect-error -- state setter is typed by the initial value
72
+ setCount('one');
73
+ const ref = ctx.hooks.useRef<HTMLSpanElement>(null);
74
+ ctx.hooks.useEffect(() => {
75
+ ref.current?.focus();
76
+ return () => undefined;
77
+ }, [count]);
78
+ // @ts-expect-error -- deps must be an array
79
+ ctx.hooks.useEffect(() => undefined, 'count');
80
+ const doubled: number = ctx.hooks.useMemo(() => count * 2, [count]);
81
+ const body = ctx.renderNodes(props.children ?? [], { nobr: true });
82
+ const label: string = ctx.collectText(props.children ?? []);
83
+ return ctx.wrap(
84
+ ctx.h(
85
+ 'span',
86
+ { ref, class: ctx.cls, title: label },
87
+ body,
88
+ countIsNumber,
89
+ doubled,
90
+ ),
91
+ );
92
+ },
93
+ };
package/types/index.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ import type { ComponentChildren, VNode, h } from 'preact';
2
+ import type {
3
+ useState,
4
+ useRef,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useCallback,
8
+ useMemo,
9
+ useContext,
10
+ } from 'preact/hooks';
11
+
1
12
  // Format metadata (used by twee-ts)
2
13
  export declare const name: string;
3
14
  export declare const version: string;
@@ -242,6 +253,68 @@ export interface MacroMetadata {
242
253
  parameters?: ParameterDef[];
243
254
  }
244
255
 
256
+ /** Plain text in a parsed passage. */
257
+ export interface TextNode {
258
+ type: 'text';
259
+ value: string;
260
+ }
261
+
262
+ /** A variable display such as `{$hp}`, `{_tmp}`, `{@local}` or `{%transient}`. */
263
+ export interface VariableNode {
264
+ type: 'variable';
265
+ name: string;
266
+ scope: 'variable' | 'temporary' | 'local' | 'transient';
267
+ className?: string;
268
+ id?: string;
269
+ }
270
+
271
+ /** An expression display such as `{= $hp * 2}`. */
272
+ export interface ExpressionNode {
273
+ type: 'expression';
274
+ expression: string;
275
+ className?: string;
276
+ id?: string;
277
+ }
278
+
279
+ /** One branch of a block macro (`{elseif}`, `{else}`, `{case}`, ...). */
280
+ export interface Branch {
281
+ rawArgs: string;
282
+ className?: string;
283
+ id?: string;
284
+ children: ASTNode[];
285
+ }
286
+
287
+ /** A macro invocation, with its body and branches for block macros. */
288
+ export interface MacroNode {
289
+ type: 'macro';
290
+ name: string;
291
+ rawArgs: string;
292
+ children: ASTNode[];
293
+ branches?: Branch[];
294
+ className?: string;
295
+ id?: string;
296
+ }
297
+
298
+ /** An HTML element written in passage markup. */
299
+ export interface HtmlNode {
300
+ type: 'html';
301
+ tag: string;
302
+ attributes: Record<string, string>;
303
+ children: ASTNode[];
304
+ }
305
+
306
+ /**
307
+ * A node of a parsed passage. Macros receive their body as `props.children`
308
+ * and render it with `ctx.renderNodes()`.
309
+ * @see {@link ../../src/markup/ast.ts} for the implementation.
310
+ */
311
+ export type ASTNode =
312
+ | TextNode
313
+ | VariableNode
314
+ | ExpressionNode
315
+ | MacroNode
316
+ | HtmlNode;
317
+
245
318
  /**
246
319
  * Props passed to a macro's render function.
247
320
  * @see {@link ../../src/registry.ts} for the implementation.
@@ -250,13 +323,8 @@ export interface MacroProps {
250
323
  rawArgs: string;
251
324
  className?: string;
252
325
  id?: string;
253
- children?: any[];
254
- branches?: Array<{
255
- rawArgs: string;
256
- className?: string;
257
- id?: string;
258
- children: any[];
259
- }>;
326
+ children?: ASTNode[];
327
+ branches?: Branch[];
260
328
  }
261
329
 
262
330
  /**
@@ -278,8 +346,9 @@ export interface UseActionOptions {
278
346
 
279
347
  /**
280
348
  * Context object passed to a macro's render function alongside props.
281
- * Internal Preact/AST types are represented as `any` since consumers
282
- * may not have Preact type definitions installed.
349
+ *
350
+ * Rendering helpers and hooks are Preact's own (`preact` is a dependency of
351
+ * this package), so `ctx.hooks.useState<T>()`, `ctx.h()` etc. are fully typed.
283
352
  * @see {@link ../../src/define-macro.ts} for the implementation.
284
353
  */
285
354
  export interface MacroContext {
@@ -301,26 +370,38 @@ export interface MacroContext {
301
370
  setValue?: (value: unknown) => void;
302
371
  getValue?: () => unknown;
303
372
  evaluate?: (expr: string) => unknown;
304
- collectText: (nodes: any[]) => string;
373
+ /** Concatenate the text nodes of an AST (e.g. a macro body holding a passage name). */
374
+ collectText: (nodes: ASTNode[]) => string;
375
+ /** Source location of the current passage, for error messages. */
305
376
  sourceLocation: () => string;
306
377
  parseVarArgs: (rawArgs: string) => { varName: string; placeholder: string };
307
- extractOptions: (children: any[]) => string[];
308
- wrap: (content: any) => any;
378
+ /** Collect the labels of `{option}` sub-macros in a macro body. */
379
+ extractOptions: (children: ASTNode[]) => string[];
380
+ /** Wrap content in a `<span>` carrying the macro's class/id, or a fragment if it has neither. */
381
+ wrap: (content: ComponentChildren) => VNode<any>;
309
382
  useAction: (opts: UseActionOptions) => string;
310
- h: (type: any, props: any, ...children: any[]) => any;
383
+ /** Preact's `h` (createElement). */
384
+ h: typeof h;
385
+ /** Render AST nodes as block content (markdown, `<p>` wrapping unless nobr). */
311
386
  renderNodes: (
312
- nodes: any[],
313
- options?: { nobr?: boolean; locals?: Record<string, unknown> },
314
- ) => any;
315
- renderInlineNodes: (nodes: any[]) => any;
387
+ nodes: ASTNode[],
388
+ options?: {
389
+ nobr?: boolean;
390
+ locals?: Record<string, unknown>;
391
+ inline?: boolean;
392
+ },
393
+ ) => ComponentChildren;
394
+ /** Render AST nodes as inline content (no markdown block processing). */
395
+ renderInlineNodes: (nodes: ASTNode[]) => ComponentChildren;
396
+ /** Preact hooks, shared with Spindle's own Preact instance. */
316
397
  hooks: {
317
- useState: any;
318
- useRef: any;
319
- useEffect: any;
320
- useLayoutEffect: any;
321
- useCallback: any;
322
- useMemo: any;
323
- useContext: any;
398
+ useState: typeof useState;
399
+ useRef: typeof useRef;
400
+ useEffect: typeof useEffect;
401
+ useLayoutEffect: typeof useLayoutEffect;
402
+ useCallback: typeof useCallback;
403
+ useMemo: typeof useMemo;
404
+ useContext: typeof useContext;
324
405
  };
325
406
  }
326
407
 
@@ -330,14 +411,21 @@ export interface MacroContext {
330
411
  */
331
412
  export interface MacroDefinition {
332
413
  name: string;
414
+ /** Sub-macro names (e.g. `['option']`); a non-empty list makes the macro a block macro. */
333
415
  subMacros?: string[];
416
+ /** Accept a `{name}...{/name}` body. Inferred from `subMacros` when omitted. */
334
417
  block?: boolean;
418
+ /** Resolve `{$var}` interpolation in the macro's class/id (`ctx.resolve`). */
335
419
  interpolate?: boolean;
420
+ /** Provide `ctx.merged` and `ctx.evaluate` (variables, temporaries, locals, transients). */
336
421
  merged?: boolean;
422
+ /** Bind the first argument as a story variable (`ctx.varName`, `ctx.value`, `ctx.setValue`). */
337
423
  storeVar?: boolean;
424
+ /** Tooling hint: one-line description shown by editors. */
338
425
  description?: string;
426
+ /** Tooling hint: positional parameters. */
339
427
  parameters?: ParameterDef[];
340
- render: (props: MacroProps, ctx: MacroContext) => any;
428
+ render: (props: MacroProps, ctx: MacroContext) => ComponentChildren;
341
429
  }
342
430
 
343
431
  /**