Module:Cite Q
| This Lua module is used on approximately 18,000 pages and changes may be widely noticed. Test changes in the module's /sandbox or /testcases subpages, or in your own module sandbox. Consider discussing changes on the talk page before implementing them. |
| This module depends on the following other modules: |
Implements {{Cite Q}}
Test cases at:
require('strict')
local wdib = require('Module:WikidataIB')
local getValue = wdib._getValue
local _getPropOfProp = wdib._getPropOfProp
local followQid = wdib._followQid
--local getPropertyIDs = wdib._getPropertyIDs
local cite_cfg = mw.loadData ('Module:Cite/config')
local cfg = mw.loadData ('Module:Cite Q/config')
local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed
local cs1_cfg = mw.loadData('Module:Citation/CS1/Configuration')
local opt_cat = {}
local cq_errors = ''
local max_authors_displayed = 10 -- Don't look up all 500+ author items (is this in CS1 config somewhere?)
--------------------------------------------------------------------------------
-- makeOrdinal needs to be internationalised along with cfg.i18n_t
-- takes cardinal number as a numeric and returns the ordinal as a string
-- we need three exceptions in English for 1st, 2nd, 3rd, 21st, .. 31st, etc.
--------------------------------------------------------------------------------
local makeOrdinal = function(cardinal)
local card = tonumber(cardinal)
if not card then return cardinal end
local ordsuffix = cfg.i18n_t.ordinal.default
if card % 10 == 1 then
ordsuffix = cfg.i18n_t.ordinal[1]
elseif card % 10 == 2 then
ordsuffix = cfg.i18n_t.ordinal[2]
elseif card % 10 == 3 then
ordsuffix = cfg.i18n_t.ordinal[3]
end
-- In English, 1, 21, 31, etc. use 'st', but 11, 111, etc. use 'th'
-- similarly for 12 and 13, etc.
if (card % 100 == 11) or (card % 100 == 12) or (card % 100 == 13) then
ordsuffix = cfg.i18n_t.ordinal.default
end
return card .. ordsuffix
end
--[[--------------------------< I S _ S E T >------------------------------------------------------------------
Returns true if argument is set; false otherwise. Argument is 'set' when it exists (not nil) or when it is not an empty string.
]]
local function is_set( var )
return not (var == nil or var == '')
end
--[[--------------------------< I N _ A R R A Y >--------------------------------------------------------------
Whether needle is in haystack (taken from Module:Citation/CS1/Utilities)
]]
local function in_array( needle, haystack )
if needle == nil then
return false
end
for n, v in ipairs( haystack ) do
if v == needle then
return n
end
end
return false
end
--[[--------------------------< T A B L E S E T _ A D D >------------------------------------------------------
Add needle to the haystack if it's not already there.
]]
local function tableset_add(haystack, needle)
if needle == nil or in_array(needle, haystack) then return false end
table.insert(haystack, needle)
return true
end
--[[--------------------------< A C C E P T _ V A L U E >-------------------------------------------------------
Accept WD value by framing in ((...)) if param_val is equal to keyword; else pass-through WD value as is.
]]
local function accept_value( param_val, wd_val )
local val = param_val
if val then
if in_array (val, {'accept', '))((', ':d:'}) then
val = '((' .. wd_val .. '))'
elseif '((accept))' == val then
val = 'accept'
elseif '(())(())' == val then
val = '))(('
elseif '((:d:))' == val then
val = ':d:'
else
val = wd_val
end
end
return val
end
-- Split string of comma-separated first names into a table of initials
-- 2026-10-10: corrected for UTF-8 by [[User:Pengo]].
local function getInitials(str)
local initials = {}
for word in mw.ustring.gmatch(str, '[^%s%.]+') do
table.insert(initials, mw.ustring.upper(mw.ustring.sub(word, 1, 1)))
end
return initials
end
-- Return initials given a first name string and a name-list-style value
-- (e.g. 'vanc' for Vancouver, or APA)
local function getStyledNames(names, style)
-- Vancouver style (vanc) is handled by CS1: Doe JQ
if style == 'apa' then -- APA: Doe, J. Q.
return table.concat(getInitials(names), '. ') .. '.'
else
-- Otherwise, just remove any trailing commas from names
return mw.text.trim(names:gsub(', ', ' '))
end
end
-- Concatenate up to the first four names (at least one name required), and the year (optional), into a CITEREF and return it; return nil else
local function make_citeref(names, year)
local refs = {}
for i, v in ipairs(names) do
local name = v.last or v.label;
if not name then break end; -- no v.last or v.label? abandon the ipairs() loop
refs[i] = mw.text.trim (name);
if i == 4 then break end -- collect only four names
end
if 0 < #refs then -- don't add <year> when there are no names in <refs>; CITEREFYYYY is more-or-less meaningless
table.insert (refs, year); -- add <year>
table.insert (refs, 1, 'CITEREF'); -- and insert the 'CITEREF' prefix
return table.concat (refs); -- concatenate and done
end
return; -- no names so return nil
end
-- function to fetch a value to display
local function makelink(v, out, maxpos, wdl, namestyle)
local label
local item_lookup = false
if v.mainsnak.snaktype == "value" then
if v.mainsnak.datatype == "wikibase-item" then
local qnumber = v.mainsnak.datavalue.value.id
local human_instance = false
local sitelink = mw.wikibase.getSitelink(qnumber)
if qnumber == "Q2818964" then sitelink = nil end -- suppress link to "Various authors"
-- Last name, first name(s) from "author given names" and "author last names" qualifiers
local last_name
local first_names
if v.qualifiers and v.qualifiers.P9688 and v.qualifiers.P9688[1] then
last_name = v.qualifiers.P9688[1].datavalue.value
end
if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then
first_names = v.qualifiers.P9687[1].datavalue.value
end
-- Otherwise, get last name, first name(s) from author item properties
-- but this requires a getValue WD item lookup for each author, and
-- lookups are limited to 500 per page, so do this after we have the
-- "series ordinal" order, then only do the first few since there are
-- article items out there with 400+ authors(!)
if not last_name or not first_names then
item_lookup = true
end
-- use object named as for the label
if v.qualifiers and v.qualifiers.P1932 then
label = v.qualifiers.P1932[1].datavalue.value -- "object named as"
else
label = mw.wikibase.getLabel(qnumber)
if label then
label = mw.text.nowiki(label)
else
label = qnumber -- should add tracking category
end
end
local position = maxpos + 1 -- Default to 'next' author.
-- use P1545 (series ordinal) instead of default position.
if v.qualifiers and v.qualifiers.P1545 and v.qualifiers.P1545[1] then
position = tonumber(v.qualifiers.P1545[1].datavalue.value)
end
maxpos = math.max(maxpos, position)
-- now try determining first/last names with item lookup, if needed
if item_lookup and position <= max_authors_displayed then
last_name = last_name or getValue({"P734", qid=qnumber, maxvals=1, ps=2})
first_names = first_names or getValue({"P735", qid=qnumber, maxvals=0, ps=2})
-- check if P31 "instance of" Q5 "human"
local instance_of = mw.wikibase.getBestStatements(qnumber, 'P31')
if instance_of and instance_of[1] then
for k, vi in pairs(instance_of) do
if vi.mainsnak.snaktype == "value" and vi.mainsnak.datatype == "wikibase-item" and vi.mainsnak.datavalue.value.id == 'Q5' then
human_instance = true
end
end
end
end
if first_names then
first_names = getStyledNames(first_names, namestyle)
end
if wdl then
-- show that there's a Wikidata entry available
out[position] = string.format(
"[[:d:Q%s|%s]] <span title='%s'>[[File:Wikidata-logo.svg|16px|class=noviewer|alt=|link=]]</span>",
v.mainsnak.datavalue.value["numeric-id"],
label,
cfg.i18n_t["errors"]["local-article-not-found"]
)
else
out[position] = {
label = label,
last = last_name or nil,
first = first_names or nil,
link = sitelink or nil,
qid = qnumber,
human = human_instance,
}
end
elseif v.mainsnak.datatype == "string" then
local position = maxpos + 1 -- Default to 'next' author.
-- use P1545 (series ordinal) instead of default position.
if v.qualifiers and v.qualifiers.P1545 and v.qualifiers.P1545[1] then
position = tonumber(v.qualifiers.P1545[1].datavalue.value)
end
maxpos = math.max(maxpos, position)
-- Names from "author given names" and "author last names" qualifiers
local last_name
local first_names
if v.qualifiers and v.qualifiers.P9688 and v.qualifiers.P9688[1] then
last_name = v.qualifiers.P9688[1].datavalue.value
end
if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then
first_names = getStyledNames(v.qualifiers.P9687[1].datavalue.value, namestyle)
end
out[position] = {
label = v.mainsnak.datavalue.value,
last = last_name or nil,
first = first_names or nil,
}
else
-- not a wikibase-item or a string!
end
else
-- try the "object named as" qualifier, otherwise use the unknown author item
if v.qualifiers and v.qualifiers.P1932 then
-- Names from "author given names" and "author last names" qualifiers
local last_name
local first_names
if v.qualifiers and v.qualifiers.P9688 and v.qualifiers.P9688[1] then
last_name = v.qualifiers.P9688[1].datavalue.value
end
if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then
first_names = getStyledNames(v.qualifiers.P9687[1].datavalue.value, namestyle)
end
label = {
label = v.qualifiers.P1932[1].datavalue.value, -- "object named as"
last = last_name or nil,
first = first_names or nil,
}
else
-- use anonymous
tableset_add(opt_cat, cfg.i18n_t.trackingcats["unknown-author"])
label = {
label = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper),
qid = 'Q4233718',
}
end
maxpos = maxpos + 1
out[maxpos] = label
end
return maxpos
end
--[=[-------------------------< G E T _ N A M E _ L I S T >----------------------------------------------------
get_name_list -- adapted from getAuthors code taken from Module:RexxS
arguments:
nl_type - type of name list to fetch: nl_type = 'author' for authors; 'editor' for editors; 'translator' for translators
args - pointer to the parameter arguments table from the template call
qid - value from |qid= parameter; the Q-id of the source (book, etc.) in qid
wdl - value from the |wdl= parameter; a Boolean passed to enable links to Wikidata when no article exists
returns name data found in Wikidata (label, first, last, link, qid); modifies the args table
]=]
local function get_name_list (nl_type, args, qid, wdl)
local propertyID
local fallbackID
local names = {}
if nl_type =="author" then
propertyID = 'P50' -- for authors
fallbackID = 'P2093' -- author-string
elseif nl_type =="editor" then
propertyID = 'P5769' -- "editor-in-chief"
fallbackID = 'P98' -- for editors - So-called "fallbacks" are actually a second set of properties processed
-- TBD. Take book series editors into account as well (if they have a separate P code as well)?
elseif nl_type == "translator" then
propertyID = 'P655' -- for translators
fallbackID = nil
-- elseif 'contributor' == nl_type then
-- f.e. author of forewords (P2679) and afterwords (P2680); requires |contribution=, |title= and |author=
-- propertyID = 'P' -- for contributors
-- fallbackID = nil
else
return -- not specified so return
end
-- wdl is a Boolean passed to enable links to Wikidata when no article exists
-- if "false" or "no" or "0" is passed set it false
-- if nothing or an empty string is passed set it false
if wdl and (#wdl > 0) then
wdl = wdl:lower()
wdl = in_array (wdl, {"false", "no", "0"})
else
-- wdl is empty, so
wdl = false
end
local props = nil
local fallback = nil
if mw.wikibase.entityExists(qid) then
props = mw.wikibase.getAllStatements(qid, propertyID)
if props and fallbackID then
fallback = mw.wikibase.getAllStatements(qid, fallbackID)
end
end
-- Make sure it actually has at least one of the properties requested
if not (props and props[1]) and not (fallback and fallback[1]) then
return nil
end
-- So now we have something to return:
-- table 'out' is going to store the name label, qid, first/last names, and link to the name's article
local out = {}
local maxpos = 0
if props and props[1] then
for k, v in pairs(props) do
if 'deprecated' ~= v.rank then -- ignore deprecated names
maxpos = makelink(v, out, maxpos, wdl, args['name-list-style'])
end
end
end
if fallback and fallback[1] then
-- second properties
for k, v in pairs(fallback) do
if 'deprecated' ~= v.rank then -- ignore deprecated names
maxpos = makelink(v, out, maxpos, wdl, args['name-list-style'])
end
end
end
-- if there's anything to return, then insert the additions in the template arguments table
-- in the form |author1=firstname secondname |author2= ...
-- Renumber, in case we have inconsistent numbering
local keys = {}
for k, v in pairs(out) do
keys[#keys + 1] = k
end
table.sort(keys) -- as they might be out of order
for i, k in ipairs(keys) do
names[i] = {}
local label = out[k].label
label = label:gsub(''', '\''); -- prevent cs1|2 multiple names categorization; replace html entity with the actual character
names[i].label = label
names[i].qid = out[k].qid or nil
names[i].human = out[k].human or nil
mw.log(i .. " " .. k .. " " .. label)
if args[nl_type .. i] then -- name gets overwritten
-- pull corresponding -link only if overwritten name is same as WD name
if out[k].link and (args[nl_type .. i] == label) then
args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or out[k].link -- author-linkn or editor-linkn
names[i].link = out[k].link
end
else -- name does not get overwritten, so pull name from WD
names[i].last = out[k].last or nil
names[i].first = out[k].first or nil
if out[k].first and out[k].last then
-- First/last names were found, so add them
args[nl_type .. '-last' .. i] = args[nl_type .. '-last' .. i] or out[k].last
args[nl_type .. '-first' .. i] = args[nl_type .. '-first' .. i] or out[k].first
else
args[nl_type .. i] = out[k].label
end
if out[k].link then
args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or out[k].link -- author-linkn or editor-linkn
names[i].link = out[k].link
end
end
end
return names
end
-- gets language codes used for a monolingual text property as a table
local function _getLangOfProp(qid, pid)
if not pid then return {} end
local out = {}
local props = mw.wikibase.getAllStatements(qid, pid)
for i, v in ipairs(props) do
if v.mainsnak.datatype == "monolingualtext" and v.mainsnak.datavalue then
out[#out + 1] = v.mainsnak.datavalue.value.language
end
end
return out
end
local function getLangOfProp(frame)
local pid = frame.args.pid or mw.text.trim(frame.args[1] or "")
if pid == "" then return end
local qid = frame.args.qid
if qid == "" then qid = nil end
return table.concat(_getLangOfProp(qid, pid), ", ")
end
-- gets the language codes of a Wikidata entry as a table
local function _lang_code(qid)
-- P407 "language of work or name"
local lc = _getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P424", ps = 1} ) -- "Wikimedia language code"
if lc then return mw.text.split( lc, "[, ]+" ) end
lc = _getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P218", ps = 1} ) -- "ISO 639-1 code"
if lc then return mw.text.split( lc, "[, ]+" ) end
return _getLangOfProp(qid, "P1476") -- "title"
end
local function lang_code(frame)
return table.concat(_lang_code(frame.args.qid or mw.text.trim(frame.args[1] or "")), ", ")
end
-- export for debug
local function getPropOfProp(frame)
return _getPropOfProp(frame.args)
end
-- wraps a string in nowiki unless disable flag is set
local function wrap_nowiki(str, disable)
if disable then return str or '' end
return mw.text.nowiki(str or '')
end
-- returns the first string sequence of 3 or more digits in a date string, or nil
local function yearFromDate(datestr)
if datestr then
return string.match(datestr, '%d%d%d+')
end
return datestr
end
-- Attempt to determine heuristically what we're citing
local function getTemplateFromInstance(qid, args)
local chapteritems = {
'Q1980247', -- chapter
}
local newsitems = {
'Q5707594', -- news article
'Q30070590', -- magazine article
-- TODO: somehow use WHERE {?item (wdt:P31|wdt:P31/wdt:P279*) wd:Q5707594.}
-- main subclasses of news article:
'Q87849013', -- AP article
'Q2144639', -- feature story
'Q139172839', -- top story
'Q18339884', -- cover story
'Q3326038', -- critical review
'Q4202018', -- interview
'Q309481', -- obituary
'Q108970988', -- column
'Q2602337', -- op-ed
'Q59908', -- opinion piece
}
-- most of the time a cheap lookup to a single value will work
if args['instance-of'] and in_array(args['instance-of'], newsitems) then
return 'news'
elseif args['instance-of'] and in_array(args['instance-of'], chapteritems) then
return 'chapter'
end
-- otherwise check multiple non-deprecated values
local instance_of = mw.wikibase.getBestStatements(qid, 'P31')
if instance_of and instance_of[1] then
for k, v in pairs(instance_of) do
if v.mainsnak.snaktype == "value" and v.mainsnak.datatype == "wikibase-item" then
if in_array(v.mainsnak.datavalue.value.id, newsitems) then
return 'news'
elseif in_array(v.mainsnak.datavalue.value.id, chapteritems) then
return 'chapter'
end
end
end
end
return ''
end
-- Figure out a URL access value, from the "online access status" (P6954) qualifier of an identifier
-- The [[Template:Cite journal]] docs state these should only allow "free"
-- e.g. DOI = P356
local function getIdentifierAccess(qid, args, identifier)
if not args[identifier] then
return nil
end
-- check DOI registrant is known free, using CS1 config
-- TODO: this should also be modelled in Wikidata
if identifier == 'doi' then
local registrant, suffix = mw.ustring.match(args.doi:lower(), '^10%.([^/]+)/([^%s–]+)$');
if registrant and suffix and cs1_cfg.known_free_doi_registrants_t[registrant] then
return 'free'
elseif registrant and suffix and cs1_cfg.extended_registrants_t[registrant] then
for _, incipit in ipairs(cs1_cfg.extended_registrants_t[registrant]) do
if mw.ustring.find (suffix, '^' .. incipit:lower()) then
return 'free'
end
end
end
end
-- fetch statement values to examine qualifiers
local ids = mw.wikibase.getBestStatements(qid, cfg.simple_properties_t[identifier]['id'])
if not ids or not ids[1] then
return nil
end
if not ids[1].qualifiers or not ids[1].qualifiers.P6954 then
return nil
end
-- Check property constraints (P2302) in definition of online access status (P6954)
-- Q66439598 = 'limited', -- partial open access
-- Q107459441 = 'registration', -- registration required
-- Q910845 = 'subscription', -- paywall
-- Q16572632 = 'subscription', -- login (see note on P6954)
local free_status = {
'Q24707952', -- free to read
'Q232932', -- open access
}
local access = ids[1].qualifiers.P6954[1]
if access.datatype == "wikibase-item" then
local accessQID = access.datavalue.value.id
if in_array(accessQID, free_status) then
return 'free'
end
end
return nil
end
-- sort sequence table whose values are key-value pairs by key
local function comp_key(a, b)
local param_a, enum_a = a[1]:match ('(%D+)(%d+)$'); -- get param name and enumerator from <a>
local param_b, enum_b = b[1]:match ('(%D+)(%d+)$'); -- get param name and enumerator from <b>
if enum_a and enum_b then -- if both parameters have neumerators
if param_a == param_b then -- are parameter names the same?
return tonumber (enum_a) < tonumber (enum_b); -- yes: compare enumerators
end
end
return a[1] < b[1]; -- alpha sort if here
end
-- sort sequence table whose values are key-value pairs by value
local function comp_val(a, b)
return a[2] < b[2]
end
--[[-------------------------< C I T E _ Q >------------------------------------------------------------------
Takes standard CS1|2 template parameters and passes all to {{citation}}. If neither of |author= and |author1=
are set, calls get_authors() to try to get an author name-list from Wikidata. The result is passed to
{{citation}} for rendering.
--]]
local function _cite_q (citeq_args)
local frame = mw.getCurrentFrame()
-- parameters that don't get passed to Citation
local expand = citeq_args.expand -- when set to anything, causes {{cite q}} to render <code><nowiki>{{citation|...}}</nowiki></code>
local qid = citeq_args.qid or citeq_args[1]
local wdl = citeq_args.wdl
local template = citeq_args.template
citeq_args.expand = nil
citeq_args[1] = nil
citeq_args.qid = nil
citeq_args.wdl = nil
citeq_args.template = nil
-- if title supplied, flag to not read html title
local titleforced = (citeq_args.title ~= nil)
local oth = {}
-- put the language codes into a sequential table langcodes[]
local langcodes = {}
if citeq_args.language then
-- check these are a supported language codes
for lc in mw.text.gsplit( citeq_args.language, "[, ]+", false ) do
langcodes[#langcodes+1] = mw.language.isSupportedLanguage(citeq_args.language) and citeq_args.language
end
end
if not langcodes[1] then
-- try to find language of work
langcodes = _lang_code(qid)
end
if not langcodes[1] then
-- try fallback to journal's language
local journal_qid = followQid({qid = qid, props = "P1433"})
langcodes = journal_qid and _lang_code(journal_qid)
end
citeq_args.language = citeq_args.language or table.concat(langcodes, ", ")
-- loop through list of simple properties and get their values in citeq_args
for name, data in pairs(cfg.simple_properties_t) do
if data.qidonly then
citeq_args[name] = followQid({qid = qid, props = data.id})
if citeq_args[name] == qid then
citeq_args[name] = nil
end
end
if not citeq_args[name] then
citeq_args[name] = getValue( {data.id, fwd = "ALL", osd = "no", noicon = "true", qid = qid, maxvals = data.maxvals, linked = data.linked, rank = data.rank or "best", citeq_args[name] } )
end
if data.populate_from_journal then
local publishedin = getValue( {"P1433", ps = 1, qid = qid, maxvals = 0, citeq_args[name], qual = data.id, qualsonly = 'yes'} )
citeq_args[name] = publishedin or _getPropOfProp({qid = qid, prop1 = "P1433", prop2 = data.id, maxvals = data.maxvals, ps = 1})
end
if citeq_args[name] and citeq_args[name]:find (cfg.i18n_t.trackingcats["missing-wikidata"], 1, true) then
-- try fallback to work's native language
citeq_args[name] = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = langcodes[1] } )
if citeq_args[name]:find('^Q%d+$') then -- qid was returned
-- try fallback to qid's native language
local qid_languages = _lang_code(citeq_args[name])
citeq_args[name] = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = qid_languages[1] } )
if citeq_args[name]:find('^Q%d+$') then -- qid was returned again
citeq_args[name] = nil
else
-- record the language found if no lang specified
citeq_args.language = citeq_args.language or qid_languages[1]
end
end
end
if data.others then
oth[#oth + 1] = citeq_args[name] and (name:gsub("^%l", string.upper) .. ": " .. citeq_args[name])
citeq_args[name] = nil
end
end
citeq_args.doi = citeq_args.doi and citeq_args.doi:lower() or nil; -- force |doi= value to be lower case
-- URL access for identifiers, from P6954 online access status
-- The [[Template:Cite journal]] docs state these should only allow "free"
citeq_args['bibcode-access'] = citeq_args['bibcode-access'] or getIdentifierAccess(qid, citeq_args, 'bibcode');
citeq_args['doi-access'] = citeq_args['doi-access'] or getIdentifierAccess(qid, citeq_args, 'doi');
citeq_args['hdl-access'] = citeq_args['hdl-access'] or getIdentifierAccess(qid, citeq_args, 'hdl');
citeq_args['jstor-access'] = citeq_args['jstor-access'] or getIdentifierAccess(qid, citeq_args, 'jstor');
-- citeq_args['ol-access'] = citeq_args['ol-access'] or getIdentifierAccess(qid, citeq_args, 'ol');
citeq_args['osti-access'] = citeq_args['osti-access'] or getIdentifierAccess(qid, citeq_args, 'osti');
citeq_args['ssrn-access'] = citeq_args['ssrn-access'] or getIdentifierAccess(qid, citeq_args, 'ssrn');
citeq_args['s2cid-access'] = citeq_args['s2cid-access'] or getIdentifierAccess(qid, citeq_args, 's2cid');
citeq_args.others = citeq_args.others or table.concat(oth, ". ")
if citeq_args.others == "" then
citeq_args.others = nil
end
citeq_args.journal = citeq_args.journal and citeq_args.journal:gsub("^''", ""):gsub("''$", ""):gsub("|''", "|"):gsub("'']]", "]]")
citeq_args.ol = (getValue( {"P648", ps = 1, qid = qid, maxvals = 1, citeq_args.ol } ) or ''):gsub("^OL(.+)$", "%1")
if citeq_args.ol == "" then
citeq_args.ol = nil
end
-- TBD. Take care of |ol-access=?
citeq_args.biorxiv = citeq_args.biorxiv and ("10.1101/" .. citeq_args.biorxiv)
citeq_args.isbn = getValue( {"P957", ps = 1, qid = qid, maxvals = 1, rank="best", citeq_args.isbn } ) -- try ISBN 10 (only one value accepted)
-- if url then see if there's an archive: citeq_args.url
local url
if not citeq_args.url then
for i, pr in ipairs( {"P953", "P856", "P2699"} ) do
url = getValue( {pr, ps = 1, qid = qid, maxvals = 1, qual="P1065" } )
if url then
citeq_args.url = mw.text.split( url, " (", true )[1]
local arcurl = mw.ustring.match( url, " %((.*)%)" ) -- when there is an archive url, <url> holds: url<space>(archive url); here extract the archive url if present
if arcurl then
local arcy, arcm, arcd = arcurl:match("(20%d%d)%p?(%d%d)%p?(%d%d)")
if arcy and arcm and arcd then
citeq_args["archive-url"] = arcurl
citeq_args["archive-date"] = tonumber(arcd) .. " " .. cfg.i18n_t.months[tonumber(arcm)] .. " " .. arcy
end
end
break
end
end
end
if citeq_args.publisher == "Unknown" then -- look for "stated as" (P1932)
local stated_as = getValue( {"P123", ps = 1, qid = qid, maxvals = 1, qual="P1932", qo="y"} )
if stated_as then citeq_args.publisher = stated_as end
end
-- Handle thesis args - degree, supervising institution
local is_thesis = false
if citeq_args.thesis_degree then
is_thesis = true
citeq_args.degree = citeq_args.thesis_degree
citeq_args.thesis_degree = nil
end
if citeq_args.thesis_submitted_to then
is_thesis = true
citeq_args.publisher = citeq_args.thesis_submitted_to
citeq_args.thesis_submitted_to = nil
end
if not titleforced then
-- Handle subtitle.
if citeq_args.title then
local subtitle = mw.wikibase.getBestStatements (qid, 'P1680');
if 0 ~= #subtitle then
subtitle = subtitle[1].mainsnak.datavalue.value.text;
citeq_args.title = citeq_args.title .. ": " .. subtitle
end
end
local htmltitle = getValue( {"P1476", qual = "P6833", ps = 1, qid = qid, maxvals = 1, qo = "y"} )
if htmltitle then
citeq_args.title = htmltitle:gsub("</?i>", "''")
else
local title_display = citeq_args.title
or mw.wikibase.getLabel(qid)
or (langcodes[1] and mw.wikibase.getLabelByLang(qid, langcodes[1]))
if citeq_args.url then
citeq_args.title = wrap_nowiki(title_display)
else
local slink = mw.wikibase.getSitelink(qid)
local slink_flag = false
local wrap_title = ''
local wslink = false
if not slink then
-- See if we have wikisource
if not citeq_args.url then
local wikisource_sitelink = mw.wikibase.getSitelink(qid, cfg.i18n_t.wikisource) or nil
if wikisource_sitelink then
slink = ':s:'..wikisource_sitelink
wslink = true
end
end
end
if citeq_args.title then
if slink then
wrap_title = wrap_nowiki(citeq_args.title)
slink_flag = true
else
citeq_args.title = wrap_nowiki(citeq_args.title)
end
else
if slink and not wslink then
if slink:lower() == title_display:lower() then
citeq_args.title = '[[' .. slink .. ']]'
else
wrap_title = wrap_nowiki(slink:gsub("%s%(.+%)$", ""):gsub(",.+$", ""))
slink_flag = true
end
elseif wslink then
wrap_title = wrap_nowiki(title_display)
slink_flag = true
else
citeq_args.title = wrap_nowiki(title_display)
end
end
if slink_flag then
if slink == wrap_title and not wslink then -- direct link
citeq_args.title = '[[' .. slink .. ']]'
else -- piped link
citeq_args.title = '[[' .. slink .. '|' .. wrap_title .. ']]'
end
end
end
end
end
-- TBD: incorporate |at, |sheets= and |sheet= here as well
-- Sort out what should happen if several of them are given at the same time
if citeq_args.page or citeq_args.p then -- let single take precedence over multiple
citeq_args.pages = nil
citeq_args.pp = nil
end
if citeq_args.pages then
local _, count = string.gsub(citeq_args.pages, "[,;%s]%d+", "")
if count == 1 then
citeq_args.page = citeq_args.pages
citeq_args.pages = nil
end
end
local names = {}
if is_set (qid) then
if not is_set (citeq_args.author) and not is_set (citeq_args.author1)
and not is_set (citeq_args.subject) and not is_set (citeq_args.subject1)
and not is_set (citeq_args.host) and not is_set (citeq_args.host1)
and not is_set (citeq_args.last) and not is_set (citeq_args.last1)
and not is_set (citeq_args.surname) and not is_set (citeq_args.surname1)
and not is_set (citeq_args['author-last']) and not is_set (citeq_args['author-last1']) and not is_set (citeq_args['author1-last'])
and not is_set (citeq_args['author-surname']) and not is_set (citeq_args['author-surname1']) and not is_set (citeq_args['author1-surname1']) then -- if neither are set, try to get authors from Wikidata
names.authors = get_name_list ('author', citeq_args, qid, wdl) or {} -- modify citeq_args table with authors from Wikidata
else
names.authors = {}
names.authors_overridden = true
end
if not is_set (citeq_args.editor) and not is_set (citeq_args.editor1)
and not is_set (citeq_args['editor-last']) and not is_set (citeq_args['editor-last1']) and not is_set (citeq_args['editor1-last'])
and not is_set (citeq_args['editor-surname']) and not is_set (citeq_args['editor-surname1']) and not is_set (citeq_args['editor1-surname']) then -- if neither are set, try to get editors from Wikidata
names.editors = get_name_list ('editor', citeq_args, qid, wdl) or {} -- modify citeq_args table with editors from Wikidata
else
names.editors = {}
names.editors_overridden = true
end
if not is_set (citeq_args.translator) and not is_set (citeq_args.translator1)
and not is_set (citeq_args['translator-last']) and not is_set (citeq_args['translator-last1']) and not is_set (citeq_args['translator1-last'])
and not is_set (citeq_args['translator-surname']) and not is_set (citeq_args['translator-surname1']) and not is_set (citeq_args['translator1-surname']) then -- if neither are set, try to get translators from Wikidata
names.translators = get_name_list ('translator', citeq_args, qid, wdl) or {} -- modify citeq_args table with translators from Wikidata
else
names.translators = {}
names.translators_overridden = true
end
end
for k, v in pairs(citeq_args) do
if in_array (v, {'(())', 'unset', 'ignore'}) or 'string' ~= type(k) then -- empty accept-as-is-written (()) markup to indicate an empty/unused parameter value, other ((...)) markups are deliberately passed down to {{citation}}
citeq_args[k] = nil
elseif in_array (v, {'((unset))', '((ignore))'}) then -- strip off markup for free-text values clashing with local keywords
citeq_args[k] = 'unset'
end
end
local author_count = 0
for k, v in pairs(citeq_args) do
if k:find("^author%d+$") or k:find("^author%-last%d+$") then
author_count = author_count + 1
end
end
if author_count > 8 then -- convention in astronomy journals, optional mode for this?
if 'all' == citeq_args['display-authors'] then
citeq_args['display-authors'] = nil; -- unset because no longer needed
else
citeq_args['display-authors'] = citeq_args['display-authors'] or 3 -- limit to three displayed names
end
end
local editor_count = 0
for k, v in pairs(citeq_args) do
if k:find("^editor%d+$") or k:find("editor%-last%d+$") then
editor_count = editor_count + 1
end
end
if editor_count > 8 then -- convention in astronomy journals, optional mode for this?
if 'all' == citeq_args['display-editors'] then
citeq_args['display-editors'] = nil; -- unset because no longer needed
else
citeq_args['display-editors'] = citeq_args['display-editors'] or 3 -- limit to three displayed names
end
end
-- check only the names being displayed for missing first/last name errors
local missing_names = {}
for i, author in pairs(names.authors or {}) do
-- mw.logObject(author)
if not author.last and i <= (tonumber(citeq_args['display-authors']) or author_count) then
if author.qid then
-- ignore anonymous or non-humans
if author.human then
table.insert(missing_names, string.format (cfg.i18n_t.errors.missing_name, author.label, 'author', author.qid));
end
else
table.insert(missing_names, author.label)
end
end
end
for i, editor in pairs(names.editors or {}) do
if not editor.last and i <= (tonumber(citeq_args['display-editors']) or editor_count) then
if editor.qid then
-- ignore anonymous or non-humans
if editor.human then
table.insert(missing_names, string.format (cfg.i18n_t.errors.missing_name, editor.label, 'editor', editor.qid));
end
else
table.insert(missing_names, editor.label)
end
end
end
for i, translator in pairs(names.translators or {}) do
if not translator.last then
if translator.qid then
-- ignore anonymous or non-humans
if translator.human then
table.insert(missing_names, string.format (cfg.i18n_t.errors.missing_name, translator.label, 'translator', translator.qid));
end
else
table.insert(missing_names, translator.label)
end
end
end
-- add category and display error if name errors found
if #missing_names > 0 and not in_array('missing-author-first-last-names', opt_cat) then
tableset_add(opt_cat, cfg.i18n_t.trackingcats['missing-author-first-last-names'])
-- TODO: format and i18n this string
cq_errors = cq_errors .. ' <span class="error" style="font-size:inherit"><code class="cs1-code">{{[[Template:Cite Q|Cite Q]]}}</code>: Missing first/last names in Wikidata for ' .. table.concat(missing_names, ', ') .. ' ([[Template:Cite Q#Error messages|help]])</span> '
end
-- change edition to ordinal if it's set and numeric
citeq_args.edition = citeq_args.edition and makeOrdinal(citeq_args.edition)
-- handle supplied {{para|ref}} for readable ID, avoid CS1 duplicate default error
local pubyear = yearFromDate(citeq_args['publication-date'])
if names.authors_overridden or (#names.authors == 0 and names.editors_overridden) then
-- if names are supplied manually, then don't mess with supplied ref parameter
elseif citeq_args.ref and 'none' ~= citeq_args.ref then
local refnames
if #names.authors > 0 then
refnames = names.authors
elseif #names.editors > 0 then
refnames = names.editors
end
local citeref = refnames and make_citeref (refnames, pubyear) or nil; -- <refnames> nil when no authors and no editors
if citeq_args.ref == citeref then -- CITEREF from wikidata authors / editors matches the value provided in |ref=
citeq_args.ref = nil -- unset to avoid CS1 maint: ref duplicates default maintenance message / category
elseif citeref then -- CITEREF from wikidata authors / editors is different from the value provided in |ref=
tableset_add(opt_cat, cfg.i18n_t.trackingcats['supplied-ref'])
local tpara = frame:expandTemplate{ title = 'para', args = {'ref', citeq_args.ref} }
-- TODO: format and i18n this string
cq_errors = cq_errors .. ' <span class="error" style="font-size:inherit"><code class="cs1-code">{{[[Template:Cite Q|Cite Q]]}}</code>: supplied ' .. tpara .. ' does not match value generated from Wikidata: <code class="cs1-code">' .. citeref .. '</code> ([[Template:Cite Q#Error messages|help]])</span> '
end
end
-- code to make a guess what template to use from the supplied parameters
-- (first draft for proof-of-concept)
local guessedType = getTemplateFromInstance(qid, citeq_args)
if guessedType == 'news' then
template = template or "news"
-- some items use P123 (publisher) as the newspaper instead of P1433 (published in)
if citeq_args.publisher and not citeq_args.journal then
citeq_args.journal = citeq_args.publisher
citeq_args.publisher = nil
end
elseif guessedType == 'chapter' then
template = template or "book"
citeq_args['publication-date'] = pubyear or citeq_args['publication-date']
citeq_args.chapter = citeq_args.title
citeq_args.title = citeq_args.journal
citeq_args.journal = nil
elseif citeq_args.journal then
template = template or "journal"
citeq_args['publication-date'] = pubyear or citeq_args['publication-date']
-- articles can be published in books/proceedings, otherwise suppress publisher
if not citeq_args.isbn then
citeq_args.publisher = nil
end
-- only display ISSN if there are no article identifiers
if citeq_args.issn then
if citeq_args.doi or citeq_args.pmid or citeq_args.pmc or citeq_args.arxiv or
citeq_args.bibcode or citeq_args.jstor or citeq_args.ssrn or citeq_args.s2cid or
citeq_args.hdl or citeq_args.biorxiv or citeq_args.osti or citeq_args.zbl or
citeq_args.jfm or citeq_args.mr then
citeq_args.issn = nil
end
end
elseif is_thesis then
template = template or "thesis"
elseif citeq_args.isbn then
template = template or "book"
citeq_args.asin = nil -- suppress ASIN if ISBN exists
citeq_args['publication-date'] = pubyear or citeq_args['publication-date']
elseif citeq_args.website then
template = template or "web"
end
-- |id= could hold more than one identifier pulled from Wikidata not supported by {{citation}}, right now only add our qid to the list
local list_sep = '. '
if citeq_args.mode ~= 'cs1' then
list_sep = ', '
end
local id = cfg.i18n_t.wdq.. ' [[:d:' .. qid .. '|' .. qid .. ']]'
local old_id = citeq_args.id
if wdl then -- show WD logo
id = id .. '[[File:Wikidata-logo.svg|16px|class=noviewer|alt=|link=]]' -- possibly replace by WD edit icon?
end
if is_set (old_id) then
citeq_args.id = old_id .. list_sep .. id -- append to user-specified contents
else
citeq_args.id = id
end
-- Indicator if item has been retracted
local retract = ''
if citeq_args.retracted_by then
tableset_add(opt_cat, cfg.i18n_t.trackingcats.retracted)
local retract_id = string.gsub(citeq_args.retracted_by, 'Q', '')
local retr_params = {
citeq = 'yes',
id = frame:expandTemplate{
title = 'QID', args = {qid = retract_id}
}
}
retr_params.doi = getValue({"P356", ps = 1, qid = citeq_args.retracted_by}) or nil
retr_params.doi = retr_params.doi and retr_params.doi:lower() or nil
retr_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args.retracted_by}) or nil
retr_params.pmc = getValue({"P932", ps = 1, qid = citeq_args.retracted_by}) or nil
retr_params.pmid = getValue({"P698", ps = 1, qid = citeq_args.retracted_by}) or nil
-- check qualifiers for access=free
retr_params['doi-access'] = getIdentifierAccess(citeq_args.retracted_by, retr_params, 'doi')
retr_params['bibcode-access'] = getIdentifierAccess(citeq_args.retracted_by, retr_params, 'bibcode')
if 'yes' == citeq_args['retracted-intentional'] then
retr_params.intentional = 'yes'
end
retract = ' ' .. frame:expandTemplate{ title = "Retracted", args = retr_params }
end
citeq_args.retracted_by = nil -- remove non-CS1 parameters
citeq_args['retracted-intentional'] = nil
citeq_args['instance-of'] = nil
if citeq_args['name-list-style'] and citeq_args['name-list-style'] == 'apa' then
citeq_args['name-list-style'] = nil
end
for k, v in pairs(citeq_args) do
if v == "" then
citeq_args[k] = nil; -- remove blank parameters
elseif 'string' == type (v) then -- here when not blank; must be a string; |pmid= and the like are type 'number'
local changed; -- flag is a number when changes have been made; nil else
local pattern = '%[%[([^|]+)|%1%]%]'; -- pattern that looks for [[<link>|<label>]] wikilinks where <link> and <label> are identical
v, changed = v:gsub (pattern, '[[%1]]'); -- where <link> and <label> are identical, rewrite all such [[<link>|<label>]] wikilinks in <v> using [[<link>]]
if changed then -- when changes have been made
citeq_args[k] = v; -- update <citeq_args[k]>
end
end
end
-- if |expand=<anything>, write a nowiki'd version to see what the {{citation}} template call looks like
if expand then
local expand_args = { "{{" .. (template and ("Cite "..template) or "Citation") } -- init with citation template
if expand == "self" then
citeq_args.id = old_id -- restore original |id= parameter
expand_args = { cfg.i18n_t.cite_q .. qid } -- expand to itself
end
-- make a sortable table and sort it by param name
local sorttable = {}
for param, val in pairs (citeq_args) do
table.insert(sorttable, {param, val})
end
table.sort(sorttable, comp_key)
-- add contents to expand_args
for idx, val in ipairs(sorttable) do
table.insert(expand_args, val[1] .. '=' .. val[2])
end
-- make the nowiki'd string and done
return frame:preprocess (table.concat ({'<syntaxhighlight lang="wikitext" inline="1">', table.concat (expand_args, ' |') .. '}}', '</syntaxhighlight>'}));
end
-- Indicator if item has corrigendum/erratum
local erratumid = ""
if citeq_args.erratum then
tableset_add(opt_cat, cfg.i18n_t.trackingcats.erratum)
local erratum_params = {
citeq = 'yes',
id = frame:expandTemplate{
title = 'QID', args = {qid = string.gsub(citeq_args.erratum, 'Q', '')}
}
}
erratum_params.doi = getValue({"P356", ps = 1, qid = citeq_args.erratum}) or nil
erratum_params.doi = erratum_params.doi and erratum_params.doi:lower() or nil
erratum_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args.erratum}) or nil
erratum_params.pmc = getValue({"P932", ps = 1, qid = citeq_args.erratum}) or nil
erratum_params.pmid = getValue({"P698", ps = 1, qid = citeq_args.erratum}) or nil
-- check qualifiers for access=free
erratum_params['doi-access'] = getIdentifierAccess(citeq_args.erratum, erratum_params, 'doi')
erratum_params['bibcode-access'] = getIdentifierAccess(citeq_args.erratum, erratum_params, 'bibcode')
if 'yes' == citeq_args['erratum-checked'] then
erratum_params.checked = 'yes'
end
erratumid = ' ' .. frame:expandTemplate{ title = "Erratum", args = erratum_params }
end
citeq_args.erratum = nil
citeq_args['erratum-checked'] = nil
-- Indicator if item has an expression of concern notice (P14891)
local eoc = ""
if citeq_args['expression-of-concern'] then
tableset_add(opt_cat, cfg.i18n_t.trackingcats['expression-of-concern'])
local eoc_params = {
citeq = 'yes',
id = frame:expandTemplate{
title = 'QID', args = {qid = string.gsub(citeq_args['expression-of-concern'], 'Q', '')}
}
}
eoc_params.doi = getValue({"P356", ps = 1, qid = citeq_args['expression-of-concern']}) or nil
eoc_params.doi = eoc_params.doi and eoc_params.doi:lower() or nil
eoc_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args['expression-of-concern']}) or nil
eoc_params.pmc = getValue({"P932", ps = 1, qid = citeq_args['expression-of-concern']}) or nil
eoc_params.pmid = getValue({"P698", ps = 1, qid = citeq_args['expression-of-concern']}) or nil
-- check qualifiers for access=free
eoc_params['doi-access'] = getIdentifierAccess(citeq_args['expression-of-concern'], eoc_params, 'doi')
eoc_params['bibcode-access'] = getIdentifierAccess(citeq_args['expression-of-concern'], eoc_params, 'bibcode')
if 'yes' == citeq_args['eoc-intentional'] then
eoc_params.intentional = 'yes'
end
eoc = ' ' .. frame:expandTemplate{ title = "Expression of concern", args = eoc_params }
end
citeq_args['expression-of-concern'] = nil
citeq_args['eoc-intentional'] = nil
if citeq_args.replaced_by then
tableset_add(opt_cat, cfg.i18n_t.trackingcats.replaced)
citeq_args.replaced_by = nil
end
if not citeq_args.title then -- emit category link if missing title
tableset_add(opt_cat, cfg.i18n_t.trackingcats['no-label-or-title'])
end
-- Disable CiteSeerX IDs for now
citeq_args.citeseerx = nil
-- render the template
local lc_template = template and template:lower() or "citation"
local output
if cite_cfg.known_templates_t[lc_template] then
local config_t = {['CitationClass'] = cite_cfg.citation_classes_t[lc_template] or lc_template}; -- set CitationClass value
output = require (cs1_module)._citation (nil, citeq_args, config_t) -- go render the citation
else
output = frame:expandTemplate{ -- render the template
title = "Cite "..template, args = citeq_args
}
tableset_add(opt_cat, cfg.i18n_t.trackingcats["unknown-template"])
end
return output .. erratumid .. eoc .. retract .. table.concat(opt_cat, ' ') .. cq_errors
end
local function cite_q (frame)
local args = {}
for k, v in pairs(frame:getParent().args) do
if v ~= "" then args[k] = v end
end
for k, v in pairs(frame.args) do
if v ~= "" then args[k] = v end
end
args.qid = args.qid or args[1] or ""
if args.qid == "" then return nil end
args[1] = nil
local citesep = (args.citesep or "")
if citesep == "" then citesep = ", " end
citesep = citesep:gsub('"', '') -- strip double quotes after setting default to allow |citesep="" as a blank separator
args.citesep = nil
local tag = args.tag or ""
if tag == "" then tag = nil end
args.tag = nil
local list = args.list or ""
if list == "" then list = nil end
args.list = nil
args.language = args.language or args.lang
args.lang = nil
local cites = {}
for q in args.qid:gmatch("Q%d+") do
-- make a new copy of the arguments
local newargs = {}
for k, v in pairs(args) do
if k ~= "qid" then
newargs[k] = v
end
end
newargs.qid = q
if tag == "ref" then
cites[#cites + 1] = frame:callParserFunction{ name = "#tag:ref", args = { _cite_q(newargs), name = q } }
-- expand like this: args = { _cite_q(newargs), name = 'foo', group = 'bar' }
else
cites[#cites + 1] = _cite_q(newargs)
end
end
if list then
return frame:expandTemplate{ title = list, args = cites }
else
return table.concat(cites, citesep)
end
end
--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------
]]
return {
makeOrdinal = makeOrdinal,
_getLangOfProp = _getLangOfProp,
getLangOfProp = getLangOfProp,
lang_code = lang_code,
getPropOfProp = getPropOfProp,
_cite_q = _cite_q,
cite_q = cite_q
}