Roovet Articles
Module:Citation/CS1/sandbox
Documentation for this module may be created at Module:Citation/CS1/sandbox/doc
-- 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