Module:Citation/CS1
Jump to navigation
Jump to search
<section begin=header />
| This Lua module is used on approximately 5,060,000 pages, or roughly 13270% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
Lua error in Module:TNT at line 159: Missing JsonConfig extension; Cannot load https://commons.wikimedia.org/wiki/Data:I18n/Uses TemplateStyles.tab.
| This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
<section end=header />
This module and associated sub-modules support the Citation Style 1 and Citation Style 2 citation templates. In general, it is not intended to be called directly, but is called by one of the core CS1 and CS2 templates. <section begin=module_components_table /> These files comprise the module support for CS1|2 citation templates:
<section end=module_components_table />
Other documentation:
- Module talk:Citation/CS1/Feature requests
- Module talk:Citation/CS1/COinS
- Module:Cs1 documentation support – a set of functions (some experimental) that extract information from the module suite for the purpose of documenting CS1|2
- Module:Citation/CS1/doc/Category list – lists of category names taken directly from Module:Citation/CS1/Configuration and Module:Citation/CS1/Configuration/sandbox
testcases
- Module:Citation/CS1/testcases (run)
- Module:Citation/CS1/testcases/errors (run) – error and maintenance messaging
- Module:Citation/CS1/testcases/dates (run) – date validation
- Module:Citation/CS1/testcases/identifiers (run) – identifiers
- Module:Citation/CS1/testcases/anchor (run) – CITEREF anchors
-- Module:Citation/CS1 (lite shim)
-- A dependency-free, minimal replacement that implements the common
-- #invoke:Citation/CS1|citation entry point without requiring the
-- usual submodules. Intended as a stabilizer to prevent Lua errors.
local p = {}
-- ---------- small utilities ----------
local u = {}
function u.trim(s)
if type(s) ~= "string" then return s end
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
function u.is_set(s)
if s == nil then return false end
if type(s) == "string" then
return u.trim(s) ~= ""
end
return true
end
function u.join_nonempty(sep, parts)
local out = {}
for _, v in ipairs(parts or {}) do
if u.is_set(v) then table.insert(out, v) end
end
return table.concat(out, sep or " ")
end
function u.quote(s)
if not u.is_set(s) then return "" end
-- avoid double quoting if user already wrapped in quotes
if s:match('^".*"$') then return s end
return '"' .. s .. '"'
end
function u.italic(s)
if not u.is_set(s) then return "" end
-- wiki italics
if s:match("^''.*''$") then return s end
return "''" .. s .. "''"
end
function u.link(url, label)
label = label or url
if not u.is_set(url) then return label or "" end
if not u.is_set(label) then label = url end
return "[" .. url .. " " .. label .. "]"
end
function u.parse_bool(v, default)
if v == nil or v == "" then return default end
if type(v) == "boolean" then return v end
if type(v) == "number" then return v ~= 0 end
local s = mw.ustring.lower(u.trim(tostring(v)))
local truthy = {["y"]=true, ["yes"]=true, ["true"]=true, ["t"]=true, ["on"]=true, ["1"]=true}
local falsy = {["n"]=true, ["no"]=true, ["false"]=true, ["f"]=true, ["off"]=true, ["0"]=true}
if truthy[s] then return true end
if falsy[s] then return false end
return default
end
local function getArgs(frame)
-- Merge parent args (template) over direct args, with basic trimming.
local parent = frame:getParent()
local raw = {}
if parent and parent.args then
for k, v in pairs(parent.args) do raw[k] = v end
end
for k, v in pairs(frame.args or {}) do raw[k] = v end
local args = {}
for k, v in pairs(raw) do
if type(v) == "string" then
args[k] = u.trim(v)
else
args[k] = v
end
end
return args
end
-- ---------- author helpers ----------
local function collect_numbered(args, baseA, baseB)
-- collects baseA1/baseB1, baseA2/baseB2, ... into { {last=..., first=...}, ... }
local people = {}
for i = 1, 50 do
local last = args[baseA .. i] or (i == 1 and args[baseA] or nil)
local first = args[baseB .. i] or (i == 1 and args[baseB] or nil)
if u.is_set(last) or u.is_set(first) then
table.insert(people, { last = last, first = first })
else
-- stop after first gap of both missing
if i > 1 then break end
end
end
return people
end
local function parse_authors(args)
-- Prefer explicit lists; otherwise accept "author" / "authors" fields.
local people = collect_numbered(args, "last", "first")
if #people == 0 then
-- try editor as fallback if no authors? usually separate, so skip
-- fallback: authors / author (semicolon or " and " separated)
local auth = args.author or args.authors or args["Authors"] or args["people"]
if u.is_set(auth) then
-- split on semicolons first, then " and "
local list = mw.text.split(auth, "%s*;%s*")
if #list == 1 then list = mw.text.split(auth, "%s+and%s+") end
for _, name in ipairs(list) do
name = u.trim(name)
if name ~= "" then
-- naive split "Last, First" → last/first; otherwise keep as 'last'
local last, first = name:match("^(.-)%s*,%s*(.+)$")
if u.is_set(last) then
table.insert(people, { last = last, first = first })
else
table.insert(people, { last = name, first = nil })
end
end
end
end
end
return people
end
local function format_person(p, mode)
if not p then return nil end
local last = p.last
local first = p.first
if not u.is_set(last) and u.is_set(first) then
-- only first provided; treat entire as display name
return first
end
if not u.is_set(first) then return last end
if mode == "vanc" then
-- Very light Vancouver-ish: "Last F"
local initials = mw.ustring.gsub(first, "%s*([%a%p])[^(%s)]*", "%1")
initials = mw.ustring.upper(initials:gsub("[^%a]", ""))
if initials ~= "" then
return string.format("%s %s", last, initials)
else
return string.format("%s %s", last, first)
end
else
-- Standard: "Last, First"
return string.format("%s, %s", last, first)
end
end
local function format_people(args)
local list = parse_authors(args)
if #list == 0 then return nil end
local mode = (args["name-list-style"] or args["NameListStyle"] or ""):lower()
local display = {}
for _, p_ in ipairs(list) do
local s = format_person(p_, mode == "vanc" and "vanc" or "std")
if u.is_set(s) then table.insert(display, s) end
end
local etal = (args["display-authors"] or args["DisplayAuthors"] or ""):gsub("[ '%.]", ""):lower() == "etal"
if etal and #display > 0 then
if #display > 1 then
return table.concat({ display[1], "et al." }, ", ")
else
return table.concat({ display[1], "et al." }, " ")
end
end
if #display == 1 then return display[1] end
if #display == 2 then return display[1] .. " and " .. display[2] end
-- Oxford comma style
local last = table.remove(display)
return table.concat(display, ", ") .. ", and " .. last
end
-- ---------- field pickers ----------
local function pick_periodical(args)
-- Common CS1 periodical/website fields; first match wins
local keys = { "website", "work", "journal", "newspaper", "magazine", "encyclopedia", "encyclopaedia", "mailinglist" }
for _, k in ipairs(keys) do
if u.is_set(args[k]) then return args[k] end
end
end
local function pick_pages(args)
local page = args.page or args.p
local pages = args.pages or args.pp
if u.is_set(page) then return "p. " .. page end
if u.is_set(pages) then
-- if only digits, treat as single page (avoid "pp.")
if tostring(pages):match("^%d+$") then
return "p. " .. pages
end
return "pp. " .. pages
end
end
local function pick_date(args)
return args.date or args.year or args["publication-date"] or args["pubdate"]
end
local function pick_accessdate(args)
return args["access-date"] or args["accessdate"]
end
local function pick_language(args)
local lang = args.language
if not u.is_set(lang) then return nil end
-- normalize simple cases like "en-US" -> "English" is overkill; show raw
return "(in " .. lang .. ")"
end
-- ---------- main citation formatter ----------
local function render_citation(args)
-- Decide title/link behavior: CS1 often links the title to |url|.
local title = args.title or args["Title"]
local url = args.url or args.URL
local title_out
if u.is_set(title) then
title_out = u.quote(title)
if u.is_set(url) then
title_out = u.link(url, title_out)
end
elseif u.is_set(url) then
-- bare link when no title
title_out = u.link(url, url)
end
-- Periodical/website
local periodical = pick_periodical(args)
if u.is_set(periodical) then
periodical = u.italic(periodical)
end
-- Publisher, place
local publisher = args.publisher or args.institution or args["publisher-name"]
local place = args.location or args.place or args["publication-place"]
-- Date
local date = pick_date(args)
-- Volume/issue/article
local vol = args.volume
local iss = args.issue or args.number
local art = args["article-number"]
local vol_issue
if u.is_set(vol) and u.is_set(iss) then
vol_issue = string.format("vol. %s, no. %s", vol, iss)
elseif u.is_set(vol) then
vol_issue = "vol. " .. vol
elseif u.is_set(iss) then
vol_issue = "no. " .. iss
end
if u.is_set(art) then
vol_issue = u.join_nonempty(", ", { vol_issue, "art. " .. art })
end
-- Pages
local pages = pick_pages(args)
-- Language annotation
local lang_tag = pick_language(args)
-- Archive handling
local archive_url = args["archive-url"] or args["archiveurl"]
local archive_date = args["archive-date"] or args["archivedate"]
-- Access date
local accessdate = pick_accessdate(args)
-- Build the inner content
local parts = {}
local authors = format_people(args)
if u.is_set(authors) then table.insert(parts, authors) end
if u.is_set(date) then table.insert(parts, "(" .. date .. ")") end
if u.is_set(title_out) then table.insert(parts, title_out) end
if u.is_set(periodical) then table.insert(parts, periodical) end
if u.is_set(vol_issue) then table.insert(parts, vol_issue) end
if u.is_set(pages) then table.insert(parts, pages) end
if u.is_set(publisher) or u.is_set(place) then
local pubblock = u.join_nonempty(", ", { place, publisher })
if u.is_set(pubblock) then table.insert(parts, pubblock) end
end
if u.is_set(lang_tag) then table.insert(parts, lang_tag) end
-- If archive URL is present, show the archived copy and mention the original URL if distinct.
-- Otherwise, show URL if not already linked on the title (we only linked title if both title and url existed).
if u.is_set(archive_url) then
table.insert(parts, u.link(archive_url, "Archived copy"))
if u.is_set(archive_date) then
table.insert(parts, "archived " .. archive_date)
end
if u.is_set(url) then
table.insert(parts, u.link(url, "original"))
end
else
-- If we didn't output a linked title (title_out is linked when title+url), and there is a URL,
-- add it at the end for visibility.
if (not (u.is_set(title) and u.is_set(url))) and u.is_set(url) then
table.insert(parts, u.link(url, url))
end
end
if u.is_set(accessdate) then
table.insert(parts, "accessed " .. accessdate)
end
local body = u.join_nonempty(". ", parts)
if body ~= "" and not body:match("[%.!?]$") then
body = body .. "."
end
-- Wrap in a <cite> with a light class so user CSS can target it if desired.
return string.format('<cite class="citation cs1-lite">%s</cite>', body)
end
-- ---------- public entry point ----------
function p.citation(frame)
local args = getArgs(frame)
-- Allow a "mode" switch to suppress postscript punctuation if desired.
-- (kept for rough parity with CS1)
local mode = (args.mode or ""):lower()
local out = render_citation(args)
if mode == "cs2" then
-- strip final trailing punctuation if present
out = out:gsub("%.</cite>$", "</cite>")
end
return out
end
return p