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 +22 -0
- package/dist/compiler.d.ts +2 -1
- package/dist/compiler.js +180 -110
- 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,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
|
-
|
|
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
|
-
// 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
-
"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": "dadcab6940e5e7ffdd600e8821c9297fdbf93160"
|
|
37
35
|
}
|