@rohal12/twee-ts 1.1.2 → 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 +214 -142
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/{chunk-QTXRKA4T.js → chunk-S4SWCQLW.js} +211 -136
- package/dist/chunk-S4SWCQLW.js.map +1 -0
- package/dist/index.cjs +244 -171
- 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 +213 -140
- 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
|
@@ -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
|
}
|
|
@@ -165,16 +178,16 @@ function passageToPassagedata(p, pid) {
|
|
|
165
178
|
let position;
|
|
166
179
|
let size;
|
|
167
180
|
if (hasMetadataPosition(p)) {
|
|
168
|
-
position = p.metadata
|
|
181
|
+
position = p.metadata?.position ?? "";
|
|
169
182
|
} else {
|
|
170
183
|
const x = pid % 10;
|
|
171
|
-
|
|
184
|
+
const y = Math.floor(pid / 10);
|
|
172
185
|
const xp = x === 0 ? 10 : x;
|
|
173
186
|
const yp = x === 0 ? y : y + 1;
|
|
174
187
|
position = `${xp * 125 - 25},${yp * 125 - 25}`;
|
|
175
188
|
}
|
|
176
189
|
if (hasMetadataSize(p)) {
|
|
177
|
-
size = p.metadata
|
|
190
|
+
size = p.metadata?.size ?? "";
|
|
178
191
|
} else {
|
|
179
192
|
size = "100,100";
|
|
180
193
|
}
|
|
@@ -183,10 +196,10 @@ function passageToPassagedata(p, pid) {
|
|
|
183
196
|
function passageToTiddler(p, pid) {
|
|
184
197
|
let position;
|
|
185
198
|
if (hasMetadataPosition(p)) {
|
|
186
|
-
position = p.metadata
|
|
199
|
+
position = p.metadata?.position ?? "";
|
|
187
200
|
} else {
|
|
188
201
|
const x = pid % 10;
|
|
189
|
-
|
|
202
|
+
const y = Math.floor(pid / 10);
|
|
190
203
|
const xp = x === 0 ? 10 : x;
|
|
191
204
|
const yp = x === 0 ? y : y + 1;
|
|
192
205
|
position = `${xp * 140 - 130},${yp * 140 - 130}`;
|
|
@@ -205,15 +218,17 @@ function countWords(p) {
|
|
|
205
218
|
}
|
|
206
219
|
function applyTagAliases(passages, aliases) {
|
|
207
220
|
const entries = Object.entries(aliases);
|
|
208
|
-
if (entries.length === 0) return;
|
|
209
|
-
|
|
210
|
-
const original =
|
|
221
|
+
if (entries.length === 0) return [...passages];
|
|
222
|
+
return passages.map((p) => {
|
|
223
|
+
const original = p.tags;
|
|
224
|
+
const added = [];
|
|
211
225
|
for (const [alias, canonical] of entries) {
|
|
212
|
-
if (original.includes(alias) && !
|
|
213
|
-
|
|
226
|
+
if (original.includes(alias) && !original.includes(canonical) && !added.includes(canonical)) {
|
|
227
|
+
added.push(canonical);
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
|
-
|
|
230
|
+
return added.length > 0 ? { ...p, tags: [...original, ...added] } : p;
|
|
231
|
+
});
|
|
217
232
|
}
|
|
218
233
|
function quote(s) {
|
|
219
234
|
return `"${s}"`;
|
|
@@ -285,27 +300,32 @@ function marshalStoryData(story) {
|
|
|
285
300
|
return JSON.stringify(data, null, " ");
|
|
286
301
|
}
|
|
287
302
|
function unmarshalStoryData(story, json) {
|
|
288
|
-
let
|
|
303
|
+
let raw;
|
|
289
304
|
try {
|
|
290
|
-
|
|
305
|
+
raw = JSON.parse(json);
|
|
291
306
|
} catch (e) {
|
|
292
307
|
return `Cannot unmarshal "StoryData"; ${e instanceof Error ? e.message : String(e)}`;
|
|
293
308
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
309
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
310
|
+
return 'Cannot unmarshal "StoryData"; expected a JSON object';
|
|
311
|
+
}
|
|
312
|
+
const data = raw;
|
|
313
|
+
if (typeof data.ifid === "string" && data.ifid) story.ifid = data.ifid.toUpperCase();
|
|
314
|
+
if (typeof data.format === "string" && data.format) story.twine2.format = data.format;
|
|
315
|
+
if (typeof data["format-version"] === "string" && data["format-version"])
|
|
316
|
+
story.twine2.formatVersion = data["format-version"];
|
|
317
|
+
if (Array.isArray(data.options)) {
|
|
298
318
|
for (const opt of data.options) {
|
|
299
|
-
story.twine2.options.set(opt, true);
|
|
319
|
+
if (typeof opt === "string") story.twine2.options.set(opt, true);
|
|
300
320
|
}
|
|
301
321
|
}
|
|
302
|
-
if (data.start) story.twine2.start = data.start;
|
|
303
|
-
if (data["tag-colors"]) {
|
|
322
|
+
if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
|
|
323
|
+
if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
|
|
304
324
|
for (const [tag, color] of Object.entries(data["tag-colors"])) {
|
|
305
|
-
story.twine2.tagColors.set(tag, color);
|
|
325
|
+
if (typeof color === "string") story.twine2.tagColors.set(tag, color);
|
|
306
326
|
}
|
|
307
327
|
}
|
|
308
|
-
if (data.zoom
|
|
328
|
+
if (typeof data.zoom === "number" && data.zoom !== 0) story.twine2.zoom = data.zoom;
|
|
309
329
|
return null;
|
|
310
330
|
}
|
|
311
331
|
function unmarshalStorySettings(story, text, diagnostics) {
|
|
@@ -348,6 +368,7 @@ function unmarshalStorySettings(story, text, diagnostics) {
|
|
|
348
368
|
}
|
|
349
369
|
}
|
|
350
370
|
function storyAdd(story, p, diagnostics) {
|
|
371
|
+
let processed = p;
|
|
351
372
|
switch (p.name) {
|
|
352
373
|
case "StoryIncludes":
|
|
353
374
|
diagnostics.push({
|
|
@@ -367,7 +388,7 @@ function storyAdd(story, p, diagnostics) {
|
|
|
367
388
|
});
|
|
368
389
|
}
|
|
369
390
|
}
|
|
370
|
-
p
|
|
391
|
+
processed = { ...p, text: marshalStoryData(story) };
|
|
371
392
|
} else {
|
|
372
393
|
diagnostics.push({
|
|
373
394
|
level: "error",
|
|
@@ -379,12 +400,14 @@ function storyAdd(story, p, diagnostics) {
|
|
|
379
400
|
case "StorySettings":
|
|
380
401
|
unmarshalStorySettings(story, p.text, diagnostics);
|
|
381
402
|
break;
|
|
382
|
-
case "StoryTitle":
|
|
383
|
-
|
|
384
|
-
|
|
403
|
+
case "StoryTitle": {
|
|
404
|
+
const trimmed = p.text.trim();
|
|
405
|
+
processed = { ...p, text: trimmed };
|
|
406
|
+
story.name = trimmed;
|
|
385
407
|
break;
|
|
408
|
+
}
|
|
386
409
|
}
|
|
387
|
-
storyAppend(story,
|
|
410
|
+
storyAppend(story, processed, diagnostics);
|
|
388
411
|
}
|
|
389
412
|
function getStoryStats(story) {
|
|
390
413
|
let storyPassages = 0;
|
|
@@ -438,7 +461,7 @@ var MEDIA_TYPE_MAP = {
|
|
|
438
461
|
webm: "video/webm"
|
|
439
462
|
};
|
|
440
463
|
function mediaTypeFromExt(ext) {
|
|
441
|
-
return MEDIA_TYPE_MAP[ext] ?? "";
|
|
464
|
+
return MEDIA_TYPE_MAP[ext] ?? "application/octet-stream";
|
|
442
465
|
}
|
|
443
466
|
function mediaTypeFromFilename(filename) {
|
|
444
467
|
return mediaTypeFromExt(normalizedFileExt(filename));
|
|
@@ -497,23 +520,49 @@ function getFilenames(pathnames, outFilename) {
|
|
|
497
520
|
}
|
|
498
521
|
|
|
499
522
|
// src/formats.ts
|
|
500
|
-
import {
|
|
523
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
501
524
|
import { join as join2 } from "path";
|
|
502
525
|
import { homedir } from "os";
|
|
526
|
+
|
|
527
|
+
// src/util.ts
|
|
528
|
+
import { readFileSync } from "fs";
|
|
529
|
+
import { parse as parsePath } from "path";
|
|
530
|
+
function readUTF8(filename) {
|
|
531
|
+
let content = readFileSync(filename, "utf-8");
|
|
532
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
533
|
+
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
534
|
+
return content;
|
|
535
|
+
}
|
|
536
|
+
function readBase64(filename) {
|
|
537
|
+
return readFileSync(filename).toString("base64");
|
|
538
|
+
}
|
|
539
|
+
function baseNameWithoutExt(filename) {
|
|
540
|
+
const { name, ext } = parsePath(filename);
|
|
541
|
+
return name || ext;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/formats.ts
|
|
503
545
|
function parseFormatJSON(source, formatId) {
|
|
504
546
|
const first = source.indexOf("{");
|
|
505
547
|
const last = source.lastIndexOf("}");
|
|
506
548
|
if (first === -1 || last === -1) return null;
|
|
507
549
|
let chunk = source.slice(first, last + 1);
|
|
550
|
+
const parse = (json) => {
|
|
551
|
+
const raw = JSON.parse(json);
|
|
552
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
553
|
+
const obj = raw;
|
|
554
|
+
if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
|
|
555
|
+
return { name: obj.name, version: obj.version, source: obj.source, proofing: obj.proofing === true };
|
|
556
|
+
};
|
|
508
557
|
try {
|
|
509
|
-
return
|
|
558
|
+
return parse(chunk);
|
|
510
559
|
} catch {
|
|
511
560
|
if (formatId.toLowerCase().startsWith("harlowe")) {
|
|
512
561
|
const setupIdx = chunk.lastIndexOf(',"setup": function');
|
|
513
562
|
if (setupIdx !== -1) {
|
|
514
563
|
chunk = chunk.slice(0, setupIdx) + "}";
|
|
515
564
|
try {
|
|
516
|
-
return
|
|
565
|
+
return parse(chunk);
|
|
517
566
|
} catch {
|
|
518
567
|
}
|
|
519
568
|
}
|
|
@@ -521,12 +570,6 @@ function parseFormatJSON(source, formatId) {
|
|
|
521
570
|
return null;
|
|
522
571
|
}
|
|
523
572
|
}
|
|
524
|
-
function readUTF8(filename) {
|
|
525
|
-
let content = readFileSync(filename, "utf-8");
|
|
526
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
527
|
-
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
528
|
-
return content;
|
|
529
|
-
}
|
|
530
573
|
function discoverFormats(searchDirs) {
|
|
531
574
|
const formats = /* @__PURE__ */ new Map();
|
|
532
575
|
const baseFilenames = ["format.js", "header.html"];
|
|
@@ -621,9 +664,11 @@ function parseSemver(v) {
|
|
|
621
664
|
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
622
665
|
}
|
|
623
666
|
function semverCompare(a, b) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
667
|
+
const [aMajor, aMinor, aPatch] = a;
|
|
668
|
+
const [bMajor, bMinor, bPatch] = b;
|
|
669
|
+
if (aMajor !== bMajor) return aMajor - bMajor;
|
|
670
|
+
if (aMinor !== bMinor) return aMinor - bMinor;
|
|
671
|
+
if (aPatch !== bPatch) return aPatch - bPatch;
|
|
627
672
|
return 0;
|
|
628
673
|
}
|
|
629
674
|
function getFormatIdByNameAndVersion(formats, name, version) {
|
|
@@ -654,7 +699,6 @@ function readFormatSource(format) {
|
|
|
654
699
|
}
|
|
655
700
|
|
|
656
701
|
// src/loader.ts
|
|
657
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
658
702
|
import { basename } from "path";
|
|
659
703
|
|
|
660
704
|
// src/lexer.ts
|
|
@@ -931,13 +975,13 @@ function hasTwee2Syntax(s) {
|
|
|
931
975
|
}
|
|
932
976
|
function twee2ToV3(s) {
|
|
933
977
|
if (!hasTwee2Syntax(s)) return s;
|
|
934
|
-
|
|
978
|
+
let result = s.replace(twee2HeaderRe, (_, p1, p2, p3) => {
|
|
935
979
|
const tags = p2 ?? "";
|
|
936
980
|
const pos = p3 ?? "";
|
|
937
981
|
return `${p1}${tags} {"position":"${pos}"}`;
|
|
938
982
|
});
|
|
939
|
-
|
|
940
|
-
return
|
|
983
|
+
result = result.replace(twee2BadPosRe, "$1");
|
|
984
|
+
return result;
|
|
941
985
|
}
|
|
942
986
|
|
|
943
987
|
// src/parser.ts
|
|
@@ -1015,7 +1059,11 @@ function parseTwee(source, options = {}) {
|
|
|
1015
1059
|
return { passages, diagnostics };
|
|
1016
1060
|
}
|
|
1017
1061
|
try {
|
|
1018
|
-
const
|
|
1062
|
+
const raw = JSON.parse(item.val);
|
|
1063
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1064
|
+
throw new Error("expected a JSON object");
|
|
1065
|
+
}
|
|
1066
|
+
const parsed = raw;
|
|
1019
1067
|
const meta = {};
|
|
1020
1068
|
if (typeof parsed.position === "string") meta.position = parsed.position;
|
|
1021
1069
|
if (typeof parsed.size === "string") meta.size = parsed.size;
|
|
@@ -1035,6 +1083,16 @@ function parseTwee(source, options = {}) {
|
|
|
1035
1083
|
current.text = trim ? item.val.trim() : item.val;
|
|
1036
1084
|
break;
|
|
1037
1085
|
}
|
|
1086
|
+
default: {
|
|
1087
|
+
const _exhaustive = item.type;
|
|
1088
|
+
diagnostics.push({
|
|
1089
|
+
level: "error",
|
|
1090
|
+
message: `line ${item.line}: Unhandled lexer item type: ${_exhaustive}.`,
|
|
1091
|
+
file: filename,
|
|
1092
|
+
line: item.line
|
|
1093
|
+
});
|
|
1094
|
+
return { passages, diagnostics };
|
|
1095
|
+
}
|
|
1038
1096
|
}
|
|
1039
1097
|
lastType = item.type;
|
|
1040
1098
|
}
|
|
@@ -1045,15 +1103,6 @@ function parseTwee(source, options = {}) {
|
|
|
1045
1103
|
}
|
|
1046
1104
|
|
|
1047
1105
|
// src/loader.ts
|
|
1048
|
-
function readUTF82(filename) {
|
|
1049
|
-
let content = readFileSync2(filename, "utf-8");
|
|
1050
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1051
|
-
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1052
|
-
return content;
|
|
1053
|
-
}
|
|
1054
|
-
function readBase64(filename) {
|
|
1055
|
-
return readFileSync2(filename).toString("base64");
|
|
1056
|
-
}
|
|
1057
1106
|
function loadSources(story, filenames, opts, diagnostics, processedFiles) {
|
|
1058
1107
|
for (const filename of filenames) {
|
|
1059
1108
|
if (processedFiles.has(filename)) {
|
|
@@ -1152,7 +1201,7 @@ function loadInlineSources(story, sources, opts, diagnostics) {
|
|
|
1152
1201
|
}
|
|
1153
1202
|
}
|
|
1154
1203
|
function loadTwee(story, filename, opts, diagnostics) {
|
|
1155
|
-
const source =
|
|
1204
|
+
const source = readUTF8(filename);
|
|
1156
1205
|
const result = parseTwee(source, {
|
|
1157
1206
|
filename,
|
|
1158
1207
|
trim: opts.trim ?? true,
|
|
@@ -1164,12 +1213,12 @@ function loadTwee(story, filename, opts, diagnostics) {
|
|
|
1164
1213
|
}
|
|
1165
1214
|
}
|
|
1166
1215
|
function loadTagged(story, tag, filename, diagnostics) {
|
|
1167
|
-
const source =
|
|
1216
|
+
const source = readUTF8(filename);
|
|
1168
1217
|
storyAdd(story, { name: basename(filename), tags: [tag], text: source }, diagnostics);
|
|
1169
1218
|
}
|
|
1170
1219
|
function loadMedia(story, tag, filename, diagnostics) {
|
|
1171
1220
|
const source = readBase64(filename);
|
|
1172
|
-
const name =
|
|
1221
|
+
const name = baseNameWithoutExt(filename);
|
|
1173
1222
|
storyAdd(
|
|
1174
1223
|
story,
|
|
1175
1224
|
{
|
|
@@ -1183,7 +1232,7 @@ function loadMedia(story, tag, filename, diagnostics) {
|
|
|
1183
1232
|
function loadFont(story, filename, diagnostics) {
|
|
1184
1233
|
const source = readBase64(filename);
|
|
1185
1234
|
const name = basename(filename);
|
|
1186
|
-
const family =
|
|
1235
|
+
const family = baseNameWithoutExt(filename);
|
|
1187
1236
|
const ext = normalizedFileExt(filename);
|
|
1188
1237
|
const mediaType = mediaTypeFromExt(ext);
|
|
1189
1238
|
const hint = fontFormatHint(ext);
|
|
@@ -1201,9 +1250,11 @@ function loadFont(story, filename, diagnostics) {
|
|
|
1201
1250
|
);
|
|
1202
1251
|
}
|
|
1203
1252
|
|
|
1253
|
+
// src/version.ts
|
|
1254
|
+
var VERSION = "1.2.0";
|
|
1255
|
+
|
|
1204
1256
|
// src/output-twine2.ts
|
|
1205
1257
|
var CREATOR_NAME = "Twee-ts";
|
|
1206
|
-
var CREATOR_VERSION = "0.1.0";
|
|
1207
1258
|
function toTwine2Archive(story, startName, diagnostics) {
|
|
1208
1259
|
return getTwine2DataChunk(story, startName, diagnostics) + "\n";
|
|
1209
1260
|
}
|
|
@@ -1231,12 +1282,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1231
1282
|
}
|
|
1232
1283
|
let styleContent = "";
|
|
1233
1284
|
if (stylesheets.length === 1) {
|
|
1234
|
-
styleContent = stylesheets[0]
|
|
1285
|
+
styleContent = stylesheets[0]?.text ?? "";
|
|
1235
1286
|
} else if (stylesheets.length > 1) {
|
|
1236
1287
|
let pidS = 1;
|
|
1237
1288
|
for (const p of stylesheets) {
|
|
1238
1289
|
if (pidS > 1 && !styleContent.endsWith("\n")) styleContent += "\n";
|
|
1239
|
-
styleContent += `/* twine-user-stylesheet #${pidS}: "${p.name}" */
|
|
1290
|
+
styleContent += `/* twine-user-stylesheet #${pidS}: "${commentSanitize(p.name)}" */
|
|
1240
1291
|
`;
|
|
1241
1292
|
styleContent += p.text;
|
|
1242
1293
|
pidS++;
|
|
@@ -1245,12 +1296,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1245
1296
|
parts.push(`<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">${styleContent}</style>`);
|
|
1246
1297
|
let scriptContent = "";
|
|
1247
1298
|
if (scripts.length === 1) {
|
|
1248
|
-
scriptContent = scripts[0]
|
|
1299
|
+
scriptContent = scripts[0]?.text ?? "";
|
|
1249
1300
|
} else if (scripts.length > 1) {
|
|
1250
1301
|
let pidS = 1;
|
|
1251
1302
|
for (const p of scripts) {
|
|
1252
1303
|
if (pidS > 1 && !scriptContent.endsWith("\n")) scriptContent += "\n";
|
|
1253
|
-
scriptContent += `/* twine-user-script #${pidS}: "${p.name}" */
|
|
1304
|
+
scriptContent += `/* twine-user-script #${pidS}: "${commentSanitize(p.name)}" */
|
|
1254
1305
|
`;
|
|
1255
1306
|
scriptContent += p.text;
|
|
1256
1307
|
pidS++;
|
|
@@ -1280,7 +1331,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
|
|
|
1280
1331
|
}
|
|
1281
1332
|
const options = opts.join(" ");
|
|
1282
1333
|
const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
|
|
1283
|
-
const wrapper = `<!-- UUID://${story.ifid}// --><tw-storydata name="${attrEscape(story.name)}" startnode="${startID}" creator="${attrEscape(CREATOR_NAME)}" creator-version="${attrEscape(
|
|
1334
|
+
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>`;
|
|
1284
1335
|
return wrapper + parts.join("") + "</tw-storydata>";
|
|
1285
1336
|
}
|
|
1286
1337
|
function ensureIFID(story, diagnostics) {
|
|
@@ -1301,10 +1352,9 @@ function ensureIFID(story, diagnostics) {
|
|
|
1301
1352
|
}
|
|
1302
1353
|
|
|
1303
1354
|
// src/output-twine1.ts
|
|
1304
|
-
import { readFileSync as
|
|
1355
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1305
1356
|
import { join as join3, dirname } from "path";
|
|
1306
1357
|
var CREATOR_NAME2 = "twee-ts";
|
|
1307
|
-
var CREATOR_VERSION2 = "0.1.0";
|
|
1308
1358
|
function toTwine1Archive(story, _startName) {
|
|
1309
1359
|
const { data, count } = getTwine1PassageChunk(story);
|
|
1310
1360
|
return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
|
|
@@ -1326,26 +1376,27 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
|
|
|
1326
1376
|
template = tryReplaceComponent(template, '"MODERNIZR"', join3(parentDir, "modernizr.js"), true);
|
|
1327
1377
|
}
|
|
1328
1378
|
const displayStart = startName === "Start" ? "" : startName;
|
|
1329
|
-
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${
|
|
1379
|
+
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
|
|
1330
1380
|
template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
1331
|
-
template = template.replace('"START_AT"', `"${displayStart}"`);
|
|
1381
|
+
template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
|
|
1332
1382
|
template = template.replace('"STORY_SIZE"', `"${count}"`);
|
|
1333
1383
|
if (template.includes('"STORY"')) {
|
|
1334
1384
|
template = template.replace('"STORY"', data);
|
|
1335
1385
|
} else {
|
|
1336
1386
|
let footer;
|
|
1337
1387
|
try {
|
|
1338
|
-
footer =
|
|
1388
|
+
footer = readFileSync2(join3(formatDir, "footer.html"), "utf-8");
|
|
1339
1389
|
} catch {
|
|
1340
1390
|
footer = "</div>\n</body>\n</html>\n";
|
|
1341
1391
|
}
|
|
1342
1392
|
template += data + footer;
|
|
1343
1393
|
}
|
|
1344
1394
|
if (story.ifid) {
|
|
1395
|
+
const safeIfid = htmlCommentSanitize(story.ifid);
|
|
1345
1396
|
if (template.includes('<div id="store-area"')) {
|
|
1346
|
-
template = template.replace('<div id="store-area"', `<!-- UUID://${
|
|
1397
|
+
template = template.replace('<div id="store-area"', `<!-- UUID://${safeIfid}// --><div id="store-area"`);
|
|
1347
1398
|
} else {
|
|
1348
|
-
template = template.replace('<div id="storeArea"', `<!-- UUID://${
|
|
1399
|
+
template = template.replace('<div id="storeArea"', `<!-- UUID://${safeIfid}// --><div id="storeArea"`);
|
|
1349
1400
|
}
|
|
1350
1401
|
}
|
|
1351
1402
|
return template;
|
|
@@ -1360,13 +1411,18 @@ function getTwine1PassageChunk(story) {
|
|
|
1360
1411
|
}
|
|
1361
1412
|
return { data, count };
|
|
1362
1413
|
}
|
|
1363
|
-
function tryReplaceComponent(template, placeholder, componentPath,
|
|
1414
|
+
function tryReplaceComponent(template, placeholder, componentPath, required) {
|
|
1364
1415
|
if (!template.includes(placeholder)) return template;
|
|
1365
1416
|
try {
|
|
1366
|
-
let content =
|
|
1417
|
+
let content = readFileSync2(componentPath, "utf-8");
|
|
1367
1418
|
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1368
1419
|
return template.replace(placeholder, content);
|
|
1369
|
-
} catch {
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
if (required) {
|
|
1422
|
+
throw new Error(
|
|
1423
|
+
`Required format component not found: ${componentPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1370
1426
|
return template;
|
|
1371
1427
|
}
|
|
1372
1428
|
}
|
|
@@ -1381,16 +1437,6 @@ function toTwee(story, outMode) {
|
|
|
1381
1437
|
}
|
|
1382
1438
|
|
|
1383
1439
|
// src/modules.ts
|
|
1384
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1385
|
-
import { basename as basename2 } from "path";
|
|
1386
|
-
function readUTF83(filename) {
|
|
1387
|
-
let content = readFileSync4(filename, "utf-8");
|
|
1388
|
-
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1389
|
-
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1390
|
-
}
|
|
1391
|
-
function readBase642(filename) {
|
|
1392
|
-
return readFileSync4(filename).toString("base64");
|
|
1393
|
-
}
|
|
1394
1440
|
function loadModules(filenames) {
|
|
1395
1441
|
const processed = /* @__PURE__ */ new Set();
|
|
1396
1442
|
const headTags = [];
|
|
@@ -1420,16 +1466,16 @@ function loadModules(filenames) {
|
|
|
1420
1466
|
return headTags.join("\n");
|
|
1421
1467
|
}
|
|
1422
1468
|
function loadModuleTagged(tag, filename) {
|
|
1423
|
-
const source =
|
|
1469
|
+
const source = readUTF8(filename).trim();
|
|
1424
1470
|
if (source.length === 0) return null;
|
|
1425
|
-
const family =
|
|
1471
|
+
const family = baseNameWithoutExt(filename);
|
|
1426
1472
|
const idSlug = `${tag}-module-${slugify(family)}`;
|
|
1427
1473
|
const mimeType = tag === "script" ? "text/javascript" : "text/css";
|
|
1428
1474
|
return `<${tag} id="${idSlug}" type="${mimeType}">${source}</${tag}>`;
|
|
1429
1475
|
}
|
|
1430
1476
|
function loadModuleFont(filename) {
|
|
1431
|
-
const source =
|
|
1432
|
-
const family =
|
|
1477
|
+
const source = readBase64(filename);
|
|
1478
|
+
const family = baseNameWithoutExt(filename);
|
|
1433
1479
|
const idSlug = `style-module-${slugify(family)}`;
|
|
1434
1480
|
const ext = normalizedFileExt(filename);
|
|
1435
1481
|
const mediaType = mediaTypeFromExt(ext);
|
|
@@ -1439,7 +1485,7 @@ function loadModuleFont(filename) {
|
|
|
1439
1485
|
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1440
1486
|
}</style>`;
|
|
1441
1487
|
}
|
|
1442
|
-
function modifyHead(html, modulePaths, headFile) {
|
|
1488
|
+
function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
1443
1489
|
const parts = [];
|
|
1444
1490
|
if (modulePaths.length > 0) {
|
|
1445
1491
|
const modules = loadModules(modulePaths).trim();
|
|
@@ -1447,9 +1493,13 @@ function modifyHead(html, modulePaths, headFile) {
|
|
|
1447
1493
|
}
|
|
1448
1494
|
if (headFile) {
|
|
1449
1495
|
try {
|
|
1450
|
-
const source =
|
|
1496
|
+
const source = readUTF8(headFile).trim();
|
|
1451
1497
|
if (source.length > 0) parts.push(source);
|
|
1452
|
-
} catch {
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
diagnostics?.push({
|
|
1500
|
+
level: "warning",
|
|
1501
|
+
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
1502
|
+
});
|
|
1453
1503
|
}
|
|
1454
1504
|
}
|
|
1455
1505
|
if (parts.length > 0) {
|
|
@@ -1460,7 +1510,7 @@ function modifyHead(html, modulePaths, headFile) {
|
|
|
1460
1510
|
}
|
|
1461
1511
|
|
|
1462
1512
|
// src/remote-formats.ts
|
|
1463
|
-
import { mkdirSync, readFileSync as
|
|
1513
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1464
1514
|
import { join as join4 } from "path";
|
|
1465
1515
|
import { homedir as homedir2 } from "os";
|
|
1466
1516
|
var DEFAULT_SFA_INDICES = [
|
|
@@ -1476,6 +1526,24 @@ var indexCache = /* @__PURE__ */ new Map();
|
|
|
1476
1526
|
function clearIndexCache() {
|
|
1477
1527
|
indexCache.clear();
|
|
1478
1528
|
}
|
|
1529
|
+
function isValidEntry(val) {
|
|
1530
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) return false;
|
|
1531
|
+
const obj = val;
|
|
1532
|
+
return typeof obj.name === "string" && typeof obj.version === "string" && typeof obj.checksums === "object" && obj.checksums !== null && !Array.isArray(obj.checksums);
|
|
1533
|
+
}
|
|
1534
|
+
function validateSFAIndex(json) {
|
|
1535
|
+
if (typeof json !== "object" || json === null) {
|
|
1536
|
+
throw new Error("SFA index is not an object");
|
|
1537
|
+
}
|
|
1538
|
+
const obj = json;
|
|
1539
|
+
const twine1 = Array.isArray(obj["twine1"]) ? obj["twine1"].filter(isValidEntry) : [];
|
|
1540
|
+
const twine2 = Array.isArray(obj["twine2"]) ? obj["twine2"].filter(isValidEntry) : [];
|
|
1541
|
+
return { twine1, twine2 };
|
|
1542
|
+
}
|
|
1543
|
+
function makeFormatId(name, version) {
|
|
1544
|
+
const major = parseSemver(version)?.[0] ?? "0";
|
|
1545
|
+
return `${name.toLowerCase().replace(/\s+/g, "-")}-${major}`;
|
|
1546
|
+
}
|
|
1479
1547
|
async function fetchIndex(url) {
|
|
1480
1548
|
const cached = indexCache.get(url);
|
|
1481
1549
|
if (cached) return cached;
|
|
@@ -1483,37 +1551,42 @@ async function fetchIndex(url) {
|
|
|
1483
1551
|
if (!res.ok) {
|
|
1484
1552
|
throw new Error(`Failed to fetch format index from ${url}: ${res.status} ${res.statusText}`);
|
|
1485
1553
|
}
|
|
1486
|
-
const data = await res.json();
|
|
1554
|
+
const data = validateSFAIndex(await res.json());
|
|
1487
1555
|
indexCache.set(url, data);
|
|
1488
1556
|
return data;
|
|
1489
1557
|
}
|
|
1490
1558
|
function findEntry(index, name, version) {
|
|
1491
1559
|
const wanted = parseSemver(version);
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1560
|
+
const searchArrays = [
|
|
1561
|
+
{ entries: index.twine2 ?? [], formatType: "twine2" },
|
|
1562
|
+
{ entries: index.twine1 ?? [], formatType: "twine1" }
|
|
1563
|
+
];
|
|
1564
|
+
let bestResult;
|
|
1494
1565
|
let bestVersion = null;
|
|
1495
|
-
for (const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (
|
|
1502
|
-
bestVersion
|
|
1503
|
-
|
|
1566
|
+
for (const { entries, formatType } of searchArrays) {
|
|
1567
|
+
for (const entry of entries) {
|
|
1568
|
+
if (entry.name.toLowerCase() !== name.toLowerCase()) continue;
|
|
1569
|
+
const have = parseSemver(entry.version);
|
|
1570
|
+
if (!have) continue;
|
|
1571
|
+
if (wanted && semverCompare(have, wanted) === 0) return { entry, formatType };
|
|
1572
|
+
if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
|
|
1573
|
+
if (!bestVersion || semverCompare(have, bestVersion) > 0) {
|
|
1574
|
+
bestVersion = have;
|
|
1575
|
+
bestResult = { entry, formatType };
|
|
1576
|
+
}
|
|
1504
1577
|
}
|
|
1505
1578
|
}
|
|
1506
1579
|
}
|
|
1507
|
-
return
|
|
1580
|
+
return bestResult;
|
|
1508
1581
|
}
|
|
1509
1582
|
async function verifySHA256(content, expectedHex) {
|
|
1510
1583
|
const digest = await crypto.subtle.digest("SHA-256", content);
|
|
1511
1584
|
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1512
1585
|
return hex === expectedHex.toLowerCase();
|
|
1513
1586
|
}
|
|
1514
|
-
function getDownloadUrl(indexUrl, entry) {
|
|
1587
|
+
function getDownloadUrl(indexUrl, entry, formatType) {
|
|
1515
1588
|
const base = indexUrl.replace(/\/index\.json$/, "");
|
|
1516
|
-
return `${base}
|
|
1589
|
+
return `${base}/${formatType}/${entry.name}/${entry.version}/format.js`;
|
|
1517
1590
|
}
|
|
1518
1591
|
async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
1519
1592
|
const res = await fetch(downloadUrl);
|
|
@@ -1524,13 +1597,14 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
|
1524
1597
|
const checksumKey = Object.keys(entry.checksums ?? {}).find((k) => k.endsWith("format.js"));
|
|
1525
1598
|
if (checksumKey) {
|
|
1526
1599
|
const expected = entry.checksums[checksumKey];
|
|
1600
|
+
if (!expected) throw new Error(`Missing checksum value for key "${checksumKey}"`);
|
|
1527
1601
|
const encoder = new TextEncoder();
|
|
1528
1602
|
const valid = await verifySHA256(encoder.encode(text), expected);
|
|
1529
1603
|
if (!valid) {
|
|
1530
1604
|
throw new Error(`Checksum verification failed for ${entry.name} ${entry.version}`);
|
|
1531
1605
|
}
|
|
1532
1606
|
}
|
|
1533
|
-
const id =
|
|
1607
|
+
const id = makeFormatId(entry.name, entry.version);
|
|
1534
1608
|
const data = parseFormatJSON(text, id);
|
|
1535
1609
|
if (!data) {
|
|
1536
1610
|
throw new Error(`Failed to parse format JSON from ${downloadUrl}`);
|
|
@@ -1560,7 +1634,7 @@ async function fetchDirectFormat(url) {
|
|
|
1560
1634
|
if (!data) {
|
|
1561
1635
|
throw new Error(`Failed to parse format JSON from ${url}`);
|
|
1562
1636
|
}
|
|
1563
|
-
const id =
|
|
1637
|
+
const id = makeFormatId(data.name, data.version);
|
|
1564
1638
|
const cacheDir = getCacheDir();
|
|
1565
1639
|
const formatDir = join4(cacheDir, data.name, data.version);
|
|
1566
1640
|
mkdirSync(formatDir, { recursive: true });
|
|
@@ -1576,6 +1650,7 @@ async function fetchDirectFormat(url) {
|
|
|
1576
1650
|
};
|
|
1577
1651
|
}
|
|
1578
1652
|
async function resolveRemoteFormat(name, version, indices, urls) {
|
|
1653
|
+
let lastError;
|
|
1579
1654
|
if (urls) {
|
|
1580
1655
|
for (const url of urls) {
|
|
1581
1656
|
try {
|
|
@@ -1587,7 +1662,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
|
|
|
1587
1662
|
return info;
|
|
1588
1663
|
}
|
|
1589
1664
|
}
|
|
1590
|
-
} catch {
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1591
1667
|
}
|
|
1592
1668
|
}
|
|
1593
1669
|
}
|
|
@@ -1595,24 +1671,26 @@ async function resolveRemoteFormat(name, version, indices, urls) {
|
|
|
1595
1671
|
for (const indexUrl of allIndices) {
|
|
1596
1672
|
try {
|
|
1597
1673
|
const index = await fetchIndex(indexUrl);
|
|
1598
|
-
const
|
|
1599
|
-
if (
|
|
1600
|
-
const cached = getCachedFormat(entry.name, entry.version);
|
|
1674
|
+
const result = findEntry(index, name, version);
|
|
1675
|
+
if (result) {
|
|
1676
|
+
const cached = getCachedFormat(result.entry.name, result.entry.version);
|
|
1601
1677
|
if (cached) return cached;
|
|
1602
|
-
const downloadUrl = getDownloadUrl(indexUrl, entry);
|
|
1603
|
-
return await fetchAndCacheFormat(entry, downloadUrl);
|
|
1678
|
+
const downloadUrl = getDownloadUrl(indexUrl, result.entry, result.formatType);
|
|
1679
|
+
return await fetchAndCacheFormat(result.entry, downloadUrl);
|
|
1604
1680
|
}
|
|
1605
|
-
} catch {
|
|
1681
|
+
} catch (e) {
|
|
1682
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
1606
1683
|
}
|
|
1607
1684
|
}
|
|
1685
|
+
if (lastError) throw lastError;
|
|
1608
1686
|
return void 0;
|
|
1609
1687
|
}
|
|
1610
1688
|
function getCachedFormat(name, version) {
|
|
1611
1689
|
const formatPath = join4(getCacheDir(), name, version, "format.js");
|
|
1612
1690
|
try {
|
|
1613
1691
|
if (!existsSync(formatPath)) return void 0;
|
|
1614
|
-
const source =
|
|
1615
|
-
const id =
|
|
1692
|
+
const source = readFileSync3(formatPath, "utf-8");
|
|
1693
|
+
const id = makeFormatId(name, version);
|
|
1616
1694
|
const data = parseFormatJSON(source, id);
|
|
1617
1695
|
if (!data) return void 0;
|
|
1618
1696
|
return {
|
|
@@ -1664,10 +1742,7 @@ async function buildOutput(options) {
|
|
|
1664
1742
|
loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
|
|
1665
1743
|
loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
|
|
1666
1744
|
if (options.tagAliases) {
|
|
1667
|
-
applyTagAliases(story.passages, options.tagAliases);
|
|
1668
|
-
}
|
|
1669
|
-
const fatalErrors = diagnostics.filter((d) => d.level === "error");
|
|
1670
|
-
if (fatalErrors.length > 0 && outputMode === "html") {
|
|
1745
|
+
story.passages = applyTagAliases(story.passages, options.tagAliases);
|
|
1671
1746
|
}
|
|
1672
1747
|
let format;
|
|
1673
1748
|
let formatId = options.formatId ?? "";
|
|
@@ -1686,7 +1761,11 @@ async function buildOutput(options) {
|
|
|
1686
1761
|
const remoteVersion = story.twine2.formatVersion || "";
|
|
1687
1762
|
try {
|
|
1688
1763
|
format = await resolveRemoteFormat(remoteName, remoteVersion, options.formatIndices, options.formatUrls);
|
|
1689
|
-
} catch {
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
diagnostics.push({
|
|
1766
|
+
level: "warning",
|
|
1767
|
+
message: `Remote format fetch failed for "${remoteName}": ${e instanceof Error ? e.message : String(e)}`
|
|
1768
|
+
});
|
|
1690
1769
|
}
|
|
1691
1770
|
}
|
|
1692
1771
|
if (!format) {
|
|
@@ -1697,14 +1776,7 @@ async function buildOutput(options) {
|
|
|
1697
1776
|
});
|
|
1698
1777
|
}
|
|
1699
1778
|
}
|
|
1700
|
-
|
|
1701
|
-
if (options.startPassage) {
|
|
1702
|
-
startName = options.startPassage;
|
|
1703
|
-
} else if (story.twine2.start) {
|
|
1704
|
-
startName = story.twine2.start;
|
|
1705
|
-
} else {
|
|
1706
|
-
startName = DEFAULT_START_NAME;
|
|
1707
|
-
}
|
|
1779
|
+
const startName = options.startPassage || story.twine2.start || DEFAULT_START_NAME;
|
|
1708
1780
|
if (testMode) {
|
|
1709
1781
|
story.twine2.options.set("debug", true);
|
|
1710
1782
|
}
|
|
@@ -1723,8 +1795,7 @@ async function buildOutput(options) {
|
|
|
1723
1795
|
case "json":
|
|
1724
1796
|
output = storyToJSON(story);
|
|
1725
1797
|
break;
|
|
1726
|
-
case "html":
|
|
1727
|
-
default: {
|
|
1798
|
+
case "html": {
|
|
1728
1799
|
if (!storyHas(story, startName)) {
|
|
1729
1800
|
diagnostics.push({
|
|
1730
1801
|
level: "error",
|
|
@@ -1746,9 +1817,13 @@ async function buildOutput(options) {
|
|
|
1746
1817
|
output = toTwine1HTML(story, format, startName, diagnostics);
|
|
1747
1818
|
}
|
|
1748
1819
|
const modulePaths = options.modules ? getFilenames(options.modules) : [];
|
|
1749
|
-
output = modifyHead(output, modulePaths, options.headFile);
|
|
1820
|
+
output = modifyHead(output, modulePaths, options.headFile, diagnostics);
|
|
1750
1821
|
break;
|
|
1751
1822
|
}
|
|
1823
|
+
default: {
|
|
1824
|
+
const _exhaustive = outputMode;
|
|
1825
|
+
throw new TweeTsError(`Unhandled output mode: ${_exhaustive}`, diagnostics);
|
|
1826
|
+
}
|
|
1752
1827
|
}
|
|
1753
1828
|
const stats = {
|
|
1754
1829
|
...getStoryStats(story),
|
|
@@ -1784,4 +1859,4 @@ function storyToJSON(story) {
|
|
|
1784
1859
|
export {
|
|
1785
1860
|
compile
|
|
1786
1861
|
};
|
|
1787
|
-
//# sourceMappingURL=chunk-
|
|
1862
|
+
//# sourceMappingURL=chunk-S4SWCQLW.js.map
|