@rohal12/spindle 0.1.0 → 0.3.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.
Files changed (41) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/package.json +5 -2
  3. package/src/action-registry.ts +83 -0
  4. package/src/automation/index.ts +11 -0
  5. package/src/automation/load-yaml.ts +20 -0
  6. package/src/automation/runner.ts +139 -0
  7. package/src/automation/types.ts +38 -0
  8. package/src/class-registry.ts +189 -0
  9. package/src/components/PassageLink.tsx +10 -0
  10. package/src/components/StoryInterface.tsx +2 -1
  11. package/src/components/macros/Back.tsx +10 -0
  12. package/src/components/macros/Button.tsx +13 -2
  13. package/src/components/macros/Checkbox.tsx +11 -0
  14. package/src/components/macros/Cycle.tsx +19 -1
  15. package/src/components/macros/Do.tsx +3 -2
  16. package/src/components/macros/Forward.tsx +10 -0
  17. package/src/components/macros/Listbox.tsx +15 -3
  18. package/src/components/macros/MacroLink.tsx +19 -8
  19. package/src/components/macros/Meter.tsx +130 -0
  20. package/src/components/macros/Numberbox.tsx +12 -3
  21. package/src/components/macros/QuickLoad.tsx +10 -0
  22. package/src/components/macros/QuickSave.tsx +9 -0
  23. package/src/components/macros/Radiobutton.tsx +11 -0
  24. package/src/components/macros/Restart.tsx +9 -0
  25. package/src/components/macros/Set.tsx +3 -2
  26. package/src/components/macros/Textarea.tsx +12 -3
  27. package/src/components/macros/Textbox.tsx +12 -3
  28. package/src/components/macros/Type.tsx +2 -9
  29. package/src/hooks/use-action.ts +49 -0
  30. package/src/index.tsx +11 -5
  31. package/src/markup/ast.ts +3 -6
  32. package/src/markup/markdown.ts +1 -4
  33. package/src/markup/render.tsx +13 -6
  34. package/src/markup/tokenizer.ts +66 -8
  35. package/src/saves/save-manager.ts +24 -4
  36. package/src/store.ts +17 -13
  37. package/src/story-api.ts +97 -0
  38. package/src/story-init.ts +3 -2
  39. package/src/story-variables.ts +3 -1
  40. package/src/styles.css +31 -1
  41. package/src/utils/extract-text.ts +8 -0
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface ForwardProps {
4
5
  className?: string;
@@ -12,6 +13,15 @@ export function Forward({ className, id }: ForwardProps) {
12
13
  );
13
14
  const cls = className ? `menubar-button ${className}` : 'menubar-button';
14
15
 
16
+ useAction({
17
+ type: 'forward',
18
+ key: 'forward',
19
+ authorId: id,
20
+ label: 'Forward',
21
+ disabled: !canGoForward,
22
+ perform: () => goForward(),
23
+ });
24
+
15
25
  return (
16
26
  <button
17
27
  id={id}
@@ -1,5 +1,6 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { extractOptions } from './option-utils';
3
+ import { useAction } from '../../hooks/use-action';
3
4
  import type { ASTNode } from '../../markup/ast';
4
5
 
5
6
  interface ListboxProps {
@@ -18,6 +19,19 @@ export function Listbox({ rawArgs, children, className, id }: ListboxProps) {
18
19
 
19
20
  const options = extractOptions(children);
20
21
 
22
+ useAction({
23
+ type: 'listbox',
24
+ key: `$${name}`,
25
+ authorId: id,
26
+ label: name,
27
+ variable: name,
28
+ options,
29
+ value,
30
+ perform: (v) => {
31
+ if (v !== undefined) setVariable(name, String(v));
32
+ },
33
+ });
34
+
21
35
  const cls = className ? `macro-listbox ${className}` : 'macro-listbox';
22
36
 
23
37
  return (
@@ -25,9 +39,7 @@ export function Listbox({ rawArgs, children, className, id }: ListboxProps) {
25
39
  id={id}
26
40
  class={cls}
27
41
  value={value == null ? '' : String(value)}
28
- onChange={(e) =>
29
- setVariable(name, (e.target as HTMLSelectElement).value)
30
- }
42
+ onChange={(e) => setVariable(name, (e.target as HTMLSelectElement).value)}
31
43
  >
32
44
  {options.map((opt) => (
33
45
  <option
@@ -1,6 +1,7 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { execute } from '../../expression';
3
3
  import type { ASTNode } from '../../markup/ast';
4
+ import { deepClone } from '../../class-registry';
4
5
 
5
6
  interface MacroLinkProps {
6
7
  rawArgs: string;
@@ -30,20 +31,16 @@ function parseArgs(rawArgs: string): {
30
31
  return { display: rawArgs.trim(), passage: null };
31
32
  }
32
33
 
33
- /**
34
- * Collect text from AST nodes for imperative execution (like Do.tsx).
35
- */
36
- function collectText(nodes: ASTNode[]): string {
37
- return nodes.map((n) => (n.type === 'text' ? n.value : '')).join('');
38
- }
34
+ import { collectText } from '../../utils/extract-text';
35
+ import { useAction } from '../../hooks/use-action';
39
36
 
40
37
  /**
41
38
  * Execute the children imperatively: walk AST for {set} and {do} macros.
42
39
  */
43
40
  function executeChildren(children: ASTNode[]) {
44
41
  const state = useStoryStore.getState();
45
- const vars = structuredClone(state.variables);
46
- const temps = structuredClone(state.temporary);
42
+ const vars = deepClone(state.variables);
43
+ const temps = deepClone(state.temporary);
47
44
 
48
45
  for (const node of children) {
49
46
  if (node.type !== 'macro') continue;
@@ -92,6 +89,20 @@ export function MacroLink({
92
89
  }
93
90
  };
94
91
 
92
+ useAction({
93
+ type: 'link',
94
+ key: passage || display,
95
+ authorId: id,
96
+ label: display,
97
+ target: passage ?? undefined,
98
+ perform: () => {
99
+ executeChildren(children);
100
+ if (passage) {
101
+ useStoryStore.getState().navigate(passage);
102
+ }
103
+ },
104
+ });
105
+
95
106
  const cls = className ? `macro-link ${className}` : 'macro-link';
96
107
 
97
108
  return (
@@ -0,0 +1,130 @@
1
+ import { useStoryStore } from '../../store';
2
+ import { useContext } from 'preact/hooks';
3
+ import { evaluate } from '../../expression';
4
+ import { LocalsContext } from '../../markup/render';
5
+
6
+ interface MeterProps {
7
+ rawArgs: string;
8
+ className?: string;
9
+ id?: string;
10
+ }
11
+
12
+ /**
13
+ * Parse rawArgs into currentExpr, maxExpr, and optional labelMode.
14
+ * Supports: {meter $current $max}, {meter $current $max "%"}, etc.
15
+ */
16
+ function parseArgs(rawArgs: string): {
17
+ currentExpr: string;
18
+ maxExpr: string;
19
+ labelMode: string;
20
+ } {
21
+ const trimmed = rawArgs.trim();
22
+
23
+ // Extract quoted label mode from the end if present
24
+ let labelMode = '';
25
+ let rest = trimmed;
26
+ const quoteMatch = rest.match(/\s+(?:"([^"]*)"|'([^']*)')$/);
27
+ if (quoteMatch) {
28
+ labelMode = quoteMatch[1] ?? quoteMatch[2];
29
+ rest = rest.slice(0, quoteMatch.index!).trim();
30
+ }
31
+
32
+ // Split remaining into two expressions.
33
+ // Expressions can contain dots, brackets, parens — we need to find the split point.
34
+ // Strategy: walk tokens, splitting on whitespace that isn't inside parens/brackets.
35
+ const exprs = splitExpressions(rest);
36
+ if (exprs.length < 2) {
37
+ throw new Error(
38
+ 'meter requires two arguments: {meter currentExpr maxExpr}',
39
+ );
40
+ }
41
+
42
+ return {
43
+ currentExpr: exprs[0],
44
+ maxExpr: exprs.slice(1).join(' '),
45
+ labelMode,
46
+ };
47
+ }
48
+
49
+ function splitExpressions(input: string): string[] {
50
+ const result: string[] = [];
51
+ let current = '';
52
+ let depth = 0;
53
+
54
+ for (let i = 0; i < input.length; i++) {
55
+ const ch = input[i];
56
+ if (ch === '(' || ch === '[') {
57
+ depth++;
58
+ current += ch;
59
+ } else if (ch === ')' || ch === ']') {
60
+ depth--;
61
+ current += ch;
62
+ } else if (/\s/.test(ch) && depth === 0 && current.length > 0) {
63
+ result.push(current);
64
+ current = '';
65
+ // Skip additional whitespace
66
+ while (i + 1 < input.length && /\s/.test(input[i + 1])) i++;
67
+ } else {
68
+ current += ch;
69
+ }
70
+ }
71
+ if (current.length > 0) result.push(current);
72
+ return result;
73
+ }
74
+
75
+ function formatLabel(
76
+ current: number,
77
+ max: number,
78
+ labelMode: string,
79
+ ): string | null {
80
+ if (labelMode === 'none') return null;
81
+ if (labelMode === '%') return `${Math.round((current / max) * 100)}%`;
82
+ if (labelMode) return `${current} ${labelMode} / ${max} ${labelMode}`;
83
+ return `${current} / ${max}`;
84
+ }
85
+
86
+ export function Meter({ rawArgs, className, id }: MeterProps) {
87
+ const variables = useStoryStore((s) => s.variables);
88
+ const temporary = useStoryStore((s) => s.temporary);
89
+ const locals = useContext(LocalsContext);
90
+
91
+ // Merge locals into variables for expression evaluation
92
+ const mergedVars = { ...variables };
93
+ const mergedTemps = { ...temporary };
94
+ for (const [key, val] of Object.entries(locals)) {
95
+ if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
96
+ else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
97
+ }
98
+
99
+ try {
100
+ const { currentExpr, maxExpr, labelMode } = parseArgs(rawArgs);
101
+ const current = Number(evaluate(currentExpr, mergedVars, mergedTemps));
102
+ const max = Number(evaluate(maxExpr, mergedVars, mergedTemps));
103
+ const pct = Math.max(0, Math.min(100, (current / max) * 100));
104
+ const label = formatLabel(current, max, labelMode);
105
+
106
+ const classes = ['macro-meter', className].filter(Boolean).join(' ');
107
+
108
+ return (
109
+ <div
110
+ class={classes}
111
+ id={id}
112
+ >
113
+ <div
114
+ class="macro-meter-fill"
115
+ style={`width: ${pct}%`}
116
+ />
117
+ {label != null && <span class="macro-meter-label">{label}</span>}
118
+ </div>
119
+ );
120
+ } catch (err) {
121
+ return (
122
+ <span
123
+ class="error"
124
+ title={String(err)}
125
+ >
126
+ {`{meter error: ${(err as Error).message}}`}
127
+ </span>
128
+ );
129
+ }
130
+ }
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface NumberboxProps {
4
5
  rawArgs: string;
@@ -7,9 +8,7 @@ interface NumberboxProps {
7
8
  }
8
9
 
9
10
  function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
10
- const match = rawArgs.match(
11
- /^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/,
12
- );
11
+ const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/);
13
12
  if (!match) {
14
13
  return { varName: rawArgs.trim(), placeholder: '' };
15
14
  }
@@ -25,6 +24,16 @@ export function Numberbox({ rawArgs, className, id }: NumberboxProps) {
25
24
  const value = useStoryStore((s) => s.variables[name]);
26
25
  const setVariable = useStoryStore((s) => s.setVariable);
27
26
 
27
+ useAction({
28
+ type: 'numberbox',
29
+ key: `$${name}`,
30
+ authorId: id,
31
+ label: placeholder || name,
32
+ variable: name,
33
+ value,
34
+ perform: (v) => setVariable(name, v !== undefined ? Number(v) : 0),
35
+ });
36
+
28
37
  const cls = className ? `macro-numberbox ${className}` : 'macro-numberbox';
29
38
 
30
39
  return (
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface QuickLoadProps {
4
5
  className?: string;
@@ -19,6 +20,15 @@ export function QuickLoad({ className, id }: QuickLoadProps) {
19
20
  }
20
21
  };
21
22
 
23
+ useAction({
24
+ type: 'load',
25
+ key: 'quickload',
26
+ authorId: id,
27
+ label: 'QuickLoad',
28
+ disabled,
29
+ perform: () => load(),
30
+ });
31
+
22
32
  return (
23
33
  <button
24
34
  id={id}
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface QuickSaveProps {
4
5
  className?: string;
@@ -9,6 +10,14 @@ export function QuickSave({ className, id }: QuickSaveProps) {
9
10
  const save = useStoryStore((s) => s.save);
10
11
  const cls = className ? `menubar-button ${className}` : 'menubar-button';
11
12
 
13
+ useAction({
14
+ type: 'save',
15
+ key: 'quicksave',
16
+ authorId: id,
17
+ label: 'QuickSave',
18
+ perform: () => save(),
19
+ });
20
+
12
21
  return (
13
22
  <button
14
23
  id={id}
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface RadiobuttonProps {
4
5
  rawArgs: string;
@@ -38,6 +39,16 @@ export function Radiobutton({ rawArgs, className, id }: RadiobuttonProps) {
38
39
  const currentValue = useStoryStore((s) => s.variables[name]);
39
40
  const setVariable = useStoryStore((s) => s.setVariable);
40
41
 
42
+ useAction({
43
+ type: 'radiobutton',
44
+ key: `$${name}:${radioValue}`,
45
+ authorId: id,
46
+ label: label || radioValue,
47
+ variable: name,
48
+ value: currentValue,
49
+ perform: () => setVariable(name, radioValue),
50
+ });
51
+
41
52
  const cls = className
42
53
  ? `macro-radiobutton ${className}`
43
54
  : 'macro-radiobutton';
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface RestartProps {
4
5
  className?: string;
@@ -15,6 +16,14 @@ export function Restart({ className, id }: RestartProps) {
15
16
  }
16
17
  };
17
18
 
19
+ useAction({
20
+ type: 'restart',
21
+ key: 'restart',
22
+ authorId: id,
23
+ label: 'Restart',
24
+ perform: () => restart(),
25
+ });
26
+
18
27
  return (
19
28
  <button
20
29
  id={id}
@@ -1,6 +1,7 @@
1
1
  import { useLayoutEffect } from 'preact/hooks';
2
2
  import { useStoryStore } from '../../store';
3
3
  import { execute } from '../../expression';
4
+ import { deepClone } from '../../class-registry';
4
5
 
5
6
  interface SetProps {
6
7
  rawArgs: string;
@@ -9,8 +10,8 @@ interface SetProps {
9
10
  export function Set({ rawArgs }: SetProps) {
10
11
  useLayoutEffect(() => {
11
12
  const state = useStoryStore.getState();
12
- const vars = structuredClone(state.variables);
13
- const temps = structuredClone(state.temporary);
13
+ const vars = deepClone(state.variables);
14
+ const temps = deepClone(state.temporary);
14
15
 
15
16
  try {
16
17
  execute(rawArgs, vars, temps);
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface TextareaProps {
4
5
  rawArgs: string;
@@ -7,9 +8,7 @@ interface TextareaProps {
7
8
  }
8
9
 
9
10
  function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
10
- const match = rawArgs.match(
11
- /^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/,
12
- );
11
+ const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/);
13
12
  if (!match) {
14
13
  return { varName: rawArgs.trim(), placeholder: '' };
15
14
  }
@@ -25,6 +24,16 @@ export function Textarea({ rawArgs, className, id }: TextareaProps) {
25
24
  const value = useStoryStore((s) => s.variables[name]);
26
25
  const setVariable = useStoryStore((s) => s.setVariable);
27
26
 
27
+ useAction({
28
+ type: 'textarea',
29
+ key: `$${name}`,
30
+ authorId: id,
31
+ label: placeholder || name,
32
+ variable: name,
33
+ value,
34
+ perform: (v) => setVariable(name, v !== undefined ? String(v) : ''),
35
+ });
36
+
28
37
  const cls = className ? `macro-textarea ${className}` : 'macro-textarea';
29
38
 
30
39
  return (
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useAction } from '../../hooks/use-action';
2
3
 
3
4
  interface TextboxProps {
4
5
  rawArgs: string;
@@ -7,9 +8,7 @@ interface TextboxProps {
7
8
  }
8
9
 
9
10
  function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
10
- const match = rawArgs.match(
11
- /^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/,
12
- );
11
+ const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/);
13
12
  if (!match) {
14
13
  return { varName: rawArgs.trim(), placeholder: '' };
15
14
  }
@@ -25,6 +24,16 @@ export function Textbox({ rawArgs, className, id }: TextboxProps) {
25
24
  const value = useStoryStore((s) => s.variables[name]);
26
25
  const setVariable = useStoryStore((s) => s.setVariable);
27
26
 
27
+ useAction({
28
+ type: 'textbox',
29
+ key: `$${name}`,
30
+ authorId: id,
31
+ label: placeholder || name,
32
+ variable: name,
33
+ value,
34
+ perform: (v) => setVariable(name, v !== undefined ? String(v) : ''),
35
+ });
36
+
28
37
  const cls = className ? `macro-textbox ${className}` : 'macro-textbox';
29
38
 
30
39
  return (
@@ -44,11 +44,7 @@ export function Type({ rawArgs, children, className, id }: TypeProps) {
44
44
 
45
45
  const done = visibleChars >= totalChars && totalChars > 0;
46
46
 
47
- const cls = [
48
- 'macro-type',
49
- done ? 'macro-type-done' : '',
50
- className || '',
51
- ]
47
+ const cls = ['macro-type', done ? 'macro-type-done' : '', className || '']
52
48
  .filter(Boolean)
53
49
  .join(' ');
54
50
 
@@ -58,10 +54,7 @@ export function Type({ rawArgs, children, className, id }: TypeProps) {
58
54
  class={cls}
59
55
  ref={containerRef}
60
56
  style={{
61
- clipPath:
62
- totalChars > 0 && !done
63
- ? undefined
64
- : undefined,
57
+ clipPath: totalChars > 0 && !done ? undefined : undefined,
65
58
  }}
66
59
  >
67
60
  <span
@@ -0,0 +1,49 @@
1
+ import { useLayoutEffect, useRef } from 'preact/hooks';
2
+ import {
3
+ registerAction,
4
+ generateActionId,
5
+ type ActionType,
6
+ type StoryAction,
7
+ } from '../action-registry';
8
+
9
+ export interface UseActionOptions {
10
+ type: ActionType;
11
+ key: string;
12
+ authorId?: string;
13
+ label: string;
14
+ target?: string;
15
+ variable?: string;
16
+ options?: string[];
17
+ value?: unknown;
18
+ disabled?: boolean;
19
+ perform: (value?: unknown) => void;
20
+ }
21
+
22
+ export function useAction(opts: UseActionOptions): string {
23
+ const idRef = useRef<string>('');
24
+
25
+ // Generate ID only once on first call
26
+ if (!idRef.current) {
27
+ idRef.current = generateActionId(opts.type, opts.key, opts.authorId);
28
+ }
29
+
30
+ const id = idRef.current;
31
+
32
+ useLayoutEffect(() => {
33
+ const action: StoryAction = {
34
+ id,
35
+ type: opts.type,
36
+ label: opts.label,
37
+ perform: opts.perform,
38
+ };
39
+ if (opts.target !== undefined) action.target = opts.target;
40
+ if (opts.variable !== undefined) action.variable = opts.variable;
41
+ if (opts.options !== undefined) action.options = opts.options;
42
+ if (opts.value !== undefined) action.value = opts.value;
43
+ if (opts.disabled !== undefined) action.disabled = opts.disabled;
44
+
45
+ return registerAction(action);
46
+ });
47
+
48
+ return id;
49
+ }
package/src/index.tsx CHANGED
@@ -3,6 +3,7 @@ import { App } from './components/App';
3
3
  import { parseStoryData } from './parser';
4
4
  import { useStoryStore } from './store';
5
5
  import { installStoryAPI } from './story-api';
6
+ import { resetIdCounters } from './action-registry';
6
7
  import { executeStoryInit } from './story-init';
7
8
  import {
8
9
  parseStoryVariables,
@@ -93,11 +94,7 @@ function boot() {
93
94
  const widgetTokens = tokenize(passage.content);
94
95
  const widgetAST = buildAST(widgetTokens);
95
96
  for (const node of widgetAST) {
96
- if (
97
- node.type === 'macro' &&
98
- node.name === 'widget' &&
99
- node.rawArgs
100
- ) {
97
+ if (node.type === 'macro' && node.name === 'widget' && node.rawArgs) {
101
98
  const widgetName = node.rawArgs.trim().replace(/["']/g, '');
102
99
  registerWidget(widgetName, node.children as ASTNode[]);
103
100
  }
@@ -105,6 +102,15 @@ function boot() {
105
102
  }
106
103
  }
107
104
 
105
+ // Reset action ID counters on passage change
106
+ let prevPassage = '';
107
+ useStoryStore.subscribe((state) => {
108
+ if (state.currentPassage !== prevPassage) {
109
+ prevPassage = state.currentPassage;
110
+ resetIdCounters();
111
+ }
112
+ });
113
+
108
114
  const root = document.getElementById('root');
109
115
  if (!root) {
110
116
  throw new Error('spindle: No <div id="root"> element found.');
package/src/markup/ast.ts CHANGED
@@ -89,11 +89,7 @@ export function buildAST(tokens: Token[]): ASTNode[] {
89
89
  if (stack.length === 0) return root;
90
90
  const top = stack[stack.length - 1].node;
91
91
  // For if-blocks, append to the last branch's children
92
- if (
93
- top.type === 'macro' &&
94
- top.branches &&
95
- top.branches.length > 0
96
- ) {
92
+ if (top.type === 'macro' && top.branches && top.branches.length > 0) {
97
93
  return top.branches[top.branches.length - 1].children;
98
94
  }
99
95
  return top.children;
@@ -204,7 +200,8 @@ export function buildAST(tokens: Token[]): ASTNode[] {
204
200
  // Handle branch macros (elseif/else, case/default, next)
205
201
  if (BRANCH_PARENT[token.name]) {
206
202
  const expectedParent = BRANCH_PARENT[token.name];
207
- const topNode = stack.length > 0 ? stack[stack.length - 1].node : null;
203
+ const topNode =
204
+ stack.length > 0 ? stack[stack.length - 1].node : null;
208
205
  if (
209
206
  !topNode ||
210
207
  topNode.type !== 'macro' ||
@@ -1,8 +1,5 @@
1
1
  import { micromark } from 'micromark';
2
- import {
3
- gfmTable,
4
- gfmTableHtml,
5
- } from 'micromark-extension-gfm-table';
2
+ import { gfmTable, gfmTableHtml } from 'micromark-extension-gfm-table';
6
3
  import {
7
4
  gfmStrikethrough,
8
5
  gfmStrikethroughHtml,
@@ -33,6 +33,7 @@ import { Stop } from '../components/macros/Stop';
33
33
  import { Type } from '../components/macros/Type';
34
34
  import { Widget } from '../components/macros/Widget';
35
35
  import { Computed } from '../components/macros/Computed';
36
+ import { Meter } from '../components/macros/Meter';
36
37
  import { getWidget } from '../widgets/widget-registry';
37
38
  import { getMacro } from '../registry';
38
39
  import { markdownToHtml } from './markdown';
@@ -119,6 +120,16 @@ function renderMacro(node: MacroNode, key: number) {
119
120
  />
120
121
  );
121
122
 
123
+ case 'meter':
124
+ return (
125
+ <Meter
126
+ key={key}
127
+ rawArgs={node.rawArgs}
128
+ className={node.className}
129
+ id={node.id}
130
+ />
131
+ );
132
+
122
133
  case 'if':
123
134
  return (
124
135
  <If
@@ -371,9 +382,7 @@ function renderMacro(node: MacroNode, key: number) {
371
382
  );
372
383
 
373
384
  case 'stop':
374
- return (
375
- <Stop key={key} />
376
- );
385
+ return <Stop key={key} />;
377
386
 
378
387
  case 'type':
379
388
  return (
@@ -487,9 +496,7 @@ function renderSingleNode(
487
496
  * Used for inline containers (button labels, link text) where block-level
488
497
  * markdown (lists, headers) would misinterpret content like "-" or "+".
489
498
  */
490
- export function renderInlineNodes(
491
- nodes: ASTNode[],
492
- ): preact.ComponentChildren {
499
+ export function renderInlineNodes(nodes: ASTNode[]): preact.ComponentChildren {
493
500
  if (nodes.length === 0) return null;
494
501
  return nodes.map((node, i) => renderSingleNode(node, i));
495
502
  }