Jump to content

Module:GrigoripediaJsonLD

From Grigoripedia

Documentation for this module may be created at Module:GrigoripediaJsonLD/doc

local p = {}

-- ============================================================
-- Utility functions
-- ============================================================

local function trim(value)
	if value == nil then
		return nil
	end

	value = mw.text.trim(tostring(value))

	if value == '' then
		return nil
	end

	return value
end


-- Convert common wikitext/HTML found in infobox parameters
-- into simple plain text suitable for JSON-LD.
local function cleanText(value)
	value = trim(value)

	if not value then
		return nil
	end

	-- Line breaks
	value = value:gsub('<br%s*/>', '; ')
	value = value:gsub('<br%s*>', '; ')

	-- Piped wikilinks:
	-- [[Watcher|Grigori]] -> Grigori
	value = value:gsub(
		'%[%[[^%]]-|([^%]]-)%]%]',
		'%1'
	)

	-- Ordinary wikilinks:
	-- [[Watcher]] -> Watcher
	value = value:gsub(
		'%[%[([^%]]-)%]%]',
		'%1'
	)

	-- Bold / italics
	value = value:gsub("'''", '')
	value = value:gsub("''", '')

	-- Remaining HTML
	value = value:gsub('<[^>]+>', '')

	-- Common entities
	value = value:gsub('&nbsp;', ' ')
	value = value:gsub('&amp;', '&')
	value = value:gsub('&quot;', '"')
	value = value:gsub('&#39;', "'")
	value = value:gsub('&lt;', '<')
	value = value:gsub('&gt;', '>')

	-- Normalize whitespace
	value = value:gsub('%s+', ' ')
	value = mw.text.trim(value)

	if value == '' then
		return nil
	end

	return value
end


-- Convert an infobox's alternative-name list
-- into Schema.org alternateName values.
local function getAlternateNames(value)
	value = trim(value)

	if not value then
		return nil
	end

	value = value:gsub('\r\n', '\n')
	value = value:gsub('\r', '\n')
	value = value:gsub('<br%s*/>', '\n')
	value = value:gsub('<br%s*>', '\n')
	value = value:gsub('•', '\n')
	value = value:gsub(';', '\n')

	local names = {}
	local seen = {}

	for part in mw.text.gsplit(value, '\n', true) do
		local name = cleanText(part)

		if name and not seen[name] then
			table.insert(names, name)
			seen[name] = true
		end
	end

	if #names == 0 then
		return nil
	elseif #names == 1 then
		return names[1]
	end

	return names
end


local function addUnique(list, seen, value)
	value = cleanText(value)

	if value and not seen[value] then
		table.insert(list, value)
		seen[value] = true
	end
end


local function setSingleOrArray(entity, property, values)
	if #values == 1 then
		entity[property] = values[1]
	elseif #values > 1 then
		entity[property] = values
	end
end


local function getImageUrl(filename)
	filename = trim(filename)

	if not filename then
		return nil
	end

	filename = filename:gsub('^File:', '')

	return tostring(
		mw.uri.fullUrl(
			'Special:FilePath/' .. filename
		)
	)
end


local function getTemplateArgs(frame)
	local parent = frame:getParent()

	if parent then
		return parent.args
	end

	return frame.args
end


-- ============================================================
-- Entity builder
-- ============================================================

local function setEntity(kind, args)

	local title = mw.title.getCurrentTitle()

	-- Only generate entity data on actual encyclopedia articles.
	if title.namespace ~= 0 then
		return ''
	end

	-- Fail harmlessly if DynamicJsonLD is unavailable.
	if
		not mw.ext
		or not mw.ext.JsonLD
		or not mw.ext.JsonLD.setMainEntity
	then
		return ''
	end

	local pageUrl = tostring(title:fullUrl())

	local entity = {
		["@type"] = "Thing",
		["@id"] = pageUrl .. "#entity",
		["name"] = cleanText(args.name) or title.text,
		["url"] = pageUrl
	}


	-- Alternate names
	local alternateNames = getAlternateNames(args.other_names)

	if alternateNames then
		entity["alternateName"] = alternateNames
	end


	-- Representative image
	local image = getImageUrl(args.image)

	if image then
		entity["image"] = image
	end


	-- Classification / entity type
	local types = {}
	local seenTypes = {}

	if kind == 'watcher' then

		-- Every Infobox Watcher entity is a Watcher.
		addUnique(types, seenTypes, 'Watcher')

		local position = cleanText(args.position)

		if
			position
			and position:find('Dekadarch', 1, true)
		then
			addUnique(types, seenTypes, 'Dekadarch')
		end

	elseif kind == 'satanael' then

		-- Every Infobox Satanael entity represents Satanael.
		addUnique(types, seenTypes, 'Satanael')

		-- Allow the template's type field to add a broader
		-- classification, such as "Deity".
		addUnique(types, seenTypes, args.type)

		-- Retained for compatibility if classification is ever
		-- supplied manually or by an older template revision.
		addUnique(types, seenTypes, args.classification)

	else

		addUnique(types, seenTypes, args.type)
		addUnique(types, seenTypes, args.classification)

	end

	setSingleOrArray(
		entity,
		'additionalType',
		types
	)

	mw.ext.JsonLD.setMainEntity(entity)

	return ''
end


-- ============================================================
-- Public functions
-- ============================================================

-- Generic being infoboxes.
function p.being(frame)
	return setEntity(
		'being',
		getTemplateArgs(frame)
	)
end


-- Watcher-specific infoboxes.
function p.watcher(frame)
	return setEntity(
		'watcher',
		getTemplateArgs(frame)
	)
end


-- Satanael-specific infobox.
function p.satanael(frame)
	return setEntity(
		'satanael',
		getTemplateArgs(frame)
	)
end


return p