@rohal12/spindle 0.21.0 → 0.23.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.21.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -1,7 +1,7 @@
1
1
  import { useMemo, useEffect, useState } from 'preact/hooks';
2
2
  import { tokenize } from '../markup/tokenizer';
3
3
  import { buildAST } from '../markup/ast';
4
- import { renderNodes } from '../markup/render';
4
+ import { renderNodes, NobrContext } from '../markup/render';
5
5
  import { useStoryStore } from '../store';
6
6
  import type { Passage as PassageData } from '../parser';
7
7
  import { sourceLocationOf } from '../utils/source-location';
@@ -9,7 +9,8 @@ import { sourceLocationOf } from '../utils/source-location';
9
9
  export function renderPassageContent(passage: PassageData) {
10
10
  const tokens = tokenize(passage.content);
11
11
  const ast = buildAST(tokens);
12
- return renderNodes(ast);
12
+ const nobr = passage.tags.includes('nobr');
13
+ return renderNodes(ast, nobr ? { nobr: true } : undefined);
13
14
  }
14
15
 
15
16
  interface PassageProps {
@@ -88,7 +89,9 @@ export function Passage({ passage, dataTransition }: PassageProps) {
88
89
  }
89
90
  }, [doneReady, donePassage?.content]);
90
91
 
91
- return (
92
+ const nobr = passage.tags.includes('nobr');
93
+
94
+ const inner = (
92
95
  <div
93
96
  class="passage"
94
97
  data-passage={passage.name}
@@ -101,4 +104,10 @@ export function Passage({ passage, dataTransition }: PassageProps) {
101
104
  {doneContent && <div hidden>{doneContent}</div>}
102
105
  </div>
103
106
  );
107
+
108
+ return nobr ? (
109
+ <NobrContext.Provider value={true}>{inner}</NobrContext.Provider>
110
+ ) : (
111
+ inner
112
+ );
104
113
  }
@@ -1,7 +1,7 @@
1
1
  import { useStoryStore } from '../store';
2
2
  import { tokenize } from '../markup/tokenizer';
3
3
  import { buildAST } from '../markup/ast';
4
- import { renderInlineNodes } from '../markup/render';
4
+ import { renderInlineNodes, NobrContext } from '../markup/render';
5
5
 
6
6
  const DEFAULT_MARKUP =
7
7
  '<header class="story-menubar">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}';
@@ -12,11 +12,17 @@ export function StoryInterface() {
12
12
  const overridePassage = storyData?.passages.get('StoryInterface');
13
13
  const markup =
14
14
  overridePassage !== undefined ? overridePassage.content : DEFAULT_MARKUP;
15
+ const nobr = overridePassage?.tags.includes('nobr') ?? false;
15
16
 
16
17
  try {
17
18
  const tokens = tokenize(markup);
18
19
  const ast = buildAST(tokens);
19
- return <>{renderInlineNodes(ast)}</>;
20
+ const rendered = <>{renderInlineNodes(ast)}</>;
21
+ return nobr ? (
22
+ <NobrContext.Provider value={true}>{rendered}</NobrContext.Provider>
23
+ ) : (
24
+ rendered
25
+ );
20
26
  } catch (err) {
21
27
  return (
22
28
  <span class="error">
@@ -1,6 +1,7 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { tokenize } from '../../markup/tokenizer';
3
3
  import { buildAST } from '../../markup/ast';
4
+ import { NobrContext } from '../../markup/render';
4
5
  import { defineMacro } from '../../define-macro';
5
6
 
6
7
  defineMacro({
@@ -33,10 +34,16 @@ defineMacro({
33
34
  );
34
35
  }
35
36
 
37
+ const nobr = passage.tags.includes('nobr');
36
38
  const tokens = tokenize(passage.content);
37
39
  const ast = buildAST(tokens);
38
- const content = inline ? ctx.renderInlineNodes(ast) : ctx.renderNodes(ast);
40
+ const content = inline
41
+ ? ctx.renderInlineNodes(ast)
42
+ : ctx.renderNodes(ast, nobr ? { nobr: true } : undefined);
39
43
 
44
+ if (nobr) {
45
+ return ctx.wrap(ctx.h(NobrContext.Provider, { value: true }, content));
46
+ }
40
47
  return ctx.wrap(content);
41
48
  },
42
49
  });
@@ -0,0 +1,15 @@
1
+ import { defineMacro } from '../../define-macro';
2
+ import { NobrContext } from '../../markup/render';
3
+
4
+ defineMacro({
5
+ name: 'nobr',
6
+ render({ children = [] }, ctx) {
7
+ return ctx.wrap(
8
+ ctx.h(
9
+ NobrContext.Provider,
10
+ { value: true },
11
+ ctx.renderNodes(children, { nobr: true }),
12
+ ),
13
+ );
14
+ },
15
+ });
@@ -20,6 +20,7 @@ import '../components/macros/Dialog';
20
20
  import '../components/macros/SaveManager';
21
21
  import '../components/macros/SettingsControls';
22
22
  import '../components/macros/Include';
23
+ import '../components/macros/Nobr';
23
24
  import '../components/macros/Goto';
24
25
  import '../components/macros/Unset';
25
26
  import '../components/macros/Textbox';
package/src/markup/ast.ts CHANGED
@@ -54,6 +54,7 @@ const BLOCK_MACROS = new Set([
54
54
  'type',
55
55
  'widget',
56
56
  'span',
57
+ 'nobr',
57
58
  ]);
58
59
 
59
60
  /** Map from branch macro name → required parent macro name */
@@ -1,4 +1,5 @@
1
1
  import { createContext } from 'preact';
2
+ import { useContext } from 'preact/hooks';
2
3
  import { VarDisplay } from '../components/macros/VarDisplay';
3
4
  import { WidgetInvocation } from '../components/macros/WidgetInvocation';
4
5
  import { getWidget } from '../widgets/widget-registry';
@@ -21,6 +22,7 @@ const defaultUpdater: LocalsUpdater = {
21
22
 
22
23
  export const LocalsValuesContext = createContext<Record<string, unknown>>({});
23
24
  export const LocalsUpdateContext = createContext<LocalsUpdater>(defaultUpdater);
25
+ export const NobrContext = createContext(false);
24
26
 
25
27
  /**
26
28
  * Convert an HTML string (from micromark) to Preact VNodes,
@@ -29,9 +31,15 @@ export const LocalsUpdateContext = createContext<LocalsUpdater>(defaultUpdater);
29
31
  function htmlToPreact(
30
32
  html: string,
31
33
  components: preact.ComponentChildren[],
34
+ nobr = false,
32
35
  ): preact.ComponentChildren {
33
36
  const temp = document.createElement('div');
34
37
  temp.innerHTML = html.trim();
38
+ if (nobr) {
39
+ for (const p of Array.from(temp.querySelectorAll(':scope > p'))) {
40
+ p.replaceWith(...Array.from(p.childNodes));
41
+ }
42
+ }
35
43
  const children = Array.from(temp.childNodes).map((child, i) =>
36
44
  convertDomNode(child, i, components),
37
45
  );
@@ -74,6 +82,7 @@ function convertDomNode(
74
82
 
75
83
  function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
76
84
  const resolve = useInterpolate();
85
+ const nobr = useContext(NobrContext);
77
86
  const attrs: Record<string, unknown> = {};
78
87
  for (const [k, v] of Object.entries(node.attributes)) {
79
88
  attrs[k] = resolve(v) ?? v;
@@ -81,7 +90,9 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
81
90
  return h(
82
91
  node.tag,
83
92
  attrs,
84
- node.children.length > 0 ? renderNodes(node.children) : undefined,
93
+ node.children.length > 0
94
+ ? renderNodes(node.children, nobr ? { nobr: true } : undefined)
95
+ : undefined,
85
96
  );
86
97
  }
87
98
 
@@ -210,7 +221,10 @@ function getVariableTextValue(node: VariableNode): string {
210
221
  * After micromark processes the combined string, the HTML is parsed back into
211
222
  * Preact VNodes with placeholders replaced by the real rendered components.
212
223
  */
213
- export function renderNodes(nodes: ASTNode[]): preact.ComponentChildren {
224
+ export function renderNodes(
225
+ nodes: ASTNode[],
226
+ options?: { nobr?: boolean },
227
+ ): preact.ComponentChildren {
214
228
  if (nodes.length === 0) return null;
215
229
 
216
230
  // If there's no text at all, render nodes directly without markdown
@@ -241,5 +255,5 @@ export function renderNodes(nodes: ASTNode[]): preact.ComponentChildren {
241
255
  const html = markdownToHtml(combined);
242
256
 
243
257
  // Convert HTML to Preact VNodes, replacing placeholders with components
244
- return htmlToPreact(html, components);
258
+ return htmlToPreact(html, components, options?.nobr);
245
259
  }