Module:RenderRecipe

From Against the Storm Official Wiki

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

-------------------------------------------------------------------------------
-- This module renders the {{Recipe}} template.
-- https://hoodedhorse.com/wiki/Against_the_Storm/Template:Recipe.
--
-- The template #invokes RenderRecipe.renderRecipe(frame), below.
-- 
-- The template requires at least one of its two arguments. The first is called
-- "product". and the second is called "building". Both are passed to 
-- Module:RecipeData module, which returns one or more recipes.
--
-- Using the recipe data returned, this module creates an HTML table that 
-- displays the recipes, which replaces the template, whereever it is written
-- in the wiki markup. The table will have exactly one row if both the product 
-- and building are specified. If either is missing, then expect a table with
-- multiple rows.
-- @module RenderRecipe
local RenderRecipe = {}

--
-- Dependencies
--
RecipeData = RecipeData or require("Module:RecipeData") -- used to get recipe data



--
-- Constants
--
-- CSS classes used to style the recipe tables
local CSS_CLASS_WIKITABLE_SORTABLE = "wikitable sortable"
local CSS_CLASS_RECIPE_TABLE = "ATSrecipe"
local CSS_CLASS_RECIPE_CAPTION = "ATSrecipecaption"
local CSS_CLASS_REQUIRED_INGREDIENT = "ATSrequired"
local CSS_CLASS_SWAPPABLE_INGREDIENT = "ATSswappable"
local CSS_CLASS_EMPTY_INGREDIENT = "ATSempty"

-- the names of other templates used, by way of frame:expandTemplate
local TEMPLATE_BUILDING_LINK = "Building_link"
local TEMPLATE_RESOURCE_LINK = "Resource_link"
local TEMPLATE_STAR_SUFFIX = "Star"

-- HTML markup
local BR = "<br />"
local NL = "\n\n"



--
-- Class Variables
--
local thisFrame = {} -- save the frame on first call for later use in expandTemplate



-- 
-- Main rendering functions
-- 

-------------------------------------------------------------------------------
-- Renders an HTML table given the MediaWiki frame (which contains the 
-- arguments given to the template).
-- 
-- Uses the MediaWiki frame to access the template arguments. One or both 
-- arguments are required; calling this template with no arguments or empty
-- strings will result in an error message.
-- @param frame a table describing the MediaWiki frame; frame['args'] is a table
-- containg the template arguments; frame['args']['product'] is the first
-- argument, assigned to the key 'product'; frame['args']['building'] is the 
-- second argument, assigned to the key 'building'
-- @return a string containing the entire table in HTML markup (or an error 
-- message if a problem occured)
function RenderRecipe.renderRecipe(frame)
	
	-- need to set the class variable so expandTemplate can work later
	thisFrame = frame

	-- extract the arguments we care about from the frame
	local argProduct = frame.args.product
	local argBuilding = frame.args.building
	
	-- build the table; we'll add to this once we know the arguments are valid, but that's after the if/then blocks start
	local wikiTable = mw.html.create('table'):addClass(CSS_CLASS_WIKITABLE_SORTABLE):addClass(CSS_CLASS_RECIPE_TABLE)
	
	-- save for later once we check the variables
	local wikiTableCaption = wikiTable:tag('caption'):addClass(CSS_CLASS_RECIPE_CAPTION)
	
	-- build the header row; don't need to save it
	wikiTable:tag('tr')
		:tag('th'):wikitext("Building"):done()
		:tag('th'):wikitext("Ingredient #1"):done()
		:tag('th'):wikitext("Ingredient #2"):done()
		:tag('th'):wikitext("Ingredient #3"):done()
		:tag('th'):wikitext("Product")
	
	-- use this to store any error messages that return from the inner function
	local errorMessage = ""
	
	--
	-- Everything else depends on whether the arguments are valid and which ones were specified.
	--
	
	-- if both product and building are specified (and valid), then we can assume exactly one recipe at one building
	if argProduct and argProduct ~= "" and argBuilding and argBuilding ~= "" then
		
		-- ask for the matching recipe and make sure there was one
		local tOneRecipe = RecipeData.getRecipeAtBuilding(argProduct, argBuilding)
		if not tOneRecipe then
			return "RenderRecipe error: no product '" .. argProduct .. "' or building '" .. argBuilding .. "' found."
		end
		
		wikiTableCaption:wikitext(argProduct .. " recipe in the " .. argBuilding .. ".")
		
		-- assume only one row to render, and renderTableRow will use only the first place in the recipe anyway
		wikiTable, errorMessage = RenderRecipe.renderTableRow(tOneRecipe, wikiTable)
		
	-- if only the product is specified, then we need to assume multiple buildings
	elseif argProduct and argProduct ~= "" then
		
		-- gets a stack of buildings for the given product, and make sure there was one
		local tRecipeStack = RecipeData.getRecipeForProduct(argProduct)
		if not tRecipeStack then
			return "RenderRecipe error: no product '" .. argProduct .. "' found."
		end
		
		wikiTableCaption:wikitext(argProduct .. " recipe.")
		
		-- there are multiple places (1-6) to loop through; places have no key, so ipairs works
		for _, place in ipairs(tRecipeStack.places) do
			
			-- make a simple version of tRecipeStack with just the one matching place to the table renderer
			-- recipes are {product, pattern, place}
			local simplerRecipe = { product=argProduct, pattern=tRecipeStack.pattern, places={place} }
			
			-- each loop calls renderTableRow to add a new row to the passed table
			wikiTable, errorMessage = RenderRecipe.renderTableRow(simplerRecipe, wikiTable)
		end
	
	-- if only the building is specified, then we need to assume multiple products, but each has exactly one place
	elseif argBuilding and argBuilding ~= "" then
		
		-- gets a table of different products' recipes, and make sure there was one
		local tProducts = RecipeData.getBuildingsRecipes(argBuilding)
		if not tProducts then
			return "RenderRecipe error: no building '" .. argBuilding .. "' found."
		end
		
		wikiTableCaption:wikitext("Recipes in the " .. argBuilding .. ".")
		
		-- there are multiple products to loop through; the products have a string key, so pairs is better than ipairs
		for _, tRecipe in pairs(tProducts) do
			
			-- each call to renderTableRow adds a new row to the passed table
			wikiTable, errorMessage = RenderRecipe.renderTableRow(tRecipe, wikiTable)
		end
	
	-- something was bad about the arguments; can't print them since they may be nil
	else
		
		return "RenderRecipe error: Invalid template parameters. This template requires at least a product or building."
	end
	
	-- check whether an error message was returned; if so, append it to the
	-- HTML table wherever it is right now
	if errorMessage and "" ~= errorMessage then
		wikiTable:wikitext(errorMessage)
	end
	
	-- finally, make sure we're returning an actual string, not a Lua table
	return mw.allToString(wikiTable)
end


-------------------------------------------------------------------------------
-- Writes a new row of HTML tags and content to the provided wiki table.
--
-- Using the provided recipe (combination of a product and a building), this
-- method uses the provided HTML table object to add an additional row 
-- containing the relevant content of the recipe. This function uses only the 
-- first 'place' (building) where the product can be made; any others are
-- ignored for a single call to this function.
--
-- @param tRecipe a table of a recipe {product, pattern, places}
-- @param wikiTable a table containing an HTML-markup table that has already 
-- been started, to which this function will add a new row, cells, and content.
-- @return the updated wikiTable
-- @return an error message or nil if successful
function RenderRecipe.renderTableRow(tRecipe, wikiTable)

	-- verify we have valid things to work with
	if not tRecipe and not wikiTable then
		return nil, nil
	end
	
	-- call the helper function to do the extraction and error checking
	local errorMessage, building, stars, speed, product, number, ingredients, quantities = RenderRecipe.extractRecipeData(tRecipe)
	if errorMessage and "" ~= errorMessage then
		return wikiTable, errorMessage
	end
	
	--
	-- with the data extracted, start writing the HTML tags and content
	--
	
	-- write the first cell with the building
	local wikiTableDataRow = wikiTable:tag('tr')
		:tag('td')
			:wikitext(RenderRecipe.blTemplate(building) .. BR .. RenderRecipe.starTemplate(stars) .. BR .. speed)
	
	-- first fill in any empty cells like in the recipe book, one for each 
	-- blank ingredient
	local blanks = 3 - #ingredients
	for h = 1,blanks do
		wikiTableDataRow:tag('td'):addClass(CSS_CLASS_EMPTY_INGREDIENT)
	end
	
	-- simultaneously loop over the groups of alternative ingredients and 
	-- quantities; so use a group of ingredients and a groupOfQuantities each
	-- the groups have no keys, so ipairs ensures we are iterating over them
	-- in a consistent order with the quantities
	for i, group in ipairs(ingredients) do
		local groupOfQuantities = quantities[i]
		
		-- start the table cell that will list the ingredients
		local wikiTableDataRowDataCell = wikiTableDataRow:tag('td')
		
		-- add a class to the table cell based on whether there's only one 
		-- (required) ingredient or more than one (swappable) ingredients
		-- in the group; if more than one, create an inner table to lay
		-- things out more nicely
		if 1 == #group then
			wikiTableDataRowDataCell:addClass(CSS_CLASS_REQUIRED_INGREDIENT)
			-- we know there's just one item in group, so we don't need to loop
			wikiTableDataRowDataCell:wikitext("<span style=\"font-weight: bold\">" .. groupOfQuantities[1] .. "</span> " .. RenderRecipe.rlTemplate(group[1]))
			
		else
			wikiTableDataRowDataCell:addClass(CSS_CLASS_SWAPPABLE_INGREDIENT)
			
			-- for alignment purposes, create a table within this cell to lay 
			-- out the quantities and ingerdients
			local innerTable = wikiTableDataRowDataCell:tag('table')
			
			-- within every group of ingredients, list out the required or 
			-- alternatives within that group
			-- the items in each group do not have a given key, so ipairs 
			-- ensures we iterate in a consistent order
			for j, alt in ipairs(group) do
				local quant = groupOfQuantities[j]
				
				-- write a new row to the inner table and fill it in
				innerTable:tag('tr')
					:tag('td'):css('text-align', 'right')
						:wikitext("<span style=\"font-weight: bold\">" .. quant .. "</span>"):done()
					:tag('td')
						:wikitext(RenderRecipe.rlTemplate(alt)):done()
			end
		end
	end
    
	-- close up the table row with the product
	wikiTableDataRow:tag('td'):wikitext("<span style=\"font-weight: bold\">" .. number .. "</span> " .. RenderRecipe.rlTemplate(product,"large"))
	
	-- pass the table back in case more rows need to be added
    return wikiTable
end



-- currently unused. need to define a new way to call the template in order to invoke this function.
-- redirect a rendering request to the recipe data and create appropriate 
-- templates for the list. The caller can decide how to output the list
function RenderRecipe.getBuildingsAndStarsForProduct(frame)

	local argProduct = frame.args.product
	
	-- make sure there's an argument to work with
	if not tableRecipe then
		return "Render_Recipe Error: no product given."
	end
	
	-- get the associated recipe data, including all buildings
	local tableRecipe = RecipeData.getAllRecipes(argProduct)
	-- make sure what's returned is valid before we loop through it
	if not tableRecipe or not tableRecipe.places or #tableRecipe.places < 1 then
		return "Render_Recipe Error: no recipes found"
	end
	
	local listOfTemplates = {}
	local buildings, stars = RecipeData.getBuildingsAndStarsLists(tRecipe)
	
	-- loop through the buildings and stars and build the templates
	for i, building in pairs(buildings) do
		listOfTemplates[i] = RenderRecipe.blTemplate(building) .. " " .. RenderRecipe.pstarTemplate(stars[i])
	end
	
	return listOfTemplates
end



--
-- Helper rendering functions
--



-------------------------------------------------------------------------------
-- Helper function to extract data from the recipe to use as table content.
--
-- Calls several RecipeData helper functions that keep the inner workings of 
-- recipes hidden.
-- This function knows only the first values in each of the returned 
-- tables are useful, so we can immediately grab only the first values,
-- after confirming something valid was returned.
-- @param tRecipe
-- @return an error message, and if there is any, the rest of the return 
-- values will be nil
-- @return the name of the building
-- @return the number of stars of efficiency
-- @return the product time, represented as a string
-- @return the name of the product
-- @return the number of products produced
-- @return a table of ingredient names
-- @return a table of quantities of ingredients
function RenderRecipe.extractRecipeData(tRecipe)
	
	-- verify we have valid things to work with
	if not tRecipe then
		return "RenderRecipe error: cannot get data from invalid recipe."
	end
	
	-- first, the name of the building and the stars of efficiency for the 
	-- recipe, and we only need the first one
	local building, stars = RecipeData.getBuildingsAndStarsLists(tRecipe)
	if building and stars then
		building, stars = building[1], stars[1]
	else
		return "RenderRecipe error: could not get the buildings or stars in the recipe."
	end
	
	-- second, the speed of production for the recipe, and we only need the
	-- first one
	local speed = RecipeData.getProductionSpeed(tRecipe)
	if speed then
		speed = speed[1]
	else
		return "RenderRecipe error: could not get the production speed in the recipe."
	end
	
	-- third, the name and output number of the product, and we only need the
	-- first one
	local product, number = RecipeData.getProductAndNumbers(tRecipe)
	if product and number then
		number = number[1]
	else
		return "RenderRecipe error: could not get the product or output number of the recipe."
	end
	
	-- fourth and finally, the list of ingredients and the list-of-list of 
	-- quantities of those ingredients; the ingredients is independent of the 
	-- places, so we don't need to extract only the first one, but we do for 
	-- the list of quantities; because they are different structures, do 
	-- separate error checking
	local ingredients, quantities = RecipeData.getIngredientsAndQuantities(tRecipe)
	if not ingredients or 0 == #ingredients then
		return "RenderRecipe error: could not get the ingredients in the recipe."
	end
	if quantities then
		quantities = quantities[1] -- now ingredients and quantities have the same structure
	else
		return "RenderRecipe error: could not get the quantities in recipe."
	end
	
	-- refer to return values specified in function comment
	return nil, building, stars, speed, product, number, ingredients, quantities
end



-------------------------------------------------------------------------------
-- Helper function to fill out the Building_link template and process it into
-- HTML to return in our table.
-- 
-- This invokes an ATS wiki templates defined elsehwere, to protect variations 
-- in the respective templates and modules. Requires the name of the building 
-- to render the link to. Assumes the size should be "large," but allows 
-- specifying which size should be used. Requires the class variable 
-- 'thisFrame' has been previously captured.
-- @param strBuilding the name of the building to link to
-- @param[opt] strSize a string describing the size of the thumbnail image that
-- accompanies the link, default is "large"
-- @return a string of HTML markup
function RenderRecipe.blTemplate(strBuilding, strSize)
	
	-- validate the mw.frame is usable
	if not thisFrame then
		return "RenderRecipe error: MediaWiki frame was not captured correctly."
	end
	
	-- validate the building name
	if not strBuilding or "" == strBuilding then
		return "RenderRecipe error: cannot make a link to an invalid building name."
	end
	
	-- choose a default size if none provided
	if not strSize then
		strSize = "large"
	end
	
	-- invoke the method to process the right template
	return thisFrame:expandTemplate{ title=TEMPLATE_BUILDING_LINK, args={ strBuilding, strSize } }
end



-------------------------------------------------------------------------------
-- Helper function to fill out the Resource_link template and process it into
-- HTML to return in our table.
-- 
-- This invokes an ATS wiki templates defined elsehwere, to protect variations 
-- in the respective templates and modules. Requires the name of the resource 
-- to render the link to. Assumes the size should be "med," but allows 
-- specifying which size should be used. Requires the class variable 
-- 'thisFrame' has been previously captured.
-- @param strResource the name of the resource to link to
-- @param[opt] strSize a string describing the size of the thumbnail image that
-- accompanies the link, default is "med"
-- @return a string of HTML markup
function RenderRecipe.rlTemplate(strResource,strSize)
	
	-- validate the mw.frame is usable
	if not thisFrame then
		return "RenderRecipe error: MediaWiki frame was not captured correctly."
	end
	
	-- validate the resource name
	if not strResource or "" == strResource then
		return "RenderRecipe error: cannot make a link to an invalid resource name."
	end
	
	-- choose a default size if none provided
	if not strSize then
		strSize = "med"
	end
	
	-- invoke the method to process the right template
	return thisFrame:expandTemplate{ title=TEMPLATE_RESOURCE_LINK, args={ strResource, strSize } }
end



-------------------------------------------------------------------------------
-- Helper function to fill out the PStars template and process it into
-- HTML to return in our table.
-- 
-- This invokes an ATS wiki templates defined elsehwere, to protect variations 
-- in the respective templates and modules. Requires the number of stars to
-- render the icons. Requires the class variable 'thisFrame' has been 
-- previously captured.
-- @param intStars the number of stars
-- @return a string of HTML markup
function RenderRecipe.pstarTemplate(intStars)
	
	-- validate the mw.frame is usable
	if not thisFrame then
		return "RenderRecipe error: MediaWiki frame was not captured correctly."
	end
	
	-- validate the number of stars
	if not intStars then
		return "RenderRecipe error: missing number of stars for template."
	end
	
	-- invoke the method to process the right template
	return thisFrame:expandTemplate{ title= "P" .. intStars .. TEMPLATE_STAR_SUFFIX}
end



-------------------------------------------------------------------------------
-- Helper function to fill out the Stars template and process it into
-- HTML to return in our table.
-- 
-- This invokes an ATS wiki templates defined elsehwere, to protect variations 
-- in the respective templates and modules. Requires the number of stars to
-- render the icons. Requires the class variable 'thisFrame' has been 
-- previously captured.
-- @param intStars the number of stars
-- @return a string of HTML markup
function RenderRecipe.starTemplate(intStars)
	
	-- validate the mw.frame is usable
	if not thisFrame then
		return "RenderRecipe error: MediaWiki frame was not captured correctly."
	end
	
	-- validate the number of stars
	if not intStars then
		return "RenderRecipe error: missing number of stars for template."
	end
	
	-- invoke the method to process the right template
	return thisFrame:expandTemplate{ title= intStars .. TEMPLATE_STAR_SUFFIX}
end



-- Return this class when required into another module.
return RenderRecipe