From 2ed4bf2b7ca47f8b2089b8467af794492cf4a7ff Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Mon, 21 Sep 2026 18:33:10 +1200 Subject: [PATCH 1/5] Fetch given/family names from author, editor, or qualifiers on name strings --- module_citeq.lua | 107 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 17 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index fe1f6fd..959332a 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -88,8 +88,16 @@ local function makelink(v, out, link, maxpos, wdl) 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" + + -- Last name, first name(s) + local last_name = getValue({"P734", qid=qnumber, maxvals=1, ps=2}) + local first_names = getValue({"P735", qid=qnumber, maxvals=0, ps=2}) + if first_names then + first_names = mw.text.trim(first_names:gsub(', ', ' ')) + end + if v.qualifiers and v.qualifiers.P1932 then - label = v.qualifiers.P1932[1].datavalue.value -- object named as + label = v.qualifiers.P1932[1].datavalue.value -- "object named as" else label = mw.wikibase.getLabel(qnumber) if label then @@ -100,14 +108,22 @@ local function makelink(v, out, link, maxpos, wdl) 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) + 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) if sitelink then -- just the plain name, -- but keep a record of the links, using the same index - out[position] = label + if last_name and first_names then + out[position] = { + label = label, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = label + end link[position] = sitelink else if wdl then @@ -120,24 +136,68 @@ local function makelink(v, out, link, maxpos, wdl) ) else -- no Wikidata links wanted, so just give the plain label - out[position] = label + if last_name and first_names then + out[position] = { + label = label, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = label + end 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["qualifiers"] and v.qualifiers["P1545"] and v.qualifiers["P1545"][1] then - position = tonumber(v.qualifiers["P1545"][1].datavalue.value) + 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) - out[position] = v.mainsnak.datavalue.value + + -- 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 = mw.text.trim(v.qualifiers.P9687[1].datavalue.value:gsub(', ', ' ')) + end + + if last_name and first_names then + out[position] = { + label = v.mainsnak.datavalue.value, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = v.mainsnak.datavalue.value + end else -- not a wikibase-item or a string! end else - -- code here if we want to return something when author is "unknown" + -- try the "object named as" qualifier, otherwise use the unknown author item if v.qualifiers and v.qualifiers.P1932 then - label = v.qualifiers.P1932[1].datavalue.value + label = v.qualifiers.P1932[1].datavalue.value -- "object named as" + + -- 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 = mw.text.trim(v.qualifiers.P9687[1].datavalue.value:gsub(', ', ' ')) + end + if last_name and first_names then + label = { + label = label, + last = last_name or nil, + first = first_names or nil, + } + end else label = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper) .. (cfg.i18n_t.trackingcats["unknown-author"] or "") end @@ -235,15 +295,27 @@ local function get_name_list (nl_type, args, qid, wdl) end table.sort(keys) -- as they might be out of order for i, k in ipairs(keys) do - out[k] = out[k]:gsub (''', '\''); -- prevent cs1|2 multiple names categorization; replace html entity with the actual character - mw.log(i .. " " .. k .. " " .. (out[k])) + local label + if out[k].label then + label = out[k].label + else + label = out[k] + end + label = label:gsub(''', '\''); -- prevent cs1|2 multiple names categorization; replace html entity with the actual character + 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 link[k] and (args[nl_type .. i] == out[k]) then + if link[k] and (args[nl_type .. i] == label) then args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or link[k] -- author-linkn or editor-linkn end else -- name does not get overwritten, so pull name from WD - args[nl_type .. i] = out[k] + 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] = label + end if link[k] then args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or link[k] -- author-linkn or editor-linkn end @@ -273,11 +345,12 @@ 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} ) + -- 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} ) + 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") + 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 "")), ", ") From 1df4c6cb25882b608971996a7f12bfcb9fe1d18c Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Thu, 24 Sep 2026 14:31:00 +1200 Subject: [PATCH 2/5] Initial support for chapters from P31 instance-of to Cite book --- module_citeq.lua | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 959332a..1b8d366 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -375,8 +375,11 @@ local function yearFromDate(datestr) return datestr end --- Attempt to determine heuristically if we're citing a news article -local function isNewsArticle(qid, args) +-- 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 @@ -395,7 +398,9 @@ local function isNewsArticle(qid, args) } -- 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 true + 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') @@ -403,12 +408,14 @@ local function isNewsArticle(qid, args) 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 true + return 'news' + elseif in_array(v.mainsnak.datavalue.value.id, chapteritems) then + return 'chapter' end end end end - return false + return '' end -- Figure out a URL access value, from the "online access status" (P6954) qualifier of an identifier @@ -775,14 +782,20 @@ local function _cite_q (citeq_args) -- code to make a guess what template to use from the supplied parameters -- (first draft for proof-of-concept) local pubyear = yearFromDate(citeq_args['publication-date']) - local is_news = isNewsArticle(qid, citeq_args) - if is_news then + 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'] From 6f13edba6e269f543308835ea19d26bac9f243fb Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sat, 26 Sep 2026 00:23:35 +1200 Subject: [PATCH 3/5] Fix author item qualifiers, Vancouver/APA name style --- module_citeq.lua | 53 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 1b8d366..985a482 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -80,8 +80,31 @@ local function accept_value( param_val, wd_val ) return val end +-- Split string of comma-separated first names into table of initials +local function getInitials(str) + local initials = {} + for _, word in pairs(mw.text.split(str, ' ', true)) do + local initial = mw.text.trim(word) + if #initial > 0 then + table.insert(initials, initial:sub(1, 1):upper()) + end + end + return initials +end + +-- Return initials given a style (e.g. Vancouver or APA) +local function getStyledNames(names, style) + if style == 'Vancouver' then + return table.concat(getInitials(names)) + elseif style == 'APA' then + return table.concat(getInitials(names), '. ') .. '.' + else + return mw.text.trim(names:gsub(', ', ' ')) + end +end + -- function to fetch a value to display -local function makelink(v, out, link, maxpos, wdl) +local function makelink(v, out, link, maxpos, wdl, namestyle) local label if v.mainsnak.snaktype == "value" then if v.mainsnak.datatype == "wikibase-item" then @@ -89,11 +112,20 @@ local function makelink(v, out, link, maxpos, wdl) local sitelink = mw.wikibase.getSitelink(qnumber) if qnumber == "Q2818964" then sitelink = nil end -- suppress link to "Various authors" - -- Last name, first name(s) - local last_name = getValue({"P734", qid=qnumber, maxvals=1, ps=2}) - local first_names = getValue({"P735", qid=qnumber, maxvals=0, ps=2}) + -- 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 + 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}) if first_names then - first_names = mw.text.trim(first_names:gsub(', ', ' ')) + first_names = getStyledNames(first_names, namestyle) end if v.qualifiers and v.qualifiers.P1932 then @@ -162,7 +194,7 @@ local function makelink(v, out, link, maxpos, wdl) last_name = v.qualifiers.P9688[1].datavalue.value end if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then - first_names = mw.text.trim(v.qualifiers.P9687[1].datavalue.value:gsub(', ', ' ')) + first_names = getStyledNames(v.qualifiers.P9687[1].datavalue.value, namestyle) end if last_name and first_names then @@ -189,7 +221,7 @@ local function makelink(v, out, link, maxpos, wdl) last_name = v.qualifiers.P9688[1].datavalue.value end if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then - first_names = mw.text.trim(v.qualifiers.P9687[1].datavalue.value:gsub(', ', ' ')) + first_names = getStyledNames(v.qualifiers.P9687[1].datavalue.value, namestyle) end if last_name and first_names then label = { @@ -273,7 +305,7 @@ local function get_name_list (nl_type, args, qid, wdl) 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, link, maxpos, wdl) + maxpos = makelink(v, out, link, maxpos, wdl, args.namestyle) end end end @@ -281,7 +313,7 @@ local function get_name_list (nl_type, args, qid, wdl) -- second properties for k, v in pairs(fallback) do if 'deprecated' ~= v.rank then -- ignore deprecated names - maxpos = makelink(v, out, link, maxpos, wdl) + maxpos = makelink(v, out, link, maxpos, wdl, args.namestyle) end end end @@ -506,6 +538,7 @@ local function _cite_q (citeq_args) citeq_args.qid = nil citeq_args.wdl = nil citeq_args.template = nil + citeq_args.namestyle = citeq_args.namestyle or citeq_args['name-style'] or nil -- if title supplied, flag to not read html title local titleforced = (citeq_args.title ~= nil) @@ -960,6 +993,8 @@ local function _cite_q (citeq_args) -- remove any remaining non-CS1 fields citeq_args['instance-of'] = nil + citeq_args.namestyle = nil + citeq_args['name-style'] = nil -- render the template local lc_template = template and template:lower() or "citation" From 43bf989f9165594963776149538a913224529dcb Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sat, 3 Oct 2026 14:50:48 +1300 Subject: [PATCH 4/5] Use existing name-list-style 'vanc' and add 'apa' handling --- module_citeq.lua | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 985a482..219353c 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -80,7 +80,7 @@ local function accept_value( param_val, wd_val ) return val end --- Split string of comma-separated first names into table of initials +-- Split string of comma-separated first names into a table of initials local function getInitials(str) local initials = {} for _, word in pairs(mw.text.split(str, ' ', true)) do @@ -92,13 +92,14 @@ local function getInitials(str) return initials end --- Return initials given a style (e.g. Vancouver or APA) +-- 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) - if style == 'Vancouver' then - return table.concat(getInitials(names)) - elseif style == 'APA' then + -- 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 @@ -305,7 +306,7 @@ local function get_name_list (nl_type, args, qid, wdl) 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, link, maxpos, wdl, args.namestyle) + maxpos = makelink(v, out, link, maxpos, wdl, args['name-list-style']) end end end @@ -313,7 +314,7 @@ local function get_name_list (nl_type, args, qid, wdl) -- second properties for k, v in pairs(fallback) do if 'deprecated' ~= v.rank then -- ignore deprecated names - maxpos = makelink(v, out, link, maxpos, wdl, args.namestyle) + maxpos = makelink(v, out, link, maxpos, wdl, args['name-list-style']) end end end @@ -538,7 +539,6 @@ local function _cite_q (citeq_args) citeq_args.qid = nil citeq_args.wdl = nil citeq_args.template = nil - citeq_args.namestyle = citeq_args.namestyle or citeq_args['name-style'] or nil -- if title supplied, flag to not read html title local titleforced = (citeq_args.title ~= nil) @@ -993,8 +993,9 @@ local function _cite_q (citeq_args) -- remove any remaining non-CS1 fields citeq_args['instance-of'] = nil - citeq_args.namestyle = nil - citeq_args['name-style'] = nil + if citeq_args['name-list-style'] and citeq_args['name-list-style'] == 'apa' then + citeq_args['name-list-style'] = nil + end -- render the template local lc_template = template and template:lower() or "citation" From b2ab54ccb4a2c86999fa4a6e3c081ce0cfb53509 Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Mon, 5 Oct 2026 21:42:08 +1300 Subject: [PATCH 5/5] Fix too many item lookups on items will silly amounts of authors --- module_citeq.lua | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 219353c..ceead63 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -107,6 +107,8 @@ end -- function to fetch a value to display local function makelink(v, out, link, maxpos, wdl, namestyle) local label + local max_authors_displayed = 10 -- TODO: this is probably in CS1 config somewhere + local item_lookup = false if v.mainsnak.snaktype == "value" then if v.mainsnak.datatype == "wikibase-item" then local qnumber = v.mainsnak.datavalue.value.id @@ -123,12 +125,15 @@ local function makelink(v, out, link, maxpos, wdl, namestyle) first_names = v.qualifiers.P9687[1].datavalue.value end -- Otherwise, get last name, first name(s) from author item properties - 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}) - if first_names then - first_names = getStyledNames(first_names, namestyle) + -- 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 @@ -145,6 +150,16 @@ local function makelink(v, out, link, maxpos, wdl, namestyle) 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}) + end + if first_names then + first_names = getStyledNames(first_names, namestyle) + end + if sitelink then -- just the plain name, -- but keep a record of the links, using the same index