@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
|
@@ -13,6 +13,19 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
function generateIFID() {
|
|
14
14
|
return randomUUID().toUpperCase();
|
|
15
15
|
}
|
|
16
|
+
/** Length of the `UUID://...//` wrapper around a 36-character UUID. */
|
|
17
|
+
const WRAPPED_LENGTH = 45;
|
|
18
|
+
/**
|
|
19
|
+
* Bring an IFID into the form the story model stores and every output writes: the uppercase
|
|
20
|
+
* bare UUID. A valid `UUID://...//` wrapped IFID loses its wrapper; outputs that need the wrapper
|
|
21
|
+
* (the Treaty of Babel comment) add exactly one. Twine 2 IFIDs use only capital letters, so the
|
|
22
|
+
* value is uppercased, as Tweego does. An invalid value is only uppercased, so that validation
|
|
23
|
+
* reports it as written.
|
|
24
|
+
*/
|
|
25
|
+
function normalizeIFID(value) {
|
|
26
|
+
const upper = value.toUpperCase();
|
|
27
|
+
return upper.length === WRAPPED_LENGTH && validateIFID(upper) === null ? upper.slice(7, 43) : upper;
|
|
28
|
+
}
|
|
16
29
|
/**
|
|
17
30
|
* Validate an IFID string.
|
|
18
31
|
* Accepts both bare UUIDs and UUID://...// wrapped format.
|
|
@@ -438,7 +451,7 @@ function unmarshalStoryData(story, json) {
|
|
|
438
451
|
}
|
|
439
452
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return "Cannot unmarshal \"StoryData\"; expected a JSON object";
|
|
440
453
|
const data = raw;
|
|
441
|
-
if (typeof data.ifid === "string" && data.ifid) story.ifid = data.ifid
|
|
454
|
+
if (typeof data.ifid === "string" && data.ifid) story.ifid = normalizeIFID(data.ifid);
|
|
442
455
|
if (typeof data.format === "string" && data.format) story.twine2.format = data.format;
|
|
443
456
|
if (typeof data["format-version"] === "string" && data["format-version"]) story.twine2.formatVersion = data["format-version"];
|
|
444
457
|
if (Array.isArray(data.options)) {
|
|
@@ -469,7 +482,7 @@ function unmarshalStorySettings(story, text, diagnostics) {
|
|
|
469
482
|
const val = line.slice(colonIdx + 1).trim().toLowerCase();
|
|
470
483
|
switch (key) {
|
|
471
484
|
case "ifid":
|
|
472
|
-
if (validateIFID(val) === null) story.legacyIFID = val
|
|
485
|
+
if (validateIFID(val) === null) story.legacyIFID = normalizeIFID(val);
|
|
473
486
|
obsolete.push("\"ifid\"");
|
|
474
487
|
continue;
|
|
475
488
|
case "zoom":
|
|
@@ -1018,7 +1031,7 @@ function isSafeSegment(segment) {
|
|
|
1018
1031
|
return segment !== "" && segment !== "." && segment !== ".." && !/[/\\\0]/.test(segment);
|
|
1019
1032
|
}
|
|
1020
1033
|
/** Whether absolute path `target` lies strictly inside absolute path `root`. */
|
|
1021
|
-
function isInside(root, target) {
|
|
1034
|
+
function isInside$1(root, target) {
|
|
1022
1035
|
const rel = relative(root, target);
|
|
1023
1036
|
return rel !== "" && !isAbsolute(rel) && rel.split(sep)[0] !== "..";
|
|
1024
1037
|
}
|
|
@@ -1028,7 +1041,7 @@ function cachedFormatDir(name, version) {
|
|
|
1028
1041
|
if (!isSafeSegment(version) || !parseSemver(version)) throw new Error(`Refusing to cache story format "${name}" with an unsafe version: ${JSON.stringify(version)}`);
|
|
1029
1042
|
const root = resolve(getCacheDir());
|
|
1030
1043
|
const dir = resolve(root, name, version);
|
|
1031
|
-
if (!isInside(root, dir)) throw new Error(`Refusing to cache a story format outside the cache directory ${root}: ${dir}`);
|
|
1044
|
+
if (!isInside$1(root, dir)) throw new Error(`Refusing to cache a story format outside the cache directory ${root}: ${dir}`);
|
|
1032
1045
|
return dir;
|
|
1033
1046
|
}
|
|
1034
1047
|
/** Create `dir` as a plain directory, or check that it already is one (a symlink could lead out of the cache). */
|
|
@@ -4463,7 +4476,7 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4463
4476
|
});
|
|
4464
4477
|
else startnode = parsed;
|
|
4465
4478
|
}
|
|
4466
|
-
if (attrs["ifid"]) story.ifid = attrs["ifid"]
|
|
4479
|
+
if (attrs["ifid"]) story.ifid = normalizeIFID(attrs["ifid"]);
|
|
4467
4480
|
if (attrs["zoom"]) {
|
|
4468
4481
|
const parsed = parseFloat(attrs["zoom"]);
|
|
4469
4482
|
if (Number.isNaN(parsed)) diagnostics.push({
|
|
@@ -4476,7 +4489,9 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4476
4489
|
if (attrs["format"]) story.twine2.format = attrs["format"];
|
|
4477
4490
|
if (attrs["format-version"]) story.twine2.formatVersion = attrs["format-version"];
|
|
4478
4491
|
if (attrs["options"]) for (const opt of attrs["options"].split(/\s+/).filter((s) => s.length > 0)) story.twine2.options.set(opt, true);
|
|
4479
|
-
|
|
4492
|
+
const children = storyData.children ?? [];
|
|
4493
|
+
const takenNames = new Set(children.filter((node) => node.type === "tag" && node.name === "tw-passagedata").map(passageDataName));
|
|
4494
|
+
for (const node of children) {
|
|
4480
4495
|
if (node.type !== "tag" && node.type !== "style" && node.type !== "script") continue;
|
|
4481
4496
|
switch (node.name) {
|
|
4482
4497
|
case "style":
|
|
@@ -4484,8 +4499,10 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4484
4499
|
const content = getTextContent(node);
|
|
4485
4500
|
if (content.trim().length === 0) continue;
|
|
4486
4501
|
const text = passageText(content);
|
|
4502
|
+
const name = freeName(node.name === "style" ? "Story Stylesheet" : "Story JavaScript", takenNames);
|
|
4503
|
+
takenNames.add(name);
|
|
4487
4504
|
storyAdd(story, {
|
|
4488
|
-
name
|
|
4505
|
+
name,
|
|
4489
4506
|
tags: node.name === "style" ? ["stylesheet"] : ["script"],
|
|
4490
4507
|
text
|
|
4491
4508
|
}, diagnostics);
|
|
@@ -4501,7 +4518,7 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4501
4518
|
case "tw-passagedata": {
|
|
4502
4519
|
let pid = 0;
|
|
4503
4520
|
const pAttrs = node.attribs ?? {};
|
|
4504
|
-
const name =
|
|
4521
|
+
const name = passageDataName(node);
|
|
4505
4522
|
const tags = pAttrs["tags"] ? pAttrs["tags"].split(/\s+/).filter((s) => s.length > 0) : [];
|
|
4506
4523
|
const metadata = {};
|
|
4507
4524
|
if (pAttrs["pid"]) {
|
|
@@ -4532,6 +4549,17 @@ function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
|
4532
4549
|
text: marshalStoryData(story)
|
|
4533
4550
|
}, diagnostics);
|
|
4534
4551
|
}
|
|
4552
|
+
function passageDataName(node) {
|
|
4553
|
+
return node.attribs?.["name"] ?? "";
|
|
4554
|
+
}
|
|
4555
|
+
/** The first of `base`, `base 2`, `base 3`, … that is not in `taken`. */
|
|
4556
|
+
function freeName(base, taken) {
|
|
4557
|
+
if (!taken.has(base)) return base;
|
|
4558
|
+
for (let n = 2;; n++) {
|
|
4559
|
+
const candidate = `${base} ${n}`;
|
|
4560
|
+
if (!taken.has(candidate)) return candidate;
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4535
4563
|
function decompileTwine1(storeArea, story, passageText, diagnostics) {
|
|
4536
4564
|
const passages = (storeArea.children ?? []).filter(isTiddler).map((node) => tiddlerToPassage(node, passageText));
|
|
4537
4565
|
const obfuscated = isRot13Obfuscated(passages);
|
|
@@ -4924,6 +4952,121 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
4924
4952
|
}, diagnostics);
|
|
4925
4953
|
}
|
|
4926
4954
|
//#endregion
|
|
4955
|
+
//#region src/template.ts
|
|
4956
|
+
/**
|
|
4957
|
+
* An HTML closing head tag: `</head` in any letter case, followed by whitespace, `/` or `>`, up to the next `>`.
|
|
4958
|
+
* This is how the HTML tokenizer reads an end tag; it ignores whatever sits between the tag name and the `>`.
|
|
4959
|
+
*/
|
|
4960
|
+
const CLOSING_HEAD_TAG = "<\\/[Hh][Ee][Aa][Dd](?=[\\t\\n\\f\\r />])[^>]*>";
|
|
4961
|
+
/** The regular expression source matching exactly `text`. */
|
|
4962
|
+
function literal(text) {
|
|
4963
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
4964
|
+
}
|
|
4965
|
+
/** Replace the `slots` placeholders found in `template`, without scanning inserted text. */
|
|
4966
|
+
function fillTemplate(template, slots) {
|
|
4967
|
+
return fillTemplateParts([{
|
|
4968
|
+
kind: "scan",
|
|
4969
|
+
text: template,
|
|
4970
|
+
slots
|
|
4971
|
+
}]);
|
|
4972
|
+
}
|
|
4973
|
+
/**
|
|
4974
|
+
* Fill the parts of a template in order, joined. A `'first'` slot replaced in one part is not replaced again in a
|
|
4975
|
+
* later part that lists the same slot object.
|
|
4976
|
+
*/
|
|
4977
|
+
function fillTemplateParts(parts) {
|
|
4978
|
+
const filled = /* @__PURE__ */ new Set();
|
|
4979
|
+
return parts.map((part) => {
|
|
4980
|
+
if (part.kind === "verbatim" || part.slots.length === 0) return part.text;
|
|
4981
|
+
const { slots } = part;
|
|
4982
|
+
const pattern = new RegExp(slots.map((slot) => `(${slot.pattern})`).join("|"), "g");
|
|
4983
|
+
return part.text.replace(pattern, (match, ...groups) => {
|
|
4984
|
+
const slot = slots.find((_, i) => groups[i] !== void 0);
|
|
4985
|
+
if (slot === void 0 || slot.occurrences === "first" && filled.has(slot)) return match;
|
|
4986
|
+
filled.add(slot);
|
|
4987
|
+
return slot.replacement(match);
|
|
4988
|
+
});
|
|
4989
|
+
}).join("");
|
|
4990
|
+
}
|
|
4991
|
+
//#endregion
|
|
4992
|
+
//#region src/modules.ts
|
|
4993
|
+
/**
|
|
4994
|
+
* Load modules and return HTML tags to inject before the closing head tag.
|
|
4995
|
+
*/
|
|
4996
|
+
function loadModules(filenames) {
|
|
4997
|
+
const processed = /* @__PURE__ */ new Set();
|
|
4998
|
+
const headTags = [];
|
|
4999
|
+
for (const filename of filenames) {
|
|
5000
|
+
if (processed.has(filename)) continue;
|
|
5001
|
+
const ext = normalizedFileExt(filename);
|
|
5002
|
+
let tag = null;
|
|
5003
|
+
switch (ext) {
|
|
5004
|
+
case "css":
|
|
5005
|
+
tag = loadModuleTagged("style", filename);
|
|
5006
|
+
break;
|
|
5007
|
+
case "js":
|
|
5008
|
+
tag = loadModuleTagged("script", filename);
|
|
5009
|
+
break;
|
|
5010
|
+
case "otf":
|
|
5011
|
+
case "ttf":
|
|
5012
|
+
case "woff":
|
|
5013
|
+
case "woff2":
|
|
5014
|
+
tag = loadModuleFont(filename);
|
|
5015
|
+
break;
|
|
5016
|
+
default: continue;
|
|
5017
|
+
}
|
|
5018
|
+
if (tag) headTags.push(tag);
|
|
5019
|
+
processed.add(filename);
|
|
5020
|
+
}
|
|
5021
|
+
return headTags.join("\n");
|
|
5022
|
+
}
|
|
5023
|
+
function loadModuleTagged(tag, filename) {
|
|
5024
|
+
const source = readUTF8(filename).trim();
|
|
5025
|
+
if (source.length === 0) return null;
|
|
5026
|
+
return `<${tag} id="${`${tag}-module-${slugify(baseNameWithoutExt(filename))}`}" type="${tag === "script" ? "text/javascript" : "text/css"}">${tag === "script" ? scriptContentEscape(source) : styleContentEscape(source)}</${tag}>`;
|
|
5027
|
+
}
|
|
5028
|
+
function loadModuleFont(filename) {
|
|
5029
|
+
const source = readBase64(filename);
|
|
5030
|
+
const family = baseNameWithoutExt(filename);
|
|
5031
|
+
const idSlug = `style-module-${slugify(family)}`;
|
|
5032
|
+
const ext = normalizedFileExt(filename);
|
|
5033
|
+
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>`;
|
|
5034
|
+
}
|
|
5035
|
+
/**
|
|
5036
|
+
* Load the module tags and head file content to inject before the closing head tag, joined by newlines,
|
|
5037
|
+
* or `''` when there is nothing to inject.
|
|
5038
|
+
*/
|
|
5039
|
+
function loadHeadContent(modulePaths, headFile, diagnostics) {
|
|
5040
|
+
const parts = [];
|
|
5041
|
+
if (modulePaths.length > 0) {
|
|
5042
|
+
const modules = loadModules(modulePaths).trim();
|
|
5043
|
+
if (modules.length > 0) parts.push(modules);
|
|
5044
|
+
}
|
|
5045
|
+
if (headFile) try {
|
|
5046
|
+
const source = readUTF8(headFile).trim();
|
|
5047
|
+
if (source.length > 0) parts.push(source);
|
|
5048
|
+
} catch (e) {
|
|
5049
|
+
diagnostics?.push({
|
|
5050
|
+
level: "warning",
|
|
5051
|
+
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
5052
|
+
});
|
|
5053
|
+
}
|
|
5054
|
+
return parts.join("\n");
|
|
5055
|
+
}
|
|
5056
|
+
/**
|
|
5057
|
+
* The template slot that inserts `content` on its own line before the first closing head tag (as Tweego does),
|
|
5058
|
+
* or `undefined` when `content` is empty. The tag may use any letter case and whitespace before its `>`, and is
|
|
5059
|
+
* kept as written.
|
|
5060
|
+
*/
|
|
5061
|
+
function headSlot(content) {
|
|
5062
|
+
if (content.length === 0) return void 0;
|
|
5063
|
+
return {
|
|
5064
|
+
pattern: CLOSING_HEAD_TAG,
|
|
5065
|
+
occurrences: "first",
|
|
5066
|
+
replacement: (tag) => `${content}\n${tag}`
|
|
5067
|
+
};
|
|
5068
|
+
}
|
|
5069
|
+
//#endregion
|
|
4927
5070
|
//#region src/version.ts
|
|
4928
5071
|
/**
|
|
4929
5072
|
* The twee-ts version, read at runtime from the package's own package.json.
|
|
@@ -4957,24 +5100,38 @@ const CREATOR_NAME$2 = "Twee-ts";
|
|
|
4957
5100
|
function toTwine2Archive(story, startName, options) {
|
|
4958
5101
|
return getTwine2DataChunk(story, startName, options) + "\n";
|
|
4959
5102
|
}
|
|
5103
|
+
/**
|
|
5104
|
+
* Fill the Twine 2 format template: every `{{STORY_NAME}}` gets the escaped story name and the first
|
|
5105
|
+
* `{{STORY_DATA}}` the story data (as Tweego does), and `head` goes before the first closing head tag. All three are
|
|
5106
|
+
* found in the template in one pass, so a story name, passage text or head content holding a placeholder or a
|
|
5107
|
+
* closing head tag stays literal.
|
|
5108
|
+
*/
|
|
4960
5109
|
function toTwine2HTML(story, format, startName, options) {
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
5110
|
+
const name = htmlEscape(story.name);
|
|
5111
|
+
const head = headSlot(options?.head ?? "");
|
|
5112
|
+
const slots = [
|
|
5113
|
+
{
|
|
5114
|
+
pattern: literal("{{STORY_NAME}}"),
|
|
5115
|
+
occurrences: "all",
|
|
5116
|
+
replacement: () => name
|
|
5117
|
+
},
|
|
5118
|
+
{
|
|
5119
|
+
pattern: literal("{{STORY_DATA}}"),
|
|
5120
|
+
occurrences: "first",
|
|
5121
|
+
replacement: () => {
|
|
5122
|
+
return getTwine2DataChunk({
|
|
5123
|
+
...story,
|
|
5124
|
+
twine2: {
|
|
5125
|
+
...story.twine2,
|
|
5126
|
+
format: format.name,
|
|
5127
|
+
formatVersion: format.version
|
|
5128
|
+
}
|
|
5129
|
+
}, startName, options);
|
|
4973
5130
|
}
|
|
4974
|
-
},
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
return
|
|
5131
|
+
},
|
|
5132
|
+
...head === void 0 ? [] : [head]
|
|
5133
|
+
];
|
|
5134
|
+
return fillTemplate(readFormatSource(format), slots);
|
|
4978
5135
|
}
|
|
4979
5136
|
const OMITTING_TAGS = [
|
|
4980
5137
|
"Twine.private",
|
|
@@ -5067,7 +5224,13 @@ function toTwine1Archive(story, _startName) {
|
|
|
5067
5224
|
const { data, count } = getTwine1PassageChunk(story);
|
|
5068
5225
|
return `<div id="storeArea" data-size="${count}">${data}</div>\n`;
|
|
5069
5226
|
}
|
|
5070
|
-
|
|
5227
|
+
/**
|
|
5228
|
+
* Fill the Twine 1 format template. The format's components are inserted first, one after another, as Tweego and
|
|
5229
|
+
* Twine 1 do. Then the first of each story placeholder (`"VERSION"`, `"TIME"`, `"START_AT"`, `"STORY_SIZE"`,
|
|
5230
|
+
* `"STORY"`), the IFID comment and `head` (before the first closing head tag) are found in one pass, so a start
|
|
5231
|
+
* passage name or story data holding a placeholder or a closing head tag stays literal.
|
|
5232
|
+
*/
|
|
5233
|
+
function toTwine1HTML(story, format, startName, options) {
|
|
5071
5234
|
const formatDir = dirname(format.filename);
|
|
5072
5235
|
const parentDir = dirname(formatDir);
|
|
5073
5236
|
let template = readFormatSource(format);
|
|
@@ -5078,28 +5241,60 @@ function toTwine1HTML(story, format, startName) {
|
|
|
5078
5241
|
template = tryReplaceComponent(template, "\"JONAH\"", join(formatDir, "code.js"), true);
|
|
5079
5242
|
if (story.twine1.settings.get("jquery") === "on") template = tryReplaceComponent(template, "\"JQUERY\"", join(parentDir, "jquery.js"), true);
|
|
5080
5243
|
if (story.twine1.settings.get("modernizr") === "on") template = tryReplaceComponent(template, "\"MODERNIZR\"", join(parentDir, "modernizr.js"), true);
|
|
5244
|
+
const isPre14 = !template.includes("\"STORY\"");
|
|
5245
|
+
const footer = isPre14 ? readFooter(formatDir) : "";
|
|
5246
|
+
const storeArea = (template + footer).includes("<div id=\"store-area\"") ? "<div id=\"store-area\"" : "<div id=\"storeArea\"";
|
|
5247
|
+
const safeIfid = htmlCommentSanitize(story.ifid);
|
|
5248
|
+
const late = [story.ifid ? {
|
|
5249
|
+
pattern: literal(storeArea),
|
|
5250
|
+
occurrences: "first",
|
|
5251
|
+
replacement: (div) => `<!-- UUID://${safeIfid}// -->${div}`
|
|
5252
|
+
} : void 0, headSlot(options?.head ?? "")].filter((slot) => slot !== void 0);
|
|
5081
5253
|
const displayStart = startName === "Start" ? "" : startName;
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5254
|
+
const slots = [
|
|
5255
|
+
firstSlot("\"VERSION\"", `Compiled with ${CREATOR_NAME$1}, ${VERSION}`),
|
|
5256
|
+
firstSlot("\"TIME\"", `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`),
|
|
5257
|
+
firstSlot("\"START_AT\"", `"${jsStringEscape(displayStart)}"`),
|
|
5258
|
+
firstSlot("\"STORY_SIZE\"", `"${count}"`),
|
|
5259
|
+
firstSlot("\"STORY\"", data),
|
|
5260
|
+
...late
|
|
5261
|
+
];
|
|
5262
|
+
return fillTemplateParts(isPre14 ? [
|
|
5263
|
+
{
|
|
5264
|
+
kind: "scan",
|
|
5265
|
+
text: template,
|
|
5266
|
+
slots
|
|
5267
|
+
},
|
|
5268
|
+
{
|
|
5269
|
+
kind: "verbatim",
|
|
5270
|
+
text: data
|
|
5271
|
+
},
|
|
5272
|
+
{
|
|
5273
|
+
kind: "scan",
|
|
5274
|
+
text: footer,
|
|
5275
|
+
slots: late
|
|
5094
5276
|
}
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5277
|
+
] : [{
|
|
5278
|
+
kind: "scan",
|
|
5279
|
+
text: template,
|
|
5280
|
+
slots
|
|
5281
|
+
}]);
|
|
5282
|
+
}
|
|
5283
|
+
/** A slot replacing the first `token` with `value`. */
|
|
5284
|
+
function firstSlot(token, value) {
|
|
5285
|
+
return {
|
|
5286
|
+
pattern: literal(token),
|
|
5287
|
+
occurrences: "first",
|
|
5288
|
+
replacement: () => value
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
/** The footer of a pre-1.4 format, or the default one when the format has none. */
|
|
5292
|
+
function readFooter(formatDir) {
|
|
5293
|
+
try {
|
|
5294
|
+
return readFileSync(join(formatDir, "footer.html"), "utf-8");
|
|
5295
|
+
} catch {
|
|
5296
|
+
return "</div>\n</body>\n</html>\n";
|
|
5101
5297
|
}
|
|
5102
|
-
return template;
|
|
5103
5298
|
}
|
|
5104
5299
|
/**
|
|
5105
5300
|
* Why Twine 1 output leaves a passage out of its tiddlers, or `undefined` when the passage is emitted.
|
|
@@ -5172,75 +5367,6 @@ function isStoryDataJSON(text) {
|
|
|
5172
5367
|
return unmarshalStoryData(createStory(), text) === null;
|
|
5173
5368
|
}
|
|
5174
5369
|
//#endregion
|
|
5175
|
-
//#region src/modules.ts
|
|
5176
|
-
/**
|
|
5177
|
-
* Load modules and return HTML tags to inject before </head>.
|
|
5178
|
-
*/
|
|
5179
|
-
function loadModules(filenames) {
|
|
5180
|
-
const processed = /* @__PURE__ */ new Set();
|
|
5181
|
-
const headTags = [];
|
|
5182
|
-
for (const filename of filenames) {
|
|
5183
|
-
if (processed.has(filename)) continue;
|
|
5184
|
-
const ext = normalizedFileExt(filename);
|
|
5185
|
-
let tag = null;
|
|
5186
|
-
switch (ext) {
|
|
5187
|
-
case "css":
|
|
5188
|
-
tag = loadModuleTagged("style", filename);
|
|
5189
|
-
break;
|
|
5190
|
-
case "js":
|
|
5191
|
-
tag = loadModuleTagged("script", filename);
|
|
5192
|
-
break;
|
|
5193
|
-
case "otf":
|
|
5194
|
-
case "ttf":
|
|
5195
|
-
case "woff":
|
|
5196
|
-
case "woff2":
|
|
5197
|
-
tag = loadModuleFont(filename);
|
|
5198
|
-
break;
|
|
5199
|
-
default: continue;
|
|
5200
|
-
}
|
|
5201
|
-
if (tag) headTags.push(tag);
|
|
5202
|
-
processed.add(filename);
|
|
5203
|
-
}
|
|
5204
|
-
return headTags.join("\n");
|
|
5205
|
-
}
|
|
5206
|
-
function loadModuleTagged(tag, filename) {
|
|
5207
|
-
const source = readUTF8(filename).trim();
|
|
5208
|
-
if (source.length === 0) return null;
|
|
5209
|
-
return `<${tag} id="${`${tag}-module-${slugify(baseNameWithoutExt(filename))}`}" type="${tag === "script" ? "text/javascript" : "text/css"}">${tag === "script" ? scriptContentEscape(source) : styleContentEscape(source)}</${tag}>`;
|
|
5210
|
-
}
|
|
5211
|
-
function loadModuleFont(filename) {
|
|
5212
|
-
const source = readBase64(filename);
|
|
5213
|
-
const family = baseNameWithoutExt(filename);
|
|
5214
|
-
const idSlug = `style-module-${slugify(family)}`;
|
|
5215
|
-
const ext = normalizedFileExt(filename);
|
|
5216
|
-
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>`;
|
|
5217
|
-
}
|
|
5218
|
-
/**
|
|
5219
|
-
* Inject modules and head file content before </head>.
|
|
5220
|
-
*/
|
|
5221
|
-
function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
5222
|
-
const parts = [];
|
|
5223
|
-
if (modulePaths.length > 0) {
|
|
5224
|
-
const modules = loadModules(modulePaths).trim();
|
|
5225
|
-
if (modules.length > 0) parts.push(modules);
|
|
5226
|
-
}
|
|
5227
|
-
if (headFile) try {
|
|
5228
|
-
const source = readUTF8(headFile).trim();
|
|
5229
|
-
if (source.length > 0) parts.push(source);
|
|
5230
|
-
} catch (e) {
|
|
5231
|
-
diagnostics?.push({
|
|
5232
|
-
level: "warning",
|
|
5233
|
-
message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
|
|
5234
|
-
});
|
|
5235
|
-
}
|
|
5236
|
-
if (parts.length > 0) {
|
|
5237
|
-
parts.push("</head>");
|
|
5238
|
-
const replacement = parts.join("\n");
|
|
5239
|
-
return html.replace("</head>", () => replacement);
|
|
5240
|
-
}
|
|
5241
|
-
return html;
|
|
5242
|
-
}
|
|
5243
|
-
//#endregion
|
|
5244
5370
|
//#region src/passage-omission.ts
|
|
5245
5371
|
/**
|
|
5246
5372
|
* Why the `target` output leaves `passage` out of its passage elements, or `undefined` when it
|
|
@@ -5295,22 +5421,16 @@ var TweeTsError = class extends Error {
|
|
|
5295
5421
|
}
|
|
5296
5422
|
};
|
|
5297
5423
|
/**
|
|
5298
|
-
*
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
/**
|
|
5304
|
-
* Compile with incremental caching support.
|
|
5305
|
-
* Plugins and advanced users can manage their own cache and changed-file tracking.
|
|
5424
|
+
* Builds as compileToFile() does for `outFile`, without writing it: the output may sit
|
|
5425
|
+
* inside a source folder, so its last build is left out of the sources and modules and
|
|
5426
|
+
* never read back. The caller decides whether the result is written. With no `outFile`
|
|
5427
|
+
* (output to stdout, or none), this is compile(). With `cache`, files are cached as
|
|
5428
|
+
* compileIncremental() caches them.
|
|
5306
5429
|
*
|
|
5307
|
-
*
|
|
5308
|
-
* With it, a file it names (by the path source discovery gives it, relative to the working
|
|
5309
|
-
* directory) is always reparsed, whatever its modification time, and every other cached
|
|
5310
|
-
* file is reused as it is.
|
|
5430
|
+
* Internal, for the CLI and the bundler plugins; not part of the public API.
|
|
5311
5431
|
*/
|
|
5312
|
-
async function
|
|
5313
|
-
return buildOutput(options, cache,
|
|
5432
|
+
async function compileForOutputFile(options, outFile, cache) {
|
|
5433
|
+
return buildOutput(options, cache, void 0, outFile);
|
|
5314
5434
|
}
|
|
5315
5435
|
/**
|
|
5316
5436
|
* `outFile`: the file the build is written to, which source and module discovery
|
|
@@ -5373,17 +5493,17 @@ async function buildOutput(options, cache, changedFiles, outFile) {
|
|
|
5373
5493
|
case "html": {
|
|
5374
5494
|
diagnostics.push(...startPassageDiagnostics(story, startName, format?.isTwine2 === false ? "twine1" : "twine2"));
|
|
5375
5495
|
if (!format) throw new TweeTsError("No story format available for HTML output.", diagnostics);
|
|
5376
|
-
if (format.isTwine2
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
message: "Special passage \"StoryTitle\" not found."
|
|
5381
|
-
});
|
|
5382
|
-
output = toTwine1HTML(story, format, startName);
|
|
5383
|
-
}
|
|
5496
|
+
if (!format.isTwine2 && story.name === "" && !storyHas(story, "StoryTitle")) diagnostics.push({
|
|
5497
|
+
level: "error",
|
|
5498
|
+
message: "Special passage \"StoryTitle\" not found."
|
|
5499
|
+
});
|
|
5384
5500
|
const modules = getFilenames(options.modules ?? [], outFile);
|
|
5385
5501
|
diagnostics.push(...modules.diagnostics);
|
|
5386
|
-
|
|
5502
|
+
const head = loadHeadContent(modules.filenames, options.headFile, diagnostics);
|
|
5503
|
+
output = format.isTwine2 ? toTwine2HTML(story, format, startName, {
|
|
5504
|
+
sourceInfo,
|
|
5505
|
+
head
|
|
5506
|
+
}) : toTwine1HTML(story, format, startName, { head });
|
|
5387
5507
|
break;
|
|
5388
5508
|
}
|
|
5389
5509
|
default: throw new TweeTsError(`Unhandled output mode: ${outputMode}`, diagnostics);
|
|
@@ -5488,6 +5608,39 @@ function fatalError(e) {
|
|
|
5488
5608
|
return e instanceof Error ? e : new Error(String(e));
|
|
5489
5609
|
}
|
|
5490
5610
|
//#endregion
|
|
5491
|
-
|
|
5611
|
+
//#region src/plugins/paths.ts
|
|
5612
|
+
/**
|
|
5613
|
+
* Path helpers for the bundler plugins. Vite reports module ids with forward
|
|
5614
|
+
* slashes on every platform, while node:path gives backslashes on Windows, so
|
|
5615
|
+
* the Vite plugin compares paths in forward-slash form only.
|
|
5616
|
+
*/
|
|
5617
|
+
/**
|
|
5618
|
+
* The absolute path a bundle writes the file it emits as `fileName` to: inside
|
|
5619
|
+
* `output.dir`, or next to `output.file`, either resolved against the working
|
|
5620
|
+
* directory, as the bundler resolves them. Undefined when the output names
|
|
5621
|
+
* neither, as for a bundle only generated in memory.
|
|
5622
|
+
*/
|
|
5623
|
+
function emittedFilePath(output, fileName) {
|
|
5624
|
+
const dir = output.dir ?? (output.file === void 0 ? void 0 : dirname(output.file));
|
|
5625
|
+
return dir === void 0 ? void 0 : resolve(dir, fileName);
|
|
5626
|
+
}
|
|
5627
|
+
/** The path with Windows separators turned into forward slashes. */
|
|
5628
|
+
function toPosix(path) {
|
|
5629
|
+
return path.replace(/\\/g, "/");
|
|
5630
|
+
}
|
|
5631
|
+
/** Whether `file` is one of `dirs` or inside one of them. All paths in forward-slash form. */
|
|
5632
|
+
function isInside(file, dirs) {
|
|
5633
|
+
return dirs.some((dir) => file === dir || file.startsWith(dir.endsWith("/") ? dir : `${dir}/`));
|
|
5634
|
+
}
|
|
5635
|
+
/**
|
|
5636
|
+
* Whether `file` is a temporary copy of a Vite config file. Vite writes one next
|
|
5637
|
+
* to the config file each time it loads it (when no node_modules folder is
|
|
5638
|
+
* above the config, and always under Deno), then deletes it.
|
|
5639
|
+
*/
|
|
5640
|
+
function isViteConfigTemp(file) {
|
|
5641
|
+
return /\.timestamp-\d+-[0-9a-f]*\.mjs$/.test(file);
|
|
5642
|
+
}
|
|
5643
|
+
//#endregion
|
|
5644
|
+
export { fatalError as a, compileForOutputFile as c, mediaTypeFromFilename as d, toPosix as i, getFilenames as l, isInside as n, formatDiagnostic as o, isViteConfigTemp as r, splitDiagnostics as s, emittedFilePath as t, isExcluded as u };
|
|
5492
5645
|
|
|
5493
|
-
//# sourceMappingURL=
|
|
5646
|
+
//# sourceMappingURL=paths-w4ulhCFb.js.map
|