@rohal12/spindle 0.36.1 → 0.37.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.36.1",
3
+ "version": "0.37.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
package/src/index.tsx CHANGED
@@ -101,6 +101,7 @@ function boot() {
101
101
 
102
102
  // Execute StoryInit passage if it exists
103
103
  executeStoryInit();
104
+ useStoryStore.getState().bumpInitCount();
104
105
 
105
106
  // Restore session state if the page was refreshed
106
107
  const sessionPayload = loadSession(storyData.ifid);
package/src/markup/ast.ts CHANGED
@@ -152,7 +152,10 @@ export function buildAST(tokens: Token[]): ASTNode[] {
152
152
  }
153
153
 
154
154
  const top = stack[stack.length - 1]!;
155
- if (top.node.type !== 'html' || top.node.tag !== token.tag) {
155
+ if (
156
+ top.node.type !== 'html' ||
157
+ top.node.tag.toLowerCase() !== token.tag.toLowerCase()
158
+ ) {
156
159
  const expected =
157
160
  top.node.type === 'html'
158
161
  ? `</${top.node.tag}>`
@@ -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 SvgContext = createContext(false);
26
27
  export const WidgetChildrenContext = createContext<ASTNode[] | null>(null);
27
28
 
28
29
  /**
@@ -85,16 +86,25 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
85
86
  const resolve = useInterpolate();
86
87
  const nobr = useContext(NobrContext);
87
88
  const locals = useContext(LocalsValuesContext);
89
+ const inSvg = useContext(SvgContext);
88
90
  const attrs: Record<string, unknown> = {};
89
91
  for (const [k, v] of Object.entries(node.attributes)) {
90
92
  attrs[k] = resolve(v) ?? v;
91
93
  }
92
- return h(
93
- node.tag,
94
- attrs,
94
+ const isSvgRoot = node.tag.toLowerCase() === 'svg';
95
+ // Inside SVG, skip markdown processing — markdown wraps content in <p> tags
96
+ // which break the SVG namespace and produce zero-dimension elements.
97
+ const children =
95
98
  node.children.length > 0
96
- ? renderNodes(node.children, { nobr, locals })
97
- : undefined,
99
+ ? inSvg || isSvgRoot
100
+ ? renderInlineNodes(node.children)
101
+ : renderNodes(node.children, { nobr, locals })
102
+ : undefined;
103
+ const element = h(node.tag, attrs, children);
104
+ return isSvgRoot ? (
105
+ <SvgContext.Provider value={true}>{element}</SvgContext.Provider>
106
+ ) : (
107
+ element
98
108
  );
99
109
  }
100
110
 
@@ -604,23 +604,11 @@ export function tokenize(input: string): Token[] {
604
604
  // Read tag name (letters, digits, hyphens for custom elements)
605
605
  const tagStart = j;
606
606
  while (j < input.length && /[a-zA-Z0-9-]/.test(input[j]!)) j++;
607
- const tag = input.slice(tagStart, j).toLowerCase();
607
+ const tag = input.slice(tagStart, j);
608
+ const tagLower = tag.toLowerCase();
608
609
 
609
610
  // Valid tag name must start with a letter
610
611
  if (tag && VALID_TAG_START.test(tag[0]!)) {
611
- // SVG is its own world — treat <svg>…</svg> as opaque text
612
- // so the markdown pipeline handles it with proper namespace.
613
- if (tag === 'svg' && !isClose) {
614
- const closeTag = '</svg>';
615
- const closeIdx = input.indexOf(closeTag, j);
616
- if (closeIdx !== -1) {
617
- const end = closeIdx + closeTag.length;
618
- // Leave the entire <svg>…</svg> block as text
619
- i = end;
620
- continue;
621
- }
622
- }
623
-
624
612
  if (isClose) {
625
613
  // Closing tag: skip whitespace, expect >
626
614
  while (j < input.length && /\s/.test(input[j]!)) j++;
@@ -645,7 +633,7 @@ export function tokenize(input: string): Token[] {
645
633
  const parsed = parseHtmlAttributes(input, j);
646
634
  j = parsed.endIdx;
647
635
 
648
- let isSelfClose = HTML_VOID_TAGS.has(tag);
636
+ let isSelfClose = HTML_VOID_TAGS.has(tagLower);
649
637
  if (input[j] === '/') {
650
638
  isSelfClose = true;
651
639
  j++;
package/src/store.ts CHANGED
@@ -207,6 +207,7 @@ export interface StoryState {
207
207
  renderCounts: Record<string, number>;
208
208
  knownSaves: Record<string, true>;
209
209
  playthroughId: string;
210
+ initCount: number;
210
211
  maxHistory: number;
211
212
  saveError: string | null;
212
213
  loadError: string | null;
@@ -214,6 +215,7 @@ export interface StoryState {
214
215
  nextTransition: TransitionConfig | null;
215
216
  nobr: boolean;
216
217
 
218
+ bumpInitCount: () => void;
217
219
  setMaxHistory: (limit: number) => void;
218
220
  init: (
219
221
  storyData: StoryData,
@@ -258,6 +260,7 @@ export const useStoryStore = create<StoryState>()(
258
260
  renderCounts: {},
259
261
  knownSaves: {},
260
262
  playthroughId: '',
263
+ initCount: 0,
261
264
  maxHistory: 40,
262
265
  saveError: null,
263
266
  loadError: null,
@@ -265,6 +268,12 @@ export const useStoryStore = create<StoryState>()(
265
268
  nextTransition: null,
266
269
  nobr: false,
267
270
 
271
+ bumpInitCount: () => {
272
+ set((state) => {
273
+ state.initCount++;
274
+ });
275
+ },
276
+
268
277
  setMaxHistory: (limit: number) => {
269
278
  set((state) => {
270
279
  state.maxHistory = Math.max(1, Math.round(limit));
@@ -494,6 +503,7 @@ export const useStoryStore = create<StoryState>()(
494
503
 
495
504
  lastNavigationVars = get().variables;
496
505
  executeStoryInit();
506
+ get().bumpInitCount();
497
507
  clearSession(storyData.ifid);
498
508
 
499
509
  // Start a new playthrough on restart
package/src/story-api.ts CHANGED
@@ -51,6 +51,7 @@ export type { StoryAction };
51
51
  export type { MacroMetadata };
52
52
 
53
53
  type NavigateCallback = (to: string, from: string) => void;
54
+ type StoryInitCallback = () => void;
54
55
  type ActionsChangedCallback = () => void;
55
56
  type VariableChangedCallback = (
56
57
  changed: Record<string, { from: unknown; to: unknown }>,
@@ -100,6 +101,7 @@ export interface StoryAPI {
100
101
  getActions(): StoryAction[];
101
102
  performAction(id: string, value?: unknown): void;
102
103
  on(event: 'navigate', callback: NavigateCallback): () => void;
104
+ on(event: 'storyinit', callback: StoryInitCallback): () => void;
103
105
  on(event: 'actionsChanged', callback: ActionsChangedCallback): () => void;
104
106
  on(event: 'variableChanged', callback: VariableChangedCallback): () => void;
105
107
  waitForActions(): Promise<StoryAction[]>;
@@ -356,6 +358,16 @@ function createStoryAPI(): StoryAPI {
356
358
  });
357
359
  }
358
360
 
361
+ if (event === 'storyinit') {
362
+ let prevCount = useStoryStore.getState().initCount;
363
+ return useStoryStore.subscribe((state) => {
364
+ if (state.initCount !== prevCount) {
365
+ prevCount = state.initCount;
366
+ (callback as StoryInitCallback)();
367
+ }
368
+ });
369
+ }
370
+
359
371
  if (event === 'actionsChanged') {
360
372
  return onActionsChanged(callback as ActionsChangedCallback);
361
373
  }