@rohal12/twee-ts 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,20 @@ 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
4547
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
4504
4548
  }
4505
4549
  if (template.includes("{{STORY_DATA}}")) {
4506
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
4550
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, options));
4507
4551
  }
4508
4552
  return template;
4509
4553
  }
4510
- function getTwine2DataChunk(story, startName, diagnostics, options) {
4511
- ensureIFID(story, diagnostics);
4554
+ function getTwine2DataChunk(story, startName, options) {
4512
4555
  const parts = [];
4513
4556
  let startID = "";
4514
4557
  let pid = 0;
@@ -4577,23 +4620,6 @@ function getTwine2DataChunk(story, startName, diagnostics, options) {
4577
4620
  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
4621
  return wrapper + parts.join("") + "</tw-storydata>";
4579
4622
  }
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
4623
 
4598
4624
  // src/output-twine1.ts
4599
4625
  var import_node_fs5 = require("fs");
@@ -4604,7 +4630,7 @@ function toTwine1Archive(story, _startName) {
4604
4630
  return `<div id="storeArea" data-size="${count}">${data}</div>
4605
4631
  `;
4606
4632
  }
4607
- function toTwine1HTML(story, format, startName, _diagnostics) {
4633
+ function toTwine1HTML(story, format, startName) {
4608
4634
  const formatDir = (0, import_node_path6.dirname)(format.filename);
4609
4635
  const parentDir = (0, import_node_path6.dirname)(formatDir);
4610
4636
  let template = readFormatSource(format);
@@ -5161,6 +5187,7 @@ async function buildOutput(options, cache, changedFiles) {
5161
5187
  if (testMode) {
5162
5188
  story.twine2.options.set("debug", true);
5163
5189
  }
5190
+ ensureIFID(story, diagnostics);
5164
5191
  let output;
5165
5192
  switch (outputMode) {
5166
5193
  case "twee3":
@@ -5168,7 +5195,7 @@ async function buildOutput(options, cache, changedFiles) {
5168
5195
  output = toTwee(story, outputMode);
5169
5196
  break;
5170
5197
  case "twine2-archive":
5171
- output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
5198
+ output = toTwine2Archive(story, startName, { sourceInfo });
5172
5199
  break;
5173
5200
  case "twine1-archive":
5174
5201
  output = toTwine1Archive(story, startName);
@@ -5187,7 +5214,7 @@ async function buildOutput(options, cache, changedFiles) {
5187
5214
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
5188
5215
  }
5189
5216
  if (format.isTwine2) {
5190
- output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
5217
+ output = toTwine2HTML(story, format, startName, { sourceInfo });
5191
5218
  } else {
5192
5219
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
5193
5220
  diagnostics.push({
@@ -5195,7 +5222,7 @@ async function buildOutput(options, cache, changedFiles) {
5195
5222
  message: 'Special passage "StoryTitle" not found.'
5196
5223
  });
5197
5224
  }
5198
- output = toTwine1HTML(story, format, startName, diagnostics);
5225
+ output = toTwine1HTML(story, format, startName);
5199
5226
  }
5200
5227
  const modulePaths = options.modules ? getFilenames(options.modules) : [];
5201
5228
  output = modifyHead(output, modulePaths, options.headFile, diagnostics);
@@ -5212,6 +5239,23 @@ async function buildOutput(options, cache, changedFiles) {
5212
5239
  };
5213
5240
  return { output, story, format, diagnostics, stats };
5214
5241
  }
5242
+ function ensureIFID(story, diagnostics) {
5243
+ if (story.ifid !== "") return;
5244
+ if (story.legacyIFID !== "") {
5245
+ story.ifid = story.legacyIFID;
5246
+ diagnostics.push({
5247
+ level: "warning",
5248
+ message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
5249
+ });
5250
+ } else {
5251
+ const ifid = generateIFID();
5252
+ story.ifid = ifid;
5253
+ diagnostics.push({
5254
+ level: "error",
5255
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
5256
+ });
5257
+ }
5258
+ }
5215
5259
  function storyToJSON(story) {
5216
5260
  const scripts = [];
5217
5261
  const stylesheets = [];
@@ -5533,6 +5577,8 @@ function scaffoldConfig() {
5533
5577
  // Annotate the CommonJS export names for ESM import in node:
5534
5578
  0 && (module.exports = {
5535
5579
  CONFIG_FILENAME,
5580
+ ItemType,
5581
+ StoryBuilder,
5536
5582
  TweeLexer,
5537
5583
  TweeTsError,
5538
5584
  applyTagAliases,
@@ -5540,6 +5586,7 @@ function scaffoldConfig() {
5540
5586
  compile,
5541
5587
  compileIncremental,
5542
5588
  compileToFile,
5589
+ createIFID,
5543
5590
  decompileHTML,
5544
5591
  discoverCachedFormats,
5545
5592
  discoverFormats,