@rohal12/twee-ts 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/twee-ts.js +328 -136
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/index.cjs +328 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -8
- package/dist/index.d.ts +22 -8
- package/dist/index.js +328 -138
- package/dist/index.js.map +1 -1
- package/dist/{diagnostics-DZNS7W41.js → paths-w4ulhCFb.js} +291 -138
- package/dist/paths-w4ulhCFb.js.map +1 -0
- package/dist/plugins/rollup.d.ts +11 -1
- package/dist/plugins/rollup.js +36 -6
- package/dist/plugins/rollup.js.map +1 -1
- package/dist/plugins/vite.js +37 -35
- package/dist/plugins/vite.js.map +1 -1
- package/package.json +1 -1
- package/dist/diagnostics-DZNS7W41.js.map +0 -1
package/dist/bin/twee-ts.js
CHANGED
|
@@ -15,6 +15,19 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
function generateIFID() {
|
|
16
16
|
return randomUUID().toUpperCase();
|
|
17
17
|
}
|
|
18
|
+
/** Length of the `UUID://...//` wrapper around a 36-character UUID. */
|
|
19
|
+
const WRAPPED_LENGTH = 45;
|
|
20
|
+
/**
|
|
21
|
+
* Bring an IFID into the form the story model stores and every output writes: the uppercase
|
|
22
|
+
* bare UUID. A valid `UUID://...//` wrapped IFID loses its wrapper; outputs that need the wrapper
|
|
23
|
+
* (the Treaty of Babel comment) add exactly one. Twine 2 IFIDs use only capital letters, so the
|
|
24
|
+
* value is uppercased, as Tweego does. An invalid value is only uppercased, so that validation
|
|
25
|
+
* reports it as written.
|
|
26
|
+
*/
|
|
27
|
+
function normalizeIFID(value) {
|
|
28
|
+
const upper = value.toUpperCase();
|
|
29
|
+
return upper.length === WRAPPED_LENGTH && validateIFID(upper) === null ? upper.slice(7, 43) : upper;
|
|
30
|
+
}
|
|
18
31
|
/**
|
|
19
32
|
* Validate an IFID string.
|
|
20
33
|
* Accepts both bare UUIDs and UUID://...// wrapped format.
|
|
@@ -443,7 +456,7 @@ function unmarshalStoryData(story, json) {
|
|
|
443
456
|
}
|
|
444
457
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return "Cannot unmarshal \"StoryData\"; expected a JSON object";
|
|
445
458
|
const data = raw;
|
|
446
|
-
if (typeof data.ifid === "string" && data.ifid) story.ifid = data.ifid
|
|
459
|
+
if (typeof data.ifid === "string" && data.ifid) story.ifid = normalizeIFID(data.ifid);
|
|
447
460
|
if (typeof data.format === "string" && data.format) story.twine2.format = data.format;
|
|
448
461
|
if (typeof data["format-version"] === "string" && data["format-version"]) story.twine2.formatVersion = data["format-version"];
|
|
449
462
|
if (Array.isArray(data.options)) {
|
|
@@ -474,7 +487,7 @@ function unmarshalStorySettings(story, text, diagnostics) {
|
|
|
474
487
|
const val = line.slice(colonIdx + 1).trim().toLowerCase();
|
|
475
488
|
switch (key) {
|
|
476
489
|
case "ifid":
|
|
477
|
-
if (validateIFID(val) === null) story.legacyIFID = val
|
|
490
|
+
if (validateIFID(val) === null) story.legacyIFID = normalizeIFID(val);
|
|
478
491
|
obsolete.push("\"ifid\"");
|
|
479
492
|
continue;
|
|
480
493
|
case "zoom":
|
|
@@ -4671,7 +4684,7 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4671
4684
|
});
|
|
4672
4685
|
else startnode = parsed;
|
|
4673
4686
|
}
|
|
4674
|
-
if (attrs["ifid"]) story.ifid = attrs["ifid"]
|
|
4687
|
+
if (attrs["ifid"]) story.ifid = normalizeIFID(attrs["ifid"]);
|
|
4675
4688
|
if (attrs["zoom"]) {
|
|
4676
4689
|
const parsed = parseFloat(attrs["zoom"]);
|
|
4677
4690
|
if (Number.isNaN(parsed)) diagnostics.push({
|
|
@@ -4684,7 +4697,9 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4684
4697
|
if (attrs["format"]) story.twine2.format = attrs["format"];
|
|
4685
4698
|
if (attrs["format-version"]) story.twine2.formatVersion = attrs["format-version"];
|
|
4686
4699
|
if (attrs["options"]) for (const opt of attrs["options"].split(/\s+/).filter((s) => s.length > 0)) story.twine2.options.set(opt, true);
|
|
4687
|
-
|
|
4700
|
+
const children = storyData.children ?? [];
|
|
4701
|
+
const takenNames = new Set(children.filter((node) => node.type === "tag" && node.name === "tw-passagedata").map(passageDataName));
|
|
4702
|
+
for (const node of children) {
|
|
4688
4703
|
if (node.type !== "tag" && node.type !== "style" && node.type !== "script") continue;
|
|
4689
4704
|
switch (node.name) {
|
|
4690
4705
|
case "style":
|
|
@@ -4692,8 +4707,10 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4692
4707
|
const content = getTextContent(node);
|
|
4693
4708
|
if (content.trim().length === 0) continue;
|
|
4694
4709
|
const text = passageText(content);
|
|
4710
|
+
const name = freeName(node.name === "style" ? "Story Stylesheet" : "Story JavaScript", takenNames);
|
|
4711
|
+
takenNames.add(name);
|
|
4695
4712
|
storyAdd(story, {
|
|
4696
|
-
name
|
|
4713
|
+
name,
|
|
4697
4714
|
tags: node.name === "style" ? ["stylesheet"] : ["script"],
|
|
4698
4715
|
text
|
|
4699
4716
|
}, diagnostics);
|
|
@@ -4709,7 +4726,7 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4709
4726
|
case "tw-passagedata": {
|
|
4710
4727
|
let pid = 0;
|
|
4711
4728
|
const pAttrs = node.attribs ?? {};
|
|
4712
|
-
const name =
|
|
4729
|
+
const name = passageDataName(node);
|
|
4713
4730
|
const tags = pAttrs["tags"] ? pAttrs["tags"].split(/\s+/).filter((s) => s.length > 0) : [];
|
|
4714
4731
|
const metadata = {};
|
|
4715
4732
|
if (pAttrs["pid"]) {
|
|
@@ -4740,6 +4757,17 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4740
4757
|
text: marshalStoryData(story)
|
|
4741
4758
|
}, diagnostics);
|
|
4742
4759
|
}
|
|
4760
|
+
function passageDataName(node) {
|
|
4761
|
+
return node.attribs?.["name"] ?? "";
|
|
4762
|
+
}
|
|
4763
|
+
/** The first of `base`, `base 2`, `base 3`, … that is not in `taken`. */
|
|
4764
|
+
function freeName(base, taken) {
|
|
4765
|
+
if (!taken.has(base)) return base;
|
|
4766
|
+
for (let n = 2;; n++) {
|
|
4767
|
+
const candidate = `${base} ${n}`;
|
|
4768
|
+
if (!taken.has(candidate)) return candidate;
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4743
4771
|
function decompileTwine1(storeArea, story, passageText, diagnostics) {
|
|
4744
4772
|
const passages = (storeArea.children ?? []).filter(isTiddler).map((node) => tiddlerToPassage(node, passageText));
|
|
4745
4773
|
const obfuscated = isRot13Obfuscated(passages);
|
|
@@ -5132,6 +5160,121 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5132
5160
|
}, diagnostics);
|
|
5133
5161
|
}
|
|
5134
5162
|
//#endregion
|
|
5163
|
+
//#region src/template.ts
|
|
5164
|
+
/**
|
|
5165
|
+
* An HTML closing head tag: `</head` in any letter case, followed by whitespace, `/` or `>`, up to the next `>`.
|
|
5166
|
+
* This is how the HTML tokenizer reads an end tag; it ignores whatever sits between the tag name and the `>`.
|
|
5167
|
+
*/
|
|
5168
|
+
const CLOSING_HEAD_TAG = "<\\/[Hh][Ee][Aa][Dd](?=[\\t\\n\\f\\r />])[^>]*>";
|
|
5169
|
+
/** The regular expression source matching exactly `text`. */
|
|
5170
|
+
function literal(text) {
|
|
5171
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
5172
|
+
}
|
|
5173
|
+
/** Replace the `slots` placeholders found in `template`, without scanning inserted text. */
|
|
5174
|
+
function fillTemplate(template, slots) {
|
|
5175
|
+
return fillTemplateParts([{
|
|
5176
|
+
kind: "scan",
|
|
5177
|
+
text: template,
|
|
5178
|
+
slots
|
|
5179
|
+
}]);
|
|
5180
|
+
}
|
|
5181
|
+
/**
|
|
5182
|
+
* Fill the parts of a template in order, joined. A `'first'` slot replaced in one part is not replaced again in a
|
|
5183
|
+
* later part that lists the same slot object.
|
|
5184
|
+
*/
|
|
5185
|
+
function fillTemplateParts(parts) {
|
|
5186
|
+
const filled = /* @__PURE__ */ new Set();
|
|
5187
|
+
return parts.map((part) => {
|
|
5188
|
+
if (part.kind === "verbatim" || part.slots.length === 0) return part.text;
|
|
5189
|
+
const { slots } = part;
|
|
5190
|
+
const pattern = new RegExp(slots.map((slot) => `(${slot.pattern})`).join("|"), "g");
|
|
5191
|
+
return part.text.replace(pattern, (match, ...groups) => {
|
|
5192
|
+
const slot = slots.find((_, i) => groups[i] !== void 0);
|
|
5193
|
+
if (slot === void 0 || slot.occurrences === "first" && filled.has(slot)) return match;
|
|
5194
|
+
filled.add(slot);
|
|
5195
|
+
return slot.replacement(match);
|
|
5196
|
+
});
|
|
5197
|
+
}).join("");
|
|
5198
|
+
}
|
|
5199
|
+
//#endregion
|
|
5200
|
+
//#region src/modules.ts
|
|
5201
|
+
/**
|
|
5202
|
+
* Load modules and return HTML tags to inject before the closing head tag.
|
|
5203
|
+
*/
|
|
5204
|
+
function loadModules(filenames) {
|
|
5205
|
+
const processed = /* @__PURE__ */ new Set();
|
|
5206
|
+
const headTags = [];
|
|
5207
|
+
for (const filename of filenames) {
|
|
5208
|
+
if (processed.has(filename)) continue;
|
|
5209
|
+
const ext = normalizedFileExt(filename);
|
|
5210
|
+
let tag = null;
|
|
5211
|
+
switch (ext) {
|
|
5212
|
+
case "css":
|
|
5213
|
+
tag = loadModuleTagged("style", filename);
|
|
5214
|
+
break;
|
|
5215
|
+
case "js":
|
|
5216
|
+
tag = loadModuleTagged("script", filename);
|
|
5217
|
+
break;
|
|
5218
|
+
case "otf":
|
|
5219
|
+
case "ttf":
|
|
5220
|
+
case "woff":
|
|
5221
|
+
case "woff2":
|
|
5222
|
+
tag = loadModuleFont(filename);
|
|
5223
|
+
break;
|
|
5224
|
+
default: continue;
|
|
5225
|
+
}
|
|
5226
|
+
if (tag) headTags.push(tag);
|
|
5227
|
+
processed.add(filename);
|
|
5228
|
+
}
|
|
5229
|
+
return headTags.join("\n");
|
|
5230
|
+
}
|
|
5231
|
+
function loadModuleTagged(tag, filename) {
|
|
5232
|
+
const source = readUTF8(filename).trim();
|
|
5233
|
+
if (source.length === 0) return null;
|
|
5234
|
+
return `<${tag} id="${`${tag}-module-${slugify(baseNameWithoutExt(filename))}`}" type="${tag === "script" ? "text/javascript" : "text/css"}">${tag === "script" ? scriptContentEscape(source) : styleContentEscape(source)}</${tag}>`;
|
|
5235
|
+
}
|
|
5236
|
+
function loadModuleFont(filename) {
|
|
5237
|
+
const source = readBase64(filename);
|
|
5238
|
+
const family = baseNameWithoutExt(filename);
|
|
5239
|
+
const idSlug = `style-module-${slugify(family)}`;
|
|
5240
|
+
const ext = normalizedFileExt(filename);
|
|
5241
|
+
return `<style id="${idSlug}" type="text/css">@font-face {\n\tfont-family: "${family}";\n\tsrc: url("data:${mediaTypeFromExt(ext)};base64,${source}") format("${fontFormatHint(ext)}");\n}</style>`;
|
|
5242
|
+
}
|
|
5243
|
+
/**
|
|
5244
|
+
* Load the module tags and head file content to inject before the closing head tag, joined by newlines,
|
|
5245
|
+
* or `''` when there is nothing to inject.
|
|
5246
|
+
*/
|
|
5247
|
+
function loadHeadContent(modulePaths, headFile, diagnostics) {
|
|
5248
|
+
const parts = [];
|
|
5249
|
+
if (modulePaths.length > 0) {
|
|
5250
|
+
const modules = loadModules(modulePaths).trim();
|
|
5251
|
+
if (modules.length > 0) parts.push(modules);
|
|
5252
|
+
}
|
|
5253
|
+
if (headFile) try {
|
|
5254
|
+
const source = readUTF8(headFile).trim();
|
|
5255
|
+
if (source.length > 0) parts.push(source);
|
|
5256
|
+
} catch (e) {
|
|
5257
|
+
diagnostics?.push({
|
|
5258
|
+
level: "warning",
|
|
5259
|
+
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
5260
|
+
});
|
|
5261
|
+
}
|
|
5262
|
+
return parts.join("\n");
|
|
5263
|
+
}
|
|
5264
|
+
/**
|
|
5265
|
+
* The template slot that inserts `content` on its own line before the first closing head tag (as Tweego does),
|
|
5266
|
+
* or `undefined` when `content` is empty. The tag may use any letter case and whitespace before its `>`, and is
|
|
5267
|
+
* kept as written.
|
|
5268
|
+
*/
|
|
5269
|
+
function headSlot(content) {
|
|
5270
|
+
if (content.length === 0) return void 0;
|
|
5271
|
+
return {
|
|
5272
|
+
pattern: CLOSING_HEAD_TAG,
|
|
5273
|
+
occurrences: "first",
|
|
5274
|
+
replacement: (tag) => `${content}\n${tag}`
|
|
5275
|
+
};
|
|
5276
|
+
}
|
|
5277
|
+
//#endregion
|
|
5135
5278
|
//#region src/version.ts
|
|
5136
5279
|
/**
|
|
5137
5280
|
* The twee-ts version, read at runtime from the package's own package.json.
|
|
@@ -5165,24 +5308,38 @@ const CREATOR_NAME$2 = "Twee-ts";
|
|
|
5165
5308
|
function toTwine2Archive(story, startName, options) {
|
|
5166
5309
|
return getTwine2DataChunk(story, startName, options) + "\n";
|
|
5167
5310
|
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Fill the Twine 2 format template: every `{{STORY_NAME}}` gets the escaped story name and the first
|
|
5313
|
+
* `{{STORY_DATA}}` the story data (as Tweego does), and `head` goes before the first closing head tag. All three are
|
|
5314
|
+
* found in the template in one pass, so a story name, passage text or head content holding a placeholder or a
|
|
5315
|
+
* closing head tag stays literal.
|
|
5316
|
+
*/
|
|
5168
5317
|
function toTwine2HTML(story, format, startName, options) {
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5318
|
+
const name = htmlEscape(story.name);
|
|
5319
|
+
const head = headSlot(options?.head ?? "");
|
|
5320
|
+
const slots = [
|
|
5321
|
+
{
|
|
5322
|
+
pattern: literal("{{STORY_NAME}}"),
|
|
5323
|
+
occurrences: "all",
|
|
5324
|
+
replacement: () => name
|
|
5325
|
+
},
|
|
5326
|
+
{
|
|
5327
|
+
pattern: literal("{{STORY_DATA}}"),
|
|
5328
|
+
occurrences: "first",
|
|
5329
|
+
replacement: () => {
|
|
5330
|
+
return getTwine2DataChunk({
|
|
5331
|
+
...story,
|
|
5332
|
+
twine2: {
|
|
5333
|
+
...story.twine2,
|
|
5334
|
+
format: format.name,
|
|
5335
|
+
formatVersion: format.version
|
|
5336
|
+
}
|
|
5337
|
+
}, startName, options);
|
|
5181
5338
|
}
|
|
5182
|
-
},
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
return
|
|
5339
|
+
},
|
|
5340
|
+
...head === void 0 ? [] : [head]
|
|
5341
|
+
];
|
|
5342
|
+
return fillTemplate(readFormatSource(format), slots);
|
|
5186
5343
|
}
|
|
5187
5344
|
const OMITTING_TAGS = [
|
|
5188
5345
|
"Twine.private",
|
|
@@ -5275,7 +5432,13 @@ function toTwine1Archive(story, _startName) {
|
|
|
5275
5432
|
const { data, count } = getTwine1PassageChunk(story);
|
|
5276
5433
|
return `<div id="storeArea" data-size="${count}">${data}</div>\n`;
|
|
5277
5434
|
}
|
|
5278
|
-
|
|
5435
|
+
/**
|
|
5436
|
+
* Fill the Twine 1 format template. The format's components are inserted first, one after another, as Tweego and
|
|
5437
|
+
* Twine 1 do. Then the first of each story placeholder (`"VERSION"`, `"TIME"`, `"START_AT"`, `"STORY_SIZE"`,
|
|
5438
|
+
* `"STORY"`), the IFID comment and `head` (before the first closing head tag) are found in one pass, so a start
|
|
5439
|
+
* passage name or story data holding a placeholder or a closing head tag stays literal.
|
|
5440
|
+
*/
|
|
5441
|
+
function toTwine1HTML(story, format, startName, options) {
|
|
5279
5442
|
const formatDir = dirname(format.filename);
|
|
5280
5443
|
const parentDir = dirname(formatDir);
|
|
5281
5444
|
let template = readFormatSource(format);
|
|
@@ -5286,28 +5449,60 @@ function toTwine1HTML(story, format, startName) {
|
|
|
5286
5449
|
template = tryReplaceComponent(template, "\"JONAH\"", join(formatDir, "code.js"), true);
|
|
5287
5450
|
if (story.twine1.settings.get("jquery") === "on") template = tryReplaceComponent(template, "\"JQUERY\"", join(parentDir, "jquery.js"), true);
|
|
5288
5451
|
if (story.twine1.settings.get("modernizr") === "on") template = tryReplaceComponent(template, "\"MODERNIZR\"", join(parentDir, "modernizr.js"), true);
|
|
5452
|
+
const isPre14 = !template.includes("\"STORY\"");
|
|
5453
|
+
const footer = isPre14 ? readFooter(formatDir) : "";
|
|
5454
|
+
const storeArea = (template + footer).includes("<div id=\"store-area\"") ? "<div id=\"store-area\"" : "<div id=\"storeArea\"";
|
|
5455
|
+
const safeIfid = htmlCommentSanitize(story.ifid);
|
|
5456
|
+
const late = [story.ifid ? {
|
|
5457
|
+
pattern: literal(storeArea),
|
|
5458
|
+
occurrences: "first",
|
|
5459
|
+
replacement: (div) => `<!-- UUID://${safeIfid}// -->${div}`
|
|
5460
|
+
} : void 0, headSlot(options?.head ?? "")].filter((slot) => slot !== void 0);
|
|
5289
5461
|
const displayStart = startName === "Start" ? "" : startName;
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5462
|
+
const slots = [
|
|
5463
|
+
firstSlot("\"VERSION\"", `Compiled with ${CREATOR_NAME$1}, ${VERSION}`),
|
|
5464
|
+
firstSlot("\"TIME\"", `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`),
|
|
5465
|
+
firstSlot("\"START_AT\"", `"${jsStringEscape(displayStart)}"`),
|
|
5466
|
+
firstSlot("\"STORY_SIZE\"", `"${count}"`),
|
|
5467
|
+
firstSlot("\"STORY\"", data),
|
|
5468
|
+
...late
|
|
5469
|
+
];
|
|
5470
|
+
return fillTemplateParts(isPre14 ? [
|
|
5471
|
+
{
|
|
5472
|
+
kind: "scan",
|
|
5473
|
+
text: template,
|
|
5474
|
+
slots
|
|
5475
|
+
},
|
|
5476
|
+
{
|
|
5477
|
+
kind: "verbatim",
|
|
5478
|
+
text: data
|
|
5479
|
+
},
|
|
5480
|
+
{
|
|
5481
|
+
kind: "scan",
|
|
5482
|
+
text: footer,
|
|
5483
|
+
slots: late
|
|
5484
|
+
}
|
|
5485
|
+
] : [{
|
|
5486
|
+
kind: "scan",
|
|
5487
|
+
text: template,
|
|
5488
|
+
slots
|
|
5489
|
+
}]);
|
|
5490
|
+
}
|
|
5491
|
+
/** A slot replacing the first `token` with `value`. */
|
|
5492
|
+
function firstSlot(token, value) {
|
|
5493
|
+
return {
|
|
5494
|
+
pattern: literal(token),
|
|
5495
|
+
occurrences: "first",
|
|
5496
|
+
replacement: () => value
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
/** The footer of a pre-1.4 format, or the default one when the format has none. */
|
|
5500
|
+
function readFooter(formatDir) {
|
|
5501
|
+
try {
|
|
5502
|
+
return readFileSync(join(formatDir, "footer.html"), "utf-8");
|
|
5503
|
+
} catch {
|
|
5504
|
+
return "</div>\n</body>\n</html>\n";
|
|
5309
5505
|
}
|
|
5310
|
-
return template;
|
|
5311
5506
|
}
|
|
5312
5507
|
/**
|
|
5313
5508
|
* Why Twine 1 output leaves a passage out of its tiddlers, or `undefined` when the passage is emitted.
|
|
@@ -5380,75 +5575,6 @@ function isStoryDataJSON(text) {
|
|
|
5380
5575
|
return unmarshalStoryData(createStory(), text) === null;
|
|
5381
5576
|
}
|
|
5382
5577
|
//#endregion
|
|
5383
|
-
//#region src/modules.ts
|
|
5384
|
-
/**
|
|
5385
|
-
* Load modules and return HTML tags to inject before </head>.
|
|
5386
|
-
*/
|
|
5387
|
-
function loadModules(filenames) {
|
|
5388
|
-
const processed = /* @__PURE__ */ new Set();
|
|
5389
|
-
const headTags = [];
|
|
5390
|
-
for (const filename of filenames) {
|
|
5391
|
-
if (processed.has(filename)) continue;
|
|
5392
|
-
const ext = normalizedFileExt(filename);
|
|
5393
|
-
let tag = null;
|
|
5394
|
-
switch (ext) {
|
|
5395
|
-
case "css":
|
|
5396
|
-
tag = loadModuleTagged("style", filename);
|
|
5397
|
-
break;
|
|
5398
|
-
case "js":
|
|
5399
|
-
tag = loadModuleTagged("script", filename);
|
|
5400
|
-
break;
|
|
5401
|
-
case "otf":
|
|
5402
|
-
case "ttf":
|
|
5403
|
-
case "woff":
|
|
5404
|
-
case "woff2":
|
|
5405
|
-
tag = loadModuleFont(filename);
|
|
5406
|
-
break;
|
|
5407
|
-
default: continue;
|
|
5408
|
-
}
|
|
5409
|
-
if (tag) headTags.push(tag);
|
|
5410
|
-
processed.add(filename);
|
|
5411
|
-
}
|
|
5412
|
-
return headTags.join("\n");
|
|
5413
|
-
}
|
|
5414
|
-
function loadModuleTagged(tag, filename) {
|
|
5415
|
-
const source = readUTF8(filename).trim();
|
|
5416
|
-
if (source.length === 0) return null;
|
|
5417
|
-
return `<${tag} id="${`${tag}-module-${slugify(baseNameWithoutExt(filename))}`}" type="${tag === "script" ? "text/javascript" : "text/css"}">${tag === "script" ? scriptContentEscape(source) : styleContentEscape(source)}</${tag}>`;
|
|
5418
|
-
}
|
|
5419
|
-
function loadModuleFont(filename) {
|
|
5420
|
-
const source = readBase64(filename);
|
|
5421
|
-
const family = baseNameWithoutExt(filename);
|
|
5422
|
-
const idSlug = `style-module-${slugify(family)}`;
|
|
5423
|
-
const ext = normalizedFileExt(filename);
|
|
5424
|
-
return `<style id="${idSlug}" type="text/css">@font-face {\n\tfont-family: "${family}";\n\tsrc: url("data:${mediaTypeFromExt(ext)};base64,${source}") format("${fontFormatHint(ext)}");\n}</style>`;
|
|
5425
|
-
}
|
|
5426
|
-
/**
|
|
5427
|
-
* Inject modules and head file content before </head>.
|
|
5428
|
-
*/
|
|
5429
|
-
function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
5430
|
-
const parts = [];
|
|
5431
|
-
if (modulePaths.length > 0) {
|
|
5432
|
-
const modules = loadModules(modulePaths).trim();
|
|
5433
|
-
if (modules.length > 0) parts.push(modules);
|
|
5434
|
-
}
|
|
5435
|
-
if (headFile) try {
|
|
5436
|
-
const source = readUTF8(headFile).trim();
|
|
5437
|
-
if (source.length > 0) parts.push(source);
|
|
5438
|
-
} catch (e) {
|
|
5439
|
-
diagnostics?.push({
|
|
5440
|
-
level: "warning",
|
|
5441
|
-
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
5442
|
-
});
|
|
5443
|
-
}
|
|
5444
|
-
if (parts.length > 0) {
|
|
5445
|
-
parts.push("</head>");
|
|
5446
|
-
const replacement = parts.join("\n");
|
|
5447
|
-
return html.replace("</head>", () => replacement);
|
|
5448
|
-
}
|
|
5449
|
-
return html;
|
|
5450
|
-
}
|
|
5451
|
-
//#endregion
|
|
5452
5578
|
//#region src/passage-omission.ts
|
|
5453
5579
|
/**
|
|
5454
5580
|
* Why the `target` output leaves `passage` out of its passage elements, or `undefined` when it
|
|
@@ -5511,12 +5637,13 @@ var TweeTsError = class extends Error {
|
|
|
5511
5637
|
* Builds as compileToFile() does for `outFile`, without writing it: the output may sit
|
|
5512
5638
|
* inside a source folder, so its last build is left out of the sources and modules and
|
|
5513
5639
|
* never read back. The caller decides whether the result is written. With no `outFile`
|
|
5514
|
-
* (output to stdout, or none), this is compile().
|
|
5640
|
+
* (output to stdout, or none), this is compile(). With `cache`, files are cached as
|
|
5641
|
+
* compileIncremental() caches them.
|
|
5515
5642
|
*
|
|
5516
|
-
* Internal, for the CLI; not part of the public API.
|
|
5643
|
+
* Internal, for the CLI and the bundler plugins; not part of the public API.
|
|
5517
5644
|
*/
|
|
5518
|
-
async function compileForOutputFile(options, outFile) {
|
|
5519
|
-
return buildOutput(options,
|
|
5645
|
+
async function compileForOutputFile(options, outFile, cache) {
|
|
5646
|
+
return buildOutput(options, cache, void 0, outFile);
|
|
5520
5647
|
}
|
|
5521
5648
|
/**
|
|
5522
5649
|
* watch(), writing to `outFile` only the builds `shouldWrite` accepts. A rejected build
|
|
@@ -5543,17 +5670,62 @@ async function watchWithWriteFilter(options, shouldWrite) {
|
|
|
5543
5670
|
const abs = resolve(filename);
|
|
5544
5671
|
return !notExcludable.some((root) => abs === root || abs.startsWith(root + sep));
|
|
5545
5672
|
};
|
|
5673
|
+
let building = false;
|
|
5674
|
+
let queued;
|
|
5675
|
+
const deliver = (outcome) => {
|
|
5676
|
+
try {
|
|
5677
|
+
if (!outcome.ok) throw outcome.error;
|
|
5678
|
+
if (shouldWrite(outcome.result)) writeFileSync(options.outFile, outcome.result.output, "utf-8");
|
|
5679
|
+
options.onBuild?.(outcome.result);
|
|
5680
|
+
} catch (e) {
|
|
5681
|
+
options.onError?.(toError(e));
|
|
5682
|
+
}
|
|
5683
|
+
};
|
|
5684
|
+
const drain = async (first) => {
|
|
5685
|
+
building = true;
|
|
5686
|
+
try {
|
|
5687
|
+
let request = first;
|
|
5688
|
+
while (request !== void 0) {
|
|
5689
|
+
const current = request;
|
|
5690
|
+
const outcome = await buildOutput(options, cache, current.changedFiles, options.outFile).then((result) => ({
|
|
5691
|
+
ok: true,
|
|
5692
|
+
result
|
|
5693
|
+
}), (e) => ({
|
|
5694
|
+
ok: false,
|
|
5695
|
+
error: toError(e)
|
|
5696
|
+
}));
|
|
5697
|
+
if (controller.signal.aborted) return;
|
|
5698
|
+
request = queued === void 0 ? void 0 : mergeBuildRequests(current, queued);
|
|
5699
|
+
queued = void 0;
|
|
5700
|
+
if (request === void 0) deliver(outcome);
|
|
5701
|
+
}
|
|
5702
|
+
} finally {
|
|
5703
|
+
building = false;
|
|
5704
|
+
}
|
|
5705
|
+
};
|
|
5546
5706
|
const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
}
|
|
5707
|
+
if (controller.signal.aborted) return;
|
|
5708
|
+
const request = { changedFiles };
|
|
5709
|
+
if (building) {
|
|
5710
|
+
queued = queued === void 0 ? request : mergeBuildRequests(queued, request);
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5713
|
+
drain(request);
|
|
5553
5714
|
}, ignore);
|
|
5554
|
-
controller.signal.addEventListener("abort", () =>
|
|
5715
|
+
controller.signal.addEventListener("abort", () => {
|
|
5716
|
+
queued = void 0;
|
|
5717
|
+
handle.close();
|
|
5718
|
+
});
|
|
5555
5719
|
return controller;
|
|
5556
5720
|
}
|
|
5721
|
+
/** One build covering both requests: a full build if either is one, else every changed file. */
|
|
5722
|
+
function mergeBuildRequests(a, b) {
|
|
5723
|
+
if (a.changedFiles === void 0 || b.changedFiles === void 0) return { changedFiles: void 0 };
|
|
5724
|
+
return { changedFiles: /* @__PURE__ */ new Set([...a.changedFiles, ...b.changedFiles]) };
|
|
5725
|
+
}
|
|
5726
|
+
function toError(e) {
|
|
5727
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
5728
|
+
}
|
|
5557
5729
|
/**
|
|
5558
5730
|
* `outFile`: the file the build is written to, which source and module discovery
|
|
5559
5731
|
* skip (as Tweego does), so an output inside a source folder is never loaded back.
|
|
@@ -5615,17 +5787,17 @@ async function buildOutput(options, cache, changedFiles, outFile) {
|
|
|
5615
5787
|
case "html": {
|
|
5616
5788
|
diagnostics.push(...startPassageDiagnostics(story, startName, format?.isTwine2 === false ? "twine1" : "twine2"));
|
|
5617
5789
|
if (!format) throw new TweeTsError("No story format available for HTML output.", diagnostics);
|
|
5618
|
-
if (format.isTwine2
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
message: "Special passage \"StoryTitle\" not found."
|
|
5623
|
-
});
|
|
5624
|
-
output = toTwine1HTML(story, format, startName);
|
|
5625
|
-
}
|
|
5790
|
+
if (!format.isTwine2 && story.name === "" && !storyHas(story, "StoryTitle")) diagnostics.push({
|
|
5791
|
+
level: "error",
|
|
5792
|
+
message: "Special passage \"StoryTitle\" not found."
|
|
5793
|
+
});
|
|
5626
5794
|
const modules = getFilenames(options.modules ?? [], outFile);
|
|
5627
5795
|
diagnostics.push(...modules.diagnostics);
|
|
5628
|
-
|
|
5796
|
+
const head = loadHeadContent(modules.filenames, options.headFile, diagnostics);
|
|
5797
|
+
output = format.isTwine2 ? toTwine2HTML(story, format, startName, {
|
|
5798
|
+
sourceInfo,
|
|
5799
|
+
head
|
|
5800
|
+
}) : toTwine1HTML(story, format, startName, { head });
|
|
5629
5801
|
break;
|
|
5630
5802
|
}
|
|
5631
5803
|
default: throw new TweeTsError(`Unhandled output mode: ${outputMode}`, diagnostics);
|
|
@@ -7126,6 +7298,16 @@ function passageLinks(p) {
|
|
|
7126
7298
|
return extractLinksFromText(textAsRead(p), false);
|
|
7127
7299
|
}
|
|
7128
7300
|
/**
|
|
7301
|
+
* Whether the `target` output keeps the links in `p`: always without a target, which describes
|
|
7302
|
+
* every source passage. With one, a passage the output leaves out never reaches the player, so it
|
|
7303
|
+
* links nowhere, except a script passage, which Twine 2 output leaves out as a passage but runs.
|
|
7304
|
+
*/
|
|
7305
|
+
function readsLinks(story, p, { target }) {
|
|
7306
|
+
if (target === void 0) return true;
|
|
7307
|
+
const omission = passageOmission(story, p, target);
|
|
7308
|
+
return omission === void 0 || omission.kind === "tag" && omission.tag === "script";
|
|
7309
|
+
}
|
|
7310
|
+
/**
|
|
7129
7311
|
* Each passage name, mapped to why the `target` output leaves that passage out, or to `undefined`
|
|
7130
7312
|
* when a link can reach it: always without a target, and otherwise when the output emits a
|
|
7131
7313
|
* passage of that name.
|
|
@@ -7183,7 +7365,9 @@ function joinLines(text) {
|
|
|
7183
7365
|
* By default, a link is broken only when no passage has its name. Pass a `target` to check links
|
|
7184
7366
|
* against what that output emits: `storyInspect(result.story, { target: 'twine2' })` also reports
|
|
7185
7367
|
* links to passages Twine 2 output leaves out, such as script, stylesheet and `Twine.private`
|
|
7186
|
-
* passages.
|
|
7368
|
+
* passages. With a target, passages that output leaves out also give no links (script passages
|
|
7369
|
+
* excepted, as Twine 2 output runs them), so they report no broken links and keep no passage from
|
|
7370
|
+
* being an orphan.
|
|
7187
7371
|
*/
|
|
7188
7372
|
function storyInspect(story, options = {}) {
|
|
7189
7373
|
const destinations = destinationOmissions(story, options);
|
|
@@ -7208,7 +7392,7 @@ function storyInspect(story, options = {}) {
|
|
|
7208
7392
|
}
|
|
7209
7393
|
list.push(p.name);
|
|
7210
7394
|
}
|
|
7211
|
-
links.set(p.name, passageLinks(p));
|
|
7395
|
+
links.set(p.name, readsLinks(story, p, options) ? passageLinks(p) : []);
|
|
7212
7396
|
}
|
|
7213
7397
|
const brokenLinks = [];
|
|
7214
7398
|
for (const [from, targets] of links) for (const to of targets) {
|
|
@@ -7622,6 +7806,10 @@ async function main() {
|
|
|
7622
7806
|
sourceInfo: values["source-info"] ?? config?.sourceInfo ?? false,
|
|
7623
7807
|
wordCountMethod
|
|
7624
7808
|
};
|
|
7809
|
+
const log = {
|
|
7810
|
+
files: outPath !== void 0 && (values["log-files"] ?? false),
|
|
7811
|
+
stats: values["log-stats"] ?? false
|
|
7812
|
+
};
|
|
7625
7813
|
if (values.watch) {
|
|
7626
7814
|
if (outPath === void 0) {
|
|
7627
7815
|
console.error("Error: Watch mode requires an output file (-o).");
|
|
@@ -7636,6 +7824,7 @@ async function main() {
|
|
|
7636
7824
|
logDiagnostics(result.diagnostics);
|
|
7637
7825
|
const errors = countErrors(result.diagnostics);
|
|
7638
7826
|
if (errors > 0) console.error(`Build has ${pluralize(errors, "error")}; output not written. Still watching for changes.`);
|
|
7827
|
+
logBuild(result, log);
|
|
7639
7828
|
},
|
|
7640
7829
|
onError(error) {
|
|
7641
7830
|
console.error(`Build error: ${error.message}`);
|
|
@@ -7650,10 +7839,13 @@ async function main() {
|
|
|
7650
7839
|
process.exitCode = 1;
|
|
7651
7840
|
} else if (outPath === void 0) process.stdout.write(result.output);
|
|
7652
7841
|
else writeFileSync(outPath, result.output, "utf-8");
|
|
7653
|
-
|
|
7654
|
-
if (values["log-stats"]) logStats(result);
|
|
7842
|
+
logBuild(result, log);
|
|
7655
7843
|
}
|
|
7656
7844
|
}
|
|
7845
|
+
function logBuild(result, log) {
|
|
7846
|
+
if (log.files) console.log(`\nFiles: ${result.stats.files.join(", ")}`);
|
|
7847
|
+
if (log.stats) logStats(result);
|
|
7848
|
+
}
|
|
7657
7849
|
function countErrors(diagnostics) {
|
|
7658
7850
|
return diagnostics.filter((d) => d.level === "error").length;
|
|
7659
7851
|
}
|