Module:Sandbox/User:4madness/Questreq table

Documentation for this module may be created at Module:Sandbox/User:4madness/Questreq table/doc

-- 
-- Variation of [[Module:Questreq]] that outputs a table instead.
-- <nowiki>
--

local p = {}

-- Load data from quest list
local quests = mw.loadData('Module:Questreq/data')

local yesno = require( 'Module:Yesno' )

-- Main function
function p.main(frame)
	local args = frame:getParent().args
	-- named args for JSCalculator support
	local quest = args[1] or args.Quest or args.quest or ''
	local limit = tonumber(args[2]) or tonumber(args.Limit) or tonumber(args.limit) or 9
	return p._main(quest,limit)
end

function p._main(quest,limit)
	if (quest == nil or trim(quest) == '') then
		return '\'\'Error: Missing quest name!\'\'\n\n'
	end
	
	-- Handle butchered page names gracefully if necessary
	quest = quest:gsub('&#039;','\'')
	
	local rettable = mw.html.create('table')
			:addClass('wikitable')
			:addClass('sortable')
		:tag('tr')
			:tag('th')
			:wikitext('[[File:Quest.png|21px|link=]] Quest Name')
			:done()
			:tag('th')
			:addClass('unsortable')
			:wikitext('Skill requirements')
			:done()
			:tag('th')
			:wikitext('Quest stage')
			:done()
		:done() -- end header row
	local quest_list = get_quest_list(quest,1,limit)
	local hasData = false
	for q,qdata in pairs(quest_list) do
		hasData = true
		break
	end
	if hasData then
		return rettable:node(assemble_quest_rows(quest_list))
	else
		return rettable:tag('tr')
				:tag('td')
					:attr('colspan', 3)
					:wikitext('\'\'Quest not found.\'\'')
				:done()
			:done()
	end
end

function assemble_quest_rows(list)
	local chain = mw.html.create(nil)
	local refundefined = true
	
	-- for each quest, assemble a table row with three columns.
	-- quest name, skill requirments, and stage of completion
	for qname, qdata in pairs(list) do
		chain = chain:tag('tr')
				:tag('td') -- start quest name cell
				:wikitext(qdata.link or qname)

		if (qdata.incomplete) then
			if (refundefined) then
				chain = chain:tag('ref')
						:attr('name', 'Incomplete')
						:attr('group', 'questreqtable')
						:wikitext('This quest has requirements that were omitted.')
					:done()
			else
				chain = chain:tag('ref', {selfClosing=true})
						:attr('name', 'Incomplete')
						:attr('group', 'questreqtable')
					:done()
			end
		end

		chain = chain:done() -- end quest name cell
				:tag('td')
				:wikitext(mw.getCurrentFrame():expandTemplate({title = 'User:4madness/Calculators/Complete quest requirements/Extract quest skills', args = {qname}}))
				:done()
				:tag('td')
				:wikitext(qdata.stage or '')
				:done()
			:done() -- end row
	end
	
	return chain
end

-- Function to flatten the table into one entry
function p.summary(frame)
	local args = frame:getParent().args
	-- named args for JSCalculator support
	local quest = args[1] or args.Quest or args.quest or ''
	local limit = tonumber(args[2]) or tonumber(args.Limit) or tonumber(args.limit) or 15
	local hideext = yesno( args.ext ) == false
	return p._summary(quest,limit,hideext)
end

function p._summary(quest,limit,hideext)
	if (quest == nil or trim(quest) == '') then
		return '\'\'Error: Missing quest name!\'\'\n\n'
	end
	
	-- Handle butchered page names gracefully if necessary
	quest = quest:gsub('&#039;','\'')
	
	-- cache frame reference
	local frame = mw.getCurrentFrame()
	
	-- get formatted quest link + validate name
	local qlink = quest
	local quest_list = get_quest_list(quest,1,0)
	local hasData = false
	for q, qdata in pairs(quest_list) do
		hasData = qdata.stage ~= nil
		qlink = qdata.link or q
	end
	if (hasData == false) then
		-- check for redirect to catch typos
		redirectedPage = mw.title.makeTitle(0, quest).redirectTarget
		if (redirectedPage) then
			return p._summary(tostring(redirectedPage),limit,hideext)
		end
	end
	
	-- get complete quest list up to limit
	quest_list = get_quest_list(quest,1,limit)
	local max_stats = {}
	
	for qname,qdata in pairs(quest_list) do
		local skill, level, value = '', 0, ''
		allRawSkillReqArgs = frame:expandTemplate({title = 'User:4madness/Calculators/Complete quest requirements/Extract quest skills.lua', args = {qname}})
		for oneRawSkillReqArgs in string.gmatch(allRawSkillReqArgs, '([^!]+)') do -- split by '!' symbols
			local args = {}
			local hasData = false -- only save stat
			for k, v in string.gmatch(oneRawSkillReqArgs, '([^|]*)=([^|]*)') do
				hasData = true
				if (k == 1 or k == '1') then
					skill = firstToUpper(v:lower())
				elseif (k == 2 or k == '2') then
					level = v
				end
			end
			if (hasData and (max_stats[skill] == nil or max_stats[skill] < level)) then
				max_stats[skill] = level
			end
		end
	end
	
	local stats_string, eol = '', hideext and '|ext=no}}' or '}}'
	for skill,level in pairsByKeys(max_stats) do -- pairs sorted alphabetically
		stats_string = stats_string .. '{{Skillreq|' .. skill .. '|' .. level .. eol
	end
	
	return mw.html.create('table')
			:addClass('wikitable')
		:tag('tr')
			:tag('th')
			:wikitext('[[File:Quest.png|21px|link=]] Quest Name')
			:done()
			:tag('th')
			:wikitext('Skill requirements')
			:done()
		:done() -- end header row
		:tag('tr')
			:tag('td')
			:wikitext(hasData and qlink or ('\'\'\'' .. qlink .. '\'\'\''))
			:done()
			:node(hasData
				and mw.html.create('td'):addClass('qc-active'):wikitext(frame:preprocess(stats_string)):done()
				or mw.html.create('td'):wikitext('N/A'):done())
		:done()
end

--
-- Recursive list function
-- Level determines how deep the indentation is
-- Replaces 'Started:' modifier
-- If the quest just listed was found in the big list and the limit for level is not reached
-- the quest's requirements will be listed as a sublist 1 level deeper
--l
function get_quest_list(quest,level,limit)
	local req_list = {}
	if quest then
		local started
		-- Look for the 'Started:' modifier and replace it
		-- If found, set boolean to true
		if quest:find('Started:') then
			quest = quest:gsub('Started:%s*','')
			started = true
		end
		
		-- new data table for current quest
		req_list[quest] = {}
		
		-- a handle for the current quest's data table
		local cur_qdata = req_list[quest]
		
		-- Look for quest in the list
		local subreqs = quests[quest]
		if subreqs then
			cur_qdata.link = tidy_link(quest)
			
			-- stage is '' for top-most quest, or one of 'started' or 'completed'
			cur_qdata.stage = level == 1 and '' or (started and 'started' or 'completed')
			if subreqs[1] then
				if level <= limit then
					for i, q in ipairs(subreqs) do
						-- get quests under subquest q, then merge with master list
						local sub_list = get_quest_list(q,level+1,limit)
						for q, qdata in pairs(sub_list) do
							req_list[q] = qdata
						end
					end
				else
					-- add marker about missing requirements
					cur_qdata.incomplete = true
				end
			end
		else
			-- skip entries containing quest points
			if (quest:lower():find('quest points')) then
				return {}
			end
		end
	end
	
	return req_list -- associative array containing quest names as keys and data as values
end

--https://stackoverflow.com/a/2421746/3068190
function firstToUpper(str)
    return (str:gsub("^%l", string.upper))
end

function trim(s)
   return s and (s:gsub("^%s*(.-)%s*$", "%1")) or ''
end

--https://www.lua.org/pil/19.3.html
function pairsByKeys (t, f)
	local a = {}
	for n in pairs(t) do table.insert(a, n) end
	table.sort(a, f)
	local i = 0      -- iterator variable
	local iter = function ()   -- iterator function
		i = i + 1
		if a[i] == nil then return nil
		else return a[i], t[a[i]]
		end
	end
	return iter
end

--
-- Function to tidy quest names into links
-- Any parenthetical (e.g. '(quest)') will be removed from the text, but remain in the link
-- 'Recipe for Disaster/' will be replaced in the RfD subquests, so that only the subquest name appears as text
-- Returns a link
-- The 'Full:' modifier is removed
--
function tidy_link(name)
	if name then
		if name:find('Full:') then
			name = name:sub(6)
		end
		local alt = name:match('(.*)%(.*%)') or name

		if name:find('Recipe for Disaster%/') then
			alt = name:gsub('Recipe for Disaster%/','')
		end

		name = string.format('[[%s|%s]]',name,alt)
	end
	return name
end

return p