@rohal12/twee-ts 1.3.0 → 1.5.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) {
@@ -174,7 +187,7 @@ function passageToTwee(p, outMode) {
174
187
  output += "\n\n";
175
188
  return output;
176
189
  }
177
- function passageToPassagedata(p, pid) {
190
+ function passageToPassagedata(p, pid, options) {
178
191
  let position;
179
192
  let size;
180
193
  if (hasMetadataPosition(p)) {
@@ -191,9 +204,13 @@ 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
+ let attrs = `<tw-passagedata pid="${pid}" name=${quote(fullAttrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} position=${quote(attrEscape(position))} size=${quote(attrEscape(size))}`;
208
+ if (options?.sourceInfo && p.source) {
209
+ attrs += ` data-source-file=${quote(attrEscape(p.source.file))} data-source-line="${p.source.line}"`;
210
+ }
211
+ return `${attrs}>${htmlEscape(p.text)}</tw-passagedata>`;
195
212
  }
196
- function passageToTiddler(p, pid) {
213
+ function passageToTiddler(p, pid, obfuscateRot13 = false) {
197
214
  let position;
198
215
  if (hasMetadataPosition(p)) {
199
216
  position = p.metadata?.position ?? "";
@@ -204,7 +221,10 @@ function passageToTiddler(p, pid) {
204
221
  const yp = x === 0 ? y : y + 1;
205
222
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
206
223
  }
207
- return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} twine-position=${quote(attrEscape(position))}>${tiddlerEscape(p.text)}</div>`;
224
+ const created = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
225
+ const content = obfuscateRot13 && p.name !== "StorySettings" ? tiddlerEscape(rot13(p.text)) : tiddlerEscape(p.text);
226
+ const nameComment = obfuscateRot13 ? `<!-- ${commentSanitize(p.name)} -->` : "";
227
+ 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
228
  }
209
229
  function countWords(p) {
210
230
  let text = p.text;
@@ -247,6 +267,7 @@ function createStory() {
247
267
  formatVersion: "",
248
268
  options: /* @__PURE__ */ new Map(),
249
269
  start: "",
270
+ tags: "",
250
271
  tagColors: /* @__PURE__ */ new Map(),
251
272
  zoom: 1
252
273
  }
@@ -293,6 +314,7 @@ function marshalStoryData(story) {
293
314
  }
294
315
  if (options.length > 0) data.options = options;
295
316
  if (story.twine2.start) data.start = story.twine2.start;
317
+ if (story.twine2.tags) data.tags = story.twine2.tags;
296
318
  if (story.twine2.tagColors.size > 0) {
297
319
  data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
298
320
  }
@@ -320,6 +342,7 @@ function unmarshalStoryData(story, json) {
320
342
  }
321
343
  }
322
344
  if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
345
+ if (typeof data.tags === "string") story.twine2.tags = data.tags;
323
346
  if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
324
347
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
325
348
  if (typeof color === "string") story.twine2.tagColors.set(tag, color);
@@ -391,7 +414,7 @@ function storyAdd(story, p, diagnostics) {
391
414
  processed = { ...p, text: marshalStoryData(story) };
392
415
  } else {
393
416
  diagnostics.push({
394
- level: "error",
417
+ level: "warning",
395
418
  message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
396
419
  });
397
420
  }
@@ -542,17 +565,41 @@ function baseNameWithoutExt(filename) {
542
565
  }
543
566
 
544
567
  // src/formats.ts
568
+ function relaxedJSONParse(json) {
569
+ try {
570
+ return JSON.parse(json);
571
+ } catch {
572
+ }
573
+ let fixed = json;
574
+ fixed = fixed.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
575
+ fixed = fixed.replace(/(?<=^|[{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/gm, '"$1":');
576
+ fixed = fixed.replace(/,\s*([}\]])/g, "$1");
577
+ return JSON.parse(fixed);
578
+ }
545
579
  function parseFormatJSON(source, formatId) {
546
580
  const first = source.indexOf("{");
547
581
  const last = source.lastIndexOf("}");
548
582
  if (first === -1 || last === -1) return null;
549
583
  let chunk = source.slice(first, last + 1);
550
584
  const parse = (json) => {
551
- const raw = JSON.parse(json);
585
+ const raw = relaxedJSONParse(json);
552
586
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
553
587
  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 };
588
+ if (typeof obj.version !== "string" || typeof obj.source !== "string") return null;
589
+ if (!parseSemver(obj.version)) return null;
590
+ const name = typeof obj.name === "string" ? obj.name : "Untitled Story Format";
591
+ const result = {
592
+ name,
593
+ version: obj.version,
594
+ source: obj.source,
595
+ proofing: obj.proofing === true
596
+ };
597
+ if (typeof obj.author === "string") result.author = obj.author;
598
+ if (typeof obj.description === "string") result.description = obj.description;
599
+ if (typeof obj.image === "string") result.image = obj.image;
600
+ if (typeof obj.url === "string") result.url = obj.url;
601
+ if (typeof obj.license === "string") result.license = obj.license;
602
+ return result;
556
603
  };
557
604
  try {
558
605
  return parse(chunk);
@@ -618,17 +665,55 @@ function discoverFormats(searchDirs) {
618
665
  format.name = data.name;
619
666
  format.version = data.version;
620
667
  format.proofing = data.proofing ?? false;
668
+ if (data.author) format.author = data.author;
669
+ if (data.description) format.description = data.description;
670
+ if (data.image) format.image = data.image;
671
+ if (data.url) format.url = data.url;
672
+ if (data.license) format.license = data.license;
621
673
  } catch {
622
674
  continue;
623
675
  }
676
+ } else {
677
+ format.name = entry;
624
678
  }
625
679
  formats.set(entry, format);
626
680
  break;
627
681
  }
628
682
  }
629
683
  }
684
+ pruneFormats(formats);
630
685
  return formats;
631
686
  }
687
+ function pruneFormats(formats) {
688
+ const bestByNameMajor = /* @__PURE__ */ new Map();
689
+ const seenNameVersion = /* @__PURE__ */ new Set();
690
+ for (const [id, f] of formats) {
691
+ if (!f.isTwine2 || !f.name || !f.version) continue;
692
+ const v = parseSemver(f.version);
693
+ if (!v) continue;
694
+ const nameVersionKey = `${f.name}@${f.version}`;
695
+ if (seenNameVersion.has(nameVersionKey)) {
696
+ formats.delete(id);
697
+ continue;
698
+ }
699
+ seenNameVersion.add(nameVersionKey);
700
+ const groupKey = `${f.name}@${v[0]}`;
701
+ const existing = bestByNameMajor.get(groupKey);
702
+ if (!existing || semverCompare(v, existing.version) > 0) {
703
+ bestByNameMajor.set(groupKey, { id, version: v });
704
+ }
705
+ }
706
+ for (const [id, f] of [...formats]) {
707
+ if (!f.isTwine2 || !f.name || !f.version) continue;
708
+ const v = parseSemver(f.version);
709
+ if (!v) continue;
710
+ const groupKey = `${f.name}@${v[0]}`;
711
+ const best = bestByNameMajor.get(groupKey);
712
+ if (best && best.id !== id) {
713
+ formats.delete(id);
714
+ }
715
+ }
716
+ }
632
717
  function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
633
718
  const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
634
719
  const basePaths = /* @__PURE__ */ new Set();
@@ -1015,7 +1100,7 @@ function parseTwee(source, options = {}) {
1015
1100
  if (pCount > 1 && current) {
1016
1101
  passages.push(current);
1017
1102
  }
1018
- current = { name: "", tags: [], text: "" };
1103
+ current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1019
1104
  break;
1020
1105
  case 3 /* Name */: {
1021
1106
  if (!current) break;
@@ -1065,8 +1150,11 @@ function parseTwee(source, options = {}) {
1065
1150
  }
1066
1151
  const parsed = raw;
1067
1152
  const meta = {};
1068
- if (typeof parsed.position === "string") meta.position = parsed.position;
1069
- if (typeof parsed.size === "string") meta.size = parsed.size;
1153
+ for (const [key, value] of Object.entries(parsed)) {
1154
+ if (typeof value === "string") {
1155
+ meta[key] = value;
1156
+ }
1157
+ }
1070
1158
  current.metadata = meta;
1071
1159
  } catch (e) {
1072
1160
  diagnostics.push({
@@ -1080,7 +1168,11 @@ function parseTwee(source, options = {}) {
1080
1168
  }
1081
1169
  case 6 /* Content */: {
1082
1170
  if (!current) break;
1083
- current.text = trim ? item.val.trim() : item.val;
1171
+ if (trim) {
1172
+ current.text = item.val.trim();
1173
+ } else {
1174
+ current.text = item.val.replace(/\n\s*$/, "");
1175
+ }
1084
1176
  break;
1085
1177
  }
1086
1178
  default: {
@@ -1255,20 +1347,20 @@ var VERSION = "1.2.0";
1255
1347
 
1256
1348
  // src/output-twine2.ts
1257
1349
  var CREATOR_NAME = "Twee-ts";
1258
- function toTwine2Archive(story, startName, diagnostics) {
1259
- return getTwine2DataChunk(story, startName, diagnostics) + "\n";
1350
+ function toTwine2Archive(story, startName, diagnostics, options) {
1351
+ return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
1260
1352
  }
1261
- function toTwine2HTML(story, format, startName, diagnostics) {
1353
+ function toTwine2HTML(story, format, startName, diagnostics, options) {
1262
1354
  let template = readFormatSource(format);
1263
1355
  if (template.includes("{{STORY_NAME}}")) {
1264
1356
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
1265
1357
  }
1266
1358
  if (template.includes("{{STORY_DATA}}")) {
1267
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics));
1359
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
1268
1360
  }
1269
1361
  return template;
1270
1362
  }
1271
- function getTwine2DataChunk(story, startName, diagnostics) {
1363
+ function getTwine2DataChunk(story, startName, diagnostics, options) {
1272
1364
  ensureIFID(story, diagnostics);
1273
1365
  const parts = [];
1274
1366
  let startID = "";
@@ -1308,8 +1400,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1308
1400
  }
1309
1401
  }
1310
1402
  parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
1403
+ const validTagColors = /* @__PURE__ */ new Set(["gray", "red", "orange", "yellow", "green", "blue", "purple"]);
1404
+ const hexColorPattern = /^#[0-9a-fA-F]{3,8}$/;
1311
1405
  for (const [tag, color] of story.twine2.tagColors) {
1312
- parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1406
+ if (validTagColors.has(color) || hexColorPattern.test(color)) {
1407
+ parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
1408
+ }
1313
1409
  }
1314
1410
  pid = 1;
1315
1411
  for (const p of story.passages) {
@@ -1319,7 +1415,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1319
1415
  if (p.name === "StorySettings" && story.twine1.settings.size === 0) {
1320
1416
  continue;
1321
1417
  }
1322
- parts.push(passageToPassagedata(p, pid));
1418
+ parts.push(passageToPassagedata(p, pid, options));
1323
1419
  if (startName === p.name) {
1324
1420
  startID = String(pid);
1325
1421
  }
@@ -1329,9 +1425,9 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1329
1425
  for (const [opt, val] of story.twine2.options) {
1330
1426
  if (val) opts.push(opt);
1331
1427
  }
1332
- const options = opts.join(" ");
1428
+ const optionsStr = opts.join(" ");
1333
1429
  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>`;
1430
+ 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(optionsStr)}" tags="${attrEscape(story.twine2.tags)}" hidden>`;
1335
1431
  return wrapper + parts.join("") + "</tw-storydata>";
1336
1432
  }
1337
1433
  function ensureIFID(story, diagnostics) {
@@ -1347,7 +1443,7 @@ function ensureIFID(story, diagnostics) {
1347
1443
  story.ifid = ifid;
1348
1444
  diagnostics.push({
1349
1445
  level: "error",
1350
- message: `Story IFID not found. Add a StoryData passage with: {"ifid":"${ifid}"}`
1446
+ message: `Story IFID not found. Add an IFID to your story: {"ifid":"${ifid}"}`
1351
1447
  });
1352
1448
  }
1353
1449
  }
@@ -1358,7 +1454,7 @@ import { join as join3, dirname } from "path";
1358
1454
  var CREATOR_NAME2 = "twee-ts";
1359
1455
  function toTwine1Archive(story, _startName) {
1360
1456
  const { data, count } = getTwine1PassageChunk(story);
1361
- return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1457
+ return `<div id="storeArea" data-size="${count}">${data}</div>
1362
1458
  `;
1363
1459
  }
1364
1460
  function toTwine1HTML(story, format, startName, _diagnostics) {
@@ -1403,12 +1499,13 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1403
1499
  return template;
1404
1500
  }
1405
1501
  function getTwine1PassageChunk(story) {
1502
+ const obfuscateRot13 = story.twine1.settings.get("obfuscate") === "rot13";
1406
1503
  let data = "";
1407
1504
  let count = 0;
1408
1505
  for (const p of story.passages) {
1409
1506
  if (hasTag(p, "Twine.private")) continue;
1410
1507
  count++;
1411
- data += passageToTiddler(p, count);
1508
+ data += passageToTiddler(p, count, obfuscateRot13);
1412
1509
  }
1413
1510
  return { data, count };
1414
1511
  }
@@ -1728,6 +1825,7 @@ async function buildOutput(options) {
1728
1825
  const twee2Compat = options.twee2Compat ?? false;
1729
1826
  const testMode = options.testMode ?? false;
1730
1827
  const noRemote = options.noRemote ?? false;
1828
+ const sourceInfo = options.sourceInfo ?? false;
1731
1829
  clearIndexCache();
1732
1830
  const filePaths = [];
1733
1831
  const inlineSources = [];
@@ -1789,7 +1887,7 @@ async function buildOutput(options) {
1789
1887
  output = toTwee(story, outputMode);
1790
1888
  break;
1791
1889
  case "twine2-archive":
1792
- output = toTwine2Archive(story, startName, diagnostics);
1890
+ output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
1793
1891
  break;
1794
1892
  case "twine1-archive":
1795
1893
  output = toTwine1Archive(story, startName);
@@ -1808,7 +1906,7 @@ async function buildOutput(options) {
1808
1906
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
1809
1907
  }
1810
1908
  if (format.isTwine2) {
1811
- output = toTwine2HTML(story, format, startName, diagnostics);
1909
+ output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
1812
1910
  } else {
1813
1911
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
1814
1912
  diagnostics.push({
@@ -1850,11 +1948,14 @@ function storyToJSON(story) {
1850
1948
  tags: p.tags,
1851
1949
  text: p.text
1852
1950
  };
1853
- if (p.metadata && (p.metadata.position || p.metadata.size)) {
1951
+ if (p.metadata) {
1854
1952
  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;
1953
+ for (const [key, value] of Object.entries(p.metadata)) {
1954
+ if (typeof value === "string") meta[key] = value;
1955
+ }
1956
+ if (Object.keys(meta).length > 0) {
1957
+ entry.metadata = meta;
1958
+ }
1858
1959
  }
1859
1960
  storyPassages.push(entry);
1860
1961
  }
@@ -1879,4 +1980,4 @@ function storyToJSON(story) {
1879
1980
  export {
1880
1981
  compile
1881
1982
  };
1882
- //# sourceMappingURL=chunk-OGP2NXLJ.js.map
1983
+ //# sourceMappingURL=chunk-KI4QMLEN.js.map