@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.
@@ -64,6 +64,10 @@ function attrEscape(s) {
64
64
  if (s.length === 0) return s;
65
65
  return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'");
66
66
  }
67
+ function fullAttrEscape(s) {
68
+ if (s.length === 0) return s;
69
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
70
+ }
67
71
  function htmlEscape(s) {
68
72
  if (s.length === 0) return s;
69
73
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -86,6 +90,13 @@ function commentSanitize(s) {
86
90
  function htmlCommentSanitize(s) {
87
91
  return s.replace(/-->/g, "-- >");
88
92
  }
93
+ function rot13(s) {
94
+ if (s.length === 0) return s;
95
+ return s.replace(/[A-Za-z]/g, (ch) => {
96
+ const base = ch <= "Z" ? 65 : 97;
97
+ return String.fromCharCode((ch.charCodeAt(0) - base + 13) % 26 + base);
98
+ });
99
+ }
89
100
  function tweeUnescape(s) {
90
101
  if (s.length === 0) return s;
91
102
  let result = "";
@@ -150,12 +161,14 @@ function hasMetadataSize(p) {
150
161
  return p.metadata != null && p.metadata.size != null && p.metadata.size !== "";
151
162
  }
152
163
  function hasAnyMetadata(p) {
153
- return hasMetadataPosition(p) || hasMetadataSize(p);
164
+ if (!p.metadata) return false;
165
+ return Object.values(p.metadata).some((v) => v != null && v !== "");
154
166
  }
155
167
  function marshalMetadata(meta) {
156
168
  const obj = {};
157
- if (meta.position) obj.position = meta.position;
158
- if (meta.size) obj.size = meta.size;
169
+ for (const [key, value] of Object.entries(meta)) {
170
+ if (typeof value === "string" && value) obj[key] = value;
171
+ }
159
172
  return JSON.stringify(obj);
160
173
  }
161
174
  function passageToTwee(p, outMode) {
@@ -198,9 +211,9 @@ function passageToPassagedata(p, pid) {
198
211
  } else {
199
212
  size = "100,100";
200
213
  }
201
- 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>`;
214
+ 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>`;
202
215
  }
203
- function passageToTiddler(p, pid) {
216
+ function passageToTiddler(p, pid, obfuscateRot13 = false) {
204
217
  let position;
205
218
  if (hasMetadataPosition(p)) {
206
219
  position = p.metadata?.position ?? "";
@@ -211,7 +224,10 @@ function passageToTiddler(p, pid) {
211
224
  const yp = x === 0 ? y : y + 1;
212
225
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
213
226
  }
214
- 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>`;
215
231
  }
216
232
  function countWords(p) {
217
233
  let text = p.text;
@@ -254,6 +270,7 @@ function createStory() {
254
270
  formatVersion: "",
255
271
  options: /* @__PURE__ */ new Map(),
256
272
  start: "",
273
+ tags: "",
257
274
  tagColors: /* @__PURE__ */ new Map(),
258
275
  zoom: 1
259
276
  }
@@ -300,6 +317,7 @@ function marshalStoryData(story) {
300
317
  }
301
318
  if (options.length > 0) data.options = options;
302
319
  if (story.twine2.start) data.start = story.twine2.start;
320
+ if (story.twine2.tags) data.tags = story.twine2.tags;
303
321
  if (story.twine2.tagColors.size > 0) {
304
322
  data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
305
323
  }
@@ -327,6 +345,7 @@ function unmarshalStoryData(story, json) {
327
345
  }
328
346
  }
329
347
  if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
348
+ if (typeof data.tags === "string") story.twine2.tags = data.tags;
330
349
  if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
331
350
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
332
351
  if (typeof color === "string") story.twine2.tagColors.set(tag, color);
@@ -398,7 +417,7 @@ function storyAdd(story, p, diagnostics) {
398
417
  processed = { ...p, text: marshalStoryData(story) };
399
418
  } else {
400
419
  diagnostics.push({
401
- level: "error",
420
+ level: "warning",
402
421
  message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
403
422
  });
404
423
  }
@@ -621,17 +640,41 @@ function baseNameWithoutExt(filename) {
621
640
  }
622
641
 
623
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
+ }
624
654
  function parseFormatJSON(source, formatId) {
625
655
  const first = source.indexOf("{");
626
656
  const last = source.lastIndexOf("}");
627
657
  if (first === -1 || last === -1) return null;
628
658
  let chunk = source.slice(first, last + 1);
629
659
  const parse = (json) => {
630
- const raw = JSON.parse(json);
660
+ const raw = relaxedJSONParse(json);
631
661
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
632
662
  const obj = raw;
633
- if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
634
- 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;
635
678
  };
636
679
  try {
637
680
  return parse(chunk);
@@ -697,17 +740,55 @@ function discoverFormats(searchDirs) {
697
740
  format.name = data.name;
698
741
  format.version = data.version;
699
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;
700
748
  } catch {
701
749
  continue;
702
750
  }
751
+ } else {
752
+ format.name = entry;
703
753
  }
704
754
  formats.set(entry, format);
705
755
  break;
706
756
  }
707
757
  }
708
758
  }
759
+ pruneFormats(formats);
709
760
  return formats;
710
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
+ }
711
792
  function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
712
793
  const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
713
794
  const basePaths = /* @__PURE__ */ new Set();
@@ -1144,8 +1225,11 @@ function parseTwee(source, options = {}) {
1144
1225
  }
1145
1226
  const parsed = raw;
1146
1227
  const meta = {};
1147
- if (typeof parsed.position === "string") meta.position = parsed.position;
1148
- if (typeof parsed.size === "string") meta.size = parsed.size;
1228
+ for (const [key, value] of Object.entries(parsed)) {
1229
+ if (typeof value === "string") {
1230
+ meta[key] = value;
1231
+ }
1232
+ }
1149
1233
  current.metadata = meta;
1150
1234
  } catch (e) {
1151
1235
  diagnostics.push({
@@ -1159,7 +1243,11 @@ function parseTwee(source, options = {}) {
1159
1243
  }
1160
1244
  case 6 /* Content */: {
1161
1245
  if (!current) break;
1162
- current.text = trim ? item.val.trim() : item.val;
1246
+ if (trim) {
1247
+ current.text = item.val.trim();
1248
+ } else {
1249
+ current.text = item.val.replace(/\n\s*$/, "");
1250
+ }
1163
1251
  break;
1164
1252
  }
1165
1253
  default: {
@@ -1387,8 +1475,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1387
1475
  }
1388
1476
  }
1389
1477
  parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
1478
+ const validTagColors = /* @__PURE__ */ new Set(["gray", "red", "orange", "yellow", "green", "blue", "purple"]);
1479
+ const hexColorPattern = /^#[0-9a-fA-F]{3,8}$/;
1390
1480
  for (const [tag, color] of story.twine2.tagColors) {
1391
- parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1481
+ if (validTagColors.has(color) || hexColorPattern.test(color)) {
1482
+ parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1483
+ }
1392
1484
  }
1393
1485
  pid = 1;
1394
1486
  for (const p of story.passages) {
@@ -1410,7 +1502,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1410
1502
  }
1411
1503
  const options = opts.join(" ");
1412
1504
  const zoom = String(story.twine2.zoom);
1413
- 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>`;
1505
+ 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>`;
1414
1506
  return wrapper + parts.join("") + "</tw-storydata>";
1415
1507
  }
1416
1508
  function ensureIFID(story, diagnostics) {
@@ -1426,7 +1518,7 @@ function ensureIFID(story, diagnostics) {
1426
1518
  story.ifid = ifid;
1427
1519
  diagnostics.push({
1428
1520
  level: "error",
1429
- message: `Story IFID not found. Add a StoryData passage with: {"ifid":"${ifid}"}`
1521
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1430
1522
  });
1431
1523
  }
1432
1524
  }
@@ -1437,7 +1529,7 @@ import { join as join3, dirname } from "path";
1437
1529
  var CREATOR_NAME2 = "twee-ts";
1438
1530
  function toTwine1Archive(story, _startName) {
1439
1531
  const { data, count } = getTwine1PassageChunk(story);
1440
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1532
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1441
1533
  `;
1442
1534
  }
1443
1535
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1482,12 +1574,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1482
1574
  return template;
1483
1575
  }
1484
1576
  function getTwine1PassageChunk(story) {
1577
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1485
1578
  let data = "";
1486
1579
  let count = 0;
1487
1580
  for (const p of story.passages) {
1488
1581
  if (hasTag(p, "Twine.private")) continue;
1489
1582
  count++;
1490
- data += passageToTiddler(p, count);
1583
+ data += passageToTiddler(p, count, obfuscateRot13);
1491
1584
  }
1492
1585
  return { data, count };
1493
1586
  }
@@ -1986,11 +2079,14 @@ function storyToJSON(story) {
1986
2079
  tags: p.tags,
1987
2080
  text: p.text
1988
2081
  };
1989
- if (p.metadata && (p.metadata.position || p.metadata.size)) {
2082
+ if (p.metadata) {
1990
2083
  const meta = {};
1991
- if (p.metadata.position) meta.position = p.metadata.position;
1992
- if (p.metadata.size) meta.size = p.metadata.size;
1993
- entry.metadata = meta;
2084
+ for (const [key, value] of Object.entries(p.metadata)) {
2085
+ if (typeof value === "string") meta[key] = value;
2086
+ }
2087
+ if (Object.keys(meta).length > 0) {
2088
+ entry.metadata = meta;
2089
+ }
1994
2090
  }
1995
2091
  storyPassages.push(entry);
1996
2092
  }