Module:Data to Table

Revision as of 17:25, 10 October 2026 by ReisuDesign (talk | contribs) (Created page with "local p = {} local CleanTT = require('Module:tt').CleanTT local lib = require('Module:Feature') local ne = lib.isNotEmpty local block = false -- Check if the entry is the first on the level local first = {true, true, true} -- Check if the entry is right after the end of a deeper level local previous = {false, false, false} -- Escape characters for Lua strings local function clean(txt) return txt :gsub('\\', '\\\\') :gsub('\'', '\\\'') end -- Parse DPL data l...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This module implements {{Data to Table}}.


local p = {}

local CleanTT = require('Module:tt').CleanTT
local lib = require('Module:Feature')
local ne = lib.isNotEmpty

local block = false

-- Check if the entry is the first on the level
local first = {true, true, true}

-- Check if the entry is right after the end of a deeper level
local previous = {false, false, false}


-- Escape characters for Lua strings
local function clean(txt)
	return txt
		:gsub('\\', '\\\\')
		:gsub('\'', '\\\'')
end


-- Parse DPL data
local function parseInfo(info, frame)
	if not info then
		return {}
	end

	if ne(frame.args.cleanTT) then
		info = CleanTT(info)
	end

	return lib.parseTemplateFormat(
		info,
		';;',
		'$$$',
		'=='
	)
end


-- Get grouped JSON data
local function getJsonGroups(frame)
	local groups = {}

	local i = 1

	while true do
		local groupName = frame.args['group' .. i]
		local groupInfo = frame.args['group' .. i .. '_info']

		-- Stop when neither argument exists.
		if groupName == nil and groupInfo == nil then
			break
		end

		if ne(groupName) and groupInfo ~= nil then

			local info = parseInfo(groupInfo, frame)
			local entries = {}

			-- Collect entries
			for _, entry in lib.spairs(
				info,
				function(tVal, a, b)
					local aPage = tostring(tVal[a].page or a)
					local bPage = tostring(tVal[b].page or b)

					return aPage < bPage
				end
			) do

				if ne(entry.page) then
					entries[#entries + 1] = entry.page
				end

			end

			-- Only add the group if it actually contains entries.
			if #entries > 0 then
				groups[groupName] = entries
			end
		end

		i = i + 1
	end

	return groups
end


-- Format grouped data as readable JSON
function p.FormatJSONGroups(groups)
	local result = {}

	table.insert(result, '{')

	-- Get group names
	local groupNames = {}

	for name in pairs(groups) do
		groupNames[#groupNames + 1] = name
	end

	table.sort(groupNames)

	-- Build each group
	for i, name in ipairs(groupNames) do

		local entries = groups[name]
		local groupComma = (i < #groupNames) and ',' or ''

		table.insert(
			result,
			'\t' ..
			mw.text.jsonEncode(name) ..
			': ['
		)

		for j, entry in ipairs(entries) do

			local entryComma = (j < #entries) and ',' or ''

			table.insert(
				result,
				'\t\t' ..
				mw.text.jsonEncode(entry) ..
				entryComma
			)

		end

		table.insert(
			result,
			'\t]' .. groupComma
		)
	end

	table.insert(result, '}')

	return table.concat(result, '\n')
end


function p.main(frame)
	local info = {}
	local dataKeys = {}
	local rawKeys = {}

	-- ============================================================
	-- JSON MODE
	-- ============================================================

	if tostring(frame.args.json) == '1' then

		-- --------------------------------------------------------
		-- Grouped JSON mode
		-- --------------------------------------------------------

		local groups = getJsonGroups(frame)

		-- If groups were supplied, output the complete JSON object.
		if next(groups) ~= nil then

			local jsonText = p.FormatJSONGroups(groups)

			return frame:extensionTag(
				'syntaxhighlight',
				jsonText,
				{
					lang = 'json',
					copy = ''
				}
			)
		end


		-- --------------------------------------------------------
		-- Simple JSON array mode
		-- --------------------------------------------------------

		if frame.args.info then

			info = parseInfo(frame.args.info, frame)

			local jsonEntries = {}

			for _, entry in lib.spairs(
				info,
				function(tVal, a, b)
					local aPage = tostring(tVal[a].page or a)
					local bPage = tostring(tVal[b].page or b)

					return aPage < bPage
				end
			) do

				if ne(entry.page) then
					jsonEntries[#jsonEntries + 1] = entry.page
				end

			end

			local jsonText = mw.text.jsonEncode(jsonEntries)

			return frame:extensionTag(
				'syntaxhighlight',
				jsonText,
				{
					lang = 'json',
					copy = ''
				}
			)
		end

		return ''
	end


	-- ============================================================
	-- NORMAL LUA MODE
	-- ============================================================

	-- Parse DPL data
	if frame.args.info then

		info = parseInfo(frame.args.info, frame)

	else
		return ''
	end


	-- Parse keys
	if frame.args.keys then
		dataKeys = p.ParseKeys(frame.args.keys)
	end


	-- Parse unquoted/raw keys
	if frame.args.unquoted then
		rawKeys = p.ParseKeys(frame.args.unquoted)
	end


	-- Block formatting
	if ne(frame.args.block) then
		block = true
	end


	local sortkey = frame.args.sortkey or 'page'
	local subtable = frame.args.subtable


	-- Constant value mode
	-- If |value=day is supplied, every page gets that value.
	local constantValue = frame.args.value
	local valueUnquoted = false

	if constantValue ~= nil then

		for _, rawKey in ipairs(rawKeys) do

			if rawKey == 'value' then
				valueUnquoted = true
				break
			end

		end

	end


	-- Check whether a key should be output as raw Lua
	local function isUnquotedKey(key)

		for _, rawKey in ipairs(rawKeys) do

			if rawKey == key then
				return true
			end

		end

		return false
	end


	-- Build result
	local result = mw.html.create()
		:wikitext('return {\n')


	for _, entry in lib.spairs(
		info,
		function(tVal, a, b)

			if sortkey == 'INPUT' then
				return a < b
			else
				return (tVal[a][sortkey] or '') <
					(tVal[b][sortkey] or '')
			end

		end
	) do

		if ne(entry.page) then

			entry.page = clean(entry.page)


			-- ----------------------------------------------------
			-- Constant value mode
			-- ----------------------------------------------------

			if constantValue ~= nil and #dataKeys == 0 then

				local value

				if valueUnquoted then
					value = clean(constantValue)
				else
					value = '\'' ..
						clean(constantValue) ..
						'\''
				end

				result:wikitext(
					'\t[\'',
					entry.page,
					'\'] = ',
					value,
					',\n'
				)


			-- ----------------------------------------------------
			-- No data keys
			-- ----------------------------------------------------

			elseif #dataKeys == 0 then

				result:wikitext(
					'\t\'',
					entry.page,
					'\',\n'
				)


			-- ----------------------------------------------------
			-- Normal nested table mode
			-- ----------------------------------------------------

			else

				-- Reset formatting state for every entry
				first = {true, true, true}
				previous = {false, false, false}

				local valid = false
				local values = mw.html.create()


				-- Entry name
				values:wikitext(
					'\t[\'',
					entry.page,
					'\'] = {'
				)


				-- Build entry
				local function parseLevels(K, level)

					level = level or 1

					if type(K) == 'string' then

						valid = true

						-- Use the value if present.
						-- BuildEntry handles missing values.
						values:node(
							p.BuildEntry(
								entry[K],
								K,
								level,
								subtable,
								isUnquotedKey(K)
							)
						)


					elseif type(K) == 'table' then

						level = level + 1

						for _, K2 in ipairs(K) do
							parseLevels(K2, level)
						end

						values:wikitext(
							'\n',
							string.rep('\t', level - 1),
							'},'
						)

					end
				end


				parseLevels(dataKeys, 1)


				-- Return table if there is valid data
				if valid then

					values:wikitext('\n')
					result:node(values)

				else

					result:wikitext(
						'\t\'',
						entry.page,
						'\',\n'
					)

				end
			end
		end
	end


	result:wikitext('}')


	-- ============================================================
	-- LUA SYNTAX HIGHLIGHT
	-- ============================================================

	local luaText = tostring(result)

	-- Decode HTML entities introduced by mw.html
	luaText = mw.text.decode(luaText)


	local output = frame:extensionTag(
		'syntaxhighlight',
		luaText,
		{
			lang = 'lua',
			copy = ''
		}
	)


	-- ============================================================
	-- COPY BUTTON
	-- ============================================================

	-- Keep Module:CopyText for normal Lua output.
	if frame.args.copy then

		output = require('Module:CopyText')._main({
			output,
			frame.args.copy,
			'Copy Table'
		})

	end


	return output
end


-- ================================================================
-- FORMAT DATA
-- ================================================================

function p.BuildEntry(value, key, level, subtable, unquoted)

	level = level or 1

	local container = mw.html.create()


	-- Default missing values
	if value == nil or value == '' then

		if key == 'number' then

			value = '0'
			unquoted = true

		else

			value = ''
			unquoted = false

		end
	end


	-- Format value
	if subtable and value ~= '' and value:find(subtable) then

		value = lib.split(
			clean(value),
			subtable
		)

		value = '{ \'' ..
			table.concat(value, '\', \'') ..
			' }'


	elseif unquoted and value ~= '' then

		-- Raw Lua value
		value = clean(value)


	else

		-- String value
		value = '\'' ..
			clean(value) ..
			'\''

	end


	-- Format key as ['key']
	local formattedKey =
		'[\'' .. clean(key) .. '\']'


	-- First item on this level
	if first[level] then

		container:wikitext(
			'\n',
			string.rep('\t', level),
			formattedKey,
			' = ',
			value,
			','
		)

		first[level] = false


		if level > 2 then
			first[2] = false
		end


		if level > 1 then
			first[1] = false
		end


	-- Subsequent item on this level
	else

		container:wikitext(
			'\n',
			string.rep('\t', level),
			formattedKey,
			' = ',
			value,
			','
		)

		previous[level] = false

	end


	return container
end


-- ================================================================
-- PARSE DATA KEYS INTO SPECIFIED LEVELS
-- ================================================================

function p.ParseKeys(keys)

	keys = lib.split(keys, ';;')

	for i, key in pairs(keys) do

		if key:find('//') then

			local keys2 = lib.split(key, '//')

			for i2, key2 in pairs(keys2) do

				if key2:find('@@') then
					keys2[i2] = lib.split(key, '@@')
				end

			end

			keys[i] = keys2
		end
	end

	return keys
end


return p