Documentation for this module may be created at Module:Cite Q/sandbox/doc

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/sandbox')
local cs1_module = 'Module:Citation/CS1/sandbox'								-- don't load the module yet, may not be needed
local cs1_cfg = mw.loadData('Module:Citation/CS1/Configuration/sandbox')
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]]&nbsp;<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('&#39;', '\'');									-- 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  -- checks for author (P50) vs. author name string (P2093)
				-- 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));
					tableset_add(opt_cat, cfg.i18n_t.trackingcats['missing-author-first-last-names'])
				end
			else
				tableset_add(opt_cat, cfg.i18n_t.trackingcats['missing-authorstring-first-last-names'])
				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
			-- 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));
				tableset_add(opt_cat, cfg.i18n_t.trackingcats['missing-author-first-last-names'])
			end
		end
	end
	for i, translator in pairs(names.translators or {}) do
		if not translator.last 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));
				tableset_add(opt_cat, cfg.i18n_t.trackingcats['missing-author-first-last-names'])
			end
		end
	end

	-- add display error if name errors found
	if #missing_names > 0 and not in_array('missing-author-first-last-names', opt_cat) then
		-- 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.. '&nbsp;[[: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
}