squiffy-compiler 6.0.0-alpha.3 → 6.0.0-beta.1
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 +22 -0
- package/dist/compiler.d.ts +2 -1
- package/dist/compiler.js +185 -108
- package/package.json +7 -9
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
|
+
|
package/dist/compiler.d.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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 ${
|
|
10
|
-
outputJs.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
51
|
-
|
|
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(
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
127
|
+
const stripLine = line.trim();
|
|
109
128
|
lineCount++;
|
|
110
|
-
|
|
129
|
+
const match = {};
|
|
111
130
|
for (const key of Object.keys(regexes)) {
|
|
112
131
|
const regex = regexes[key];
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 ==
|
|
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
|
-
|
|
191
|
+
const importFilenames = await settings.externalFiles.getMatchingFilenames(match.import[1]);
|
|
155
192
|
for (const importFilename of importFilenames) {
|
|
156
|
-
if (importFilename.endsWith(
|
|
193
|
+
if (importFilename.endsWith(".squiffy")) {
|
|
157
194
|
const content = await settings.externalFiles.getContent(importFilename);
|
|
158
|
-
|
|
195
|
+
const success = await processFileText(content, importFilename, false);
|
|
159
196
|
if (!success)
|
|
160
197
|
return false;
|
|
161
198
|
}
|
|
162
|
-
else if (importFilename.endsWith(
|
|
199
|
+
else if (importFilename.endsWith(".js")) {
|
|
163
200
|
story.scripts.push(settings.externalFiles.getLocalFilename(importFilename));
|
|
164
201
|
}
|
|
165
|
-
else if (importFilename.endsWith(
|
|
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(
|
|
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] +
|
|
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] +
|
|
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 (
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
255
|
-
|
|
326
|
+
const namedSectionLinkRegex = /\[\[([^\]]*?)\]\]\((.*?)\)/g;
|
|
327
|
+
let links = allMatchesForGroup(input, namedSectionLinkRegex, 2);
|
|
256
328
|
checkSectionLinks(links, section, passage);
|
|
257
|
-
input = input.replace(namedSectionLinkRegex,
|
|
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,68 @@ 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
|
-
|
|
340
|
+
const namedPassageLinkRegex = /\[([^\]]*?)\]\(((?!https?:\/\/).*?)\)/g;
|
|
266
341
|
links = allMatchesForGroup(input, namedPassageLinkRegex, 2);
|
|
267
342
|
checkPassageLinks(links, section, passage);
|
|
268
|
-
input = input.replace(namedPassageLinkRegex,
|
|
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
|
-
|
|
351
|
+
const unnamedSectionLinkRegex = /\[\[(.*?)\]\]/g;
|
|
274
352
|
links = allMatchesForGroup(input, unnamedSectionLinkRegex, 1);
|
|
275
353
|
checkSectionLinks(links, section, passage);
|
|
276
|
-
input = input.replace(unnamedSectionLinkRegex, '
|
|
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
|
-
|
|
360
|
+
const unnamedPassageLinkRegex = /\[(.*?)\]([^(]|$)/g;
|
|
283
361
|
links = allMatchesForGroup(input, unnamedPassageLinkRegex, 1);
|
|
284
362
|
checkPassageLinks(links, section, passage);
|
|
285
|
-
input = input.replace(unnamedPassageLinkRegex, '
|
|
286
|
-
return
|
|
363
|
+
input = input.replace(unnamedPassageLinkRegex, '{{passage "$1"}}$2');
|
|
364
|
+
return input;
|
|
287
365
|
}
|
|
288
|
-
;
|
|
289
366
|
function allMatchesForGroup(input, regex, groupNumber) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
while (
|
|
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
|
-
|
|
300
|
-
showBadLinksWarning(badLinks,
|
|
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
|
-
|
|
305
|
-
showBadLinksWarning(badLinks,
|
|
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
|
-
//
|
|
313
|
-
// @replace 2=some words
|
|
386
|
+
// passage name with a comma, in the name
|
|
314
387
|
// We're only interested in checking if the named passage or section exists.
|
|
315
|
-
|
|
316
|
-
|
|
388
|
+
// First check if the full link is a valid destination (handles names containing commas).
|
|
389
|
+
const trimmedLink = link.trim();
|
|
390
|
+
if (Object.keys(keys).includes(trimmedLink)) {
|
|
317
391
|
return true;
|
|
318
392
|
}
|
|
393
|
+
// Otherwise, assume a comma separates the destination from setters.
|
|
394
|
+
const linkDestination = trimmedLink.split(",")[0].trim();
|
|
319
395
|
return Object.keys(keys).includes(linkDestination);
|
|
320
396
|
}
|
|
321
|
-
;
|
|
322
397
|
function showBadLinksWarning(badLinks, linkTo, before, after, section, passage) {
|
|
323
398
|
if (!settings.onWarning)
|
|
324
399
|
return;
|
|
325
400
|
for (const badLink of badLinks) {
|
|
326
|
-
|
|
401
|
+
let warning;
|
|
327
402
|
if (!passage) {
|
|
328
403
|
warning = `${section.filename} line ${section.line}: In section '${section.name}'`;
|
|
329
404
|
}
|
|
@@ -333,18 +408,19 @@ export async function compile(settings) {
|
|
|
333
408
|
settings.onWarning(`WARNING: ${warning} there is a link to a ${linkTo} called ${before}${badLink}${after}, which doesn't exist`);
|
|
334
409
|
}
|
|
335
410
|
}
|
|
336
|
-
;
|
|
337
411
|
function writeJs(outputJsFile, tabCount, js) {
|
|
338
|
-
|
|
412
|
+
const tabs = new Array(tabCount + 1).join("\t");
|
|
339
413
|
outputJsFile.push(`${tabs}(squiffy, get, set) => {`);
|
|
340
414
|
for (const jsLine of js) {
|
|
341
415
|
outputJsFile.push(`${tabs}\t${jsLine}`);
|
|
342
416
|
}
|
|
343
417
|
outputJsFile.push(`${tabs}},`);
|
|
344
418
|
}
|
|
345
|
-
;
|
|
346
419
|
const success = await processFileText(settings.script, settings.scriptBaseFilename, true);
|
|
347
420
|
if (success) {
|
|
421
|
+
if (!Object.keys(story.sections).length) {
|
|
422
|
+
ensureSectionExists(null, true, settings.scriptBaseFilename, 0);
|
|
423
|
+
}
|
|
348
424
|
const storyData = await getStoryData();
|
|
349
425
|
return {
|
|
350
426
|
success: true,
|
|
@@ -369,7 +445,7 @@ export async function compile(settings) {
|
|
|
369
445
|
}
|
|
370
446
|
}
|
|
371
447
|
class Story {
|
|
372
|
-
constructor(
|
|
448
|
+
constructor() {
|
|
373
449
|
Object.defineProperty(this, "sections", {
|
|
374
450
|
enumerable: true,
|
|
375
451
|
configurable: true,
|
|
@@ -380,7 +456,7 @@ class Story {
|
|
|
380
456
|
enumerable: true,
|
|
381
457
|
configurable: true,
|
|
382
458
|
writable: true,
|
|
383
|
-
value:
|
|
459
|
+
value: ""
|
|
384
460
|
});
|
|
385
461
|
Object.defineProperty(this, "scripts", {
|
|
386
462
|
enumerable: true,
|
|
@@ -398,7 +474,7 @@ class Story {
|
|
|
398
474
|
enumerable: true,
|
|
399
475
|
configurable: true,
|
|
400
476
|
writable: true,
|
|
401
|
-
value:
|
|
477
|
+
value: ""
|
|
402
478
|
});
|
|
403
479
|
Object.defineProperty(this, "id", {
|
|
404
480
|
enumerable: true,
|
|
@@ -406,14 +482,22 @@ class Story {
|
|
|
406
482
|
writable: true,
|
|
407
483
|
value: null
|
|
408
484
|
});
|
|
409
|
-
this
|
|
485
|
+
Object.defineProperty(this, "uiJs", {
|
|
486
|
+
enumerable: true,
|
|
487
|
+
configurable: true,
|
|
488
|
+
writable: true,
|
|
489
|
+
value: []
|
|
490
|
+
});
|
|
491
|
+
this.id = crypto.randomUUID();
|
|
410
492
|
}
|
|
411
493
|
addSection(name, filename, line) {
|
|
412
494
|
const section = new Section(name, filename, line);
|
|
413
495
|
this.sections[name] = section;
|
|
414
496
|
return section;
|
|
415
497
|
}
|
|
416
|
-
|
|
498
|
+
addUiJs(text) {
|
|
499
|
+
this.uiJs.push(text);
|
|
500
|
+
}
|
|
417
501
|
}
|
|
418
502
|
class Section {
|
|
419
503
|
constructor(name, filename, line) {
|
|
@@ -470,23 +554,19 @@ class Section {
|
|
|
470
554
|
this.line = line;
|
|
471
555
|
}
|
|
472
556
|
addPassage(name, line) {
|
|
473
|
-
|
|
557
|
+
const passage = new Passage(name, line);
|
|
474
558
|
this.passages[name] = passage;
|
|
475
559
|
return passage;
|
|
476
560
|
}
|
|
477
|
-
;
|
|
478
561
|
addText(text) {
|
|
479
562
|
this.text.push(text);
|
|
480
563
|
}
|
|
481
|
-
;
|
|
482
564
|
addJS(text) {
|
|
483
565
|
this.js.push(text);
|
|
484
566
|
}
|
|
485
|
-
;
|
|
486
567
|
addAttribute(text) {
|
|
487
568
|
this.attributes.push(text);
|
|
488
569
|
}
|
|
489
|
-
;
|
|
490
570
|
}
|
|
491
571
|
class Passage {
|
|
492
572
|
constructor(name, line) {
|
|
@@ -532,13 +612,10 @@ class Passage {
|
|
|
532
612
|
addText(text) {
|
|
533
613
|
this.text.push(text);
|
|
534
614
|
}
|
|
535
|
-
;
|
|
536
615
|
addJS(text) {
|
|
537
616
|
this.js.push(text);
|
|
538
617
|
}
|
|
539
|
-
;
|
|
540
618
|
addAttribute(text) {
|
|
541
619
|
this.attributes.push(text);
|
|
542
620
|
}
|
|
543
|
-
;
|
|
544
621
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squiffy-compiler",
|
|
3
|
-
"version": "6.0.0-
|
|
3
|
+
"version": "6.0.0-beta.1",
|
|
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
|
-
"glob": "^11.0.
|
|
23
|
-
"typescript": "^5.
|
|
24
|
-
"vitest": "^2.
|
|
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": "da6023a5cde6b179ce756f8f03bfc2b4ff7a0274"
|
|
37
35
|
}
|