@rohal12/twee-ts 1.12.0 → 1.13.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/index.cjs CHANGED
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  CONFIG_FILENAME: () => CONFIG_FILENAME,
24
+ ItemType: () => ItemType,
25
+ StoryBuilder: () => StoryBuilder,
24
26
  TweeLexer: () => TweeLexer,
25
27
  TweeTsError: () => TweeTsError,
26
28
  applyTagAliases: () => applyTagAliases,
@@ -28,6 +30,7 @@ __export(src_exports, {
28
30
  compile: () => compile,
29
31
  compileIncremental: () => compileIncremental,
30
32
  compileToFile: () => compileToFile,
33
+ createIFID: () => createIFID,
31
34
  decompileHTML: () => decompileHTML,
32
35
  discoverCachedFormats: () => discoverCachedFormats,
33
36
  discoverFormats: () => discoverFormats,
@@ -64,6 +67,11 @@ var import_node_crypto = require("crypto");
64
67
  function generateIFID() {
65
68
  return (0, import_node_crypto.randomUUID)().toUpperCase();
66
69
  }
70
+ function createIFID(value) {
71
+ const err = validateIFID(value);
72
+ if (err) throw new Error(`Invalid IFID: ${err}`);
73
+ return value.toUpperCase();
74
+ }
67
75
  function validateIFID(ifid) {
68
76
  let uuid = ifid;
69
77
  switch (ifid.length) {
@@ -545,6 +553,25 @@ function getStoryStats(story, wordCountMethod = "tweego") {
545
553
  }
546
554
  return { passages: story.passages.length, storyPassages, words };
547
555
  }
556
+ var StoryBuilder = class {
557
+ /** The mutable story, accessible during construction for loader functions. */
558
+ story;
559
+ constructor() {
560
+ this.story = createStory();
561
+ }
562
+ /** Add a passage, handling special passages (StoryData, StoryTitle, etc.). */
563
+ add(passage, diagnostics) {
564
+ storyAdd(this.story, passage, diagnostics);
565
+ }
566
+ /** Check if a passage name exists. */
567
+ has(name) {
568
+ return storyHas(this.story, name);
569
+ }
570
+ /** Finalize and return the story as read-only. */
571
+ build() {
572
+ return this.story;
573
+ }
574
+ };
548
575
 
549
576
  // src/filesystem.ts
550
577
  var import_node_fs = require("fs");
@@ -968,6 +995,17 @@ function readFormatSource(format) {
968
995
  var import_node_path5 = require("path");
969
996
  var import_node_fs4 = require("fs");
970
997
 
998
+ // src/types.ts
999
+ var ItemType = {
1000
+ Error: 0,
1001
+ EOF: 1,
1002
+ Header: 2,
1003
+ Name: 3,
1004
+ Tags: 4,
1005
+ Metadata: 5,
1006
+ Content: 6
1007
+ };
1008
+
971
1009
  // src/lexer.ts
972
1010
  var EOF = -1;
973
1011
  var HEADER_DELIM = "::";
@@ -1000,10 +1038,16 @@ var LexerContext = class {
1000
1038
  throw new Error("backup would leave pos < start");
1001
1039
  }
1002
1040
  }
1041
+ // Line-counting invariant: next() increments this.line for each newline during
1042
+ // character-by-character scanning. However, Content items are produced by jumping
1043
+ // this.pos directly (e.g. via indexOf) without calling next(), so those newlines
1044
+ // haven't been counted yet. emit() re-counts newlines in [start, pos) for Content
1045
+ // items to compensate. This is safe because this.start is reset to this.pos after
1046
+ // every emit(), so no range is ever counted twice.
1003
1047
  emit(type) {
1004
1048
  const val = this.input.slice(this.start, this.pos);
1005
1049
  this.items.push({ type, line: this.line, pos: this.start, val });
1006
- if (type === 6 /* Content */) {
1050
+ if (type === ItemType.Content) {
1007
1051
  for (let i = this.start; i < this.pos; i++) {
1008
1052
  if (this.input.charCodeAt(i) === 10) this.line++;
1009
1053
  }
@@ -1024,7 +1068,7 @@ var LexerContext = class {
1024
1068
  }
1025
1069
  errorf(message) {
1026
1070
  this.items.push({
1027
- type: 0 /* Error */,
1071
+ type: ItemType.Error,
1028
1072
  line: this.line,
1029
1073
  pos: this.start,
1030
1074
  val: message
@@ -1066,7 +1110,7 @@ function lexProlog(ctx) {
1066
1110
  ctx.ignore();
1067
1111
  return lexHeaderDelim;
1068
1112
  }
1069
- ctx.emit(1 /* EOF */);
1113
+ ctx.emit(ItemType.EOF);
1070
1114
  return null;
1071
1115
  }
1072
1116
  function lexContent(ctx) {
@@ -1076,19 +1120,19 @@ function lexContent(ctx) {
1076
1120
  const i = ctx.input.indexOf(NEWLINE_HEADER_DELIM, ctx.pos);
1077
1121
  if (i > -1) {
1078
1122
  ctx.pos = i + 1;
1079
- ctx.emit(6 /* Content */);
1123
+ ctx.emit(ItemType.Content);
1080
1124
  return lexHeaderDelim;
1081
1125
  }
1082
1126
  ctx.pos = ctx.input.length;
1083
1127
  if (ctx.pos > ctx.start) {
1084
- ctx.emit(6 /* Content */);
1128
+ ctx.emit(ItemType.Content);
1085
1129
  }
1086
- ctx.emit(1 /* EOF */);
1130
+ ctx.emit(ItemType.EOF);
1087
1131
  return null;
1088
1132
  }
1089
1133
  function lexHeaderDelim(ctx) {
1090
1134
  ctx.pos += HEADER_DELIM.length;
1091
- ctx.emit(2 /* Header */);
1135
+ ctx.emit(ItemType.Header);
1092
1136
  return lexName;
1093
1137
  }
1094
1138
  function lexName(ctx) {
@@ -1117,7 +1161,7 @@ function lexName(ctx) {
1117
1161
  break outer;
1118
1162
  }
1119
1163
  }
1120
- ctx.emit(3 /* Name */);
1164
+ ctx.emit(ItemType.Name);
1121
1165
  switch (r) {
1122
1166
  case 91:
1123
1167
  return lexTags;
@@ -1134,7 +1178,7 @@ function lexName(ctx) {
1134
1178
  ctx.ignore();
1135
1179
  return lexContent;
1136
1180
  }
1137
- ctx.emit(1 /* EOF */);
1181
+ ctx.emit(ItemType.EOF);
1138
1182
  return null;
1139
1183
  }
1140
1184
  function lexNextOptionalBlock(ctx) {
@@ -1157,7 +1201,7 @@ function lexNextOptionalBlock(ctx) {
1157
1201
  ctx.ignore();
1158
1202
  return lexContent;
1159
1203
  case EOF:
1160
- ctx.emit(1 /* EOF */);
1204
+ ctx.emit(ItemType.EOF);
1161
1205
  return null;
1162
1206
  }
1163
1207
  return ctx.errorf(`illegal character '${String.fromCharCode(r)}' amid the optional blocks`);
@@ -1177,7 +1221,7 @@ function lexTags(ctx) {
1177
1221
  if (r === 10) ctx.backup();
1178
1222
  return ctx.errorf("unterminated tag block");
1179
1223
  case 93:
1180
- if (ctx.pos > ctx.start) ctx.emit(4 /* Tags */);
1224
+ if (ctx.pos > ctx.start) ctx.emit(ItemType.Tags);
1181
1225
  return lexNextOptionalBlock;
1182
1226
  case 91:
1183
1227
  return ctx.errorf(`unexpected left square bracket '['`);
@@ -1210,7 +1254,7 @@ function lexMetadata(ctx) {
1210
1254
  case 125:
1211
1255
  depth--;
1212
1256
  if (depth === 0) {
1213
- if (ctx.pos > ctx.start) ctx.emit(5 /* Metadata */);
1257
+ if (ctx.pos > ctx.start) ctx.emit(ItemType.Metadata);
1214
1258
  return lexNextOptionalBlock;
1215
1259
  }
1216
1260
  break;
@@ -1240,7 +1284,7 @@ var TweeLexer = class {
1240
1284
  nextItem() {
1241
1285
  const result = this.gen.next();
1242
1286
  if (result.done) {
1243
- return { item: { type: 1 /* EOF */, line: 0, pos: 0, val: "" }, done: true };
1287
+ return { item: { type: ItemType.EOF, line: 0, pos: 0, val: "" }, done: true };
1244
1288
  }
1245
1289
  return { item: result.value, done: false };
1246
1290
  }
@@ -1277,10 +1321,10 @@ function parseTwee(source, options = {}) {
1277
1321
  const passages = [];
1278
1322
  let current = null;
1279
1323
  let pCount = 0;
1280
- let lastType = 1 /* EOF */;
1324
+ let lastType = ItemType.EOF;
1281
1325
  for (const item of tweeLexer(source)) {
1282
1326
  switch (item.type) {
1283
- case 0 /* Error */:
1327
+ case ItemType.Error:
1284
1328
  diagnostics.push({
1285
1329
  level: "error",
1286
1330
  message: `line ${item.line}: Malformed twee source; ${item.val}.`,
@@ -1288,19 +1332,19 @@ function parseTwee(source, options = {}) {
1288
1332
  line: item.line
1289
1333
  });
1290
1334
  return { passages, diagnostics };
1291
- case 1 /* EOF */:
1335
+ case ItemType.EOF:
1292
1336
  if (pCount > 0 && current) {
1293
1337
  passages.push(current);
1294
1338
  }
1295
1339
  return { passages, diagnostics };
1296
- case 2 /* Header */:
1340
+ case ItemType.Header:
1297
1341
  pCount++;
1298
1342
  if (pCount > 1 && current) {
1299
1343
  passages.push(current);
1300
1344
  }
1301
1345
  current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1302
1346
  break;
1303
- case 3 /* Name */: {
1347
+ case ItemType.Name: {
1304
1348
  if (!current) break;
1305
1349
  const name = tweeUnescape(item.val).trim();
1306
1350
  if (name.length === 0) {
@@ -1315,9 +1359,9 @@ function parseTwee(source, options = {}) {
1315
1359
  current.name = name;
1316
1360
  break;
1317
1361
  }
1318
- case 4 /* Tags */: {
1362
+ case ItemType.Tags: {
1319
1363
  if (!current) break;
1320
- if (lastType !== 3 /* Name */) {
1364
+ if (lastType !== ItemType.Name) {
1321
1365
  diagnostics.push({
1322
1366
  level: "error",
1323
1367
  message: `line ${item.line}: Malformed twee source; optional tags block must immediately follow the passage name.`,
@@ -1330,9 +1374,9 @@ function parseTwee(source, options = {}) {
1330
1374
  current.tags = tweeUnescape(inner).split(/\s+/).filter(Boolean);
1331
1375
  break;
1332
1376
  }
1333
- case 5 /* Metadata */: {
1377
+ case ItemType.Metadata: {
1334
1378
  if (!current) break;
1335
- if (lastType !== 3 /* Name */ && lastType !== 4 /* Tags */) {
1379
+ if (lastType !== ItemType.Name && lastType !== ItemType.Tags) {
1336
1380
  diagnostics.push({
1337
1381
  level: "error",
1338
1382
  message: `line ${item.line}: Malformed twee source; optional metadata block must immediately follow the passage name or tags block.`,
@@ -1364,7 +1408,7 @@ function parseTwee(source, options = {}) {
1364
1408
  }
1365
1409
  break;
1366
1410
  }
1367
- case 6 /* Content */: {
1411
+ case ItemType.Content: {
1368
1412
  if (!current) break;
1369
1413
  if (trim) {
1370
1414
  current.text = item.val.trim();
@@ -4494,21 +4538,22 @@ var VERSION = "1.2.0";
4494
4538
 
4495
4539
  // src/output-twine2.ts
4496
4540
  var CREATOR_NAME = "Twee-ts";
4497
- function toTwine2Archive(story, startName, diagnostics, options) {
4498
- return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
4541
+ function toTwine2Archive(story, startName, options) {
4542
+ return getTwine2DataChunk(story, startName, options) + "\n";
4499
4543
  }
4500
- function toTwine2HTML(story, format, startName, diagnostics, options) {
4544
+ function toTwine2HTML(story, format, startName, options) {
4501
4545
  let template = readFormatSource(format);
4502
4546
  if (template.includes("{{STORY_NAME}}")) {
4503
- template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
4547
+ const name = htmlEscape(story.name);
4548
+ template = template.replaceAll("{{STORY_NAME}}", () => name);
4504
4549
  }
4505
4550
  if (template.includes("{{STORY_DATA}}")) {
4506
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
4551
+ const data = getTwine2DataChunk(story, startName, options);
4552
+ template = template.replace("{{STORY_DATA}}", () => data);
4507
4553
  }
4508
4554
  return template;
4509
4555
  }
4510
- function getTwine2DataChunk(story, startName, diagnostics, options) {
4511
- ensureIFID(story, diagnostics);
4556
+ function getTwine2DataChunk(story, startName, options) {
4512
4557
  const parts = [];
4513
4558
  let startID = "";
4514
4559
  let pid = 0;
@@ -4577,23 +4622,6 @@ function getTwine2DataChunk(story, startName, diagnostics, options) {
4577
4622
  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(optionsStr)}" tags="${attrEscape(story.twine2.tags)}" hidden>`;
4578
4623
  return wrapper + parts.join("") + "</tw-storydata>";
4579
4624
  }
4580
- function ensureIFID(story, diagnostics) {
4581
- if (story.ifid !== "") return;
4582
- if (story.legacyIFID !== "") {
4583
- story.ifid = story.legacyIFID;
4584
- diagnostics.push({
4585
- level: "warning",
4586
- message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
4587
- });
4588
- } else {
4589
- const ifid = generateIFID();
4590
- story.ifid = ifid;
4591
- diagnostics.push({
4592
- level: "error",
4593
- message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
4594
- });
4595
- }
4596
- }
4597
4625
 
4598
4626
  // src/output-twine1.ts
4599
4627
  var import_node_fs5 = require("fs");
@@ -4604,7 +4632,7 @@ function toTwine1Archive(story, _startName) {
4604
4632
  return `<div id="storeArea" data-size="${count}">${data}</div>
4605
4633
  `;
4606
4634
  }
4607
- function toTwine1HTML(story, format, startName, _diagnostics) {
4635
+ function toTwine1HTML(story, format, startName) {
4608
4636
  const formatDir = (0, import_node_path6.dirname)(format.filename);
4609
4637
  const parentDir = (0, import_node_path6.dirname)(formatDir);
4610
4638
  let template = readFormatSource(format);
@@ -4622,10 +4650,11 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
4622
4650
  const displayStart = startName === "Start" ? "" : startName;
4623
4651
  template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
4624
4652
  template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
4625
- template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
4653
+ const startAtValue = `"${jsStringEscape(displayStart)}"`;
4654
+ template = template.replace('"START_AT"', () => startAtValue);
4626
4655
  template = template.replace('"STORY_SIZE"', `"${count}"`);
4627
4656
  if (template.includes('"STORY"')) {
4628
- template = template.replace('"STORY"', data);
4657
+ template = template.replace('"STORY"', () => data);
4629
4658
  } else {
4630
4659
  let footer;
4631
4660
  try {
@@ -4661,7 +4690,7 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
4661
4690
  try {
4662
4691
  let content = (0, import_node_fs5.readFileSync)(componentPath, "utf-8");
4663
4692
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
4664
- return template.replace(placeholder, content);
4693
+ return template.replace(placeholder, () => content);
4665
4694
  } catch (e) {
4666
4695
  if (required) {
4667
4696
  throw new Error(
@@ -4749,7 +4778,8 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
4749
4778
  }
4750
4779
  if (parts.length > 0) {
4751
4780
  parts.push("</head>");
4752
- return html.replace("</head>", parts.join("\n"));
4781
+ const replacement = parts.join("\n");
4782
+ return html.replace("</head>", () => replacement);
4753
4783
  }
4754
4784
  return html;
4755
4785
  }
@@ -5161,6 +5191,7 @@ async function buildOutput(options, cache, changedFiles) {
5161
5191
  if (testMode) {
5162
5192
  story.twine2.options.set("debug", true);
5163
5193
  }
5194
+ ensureIFID(story, diagnostics);
5164
5195
  let output;
5165
5196
  switch (outputMode) {
5166
5197
  case "twee3":
@@ -5168,7 +5199,7 @@ async function buildOutput(options, cache, changedFiles) {
5168
5199
  output = toTwee(story, outputMode);
5169
5200
  break;
5170
5201
  case "twine2-archive":
5171
- output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
5202
+ output = toTwine2Archive(story, startName, { sourceInfo });
5172
5203
  break;
5173
5204
  case "twine1-archive":
5174
5205
  output = toTwine1Archive(story, startName);
@@ -5187,7 +5218,7 @@ async function buildOutput(options, cache, changedFiles) {
5187
5218
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
5188
5219
  }
5189
5220
  if (format.isTwine2) {
5190
- output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
5221
+ output = toTwine2HTML(story, format, startName, { sourceInfo });
5191
5222
  } else {
5192
5223
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
5193
5224
  diagnostics.push({
@@ -5195,7 +5226,7 @@ async function buildOutput(options, cache, changedFiles) {
5195
5226
  message: 'Special passage "StoryTitle" not found.'
5196
5227
  });
5197
5228
  }
5198
- output = toTwine1HTML(story, format, startName, diagnostics);
5229
+ output = toTwine1HTML(story, format, startName);
5199
5230
  }
5200
5231
  const modulePaths = options.modules ? getFilenames(options.modules) : [];
5201
5232
  output = modifyHead(output, modulePaths, options.headFile, diagnostics);
@@ -5212,6 +5243,23 @@ async function buildOutput(options, cache, changedFiles) {
5212
5243
  };
5213
5244
  return { output, story, format, diagnostics, stats };
5214
5245
  }
5246
+ function ensureIFID(story, diagnostics) {
5247
+ if (story.ifid !== "") return;
5248
+ if (story.legacyIFID !== "") {
5249
+ story.ifid = story.legacyIFID;
5250
+ diagnostics.push({
5251
+ level: "warning",
5252
+ message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
5253
+ });
5254
+ } else {
5255
+ const ifid = generateIFID();
5256
+ story.ifid = ifid;
5257
+ diagnostics.push({
5258
+ level: "error",
5259
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
5260
+ });
5261
+ }
5262
+ }
5215
5263
  function storyToJSON(story) {
5216
5264
  const scripts = [];
5217
5265
  const stylesheets = [];
@@ -5533,6 +5581,8 @@ function scaffoldConfig() {
5533
5581
  // Annotate the CommonJS export names for ESM import in node:
5534
5582
  0 && (module.exports = {
5535
5583
  CONFIG_FILENAME,
5584
+ ItemType,
5585
+ StoryBuilder,
5536
5586
  TweeLexer,
5537
5587
  TweeTsError,
5538
5588
  applyTagAliases,
@@ -5540,6 +5590,7 @@ function scaffoldConfig() {
5540
5590
  compile,
5541
5591
  compileIncremental,
5542
5592
  compileToFile,
5593
+ createIFID,
5543
5594
  decompileHTML,
5544
5595
  discoverCachedFormats,
5545
5596
  discoverFormats,