Module:Preamble
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Preamble/doc
-- Lead section. Wikibase access goes through Module:PBD and mw.wikibase.
--
-- p._main(args) is safe to call from other modules. Omit flags for article
-- behavior. Any nonempty value enables a flag:
-- from / qid / eid item id (otherwise the current page sitelink)
-- nocat do not add [[Category:...]]
-- noref do not emit Cite <ref> via PBD
-- nonotice do not append the incomplete-preamble editor box
-- debug always show the editor box
local PBD = require('Module:PBD')
local Dates = require('Module:Preamble/dates')
local Places = require('Module:Preamble/places')
local AOrAn = require('Module:A or an')
local getArgs = require('Module:Arguments').getArgs
local Config = mw.loadData('Module:Preamble/config')
local p = {}
local function trim(s)
if s == nil then
return ''
end
return mw.text.trim(tostring(s))
end
local function nonempty(s)
return trim(s) ~= ''
end
local function pbd(command, flags, propertyId, extra)
extra = extra or {}
local args = {}
for _, flag in ipairs(flags or {}) do
-- noref is a Preamble flag, not a PBD argument. Drop the references
-- command so Cite never registers footnotes on this parse.
if extra.noref and (flag == 'references' or flag == 'reference') then
-- skip
else
args[#args + 1] = flag
end
end
args[#args + 1] = propertyId
for key, value in pairs(extra) do
if key ~= 'noref' and key ~= 'nocat' and key ~= 'nonotice' then
args[key] = value
end
end
local ok, value = pcall(function()
return PBD['_' .. command](args)
end)
if not ok or value == nil then
return ''
end
return trim(value)
end
local function extraArgs(opts)
local extra = {}
if opts.eid and opts.eid ~= '' then
extra.eid = opts.eid
end
if opts.noref then
extra.noref = true
end
return extra
end
local function qidRank(list)
local rank = {}
for i, qid in ipairs(list or {}) do
rank[qid] = i
end
return rank
end
local OCC = Config.occupations or {}
local CAREER = OCC.career or {}
local LEAD_RANK = qidRank(OCC.lead)
local MODEL_RANK = qidRank(OCC.models)
local FORMER_JOB = qidRank(CAREER.former_jobs)
local GENERIC_MODEL_QID = OCC.generic_model or 'Q527'
local function visibleLabel(link)
return link:match('%[%[[^%]|]+|([^%]]+)%]%]%s*$')
or link:match('%[%[([^%]|]+)%]%]%s*$')
or link
end
local function withLabel(link, label)
local target = link:match('%[%[([^%]|]+)|') or link:match('%[%[([^%]]+)%]%]')
if target then
return '[[' .. target .. '|' .. label .. ']]'
end
return label
end
local function modelStem(label)
label = trim(label)
if label == 'model' then
return ''
end
return label:match('^(.*) model$')
end
local function isModelJob(item)
if MODEL_RANK[item.qid] then
return true
end
return modelStem(visibleLabel(item.link)) ~= nil
end
local function jobItems(jobs)
local items = {}
local seen = {}
for i, qid in ipairs(jobs.qids or {}) do
if qid and not seen[qid] then
seen[qid] = true
items[#items + 1] = {
qid = qid,
link = jobs.texts[i],
ref = (jobs.refs and jobs.refs[i]) or '',
index = i
}
end
end
return items
end
local function splitJobs(items)
local leadByQid, mid, models = {}, {}, {}
for _, item in ipairs(items) do
if LEAD_RANK[item.qid] then
leadByQid[item.qid] = item
elseif isModelJob(item) then
models[#models + 1] = item
else
mid[#mid + 1] = item
end
end
table.sort(models, function(a, b)
local ra = MODEL_RANK[a.qid] or (100 + a.index)
local rb = MODEL_RANK[b.qid] or (100 + b.index)
if ra ~= rb then
return ra < rb
end
return a.index < b.index
end)
-- Bare "model" is redundant when a more specific *model is present.
if #models > 1 then
local filtered = {}
for _, item in ipairs(models) do
if item.qid ~= GENERIC_MODEL_QID and modelStem(visibleLabel(item.link)) ~= '' then
filtered[#filtered + 1] = item
end
end
if #filtered > 0 then
models = filtered
end
end
local prefix = {}
for _, qid in ipairs(OCC.lead or {}) do
if leadByQid[qid] then
prefix[#prefix + 1] = leadByQid[qid]
end
end
for _, item in ipairs(mid) do
prefix[#prefix + 1] = item
end
return prefix, models
end
-- "adult model" + "glamour model" → "[[…|adult]] and [[…|glamour model]]"
local function formatModelTail(models)
if #models == 0 then
return ''
end
local parts = {}
for i, item in ipairs(models) do
if i < #models then
local stem = modelStem(visibleLabel(item.link))
parts[#parts + 1] = ((stem and stem ~= '') and withLabel(item.link, stem) or item.link) .. (item.ref or '')
else
parts[#parts + 1] = item.link .. (item.ref or '')
end
end
return mw.text.listToText(parts, ', ', ' and ')
end
local function formatJobList(jobs, former)
jobs = jobs or { qids = {}, texts = {} }
local prefix, models = splitJobs(jobItems(jobs))
local prefixLinks = {}
for _, item in ipairs(prefix) do
local link = item.link
if former and FORMER_JOB[item.qid] then
link = 'former ' .. link
end
prefixLinks[#prefixLinks + 1] = link .. (item.ref or '')
end
local modelTail = formatModelTail(models)
if modelTail == '' then
return mw.text.listToText(prefixLinks, ', ', ' and ')
end
if #prefixLinks == 0 then
return modelTail
end
-- Two or more *model types already have "and" inside the tail.
if #models > 1 then
return table.concat(prefixLinks, ', ') .. ', ' .. modelTail
end
prefixLinks[#prefixLinks + 1] = modelTail
return mw.text.listToText(prefixLinks, ', ', ' and ')
end
local NAT = Config.nationality or {}
local AMERICAN_QID = NAT.american or 'Q165'
local BRITISH_QID = NAT.british or 'Q505'
local UNITED_STATES_QID = NAT.united_states or 'Q143'
local COUNTRY_DEMONYM = NAT.country_demonym or {}
local SKIP_NAT = qidRank(NAT.skip)
local UK_NATION = qidRank(NAT.uk)
local function qidIn(list, qid)
for _, id in ipairs(list or {}) do
if id == qid then
return true
end
end
return false
end
local function bornInUnitedStates(entity)
if qidIn(Places.linkedValues(entity, 'P144').qids, UNITED_STATES_QID) then
return true
end
return qidIn(Places.linkedValues(entity, 'P20').qids, UNITED_STATES_QID)
end
local function birthDemonymQid(entity, natSet)
local function match(qids)
for _, qid in ipairs(qids or {}) do
local dem = COUNTRY_DEMONYM[qid]
if dem and natSet[dem] then
return dem
end
if natSet[qid] then
return qid
end
end
return nil
end
return match(Places.linkedValues(entity, 'P144').qids)
or match(Places.linkedValues(entity, 'P20').qids)
end
-- "English Canadian" refines "Canadian"; keep the shorter base in the lead.
local function isNatRefinement(label, base)
label = mw.ustring.lower(trim(label))
base = mw.ustring.lower(trim(base))
if label == '' or base == '' or label == base or mw.ustring.len(base) >= mw.ustring.len(label) then
return false
end
local escaped = mw.ustring.gsub(base, '([^%w])', '%%%1')
return mw.ustring.find(label, '%f[%w]' .. escaped .. '%f[%W]') ~= nil
end
local function filterNationalities(nats)
local items = {}
for i, qid in ipairs(nats.qids or {}) do
items[#items + 1] = {
qid = qid,
link = nats.texts[i],
label = visibleLabel(nats.texts[i])
}
end
local present = {}
for _, item in ipairs(items) do
present[item.qid] = true
end
local keep = {}
for i, item in ipairs(items) do
local drop = false
for j, other in ipairs(items) do
if i ~= j and isNatRefinement(item.label, other.label) then
drop = true
break
end
end
if not drop and SKIP_NAT[item.qid] then
for j, other in ipairs(items) do
if i ~= j and not SKIP_NAT[other.qid] then
drop = true
break
end
end
end
-- English / Welsh / Scottish / Northern Irish refine British, not each other.
if not drop and UK_NATION[item.qid] and present[BRITISH_QID] then
drop = true
end
if not drop then
keep[#keep + 1] = item
end
end
return keep
end
-- Dual+ nationality: American-British if US-born, British-American otherwise.
local function formatNationality(entity)
local nats = Places.linkedValues(entity, 'P131')
if nats.empty then
return ''
end
local items = filterNationalities(nats)
if #items == 0 then
return nats.texts[1] or ''
end
if #items == 1 then
return items[1].link
end
local byQid = {}
local natSet = {}
local qids = {}
for _, item in ipairs(items) do
byQid[item.qid] = item.link
natSet[item.qid] = true
qids[#qids + 1] = item.qid
end
local ordered = {}
local used = {}
local function push(qid)
if qid and byQid[qid] and not used[qid] then
used[qid] = true
ordered[#ordered + 1] = byQid[qid]
end
end
local hasAmerican = natSet[AMERICAN_QID]
if hasAmerican and bornInUnitedStates(entity) then
push(AMERICAN_QID)
for _, qid in ipairs(qids) do
push(qid)
end
else
push(birthDemonymQid(entity, natSet))
for _, qid in ipairs(qids) do
if qid ~= AMERICAN_QID then
push(qid)
end
end
if hasAmerican then
push(AMERICAN_QID)
end
end
return table.concat(ordered, '-')
end
local function getEntity(eid)
if eid and eid ~= '' then
return mw.wikibase.getEntity(eid)
end
return mw.wikibase.getEntity()
end
local function isMain()
return mw.title.getCurrentTitle().namespace == 0
end
local function cat(name, nocat)
if nocat or not isMain() or not name or name == '' then
return ''
end
return '[[Category:' .. name .. ']]'
end
local function join(parts, sep)
local out = {}
for _, part in ipairs(parts) do
if nonempty(part) then
out[#out + 1] = part
end
end
return table.concat(out, sep)
end
-- Same as Pornbasedata monolingualLangTemplate=lang: {{lang-ru|…}} → "Russian: …"
local function expandLang(lang, text)
local frame = mw.getCurrentFrame()
if frame then
local ok, html = pcall(function()
return frame:expandTemplate{ title = 'lang-' .. lang, args = { text } }
end)
if ok and html and html ~= '' then
return html
end
ok, html = pcall(function()
return frame:expandTemplate{ title = 'Lang', args = { lang, text } }
end)
if ok and html and html ~= '' then
return html
end
end
return '<span lang="' .. lang .. '">' .. text .. '</span>'
end
local function trackQids(opts, result)
if not opts or not opts.catQids or not result then
return
end
for _, qid in ipairs(result.qids or {}) do
opts.catQids[#opts.catQids + 1] = qid
end
end
local function formatNativeNames(entity, skipText)
if not entity or not entity.claims or not entity.claims.P13 then
return ''
end
skipText = trim(skipText)
local names = {}
for _, statement in ipairs(Dates.statements(entity, 'P13')) do
local snak = statement.mainsnak
if snak and snak.snaktype == 'value' and snak.datavalue and snak.datavalue.value then
local value = snak.datavalue.value
local lang = value.language
local text = value.text
if text and text ~= '' and lang and lang ~= 'en' and text ~= skipText then
names[#names + 1] = expandLang(lang, text)
end
end
end
return table.concat(names, ', ')
end
local function formatVital(entity, opts)
local birthYear = Dates.yearForPlaces(entity, 'P9')
local deathYear = Dates.yearForPlaces(entity, 'P247')
local birth = Dates.formatBirth(entity, opts)
local death = Dates.formatDeath(entity, opts)
local extra = extraArgs(opts)
local placeBirth = Places.linkedValues(entity, 'P20')
if placeBirth.empty then
placeBirth = Places.formatProperty(entity, 'P20', {
eid = opts.eid,
atYear = birthYear,
noref = opts.noref
})
end
local countryBirth = Places.linkedValues(entity, 'P144')
if countryBirth.empty then
countryBirth = Places.countryOnly(entity, 'P144', {
eid = opts.eid,
noref = opts.noref
})
end
-- Death places stay plain text: Category:Barcelona is for births, not deaths.
local placeDeath = Places.plainValues(entity, 'P23')
if placeDeath.empty then
placeDeath = Places.formatProperty(entity, 'P23', {
eid = opts.eid,
atYear = deathYear or birthYear,
plain = true,
noref = opts.noref
})
end
trackQids(opts, placeBirth)
trackQids(opts, countryBirth)
local placeBirthText = placeBirth.text or ''
local countryBirthText = countryBirth.text or ''
local placeDeathText = placeDeath.text or ''
if nonempty(countryBirthText) then
local skip = false
for _, qid in ipairs(countryBirth.qids or {}) do
if Places.containsQid(placeBirth, qid) then
skip = true
break
end
end
if skip then
countryBirthText = ''
end
end
local hasDate = not birth.empty
local hasBirthName = nonempty(pbd('property', { 'multilanguage' }, 'P5', extra))
local birthBits = {}
if hasDate then
birthBits[#birthBits + 1] = birth.text
end
if nonempty(placeBirthText) then
-- Old template: "in PLACE" when a birth name (P5) is present, even with a date.
if hasDate and not hasBirthName then
birthBits[#birthBits + 1] = placeBirthText
else
birthBits[#birthBits + 1] = 'in ' .. placeBirthText
end
end
if nonempty(countryBirthText) then
if #birthBits > 0 then
birthBits[#birthBits + 1] = countryBirthText
else
birthBits[#birthBits + 1] = 'in ' .. countryBirthText
end
end
local extraCats = ''
if birth.empty and (nonempty(placeBirthText) or nonempty(countryBirthText) or not death.empty) then
extraCats = cat('Date of birth needed', opts.nocat)
end
local birthText = join(birthBits, ', ')
local deathText = join({ death.text, placeDeathText }, ', ')
local hasBirth = nonempty(birthText)
local hasDeathSide = nonempty(deathText)
if not hasBirth and not hasDeathSide then
return { text = extraCats, hasDeath = not death.empty, hasExactDate = false }
end
if hasBirth and hasDeathSide then
-- Dash is the lifespan mark; "died" labels the second cluster when places are present.
return { text = birthText .. ' — died ' .. deathText .. extraCats, hasDeath = true, hasExactDate = hasDate }
end
if hasDeathSide then
return { text = 'died ' .. deathText .. extraCats, hasDeath = true, hasExactDate = false }
end
return { text = birthText .. extraCats, hasDeath = false, hasExactDate = hasDate }
end
local function onlyCategories(s)
return trim((s or ''):gsub('%[%[Category:[^%]]+%]%]', '')) == ''
end
local function formatNameParts(opts, native, vitalText)
local aliases = pbd('properties', { 'multilanguage' }, 'P140', extraArgs(opts))
local real753 = pbd('property', { 'multilanguage', 'references', 'normal+' }, 'P753', extraArgs(opts))
local real648 = pbd('property', { 'multilanguage', 'references', 'normal+' }, 'P648', extraArgs(opts))
local birthName = pbd('property', { 'multilanguage', 'references', 'normal+' }, 'P5', extraArgs(opts))
local afterAka = {}
if nonempty(real753) then
afterAka[#afterAka + 1] = "real name '''''" .. real753 .. "'''''"
end
if nonempty(birthName) then
if nonempty(real648) then
afterAka[#afterAka + 1] = "real name '''''" .. real648 .. "'''''"
afterAka[#afterAka + 1] = "né '''''" .. birthName .. "'''''"
else
afterAka[#afterAka + 1] = "born '''''" .. birthName .. "'''''"
end
elseif nonempty(real648) then
afterAka[#afterAka + 1] = "real name '''''" .. real648 .. "'''''"
end
if nonempty(native) then
afterAka[#afterAka + 1] = native
end
if nonempty(vitalText) then
if onlyCategories(vitalText) then
afterAka[#afterAka + 1] = vitalText
elseif nonempty(birthName) or vitalText:match('^died ') then
afterAka[#afterAka + 1] = vitalText
else
afterAka[#afterAka + 1] = 'born ' .. vitalText
end
end
if nonempty(aliases) then
local rest = join(afterAka, ', ')
if nonempty(rest) then
return "(<abbr title=\"also known as\">a.k.a.</abbr> '''''" .. aliases .. "'''''; " .. rest .. ')'
end
return "(<abbr title=\"also known as\">a.k.a.</abbr> '''''" .. aliases .. "''''')"
end
if #afterAka > 0 then
return '(' .. join(afterAka, ', ') .. ')'
end
return ''
end
local function formatOccupation(entity, opts, hasDeath, nocat)
local nats = Places.linkedValues(entity, 'P131')
local nationality = formatNationality(entity)
local orientation = Places.linkedValues(entity, 'P347')
local jobs = Places.linkedValues(entity, 'P124', {
withRefs = not opts.noref,
eid = opts.eid,
noref = opts.noref
})
local retired = qidIn(Places.linkedValues(entity, CAREER.property or 'P154').qids, CAREER.retired or 'Q194')
local hasActorJob = false
for _, qid in ipairs(jobs.qids or {}) do
if FORMER_JOB[qid] then
hasActorJob = true
break
end
end
-- "former" = left the industry while alive. Death already uses "was"; do not stack both.
local former = hasActorJob and retired and not hasDeath
local jobList = formatJobList(jobs, former)
trackQids(opts, nats)
trackQids(opts, orientation)
trackQids(opts, jobs)
local verb = hasDeath and 'was' or 'is'
local bits = {}
if nonempty(nationality) then
bits[#bits + 1] = nationality
end
if nonempty(orientation.text) then
bits[#bits + 1] = orientation.text
end
if nonempty(jobList) then
bits[#bits + 1] = jobList
end
if #bits == 0 then
return ''
end
-- a/an on the first word after the verb: nationality, else orientation, else occupation.
bits[1] = AOrAn._main({ bits[1] })
local deadCat = hasDeath and cat('Dead porn stars', nocat) or ''
return verb .. ' ' .. join(bits, ' ') .. deadCat .. '.'
end
local function claimHasValue(entity, propertyId)
if not entity then
return false
end
for _, statement in ipairs(Dates.statements(entity, propertyId)) do
local snaktype = statement.mainsnak and statement.mainsnak.snaktype
if snaktype == 'value' or snaktype == 'somevalue' then
return true
end
end
return false
end
local function propLink(row)
return '[[pbd:Property:' .. row.id .. '|' .. row.label .. ' (' .. row.id .. ')]]'
end
local function checkRows(entity, rows, missing, present)
for _, row in ipairs(rows or {}) do
local label = propLink(row)
if claimHasValue(entity, row.id) then
present[#present + 1] = label
elseif missing then
missing[#missing + 1] = label
end
end
end
local function missingRequired(entity)
local schema = Config.properties or {}
local missing, present = {}, {}
checkRows(entity, schema.required, missing, present)
if claimHasValue(entity, 'P247') then
checkRows(entity, schema.required_if_dead, missing, present)
end
checkRows(entity, schema.optional, nil, present)
return missing, present
end
local function formatIncomplete(eid, entity, nocat, missing, present)
local bits = {
'<div class="preamble-incomplete" style="border:1px solid #a2a9b1;background:#f8f9fa;padding:0.6em 0.8em;margin:0.5em 0">'
}
if not entity then
bits[#bits + 1] = "'''Preamble''' cannot be generated: this page is not linked to a Porn Base Data item. "
bits[#bits + 1] = 'Add a sitelink from the item to this article, or call the module with <code>|from=Q…</code>.'
else
local qid = (eid and eid ~= '') and eid or (mw.wikibase.getEntityIdForCurrentPage() or '')
bits[#bits + 1] = "'''Preamble''' is incomplete"
if qid ~= '' then
bits[#bits + 1] = ' for [[pbd:' .. qid .. '|' .. qid .. ']]'
end
bits[#bits + 1] = '.'
if missing and #missing > 0 then
bits[#bits + 1] = ' Missing required: ' .. table.concat(missing, ', ') .. '.'
end
if present and #present > 0 then
bits[#bits + 1] = ' Filled: ' .. table.concat(present, ', ') .. '.'
end
if claimHasValue(entity, 'P247') then
bits[#bits + 1] = ' Date of death (P247) is present → verb <i>was</i>.'
else
bits[#bits + 1] = ' No date of death (P247) → verb <i>is</i>.'
end
end
bits[#bits + 1] = '</div>'
return table.concat(bits) .. cat('Articles with incomplete preamble', nocat)
end
function p._main(args)
args = args or {}
local eid = trim(args.from or args.qid or args.eid)
if eid == '' then
eid = mw.wikibase.getEntityIdForCurrentPage() or ''
end
local nocat = nonempty(args.nocat)
local noref = nonempty(args.noref)
local nonotice = nonempty(args.nonotice)
local debug = nonempty(args.debug)
local opts = { eid = eid ~= '' and eid or nil, nocat = nocat, noref = noref, catQids = {} }
local entity = getEntity(opts.eid)
if not entity then
if nonotice then
return ''
end
return formatIncomplete(eid, nil, nocat)
end
local missing, present = missingRequired(entity)
local name = pbd('property', { 'multilanguage' }, 'P7', extraArgs(opts))
local native = formatNativeNames(entity, name)
local vital = formatVital(entity, opts)
local hasDeath = vital.hasDeath
or nonempty(pbd('property', { 'normal+' }, 'P247', extraArgs(opts)))
local paren = formatNameParts(opts, native, vital.text)
local occupation = formatOccupation(entity, opts, hasDeath, nocat)
local lead = join({
nonempty(name) and "'''" .. name .. "'''" or nil,
paren,
occupation
}, ' ')
local notice = ''
if not nonotice and (debug or #missing > 0 or lead == '') then
notice = formatIncomplete(eid, entity, nocat, missing, present)
end
-- A lead without P7 is not a preamble; show the editor notice only.
if lead == '' or name == '' then
return notice
end
return lead .. Places.categoriesFromQids(opts.catQids, nocat) .. notice
end
function p.main(frame)
return p._main(getArgs(frame, {
wrappers = { 'Template:Preamble', 'Template:Preamble-test' }
}))
end
return p