@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.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,20 @@ 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
4486
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
4446
4487
  }
4447
4488
  if (template.includes("{{STORY_DATA}}")) {
4448
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
4489
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, options));
4449
4490
  }
4450
4491
  return template;
4451
4492
  }
4452
- function getTwine2DataChunk(story, startName, diagnostics, options) {
4453
- ensureIFID(story, diagnostics);
4493
+ function getTwine2DataChunk(story, startName, options) {
4454
4494
  const parts = [];
4455
4495
  let startID = "";
4456
4496
  let pid = 0;
@@ -4519,23 +4559,6 @@ function getTwine2DataChunk(story, startName, diagnostics, options) {
4519
4559
  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
4560
  return wrapper + parts.join("") + "</tw-storydata>";
4521
4561
  }
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
4562
 
4540
4563
  // src/output-twine1.ts
4541
4564
  import { readFileSync as readFileSync2 } from "fs";
@@ -4546,7 +4569,7 @@ function toTwine1Archive(story, _startName) {
4546
4569
  return `<div id="storeArea" data-size="${count}">${data}</div>
4547
4570
  `;
4548
4571
  }
4549
- function toTwine1HTML(story, format, startName, _diagnostics) {
4572
+ function toTwine1HTML(story, format, startName) {
4550
4573
  const formatDir = dirname(format.filename);
4551
4574
  const parentDir = dirname(formatDir);
4552
4575
  let template = readFormatSource(format);
@@ -5103,6 +5126,7 @@ async function buildOutput(options, cache, changedFiles) {
5103
5126
  if (testMode) {
5104
5127
  story.twine2.options.set("debug", true);
5105
5128
  }
5129
+ ensureIFID(story, diagnostics);
5106
5130
  let output;
5107
5131
  switch (outputMode) {
5108
5132
  case "twee3":
@@ -5110,7 +5134,7 @@ async function buildOutput(options, cache, changedFiles) {
5110
5134
  output = toTwee(story, outputMode);
5111
5135
  break;
5112
5136
  case "twine2-archive":
5113
- output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
5137
+ output = toTwine2Archive(story, startName, { sourceInfo });
5114
5138
  break;
5115
5139
  case "twine1-archive":
5116
5140
  output = toTwine1Archive(story, startName);
@@ -5129,7 +5153,7 @@ async function buildOutput(options, cache, changedFiles) {
5129
5153
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
5130
5154
  }
5131
5155
  if (format.isTwine2) {
5132
- output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
5156
+ output = toTwine2HTML(story, format, startName, { sourceInfo });
5133
5157
  } else {
5134
5158
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
5135
5159
  diagnostics.push({
@@ -5137,7 +5161,7 @@ async function buildOutput(options, cache, changedFiles) {
5137
5161
  message: 'Special passage "StoryTitle" not found.'
5138
5162
  });
5139
5163
  }
5140
- output = toTwine1HTML(story, format, startName, diagnostics);
5164
+ output = toTwine1HTML(story, format, startName);
5141
5165
  }
5142
5166
  const modulePaths = options.modules ? getFilenames(options.modules) : [];
5143
5167
  output = modifyHead(output, modulePaths, options.headFile, diagnostics);
@@ -5154,6 +5178,23 @@ async function buildOutput(options, cache, changedFiles) {
5154
5178
  };
5155
5179
  return { output, story, format, diagnostics, stats };
5156
5180
  }
5181
+ function ensureIFID(story, diagnostics) {
5182
+ if (story.ifid !== "") return;
5183
+ if (story.legacyIFID !== "") {
5184
+ story.ifid = story.legacyIFID;
5185
+ diagnostics.push({
5186
+ level: "warning",
5187
+ message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
5188
+ });
5189
+ } else {
5190
+ const ifid = generateIFID();
5191
+ story.ifid = ifid;
5192
+ diagnostics.push({
5193
+ level: "error",
5194
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
5195
+ });
5196
+ }
5197
+ }
5157
5198
  function storyToJSON(story) {
5158
5199
  const scripts = [];
5159
5200
  const stylesheets = [];
@@ -5474,6 +5515,8 @@ function scaffoldConfig() {
5474
5515
  }
5475
5516
  export {
5476
5517
  CONFIG_FILENAME,
5518
+ ItemType,
5519
+ StoryBuilder,
5477
5520
  TweeLexer,
5478
5521
  TweeTsError,
5479
5522
  applyTagAliases,
@@ -5481,6 +5524,7 @@ export {
5481
5524
  compile,
5482
5525
  compileIncremental,
5483
5526
  compileToFile,
5527
+ createIFID,
5484
5528
  decompileHTML,
5485
5529
  discoverCachedFormats,
5486
5530
  discoverFormats,