Module:PageContext

Current answers. Practical procedures. Reliable reference.
Revision as of 21:39, 24 August 2026 by Sythan (talk | contribs) (Expose display-safe content type for article header)
Jump to navigation Jump to search

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.getDisplayContentType(frame)
	local args = frame.args or {}
	local sectionOverride = args.section or args.primarySection or args.topic
	local typeOverride = args.type or args.contentType
	local primary = p.getPrimarySectionValue(args.page or args[1], sectionOverride)
	local contentType = p.getContentTypeValue(args.page or args[1], typeOverride)
	if not isNonEmpty(contentType) then return '' end
	if primary == (config.fallbackPrimarySection or 'Reference') and not isNonEmpty(sectionOverride) then
		return ''
	end
	return 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