@rohal12/twee-ts 1.2.0 → 1.4.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
@@ -76,6 +76,7 @@ interface CompileStats {
76
76
  interface PassageMetadata {
77
77
  position?: string;
78
78
  size?: string;
79
+ [key: string]: string | undefined;
79
80
  }
80
81
  interface Passage {
81
82
  name: string;
@@ -91,6 +92,7 @@ interface Twine2Metadata {
91
92
  formatVersion: string;
92
93
  options: Map<string, boolean>;
93
94
  start: string;
95
+ tags: string;
94
96
  tagColors: Map<string, string>;
95
97
  zoom: number;
96
98
  }
@@ -109,12 +111,22 @@ interface StoryFormatInfo {
109
111
  name: string;
110
112
  version: string;
111
113
  proofing: boolean;
114
+ author?: string;
115
+ description?: string;
116
+ image?: string;
117
+ url?: string;
118
+ license?: string;
112
119
  }
113
120
  interface Twine2FormatJSON {
114
121
  name: string;
115
122
  version: string;
116
123
  proofing?: boolean;
117
124
  source: string;
125
+ author?: string;
126
+ description?: string;
127
+ image?: string;
128
+ url?: string;
129
+ license?: string;
118
130
  }
119
131
  declare const enum ItemType {
120
132
  Error = 0,
@@ -301,10 +313,13 @@ declare function parseTwee(source: string, options?: ParseOptions): ParseResult;
301
313
  /**
302
314
  * Parse the Twine 2 format.js JSON chunk.
303
315
  * Handles Harlowe's malformed JSON by stripping the "setup" function property.
316
+ * Per spec, the name key is Optional.
304
317
  */
305
318
  declare function parseFormatJSON(source: string, formatId: string): Twine2FormatJSON | null;
306
319
  /**
307
320
  * Discover all story formats in the given search directories.
321
+ * Implements SemVer-based version pruning: within each (name, major) group,
322
+ * only the highest minor.patch survives.
308
323
  */
309
324
  declare function discoverFormats(searchDirs: string[]): Map<string, StoryFormatInfo>;
310
325
  /**
package/dist/index.d.ts CHANGED
@@ -76,6 +76,7 @@ interface CompileStats {
76
76
  interface PassageMetadata {
77
77
  position?: string;
78
78
  size?: string;
79
+ [key: string]: string | undefined;
79
80
  }
80
81
  interface Passage {
81
82
  name: string;
@@ -91,6 +92,7 @@ interface Twine2Metadata {
91
92
  formatVersion: string;
92
93
  options: Map<string, boolean>;
93
94
  start: string;
95
+ tags: string;
94
96
  tagColors: Map<string, string>;
95
97
  zoom: number;
96
98
  }
@@ -109,12 +111,22 @@ interface StoryFormatInfo {
109
111
  name: string;
110
112
  version: string;
111
113
  proofing: boolean;
114
+ author?: string;
115
+ description?: string;
116
+ image?: string;
117
+ url?: string;
118
+ license?: string;
112
119
  }
113
120
  interface Twine2FormatJSON {
114
121
  name: string;
115
122
  version: string;
116
123
  proofing?: boolean;
117
124
  source: string;
125
+ author?: string;
126
+ description?: string;
127
+ image?: string;
128
+ url?: string;
129
+ license?: string;
118
130
  }
119
131
  declare const enum ItemType {
120
132
  Error = 0,
@@ -301,10 +313,13 @@ declare function parseTwee(source: string, options?: ParseOptions): ParseResult;
301
313
  /**
302
314
  * Parse the Twine 2 format.js JSON chunk.
303
315
  * Handles Harlowe's malformed JSON by stripping the "setup" function property.
316
+ * Per spec, the name key is Optional.
304
317
  */
305
318
  declare function parseFormatJSON(source: string, formatId: string): Twine2FormatJSON | null;
306
319
  /**
307
320
  * Discover all story formats in the given search directories.
321
+ * Implements SemVer-based version pruning: within each (name, major) group,
322
+ * only the highest minor.patch survives.
308
323
  */
309
324
  declare function discoverFormats(searchDirs: string[]): Map<string, StoryFormatInfo>;
310
325
  /**
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) {
@@ -194,9 +207,9 @@ 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
+ return `<tw-passagedata pid="${pid}" name=${quote(fullAttrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} position=${quote(attrEscape(position))} size=${quote(attrEscape(size))}>${htmlEscape(p.text)}</tw-passagedata>`;
198
211
  }
199
- function passageToTiddler(p, pid) {
212
+ function passageToTiddler(p, pid, obfuscateRot13 = false) {
200
213
  let position;
201
214
  if (hasMetadataPosition(p)) {
202
215
  position = p.metadata?.position ?? "";
@@ -207,7 +220,10 @@ function passageToTiddler(p, pid) {
207
220
  const yp = x === 0 ? y : y + 1;
208
221
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
209
222
  }
210
- return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} twine-position=${quote(attrEscape(position))}>${tiddlerEscape(p.text)}</div>`;
223
+ const created = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
224
+ const content = obfuscateRot13 && p.name !== "StorySettings" ? tiddlerEscape(rot13(p.text)) : tiddlerEscape(p.text);
225
+ const nameComment = obfuscateRot13 ? `<!-- ${commentSanitize(p.name)} -->` : "";
226
+ 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
227
  }
212
228
  function countWords(p) {
213
229
  let text = p.text;
@@ -250,6 +266,7 @@ function createStory() {
250
266
  formatVersion: "",
251
267
  options: /* @__PURE__ */ new Map(),
252
268
  start: "",
269
+ tags: "",
253
270
  tagColors: /* @__PURE__ */ new Map(),
254
271
  zoom: 1
255
272
  }
@@ -296,6 +313,7 @@ function marshalStoryData(story) {
296
313
  }
297
314
  if (options.length > 0) data.options = options;
298
315
  if (story.twine2.start) data.start = story.twine2.start;
316
+ if (story.twine2.tags) data.tags = story.twine2.tags;
299
317
  if (story.twine2.tagColors.size > 0) {
300
318
  data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
301
319
  }
@@ -323,6 +341,7 @@ function unmarshalStoryData(story, json) {
323
341
  }
324
342
  }
325
343
  if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
344
+ if (typeof data.tags === "string") story.twine2.tags = data.tags;
326
345
  if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
327
346
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
328
347
  if (typeof color === "string") story.twine2.tagColors.set(tag, color);
@@ -394,7 +413,7 @@ function storyAdd(story, p, diagnostics) {
394
413
  processed = { ...p, text: marshalStoryData(story) };
395
414
  } else {
396
415
  diagnostics.push({
397
- level: "error",
416
+ level: "warning",
398
417
  message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
399
418
  });
400
419
  }
@@ -617,17 +636,41 @@ function baseNameWithoutExt(filename) {
617
636
  }
618
637
 
619
638
  // src/formats.ts
639
+ function relaxedJSONParse(json) {
640
+ try {
641
+ return JSON.parse(json);
642
+ } catch {
643
+ }
644
+ let fixed = json;
645
+ fixed = fixed.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
646
+ fixed = fixed.replace(/(?<=^|[{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/gm, '"$1":');
647
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
648
+ return JSON.parse(fixed);
649
+ }
620
650
  function parseFormatJSON(source, formatId) {
621
651
  const first = source.indexOf("{");
622
652
  const last = source.lastIndexOf("}");
623
653
  if (first === -1 || last === -1) return null;
624
654
  let chunk = source.slice(first, last + 1);
625
655
  const parse = (json) => {
626
- const raw = JSON.parse(json);
656
+ const raw = relaxedJSONParse(json);
627
657
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
628
658
  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 };
659
+ if (typeof obj.version !== "string" || typeof obj.source !== "string") return null;
660
+ if (!parseSemver(obj.version)) return null;
661
+ const name = typeof obj.name === "string" ? obj.name : "Untitled Story Format";
662
+ const result = {
663
+ name,
664
+ version: obj.version,
665
+ source: obj.source,
666
+ proofing: obj.proofing === true
667
+ };
668
+ if (typeof obj.author === "string") result.author = obj.author;
669
+ if (typeof obj.description === "string") result.description = obj.description;
670
+ if (typeof obj.image === "string") result.image = obj.image;
671
+ if (typeof obj.url === "string") result.url = obj.url;
672
+ if (typeof obj.license === "string") result.license = obj.license;
673
+ return result;
631
674
  };
632
675
  try {
633
676
  return parse(chunk);
@@ -693,17 +736,55 @@ function discoverFormats(searchDirs) {
693
736
  format.name = data.name;
694
737
  format.version = data.version;
695
738
  format.proofing = data.proofing ?? false;
739
+ if (data.author) format.author = data.author;
740
+ if (data.description) format.description = data.description;
741
+ if (data.image) format.image = data.image;
742
+ if (data.url) format.url = data.url;
743
+ if (data.license) format.license = data.license;
696
744
  } catch {
697
745
  continue;
698
746
  }
747
+ } else {
748
+ format.name = entry;
699
749
  }
700
750
  formats.set(entry, format);
701
751
  break;
702
752
  }
703
753
  }
704
754
  }
755
+ pruneFormats(formats);
705
756
  return formats;
706
757
  }
758
+ function pruneFormats(formats) {
759
+ const bestByNameMajor = /* @__PURE__ */ new Map();
760
+ const seenNameVersion = /* @__PURE__ */ new Set();
761
+ for (const [id, f] of formats) {
762
+ if (!f.isTwine2 || !f.name || !f.version) continue;
763
+ const v = parseSemver(f.version);
764
+ if (!v) continue;
765
+ const nameVersionKey = `${f.name}@${f.version}`;
766
+ if (seenNameVersion.has(nameVersionKey)) {
767
+ formats.delete(id);
768
+ continue;
769
+ }
770
+ seenNameVersion.add(nameVersionKey);
771
+ const groupKey = `${f.name}@${v[0]}`;
772
+ const existing = bestByNameMajor.get(groupKey);
773
+ if (!existing || semverCompare(v, existing.version) > 0) {
774
+ bestByNameMajor.set(groupKey, { id, version: v });
775
+ }
776
+ }
777
+ for (const [id, f] of [...formats]) {
778
+ if (!f.isTwine2 || !f.name || !f.version) continue;
779
+ const v = parseSemver(f.version);
780
+ if (!v) continue;
781
+ const groupKey = `${f.name}@${v[0]}`;
782
+ const best = bestByNameMajor.get(groupKey);
783
+ if (best && best.id !== id) {
784
+ formats.delete(id);
785
+ }
786
+ }
787
+ }
707
788
  function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
708
789
  const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
709
790
  const basePaths = /* @__PURE__ */ new Set();
@@ -1156,8 +1237,11 @@ function parseTwee(source, options = {}) {
1156
1237
  }
1157
1238
  const parsed = raw;
1158
1239
  const meta = {};
1159
- if (typeof parsed.position === "string") meta.position = parsed.position;
1160
- if (typeof parsed.size === "string") meta.size = parsed.size;
1240
+ for (const [key, value] of Object.entries(parsed)) {
1241
+ if (typeof value === "string") {
1242
+ meta[key] = value;
1243
+ }
1244
+ }
1161
1245
  current.metadata = meta;
1162
1246
  } catch (e) {
1163
1247
  diagnostics.push({
@@ -1171,7 +1255,11 @@ function parseTwee(source, options = {}) {
1171
1255
  }
1172
1256
  case 6 /* Content */: {
1173
1257
  if (!current) break;
1174
- current.text = trim ? item.val.trim() : item.val;
1258
+ if (trim) {
1259
+ current.text = item.val.trim();
1260
+ } else {
1261
+ current.text = item.val.replace(/\n\s*$/, "");
1262
+ }
1175
1263
  break;
1176
1264
  }
1177
1265
  default: {
@@ -1399,8 +1487,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1399
1487
  }
1400
1488
  }
1401
1489
  parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
1490
+ const validTagColors = /* @__PURE__ */ new Set(["gray", "red", "orange", "yellow", "green", "blue", "purple"]);
1491
+ const hexColorPattern = /^#[0-9a-fA-F]{3,8}$/;
1402
1492
  for (const [tag, color] of story.twine2.tagColors) {
1403
- parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1493
+ if (validTagColors.has(color) || hexColorPattern.test(color)) {
1494
+ parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1495
+ }
1404
1496
  }
1405
1497
  pid = 1;
1406
1498
  for (const p of story.passages) {
@@ -1421,23 +1513,24 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1421
1513
  if (val) opts.push(opt);
1422
1514
  }
1423
1515
  const options = opts.join(" ");
1424
- const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
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>`;
1516
+ const zoom = String(story.twine2.zoom);
1517
+ 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)}" tags="${attrEscape(story.twine2.tags)}" hidden>`;
1426
1518
  return wrapper + parts.join("") + "</tw-storydata>";
1427
1519
  }
1428
1520
  function ensureIFID(story, diagnostics) {
1429
1521
  if (story.ifid !== "") return;
1430
1522
  if (story.legacyIFID !== "") {
1523
+ story.ifid = story.legacyIFID;
1431
1524
  diagnostics.push({
1432
1525
  level: "warning",
1433
1526
  message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
1434
1527
  });
1435
- story.ifid = story.legacyIFID;
1436
1528
  } else {
1437
- story.ifid = generateIFID();
1529
+ const ifid = generateIFID();
1530
+ story.ifid = ifid;
1438
1531
  diagnostics.push({
1439
- level: "warning",
1440
- message: `Story IFID not found; generated ${story.ifid} for your project. Add it to a StoryData passage.`
1532
+ level: "error",
1533
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1441
1534
  });
1442
1535
  }
1443
1536
  }
@@ -1448,7 +1541,7 @@ import { join as join3, dirname } from "path";
1448
1541
  var CREATOR_NAME2 = "twee-ts";
1449
1542
  function toTwine1Archive(story, _startName) {
1450
1543
  const { data, count } = getTwine1PassageChunk(story);
1451
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1544
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1452
1545
  `;
1453
1546
  }
1454
1547
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1493,12 +1586,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1493
1586
  return template;
1494
1587
  }
1495
1588
  function getTwine1PassageChunk(story) {
1589
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1496
1590
  let data = "";
1497
1591
  let count = 0;
1498
1592
  for (const p of story.passages) {
1499
1593
  if (hasTag(p, "Twine.private")) continue;
1500
1594
  count++;
1501
- data += passageToTiddler(p, count);
1595
+ data += passageToTiddler(p, count, obfuscateRot13);
1502
1596
  }
1503
1597
  return { data, count };
1504
1598
  }
@@ -1834,6 +1928,7 @@ function discoverCachedFormats() {
1834
1928
  }
1835
1929
 
1836
1930
  // src/compiler.ts
1931
+ var CREATOR_NAME3 = "Twee-ts";
1837
1932
  var DEFAULT_FORMAT_ID = "sugarcube-2";
1838
1933
  var DEFAULT_START_NAME = "Start";
1839
1934
  var TweeTsError = class extends Error {
@@ -1980,28 +2075,49 @@ async function buildOutput(options) {
1980
2075
  return { output, story, format, diagnostics, stats };
1981
2076
  }
1982
2077
  function storyToJSON(story) {
1983
- return JSON.stringify(
1984
- {
1985
- name: story.name,
1986
- ifid: story.ifid,
1987
- passages: story.passages.map((p) => ({
2078
+ const scripts = [];
2079
+ const stylesheets = [];
2080
+ const storyPassages = [];
2081
+ for (const p of story.passages) {
2082
+ if (p.name === "StoryTitle" || p.name === "StoryData") continue;
2083
+ if (hasTag(p, "Twine.private")) continue;
2084
+ if (hasTag(p, "script")) {
2085
+ scripts.push(p.text);
2086
+ } else if (hasTag(p, "stylesheet")) {
2087
+ stylesheets.push(p.text);
2088
+ } else {
2089
+ const entry = {
1988
2090
  name: p.name,
1989
2091
  tags: p.tags,
1990
- text: p.text,
1991
- metadata: p.metadata ?? null
1992
- })),
1993
- twine2: {
1994
- format: story.twine2.format,
1995
- formatVersion: story.twine2.formatVersion,
1996
- start: story.twine2.start,
1997
- tagColors: Object.fromEntries(story.twine2.tagColors),
1998
- options: Object.fromEntries(story.twine2.options),
1999
- zoom: story.twine2.zoom
2092
+ text: p.text
2093
+ };
2094
+ if (p.metadata) {
2095
+ const meta = {};
2096
+ for (const [key, value] of Object.entries(p.metadata)) {
2097
+ if (typeof value === "string") meta[key] = value;
2098
+ }
2099
+ if (Object.keys(meta).length > 0) {
2100
+ entry.metadata = meta;
2101
+ }
2000
2102
  }
2001
- },
2002
- null,
2003
- 2
2004
- );
2103
+ storyPassages.push(entry);
2104
+ }
2105
+ }
2106
+ const obj = {
2107
+ name: story.name
2108
+ };
2109
+ if (story.ifid) obj.ifid = story.ifid;
2110
+ if (story.twine2.format) obj.format = story.twine2.format;
2111
+ if (story.twine2.formatVersion) obj["format-version"] = story.twine2.formatVersion;
2112
+ if (story.twine2.start) obj.start = story.twine2.start;
2113
+ if (story.twine2.tagColors.size > 0) obj["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
2114
+ if (story.twine2.zoom !== 1) obj.zoom = story.twine2.zoom;
2115
+ obj.creator = CREATOR_NAME3;
2116
+ obj["creator-version"] = VERSION;
2117
+ obj.style = stylesheets.join("\n");
2118
+ obj.script = scripts.join("\n");
2119
+ obj.passages = storyPassages;
2120
+ return JSON.stringify(obj, null, 2);
2005
2121
  }
2006
2122
 
2007
2123
  // src/inspect.ts