@rohal12/twee-ts 1.16.1 → 1.17.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.
@@ -4419,14 +4419,16 @@ function parseDocument(data, options) {
4419
4419
  */
4420
4420
  /**
4421
4421
  * Parse a Twine 2 or Twine 1 compiled HTML file back into a Story model.
4422
+ * Tweego always trims passage text; here `trim: false` keeps it exactly, as for Twee sources.
4422
4423
  */
4423
- function decompileHTML(html) {
4424
+ function decompileHTML(html, options = {}) {
4424
4425
  const diagnostics = [];
4425
4426
  const story = createStory();
4427
+ const passageText = options.trim === false ? (text) => text : (text) => text.trim();
4426
4428
  const doc = parseDocument(html);
4427
4429
  const twine2Data = findElement(doc, "tw-storydata");
4428
4430
  if (twine2Data) {
4429
- decompileTwine2(twine2Data, story, diagnostics);
4431
+ decompileTwine2(twine2Data, story, passageText, diagnostics);
4430
4432
  return {
4431
4433
  story,
4432
4434
  diagnostics
@@ -4434,7 +4436,7 @@ function decompileHTML(html) {
4434
4436
  }
4435
4437
  const twine1Data = findElementByIdPattern(doc, /^store(?:-a|A)rea$/);
4436
4438
  if (twine1Data) {
4437
- decompileTwine1(twine1Data, story, diagnostics);
4439
+ decompileTwine1(twine1Data, story, passageText, diagnostics);
4438
4440
  return {
4439
4441
  story,
4440
4442
  diagnostics
@@ -4449,7 +4451,7 @@ function decompileHTML(html) {
4449
4451
  diagnostics
4450
4452
  };
4451
4453
  }
4452
- function decompileTwine2(storyData, story, diagnostics) {
4454
+ function decompileTwine2(storyData, story, passageText, diagnostics) {
4453
4455
  let startnode = 0;
4454
4456
  const attrs = storyData.attribs ?? {};
4455
4457
  if (attrs["name"]) story.name = attrs["name"];
@@ -4479,8 +4481,9 @@ function decompileTwine2(storyData, story, diagnostics) {
4479
4481
  switch (node.name) {
4480
4482
  case "style":
4481
4483
  case "script": {
4482
- const text = getTextContent(node).trim();
4483
- if (text.length === 0) continue;
4484
+ const content = getTextContent(node);
4485
+ if (content.trim().length === 0) continue;
4486
+ const text = passageText(content);
4484
4487
  storyAdd(story, {
4485
4488
  name: node.name === "style" ? "Story Stylesheet" : "Story JavaScript",
4486
4489
  tags: node.name === "style" ? ["stylesheet"] : ["script"],
@@ -4515,7 +4518,7 @@ function decompileTwine2(storyData, story, diagnostics) {
4515
4518
  const passage = {
4516
4519
  name,
4517
4520
  tags,
4518
- text: getTextContent(node).trim()
4521
+ text: passageText(getTextContent(node))
4519
4522
  };
4520
4523
  if (metadata.position || metadata.size) passage.metadata = metadata;
4521
4524
  storyAdd(story, passage, diagnostics);
@@ -4529,8 +4532,8 @@ function decompileTwine2(storyData, story, diagnostics) {
4529
4532
  text: marshalStoryData(story)
4530
4533
  }, diagnostics);
4531
4534
  }
4532
- function decompileTwine1(storeArea, story, diagnostics) {
4533
- const passages = (storeArea.children ?? []).filter(isTiddler).map(tiddlerToPassage);
4535
+ function decompileTwine1(storeArea, story, passageText, diagnostics) {
4536
+ const passages = (storeArea.children ?? []).filter(isTiddler).map((node) => tiddlerToPassage(node, passageText));
4534
4537
  const obfuscated = isRot13Obfuscated(passages);
4535
4538
  for (const p of passages) storyAdd(story, obfuscated && p.name !== "StorySettings" ? {
4536
4539
  ...p,
@@ -4540,12 +4543,12 @@ function decompileTwine1(storeArea, story, diagnostics) {
4540
4543
  function isTiddler(node) {
4541
4544
  return node.type === "tag" && node.name === "div" && node.attribs !== void 0 && "tiddler" in node.attribs;
4542
4545
  }
4543
- function tiddlerToPassage(node) {
4546
+ function tiddlerToPassage(node, passageText) {
4544
4547
  const nodeAttrs = node.attribs ?? {};
4545
4548
  const passage = {
4546
4549
  name: nodeAttrs["tiddler"] ?? "",
4547
4550
  tags: nodeAttrs["tags"] ? nodeAttrs["tags"].split(/\s+/).filter((s) => s.length > 0) : [],
4548
- text: tiddlerUnescape(getTextContent(node)).trim()
4551
+ text: passageText(tiddlerUnescape(getTextContent(node)))
4549
4552
  };
4550
4553
  const position = nodeAttrs["twine-position"];
4551
4554
  if (position) passage.metadata = { position };
@@ -4610,7 +4613,7 @@ function loadSources(story, filenames, opts, diagnostics, processedFiles) {
4610
4613
  break;
4611
4614
  case "htm":
4612
4615
  case "html":
4613
- loadHTML(story, filename, diagnostics);
4616
+ loadHTML(story, filename, opts, diagnostics);
4614
4617
  break;
4615
4618
  case "css":
4616
4619
  loadTagged(story, "stylesheet", filename, diagnostics);
@@ -4764,8 +4767,8 @@ function parseFontFile(filename) {
4764
4767
  diagnostics: []
4765
4768
  };
4766
4769
  }
4767
- function parseHTMLFile(filename) {
4768
- const { story, diagnostics } = decompileHTML(readUTF8(filename));
4770
+ function parseHTMLFile(filename, opts) {
4771
+ const { story, diagnostics } = decompileHTML(readUTF8(filename), { trim: opts.trim ?? true });
4769
4772
  return {
4770
4773
  passages: story.name !== "" && !storyHas(story, "StoryTitle") ? [{
4771
4774
  name: "StoryTitle",
@@ -4777,14 +4780,17 @@ function parseHTMLFile(filename) {
4777
4780
  }
4778
4781
  /**
4779
4782
  * Identity of the options that affect how a file parses, stored on its cache entry.
4780
- * Only Twee files depend on them; every other file type yields the same key for all options.
4783
+ * Twee files depend on `trim` and `twee2Compat`, HTML files on `trim`; every other file type
4784
+ * yields the same key for all options.
4781
4785
  */
4782
4786
  function parseOptionsKey(filename, opts) {
4783
4787
  const ext = normalizedFileExt(filename);
4788
+ const trim = opts.trim ?? true;
4789
+ if (ext === "htm" || ext === "html") return JSON.stringify({ trim });
4784
4790
  const twee2File = ext === "tw2" || ext === "twee2";
4785
4791
  if (!twee2File && ext !== "tw" && ext !== "twee") return "";
4786
4792
  return JSON.stringify({
4787
- trim: opts.trim ?? true,
4793
+ trim,
4788
4794
  twee2Compat: twee2File || (opts.twee2Compat ?? false)
4789
4795
  });
4790
4796
  }
@@ -4798,7 +4804,7 @@ function parseFile(filename, opts) {
4798
4804
  twee2Compat: true
4799
4805
  });
4800
4806
  case "htm":
4801
- case "html": return parseHTMLFile(filename);
4807
+ case "html": return parseHTMLFile(filename, opts);
4802
4808
  case "css": return parseTaggedFile("stylesheet", filename);
4803
4809
  case "js": return parseTaggedFile("script", filename);
4804
4810
  case "otf":
@@ -4830,8 +4836,8 @@ function parseFile(filename, opts) {
4830
4836
  default: return;
4831
4837
  }
4832
4838
  }
4833
- function loadHTML(story, filename, diagnostics) {
4834
- const result = parseHTMLFile(filename);
4839
+ function loadHTML(story, filename, opts, diagnostics) {
4840
+ const result = parseHTMLFile(filename, opts);
4835
4841
  diagnostics.push(...result.diagnostics);
4836
4842
  for (const p of result.passages) storyAdd(story, p, diagnostics);
4837
4843
  }
@@ -4870,7 +4876,8 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
4870
4876
  const optionsKey = parseOptionsKey(filename, opts);
4871
4877
  const entry = cache.get(filename);
4872
4878
  const cached = entry?.parseOptionsKey === optionsKey ? entry : void 0;
4873
- if (changedFiles && cached && !changedFiles.has(filename)) {
4879
+ const changed = changedFiles?.has(filename) ?? false;
4880
+ if (changedFiles && cached && !changed) {
4874
4881
  diagnostics.push(...cached.diagnostics);
4875
4882
  for (const p of cached.passages) storyAdd(story, p, diagnostics);
4876
4883
  processedFiles.add(filename);
@@ -4882,7 +4889,7 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
4882
4889
  } catch {
4883
4890
  continue;
4884
4891
  }
4885
- if (cached && cached.mtimeMs === mtimeMs) {
4892
+ if (cached && !changed && cached.mtimeMs === mtimeMs) {
4886
4893
  diagnostics.push(...cached.diagnostics);
4887
4894
  for (const p of cached.passages) storyAdd(story, p, diagnostics);
4888
4895
  processedFiles.add(filename);
@@ -5130,10 +5137,39 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
5130
5137
  }
5131
5138
  //#endregion
5132
5139
  //#region src/output-twee.ts
5133
- function toTwee(story, outMode) {
5134
- let data = "";
5135
- for (const p of story.passages) data += passageToTwee(p, outMode);
5136
- return data;
5140
+ /**
5141
+ * Serialize the story as Twee. The StoryData passage is written from the story model rather than
5142
+ * from its loaded text, so changes made after loading (a start passage override, test mode) are kept.
5143
+ * Tweego writes the loaded text; its own normalization makes the two the same for an unchanged story.
5144
+ *
5145
+ * `addStoryData`: add a StoryData passage when the story has none, so that StoryData fields the
5146
+ * compile options set are recorded. Default: false.
5147
+ */
5148
+ function toTwee(story, outMode, options = {}) {
5149
+ return withEffectiveStoryData(story, options.addStoryData ?? false).map((p) => passageToTwee(p, outMode)).join("");
5150
+ }
5151
+ function withEffectiveStoryData(story, addStoryData) {
5152
+ const storyData = {
5153
+ name: "StoryData",
5154
+ tags: [],
5155
+ text: marshalStoryData(story)
5156
+ };
5157
+ const index = story.passages.findIndex((p) => p.name === "StoryData");
5158
+ if (index !== -1) return story.passages.map((p, i) => i === index && isStoryDataJSON(p.text) ? {
5159
+ ...p,
5160
+ text: storyData.text
5161
+ } : p);
5162
+ if (!addStoryData) return story.passages;
5163
+ const at = story.passages[0]?.name === "StoryTitle" ? 1 : 0;
5164
+ return [
5165
+ ...story.passages.slice(0, at),
5166
+ storyData,
5167
+ ...story.passages.slice(at)
5168
+ ];
5169
+ }
5170
+ /** Whether the text loaded as StoryData. Text that did not is kept as written, for the author to fix. */
5171
+ function isStoryDataJSON(text) {
5172
+ return unmarshalStoryData(createStory(), text) === null;
5137
5173
  }
5138
5174
  //#endregion
5139
5175
  //#region src/modules.ts
@@ -5205,7 +5241,19 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
5205
5241
  return html;
5206
5242
  }
5207
5243
  //#endregion
5208
- //#region src/start-passage.ts
5244
+ //#region src/passage-omission.ts
5245
+ /**
5246
+ * Why the `target` output leaves `passage` out of its passage elements, or `undefined` when it
5247
+ * emits the passage (and so a link or the start can name it).
5248
+ */
5249
+ function passageOmission(story, passage, target) {
5250
+ switch (target) {
5251
+ case "twine2": return twine2PassageOmission(story, passage);
5252
+ case "twine1": return twine1PassageOmission(passage);
5253
+ default: throw new Error(`unhandled passage output target: ${String(target)}`);
5254
+ }
5255
+ }
5256
+ /** Why a passage is left out, as a predicate: `Passage "X" ${describeOmission(o)}`. */
5209
5257
  function describeOmission(omission) {
5210
5258
  switch (omission.kind) {
5211
5259
  case "special-name": return `is the special "${omission.name}" passage`;
@@ -5214,13 +5262,8 @@ function describeOmission(omission) {
5214
5262
  default: throw new Error(`unhandled passage omission: ${JSON.stringify(omission)}`);
5215
5263
  }
5216
5264
  }
5217
- function omissionFor(story, passage, target) {
5218
- switch (target) {
5219
- case "twine2": return twine2PassageOmission(story, passage);
5220
- case "twine1": return twine1PassageOmission(passage);
5221
- default: throw new Error(`unhandled start passage target: ${String(target)}`);
5222
- }
5223
- }
5265
+ //#endregion
5266
+ //#region src/start-passage.ts
5224
5267
  /**
5225
5268
  * Check that `startName` names a passage the `target` output emits.
5226
5269
  * Returns the error diagnostics to report (empty when the start is usable).
@@ -5231,7 +5274,7 @@ function startPassageDiagnostics(story, startName, target) {
5231
5274
  level: "error",
5232
5275
  message: `Starting passage "${startName}" not found.`
5233
5276
  }];
5234
- const omission = omissionFor(story, passage, target);
5277
+ const omission = passageOmission(story, passage, target);
5235
5278
  if (omission === void 0) return [];
5236
5279
  return [{
5237
5280
  level: "error",
@@ -5260,6 +5303,11 @@ async function compile(options) {
5260
5303
  /**
5261
5304
  * Compile with incremental caching support.
5262
5305
  * Plugins and advanced users can manage their own cache and changed-file tracking.
5306
+ *
5307
+ * Without `changedFiles`, a cached file is reused while its modification time is unchanged.
5308
+ * With it, a file it names (by the path source discovery gives it, relative to the working
5309
+ * directory) is always reparsed, whatever its modification time, and every other cached
5310
+ * file is reused as it is.
5263
5311
  */
5264
5312
  async function compileIncremental(options, cache, changedFiles) {
5265
5313
  return buildOutput(options, cache, changedFiles);
@@ -5311,7 +5359,7 @@ async function buildOutput(options, cache, changedFiles, outFile) {
5311
5359
  switch (outputMode) {
5312
5360
  case "twee3":
5313
5361
  case "twee1":
5314
- output = toTwee(story, outputMode);
5362
+ output = toTwee(story, outputMode, { addStoryData: Boolean(options.startPassage) || testMode });
5315
5363
  break;
5316
5364
  case "twine2-archive":
5317
5365
  output = toTwine2Archive(story, startName, { sourceInfo });
@@ -5411,6 +5459,35 @@ function storyToJSON(story) {
5411
5459
  return JSON.stringify(obj, null, 2);
5412
5460
  }
5413
5461
  //#endregion
5414
- export { isExcluded as a, getFilenames as i, compile as n, mediaTypeFromFilename as o, compileIncremental as r, TweeTsError as t };
5462
+ //#region src/plugins/diagnostics.ts
5463
+ function formatDiagnostic(d) {
5464
+ return `${d.file ? `${d.file}${d.line ? `:${d.line}` : ""}: ` : ""}${d.file && d.line ? d.message.replace(/^line \d+: /, "") : d.message}`;
5465
+ }
5466
+ /** Returns the warnings; throws a LocatedError when the compile reported errors. */
5467
+ function splitDiagnostics(result) {
5468
+ const errors = result.diagnostics.filter((d) => d.level === "error");
5469
+ if (errors.length === 0) return result.diagnostics.filter((d) => d.level === "warning");
5470
+ const error = new Error(errors.map(formatDiagnostic).join("\n"));
5471
+ const [first] = errors;
5472
+ if (first?.file) {
5473
+ error.id = first.file;
5474
+ error.loc = {
5475
+ file: first.file,
5476
+ line: first.line ?? 1,
5477
+ column: 1
5478
+ };
5479
+ }
5480
+ throw error;
5481
+ }
5482
+ /** An error thrown by the compiler or bundler, with a TweeTsError's error diagnostics folded into its message. */
5483
+ function fatalError(e) {
5484
+ if (e instanceof TweeTsError) {
5485
+ const errors = e.diagnostics.filter((d) => d.level === "error").map(formatDiagnostic);
5486
+ return new Error([...errors, e.message].join("\n"));
5487
+ }
5488
+ return e instanceof Error ? e : new Error(String(e));
5489
+ }
5490
+ //#endregion
5491
+ export { compileIncremental as a, mediaTypeFromFilename as c, compile as i, formatDiagnostic as n, getFilenames as o, splitDiagnostics as r, isExcluded as s, fatalError as t };
5415
5492
 
5416
- //# sourceMappingURL=compiler-Bmpbelqj.js.map
5493
+ //# sourceMappingURL=diagnostics-DZNS7W41.js.map