@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.
@@ -57,6 +57,10 @@ function attrEscape(s) {
57
57
  if (s.length === 0) return s;
58
58
  return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'");
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 = "";
@@ -143,12 +154,14 @@ function hasMetadataSize(p) {
143
154
  return p.metadata != null && p.metadata.size != null && p.metadata.size !== "";
144
155
  }
145
156
  function hasAnyMetadata(p) {
146
- return hasMetadataPosition(p) || hasMetadataSize(p);
157
+ if (!p.metadata) return false;
158
+ return Object.values(p.metadata).some((v) => v != null && v !== "");
147
159
  }
148
160
  function marshalMetadata(meta) {
149
161
  const obj = {};
150
- if (meta.position) obj.position = meta.position;
151
- if (meta.size) obj.size = meta.size;
162
+ for (const [key, value] of Object.entries(meta)) {
163
+ if (typeof value === "string" && value) obj[key] = value;
164
+ }
152
165
  return JSON.stringify(obj);
153
166
  }
154
167
  function passageToTwee(p, outMode) {
@@ -191,9 +204,9 @@ function passageToPassagedata(p, pid) {
191
204
  } else {
192
205
  size = "100,100";
193
206
  }
194
- 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>`;
207
+ 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>`;
195
208
  }
196
- function passageToTiddler(p, pid) {
209
+ function passageToTiddler(p, pid, obfuscateRot13 = false) {
197
210
  let position;
198
211
  if (hasMetadataPosition(p)) {
199
212
  position = p.metadata?.position ?? "";
@@ -204,7 +217,10 @@ function passageToTiddler(p, pid) {
204
217
  const yp = x === 0 ? y : y + 1;
205
218
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
206
219
  }
207
- return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} twine-position=${quote(attrEscape(position))}>${tiddlerEscape(p.text)}</div>`;
220
+ const created = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
221
+ const content = obfuscateRot13 && p.name !== "StorySettings" ? tiddlerEscape(rot13(p.text)) : tiddlerEscape(p.text);
222
+ const nameComment = obfuscateRot13 ? `<!-- ${commentSanitize(p.name)} -->` : "";
223
+ 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>`;
208
224
  }
209
225
  function countWords(p) {
210
226
  let text = p.text;
@@ -247,6 +263,7 @@ function createStory() {
247
263
  formatVersion: "",
248
264
  options: /* @__PURE__ */ new Map(),
249
265
  start: "",
266
+ tags: "",
250
267
  tagColors: /* @__PURE__ */ new Map(),
251
268
  zoom: 1
252
269
  }
@@ -293,6 +310,7 @@ function marshalStoryData(story) {
293
310
  }
294
311
  if (options.length > 0) data.options = options;
295
312
  if (story.twine2.start) data.start = story.twine2.start;
313
+ if (story.twine2.tags) data.tags = story.twine2.tags;
296
314
  if (story.twine2.tagColors.size > 0) {
297
315
  data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
298
316
  }
@@ -320,6 +338,7 @@ function unmarshalStoryData(story, json) {
320
338
  }
321
339
  }
322
340
  if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
341
+ if (typeof data.tags === "string") story.twine2.tags = data.tags;
323
342
  if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
324
343
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
325
344
  if (typeof color === "string") story.twine2.tagColors.set(tag, color);
@@ -391,7 +410,7 @@ function storyAdd(story, p, diagnostics) {
391
410
  processed = { ...p, text: marshalStoryData(story) };
392
411
  } else {
393
412
  diagnostics.push({
394
- level: "error",
413
+ level: "warning",
395
414
  message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
396
415
  });
397
416
  }
@@ -542,17 +561,41 @@ function baseNameWithoutExt(filename) {
542
561
  }
543
562
 
544
563
  // src/formats.ts
564
+ function relaxedJSONParse(json) {
565
+ try {
566
+ return JSON.parse(json);
567
+ } catch {
568
+ }
569
+ let fixed = json;
570
+ fixed = fixed.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
571
+ fixed = fixed.replace(/(?<=^|[{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/gm, '"$1":');
572
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
573
+ return JSON.parse(fixed);
574
+ }
545
575
  function parseFormatJSON(source, formatId) {
546
576
  const first = source.indexOf("{");
547
577
  const last = source.lastIndexOf("}");
548
578
  if (first === -1 || last === -1) return null;
549
579
  let chunk = source.slice(first, last + 1);
550
580
  const parse = (json) => {
551
- const raw = JSON.parse(json);
581
+ const raw = relaxedJSONParse(json);
552
582
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
553
583
  const obj = raw;
554
- if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
555
- return { name: obj.name, version: obj.version, source: obj.source, proofing: obj.proofing === true };
584
+ if (typeof obj.version !== "string" || typeof obj.source !== "string") return null;
585
+ if (!parseSemver(obj.version)) return null;
586
+ const name = typeof obj.name === "string" ? obj.name : "Untitled Story Format";
587
+ const result = {
588
+ name,
589
+ version: obj.version,
590
+ source: obj.source,
591
+ proofing: obj.proofing === true
592
+ };
593
+ if (typeof obj.author === "string") result.author = obj.author;
594
+ if (typeof obj.description === "string") result.description = obj.description;
595
+ if (typeof obj.image === "string") result.image = obj.image;
596
+ if (typeof obj.url === "string") result.url = obj.url;
597
+ if (typeof obj.license === "string") result.license = obj.license;
598
+ return result;
556
599
  };
557
600
  try {
558
601
  return parse(chunk);
@@ -618,17 +661,55 @@ function discoverFormats(searchDirs) {
618
661
  format.name = data.name;
619
662
  format.version = data.version;
620
663
  format.proofing = data.proofing ?? false;
664
+ if (data.author) format.author = data.author;
665
+ if (data.description) format.description = data.description;
666
+ if (data.image) format.image = data.image;
667
+ if (data.url) format.url = data.url;
668
+ if (data.license) format.license = data.license;
621
669
  } catch {
622
670
  continue;
623
671
  }
672
+ } else {
673
+ format.name = entry;
624
674
  }
625
675
  formats.set(entry, format);
626
676
  break;
627
677
  }
628
678
  }
629
679
  }
680
+ pruneFormats(formats);
630
681
  return formats;
631
682
  }
683
+ function pruneFormats(formats) {
684
+ const bestByNameMajor = /* @__PURE__ */ new Map();
685
+ const seenNameVersion = /* @__PURE__ */ new Set();
686
+ for (const [id, f] of formats) {
687
+ if (!f.isTwine2 || !f.name || !f.version) continue;
688
+ const v = parseSemver(f.version);
689
+ if (!v) continue;
690
+ const nameVersionKey = `${f.name}@${f.version}`;
691
+ if (seenNameVersion.has(nameVersionKey)) {
692
+ formats.delete(id);
693
+ continue;
694
+ }
695
+ seenNameVersion.add(nameVersionKey);
696
+ const groupKey = `${f.name}@${v[0]}`;
697
+ const existing = bestByNameMajor.get(groupKey);
698
+ if (!existing || semverCompare(v, existing.version) > 0) {
699
+ bestByNameMajor.set(groupKey, { id, version: v });
700
+ }
701
+ }
702
+ for (const [id, f] of [...formats]) {
703
+ if (!f.isTwine2 || !f.name || !f.version) continue;
704
+ const v = parseSemver(f.version);
705
+ if (!v) continue;
706
+ const groupKey = `${f.name}@${v[0]}`;
707
+ const best = bestByNameMajor.get(groupKey);
708
+ if (best && best.id !== id) {
709
+ formats.delete(id);
710
+ }
711
+ }
712
+ }
632
713
  function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
633
714
  const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
634
715
  const basePaths = /* @__PURE__ */ new Set();
@@ -1065,8 +1146,11 @@ function parseTwee(source, options = {}) {
1065
1146
  }
1066
1147
  const parsed = raw;
1067
1148
  const meta = {};
1068
- if (typeof parsed.position === "string") meta.position = parsed.position;
1069
- if (typeof parsed.size === "string") meta.size = parsed.size;
1149
+ for (const [key, value] of Object.entries(parsed)) {
1150
+ if (typeof value === "string") {
1151
+ meta[key] = value;
1152
+ }
1153
+ }
1070
1154
  current.metadata = meta;
1071
1155
  } catch (e) {
1072
1156
  diagnostics.push({
@@ -1080,7 +1164,11 @@ function parseTwee(source, options = {}) {
1080
1164
  }
1081
1165
  case 6 /* Content */: {
1082
1166
  if (!current) break;
1083
- current.text = trim ? item.val.trim() : item.val;
1167
+ if (trim) {
1168
+ current.text = item.val.trim();
1169
+ } else {
1170
+ current.text = item.val.replace(/\n\s*$/, "");
1171
+ }
1084
1172
  break;
1085
1173
  }
1086
1174
  default: {
@@ -1308,8 +1396,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1308
1396
  }
1309
1397
  }
1310
1398
  parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
1399
+ const validTagColors = /* @__PURE__ */ new Set(["gray", "red", "orange", "yellow", "green", "blue", "purple"]);
1400
+ const hexColorPattern = /^#[0-9a-fA-F]{3,8}$/;
1311
1401
  for (const [tag, color] of story.twine2.tagColors) {
1312
- parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1402
+ if (validTagColors.has(color) || hexColorPattern.test(color)) {
1403
+ parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1404
+ }
1313
1405
  }
1314
1406
  pid = 1;
1315
1407
  for (const p of story.passages) {
@@ -1331,7 +1423,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1331
1423
  }
1332
1424
  const options = opts.join(" ");
1333
1425
  const zoom = String(story.twine2.zoom);
1334
- 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>`;
1426
+ 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>`;
1335
1427
  return wrapper + parts.join("") + "</tw-storydata>";
1336
1428
  }
1337
1429
  function ensureIFID(story, diagnostics) {
@@ -1347,7 +1439,7 @@ function ensureIFID(story, diagnostics) {
1347
1439
  story.ifid = ifid;
1348
1440
  diagnostics.push({
1349
1441
  level: "error",
1350
- message: `Story IFID not found. Add a StoryData passage with: {"ifid":"${ifid}"}`
1442
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1351
1443
  });
1352
1444
  }
1353
1445
  }
@@ -1358,7 +1450,7 @@ import { join as join3, dirname } from "path";
1358
1450
  var CREATOR_NAME2 = "twee-ts";
1359
1451
  function toTwine1Archive(story, _startName) {
1360
1452
  const { data, count } = getTwine1PassageChunk(story);
1361
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1453
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1362
1454
  `;
1363
1455
  }
1364
1456
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1403,12 +1495,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1403
1495
  return template;
1404
1496
  }
1405
1497
  function getTwine1PassageChunk(story) {
1498
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1406
1499
  let data = "";
1407
1500
  let count = 0;
1408
1501
  for (const p of story.passages) {
1409
1502
  if (hasTag(p, "Twine.private")) continue;
1410
1503
  count++;
1411
- data += passageToTiddler(p, count);
1504
+ data += passageToTiddler(p, count, obfuscateRot13);
1412
1505
  }
1413
1506
  return { data, count };
1414
1507
  }
@@ -1850,11 +1943,14 @@ function storyToJSON(story) {
1850
1943
  tags: p.tags,
1851
1944
  text: p.text
1852
1945
  };
1853
- if (p.metadata && (p.metadata.position || p.metadata.size)) {
1946
+ if (p.metadata) {
1854
1947
  const meta = {};
1855
- if (p.metadata.position) meta.position = p.metadata.position;
1856
- if (p.metadata.size) meta.size = p.metadata.size;
1857
- entry.metadata = meta;
1948
+ for (const [key, value] of Object.entries(p.metadata)) {
1949
+ if (typeof value === "string") meta[key] = value;
1950
+ }
1951
+ if (Object.keys(meta).length > 0) {
1952
+ entry.metadata = meta;
1953
+ }
1858
1954
  }
1859
1955
  storyPassages.push(entry);
1860
1956
  }
@@ -1879,4 +1975,4 @@ function storyToJSON(story) {
1879
1975
  export {
1880
1976
  compile
1881
1977
  };
1882
- //# sourceMappingURL=chunk-OGP2NXLJ.js.map
1978
+ //# sourceMappingURL=chunk-76URFKGD.js.map