Misplaced Pages

Module:Cite Q: Difference between revisions

Article snapshot taken from Wikipedia with creative commons attribution-sharealike license. Give it a read and then ask your questions in the chat. We can research this topic together.
Browse history interactively← Previous editContent deleted Content added
Revision as of 16:14, 10 October 2017 view sourceChire (talk | contribs)Extended confirmed users1,803 edits Prepare for "series ordinal" by introducing a "position" local variable.← Previous edit Latest revision as of 13:32, 5 July 2024 view source Trappist the monk (talk | contribs)Administrators479,914 editsNo edit summary 
(32 intermediate revisions by 17 users not shown)
Line 1: Line 1:
-- Version: 2021-10-19
require('Module:No globals')


local citeq = {} local p = {}


require('strict')
--[[--------------------------< I S _ S E T >------------------------------------------------------------------
local wdib = require('Module:WikidataIB')
local getValue = wdib._getValue
local getPropOfProp = wdib._getPropOfProp
local followQid = wdib._followQid
local getPropertyIDs = wdib._getPropertyIDs


local i18n = {
Returns true if argument is set; false otherwise. Argument is 'set' when it exists (not nil) or when it is not an empty string.
= mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper),
= "]",
= {
= "st",
= "nd",
= "rd",
= "th"
},
= {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
},
}


-------------------------------------------------------------------------------
-- makeOrdinal needs to be internationalised along with the above i18n
-- 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.
-------------------------------------------------------------------------------
p.makeOrdinal = function(cardinal)
local card = tonumber(cardinal)
if not card then return cardinal end
local ordsuffix = i18n.ordinal.default
if card % 10 == 1 then
ordsuffix = i18n.ordinal
elseif card % 10 == 2 then
ordsuffix = i18n.ordinal
elseif card % 10 == 3 then
ordsuffix = i18n.ordinal
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 = i18n.ordinal.default
end
return card .. ordsuffix
end

-- Table of simple properties that can be fetched in roughly the same way:
-- id = PXXX
-- maxvals = maximum number of multiple values (0 for all)
-- linked = "no" suppresses linking
-- populate_from_journal = true/false determines whether to look in a journal where the source is published
-- rank = "best", "preferred", normal, etc. determines how Wikidata ranks are treated
-- others = true - the value for the property goes to "others" section
local simple_properties = {
publisher = {id = "P123", maxvals = 1},
oclc = {id = "P243", maxvals = 1},
= {id = "P291", maxvals = 0, linked = 'no'}, -- publication place (don't put into |place=; is treated specially in {{citation}} if both are given)
doi = {id = "P356", maxvals = 1}, -- take care of |doi-broken-date= (WD "reason for deprecation"/"stated as") and |doi-access= (WD "access status")?
issue = {id = "P433", maxvals = 0, populate_from_journal = true}, -- distinguish from |number= ("P1545"?) if both are given (still blocked by {{citation}}, but will be supported in the future)
pmid = {id = "P698", maxvals = 1},
-- gbooks = {id = "P675", maxvals = 1}, -- to be added to {{citation}}
-- ia = {id = "P724", maxvals = 1}, -- to be added to {{citation}}
arxiv = {id = "P818", maxvals = 1},
bibcode = {id = "P819", maxvals = 1}, -- take care of |bibcode-access=?
jstor = {id = "P888", maxvals = 1}, -- take care of |jstor-access=?
mr = {id = "P889", maxvals = 1},
rfc = {id = "P892", maxvals = 1},
zbl = {id = "P894", maxvals = 1},
ssrn = {id = "P893", maxvals = 1},
place = {id = "P1071", maxvals = 0, linked = 'no'}, -- written-at place
-- = {id = "P1104", maxvals = 0, linked = 'no'}, -- to be added to {{citation}} / COinS &rft.tpages=
-- coden = {id = "P1159", maxvals = 1}, -- to be added to {{citation}} / COinS &rft.coden=
s2cid = {id = "P8299", maxvals = 1}, -- take care of |s2cid-access=?
pmc = {id = "P932", maxvals = 1}, -- take care of |pmc-embargo-date= (WD "reason for deprecation")?
lccn = {id = "P1144", maxvals = 1},
hdl = {id = "P1184", maxvals = 1}, -- take care of |hdl-access=?
ismn = {id = "P1208", maxvals = 1},
journal = {id = "P1433", maxvals = 1},
citeseerx = {id = "P3784", maxvals = 1},
osti = {id = "P3894", maxvals = 1}, -- take care of |osti-access=?
biorxiv = {id = "P3951", maxvals = 1},
asin = {id = "P5749", maxvals = 1}, -- What about |asin-tld=? (WD examples resolve to .com at present, but may change)
-- = {id = "P528", maxvals = 0}, -- to be added to {{citation}} / COinS &rft.artnum=
isbn = {id = "P212", maxvals = 1, populate_from_journal = true}, -- ISBN 13
issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues?
-- jfm = {id = "P?", maxvals = 1}, -- Jahrbuch über die Fortschritte der Mathematik (not Zbl)
-- sbn = {id = "P?", maxvals = 1}, -- Standard Book Number (predecessor of ISBN, not ICCU)
-- message-id = {id = "P?", maxvals = 1}, -- Usenet message ID
chapter = {id = "P792", maxvals = 1},
= {id = "P577", maxvals = 1, populate_from_journal = true}, -- publication date (don't use |date=; is treated specially in {{citation}} if both are given.)
series = {id = "P179", maxvals = 1, populate_from_journal = true},
version = {id = "P348", maxvals = 0},
edition = {id = "P393", maxvals = 0},
volume = {id = "P478", maxvals = 0, populate_from_journal = true},
-- part = {id = "P1545"?, maxvals = 0}, -- to be added to {{citation}} / COinS &rft.part=
title = {id = "P1476", rank="p n"},
-- url = {id = "P953", maxvals = 1}, -- deal with this along with archive-url
pages = {id = "P304", maxvals = 0, populate_from_journal = true},
at = {id = "P958", maxvals = 0, populate_from_journal = true}, -- also incorporate lines (P7421) and columns (P3903) into this (cite map also supports |section=)
-- sheets = {id = "P7416", maxvals = 0, populate_from_journal = true},
-- interviewer = {id = "P?", maxvals = 0}, -- does **not** go to "others" section! Multiple interviewers should be n-enumerated
illustrator = {id = "P110", maxvals = 10, others = true}, -- goes to "others" section
-- foreword and afterword, when contributions to another author's work, are contributions so belong in |contribution=;
-- the writer's name goes in |contributor=; requires |title= and |author=
-- However, this might need to add support for multiple contributors and their roles to {{citation}}, see Help_talk:Citation_Style_1#Others
-- foreword = {id = "P2679", maxvals = 10, others = true}, -- goes to "others" section
-- afterword = {id = "P2680", maxvals = 10, others = true}, -- goes to "others" section
composer = {id = "P86", maxvals = 10, others = true}, -- goes to "others" section
animator = {id = "P6942", maxvals = 10, others = true}, -- goes to "others" section
director = {id = "P57", maxvals = 10, others = true}, -- goes to "others" section
screenwriter = {id = "P58", maxvals = 10, others = true}, -- goes to "others" section
signatory = {id = "P1891", maxvals = 10, others = true}, -- goes to "others" section
presenter = {id = "P371", maxvals = 10, others = true}, -- goes to "others" section
performer = {id = "P175", maxvals = 10, others = true}, -- goes to "others" section
}

--[[--------------------------< 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 ) local function is_set( var )
return not (var == nil or var == ''); return not (var == nil or var == '')
end 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


--[=[-------------------------< G E T _ N A M E _ L I S T >----------------------------------------------------


--[[--------------------------< A C C E P T _ V A L U E >-------------------------------------------------------
get_name_list -- adapted from getAuthors code taken from ]
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

-- function to fetch a value to display
local function makelink(v, out, link, maxpos, wdl)
local label
if v.mainsnak.snaktype == "value" then
if v.mainsnak.datatype == "wikibase-item" then
local qnumber = v.mainsnak.datavalue.value.id
local sitelink = mw.wikibase.getSitelink(qnumber)
if qnumber == "Q2818964" then sitelink = nil end -- suppress link to "Various authors"
if v.qualifiers and v.qualifiers.P1932 then
label = v.qualifiers.P1932.datavalue.value
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 and v.qualifiers and v.qualifiers then
position = tonumber(v.qualifiers.datavalue.value)
end
maxpos = math.max(maxpos, position)
if sitelink then
-- just the plain name,
-- but keep a record of the links, using the same index
out = label
link = sitelink
else
if wdl then
-- show that there's a Wikidata entry available
out = " .. "|" .. label .. "]]&nbsp;<span title='" .. i18n .. "'>]</span>"
else
-- no Wikidata links wanted, so just give the plain label
out = label
end
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 and v.qualifiers and v.qualifiers then
position = tonumber(v.qualifiers.datavalue.value)
end
maxpos = math.max(maxpos, position)
out = v.mainsnak.datavalue.value
else
-- not a wikibase-item or a string!
end
else
-- code here if we want to return something when author is "unknown"
if v.qualifiers and v.qualifiers.P1932 then
label = v.qualifiers.P1932.datavalue.value
else
label = i18n .. (i18n or "")
end
maxpos = maxpos + 1
out = 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: arguments:
nl_type - type of name list to fetch: nl_type = 'author' for authors; 'editor' for editors 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 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 qid - value from |qid= parameter; the Q-id of the source (book, etc.) in qid
wdl - value from the |wdlinks= parameter; a boolean passed to enable links to Wikidata when no article exists wdl - value from the |wdl= parameter; a Boolean passed to enable links to Wikidata when no article exists

returns nothing; modifies the args table returns nothing; modifies the args table

]=] ]=]


local function get_name_list (nl_type, args, qid, wdl) local function get_name_list (nl_type, args, qid, wdl)
local propertyID = "P50" local propertyID = "P50"
local fallbackID = "P2093" -- author name string

if 'author' == nl_type then
if nl_type =="author" then
propertyID = 'P50'; -- for authors
propertyID = 'P50' -- for authors
elseif 'editor' == nl_type then
propertyID = 'P98'; -- for editors 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 else
return; -- not specified so return return -- not specified so return
end end

-- wdlinks is a boolean passed to enable links to Wikidata when no article exists -- 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 "false" or "no" or "0" is passed set it false
-- if nothing or an empty string is passed set it false -- if nothing or an empty string is passed set it false
if wdl and (#wdl > 0) then if wdl and (#wdl > 0) then
wdl = wdl:lower() wdl = wdl:lower()
if (wdl == "false") or (wdl == "no") or (wdl == "0") then wdl = in_array (wdl, {"false", "no", "0"})
wdl = false
else
wdl = true
end
else else
-- wdl is empty, so -- wdl is empty, so
wdl = false wdl = false
end end

local entity, props local props = nil
local entity = mw.wikibase.getEntity(qid) local fallback = nil
if mw.wikibase.entityExists(qid) then
if entity and entity.claims then
props = entity.claims props = mw.wikibase.getAllStatements(qid, propertyID)
if props and fallbackID then
else
fallback = mw.wikibase.getAllStatements(qid, fallbackID)
-- there's no such entity or no claims for the entity
end
return nil
end end

-- Make sure it actually has the property requested -- Make sure it actually has at least one of the properties requested
if not props or not props then if not (props and props) and not (fallback and fallback) then
return nil return nil
end end

-- So now we have something to return: -- So now we have something to return:
-- table 'out' is going to store the names(s): -- table 'out' is going to store the names(s):
Line 71: Line 289:
local out = {} local out = {}
local link = {} local link = {}
local maxpos = 0
if props.mainsnak.datavalue.type == "wikibase-entityid" then
if props and props then
-- it's wiki-linked value, so output as link if possible
local maxpos = 0
for k, v in pairs(props) do for k, v in pairs(props) do
maxpos = makelink(v, out, link, maxpos, wdl)
local qnumber = "Q" .. v.mainsnak.datavalue.value
local sitelink = mw.wikibase.sitelink(qnumber)
local label = mw.wikibase.label(qnumber)
if label then
label = mw.text.nowiki(label)
else
label = qnumber
end
local position = maxpos + 1 -- Default to 'next' author.
-- todo: use P1545 (series ordinal) instead of default position.
maxpos = math.max(maxpos, position)
if sitelink then
-- just the plain name,
-- but keep a record of the links, using the same index
out = label
link = sitelink
else
-- no sitelink, so check first for a redirect with that label
-- this code works, but causes the article to appear in WhatLinksHere for the possible destination, so remove
-- local artitle = mw.title.new(label, 0)
-- if artitle.id > 0 then
-- if artitle.isRedirect then
-- no sitelink,
-- but there's a redirect with the same title as the label;
-- so store the link to that
-- out = label
-- link = label
-- else
-- no sitelink and not a redirect but an article exists with the same title as the label
-- that's probably a dab page, so output the plain label
-- out = label
-- end
--else
-- no article or redirect with the same title as the label
if wdl then
-- show that there's a Wikidata entry available
out = " .. "|" .. label .. "]]&nbsp;<span title='" .. i18n .. "'>]</span>"
else
-- no wikidata links wanted, so just give the plain label
out = label
end
-- end
end
end end
else
-- not a linkable article title
out = entity:formatPropertyValues(propertyID).value
end end
if fallback and fallback then
-- second properties
for k, v in pairs(fallback) do
maxpos = makelink(v, out, link, maxpos, wdl)
end
end

-- if there's anything to return, then insert the additions in the template arguments table -- if there's anything to return, then insert the additions in the template arguments table
-- in the form |author1=firstname secondname |author2= ... -- in the form |author1=firstname secondname |author2= ...
-- Renumber, in case we have inconsistent numbering
if #out > 0 then
local keys = {}
for k,v in ipairs(out) do -- authorn or editorn
for k, v in pairs(out) do
args = v
keys = k
end
table.sort(keys) -- as they might be out of order
for i, k in ipairs(keys) do
out = out:gsub ('&#39;', '\''); -- prevent cs1|2 multiple names categorization; replace html entity with the actual character
mw.log(i .. " " .. k .. " " .. (out))
if args then -- name gets overwritten
-- pull corresponding -link only if overwritten name is same as WD name
if link and (args == out) then
args = args or link -- author-linkn or editor-linkn
end
else -- name does not get overwritten, so pull name from WD
args = out
if link then
args = args or link -- author-linkn or editor-linkn
end
end end
end
for k,v in pairs(link) do -- author-linkn or editor-linkn
end
args = v

-- gets language codes used for a monolingual text property as a table
function p._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 = v.mainsnak.datavalue.value.language
end end
end end
return out
end
function p.getLangOfProp(frame)
local pid = frame.args.pid or mw.text.trim(frame.args or "")
if pid == "" then return end
local qid = frame.args.qid
if qid == "" then qid = nil end
return table.concat(p._getLangOfProp(qid, pid), ", ")
end end


-- gets the language codes of a Wikidata entry as a table
local function _lang_code(qid)
local lc = getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P424", ps = 1} )
if lc then return mw.text.split( lc, "+" ) end
lc = getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P218", ps = 1} )
if lc then return mw.text.split( lc, "+" ) end
return p._getLangOfProp(qid, "P1476")
end
function p.lang_code(frame)
return table.concat(_lang_code(frame.args.qid or mw.text.trim(frame.args or "")), ", ")
end


-- export for debug
--[[-------------------------< C I T E _ Q >------------------------------------------------------------------
function p.getPropOfProp(frame)
return getPropOfProp(frame.args)
end


-- wraps a string in nowiki unless disable flag is set
Takes standard cs1|2 template parameters and passes all to {{citation}}. If neither of |author= and |author1=
local function wrap_nowiki(str, disable)
are set, calls get_authors() to try to get an author name-list from wikidata. The result is passed to
if disable then return str or '' end
return mw.text.nowiki(str or '')
end

-- sort sequence table whose values are key-value pairs by key
local function comp_key(a, b)
return a < b
end

-- sort sequence table whose values are key-value pairs by value
local function comp_val(a, b)
return a < b
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. {{citation}} for rendering.
--]]
function p._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
local wdl = citeq_args.wdl
local template = citeq_args.template
citeq_args.expand = nil
citeq_args = nil
citeq_args.qid = nil
citeq_args.wdl = nil
citeq_args.template = nil


-- if title supplied, flag to not read html title
function citeq.cite_q (frame)
local citeq_args = {}; local titleforced = (citeq_args.title ~= nil)
local qid;
local wdl;


local pframe = frame:getParent() local oth = {}
local args = pframe.args; -- first get parent frame arguments - these from the template call


-- put the language codes into a sequential table langcodes
for k, v in pairs (args) do -- copy named parameters and their values into citeq_args
local langcodes = {}
if type( k ) == 'string' then -- numbered parameters ignored
if citeq_args.language then
citeq_args = v;
-- check these are a supported language codes
for lc in mw.text.gsplit( citeq_args.language, "+", false ) do
langcodes = mw.language.isSupportedLanguage(citeq_args.language) and citeq_args.language
end end
end end
if not langcodes then
-- try to find language of work
args = frame.args; -- now get frame arguments (from the template wikisource)
langcodes = _lang_code(qid)
end
if not langcodes 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, ", ")


for k, v in pairs (args) do -- copy args into citeq_args -- loop through list of simple properties and get their values in citeq_args
for name, data in pairs(simple_properties) do
if 'qid' == k then -- don't copy qid
citeq_args = getValue( {data.id, fwd = "ALL", osd = "no", noicon = "true", qid = qid, maxvals = data.maxvals, linked = data.linked, rank = data.rank or "best", citeq_args } )
qid = v; -- save its value
if data.populate_from_journal then
elseif 'wdlinks' == k then -- don't copy wdlinks
local publishedin = getValue( {"P1433", ps = 1, qid = qid, maxvals = 0, citeq_args, qual = data.id, qualsonly = 'yes'} )
wdl = v; -- save its value
citeq_args = publishedin or getPropOfProp({qid = qid, prop1 = "P1433", prop2 = data.id, maxvals = data.maxvals, ps = 1})
end
if citeq_args and citeq_args:find(']', 1, true) then
-- try fallback to work's native language
citeq_args = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = langcodes } )
if citeq_args:find('^Q%d+$') then -- qid was returned
-- try fallback to qid's native language
local qid_languages = _lang_code(citeq_args)
citeq_args = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = qid_languages } )
if citeq_args:find('^Q%d+$') then -- qid was returned again
citeq_args = nil
else
-- record the language found if no lang specified
citeq_args.language = citeq_args.language or qid_languages
end
end
end
if data.others then
oth = citeq_args and (name:gsub("^%l", string.upper) .. ": " .. citeq_args)
citeq_args = nil
end
end

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 )
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 = arcurl
citeq_args = tonumber(arcd) .. " " .. i18n.months .. " " .. 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

if not titleforced then
-- Handle subtitle.
if citeq_args.title then
local subtitle = mw.wikibase.getBestStatements (qid, 'P1680');
if 0 ~= #subtitle then
subtitle = subtitle.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 else
local title_display = citeq_args.title
citeq_args = v -- but copy everything else
or mw.wikibase.getLabel(qid)
or (langcodes and mw.wikibase.getLabelByLang(qid, langcodes))
or ("No label or title -- debug: " .. qid)
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, "enwikisource") 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 = ']'
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 = ']'
else -- piped link
citeq_args.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, "%d+", "")
if count == 1 then
citeq_args.page = citeq_args.pages
citeq_args.pages = nil
end end
end end


if is_set (qid) then if is_set (qid) then
if not is_set (citeq_args.author) and not is_set (citeq_args.author1) then -- if neither are set, try to get authors from wikidata if not is_set (citeq_args.author) and not is_set (citeq_args.author1)
get_name_list ('author', citeq_args, qid, wdl); -- modify citeq_args table with authors from wikidata 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) and not is_set (citeq_args) and not is_set (citeq_args)
and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args) then -- if neither are set, try to get authors from Wikidata
get_name_list ('author', citeq_args, qid, wdl) -- modify citeq_args table with authors from Wikidata
end end


if not is_set (citeq_args.editor) and not is_set (citeq_args.editor1) then -- if neither are set, try to get editors from wikidata if not is_set (citeq_args.editor) and not is_set (citeq_args.editor1)
and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args)
get_name_list ('editor', citeq_args, qid, wdl); -- modify citeq_args table with editors from wikidata
and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args) then -- if neither are set, try to get editors from Wikidata
get_name_list ('editor', citeq_args, qid, wdl) -- modify citeq_args table with editors from Wikidata
end

if not is_set (citeq_args.translator) and not is_set (citeq_args.translator1)
and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args)
and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args) then -- if neither are set, try to get translators from Wikidata
get_name_list ('translator', citeq_args, qid, wdl) -- modify citeq_args table with translators from Wikidata
end end
end end


for k, v in pairs(citeq_args) do
return frame:expandTemplate{title = 'citation', args = citeq_args}; -- render the citation
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 = nil
elseif in_array (v, {'((unset))', '((ignore))'}) then -- strip off markup for free-text values clashing with local keywords
citeq_args = 'unset'
end
end

local author_count = 0
for k, v in pairs(citeq_args) do
if k:find("^author%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 then
citeq_args = nil; -- unset because no longer needed
else
citeq_args = citeq_args 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+$") 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 then
citeq_args = nil; -- unset because no longer needed
else
citeq_args = citeq_args or 3 -- limit to three displayed names
end
end

-- change edition to ordinal if it's set and numeric
citeq_args.edition = citeq_args.edition and p.makeOrdinal(citeq_args.edition)

-- code to make a guess what template to use from the supplied parameters
-- (first draft for proof-of-concept)
if citeq_args.isbn then
template = template or "book"
citeq_args.asin = nil -- suppress ASIN if ISBN exists
elseif citeq_args.journal then
template = template or "journal"
elseif citeq_args.website then
template = template or "web"
end

-- template is CS1 designator: journal, web, news, etc.
if template then
-- citeq_args.mode = citeq_args.mode or "cs1" -- a cs1 template already knows that it is cs1 so this line is superfluous
template = "Cite " .. template
else
-- citeq_args.mode = citeq_args.mode or "cs2" -- a cs2 template already knows that it is cs2 so this line is superfluous
template = "Citation"
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 = ']&nbsp;]' -- go through "WDQ (identifier)" redirect to reduce clutter in "What links here" and improve reverse lookup. Keep in sync with {{QID}}.
local old_id = citeq_args.id
if wdl then -- show WD logo
id = id .. ']' -- 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

-- clean up any blank parameters
for k, v in pairs(citeq_args) do
if v == "" then citeq_args = nil 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 } -- init with citation template
if expand == "self" then
citeq_args.id = old_id -- restore original |id= parameter
expand_args = { "{{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 .. '=' .. val)
end
-- make the nowiki'd string and done
return frame:preprocess (table.concat ({'<syntaxhighlight lang="wikitext" inline="1">', table.concat (expand_args, ' |') .. '}}', '</syntaxhighlight>'}));
end

local erratumid = getPropertyIDs( { "P2507", qid = qid, fwd = "ALL", osd = "no", rank = "best", maxvals = 1 } )
if erratumid then
erratumid = " ]" .. "]"
else
erratumid = ""
end

local opt_cat = ''
if getValue( {"P5824", ps = 1, qid = qid} ) then
opt_cat = ']<!-- retracted -->'
end
if getValue( {"P1366", ps = 1, qid = qid} ) then
opt_cat = opt_cat .. ']<!-- replaced -->'
end
return frame:expandTemplate{title = template, args = citeq_args} .. erratumid .. opt_cat -- render the template
end

function p.cite_q (frame)
local args = {}
for k, v in pairs(frame:getParent().args) do
if v ~= "" then args = v end
end
for k, v in pairs(frame.args) do
if v ~= "" then args = v end
end
args.qid = args.qid or args or ""
if args.qid == "" then return nil end
args = 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 = v
end
end
newargs.qid = q
if tag == "ref" then
cites = frame:callParserFunction{ name = "#tag:ref", args = { p._cite_q(newargs), name = q } }
-- expand like this: args = { p._cite_q(newargs), name = 'foo', group = 'bar' }
else
cites = p._cite_q(newargs)
end
end

if list then
return frame:expandTemplate{ title = list, args = cites }
else
return table.concat(cites, citesep)
end
end end


return citeq return p

Latest revision as of 13:32, 5 July 2024

Module documentation[view] [edit] [history] [purge]
WarningThis Lua module is used on approximately 53,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.

Implements {{Cite Q}}

Test cases at:

The above documentation is transcluded from Module:Cite Q/doc. (edit | history)
Editors can experiment in this module's sandbox (edit | diff) and testcases (create) pages.
Subpages of this module.

-- Version: 2021-10-19

local p = {}

require('strict')
local wdib = require('Module:WikidataIB')
local getValue = wdib._getValue
local getPropOfProp = wdib._getPropOfProp
local followQid = wdib._followQid
local getPropertyIDs = wdib._getPropertyIDs

local i18n = {
	 = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper),
	 = "]",
	 = {
		 = "st",
		 = "nd",
		 = "rd",
		 = "th"
	},
	 = {
		"January", "February", "March", "April", "May", "June",
		"July", "August", "September", "October", "November", "December"
	},
}

-------------------------------------------------------------------------------
-- makeOrdinal needs to be internationalised along with the above i18n
-- 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.
-------------------------------------------------------------------------------
p.makeOrdinal = function(cardinal)
	local card = tonumber(cardinal)
	if not card then return cardinal end
	local ordsuffix = i18n.ordinal.default
	if card % 10 == 1 then
		ordsuffix = i18n.ordinal
	elseif card % 10 == 2 then
		ordsuffix = i18n.ordinal
	elseif card % 10 == 3 then
		ordsuffix = i18n.ordinal
	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 = i18n.ordinal.default
	end
	return card .. ordsuffix
end

-- Table of simple properties that can be fetched in roughly the same way:
-- id = PXXX
-- maxvals = maximum number of multiple values (0 for all)
-- linked = "no" suppresses linking
-- populate_from_journal = true/false determines whether to look in a journal where the source is published
-- rank = "best", "preferred", normal, etc. determines how Wikidata ranks are treated
-- others = true - the value for the property goes to "others" section
local simple_properties = {
	publisher = {id = "P123", maxvals = 1},
	oclc = {id = "P243", maxvals = 1},
	 = {id = "P291", maxvals = 0, linked = 'no'}, -- publication place (don't put into |place=; is treated specially in {{citation}} if both are given)
	doi = {id = "P356", maxvals = 1}, -- take care of |doi-broken-date= (WD "reason for deprecation"/"stated as") and |doi-access= (WD "access status")?
	issue = {id = "P433", maxvals = 0, populate_from_journal = true}, -- distinguish from |number= ("P1545"?) if both are given (still blocked by {{citation}}, but will be supported in the future)
	pmid = {id = "P698", maxvals = 1},
--	gbooks = {id = "P675", maxvals = 1}, -- to be added to {{citation}}
--	ia = {id = "P724", maxvals = 1}, -- to be added to {{citation}}
	arxiv = {id = "P818", maxvals = 1},
	bibcode = {id = "P819", maxvals = 1}, -- take care of |bibcode-access=?
	jstor = {id = "P888", maxvals = 1}, -- take care of |jstor-access=?
	mr = {id = "P889", maxvals = 1},
	rfc = {id = "P892", maxvals = 1},
	zbl = {id = "P894", maxvals = 1},
	ssrn = {id = "P893", maxvals = 1},
	place = {id = "P1071", maxvals = 0, linked = 'no'}, -- written-at place
--	 = {id = "P1104", maxvals = 0, linked = 'no'}, -- to be added to {{citation}} / COinS &rft.tpages=
--	coden = {id = "P1159", maxvals = 1}, -- to be added to {{citation}} / COinS &rft.coden=
	s2cid = {id = "P8299", maxvals = 1}, -- take care of |s2cid-access=?
	pmc = {id = "P932", maxvals = 1}, -- take care of |pmc-embargo-date= (WD "reason for deprecation")?
	lccn = {id = "P1144", maxvals = 1},
	hdl = {id = "P1184", maxvals = 1}, -- take care of |hdl-access=?
	ismn = {id = "P1208", maxvals = 1},
	journal = {id = "P1433", maxvals = 1},
	citeseerx = {id = "P3784", maxvals = 1},
	osti = {id = "P3894", maxvals = 1}, -- take care of |osti-access=?
	biorxiv = {id = "P3951", maxvals = 1},
	asin = {id = "P5749", maxvals = 1}, -- What about |asin-tld=? (WD examples resolve to .com at present, but may change)
--	 = {id = "P528", maxvals = 0}, -- to be added to {{citation}} / COinS &rft.artnum=
	isbn = {id = "P212", maxvals = 1, populate_from_journal = true}, -- ISBN 13
	issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues?
--	jfm = {id = "P?", maxvals = 1}, -- Jahrbuch über die Fortschritte der Mathematik (not Zbl)
--	sbn = {id = "P?", maxvals = 1}, -- Standard Book Number (predecessor of ISBN, not ICCU)
--	message-id = {id = "P?", maxvals = 1}, -- Usenet message ID
	chapter = {id = "P792", maxvals = 1},
	 = {id = "P577", maxvals = 1, populate_from_journal = true}, -- publication date (don't use |date=; is treated specially in {{citation}} if both are given.)
	series = {id = "P179", maxvals = 1, populate_from_journal = true},
	version = {id = "P348", maxvals = 0},
	edition = {id = "P393", maxvals = 0},
	volume = {id = "P478", maxvals = 0, populate_from_journal = true},
--	part = {id = "P1545"?, maxvals = 0}, --  to be added to {{citation}} / COinS &rft.part=
	title = {id = "P1476", rank="p n"},
--	url = {id = "P953", maxvals = 1}, -- deal with this along with archive-url
	pages = {id = "P304", maxvals = 0, populate_from_journal = true},
	at = {id = "P958", maxvals = 0, populate_from_journal = true}, -- also incorporate lines (P7421) and columns (P3903) into this (cite map also supports |section=)
--	sheets = {id = "P7416", maxvals = 0, populate_from_journal = true},
--	interviewer = {id = "P?", maxvals = 0}, -- does **not** go to "others" section! Multiple interviewers should be n-enumerated
	illustrator = {id = "P110", maxvals = 10, others = true}, -- goes to "others" section
-- foreword and afterword, when contributions to another author's work, are contributions so belong in |contribution=;
-- the writer's name goes in |contributor=; requires |title= and |author=
-- However, this might need to add support for multiple contributors and their roles to {{citation}}, see Help_talk:Citation_Style_1#Others
--	foreword = {id = "P2679", maxvals = 10, others = true}, -- goes to "others" section
--	afterword = {id = "P2680", maxvals = 10, others = true}, -- goes to "others" section
	composer = {id = "P86", maxvals = 10, others = true}, -- goes to "others" section
	animator = {id = "P6942", maxvals = 10, others = true}, -- goes to "others" section
	director = {id = "P57", maxvals = 10, others = true}, -- goes to "others" section
	screenwriter = {id = "P58", maxvals = 10, others = true}, -- goes to "others" section
	signatory = {id = "P1891", maxvals = 10, others = true}, -- goes to "others" section
	presenter = {id = "P371", maxvals = 10, others = true}, -- goes to "others" section
	performer = {id = "P175", maxvals = 10, others = true}, -- goes to "others" section
}

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


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

-- function to fetch a value to display
local function makelink(v, out, link, maxpos, wdl)
	local label
	if v.mainsnak.snaktype == "value" then
		if v.mainsnak.datatype == "wikibase-item" then
			local qnumber = v.mainsnak.datavalue.value.id
			local sitelink = mw.wikibase.getSitelink(qnumber)
			if qnumber == "Q2818964" then sitelink = nil end -- suppress link to "Various authors"
			if v.qualifiers and v.qualifiers.P1932 then
				label = v.qualifiers.P1932.datavalue.value
			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 and v.qualifiers and v.qualifiers then
				position = tonumber(v.qualifiers.datavalue.value)
			end
			maxpos = math.max(maxpos, position)
			if sitelink then
				-- just the plain name,
				-- but keep a record of the links, using the same index
				out = label
				link = sitelink
			else
				if wdl then
					-- show that there's a Wikidata entry available
					out = " .. "|" .. label .. "]]&nbsp;<span title='" .. i18n .. "'>]</span>"
				else
					-- no Wikidata links wanted, so just give the plain label
					out = label
				end
			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 and v.qualifiers and v.qualifiers then
				position = tonumber(v.qualifiers.datavalue.value)
			end
			maxpos = math.max(maxpos, position)
			out = v.mainsnak.datavalue.value
		else
			-- not a wikibase-item or a string!
		end
	else
		-- code here if we want to return something when author is "unknown"
		if v.qualifiers and v.qualifiers.P1932 then
			label = v.qualifiers.P1932.datavalue.value
		else
			label = i18n .. (i18n or "")
		end
		maxpos = maxpos + 1
		out = 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 nothing; modifies the args table
]=]

local function get_name_list (nl_type, args, qid, wdl)
	local propertyID = "P50"
	local fallbackID = "P2093" -- author name string

	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) and not (fallback and fallback) then
		return nil
	end

	-- So now we have something to return:
	-- table 'out' is going to store the names(s):
	-- and table 'link' will store any links to the name's article
	local out = {}
	local link = {}
	local maxpos = 0
	if props and props then
		for k, v in pairs(props) do
			maxpos = makelink(v, out, link, maxpos, wdl)
		end
	end
	if fallback and fallback then
		-- second properties
		for k, v in pairs(fallback) do
			maxpos = makelink(v, out, link, maxpos, wdl)
		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 = k
	end
	table.sort(keys) -- as they might be out of order
	for i, k in ipairs(keys) do
		out = out:gsub ('&#39;', '\'');									-- prevent cs1|2 multiple names categorization; replace html entity with the actual character
		mw.log(i .. " " .. k .. " " .. (out))
		if args then -- name gets overwritten
			-- pull corresponding -link only if overwritten name is same as WD name
			if link and (args == out) then
				args = args or link -- author-linkn or editor-linkn
			end
		else -- name does not get overwritten, so pull name from WD
			args = out
			if link then
				args = args or link -- author-linkn or editor-linkn
			end
		end
	end
end

-- gets language codes used for a monolingual text property as a table
function p._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 = v.mainsnak.datavalue.value.language
		end
	end
	return out
end
function p.getLangOfProp(frame)
	local pid = frame.args.pid or mw.text.trim(frame.args or "")
	if pid == "" then return end
	local qid = frame.args.qid
	if qid == "" then qid = nil end
	return table.concat(p._getLangOfProp(qid, pid), ", ")
end

-- gets the language codes of a Wikidata entry as a table
local function _lang_code(qid)
	local lc = getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P424", ps = 1} )
	if lc then return mw.text.split( lc, "+" ) end
	lc = getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P218", ps = 1} )
	if lc then return mw.text.split( lc, "+" ) end
	return p._getLangOfProp(qid, "P1476")
end
function p.lang_code(frame)
	return table.concat(_lang_code(frame.args.qid or mw.text.trim(frame.args or "")), ", ")
end

-- export for debug
function p.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

-- sort sequence table whose values are key-value pairs by key
local function comp_key(a, b)
	return a < b
end

-- sort sequence table whose values are key-value pairs by value
local function comp_val(a, b)
	return a < b
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.
--]]
function p._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
	local wdl = citeq_args.wdl
	local template = citeq_args.template
	citeq_args.expand = nil
	citeq_args = 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 = mw.language.isSupportedLanguage(citeq_args.language) and citeq_args.language
		end
	end
	if not langcodes then
		-- try to find language of work
		langcodes = _lang_code(qid)
	end
	if not langcodes 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(simple_properties) do
		citeq_args = getValue( {data.id, fwd = "ALL", osd = "no", noicon = "true", qid = qid, maxvals = data.maxvals, linked = data.linked, rank = data.rank or "best", citeq_args } )
		if data.populate_from_journal then
			local publishedin = getValue( {"P1433", ps = 1, qid = qid, maxvals = 0, citeq_args, qual = data.id, qualsonly = 'yes'} )
			citeq_args = publishedin or getPropOfProp({qid = qid, prop1 = "P1433", prop2 = data.id, maxvals = data.maxvals, ps = 1})
		end
		if citeq_args and citeq_args:find(']', 1, true) then
			-- try fallback to work's native language
			citeq_args = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = langcodes } )
			if citeq_args:find('^Q%d+$') then -- qid was returned
				-- try fallback to qid's native language
				local qid_languages = _lang_code(citeq_args)
				citeq_args = getValue( {data.id, ps = 1, qid = qid, maxvals = data.maxvals, linked = "no", lang = qid_languages } )
				if citeq_args:find('^Q%d+$') then -- qid was returned again
					citeq_args = nil
				else
					-- record the language found if no lang specified
					citeq_args.language = citeq_args.language or qid_languages
				end
			end
		end
		if data.others then
			oth = citeq_args and (name:gsub("^%l", string.upper) .. ": " .. citeq_args)
			citeq_args = nil
		end
	end

	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 )
				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 = arcurl
						citeq_args = tonumber(arcd) .. " " .. i18n.months .. " " .. 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

	if not titleforced then
		-- Handle subtitle.
		if citeq_args.title then
			local subtitle = mw.wikibase.getBestStatements (qid, 'P1680');
			if 0 ~= #subtitle then
				subtitle = subtitle.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 and mw.wikibase.getLabelByLang(qid, langcodes))
				or ("No label or title -- debug: " .. qid)
			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, "enwikisource") 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 = ']'
						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 = ']'
					else -- piped link
						citeq_args.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, "%d+", "")
		if count == 1 then
			citeq_args.page = citeq_args.pages
			citeq_args.pages = nil
		end
	end

	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) and not is_set (citeq_args) and not is_set (citeq_args)
			and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args) then	-- if neither are set, try to get authors from Wikidata
			get_name_list ('author', citeq_args, qid, wdl)				-- modify citeq_args table with authors from Wikidata
		end

		if not is_set (citeq_args.editor) and not is_set (citeq_args.editor1)
			and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args)
			and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args) then	-- if neither are set, try to get editors from Wikidata
			get_name_list ('editor', citeq_args, qid, wdl)				-- modify citeq_args table with editors from Wikidata
		end

		if not is_set (citeq_args.translator) and not is_set (citeq_args.translator1)
			and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args)
			and not is_set (citeq_args) and not is_set (citeq_args) and not is_set (citeq_args) then	-- if neither are set, try to get translators from Wikidata
			get_name_list ('translator', citeq_args, qid, wdl)			-- modify citeq_args table with translators from Wikidata
		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 = nil
		elseif in_array (v, {'((unset))', '((ignore))'}) then -- strip off markup for free-text values clashing with local keywords
			citeq_args = 'unset'
		end
	end

	local author_count = 0
	for k, v in pairs(citeq_args) do
		if k:find("^author%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 then
			citeq_args = nil;								-- unset because no longer needed
		else
			citeq_args = citeq_args 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+$") 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 then
			citeq_args = nil;								-- unset because no longer needed
		else
			citeq_args = citeq_args or 3	-- limit to three displayed names
		end
	end

	-- change edition to ordinal if it's set and numeric
	citeq_args.edition = citeq_args.edition and p.makeOrdinal(citeq_args.edition)

	-- code to make a guess what template to use from the supplied parameters
	-- (first draft for proof-of-concept)
	if citeq_args.isbn then
		template = template or "book"
		citeq_args.asin = nil -- suppress ASIN if ISBN exists
	elseif citeq_args.journal then
		template = template or "journal"
	elseif citeq_args.website then
		template = template or "web"
	end

	-- template is CS1 designator: journal, web, news, etc.
	if template then
--		citeq_args.mode = citeq_args.mode or "cs1"								-- a cs1 template already knows that it is cs1 so this line is superfluous
		template = "Cite " .. template
	else
--		citeq_args.mode = citeq_args.mode or "cs2"								-- a cs2 template already knows that it is cs2 so this line is superfluous
		template = "Citation"
	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 = ']&nbsp;]' -- go through "WDQ (identifier)" redirect to reduce clutter in "What links here" and improve reverse lookup. Keep in sync with {{QID}}.
	local old_id = citeq_args.id
	if wdl then -- show WD logo
		id = id .. ']' -- 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

	-- clean up any blank parameters
	for k, v in pairs(citeq_args) do
		if v == "" then citeq_args = nil 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 }		-- init with citation template
		if expand == "self" then
			citeq_args.id = old_id -- restore original |id= parameter
			expand_args = { "{{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 .. '=' .. val)
		end
		-- make the nowiki'd string and done
		return frame:preprocess (table.concat ({'<syntaxhighlight lang="wikitext" inline="1">', table.concat (expand_args, ' |') .. '}}', '</syntaxhighlight>'}));
	end

	local erratumid = getPropertyIDs( { "P2507", qid = qid, fwd = "ALL", osd = "no", rank = "best", maxvals = 1 } )
	if erratumid then
		erratumid = " ]" .. "]"
	else
		erratumid = ""
	end

	local opt_cat = ''
	if getValue( {"P5824", ps = 1, qid = qid} ) then
		opt_cat = ']<!-- retracted -->'
	end
	if getValue( {"P1366", ps = 1, qid = qid} ) then
		opt_cat = opt_cat .. ']<!-- replaced -->'
	end
	return frame:expandTemplate{title = template, args = citeq_args} .. erratumid .. opt_cat -- render the template
end

function p.cite_q (frame)
	local args = {}
	for k, v in pairs(frame:getParent().args) do
		if v ~= "" then args = v end
	end
	for k, v in pairs(frame.args) do
		if v ~= "" then args = v end
	end
	args.qid = args.qid or args or ""
	if args.qid == "" then return nil end
	args = 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 = v
			end
		end
		newargs.qid = q
		if tag == "ref" then
			cites = frame:callParserFunction{ name = "#tag:ref", args = { p._cite_q(newargs), name = q } }
			-- expand like this: args = { p._cite_q(newargs), name = 'foo', group = 'bar' }
		else
			cites = p._cite_q(newargs)
		end
	end

	if list then
		return frame:expandTemplate{ title = list, args = cites }
	else
		return table.concat(cites, citesep)
	end
end

return p