@rohal12/spindle 0.28.4 → 0.29.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.28.4",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -9,6 +9,7 @@ import {
9
9
  LocalsValuesContext,
10
10
  LocalsUpdateContext,
11
11
  NobrContext,
12
+ WidgetChildrenContext,
12
13
  renderNodes,
13
14
  } from '../../markup/render';
14
15
  import { useMergedLocals } from '../../hooks/use-merged-locals';
@@ -19,6 +20,7 @@ interface WidgetInvocationProps {
19
20
  body: ASTNode[];
20
21
  params: string[];
21
22
  rawArgs?: string;
23
+ invocationChildren?: ASTNode[];
22
24
  }
23
25
 
24
26
  /**
@@ -110,13 +112,20 @@ export function WidgetInvocation({
110
112
  body,
111
113
  params,
112
114
  rawArgs,
115
+ invocationChildren,
113
116
  }: WidgetInvocationProps) {
114
117
  const parentValues = useContext(LocalsValuesContext);
115
118
  const nobr = useContext(NobrContext);
116
119
  const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
117
120
 
121
+ const childrenValue = invocationChildren?.length ? invocationChildren : null;
122
+
118
123
  if (params.length === 0 || !rawArgs) {
119
- return <>{renderNodes(body, { nobr, locals: parentValues })}</>;
124
+ return (
125
+ <WidgetChildrenContext.Provider value={childrenValue}>
126
+ {renderNodes(body, { nobr, locals: parentValues })}
127
+ </WidgetChildrenContext.Provider>
128
+ );
120
129
  }
121
130
 
122
131
  const argExprs = splitArgs(rawArgs);
@@ -137,10 +146,12 @@ export function WidgetInvocation({
137
146
  }
138
147
 
139
148
  return (
140
- <WidgetBody
141
- body={body}
142
- parentValues={parentValues}
143
- ownKeys={ownKeys}
144
- />
149
+ <WidgetChildrenContext.Provider value={childrenValue}>
150
+ <WidgetBody
151
+ body={body}
152
+ parentValues={parentValues}
153
+ ownKeys={ownKeys}
154
+ />
155
+ </WidgetChildrenContext.Provider>
145
156
  );
146
157
  }
package/src/index.tsx CHANGED
@@ -13,8 +13,9 @@ import {
13
13
  extractDefaults,
14
14
  } from './story-variables';
15
15
  import { tokenize } from './markup/tokenizer';
16
- import { buildAST } from './markup/ast';
16
+ import { buildAST, registerBlockMacro } from './markup/ast';
17
17
  import { registerWidget } from './widgets/widget-registry';
18
+ import { astContainsChildren } from './widgets/ast-scanner';
18
19
  import type { ASTNode } from './markup/ast';
19
20
  import './macros/register-builtins';
20
21
  import builtinCSS from './styles.css?inline';
@@ -122,7 +123,12 @@ function boot() {
122
123
  (t) =>
123
124
  t.startsWith('$') || t.startsWith('_') || t.startsWith('@'),
124
125
  );
125
- registerWidget(widgetName, node.children as ASTNode[], params);
126
+ const children = node.children as ASTNode[];
127
+ const isBlock = astContainsChildren(children);
128
+ registerWidget(widgetName, children, params, isBlock);
129
+ if (isBlock) {
130
+ registerBlockMacro(widgetName);
131
+ }
126
132
  }
127
133
  }
128
134
  }
@@ -23,6 +23,7 @@ const defaultUpdater: LocalsUpdater = {
23
23
  export const LocalsValuesContext = createContext<Record<string, unknown>>({});
24
24
  export const LocalsUpdateContext = createContext<LocalsUpdater>(defaultUpdater);
25
25
  export const NobrContext = createContext(false);
26
+ export const WidgetChildrenContext = createContext<ASTNode[] | null>(null);
26
27
 
27
28
  /**
28
29
  * Convert an HTML string (from micromark) to Preact VNodes,
@@ -97,6 +98,14 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
97
98
  );
98
99
  }
99
100
 
101
+ function ChildrenSlot() {
102
+ const childrenAST = useContext(WidgetChildrenContext);
103
+ const nobr = useContext(NobrContext);
104
+ const locals = useContext(LocalsValuesContext);
105
+ if (!childrenAST || childrenAST.length === 0) return null;
106
+ return <>{renderNodes(childrenAST, { nobr, locals })}</>;
107
+ }
108
+
100
109
  function renderMacro(node: MacroNode, key: number) {
101
110
  if (isSubMacro(node.name)) return null;
102
111
 
@@ -108,6 +117,7 @@ function renderMacro(node: MacroNode, key: number) {
108
117
  body={widget.body}
109
118
  params={widget.params}
110
119
  rawArgs={node.rawArgs}
120
+ invocationChildren={node.children}
111
121
  />
112
122
  );
113
123
  }
@@ -148,6 +158,9 @@ function renderSingleNode(
148
158
  return node.value;
149
159
 
150
160
  case 'variable':
161
+ if (node.scope === 'local' && node.name === 'children') {
162
+ return <ChildrenSlot key={key} />;
163
+ }
151
164
  return (
152
165
  <VarDisplay
153
166
  key={key}
@@ -0,0 +1,29 @@
1
+ import type { ASTNode } from '../markup/ast';
2
+
3
+ /**
4
+ * Recursively scan an AST node array for a VariableNode
5
+ * with scope 'local' and name 'children' (@children).
6
+ */
7
+ export function astContainsChildren(nodes: ASTNode[]): boolean {
8
+ for (const node of nodes) {
9
+ if (
10
+ node.type === 'variable' &&
11
+ node.scope === 'local' &&
12
+ node.name === 'children'
13
+ ) {
14
+ return true;
15
+ }
16
+ if (node.type === 'html' && astContainsChildren(node.children)) {
17
+ return true;
18
+ }
19
+ if (node.type === 'macro') {
20
+ if (astContainsChildren(node.children)) return true;
21
+ if (node.branches) {
22
+ for (const branch of node.branches) {
23
+ if (astContainsChildren(branch.children)) return true;
24
+ }
25
+ }
26
+ }
27
+ }
28
+ return false;
29
+ }
@@ -3,6 +3,7 @@ import type { ASTNode } from '../markup/ast';
3
3
  interface WidgetEntry {
4
4
  body: ASTNode[];
5
5
  params: string[];
6
+ isBlock: boolean;
6
7
  }
7
8
 
8
9
  const widgets = new Map<string, WidgetEntry>();
@@ -11,14 +12,24 @@ export function registerWidget(
11
12
  name: string,
12
13
  bodyAST: ASTNode[],
13
14
  params: string[],
15
+ isBlock = false,
14
16
  ): void {
15
- widgets.set(name.toLowerCase(), { body: bodyAST, params });
17
+ const filteredParams = params.filter((p) => p !== '@children');
18
+ widgets.set(name.toLowerCase(), {
19
+ body: bodyAST,
20
+ params: filteredParams,
21
+ isBlock,
22
+ });
16
23
  }
17
24
 
18
25
  export function getWidget(name: string): WidgetEntry | undefined {
19
26
  return widgets.get(name.toLowerCase());
20
27
  }
21
28
 
29
+ export function isBlockWidget(name: string): boolean {
30
+ return widgets.get(name.toLowerCase())?.isBlock ?? false;
31
+ }
32
+
22
33
  export function clearWidgets(): void {
23
34
  widgets.clear();
24
35
  }