@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.
@@ -917,6 +917,17 @@ function readFormatSource(format) {
917
917
  import { basename } from "path";
918
918
  import { statSync as statSync3 } from "fs";
919
919
 
920
+ // src/types.ts
921
+ var ItemType = {
922
+ Error: 0,
923
+ EOF: 1,
924
+ Header: 2,
925
+ Name: 3,
926
+ Tags: 4,
927
+ Metadata: 5,
928
+ Content: 6
929
+ };
930
+
920
931
  // src/lexer.ts
921
932
  var EOF = -1;
922
933
  var HEADER_DELIM = "::";
@@ -949,10 +960,16 @@ var LexerContext = class {
949
960
  throw new Error("backup would leave pos < start");
950
961
  }
951
962
  }
963
+ // Line-counting invariant: next() increments this.line for each newline during
964
+ // character-by-character scanning. However, Content items are produced by jumping
965
+ // this.pos directly (e.g. via indexOf) without calling next(), so those newlines
966
+ // haven't been counted yet. emit() re-counts newlines in [start, pos) for Content
967
+ // items to compensate. This is safe because this.start is reset to this.pos after
968
+ // every emit(), so no range is ever counted twice.
952
969
  emit(type) {
953
970
  const val = this.input.slice(this.start, this.pos);
954
971
  this.items.push({ type, line: this.line, pos: this.start, val });
955
- if (type === 6 /* Content */) {
972
+ if (type === ItemType.Content) {
956
973
  for (let i = this.start; i < this.pos; i++) {
957
974
  if (this.input.charCodeAt(i) === 10) this.line++;
958
975
  }
@@ -973,7 +990,7 @@ var LexerContext = class {
973
990
  }
974
991
  errorf(message) {
975
992
  this.items.push({
976
- type: 0 /* Error */,
993
+ type: ItemType.Error,
977
994
  line: this.line,
978
995
  pos: this.start,
979
996
  val: message
@@ -1015,7 +1032,7 @@ function lexProlog(ctx) {
1015
1032
  ctx.ignore();
1016
1033
  return lexHeaderDelim;
1017
1034
  }
1018
- ctx.emit(1 /* EOF */);
1035
+ ctx.emit(ItemType.EOF);
1019
1036
  return null;
1020
1037
  }
1021
1038
  function lexContent(ctx) {
@@ -1025,19 +1042,19 @@ function lexContent(ctx) {
1025
1042
  const i = ctx.input.indexOf(NEWLINE_HEADER_DELIM, ctx.pos);
1026
1043
  if (i > -1) {
1027
1044
  ctx.pos = i + 1;
1028
- ctx.emit(6 /* Content */);
1045
+ ctx.emit(ItemType.Content);
1029
1046
  return lexHeaderDelim;
1030
1047
  }
1031
1048
  ctx.pos = ctx.input.length;
1032
1049
  if (ctx.pos > ctx.start) {
1033
- ctx.emit(6 /* Content */);
1050
+ ctx.emit(ItemType.Content);
1034
1051
  }
1035
- ctx.emit(1 /* EOF */);
1052
+ ctx.emit(ItemType.EOF);
1036
1053
  return null;
1037
1054
  }
1038
1055
  function lexHeaderDelim(ctx) {
1039
1056
  ctx.pos += HEADER_DELIM.length;
1040
- ctx.emit(2 /* Header */);
1057
+ ctx.emit(ItemType.Header);
1041
1058
  return lexName;
1042
1059
  }
1043
1060
  function lexName(ctx) {
@@ -1066,7 +1083,7 @@ function lexName(ctx) {
1066
1083
  break outer;
1067
1084
  }
1068
1085
  }
1069
- ctx.emit(3 /* Name */);
1086
+ ctx.emit(ItemType.Name);
1070
1087
  switch (r) {
1071
1088
  case 91:
1072
1089
  return lexTags;
@@ -1083,7 +1100,7 @@ function lexName(ctx) {
1083
1100
  ctx.ignore();
1084
1101
  return lexContent;
1085
1102
  }
1086
- ctx.emit(1 /* EOF */);
1103
+ ctx.emit(ItemType.EOF);
1087
1104
  return null;
1088
1105
  }
1089
1106
  function lexNextOptionalBlock(ctx) {
@@ -1106,7 +1123,7 @@ function lexNextOptionalBlock(ctx) {
1106
1123
  ctx.ignore();
1107
1124
  return lexContent;
1108
1125
  case EOF:
1109
- ctx.emit(1 /* EOF */);
1126
+ ctx.emit(ItemType.EOF);
1110
1127
  return null;
1111
1128
  }
1112
1129
  return ctx.errorf(`illegal character '${String.fromCharCode(r)}' amid the optional blocks`);
@@ -1126,7 +1143,7 @@ function lexTags(ctx) {
1126
1143
  if (r === 10) ctx.backup();
1127
1144
  return ctx.errorf("unterminated tag block");
1128
1145
  case 93:
1129
- if (ctx.pos > ctx.start) ctx.emit(4 /* Tags */);
1146
+ if (ctx.pos > ctx.start) ctx.emit(ItemType.Tags);
1130
1147
  return lexNextOptionalBlock;
1131
1148
  case 91:
1132
1149
  return ctx.errorf(`unexpected left square bracket '['`);
@@ -1159,7 +1176,7 @@ function lexMetadata(ctx) {
1159
1176
  case 125:
1160
1177
  depth--;
1161
1178
  if (depth === 0) {
1162
- if (ctx.pos > ctx.start) ctx.emit(5 /* Metadata */);
1179
+ if (ctx.pos > ctx.start) ctx.emit(ItemType.Metadata);
1163
1180
  return lexNextOptionalBlock;
1164
1181
  }
1165
1182
  break;
@@ -1210,10 +1227,10 @@ function parseTwee(source, options = {}) {
1210
1227
  const passages = [];
1211
1228
  let current = null;
1212
1229
  let pCount = 0;
1213
- let lastType = 1 /* EOF */;
1230
+ let lastType = ItemType.EOF;
1214
1231
  for (const item of tweeLexer(source)) {
1215
1232
  switch (item.type) {
1216
- case 0 /* Error */:
1233
+ case ItemType.Error:
1217
1234
  diagnostics.push({
1218
1235
  level: "error",
1219
1236
  message: `line ${item.line}: Malformed twee source; ${item.val}.`,
@@ -1221,19 +1238,19 @@ function parseTwee(source, options = {}) {
1221
1238
  line: item.line
1222
1239
  });
1223
1240
  return { passages, diagnostics };
1224
- case 1 /* EOF */:
1241
+ case ItemType.EOF:
1225
1242
  if (pCount > 0 && current) {
1226
1243
  passages.push(current);
1227
1244
  }
1228
1245
  return { passages, diagnostics };
1229
- case 2 /* Header */:
1246
+ case ItemType.Header:
1230
1247
  pCount++;
1231
1248
  if (pCount > 1 && current) {
1232
1249
  passages.push(current);
1233
1250
  }
1234
1251
  current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1235
1252
  break;
1236
- case 3 /* Name */: {
1253
+ case ItemType.Name: {
1237
1254
  if (!current) break;
1238
1255
  const name = tweeUnescape(item.val).trim();
1239
1256
  if (name.length === 0) {
@@ -1248,9 +1265,9 @@ function parseTwee(source, options = {}) {
1248
1265
  current.name = name;
1249
1266
  break;
1250
1267
  }
1251
- case 4 /* Tags */: {
1268
+ case ItemType.Tags: {
1252
1269
  if (!current) break;
1253
- if (lastType !== 3 /* Name */) {
1270
+ if (lastType !== ItemType.Name) {
1254
1271
  diagnostics.push({
1255
1272
  level: "error",
1256
1273
  message: `line ${item.line}: Malformed twee source; optional tags block must immediately follow the passage name.`,
@@ -1263,9 +1280,9 @@ function parseTwee(source, options = {}) {
1263
1280
  current.tags = tweeUnescape(inner).split(/\s+/).filter(Boolean);
1264
1281
  break;
1265
1282
  }
1266
- case 5 /* Metadata */: {
1283
+ case ItemType.Metadata: {
1267
1284
  if (!current) break;
1268
- if (lastType !== 3 /* Name */ && lastType !== 4 /* Tags */) {
1285
+ if (lastType !== ItemType.Name && lastType !== ItemType.Tags) {
1269
1286
  diagnostics.push({
1270
1287
  level: "error",
1271
1288
  message: `line ${item.line}: Malformed twee source; optional metadata block must immediately follow the passage name or tags block.`,
@@ -1297,7 +1314,7 @@ function parseTwee(source, options = {}) {
1297
1314
  }
1298
1315
  break;
1299
1316
  }
1300
- case 6 /* Content */: {
1317
+ case ItemType.Content: {
1301
1318
  if (!current) break;
1302
1319
  if (trim) {
1303
1320
  current.text = item.val.trim();
@@ -4427,21 +4444,22 @@ var VERSION = "1.2.0";
4427
4444
 
4428
4445
  // src/output-twine2.ts
4429
4446
  var CREATOR_NAME = "Twee-ts";
4430
- function toTwine2Archive(story, startName, diagnostics, options) {
4431
- return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
4447
+ function toTwine2Archive(story, startName, options) {
4448
+ return getTwine2DataChunk(story, startName, options) + "\n";
4432
4449
  }
4433
- function toTwine2HTML(story, format, startName, diagnostics, options) {
4450
+ function toTwine2HTML(story, format, startName, options) {
4434
4451
  let template = readFormatSource(format);
4435
4452
  if (template.includes("{{STORY_NAME}}")) {
4436
- template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
4453
+ const name = htmlEscape(story.name);
4454
+ template = template.replaceAll("{{STORY_NAME}}", () => name);
4437
4455
  }
4438
4456
  if (template.includes("{{STORY_DATA}}")) {
4439
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
4457
+ const data = getTwine2DataChunk(story, startName, options);
4458
+ template = template.replace("{{STORY_DATA}}", () => data);
4440
4459
  }
4441
4460
  return template;
4442
4461
  }
4443
- function getTwine2DataChunk(story, startName, diagnostics, options) {
4444
- ensureIFID(story, diagnostics);
4462
+ function getTwine2DataChunk(story, startName, options) {
4445
4463
  const parts = [];
4446
4464
  let startID = "";
4447
4465
  let pid = 0;
@@ -4510,23 +4528,6 @@ function getTwine2DataChunk(story, startName, diagnostics, options) {
4510
4528
  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>`;
4511
4529
  return wrapper + parts.join("") + "</tw-storydata>";
4512
4530
  }
4513
- function ensureIFID(story, diagnostics) {
4514
- if (story.ifid !== "") return;
4515
- if (story.legacyIFID !== "") {
4516
- story.ifid = story.legacyIFID;
4517
- diagnostics.push({
4518
- level: "warning",
4519
- message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
4520
- });
4521
- } else {
4522
- const ifid = generateIFID();
4523
- story.ifid = ifid;
4524
- diagnostics.push({
4525
- level: "error",
4526
- message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
4527
- });
4528
- }
4529
- }
4530
4531
 
4531
4532
  // src/output-twine1.ts
4532
4533
  import { readFileSync as readFileSync2 } from "fs";
@@ -4537,7 +4538,7 @@ function toTwine1Archive(story, _startName) {
4537
4538
  return `<div id="storeArea" data-size="${count}">${data}</div>
4538
4539
  `;
4539
4540
  }
4540
- function toTwine1HTML(story, format, startName, _diagnostics) {
4541
+ function toTwine1HTML(story, format, startName) {
4541
4542
  const formatDir = dirname(format.filename);
4542
4543
  const parentDir = dirname(formatDir);
4543
4544
  let template = readFormatSource(format);
@@ -4555,10 +4556,11 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
4555
4556
  const displayStart = startName === "Start" ? "" : startName;
4556
4557
  template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
4557
4558
  template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
4558
- template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
4559
+ const startAtValue = `"${jsStringEscape(displayStart)}"`;
4560
+ template = template.replace('"START_AT"', () => startAtValue);
4559
4561
  template = template.replace('"STORY_SIZE"', `"${count}"`);
4560
4562
  if (template.includes('"STORY"')) {
4561
- template = template.replace('"STORY"', data);
4563
+ template = template.replace('"STORY"', () => data);
4562
4564
  } else {
4563
4565
  let footer;
4564
4566
  try {
@@ -4594,7 +4596,7 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
4594
4596
  try {
4595
4597
  let content = readFileSync2(componentPath, "utf-8");
4596
4598
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
4597
- return template.replace(placeholder, content);
4599
+ return template.replace(placeholder, () => content);
4598
4600
  } catch (e) {
4599
4601
  if (required) {
4600
4602
  throw new Error(
@@ -4682,7 +4684,8 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
4682
4684
  }
4683
4685
  if (parts.length > 0) {
4684
4686
  parts.push("</head>");
4685
- return html.replace("</head>", parts.join("\n"));
4687
+ const replacement = parts.join("\n");
4688
+ return html.replace("</head>", () => replacement);
4686
4689
  }
4687
4690
  return html;
4688
4691
  }
@@ -5091,6 +5094,7 @@ async function buildOutput(options, cache, changedFiles) {
5091
5094
  if (testMode) {
5092
5095
  story.twine2.options.set("debug", true);
5093
5096
  }
5097
+ ensureIFID(story, diagnostics);
5094
5098
  let output;
5095
5099
  switch (outputMode) {
5096
5100
  case "twee3":
@@ -5098,7 +5102,7 @@ async function buildOutput(options, cache, changedFiles) {
5098
5102
  output = toTwee(story, outputMode);
5099
5103
  break;
5100
5104
  case "twine2-archive":
5101
- output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
5105
+ output = toTwine2Archive(story, startName, { sourceInfo });
5102
5106
  break;
5103
5107
  case "twine1-archive":
5104
5108
  output = toTwine1Archive(story, startName);
@@ -5117,7 +5121,7 @@ async function buildOutput(options, cache, changedFiles) {
5117
5121
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
5118
5122
  }
5119
5123
  if (format.isTwine2) {
5120
- output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
5124
+ output = toTwine2HTML(story, format, startName, { sourceInfo });
5121
5125
  } else {
5122
5126
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
5123
5127
  diagnostics.push({
@@ -5125,7 +5129,7 @@ async function buildOutput(options, cache, changedFiles) {
5125
5129
  message: 'Special passage "StoryTitle" not found.'
5126
5130
  });
5127
5131
  }
5128
- output = toTwine1HTML(story, format, startName, diagnostics);
5132
+ output = toTwine1HTML(story, format, startName);
5129
5133
  }
5130
5134
  const modulePaths = options.modules ? getFilenames(options.modules) : [];
5131
5135
  output = modifyHead(output, modulePaths, options.headFile, diagnostics);
@@ -5142,6 +5146,23 @@ async function buildOutput(options, cache, changedFiles) {
5142
5146
  };
5143
5147
  return { output, story, format, diagnostics, stats };
5144
5148
  }
5149
+ function ensureIFID(story, diagnostics) {
5150
+ if (story.ifid !== "") return;
5151
+ if (story.legacyIFID !== "") {
5152
+ story.ifid = story.legacyIFID;
5153
+ diagnostics.push({
5154
+ level: "warning",
5155
+ message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
5156
+ });
5157
+ } else {
5158
+ const ifid = generateIFID();
5159
+ story.ifid = ifid;
5160
+ diagnostics.push({
5161
+ level: "error",
5162
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
5163
+ });
5164
+ }
5165
+ }
5145
5166
  function storyToJSON(story) {
5146
5167
  const scripts = [];
5147
5168
  const stylesheets = [];