Module:PageContext: Difference between revisions

Current answers. Practical procedures. Reliable reference.
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 section resolver for Article header eyebrows.
-- Centralized EDFM page context resolver for Article header eyebrows.
-- Keep page-specific overrides small; prefer structured datasets and family rules.
-- 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 pageInMiningDataset(title, dataTitle, field)
local function findRecordInLuaList(title, dataTitle, fields)
local data = loadLuaData(dataTitle)
local data = loadLuaData(dataTitle)
if type(data) ~= 'table' then return false end
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' and (record[field or 'page'] == title or record.page == title or record.label == title or record.title == title) then
if type(record) == 'table' then
return true
for _, field in ipairs(fields) do
if record[field] == title then return record end
end
end
end
end
end
return false
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 startsWith(value, prefix)
local function normalizeProcedureTopic(topic)
return value:sub(1, #prefix) == prefix
if topic == 'Odyssey / On-foot' then return 'Odyssey' end
return topic
end
end


local function typeToSection(pageType)
local function typeToPrimarySection(pageType)
return config.typeToSection and config.typeToSection[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)
if cache['type:' .. title] then return cache['type:' .. title] end
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 pageInMiningDataset(title, 'Module:Data/Procedures/Mining', 'page') then
elseif miningProcedureRecord(title) then
pageType = 'field_procedure'
pageType = 'field_procedure'
elseif pageInShipModules(title) or pageInModulesJson(title) then
else
if guardianLike(title) then
local unlockTarget = afterPrefix(title, 'Unlocking ')
pageType = 'guardian_module'
if unlockTarget and pageInEngineers(unlockTarget) then
elseif thargoidLike(title) then
pageType = 'field_procedure'
pageType = 'thargoid_module'
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
pageType = 'ship_module'
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['type:' .. title] = pageType or ''
cache[key] = contentType or ''
return pageType
return contentType
end
end


function p.getSectionValue(pageName, override)
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 = 'section:' .. title
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 {}
if exact[title] then
local overrides = config.primarySectionOverrides or {}
section = exact[title]
 
else
local proc = miningProcedureRecord(title)
local pageType = p.getPageTypeValue(title)
if proc and isNonEmpty(proc.topic) then
section = typeToSection(pageType)
section = normalizeProcedureTopic(proc.topic)
end
end


if not section then
if not section then
for _, rule in ipairs(config.familyRules or {}) do
local unlockTarget = afterPrefix(title, 'Unlocking ')
if rule.prefix and startsWith(title, rule.prefix) then
local record = unlockTarget and engineerRecord(unlockTarget) or nil
section = rule.section
if record then
break
local scope = record.engineer_scope or 'ship'
end
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)
if rule.text and title:find(rule.text, 1, true) then
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 {}
return p.getSectionValue(args.page or args[1], args.section or args.override)
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 'EDFM // ' .. p.getSectionValue(args.page or args[1], args.section or args.override)
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