BibTeXATM
How to cite this: the BibTeX you author, shown the way the reader needs it - as BibTeX (normalized, the fields aligned, highlighted) or formatted as an APA, MLA, Chicago, Harvard or IEEE reference. Several entries become a reference list, sorted or numbered as the style wants. DOIs and URLs are clickable links; the copy button copies exactly what is shown - as plain text, never markup - and says so. Without JavaScript the source stays readable as-is. Used by the Modern Paper template.
On this page (16)
§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 ofconfig.format(data-formaton the block)copied- the copy feedback: the button says Copied (data-copied), the status announces it; back todefaultafter 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:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
format | enum | bibtex, apa, mla, chicago, harvard, ieee | bibtex | The format shown - setState('default', { format: 'apa' }); mirrored as data-format on the block. |
copied | boolean | true, false | false | The 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.
| State | Description | ||||||
|---|---|---|---|---|---|---|---|
default | The source shown in one format.
| ||||||
copied | The copy feedback - the button says Copied and the status announces it; back to default after two seconds.
|
Every element
| Member | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
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.
Returns | |||||||||
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 | |||||||||
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.
Returns | |||||||||
el.api.settled(): Promise<void> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). Returns | |||||||||
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
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
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.
Returns | ||||||||||||
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.
Returns | ||||||||||||
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.
Returns | ||||||||||||
df$.shadcn.bibtexApi.store(el: HTMLElement): Store<{ name: BibtexState; config: BibtexStateConfigs[BibtexState] }> | The element's store (bindComponent made it).
Returns | ||||||||||||
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.
| ||||||||||||
df$.shadcn.bibtexStates: BibtexState[] | The declared states, 'default' first: default, copied. |
df$.shadcn.bibtex
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
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. */
Returns | ||||||||||||
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).
Returns | ||||||||||||
show(target: string | HTMLElement, fmt: BibtexStyle): void | Show a format on an element (state 'default' with that format).
| ||||||||||||
copy(target: string | HTMLElement): Promise<boolean> | Copy what an element shows.
Returns | ||||||||||||
text(target: string | HTMLElement): string | The text a copy of the element takes now.
Returns | ||||||||||||
entries(target: string | HTMLElement): BibtexEntry[] | The element's parsed entries.
Returns |
Events
| Event | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
bibtex-copy | Fires after a copy - the format, the text copied and whether the clipboard took it (false: the view was selected instead).
| ||||||||||||
bibtex-format | Fires when the shown format changes - by a tab, the keyboard or setState; the format and the text a copy takes.
|
Types
| Type | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
BibtexCopyDetail | What bibtex-copy carries.
| ||||||||||||||||||
BibtexEntry | One parsed BibTeX entry.
| ||||||||||||||||||
BibtexFormatDetail | What bibtex-format carries.
| ||||||||||||||||||
BibtexOutput | What format() returns.
| ||||||||||||||||||
BibtexStyle | A citation format - the tab order. = |
§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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');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), ', ', ', & ') : 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(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/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