@rohal12/twee-ts 1.1.1 → 1.2.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/dist/bin/twee-ts.js +266 -184
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/{chunk-QPA7RLK5.js → chunk-S4SWCQLW.js} +253 -174
- package/dist/chunk-S4SWCQLW.js.map +1 -0
- package/dist/index.cjs +286 -209
- 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 +255 -178
- 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 +5 -2
- package/dist/chunk-QPA7RLK5.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,38 +1304,48 @@ 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 =
|
|
1266
|
-
storyAdd(
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1312
|
+
const name = baseNameWithoutExt(filename);
|
|
1313
|
+
storyAdd(
|
|
1314
|
+
story,
|
|
1315
|
+
{
|
|
1316
|
+
name,
|
|
1317
|
+
tags: [tag],
|
|
1318
|
+
text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
|
|
1319
|
+
},
|
|
1320
|
+
diagnostics
|
|
1321
|
+
);
|
|
1271
1322
|
}
|
|
1272
1323
|
function loadFont(story, filename, diagnostics) {
|
|
1273
1324
|
const source = readBase64(filename);
|
|
1274
1325
|
const name = basename(filename);
|
|
1275
|
-
const family =
|
|
1326
|
+
const family = baseNameWithoutExt(filename);
|
|
1276
1327
|
const ext = normalizedFileExt(filename);
|
|
1277
1328
|
const mediaType = mediaTypeFromExt(ext);
|
|
1278
1329
|
const hint = fontFormatHint(ext);
|
|
1279
|
-
storyAdd(
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1330
|
+
storyAdd(
|
|
1331
|
+
story,
|
|
1332
|
+
{
|
|
1333
|
+
name,
|
|
1334
|
+
tags: ["stylesheet"],
|
|
1335
|
+
text: `@font-face {
|
|
1283
1336
|
font-family: "${family}";
|
|
1284
1337
|
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1285
1338
|
}`
|
|
1286
|
-
|
|
1339
|
+
},
|
|
1340
|
+
diagnostics
|
|
1341
|
+
);
|
|
1287
1342
|
}
|
|
1288
1343
|
|
|
1344
|
+
// src/version.ts
|
|
1345
|
+
var VERSION = "1.2.0";
|
|
1346
|
+
|
|
1289
1347
|
// src/output-twine2.ts
|
|
1290
1348
|
var CREATOR_NAME = "Twee-ts";
|
|
1291
|
-
var CREATOR_VERSION = "0.1.0";
|
|
1292
1349
|
function toTwine2Archive(story, startName, diagnostics) {
|
|
1293
1350
|
return getTwine2DataChunk(story, startName, diagnostics) + "\n";
|
|
1294
1351
|
}
|
|
@@ -1316,12 +1373,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1316
1373
|
}
|
|
1317
1374
|
let styleContent = "";
|
|
1318
1375
|
if (stylesheets.length === 1) {
|
|
1319
|
-
styleContent = stylesheets[0]
|
|
1376
|
+
styleContent = stylesheets[0]?.text ?? "";
|
|
1320
1377
|
} else if (stylesheets.length > 1) {
|
|
1321
1378
|
let pidS = 1;
|
|
1322
1379
|
for (const p of stylesheets) {
|
|
1323
1380
|
if (pidS > 1 && !styleContent.endsWith("\n")) styleContent += "\n";
|
|
1324
|
-
styleContent += `/* twine-user-stylesheet #${pidS}: "${p.name}" */
|
|
1381
|
+
styleContent += `/* twine-user-stylesheet #${pidS}: "${commentSanitize(p.name)}" */
|
|
1325
1382
|
`;
|
|
1326
1383
|
styleContent += p.text;
|
|
1327
1384
|
pidS++;
|
|
@@ -1330,12 +1387,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1330
1387
|
parts.push(`<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">${styleContent}</style>`);
|
|
1331
1388
|
let scriptContent = "";
|
|
1332
1389
|
if (scripts.length === 1) {
|
|
1333
|
-
scriptContent = scripts[0]
|
|
1390
|
+
scriptContent = scripts[0]?.text ?? "";
|
|
1334
1391
|
} else if (scripts.length > 1) {
|
|
1335
1392
|
let pidS = 1;
|
|
1336
1393
|
for (const p of scripts) {
|
|
1337
1394
|
if (pidS > 1 && !scriptContent.endsWith("\n")) scriptContent += "\n";
|
|
1338
|
-
scriptContent += `/* twine-user-script #${pidS}: "${p.name}" */
|
|
1395
|
+
scriptContent += `/* twine-user-script #${pidS}: "${commentSanitize(p.name)}" */
|
|
1339
1396
|
`;
|
|
1340
1397
|
scriptContent += p.text;
|
|
1341
1398
|
pidS++;
|
|
@@ -1365,7 +1422,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1365
1422
|
}
|
|
1366
1423
|
const options = opts.join(" ");
|
|
1367
1424
|
const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
|
|
1368
|
-
const wrapper = `<!-- UUID://${story.ifid}// --><tw-storydata name="${attrEscape(story.name)}" startnode="${startID}" creator="${attrEscape(CREATOR_NAME)}" creator-version="${attrEscape(
|
|
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>`;
|
|
1369
1426
|
return wrapper + parts.join("") + "</tw-storydata>";
|
|
1370
1427
|
}
|
|
1371
1428
|
function ensureIFID(story, diagnostics) {
|
|
@@ -1386,10 +1443,9 @@ function ensureIFID(story, diagnostics) {
|
|
|
1386
1443
|
}
|
|
1387
1444
|
|
|
1388
1445
|
// src/output-twine1.ts
|
|
1389
|
-
import { readFileSync as
|
|
1446
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1390
1447
|
import { join as join3, dirname } from "path";
|
|
1391
1448
|
var CREATOR_NAME2 = "twee-ts";
|
|
1392
|
-
var CREATOR_VERSION2 = "0.1.0";
|
|
1393
1449
|
function toTwine1Archive(story, _startName) {
|
|
1394
1450
|
const { data, count } = getTwine1PassageChunk(story);
|
|
1395
1451
|
return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
|
|
@@ -1411,26 +1467,27 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
|
|
|
1411
1467
|
template = tryReplaceComponent(template, '"MODERNIZR"', join3(parentDir, "modernizr.js"), true);
|
|
1412
1468
|
}
|
|
1413
1469
|
const displayStart = startName === "Start" ? "" : startName;
|
|
1414
|
-
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${
|
|
1470
|
+
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
|
|
1415
1471
|
template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
1416
|
-
template = template.replace('"START_AT"', `"${displayStart}"`);
|
|
1472
|
+
template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
|
|
1417
1473
|
template = template.replace('"STORY_SIZE"', `"${count}"`);
|
|
1418
1474
|
if (template.includes('"STORY"')) {
|
|
1419
1475
|
template = template.replace('"STORY"', data);
|
|
1420
1476
|
} else {
|
|
1421
1477
|
let footer;
|
|
1422
1478
|
try {
|
|
1423
|
-
footer =
|
|
1479
|
+
footer = readFileSync2(join3(formatDir, "footer.html"), "utf-8");
|
|
1424
1480
|
} catch {
|
|
1425
1481
|
footer = "</div>\n</body>\n</html>\n";
|
|
1426
1482
|
}
|
|
1427
1483
|
template += data + footer;
|
|
1428
1484
|
}
|
|
1429
1485
|
if (story.ifid) {
|
|
1486
|
+
const safeIfid = htmlCommentSanitize(story.ifid);
|
|
1430
1487
|
if (template.includes('<div id="store-area"')) {
|
|
1431
|
-
template = template.replace('<div id="store-area"', `<!-- UUID://${
|
|
1488
|
+
template = template.replace('<div id="store-area"', `<!-- UUID://${safeIfid}// --><div id="store-area"`);
|
|
1432
1489
|
} else {
|
|
1433
|
-
template = template.replace('<div id="storeArea"', `<!-- UUID://${
|
|
1490
|
+
template = template.replace('<div id="storeArea"', `<!-- UUID://${safeIfid}// --><div id="storeArea"`);
|
|
1434
1491
|
}
|
|
1435
1492
|
}
|
|
1436
1493
|
return template;
|
|
@@ -1445,13 +1502,18 @@ function getTwine1PassageChunk(story) {
|
|
|
1445
1502
|
}
|
|
1446
1503
|
return { data, count };
|
|
1447
1504
|
}
|
|
1448
|
-
function tryReplaceComponent(template, placeholder, componentPath,
|
|
1505
|
+
function tryReplaceComponent(template, placeholder, componentPath, required) {
|
|
1449
1506
|
if (!template.includes(placeholder)) return template;
|
|
1450
1507
|
try {
|
|
1451
|
-
let content =
|
|
1508
|
+
let content = readFileSync2(componentPath, "utf-8");
|
|
1452
1509
|
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1453
1510
|
return template.replace(placeholder, content);
|
|
1454
|
-
} catch {
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
if (required) {
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`Required format component not found: ${componentPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1455
1517
|
return template;
|
|
1456
1518
|
}
|
|
1457
1519
|
}
|
|
@@ -1466,16 +1528,6 @@ function toTwee(story, outMode) {
|
|
|
1466
1528
|
}
|
|
1467
1529
|
|
|
1468
1530
|
// src/modules.ts
|
|
1469
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1470
|
-
import { basename as basename2 } from "path";
|
|
1471
|
-
function readUTF83(filename) {
|
|
1472
|
-
let content = readFileSync4(filename, "utf-8");
|
|
1473
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1474
|
-
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1475
|
-
}
|
|
1476
|
-
function readBase642(filename) {
|
|
1477
|
-
return readFileSync4(filename).toString("base64");
|
|
1478
|
-
}
|
|
1479
1531
|
function loadModules(filenames) {
|
|
1480
1532
|
const processed = /* @__PURE__ */ new Set();
|
|
1481
1533
|
const headTags = [];
|
|
@@ -1505,16 +1557,16 @@ function loadModules(filenames) {
|
|
|
1505
1557
|
return headTags.join("\n");
|
|
1506
1558
|
}
|
|
1507
1559
|
function loadModuleTagged(tag, filename) {
|
|
1508
|
-
const source =
|
|
1560
|
+
const source = readUTF8(filename).trim();
|
|
1509
1561
|
if (source.length === 0) return null;
|
|
1510
|
-
const family =
|
|
1562
|
+
const family = baseNameWithoutExt(filename);
|
|
1511
1563
|
const idSlug = `${tag}-module-${slugify(family)}`;
|
|
1512
1564
|
const mimeType = tag === "script" ? "text/javascript" : "text/css";
|
|
1513
1565
|
return `<${tag} id="${idSlug}" type="${mimeType}">${source}</${tag}>`;
|
|
1514
1566
|
}
|
|
1515
1567
|
function loadModuleFont(filename) {
|
|
1516
|
-
const source =
|
|
1517
|
-
const family =
|
|
1568
|
+
const source = readBase64(filename);
|
|
1569
|
+
const family = baseNameWithoutExt(filename);
|
|
1518
1570
|
const idSlug = `style-module-${slugify(family)}`;
|
|
1519
1571
|
const ext = normalizedFileExt(filename);
|
|
1520
1572
|
const mediaType = mediaTypeFromExt(ext);
|
|
@@ -1524,7 +1576,7 @@ function loadModuleFont(filename) {
|
|
|
1524
1576
|
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1525
1577
|
}</style>`;
|
|
1526
1578
|
}
|
|
1527
|
-
function modifyHead(html, modulePaths, headFile) {
|
|
1579
|
+
function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
1528
1580
|
const parts = [];
|
|
1529
1581
|
if (modulePaths.length > 0) {
|
|
1530
1582
|
const modules = loadModules(modulePaths).trim();
|
|
@@ -1532,9 +1584,13 @@ function modifyHead(html, modulePaths, headFile) {
|
|
|
1532
1584
|
}
|
|
1533
1585
|
if (headFile) {
|
|
1534
1586
|
try {
|
|
1535
|
-
const source =
|
|
1587
|
+
const source = readUTF8(headFile).trim();
|
|
1536
1588
|
if (source.length > 0) parts.push(source);
|
|
1537
|
-
} catch {
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
diagnostics?.push({
|
|
1591
|
+
level: "warning",
|
|
1592
|
+
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
1593
|
+
});
|
|
1538
1594
|
}
|
|
1539
1595
|
}
|
|
1540
1596
|
if (parts.length > 0) {
|
|
@@ -1545,7 +1601,7 @@ function modifyHead(html, modulePaths, headFile) {
|
|
|
1545
1601
|
}
|
|
1546
1602
|
|
|
1547
1603
|
// src/remote-formats.ts
|
|
1548
|
-
import { mkdirSync, readFileSync as
|
|
1604
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1549
1605
|
import { join as join4 } from "path";
|
|
1550
1606
|
import { homedir as homedir2 } from "os";
|
|
1551
1607
|
var DEFAULT_SFA_INDICES = [
|
|
@@ -1561,6 +1617,24 @@ var indexCache = /* @__PURE__ */ new Map();
|
|
|
1561
1617
|
function clearIndexCache() {
|
|
1562
1618
|
indexCache.clear();
|
|
1563
1619
|
}
|
|
1620
|
+
function isValidEntry(val) {
|
|
1621
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) return false;
|
|
1622
|
+
const obj = val;
|
|
1623
|
+
return typeof obj.name === "string" && typeof obj.version === "string" && typeof obj.checksums === "object" && obj.checksums !== null && !Array.isArray(obj.checksums);
|
|
1624
|
+
}
|
|
1625
|
+
function validateSFAIndex(json) {
|
|
1626
|
+
if (typeof json !== "object" || json === null) {
|
|
1627
|
+
throw new Error("SFA index is not an object");
|
|
1628
|
+
}
|
|
1629
|
+
const obj = json;
|
|
1630
|
+
const twine1 = Array.isArray(obj["twine1"]) ? obj["twine1"].filter(isValidEntry) : [];
|
|
1631
|
+
const twine2 = Array.isArray(obj["twine2"]) ? obj["twine2"].filter(isValidEntry) : [];
|
|
1632
|
+
return { twine1, twine2 };
|
|
1633
|
+
}
|
|
1634
|
+
function makeFormatId(name, version) {
|
|
1635
|
+
const major = parseSemver(version)?.[0] ?? "0";
|
|
1636
|
+
return `${name.toLowerCase().replace(/\s+/g, "-")}-${major}`;
|
|
1637
|
+
}
|
|
1564
1638
|
async function fetchIndex(url) {
|
|
1565
1639
|
const cached = indexCache.get(url);
|
|
1566
1640
|
if (cached) return cached;
|
|
@@ -1568,37 +1642,42 @@ async function fetchIndex(url) {
|
|
|
1568
1642
|
if (!res.ok) {
|
|
1569
1643
|
throw new Error(`Failed to fetch format index from ${url}: ${res.status} ${res.statusText}`);
|
|
1570
1644
|
}
|
|
1571
|
-
const data = await res.json();
|
|
1645
|
+
const data = validateSFAIndex(await res.json());
|
|
1572
1646
|
indexCache.set(url, data);
|
|
1573
1647
|
return data;
|
|
1574
1648
|
}
|
|
1575
1649
|
function findEntry(index, name, version) {
|
|
1576
1650
|
const wanted = parseSemver(version);
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1651
|
+
const searchArrays = [
|
|
1652
|
+
{ entries: index.twine2 ?? [], formatType: "twine2" },
|
|
1653
|
+
{ entries: index.twine1 ?? [], formatType: "twine1" }
|
|
1654
|
+
];
|
|
1655
|
+
let bestResult;
|
|
1579
1656
|
let bestVersion = null;
|
|
1580
|
-
for (const
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
if (
|
|
1587
|
-
bestVersion
|
|
1588
|
-
|
|
1657
|
+
for (const { entries, formatType } of searchArrays) {
|
|
1658
|
+
for (const entry of entries) {
|
|
1659
|
+
if (entry.name.toLowerCase() !== name.toLowerCase()) continue;
|
|
1660
|
+
const have = parseSemver(entry.version);
|
|
1661
|
+
if (!have) continue;
|
|
1662
|
+
if (wanted && semverCompare(have, wanted) === 0) return { entry, formatType };
|
|
1663
|
+
if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
|
|
1664
|
+
if (!bestVersion || semverCompare(have, bestVersion) > 0) {
|
|
1665
|
+
bestVersion = have;
|
|
1666
|
+
bestResult = { entry, formatType };
|
|
1667
|
+
}
|
|
1589
1668
|
}
|
|
1590
1669
|
}
|
|
1591
1670
|
}
|
|
1592
|
-
return
|
|
1671
|
+
return bestResult;
|
|
1593
1672
|
}
|
|
1594
1673
|
async function verifySHA256(content, expectedHex) {
|
|
1595
1674
|
const digest = await crypto.subtle.digest("SHA-256", content);
|
|
1596
1675
|
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1597
1676
|
return hex === expectedHex.toLowerCase();
|
|
1598
1677
|
}
|
|
1599
|
-
function getDownloadUrl(indexUrl, entry) {
|
|
1678
|
+
function getDownloadUrl(indexUrl, entry, formatType) {
|
|
1600
1679
|
const base = indexUrl.replace(/\/index\.json$/, "");
|
|
1601
|
-
return `${base}
|
|
1680
|
+
return `${base}/${formatType}/${entry.name}/${entry.version}/format.js`;
|
|
1602
1681
|
}
|
|
1603
1682
|
async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
1604
1683
|
const res = await fetch(downloadUrl);
|
|
@@ -1609,13 +1688,14 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
|
1609
1688
|
const checksumKey = Object.keys(entry.checksums ?? {}).find((k) => k.endsWith("format.js"));
|
|
1610
1689
|
if (checksumKey) {
|
|
1611
1690
|
const expected = entry.checksums[checksumKey];
|
|
1691
|
+
if (!expected) throw new Error(`Missing checksum value for key "${checksumKey}"`);
|
|
1612
1692
|
const encoder = new TextEncoder();
|
|
1613
1693
|
const valid = await verifySHA256(encoder.encode(text), expected);
|
|
1614
1694
|
if (!valid) {
|
|
1615
1695
|
throw new Error(`Checksum verification failed for ${entry.name} ${entry.version}`);
|
|
1616
1696
|
}
|
|
1617
1697
|
}
|
|
1618
|
-
const id =
|
|
1698
|
+
const id = makeFormatId(entry.name, entry.version);
|
|
1619
1699
|
const data = parseFormatJSON(text, id);
|
|
1620
1700
|
if (!data) {
|
|
1621
1701
|
throw new Error(`Failed to parse format JSON from ${downloadUrl}`);
|
|
@@ -1645,7 +1725,7 @@ async function fetchDirectFormat(url) {
|
|
|
1645
1725
|
if (!data) {
|
|
1646
1726
|
throw new Error(`Failed to parse format JSON from ${url}`);
|
|
1647
1727
|
}
|
|
1648
|
-
const id =
|
|
1728
|
+
const id = makeFormatId(data.name, data.version);
|
|
1649
1729
|
const cacheDir = getCacheDir();
|
|
1650
1730
|
const formatDir = join4(cacheDir, data.name, data.version);
|
|
1651
1731
|
mkdirSync(formatDir, { recursive: true });
|
|
@@ -1661,6 +1741,7 @@ async function fetchDirectFormat(url) {
|
|
|
1661
1741
|
};
|
|
1662
1742
|
}
|
|
1663
1743
|
async function resolveRemoteFormat(name, version, indices, urls) {
|
|
1744
|
+
let lastError;
|
|
1664
1745
|
if (urls) {
|
|
1665
1746
|
for (const url of urls) {
|
|
1666
1747
|
try {
|
|
@@ -1672,7 +1753,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
|
|
|
1672
1753
|
return info;
|
|
1673
1754
|
}
|
|
1674
1755
|
}
|
|
1675
|
-
} catch {
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1676
1758
|
}
|
|
1677
1759
|
}
|
|
1678
1760
|
}
|
|
@@ -1680,24 +1762,26 @@ async function resolveRemoteFormat(name, version, indices, urls) {
|
|
|
1680
1762
|
for (const indexUrl of allIndices) {
|
|
1681
1763
|
try {
|
|
1682
1764
|
const index = await fetchIndex(indexUrl);
|
|
1683
|
-
const
|
|
1684
|
-
if (
|
|
1685
|
-
const cached = getCachedFormat(entry.name, entry.version);
|
|
1765
|
+
const result = findEntry(index, name, version);
|
|
1766
|
+
if (result) {
|
|
1767
|
+
const cached = getCachedFormat(result.entry.name, result.entry.version);
|
|
1686
1768
|
if (cached) return cached;
|
|
1687
|
-
const downloadUrl = getDownloadUrl(indexUrl, entry);
|
|
1688
|
-
return await fetchAndCacheFormat(entry, downloadUrl);
|
|
1769
|
+
const downloadUrl = getDownloadUrl(indexUrl, result.entry, result.formatType);
|
|
1770
|
+
return await fetchAndCacheFormat(result.entry, downloadUrl);
|
|
1689
1771
|
}
|
|
1690
|
-
} catch {
|
|
1772
|
+
} catch (e) {
|
|
1773
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1691
1774
|
}
|
|
1692
1775
|
}
|
|
1776
|
+
if (lastError) throw lastError;
|
|
1693
1777
|
return void 0;
|
|
1694
1778
|
}
|
|
1695
1779
|
function getCachedFormat(name, version) {
|
|
1696
1780
|
const formatPath = join4(getCacheDir(), name, version, "format.js");
|
|
1697
1781
|
try {
|
|
1698
1782
|
if (!existsSync(formatPath)) return void 0;
|
|
1699
|
-
const source =
|
|
1700
|
-
const id =
|
|
1783
|
+
const source = readFileSync3(formatPath, "utf-8");
|
|
1784
|
+
const id = makeFormatId(name, version);
|
|
1701
1785
|
const data = parseFormatJSON(source, id);
|
|
1702
1786
|
if (!data) return void 0;
|
|
1703
1787
|
return {
|
|
@@ -1806,18 +1890,12 @@ async function buildOutput(options) {
|
|
|
1806
1890
|
loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
|
|
1807
1891
|
loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
|
|
1808
1892
|
if (options.tagAliases) {
|
|
1809
|
-
applyTagAliases(story.passages, options.tagAliases);
|
|
1810
|
-
}
|
|
1811
|
-
const fatalErrors = diagnostics.filter((d) => d.level === "error");
|
|
1812
|
-
if (fatalErrors.length > 0 && outputMode === "html") {
|
|
1893
|
+
story.passages = applyTagAliases(story.passages, options.tagAliases);
|
|
1813
1894
|
}
|
|
1814
1895
|
let format;
|
|
1815
1896
|
let formatId = options.formatId ?? "";
|
|
1816
1897
|
if (outputMode === "html") {
|
|
1817
|
-
const formatSearchDirs = getFormatSearchDirs(
|
|
1818
|
-
options.formatPaths,
|
|
1819
|
-
options.useTweegoPath ?? true
|
|
1820
|
-
);
|
|
1898
|
+
const formatSearchDirs = getFormatSearchDirs(options.formatPaths, options.useTweegoPath ?? true);
|
|
1821
1899
|
const formats = discoverFormats(formatSearchDirs);
|
|
1822
1900
|
if (!formatId && story.twine2.format) {
|
|
1823
1901
|
formatId = getFormatIdByNameAndVersion(formats, story.twine2.format, story.twine2.formatVersion) ?? "";
|
|
@@ -1830,13 +1908,12 @@ async function buildOutput(options) {
|
|
|
1830
1908
|
const remoteName = story.twine2.format || formatId;
|
|
1831
1909
|
const remoteVersion = story.twine2.formatVersion || "";
|
|
1832
1910
|
try {
|
|
1833
|
-
format = await resolveRemoteFormat(
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
);
|
|
1839
|
-
} catch {
|
|
1911
|
+
format = await resolveRemoteFormat(remoteName, remoteVersion, options.formatIndices, options.formatUrls);
|
|
1912
|
+
} catch (e) {
|
|
1913
|
+
diagnostics.push({
|
|
1914
|
+
level: "warning",
|
|
1915
|
+
message: `Remote format fetch failed for "${remoteName}": ${e instanceof Error ? e.message : String(e)}`
|
|
1916
|
+
});
|
|
1840
1917
|
}
|
|
1841
1918
|
}
|
|
1842
1919
|
if (!format) {
|
|
@@ -1847,14 +1924,7 @@ async function buildOutput(options) {
|
|
|
1847
1924
|
});
|
|
1848
1925
|
}
|
|
1849
1926
|
}
|
|
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
|
-
}
|
|
1927
|
+
const startName = options.startPassage || story.twine2.start || DEFAULT_START_NAME;
|
|
1858
1928
|
if (testMode) {
|
|
1859
1929
|
story.twine2.options.set("debug", true);
|
|
1860
1930
|
}
|
|
@@ -1873,8 +1943,7 @@ async function buildOutput(options) {
|
|
|
1873
1943
|
case "json":
|
|
1874
1944
|
output = storyToJSON(story);
|
|
1875
1945
|
break;
|
|
1876
|
-
case "html":
|
|
1877
|
-
default: {
|
|
1946
|
+
case "html": {
|
|
1878
1947
|
if (!storyHas(story, startName)) {
|
|
1879
1948
|
diagnostics.push({
|
|
1880
1949
|
level: "error",
|
|
@@ -1896,9 +1965,13 @@ async function buildOutput(options) {
|
|
|
1896
1965
|
output = toTwine1HTML(story, format, startName, diagnostics);
|
|
1897
1966
|
}
|
|
1898
1967
|
const modulePaths = options.modules ? getFilenames(options.modules) : [];
|
|
1899
|
-
output = modifyHead(output, modulePaths, options.headFile);
|
|
1968
|
+
output = modifyHead(output, modulePaths, options.headFile, diagnostics);
|
|
1900
1969
|
break;
|
|
1901
1970
|
}
|
|
1971
|
+
default: {
|
|
1972
|
+
const _exhaustive = outputMode;
|
|
1973
|
+
throw new TweeTsError(`Unhandled output mode: ${_exhaustive}`, diagnostics);
|
|
1974
|
+
}
|
|
1902
1975
|
}
|
|
1903
1976
|
const stats = {
|
|
1904
1977
|
...getStoryStats(story),
|
|
@@ -1907,24 +1980,28 @@ async function buildOutput(options) {
|
|
|
1907
1980
|
return { output, story, format, diagnostics, stats };
|
|
1908
1981
|
}
|
|
1909
1982
|
function storyToJSON(story) {
|
|
1910
|
-
return JSON.stringify(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1983
|
+
return JSON.stringify(
|
|
1984
|
+
{
|
|
1985
|
+
name: story.name,
|
|
1986
|
+
ifid: story.ifid,
|
|
1987
|
+
passages: story.passages.map((p) => ({
|
|
1988
|
+
name: p.name,
|
|
1989
|
+
tags: p.tags,
|
|
1990
|
+
text: p.text,
|
|
1991
|
+
metadata: p.metadata ?? null
|
|
1992
|
+
})),
|
|
1993
|
+
twine2: {
|
|
1994
|
+
format: story.twine2.format,
|
|
1995
|
+
formatVersion: story.twine2.formatVersion,
|
|
1996
|
+
start: story.twine2.start,
|
|
1997
|
+
tagColors: Object.fromEntries(story.twine2.tagColors),
|
|
1998
|
+
options: Object.fromEntries(story.twine2.options),
|
|
1999
|
+
zoom: story.twine2.zoom
|
|
2000
|
+
}
|
|
2001
|
+
},
|
|
2002
|
+
null,
|
|
2003
|
+
2
|
|
2004
|
+
);
|
|
1928
2005
|
}
|
|
1929
2006
|
|
|
1930
2007
|
// src/inspect.ts
|
|
@@ -2030,7 +2107,7 @@ function storyInspect(story) {
|
|
|
2030
2107
|
}
|
|
2031
2108
|
|
|
2032
2109
|
// src/config.ts
|
|
2033
|
-
import { readFileSync as
|
|
2110
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
2034
2111
|
import { join as join5 } from "path";
|
|
2035
2112
|
var CONFIG_FILENAME = "twee-ts.config.json";
|
|
2036
2113
|
var VALID_OUTPUT_MODES = ["html", "twee3", "twee1", "twine2-archive", "twine1-archive", "json"];
|
|
@@ -2040,7 +2117,7 @@ function loadConfig(dir) {
|
|
|
2040
2117
|
if (!existsSync2(configPath)) return null;
|
|
2041
2118
|
let raw;
|
|
2042
2119
|
try {
|
|
2043
|
-
raw =
|
|
2120
|
+
raw = readFileSync4(configPath, "utf-8");
|
|
2044
2121
|
} catch {
|
|
2045
2122
|
return null;
|
|
2046
2123
|
}
|
|
@@ -2060,7 +2137,7 @@ function loadConfig(dir) {
|
|
|
2060
2137
|
function loadConfigFile(filePath) {
|
|
2061
2138
|
let raw;
|
|
2062
2139
|
try {
|
|
2063
|
-
raw =
|
|
2140
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
2064
2141
|
} catch (e) {
|
|
2065
2142
|
throw new Error(`Cannot read config file ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2066
2143
|
}
|