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