Module:RenderRecipe: Difference between revisions

From Against the Storm Official Wiki
No edit summary
m (making numbers in swappable ingredients bold too)
 
(50 intermediate revisions by the same user not shown)
Line 1: Line 1:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Renders the {{recipe}} template
-- This module renders the {{Recipe}} template.
-- https://hoodedhorse.com/wiki/Against_the_Storm/Template:Recipe.
--
--
-- Takes at least one argument. The first, requried, is the name of the
-- The template #invokes RenderRecipe.renderRecipe(frame), below.
-- resource for which the recipes are needed. Optionally, the second argument
--
-- is the name of the building.
-- The template requires at least one of its two arguments. The first is called
-- This module renders small wikimarkup tables to represent one or more  
-- "product". and the second is called "building". Both are passed to
-- recipes: one table if the building was specified. several tables
-- Module:RecipeData module, which returns one or more recipes.
-- corresponding to all the buildings in which the resource can be produced.
--
-------------------------------------------------------------------------------
-- 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 = {}
local RenderRecipe = {}


local RecipeData = require("Module:RecipeData") -- lookup table for recipes
--
-- Dependencies
--
RecipeData = RecipeData or require("Module:RecipeData") -- used to get recipe data






-------------------------------------------------------------------------------
--
-- Constants
-- Constants
-------------------------------------------------------------------------------
--
local CSS_CLASS_RECIPE_TABLE = "class=\"ATSrecipe\""
-- CSS classes used to style the recipe tables
local CSS_CLASS_REQUIRED_INGREDIENT = "class=\"ATSrequired\""
local CSS_CLASS_WIKITABLE_SORTABLE = "wikitable sortable"
local CSS_CLASS_SWAPPABLE_INGREDIENT = "class=\"ATSswappable\""
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_BUILDING_LINK = "Building_link"
local TEMPLATE_RESOURCE_LINK = "Resource_link"
local TEMPLATE_RESOURCE_LINK = "Resource_link"
local TEMPLATE_STAR_SUFFIX = "Star"
local TEMPLATE_STAR_SUFFIX = "Star"
local BR = " <br /> "
local NL = " \n "


-- 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
--


-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Main rendering function
-- Renders an HTML table given the MediaWiki frame (which contains the  
-- uses ResourceData lookup function, parses the result and handles errors
-- arguments given to the template).
-- with default values, then assembles a final string to return to the wiki
--  
-------------------------------------------------------------------------------
-- Uses the MediaWiki frame to access the template arguments. One or both
-- need this in whole-class scope, sorry
-- arguments are required; calling this template with no arguments or empty
local thisFrame = {}
-- 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)
function RenderRecipe.renderRecipe(frame)
 
-- need to set the class variable so expandTemplate can work later
thisFrame = frame
thisFrame = frame


local argProductName = frame.args.product
-- extract the arguments we care about from the frame
local argBuildingName = frame.args.building
local argProduct = frame.args.product
local argBuilding = frame.args.building
if not argProductName then
-- build the table; we'll add to this once we know the arguments are valid, but that's after the if/then blocks start
return "Render_Recipe Error: no product given."
local wikiTable = mw.html.create('table'):addClass(CSS_CLASS_WIKITABLE_SORTABLE):addClass(CSS_CLASS_RECIPE_TABLE)
end
-- 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 = ""
-- used by second case where we need to loop over the results
--
local strToReturn = ""
-- Everything else depends on whether the arguments are valid and which ones were specified.
--
-- decide how to proceed based on whether the building was specified
-- if both product and building are specified (and valid), then we can assume exactly one recipe at one building
if argBuildingName then -- 1. easier case, when the building was specified
if argProduct and argProduct ~= "" and argBuilding and argBuilding ~= "" then
local tRecipe = RecipeData.getRecipeAtBuilding(argProductName, argBuildingName)
if not tRecipe then
-- ask for the matching recipe and make sure there was one
return "Render_Recipe Error: no recipe found."
local tOneRecipe = RecipeData.getRecipeAtBuilding(argProduct, argBuilding)
if not tOneRecipe then
return "RenderRecipe error: no product '" .. argProduct .. "' or building '" .. argBuilding .. "' found."
end
end
--renderTable will use just the first place in the recipe, which is perfect for this version
wikiTableCaption:wikitext(argProduct .. " recipe in the " .. argBuilding .. ".")
return RenderRecipe.renderTable(tRecipe)
else -- 2. harder case, when no building was specified, need to loop
-- assume only one row to render, and renderTableRow will use only the first place in the recipe anyway
wikiTable, errorMessage = RenderRecipe.renderTableRow(tOneRecipe, wikiTable)
local tRecipe = RecipeData.getAllRecipes(argProductName)
-- if only the product is specified, then we need to assume multiple buildings
elseif argProduct and argProduct ~= "" then
if not tRecipe then
-- gets a stack of buildings for the given product, and make sure there was one
return "Render_Recipe Error: no recipe found."
local tRecipeStack = RecipeData.getRecipeForProduct(argProduct)
if not tRecipeStack then
return "RenderRecipe error: no product '" .. argProduct .. "' found."
end
end
-- go through each place and send a shallow copy version of tRecipe to the table renderer,
wikiTableCaption:wikitext(argProduct .. " recipe.")
-- then concatenate them with appropriate wiki markup
for i, place in pairs(tRecipe.places) do
-- there are multiple places (1-6) to loop through; places have no key, so ipairs works
local tempRecipe = { product=argProductName, pattern=tRecipe.pattern, places={place} }
for _, place in ipairs(tRecipeStack.places) do
-- add newlines between tables
-- make a simple version of tRecipeStack with just the one matching place to the table renderer
if i > 1 then
-- recipes are {product, pattern, place}
strToReturn = strToReturn .. "\n\n"
local simplerRecipe = { product=argProduct, pattern=tRecipeStack.pattern, places={place} }
end
-- 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
strToReturn = strToReturn .. RenderRecipe.renderTable(tRecipe)
-- each call to renderTableRow adds a new row to the passed table
wikiTable, errorMessage = RenderRecipe.renderTableRow(tRecipe, wikiTable)
end
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
end
return strToReturn
-- 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
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)


-- writes the wiki table markup to represent a recipe
-- verify we have valid things to work with
-- takes a recipe table (pattern and place), and uses only the first place
if not tRecipe and not wikiTable then
function RenderRecipe.renderTable(tRecipe)
return nil, nil
 
if not tRecipe then
return nil
end
end
-- call the helper functions to extract the data, make sure we have something usable,
-- call the helper function to do the extraction and error checking
-- then get only the first values since we're assuming that there's only one top-level record in tRecipe
local errorMessage, building, stars, speed, product, number, ingredients, quantities = RenderRecipe.extractRecipeData(tRecipe)
local building, stars = RecipeData.getBuildingsAndStarsLists(tRecipe)
if errorMessage and "" ~= errorMessage then
if building and stars then
return wikiTable, errorMessage
building, stars = building[1], stars[1]
else
return "Render_Recipe Error: did not find building or stars in recipe."
end
end
local speed = RecipeData.getProductionSpeed(tRecipe)
--
if speed then
-- with the data extracted, start writing the HTML tags and content
speed = speed[1]
--
else
return "Render_Recipe Error: did not find production speed in recipe."
end
local product, number = RecipeData.getProductAndNumbers(tRecipe)
-- write the first cell with the building
if product and number then
local wikiTableDataRow = wikiTable:tag('tr')
number = number[1]
:tag('td')
else
:wikitext(RenderRecipe.blTemplate(building) .. BR .. RenderRecipe.starTemplate(stars) .. BR .. speed)
return "Render_Recipe Error: did not find product or output numbers in recipe."
end
-- need separate error checking, because ingredients is the same structure as EACH
-- first fill in any empty cells like in the recipe book, one for each
-- of the top-level elements of quantities
-- blank ingredient
local ingredients, quantities = RecipeData.getIngredientsAndQuantities(tRecipe)
local blanks = 3 - #ingredients
if not ingredients or 0 == #ingredients then
for h = 1,blanks do
return "Render_Recipe Error: did not find ingredients in recipe."
wikiTableDataRow:tag('td'):addClass(CSS_CLASS_EMPTY_INGREDIENT)
end
-- just want the first values since we're assuming there's only one top-level record in tRecipe
if quantities then
quantities = quantities[1] -- now ingredients and quantities have the same structure
else
return "Render_Recipe Error: did not find quantities in recipe."
end
end
local wikiTable = "<nowiki>{| " .. CSS_CLASS_RECIPE_TABLE .. NL ..
-- simultaneously loop over the groups of alternative ingredients and  
    "| " .. RenderRecipe.blTemplate(building) .. BR .. RenderRecipe.starTemplate(stars) .. BR .. speed .. NL
-- quantities; so use a group of ingredients and a groupOfQuantities each
-- the groups have no keys, so ipairs ensures we are iterating over them
-- looping over groups of alternative ingredients and quantities simultaneously,
-- in a consistent order with the quantities
-- so use local group and groupOfQuantities for each i
for i, group in ipairs(ingredients) do
for i, group in ipairs(ingredients) do
local groupOfQuantities = quantities[i]
local groupOfQuantities = quantities[i]
-- on all but the first group, create a new cell with a + sign to separate ingredient groups
-- start the table cell that will list the ingredients
if i > 1 then
local wikiTableDataRowDataCell = wikiTableDataRow:tag('td')
wikiTable = wikiTable .. " || + "
end
-- prefix the table cell based on whether there's only one (required) or more (swappable) ingredients
-- add a class to the table cell based on whether there's only one  
-- in the group
-- (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
if 1 == #group then
wikiTable = wikiTable ..
wikiTableDataRowDataCell:addClass(CSS_CLASS_REQUIRED_INGREDIENT)
"|" .. CSS_CLASS_REQUIRED_INGREDIENT .. "| " .. NL
-- 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
else
wikiTable = wikiTable ..
wikiTableDataRowDataCell:addClass(CSS_CLASS_SWAPPABLE_INGREDIENT)
"|" .. CSS_CLASS_SWAPPABLE_INGREDIENT .. "| " .. NL
end
-- for alignment purposes, create a table within this cell to lay
-- out the quantities and ingerdients
-- for every group, list out the required or alternatives within that group
local innerTable = wikiTableDataRowDataCell:tag('table')
for j, alt in ipairs(group) do
local quant = groupOfQuantities[j]
-- need to add breaks only between the first and any subsequent options
-- within every group of ingredients, list out the required or
if j > 1 then
-- alternatives within that group
wikiTable = wikiTable .. BR .. NL
-- 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
wikiTable = wikiTable .. quant .. " " .. RenderRecipe.rlTemplate(alt) .. NL
end
end
end
end
      
      
wikiTable = wikiTable .. NL ..
-- close up the table row with the product
"| = " .. NL ..
wikiTableDataRow:tag('td'):wikitext("<span style=\"font-weight: bold\">" .. number .. "</span> " .. RenderRecipe.rlTemplate(product,"large"))
"| " .. number .. " " .. RenderRecipe.rlTemplate(product,"large") .. NL ..
"|}</nowiki>"
-- pass the table back in case more rows need to be added
     return wikiTable
     return wikiTable
end
end
Line 182: Line 266:




-- 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  
-- redirect a rendering request to the recipe data and create appropriate  
-- templates for the list. The caller can decide how to output the list
-- templates for the list. The caller can decide how to output the list
function RenderRecipe.getBuildingsAndStarsForProduct(frame)
function RenderRecipe.getBuildingsAndStarsForProduct(frame)


local argProductName = frame.args.product
local argProduct = frame.args.product
-- make sure there's an argument to work with
-- make sure there's an argument to work with
Line 194: Line 279:
-- get the associated recipe data, including all buildings
-- get the associated recipe data, including all buildings
local tableRecipe = RecipeData.getAllRecipes(argProductName)
local tableRecipe = RecipeData.getAllRecipes(argProduct)
-- make sure what's returned is valid before we loop through it
-- make sure what's returned is valid before we loop through it
if not tableRecipe or not tableRecipe.places or #tableRecipe.places < 1 then
if not tableRecipe or not tableRecipe.places or #tableRecipe.places < 1 then
Line 210: Line 295:
return listOfTemplates
return listOfTemplates
end
end
--
-- Helper rendering functions
--






-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Helper rendering functions
-- Helper function to extract data from the recipe to use as table content.
-- these do not call member functions, but write out the actual templates
--
-- (which will in turn invoke the code. this is to protect variations in the
-- Calls several RecipeData helper functions that keep the inner workings of
-- modules, but we may decide later it should be different)
-- 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
 
 
 
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
--use huge size for recipes unless speified
-- Helper function to fill out the Building_link template and process it into
function RenderRecipe.blTemplate(strBuilding,strSize)
-- 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
if not strSize then
strSize = "huge"
strSize = "large"
end
end
return "{{" .. TEMPLATE_BUILDING_LINK .. "|" .. strBuilding .. "|" .. strSize .. "}}"
-- invoke the method to process the right template
--return thisFrame:expandTemplate{ title=TEMPLATE_BUILDING_LINK, args={ strBuilding, strSize } }
return thisFrame:expandTemplate{ title=TEMPLATE_BUILDING_LINK, args={ strBuilding, strSize } }
end
end


-- use recipe-sized icons, "med", unless specified
 
 
-------------------------------------------------------------------------------
-- 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)
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
if not strSize then
strSize = "med"
strSize = "med"
end
end
return "{{" .. TEMPLATE_RESOURCE_LINK .. "|" .. strResource .. "|" .. strSize .. "}}"
-- invoke the method to process the right template
--return thisFrame:expandTemplate{ title=TEMPLATE_RESOURCE_LINK, args={ strResource, strSize } }
return thisFrame:expandTemplate{ title=TEMPLATE_RESOURCE_LINK, args={ strResource, strSize } }
end
end


-- use the parentheses version, with "P"
 
 
-------------------------------------------------------------------------------
-- 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)
function RenderRecipe.pstarTemplate(intStars)
return "{{P" .. intStars .. "star}}"
-- validate the mw.frame is usable
--return thisFrame:expandTemplate{ title= "P" .. intStars .. TEMPLATE_STAR_SUFFIX}
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
end


-- no parens version
 
 
-------------------------------------------------------------------------------
-- 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)
function RenderRecipe.starTemplate(intStars)
return "{{" .. intStars .. "star}}"
-- validate the mw.frame is usable
--return thisFrame:expandTemplate{ title= intStars .. TEMPLATE_STAR_SUFFIX}
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
end






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

Latest revision as of 14:31, 19 February 2023

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