@rohal12/twee-ts 1.1.2 → 1.3.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/README.md +9 -0
- package/dist/bin/twee-ts.js +258 -166
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/{chunk-QTXRKA4T.js → chunk-OGP2NXLJ.js} +255 -160
- package/dist/chunk-OGP2NXLJ.js.map +1 -0
- package/dist/index.cjs +288 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +257 -164
- package/dist/index.js.map +1 -1
- package/dist/plugins/rollup.d.ts +7 -1
- package/dist/plugins/rollup.js +2 -2
- package/dist/plugins/rollup.js.map +1 -1
- package/dist/plugins/vite.d.ts +7 -1
- package/dist/plugins/vite.js +2 -2
- package/dist/plugins/vite.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-QTXRKA4T.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -69,13 +69,26 @@ function tweeEscape(s) {
|
|
|
69
69
|
if (s.length === 0) return s;
|
|
70
70
|
return s.replace(/[\\[\]{}]/g, (ch) => "\\" + ch);
|
|
71
71
|
}
|
|
72
|
+
function jsStringEscape(s) {
|
|
73
|
+
if (s.length === 0) return s;
|
|
74
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
75
|
+
}
|
|
76
|
+
function commentSanitize(s) {
|
|
77
|
+
return s.replace(/\*\//g, "* /");
|
|
78
|
+
}
|
|
79
|
+
function htmlCommentSanitize(s) {
|
|
80
|
+
return s.replace(/-->/g, "-- >");
|
|
81
|
+
}
|
|
72
82
|
function tweeUnescape(s) {
|
|
73
83
|
if (s.length === 0) return s;
|
|
74
84
|
let result = "";
|
|
75
85
|
for (let i = 0; i < s.length; i++) {
|
|
76
86
|
if (s[i] === "\\") {
|
|
77
87
|
i++;
|
|
78
|
-
if (i >= s.length)
|
|
88
|
+
if (i >= s.length) {
|
|
89
|
+
result += "\\";
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
79
92
|
}
|
|
80
93
|
result += s[i];
|
|
81
94
|
}
|
|
@@ -168,16 +181,16 @@ function passageToPassagedata(p, pid) {
|
|
|
168
181
|
let position;
|
|
169
182
|
let size;
|
|
170
183
|
if (hasMetadataPosition(p)) {
|
|
171
|
-
position = p.metadata
|
|
184
|
+
position = p.metadata?.position ?? "";
|
|
172
185
|
} else {
|
|
173
186
|
const x = pid % 10;
|
|
174
|
-
|
|
187
|
+
const y = Math.floor(pid / 10);
|
|
175
188
|
const xp = x === 0 ? 10 : x;
|
|
176
189
|
const yp = x === 0 ? y : y + 1;
|
|
177
190
|
position = `${xp * 125 - 25},${yp * 125 - 25}`;
|
|
178
191
|
}
|
|
179
192
|
if (hasMetadataSize(p)) {
|
|
180
|
-
size = p.metadata
|
|
193
|
+
size = p.metadata?.size ?? "";
|
|
181
194
|
} else {
|
|
182
195
|
size = "100,100";
|
|
183
196
|
}
|
|
@@ -186,10 +199,10 @@ function passageToPassagedata(p, pid) {
|
|
|
186
199
|
function passageToTiddler(p, pid) {
|
|
187
200
|
let position;
|
|
188
201
|
if (hasMetadataPosition(p)) {
|
|
189
|
-
position = p.metadata
|
|
202
|
+
position = p.metadata?.position ?? "";
|
|
190
203
|
} else {
|
|
191
204
|
const x = pid % 10;
|
|
192
|
-
|
|
205
|
+
const y = Math.floor(pid / 10);
|
|
193
206
|
const xp = x === 0 ? 10 : x;
|
|
194
207
|
const yp = x === 0 ? y : y + 1;
|
|
195
208
|
position = `${xp * 140 - 130},${yp * 140 - 130}`;
|
|
@@ -208,15 +221,17 @@ function countWords(p) {
|
|
|
208
221
|
}
|
|
209
222
|
function applyTagAliases(passages, aliases) {
|
|
210
223
|
const entries = Object.entries(aliases);
|
|
211
|
-
if (entries.length === 0) return;
|
|
212
|
-
|
|
213
|
-
const original =
|
|
224
|
+
if (entries.length === 0) return [...passages];
|
|
225
|
+
return passages.map((p) => {
|
|
226
|
+
const original = p.tags;
|
|
227
|
+
const added = [];
|
|
214
228
|
for (const [alias, canonical] of entries) {
|
|
215
|
-
if (original.includes(alias) && !
|
|
216
|
-
|
|
229
|
+
if (original.includes(alias) && !original.includes(canonical) && !added.includes(canonical)) {
|
|
230
|
+
added.push(canonical);
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
|
-
|
|
233
|
+
return added.length > 0 ? { ...p, tags: [...original, ...added] } : p;
|
|
234
|
+
});
|
|
220
235
|
}
|
|
221
236
|
function quote(s) {
|
|
222
237
|
return `"${s}"`;
|
|
@@ -288,27 +303,32 @@ function marshalStoryData(story) {
|
|
|
288
303
|
return JSON.stringify(data, null, " ");
|
|
289
304
|
}
|
|
290
305
|
function unmarshalStoryData(story, json) {
|
|
291
|
-
let
|
|
306
|
+
let raw;
|
|
292
307
|
try {
|
|
293
|
-
|
|
308
|
+
raw = JSON.parse(json);
|
|
294
309
|
} catch (e) {
|
|
295
310
|
return `Cannot unmarshal "StoryData"; ${e instanceof Error ? e.message : String(e)}`;
|
|
296
311
|
}
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
313
|
+
return 'Cannot unmarshal "StoryData"; expected a JSON object';
|
|
314
|
+
}
|
|
315
|
+
const data = raw;
|
|
316
|
+
if (typeof data.ifid === "string" && data.ifid) story.ifid = data.ifid.toUpperCase();
|
|
317
|
+
if (typeof data.format === "string" && data.format) story.twine2.format = data.format;
|
|
318
|
+
if (typeof data["format-version"] === "string" && data["format-version"])
|
|
319
|
+
story.twine2.formatVersion = data["format-version"];
|
|
320
|
+
if (Array.isArray(data.options)) {
|
|
301
321
|
for (const opt of data.options) {
|
|
302
|
-
story.twine2.options.set(opt, true);
|
|
322
|
+
if (typeof opt === "string") story.twine2.options.set(opt, true);
|
|
303
323
|
}
|
|
304
324
|
}
|
|
305
|
-
if (data.start) story.twine2.start = data.start;
|
|
306
|
-
if (data["tag-colors"]) {
|
|
325
|
+
if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
|
|
326
|
+
if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
|
|
307
327
|
for (const [tag, color] of Object.entries(data["tag-colors"])) {
|
|
308
|
-
story.twine2.tagColors.set(tag, color);
|
|
328
|
+
if (typeof color === "string") story.twine2.tagColors.set(tag, color);
|
|
309
329
|
}
|
|
310
330
|
}
|
|
311
|
-
if (data.zoom
|
|
331
|
+
if (typeof data.zoom === "number" && data.zoom !== 0) story.twine2.zoom = data.zoom;
|
|
312
332
|
return null;
|
|
313
333
|
}
|
|
314
334
|
function unmarshalStorySettings(story, text, diagnostics) {
|
|
@@ -351,6 +371,7 @@ function unmarshalStorySettings(story, text, diagnostics) {
|
|
|
351
371
|
}
|
|
352
372
|
}
|
|
353
373
|
function storyAdd(story, p, diagnostics) {
|
|
374
|
+
let processed = p;
|
|
354
375
|
switch (p.name) {
|
|
355
376
|
case "StoryIncludes":
|
|
356
377
|
diagnostics.push({
|
|
@@ -370,7 +391,7 @@ function storyAdd(story, p, diagnostics) {
|
|
|
370
391
|
});
|
|
371
392
|
}
|
|
372
393
|
}
|
|
373
|
-
p
|
|
394
|
+
processed = { ...p, text: marshalStoryData(story) };
|
|
374
395
|
} else {
|
|
375
396
|
diagnostics.push({
|
|
376
397
|
level: "error",
|
|
@@ -382,12 +403,14 @@ function storyAdd(story, p, diagnostics) {
|
|
|
382
403
|
case "StorySettings":
|
|
383
404
|
unmarshalStorySettings(story, p.text, diagnostics);
|
|
384
405
|
break;
|
|
385
|
-
case "StoryTitle":
|
|
386
|
-
|
|
387
|
-
|
|
406
|
+
case "StoryTitle": {
|
|
407
|
+
const trimmed = p.text.trim();
|
|
408
|
+
processed = { ...p, text: trimmed };
|
|
409
|
+
story.name = trimmed;
|
|
388
410
|
break;
|
|
411
|
+
}
|
|
389
412
|
}
|
|
390
|
-
storyAppend(story,
|
|
413
|
+
storyAppend(story, processed, diagnostics);
|
|
391
414
|
}
|
|
392
415
|
function getStoryStats(story) {
|
|
393
416
|
let storyPassages = 0;
|
|
@@ -441,7 +464,7 @@ var MEDIA_TYPE_MAP = {
|
|
|
441
464
|
webm: "video/webm"
|
|
442
465
|
};
|
|
443
466
|
function mediaTypeFromExt(ext) {
|
|
444
|
-
return MEDIA_TYPE_MAP[ext] ?? "";
|
|
467
|
+
return MEDIA_TYPE_MAP[ext] ?? "application/octet-stream";
|
|
445
468
|
}
|
|
446
469
|
function mediaTypeFromFilename(filename) {
|
|
447
470
|
return mediaTypeFromExt(normalizedFileExt(filename));
|
|
@@ -463,8 +486,6 @@ var KNOWN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
463
486
|
"twee",
|
|
464
487
|
"tw2",
|
|
465
488
|
"twee2",
|
|
466
|
-
"htm",
|
|
467
|
-
"html",
|
|
468
489
|
"css",
|
|
469
490
|
"js",
|
|
470
491
|
"otf",
|
|
@@ -574,23 +595,49 @@ function watchFilesystem(pathnames, outFilename, callback) {
|
|
|
574
595
|
}
|
|
575
596
|
|
|
576
597
|
// src/formats.ts
|
|
577
|
-
import {
|
|
598
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
578
599
|
import { join as join2 } from "path";
|
|
579
600
|
import { homedir } from "os";
|
|
601
|
+
|
|
602
|
+
// src/util.ts
|
|
603
|
+
import { readFileSync } from "fs";
|
|
604
|
+
import { parse as parsePath } from "path";
|
|
605
|
+
function readUTF8(filename) {
|
|
606
|
+
let content = readFileSync(filename, "utf-8");
|
|
607
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
608
|
+
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
609
|
+
return content;
|
|
610
|
+
}
|
|
611
|
+
function readBase64(filename) {
|
|
612
|
+
return readFileSync(filename).toString("base64");
|
|
613
|
+
}
|
|
614
|
+
function baseNameWithoutExt(filename) {
|
|
615
|
+
const { name, ext } = parsePath(filename);
|
|
616
|
+
return name || ext;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/formats.ts
|
|
580
620
|
function parseFormatJSON(source, formatId) {
|
|
581
621
|
const first = source.indexOf("{");
|
|
582
622
|
const last = source.lastIndexOf("}");
|
|
583
623
|
if (first === -1 || last === -1) return null;
|
|
584
624
|
let chunk = source.slice(first, last + 1);
|
|
625
|
+
const parse = (json) => {
|
|
626
|
+
const raw = JSON.parse(json);
|
|
627
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
628
|
+
const obj = raw;
|
|
629
|
+
if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
|
|
630
|
+
return { name: obj.name, version: obj.version, source: obj.source, proofing: obj.proofing === true };
|
|
631
|
+
};
|
|
585
632
|
try {
|
|
586
|
-
return
|
|
633
|
+
return parse(chunk);
|
|
587
634
|
} catch {
|
|
588
635
|
if (formatId.toLowerCase().startsWith("harlowe")) {
|
|
589
636
|
const setupIdx = chunk.lastIndexOf(',"setup": function');
|
|
590
637
|
if (setupIdx !== -1) {
|
|
591
638
|
chunk = chunk.slice(0, setupIdx) + "}";
|
|
592
639
|
try {
|
|
593
|
-
return
|
|
640
|
+
return parse(chunk);
|
|
594
641
|
} catch {
|
|
595
642
|
}
|
|
596
643
|
}
|
|
@@ -598,12 +645,6 @@ function parseFormatJSON(source, formatId) {
|
|
|
598
645
|
return null;
|
|
599
646
|
}
|
|
600
647
|
}
|
|
601
|
-
function readUTF8(filename) {
|
|
602
|
-
let content = readFileSync(filename, "utf-8");
|
|
603
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
604
|
-
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
605
|
-
return content;
|
|
606
|
-
}
|
|
607
648
|
function discoverFormats(searchDirs) {
|
|
608
649
|
const formats = /* @__PURE__ */ new Map();
|
|
609
650
|
const baseFilenames = ["format.js", "header.html"];
|
|
@@ -698,9 +739,11 @@ function parseSemver(v) {
|
|
|
698
739
|
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
699
740
|
}
|
|
700
741
|
function semverCompare(a, b) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
742
|
+
const [aMajor, aMinor, aPatch] = a;
|
|
743
|
+
const [bMajor, bMinor, bPatch] = b;
|
|
744
|
+
if (aMajor !== bMajor) return aMajor - bMajor;
|
|
745
|
+
if (aMinor !== bMinor) return aMinor - bMinor;
|
|
746
|
+
if (aPatch !== bPatch) return aPatch - bPatch;
|
|
704
747
|
return 0;
|
|
705
748
|
}
|
|
706
749
|
function getFormatIdByNameAndVersion(formats, name, version) {
|
|
@@ -731,7 +774,6 @@ function readFormatSource(format) {
|
|
|
731
774
|
}
|
|
732
775
|
|
|
733
776
|
// src/loader.ts
|
|
734
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
735
777
|
import { basename } from "path";
|
|
736
778
|
|
|
737
779
|
// src/lexer.ts
|
|
@@ -1024,13 +1066,13 @@ function hasTwee2Syntax(s) {
|
|
|
1024
1066
|
}
|
|
1025
1067
|
function twee2ToV3(s) {
|
|
1026
1068
|
if (!hasTwee2Syntax(s)) return s;
|
|
1027
|
-
|
|
1069
|
+
let result = s.replace(twee2HeaderRe, (_, p1, p2, p3) => {
|
|
1028
1070
|
const tags = p2 ?? "";
|
|
1029
1071
|
const pos = p3 ?? "";
|
|
1030
1072
|
return `${p1}${tags} {"position":"${pos}"}`;
|
|
1031
1073
|
});
|
|
1032
|
-
|
|
1033
|
-
return
|
|
1074
|
+
result = result.replace(twee2BadPosRe, "$1");
|
|
1075
|
+
return result;
|
|
1034
1076
|
}
|
|
1035
1077
|
|
|
1036
1078
|
// src/parser.ts
|
|
@@ -1108,7 +1150,11 @@ function parseTwee(source, options = {}) {
|
|
|
1108
1150
|
return { passages, diagnostics };
|
|
1109
1151
|
}
|
|
1110
1152
|
try {
|
|
1111
|
-
const
|
|
1153
|
+
const raw = JSON.parse(item.val);
|
|
1154
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1155
|
+
throw new Error("expected a JSON object");
|
|
1156
|
+
}
|
|
1157
|
+
const parsed = raw;
|
|
1112
1158
|
const meta = {};
|
|
1113
1159
|
if (typeof parsed.position === "string") meta.position = parsed.position;
|
|
1114
1160
|
if (typeof parsed.size === "string") meta.size = parsed.size;
|
|
@@ -1128,6 +1174,16 @@ function parseTwee(source, options = {}) {
|
|
|
1128
1174
|
current.text = trim ? item.val.trim() : item.val;
|
|
1129
1175
|
break;
|
|
1130
1176
|
}
|
|
1177
|
+
default: {
|
|
1178
|
+
const _exhaustive = item.type;
|
|
1179
|
+
diagnostics.push({
|
|
1180
|
+
level: "error",
|
|
1181
|
+
message: `line ${item.line}: Unhandled lexer item type: ${_exhaustive}.`,
|
|
1182
|
+
file: filename,
|
|
1183
|
+
line: item.line
|
|
1184
|
+
});
|
|
1185
|
+
return { passages, diagnostics };
|
|
1186
|
+
}
|
|
1131
1187
|
}
|
|
1132
1188
|
lastType = item.type;
|
|
1133
1189
|
}
|
|
@@ -1138,15 +1194,6 @@ function parseTwee(source, options = {}) {
|
|
|
1138
1194
|
}
|
|
1139
1195
|
|
|
1140
1196
|
// src/loader.ts
|
|
1141
|
-
function readUTF82(filename) {
|
|
1142
|
-
let content = readFileSync2(filename, "utf-8");
|
|
1143
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1144
|
-
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1145
|
-
return content;
|
|
1146
|
-
}
|
|
1147
|
-
function readBase64(filename) {
|
|
1148
|
-
return readFileSync2(filename).toString("base64");
|
|
1149
|
-
}
|
|
1150
1197
|
function loadSources(story, filenames, opts, diagnostics, processedFiles) {
|
|
1151
1198
|
for (const filename of filenames) {
|
|
1152
1199
|
if (processedFiles.has(filename)) {
|
|
@@ -1245,7 +1292,7 @@ function loadInlineSources(story, sources, opts, diagnostics) {
|
|
|
1245
1292
|
}
|
|
1246
1293
|
}
|
|
1247
1294
|
function loadTwee(story, filename, opts, diagnostics) {
|
|
1248
|
-
const source =
|
|
1295
|
+
const source = readUTF8(filename);
|
|
1249
1296
|
const result = parseTwee(source, {
|
|
1250
1297
|
filename,
|
|
1251
1298
|
trim: opts.trim ?? true,
|
|
@@ -1257,12 +1304,12 @@ function loadTwee(story, filename, opts, diagnostics) {
|
|
|
1257
1304
|
}
|
|
1258
1305
|
}
|
|
1259
1306
|
function loadTagged(story, tag, filename, diagnostics) {
|
|
1260
|
-
const source =
|
|
1307
|
+
const source = readUTF8(filename);
|
|
1261
1308
|
storyAdd(story, { name: basename(filename), tags: [tag], text: source }, diagnostics);
|
|
1262
1309
|
}
|
|
1263
1310
|
function loadMedia(story, tag, filename, diagnostics) {
|
|
1264
1311
|
const source = readBase64(filename);
|
|
1265
|
-
const name =
|
|
1312
|
+
const name = baseNameWithoutExt(filename);
|
|
1266
1313
|
storyAdd(
|
|
1267
1314
|
story,
|
|
1268
1315
|
{
|
|
@@ -1276,7 +1323,7 @@ function loadMedia(story, tag, filename, diagnostics) {
|
|
|
1276
1323
|
function loadFont(story, filename, diagnostics) {
|
|
1277
1324
|
const source = readBase64(filename);
|
|
1278
1325
|
const name = basename(filename);
|
|
1279
|
-
const family =
|
|
1326
|
+
const family = baseNameWithoutExt(filename);
|
|
1280
1327
|
const ext = normalizedFileExt(filename);
|
|
1281
1328
|
const mediaType = mediaTypeFromExt(ext);
|
|
1282
1329
|
const hint = fontFormatHint(ext);
|
|
@@ -1294,9 +1341,11 @@ function loadFont(story, filename, diagnostics) {
|
|
|
1294
1341
|
);
|
|
1295
1342
|
}
|
|
1296
1343
|
|
|
1344
|
+
// src/version.ts
|
|
1345
|
+
var VERSION = "1.2.0";
|
|
1346
|
+
|
|
1297
1347
|
// src/output-twine2.ts
|
|
1298
1348
|
var CREATOR_NAME = "Twee-ts";
|
|
1299
|
-
var CREATOR_VERSION = "0.1.0";
|
|
1300
1349
|
function toTwine2Archive(story, startName, diagnostics) {
|
|
1301
1350
|
return getTwine2DataChunk(story, startName, diagnostics) + "\n";
|
|
1302
1351
|
}
|
|
@@ -1324,12 +1373,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1324
1373
|
}
|
|
1325
1374
|
let styleContent = "";
|
|
1326
1375
|
if (stylesheets.length === 1) {
|
|
1327
|
-
styleContent = stylesheets[0]
|
|
1376
|
+
styleContent = stylesheets[0]?.text ?? "";
|
|
1328
1377
|
} else if (stylesheets.length > 1) {
|
|
1329
1378
|
let pidS = 1;
|
|
1330
1379
|
for (const p of stylesheets) {
|
|
1331
1380
|
if (pidS > 1 && !styleContent.endsWith("\n")) styleContent += "\n";
|
|
1332
|
-
styleContent += `/* twine-user-stylesheet #${pidS}: "${p.name}" */
|
|
1381
|
+
styleContent += `/* twine-user-stylesheet #${pidS}: "${commentSanitize(p.name)}" */
|
|
1333
1382
|
`;
|
|
1334
1383
|
styleContent += p.text;
|
|
1335
1384
|
pidS++;
|
|
@@ -1338,12 +1387,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1338
1387
|
parts.push(`<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">${styleContent}</style>`);
|
|
1339
1388
|
let scriptContent = "";
|
|
1340
1389
|
if (scripts.length === 1) {
|
|
1341
|
-
scriptContent = scripts[0]
|
|
1390
|
+
scriptContent = scripts[0]?.text ?? "";
|
|
1342
1391
|
} else if (scripts.length > 1) {
|
|
1343
1392
|
let pidS = 1;
|
|
1344
1393
|
for (const p of scripts) {
|
|
1345
1394
|
if (pidS > 1 && !scriptContent.endsWith("\n")) scriptContent += "\n";
|
|
1346
|
-
scriptContent += `/* twine-user-script #${pidS}: "${p.name}" */
|
|
1395
|
+
scriptContent += `/* twine-user-script #${pidS}: "${commentSanitize(p.name)}" */
|
|
1347
1396
|
`;
|
|
1348
1397
|
scriptContent += p.text;
|
|
1349
1398
|
pidS++;
|
|
@@ -1372,32 +1421,32 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1372
1421
|
if (val) opts.push(opt);
|
|
1373
1422
|
}
|
|
1374
1423
|
const options = opts.join(" ");
|
|
1375
|
-
const zoom =
|
|
1376
|
-
const wrapper = `<!-- UUID://${story.ifid}// --><tw-storydata name="${attrEscape(story.name)}" startnode="${startID}" creator="${attrEscape(CREATOR_NAME)}" creator-version="${attrEscape(
|
|
1424
|
+
const zoom = String(story.twine2.zoom);
|
|
1425
|
+
const wrapper = `<!-- UUID://${htmlCommentSanitize(story.ifid)}// --><tw-storydata name="${attrEscape(story.name)}" startnode="${attrEscape(startID)}" creator="${attrEscape(CREATOR_NAME)}" creator-version="${attrEscape(VERSION)}" ifid="${attrEscape(story.ifid)}" zoom="${attrEscape(zoom)}" format="${attrEscape(story.twine2.format)}" format-version="${attrEscape(story.twine2.formatVersion)}" options="${attrEscape(options)}" hidden>`;
|
|
1377
1426
|
return wrapper + parts.join("") + "</tw-storydata>";
|
|
1378
1427
|
}
|
|
1379
1428
|
function ensureIFID(story, diagnostics) {
|
|
1380
1429
|
if (story.ifid !== "") return;
|
|
1381
1430
|
if (story.legacyIFID !== "") {
|
|
1431
|
+
story.ifid = story.legacyIFID;
|
|
1382
1432
|
diagnostics.push({
|
|
1383
1433
|
level: "warning",
|
|
1384
1434
|
message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
|
|
1385
1435
|
});
|
|
1386
|
-
story.ifid = story.legacyIFID;
|
|
1387
1436
|
} else {
|
|
1388
|
-
|
|
1437
|
+
const ifid = generateIFID();
|
|
1438
|
+
story.ifid = ifid;
|
|
1389
1439
|
diagnostics.push({
|
|
1390
|
-
level: "
|
|
1391
|
-
message: `Story IFID not found
|
|
1440
|
+
level: "error",
|
|
1441
|
+
message: `Story IFID not found. Add a StoryData passage with: {"ifid":"${ifid}"}`
|
|
1392
1442
|
});
|
|
1393
1443
|
}
|
|
1394
1444
|
}
|
|
1395
1445
|
|
|
1396
1446
|
// src/output-twine1.ts
|
|
1397
|
-
import { readFileSync as
|
|
1447
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1398
1448
|
import { join as join3, dirname } from "path";
|
|
1399
1449
|
var CREATOR_NAME2 = "twee-ts";
|
|
1400
|
-
var CREATOR_VERSION2 = "0.1.0";
|
|
1401
1450
|
function toTwine1Archive(story, _startName) {
|
|
1402
1451
|
const { data, count } = getTwine1PassageChunk(story);
|
|
1403
1452
|
return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
|
|
@@ -1419,26 +1468,27 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
|
|
|
1419
1468
|
template = tryReplaceComponent(template, '"MODERNIZR"', join3(parentDir, "modernizr.js"), true);
|
|
1420
1469
|
}
|
|
1421
1470
|
const displayStart = startName === "Start" ? "" : startName;
|
|
1422
|
-
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${
|
|
1471
|
+
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
|
|
1423
1472
|
template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
1424
|
-
template = template.replace('"START_AT"', `"${displayStart}"`);
|
|
1473
|
+
template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
|
|
1425
1474
|
template = template.replace('"STORY_SIZE"', `"${count}"`);
|
|
1426
1475
|
if (template.includes('"STORY"')) {
|
|
1427
1476
|
template = template.replace('"STORY"', data);
|
|
1428
1477
|
} else {
|
|
1429
1478
|
let footer;
|
|
1430
1479
|
try {
|
|
1431
|
-
footer =
|
|
1480
|
+
footer = readFileSync2(join3(formatDir, "footer.html"), "utf-8");
|
|
1432
1481
|
} catch {
|
|
1433
1482
|
footer = "</div>\n</body>\n</html>\n";
|
|
1434
1483
|
}
|
|
1435
1484
|
template += data + footer;
|
|
1436
1485
|
}
|
|
1437
1486
|
if (story.ifid) {
|
|
1487
|
+
const safeIfid = htmlCommentSanitize(story.ifid);
|
|
1438
1488
|
if (template.includes('<div id="store-area"')) {
|
|
1439
|
-
template = template.replace('<div id="store-area"', `<!-- UUID://${
|
|
1489
|
+
template = template.replace('<div id="store-area"', `<!-- UUID://${safeIfid}// --><div id="store-area"`);
|
|
1440
1490
|
} else {
|
|
1441
|
-
template = template.replace('<div id="storeArea"', `<!-- UUID://${
|
|
1491
|
+
template = template.replace('<div id="storeArea"', `<!-- UUID://${safeIfid}// --><div id="storeArea"`);
|
|
1442
1492
|
}
|
|
1443
1493
|
}
|
|
1444
1494
|
return template;
|
|
@@ -1453,13 +1503,18 @@ function getTwine1PassageChunk(story) {
|
|
|
1453
1503
|
}
|
|
1454
1504
|
return { data, count };
|
|
1455
1505
|
}
|
|
1456
|
-
function tryReplaceComponent(template, placeholder, componentPath,
|
|
1506
|
+
function tryReplaceComponent(template, placeholder, componentPath, required) {
|
|
1457
1507
|
if (!template.includes(placeholder)) return template;
|
|
1458
1508
|
try {
|
|
1459
|
-
let content =
|
|
1509
|
+
let content = readFileSync2(componentPath, "utf-8");
|
|
1460
1510
|
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1461
1511
|
return template.replace(placeholder, content);
|
|
1462
|
-
} catch {
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
if (required) {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`Required format component not found: ${componentPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1463
1518
|
return template;
|
|
1464
1519
|
}
|
|
1465
1520
|
}
|
|
@@ -1474,16 +1529,6 @@ function toTwee(story, outMode) {
|
|
|
1474
1529
|
}
|
|
1475
1530
|
|
|
1476
1531
|
// src/modules.ts
|
|
1477
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1478
|
-
import { basename as basename2 } from "path";
|
|
1479
|
-
function readUTF83(filename) {
|
|
1480
|
-
let content = readFileSync4(filename, "utf-8");
|
|
1481
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1482
|
-
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1483
|
-
}
|
|
1484
|
-
function readBase642(filename) {
|
|
1485
|
-
return readFileSync4(filename).toString("base64");
|
|
1486
|
-
}
|
|
1487
1532
|
function loadModules(filenames) {
|
|
1488
1533
|
const processed = /* @__PURE__ */ new Set();
|
|
1489
1534
|
const headTags = [];
|
|
@@ -1513,16 +1558,16 @@ function loadModules(filenames) {
|
|
|
1513
1558
|
return headTags.join("\n");
|
|
1514
1559
|
}
|
|
1515
1560
|
function loadModuleTagged(tag, filename) {
|
|
1516
|
-
const source =
|
|
1561
|
+
const source = readUTF8(filename).trim();
|
|
1517
1562
|
if (source.length === 0) return null;
|
|
1518
|
-
const family =
|
|
1563
|
+
const family = baseNameWithoutExt(filename);
|
|
1519
1564
|
const idSlug = `${tag}-module-${slugify(family)}`;
|
|
1520
1565
|
const mimeType = tag === "script" ? "text/javascript" : "text/css";
|
|
1521
1566
|
return `<${tag} id="${idSlug}" type="${mimeType}">${source}</${tag}>`;
|
|
1522
1567
|
}
|
|
1523
1568
|
function loadModuleFont(filename) {
|
|
1524
|
-
const source =
|
|
1525
|
-
const family =
|
|
1569
|
+
const source = readBase64(filename);
|
|
1570
|
+
const family = baseNameWithoutExt(filename);
|
|
1526
1571
|
const idSlug = `style-module-${slugify(family)}`;
|
|
1527
1572
|
const ext = normalizedFileExt(filename);
|
|
1528
1573
|
const mediaType = mediaTypeFromExt(ext);
|
|
@@ -1532,7 +1577,7 @@ function loadModuleFont(filename) {
|
|
|
1532
1577
|
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1533
1578
|
}</style>`;
|
|
1534
1579
|
}
|
|
1535
|
-
function modifyHead(html, modulePaths, headFile) {
|
|
1580
|
+
function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
1536
1581
|
const parts = [];
|
|
1537
1582
|
if (modulePaths.length > 0) {
|
|
1538
1583
|
const modules = loadModules(modulePaths).trim();
|
|
@@ -1540,9 +1585,13 @@ function modifyHead(html, modulePaths, headFile) {
|
|
|
1540
1585
|
}
|
|
1541
1586
|
if (headFile) {
|
|
1542
1587
|
try {
|
|
1543
|
-
const source =
|
|
1588
|
+
const source = readUTF8(headFile).trim();
|
|
1544
1589
|
if (source.length > 0) parts.push(source);
|
|
1545
|
-
} catch {
|
|
1590
|
+
} catch (e) {
|
|
1591
|
+
diagnostics?.push({
|
|
1592
|
+
level: "warning",
|
|
1593
|
+
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
1594
|
+
});
|
|
1546
1595
|
}
|
|
1547
1596
|
}
|
|
1548
1597
|
if (parts.length > 0) {
|
|
@@ -1553,7 +1602,7 @@ function modifyHead(html, modulePaths, headFile) {
|
|
|
1553
1602
|
}
|
|
1554
1603
|
|
|
1555
1604
|
// src/remote-formats.ts
|
|
1556
|
-
import { mkdirSync, readFileSync as
|
|
1605
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1557
1606
|
import { join as join4 } from "path";
|
|
1558
1607
|
import { homedir as homedir2 } from "os";
|
|
1559
1608
|
var DEFAULT_SFA_INDICES = [
|
|
@@ -1569,6 +1618,24 @@ var indexCache = /* @__PURE__ */ new Map();
|
|
|
1569
1618
|
function clearIndexCache() {
|
|
1570
1619
|
indexCache.clear();
|
|
1571
1620
|
}
|
|
1621
|
+
function isValidEntry(val) {
|
|
1622
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) return false;
|
|
1623
|
+
const obj = val;
|
|
1624
|
+
return typeof obj.name === "string" && typeof obj.version === "string" && typeof obj.checksums === "object" && obj.checksums !== null && !Array.isArray(obj.checksums);
|
|
1625
|
+
}
|
|
1626
|
+
function validateSFAIndex(json) {
|
|
1627
|
+
if (typeof json !== "object" || json === null) {
|
|
1628
|
+
throw new Error("SFA index is not an object");
|
|
1629
|
+
}
|
|
1630
|
+
const obj = json;
|
|
1631
|
+
const twine1 = Array.isArray(obj["twine1"]) ? obj["twine1"].filter(isValidEntry) : [];
|
|
1632
|
+
const twine2 = Array.isArray(obj["twine2"]) ? obj["twine2"].filter(isValidEntry) : [];
|
|
1633
|
+
return { twine1, twine2 };
|
|
1634
|
+
}
|
|
1635
|
+
function makeFormatId(name, version) {
|
|
1636
|
+
const major = parseSemver(version)?.[0] ?? "0";
|
|
1637
|
+
return `${name.toLowerCase().replace(/\s+/g, "-")}-${major}`;
|
|
1638
|
+
}
|
|
1572
1639
|
async function fetchIndex(url) {
|
|
1573
1640
|
const cached = indexCache.get(url);
|
|
1574
1641
|
if (cached) return cached;
|
|
@@ -1576,37 +1643,42 @@ async function fetchIndex(url) {
|
|
|
1576
1643
|
if (!res.ok) {
|
|
1577
1644
|
throw new Error(`Failed to fetch format index from ${url}: ${res.status} ${res.statusText}`);
|
|
1578
1645
|
}
|
|
1579
|
-
const data = await res.json();
|
|
1646
|
+
const data = validateSFAIndex(await res.json());
|
|
1580
1647
|
indexCache.set(url, data);
|
|
1581
1648
|
return data;
|
|
1582
1649
|
}
|
|
1583
1650
|
function findEntry(index, name, version) {
|
|
1584
1651
|
const wanted = parseSemver(version);
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1652
|
+
const searchArrays = [
|
|
1653
|
+
{ entries: index.twine2 ?? [], formatType: "twine2" },
|
|
1654
|
+
{ entries: index.twine1 ?? [], formatType: "twine1" }
|
|
1655
|
+
];
|
|
1656
|
+
let bestResult;
|
|
1587
1657
|
let bestVersion = null;
|
|
1588
|
-
for (const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
if (
|
|
1595
|
-
bestVersion
|
|
1596
|
-
|
|
1658
|
+
for (const { entries, formatType } of searchArrays) {
|
|
1659
|
+
for (const entry of entries) {
|
|
1660
|
+
if (entry.name.toLowerCase() !== name.toLowerCase()) continue;
|
|
1661
|
+
const have = parseSemver(entry.version);
|
|
1662
|
+
if (!have) continue;
|
|
1663
|
+
if (wanted && semverCompare(have, wanted) === 0) return { entry, formatType };
|
|
1664
|
+
if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
|
|
1665
|
+
if (!bestVersion || semverCompare(have, bestVersion) > 0) {
|
|
1666
|
+
bestVersion = have;
|
|
1667
|
+
bestResult = { entry, formatType };
|
|
1668
|
+
}
|
|
1597
1669
|
}
|
|
1598
1670
|
}
|
|
1599
1671
|
}
|
|
1600
|
-
return
|
|
1672
|
+
return bestResult;
|
|
1601
1673
|
}
|
|
1602
1674
|
async function verifySHA256(content, expectedHex) {
|
|
1603
1675
|
const digest = await crypto.subtle.digest("SHA-256", content);
|
|
1604
1676
|
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1605
1677
|
return hex === expectedHex.toLowerCase();
|
|
1606
1678
|
}
|
|
1607
|
-
function getDownloadUrl(indexUrl, entry) {
|
|
1679
|
+
function getDownloadUrl(indexUrl, entry, formatType) {
|
|
1608
1680
|
const base = indexUrl.replace(/\/index\.json$/, "");
|
|
1609
|
-
return `${base}
|
|
1681
|
+
return `${base}/${formatType}/${entry.name}/${entry.version}/format.js`;
|
|
1610
1682
|
}
|
|
1611
1683
|
async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
1612
1684
|
const res = await fetch(downloadUrl);
|
|
@@ -1617,13 +1689,14 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
|
1617
1689
|
const checksumKey = Object.keys(entry.checksums ?? {}).find((k) => k.endsWith("format.js"));
|
|
1618
1690
|
if (checksumKey) {
|
|
1619
1691
|
const expected = entry.checksums[checksumKey];
|
|
1692
|
+
if (!expected) throw new Error(`Missing checksum value for key "${checksumKey}"`);
|
|
1620
1693
|
const encoder = new TextEncoder();
|
|
1621
1694
|
const valid = await verifySHA256(encoder.encode(text), expected);
|
|
1622
1695
|
if (!valid) {
|
|
1623
1696
|
throw new Error(`Checksum verification failed for ${entry.name} ${entry.version}`);
|
|
1624
1697
|
}
|
|
1625
1698
|
}
|
|
1626
|
-
const id =
|
|
1699
|
+
const id = makeFormatId(entry.name, entry.version);
|
|
1627
1700
|
const data = parseFormatJSON(text, id);
|
|
1628
1701
|
if (!data) {
|
|
1629
1702
|
throw new Error(`Failed to parse format JSON from ${downloadUrl}`);
|
|
@@ -1653,7 +1726,7 @@ async function fetchDirectFormat(url) {
|
|
|
1653
1726
|
if (!data) {
|
|
1654
1727
|
throw new Error(`Failed to parse format JSON from ${url}`);
|
|
1655
1728
|
}
|
|
1656
|
-
const id =
|
|
1729
|
+
const id = makeFormatId(data.name, data.version);
|
|
1657
1730
|
const cacheDir = getCacheDir();
|
|
1658
1731
|
const formatDir = join4(cacheDir, data.name, data.version);
|
|
1659
1732
|
mkdirSync(formatDir, { recursive: true });
|
|
@@ -1669,6 +1742,7 @@ async function fetchDirectFormat(url) {
|
|
|
1669
1742
|
};
|
|
1670
1743
|
}
|
|
1671
1744
|
async function resolveRemoteFormat(name, version, indices, urls) {
|
|
1745
|
+
let lastError;
|
|
1672
1746
|
if (urls) {
|
|
1673
1747
|
for (const url of urls) {
|
|
1674
1748
|
try {
|
|
@@ -1680,7 +1754,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
|
|
|
1680
1754
|
return info;
|
|
1681
1755
|
}
|
|
1682
1756
|
}
|
|
1683
|
-
} catch {
|
|
1757
|
+
} catch (e) {
|
|
1758
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1684
1759
|
}
|
|
1685
1760
|
}
|
|
1686
1761
|
}
|
|
@@ -1688,24 +1763,26 @@ async function resolveRemoteFormat(name, version, indices, urls) {
|
|
|
1688
1763
|
for (const indexUrl of allIndices) {
|
|
1689
1764
|
try {
|
|
1690
1765
|
const index = await fetchIndex(indexUrl);
|
|
1691
|
-
const
|
|
1692
|
-
if (
|
|
1693
|
-
const cached = getCachedFormat(entry.name, entry.version);
|
|
1766
|
+
const result = findEntry(index, name, version);
|
|
1767
|
+
if (result) {
|
|
1768
|
+
const cached = getCachedFormat(result.entry.name, result.entry.version);
|
|
1694
1769
|
if (cached) return cached;
|
|
1695
|
-
const downloadUrl = getDownloadUrl(indexUrl, entry);
|
|
1696
|
-
return await fetchAndCacheFormat(entry, downloadUrl);
|
|
1770
|
+
const downloadUrl = getDownloadUrl(indexUrl, result.entry, result.formatType);
|
|
1771
|
+
return await fetchAndCacheFormat(result.entry, downloadUrl);
|
|
1697
1772
|
}
|
|
1698
|
-
} catch {
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1699
1775
|
}
|
|
1700
1776
|
}
|
|
1777
|
+
if (lastError) throw lastError;
|
|
1701
1778
|
return void 0;
|
|
1702
1779
|
}
|
|
1703
1780
|
function getCachedFormat(name, version) {
|
|
1704
1781
|
const formatPath = join4(getCacheDir(), name, version, "format.js");
|
|
1705
1782
|
try {
|
|
1706
1783
|
if (!existsSync(formatPath)) return void 0;
|
|
1707
|
-
const source =
|
|
1708
|
-
const id =
|
|
1784
|
+
const source = readFileSync3(formatPath, "utf-8");
|
|
1785
|
+
const id = makeFormatId(name, version);
|
|
1709
1786
|
const data = parseFormatJSON(source, id);
|
|
1710
1787
|
if (!data) return void 0;
|
|
1711
1788
|
return {
|
|
@@ -1758,6 +1835,7 @@ function discoverCachedFormats() {
|
|
|
1758
1835
|
}
|
|
1759
1836
|
|
|
1760
1837
|
// src/compiler.ts
|
|
1838
|
+
var CREATOR_NAME3 = "Twee-ts";
|
|
1761
1839
|
var DEFAULT_FORMAT_ID = "sugarcube-2";
|
|
1762
1840
|
var DEFAULT_START_NAME = "Start";
|
|
1763
1841
|
var TweeTsError = class extends Error {
|
|
@@ -1814,10 +1892,7 @@ async function buildOutput(options) {
|
|
|
1814
1892
|
loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
|
|
1815
1893
|
loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
|
|
1816
1894
|
if (options.tagAliases) {
|
|
1817
|
-
applyTagAliases(story.passages, options.tagAliases);
|
|
1818
|
-
}
|
|
1819
|
-
const fatalErrors = diagnostics.filter((d) => d.level === "error");
|
|
1820
|
-
if (fatalErrors.length > 0 && outputMode === "html") {
|
|
1895
|
+
story.passages = applyTagAliases(story.passages, options.tagAliases);
|
|
1821
1896
|
}
|
|
1822
1897
|
let format;
|
|
1823
1898
|
let formatId = options.formatId ?? "";
|
|
@@ -1836,7 +1911,11 @@ async function buildOutput(options) {
|
|
|
1836
1911
|
const remoteVersion = story.twine2.formatVersion || "";
|
|
1837
1912
|
try {
|
|
1838
1913
|
format = await resolveRemoteFormat(remoteName, remoteVersion, options.formatIndices, options.formatUrls);
|
|
1839
|
-
} catch {
|
|
1914
|
+
} catch (e) {
|
|
1915
|
+
diagnostics.push({
|
|
1916
|
+
level: "warning",
|
|
1917
|
+
message: `Remote format fetch failed for "${remoteName}": ${e instanceof Error ? e.message : String(e)}`
|
|
1918
|
+
});
|
|
1840
1919
|
}
|
|
1841
1920
|
}
|
|
1842
1921
|
if (!format) {
|
|
@@ -1847,14 +1926,7 @@ async function buildOutput(options) {
|
|
|
1847
1926
|
});
|
|
1848
1927
|
}
|
|
1849
1928
|
}
|
|
1850
|
-
|
|
1851
|
-
if (options.startPassage) {
|
|
1852
|
-
startName = options.startPassage;
|
|
1853
|
-
} else if (story.twine2.start) {
|
|
1854
|
-
startName = story.twine2.start;
|
|
1855
|
-
} else {
|
|
1856
|
-
startName = DEFAULT_START_NAME;
|
|
1857
|
-
}
|
|
1929
|
+
const startName = options.startPassage || story.twine2.start || DEFAULT_START_NAME;
|
|
1858
1930
|
if (testMode) {
|
|
1859
1931
|
story.twine2.options.set("debug", true);
|
|
1860
1932
|
}
|
|
@@ -1873,8 +1945,7 @@ async function buildOutput(options) {
|
|
|
1873
1945
|
case "json":
|
|
1874
1946
|
output = storyToJSON(story);
|
|
1875
1947
|
break;
|
|
1876
|
-
case "html":
|
|
1877
|
-
default: {
|
|
1948
|
+
case "html": {
|
|
1878
1949
|
if (!storyHas(story, startName)) {
|
|
1879
1950
|
diagnostics.push({
|
|
1880
1951
|
level: "error",
|
|
@@ -1896,9 +1967,13 @@ async function buildOutput(options) {
|
|
|
1896
1967
|
output = toTwine1HTML(story, format, startName, diagnostics);
|
|
1897
1968
|
}
|
|
1898
1969
|
const modulePaths = options.modules ? getFilenames(options.modules) : [];
|
|
1899
|
-
output = modifyHead(output, modulePaths, options.headFile);
|
|
1970
|
+
output = modifyHead(output, modulePaths, options.headFile, diagnostics);
|
|
1900
1971
|
break;
|
|
1901
1972
|
}
|
|
1973
|
+
default: {
|
|
1974
|
+
const _exhaustive = outputMode;
|
|
1975
|
+
throw new TweeTsError(`Unhandled output mode: ${_exhaustive}`, diagnostics);
|
|
1976
|
+
}
|
|
1902
1977
|
}
|
|
1903
1978
|
const stats = {
|
|
1904
1979
|
...getStoryStats(story),
|
|
@@ -1907,28 +1982,46 @@ async function buildOutput(options) {
|
|
|
1907
1982
|
return { output, story, format, diagnostics, stats };
|
|
1908
1983
|
}
|
|
1909
1984
|
function storyToJSON(story) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1985
|
+
const scripts = [];
|
|
1986
|
+
const stylesheets = [];
|
|
1987
|
+
const storyPassages = [];
|
|
1988
|
+
for (const p of story.passages) {
|
|
1989
|
+
if (p.name === "StoryTitle" || p.name === "StoryData") continue;
|
|
1990
|
+
if (hasTag(p, "Twine.private")) continue;
|
|
1991
|
+
if (hasTag(p, "script")) {
|
|
1992
|
+
scripts.push(p.text);
|
|
1993
|
+
} else if (hasTag(p, "stylesheet")) {
|
|
1994
|
+
stylesheets.push(p.text);
|
|
1995
|
+
} else {
|
|
1996
|
+
const entry = {
|
|
1915
1997
|
name: p.name,
|
|
1916
1998
|
tags: p.tags,
|
|
1917
|
-
text: p.text
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
tagColors: Object.fromEntries(story.twine2.tagColors),
|
|
1925
|
-
options: Object.fromEntries(story.twine2.options),
|
|
1926
|
-
zoom: story.twine2.zoom
|
|
1999
|
+
text: p.text
|
|
2000
|
+
};
|
|
2001
|
+
if (p.metadata && (p.metadata.position || p.metadata.size)) {
|
|
2002
|
+
const meta = {};
|
|
2003
|
+
if (p.metadata.position) meta.position = p.metadata.position;
|
|
2004
|
+
if (p.metadata.size) meta.size = p.metadata.size;
|
|
2005
|
+
entry.metadata = meta;
|
|
1927
2006
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2007
|
+
storyPassages.push(entry);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const obj = {
|
|
2011
|
+
name: story.name
|
|
2012
|
+
};
|
|
2013
|
+
if (story.ifid) obj.ifid = story.ifid;
|
|
2014
|
+
if (story.twine2.format) obj.format = story.twine2.format;
|
|
2015
|
+
if (story.twine2.formatVersion) obj["format-version"] = story.twine2.formatVersion;
|
|
2016
|
+
if (story.twine2.start) obj.start = story.twine2.start;
|
|
2017
|
+
if (story.twine2.tagColors.size > 0) obj["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
|
|
2018
|
+
if (story.twine2.zoom !== 1) obj.zoom = story.twine2.zoom;
|
|
2019
|
+
obj.creator = CREATOR_NAME3;
|
|
2020
|
+
obj["creator-version"] = VERSION;
|
|
2021
|
+
obj.style = stylesheets.join("\n");
|
|
2022
|
+
obj.script = scripts.join("\n");
|
|
2023
|
+
obj.passages = storyPassages;
|
|
2024
|
+
return JSON.stringify(obj, null, 2);
|
|
1932
2025
|
}
|
|
1933
2026
|
|
|
1934
2027
|
// src/inspect.ts
|
|
@@ -2034,7 +2127,7 @@ function storyInspect(story) {
|
|
|
2034
2127
|
}
|
|
2035
2128
|
|
|
2036
2129
|
// src/config.ts
|
|
2037
|
-
import { readFileSync as
|
|
2130
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
2038
2131
|
import { join as join5 } from "path";
|
|
2039
2132
|
var CONFIG_FILENAME = "twee-ts.config.json";
|
|
2040
2133
|
var VALID_OUTPUT_MODES = ["html", "twee3", "twee1", "twine2-archive", "twine1-archive", "json"];
|
|
@@ -2044,7 +2137,7 @@ function loadConfig(dir) {
|
|
|
2044
2137
|
if (!existsSync2(configPath)) return null;
|
|
2045
2138
|
let raw;
|
|
2046
2139
|
try {
|
|
2047
|
-
raw =
|
|
2140
|
+
raw = readFileSync4(configPath, "utf-8");
|
|
2048
2141
|
} catch {
|
|
2049
2142
|
return null;
|
|
2050
2143
|
}
|
|
@@ -2064,7 +2157,7 @@ function loadConfig(dir) {
|
|
|
2064
2157
|
function loadConfigFile(filePath) {
|
|
2065
2158
|
let raw;
|
|
2066
2159
|
try {
|
|
2067
|
-
raw =
|
|
2160
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
2068
2161
|
} catch (e) {
|
|
2069
2162
|
throw new Error(`Cannot read config file ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2070
2163
|
}
|