@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.cjs CHANGED
@@ -108,6 +108,10 @@ function attrEscape(s) {
108
108
  if (s.length === 0) return s;
109
109
  return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'");
110
110
  }
111
+ function fullAttrEscape(s) {
112
+ if (s.length === 0) return s;
113
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
114
+ }
111
115
  function htmlEscape(s) {
112
116
  if (s.length === 0) return s;
113
117
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -130,6 +134,13 @@ function commentSanitize(s) {
130
134
  function htmlCommentSanitize(s) {
131
135
  return s.replace(/-->/g, "-- >");
132
136
  }
137
+ function rot13(s) {
138
+ if (s.length === 0) return s;
139
+ return s.replace(/[A-Za-z]/g, (ch) => {
140
+ const base = ch <= "Z" ? 65 : 97;
141
+ return String.fromCharCode((ch.charCodeAt(0) - base + 13) % 26 + base);
142
+ });
143
+ }
133
144
  function tweeUnescape(s) {
134
145
  if (s.length === 0) return s;
135
146
  let result = "";
@@ -197,12 +208,14 @@ function hasMetadataSize(p) {
197
208
  return p.metadata != null && p.metadata.size != null && p.metadata.size !== "";
198
209
  }
199
210
  function hasAnyMetadata(p) {
200
- return hasMetadataPosition(p) || hasMetadataSize(p);
211
+ if (!p.metadata) return false;
212
+ return Object.values(p.metadata).some((v) => v != null && v !== "");
201
213
  }
202
214
  function marshalMetadata(meta) {
203
215
  const obj = {};
204
- if (meta.position) obj.position = meta.position;
205
- if (meta.size) obj.size = meta.size;
216
+ for (const [key, value] of Object.entries(meta)) {
217
+ if (typeof value === "string" && value) obj[key] = value;
218
+ }
206
219
  return JSON.stringify(obj);
207
220
  }
208
221
  function passageToTwee(p, outMode) {
@@ -245,9 +258,9 @@ function passageToPassagedata(p, pid) {
245
258
  } else {
246
259
  size = "100,100";
247
260
  }
248
- 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>`;
261
+ 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>`;
249
262
  }
250
- function passageToTiddler(p, pid) {
263
+ function passageToTiddler(p, pid, obfuscateRot13 = false) {
251
264
  let position;
252
265
  if (hasMetadataPosition(p)) {
253
266
  position = p.metadata?.position ?? "";
@@ -258,7 +271,10 @@ function passageToTiddler(p, pid) {
258
271
  const yp = x === 0 ? y : y + 1;
259
272
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
260
273
  }
261
- return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} twine-position=${quote(attrEscape(position))}>${tiddlerEscape(p.text)}</div>`;
274
+ const created = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
275
+ const content = obfuscateRot13 && p.name !== "StorySettings" ? tiddlerEscape(rot13(p.text)) : tiddlerEscape(p.text);
276
+ const nameComment = obfuscateRot13 ? `<!-- ${commentSanitize(p.name)} -->` : "";
277
+ 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>`;
262
278
  }
263
279
  function countWords(p) {
264
280
  let text = p.text;
@@ -301,6 +317,7 @@ function createStory() {
301
317
  formatVersion: "",
302
318
  options: /* @__PURE__ */ new Map(),
303
319
  start: "",
320
+ tags: "",
304
321
  tagColors: /* @__PURE__ */ new Map(),
305
322
  zoom: 1
306
323
  }
@@ -347,6 +364,7 @@ function marshalStoryData(story) {
347
364
  }
348
365
  if (options.length > 0) data.options = options;
349
366
  if (story.twine2.start) data.start = story.twine2.start;
367
+ if (story.twine2.tags) data.tags = story.twine2.tags;
350
368
  if (story.twine2.tagColors.size > 0) {
351
369
  data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
352
370
  }
@@ -374,6 +392,7 @@ function unmarshalStoryData(story, json) {
374
392
  }
375
393
  }
376
394
  if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
395
+ if (typeof data.tags === "string") story.twine2.tags = data.tags;
377
396
  if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
378
397
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
379
398
  if (typeof color === "string") story.twine2.tagColors.set(tag, color);
@@ -445,7 +464,7 @@ function storyAdd(story, p, diagnostics) {
445
464
  processed = { ...p, text: marshalStoryData(story) };
446
465
  } else {
447
466
  diagnostics.push({
448
- level: "error",
467
+ level: "warning",
449
468
  message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
450
469
  });
451
470
  }
@@ -668,17 +687,41 @@ function baseNameWithoutExt(filename) {
668
687
  }
669
688
 
670
689
  // src/formats.ts
690
+ function relaxedJSONParse(json) {
691
+ try {
692
+ return JSON.parse(json);
693
+ } catch {
694
+ }
695
+ let fixed = json;
696
+ fixed = fixed.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
697
+ fixed = fixed.replace(/(?<=^|[{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/gm, '"$1":');
698
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
699
+ return JSON.parse(fixed);
700
+ }
671
701
  function parseFormatJSON(source, formatId) {
672
702
  const first = source.indexOf("{");
673
703
  const last = source.lastIndexOf("}");
674
704
  if (first === -1 || last === -1) return null;
675
705
  let chunk = source.slice(first, last + 1);
676
706
  const parse = (json) => {
677
- const raw = JSON.parse(json);
707
+ const raw = relaxedJSONParse(json);
678
708
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
679
709
  const obj = raw;
680
- if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
681
- return { name: obj.name, version: obj.version, source: obj.source, proofing: obj.proofing === true };
710
+ if (typeof obj.version !== "string" || typeof obj.source !== "string") return null;
711
+ if (!parseSemver(obj.version)) return null;
712
+ const name = typeof obj.name === "string" ? obj.name : "Untitled Story Format";
713
+ const result = {
714
+ name,
715
+ version: obj.version,
716
+ source: obj.source,
717
+ proofing: obj.proofing === true
718
+ };
719
+ if (typeof obj.author === "string") result.author = obj.author;
720
+ if (typeof obj.description === "string") result.description = obj.description;
721
+ if (typeof obj.image === "string") result.image = obj.image;
722
+ if (typeof obj.url === "string") result.url = obj.url;
723
+ if (typeof obj.license === "string") result.license = obj.license;
724
+ return result;
682
725
  };
683
726
  try {
684
727
  return parse(chunk);
@@ -744,17 +787,55 @@ function discoverFormats(searchDirs) {
744
787
  format.name = data.name;
745
788
  format.version = data.version;
746
789
  format.proofing = data.proofing ?? false;
790
+ if (data.author) format.author = data.author;
791
+ if (data.description) format.description = data.description;
792
+ if (data.image) format.image = data.image;
793
+ if (data.url) format.url = data.url;
794
+ if (data.license) format.license = data.license;
747
795
  } catch {
748
796
  continue;
749
797
  }
798
+ } else {
799
+ format.name = entry;
750
800
  }
751
801
  formats.set(entry, format);
752
802
  break;
753
803
  }
754
804
  }
755
805
  }
806
+ pruneFormats(formats);
756
807
  return formats;
757
808
  }
809
+ function pruneFormats(formats) {
810
+ const bestByNameMajor = /* @__PURE__ */ new Map();
811
+ const seenNameVersion = /* @__PURE__ */ new Set();
812
+ for (const [id, f] of formats) {
813
+ if (!f.isTwine2 || !f.name || !f.version) continue;
814
+ const v = parseSemver(f.version);
815
+ if (!v) continue;
816
+ const nameVersionKey = `${f.name}@${f.version}`;
817
+ if (seenNameVersion.has(nameVersionKey)) {
818
+ formats.delete(id);
819
+ continue;
820
+ }
821
+ seenNameVersion.add(nameVersionKey);
822
+ const groupKey = `${f.name}@${v[0]}`;
823
+ const existing = bestByNameMajor.get(groupKey);
824
+ if (!existing || semverCompare(v, existing.version) > 0) {
825
+ bestByNameMajor.set(groupKey, { id, version: v });
826
+ }
827
+ }
828
+ for (const [id, f] of [...formats]) {
829
+ if (!f.isTwine2 || !f.name || !f.version) continue;
830
+ const v = parseSemver(f.version);
831
+ if (!v) continue;
832
+ const groupKey = `${f.name}@${v[0]}`;
833
+ const best = bestByNameMajor.get(groupKey);
834
+ if (best && best.id !== id) {
835
+ formats.delete(id);
836
+ }
837
+ }
838
+ }
758
839
  function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
759
840
  const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
760
841
  const basePaths = /* @__PURE__ */ new Set();
@@ -1207,8 +1288,11 @@ function parseTwee(source, options = {}) {
1207
1288
  }
1208
1289
  const parsed = raw;
1209
1290
  const meta = {};
1210
- if (typeof parsed.position === "string") meta.position = parsed.position;
1211
- if (typeof parsed.size === "string") meta.size = parsed.size;
1291
+ for (const [key, value] of Object.entries(parsed)) {
1292
+ if (typeof value === "string") {
1293
+ meta[key] = value;
1294
+ }
1295
+ }
1212
1296
  current.metadata = meta;
1213
1297
  } catch (e) {
1214
1298
  diagnostics.push({
@@ -1222,7 +1306,11 @@ function parseTwee(source, options = {}) {
1222
1306
  }
1223
1307
  case 6 /* Content */: {
1224
1308
  if (!current) break;
1225
- current.text = trim ? item.val.trim() : item.val;
1309
+ if (trim) {
1310
+ current.text = item.val.trim();
1311
+ } else {
1312
+ current.text = item.val.replace(/\n\s*$/, "");
1313
+ }
1226
1314
  break;
1227
1315
  }
1228
1316
  default: {
@@ -1450,8 +1538,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1450
1538
  }
1451
1539
  }
1452
1540
  parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
1541
+ const validTagColors = /* @__PURE__ */ new Set(["gray", "red", "orange", "yellow", "green", "blue", "purple"]);
1542
+ const hexColorPattern = /^#[0-9a-fA-F]{3,8}$/;
1453
1543
  for (const [tag, color] of story.twine2.tagColors) {
1454
- parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1544
+ if (validTagColors.has(color) || hexColorPattern.test(color)) {
1545
+ parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1546
+ }
1455
1547
  }
1456
1548
  pid = 1;
1457
1549
  for (const p of story.passages) {
@@ -1472,23 +1564,24 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1472
1564
  if (val) opts.push(opt);
1473
1565
  }
1474
1566
  const options = opts.join(" ");
1475
- const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
1476
- 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>`;
1567
+ const zoom = String(story.twine2.zoom);
1568
+ 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>`;
1477
1569
  return wrapper + parts.join("") + "</tw-storydata>";
1478
1570
  }
1479
1571
  function ensureIFID(story, diagnostics) {
1480
1572
  if (story.ifid !== "") return;
1481
1573
  if (story.legacyIFID !== "") {
1574
+ story.ifid = story.legacyIFID;
1482
1575
  diagnostics.push({
1483
1576
  level: "warning",
1484
1577
  message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
1485
1578
  });
1486
- story.ifid = story.legacyIFID;
1487
1579
  } else {
1488
- story.ifid = generateIFID();
1580
+ const ifid = generateIFID();
1581
+ story.ifid = ifid;
1489
1582
  diagnostics.push({
1490
- level: "warning",
1491
- message: `Story IFID not found; generated ${story.ifid} for your project. Add it to a StoryData passage.`
1583
+ level: "error",
1584
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1492
1585
  });
1493
1586
  }
1494
1587
  }
@@ -1499,7 +1592,7 @@ var import_node_path6 = require("path");
1499
1592
  var CREATOR_NAME2 = "twee-ts";
1500
1593
  function toTwine1Archive(story, _startName) {
1501
1594
  const { data, count } = getTwine1PassageChunk(story);
1502
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1595
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1503
1596
  `;
1504
1597
  }
1505
1598
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1544,12 +1637,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1544
1637
  return template;
1545
1638
  }
1546
1639
  function getTwine1PassageChunk(story) {
1640
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1547
1641
  let data = "";
1548
1642
  let count = 0;
1549
1643
  for (const p of story.passages) {
1550
1644
  if (hasTag(p, "Twine.private")) continue;
1551
1645
  count++;
1552
- data += passageToTiddler(p, count);
1646
+ data += passageToTiddler(p, count, obfuscateRot13);
1553
1647
  }
1554
1648
  return { data, count };
1555
1649
  }
@@ -1885,6 +1979,7 @@ function discoverCachedFormats() {
1885
1979
  }
1886
1980
 
1887
1981
  // src/compiler.ts
1982
+ var CREATOR_NAME3 = "Twee-ts";
1888
1983
  var DEFAULT_FORMAT_ID = "sugarcube-2";
1889
1984
  var DEFAULT_START_NAME = "Start";
1890
1985
  var TweeTsError = class extends Error {
@@ -2031,28 +2126,49 @@ async function buildOutput(options) {
2031
2126
  return { output, story, format, diagnostics, stats };
2032
2127
  }
2033
2128
  function storyToJSON(story) {
2034
- return JSON.stringify(
2035
- {
2036
- name: story.name,
2037
- ifid: story.ifid,
2038
- passages: story.passages.map((p) => ({
2129
+ const scripts = [];
2130
+ const stylesheets = [];
2131
+ const storyPassages = [];
2132
+ for (const p of story.passages) {
2133
+ if (p.name === "StoryTitle" || p.name === "StoryData") continue;
2134
+ if (hasTag(p, "Twine.private")) continue;
2135
+ if (hasTag(p, "script")) {
2136
+ scripts.push(p.text);
2137
+ } else if (hasTag(p, "stylesheet")) {
2138
+ stylesheets.push(p.text);
2139
+ } else {
2140
+ const entry = {
2039
2141
  name: p.name,
2040
2142
  tags: p.tags,
2041
- text: p.text,
2042
- metadata: p.metadata ?? null
2043
- })),
2044
- twine2: {
2045
- format: story.twine2.format,
2046
- formatVersion: story.twine2.formatVersion,
2047
- start: story.twine2.start,
2048
- tagColors: Object.fromEntries(story.twine2.tagColors),
2049
- options: Object.fromEntries(story.twine2.options),
2050
- zoom: story.twine2.zoom
2143
+ text: p.text
2144
+ };
2145
+ if (p.metadata) {
2146
+ const meta = {};
2147
+ for (const [key, value] of Object.entries(p.metadata)) {
2148
+ if (typeof value === "string") meta[key] = value;
2149
+ }
2150
+ if (Object.keys(meta).length > 0) {
2151
+ entry.metadata = meta;
2152
+ }
2051
2153
  }
2052
- },
2053
- null,
2054
- 2
2055
- );
2154
+ storyPassages.push(entry);
2155
+ }
2156
+ }
2157
+ const obj = {
2158
+ name: story.name
2159
+ };
2160
+ if (story.ifid) obj.ifid = story.ifid;
2161
+ if (story.twine2.format) obj.format = story.twine2.format;
2162
+ if (story.twine2.formatVersion) obj["format-version"] = story.twine2.formatVersion;
2163
+ if (story.twine2.start) obj.start = story.twine2.start;
2164
+ if (story.twine2.tagColors.size > 0) obj["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
2165
+ if (story.twine2.zoom !== 1) obj.zoom = story.twine2.zoom;
2166
+ obj.creator = CREATOR_NAME3;
2167
+ obj["creator-version"] = VERSION;
2168
+ obj.style = stylesheets.join("\n");
2169
+ obj.script = scripts.join("\n");
2170
+ obj.passages = storyPassages;
2171
+ return JSON.stringify(obj, null, 2);
2056
2172
  }
2057
2173
 
2058
2174
  // src/inspect.ts