@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.d.cts CHANGED
@@ -54,17 +54,23 @@ interface WatchOptions extends CompileToFileOptions {
54
54
  /** Called on build errors. */
55
55
  onError?: (error: Error) => void;
56
56
  }
57
- interface Diagnostic {
58
- level: 'warning' | 'error';
57
+ type Diagnostic = {
58
+ level: 'warning';
59
59
  message: string;
60
60
  file?: string;
61
61
  line?: number;
62
- }
62
+ } | {
63
+ level: 'error';
64
+ message: string;
65
+ file?: string;
66
+ line?: number;
67
+ fatal?: boolean;
68
+ };
63
69
  interface CompileResult {
64
70
  /** The compiled output string (HTML, Twee, JSON, etc.). */
65
71
  output: string;
66
- /** The parsed story model. */
67
- story: Story;
72
+ /** The parsed story model (read-only after compilation). */
73
+ story: ReadonlyStory;
68
74
  /** The format used for compilation (undefined for non-HTML modes). */
69
75
  format?: StoryFormatInfo;
70
76
  /** Collected diagnostics. */
@@ -94,6 +100,9 @@ interface Passage {
94
100
  metadata?: PassageMetadata;
95
101
  source?: SourceLocation;
96
102
  }
103
+ type IFID = string & {
104
+ readonly __brand: 'IFID';
105
+ };
97
106
  interface Twine1Metadata {
98
107
  settings: Map<string, string>;
99
108
  }
@@ -108,12 +117,25 @@ interface Twine2Metadata {
108
117
  }
109
118
  interface Story {
110
119
  name: string;
111
- ifid: string;
120
+ ifid: IFID;
112
121
  passages: Passage[];
113
- legacyIFID: string;
122
+ legacyIFID: IFID;
114
123
  twine1: Twine1Metadata;
115
124
  twine2: Twine2Metadata;
116
125
  }
126
+ type ReadonlyPassage = Readonly<Passage> & {
127
+ readonly tags: readonly string[];
128
+ };
129
+ type ReadonlyStory = Readonly<Omit<Story, 'passages' | 'twine1' | 'twine2'>> & {
130
+ readonly passages: readonly ReadonlyPassage[];
131
+ readonly twine1: Readonly<Omit<Twine1Metadata, 'settings'>> & {
132
+ readonly settings: ReadonlyMap<string, string>;
133
+ };
134
+ readonly twine2: Readonly<Omit<Twine2Metadata, 'options' | 'tagColors'>> & {
135
+ readonly options: ReadonlyMap<string, boolean>;
136
+ readonly tagColors: ReadonlyMap<string, string>;
137
+ };
138
+ };
117
139
  interface StoryFormatInfo {
118
140
  id: string;
119
141
  filename: string;
@@ -138,15 +160,16 @@ interface Twine2FormatJSON {
138
160
  url?: string;
139
161
  license?: string;
140
162
  }
141
- declare const enum ItemType {
142
- Error = 0,
143
- EOF = 1,
144
- Header = 2,
145
- Name = 3,
146
- Tags = 4,
147
- Metadata = 5,
148
- Content = 6
149
- }
163
+ declare const ItemType: {
164
+ readonly Error: 0;
165
+ readonly EOF: 1;
166
+ readonly Header: 2;
167
+ readonly Name: 3;
168
+ readonly Tags: 4;
169
+ readonly Metadata: 5;
170
+ readonly Content: 6;
171
+ };
172
+ type ItemType = (typeof ItemType)[keyof typeof ItemType];
150
173
  interface LexerItem {
151
174
  type: ItemType;
152
175
  line: number;
@@ -273,7 +296,29 @@ interface BrokenLink {
273
296
  * expect(map.deadEnds).not.toContain('Start');
274
297
  * ```
275
298
  */
276
- declare function storyInspect(story: Story): StoryMap;
299
+ declare function storyInspect(story: ReadonlyStory): StoryMap;
300
+
301
+ /**
302
+ * Story model + StoryData JSON marshal/unmarshal.
303
+ * Ported from story.go + storydata.go.
304
+ */
305
+
306
+ /**
307
+ * Builder that separates the mutable construction phase from the immutable consumption phase.
308
+ * During construction, the internal `Story` is mutable via `add()` and direct access.
309
+ * After `build()`, the story is returned as `ReadonlyStory`.
310
+ */
311
+ declare class StoryBuilder {
312
+ /** The mutable story, accessible during construction for loader functions. */
313
+ readonly story: Story;
314
+ constructor();
315
+ /** Add a passage, handling special passages (StoryData, StoryTitle, etc.). */
316
+ add(passage: Passage, diagnostics: Diagnostic[]): void;
317
+ /** Check if a passage name exists. */
318
+ has(name: string): boolean;
319
+ /** Finalize and return the story as read-only. */
320
+ build(): ReadonlyStory;
321
+ }
277
322
 
278
323
  interface DecompileResult {
279
324
  story: Story;
@@ -401,7 +446,9 @@ declare function parseSemver(v: string): [number, number, number] | null;
401
446
  declare function semverCompare(a: [number, number, number], b: [number, number, number]): number;
402
447
 
403
448
  /** Generate a new IFID (UUID v4, uppercase). */
404
- declare function generateIFID(): string;
449
+ declare function generateIFID(): IFID;
450
+ /** Validate and brand an IFID string. Throws on invalid input. */
451
+ declare function createIFID(value: string): IFID;
405
452
  /**
406
453
  * Validate an IFID string.
407
454
  * Accepts both bare UUIDs and UUID://...// wrapped format.
@@ -451,4 +498,4 @@ declare function getCacheSize(): {
451
498
  count: number;
452
499
  };
453
500
 
454
- export { type BrokenLink, CONFIG_FILENAME, type CachedFormatEntry, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type DecompileResult, type Diagnostic, type FileCacheEntry, type InlineSource, ItemType, type LexerItem, type LintResult, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type SourceLocation, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, type WordCountMethod, applyTagAliases, clearCachedFormats, compile, compileIncremental, compileToFile, decompileHTML, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, formatLintReport, generateIFID, getCacheDir, getCacheSize, getFormatSearchDirs, lint, listCachedFormats, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
501
+ export { type BrokenLink, CONFIG_FILENAME, type CachedFormatEntry, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type DecompileResult, type Diagnostic, type FileCacheEntry, type IFID, type InlineSource, ItemType, type LexerItem, type LintResult, type OutputMode, type Passage, type PassageMetadata, type ReadonlyPassage, type ReadonlyStory, type SFAIndex, type SFAIndexEntry, type SourceInput, type SourceLocation, type Story, StoryBuilder, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, type WordCountMethod, applyTagAliases, clearCachedFormats, compile, compileIncremental, compileToFile, createIFID, decompileHTML, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, formatLintReport, generateIFID, getCacheDir, getCacheSize, getFormatSearchDirs, lint, listCachedFormats, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
package/dist/index.d.ts CHANGED
@@ -54,17 +54,23 @@ interface WatchOptions extends CompileToFileOptions {
54
54
  /** Called on build errors. */
55
55
  onError?: (error: Error) => void;
56
56
  }
57
- interface Diagnostic {
58
- level: 'warning' | 'error';
57
+ type Diagnostic = {
58
+ level: 'warning';
59
59
  message: string;
60
60
  file?: string;
61
61
  line?: number;
62
- }
62
+ } | {
63
+ level: 'error';
64
+ message: string;
65
+ file?: string;
66
+ line?: number;
67
+ fatal?: boolean;
68
+ };
63
69
  interface CompileResult {
64
70
  /** The compiled output string (HTML, Twee, JSON, etc.). */
65
71
  output: string;
66
- /** The parsed story model. */
67
- story: Story;
72
+ /** The parsed story model (read-only after compilation). */
73
+ story: ReadonlyStory;
68
74
  /** The format used for compilation (undefined for non-HTML modes). */
69
75
  format?: StoryFormatInfo;
70
76
  /** Collected diagnostics. */
@@ -94,6 +100,9 @@ interface Passage {
94
100
  metadata?: PassageMetadata;
95
101
  source?: SourceLocation;
96
102
  }
103
+ type IFID = string & {
104
+ readonly __brand: 'IFID';
105
+ };
97
106
  interface Twine1Metadata {
98
107
  settings: Map<string, string>;
99
108
  }
@@ -108,12 +117,25 @@ interface Twine2Metadata {
108
117
  }
109
118
  interface Story {
110
119
  name: string;
111
- ifid: string;
120
+ ifid: IFID;
112
121
  passages: Passage[];
113
- legacyIFID: string;
122
+ legacyIFID: IFID;
114
123
  twine1: Twine1Metadata;
115
124
  twine2: Twine2Metadata;
116
125
  }
126
+ type ReadonlyPassage = Readonly<Passage> & {
127
+ readonly tags: readonly string[];
128
+ };
129
+ type ReadonlyStory = Readonly<Omit<Story, 'passages' | 'twine1' | 'twine2'>> & {
130
+ readonly passages: readonly ReadonlyPassage[];
131
+ readonly twine1: Readonly<Omit<Twine1Metadata, 'settings'>> & {
132
+ readonly settings: ReadonlyMap<string, string>;
133
+ };
134
+ readonly twine2: Readonly<Omit<Twine2Metadata, 'options' | 'tagColors'>> & {
135
+ readonly options: ReadonlyMap<string, boolean>;
136
+ readonly tagColors: ReadonlyMap<string, string>;
137
+ };
138
+ };
117
139
  interface StoryFormatInfo {
118
140
  id: string;
119
141
  filename: string;
@@ -138,15 +160,16 @@ interface Twine2FormatJSON {
138
160
  url?: string;
139
161
  license?: string;
140
162
  }
141
- declare const enum ItemType {
142
- Error = 0,
143
- EOF = 1,
144
- Header = 2,
145
- Name = 3,
146
- Tags = 4,
147
- Metadata = 5,
148
- Content = 6
149
- }
163
+ declare const ItemType: {
164
+ readonly Error: 0;
165
+ readonly EOF: 1;
166
+ readonly Header: 2;
167
+ readonly Name: 3;
168
+ readonly Tags: 4;
169
+ readonly Metadata: 5;
170
+ readonly Content: 6;
171
+ };
172
+ type ItemType = (typeof ItemType)[keyof typeof ItemType];
150
173
  interface LexerItem {
151
174
  type: ItemType;
152
175
  line: number;
@@ -273,7 +296,29 @@ interface BrokenLink {
273
296
  * expect(map.deadEnds).not.toContain('Start');
274
297
  * ```
275
298
  */
276
- declare function storyInspect(story: Story): StoryMap;
299
+ declare function storyInspect(story: ReadonlyStory): StoryMap;
300
+
301
+ /**
302
+ * Story model + StoryData JSON marshal/unmarshal.
303
+ * Ported from story.go + storydata.go.
304
+ */
305
+
306
+ /**
307
+ * Builder that separates the mutable construction phase from the immutable consumption phase.
308
+ * During construction, the internal `Story` is mutable via `add()` and direct access.
309
+ * After `build()`, the story is returned as `ReadonlyStory`.
310
+ */
311
+ declare class StoryBuilder {
312
+ /** The mutable story, accessible during construction for loader functions. */
313
+ readonly story: Story;
314
+ constructor();
315
+ /** Add a passage, handling special passages (StoryData, StoryTitle, etc.). */
316
+ add(passage: Passage, diagnostics: Diagnostic[]): void;
317
+ /** Check if a passage name exists. */
318
+ has(name: string): boolean;
319
+ /** Finalize and return the story as read-only. */
320
+ build(): ReadonlyStory;
321
+ }
277
322
 
278
323
  interface DecompileResult {
279
324
  story: Story;
@@ -401,7 +446,9 @@ declare function parseSemver(v: string): [number, number, number] | null;
401
446
  declare function semverCompare(a: [number, number, number], b: [number, number, number]): number;
402
447
 
403
448
  /** Generate a new IFID (UUID v4, uppercase). */
404
- declare function generateIFID(): string;
449
+ declare function generateIFID(): IFID;
450
+ /** Validate and brand an IFID string. Throws on invalid input. */
451
+ declare function createIFID(value: string): IFID;
405
452
  /**
406
453
  * Validate an IFID string.
407
454
  * Accepts both bare UUIDs and UUID://...// wrapped format.
@@ -451,4 +498,4 @@ declare function getCacheSize(): {
451
498
  count: number;
452
499
  };
453
500
 
454
- export { type BrokenLink, CONFIG_FILENAME, type CachedFormatEntry, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type DecompileResult, type Diagnostic, type FileCacheEntry, type InlineSource, ItemType, type LexerItem, type LintResult, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type SourceLocation, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, type WordCountMethod, applyTagAliases, clearCachedFormats, compile, compileIncremental, compileToFile, decompileHTML, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, formatLintReport, generateIFID, getCacheDir, getCacheSize, getFormatSearchDirs, lint, listCachedFormats, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
501
+ export { type BrokenLink, CONFIG_FILENAME, type CachedFormatEntry, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type DecompileResult, type Diagnostic, type FileCacheEntry, type IFID, type InlineSource, ItemType, type LexerItem, type LintResult, type OutputMode, type Passage, type PassageMetadata, type ReadonlyPassage, type ReadonlyStory, type SFAIndex, type SFAIndexEntry, type SourceInput, type SourceLocation, type Story, StoryBuilder, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, type WordCountMethod, applyTagAliases, clearCachedFormats, compile, compileIncremental, compileToFile, createIFID, decompileHTML, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, formatLintReport, generateIFID, getCacheDir, getCacheSize, getFormatSearchDirs, lint, listCachedFormats, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
package/dist/index.js CHANGED
@@ -6,6 +6,11 @@ import { randomUUID } from "crypto";
6
6
  function generateIFID() {
7
7
  return randomUUID().toUpperCase();
8
8
  }
9
+ function createIFID(value) {
10
+ const err = validateIFID(value);
11
+ if (err) throw new Error(`Invalid IFID: ${err}`);
12
+ return value.toUpperCase();
13
+ }
9
14
  function validateIFID(ifid) {
10
15
  let uuid = ifid;
11
16
  switch (ifid.length) {
@@ -487,6 +492,25 @@ function getStoryStats(story, wordCountMethod = "tweego") {
487
492
  }
488
493
  return { passages: story.passages.length, storyPassages, words };
489
494
  }
495
+ var StoryBuilder = class {
496
+ /** The mutable story, accessible during construction for loader functions. */
497
+ story;
498
+ constructor() {
499
+ this.story = createStory();
500
+ }
501
+ /** Add a passage, handling special passages (StoryData, StoryTitle, etc.). */
502
+ add(passage, diagnostics) {
503
+ storyAdd(this.story, passage, diagnostics);
504
+ }
505
+ /** Check if a passage name exists. */
506
+ has(name) {
507
+ return storyHas(this.story, name);
508
+ }
509
+ /** Finalize and return the story as read-only. */
510
+ build() {
511
+ return this.story;
512
+ }
513
+ };
490
514
 
491
515
  // src/filesystem.ts
492
516
  import { readdirSync, statSync, watch as fsWatch } from "fs";
@@ -910,6 +934,17 @@ function readFormatSource(format) {
910
934
  import { basename } from "path";
911
935
  import { statSync as statSync3 } from "fs";
912
936
 
937
+ // src/types.ts
938
+ var ItemType = {
939
+ Error: 0,
940
+ EOF: 1,
941
+ Header: 2,
942
+ Name: 3,
943
+ Tags: 4,
944
+ Metadata: 5,
945
+ Content: 6
946
+ };
947
+
913
948
  // src/lexer.ts
914
949
  var EOF = -1;
915
950
  var HEADER_DELIM = "::";
@@ -942,10 +977,16 @@ var LexerContext = class {
942
977
  throw new Error("backup would leave pos < start");
943
978
  }
944
979
  }
980
+ // Line-counting invariant: next() increments this.line for each newline during
981
+ // character-by-character scanning. However, Content items are produced by jumping
982
+ // this.pos directly (e.g. via indexOf) without calling next(), so those newlines
983
+ // haven't been counted yet. emit() re-counts newlines in [start, pos) for Content
984
+ // items to compensate. This is safe because this.start is reset to this.pos after
985
+ // every emit(), so no range is ever counted twice.
945
986
  emit(type) {
946
987
  const val = this.input.slice(this.start, this.pos);
947
988
  this.items.push({ type, line: this.line, pos: this.start, val });
948
- if (type === 6 /* Content */) {
989
+ if (type === ItemType.Content) {
949
990
  for (let i = this.start; i < this.pos; i++) {
950
991
  if (this.input.charCodeAt(i) === 10) this.line++;
951
992
  }
@@ -966,7 +1007,7 @@ var LexerContext = class {
966
1007
  }
967
1008
  errorf(message) {
968
1009
  this.items.push({
969
- type: 0 /* Error */,
1010
+ type: ItemType.Error,
970
1011
  line: this.line,
971
1012
  pos: this.start,
972
1013
  val: message
@@ -1008,7 +1049,7 @@ function lexProlog(ctx) {
1008
1049
  ctx.ignore();
1009
1050
  return lexHeaderDelim;
1010
1051
  }
1011
- ctx.emit(1 /* EOF */);
1052
+ ctx.emit(ItemType.EOF);
1012
1053
  return null;
1013
1054
  }
1014
1055
  function lexContent(ctx) {
@@ -1018,19 +1059,19 @@ function lexContent(ctx) {
1018
1059
  const i = ctx.input.indexOf(NEWLINE_HEADER_DELIM, ctx.pos);
1019
1060
  if (i > -1) {
1020
1061
  ctx.pos = i + 1;
1021
- ctx.emit(6 /* Content */);
1062
+ ctx.emit(ItemType.Content);
1022
1063
  return lexHeaderDelim;
1023
1064
  }
1024
1065
  ctx.pos = ctx.input.length;
1025
1066
  if (ctx.pos > ctx.start) {
1026
- ctx.emit(6 /* Content */);
1067
+ ctx.emit(ItemType.Content);
1027
1068
  }
1028
- ctx.emit(1 /* EOF */);
1069
+ ctx.emit(ItemType.EOF);
1029
1070
  return null;
1030
1071
  }
1031
1072
  function lexHeaderDelim(ctx) {
1032
1073
  ctx.pos += HEADER_DELIM.length;
1033
- ctx.emit(2 /* Header */);
1074
+ ctx.emit(ItemType.Header);
1034
1075
  return lexName;
1035
1076
  }
1036
1077
  function lexName(ctx) {
@@ -1059,7 +1100,7 @@ function lexName(ctx) {
1059
1100
  break outer;
1060
1101
  }
1061
1102
  }
1062
- ctx.emit(3 /* Name */);
1103
+ ctx.emit(ItemType.Name);
1063
1104
  switch (r) {
1064
1105
  case 91:
1065
1106
  return lexTags;
@@ -1076,7 +1117,7 @@ function lexName(ctx) {
1076
1117
  ctx.ignore();
1077
1118
  return lexContent;
1078
1119
  }
1079
- ctx.emit(1 /* EOF */);
1120
+ ctx.emit(ItemType.EOF);
1080
1121
  return null;
1081
1122
  }
1082
1123
  function lexNextOptionalBlock(ctx) {
@@ -1099,7 +1140,7 @@ function lexNextOptionalBlock(ctx) {
1099
1140
  ctx.ignore();
1100
1141
  return lexContent;
1101
1142
  case EOF:
1102
- ctx.emit(1 /* EOF */);
1143
+ ctx.emit(ItemType.EOF);
1103
1144
  return null;
1104
1145
  }
1105
1146
  return ctx.errorf(`illegal character '${String.fromCharCode(r)}' amid the optional blocks`);
@@ -1119,7 +1160,7 @@ function lexTags(ctx) {
1119
1160
  if (r === 10) ctx.backup();
1120
1161
  return ctx.errorf("unterminated tag block");
1121
1162
  case 93:
1122
- if (ctx.pos > ctx.start) ctx.emit(4 /* Tags */);
1163
+ if (ctx.pos > ctx.start) ctx.emit(ItemType.Tags);
1123
1164
  return lexNextOptionalBlock;
1124
1165
  case 91:
1125
1166
  return ctx.errorf(`unexpected left square bracket '['`);
@@ -1152,7 +1193,7 @@ function lexMetadata(ctx) {
1152
1193
  case 125:
1153
1194
  depth--;
1154
1195
  if (depth === 0) {
1155
- if (ctx.pos > ctx.start) ctx.emit(5 /* Metadata */);
1196
+ if (ctx.pos > ctx.start) ctx.emit(ItemType.Metadata);
1156
1197
  return lexNextOptionalBlock;
1157
1198
  }
1158
1199
  break;
@@ -1182,7 +1223,7 @@ var TweeLexer = class {
1182
1223
  nextItem() {
1183
1224
  const result = this.gen.next();
1184
1225
  if (result.done) {
1185
- return { item: { type: 1 /* EOF */, line: 0, pos: 0, val: "" }, done: true };
1226
+ return { item: { type: ItemType.EOF, line: 0, pos: 0, val: "" }, done: true };
1186
1227
  }
1187
1228
  return { item: result.value, done: false };
1188
1229
  }
@@ -1219,10 +1260,10 @@ function parseTwee(source, options = {}) {
1219
1260
  const passages = [];
1220
1261
  let current = null;
1221
1262
  let pCount = 0;
1222
- let lastType = 1 /* EOF */;
1263
+ let lastType = ItemType.EOF;
1223
1264
  for (const item of tweeLexer(source)) {
1224
1265
  switch (item.type) {
1225
- case 0 /* Error */:
1266
+ case ItemType.Error:
1226
1267
  diagnostics.push({
1227
1268
  level: "error",
1228
1269
  message: `line ${item.line}: Malformed twee source; ${item.val}.`,
@@ -1230,19 +1271,19 @@ function parseTwee(source, options = {}) {
1230
1271
  line: item.line
1231
1272
  });
1232
1273
  return { passages, diagnostics };
1233
- case 1 /* EOF */:
1274
+ case ItemType.EOF:
1234
1275
  if (pCount > 0 && current) {
1235
1276
  passages.push(current);
1236
1277
  }
1237
1278
  return { passages, diagnostics };
1238
- case 2 /* Header */:
1279
+ case ItemType.Header:
1239
1280
  pCount++;
1240
1281
  if (pCount > 1 && current) {
1241
1282
  passages.push(current);
1242
1283
  }
1243
1284
  current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1244
1285
  break;
1245
- case 3 /* Name */: {
1286
+ case ItemType.Name: {
1246
1287
  if (!current) break;
1247
1288
  const name = tweeUnescape(item.val).trim();
1248
1289
  if (name.length === 0) {
@@ -1257,9 +1298,9 @@ function parseTwee(source, options = {}) {
1257
1298
  current.name = name;
1258
1299
  break;
1259
1300
  }
1260
- case 4 /* Tags */: {
1301
+ case ItemType.Tags: {
1261
1302
  if (!current) break;
1262
- if (lastType !== 3 /* Name */) {
1303
+ if (lastType !== ItemType.Name) {
1263
1304
  diagnostics.push({
1264
1305
  level: "error",
1265
1306
  message: `line ${item.line}: Malformed twee source; optional tags block must immediately follow the passage name.`,
@@ -1272,9 +1313,9 @@ function parseTwee(source, options = {}) {
1272
1313
  current.tags = tweeUnescape(inner).split(/\s+/).filter(Boolean);
1273
1314
  break;
1274
1315
  }
1275
- case 5 /* Metadata */: {
1316
+ case ItemType.Metadata: {
1276
1317
  if (!current) break;
1277
- if (lastType !== 3 /* Name */ && lastType !== 4 /* Tags */) {
1318
+ if (lastType !== ItemType.Name && lastType !== ItemType.Tags) {
1278
1319
  diagnostics.push({
1279
1320
  level: "error",
1280
1321
  message: `line ${item.line}: Malformed twee source; optional metadata block must immediately follow the passage name or tags block.`,
@@ -1306,7 +1347,7 @@ function parseTwee(source, options = {}) {
1306
1347
  }
1307
1348
  break;
1308
1349
  }
1309
- case 6 /* Content */: {
1350
+ case ItemType.Content: {
1310
1351
  if (!current) break;
1311
1352
  if (trim) {
1312
1353
  current.text = item.val.trim();
@@ -4436,21 +4477,22 @@ var VERSION = "1.2.0";
4436
4477
 
4437
4478
  // src/output-twine2.ts
4438
4479
  var CREATOR_NAME = "Twee-ts";
4439
- function toTwine2Archive(story, startName, diagnostics, options) {
4440
- return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
4480
+ function toTwine2Archive(story, startName, options) {
4481
+ return getTwine2DataChunk(story, startName, options) + "\n";
4441
4482
  }
4442
- function toTwine2HTML(story, format, startName, diagnostics, options) {
4483
+ function toTwine2HTML(story, format, startName, options) {
4443
4484
  let template = readFormatSource(format);
4444
4485
  if (template.includes("{{STORY_NAME}}")) {
4445
- template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
4486
+ const name = htmlEscape(story.name);
4487
+ template = template.replaceAll("{{STORY_NAME}}", () => name);
4446
4488
  }
4447
4489
  if (template.includes("{{STORY_DATA}}")) {
4448
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
4490
+ const data = getTwine2DataChunk(story, startName, options);
4491
+ template = template.replace("{{STORY_DATA}}", () => data);
4449
4492
  }
4450
4493
  return template;
4451
4494
  }
4452
- function getTwine2DataChunk(story, startName, diagnostics, options) {
4453
- ensureIFID(story, diagnostics);
4495
+ function getTwine2DataChunk(story, startName, options) {
4454
4496
  const parts = [];
4455
4497
  let startID = "";
4456
4498
  let pid = 0;
@@ -4519,23 +4561,6 @@ function getTwine2DataChunk(story, startName, diagnostics, options) {
4519
4561
  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>`;
4520
4562
  return wrapper + parts.join("") + "</tw-storydata>";
4521
4563
  }
4522
- function ensureIFID(story, diagnostics) {
4523
- if (story.ifid !== "") return;
4524
- if (story.legacyIFID !== "") {
4525
- story.ifid = story.legacyIFID;
4526
- diagnostics.push({
4527
- level: "warning",
4528
- message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
4529
- });
4530
- } else {
4531
- const ifid = generateIFID();
4532
- story.ifid = ifid;
4533
- diagnostics.push({
4534
- level: "error",
4535
- message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
4536
- });
4537
- }
4538
- }
4539
4564
 
4540
4565
  // src/output-twine1.ts
4541
4566
  import { readFileSync as readFileSync2 } from "fs";
@@ -4546,7 +4571,7 @@ function toTwine1Archive(story, _startName) {
4546
4571
  return `<div id="storeArea" data-size="${count}">${data}</div>
4547
4572
  `;
4548
4573
  }
4549
- function toTwine1HTML(story, format, startName, _diagnostics) {
4574
+ function toTwine1HTML(story, format, startName) {
4550
4575
  const formatDir = dirname(format.filename);
4551
4576
  const parentDir = dirname(formatDir);
4552
4577
  let template = readFormatSource(format);
@@ -4564,10 +4589,11 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
4564
4589
  const displayStart = startName === "Start" ? "" : startName;
4565
4590
  template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
4566
4591
  template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
4567
- template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
4592
+ const startAtValue = `"${jsStringEscape(displayStart)}"`;
4593
+ template = template.replace('"START_AT"', () => startAtValue);
4568
4594
  template = template.replace('"STORY_SIZE"', `"${count}"`);
4569
4595
  if (template.includes('"STORY"')) {
4570
- template = template.replace('"STORY"', data);
4596
+ template = template.replace('"STORY"', () => data);
4571
4597
  } else {
4572
4598
  let footer;
4573
4599
  try {
@@ -4603,7 +4629,7 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
4603
4629
  try {
4604
4630
  let content = readFileSync2(componentPath, "utf-8");
4605
4631
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
4606
- return template.replace(placeholder, content);
4632
+ return template.replace(placeholder, () => content);
4607
4633
  } catch (e) {
4608
4634
  if (required) {
4609
4635
  throw new Error(
@@ -4691,7 +4717,8 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
4691
4717
  }
4692
4718
  if (parts.length > 0) {
4693
4719
  parts.push("</head>");
4694
- return html.replace("</head>", parts.join("\n"));
4720
+ const replacement = parts.join("\n");
4721
+ return html.replace("</head>", () => replacement);
4695
4722
  }
4696
4723
  return html;
4697
4724
  }
@@ -5103,6 +5130,7 @@ async function buildOutput(options, cache, changedFiles) {
5103
5130
  if (testMode) {
5104
5131
  story.twine2.options.set("debug", true);
5105
5132
  }
5133
+ ensureIFID(story, diagnostics);
5106
5134
  let output;
5107
5135
  switch (outputMode) {
5108
5136
  case "twee3":
@@ -5110,7 +5138,7 @@ async function buildOutput(options, cache, changedFiles) {
5110
5138
  output = toTwee(story, outputMode);
5111
5139
  break;
5112
5140
  case "twine2-archive":
5113
- output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
5141
+ output = toTwine2Archive(story, startName, { sourceInfo });
5114
5142
  break;
5115
5143
  case "twine1-archive":
5116
5144
  output = toTwine1Archive(story, startName);
@@ -5129,7 +5157,7 @@ async function buildOutput(options, cache, changedFiles) {
5129
5157
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
5130
5158
  }
5131
5159
  if (format.isTwine2) {
5132
- output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
5160
+ output = toTwine2HTML(story, format, startName, { sourceInfo });
5133
5161
  } else {
5134
5162
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
5135
5163
  diagnostics.push({
@@ -5137,7 +5165,7 @@ async function buildOutput(options, cache, changedFiles) {
5137
5165
  message: 'Special passage "StoryTitle" not found.'
5138
5166
  });
5139
5167
  }
5140
- output = toTwine1HTML(story, format, startName, diagnostics);
5168
+ output = toTwine1HTML(story, format, startName);
5141
5169
  }
5142
5170
  const modulePaths = options.modules ? getFilenames(options.modules) : [];
5143
5171
  output = modifyHead(output, modulePaths, options.headFile, diagnostics);
@@ -5154,6 +5182,23 @@ async function buildOutput(options, cache, changedFiles) {
5154
5182
  };
5155
5183
  return { output, story, format, diagnostics, stats };
5156
5184
  }
5185
+ function ensureIFID(story, diagnostics) {
5186
+ if (story.ifid !== "") return;
5187
+ if (story.legacyIFID !== "") {
5188
+ story.ifid = story.legacyIFID;
5189
+ diagnostics.push({
5190
+ level: "warning",
5191
+ message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
5192
+ });
5193
+ } else {
5194
+ const ifid = generateIFID();
5195
+ story.ifid = ifid;
5196
+ diagnostics.push({
5197
+ level: "error",
5198
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
5199
+ });
5200
+ }
5201
+ }
5157
5202
  function storyToJSON(story) {
5158
5203
  const scripts = [];
5159
5204
  const stylesheets = [];
@@ -5474,6 +5519,8 @@ function scaffoldConfig() {
5474
5519
  }
5475
5520
  export {
5476
5521
  CONFIG_FILENAME,
5522
+ ItemType,
5523
+ StoryBuilder,
5477
5524
  TweeLexer,
5478
5525
  TweeTsError,
5479
5526
  applyTagAliases,
@@ -5481,6 +5528,7 @@ export {
5481
5528
  compile,
5482
5529
  compileIncremental,
5483
5530
  compileToFile,
5531
+ createIFID,
5484
5532
  decompileHTML,
5485
5533
  discoverCachedFormats,
5486
5534
  discoverFormats,