{"version":3,"file":"storybook.es.mjs","names":[],"sources":["../src/storybook/unsafe-html.ts","../src/storybook/story-html.ts","../src/storybook/story-svg.ts","../src/storybook/helpers.ts"],"sourcesContent":["/**\n * `unsafeHtml()` — opt-in sanitizer bypass marker for {@link storyHtml}.\n *\n * @module bquery/storybook\n */\n\n/** @internal */\nexport const UNSAFE_HTML_PLACEHOLDER_PREFIX = '\\u0001BQ_UNSAFE_HTML_';\n/** @internal */\nexport const UNSAFE_HTML_PLACEHOLDER_SUFFIX = '_END\\u0001';\n\n/** @internal */\nconst UNSAFE_HTML_BRAND: unique symbol = Symbol('bquery.storybook.unsafeHtml');\n\n/**\n * Marker returned by {@link unsafeHtml}. Identifies an interpolated story\n * value that should bypass the {@link storyHtml} sanitizer.\n */\nexport interface UnsafeHtmlMarker {\n  /** @internal */\n  readonly [UNSAFE_HTML_BRAND]: true;\n  /** The raw HTML that will be re-inserted after sanitization. */\n  readonly value: string;\n}\n\nlet warnedOnce = false;\n\n/** @internal */\nexport const createUnsafeHtmlMarker = (value: string): UnsafeHtmlMarker => ({\n  [UNSAFE_HTML_BRAND]: true,\n  value,\n});\n\nconst emitDevWarning = (): void => {\n  if (warnedOnce) return;\n  warnedOnce = true;\n  if (typeof console === 'undefined' || typeof console.warn !== 'function') return;\n  console.warn(\n    '[bquery/storybook] unsafeHtml() bypasses sanitization for the wrapped value. ' +\n      'Only use it with trusted, author-controlled markup.'\n  );\n};\n\n/**\n * Wraps an HTML fragment so {@link storyHtml} re-inserts it verbatim after\n * sanitizing the surrounding template. The wrapped value is **not** sanitized.\n *\n * @remarks\n * Mirrors `lit-html`'s `unsafeHTML` directive. Only use with trusted,\n * author-controlled markup — never with user-supplied input.\n *\n * @param value - The HTML fragment to insert verbatim\n * @returns An opaque marker recognised by {@link storyHtml}\n *\n * @example\n * ```ts\n * import { storyHtml, unsafeHtml } from '@bquery/bquery/storybook';\n *\n * const trustedSvg = '<svg width=\"16\" height=\"16\"><circle cx=\"8\" cy=\"8\" r=\"6\"/></svg>';\n * storyHtml`<bq-icon>${unsafeHtml(trustedSvg)}</bq-icon>`;\n * ```\n */\nexport const unsafeHtml = (value: string): UnsafeHtmlMarker => {\n  emitDevWarning();\n  return createUnsafeHtmlMarker(value);\n};\n\n/**\n * Type guard for {@link UnsafeHtmlMarker}.\n * @internal\n */\nexport const isUnsafeHtmlMarker = (value: unknown): value is UnsafeHtmlMarker => {\n  if (value === null || typeof value !== 'object') return false;\n  return (value as { [UNSAFE_HTML_BRAND]?: true })[UNSAFE_HTML_BRAND] === true;\n};\n","/**\n * Storybook template helpers for authoring bQuery component stories.\n *\n * `storyHtml` mirrors bQuery's string-based `html` tag while adding support for\n * Storybook-friendly boolean attribute shorthand (`?disabled=${true}`).\n *\n * @module bquery/storybook\n */\n\nimport { sanitizeHtml } from '../security/sanitize';\nimport { isUnsafeHtmlMarker, UNSAFE_HTML_PLACEHOLDER_PREFIX, UNSAFE_HTML_PLACEHOLDER_SUFFIX } from './unsafe-html';\nimport type { UnsafeHtmlMarker } from './unsafe-html';\n\n/**\n * A value that can be interpolated into a {@link storyHtml} template.\n *\n * Primitive values are coerced to strings; arrays are flattened; functions are\n * invoked and their return values are recursively resolved. {@link unsafeHtml}\n * markers bypass sanitization for the wrapped fragment.\n */\nexport type StoryValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | UnsafeHtmlMarker\n  | StoryValue[]\n  | (() => StoryValue);\n\n/**\n * Marks template interpolation boundaries while inferring sanitizer allowlists.\n * Uses the null character because authored HTML/template strings should not\n * contain it, making it a safe internal sentinel during parsing.\n */\nconst INTERPOLATION_BOUNDARY_MARKER = '\\u0000';\n\nconst isWhitespace = (value: string): boolean => {\n  return value === ' ' || value === '\\t' || value === '\\n' || value === '\\r' || value === '\\f';\n};\n\n/**\n * Detects interpolation boundaries embedded into joined template string fragments.\n */\nconst isInterpolationBoundaryMarker = (value: string): boolean =>\n  value === INTERPOLATION_BOUNDARY_MARKER;\n\nconst isAttributeSeparator = (value: string): boolean => {\n  return isWhitespace(value) || isInterpolationBoundaryMarker(value);\n};\n\nconst isAsciiLetter = (value: string): boolean => {\n  const code = value.charCodeAt(0);\n\n  return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n};\n\nconst isAttributeNameStart = (value: string): boolean => isAsciiLetter(value);\n\nconst isAttributeNameChar = (value: string): boolean => {\n  const code = value.charCodeAt(0);\n\n  return (\n    isAsciiLetter(value) ||\n    (code >= 48 && code <= 57) ||\n    value === ':' ||\n    value === '.' ||\n    value === '_' ||\n    value === '-'\n  );\n};\n\nconst isQuoteChar = (value: string): boolean => value === '\"' || value === \"'\";\n\nconst isTagNameChar = (value: string): boolean => {\n  const code = value.charCodeAt(0);\n\n  return (\n    isAsciiLetter(value) ||\n    (code >= 48 && code <= 57) ||\n    value === '.' ||\n    value === '_' ||\n    value === '-'\n  );\n};\n\nconst hasLineBreak = (value: string): boolean => {\n  for (let index = 0; index < value.length; index += 1) {\n    if (value[index] === '\\n' || value[index] === '\\r') {\n      return true;\n    }\n  }\n\n  return false;\n};\n\nconst getTagNameEnd = (fragment: string): number => {\n  let index = 0;\n\n  while (\n    index < fragment.length &&\n    !isAttributeSeparator(fragment[index]) &&\n    fragment[index] !== '/' &&\n    fragment[index] !== '>'\n  ) {\n    index += 1;\n  }\n\n  return index;\n};\n\nconst isCustomElementTagName = (tagName: string): boolean => {\n  if (!tagName.includes('-') || !isAsciiLetter(tagName[0])) {\n    return false;\n  }\n\n  const last = tagName[tagName.length - 1];\n  const code = last.charCodeAt(0);\n\n  return isAsciiLetter(last) || (code >= 48 && code <= 57) || last === '.' || last === '_';\n};\n\nconst isAutoAllowedStoryAttribute = (attributeName: string): boolean => {\n  return attributeName !== 'style' && !attributeName.startsWith('on');\n};\n\nconst findBooleanAttributeSuffix = (\n  part: string\n): { attribute: string; basePart: string; spacing: string } | null => {\n  let index = part.length - 1;\n\n  while (index >= 0 && isWhitespace(part[index])) {\n    index -= 1;\n  }\n\n  if (index < 0 || part[index] !== '=') {\n    return null;\n  }\n\n  index -= 1;\n\n  while (index >= 0 && isWhitespace(part[index])) {\n    index -= 1;\n  }\n\n  const attributeEnd = index;\n\n  while (\n    index >= 0 &&\n    !isWhitespace(part[index]) &&\n    part[index] !== '?' &&\n    part[index] !== '=' &&\n    part[index] !== '/' &&\n    part[index] !== '>'\n  ) {\n    index -= 1;\n  }\n\n  const attributeStart = index + 1;\n\n  if (attributeStart > attributeEnd || part[index] !== '?') {\n    return null;\n  }\n\n  const questionMarkIndex = index;\n  let spacingStart = questionMarkIndex;\n\n  while (spacingStart > 0 && isWhitespace(part[spacingStart - 1])) {\n    spacingStart -= 1;\n  }\n\n  return {\n    attribute: part.slice(attributeStart, attributeEnd + 1),\n    basePart: part.slice(0, spacingStart),\n    spacing: part.slice(spacingStart, questionMarkIndex),\n  };\n};\n\nconst collectOpeningTagFragments = (template: string): string[] => {\n  const fragments: string[] = [];\n  let index = 0;\n\n  while (index < template.length) {\n    if (template[index] !== '<') {\n      index += 1;\n      continue;\n    }\n\n    const next = template[index + 1];\n\n    if (!next || next === '/' || next === '!' || next === '?') {\n      index += 1;\n      continue;\n    }\n\n    let cursor = index + 1;\n\n    if (!isAsciiLetter(template[cursor])) {\n      index += 1;\n      continue;\n    }\n\n    while (cursor < template.length && isTagNameChar(template[cursor])) {\n      cursor += 1;\n    }\n\n    const tagStart = index + 1;\n    const tagName = template.slice(tagStart, cursor);\n\n    if (!tagName) {\n      index += 1;\n      continue;\n    }\n\n    let inQuote: '\"' | \"'\" | null = null;\n    let tagEnd = cursor;\n\n    while (tagEnd < template.length) {\n      const char = template[tagEnd];\n\n      if (inQuote) {\n        if (char === inQuote) {\n          inQuote = null;\n        }\n\n        tagEnd += 1;\n        continue;\n      }\n\n      if (char === '\"' || char === \"'\") {\n        inQuote = char;\n        tagEnd += 1;\n        continue;\n      }\n\n      if (char === '>') {\n        fragments.push(template.slice(index + 1, tagEnd));\n        tagEnd += 1;\n        break;\n      }\n\n      tagEnd += 1;\n    }\n\n    index = tagEnd;\n  }\n\n  return fragments;\n};\n\n/**\n * Consumes a literal HTML attribute value starting at the given index.\n *\n * Returns the position immediately after a quoted or unquoted value. When the\n * current position reaches an interpolation boundary (optionally after\n * whitespace), the returned index advances past the boundary marker so\n * interpolated attributes do not swallow following authored attributes during\n * sanitizer allowlist inference.\n *\n * @param fragment - The opening-tag fragment currently being scanned\n * @param index - The position immediately after the `=` sign\n * @returns The index after the consumed literal value, or just past an\n * interpolation boundary when no literal value should be consumed from the\n * current template fragment.\n */\nconst skipAttributeValue = (fragment: string, index: number): number => {\n  if (index >= fragment.length) {\n    return index;\n  }\n\n  let cursor = index;\n\n  if (isInterpolationBoundaryMarker(fragment[cursor])) {\n    return cursor + 1;\n  }\n\n  while (cursor < fragment.length && isWhitespace(fragment[cursor])) {\n    cursor += 1;\n  }\n\n  if (cursor >= fragment.length) {\n    return cursor;\n  }\n\n  if (isInterpolationBoundaryMarker(fragment[cursor])) {\n    return cursor + 1;\n  }\n\n  const quote = fragment[cursor];\n\n  if (isQuoteChar(quote)) {\n    cursor += 1;\n\n    while (cursor < fragment.length) {\n      if (fragment[cursor] === quote) {\n        return cursor + 1;\n      }\n\n      cursor += 1;\n    }\n\n    return cursor;\n  }\n\n  while (\n    cursor < fragment.length &&\n    !isAttributeSeparator(fragment[cursor]) &&\n    fragment[cursor] !== '/' &&\n    fragment[cursor] !== '>'\n  ) {\n    cursor += 1;\n  }\n\n  if (cursor < fragment.length && isInterpolationBoundaryMarker(fragment[cursor])) {\n    return cursor + 1;\n  }\n\n  return cursor;\n};\n\nconst collectAttributesFromTagFragment = (\n  fragment: string,\n  allowAttributes: Set<string>,\n  autoAllowAttributes: boolean\n): void => {\n  let index = 0;\n\n  while (index < fragment.length && !isAttributeSeparator(fragment[index])) {\n    index += 1;\n  }\n\n  while (index < fragment.length) {\n    while (index < fragment.length && isAttributeSeparator(fragment[index])) {\n      index += 1;\n    }\n\n    if (index >= fragment.length || fragment[index] === '/') {\n      return;\n    }\n\n    // Skip standalone colons (e.g. namespace prefixes or framework-specific bindings)\n    if (fragment[index] === ':') {\n      index += 1;\n      continue;\n    }\n\n    const hasBooleanPrefix = fragment[index] === '?';\n\n    if (hasBooleanPrefix) {\n      index += 1;\n    }\n\n    if (index >= fragment.length || !isAttributeNameStart(fragment[index])) {\n      // Skip unrecognised character to avoid an infinite loop\n      index += 1;\n      continue;\n    }\n\n    const nameStart = index;\n\n    index += 1;\n\n    while (index < fragment.length && isAttributeNameChar(fragment[index])) {\n      index += 1;\n    }\n\n    const attributeName = fragment.slice(nameStart, index).toLowerCase();\n\n    while (index < fragment.length && isAttributeSeparator(fragment[index])) {\n      index += 1;\n    }\n\n    if (index < fragment.length && fragment[index] === '=') {\n      if (autoAllowAttributes && isAutoAllowedStoryAttribute(attributeName)) {\n        allowAttributes.add(attributeName);\n      }\n      index = skipAttributeValue(fragment, index + 1);\n      continue;\n    }\n\n    if (hasBooleanPrefix && autoAllowAttributes && isAutoAllowedStoryAttribute(attributeName)) {\n      allowAttributes.add(attributeName);\n    }\n  }\n};\n\nconst collectTemplateSanitizeOptions = (strings: TemplateStringsArray) => {\n  const template = strings.join(INTERPOLATION_BOUNDARY_MARKER);\n  const allowTags = new Set<string>();\n  const allowAttributes = new Set<string>();\n\n  for (const fragment of collectOpeningTagFragments(template)) {\n    const tagName = fragment.slice(0, getTagNameEnd(fragment)).toLowerCase();\n    const isCustomElement = isCustomElementTagName(tagName);\n\n    if (isCustomElement) {\n      allowTags.add(tagName);\n    }\n\n    collectAttributesFromTagFragment(fragment, allowAttributes, isCustomElement);\n  }\n\n  return {\n    allowTags: Array.from(allowTags),\n    allowAttributes: Array.from(allowAttributes),\n  };\n};\n\nconst resolveStoryValue = (value: StoryValue, unsafeFragments?: string[]): string => {\n  if (value == null) {\n    return '';\n  }\n\n  if (isUnsafeHtmlMarker(value)) {\n    if (!unsafeFragments) {\n      // Outside of storyHtml (e.g. in `when()`), resolve to the raw value so\n      // callers can compose unsafe markers freely. The marker only carries\n      // semantic weight when consumed by storyHtml itself.\n      return value.value;\n    }\n    const placeholder = `${UNSAFE_HTML_PLACEHOLDER_PREFIX}${unsafeFragments.length}${UNSAFE_HTML_PLACEHOLDER_SUFFIX}`;\n    unsafeFragments.push(value.value);\n    return placeholder;\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((item) => resolveStoryValue(item, unsafeFragments)).join('');\n  }\n\n  if (typeof value === 'function') {\n    return resolveStoryValue(value(), unsafeFragments);\n  }\n\n  return String(value);\n};\n\nconst resolveBooleanStoryValue = (value: StoryValue): boolean => {\n  if (value == null) {\n    return false;\n  }\n\n  if (isUnsafeHtmlMarker(value)) {\n    return Boolean(value.value);\n  }\n\n  if (typeof value === 'function') {\n    return resolveBooleanStoryValue(value());\n  }\n\n  if (Array.isArray(value)) {\n    return Boolean(resolveStoryValue(value));\n  }\n\n  if (typeof value === 'boolean') {\n    return value;\n  }\n\n  return Boolean(value);\n};\n\n/**\n * Tagged template literal for Storybook render functions.\n *\n * Supports boolean attribute shorthand compatible with Storybook's string\n * renderer:\n *\n * ```ts\n * storyHtml`<bq-button ?disabled=${true}>Save</bq-button>`;\n * // => '<bq-button disabled=\"\">Save</bq-button>'\n * ```\n *\n * @param strings - Template literal string parts\n * @param values - Interpolated values\n * @returns HTML string compatible with `@storybook/web-components`\n */\nexport const storyHtml = (strings: TemplateStringsArray, ...values: StoryValue[]): string => {\n  const unsafeFragments: string[] = [];\n  const rendered = strings.reduce((acc, part, index) => {\n    if (index >= values.length) {\n      return `${acc}${part}`;\n    }\n\n    const booleanAttributeMatch = findBooleanAttributeSuffix(part);\n\n    if (booleanAttributeMatch) {\n      const { attribute, basePart, spacing } = booleanAttributeMatch;\n      const preservedSpacing = hasLineBreak(spacing) ? spacing : '';\n      const isEnabled = resolveBooleanStoryValue(values[index]);\n\n      return `${acc}${basePart}${isEnabled ? `${spacing}${attribute}` : preservedSpacing}`;\n    }\n\n    return `${acc}${part}${resolveStoryValue(values[index], unsafeFragments)}`;\n  }, '');\n\n  const sanitized = sanitizeHtml(rendered, collectTemplateSanitizeOptions(strings));\n\n  if (unsafeFragments.length === 0) {\n    return sanitized;\n  }\n\n  // Re-insert `unsafeHtml()` fragments verbatim after sanitization. Callers\n  // explicitly opted out of sanitization for these values; their security is\n  // their responsibility.\n  return sanitized.replace(\n    new RegExp(\n      `${escapeRegExp(UNSAFE_HTML_PLACEHOLDER_PREFIX)}(\\\\d+)${escapeRegExp(UNSAFE_HTML_PLACEHOLDER_SUFFIX)}`,\n      'g'\n    ),\n    (_match, indexStr: string) => {\n      const fragmentIndex = Number.parseInt(indexStr, 10);\n      return unsafeFragments[fragmentIndex] ?? '';\n    }\n  );\n};\n\nconst escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Conditionally render a value or template fragment.\n *\n * @param condition - Condition that controls rendering\n * @param truthyValue - Value or callback rendered when the condition is truthy\n * @param falsyValue - Optional value or callback rendered when the condition is falsy\n * @returns Rendered string fragment, or an empty string when the condition is\n * falsy and no fallback is provided\n */\nexport const when = (\n  condition: unknown,\n  truthyValue: StoryValue,\n  falsyValue?: StoryValue\n): string => {\n  return resolveStoryValue(condition ? truthyValue : falsyValue);\n};\n","/**\n * `storySvg` — sibling of {@link storyHtml} for authoring SVG-rooted stories.\n *\n * @module bquery/storybook\n */\n\nimport { escapeHtml } from '../security/sanitize';\nimport type { StoryValue } from './story-html';\nimport { isUnsafeHtmlMarker } from './unsafe-html';\n\nconst resolveSvgValue = (value: StoryValue): string => {\n  if (value === null || value === undefined) return '';\n  if (isUnsafeHtmlMarker(value)) {\n    // `unsafeHtml()` is an explicit opt-in to bypass escaping. Authors take\n    // responsibility for the contents of the marker.\n    return value.value;\n  }\n  if (Array.isArray(value)) return value.map(resolveSvgValue).join('');\n  if (typeof value === 'function') return resolveSvgValue(value());\n  return escapeHtml(String(value));\n};\n\n/**\n * Tagged template literal for authoring SVG-rooted Storybook stories.\n *\n * @remarks\n * Unlike {@link storyHtml}, `storySvg` does not run the SVG output through the\n * HTML sanitizer (the HTML sanitizer blocks `<svg>` as a dangerous tag because\n * the SVG namespace can host script-execution sinks). Instead, the static\n * template is treated as developer-authored (and therefore trusted), while\n * every interpolated value is HTML-escaped so user-supplied args cannot break\n * out of an attribute slot or inject elements. Use {@link unsafeHtml} for the\n * rare case where you need to splice in pre-built SVG markup.\n *\n * @example\n * ```ts\n * import { storySvg } from '@bquery/bquery/storybook';\n *\n * export const Icon = {\n *   args: { size: 24, color: 'currentColor' },\n *   render: ({ size, color }) => storySvg`\n *     <svg viewBox=\"0 0 24 24\" width=\"${size}\" height=\"${size}\" aria-hidden=\"true\">\n *       <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"${color}\" />\n *     </svg>\n *   `,\n * };\n * ```\n */\nexport const storySvg = (strings: TemplateStringsArray, ...values: StoryValue[]): string => {\n  return strings.reduce((acc, part, index) => {\n    if (index >= values.length) return `${acc}${part}`;\n    return `${acc}${part}${resolveSvgValue(values[index])}`;\n  }, '');\n};\n\n","/**\n * Story authoring helpers: attribute and class-map builders, conditional\n * attribute helpers, keyed list rendering, and escape-only text rendering.\n *\n * @module bquery/storybook\n */\n\nimport { escapeHtml } from '../security/sanitize';\nimport type { StoryValue } from './story-html';\nimport { createUnsafeHtmlMarker, isUnsafeHtmlMarker } from './unsafe-html';\nimport type { UnsafeHtmlMarker } from './unsafe-html';\n\n/**\n * Sentinel value emitted by {@link ifDefined} when a story attribute should\n * resolve to an empty string.\n *\n * @remarks\n * The empty string keeps string-based Storybook templates simple: when used as\n * `attr=\"${ifDefined(...)}\"`, the rendered output becomes `attr=\"\"`.\n */\nconst IF_DEFINED_OMITTED = '';\n\n/**\n * Builds a space-joined class value from a record of class → boolean.\n * Truthy values include the class, falsy values skip it.\n *\n * @remarks\n * Mirrors the `lit-html` `classMap` directive for ergonomic parity with\n * existing community examples.\n *\n * @param classes - Record of class names → truthiness predicate\n * @returns A space-joined class value, or empty when no classes apply\n *\n * @example\n * ```ts\n * storyHtml`<bq-button class=\"${classMap({ primary: true, disabled: isDisabled })}\">Save</bq-button>`;\n * ```\n */\nexport const classMap = (classes: Record<string, unknown>): string => {\n  const enabled: string[] = [];\n  for (const key of Object.keys(classes)) {\n    if (classes[key]) enabled.push(key);\n  }\n  return enabled.join(' ');\n};\n\n/**\n * Builds a `style=\"...\"` attribute value from a record of CSS properties.\n *\n * @remarks\n * Property names are passed through unchanged — supply hyphenated names like\n * `'background-color'`, or camelCase names which are converted to hyphenated\n * form (matching the `lit-html` `styleMap` semantics).\n *\n * Values are coerced to strings; `null`/`undefined`/`false` skip the property.\n *\n * @example\n * ```ts\n * storyHtml`<div style=${styleMap({ color: 'red', backgroundColor: theme.bg })}></div>`;\n * ```\n */\nexport const styleMap = (styles: Record<string, string | number | null | undefined | false>): string => {\n  const parts: string[] = [];\n  for (const key of Object.keys(styles)) {\n    const value = styles[key];\n    if (value === null || value === undefined || value === false) continue;\n    const property = key.includes('-') ? key : key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n    parts.push(`${property}:${value}`);\n  }\n  return parts.join(';');\n};\n\n/**\n * Conditionally includes an attribute value. When `value` is `null` or\n * `undefined`, returns an empty string so the surrounding template emits an\n * empty attribute value (`attr=\"\"`). Otherwise the value is stringified.\n *\n * @example\n * ```ts\n * storyHtml`<bq-input placeholder=\"${ifDefined(args.placeholder)}\"></bq-input>`;\n * ```\n */\nexport const ifDefined = (value: string | number | null | undefined): string => {\n  if (value === null || value === undefined) return IF_DEFINED_OMITTED;\n  return String(value);\n};\n\n/**\n * Maps each item in `items` through `render` and joins the results, attaching\n * a stable key marker to each item so consumers can identify list entries.\n *\n * @remarks\n * Because Storybook's string renderer has no DOM diffing, `repeat` simply\n * concatenates fragments. Plain-string results are escaped as text; to insert\n * trusted markup, wrap the fragment with {@link unsafeHtml}. The key is\n * rendered as a `data-bq-key` attribute on the first opening tag of each\n * fragment if present, otherwise the fragment is inserted unchanged. The\n * optional `key` function is invoked once per item.\n *\n * @example\n * ```ts\n * storyHtml`<ul>${repeat(items, (item) => unsafeHtml(storyHtml`<li>${item.label}</li>`), (item) => item.id)}</ul>`;\n * ```\n */\nexport const repeat = <T>(\n  items: readonly T[],\n  render: (item: T, index: number) => StoryValue,\n  key?: (item: T, index: number) => string | number\n): UnsafeHtmlMarker => {\n  const rendered: string[] = [];\n  for (let index = 0; index < items.length; index += 1) {\n    const item = items[index];\n    const value = render(item, index);\n    const fragment = resolvePlain(value);\n    if (key === undefined) {\n      rendered.push(fragment);\n      continue;\n    }\n    const keyValue = String(key(item, index));\n    rendered.push(injectKeyAttribute(fragment, keyValue));\n  }\n  // Fragments are escaped by default; callers must opt into trusted markup via\n  // `unsafeHtml(...)`. Wrap the final concatenation in an internal marker so\n  // surrounding `storyHtml` preserves the authored fragment structure without\n  // emitting the opt-in warning for safe-by-default plain strings.\n  return createUnsafeHtmlMarker(rendered.join(''));\n};\n\n/**\n * Resolves a {@link StoryValue} to a plain string without sanitization.\n *\n * @remarks\n * Used by `repeat()` to produce the raw concatenation. `repeat()` itself\n * returns an `unsafeHtml` marker so the surrounding `storyHtml` call does not\n * re-sanitize fragments that have already been sanitized by a nested\n * `storyHtml` call.\n *\n * @internal\n */\nconst resolvePlain = (value: StoryValue): string => {\n  if (value === null || value === undefined) return '';\n  if (Array.isArray(value)) return value.map(resolvePlain).join('');\n  if (typeof value === 'function') return resolvePlain(value());\n  if (isUnsafeHtmlMarker(value)) {\n    return value.value;\n  }\n  return escapeHtml(String(value));\n};\n\n/**\n * Injects a `data-bq-key` attribute into the first opening tag of `fragment`,\n * or returns the fragment unchanged if no opening tag is present.\n *\n * @internal\n */\nconst injectKeyAttribute = (fragment: string, key: string): string => {\n  const escaped = escapeHtml(key);\n  // Find the first `<tagName` followed by space, `/`, or `>`.\n  const match = fragment.match(/<([A-Za-z][A-Za-z0-9-]*)(\\s|\\/|>)/);\n  if (!match) return fragment;\n  const tagEnd = match.index! + 1 + match[1].length;\n  return `${fragment.slice(0, tagEnd)} data-bq-key=\"${escaped}\"${fragment.slice(tagEnd)}`;\n};\n\n/**\n * Escape-only helper for text-only fragments. Returns the input with HTML\n * entities encoded so it is safe to interpolate directly into a story\n * template.\n *\n * @remarks\n * Unlike {@link storyHtml}, `storyText` does not parse the template for tags\n * or attributes — it is intended for plain text. Use it whenever you want\n * args-provided strings to render verbatim without HTML interpretation.\n *\n * @example\n * ```ts\n * storyHtml`<bq-tooltip>${storyText(args.label)}</bq-tooltip>`;\n * ```\n */\nexport const storyText = (text: string | number | null | undefined): string => {\n  if (text === null || text === undefined) return '';\n  return escapeHtml(String(text));\n};\n"],"mappings":"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"}