@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/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/index.tsx +1 -0
- package/src/markup/ast.ts +4 -1
- package/src/markup/render.tsx +15 -5
- package/src/markup/tokenizer.ts +3 -15
- package/src/store.ts +10 -0
- package/src/story-api.ts +12 -0
package/package.json
CHANGED
package/src/index.tsx
CHANGED
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 (
|
|
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}>`
|
package/src/markup/render.tsx
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
?
|
|
97
|
-
|
|
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
|
|
package/src/markup/tokenizer.ts
CHANGED
|
@@ -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)
|
|
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(
|
|
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
|
}
|