executable-stories-formatters 0.13.0 → 0.14.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.
package/dist/index.d.cts CHANGED
@@ -360,6 +360,10 @@ interface MarkdownFormatterOptions {
360
360
  includeSourceLinks?: boolean;
361
361
  /** Custom renderers for doc entries */
362
362
  customRenderers?: MarkdownRenderers;
363
+ /** Emit a stable per-scenario anchor for deep-linking. Returns the id, or undefined to skip. */
364
+ scenarioAnchor?: (tc: TestCaseResult) => string | undefined;
365
+ /** Render a badge line under a scenario heading (e.g. a what's-changed marker). Undefined to skip. */
366
+ scenarioBadge?: (tc: TestCaseResult) => string | undefined;
363
367
  }
364
368
 
365
369
  /** Astro/Starlight formatter options */
@@ -487,6 +491,8 @@ interface ResolvedFormatterOptions {
487
491
  ticketUrlTemplate?: string;
488
492
  traceUrlTemplate?: string;
489
493
  customRenderers?: MarkdownRenderers;
494
+ scenarioAnchor?: (tc: TestCaseResult) => string | undefined;
495
+ scenarioBadge?: (tc: TestCaseResult) => string | undefined;
490
496
  };
491
497
  };
492
498
  assetMode: "none" | "copy";
@@ -1924,6 +1930,19 @@ interface MarkdownOptions {
1924
1930
  includeSourceLinks?: boolean;
1925
1931
  /** Custom renderers for doc entries */
1926
1932
  customRenderers?: MarkdownRenderers;
1933
+ /**
1934
+ * Emit a stable in-page anchor before each scenario heading, so external tools
1935
+ * can deep-link to a scenario by fragment. Given a test case, return the anchor
1936
+ * id (without `#`), or undefined to skip. Off by default — only the living-docs
1937
+ * site opts in, so plain markdown output is unchanged.
1938
+ */
1939
+ scenarioAnchor?: (tc: TestCaseResult) => string | undefined;
1940
+ /**
1941
+ * Render a short badge line directly under a scenario heading (e.g. a
1942
+ * what's-changed marker like "🆕 New" / "⚠️ Regressed"). Return the markdown
1943
+ * line, or undefined to skip. Off by default — only the living-docs site opts in.
1944
+ */
1945
+ scenarioBadge?: (tc: TestCaseResult) => string | undefined;
1927
1946
  }
1928
1947
  /**
1929
1948
  * Markdown Formatter.
package/dist/index.d.ts CHANGED
@@ -360,6 +360,10 @@ interface MarkdownFormatterOptions {
360
360
  includeSourceLinks?: boolean;
361
361
  /** Custom renderers for doc entries */
362
362
  customRenderers?: MarkdownRenderers;
363
+ /** Emit a stable per-scenario anchor for deep-linking. Returns the id, or undefined to skip. */
364
+ scenarioAnchor?: (tc: TestCaseResult) => string | undefined;
365
+ /** Render a badge line under a scenario heading (e.g. a what's-changed marker). Undefined to skip. */
366
+ scenarioBadge?: (tc: TestCaseResult) => string | undefined;
363
367
  }
364
368
 
365
369
  /** Astro/Starlight formatter options */
@@ -487,6 +491,8 @@ interface ResolvedFormatterOptions {
487
491
  ticketUrlTemplate?: string;
488
492
  traceUrlTemplate?: string;
489
493
  customRenderers?: MarkdownRenderers;
494
+ scenarioAnchor?: (tc: TestCaseResult) => string | undefined;
495
+ scenarioBadge?: (tc: TestCaseResult) => string | undefined;
490
496
  };
491
497
  };
492
498
  assetMode: "none" | "copy";
@@ -1924,6 +1930,19 @@ interface MarkdownOptions {
1924
1930
  includeSourceLinks?: boolean;
1925
1931
  /** Custom renderers for doc entries */
1926
1932
  customRenderers?: MarkdownRenderers;
1933
+ /**
1934
+ * Emit a stable in-page anchor before each scenario heading, so external tools
1935
+ * can deep-link to a scenario by fragment. Given a test case, return the anchor
1936
+ * id (without `#`), or undefined to skip. Off by default — only the living-docs
1937
+ * site opts in, so plain markdown output is unchanged.
1938
+ */
1939
+ scenarioAnchor?: (tc: TestCaseResult) => string | undefined;
1940
+ /**
1941
+ * Render a short badge line directly under a scenario heading (e.g. a
1942
+ * what's-changed marker like "🆕 New" / "⚠️ Regressed"). Return the markdown
1943
+ * line, or undefined to skip. Off by default — only the living-docs site opts in.
1944
+ */
1945
+ scenarioBadge?: (tc: TestCaseResult) => string | undefined;
1927
1946
  }
1928
1947
  /**
1929
1948
  * Markdown Formatter.
package/dist/index.js CHANGED
@@ -15124,7 +15124,9 @@ var MarkdownFormatter = class {
15124
15124
  ticketUrlTemplate: options.ticketUrlTemplate,
15125
15125
  traceUrlTemplate: options.traceUrlTemplate,
15126
15126
  includeSourceLinks: options.includeSourceLinks ?? true,
15127
- customRenderers: options.customRenderers
15127
+ customRenderers: options.customRenderers,
15128
+ scenarioAnchor: options.scenarioAnchor,
15129
+ scenarioBadge: options.scenarioBadge
15128
15130
  };
15129
15131
  }
15130
15132
  /**
@@ -15311,6 +15313,11 @@ var MarkdownFormatter = class {
15311
15313
  * Render a single scenario.
15312
15314
  */
15313
15315
  renderScenario(lines, tc) {
15316
+ const anchorId = this.options.scenarioAnchor?.(tc);
15317
+ if (anchorId) {
15318
+ lines.push(`<a id="${anchorId}"></a>`);
15319
+ lines.push("");
15320
+ }
15314
15321
  if (this.options.customRenderers?.renderScenarioHeader) {
15315
15322
  const custom = this.options.customRenderers.renderScenarioHeader(tc);
15316
15323
  if (custom !== null) {
@@ -15326,6 +15333,10 @@ var MarkdownFormatter = class {
15326
15333
  icon = this.getStatusIcon(tc.status) + " ";
15327
15334
  }
15328
15335
  lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
15336
+ const badge = this.options.scenarioBadge?.(tc);
15337
+ if (badge) {
15338
+ lines.push(badge);
15339
+ }
15329
15340
  if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
15330
15341
  const permalink = this.buildPermalink(tc);
15331
15342
  lines.push(`Source: [${tc.sourceFile}](${permalink})`);
@@ -18142,12 +18153,16 @@ function groupBy7(items, keyFn) {
18142
18153
  import * as fs5 from "fs";
18143
18154
  import * as path6 from "path";
18144
18155
  var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
18145
- function isLocalPath(src) {
18156
+ function isRemoteRef(src) {
18146
18157
  const trimmed = src.trim();
18147
- if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
18148
- return false;
18149
- }
18150
- return !path6.posix.isAbsolute(trimmed) && !path6.win32.isAbsolute(trimmed);
18158
+ return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
18159
+ }
18160
+ function isAbsoluteRef(src) {
18161
+ const trimmed = src.trim();
18162
+ return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
18163
+ }
18164
+ function isRelativeLocalPath(src) {
18165
+ return !isRemoteRef(src) && !isAbsoluteRef(src);
18151
18166
  }
18152
18167
  function stripCodeContent(markdown) {
18153
18168
  let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
@@ -18163,21 +18178,21 @@ function scanMarkdownAssets(markdown) {
18163
18178
  let match;
18164
18179
  while ((match = mdImageRe.exec(stripped)) !== null) {
18165
18180
  const src = match[1].trim();
18166
- if (isLocalPath(src)) {
18181
+ if (!isRemoteRef(src)) {
18167
18182
  found.add(src);
18168
18183
  }
18169
18184
  }
18170
18185
  const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
18171
18186
  while ((match = htmlSrcRe.exec(stripped)) !== null) {
18172
18187
  const src = match[1].trim();
18173
- if (isLocalPath(src)) {
18188
+ if (!isRemoteRef(src)) {
18174
18189
  found.add(src);
18175
18190
  }
18176
18191
  }
18177
18192
  const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
18178
18193
  while ((match = posterRe.exec(stripped)) !== null) {
18179
18194
  const src = match[1].trim();
18180
- if (isLocalPath(src)) {
18195
+ if (!isRemoteRef(src)) {
18181
18196
  found.add(src);
18182
18197
  }
18183
18198
  }
@@ -18203,48 +18218,21 @@ function isCode(segment) {
18203
18218
  const trimmed = segment.trimStart();
18204
18219
  return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
18205
18220
  }
18221
+ function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
18222
+ if (isRemoteRef(trimmed)) return null;
18223
+ if (pathMap) {
18224
+ const mapped = pathMap.get(trimmed);
18225
+ return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
18226
+ }
18227
+ if (!isRelativeLocalPath(trimmed)) return null;
18228
+ return `${assetsBaseUrl}/${trimmed}`;
18229
+ }
18206
18230
  function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
18207
- let result = prose;
18208
- result = result.replace(
18209
- /(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g,
18210
- (full, pre, src, post) => {
18211
- const trimmed = src.trim();
18212
- if (!isLocalPath(trimmed)) return full;
18213
- if (pathMap) {
18214
- const mapped = pathMap.get(trimmed);
18215
- if (mapped === void 0) return full;
18216
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18217
- }
18218
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18219
- }
18220
- );
18221
- result = result.replace(
18222
- /(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
18223
- (full, pre, src, post) => {
18224
- const trimmed = src.trim();
18225
- if (!isLocalPath(trimmed)) return full;
18226
- if (pathMap) {
18227
- const mapped = pathMap.get(trimmed);
18228
- if (mapped === void 0) return full;
18229
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18230
- }
18231
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18232
- }
18233
- );
18234
- result = result.replace(
18235
- /(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
18236
- (full, pre, src, post) => {
18237
- const trimmed = src.trim();
18238
- if (!isLocalPath(trimmed)) return full;
18239
- if (pathMap) {
18240
- const mapped = pathMap.get(trimmed);
18241
- if (mapped === void 0) return full;
18242
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18243
- }
18244
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18245
- }
18246
- );
18247
- return result;
18231
+ const rewrite = (full, pre, src, post) => {
18232
+ const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
18233
+ return target === null ? full : `${pre}${target}${post}`;
18234
+ };
18235
+ return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
18248
18236
  }
18249
18237
  function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
18250
18238
  return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
@@ -18261,8 +18249,9 @@ function copyMarkdownAssets(options) {
18261
18249
  const pathMap = /* @__PURE__ */ new Map();
18262
18250
  const missing = [];
18263
18251
  for (const ref of refs) {
18264
- const absPath = path6.resolve(markdownDir, ref);
18252
+ const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
18265
18253
  if (!fs5.existsSync(absPath)) {
18254
+ if (isAbsoluteRef(ref)) continue;
18266
18255
  if (!allowMissing) {
18267
18256
  throw new Error(`Asset not found: ${absPath}`);
18268
18257
  }
@@ -21888,7 +21877,9 @@ var ReportGenerator = class {
21888
21877
  permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
21889
21878
  ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
21890
21879
  traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
21891
- customRenderers: options.astro?.markdown?.customRenderers
21880
+ customRenderers: options.astro?.markdown?.customRenderers,
21881
+ scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
21882
+ scenarioBadge: options.astro?.markdown?.scenarioBadge
21892
21883
  }
21893
21884
  },
21894
21885
  assetMode: options.assetMode ?? "none",