Skip to content

Commit 5960cca

Browse files
rohal12clemclaude
authored
fix: two-pass widget registration for parse-order dependency (#87) (#88)
* fix: resolve 12 open bug issues (#66#84) - #66: Make template literal interpolation loop string-aware (skip quoted strings and nested backtick templates) - #67: Use consistent hook values in Computed macro instead of mixing getState() with hook-derived values - #68: Add useEffect cleanup to clear SaveManager status timeout on unmount - #69: Refactor ForIteration to derive localState from props each render (WidgetBody pattern), fixing stale parent locals - #70: Disable CommonMark codeIndented via micromark extension and remove the regex that collapsed 4+ space indentation - #71: Replace unsafe (err as Error).message with instanceof guard in Passage.tsx and PassageDialog.tsx - #74: Keep StoryInit mounted in hidden container so async effects fire - #75: Use visibleCharsRef in Type macro to prevent stale closure in guard - #76: Propagate NobrContext into detached renders in Button and MacroLink - #77: Add proper dependencies to Widget registration useLayoutEffect - #83: Tag Date/RegExp in serialize with __Date__/__RegExp__ markers and reconstitute in deserialize for proper round-tripping - #84: Reject empty history arrays in isSavePayload/isSaveExport validators and add guard in loadFromPayload release-npm Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: two-pass widget registration to resolve parse-order dependency (#87) Block widgets used inside other widget definitions failed to parse if their defining passage hadn't been processed yet. Pre-scan all widget passages with regex to register block macros before any AST parsing. release-npm Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: clem <clem@envy-new> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4990b1d commit 5960cca

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

src/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,27 @@ function boot() {
108108
useStoryStore.getState().loadFromPayload(sessionPayload);
109109
}
110110

111-
// Register widgets from passages tagged "widget"
111+
// Pass 1: Pre-scan all widget passages to discover block widgets.
112+
// Register them as block macros BEFORE any tokenize/buildAST calls,
113+
// so that widget bodies using other block widgets parse correctly
114+
// regardless of passage order.
115+
const blockWidgetPattern =
116+
/\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
117+
for (const [, passage] of storyData.passages) {
118+
if (passage.tags.includes('widget')) {
119+
let match;
120+
while ((match = blockWidgetPattern.exec(passage.content)) !== null) {
121+
const name = match[1]!;
122+
const body = match[2]!;
123+
if (/\{@children\}/.test(body)) {
124+
registerBlockMacro(name);
125+
}
126+
}
127+
blockWidgetPattern.lastIndex = 0;
128+
}
129+
}
130+
131+
// Pass 2: Full parse and register widgets from passages tagged "widget"
112132
for (const [, passage] of storyData.passages) {
113133
if (passage.tags.includes('widget')) {
114134
const widgetTokens = tokenize(passage.content);

test/dom/block-widget.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,35 @@ describe('block widgets', () => {
186186
expect(isBlockWidget('plain')).toBe(false);
187187
});
188188

189+
it('block widget used inside another widget definition parses regardless of order', () => {
190+
// Simulate the two-pass approach from index.tsx:
191+
// Pass 1: pre-scan raw text to discover block widgets
192+
const passageA =
193+
'{widget "Section" @title}<section><h3>{@title}</h3>{@children}</section>{/widget}';
194+
const passageB =
195+
'{widget "Card" @name}{Section "Details"}{@name}{/Section}{/widget}';
196+
197+
// If we parse passage B first WITHOUT pre-scanning, Section isn't
198+
// in BLOCK_MACROS and buildAST would fail on {/Section}.
199+
// Pre-scan passageA to register Section as block macro first.
200+
const pattern = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
201+
let m;
202+
while ((m = pattern.exec(passageA)) !== null) {
203+
if (/\{@children\}/.test(m[2]!)) {
204+
registerBlockMacro(m[1]!);
205+
registeredBlockMacros.push(m[1]!);
206+
}
207+
}
208+
209+
// Now parse B first (the problematic order), then A
210+
defineWidget(passageB);
211+
defineWidget(passageA);
212+
213+
const el = renderPassage('{Card "Alice"}');
214+
expect(el.textContent).toContain('Details');
215+
expect(el.textContent).toContain('Alice');
216+
});
217+
189218
it('renders @children inside {if} within widget body', () => {
190219
defineAndTrack(
191220
'{widget "Conditional" @show}{if @show}<div class="wrap">{@children}</div>{/if}{/widget}',

0 commit comments

Comments
 (0)