Theme
Design your own
On this page (16)
Component Skill — components/bibtex/component-skill.md

Native basis

A <figure> (or a <span> inline) around the authored source in <code class="bibtex-source">. The runtime parses it and adds the bar - format tabs (role="tablist") and the copy <button> - and the view; a role="status" region announces the copy.

Web Platform APIs

Clipboard APISelectionString.normalize()APG Tabstext-indent

Classes

.bibtex.bibtex-source.bibtex-bar.bibtex-tabs.bibtex-tab.bibtex-copy.bibtex-view.bibtex-code.bibtex-ref.bibtex-list

Data attributes

data-formats (the tabs: bibtex apa mla chicago harvard ieee), data-format (the first shown), data-variant="minimal|inline", data-size="sm", data-align="none", data-highlight="none", data-copy="none".

§Formats

§Six formats, one source

The authored BibTeX is parsed once; the tabs show it as BibTeX (normalized, aligned, highlighted) or as an APA, MLA, Chicago, Harvard or IEEE reference. DOIs and URLs are links (a new tab); Copy takes exactly what is shown, as plain text. ← / → move between the tabs.

§Reference list

§Several entries become a reference list

IEEE numbers the entries in source order; APA and Chicago sort them by author and year and hang the indent. data-formats picks the tabs and their order, data-format the one that shows first.

§Minimal

§Minimal - one format, a floating copy button

data-variant="minimal": no bar; the copy button floats in the corner while the block is hovered or focused (always on touch screens).

§Inline

§Inline - a reference inside running text

data-variant="inline" on a <span>: the formatted reference flows with the sentence, a small copy icon follows it.

§Rendering options

§Small, plain BibTeX, no copy button

data-size="sm" for dense pages, data-align="none" keeps the fields unaligned, data-highlight="none" drops the colors, data-copy="none" removes the copy button.

§From script

§Driven from outside

df$.shadcn.bibtex.show(el, format) switches the format, .copy(el) copies, .format(text, style) formats without an element; bibtex-format and bibtex-copy report every change.

§States

Named states via the shared State API, bound on each block:

  • default - the source in the format of config.format (data-format on the block)
  • copied - the copy feedback: the button says Copied (data-copied), the status announces it; back to default after two seconds

The config is { format }. The first example carries data-state-demo; bun run screenshots drives it via el.api.setState(name) and captures screenshots/{mode}/bibtex-{state}.png.

Machine contract - verified against bibtex.schema.json by bun run verify:

StateTypeValuesDefaultDescription
formatenumbibtex, apa, mla, chicago, harvard, ieeebibtexThe format shown - setState('default', { format: 'apa' }); mirrored as data-format on the block.
copiedbooleantrue, falsefalseThe copy feedback - setState('copied'); returns to default after two seconds.

§API

Generated from bibtex.ts and the shared State API - the descriptions are their JSDoc, the types are checked by the compiler.

States

type BibtexState = 'default' | 'copied' - setState(name, config) takes the config of the state it names.

StateDescription
default
The source shown in one format.
Config fieldTypeDescription
format?BibtexStylethe format to show (one the element offers); default: the authored data-format, else the first offered
copied
The copy feedback - the button says Copied and the status announces it; back to default after two seconds.
Config fieldTypeDescription
format?BibtexStylethe format that was copied (default: the one shown)

Every element

MemberDescription
el.api.setState<S extends BibtexState>(name: S, config?: BibtexStateConfigs[S]): unknown
Enter a state: the DOM work runs (also when it is the current state), the store records it.
ArgumentTypeDescription
nameSa declared state (an unknown name throws)
config?BibtexStateConfigs[S]that state's config (merged into the stored one when the component merges)

Returns unknown - what the state's DOM work returned - a Promise for an async state (or await settled())

el.api.getState(): { name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel }
The state the element shows now - read back from the DOM, so it includes what the user changed.

Returns { name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel } - the state's name, its config and the authored markup model render() starts from

el.api.render(state?: { name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel }): string
The element's markup in a state - the authored markup with that state applied; a pure function of the state.
ArgumentTypeDescription
state?{ name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel }a state as getState() returns it (default: the current one)

Returns string - the element's outer HTML in that state

el.api.settled(): Promise<void>
Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting).

Returns Promise<void> - resolves when nothing is pending

el.store: Store<{ name: BibtexState; config: BibtexStateConfigs[BibtexState] }>A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component.

Registry

MemberDescription
df$.shadcn.bibtexApi.setState<S extends BibtexState>(el: HTMLElement, name: S, config?: BibtexStateConfigs[S]): unknown
Enter a state: the DOM work runs (also when it is the current state), the store records it.
ArgumentTypeDescription
elHTMLElementthe component's element
nameSa declared state (an unknown name throws)
config?BibtexStateConfigs[S]that state's config (merged into the stored one when the component merges)

Returns unknown - what the state's DOM work returned - a Promise for an async state (await it, or el.api.settled())

df$.shadcn.bibtexApi.getState(el: HTMLElement): { name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel }
The state the element shows now - read back from the DOM, so it includes what the user changed.
ArgumentTypeDescription
elHTMLElementthe component's element

Returns { name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel } - the state's name, its config and the authored markup model render() starts from

df$.shadcn.bibtexApi.render(state: { name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel }): string
The element's markup in a state - the authored markup with that state applied; a pure function of the state.
ArgumentTypeDescription
state{ name: BibtexState; config: BibtexStateConfigs[BibtexState]; model?: ElementModel }a state as getState() returns it (with its model)

Returns string - the element's outer HTML in that state

df$.shadcn.bibtexApi.store(el: HTMLElement): Store<{ name: BibtexState; config: BibtexStateConfigs[BibtexState] }>
The element's store (bindComponent made it).
ArgumentTypeDescription
elHTMLElementthe component's element

Returns Store<{ name: BibtexState; config: BibtexStateConfigs[BibtexState] }> - a defuss-store store of { name, config } - subscribe to follow every change, set it to drive the component

df$.shadcn.bibtexApi.commit<S extends BibtexState>(el: HTMLElement, name: S, config?: BibtexStateConfigs[S]): void
Record a state the element reached on its own (no DOM work) - for a component's own handlers.
ArgumentTypeDescription
elHTMLElementthe component's element
nameSthe state it is in
config?BibtexStateConfigs[S]its config
df$.shadcn.bibtexStates: BibtexState[]The declared states, 'default' first: default, copied.

df$.shadcn.bibtex

MemberDescription
formats: BibtexStyle[]
Every format, in tab order: bibtex, apa, mla, chicago, harvard, ieee.
parse(text: string): BibtexEntry[]
Parse BibTeX text into entries - [{ type, key, fields, order }]; @comment, @preamble and @string are skipped. */
ArgumentTypeDescription
textstringBibTeX source, any number of entries

Returns BibtexEntry[] - the entries, in source order

format(entries: BibtexEntry[] | string, style: BibtexStyle, options?: { align?: boolean; highlight?: boolean }): BibtexOutput
Format entries (or BibTeX text) in one style - { html, text, list }: html is a string for bibtex, else one reference per entry. Options: align, highlight (bibtex).
ArgumentTypeDescription
entriesBibtexEntry[] | stringparsed entries, or BibTeX text to parse first
styleBibtexStylethe citation format
options?{ align?: boolean; highlight?: boolean }bibtex only: align the = signs (default true), highlight the parts (default true)

Returns BibtexOutput - the markup, the plain text and whether it is a list

show(target: string | HTMLElement, fmt: BibtexStyle): void
Show a format on an element (state 'default' with that format).
ArgumentTypeDescription
targetstring | HTMLElementthe .bibtex element or its selector
fmtBibtexStylethe format to show (one the element offers)
copy(target: string | HTMLElement): Promise<boolean>
Copy what an element shows.
ArgumentTypeDescription
targetstring | HTMLElementthe .bibtex element or its selector

Returns Promise<boolean> - true when the clipboard took it; false when the view was selected for a manual copy

text(target: string | HTMLElement): string
The text a copy of the element takes now.
ArgumentTypeDescription
targetstring | HTMLElementthe .bibtex element or its selector

Returns string - the shown format as plain text ('' before the first render)

entries(target: string | HTMLElement): BibtexEntry[]
The element's parsed entries.
ArgumentTypeDescription
targetstring | HTMLElementthe .bibtex element or its selector

Returns BibtexEntry[] - a copy of the entries its source holds

Events

EventDescription
bibtex-copy
Fires after a copy - the format, the text copied and whether the clipboard took it (false: the view was selected instead).

detail: BibtexCopyDetail

FieldTypeDescription
formatBibtexStylethe format that was copied (the element's data-format - always one it offers)
textstringthe text copied
okbooleantrue when the clipboard took it; false: the view was selected for a manual copy
bibtex-format
Fires when the shown format changes - by a tab, the keyboard or setState; the format and the text a copy takes.

detail: BibtexFormatDetail

FieldTypeDescription
formatBibtexStylethe format shown now (the element's data-format - always one it offers)
textstringthe text a copy takes in it

Types

TypeDescription
BibtexCopyDetail
What bibtex-copy carries.
FieldTypeDescription
formatBibtexStylethe format that was copied (the element's data-format - always one it offers)
textstringthe text copied
okbooleantrue when the clipboard took it; false: the view was selected for a manual copy
BibtexEntry
One parsed BibTeX entry.
FieldTypeDescription
typestringthe entry type, lowercase (article, inproceedings, book, misc, ...)
keystringthe citation key
fieldsRecord<string, string>every field's raw value as written (braces and quotes removed, macros kept)
orderstring[]the field names in source order
bareRecord<string, string>the fields written without braces or quotes (a macro like jan, a number)
BibtexFormatDetail
What bibtex-format carries.
FieldTypeDescription
formatBibtexStylethe format shown now (the element's data-format - always one it offers)
textstringthe text a copy takes in it
BibtexOutput
What format() returns.
FieldTypeDescription
htmlstring | string[]the formatted markup: one string for bibtex, else one reference per entry
textstringthe same as plain text - what a copy takes
listbooleantrue when several entries became a list
BibtexStyle
A citation format - the tab order.

= 'bibtex' | 'apa' | 'mla' | 'chicago' | 'harvard' | 'ieee'

§CSS view file

/* -- BibTeX ------------------------------------------------------------------
   A citation block: the BibTeX itself (aligned, highlighted) or a formatted
   reference - APA, MLA, Chicago, Harvard, IEEE - with format tabs and a copy
   button that copies exactly what is shown. Without JS the authored source
   (.bibtex-source) is what shows. Every color, radius and font is the theme's. */
@layer components {
  :where(.bibtex, .bibtex *, .bibtex *::before, .bibtex *::after) {
    box-sizing: border-box;
  }
  .bibtex {
    --bibtex-pad: 1rem 1.25rem;
    --bibtex-code: 0.8125rem;
    --bibtex-text: 0.9375rem;
    position: relative;
    display: grid;
    margin: 0;
    min-inline-size: 0;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--card);
    color: var(--card-foreground);
    & > figcaption {
      padding: 0.75rem 1.25rem 0;
      color: var(--muted-foreground);
      font-family: var(--font-mono);
      font-size: 0.75rem;
      letter-spacing: 0.06em;
      text-transform: uppercase;
    }
    &[data-size="sm"] {
      --bibtex-pad: 0.75rem 0.875rem;
      --bibtex-code: 0.75rem;
      --bibtex-text: 0.8125rem;
    }
  }
  /* the authored source: shown without JS, replaced by the view with it */
  .bibtex > pre:has(> .bibtex-source) {
    margin: 0;
    padding: var(--bibtex-pad);
    overflow-x: auto;
    line-height: 1.6;
  }
  .bibtex-source {
    font-family: var(--font-mono);
    font-size: var(--bibtex-code);
  }
  .bibtex[data-init] > .bibtex-source,
  .bibtex[data-init] > pre:has(> .bibtex-source) {
    display: none;
  }
  /* -- the bar: format tabs + copy ------------------------------------------ */
  .bibtex-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
    border-block-end: 1px solid var(--border);
    background: color-mix(in oklab, var(--muted) 55%, var(--card));
  }
  .bibtex-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-inline-end: auto;
  }
  .bibtex-tab,
  .bibtex-copy {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    block-size: 1.875rem;
    padding: 0 0.625rem;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--muted-foreground);
    font: 500 0.75rem/1 var(--font-mono);
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: color 0.15s, background-color 0.15s, border-color 0.15s;
    &:hover {
      color: var(--foreground);
    }
    &:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: 1px;
    }
  }
  .bibtex-tab[aria-selected="true"] {
    border-color: var(--border);
    background: var(--background);
    color: var(--foreground);
    box-shadow: var(--shadow-2xs);
  }
  .bibtex-copy {
    margin-inline-start: auto;
    border-color: var(--border);
    background: var(--background);
    & svg {
      inline-size: 0.875rem;
      block-size: 0.875rem;
      fill: none;
      stroke: currentColor;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    & .bibtex-icon-done {
      display: none;
    }
  }
  .bibtex[data-copied] .bibtex-copy {
    color: var(--foreground);
    & .bibtex-icon-copy { display: none; }
    & .bibtex-icon-done { display: block; color: var(--chart-2); }
  }
  .bibtex-status {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* -- the view ---------------------------------------------------------------- */
  .bibtex-view {
    min-inline-size: 0;
    outline: none;
    &:focus-visible {
      box-shadow: inset 0 0 0 2px var(--ring);
    }
  }
  .bibtex-code {
    margin: 0;
    padding: var(--bibtex-pad);
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: var(--bibtex-code);
    line-height: 1.6;
    tab-size: 2;
  }
  /* the view owns its code typography - a host page's pre / code / span
     styles (chips, borders, backgrounds) never leak in */
  .bibtex-code,
  .bibtex-code :is(code, span) {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: none;
    font-family: var(--font-mono);
    letter-spacing: normal;
  }
  .bibtex-code {
    background: var(--card);
  }
  .bibtex-code :is(code, span) {
    margin: 0;
    padding: 0;
    font-size: inherit;
    line-height: inherit;
  }
  .bibtex-type { color: var(--chart-4); font-weight: 600; }
  .bibtex-key { color: var(--foreground); font-weight: 600; }
  .bibtex-field { color: var(--chart-2); }
  .bibtex-value { color: var(--foreground); }
  .bibtex[data-highlight="none"] .bibtex-code {
    color: var(--foreground);
  }
  /* DOIs and URLs are links in the view - the copy stays plain text */
  .bibtex-link {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: color-mix(in oklab, currentColor 40%, transparent);
    text-underline-offset: 0.2em;
    overflow-wrap: anywhere;
    &:hover {
      text-decoration-color: currentColor;
    }
    &:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
      border-radius: 2px;
    }
  }
  /* formatted references: hanging indent, the academic way */
  .bibtex-ref {
    margin: 0;
    font-size: var(--bibtex-text);
    line-height: 1.65;
    overflow-wrap: anywhere;
  }
  .bibtex-view > .bibtex-ref {
    padding: var(--bibtex-pad);
  }
  .bibtex-list {
    display: grid;
    gap: 0.625rem;
    margin: 0;
    padding: var(--bibtex-pad);
    list-style: none;
    & > .bibtex-ref {
      padding-inline-start: 2em;
      text-indent: -2em;
    }
  }
  .bibtex[data-format="ieee"] .bibtex-list > .bibtex-ref {
    padding-inline-start: 2.25em;
    text-indent: -2.25em;
  }
  .bibtex-num {
    display: inline-block;
    min-inline-size: 2.25em;
    text-indent: 0;
    color: var(--muted-foreground);
    font-variant-numeric: tabular-nums;
  }
  /* -- minimal: no bar, the copy button floats in the corner ---------------- */
  .bibtex[data-variant="minimal"] {
    & > .bibtex-bar {
      position: absolute;
      inset-block-start: 0.5rem;
      inset-inline-end: 0.5rem;
      z-index: 1;
      padding: 0;
      border: 0;
      background: none;
      opacity: 0;
      transition: opacity 0.15s;
    }
    &:is(:hover, :focus-within, [data-copied]) > .bibtex-bar {
      opacity: 1;
    }
    & .bibtex-copy-label {
      display: none;
    }
    & .bibtex-copy {
      padding: 0;
      inline-size: 1.875rem;
      justify-content: center;
    }
  }
  @media (hover: none) {
    .bibtex[data-variant="minimal"] > .bibtex-bar {
      opacity: 1;
    }
  }
  /* -- inline: one reference inside running text --------------------------- */
  .bibtex[data-variant="inline"] {
    display: inline;
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: none;
    color: inherit;
    & > .bibtex-source {
      font-family: inherit;
      font-size: inherit;
    }
    & > .bibtex-bar {
      display: inline-flex;
      vertical-align: -0.2em;
      padding: 0;
      margin-inline-start: 0.25rem;
      border: 0;
      background: none;
    }
    & .bibtex-view,
    & .bibtex-ref {
      display: inline;
      padding: 0;
      font-size: inherit;
      line-height: inherit;
    }
    & .bibtex-copy {
      block-size: 1.375rem;
      inline-size: 1.375rem;
      padding: 0;
      justify-content: center;
    }
    & .bibtex-copy-label {
      display: none;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .bibtex-tab,
    .bibtex-copy,
    .bibtex[data-variant="minimal"] > .bibtex-bar {
      transition: none;
    }
  }
  @media (prefers-contrast: more) {
    .bibtex,
    .bibtex-bar,
    .bibtex-copy,
    .bibtex-tab[aria-selected="true"] {
      border-color: var(--foreground);
    }
    .bibtex-tab,
    .bibtex-num {
      color: var(--foreground);
    }
  }
  @media (forced-colors: active) {
    .bibtex,
    .bibtex-bar,
    .bibtex-copy {
      border-color: CanvasText;
    }
    .bibtex-tab[aria-selected="true"] {
      border-color: Highlight;
      color: Highlight;
    }
    .bibtex[data-variant="minimal"] > .bibtex-bar {
      opacity: 1;
    }
  }
}

§JS view file

// -- BibTeX ---------------------------------------------------
// A citation block: the authored BibTeX (.bibtex-source) is parsed once and
// shown in the format the reader picks - the BibTeX itself (normalized,
// aligned, highlighted) or a formatted reference (APA, MLA, Chicago, Harvard,
// IEEE; several entries become a reference list) - with a copy button that
// copies exactly what is shown. Without JS the source stays readable as-is.
// State API (AGENTS.md "State API"): 'default' and 'copied', the format is
// the config ({ format }); render() reproduces the authored markup 1:1.
// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —
// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.
import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';
const df$ = defussGlobals();
const dfDollar = defussQuery();
// VERIFIED: (verify's component types ratchet - tsc -p tsconfig.components.json) every type
// this file's API docs state - arguments, return values, event details - holds
// against its code: a wrong one is a new type error and fails the build.
/** A citation format - the tab order. */
type BibtexStyle = 'bibtex' | 'apa' | 'mla' | 'chicago' | 'harvard' | 'ieee';
/** One parsed BibTeX entry. */
interface BibtexEntry {
  /** the entry type, lowercase (article, inproceedings, book, misc, ...) */
  type: string;
  /** the citation key */
  key: string;
  /** every field's raw value as written (braces and quotes removed, macros kept) */
  fields: Record<string, string>;
  /** the field names in source order */
  order: string[];
  /** the fields written without braces or quotes (a macro like jan, a number) */
  bare: Record<string, string>;
}
/** What format() returns. */
interface BibtexOutput {
  /** the formatted markup: one string for bibtex, else one reference per entry */
  html: string | string[];
  /** the same as plain text - what a copy takes */
  text: string;
  /** true when several entries became a list */
  list: boolean;
}
/** What bibtex-format carries. */
interface BibtexFormatDetail {
  /** the format shown now (the element's data-format - always one it offers) */
  format: BibtexStyle;
  /** the text a copy takes in it */
  text: string;
}
/** What bibtex-copy carries. */
interface BibtexCopyDetail {
  /** the format that was copied (the element's data-format - always one it offers) */
  format: BibtexStyle;
  /** the text copied */
  text: string;
  /** true when the clipboard took it; false: the view was selected for a manual copy */
  ok: boolean;
}
const bibtexStates = ['default', 'copied'];
/** setState() configs per state (getState() reports the format shown). */
export interface BibtexStateConfigs {
  /** The source shown in one format. */
  default: {
    /** the format to show (one the element offers); default: the authored data-format, else the first offered */
    format?: BibtexStyle;
  };
  /** The copy feedback - the button says Copied and the status announces it; back to default after two seconds. */
  copied: {
    /** the format that was copied (default: the one shown) */
    format?: BibtexStyle;
  };
}
/** every format, in the order the tabs show them */
const FORMATS: BibtexStyle[] = ['bibtex', 'apa', 'mla', 'chicago', 'harvard', 'ieee'];
const LABELS = { bibtex: 'BibTeX', apa: 'APA', mla: 'MLA', chicago: 'Chicago', harvard: 'Harvard', ieee: 'IEEE' };
/** how long the copy button says "Copied" */
const COPIED_MS = 2000;
// -- parsing -------------------------------------------------------------------------------------
/** the index after the brace / quote group opening at s[i] */
function skipGroup(s, i) {
  const open = s[i];
  if (open === '"') {
    let depth = 0;
    for (let j = i + 1; j < s.length; j++) {
      if (s[j] === '\\') { j++; continue; }
      if (s[j] === '{') depth++;
      else if (s[j] === '}') depth--;
      else if (s[j] === '"' && depth === 0) return j + 1;
    }
    return s.length;
  }
  let depth = 0;
  for (let j = i; j < s.length; j++) {
    if (s[j] === '\\') { j++; continue; }
    if (s[j] === '{' || s[j] === '(') depth++;
    else if (s[j] === '}' || s[j] === ')') {
      depth--;
      if (depth === 0) return j + 1;
    }
  }
  return s.length;
}
/**
 * BibTeX text → [{ type, key, fields: { name: raw value }, order: [names], bare: { name: value written without braces } }] - @comment / @preamble / @string are skipped
 * @param text - BibTeX source, any number of entries
 * @returns the entries, in source order
 */
function parse(text: string): BibtexEntry[] {
  const s = String(text ?? '');
  const entries = [];
  let i = 0;
  while ((i = s.indexOf('@', i)) !== -1) {
    const head = /^@\s*([a-zA-Z]+)\s*([{(])/.exec(s.slice(i));
    if (!head) { i++; continue; }
    const type = head[1].toLowerCase();
    const start = i + head[0].length - 1;
    const end = skipGroup(s, start);
    if (type === 'comment' || type === 'preamble' || type === 'string') { i = end; continue; }
    const body = s.slice(start + 1, end - 1);
    const comma = body.indexOf(',');
    const key = (comma < 0 ? body : body.slice(0, comma)).trim();
    const fields = {};
    const order = [];
    const bare = {};
    let p = comma < 0 ? body.length : comma + 1;
    while (p < body.length) {
      const m = /^[\s,]*([A-Za-z][\w:-]*)\s*=\s*/.exec(body.slice(p));
      if (!m) break;
      p += m[0].length;
      let value = '';
      let pieces = 0;
      let bareOnly = true;
      // a value: {...}, "...", a number or a macro - pieces joined with #
      for (;;) {
        pieces++;
        if (body[p] === '{' || body[p] === '"') {
          const q = skipGroup(body, p);
          value += body.slice(p + 1, q - 1);
          p = q;
          bareOnly = false;
        } else {
          const bare = /^[^,#}\s]+/.exec(body.slice(p));
          if (bare) { value += bare[0]; p += bare[0].length; }
        }
        const more = /^\s*#\s*/.exec(body.slice(p));
        if (!more) break;
        p += more[0].length;
      }
      const name = m[1].toLowerCase();
      if (!(name in fields)) order.push(name);
      fields[name] = name === 'month' && bareOnly ? (MONTHS[value.trim().toLowerCase().slice(0, 3)] ?? value.trim()) : value.trim();
      if (bareOnly && pieces === 1) bare[name] = value.trim();
      else delete bare[name];
    }
    entries.push({ type, key, fields, order, bare });
    i = end;
  }
  return entries;
}
/** the month macros of BibTeX (jan ... dec) */
const MONTHS = { jan: 'January', feb: 'February', mar: 'March', apr: 'April', may: 'May', jun: 'June', jul: 'July', aug: 'August', sep: 'September', oct: 'October', nov: 'November', dec: 'December' };
/** IEEE's month abbreviations */
const IEEE_MONTH = { January: 'Jan.', February: 'Feb.', March: 'Mar.', April: 'Apr.', May: 'May', June: 'Jun.', July: 'Jul.', August: 'Aug.', September: 'Sep.', October: 'Oct.', November: 'Nov.', December: 'Dec.' };
const ACCENTS = { '"': '̈', "'": '́', '`': '̀', '^': '̂', '~': '̃', '=': '̄', '.': '̇', c: '̧', v: '̌', u: '̆', H: '̋', k: '̨' };
const SYMBOLS = { ss: 'ß', o: 'ø', O: 'Ø', aa: 'å', AA: 'Å', ae: 'æ', AE: 'Æ', l: 'ł', L: 'Ł', i: 'ı', oe: 'œ', OE: 'Œ' };
/** a LaTeX-ish value as plain text: accents, dashes, ties, escapes; braces and commands dropped */
function clean(value) {
  return String(value ?? '')
    .replace(/\{?\\([`'"^~=.])\s*\{?([A-Za-z])\}?\}?/g, (_m, a, ch) => (ch + ACCENTS[a]).normalize('NFC'))
    .replace(/\{?\\([cvuHk])\s*\{([A-Za-z])\}\}?/g, (_m, a, ch) => (ch + ACCENTS[a]).normalize('NFC'))
    .replace(/\{?\\(ss|aa|AA|ae|AE|oe|OE|o|O|l|L|i)\b\s*\}?/g, (_m, c) => SYMBOLS[c])
    .replace(/\\(TeX|LaTeX|BibTeX|XeTeX|LuaTeX)\b\s*(\{\})?/g, '$1')
    .replace(/\\([&%$#_{}])/g, '$1')
    .replace(/---/g, '—')
    .replace(/--/g, '–')
    .replace(/~/g, ' ')
    .replace(/\\[A-Za-z]+\s*/g, '')
    .replace(/[{}]/g, '')
    .replace(/\s+/g, ' ')
    .trim();
}
/** split at a top-level separator (outside braces) */
function splitTop(value, sep) {
  const out = [];
  let depth = 0;
  let cur = '';
  const s = String(value);
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (ch === '{') depth++;
    else if (ch === '}') depth--;
    if (depth === 0 && sep.test(s.slice(i))) {
      const m = sep.exec(s.slice(i));
      out.push(cur);
      cur = '';
      i += m[0].length - 1;
      continue;
    }
    cur += ch;
  }
  out.push(cur);
  return out.map((x) => x.trim()).filter(Boolean);
}
/** one name: { first, last } - "Last, First", "First von Last" or a braced {Corporate Name} */
function parseName(raw) {
  const r = raw.trim();
  // one braced group around the whole name: an organization, never split
  if (r.startsWith('{') && skipGroup(r, 0) === r.length) return { first: '', last: clean(r), corporate: true };
  const parts = splitTop(r, /^,/);
  if (parts.length >= 2) return { first: clean(parts[parts.length - 1]), last: clean(parts[0]) };
  const words = splitTop(r, /^\s+/);
  if (words.length === 1) return { first: '', last: clean(words[0]) };
  let k = words.length - 1;
  while (k > 1 && /^[a-z]/.test(clean(words[k - 1]))) k--; // "von", "van der", "de la"
  return { first: clean(words.slice(0, k).join(' ')), last: clean(words.slice(k).join(' ')) };
}
/** the names of an author / editor field */
const names = (value) => (value ? splitTop(value, /^\s+and\s+/i).map(parseName) : []);
/** "Jean-Paul Marie" → "J.-P. M." */
const initials = (first) =>
  first.split(/\s+/).filter(Boolean).map((w) => w.split('-').map((p) => (p ? `${p[0].toUpperCase()}.` : '')).join('-')).join(' ');
// -- formatting ----------------------------------------------------------------------------------
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const it = (s) => `<i>${esc(s)}</i>`;
/** text with a closing mark unless it already ends in one */
const end = (s, mark = '.') => (/[.?!]$/.test(s) ? s : s + mark);
const list = (items, sep, last) => (items.length < 2 ? items.join('') : `${items.slice(0, -1).join(sep)}${last}${items[items.length - 1]}`);
const pages = (p) => clean(p).replace(/\s*[-–]+\s*/g, '–');
/** the fields every style reads, cleaned */
function facts(entry) {
  const f = entry.fields;
  const c = (k) => (f[k] ? clean(f[k]) : '');
  return {
    type: entry.type,
    authors: names(f.author),
    editors: names(f.editor),
    title: c('title'),
    container: c('journal') || c('booktitle') || c('series'),
    year: c('year') || (f.date ? clean(f.date).slice(0, 4) : ''),
    month: c('month'),
    volume: c('volume'),
    number: c('number') || c('issue'),
    pages: f.pages ? pages(f.pages) : '',
    publisher: c('publisher') || c('institution') || c('school') || c('organization'),
    address: c('address') || c('location'),
    edition: c('edition'),
    doi: c('doi').replace(/^https?:\/\/(dx\.)?doi\.org\//, ''),
    url: f.url ? clean(f.url).replace(/\s/g, '') : '',
    note: c('note') || c('howpublished'),
  };
}
const isArticle = (t) => t === 'article';
const isPart = (t) => t === 'inproceedings' || t === 'incollection' || t === 'inbook' || t === 'conference';
/**
 * A clickable link - an <a> in the view only: the copy is built from the
 * text (plain() drops the tags), so it never carries markup. Only http(s)
 * targets become links; anything else stays text.
 */
const anchor = (href, text = href) =>
  /^https?:\/\//i.test(href) ? `<a class="bibtex-link" href="${esc(href)}" target="_blank" rel="noopener">${esc(text)}</a>` : esc(text);
const doiUrl = (doi) => `https://doi.org/${doi}`;
const link = (x) => (x.doi ? ` ${anchor(doiUrl(x.doi))}` : x.url ? ` ${anchor(x.url)}` : '');
const STYLES = {
  apa(x) {
    const who = x.authors.length ? x.authors : x.editors;
    const one = (n) => (n.corporate || !n.first ? esc(n.last) : `${esc(n.last)}, ${esc(initials(n.first))}`);
    let out = who.length ? end(who.length > 1 ? list(who.map(one), ', ', ', &amp; ') : one(who[0])) : '';
    if (!x.authors.length && x.editors.length) out = out.replace(/\.$/, '') + ` (Ed${x.editors.length > 1 ? 's' : ''}.).`;
    out += ` (${esc(x.year || 'n.d.')}). `;
    if (isArticle(x.type)) {
      out += `${esc(end(x.title))} ${x.container ? it(x.container) : ''}${x.volume ? `, ${it(x.volume)}` : ''}${x.number ? `(${esc(x.number)})` : ''}${x.pages ? `, ${esc(x.pages)}` : ''}.`;
    } else if (isPart(x.type)) {
      out += `${esc(end(x.title))} In ${x.container ? it(x.container) : ''}${x.pages ? ` (pp. ${esc(x.pages)})` : ''}.${x.publisher ? ` ${esc(end(x.publisher))}` : ''}`;
    } else {
      out += `${it(x.title)}${x.type === 'techreport' && x.number ? ` (Report No. ${esc(x.number)})` : ''}${x.edition ? ` (${esc(x.edition)} ed.)` : ''}.${x.publisher ? ` ${esc(end(x.publisher))}` : ''}`;
    }
    return out + link(x);
  },
  mla(x) {
    const a = x.authors;
    const full = (n) => (n.corporate || !n.first ? esc(n.last) : `${esc(n.first)} ${esc(n.last)}`);
    const inv = (n) => (n.corporate || !n.first ? esc(n.last) : `${esc(n.last)}, ${esc(n.first)}`);
    let out = a.length === 0 ? '' : a.length === 1 ? inv(a[0]) : a.length === 2 ? `${inv(a[0])}, and ${full(a[1])}` : `${inv(a[0])}, et al`;
    if (out) out = `${end(out)} `;
    const bits = [];
    if (isArticle(x.type) || isPart(x.type)) {
      out += `“${esc(end(x.title))}” `;
      if (x.container) bits.push(it(x.container));
      if (x.volume) bits.push(`vol. ${esc(x.volume)}`);
      if (x.number) bits.push(`no. ${esc(x.number)}`);
      if (x.publisher && isPart(x.type)) bits.push(esc(x.publisher));
      if (x.year) bits.push(esc(x.year));
      if (x.pages) bits.push(`pp. ${esc(x.pages)}`);
    } else {
      out += `${it(end(x.title))} `;
      if (x.publisher) bits.push(esc(x.publisher));
      if (x.year) bits.push(esc(x.year));
    }
    out += bits.length ? `${bits.join(', ')}.` : '';
    return out.trim() + link(x).replace(/^ (.+)$/, ' $1.');
  },
  chicago(x) {
    const a = x.authors;
    const full = (n) => (n.corporate || !n.first ? esc(n.last) : `${esc(n.first)} ${esc(n.last)}`);
    const inv = (n) => (n.corporate || !n.first ? esc(n.last) : `${esc(n.last)}, ${esc(n.first)}`);
    const who = a.length ? [inv(a[0]), ...a.slice(1).map(full)] : [];
    let out = who.length ? `${end(list(who, ', ', who.length > 2 ? ', and ' : ' and '))} ` : '';
    out += `${esc(x.year || 'n.d.')}. `;
    if (isArticle(x.type)) {
      out += `“${esc(end(x.title))}” ${x.container ? it(x.container) : ''}${x.volume ? ` ${esc(x.volume)}` : ''}${x.number ? ` (${esc(x.number)})` : ''}${x.pages ? `: ${esc(x.pages)}` : ''}.`;
    } else if (isPart(x.type)) {
      out += `“${esc(end(x.title))}” In ${x.container ? it(x.container) : ''}${x.pages ? `, ${esc(x.pages)}` : ''}.${x.publisher ? ` ${x.address ? `${esc(x.address)}: ` : ''}${esc(end(x.publisher))}` : ''}`;
    } else {
      out += `${it(end(x.title))}${x.publisher ? ` ${x.address ? `${esc(x.address)}: ` : ''}${esc(end(x.publisher))}` : ''}`;
    }
    return out + link(x).replace(/^ (.+)$/, ' $1.');
  },
  harvard(x) {
    const a = x.authors;
    const one = (n) => (n.corporate || !n.first ? esc(n.last) : `${esc(n.last)}, ${esc(initials(n.first).replace(/ /g, ''))}`);
    let out = a.length ? `${list(a.map(one), ', ', ' and ')} ` : '';
    out += `(${esc(x.year || 'n.d.')}) `;
    if (isArticle(x.type)) {
      out += `‘${esc(x.title)}’, ${x.container ? it(x.container) : ''}${x.volume ? `, ${esc(x.volume)}` : ''}${x.number ? `(${esc(x.number)})` : ''}${x.pages ? `, pp. ${esc(x.pages)}` : ''}.`;
    } else if (isPart(x.type)) {
      out += `‘${esc(x.title)}’, in ${x.container ? it(x.container) : ''}${x.pages ? `, pp. ${esc(x.pages)}` : ''}.${x.publisher ? ` ${esc(end(x.publisher))}` : ''}`;
    } else {
      out += `${it(x.title)}.${x.publisher ? ` ${x.address ? `${esc(x.address)}: ` : ''}${esc(end(x.publisher))}` : ''}`;
    }
    if (x.doi) out += ` doi:${anchor(doiUrl(x.doi), x.doi)}.`;
    else if (x.url) out += ` Available at: ${anchor(x.url)}.`;
    return out;
  },
  ieee(x, n) {
    const a = x.authors;
    const one = (p) => (p.corporate || !p.first ? esc(p.last) : `${esc(initials(p.first))} ${esc(p.last)}`);
    const who = a.length > 6 ? `${one(a[0])} <i>et al.</i>` : list(a.map(one), ', ', a.length > 2 ? ', and ' : ' and ');
    let out = `<span class="bibtex-num">[${n}]</span> ${who ? `${who}, ` : ''}`;
    const when = [IEEE_MONTH[x.month] ?? x.month, x.year].filter(Boolean).map(esc).join(' ');
    if (isArticle(x.type)) {
      out += `“${esc(x.title)},” ${[x.container ? it(x.container) : '', x.volume && `vol. ${esc(x.volume)}`, x.number && `no. ${esc(x.number)}`, x.pages && `pp. ${esc(x.pages)}`, when].filter(Boolean).join(', ')}`;
    } else if (isPart(x.type)) {
      out += `“${esc(x.title)},” in ${[x.container ? it(x.container) : '', x.address && esc(x.address), when, x.pages && `pp. ${esc(x.pages)}`].filter(Boolean).join(', ')}`;
    } else if (x.type === 'techreport') {
      out += `“${esc(x.title)},” ${[x.publisher && esc(x.publisher), x.address && esc(x.address), `Tech. Rep.${x.number ? ` ${esc(x.number)}` : ''}`, when].filter(Boolean).join(', ')}`;
    } else {
      out += `${it(x.title)}${x.edition ? `, ${esc(x.edition)} ed` : ''}. ${[x.address && esc(x.address), x.publisher && esc(x.publisher)].filter(Boolean).join(': ')}${x.publisher || x.address ? ', ' : ''}${esc(x.year)}`;
    }
    out = end(out);
    if (x.doi) out = `${out.replace(/\.$/, '')}, doi: ${anchor(doiUrl(x.doi), x.doi)}.`;
    else if (x.url) out += ` [Online]. Available: ${anchor(x.url)}`;
    return out;
  },
};
/** the sort key of a reference list: first author's last name, then the year */
const sortKey = (x) => `${(x.authors[0] ?? x.editors[0])?.last ?? x.title}`.toLowerCase() + ' ' + x.year;
/** the normalized BibTeX of entries - aligned "=" unless align is false; highlighted HTML and plain text */
function bibtexOut(entries, { align = true, highlight = true } = {}) {
  const span = (cls, s) => (highlight ? `<span class="bibtex-${cls}">${esc(s)}</span>` : esc(s));
  const html = [];
  const text = [];
  for (const e of entries) {
    const width = align ? Math.max(0, ...e.order.map((k) => k.length)) : 0;
    const lines = e.order.map((k) => {
      const pad = ' '.repeat(Math.max(0, width - k.length));
      const raw = e.bare?.[k];
      const value = raw ?? e.fields[k];
      const href = k === 'doi' ? doiUrl(value.replace(/^https?:\/\/(dx\.)?doi\.org\//, '')) : /^https?:\/\/\S+$/.test(value) ? value : null;
      const shown = href && highlight ? `<span class="bibtex-value">${anchor(href, value)}</span>` : span('value', value);
      const [open, close] = raw === undefined ? ['{', '}'] : ['', ''];
      return { html: `  ${span('field', k)}${pad} = ${open}${href && !highlight ? anchor(href, value) : shown}${close},`, text: `  ${k}${pad} = ${open}${value}${close},` };
    });
    html.push([`@${span('type', e.type)}{${span('key', e.key)},`, ...lines.map((l) => l.html), '}'].join('\n'));
    text.push([`@${e.type}{${e.key},`, ...lines.map((l) => l.text), '}'].join('\n'));
  }
  return { html: html.join('\n\n'), text: text.join('\n\n') };
}
/** HTML → its text (the copy of a formatted reference) */
const plain = (html) =>
  html.replace(/<[^>]+>/g, '').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
/**
 * Format entries in one style. A single entry is one reference; several are a
 * list - IEEE in source order, numbered; the others sorted by author and year.
 */
function format(entries, style, { align = true, highlight = true } = {}) {
  if (style === 'bibtex') return { ...bibtexOut(entries, { align, highlight }), list: false };
  const fn = STYLES[style];
  if (!fn) throw new Error(`bibtex: unknown format "${style}" (supported: ${FORMATS.join(', ')})`);
  let rows = entries.map((e, i) => ({ x: facts(e), i }));
  if (style !== 'ieee') rows = rows.sort((a, b) => sortKey(a.x).localeCompare(sortKey(b.x)));
  const refs = rows.map(({ x, i }) => fn(x, i + 1));
  return { html: refs, text: refs.map(plain).join('\n\n'), list: refs.length > 1 };
}
// -- the element ---------------------------------------------------------------------------------
/** a known format name */
const isStyle = (f: string): f is BibtexStyle => (FORMATS as readonly string[]).includes(f);
/** the formats an element offers (data-formats, else every one) */
function formatsOf(el): BibtexStyle[] {
  const own = String(dfDollar(el).attr('data-formats') ?? '').split(/[\s,]+/).filter(isStyle);
  return own.length ? own : FORMATS;
}
/** the format a state shows: its config, else the authored data-format, else the first offered */
function formatFor(el, state, authored) {
  const offered = formatsOf(el);
  const want = state?.config?.format;
  if (want && offered.includes(want)) return want;
  return authored && offered.includes(authored) ? authored : offered[0];
}
/**
 * The markup of a state - the one place a state becomes attributes on the
 * root: the format shown (data-format) and the copy feedback (data-copied).
 * setState runs it on the live element, render() on a detached copy.
 */
function applyMarkup(el, state, authored) {
  dfDollar(el).attr('data-format', formatFor(el, state, authored));
  dfDollar(el).attr('data-copied', state.name === 'copied' ? '' : null);
}
const COPY_ICON = '<svg class="bibtex-icon-copy" viewBox="0 0 24 24" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
const DONE_ICON = '<svg class="bibtex-icon-done" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
/** build the bar (format tabs + copy) and the view once - runtime-owned parts */
function build(root) {
  const inline = root.tagName === 'SPAN';
  const tag = inline ? 'span' : 'div';
  const variant = dfDollar(root).attr('data-variant');
  const offered = formatsOf(root);
  const tabs = offered.length > 1 && variant !== 'minimal' && variant !== 'inline'
    ? `<${tag} class="bibtex-tabs" role="tablist" aria-label="Citation format">${offered
        .map((f) => `<button type="button" class="bibtex-tab" role="tab" data-format="${f}" aria-selected="false" tabindex="-1">${LABELS[f]}</button>`)
        .join('')}</${tag}>`
    : '';
  const copy = dfDollar(root).attr('data-copy') === 'none'
    ? ''
    : `<button type="button" class="bibtex-copy">${COPY_ICON}${DONE_ICON}<span class="bibtex-copy-label">Copy</span></button>`;
  const bar = `<${tag} class="bibtex-bar">${tabs}${copy}<${tag} class="bibtex-status" role="status"></${tag}></${tag}>`;
  const view = `<${tag} class="bibtex-view"${tabs ? ' role="tabpanel"' : ''}></${tag}>`;
  const caption = dfDollar(root).children('figcaption').get(0);
  if (inline) {
    // inside running text: the reference first, the copy icon after it
    dfDollar(root).append(dfDollar(view));
    dfDollar(root).append(dfDollar(bar));
  } else {
    if (caption) dfDollar(caption).after(dfDollar(bar));
    else dfDollar(root).append(dfDollar(bar));
    dfDollar(root).append(dfDollar(view));
  }
  root._bar = dfDollar(root).children('.bibtex-bar').get(0);
  root._view = dfDollar(root).children('.bibtex-view').get(0);
  const tablist = dfDollar(root._bar).find('[role="tablist"]').get(0);
  if (tablist) {
    dfDollar(root._view).attr('id', root._view.id || `${root.id || 'bibtex'}-view-${Math.random().toString(36).slice(2, 7)}`);
    dfDollar(tablist).find('[role="tab"]').attr('aria-controls', root._view.id);
  }
}
/** fill the view with a format; remember the text a copy takes */
function show(root, fmt) {
  const out = format(root._entries, fmt, {
    align: dfDollar(root).attr('data-align') !== 'none',
    highlight: dfDollar(root).attr('data-highlight') !== 'none',
  });
  root._text = out.text;
  const inline = root.tagName === 'SPAN';
  let markup;
  if (fmt === 'bibtex') markup = inline ? `<code class="bibtex-code">${out.html}</code>` : `<pre class="bibtex-code"><code>${out.html}</code></pre>`;
  else if (out.list && !inline) markup = `<ol class="bibtex-list">${out.html.map((r) => `<li class="bibtex-ref">${r}</li>`).join('')}</ol>`;
  else markup = inline ? `<span class="bibtex-ref">${out.html.join(' ')}</span>` : `<p class="bibtex-ref">${out.html.join(' ')}</p>`;
  dfDollar(root._view).html(markup);
  dfDollar(root._bar).find('.bibtex-tab').each((_i, tab) => {
    const on = dfDollar(tab).attr('data-format') === fmt;
    dfDollar(tab).attr('aria-selected', String(on)).attr('tabindex', on ? '0' : '-1');
  });
  dfDollar(root._bar).find('.bibtex-copy').attr('aria-label', `Copy ${LABELS[fmt]}`);
}
/** UI side of setState (AGENTS.md "State API"): the attributes, the view, the copy feedback */
function triggerStateChange(root, state) {
  // the copy feedback keeps the format on show when none is named
  if (state.name === 'copied' && !state.config?.format && root._shown) state = { ...state, config: { ...state.config, format: root._shown } };
  applyMarkup(root, state, root._authored);
  const fmt = dfDollar(root).attr('data-format');
  if (fmt !== root._shown) {
    root._shown = fmt;
    show(root, fmt);
    // Fires when the shown format changes - by a tab, the keyboard or setState; the format and the text a copy takes.
    root.dispatchEvent(new CustomEvent<BibtexFormatDetail>('bibtex-format', { bubbles: true, detail: { format: fmt as BibtexStyle, text: root._text } }));
  }
  const copied = state.name === 'copied';
  dfDollar(root._bar).find('.bibtex-copy-label').text(copied ? 'Copied' : 'Copy');
  dfDollar(root._bar).find('.bibtex-status').text(copied ? `${LABELS[fmt]} copied to the clipboard` : '');
  clearTimeout(root._copiedTimer);
  if (copied) {
    root._copiedTimer = setTimeout(() => {
      if (root.isConnected && root.store?.value.name === 'copied') root.api.setState('default', { format: fmt });
    }, COPIED_MS);
  }
}
/** Registry-level API; pass the element explicitly. Unknown names throw. */
export const bibtexApi = componentState({
  component: 'bibtex',
  states: bibtexStates,
  apply: (root, state) => triggerStateChange(root, state),
  // getState reports the format the block shows, whatever set it
  read: (root, state) => ({ name: dfDollar(root).attr('data-state-name') || state.name, config: { ...state.config, format: dfDollar(root).attr('data-format') } }),
  markup: (el, state) => {
    const authored = state.model.attrs.find(([name]) => name === 'data-format');
    applyMarkup(el, state, authored ? authored[1] : null);
  },
});
df$.bibtexApi = bibtexApi;
df$.bibtexStates = bibtexStates;
/** copy what the element shows; true when the clipboard took it (else the view is selected for a manual copy) */
async function copy(root) {
  const fmt = dfDollar(root).attr('data-format');
  let ok = true;
  try {
    await navigator.clipboard.writeText(root._text ?? '');
  } catch {
    ok = false;
    globalThis.getSelection?.()?.selectAllChildren(root._view);
  }
  if (ok) root.api.setState('copied', { format: fmt });
  // Fires after a copy - the format, the text copied and whether the clipboard took it (false: the view was selected instead).
  root.dispatchEvent(new CustomEvent<BibtexCopyDetail>('bibtex-copy', { bubbles: true, detail: { format: fmt as BibtexStyle, text: root._text, ok } }));
  return ok;
}
const resolve = (target) => (typeof target === 'string' ? dfDollar(target).get(0) : target);
df$.bibtex = {
  /** Every format, in tab order: bibtex, apa, mla, chicago, harvard, ieee. */
  formats: FORMATS,
  /** Parse BibTeX text into entries - [{ type, key, fields, order }]; @comment, @preamble and @string are skipped. */
  parse,
  /**
   * Format entries (or BibTeX text) in one style - { html, text, list }: html is a string for bibtex, else one reference per entry. Options: align, highlight (bibtex).
   * @param entries - parsed entries, or BibTeX text to parse first
   * @param style - the citation format
   * @param options - bibtex only: align the = signs (default true), highlight the parts (default true)
   * @returns the markup, the plain text and whether it is a list
   */
  format: (entries: BibtexEntry[] | string, style: BibtexStyle, options?: { align?: boolean; highlight?: boolean }): BibtexOutput => format(typeof entries === 'string' ? parse(entries) : entries, style, options),
  /**
   * Show a format on an element (state 'default' with that format).
   * @param target - the .bibtex element or its selector
   * @param fmt - the format to show (one the element offers)
   */
  show: (target: string | HTMLElement, fmt: BibtexStyle): void => { resolve(target)?.api.setState('default', { format: fmt }); },
  /**
   * Copy what an element shows.
   * @param target - the .bibtex element or its selector
   * @returns true when the clipboard took it; false when the view was selected for a manual copy
   */
  copy: (target: string | HTMLElement): Promise<boolean> => copy(resolve(target)),
  /**
   * The text a copy of the element takes now.
   * @param target - the .bibtex element or its selector
   * @returns the shown format as plain text ('' before the first render)
   */
  text: (target: string | HTMLElement): string => resolve(target)?._text ?? '',
  /**
   * The element's parsed entries.
   * @param target - the .bibtex element or its selector
   * @returns a copy of the entries its source holds
   */
  entries: (target: string | HTMLElement): BibtexEntry[] => structuredClone(resolve(target)?._entries ?? []),
};
function init() {
  dfDollar('.bibtex:not([data-init])').each((_i, root) => {
    dfDollar(root).data('init', '');
    const src = dfDollar(root).find('.bibtex-source').get(0);
    root._entries = parse(src ? dfDollar(src).text() : '');
    root._authored = dfDollar(root).attr('data-format') ?? null;
    build(root);
    // el.store + el.api: `$('#cite').api.setState('default', { format: 'apa' })`
    bindComponent(root, bibtexApi, { name: 'default', config: { format: formatFor(root, null, root._authored) } });
    triggerStateChange(root, root.store.value);
    dfDollar(root._bar).on('click', (e) => {
      const tab = (e.target as HTMLElement).closest?.<HTMLElement>('.bibtex-tab');
      if (tab) root.api.setState('default', { format: dfDollar(tab).attr('data-format') });
      else if ((e.target as HTMLElement).closest?.('.bibtex-copy')) copy(root);
    });
    // the tabs: ← / → / Home / End move and show (automatic activation, APG tabs)
    dfDollar(root._bar).on('keydown', (e) => {
      const tab = (e.target as HTMLElement).closest?.<HTMLElement>('.bibtex-tab');
      if (!tab) return;
      const tabs = dfDollar(root._bar).find('.bibtex-tab').toArray();
      const at = tabs.indexOf(tab);
      const to = { ArrowRight: at + 1, ArrowLeft: at - 1, Home: 0, End: tabs.length - 1 }[e.key];
      if (to === undefined) return;
      e.preventDefault();
      const next = tabs[(to + tabs.length) % tabs.length];
      root.api.setState('default', { format: dfDollar(next).attr('data-format') });
      next.focus();
    });
  });
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });

Comments, ideas or improvements? Edit this page's source on GitHub