Typography

CitationBuilder

Builds APA, MLA, Chicago and BibTeX citations for a book, article or web page, reading author names however they are written and flagging what is missing.

#smart#interactive#citation#bibliography#writing#reference

Demo

Preview
Source
One a line, or with semicolons: Last, First. Put an organisation in { }.

Read as: Le Guin, Ursula K.

Citation ready.

APA 7

Le Guin, U. K. (1974). The Dispossessed (2nd ed.). Harper & Row.

MLA 9

Le Guin, Ursula K. The Dispossessed. 2nd ed., Harper & Row, 1974.

Chicago 17

Le Guin, Ursula K. The Dispossessed. 2nd ed. New York: Harper & Row, 1974.

BibTeX

@book{leguin1974dispossessed,
  author = {Le Guin, Ursula K.},
  title = {The Dispossessed},
  publisher = {Harper \& Row},
  address = {New York},
  year = {1974},
  edition = {2nd}
}
Source
One a line, or with semicolons: Last, First. Put an organisation in { }.

Read as: Watson, James D.; Crick, Francis

Citation ready.

APA 7

Watson, J. D., & Crick, F. (1953). Molecular structure of nucleic acids. Nature, 171(4356), 737–738. https://doi.org/10.1038/171737a0

MLA 9

Watson, James D., and Francis Crick. “Molecular structure of nucleic acids.” Nature, vol. 171, no. 4356, 1953, pp. 737-38, https://doi.org/10.1038/171737a0.

Chicago 17

Watson, James D., and Francis Crick. “Molecular structure of nucleic acids.” Nature 171, no. 4356 (1953): 737–738. https://doi.org/10.1038/171737a0.

BibTeX

@article{watson1953molecular,
  author = {Watson, James D. and Crick, Francis},
  title = {Molecular structure of nucleic acids},
  journal = {Nature},
  year = {1953},
  volume = {171},
  number = {4356},
  pages = {737--738},
  doi = {10.1038/171737a0}
}
Source
One a line, or with semicolons: Last, First. Put an organisation in { }.

Read as: World Health Organization (organisation)

Citation built. Still needed: Site, Address.

APA 7

World Health Organization. (n.d.). Drinking-water.

MLA 9

World Health Organization. “Drinking-water.” Accessed 1 Nov. 2026.

Chicago 17

World Health Organization. “Drinking-water.” Accessed November 1, 2026.

BibTeX

@misc{worldhealthorganizationdrinking,
  author = {{World Health Organization}},
  title = {Drinking-water},
  note = {Accessed 2026-11-01}
}

Change a field to see on_change.

Get it

It needs theme.qwe, as every component does, and copy_button.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/copy_button.qwe$ curl -O https://quillolang.org/components/ui/citation_builder.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/citation_builder.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/citation_builder.qwe
let citation_builder_demo_book = { authors: "Le Guin, Ursula K.", title: "The Dispossessed", year: "1974", publisher: "Harper & Row", place: "New York", edition: "2" }let citation_builder_demo_article = { authors: "Watson, James D.; Crick, Francis", title: "Molecular structure of nucleic acids", journal: "Nature", year: "1953", volume: "171", issue: "4356", pages: "737-738",                                      doi: "https://doi.org/10.1038/171737a0" }component CitationBuilderDemoLive() {    state apa = null    <div.demo-stack>        <CitationBuilder id="citation-builder-demo-live" name="cite" kind="web" today="2026-11-01" fields={{ title: "Drinking-water", authors: "{World Health Organization}" }} on_change={(c) => { apa = c.apa_text }}/>        <p.demo-note>{apa == null ? "Change a field to see on_change." : `on_change: ${apa}`}</p>    </div>}view CitationBuilderDemo() =    <div.demo-stack>        <CitationBuilder id="citation-builder-demo" fields={citation_builder_demo_book} today="2026-11-01"/>        <CitationBuilder id="citation-builder-demo-article" kind="article" fields={citation_builder_demo_article} today="2026-11-01"/>        <CitationBuilderDemoLive/>    </div>

Source

ui/citation_builder.qwe
// CitationBuilder: a reference for a book, a journal article or a web page, in APA 7, MLA// 9, Chicago 17 (bibliography) and BibTeX at once, built from fields as they are typed.// It reads author names however they are written ("Le Guin, Ursula K.", "Ursula K. Le// Guin", several with semicolons, "van der Berg, Anna", "King, Martin Luther, Jr."), shows// how it read them, and leaves organisations whole ("World Health Organization", or any// name in { }). Each style's rules for one, two and many authors are followed, titles// are in italics or quotes as the style asks, and each has a Copy button. What a style// needs and is missing is listed, and so is what looks wrong: a date that does not// exist, a DOI that is not one, a title in the wrong capitals for APA.////   <CitationBuilder kind="book" fields={{ authors: "Le Guin, Ursula K.", title: "The Dispossessed", year: "1974", publisher: "Harper & Row", place: "New York" }}/>//   <CitationBuilder kind="web" today="2026-11-01" on_change={(c) => { reference = c.apa_text }}/>//// kind: "book" (the default), "article" or "web"; the person can switch it. fields: what// the boxes start with, a map of authors, title, year, edition, publisher, place,// journal, volume, issue, pages, doi, site, date, url and accessed (each a string;// year is the book's or article's, date a web page's, "2026", "2026-03" or "2026-03-05"// as accessed is). today: the date ("2026-11-01"), for noticing a date yet to come// and to fill in accessed; left out, neither. They are real fields named those, with// kind for the buttons, so a plain <form> sends them; name: a prefix for the names ("cite"// names them cite_title, ...). id: a prefix for its ids ("citation-builder"), unique on// the page. on_change(citation): inside one of your components, given the map below// as it is typed. class: more classes for its outer element.//// citation_builder_of(kind, fields, today) is the whole thing, for a program's own use:// { apa, mla, chicago, bibtex, apa_text, mla_text, chicago_text, authors, missing,// warnings }. apa, mla and chicago are lists of { text, italic } to show, the _text ones// the same as plain text, bibtex is text, authors is what citation_builder_authors(text)// gives, each a { kind: "person" | "org", family, given, suffix } (kind "org" has only// family), missing lists field labels and warnings sentences.import ui/theme.qwe use ui_clsimport ui/copy_button.qwe use CopyButtonlet citation_builder_css = css`.ui-citation-builder { display: grid; gap: 14px; min-width: 0; max-width: 760px; font: 14px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-citation-builder-kind { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0; padding: 0; border: 0; min-width: 0; }.ui-citation-builder-kind legend { margin-bottom: 4px; padding: 0; font-size: 13px; font-weight: 500; }.ui-citation-builder-kind label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }.ui-citation-builder-kind input { margin: 0; accent-color: var(--ui-accent); }.ui-citation-builder-kind input:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-citation-builder-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px 14px; min-width: 0; }.ui-citation-builder-field { display: grid; align-content: start; gap: 4px; min-width: 0; }.ui-citation-builder-wide { grid-column: 1 / -1; }.ui-citation-builder-field label { font-size: 13px; font-weight: 500; }.ui-citation-builder-field small { color: var(--ui-muted); font-size: 12px; }.ui-citation-builder-field input, .ui-citation-builder-field textarea { box-sizing: border-box; display: block; width: 100%; min-width: 0; padding: 7px 10px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); resize: vertical; }.ui-citation-builder-field input:hover, .ui-citation-builder-field textarea:hover { border-color: var(--ui-strong); }.ui-citation-builder-field input:focus, .ui-citation-builder-field textarea:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-citation-builder-need input, .ui-citation-builder-need textarea { border-color: var(--ui-warning); }.ui-citation-builder-read { margin: 0; color: var(--ui-muted); font-size: 13px; overflow-wrap: anywhere; }.ui-citation-builder-said { margin: 0; font-weight: 600; }.ui-citation-builder-notes { display: grid; gap: 4px; margin: 0; padding: 8px 12px 8px 28px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft); color: var(--ui-warning); font-size: 13px; }.ui-citation-builder-out { display: grid; gap: 10px; min-width: 0; }.ui-citation-builder-style { display: grid; gap: 6px; min-width: 0; padding: 10px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); }.ui-citation-builder-style h4 { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0; font-size: 13px; font-weight: 600; }.ui-citation-builder-style p { margin: 0; overflow-wrap: anywhere; }.ui-citation-builder-style pre { margin: 0; overflow-x: auto; font: 12.5px/1.5 var(--ui-mono); white-space: pre-wrap; overflow-wrap: anywhere; }.ui-citation-builder-copy { height: 28px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg);  font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-citation-builder-copy:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-citation-builder-copy:focus-visible { outline: none; box-shadow: var(--ui-focus); }`let citation_builder_kinds = [["book", "Book"], ["article", "Journal article"], ["web", "Web page"]]// Each kind's boxes, in order: [field, label, needed by every style, wide].let citation_builder_boxes = {    book: [["authors", "Authors", true, true], ["title", "Title", true, true], ["year", "Year", true, false], ["publisher", "Publisher", true, false], ["place", "City", false, false],           ["edition", "Edition", false, false], ["doi", "DOI", false, false], ["url", "Address", false, false]],    article: [["authors", "Authors", true, true], ["title", "Title", true, true], ["journal", "Journal", true, true], ["year", "Year", true, false], ["volume", "Volume", true, false],              ["issue", "Issue", false, false], ["pages", "Pages", true, false], ["doi", "DOI", false, false], ["url", "Address", false, false]],    web: [["authors", "Authors", false, true], ["title", "Title", true, true], ["site", "Site", true, false], ["date", "Date published", false, false], ["url", "Address", true, true],          ["accessed", "Date accessed", false, false]],}let citation_builder_org_words = "academy agency alliance association authority bank board bureau center centre college commission committee company corp corporation council department foundation government group inc institute institution league limited llc ltd ministry museum network office organisation organization press society union university".split(" ")let citation_builder_particles = ["van", "von", "der", "den", "de", "del", "della", "di", "da", "dos", "du", "la", "le", "bin", "ibn", "al", "ter", "ten"]let citation_builder_suffixes = ["jr", "sr", "ii", "iii", "iv"]let citation_builder_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]let citation_builder_mla_months = ["Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."]fn citation_builder_is_org(name: str) -> bool {    let words = re.split(lower(name), "[^a-z]+")    any(words, (w) => contains(citation_builder_org_words, w))}fn citation_builder_is_suffix(s: str) -> bool = contains(citation_builder_suffixes, re.replace(lower(s), "\\.", ""))// "Anna van der Berg" as a person: the small words before the last name go with it. A// family name of two capitalised words cannot be told this way: the "Le Guin, Ursula"// form says it, and the page shows how a name was read.fn citation_builder_natural(name: str, suffix: str = "") {    let words = re.split(trim(name), "\\s+")    let n = len(words)    if n == 1 { return { kind: "person", family: words[0], given: "", suffix } }    let from = n - 1    while from > 1 && contains(citation_builder_particles, words[from - 1]) { from -= 1 }    { kind: "person", family: join(drop(words, from), " "), given: join(take(words, from), " "), suffix }}fn citation_builder_word_count(s: str) -> int = len(re.split(trim(s), "\\s+"))// One stretch of names with no "and" or semicolon in it.fn citation_builder_names(chunk: str) -> list {    let t = trim(chunk)    if t == "" { return [] }    if citation_builder_is_org(t) { return [{ kind: "org", family: t, given: "", suffix: "" }] }    let parts = re.split(t, "\\s*,\\s*") | filter((p) => p != "")    let n = len(parts)    let tail = parts[n - 1]    if n == 1 { return [citation_builder_natural(t)] }    if n == 2 && citation_builder_is_suffix(tail) { return [citation_builder_natural(parts[0], tail)] }    if n == 3 && citation_builder_is_suffix(tail) { return [{ kind: "person", family: parts[0], given: parts[1], suffix: tail }] }    if n == 2 {        let first_is_name = citation_builder_word_count(parts[0]) >= 2 && !contains(citation_builder_particles, lower(re.split(parts[0], "\\s+")[0]))        if first_is_name && citation_builder_word_count(parts[1]) >= 2 { return parts | map((p) => citation_builder_natural(p)) }        return [{ kind: "person", family: parts[0], given: parts[1], suffix: "" }]    }    // "Smith, John, Doe, Jane" pairs up; "John Smith, Jane Doe, Al Poe" does not.    if n % 2 == 0 && all(parts, (p) => citation_builder_word_count(p) == 1) {        return range(0, floor(n / 2)) | map((i) => { kind: "person", family: parts[i * 2], given: parts[i * 2 + 1], suffix: "" })    }    parts | map((p) => citation_builder_natural(p))}// The authors in a box: a semicolon, a line, "and" or "&" parts them. Names in { } stay whole.fn citation_builder_authors(text: str) -> list {    let t = trim(text ?? "")    if t == "" { return [] }    let held = re.find_all(t, "\\{[^{}]*\\}|[^;\\n{}]+")    flatten(held | map((m) => {        if starts_with(m.text, "{") {            let name = trim(slice(m.text, 1, len(m.text) - 1))            return name == "" ? [] : [{ kind: "org", family: name, given: "", suffix: "" }]        }        if citation_builder_is_org(m.text) && !re.test(m.text, ",") { return citation_builder_names(m.text) }        flatten(re.split(m.text, ",?\\s+(?:and|&)\\s+") | map(citation_builder_names))    }))}// "John R. R." as "J. R. R.", "Jean-Paul" as "J.-P.".fn citation_builder_initials(given: str) -> str {    let words = re.split(trim(given), "[\\s.]+") | filter((w) => w != "")    words | map((w) => split(w, "-") | filter((p) => p != "") | map((p) => upper(split(p, "")[0]) + ".") | join("-")) | join(" ")}fn citation_builder_with_suffix(name: str, suffix: str) -> str = suffix == "" ? name : `${name}, ${suffix}`fn citation_builder_inverted(a) -> str = a.given == "" ? citation_builder_with_suffix(a.family, a.suffix) : citation_builder_with_suffix(`${a.family}, ${a.given}`, a.suffix)fn citation_builder_direct(a) -> str = citation_builder_with_suffix(a.given == "" ? a.family : `${a.given} ${a.family}`, a.suffix)fn citation_builder_apa_name(a) -> str {    if a.kind == "org" || a.given == "" { return citation_builder_with_suffix(a.family, a.suffix) }    citation_builder_with_suffix(`${a.family}, ${citation_builder_initials(a.given)}`, a.suffix)}// APA 7: all up to 20, "&" before the last; past 20, the first 19, dots and the last.fn citation_builder_apa_authors(authors: list) -> str {    let names = authors | map(citation_builder_apa_name)    let n = len(names)    if n == 1 { return names[0] }    if n > 20 { return join(take(names, 19), ", ") + ", . . . " + names[n - 1] }    join(take(names, n - 1), ", ") + ", & " + names[n - 1]}// MLA 9: one; two with "and"; three or more, the first and "et al."fn citation_builder_mla_authors(authors: list) -> str {    let n = len(authors)    if n == 1 { return citation_builder_inverted(authors[0]) }    if n == 2 { return `${citation_builder_inverted(authors[0])}, and ${citation_builder_direct(authors[1])}` }    `${citation_builder_inverted(authors[0])}, et al`}// Chicago 17: all up to ten, only the first inverted; past ten, the first seven and "et al."fn citation_builder_chicago_authors(authors: list) -> str {    let n = len(authors)    let lead = citation_builder_inverted(authors[0])    if n == 1 { return lead }    let rest = drop(authors, 1) | map(citation_builder_direct)    if n > 10 { return join([lead, ...take(rest, 6)], ", ") + ", et al" }    if n == 2 { return `${lead}, and ${rest[0]}` }    join([lead, ...take(rest, n - 2)], ", ") + `, and ${rest[n - 2]}`}// A sentence's end: a full stop, unless it has one, or a ? or !.fn citation_builder_stop(s: str) -> str = re.test(s, "[.?!]$") ? "" : "."fn citation_builder_stopped(s: str) -> str = s + citation_builder_stop(s)// A date as text: { year, month, day }, of ints, the last two null when not given; null if it is no date.fn citation_builder_date(text) {    let t = trim(text ?? "")    let m = re.find(t, "^([0-9]{4})(?:-([0-9]{1,2})(?:-([0-9]{1,2}))?)?$")    if m == null { return null }    let year = int(m.groups[0])    let month = m.groups[1] == null ? null : int(m.groups[1])    let day = m.groups[2] == null ? null : int(m.groups[2])    if month != null && (month < 1 || month > 12) { return null }    if day != null && !date.valid(`${m.groups[0]}-${slice(`0${month}`, -2)}-${slice(`0${day}`, -2)}`) { return null }    { year, month, day }}fn citation_builder_apa_date(d) -> str {    if d == null { return "n.d." }    if d.month == null { return str(d.year) }    let name = citation_builder_months[d.month - 1]    d.day == null ? `${d.year}, ${name}` : `${d.year}, ${name} ${d.day}`}fn citation_builder_mla_date(d) -> str {    if d.month == null { return str(d.year) }    let name = citation_builder_mla_months[d.month - 1]    d.day == null ? `${name} ${d.year}` : `${d.day} ${name} ${d.year}`}fn citation_builder_chicago_date(d) -> str {    if d.month == null { return str(d.year) }    let name = citation_builder_months[d.month - 1]    d.day == null ? `${name} ${d.year}` : `${name} ${d.day}, ${d.year}`}// "2", "2nd" and "2nd ed." all as "2nd".fn citation_builder_ordinal(edition: str) -> str {    let m = re.find(edition, "[0-9]+")    if m == null { return re.replace(trim(edition), "\\s+(?:ed\\.?|edition)$", "", "i") }    let n = int(m.text)    let tens = n % 100    let unit = n % 10    `${n}${tens >= 11 && tens <= 13 ? "th" : unit == 1 ? "st" : unit == 2 ? "nd" : unit == 3 ? "rd" : "th"}`}// "e123", "12 - 34", "12–34": the two ends with `dash` between.fn citation_builder_pages(pages: str, dash: str) -> str = re.replace(trim(pages), "\\s*(?:-+|–|—)\\s*", dash)// MLA's page ranges keep the last two figures of the second number, or more where needed: 112-34, 195-205.fn citation_builder_mla_pages(pages: str) -> str {    let m = re.find(trim(pages), "^([0-9]+)\\s*(?:-+|–|—)\\s*([0-9]+)$")    if m == null { return citation_builder_pages(pages, "-") }    let a = m.groups[0]    let b = m.groups[1]    if len(a) != len(b) || len(a) < 3 { return `${a}-${b}` }    let n = len(b)    let tail = slice(b, n - 2)    slice(a, 0, n - 2) == slice(b, 0, n - 2) ? `${a}-${tail}` : `${a}-${b}`}// A DOI, however it was pasted, as an address.fn citation_builder_doi(doi: str) -> str = re.replace(trim(doi), "^(?:https?://(?:dx\\.)?doi\\.org/|doi:\\s*)", "", "i")fn citation_builder_address(f) -> str {    let d = citation_builder_doi(f.doi ?? "")    d != "" ? `https://doi.org/${d}` : trim(f.url ?? "")}// Text the parts of a reference, without marks.// How an author was read, for the line under the field.fn citation_builder_shown_as(a) = a.kind == "org" ? `${a.family} (organisation)` : citation_builder_inverted(a)fn citation_builder_plain(parts: list) -> str = parts | map((p) => p.text) | join("")// Plain and italic pieces, joined by the styles below.fn citation_builder_t(text: str) = { text, italic: false }fn citation_builder_i(text: str) = { text, italic: true }fn citation_builder_apa(kind: str, f, authors: list) -> list {    let txt = citation_builder_t    let i = citation_builder_i    let title = trim(f.title ?? "")    let when = kind == "web" ? citation_builder_apa_date(citation_builder_date(f.date)) : (filled(trim(f.year ?? "")) ?? "n.d.")    let at = citation_builder_address(f)    let tail = at == "" ? [] : [txt(` ${at}`)]    let named = len(authors) > 0    let lead = named ? citation_builder_apa_authors(authors) : ""    let head = named ? [txt(`${citation_builder_stopped(lead)} (${when}). `)] : []    let dated = named ? [] : [txt(` (${when}).`)]    let stop = (s: str) => named ? citation_builder_stop(s) : ""    match kind {        "article" => {            let vol = trim(f.volume ?? "")            let iss = trim(f.issue ?? "")            let pages = citation_builder_pages(f.pages ?? "", "–")            let journal = trim(f.journal ?? "")            let source = [...(journal != "" ? [i(journal)] : []), ...(vol != "" ? [txt(journal != "" ? ", " : ""), i(vol)] : []), ...(iss != "" ? [txt(`(${iss})`)] : []),                          ...(pages != "" ? [txt(`, ${pages}`)] : []), txt(journal != "" || vol != "" ? "." : "")]            head + [txt(title + (named ? citation_builder_stop(title) : ""))] + dated + [txt(" ")] + source + tail        }        "web" => {            let site = trim(f.site ?? "")            let same = len(authors) == 1 && lower(authors[0].family) == lower(site)            head + [i(title), txt(stop(title))] + dated + (site != "" && !same ? [txt(` ${citation_builder_stopped(site)}`)] : []) + tail        }        _ => {            let edition = trim(f.edition ?? "")            let ed = edition == "" ? "" : ` (${citation_builder_ordinal(edition)} ed.)`            let publisher = trim(f.publisher ?? "")            head + [i(title), txt(ed == "" ? stop(title) : `${ed}${named ? "." : ""}`)] + dated + (publisher != "" ? [txt(` ${citation_builder_stopped(publisher)}`)] : []) + tail        }    }}// MLA leaves the scheme off an address; a DOI keeps its own.fn citation_builder_mla_address(f) -> str {    let d = citation_builder_doi(f.doi ?? "")    d != "" ? `https://doi.org/${d}` : re.replace(trim(f.url ?? ""), "^https?://", "", "i")}fn citation_builder_mla(kind: str, f, authors: list) -> list {    let txt = citation_builder_t    let i = citation_builder_i    let title = trim(f.title ?? "")    let lead = len(authors) > 0 ? [txt(`${citation_builder_stopped(citation_builder_mla_authors(authors))} `)] : []    let quoted = title == "" ? "" : `“${title}${citation_builder_stop(title)}”`    let at = citation_builder_mla_address(f)    let year = trim(f.year ?? "")    match kind {        "article" => {            let bits = [filled(trim(f.volume ?? "")) == null ? null : `vol. ${trim(f.volume)}`, filled(trim(f.issue ?? "")) == null ? null : `no. ${trim(f.issue)}`, filled(year),                        filled(trim(f.pages ?? "")) == null ? null : `${contains(citation_builder_pages(f.pages, "-"), "-") ? "pp." : "p."} ${citation_builder_mla_pages(f.pages)}`, filled(at)] | filter((b) => b != null)            let journal = trim(f.journal ?? "")            lead + [txt(`${quoted} `)] + (journal != "" ? [i(journal), txt(len(bits) > 0 ? ", " : ".")] : []) + (len(bits) > 0 ? [txt(citation_builder_stopped(join(bits, ", ")))] : [])        }        "web" => {            let site = trim(f.site ?? "")            let d = citation_builder_date(f.date)            let seen = citation_builder_date(f.accessed)            let bits = [d == null ? null : citation_builder_mla_date(d), filled(at)] | filter((b) => b != null)            lead + [txt(`${quoted} `)] + (site != "" ? [i(site), txt(len(bits) > 0 ? ", " : ".")] : []) + (len(bits) > 0 ? [txt(citation_builder_stopped(join(bits, ", ")))] : [])                + (d == null && seen != null ? [txt(` Accessed ${citation_builder_mla_date(seen)}.`)] : [])        }        _ => {            let edition = trim(f.edition ?? "")            let bits = [edition == "" ? null : `${citation_builder_ordinal(edition)} ed.`, filled(trim(f.publisher ?? "")), filled(year)] | filter((b) => b != null)            lead + [i(title), txt(citation_builder_stop(title))] + (len(bits) > 0 ? [txt(` ${citation_builder_stopped(join(bits, ", "))}`)] : []) + (at != "" ? [txt(` ${at}${citation_builder_stop(at)}`)] : [])        }    }}fn citation_builder_chicago(kind: str, f, authors: list) -> list {    let txt = citation_builder_t    let i = citation_builder_i    let title = trim(f.title ?? "")    let lead = len(authors) > 0 ? [txt(`${citation_builder_stopped(citation_builder_chicago_authors(authors))} `)] : []    let quoted = title == "" ? "" : `“${title}${citation_builder_stop(title)}”`    let at = citation_builder_address(f)    let tail = at == "" ? [] : [txt(` ${citation_builder_stopped(at)}`)]    let year = trim(f.year ?? "")    match kind {        "article" => {            let vol = trim(f.volume ?? "")            let iss = trim(f.issue ?? "")            let pages = citation_builder_pages(f.pages ?? "", "–")            let journal = trim(f.journal ?? "")            let rest = `${vol != "" ? ` ${vol}` : ""}${iss != "" ? `${vol != "" ? "," : ""} no. ${iss}` : ""}${year != "" ? ` (${year})` : ""}${pages != "" ? `: ${pages}` : ""}.`            lead + [txt(`${quoted} `)] + (journal != "" ? [i(journal), txt(rest)] : [txt(trim(rest))]) + tail        }        "web" => {            let site = trim(f.site ?? "")            let d = citation_builder_date(f.date)            let seen = citation_builder_date(f.accessed)            let when = d != null ? ` ${citation_builder_chicago_date(d)}.` : seen != null ? ` Accessed ${citation_builder_chicago_date(seen)}.` : ""            lead + [txt(quoted)] + (site != "" ? [txt(` ${citation_builder_stopped(site)}`)] : []) + [txt(when)] + tail        }        _ => {            let edition = trim(f.edition ?? "")            let place = trim(f.place ?? "")            let publisher = trim(f.publisher ?? "")            let press = place != "" && publisher != "" ? `${place}: ${publisher}` : filled(place) ?? publisher            let bits = [filled(press), filled(year)] | filter((b) => b != null)            lead + [i(title), txt(citation_builder_stop(title))] + (edition != "" ? [txt(` ${citation_builder_ordinal(edition)} ed.`)] : []) + (len(bits) > 0 ? [txt(` ${citation_builder_stopped(join(bits, ", "))}`)] : []) + tail        }    }}// A value for BibTeX, whose special characters would otherwise end or break the entry.fn citation_builder_tex(s) -> str =    re.replace(trim(s ?? ""), "[\\\\{}&%$#_^~]", (m) => {        if m.text == "\\" { "\\textbackslash{}" } else if m.text == "^" { "\\textasciicircum{}" } else if m.text == "~" { "\\textasciitilde{}" } else { `\\${m.text}` }    })// The key: the first author's family name, the year and the title's first long word, in plain letters.fn citation_builder_key(authors: list, year: str, title: str) -> str {    let family = len(authors) > 0 ? lower(authors[0].family) : "anon"    let word = first(re.find_all(lower(title), "[a-z0-9]{4,}") | map((m) => m.text)) ?? first(re.find_all(lower(title), "[a-z0-9]+") | map((m) => m.text)) ?? ""    let plain = re.replace(family, "[^a-z0-9]", "")    `${plain == "" ? "anon" : plain}${year}${word}`}fn citation_builder_bibtex_name(a) -> str {    if a.kind == "org" { return `{${citation_builder_tex(a.family)}}` }    if a.given == "" { return citation_builder_tex(a.family) }    citation_builder_tex(a.suffix == "" ? `${a.family}, ${a.given}` : `${a.family}, ${a.suffix}, ${a.given}`)}fn citation_builder_bibtex(kind: str, f, authors: list) -> str {    let published = citation_builder_date(f.date)    let year = kind == "web" ? (published == null ? "" : str(published.year)) : trim(f.year ?? "")    let names = authors | map(citation_builder_bibtex_name) | join(" and ")    let doi = citation_builder_doi(f.doi ?? "")    let url = trim(f.url ?? "")    let seen = citation_builder_date(f.accessed)    let shared = [["author", names], ["title", citation_builder_tex(f.title)]]    let own = match kind {        "article" => [["journal", citation_builder_tex(f.journal)], ["year", year], ["volume", citation_builder_tex(f.volume)], ["number", citation_builder_tex(f.issue)],                      ["pages", citation_builder_pages(f.pages ?? "", "--")], ["doi", doi], ["url", doi == "" ? url : ""]]        "web" => [["howpublished", url == "" ? "" : `\\url{${url}}`], ["year", year], ["note", citation_builder_tex(f.site) + (seen == null ? "" : `${trim(f.site ?? "") == "" ? "" : ". "}Accessed ${trim(f.accessed)}`)]]        _ => [["publisher", citation_builder_tex(f.publisher)], ["address", citation_builder_tex(f.place)], ["year", year], ["edition", trim(f.edition ?? "") == "" ? "" : citation_builder_ordinal(f.edition)], ["doi", doi], ["url", doi == "" ? url : ""]]    }    let entries = (shared + own) | filter((e) => e[1] != "") | map((e) => `  ${e[0]} = {${e[1]}}`)    let entry = match kind { "article" => "article"; "web" => "misc"; _ => "book" }    `@${entry}{${citation_builder_key(authors, year, f.title ?? "")},\n${join(entries, ",\n")}\n}`}// The boxes a kind needs that are empty, by label.fn citation_builder_missing(kind: str, f) -> list =    get(citation_builder_boxes, kind, citation_builder_boxes.book) | filter((b) => b[2] && trim(get(f, b[0], "") ?? "") == "") | map((b) => b[1])// Whether a title is in Title Case: most of its longer words start with a capital.fn citation_builder_title_cased(title: str) -> bool {    let words = re.find_all(title, "[A-Za-z]{4,}") | map((m) => m.text)    let tail = drop(words, 1)    len(tail) >= 2 && count(tail, (w) => re.test(w, "^[A-Z][a-z]")) * 10 >= len(tail) * 8}// What looks wrong, in sentences.fn citation_builder_warnings(kind: str, f, today: str?) -> list {    let year = trim(f.year ?? "")    let doi = citation_builder_doi(f.doi ?? "")    let url = trim(f.url ?? "")    let now_year = today == null ? null : int(slice(today, 0, 4))    let shown = (key: str) => trim(get(f, key, "") ?? "") != ""    let on = (key: str) => any(get(citation_builder_boxes, kind, []), (b) => b[0] == key)    let dated = (key: str, label: str) => {        let text = trim(get(f, key, "") ?? "")        if !on(key) || text == "" { return null }        if citation_builder_date(text) == null { return `${label} is not a date: write it as 2026, 2026-03 or 2026-03-05.` }        if today != null && len(text) == 10 && text > today { return `${label} is after today.` }        null    }    let notes = [        on("year") && year != "" && !re.test(year, "^[0-9]{3,4}[a-z]?$") ? "Year is not a year: write it as 2026." : null,        on("year") && now_year != null && re.test(year, "^[0-9]{4}$") && int(year) > now_year ? "Year is after this year." : null,        dated("date", "Date published"),        dated("accessed", "Date accessed"),        on("doi") && doi != "" && !re.test(doi, "^10\\.[0-9]{4,9}/\\S+$") ? "DOI does not look like one: it starts 10. and has a slash, such as 10.1000/xyz123." : null,        on("url") && url != "" && !re.test(url, "^https?://\\S+$", "i") ? "Address does not start with http:// or https://." : null,        on("pages") && shown("pages") && !re.test(trim(f.pages), "^[A-Za-z]?[0-9]+(?:\\s*(?:-+|–|—)\\s*[A-Za-z]?[0-9]+)?(?:\\s*,\\s*[A-Za-z]?[0-9]+(?:\\s*(?:-+|–|—)\\s*[A-Za-z]?[0-9]+)?)*$") ? "Pages are not a range: write them as 12-34." : null,        citation_builder_title_cased(f.title ?? "") ? "APA capitalises only a title's first word and proper nouns: change the APA line by hand if this title has more." : null,    ]    notes | filter((n) => n != null)}// The whole thing (see the top of the file).fn citation_builder_of(kind: str, fields, today: str? = null) {    let k = contains(["book", "article", "web"], kind) ? kind : "book"    let authors = citation_builder_authors(fields.authors ?? "")    let apa = citation_builder_apa(k, fields, authors)    let mla = citation_builder_mla(k, fields, authors)    let chicago = citation_builder_chicago(k, fields, authors)    { apa, mla, chicago, bibtex: citation_builder_bibtex(k, fields, authors), apa_text: citation_builder_plain(apa), mla_text: citation_builder_plain(mla), chicago_text: citation_builder_plain(chicago),      authors, missing: citation_builder_missing(k, fields), warnings: citation_builder_warnings(k, fields, today) }}component CitationBuilder(kind: str = "book", fields = null, today: str? = null, name: str? = null, id: str = "citation-builder", on_change = null, class: str? = null) {    style global citation_builder_css    state on = contains(["book", "article", "web"], kind) ? kind : "book"    state f = { authors: "", title: "", year: "", edition: "", publisher: "", place: "", journal: "", volume: "", issue: "", pages: "", doi: "", site: "", date: "", url: "",                accessed: today ?? "", ...(fields ?? {}) }    let named = (n: str) => name != null ? `${name}_${n}` : n    let c = citation_builder_of(on, f, today)    let blank = trim(f.title) == "" && trim(f.authors) == ""    let said = blank ? "Fill in the fields to build the citation."        : len(c.missing) == 0 ? "Citation ready." : `Citation built. Still needed: ${join(c.missing, ", ")}.`    let tell = (at_kind: str, at_f) => { on_change && on_change(citation_builder_of(at_kind, at_f, today)) }    let put = (key: str, v: str) => {        f = set(f, key, v)        tell(on, f)    }    let line = (parts: list) => <p>{for [i, p] in enumerate(parts) => p.italic ? <i key={i}>{p.text}</i> : p.text}</p>    <section class={ui_cls(["ui-citation-builder", class])} aria-labelledby={`${id}-said`}>        <fieldset.ui-citation-builder-kind>            <legend>Source</legend>            {for k in citation_builder_kinds => <label key={k[0]}>                <input type="radio" name={named("kind")} value={k[0]} checked={on == k[0]} @change={(e) => { on = k[0]; tell(k[0], f) }}/> {k[1]}            </label>}        </fieldset>        <div.ui-citation-builder-grid>            {for b in get(citation_builder_boxes, on, []) => <div key={b[0]} class={ui_cls(["ui-citation-builder-field", b[3] && "ui-citation-builder-wide", b[2] && trim(get(f, b[0], "")) == "" && !blank && "ui-citation-builder-need"])}>                <label for={`${id}-${b[0]}`}>{b[1]}</label>                {b[0] == "authors"                    ? <textarea id={`${id}-authors`} name={named("authors")} rows="2" value={f.authors} aria-describedby={`${id}-authors-hint`} @input={(e) => { put("authors", e.value) }}></textarea>                    : <input type="text" id={`${id}-${b[0]}`} name={named(b[0])} value={get(f, b[0], "")} autocomplete="off" @input={(e) => { put(b[0], e.value) }}/>}                {b[0] == "authors" && <small id={`${id}-authors-hint`}>One a line, or with semicolons: Last, First. Put an organisation in {"{ }"}.</small>}                {b[0] == "authors" && len(c.authors) > 0 && <p.ui-citation-builder-read>Read as: {join(c.authors | map(citation_builder_shown_as), "; ")}</p>}            </div>}        </div>        <p.ui-citation-builder-said id={`${id}-said`} aria-live="polite">{said}</p>        {len(c.warnings) > 0 && <ul.ui-citation-builder-notes>{for w in c.warnings => <li key={w}>{w}</li>}</ul>}        {!blank && <div.ui-citation-builder-out>            <div.ui-citation-builder-style id={`${id}-apa`}><h4>APA 7 <CopyButton text={c.apa_text} label="Copy APA" size="sm" class="ui-citation-builder-copy"/></h4>{line(c.apa)}</div>            <div.ui-citation-builder-style id={`${id}-mla`}><h4>MLA 9 <CopyButton text={c.mla_text} label="Copy MLA" size="sm" class="ui-citation-builder-copy"/></h4>{line(c.mla)}</div>            <div.ui-citation-builder-style id={`${id}-chicago`}><h4>Chicago 17 <CopyButton text={c.chicago_text} label="Copy Chicago" size="sm" class="ui-citation-builder-copy"/></h4>{line(c.chicago)}</div>            <div.ui-citation-builder-style id={`${id}-bibtex`}><h4>BibTeX <CopyButton text={c.bibtex} label="Copy BibTeX" size="sm" class="ui-citation-builder-copy"/></h4><pre>{c.bibtex}</pre></div>        </div>}    </section>}
Thread
35Useful? Like it, or add to its thread below.

Thread 0 posts

On CitationBuilder: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.