Module:Citation/CS1

From Roovet Articles
Jump to navigation Jump to search

<section begin=header />

Lua error in Module:TNT at line 159: Missing JsonConfig extension; Cannot load https://commons.wikimedia.org/wiki/Data:I18n/Uses TemplateStyles.tab.

<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:

CS1 | CS2 modules
live sandbox diff description
Gold padlock Module:Citation/CS1 Module:Citation/CS1/sandbox [edit] diff Rendering and support functions
Module:Citation/CS1/Configuration Module:Citation/CS1/Configuration/sandbox [edit] diff Translation tables; error and identifier handlers
Module:Citation/CS1/Whitelist Module:Citation/CS1/Whitelist/sandbox [edit] diff List of active and deprecated CS1|2 parameters
Module:Citation/CS1/Date validation Module:Citation/CS1/Date validation/sandbox [edit] diff Date format validation functions
Module:Citation/CS1/Identifiers Module:Citation/CS1/Identifiers/sandbox [edit] diff Functions that support the named identifiers (ISBN, DOI, PMID, etc.)
Module:Citation/CS1/Utilities Module:Citation/CS1/Utilities/sandbox [edit] diff Common functions and tables
Module:Citation/CS1/COinS Module:Citation/CS1/COinS/sandbox [edit] diff Functions that render a CS1|2 template's metadata
Module:Citation/CS1/styles.css Module:Citation/CS1/sandbox/styles.css [edit] diff CSS styles applied to the CS1|2 templates
Silver padlock Module:Citation/CS1/Suggestions Module:Citation/CS1/Suggestions/sandbox [edit] diff List that maps common erroneous parameter names to valid parameter names

<section end=module_components_table />

Other documentation:

testcases


-- 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