@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.
@@ -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) {
@@ -1330,23 +1422,24 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1330
1422
  if (val) opts.push(opt);
1331
1423
  }
1332
1424
  const options = opts.join(" ");
1333
- const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
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>`;
1425
+ const zoom = String(story.twine2.zoom);
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) {
1338
1430
  if (story.ifid !== "") return;
1339
1431
  if (story.legacyIFID !== "") {
1432
+ story.ifid = story.legacyIFID;
1340
1433
  diagnostics.push({
1341
1434
  level: "warning",
1342
1435
  message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
1343
1436
  });
1344
- story.ifid = story.legacyIFID;
1345
1437
  } else {
1346
- story.ifid = generateIFID();
1438
+ const ifid = generateIFID();
1439
+ story.ifid = ifid;
1347
1440
  diagnostics.push({
1348
- level: "warning",
1349
- message: `Story IFID not found; generated ${story.ifid} for your project. Add it to a StoryData passage.`
1441
+ level: "error",
1442
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1350
1443
  });
1351
1444
  }
1352
1445
  }
@@ -1357,7 +1450,7 @@ import { join as join3, dirname } from "path";
1357
1450
  var CREATOR_NAME2 = "twee-ts";
1358
1451
  function toTwine1Archive(story, _startName) {
1359
1452
  const { data, count } = getTwine1PassageChunk(story);
1360
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1453
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1361
1454
  `;
1362
1455
  }
1363
1456
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1402,12 +1495,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1402
1495
  return template;
1403
1496
  }
1404
1497
  function getTwine1PassageChunk(story) {
1498
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1405
1499
  let data = "";
1406
1500
  let count = 0;
1407
1501
  for (const p of story.passages) {
1408
1502
  if (hasTag(p, "Twine.private")) continue;
1409
1503
  count++;
1410
- data += passageToTiddler(p, count);
1504
+ data += passageToTiddler(p, count, obfuscateRot13);
1411
1505
  }
1412
1506
  return { data, count };
1413
1507
  }
@@ -1707,6 +1801,7 @@ function getCachedFormat(name, version) {
1707
1801
  }
1708
1802
 
1709
1803
  // src/compiler.ts
1804
+ var CREATOR_NAME3 = "Twee-ts";
1710
1805
  var DEFAULT_FORMAT_ID = "sugarcube-2";
1711
1806
  var DEFAULT_START_NAME = "Start";
1712
1807
  var TweeTsError = class extends Error {
@@ -1832,31 +1927,52 @@ async function buildOutput(options) {
1832
1927
  return { output, story, format, diagnostics, stats };
1833
1928
  }
1834
1929
  function storyToJSON(story) {
1835
- return JSON.stringify(
1836
- {
1837
- name: story.name,
1838
- ifid: story.ifid,
1839
- passages: story.passages.map((p) => ({
1930
+ const scripts = [];
1931
+ const stylesheets = [];
1932
+ const storyPassages = [];
1933
+ for (const p of story.passages) {
1934
+ if (p.name === "StoryTitle" || p.name === "StoryData") continue;
1935
+ if (hasTag(p, "Twine.private")) continue;
1936
+ if (hasTag(p, "script")) {
1937
+ scripts.push(p.text);
1938
+ } else if (hasTag(p, "stylesheet")) {
1939
+ stylesheets.push(p.text);
1940
+ } else {
1941
+ const entry = {
1840
1942
  name: p.name,
1841
1943
  tags: p.tags,
1842
- text: p.text,
1843
- metadata: p.metadata ?? null
1844
- })),
1845
- twine2: {
1846
- format: story.twine2.format,
1847
- formatVersion: story.twine2.formatVersion,
1848
- start: story.twine2.start,
1849
- tagColors: Object.fromEntries(story.twine2.tagColors),
1850
- options: Object.fromEntries(story.twine2.options),
1851
- zoom: story.twine2.zoom
1944
+ text: p.text
1945
+ };
1946
+ if (p.metadata) {
1947
+ const 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
+ }
1852
1954
  }
1853
- },
1854
- null,
1855
- 2
1856
- );
1955
+ storyPassages.push(entry);
1956
+ }
1957
+ }
1958
+ const obj = {
1959
+ name: story.name
1960
+ };
1961
+ if (story.ifid) obj.ifid = story.ifid;
1962
+ if (story.twine2.format) obj.format = story.twine2.format;
1963
+ if (story.twine2.formatVersion) obj["format-version"] = story.twine2.formatVersion;
1964
+ if (story.twine2.start) obj.start = story.twine2.start;
1965
+ if (story.twine2.tagColors.size > 0) obj["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
1966
+ if (story.twine2.zoom !== 1) obj.zoom = story.twine2.zoom;
1967
+ obj.creator = CREATOR_NAME3;
1968
+ obj["creator-version"] = VERSION;
1969
+ obj.style = stylesheets.join("\n");
1970
+ obj.script = scripts.join("\n");
1971
+ obj.passages = storyPassages;
1972
+ return JSON.stringify(obj, null, 2);
1857
1973
  }
1858
1974
 
1859
1975
  export {
1860
1976
  compile
1861
1977
  };
1862
- //# sourceMappingURL=chunk-S4SWCQLW.js.map
1978
+ //# sourceMappingURL=chunk-76URFKGD.js.map