@rohal12/twee-ts 1.3.0 → 1.5.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
@@ -38,6 +38,8 @@ interface CompileOptions {
38
38
  noRemote?: boolean;
39
39
  /** Map alias tags to canonical special tags (e.g. { library: 'script' }). */
40
40
  tagAliases?: Record<string, string>;
41
+ /** Emit source file and line as data- attributes on passage elements. Default: false. */
42
+ sourceInfo?: boolean;
41
43
  }
42
44
  interface CompileToFileOptions extends CompileOptions {
43
45
  /** Output file path. */
@@ -73,15 +75,21 @@ interface CompileStats {
73
75
  words: number;
74
76
  files: string[];
75
77
  }
78
+ interface SourceLocation {
79
+ readonly file: string;
80
+ readonly line: number;
81
+ }
76
82
  interface PassageMetadata {
77
83
  position?: string;
78
84
  size?: string;
85
+ [key: string]: string | undefined;
79
86
  }
80
87
  interface Passage {
81
88
  name: string;
82
89
  tags: string[];
83
90
  text: string;
84
91
  metadata?: PassageMetadata;
92
+ source?: SourceLocation;
85
93
  }
86
94
  interface Twine1Metadata {
87
95
  settings: Map<string, string>;
@@ -91,6 +99,7 @@ interface Twine2Metadata {
91
99
  formatVersion: string;
92
100
  options: Map<string, boolean>;
93
101
  start: string;
102
+ tags: string;
94
103
  tagColors: Map<string, string>;
95
104
  zoom: number;
96
105
  }
@@ -109,12 +118,22 @@ interface StoryFormatInfo {
109
118
  name: string;
110
119
  version: string;
111
120
  proofing: boolean;
121
+ author?: string;
122
+ description?: string;
123
+ image?: string;
124
+ url?: string;
125
+ license?: string;
112
126
  }
113
127
  interface Twine2FormatJSON {
114
128
  name: string;
115
129
  version: string;
116
130
  proofing?: boolean;
117
131
  source: string;
132
+ author?: string;
133
+ description?: string;
134
+ image?: string;
135
+ url?: string;
136
+ license?: string;
118
137
  }
119
138
  declare const enum ItemType {
120
139
  Error = 0,
@@ -159,6 +178,7 @@ interface TweeTsConfig {
159
178
  testMode?: boolean;
160
179
  noRemote?: boolean;
161
180
  tagAliases?: Record<string, string>;
181
+ sourceInfo?: boolean;
162
182
  }
163
183
 
164
184
  declare class TweeTsError extends Error {
@@ -301,10 +321,13 @@ declare function parseTwee(source: string, options?: ParseOptions): ParseResult;
301
321
  /**
302
322
  * Parse the Twine 2 format.js JSON chunk.
303
323
  * Handles Harlowe's malformed JSON by stripping the "setup" function property.
324
+ * Per spec, the name key is Optional.
304
325
  */
305
326
  declare function parseFormatJSON(source: string, formatId: string): Twine2FormatJSON | null;
306
327
  /**
307
328
  * Discover all story formats in the given search directories.
329
+ * Implements SemVer-based version pruning: within each (name, major) group,
330
+ * only the highest minor.patch survives.
308
331
  */
309
332
  declare function discoverFormats(searchDirs: string[]): Map<string, StoryFormatInfo>;
310
333
  /**
@@ -350,4 +373,4 @@ declare function resolveRemoteFormat(name: string, version: string, indices?: st
350
373
  /** Discover all cached remote formats. Returns a Map like discoverFormats(). */
351
374
  declare function discoverCachedFormats(): Map<string, StoryFormatInfo>;
352
375
 
353
- export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
376
+ export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, 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, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
package/dist/index.d.ts CHANGED
@@ -38,6 +38,8 @@ interface CompileOptions {
38
38
  noRemote?: boolean;
39
39
  /** Map alias tags to canonical special tags (e.g. { library: 'script' }). */
40
40
  tagAliases?: Record<string, string>;
41
+ /** Emit source file and line as data- attributes on passage elements. Default: false. */
42
+ sourceInfo?: boolean;
41
43
  }
42
44
  interface CompileToFileOptions extends CompileOptions {
43
45
  /** Output file path. */
@@ -73,15 +75,21 @@ interface CompileStats {
73
75
  words: number;
74
76
  files: string[];
75
77
  }
78
+ interface SourceLocation {
79
+ readonly file: string;
80
+ readonly line: number;
81
+ }
76
82
  interface PassageMetadata {
77
83
  position?: string;
78
84
  size?: string;
85
+ [key: string]: string | undefined;
79
86
  }
80
87
  interface Passage {
81
88
  name: string;
82
89
  tags: string[];
83
90
  text: string;
84
91
  metadata?: PassageMetadata;
92
+ source?: SourceLocation;
85
93
  }
86
94
  interface Twine1Metadata {
87
95
  settings: Map<string, string>;
@@ -91,6 +99,7 @@ interface Twine2Metadata {
91
99
  formatVersion: string;
92
100
  options: Map<string, boolean>;
93
101
  start: string;
102
+ tags: string;
94
103
  tagColors: Map<string, string>;
95
104
  zoom: number;
96
105
  }
@@ -109,12 +118,22 @@ interface StoryFormatInfo {
109
118
  name: string;
110
119
  version: string;
111
120
  proofing: boolean;
121
+ author?: string;
122
+ description?: string;
123
+ image?: string;
124
+ url?: string;
125
+ license?: string;
112
126
  }
113
127
  interface Twine2FormatJSON {
114
128
  name: string;
115
129
  version: string;
116
130
  proofing?: boolean;
117
131
  source: string;
132
+ author?: string;
133
+ description?: string;
134
+ image?: string;
135
+ url?: string;
136
+ license?: string;
118
137
  }
119
138
  declare const enum ItemType {
120
139
  Error = 0,
@@ -159,6 +178,7 @@ interface TweeTsConfig {
159
178
  testMode?: boolean;
160
179
  noRemote?: boolean;
161
180
  tagAliases?: Record<string, string>;
181
+ sourceInfo?: boolean;
162
182
  }
163
183
 
164
184
  declare class TweeTsError extends Error {
@@ -301,10 +321,13 @@ declare function parseTwee(source: string, options?: ParseOptions): ParseResult;
301
321
  /**
302
322
  * Parse the Twine 2 format.js JSON chunk.
303
323
  * Handles Harlowe's malformed JSON by stripping the "setup" function property.
324
+ * Per spec, the name key is Optional.
304
325
  */
305
326
  declare function parseFormatJSON(source: string, formatId: string): Twine2FormatJSON | null;
306
327
  /**
307
328
  * Discover all story formats in the given search directories.
329
+ * Implements SemVer-based version pruning: within each (name, major) group,
330
+ * only the highest minor.patch survives.
308
331
  */
309
332
  declare function discoverFormats(searchDirs: string[]): Map<string, StoryFormatInfo>;
310
333
  /**
@@ -350,4 +373,4 @@ declare function resolveRemoteFormat(name: string, version: string, indices?: st
350
373
  /** Discover all cached remote formats. Returns a Map like discoverFormats(). */
351
374
  declare function discoverCachedFormats(): Map<string, StoryFormatInfo>;
352
375
 
353
- export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
376
+ export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, 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, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
package/dist/index.js CHANGED
@@ -57,6 +57,10 @@ function attrEscape(s) {
57
57
  if (s.length === 0) return s;
58
58
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
59
59
  }
60
+ function fullAttrEscape(s) {
61
+ if (s.length === 0) return s;
62
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
63
+ }
60
64
  function htmlEscape(s) {
61
65
  if (s.length === 0) return s;
62
66
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -79,6 +83,13 @@ function commentSanitize(s) {
79
83
  function htmlCommentSanitize(s) {
80
84
  return s.replace(/-->/g, "-- >");
81
85
  }
86
+ function rot13(s) {
87
+ if (s.length === 0) return s;
88
+ return s.replace(/[A-Za-z]/g, (ch) => {
89
+ const base = ch <= "Z" ? 65 : 97;
90
+ return String.fromCharCode((ch.charCodeAt(0) - base + 13) % 26 + base);
91
+ });
92
+ }
82
93
  function tweeUnescape(s) {
83
94
  if (s.length === 0) return s;
84
95
  let result = "";
@@ -146,12 +157,14 @@ function hasMetadataSize(p) {
146
157
  return p.metadata != null && p.metadata.size != null && p.metadata.size !== "";
147
158
  }
148
159
  function hasAnyMetadata(p) {
149
- return hasMetadataPosition(p) || hasMetadataSize(p);
160
+ if (!p.metadata) return false;
161
+ return Object.values(p.metadata).some((v) => v != null && v !== "");
150
162
  }
151
163
  function marshalMetadata(meta) {
152
164
  const obj = {};
153
- if (meta.position) obj.position = meta.position;
154
- if (meta.size) obj.size = meta.size;
165
+ for (const [key, value] of Object.entries(meta)) {
166
+ if (typeof value === "string" && value) obj[key] = value;
167
+ }
155
168
  return JSON.stringify(obj);
156
169
  }
157
170
  function passageToTwee(p, outMode) {
@@ -177,7 +190,7 @@ function passageToTwee(p, outMode) {
177
190
  output += "\n\n";
178
191
  return output;
179
192
  }
180
- function passageToPassagedata(p, pid) {
193
+ function passageToPassagedata(p, pid, options) {
181
194
  let position;
182
195
  let size;
183
196
  if (hasMetadataPosition(p)) {
@@ -194,9 +207,13 @@ function passageToPassagedata(p, pid) {
194
207
  } else {
195
208
  size = "100,100";
196
209
  }
197
- return `<tw-passagedata pid="${pid}" name=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} position=${quote(attrEscape(position))} size=${quote(attrEscape(size))}>${htmlEscape(p.text)}</tw-passagedata>`;
210
+ let attrs = `<tw-passagedata pid="${pid}" name=${quote(fullAttrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} position=${quote(attrEscape(position))} size=${quote(attrEscape(size))}`;
211
+ if (options?.sourceInfo && p.source) {
212
+ attrs += ` data-source-file=${quote(attrEscape(p.source.file))} data-source-line="${p.source.line}"`;
213
+ }
214
+ return `${attrs}>${htmlEscape(p.text)}</tw-passagedata>`;
198
215
  }
199
- function passageToTiddler(p, pid) {
216
+ function passageToTiddler(p, pid, obfuscateRot13 = false) {
200
217
  let position;
201
218
  if (hasMetadataPosition(p)) {
202
219
  position = p.metadata?.position ?? "";
@@ -207,7 +224,10 @@ function passageToTiddler(p, pid) {
207
224
  const yp = x === 0 ? y : y + 1;
208
225
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
209
226
  }
210
- return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} twine-position=${quote(attrEscape(position))}>${tiddlerEscape(p.text)}</div>`;
227
+ const created = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
228
+ const content = obfuscateRot13 && p.name !== "StorySettings" ? tiddlerEscape(rot13(p.text)) : tiddlerEscape(p.text);
229
+ const nameComment = obfuscateRot13 ? `<!-- ${commentSanitize(p.name)} -->` : "";
230
+ return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} created=${quote(created)} modifier=${quote("twee")} twine-position=${quote(attrEscape(position))}>${nameComment}${content}</div>`;
211
231
  }
212
232
  function countWords(p) {
213
233
  let text = p.text;
@@ -250,6 +270,7 @@ function createStory() {
250
270
  formatVersion: "",
251
271
  options: /* @__PURE__ */ new Map(),
252
272
  start: "",
273
+ tags: "",
253
274
  tagColors: /* @__PURE__ */ new Map(),
254
275
  zoom: 1
255
276
  }
@@ -296,6 +317,7 @@ function marshalStoryData(story) {
296
317
  }
297
318
  if (options.length > 0) data.options = options;
298
319
  if (story.twine2.start) data.start = story.twine2.start;
320
+ if (story.twine2.tags) data.tags = story.twine2.tags;
299
321
  if (story.twine2.tagColors.size > 0) {
300
322
  data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
301
323
  }
@@ -323,6 +345,7 @@ function unmarshalStoryData(story, json) {
323
345
  }
324
346
  }
325
347
  if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
348
+ if (typeof data.tags === "string") story.twine2.tags = data.tags;
326
349
  if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
327
350
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
328
351
  if (typeof color === "string") story.twine2.tagColors.set(tag, color);
@@ -394,7 +417,7 @@ function storyAdd(story, p, diagnostics) {
394
417
  processed = { ...p, text: marshalStoryData(story) };
395
418
  } else {
396
419
  diagnostics.push({
397
- level: "error",
420
+ level: "warning",
398
421
  message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
399
422
  });
400
423
  }
@@ -617,17 +640,41 @@ function baseNameWithoutExt(filename) {
617
640
  }
618
641
 
619
642
  // src/formats.ts
643
+ function relaxedJSONParse(json) {
644
+ try {
645
+ return JSON.parse(json);
646
+ } catch {
647
+ }
648
+ let fixed = json;
649
+ fixed = fixed.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
650
+ fixed = fixed.replace(/(?<=^|[{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/gm, '"$1":');
651
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
652
+ return JSON.parse(fixed);
653
+ }
620
654
  function parseFormatJSON(source, formatId) {
621
655
  const first = source.indexOf("{");
622
656
  const last = source.lastIndexOf("}");
623
657
  if (first === -1 || last === -1) return null;
624
658
  let chunk = source.slice(first, last + 1);
625
659
  const parse = (json) => {
626
- const raw = JSON.parse(json);
660
+ const raw = relaxedJSONParse(json);
627
661
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
628
662
  const obj = raw;
629
- if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
630
- return { name: obj.name, version: obj.version, source: obj.source, proofing: obj.proofing === true };
663
+ if (typeof obj.version !== "string" || typeof obj.source !== "string") return null;
664
+ if (!parseSemver(obj.version)) return null;
665
+ const name = typeof obj.name === "string" ? obj.name : "Untitled Story Format";
666
+ const result = {
667
+ name,
668
+ version: obj.version,
669
+ source: obj.source,
670
+ proofing: obj.proofing === true
671
+ };
672
+ if (typeof obj.author === "string") result.author = obj.author;
673
+ if (typeof obj.description === "string") result.description = obj.description;
674
+ if (typeof obj.image === "string") result.image = obj.image;
675
+ if (typeof obj.url === "string") result.url = obj.url;
676
+ if (typeof obj.license === "string") result.license = obj.license;
677
+ return result;
631
678
  };
632
679
  try {
633
680
  return parse(chunk);
@@ -693,17 +740,55 @@ function discoverFormats(searchDirs) {
693
740
  format.name = data.name;
694
741
  format.version = data.version;
695
742
  format.proofing = data.proofing ?? false;
743
+ if (data.author) format.author = data.author;
744
+ if (data.description) format.description = data.description;
745
+ if (data.image) format.image = data.image;
746
+ if (data.url) format.url = data.url;
747
+ if (data.license) format.license = data.license;
696
748
  } catch {
697
749
  continue;
698
750
  }
751
+ } else {
752
+ format.name = entry;
699
753
  }
700
754
  formats.set(entry, format);
701
755
  break;
702
756
  }
703
757
  }
704
758
  }
759
+ pruneFormats(formats);
705
760
  return formats;
706
761
  }
762
+ function pruneFormats(formats) {
763
+ const bestByNameMajor = /* @__PURE__ */ new Map();
764
+ const seenNameVersion = /* @__PURE__ */ new Set();
765
+ for (const [id, f] of formats) {
766
+ if (!f.isTwine2 || !f.name || !f.version) continue;
767
+ const v = parseSemver(f.version);
768
+ if (!v) continue;
769
+ const nameVersionKey = `${f.name}@${f.version}`;
770
+ if (seenNameVersion.has(nameVersionKey)) {
771
+ formats.delete(id);
772
+ continue;
773
+ }
774
+ seenNameVersion.add(nameVersionKey);
775
+ const groupKey = `${f.name}@${v[0]}`;
776
+ const existing = bestByNameMajor.get(groupKey);
777
+ if (!existing || semverCompare(v, existing.version) > 0) {
778
+ bestByNameMajor.set(groupKey, { id, version: v });
779
+ }
780
+ }
781
+ for (const [id, f] of [...formats]) {
782
+ if (!f.isTwine2 || !f.name || !f.version) continue;
783
+ const v = parseSemver(f.version);
784
+ if (!v) continue;
785
+ const groupKey = `${f.name}@${v[0]}`;
786
+ const best = bestByNameMajor.get(groupKey);
787
+ if (best && best.id !== id) {
788
+ formats.delete(id);
789
+ }
790
+ }
791
+ }
707
792
  function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
708
793
  const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
709
794
  const basePaths = /* @__PURE__ */ new Set();
@@ -1106,7 +1191,7 @@ function parseTwee(source, options = {}) {
1106
1191
  if (pCount > 1 && current) {
1107
1192
  passages.push(current);
1108
1193
  }
1109
- current = { name: "", tags: [], text: "" };
1194
+ current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1110
1195
  break;
1111
1196
  case 3 /* Name */: {
1112
1197
  if (!current) break;
@@ -1156,8 +1241,11 @@ function parseTwee(source, options = {}) {
1156
1241
  }
1157
1242
  const parsed = raw;
1158
1243
  const meta = {};
1159
- if (typeof parsed.position === "string") meta.position = parsed.position;
1160
- if (typeof parsed.size === "string") meta.size = parsed.size;
1244
+ for (const [key, value] of Object.entries(parsed)) {
1245
+ if (typeof value === "string") {
1246
+ meta[key] = value;
1247
+ }
1248
+ }
1161
1249
  current.metadata = meta;
1162
1250
  } catch (e) {
1163
1251
  diagnostics.push({
@@ -1171,7 +1259,11 @@ function parseTwee(source, options = {}) {
1171
1259
  }
1172
1260
  case 6 /* Content */: {
1173
1261
  if (!current) break;
1174
- current.text = trim ? item.val.trim() : item.val;
1262
+ if (trim) {
1263
+ current.text = item.val.trim();
1264
+ } else {
1265
+ current.text = item.val.replace(/\n\s*$/, "");
1266
+ }
1175
1267
  break;
1176
1268
  }
1177
1269
  default: {
@@ -1346,20 +1438,20 @@ var VERSION = "1.2.0";
1346
1438
 
1347
1439
  // src/output-twine2.ts
1348
1440
  var CREATOR_NAME = "Twee-ts";
1349
- function toTwine2Archive(story, startName, diagnostics) {
1350
- return getTwine2DataChunk(story, startName, diagnostics) + "\n";
1441
+ function toTwine2Archive(story, startName, diagnostics, options) {
1442
+ return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
1351
1443
  }
1352
- function toTwine2HTML(story, format, startName, diagnostics) {
1444
+ function toTwine2HTML(story, format, startName, diagnostics, options) {
1353
1445
  let template = readFormatSource(format);
1354
1446
  if (template.includes("{{STORY_NAME}}")) {
1355
1447
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
1356
1448
  }
1357
1449
  if (template.includes("{{STORY_DATA}}")) {
1358
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics));
1450
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
1359
1451
  }
1360
1452
  return template;
1361
1453
  }
1362
- function getTwine2DataChunk(story, startName, diagnostics) {
1454
+ function getTwine2DataChunk(story, startName, diagnostics, options) {
1363
1455
  ensureIFID(story, diagnostics);
1364
1456
  const parts = [];
1365
1457
  let startID = "";
@@ -1399,8 +1491,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1399
1491
  }
1400
1492
  }
1401
1493
  parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
1494
+ const validTagColors = /* @__PURE__ */ new Set(["gray", "red", "orange", "yellow", "green", "blue", "purple"]);
1495
+ const hexColorPattern = /^#[0-9a-fA-F]{3,8}$/;
1402
1496
  for (const [tag, color] of story.twine2.tagColors) {
1403
- parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1497
+ if (validTagColors.has(color) || hexColorPattern.test(color)) {
1498
+ parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1499
+ }
1404
1500
  }
1405
1501
  pid = 1;
1406
1502
  for (const p of story.passages) {
@@ -1410,7 +1506,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1410
1506
  if (p.name === "StorySettings" && story.twine1.settings.size === 0) {
1411
1507
  continue;
1412
1508
  }
1413
- parts.push(passageToPassagedata(p, pid));
1509
+ parts.push(passageToPassagedata(p, pid, options));
1414
1510
  if (startName === p.name) {
1415
1511
  startID = String(pid);
1416
1512
  }
@@ -1420,9 +1516,9 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1420
1516
  for (const [opt, val] of story.twine2.options) {
1421
1517
  if (val) opts.push(opt);
1422
1518
  }
1423
- const options = opts.join(" ");
1519
+ const optionsStr = opts.join(" ");
1424
1520
  const zoom = String(story.twine2.zoom);
1425
- 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(options)}" hidden>`;
1521
+ 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>`;
1426
1522
  return wrapper + parts.join("") + "</tw-storydata>";
1427
1523
  }
1428
1524
  function ensureIFID(story, diagnostics) {
@@ -1438,7 +1534,7 @@ function ensureIFID(story, diagnostics) {
1438
1534
  story.ifid = ifid;
1439
1535
  diagnostics.push({
1440
1536
  level: "error",
1441
- message: `Story IFID not found. Add a StoryData passage with: {"ifid":"${ifid}"}`
1537
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1442
1538
  });
1443
1539
  }
1444
1540
  }
@@ -1449,7 +1545,7 @@ import { join as join3, dirname } from "path";
1449
1545
  var CREATOR_NAME2 = "twee-ts";
1450
1546
  function toTwine1Archive(story, _startName) {
1451
1547
  const { data, count } = getTwine1PassageChunk(story);
1452
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1548
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1453
1549
  `;
1454
1550
  }
1455
1551
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1494,12 +1590,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1494
1590
  return template;
1495
1591
  }
1496
1592
  function getTwine1PassageChunk(story) {
1593
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1497
1594
  let data = "";
1498
1595
  let count = 0;
1499
1596
  for (const p of story.passages) {
1500
1597
  if (hasTag(p, "Twine.private")) continue;
1501
1598
  count++;
1502
- data += passageToTiddler(p, count);
1599
+ data += passageToTiddler(p, count, obfuscateRot13);
1503
1600
  }
1504
1601
  return { data, count };
1505
1602
  }
@@ -1876,6 +1973,7 @@ async function buildOutput(options) {
1876
1973
  const twee2Compat = options.twee2Compat ?? false;
1877
1974
  const testMode = options.testMode ?? false;
1878
1975
  const noRemote = options.noRemote ?? false;
1976
+ const sourceInfo = options.sourceInfo ?? false;
1879
1977
  clearIndexCache();
1880
1978
  const filePaths = [];
1881
1979
  const inlineSources = [];
@@ -1937,7 +2035,7 @@ async function buildOutput(options) {
1937
2035
  output = toTwee(story, outputMode);
1938
2036
  break;
1939
2037
  case "twine2-archive":
1940
- output = toTwine2Archive(story, startName, diagnostics);
2038
+ output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
1941
2039
  break;
1942
2040
  case "twine1-archive":
1943
2041
  output = toTwine1Archive(story, startName);
@@ -1956,7 +2054,7 @@ async function buildOutput(options) {
1956
2054
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
1957
2055
  }
1958
2056
  if (format.isTwine2) {
1959
- output = toTwine2HTML(story, format, startName, diagnostics);
2057
+ output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
1960
2058
  } else {
1961
2059
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
1962
2060
  diagnostics.push({
@@ -1998,11 +2096,14 @@ function storyToJSON(story) {
1998
2096
  tags: p.tags,
1999
2097
  text: p.text
2000
2098
  };
2001
- if (p.metadata && (p.metadata.position || p.metadata.size)) {
2099
+ if (p.metadata) {
2002
2100
  const meta = {};
2003
- if (p.metadata.position) meta.position = p.metadata.position;
2004
- if (p.metadata.size) meta.size = p.metadata.size;
2005
- entry.metadata = meta;
2101
+ for (const [key, value] of Object.entries(p.metadata)) {
2102
+ if (typeof value === "string") meta[key] = value;
2103
+ }
2104
+ if (Object.keys(meta).length > 0) {
2105
+ entry.metadata = meta;
2106
+ }
2006
2107
  }
2007
2108
  storyPassages.push(entry);
2008
2109
  }
@@ -2186,7 +2287,7 @@ function validateConfig(data) {
2186
2287
  errors.push(`"${key}" must be a string.`);
2187
2288
  }
2188
2289
  }
2189
- for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote"]) {
2290
+ for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote", "sourceInfo"]) {
2190
2291
  if (key in obj && typeof obj[key] !== "boolean") {
2191
2292
  errors.push(`"${key}" must be a boolean.`);
2192
2293
  }