Module:PageContext: Difference between revisions
Jump to navigation
Jump to search
Create centralized page context resolver for article eyebrows |
Resolve article eyebrow primary section and optional content type |
||
| Line 1: | Line 1: | ||
-- Module:PageContext | -- Module:PageContext | ||
-- Centralized EDFM page | -- Centralized EDFM page context resolver for Article header eyebrows. | ||
-- | -- Resolves two independent dimensions: primary subject section and optional content type. | ||
local p = {} | local p = {} | ||
| Line 45: | Line 45: | ||
end | end | ||
return false | return false | ||
end | |||
local function startsWith(value, prefix) | |||
return value:sub(1, #prefix) == prefix | |||
end | |||
local function afterPrefix(value, prefix) | |||
if startsWith(value, prefix) then return value:sub(#prefix + 1) end | |||
return nil | |||
end | end | ||
| Line 50: | Line 59: | ||
local index = loadJsonPage('Module:Data/Engineers/index') | local index = loadJsonPage('Module:Data/Engineers/index') | ||
return listContains(index, title) | return listContains(index, title) | ||
end | |||
local function engineerRecord(name) | |||
if not isNonEmpty(name) or not pageInEngineers(name) then return nil end | |||
return loadJsonPage('Module:Data/Engineers/' .. name) | |||
end | end | ||
| Line 68: | Line 82: | ||
end | end | ||
local function | local function findRecordInLuaList(title, dataTitle, fields) | ||
local data = loadLuaData(dataTitle) | local data = loadLuaData(dataTitle) | ||
if type(data) ~= 'table' then return | if type(data) ~= 'table' then return nil end | ||
fields = fields or { 'page', 'title', 'label' } | |||
for _, record in ipairs(data) do | for _, record in ipairs(data) do | ||
if type(record) == 'table' | if type(record) == 'table' then | ||
for _, field in ipairs(fields) do | |||
if record[field] == title then return record end | |||
end | |||
end | end | ||
end | end | ||
return | return nil | ||
end | |||
local function pageInMiningDataset(title, dataTitle, field) | |||
return findRecordInLuaList(title, dataTitle, field and { field, 'page', 'label', 'title' } or nil) ~= nil | |||
end | |||
local function miningProcedureRecord(title) | |||
return findRecordInLuaList(title, 'Module:Data/Procedures/Mining', { 'page', 'title' }) | |||
end | end | ||
| Line 87: | Line 112: | ||
end | end | ||
local function | local function normalizeProcedureTopic(topic) | ||
if topic == 'Odyssey / On-foot' then return 'Odyssey' end | |||
return topic | |||
end | end | ||
local function | local function typeToPrimarySection(pageType) | ||
local map = config.typeToPrimarySection or config.typeToSection or {} | |||
return map[pageType] | |||
end | end | ||
function p.getPageTypeValue(pageName) | function p.getPageTypeValue(pageName) | ||
local title = normalizeTitle(pageName) | local title = normalizeTitle(pageName) | ||
local key = 'type:' .. title | |||
if cache[key] ~= nil then return cache[key] ~= '' and cache[key] or nil end | |||
local pageType = nil | local pageType = nil | ||
| Line 110: | Line 138: | ||
elseif pageInMiningDataset(title, 'Module:Data/Mining/Commodities') then | elseif pageInMiningDataset(title, 'Module:Data/Mining/Commodities') then | ||
pageType = 'mining_commodity' | pageType = 'mining_commodity' | ||
elseif | elseif miningProcedureRecord(title) then | ||
pageType = 'field_procedure' | pageType = 'field_procedure' | ||
elseif pageInShipModules(title) or pageInModulesJson(title) then | else | ||
local unlockTarget = afterPrefix(title, 'Unlocking ') | |||
if unlockTarget and pageInEngineers(unlockTarget) then | |||
pageType = 'field_procedure' | |||
pageType = ' | elseif pageInShipModules(title) or pageInModulesJson(title) then | ||
if guardianLike(title) then | |||
pageType = 'guardian_module' | |||
elseif thargoidLike(title) then | |||
pageType = 'thargoid_module' | |||
else | |||
pageType = 'ship_module' | |||
end | |||
end | |||
end | |||
cache[key] = pageType or '' | |||
return pageType | |||
end | |||
function p.getContentTypeValue(pageName, override) | |||
local title = normalizeTitle(pageName) | |||
if isNonEmpty(override) then return trim(override) end | |||
local key = 'contentType:' .. title | |||
if cache[key] ~= nil then return cache[key] ~= '' and cache[key] or nil end | |||
local contentType = nil | |||
local overrides = config.contentTypeOverrides or {} | |||
if overrides[title] then | |||
contentType = overrides[title] | |||
elseif miningProcedureRecord(title) then | |||
contentType = config.defaultFieldProcedureContentType or 'Field Procedures' | |||
else | |||
local unlockTarget = afterPrefix(title, 'Unlocking ') | |||
if unlockTarget and pageInEngineers(unlockTarget) then | |||
contentType = config.defaultFieldProcedureContentType or 'Field Procedures' | |||
else | else | ||
for _, rule in ipairs(config.contentTypeFamilyRules or {}) do | |||
if rule.prefix and startsWith(title, rule.prefix) then | |||
contentType = rule.contentType | |||
break | |||
end | |||
end | |||
end | end | ||
end | end | ||
cache[ | cache[key] = contentType or '' | ||
return | return contentType | ||
end | end | ||
function p. | function p.getPrimarySectionValue(pageName, override) | ||
local title = normalizeTitle(pageName) | local title = normalizeTitle(pageName) | ||
if isNonEmpty(override) then return trim(override) end | if isNonEmpty(override) then return trim(override) end | ||
local key = ' | local key = 'primary:' .. title | ||
if cache[key] then return cache[key] end | if cache[key] then return cache[key] end | ||
local section = nil | local section = nil | ||
local exact = config.exactSections or {} | local exact = config.exactPrimarySections or config.exactSections or {} | ||
local overrides = config.primarySectionOverrides or {} | |||
local proc = miningProcedureRecord(title) | |||
if proc and isNonEmpty(proc.topic) then | |||
section = | section = normalizeProcedureTopic(proc.topic) | ||
end | end | ||
if not section then | if not section then | ||
local unlockTarget = afterPrefix(title, 'Unlocking ') | |||
local record = unlockTarget and engineerRecord(unlockTarget) or nil | |||
if record then | |||
local scope = record.engineer_scope or 'ship' | |||
section = scope == 'odyssey' and 'Odyssey' or 'Engineering' | |||
end | end | ||
end | end | ||
if not section then | if not section then | ||
for _, rule in ipairs(config.titleContainsRules or {}) do | local pageType = p.getPageTypeValue(title) | ||
section = typeToPrimarySection(pageType) | |||
section = rule.section | end | ||
if not section and exact[title] then | |||
section = exact[title] | |||
end | |||
if not section then | |||
for _, rule in ipairs(config.topicFamilyRules or config.titleContainsRules or {}) do | |||
local text = rule.text | |||
if text and title:find(text, 1, true) then | |||
section = rule.primarySection or rule.section | |||
break | break | ||
end | end | ||
| Line 159: | Line 232: | ||
end | end | ||
section = section or config.fallback or 'Reference' | if not section and overrides[title] then | ||
section = overrides[title] | |||
end | |||
section = section or config.fallbackPrimarySection or config.fallback or 'Reference' | |||
cache[key] = section | cache[key] = section | ||
return section | return section | ||
end | end | ||
function p.getContextValue(pageName, sectionOverride, contentTypeOverride) | |||
local primary = p.getPrimarySectionValue(pageName, sectionOverride) | |||
local contentType = p.getContentTypeValue(pageName, contentTypeOverride) | |||
return { | |||
primarySection = primary, | |||
contentType = contentType, | |||
} | |||
end | |||
function p.formatEyebrowValue(pageName, sectionOverride, contentTypeOverride) | |||
local context = p.getContextValue(pageName, sectionOverride, contentTypeOverride) | |||
local primary = context.primarySection or config.fallbackPrimarySection or 'Reference' | |||
local suffix = '' | |||
-- If primary fell back to Reference because nothing resolved, suppress a content-type suffix unless explicitly provided. | |||
if isNonEmpty(context.contentType) and (primary ~= (config.fallbackPrimarySection or 'Reference') or isNonEmpty(sectionOverride)) then | |||
suffix = ' / ' .. context.contentType | |||
end | |||
return 'EDFM // ' .. primary .. suffix | |||
end | |||
-- Public #invoke API. Lua callers may use the *Value functions above for semantic values/tables. | |||
function p.getPageType(frame) | function p.getPageType(frame) | ||
local args = frame.args or {} | local args = frame.args or {} | ||
| Line 169: | Line 267: | ||
end | end | ||
function p.getPrimarySection(frame) | |||
local args = frame.args or {} | |||
return p.getPrimarySectionValue(args.page or args[1], args.section or args.primarySection or args.topic or args.override) | |||
end | |||
-- Backward-compatible alias. | |||
function p.getSection(frame) | function p.getSection(frame) | ||
return p.getPrimarySection(frame) | |||
end | |||
function p.getContentType(frame) | |||
local args = frame.args or {} | |||
return p.getContentTypeValue(args.page or args[1], args.type or args.contentType) | |||
end | |||
function p.getContext(frame) | |||
local args = frame.args or {} | local args = frame.args or {} | ||
local context = p.getContextValue(args.page or args[1], args.section or args.primarySection or args.topic, args.type or args.contentType) | |||
return mw.text.jsonEncode(context) | |||
end | end | ||
function p.eyebrow(frame) | function p.eyebrow(frame) | ||
local args = frame.args or {} | local args = frame.args or {} | ||
return | return p.formatEyebrowValue(args.page or args[1], args.section or args.primarySection or args.topic or args.override, args.type or args.contentType) | ||
end | end | ||
return p | return p | ||
Revision as of 21:39, 24 August 2026
Documentation for this module may be created at Module:PageContext/doc
-- Module:PageContext
-- Centralized EDFM page context resolver for Article header eyebrows.
-- Resolves two independent dimensions: primary subject section and optional content type.
local p = {}
local config = mw.loadData('Module:Data/PageSections')
local cache = {}
local function trim(value)
return mw.text.trim(tostring(value or ''))
end
local function isNonEmpty(value)
return value ~= nil and trim(value) ~= ''
end
local function normalizeTitle(pageName)
if isNonEmpty(pageName) then
local t = mw.title.new(trim(pageName))
return t and t.text or trim(pageName)
end
return mw.title.getCurrentTitle().text
end
local function loadLuaData(title)
local ok, data = pcall(mw.loadData, title)
if ok and type(data) == 'table' then return data end
return nil
end
local function loadJsonPage(titleText)
local title = mw.title.new(titleText)
local content = title and title:getContent()
if not content then return nil end
local ok, data = pcall(mw.text.jsonDecode, content)
if ok and type(data) == 'table' then return data end
return nil
end
local function listContains(list, title)
if type(list) ~= 'table' then return false end
for _, value in ipairs(list) do
if value == title then return true end
end
return false
end
local function startsWith(value, prefix)
return value:sub(1, #prefix) == prefix
end
local function afterPrefix(value, prefix)
if startsWith(value, prefix) then return value:sub(#prefix + 1) end
return nil
end
local function pageInEngineers(title)
local index = loadJsonPage('Module:Data/Engineers/index')
return listContains(index, title)
end
local function engineerRecord(name)
if not isNonEmpty(name) or not pageInEngineers(name) then return nil end
return loadJsonPage('Module:Data/Engineers/' .. name)
end
local function pageInShipModules(title)
local index = loadLuaData('Module:Data/ShipModules/index')
return listContains(index, title)
end
local function pageInModulesJson(title)
local index = loadJsonPage('Module:Data/Modules/index')
if type(index) ~= 'table' or type(index.modules) ~= 'table' then return false end
for _, record in pairs(index.modules) do
if type(record) == 'table' and (record.page == title or record.name == title) then
return true
end
end
return false
end
local function findRecordInLuaList(title, dataTitle, fields)
local data = loadLuaData(dataTitle)
if type(data) ~= 'table' then return nil end
fields = fields or { 'page', 'title', 'label' }
for _, record in ipairs(data) do
if type(record) == 'table' then
for _, field in ipairs(fields) do
if record[field] == title then return record end
end
end
end
return nil
end
local function pageInMiningDataset(title, dataTitle, field)
return findRecordInLuaList(title, dataTitle, field and { field, 'page', 'label', 'title' } or nil) ~= nil
end
local function miningProcedureRecord(title)
return findRecordInLuaList(title, 'Module:Data/Procedures/Mining', { 'page', 'title' })
end
local function guardianLike(title)
return title:find('Guardian', 1, true) ~= nil or title:find('Nanite Torpedo', 1, true) ~= nil
end
local function thargoidLike(title)
return title:find('Thargoid', 1, true) ~= nil or title:find('Xeno', 1, true) ~= nil or title:find('AX ', 1, true) ~= nil or title:find('Caustic', 1, true) ~= nil
end
local function normalizeProcedureTopic(topic)
if topic == 'Odyssey / On-foot' then return 'Odyssey' end
return topic
end
local function typeToPrimarySection(pageType)
local map = config.typeToPrimarySection or config.typeToSection or {}
return map[pageType]
end
function p.getPageTypeValue(pageName)
local title = normalizeTitle(pageName)
local key = 'type:' .. title
if cache[key] ~= nil then return cache[key] ~= '' and cache[key] or nil end
local pageType = nil
if pageInEngineers(title) then
pageType = 'engineer'
elseif pageInMiningDataset(title, 'Module:Data/Mining/Methods') then
pageType = 'mining_method'
elseif pageInMiningDataset(title, 'Module:Data/Mining/Equipment') then
pageType = 'mining_equipment'
elseif pageInMiningDataset(title, 'Module:Data/Mining/Locations') then
pageType = 'mining_location'
elseif pageInMiningDataset(title, 'Module:Data/Mining/Commodities') then
pageType = 'mining_commodity'
elseif miningProcedureRecord(title) then
pageType = 'field_procedure'
else
local unlockTarget = afterPrefix(title, 'Unlocking ')
if unlockTarget and pageInEngineers(unlockTarget) then
pageType = 'field_procedure'
elseif pageInShipModules(title) or pageInModulesJson(title) then
if guardianLike(title) then
pageType = 'guardian_module'
elseif thargoidLike(title) then
pageType = 'thargoid_module'
else
pageType = 'ship_module'
end
end
end
cache[key] = pageType or ''
return pageType
end
function p.getContentTypeValue(pageName, override)
local title = normalizeTitle(pageName)
if isNonEmpty(override) then return trim(override) end
local key = 'contentType:' .. title
if cache[key] ~= nil then return cache[key] ~= '' and cache[key] or nil end
local contentType = nil
local overrides = config.contentTypeOverrides or {}
if overrides[title] then
contentType = overrides[title]
elseif miningProcedureRecord(title) then
contentType = config.defaultFieldProcedureContentType or 'Field Procedures'
else
local unlockTarget = afterPrefix(title, 'Unlocking ')
if unlockTarget and pageInEngineers(unlockTarget) then
contentType = config.defaultFieldProcedureContentType or 'Field Procedures'
else
for _, rule in ipairs(config.contentTypeFamilyRules or {}) do
if rule.prefix and startsWith(title, rule.prefix) then
contentType = rule.contentType
break
end
end
end
end
cache[key] = contentType or ''
return contentType
end
function p.getPrimarySectionValue(pageName, override)
local title = normalizeTitle(pageName)
if isNonEmpty(override) then return trim(override) end
local key = 'primary:' .. title
if cache[key] then return cache[key] end
local section = nil
local exact = config.exactPrimarySections or config.exactSections or {}
local overrides = config.primarySectionOverrides or {}
local proc = miningProcedureRecord(title)
if proc and isNonEmpty(proc.topic) then
section = normalizeProcedureTopic(proc.topic)
end
if not section then
local unlockTarget = afterPrefix(title, 'Unlocking ')
local record = unlockTarget and engineerRecord(unlockTarget) or nil
if record then
local scope = record.engineer_scope or 'ship'
section = scope == 'odyssey' and 'Odyssey' or 'Engineering'
end
end
if not section then
local pageType = p.getPageTypeValue(title)
section = typeToPrimarySection(pageType)
end
if not section and exact[title] then
section = exact[title]
end
if not section then
for _, rule in ipairs(config.topicFamilyRules or config.titleContainsRules or {}) do
local text = rule.text
if text and title:find(text, 1, true) then
section = rule.primarySection or rule.section
break
end
end
end
if not section and overrides[title] then
section = overrides[title]
end
section = section or config.fallbackPrimarySection or config.fallback or 'Reference'
cache[key] = section
return section
end
function p.getContextValue(pageName, sectionOverride, contentTypeOverride)
local primary = p.getPrimarySectionValue(pageName, sectionOverride)
local contentType = p.getContentTypeValue(pageName, contentTypeOverride)
return {
primarySection = primary,
contentType = contentType,
}
end
function p.formatEyebrowValue(pageName, sectionOverride, contentTypeOverride)
local context = p.getContextValue(pageName, sectionOverride, contentTypeOverride)
local primary = context.primarySection or config.fallbackPrimarySection or 'Reference'
local suffix = ''
-- If primary fell back to Reference because nothing resolved, suppress a content-type suffix unless explicitly provided.
if isNonEmpty(context.contentType) and (primary ~= (config.fallbackPrimarySection or 'Reference') or isNonEmpty(sectionOverride)) then
suffix = ' / ' .. context.contentType
end
return 'EDFM // ' .. primary .. suffix
end
-- Public #invoke API. Lua callers may use the *Value functions above for semantic values/tables.
function p.getPageType(frame)
local args = frame.args or {}
return p.getPageTypeValue(args.page or args[1]) or ''
end
function p.getPrimarySection(frame)
local args = frame.args or {}
return p.getPrimarySectionValue(args.page or args[1], args.section or args.primarySection or args.topic or args.override)
end
-- Backward-compatible alias.
function p.getSection(frame)
return p.getPrimarySection(frame)
end
function p.getContentType(frame)
local args = frame.args or {}
return p.getContentTypeValue(args.page or args[1], args.type or args.contentType)
end
function p.getContext(frame)
local args = frame.args or {}
local context = p.getContextValue(args.page or args[1], args.section or args.primarySection or args.topic, args.type or args.contentType)
return mw.text.jsonEncode(context)
end
function p.eyebrow(frame)
local args = frame.args or {}
return p.formatEyebrowValue(args.page or args[1], args.section or args.primarySection or args.topic or args.override, args.type or args.contentType)
end
return p