/** * Code-fence meta. A Shiki transformer reads the tokens after the language and * promotes them to attributes on the rendered `
`:
 *
 * - a title — the first bare token (```ts blume.config.ts) or `title="..."` —
 *   becomes `data-title`; the theme's code header shows it, falling back to the
 *   language label.
 * - the `lineNumbers` keyword (```ts file.ts lineNumbers) becomes
 *   `data-line-numbers`; the theme renders a counter-driven line-number gutter.
 */

/** The slice of Shiki's transformer `this` context Blume reads. */
interface CodeMetaContext {
  options: { meta?: { __raw?: string } };
}

/** The `
` hast node a Shiki `pre` hook receives. */
interface PreNode {
  properties: Record;
}

/** A Shiki-compatible transformer, typed structurally to avoid a Shiki dep. */
export interface CodeTitleTransformer {
  name: string;
  pre: (this: CodeMetaContext, node: PreNode) => void;
}

// The body excludes only the delimiting quote, so `title="foo's file.ts"`
// (an apostrophe inside double quotes) still matches. The left boundary stops
// `subtitle="..."` (or any `*title=` attr) from reading as a title.
const TITLE_ATTR = /(?:^|\s)title=(?:"(?[^"]*)"|'(?[^']*)')/u;
const LINE_NUMBERS = /(?:^|\s)lineNumbers(?=\s|$)/u;
// Any quoted `key="..."` attr — blanked before keyword/bare-token scans so a
// quoted value can't leak tokens (`title="enable lineNumbers later"`).
const QUOTED_ATTR = /[\w-]+=(?:"[^"]*"|'[^']*')/gu;

const withoutQuotedAttrs = (raw: string): string =>
  raw.replace(QUOTED_ATTR, " ");

// The first bare token is the title (```ts blume.config.ts): a non-empty token
// that isn't a Shiki line range (`{1,3-5}`), a `key=value` attr, or a reserved
// `lineNumbers`/`twoslash` keyword.
const isTitleToken = (token: string): boolean => {
  if (token.length === 0 || token.startsWith("{") || token.includes("=")) {
    return false;
  }
  return token !== "lineNumbers" && token !== "twoslash";
};

const parseTitle = (raw: string | undefined): string | undefined => {
  if (!raw) {
    return undefined;
  }
  // Blank every *other* quoted attr first, so a `title="…"` embedded in
  // another attribute's value (`caption='set title="X" here'`) can't be
  // promoted to the block title.
  const scrubbed = raw.replace(QUOTED_ATTR, (attr) =>
    attr.startsWith("title=") ? attr : " "
  );
  const explicit = scrubbed.match(TITLE_ATTR);
  const attrTitle = explicit?.groups?.dq ?? explicit?.groups?.sq;
  if (attrTitle) {
    return attrTitle;
  }
  return withoutQuotedAttrs(raw).trim().split(/\s+/u).find(isTitleToken);
};

const hasLineNumbers = (raw: string | undefined): boolean =>
  Boolean(raw && LINE_NUMBERS.test(withoutQuotedAttrs(raw)));

/** Build the transformer. Runs after Shiki's built-in `data-language` hook. */
export const codeTitleTransformer = (): CodeTitleTransformer => ({
  name: "blume:code-meta",
  pre(node) {
    const raw = this.options.meta?.__raw;
    const title = parseTitle(raw);
    if (title) {
      node.properties.dataTitle = title;
    }
    if (hasLineNumbers(raw)) {
      node.properties.dataLineNumbers = true;
    }
  },
});