Module:ImportPerformances

From Angelina Jordan Wiki

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

-- Module:ImportPerformances
-- Reads a wiki page containing JSON (an array of objects) and emits literal transclusion calls
-- of Performance and Video for each object.
-- Usage:
--   {{#invoke:ImportPerformances|import|source=PageWithJSON|headings=no|pre=no|filter_song=SongTitle}}

local p = {}

-- Helper: safe tostring
local function s(v)
	if v == nil then return nil end
	return tostring(v)
end

-- Escape text for safe literal display in HTML output.
-- Converts <, >, {, } to entities, but leaves '&' alone
-- so URLs like ?a=1&b=2 are not double-escaped.
local function escapeForOutput(str)
	if str == nil then return nil end
	local t = s(str)
	-- Do NOT encode '&' to avoid double escaping
	t = t:gsub('<', '&lt;')
	t = t:gsub('>', '&gt;')
	t = t:gsub('{', '&#123;')
	t = t:gsub('}', '&#125;')
	return t
end


-- Escape URLs so they display literally (no auto-linking, pipes preserved)
local function escapeURL(url)
	if not url or url == '' then return '' end
	local safe = s(url)
	safe = safe .. " " -- prevent autolinking by trailing space
	return escapeForOutput(safe)
end

-- Build a template call as literal text with predictable argument order when provided.
local function makeTemplate(name, args, order)
	local parts = {}
	if order and type(order) == 'table' then
		for _, k in ipairs(order) do
			local v = args[k]
			if v ~= nil and v ~= '' then
				table.insert(parts, '|' .. k .. '=' .. escapeForOutput(v))
			end
		end
		for k, v in pairs(args) do
			local found = false
			for _, ok in ipairs(order) do if ok == k then found = true break end end
			if not found and v ~= nil and v ~= '' then
				table.insert(parts, '|' .. k .. '=' .. escapeForOutput(v))
			end
		end
	else
		for k, v in pairs(args) do
			if v ~= nil and v ~= '' then
				table.insert(parts, '|' .. k .. '=' .. escapeForOutput(v))
			end
		end
	end
	return '{{' .. name .. table.concat(parts, '') .. '}}'
end

-- Sorting helper: compare by song, then by date
local function sortPerformances(a, b)
	local sa, sb = (a.song or ''), (b.song or '')
	if sa == sb then
		local da, db = (a.date or ''), (b.date or '')
		return da < db
	else
		return sa < sb
	end
end

function p.import(frame)
	local args = frame.args or {}
	local source = args.source or args[1]
	if not source or source == "" then
		return "Error: supply a 'source' page name containing JSON, e.g. |source=Module:MyData"
	end

	local titleObj = mw.title.new(source)
	if not titleObj then
		return "Error: invalid page name: " .. source
	end

	local raw = titleObj:getContent()
	if not raw then
		return "Error: could not read page content: " .. source
	end

	-- JSON decode function
	local decode
	if mw.text and mw.text.jsonDecode then
		decode = mw.text.jsonDecode
	else
		local ok, json = pcall(require, 'Module:JSON')
		if ok and json and type(json.decode) == 'function' then
			decode = json.decode
		else
			return "Error: No JSON decoder available (need mw.text.jsonDecode or Module:JSON)."
		end
	end

	local ok2, data = pcall(decode, raw)
	if not ok2 then
		return "Error decoding JSON: " .. tostring(data)
	end
	if type(data) ~= 'table' then
		return "Error: JSON must be an array of objects"
	end

	-- Optional filtering
	local filter_song = args.filter_song and mw.text.trim(args.filter_song) or nil
	local filterPage = (args.filterPage and args.filterPage:lower() == 'yes')
	if filterPage then
		filter_song = mw.title.getCurrentTitle().text			
	end
	
	-- Sort by song, then by date
	table.sort(data, sortPerformances)

	local out = {}
	local id = 0
	local prevSong = nil
	local enableHeadings = not (args.headings and args.headings:lower() == 'no')
	local enablePre = not (args.pre and args.pre:lower() == 'no')

	for _, obj in ipairs(data) do
		local songText = obj.song or ''

		-- Skip entries that do not match filter
		if not filter_song or songText == filter_song then
			-- Increment id only for included items
			id = id + 1

			-- Add heading when song changes
			if enableHeadings and songText ~= prevSong then
				prevSong = songText
				local escSong = escapeForOutput(songText)
				table.insert(out, '&#61;&#61;&#61; ' .. escSong .. ' &#61;&#61;&#61;')
			end

			-- Build args for Performance
			local perfArgs = {}
			if obj.song then perfArgs.song = s(obj.song) end
			if obj.event then perfArgs.event = s(obj.event) end
			if obj.context then
				if type(obj.context) == 'table' then
					perfArgs.context = table.concat(obj.context, '#')
				else
					perfArgs.context = s(obj.context)
				end
			end
			if obj.date then perfArgs.date = s(obj.date) end
			if obj.type then perfArgs["type"] = s(obj.type) end
			if obj.pos then perfArgs.pos = s(obj.pos) end
			if obj["partners"] then perfArgs["partners"] = s(obj["partners"]) end
			if obj.comment then perfArgs.comment = s(obj.comment) end
			perfArgs.id = tostring(id)

			local perfOrder = { 'song', 'event', 'context', 'date', 'type', 'pos', 'with', 'comment', 'id' }
			table.insert(out, '* ' .. makeTemplate('Performance', perfArgs, perfOrder))

			-- Video entries
			local function addVideo(url, duration, quality)
				if not url or url == '' then return end
				local vArgs = {
					pid = tostring(id),
					url = escapeURL(url),  -- avoid autolinking and preserve pipes
				}
				if duration and duration ~= '' then vArgs.duration = s(duration) end
				if quality and quality ~= '' then vArgs.quality = s(quality) end
				local videoOrder = { 'pid', 'url', 'duration', 'quality' }
				table.insert(out, '*: ' .. makeTemplate('Video', vArgs, videoOrder))
			end

			if obj.url and obj.url ~= '' then
				addVideo(obj.url, obj.duration, obj.quality)
			end
			if obj.videos and type(obj.videos) == 'table' then
				for _, v in ipairs(obj.videos) do
					if v ~= nil then
						local vurl = v.url or v["url"]
						local vdur = v.duration or obj.duration
						local vqual = v.quality or obj.quality
						addVideo(vurl, vdur, vqual)
					end
				end
			end
		end
	end

	-- Output all lines within <pre> for safe copying
	if enablePre then
		return '<pre>' .. table.concat(out, '\n') .. '</pre>'
	else
		return table.concat(out, '\n')
	end		
end

function p.importPartners(frame)
	local args = frame.args or {}
	local source = args.source or args[1]
	if not source or source == "" then
		return "Error: supply a 'source' page name containing JSON, e.g. |source=Module:MyData"
	end

	local titleObj = mw.title.new(source)
	if not titleObj then
		return "Error: invalid page name: " .. source
	end

	local raw = titleObj:getContent()
	if not raw then
		return "Error: could not read page content: " .. source
	end

	-- JSON decode function
	local decode
	if mw.text and mw.text.jsonDecode then
		decode = mw.text.jsonDecode
	else
		local ok, json = pcall(require, 'Module:JSON')
		if ok and json and type(json.decode) == 'function' then
			decode = json.decode
		else
			return "Error: No JSON decoder available (need mw.text.jsonDecode or Module:JSON)."
		end
	end

	local ok2, data = pcall(decode, raw)
	if not ok2 then
		return "Error decoding JSON: " .. tostring(data)
	end
	if type(data) ~= 'table' then
		return "Error: JSON must be an array of objects"
	end

	-- Optional filtering
	local filter_song = args.filter_song and mw.text.trim(args.filter_song) or nil
	local filterPage = (args.filterPage and args.filterPage:lower() == 'yes')
	if filterPage then
		filter_song = mw.title.getCurrentTitle().text			
	end
	
	-- Sort by song, then by date
	table.sort(data, sortPerformances)

	local out = {}
	local id = 0
	local prevSong = nil
	local enableHeadings = not (args.headings and args.headings:lower() == 'no')
	local enablePre = not (args.pre and args.pre:lower() == 'no')

	for _, obj in ipairs(data) do
		local songText = obj.song or ''

		-- Skip entries that do not match filter
		if not filter_song or songText == filter_song then
			-- Increment id only for included items
			id = id + 1

			-- Add heading when song changes
			if enableHeadings and songText ~= prevSong then
				prevSong = songText
				local escSong = escapeForOutput(songText)
				table.insert(out, '&#61;&#61;&#61; ' .. escSong .. ' &#61;&#61;&#61;')
			end

			-- Build args for Performance
			local perfArgs = {}
			if obj.song then perfArgs.song = s(obj.song) end
			if obj.event then perfArgs.event = s(obj.event) end
			if obj.context then
				if type(obj.context) == 'table' then
					perfArgs.context = table.concat(obj.context, '#')
				else
					perfArgs.context = s(obj.context)
				end
			end
			if obj.date then perfArgs.date = s(obj.date) end
			if obj.type then perfArgs["type"] = s(obj.type) end
			if obj.pos then perfArgs.pos = s(obj.pos) end
			if obj["partners"] then perfArgs["partners"] = s(obj["partners"]) end
			if obj.comment then perfArgs.comment = s(obj.comment) end
			perfArgs.id = tostring(id)

			if obj["partners"] then -- only export if partner entries exist
				local perfOrder = { 'song', 'event', 'context', 'date', 'type', 'pos', 'with', 'comment', 'id' }
				table.insert(out, makeTemplate('no_Performance', perfArgs, perfOrder))
			end
		end
	end

	-- Output all lines within <pre> for safe copying
	if enablePre then
		return '<pre>' .. table.concat(out, '\n') .. '</pre>'
	else
		return table.concat(out, '\n')
	end		
end

function p.preloadSongPage(frame)
	local title = mw.title.getCurrentTitle().text
	return "This page is about: " .. title
end

return p