@rohal12/twee-ts 1.3.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) {
@@ -1473,7 +1565,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1473
1565
  }
1474
1566
  const options = opts.join(" ");
1475
1567
  const zoom = String(story.twine2.zoom);
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>`;
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) {
@@ -1489,7 +1581,7 @@ function ensureIFID(story, diagnostics) {
1489
1581
  story.ifid = ifid;
1490
1582
  diagnostics.push({
1491
1583
  level: "error",
1492
- message: `Story IFID not found. Add a StoryData passage with: {"ifid":"${ifid}"}`
1584
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1493
1585
  });
1494
1586
  }
1495
1587
  }
@@ -1500,7 +1592,7 @@ var import_node_path6 = require("path");
1500
1592
  var CREATOR_NAME2 = "twee-ts";
1501
1593
  function toTwine1Archive(story, _startName) {
1502
1594
  const { data, count } = getTwine1PassageChunk(story);
1503
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1595
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1504
1596
  `;
1505
1597
  }
1506
1598
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1545,12 +1637,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1545
1637
  return template;
1546
1638
  }
1547
1639
  function getTwine1PassageChunk(story) {
1640
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1548
1641
  let data = "";
1549
1642
  let count = 0;
1550
1643
  for (const p of story.passages) {
1551
1644
  if (hasTag(p, "Twine.private")) continue;
1552
1645
  count++;
1553
- data += passageToTiddler(p, count);
1646
+ data += passageToTiddler(p, count, obfuscateRot13);
1554
1647
  }
1555
1648
  return { data, count };
1556
1649
  }
@@ -2049,11 +2142,14 @@ function storyToJSON(story) {
2049
2142
  tags: p.tags,
2050
2143
  text: p.text
2051
2144
  };
2052
- if (p.metadata && (p.metadata.position || p.metadata.size)) {
2145
+ if (p.metadata) {
2053
2146
  const meta = {};
2054
- if (p.metadata.position) meta.position = p.metadata.position;
2055
- if (p.metadata.size) meta.size = p.metadata.size;
2056
- entry.metadata = 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
+ }
2057
2153
  }
2058
2154
  storyPassages.push(entry);
2059
2155
  }