Module:Technique
From The Goon Show Depository
--[=[
This module is intended to be the engine behind "Template:Technique".
It can also be used directly from Lua using the function p._technique().
Setting "compat=yes" makes the functions p.technique() and p._technique()
behave more like the old "Template:Technique".
Differences to the old "Template:Technique" also in compat mode:
* if following non-fitting parameters are set without their predecessors
special cases aren't used (unlike in "Template:Technique" as of 2019-08-11T12)
* use of [[Module:Linguistic]] with different order of words in
function p.noungroup() in gl, vi
]=]
local p = {}
require('Module:No globals') -- used for debugging purposes as it detects cases of unintended global variables
local linguistic = require('Module:Linguistic')
local synonyms = mw.loadData('Module:Technique/synonyms')
local declension = require('Module:Declension')
local fallback = require('Module:Fallback')
local function getSingularTerm(term)
local plurals = mw.loadData('Module:Technique/WikidataLUT').plurals
local singular = nil
if plurals[term] then
singular = plurals[term]
end
return singular
end
local function getWDmapping(term, wordtype)
term = synonyms[wordtype][term] or term
local qid_LUT = mw.loadData('Module:Technique/WikidataLUT')[wordtype]
return qid_LUT[term]
end
-- return table of fallback languages and their existing subpages for the language
local function findLang(lang)
local langList = mw.language.getFallbacksFor(lang)
table.insert(langList,1,lang)
for i,l in ipairs(langList) do
local page = mw.title.new('Technique/' .. l, 'Module')
langList[i] = {lang = l, subpage = page.exists and 'Module:Technique/' .. l or nil}
--[[page.exist checking could be swapped out from here to reduce
its expensive access ]]
end
return langList
end
local function getDeprecatedCat(term, wordtype)
local result = ''
local map = getWDmapping(term, wordtype)
if map and map.deprecated then
local sortTerm = synonyms[wordtype][term] or term
local adjectiveMark = (wordtype == 'adjectives') and '!' or ''
result = ('[[Category:Pages using Template:Technique with deprecated term|'
.. adjectiveMark .. sortTerm .. ']]')
end
return result
end
local function makeQIDnoLabelText(term, qid)
return '<span style="color:red">[[d:' .. qid .. '|' .. term .. ']]</span>'
end
-- if the reference item isn't a material, but is a technique and has one
-- valid product statement then return the qid of the product (for labeling)
function p.getProductQid(map)
local productQid
if not map.material and map.process then
local productStatements = mw.wikibase.getBestStatements(map.qid, 'P1056')
if #productStatements == 1 then
productQid = productStatements[1].mainsnak.datavalue.value.id
end
end
return productQid
end
local function getUnsupportedLink(lang, wordtype, label, system)
local translationTargetPage
if system == 'modules' then
if wordtype == 'adjectives' then
translationTargetPage = 'Module:Technique/' .. lang
else
translationTargetPage = 'Module:Technique/WikidataLUT'
end
else
translationTargetPage = 'Template:Technique/' .. lang .. ((wordtype == 'adjectives') and '/adjectives' or '')
end
local url = ('//commons.wikimedia.org/w/index.php?title='
.. translationTargetPage
.. '&action=edit')
local link = mw.ustring.format(
'<span class="plainlinks">[%s <span style="color:red">%s</span>]</span>',
url, label)
return link
end
local function getUnsupportedCatLink(term, wordtype)
local adjectiveMark = (wordtype == 'adjectives') and '!' or ''
return '[[Category:Unsupported technique|' .. adjectiveMark .. term .. ']]'
end
local function getUnsupportedString(term, wordtype, lang, label, system, compat)
local compatUnsupportedLinkSystem = system
if compat == 'yes' then
compatUnsupportedLinkSystem = 'templates'
end
local unsupportedLink = getUnsupportedLink(lang, wordtype, label, compatUnsupportedLinkSystem)
local unsupportedCatLink = (lang == 'en') and getUnsupportedCatLink(term, wordtype) or ''
return unsupportedLink .. unsupportedCatLink
end
-- fallback chain: Commons module subpage -> Wikidata label -> those two in next language -> …
function p.getLabelTranslationFallback(term, wordtype, langList, system, compat, lenient)
local labelQid, map, label, usedLang
if string.find(term, '^q%d+$') then
-- term is a Wikidata Q-ID, so we use it to create the label
labelQid = string.upper(term)
end
local canonizedTerm = synonyms[wordtype][term]
-- loop over language fallback list looking for label in the specific language
for i,langTable in pairs(langList) do
if langTable.subpage and i>1 then
local langData = require(langTable.subpage)
local termData = langData[wordtype][term] or (canonizedTerm and langData[wordtype][canonizedTerm])
label = termData and ((type(termData) == 'string') and termData or termData.default or termData.n)
-- TODO: adapt for adjectives
if label then
usedLang = langTable.lang
break -- label found and we are done
end
end
-- if for given language no noun label was found on Commons look at Wikidata:
if wordtype == 'nouns' and not labelQid then
-- only run if not already run before
map = getWDmapping(term, 'nouns')
labelQid = (map and map['qid']) and (p.getProductQid(map) or map['qid']) or '0'
end
if wordtype == 'nouns' and labelQid ~= 0 then
label = mw.wikibase.getLabelByLang(labelQid, langTable.lang)
-- gives nil if not found
if label then
usedLang = langTable.lang
break -- label found and we are done
end
end
end
if label and usedLang ~= langList[1].lang then
label = mw.ustring.format('<span lang="%s">\'\'%s\'\'</span>', usedLang, label)
end
if not label and map and map['qid'] then
label = makeQIDnoLabelText(term, map['qid'])
return label
end
if not label or (usedLang ~= langList[1].lang and wordtype == 'adjectives') then
if lenient then
label = term
else
label = getUnsupportedString(term, wordtype, langList[1].lang,
label or term, system, compat)
end
end
-- now label should always be a string
return label
end
function p._wikidataLinkFallback(lang, term, wikitext)
if not (mw.ustring.find(wikitext, '%[%[') or mw.ustring.find(wikitext, '//')) then
local map
if string.find(term, '^q%d+$') then
-- term is a Wikidata Q-ID, so we use it to create the link
map = {qid = string.upper(term)}
else
term = synonyms.nouns[term] or term
term = getSingularTerm(term) or term
map = getWDmapping(term, 'nouns')
end
if map and map['qid'] then
local qids = {map['qid']}
if map['altQids'] then
for _,qid in ipairs(map['altQids']) do
qids[#qids+1] = qid
end
end
-- use primary language subtag for link creation
local lang = mw.text.split(lang, '-', true)[1]
local sitelink, interwikiPrefix
for _,qid in ipairs(qids) do
sitelink = mw.wikibase.getSitelink(qid, lang .. 'wiki')
if sitelink then
interwikiPrefix = lang .. ':'
break
end
end
if not sitelink then
for _,qid in ipairs(qids) do
sitelink = mw.wikibase.getSitelink(qid, 'commonswiki')
if sitelink then
interwikiPrefix = ''
break
end
end
end
if sitelink then
wikitext = '[[:' .. interwikiPrefix .. sitelink .. '|' .. wikitext .. ']]'
else
wikitext = '[[d:' .. map['qid'] .. '|' .. wikitext .. ']]'
end
end
end
return wikitext
end
local function getAgreement(lang, noun, query, caseletter)
local agreement = mw.getCurrentFrame():expandTemplate{
title = 'technique/' .. lang,
args={noun, query = 'gender'}}
-- give a default gender and number agreement for some languages
-- that don't specify a default ending
-- [[Template:Technique/ca]] doesn't have switches for all terms which means
-- translations are given as agreement
-- --> also give a default gender and number agreement for
-- cases where the agreement doesn't match '^[a-z][a-z]$'
local defaultGenderNumber = {
ca='m', da='c', fr='m', gl='m', it='m', pl='ms', pt='m', ro='m', scn='m'}
if defaultGenderNumber[lang] and not string.find(agreement, '^[a-z][a-z]?$') then
agreement = defaultGenderNumber[lang]
end
agreement = agreement .. (caseletter or '')
return agreement
end
local function termToTemplateWikitext(wordtype, lang, term, query, agreement)
local frame = mw.getCurrentFrame()
local wikitext
if wordtype == 'nouns' then
local queryCase = frame:expandTemplate{title='technique/' .. lang, args={'case', query=query}}
wikitext = frame:expandTemplate{title='technique/' .. lang, args={term, query=queryCase}}
elseif wordtype == 'adjectives' then
wikitext = frame:expandTemplate{title='technique/' .. lang .. '/adjectives', args={term, agreement=agreement}}
end
return wikitext
end
local function getTermWikitextWithTemplates(term, wordtype, lang, query, noun, caseletter, lenient)
if term == '' then
return ''
end
-- find out agreement for adjectives:
local agreement = noun and getAgreement(lang, noun, query, caseletter)
local wikitext = termToTemplateWikitext(wordtype, lang, term, query, agreement)
if wikitext == '' then
local canonicalTerm = synonyms[wordtype][term]
if canonicalTerm then
local agreement = getAgreement(lang, canonicalTerm, query, caseletter)
wikitext = termToTemplateWikitext(wordtype, lang, canonicalTerm, query, agreement)
end
if wikitext == '' then
local langList = findLang(lang)
local lenient = lenient ~= '' and lenient
wikitext = p.getLabelTranslationFallback(
term, wordtype, langList, 'templates', 'yes', lenient)
end
end
if wordtype == 'nouns' then
wikitext = p._wikidataLinkFallback(lang, term, wikitext)
end
wikitext = wikitext .. getDeprecatedCat(term, wordtype)
return wikitext
end
function p.wikidataLinkFallback(frame)
local args = frame.args
local wordtype = args.wordtype or 'nouns'
return getTermWikitextWithTemplates(args.term, wordtype, args.lang,
args.query, args.noun, args.caseletter, args.lenient)
end
local function processAdjective(adj, nounData, langData, langList, case, compat, lenient)
if adj == '' then
return nil
end
local adj0 = adj
adj = synonyms.adjectives[adj] or adj
local adjData = langData.adjectives[adj0] or langData.adjectives[adj]
if adj and not langData.adjectives[adj] then
adjData = p.getLabelTranslationFallback(adj0, 'adjectives', langList, 'modules', compat, lenient)
end
local gender, number, decl
-- adjectives with declension
if type(adjData) == 'table' then
local genderConvert = {m=1, f=2, n=3, c=1}
gender = nounData and nounData.gender and genderConvert[nounData.gender]
number = nounData and nounData.number
decl = langData.declension[case]
local parts = adjData.parts
for i = 1,#parts do
if type(parts[i]) == 'table' then
parts[i] = declension.selectAdjectiveForm(parts[i], {number=number, case=decl, gender=gender})
end
end
adj = table.concat(parts)
-- invariable adjectives
elseif type(adjData) == 'string' then
adj = adjData
end
adj = adj .. getDeprecatedCat(adj0, 'adjectives')
return adj
end
local function makegroupWithTemplates(noun, adj, lang, case, compat, lenient, beforeMountedGrammarNoun)
local frame = mw.getCurrentFrame()
if case == 'default' then
case = 'basic'
end
local caseletter = frame:expandTemplate{title='technique/' .. lang, args={
'case', query=case}}
adj = adj or {}
local adjCounter = #adj
while adjCounter > 0 do
adj[adjCounter] = getTermWikitextWithTemplates(adj[adjCounter], 'adjectives', lang, case, noun, caseletter, lenient)
adjCounter = adjCounter - 1
end
adj = linguistic.conj(adj, lang)
local noun = noun and getTermWikitextWithTemplates(noun, 'nouns', lang, case, nil, caseletter, lenient)
local gender_on = (case == 'mounted' and beforeMountedGrammarNoun
and frame:expandTemplate{title='technique/' .. lang, args={
beforeMountedGrammarNoun, query='gender'}})
local wikitext = frame:expandTemplate{title='technique/' .. lang, args={
case, noun = noun, adj = adj, ['gender on'] = gender_on}}
return wikitext
end
-- turn a adj + noun group into a human-readable string
local function makegroup(noun, adj, langData, lang, case, compat, lenient, beforeMountedGrammar)
local langList = findLang(lang)
if noun == '' then noun = nil end
noun = synonyms.nouns[noun] or noun
local canonicalNoun = noun
local nounData = langData.nouns[noun] or noun and p.getLabelTranslationFallback(
noun, 'nouns', langList, 'modules', compat, lenient)
adj = adj or {}
local adjCounter = #adj
while adjCounter > 0 do
adj[adjCounter] = processAdjective(
adj[adjCounter], nounData, langData, langList, case, compat, lenient)
adjCounter = adjCounter - 1
end
if not noun and #adj == 0 then
return nil
end
adj = linguistic.conj(adj, lang)
-- process noun
local group = ''
if noun then
local nounLabel
if type(nounData) == 'table' then -- complex languages
nounLabel = nounData[langData.declension[case] or 'default']
or nounData.default
or nounData.n
-- if langData doesn't define cases use 'default'
-- if desired case isn't available, fall back to 'default' then nominative
-- TODO: use nominative/'n' at all (here)?
if not nounLabel then
-- TODO: cleanup! ((use for adjectives and nouns or neither?))
nounLabel = p.getLabelTranslationFallback(noun, 'nouns', langList, 'modules', compat)
end
elseif type(nounData) == 'string' then -- languages without declension
nounLabel = nounData
end
local outNoun
if nounData.link and not mw.ustring.find(nounLabel, '%[%[') then
-- TODO: remove the 2nd part! (once not needed any more)
outNoun = '[[:' .. nounData.link .. '|' .. nounLabel .. ']]'
else
outNoun = p._wikidataLinkFallback(lang, canonicalNoun, nounLabel)
end
outNoun = outNoun .. getDeprecatedCat(canonicalNoun, 'nouns')
group = linguistic.noungroup(outNoun, adj, lang)
else
group = adj .. '[[Category:Pages with incorrect template usage/Technique]]'
-- add maintenance category if noun is missing
end
-- finalize
return langData.nomgroup(group, case, beforeMountedGrammar)
end
local function makeQSstatementCore(term, case, compat)
-- currently only supports default and "on" cases
local qid
local map = getWDmapping(term, 'nouns')
local core
if compat == 'yes' then
qid = mw.getCurrentFrame():expandTemplate{title='Technique/WikidataLUT', args={term}}
elseif map then
qid = map['qid']
end
if qid and qid ~= '' then
local prop
if map['material'] or compat == 'yes' then
prop = 'P186'
elseif map['process'] then
prop = 'P2079'
end
if prop then
core = prop .. ',' .. qid
if case and case == 'on' then
core = core .. ',P518,Q861259'
end
end
end
return core
end
local function makeQSstring(args, isSimple)
local QScode = ''
if not (isSimple and args.caseGroups.default[1]) then
return ''
end
local fragments = {'default', 'on'}
local statements = {}
local isAllFound = true
for _,f in ipairs(fragments) do
if args.caseGroups[f][1] and args.caseGroups[f][1].noun then
-- only continue if the term is given
local case
if f == 'on' then
case = 'on'
end
local statement = makeQSstatementCore(args.caseGroups[f][1].noun, case, args.compat)
if statement and statement ~= '' then
table.insert(statements, statement)
else
isAllFound = false -- TODO: simply return '' and get rid of this variable??
end
end
end
if isAllFound then
QScode = '<div style="display: none;">medium QS:' .. table.concat(statements, ';') .. '</div>'
-- QScode = '<i>medium QS:' .. table.concat(statements, ';') .. '</i>' -- for debugging
end
return QScode
end
local function getBeforeMountedGrammarNoun(caseGroups)
local cases = {'on', 'over', 'default'}
for _,case in ipairs(cases) do
if #caseGroups[case] > 0 then
return caseGroups[case][#caseGroups[case]].noun
end
end
end
local function getBeforeMountedGrammar(noun, langData)
local beforeMountedData
if noun then
beforeMountedData = langData.nouns[noun]
end
local beforeMountedGender = beforeMountedData and beforeMountedData.gender
local genderConvert = {m=1, f=2, n=3, c=1}
beforeMountedGender = beforeMountedGender and genderConvert[beforeMountedGender]
local beforeMountedNumber = beforeMountedData and beforeMountedData.number
local beforeMountedGrammar = {
gender = beforeMountedGender,
number = beforeMountedNumber}
return beforeMountedGrammar
end
local function getIsSimple(caseGroups)
local isSimple = false
local defaultOnOnly = (#caseGroups.over == 0 and #caseGroups.mounted == 0)
local defaultOnOneGroup = (#caseGroups.default <= 1 and #caseGroups.on <=1)
local default1NoAdj = (#caseGroups.default == 0 or caseGroups.default[1] and caseGroups.default[1].adj == nil)
local on1NoAdj = (#caseGroups.on == 0 or caseGroups.on[1] and caseGroups.on[1].adj == nil)
return defaultOnOnly and defaultOnOneGroup and default1NoAdj and on1NoAdj
end
local function getQidToEnglishTermTable(wordtype)
local qid_LUT = mw.loadData('Module:Technique/WikidataLUT')[wordtype]
local QidToEnglishTermTable = {}
for term,termTable in pairs(qid_LUT) do
if termTable.qid then
QidToEnglishTermTable[termTable.qid] = term
end
end
return QidToEnglishTermTable
end
local function qidsToEnglishTerms(caseGroups)
local qidToNounTable = getQidToEnglishTermTable('nouns')
local qidToAdjTable = getQidToEnglishTermTable('adjectives')
for case,caseGroup in pairs(caseGroups) do
for i,group in pairs(caseGroup) do
if group.noun then
caseGroups[case][i].noun = qidToNounTable[string.upper(group.noun)] or group.noun
end
if group.adj then
for j,adj in ipairs(group.adj) do
caseGroups[case][i].adj[j] = qidToAdjTable[string.upper(adj)] or adj
end
end
end
end
return caseGroups
end
local function getCaseStrings(caseGroups, langData, lang, system, compat, lenient, beforeMountedGrammar, beforeMountedGrammarNoun)
local caseStrings = {}
for case, caseData in pairs(caseGroups) do
local caseGroupStrings = {}
for _,group in pairs(caseData or {}) do
-- caseData should always be a table, "or" just for compatibility with noncompliant input
local groupString = nil
if system == 'modules' then
groupString = (
makegroup(group.noun, group.adj, langData, lang, case,
compat, lenient, beforeMountedGrammar))
elseif system == 'templates' then
groupString = (
makegroupWithTemplates(group.noun, group.adj, lang, case,
compat, lenient, beforeMountedGrammarNoun))
end
caseGroupStrings[#caseGroupStrings+1] = groupString
end
caseStrings[case] = linguistic.conj(caseGroupStrings, lang)
end
return caseStrings
end
-- main function used by the module
function p._technique(args)
local lang = args.lang
local caseGroups = qidsToEnglishTerms(args.caseGroups)
local isSimple = getIsSimple(caseGroups)
local function vOrNil(tab, key)
return tab and tab[key]
end
if isSimple and vOrNil(caseGroups.default[1], 'noun') == 'oil' and vOrNil(caseGroups.on[1], 'noun') == 'canvas' then
local QScode = makeQSstring(args, isSimple)
return fallback.translatelua({args={'I18n/oil on canvas', lang=lang}}) .. QScode
elseif isSimple and vOrNil(caseGroups.default[1], 'noun') == 'oil' and (vOrNil(caseGroups.on[1], 'noun') == 'wood' or vOrNil(caseGroups.on[1], 'noun') == 'panel') then
local QScode = makeQSstring(args, isSimple)
return fallback.translatelua({args={'I18n/oil on panel', lang=lang}}) .. QScode
elseif isSimple and vOrNil(caseGroups.default[1], 'noun') == 'unknown' and not vOrNil(caseGroups.on[1], 'noun') then
return mw.getCurrentFrame():expandTemplate{title='unknown', args={'technique'}}
end
local beforeMountedGrammarNoun = getBeforeMountedGrammarNoun(caseGroups)
local result = nil
if args.system == 'templates' then
local frame = mw.getCurrentFrame()
local templateLang = frame:expandTemplate{title='fallback', args={'Technique', lang}}
local caseStrings = getCaseStrings(caseGroups, nil, templateLang,
'templates', args.compat, args.lenient, nil, beforeMountedGrammarNoun)
result = frame:expandTemplate{title='technique/' .. templateLang, args={
'order', A=caseStrings.default, over=caseStrings.over,
on=caseStrings.on, mounted=caseStrings.mounted}}
else -- system 'modules'
local langData
local langList = findLang(lang)
for _,t in pairs(langList) do
if t.subpage then
langData = require(t.subpage)
break
end
end
local beforeMountedGrammar = getBeforeMountedGrammar(beforeMountedGrammarNoun, langData)
local caseStrings = getCaseStrings(caseGroups, langData, lang,
'modules', args.compat, args.lenient, beforeMountedGrammar, nil)
result = langData.grouporder(caseStrings.default, caseStrings.over, caseStrings.on, caseStrings.mounted)
result = mw.text.trim(result)
-- maybe useful for compatibility:
result = mw.ustring.gsub(result, '%s+', ' ')
-- the following should improve word order in some situations with mixed
-- RTL and LTR text
if mw.language.new(lang):isRTL() then
result = string.format('<span dir="rtl">%s</span>', result)
end
end
local QScode = makeQSstring(args, isSimple)
result = result .. QScode
if not isSimple then
result = result .. '[[Category:Pages with complex technique templates]]'
end
return result
end
local function stringStartsWith(inString, matchTable)
local start
local rest = inString
for _,m in ipairs(matchTable) do
if string.sub(inString, 1, #m) == m then
start = m
rest = string.sub(inString, #m + 1)
break
end
end
return start, rest
end
-- provide the keys of a table in sorted order
local function getSortedKeys(inTable)
local sortedKeys = {}
for key,_ in pairs(inTable) do
table.insert(sortedKeys, key)
end
table.sort(sortedKeys)
return sortedKeys
end
function p.read_input_parameters(templateargs)
local templateargs2 = {}
for key, value in pairs(templateargs) do
if value ~= '' then -- nuke empty strings
templateargs2[key] = string.lower(mw.text.trim(value))
end
end
-- move keys without explicit case
templateargs2.and0 = templateargs2[1]
templateargs2.adjand0 = templateargs2.adj or templateargs2.color
templateargs2.and1 = templateargs2['and']
templateargs2.adjand1 = templateargs2.adjand or templateargs2.colorand
templateargs2.on1 = templateargs2.on or templateargs2[2]
templateargs2[1] = nil
templateargs2.adj = nil
templateargs2.color = nil
templateargs2['and'] = nil
templateargs2.adjand = nil
templateargs2.colorand = nil
templateargs2.on = nil
templateargs2[2] = nil
local args2 = {
compat = templateargs2.compat,
system = templateargs2.system,
lang = templateargs2.lang,
lenient = templateargs2.lenient,
-- a search for "all: insource:/\| *lenient/ insource:technique -intitle:technique"
-- finds the templates where this parameter is used
caseGroups = {default = {}, over = {}, on = {}, mounted = {}}}
for key, value in pairs(templateargs2) do
if value == '' then
break
end
value = mw.text.trim(value)
local adjPart, rest = stringStartsWith(key, {'adj', 'color'})
local casePart, rest = stringStartsWith(rest, {'and', 'on', 'over', 'mounted'})
if casePart and rest == nil or rest == '' then rest = 1 end
local counter = tonumber(rest)
if casePart and type(counter) == 'number' and counter == math.abs(math.floor(counter)) then
-- counter is 0 or positive integer
if casePart == 'and' then
casePart = 'default'
counter = counter + 1
end
local wordtype = 'noun'
if adjPart == 'adj' or adjPart == 'color' then
wordtype = 'adj'
value = mw.text.split(value, ';')
end
if not args2.caseGroups[casePart][counter] then
args2.caseGroups[casePart][counter] = {}
end
args2.caseGroups[casePart][counter][wordtype] = value
end
end
-- TODO: is the following needed? for coping with left out numbers? write test case
for _,case in ipairs(args2.caseGroups) do
local sortedCaseKeys = getSortedKeys(case)
local gaplessCaseGroups = {}
for _,key in ipairs(sortedCaseKeys) do
table.insert(gaplessCaseGroups, case[key])
end
args2.caseGroups[case] = gaplessCaseGroups
end
return args2
end
-- function to be called from template namespace
function p.technique(frame)
local templateargs = frame:getParent().args
local args = p.read_input_parameters(templateargs)
args.system = frame.args.system
args.compat = args.compat or frame.args.compat
if not args.lang or args.lang == '' then
-- get user's chosen language
args.lang = frame:callParserFunction('int', 'lang')
end
return p._technique(args)
end
return p