Module:RenderRecipe: Difference between revisions

From Against the Storm Official Wiki
(changed renderTable inner funcion to just make a row of a table; now the result is one table in all cases--some will have multiple rows)
(added better commenting. added inner tables to better lay out ingredient quantities. separated the extraction of data from the recipe into a separate helper method. added a debug-style argument possibility to make the template print itself from the Template:Recipe page)
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 the
-- recipes: one table if the building was specified. several tables
-- 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 = {}
--
-- Class Variables
--
local RecipeData = require("Module:RecipeData") -- used to get recipe data


local RecipeData = require("Module:RecipeData") -- lookup table for recipes
local thisFrame = {} -- save the frame on first call for later use in expandTemplate






-------------------------------------------------------------------------------
--
-- Constants
-- Constants
-------------------------------------------------------------------------------
--
-- CSS classes used to style the recipe tables
local CSS_CLASS_WIKITABLE_SORTABLE = "wikitable sortable"
local CSS_CLASS_WIKITABLE_SORTABLE = "wikitable sortable"
local CSS_CLASS_RECIPE_TABLE = "ATSrecipe"
local CSS_CLASS_RECIPE_TABLE = "ATSrecipe"
Line 25: Line 38:
local CSS_CLASS_SWAPPABLE_INGREDIENT = "ATSswappable"
local CSS_CLASS_SWAPPABLE_INGREDIENT = "ATSswappable"
local CSS_CLASS_EMPTY_INGREDIENT = "ATSempty"
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"
-- HTML markup
local BR = "<br />"
local BR = "<br />"
local NL = "\n\n"
local NL = "\n\n"
--
-- 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)
Line 46: Line 75:
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
-- build the table header
-- 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)
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')
wikiTable:tag('tr')
:tag('th'):wikitext("Building"):done()
:tag('th'):wikitext("Building"):done()
Line 58: Line 93:
:tag('th'):wikitext("Product")
:tag('th'):wikitext("Product")
-- both product and building are specified
-- use this to store any error messages that return from the inner function
if argProductName and argProductName ~= "" and argBuildingName and argBuildingName ~= "" then
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
-- returns exactly one recipe with one place
-- ask for the matching recipe and make sure there was one
local recipe = RecipeData.getRecipeAtBuilding(argProductName, argBuildingName)
local tOneRecipe = RecipeData.getRecipeAtBuilding(argProduct, argBuilding)
if not recipe then
if not tOneRecipe then
return "Render_Recipe Error: no recipe found with product=" .. argProductName .. " and building=" .. argBuildingName .. "."
return "RenderRecipe error: no product '" .. argProduct .. "' or building '" .. argBuilding .. "' found."
end
end
-- only one to render; renderTableRow returns the wikiTable passed to it
wikiTableCaption:wikitext(argProduct .. " recipe in the " .. argBuilding .. ".")
-- renderTableRow will use just the first place in the recipe, which is perfect for this version with only one place
wikiTable = RenderRecipe.renderTableRow(recipe, wikiTable)
-- assume only one row to render, and renderTableRow will use only the first place in the recipe anyway
wikiTable, errorMessage = RenderRecipe.renderTableRow(tOneRecipe, wikiTable)
-- only product is specified
elseif argProductName and argProductName ~= "" then
-- if only the product is specified, then we need to assume multiple buildings
elseif argProduct and argProduct ~= "" then
-- gets a whole product's recipe stack, which may be any number of places (1-6)
-- gets a stack of buildings for the given product, and make sure there was one
local tRecipe = RecipeData.getRecipeForProduct(argProductName)
local tRecipeStack = RecipeData.getRecipeForProduct(argProduct)
if not tRecipe then
if not tRecipeStack then
return "Render_Recipe Error: no recipe found with product=" .. argProductName .. "."
return "RenderRecipe error: no product '" .. argProduct .. "' found."
end
end
-- go through each place and send a simplified (one place) version of tRecipe to the table renderer
wikiTableCaption:wikitext(argProduct .. " recipe.")
for i, place in ipairs(tRecipe.places) do
local tempRecipe = { product=argProductName, pattern=tRecipe.pattern, places={place} }
-- there are multiple places (1-6) to loop through; places have no key, so ipairs works
for _, place in ipairs(tRecipeStack.places) do
-- each call to renderTableRow adds a new row to the passed table
-- make a simple version of tRecipeStack with just the one matching place to the table renderer
wikiTable = RenderRecipe.renderTableRow(tempRecipe, wikiTable)
-- 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
end
-- only building is specified
-- if only the building is specified, then we need to assume multiple products, but each has exactly one place
elseif argBuildingName and argBuildingName ~= "" then
elseif argBuilding and argBuilding ~= "" then
-- gets a table of recipes for different products, each one has exactly one place
-- gets a table of different products' recipes, and make sure there was one
local tableOfRecipes = RecipeData.getBuildingsRecipes(argBuildingName)
local tProducts = RecipeData.getBuildingsRecipes(argBuilding)
if not tableOfRecipes then
if not tProducts then
return "Render_Recipe Error: no recipes found at building=" .. argBuildingName .. "."
return "RenderRecipe error: no building '" .. argBuilding .. "' found."
end
end
local strToReturn = ""
wikiTableCaption:wikitext("Recipes in the " .. argBuilding .. ".")
-- go through each recipe and just send it straight to the table renderer, since the place already matches
for j, recipe in ipairs(tableOfRecipes) do
-- 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
if j > 1 then
strToReturn = strToReturn .. NL -- separate tables with new lines
end
-- each call to renderTableRow adds a new row to the passed table
-- each call to renderTableRow adds a new row to the passed table
wikiTable = RenderRecipe.renderTableRow(recipe, wikiTable)
wikiTable, errorMessage = RenderRecipe.renderTableRow(tRecipe, wikiTable)
end
end
-- something was bad about the arguments; can't print them since they may be nil
-- something was bad about the arguments; can't print them since they may be nil
else
else
return "Render_Recipe Error: invalid template parameters. at least a product or building is required"
-- show the template code itself when we're getting called from the template page; this argument shouldn't ever have this value anywhere else
if argProduct and "on_the_template_page" == argProduct then
return "{{#invoke:RenderRecipe|renderRecipe|product={{{1|}}}|building={{{2|}}}}}"
end
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
end
-- finally, make sure we're returning an actual string, not a Lua table
return mw.allToString(wikiTable)
return mw.allToString(wikiTable)
end
end




 
-------------------------------------------------------------------------------
-- writes the wiki table markup to represent a recipe
-- Writes a new row of HTML tags and content to the provided wiki table.
-- takes a recipe table (pattern and place), and uses only the first place
--
-- 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)
function RenderRecipe.renderTableRow(tRecipe, wikiTable)


if not tRecipe then
-- verify we have valid things to work with
return nil
if not tRecipe and not wikiTable then
return nil, 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)
if product and number then
number = number[1]
else
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
-- of the top-level elements of quantities
local ingredients, quantities = RecipeData.getIngredientsAndQuantities(tRecipe)
if not ingredients or 0 == #ingredients then
return "Render_Recipe Error: did not find ingredients in recipe."
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
-- write the first cell with the building
-- write the first cell with the building
Line 168: Line 210:
:tag('td'):wikitext(RenderRecipe.blTemplate(building) .. BR .. RenderRecipe.starTemplate(stars) .. BR .. speed)
:tag('td'):wikitext(RenderRecipe.blTemplate(building) .. BR .. RenderRecipe.starTemplate(stars) .. BR .. speed)
-- first fill in empty cells like in the recipe book, one for the blank ingredient with a border, then a space for a missing plus sign
-- first fill in any empty cells like in the recipe book, one for each
-- blank ingredient
local blanks = 3 - #ingredients
local blanks = 3 - #ingredients
for h = 1,blanks do
for h = 1,blanks do
wikiTableDataRow:tag('td'):addClass(CSS_CLASS_EMPTY_INGREDIENT)
wikiTableDataRow:tag('td'):addClass(CSS_CLASS_EMPTY_INGREDIENT)
end
end
-- looping over groups of alternative ingredients and quantities simultaneously,
-- simultaneously loop over the groups of alternative ingredients and  
-- so use local group and groupOfQuantities for each i
-- quantities; so use a group of ingredients and a groupOfQuantities each
for i, group in ipairs(ingredients) do
-- the groups have no keys, so ipairs ensures we are iterating over them
-- in a consistent order with the quantities
for _, group in ipairs(ingredients) do
local groupOfQuantities = quantities[i]
local groupOfQuantities = quantities[i]
-- start the table cell that will list the ingredients
local wikiTableDataRowDataCell = wikiTableDataRow:tag('td')
local wikiTableDataRowDataCell = wikiTableDataRow:tag('td')
-- add a class to 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
wikiTableDataRowDataCell:addClass(CSS_CLASS_REQUIRED_INGREDIENT)
wikiTableDataRowDataCell:addClass(CSS_CLASS_REQUIRED_INGREDIENT)
-- we know there's just one item in group, so we don't need to loop
wikiTableDataRowDataCell:wikitext(groupOfQuantities[1] .. " " .. RenderRecipe.rlTemplate(ingredients[1]))
else
else
wikiTableDataRowDataCell:addClass(CSS_CLASS_SWAPPABLE_INGREDIENT)
wikiTableDataRowDataCell:addClass(CSS_CLASS_SWAPPABLE_INGREDIENT)
end
-- for every group, list out the required or alternatives within that group
for j, alt in ipairs(group) do
local quant = groupOfQuantities[j]
-- need to add breaks only between the first and any subsequent options
-- for alignment purposes, create a table within this cell to lay
if j > 1 then
-- out the quantities and ingerdients
wikiTableDataRowDataCell:wikitext(BR)
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(quant).done()
:tag('td'):wikitext(RenderRecipe.rlTemplate(alt)).done()
end
end
wikiTableDataRowDataCell:wikitext(quant .. " " .. RenderRecipe.rlTemplate(alt))
end
end
end
end
      
      
Line 214: Line 267:




-- 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 226: Line 280:
-- 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 242: Line 296:
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 large 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 = "large"
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
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}
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
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}
return thisFrame:expandTemplate{ title= intStars .. TEMPLATE_STAR_SUFFIX}
end
end
Line 289: Line 506:




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

Revision as of 13:48, 12 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 the
-- 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 = {}



--
-- Class Variables
--
local RecipeData = require("Module:RecipeData") -- used to get recipe data

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



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



-- 
-- 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
		-- show the template code itself when we're getting called from the template page; this argument shouldn't ever have this value anywhere else
		if argProduct and "on_the_template_page" == argProduct then
			return "{{#invoke:RenderRecipe|renderRecipe|product={{{1|}}}|building={{{2|}}}}}"
		end
		
		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 _, 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(groupOfQuantities[1] .. " " .. RenderRecipe.rlTemplate(ingredients[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(quant).done()
					:tag('td'):wikitext(RenderRecipe.rlTemplate(alt)).done()
			end
		end
	end
    
	-- close up the table row with the product
	wikiTableDataRow:tag('td'):wikitext(number .. " " .. 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