squiffy-compiler 6.0.0-alpha.3 → 6.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014-2026 Alex Warren, textadventures.co.uk and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -1,4 +1,3 @@
1
- export declare const SQUIFFY_VERSION = "6.0.0-alpha.2";
2
1
  export interface Output {
3
2
  story: OutputStory;
4
3
  js: string[][];
@@ -6,6 +5,7 @@ export interface Output {
6
5
  interface OutputStory {
7
6
  start: string;
8
7
  id: string | null;
8
+ uiJsIndex?: number;
9
9
  sections: Record<string, OutputSection>;
10
10
  }
11
11
  interface OutputSection {
@@ -27,6 +27,7 @@ interface CompilerSettings {
27
27
  script: string;
28
28
  onWarning?: (message: string) => void;
29
29
  externalFiles?: ExternalFiles;
30
+ globalJs?: boolean;
30
31
  }
31
32
  interface ExternalFiles {
32
33
  getMatchingFilenames(pattern: string): Promise<string[]>;
package/dist/compiler.js CHANGED
@@ -1,24 +1,33 @@
1
- import * as marked from 'marked';
2
- export const SQUIFFY_VERSION = '6.0.0-alpha.2';
1
+ import pkg from "../package.json" with { type: "json" };
2
+ const version = pkg.version;
3
3
  export async function compile(settings) {
4
- const story = new Story(settings.scriptBaseFilename);
4
+ const story = new Story();
5
5
  const errors = [];
6
+ let autoSectionCount = 0;
6
7
  async function getJs(storyData, excludeHeader) {
7
8
  const outputJs = [];
8
9
  if (!excludeHeader) {
9
- outputJs.push(`// Created with Squiffy ${SQUIFFY_VERSION}`);
10
- outputJs.push('// https://github.com/textadventures/squiffy');
10
+ outputJs.push(`// Created with Squiffy ${version}`);
11
+ outputJs.push("// https://github.com/textadventures/squiffy");
12
+ }
13
+ if (settings.globalJs) {
14
+ outputJs.push("var story = {};");
15
+ }
16
+ else {
17
+ outputJs.push("export const story = {};");
11
18
  }
12
- outputJs.push('export const story = {};');
13
19
  outputJs.push(`story.id = ${JSON.stringify(storyData.story.id, null, 4)};`);
20
+ if (storyData.story.uiJsIndex !== undefined) {
21
+ outputJs.push(`story.uiJsIndex = ${storyData.story.uiJsIndex};`);
22
+ }
14
23
  outputJs.push(`story.start = ${JSON.stringify(storyData.story.start, null, 4)};`);
15
24
  outputJs.push(`story.sections = ${JSON.stringify(storyData.story.sections, null, 4)};`);
16
- outputJs.push('story.js = [');
25
+ outputJs.push("story.js = [");
17
26
  for (const js of storyData.js) {
18
27
  writeJs(outputJs, 1, js);
19
28
  }
20
- outputJs.push('];');
21
- return outputJs.join('\n');
29
+ outputJs.push("];");
30
+ return outputJs.join("\n");
22
31
  }
23
32
  async function getStoryData() {
24
33
  if (!story.start) {
@@ -32,6 +41,10 @@ export async function compile(settings) {
32
41
  },
33
42
  js: [],
34
43
  };
44
+ if (story.uiJs.length > 0) {
45
+ output.js.push(story.uiJs);
46
+ output.story.uiJsIndex = output.js.length - 1;
47
+ }
35
48
  for (const sectionName of Object.keys(story.sections)) {
36
49
  const section = story.sections[sectionName];
37
50
  const outputSection = {};
@@ -39,7 +52,7 @@ export async function compile(settings) {
39
52
  if (section.clear) {
40
53
  outputSection.clear = true;
41
54
  }
42
- outputSection.text = await processText(section.text.join('\n'), section, null);
55
+ outputSection.text = await processText(section.text.join("\n"), section, null);
43
56
  if (section.attributes.length > 0) {
44
57
  outputSection.attributes = section.attributes;
45
58
  }
@@ -47,10 +60,10 @@ export async function compile(settings) {
47
60
  output.js.push(section.js);
48
61
  outputSection.jsIndex = output.js.length - 1;
49
62
  }
50
- if ('@last' in section.passages) {
51
- var passageCount = 0;
63
+ if ("@last" in section.passages) {
64
+ let passageCount = 0;
52
65
  for (const passageName of Object.keys(section.passages)) {
53
- if (passageName.length > 0 && passageName?.substring(0, 1) !== '@') {
66
+ if (passageName.length > 0 && passageName?.substring(0, 1) !== "@") {
54
67
  passageCount++;
55
68
  }
56
69
  }
@@ -66,7 +79,7 @@ export async function compile(settings) {
66
79
  if (passage.clear) {
67
80
  outputPassage.clear = true;
68
81
  }
69
- outputPassage.text = await processText(passage.text.join('\n'), section, passage);
82
+ outputPassage.text = await processText(passage.text.join("\n"), section, passage);
70
83
  if (passage.attributes.length > 0) {
71
84
  outputPassage.attributes = passage.attributes;
72
85
  }
@@ -78,7 +91,6 @@ export async function compile(settings) {
78
91
  }
79
92
  return output;
80
93
  }
81
- ;
82
94
  const regexes = {
83
95
  section: /^\[\[(.*)\]\]:$/,
84
96
  passage: /^\[(.*)\]:$/,
@@ -89,28 +101,38 @@ export async function compile(settings) {
89
101
  unset: /^@unset (.*)$/,
90
102
  inc: /^@inc (\S+)(?: (\d+))?$/,
91
103
  dec: /^@dec (\S+)(?: (\d+))?$/,
92
- replace: /^@replace (.*$)/,
93
104
  js: /^(\t| {4})(.*)$/,
94
105
  continue: /^\+\+\+(.*)$/,
106
+ ui: /^@ui (.*)$/,
95
107
  };
96
108
  async function processFileText(inputText, inputFilename, isFirst) {
97
- var inputLines = inputText.replace(/\r/g, '').split('\n');
98
- var lineCount = 0;
99
- var autoSectionCount = 0;
100
- var section = null;
101
- var passage = null; // annotated differently to section, as a workaround for TypeScript "Property does not exist on type never"
102
- var textStarted = false;
103
- var ensureThisSectionExists = () => {
109
+ const inputLines = inputText.replace(/\r/g, "").split("\n");
110
+ let lineCount = 0;
111
+ let section = null;
112
+ let passage = null; // annotated differently to section, as a workaround for TypeScript "Property does not exist on type never"
113
+ let textStarted = false;
114
+ let inUiBlock = false;
115
+ const ensureThisSectionExists = () => {
104
116
  return ensureSectionExists(section, isFirst, inputFilename, lineCount);
105
117
  };
106
- const secondPass = [];
118
+ const addAutoSection = () => {
119
+ autoSectionCount++;
120
+ const autoSectionName = `_continue${autoSectionCount}`;
121
+ section = story.addSection(autoSectionName, inputFilename, lineCount);
122
+ passage = null;
123
+ textStarted = false;
124
+ return autoSectionName;
125
+ };
107
126
  for (const line of inputLines) {
108
- var stripLine = line.trim();
127
+ const stripLine = line.trim();
109
128
  lineCount++;
110
- var match = {};
129
+ const match = {};
111
130
  for (const key of Object.keys(regexes)) {
112
131
  const regex = regexes[key];
113
- match[key] = key == 'js' ? regex.exec(line) : regex.exec(stripLine);
132
+ const result = key == "js" ? regex.exec(line) : regex.exec(stripLine);
133
+ if (result) {
134
+ match[key] = result;
135
+ }
114
136
  }
115
137
  if (match.section) {
116
138
  section = story.addSection(match.section[1], inputFilename, lineCount);
@@ -119,7 +141,7 @@ export async function compile(settings) {
119
141
  }
120
142
  else if (match.passage) {
121
143
  if (!section) {
122
- errors.push(`ERROR: ${inputFilename} line ${lineCount}: Can\'t add passage "${match.passage[1]}" as no section has been created.`);
144
+ errors.push(`ERROR: ${inputFilename} line ${lineCount}: Can't add passage "${match.passage[1]}" as no section has been created.`);
123
145
  return false;
124
146
  }
125
147
  section = ensureThisSectionExists();
@@ -128,14 +150,29 @@ export async function compile(settings) {
128
150
  }
129
151
  else if (match.continue) {
130
152
  section = ensureThisSectionExists();
131
- autoSectionCount++;
132
- var autoSectionName = `_continue${autoSectionCount}`;
133
- section.addText(`[[${match.continue[1]}]](${autoSectionName})`);
134
- section = story.addSection(autoSectionName, inputFilename, lineCount);
135
- passage = null;
136
- textStarted = false;
153
+ const previousSection = section;
154
+ const autoSectionName = addAutoSection();
155
+ const text = match.continue[1] || "Continue...";
156
+ previousSection.addText(`[[${text}]](${autoSectionName})`);
137
157
  }
138
- else if (stripLine == '@clear') {
158
+ else if (stripLine == "---") {
159
+ inUiBlock = false;
160
+ if (!section) {
161
+ // Just add the _default section if we haven't started yet
162
+ ensureThisSectionExists();
163
+ }
164
+ else {
165
+ addAutoSection();
166
+ }
167
+ }
168
+ else if (stripLine == "@ui") {
169
+ inUiBlock = true;
170
+ }
171
+ else if (match.ui && settings.externalFiles) {
172
+ const content = await settings.externalFiles.getContent(match.ui[1]);
173
+ story.addUiJs(content);
174
+ }
175
+ else if (stripLine == "@clear") {
139
176
  if (!passage) {
140
177
  section = ensureThisSectionExists();
141
178
  section.clear = true;
@@ -151,18 +188,18 @@ export async function compile(settings) {
151
188
  story.start = match.start[1];
152
189
  }
153
190
  else if (match.import && settings.externalFiles) {
154
- var importFilenames = await settings.externalFiles.getMatchingFilenames(match.import[1]);
191
+ const importFilenames = await settings.externalFiles.getMatchingFilenames(match.import[1]);
155
192
  for (const importFilename of importFilenames) {
156
- if (importFilename.endsWith('.squiffy')) {
193
+ if (importFilename.endsWith(".squiffy")) {
157
194
  const content = await settings.externalFiles.getContent(importFilename);
158
- var success = await processFileText(content, importFilename, false);
195
+ const success = await processFileText(content, importFilename, false);
159
196
  if (!success)
160
197
  return false;
161
198
  }
162
- else if (importFilename.endsWith('.js')) {
199
+ else if (importFilename.endsWith(".js")) {
163
200
  story.scripts.push(settings.externalFiles.getLocalFilename(importFilename));
164
201
  }
165
- else if (importFilename.endsWith('.css')) {
202
+ else if (importFilename.endsWith(".css")) {
166
203
  story.stylesheets.push(settings.externalFiles.getLocalFilename(importFilename));
167
204
  }
168
205
  }
@@ -173,31 +210,21 @@ export async function compile(settings) {
173
210
  }
174
211
  else if (match.unset) {
175
212
  section = ensureThisSectionExists();
176
- section = addAttribute('not ' + match.unset[1], section, passage, isFirst, inputFilename, lineCount);
213
+ section = addAttribute("not " + match.unset[1], section, passage, isFirst, inputFilename, lineCount);
177
214
  }
178
215
  else if (match.inc) {
179
216
  section = ensureThisSectionExists();
180
- section = addAttribute(match.inc[1] + '+=' + (match.inc[2] === undefined ? '1' : match.inc[2]), section, passage, isFirst, inputFilename, lineCount);
217
+ section = addAttribute(match.inc[1] + "+=" + (match.inc[2] === undefined ? "1" : match.inc[2]), section, passage, isFirst, inputFilename, lineCount);
181
218
  }
182
219
  else if (match.dec) {
183
220
  section = ensureThisSectionExists();
184
- section = addAttribute(match.dec[1] + '-=' + (match.dec[2] === undefined ? '1' : match.dec[2]), section, passage, isFirst, inputFilename, lineCount);
185
- }
186
- else if (match.replace) {
187
- const thisSection = ensureThisSectionExists();
188
- const thisPassage = passage;
189
- var replaceAttribute = match.replace[1];
190
- var attributeMatch = /^(.*?)=(.*)$/.exec(replaceAttribute);
191
- secondPass.push(async () => {
192
- // add this to secondPass functions, because processText might result in links to passages which have not been created yet
193
- if (attributeMatch) {
194
- replaceAttribute = attributeMatch[1] + '=' + await processText(attributeMatch[2], thisSection, null);
195
- }
196
- addAttribute('@replace ' + replaceAttribute, section, thisPassage, isFirst, inputFilename, lineCount);
197
- });
221
+ section = addAttribute(match.dec[1] + "-=" + (match.dec[2] === undefined ? "1" : match.dec[2]), section, passage, isFirst, inputFilename, lineCount);
198
222
  }
199
223
  else if (!textStarted && match.js) {
200
- if (!passage) {
224
+ if (inUiBlock) {
225
+ story.addUiJs(match.js[2]);
226
+ }
227
+ else if (!passage) {
201
228
  section = ensureThisSectionExists();
202
229
  section.addJS(match.js[2]);
203
230
  }
@@ -206,7 +233,11 @@ export async function compile(settings) {
206
233
  }
207
234
  }
208
235
  else if (textStarted || stripLine.length > 0) {
209
- if (!passage) {
236
+ if (inUiBlock) {
237
+ errors.push(`ERROR: ${inputFilename} line ${lineCount}: Unexpected text in @ui block.`);
238
+ return false;
239
+ }
240
+ else if (!passage) {
210
241
  section = ensureThisSectionExists();
211
242
  if (section) {
212
243
  section.addText(line);
@@ -219,19 +250,14 @@ export async function compile(settings) {
219
250
  }
220
251
  }
221
252
  }
222
- for (const fn of secondPass) {
223
- await fn();
224
- }
225
253
  return true;
226
254
  }
227
- ;
228
255
  function ensureSectionExists(section, isFirst, inputFilename, lineCount) {
229
256
  if (!section && isFirst) {
230
- section = story.addSection('_default', inputFilename, lineCount);
257
+ section = story.addSection("_default", inputFilename, lineCount);
231
258
  }
232
259
  return section;
233
260
  }
234
- ;
235
261
  function addAttribute(attribute, section, passage, isFirst, inputFilename, lineCount) {
236
262
  if (!passage) {
237
263
  section = ensureSectionExists(section, isFirst, inputFilename, lineCount);
@@ -242,8 +268,54 @@ export async function compile(settings) {
242
268
  }
243
269
  return section;
244
270
  }
245
- ;
271
+ function extractLinkFunctions(link) {
272
+ const fragments = link.split(",");
273
+ return {
274
+ target: fragments[0].trim(),
275
+ setters: fragments.slice(1).join(", ")
276
+ };
277
+ }
278
+ function getAdditionalLinkParameters(link) {
279
+ const functions = extractLinkFunctions(link);
280
+ let additionalParameters = "";
281
+ if (functions.setters.length > 0) {
282
+ additionalParameters += ` set="${functions.setters}"`;
283
+ }
284
+ return {
285
+ target: functions.target,
286
+ additionalParameters
287
+ };
288
+ }
246
289
  async function processText(input, section, passage) {
290
+ // Helper to get the next section name
291
+ const getNextSectionName = () => {
292
+ const sectionNames = Object.keys(story.sections);
293
+ const currentIndex = sectionNames.indexOf(section.name);
294
+ return sectionNames[currentIndex + 1] || null;
295
+ };
296
+ // nextSectionLinkRegex matches [[text>]] - a link to the next section
297
+ const nextSectionLinkRegex = /\[\[([^\]]*?)>\]\]/g;
298
+ input = input.replace(nextSectionLinkRegex, (_match, text) => {
299
+ const nextSectionName = getNextSectionName();
300
+ if (!nextSectionName) {
301
+ settings.onWarning?.(`WARNING: ${section.filename} line ${section.line}: In section '${section.name}', there is a [[${text}>]] link but no following section exists`);
302
+ return `[[${text}]]`; // fallback
303
+ }
304
+ return `{{section "${nextSectionName}" text="${text}"}}`;
305
+ });
306
+ // namedNextSectionLinkRegex matches [[text]](>) or [[text]](>, setter=value)
307
+ // - a named link where target starts with >
308
+ const namedNextSectionLinkRegex = /\[\[([^\]]*?)\]\]\(>(.*?)\)/g;
309
+ input = input.replace(namedNextSectionLinkRegex, (_match, text, rest) => {
310
+ const nextSectionName = getNextSectionName();
311
+ if (!nextSectionName) {
312
+ settings.onWarning?.(`WARNING: ${section.filename} line ${section.line}: In section '${section.name}', there is a [[${text}]](>) link but no following section exists`);
313
+ return `[[${text}]]`; // fallback
314
+ }
315
+ // rest could be empty or ", setter=value"
316
+ const parsedName = getAdditionalLinkParameters(nextSectionName + rest);
317
+ return `{{section "${parsedName.target}" text="${text}"${parsedName.additionalParameters}}}`;
318
+ });
247
319
  // namedSectionLinkRegex matches:
248
320
  // open [[
249
321
  // any text - the link text
@@ -251,10 +323,13 @@ export async function compile(settings) {
251
323
  // open bracket
252
324
  // any text - the name of the section
253
325
  // closing bracket
254
- var namedSectionLinkRegex = /\[\[([^\]]*?)\]\]\((.*?)\)/g;
255
- var links = allMatchesForGroup(input, namedSectionLinkRegex, 2);
326
+ const namedSectionLinkRegex = /\[\[([^\]]*?)\]\]\((.*?)\)/g;
327
+ let links = allMatchesForGroup(input, namedSectionLinkRegex, 2);
256
328
  checkSectionLinks(links, section, passage);
257
- input = input.replace(namedSectionLinkRegex, '<a class="squiffy-link link-section" data-section="$2" role="link" tabindex="0">$1</a>');
329
+ input = input.replace(namedSectionLinkRegex, (_match, text /* $1 */, name /* $2 */) => {
330
+ const parsedName = getAdditionalLinkParameters(name);
331
+ return `{{section "${parsedName.target}" text="${text}"${parsedName.additionalParameters}}}`;
332
+ });
258
333
  // namedPassageLinkRegex matches:
259
334
  // open [
260
335
  // any text - the link text
@@ -262,68 +337,61 @@ export async function compile(settings) {
262
337
  // open bracket, but not http(s):// after it
263
338
  // any text - the name of the passage
264
339
  // closing bracket
265
- var namedPassageLinkRegex = /\[([^\]]*?)\]\(((?!https?:\/\/).*?)\)/g;
340
+ const namedPassageLinkRegex = /\[([^\]]*?)\]\(((?!https?:\/\/).*?)\)/g;
266
341
  links = allMatchesForGroup(input, namedPassageLinkRegex, 2);
267
342
  checkPassageLinks(links, section, passage);
268
- input = input.replace(namedPassageLinkRegex, '<a class="squiffy-link link-passage" data-passage="$2" role="link" tabindex="0">$1</a>');
343
+ input = input.replace(namedPassageLinkRegex, (_match, text /* $1 */, name /* $2 */) => {
344
+ const parsedName = getAdditionalLinkParameters(name);
345
+ return `{{passage "${parsedName.target}" text="${text}"${parsedName.additionalParameters}}}`;
346
+ });
269
347
  // unnamedSectionLinkRegex matches:
270
348
  // open [[
271
349
  // any text - the link text
272
350
  // closing ]]
273
- var unnamedSectionLinkRegex = /\[\[(.*?)\]\]/g;
351
+ const unnamedSectionLinkRegex = /\[\[(.*?)\]\]/g;
274
352
  links = allMatchesForGroup(input, unnamedSectionLinkRegex, 1);
275
353
  checkSectionLinks(links, section, passage);
276
- input = input.replace(unnamedSectionLinkRegex, '<a class="squiffy-link link-section" data-section="$1" role="link" tabindex="0">$1</a>');
354
+ input = input.replace(unnamedSectionLinkRegex, '{{section "$1"}}');
277
355
  // unnamedPassageLinkRegex matches:
278
356
  // open [
279
357
  // any text - the link text
280
358
  // closing ]
281
359
  // no bracket after
282
- var unnamedPassageLinkRegex = /\[(.*?)\]([^\(]|$)/g;
360
+ const unnamedPassageLinkRegex = /\[(.*?)\]([^(]|$)/g;
283
361
  links = allMatchesForGroup(input, unnamedPassageLinkRegex, 1);
284
362
  checkPassageLinks(links, section, passage);
285
- input = input.replace(unnamedPassageLinkRegex, '<a class="squiffy-link link-passage" data-passage="$1" role="link" tabindex="0">$1</a>$2');
286
- return (await marked.parse(input)).trim();
363
+ input = input.replace(unnamedPassageLinkRegex, '{{passage "$1"}}$2');
364
+ return input;
287
365
  }
288
- ;
289
366
  function allMatchesForGroup(input, regex, groupNumber) {
290
- var result = [];
291
- var match;
292
- while (!!(match = regex.exec(input))) {
367
+ const result = [];
368
+ let match;
369
+ while ((match = regex.exec(input))) {
293
370
  result.push(match[groupNumber]);
294
371
  }
295
372
  return result;
296
373
  }
297
- ;
298
374
  function checkSectionLinks(links, section, passage) {
299
- var badLinks = links.filter(m => !linkDestinationExists(m, story.sections));
300
- showBadLinksWarning(badLinks, 'section', '[[', ']]', section, passage);
375
+ const badLinks = links.filter(m => !linkDestinationExists(m, story.sections));
376
+ showBadLinksWarning(badLinks, "section", "[[", "]]", section, passage);
301
377
  }
302
- ;
303
378
  function checkPassageLinks(links, section, passage) {
304
- var badLinks = links.filter(m => !linkDestinationExists(m, section.passages));
305
- showBadLinksWarning(badLinks, 'passage', '[', ']', section, passage);
379
+ const badLinks = links.filter(m => !linkDestinationExists(m, section.passages));
380
+ showBadLinksWarning(badLinks, "passage", "[", "]", section, passage);
306
381
  }
307
- ;
308
382
  function linkDestinationExists(link, keys) {
309
383
  // Link destination data may look like:
310
384
  // passageName
311
385
  // passageName, my_attribute=2
312
- // passageName, @replace 1=new text, some_attribute=5
313
- // @replace 2=some words
314
386
  // We're only interested in checking if the named passage or section exists.
315
- var linkDestination = link.split(',')[0];
316
- if (linkDestination.substring(0, 1) == '@') {
317
- return true;
318
- }
387
+ const linkDestination = link.split(",")[0];
319
388
  return Object.keys(keys).includes(linkDestination);
320
389
  }
321
- ;
322
390
  function showBadLinksWarning(badLinks, linkTo, before, after, section, passage) {
323
391
  if (!settings.onWarning)
324
392
  return;
325
393
  for (const badLink of badLinks) {
326
- var warning;
394
+ let warning;
327
395
  if (!passage) {
328
396
  warning = `${section.filename} line ${section.line}: In section '${section.name}'`;
329
397
  }
@@ -333,18 +401,19 @@ export async function compile(settings) {
333
401
  settings.onWarning(`WARNING: ${warning} there is a link to a ${linkTo} called ${before}${badLink}${after}, which doesn't exist`);
334
402
  }
335
403
  }
336
- ;
337
404
  function writeJs(outputJsFile, tabCount, js) {
338
- var tabs = new Array(tabCount + 1).join('\t');
405
+ const tabs = new Array(tabCount + 1).join("\t");
339
406
  outputJsFile.push(`${tabs}(squiffy, get, set) => {`);
340
407
  for (const jsLine of js) {
341
408
  outputJsFile.push(`${tabs}\t${jsLine}`);
342
409
  }
343
410
  outputJsFile.push(`${tabs}},`);
344
411
  }
345
- ;
346
412
  const success = await processFileText(settings.script, settings.scriptBaseFilename, true);
347
413
  if (success) {
414
+ if (!Object.keys(story.sections).length) {
415
+ ensureSectionExists(null, true, settings.scriptBaseFilename, 0);
416
+ }
348
417
  const storyData = await getStoryData();
349
418
  return {
350
419
  success: true,
@@ -369,7 +438,7 @@ export async function compile(settings) {
369
438
  }
370
439
  }
371
440
  class Story {
372
- constructor(inputFilename) {
441
+ constructor() {
373
442
  Object.defineProperty(this, "sections", {
374
443
  enumerable: true,
375
444
  configurable: true,
@@ -380,7 +449,7 @@ class Story {
380
449
  enumerable: true,
381
450
  configurable: true,
382
451
  writable: true,
383
- value: ''
452
+ value: ""
384
453
  });
385
454
  Object.defineProperty(this, "scripts", {
386
455
  enumerable: true,
@@ -398,7 +467,7 @@ class Story {
398
467
  enumerable: true,
399
468
  configurable: true,
400
469
  writable: true,
401
- value: ''
470
+ value: ""
402
471
  });
403
472
  Object.defineProperty(this, "id", {
404
473
  enumerable: true,
@@ -406,14 +475,22 @@ class Story {
406
475
  writable: true,
407
476
  value: null
408
477
  });
409
- this.id = inputFilename || null;
478
+ Object.defineProperty(this, "uiJs", {
479
+ enumerable: true,
480
+ configurable: true,
481
+ writable: true,
482
+ value: []
483
+ });
484
+ this.id = crypto.randomUUID();
410
485
  }
411
486
  addSection(name, filename, line) {
412
487
  const section = new Section(name, filename, line);
413
488
  this.sections[name] = section;
414
489
  return section;
415
490
  }
416
- ;
491
+ addUiJs(text) {
492
+ this.uiJs.push(text);
493
+ }
417
494
  }
418
495
  class Section {
419
496
  constructor(name, filename, line) {
@@ -470,23 +547,19 @@ class Section {
470
547
  this.line = line;
471
548
  }
472
549
  addPassage(name, line) {
473
- var passage = new Passage(name, line);
550
+ const passage = new Passage(name, line);
474
551
  this.passages[name] = passage;
475
552
  return passage;
476
553
  }
477
- ;
478
554
  addText(text) {
479
555
  this.text.push(text);
480
556
  }
481
- ;
482
557
  addJS(text) {
483
558
  this.js.push(text);
484
559
  }
485
- ;
486
560
  addAttribute(text) {
487
561
  this.attributes.push(text);
488
562
  }
489
- ;
490
563
  }
491
564
  class Passage {
492
565
  constructor(name, line) {
@@ -532,13 +605,10 @@ class Passage {
532
605
  addText(text) {
533
606
  this.text.push(text);
534
607
  }
535
- ;
536
608
  addJS(text) {
537
609
  this.js.push(text);
538
610
  }
539
- ;
540
611
  addAttribute(text) {
541
612
  this.attributes.push(text);
542
613
  }
543
- ;
544
614
  }
package/package.json CHANGED
@@ -1,10 +1,7 @@
1
1
  {
2
2
  "name": "squiffy-compiler",
3
- "version": "6.0.0-alpha.3",
3
+ "version": "6.0.0-beta.0",
4
4
  "description": "A tool for creating multiple-choice interactive stories",
5
- "dependencies": {
6
- "marked": "^13.0.2"
7
- },
8
5
  "author": "Alex Warren",
9
6
  "contributors": [
10
7
  "CrisisSDK",
@@ -18,10 +15,10 @@
18
15
  "license": "MIT",
19
16
  "preferGlobal": true,
20
17
  "devDependencies": {
21
- "@types/node": "^22.5.5",
22
- "glob": "^11.0.0",
23
- "typescript": "^5.6.2",
24
- "vitest": "^2.1.1"
18
+ "@types/node": "^24.3.0",
19
+ "glob": "^11.0.3",
20
+ "typescript": "^5.9.2",
21
+ "vitest": "^3.2.4"
25
22
  },
26
23
  "scripts": {
27
24
  "test": "vitest --run",
@@ -33,5 +30,6 @@
33
30
  "files": [
34
31
  "dist"
35
32
  ],
36
- "type": "module"
33
+ "type": "module",
34
+ "gitHead": "dadcab6940e5e7ffdd600e8821c9297fdbf93160"
37
35
  }