Module:RecipeView: Difference between revisions

From Against the Storm Official Wiki
m (Fixing a typo in member function name)
(bypassing the error returned by resource link until I refactor this.)
 
(37 intermediate revisions by the same user not shown)
Line 1: Line 1:
---
--- View side of model–view–controller framework for displaying recipes. Owns
--- and handles all choices of display, layout, rendering, and styling.
---
--- Should _only_ be invoked from RecipeController.
---
--- @module RecipeView
--- @module RecipeView
local RecipeView = {}
local RecipeView = {}
StyleUtils = require("Module:StyleUtils")




Line 10: Line 20:
ending = ""
ending = ""


local imgS = mw.getCurrentFrame():expandTemplate{ title = "ImgS" }
--endregion
local imgM = mw.getCurrentFrame():expandTemplate{ title = "ImgM" }
local imgL = mw.getCurrentFrame():expandTemplate{ title = "ImgL" }


local stars = {
["0"] = mw.getCurrentFrame():expandTemplate{ title = "0Star" },
["1"] = mw.getCurrentFrame():expandTemplate{ title = "1Star" },
["2"] = mw.getCurrentFrame():expandTemplate{ title = "2Star" },
["3"] = mw.getCurrentFrame():expandTemplate{ title = "3Star" }
}


--endregion


--region Private constants
local PARAMETER_DISPLAY_OVERRIDE_LIST = "list"


local ATTR_SORT_VALUE = "data-sort-value"


--region Private constants
local ICON_SIZE_LARGE = "large"
local ICON_SIZE_SMALL = "small"
local ICON_NONE = "none"


local INDEX_OPTION_STACK_SIZE = "stackSize"
local INDEX_OPTION_STACK_SIZE = "stackSize"
Line 31: Line 38:
local INDEX_OPTION_GOOD_ICON = "icon"
local INDEX_OPTION_GOOD_ICON = "icon"


local PARAMETER_DISPLAY_OVERRIDE_LIST = "list"
local MAX_INGREDIENTS = 3


local MAX_INGREDIENTS = 3
local DEFAULT_RESULT_HEADER = "Result"
local DEFAULT_RESULT_HEADER = "Result"


-- wikitable classes for tables
-- Page anchors for links
local CLASS_WIKITABLE_SORTABLE_COLLAPSIBLE = "wikitable sortable mw-collapsible"
local LINK_ANCHOR_PRODUCT = "#Product"
 
local LINK_ANCHOR_INGREDIENT = "#Ingredient"
-- Shortcut markup that's easier for Lua string concatenations
local BR = "<br />"
local NBSP = "&nbsp;"
local BOLD = "'''"


--endregion
--endregion
Line 51: Line 53:


---
---
--- Formats a standard link to a building. Does not invoke the template, but it
--- Formats seconds as mm:ss format, with leading zeros where necessary.
--- could be modified to do so in the future.
---
---
---@param buildingName string the display name of the building
---@param seconds number of seconds to reformat (if any, can be nil)
---@param buildingIcon string the filename for the icon
---@return string representing mm:ss time format, or empty string if not needed
---@param iconSize string representing the desired size of the icon
local function formatTime(seconds)
---@return string an assembled link, with icon (if large enough) and text link to page
local function buildingLink(buildingName, buildingIcon, iconSize)
 
if not buildingIcon or not iconSize or not buildingName then
error("Building parameters cannot be nil.")
end


-- Skip the smallest icons for the building links.
if not seconds or seconds == 0 then
local iconPart = ""
return ""
if iconSize > imgS then
iconPart = "[[File:" .. buildingIcon .. "|" .. iconSize ..
"|alt=" .. buildingName .."|link=" .. buildingName .. "]]" ..
NBSP
end
end


local linkPart = "[[" .. buildingName .. "]]"
local minutes = math.floor(seconds/60)
 
local remainingSeconds = seconds % 60
return iconPart .. linkPart
return string.format("%02d:%02d", minutes, remainingSeconds)
end
end


Line 86: Line 77:
---@param resourceIcon string the filename for the icon
---@param resourceIcon string the filename for the icon
---@param iconSize string representing the desired size of the icon
---@param iconSize string representing the desired size of the icon
---@param linkAnchor string an anchor on the page, if needed
---@return string an assembled link, with icon and text link to page
---@return string an assembled link, with icon and text link to page
local function resourceLink(resourceName, resourceIcon, iconSize)
local function resourceLinkWithAnchor(resourceName, resourceIcon, iconSize, linkAnchor)


if not resourceName or not resourceIcon or not iconSize then
if not resourceName or not resourceIcon or not iconSize then
Line 93: Line 85:
end
end


local iconPart = "[[File:" .. resourceIcon .. "|" .. iconSize .. "|alt=" .. resourceName .."|link=" .. resourceName .. "]]"
if not linkAnchor then
local linkPart = "[[" .. resourceName .. "]]"
linkAnchor = ""
end
 
local iconPart = "[[File:" .. resourceIcon .. "|" .. iconSize ..
"|alt=" .. resourceName .."|link=" .. resourceName .. linkAnchor .. "]]"
local linkPart = "[[" .. resourceName .. linkAnchor .. "|" .. resourceName .. "]]"


return iconPart .. NBSP .. linkPart
return iconPart .. StyleUtils.NBSP .. linkPart
end
end


Line 123: Line 120:


for i = 1, math.min(numIngredients, MAX_INGREDIENTS) do
for i = 1, math.min(numIngredients, MAX_INGREDIENTS) do
row:tag("th"):wikitext("Ingredient #" .. i .. "options"):done()
local numbering = ""
if numIngredients > 1 then
numbering = StyleUtils.NBSP .. "#" .. i
end
row:tag("th"):wikitext("Ingredient" .. numbering):done()
end
end


Line 134: Line 135:


---
---
--- Returns a standard MediaWiki HTML table entity created with standard
--- Returns a new recipe HTML table entity created with standard
--- display and interactivity classes.
--- display and interactivity classes.
---
---
---@return table standard MediaWiki HTML table entity
---@return table standard MediaWiki HTML table entity
local function startNewWikiTable()
local function startNewRecipeTable()


return mw.html.create("table"):addClass(CLASS_WIKITABLE_SORTABLE_COLLAPSIBLE):newline()
local recipeTable = mw.html.create("table")
StyleUtils.styleRecipeTable(recipeTable)
recipeTable:newline()
 
return recipeTable
end
end


Line 150: Line 155:
---
---
---@return table invisible MediaWiki HTML table entity
---@return table invisible MediaWiki HTML table entity
function RecipeView.startNewInvisibleTable()
local function startNewIngredientsTable()


return mw.html.create("table"):newline()
local ingTable = mw.html.create("table")
StyleUtils.styleIngredientsSubtable(ingTable)
ingTable:newline()
 
return ingTable
end
end


Line 163: Line 172:
---@param htmlTableRow table MediaWiki HTML table row entity
---@param htmlTableRow table MediaWiki HTML table row entity
---@param buildingName string the name of the building
---@param buildingName string the name of the building
---@param buildingIcon string the filename for the icon
local function makeBuildingColumn(htmlTableRow, buildingName)
function RecipeView.makeBuildingColumn(htmlTableRow, buildingName, buildingIcon)


htmlTableRow:tag("td"):wikitext(buildingLink(buildingName, buildingIcon, imgL)):done():newline()
htmlTableRow:tag("td")
:attr(ATTR_SORT_VALUE, buildingName)
:tag("div"):addClass(StyleUtils.CLASS_BUILDING_ICON)
:wikitext(StyleUtils.BUILDING_LINK(buildingName, ICON_SIZE_LARGE))
:done():newline()
:done():newline()
end
end


Line 178: Line 191:
---@param gradeStars number the stars of efficiency
---@param gradeStars number the stars of efficiency
---@param productionTime string the formatted for display time
---@param productionTime string the formatted for display time
function RecipeView.makeEfficiencyColumn(htmlTableRow, gradeStars, productionTime)
local function makeEfficiencyColumn(htmlTableRow, gradeStars, productionTime)
 
htmlTableRow:tag("td"):attr(ATTR_SORT_VALUE, gradeStars)
:wikitext(StyleUtils.STARS[gradeStars]() .. StyleUtils.BR .. productionTime)
:done():newline()
end
 
 
 
---
--- Pads the table row with empty cells depending on how many ingredient groups
--- are in the recipe from which this is called vs. the number of ingredient
--- columns in the table overall.
---
---@param htmlTableRow table MediaWiki HTML table row entity
---@param numIngredientInRecipe number of ingredient groups in the recipe
---@param numIngredientsTotal number of ingredient groups in the table overall
local function makeFillerBlankCells(htmlTableRow, numIngredientInRecipe, numIngredientsTotal)


htmlTableRow:tag("td"):wikitext(stars[gradeStars] .. BR .. productionTime):done():newline()
if numIngredientInRecipe < numIngredientsTotal then
htmlTableRow:tag("td"):wikitext("—"):done()
makeFillerBlankCells(htmlTableRow, numIngredientInRecipe + 1, numIngredientsTotal)
end
end
end


Line 198: Line 231:
---@param htmlTable table MediaWiki HTML table entity
---@param htmlTable table MediaWiki HTML table entity
---@param optionTable table of one good's option data: stack size, name, icon
---@param optionTable table of one good's option data: stack size, name, icon
function RecipeView.makeOptionRow(htmlTable, optionTable)
---@param numOptions number how many options total, for labeling the data
local function makeOptionRow(htmlTable, optionTable, numOptions)


local row = htmlTable:tag("tr")
local row = htmlTable:tag("tr")
row:tag("td"):wikitext(BOLD .. optionTable[INDEX_OPTION_STACK_SIZE] .. BOLD):done()
row:tag("td")
row:tag("td"):wikitext(resourceLink(optionTable[INDEX_OPTION_GOOD_NAME], optionTable[INDEX_OPTION_GOOD_ICON], imgM)):done()
:wikitext(optionTable[INDEX_OPTION_STACK_SIZE])
:done()
 
local optionDiv = row:tag("td"):tag("div")
if numOptions > 1 then
optionDiv:addClass(StyleUtils.CLASS_OPTIONAL_INGREDIENT_ICON)
else
optionDiv:addClass(StyleUtils.CLASS_SINGLE_INGREDIENT_ICON)
end
 
optionDiv:wikitext(resourceLinkWithAnchor(optionTable[INDEX_OPTION_GOOD_NAME],
optionTable[INDEX_OPTION_GOOD_ICON], StyleUtils.IMG_M(),
LINK_ANCHOR_PRODUCT))
:done()
 
row:done():newline()
row:done():newline()
end
end
Line 214: Line 262:
---@param htmlTableRow table MediaWiki HTML table row entity
---@param htmlTableRow table MediaWiki HTML table row entity
---@param resultStackSize number of results provided by the recipe
---@param resultStackSize number of results provided by the recipe
---@param resultName string the display name of the result
---@param resultIcon string the filename of the result's icon
---@param resultIcon string the filename of the result's icon
---@param resultName string the display name of the result
local function makeResultColumn(htmlTableRow, resultStackSize, resultName, resultIcon)
function RecipeView.makeResultColumn(htmlTableRow, resultStackSize, resultIcon, resultName)
 
if not resultStackSize or resultStackSize == 0 then
resultStackSize = 1
end


local stackSizePart = BOLD .. resultStackSize .. BOLD
local iconAndLinkPart = resourceLinkWithAnchor(resultName, resultIcon,
local iconAndLinkPart = resourceLink(resultName, resultIcon, imgL)
StyleUtils.IMG_L(), LINK_ANCHOR_INGREDIENT )


htmlTableRow:tag("td"):wikitext(stackSizePart .. NBSP .. iconAndLinkPart):done():newline()
htmlTableRow:tag("td"):attr(ATTR_SORT_VALUE, resultName)
:wikitext(resultStackSize)
:tag("div"):addClass(StyleUtils.CLASS_PRODUCT_ICON):newline()
:wikitext(iconAndLinkPart)
:done():newline()
end
end


Line 229: Line 285:


--region Public methods
--region Public methods
---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on ingredients at one building.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param ingredientName string the name of the ingredient this table is based on
---@param buildingName string the name of the building
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForIngredientAndBuilding(ingredientName, buildingName, displayOverride, numRecipes, numIngredients, finalColumnName)
-- These should never be nil at runtime.
if not ingredientName then
error("Parameter is invalid for ingredient name.")
end
if not buildingName then
error("Parameter is invalid for building name.")
end
-- Use generic defaults if they are not provided.
if not numRecipes then
numRecipes = "All"
end
local newNode
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
newNode = mw.html.create("ul")
else
newNode = startNewRecipeTable()
newNode:tag("caption"):wikitext(numRecipes .. " recipes using " .. ingredientName .. " in the " .. buildingName .. "."):done():newline()
makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
end
beginning = newNode
-- Return value is not used by the Controller, but for debugging.
return newNode
end


---
---
Line 257: Line 357:
newNode = mw.html.create("ul")
newNode = mw.html.create("ul")
else
else
newNode = startNewWikiTable()
newNode = startNewRecipeTable()
newNode:tag("caption"):wikitext(numRecipes .. " recipes that require " .. ingredientName):done():newline()
newNode:tag("caption"):wikitext(numRecipes .. " recipes using " .. ingredientName .. "."):done():newline()
 
makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
end
 
beginning = newNode
-- Return value is not used by the Controller, but for debugging.
return newNode
end
 
 
 
---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on a product.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param productName string the name of the product this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForProduct(productName, displayOverride, numRecipes, numIngredients, finalColumnName)
 
-- These should never be nil at runtime.
if not productName then
error("Parameter is invalid for product name.")
end
-- Use generic defaults if they are not provided.
if not numRecipes then
numRecipes = "All"
end
 
local newNode
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
newNode = mw.html.create("ul")
else
newNode = startNewRecipeTable()
newNode:tag("caption"):wikitext(numRecipes .. " recipes for " .. productName .. "."):done():newline()
 
makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
end
 
beginning = newNode
-- Return value is not used by the Controller, but for debugging.
return newNode
end
 
 
 
---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on a product at one building.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param productName string the name of the product this table is based on
---@param buildingName string the name of the building this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForProductAndBuilding(productName, buildingName, displayOverride, numRecipes, numIngredients, finalColumnName)
 
-- These should never be nil at runtime.
if not productName then
error("Parameter is invalid for product name.")
end
if not buildingName then
error("Parameter is invalid for building name.")
end
-- Use generic defaults if they are not provided.
if not numRecipes then
numRecipes = "All"
end
 
local newNode
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
newNode = mw.html.create("ul")
else
newNode = startNewRecipeTable()
newNode:tag("caption"):wikitext(numRecipes .. " recipe in the " .. buildingName .. " for " .. productName .. "."):done():newline()
 
makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
end
 
beginning = newNode
-- Return value is not used by the Controller, but for debugging.
return newNode
end
 
 
 
---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on a building.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param buildingName string the name of the building this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForBuilding(buildingName, displayOverride, numRecipes, numIngredients, finalColumnName)
 
-- These should never be nil at runtime.
if not buildingName then
error("Parameter is invalid for building name.")
end
-- Use generic defaults if they are not provided.
if not numRecipes then
numRecipes = "All"
end
 
local newNode
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
newNode = mw.html.create("ul")
else
newNode = startNewRecipeTable()
newNode:tag("caption"):wikitext(numRecipes .. " recipes in the " .. buildingName .. "."):done():newline()


makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
Line 300: Line 524:
---@param buildingIcon string the icon of the building
---@param buildingIcon string the icon of the building
---@param gradeStars number of stars of efficiency of this recipe
---@param gradeStars number of stars of efficiency of this recipe
---@param productionTime string formatted time to produce
---@param productionTime number of seconds to produce (if any--nil okay for none)
---@param ingredientsList table of ingredient subtables of options
---@param ingredientsList table of ingredient subtables of options
---@param productStackSize number how many of the products are produced by this recipe
---@param productStackSize number how many of the products are produced by this recipe (if any--nil okay for none)
---@param productName string the display name of the product
---@param productName string the display name of the product
---@param productIcon string the icon of the product
---@param productIcon string the icon of the product
function RecipeView.addRowForRecipe(displayOverride, buildingName, buildingIcon, gradeStars, productionTime, ingredientsList, productStackSize, productIcon, productName)
function RecipeView.addRowForRecipe(displayOverride, numIngredients,
buildingName, buildingIcon,
gradeStars, productionTime, ingredientsList,
productStackSize, productName, productIcon,
requiredBuilding)


local newNode
local newNode
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
--List version.
-- List version. If the building is required, then show the product
newNode = mw.html.create("li"):wikitext(buildingLink(buildingName, buildingIcon, imgS))
-- name with stars instead of showing the building name N times.
if not requiredBuilding then
newNode = mw.html.create("li")
:wikitext(StyleUtils.BUILDING_LINK(buildingName, ICON_NONE)
.. StyleUtils.NBSP .. StyleUtils.PAREN_STARS[gradeStars]())
else
 
local _, linkText = pcall(StyleUtils.RESOURCE_LINK, productName, ICON_SIZE_SMALL)
-- If no good found, it might be a service instead.
if string.find(linkText, "Lua error") then
linkText = StyleUtils.SERVICE_LINK(productName, ICON_SIZE_SMALL)
end
-- If no service found, then return something more graceful
if string.find(linkText, "^No service found") then
linkText = "N/A"
end
 
newNode = mw.html.create("li")
:wikitext(linkText .. StyleUtils.NBSP
.. StyleUtils.PAREN_STARS[gradeStars]())
end
else
else
-- Table version.
-- Table version.
newNode = mw.html.create("tr"):newline()
newNode = mw.html.create("tr"):newline()
RecipeView.makeBuildingColumn(newNode, buildingName, buildingIcon)
makeBuildingColumn(newNode, buildingName, buildingIcon)
RecipeView.makeEfficiencyColumn(newNode, gradeStars, productionTime)
makeEfficiencyColumn(newNode, gradeStars, formatTime(productionTime))
 
-- Fill in with any needed blank cells.
makeFillerBlankCells(newNode, #ingredientsList, numIngredients)


for _, iList in ipairs(ingredientsList) do
for _, iList in ipairs(ingredientsList) do


local optionsGrid = RecipeView.startNewInvisibleTable()
local numForSort = 0
local optionsGrid = startNewIngredientsTable()
for _, option in ipairs(iList) do
for _, option in ipairs(iList) do
RecipeView.makeOptionRow(optionsGrid, option)
makeOptionRow(optionsGrid, option, #iList)
numForSort = math.max(numForSort, option[INDEX_OPTION_STACK_SIZE])
end
end


optionsGrid:done()
optionsGrid:done()
newNode:tag("td"):node(optionsGrid):done():newline()
newNode:tag("td"):attr(ATTR_SORT_VALUE, numForSort)
:node(optionsGrid):done():newline()
end
end


RecipeView.makeResultColumn(newNode, productStackSize, productIcon, productName)
makeResultColumn(newNode, productStackSize, productName, productIcon)
end
end


Line 340: Line 594:


---
---
--- Wraps up anything outstanding for the view based on ingredients, then
--- Wraps up anything outstanding for the view, then returns the entire
--- returns the entire MediaWiki entity originally started in the method
--- MediaWiki entity originally started in the respective method.
--- startViewForIngredient.
---
---
---@return string the HTML markup of the completed view
---@return string the HTML markup of the completed view
function RecipeView.endViewForIngredient()
function RecipeView.endView(displayOverride)


beginning:node(middle)
beginning:node(middle)
beginning:done()
beginning:done()


return beginning
middle = ""
 
if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
 
return beginning
else
 
local divContainer = StyleUtils.styleRecipeContainer(mw.html.create("div"))
divContainer:newline()
divContainer:node(beginning)
 
return divContainer
end
end
end



Latest revision as of 18:29, 19 October 2024

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

---
--- View side of model–view–controller framework for displaying recipes. Owns
--- and handles all choices of display, layout, rendering, and styling.
---
--- Should _only_ be invoked from RecipeController.
---
--- @module RecipeView
local RecipeView = {}



StyleUtils = require("Module:StyleUtils")



--region Private member variables

beginning = ""
middle = ""
ending = ""

--endregion



--region Private constants

local PARAMETER_DISPLAY_OVERRIDE_LIST = "list"

local ATTR_SORT_VALUE = "data-sort-value"

local ICON_SIZE_LARGE = "large"
local ICON_SIZE_SMALL = "small"
local ICON_NONE = "none"

local INDEX_OPTION_STACK_SIZE = "stackSize"
local INDEX_OPTION_GOOD_NAME = "name"
local INDEX_OPTION_GOOD_ICON = "icon"

local MAX_INGREDIENTS = 3

local DEFAULT_RESULT_HEADER = "Result"

-- Page anchors for links
local LINK_ANCHOR_PRODUCT = "#Product"
local LINK_ANCHOR_INGREDIENT = "#Ingredient"

--endregion



--region Private methods

---
--- Formats seconds as mm:ss format, with leading zeros where necessary.
---
---@param seconds number of seconds to reformat (if any, can be nil)
---@return string representing mm:ss time format, or empty string if not needed
local function formatTime(seconds)

	if not seconds or seconds == 0 then
		return ""
	end

	local minutes = math.floor(seconds/60)
	local remainingSeconds = seconds % 60
	return string.format("%02d:%02d", minutes, remainingSeconds)
end



---
--- Formats a standard link to a resource. Does not invoke the template, but it
--- could be modified to do so in the future.
---
---@param resourceName string the display name of the good
---@param resourceIcon string the filename for the icon
---@param iconSize string representing the desired size of the icon
---@param linkAnchor string an anchor on the page, if needed
---@return string an assembled link, with icon and text link to page
local function resourceLinkWithAnchor(resourceName, resourceIcon, iconSize, linkAnchor)

	if not resourceName or not resourceIcon or not iconSize then
		error("Resource parameters cannot be nil.")
	end

	if not linkAnchor then
		linkAnchor = ""
	end

	local iconPart = "[[File:" .. resourceIcon .. "|" .. iconSize ..
			"|alt=" .. resourceName .."|link=" .. resourceName .. linkAnchor .. "]]"
	local linkPart = "[[" .. resourceName .. linkAnchor .. "|" .. resourceName .. "]]"

	return iconPart .. StyleUtils.NBSP .. linkPart
end



---
--- Creates a header row under the provided MediaWiki HTML table entity.
---
---@param tableNode table MediaWiki HTML table entity
---@param numIngredients number of ingredient columns
---@param finalColumnName string the title for the final/result column.
local function makeStandardRecipeTableHeaderRow(tableNode, numIngredients, finalColumnName)

	-- Use generic defaults if they are not provided.
	if not numIngredients then
		numIngredients = MAX_INGREDIENTS
	end
	if not finalColumnName then
		finalColumnName = DEFAULT_RESULT_HEADER
	end

	local row = tableNode:tag("tr")

	row:tag("th"):wikitext("Building"):done()
	row:tag("th"):wikitext("Efficiency"):done()

	for i = 1, math.min(numIngredients, MAX_INGREDIENTS) do
		local numbering = ""
		if numIngredients > 1 then
			numbering = StyleUtils.NBSP .. "#" .. i
		end
		row:tag("th"):wikitext("Ingredient" .. numbering):done()
	end

	row:tag("th"):wikitext(finalColumnName):done()

	row:done():newline()
end



---
--- Returns a new recipe HTML table entity created with standard
--- display and interactivity classes.
---
---@return table standard MediaWiki HTML table entity
local function startNewRecipeTable()

	local recipeTable = mw.html.create("table")
	StyleUtils.styleRecipeTable(recipeTable)
	recipeTable:newline()

	return recipeTable
end



---
--- Returns a standard MediaWiki HTML table entity created with no classes or
--- interactivity, making it invisible, for layout purposes only.
---
---@return table invisible MediaWiki HTML table entity
local function startNewIngredientsTable()

	local ingTable = mw.html.create("table")
	StyleUtils.styleIngredientsSubtable(ingTable)
	ingTable:newline()

	return ingTable
end



---
--- Makes the content of the building column under the provided HTML table row
--- entity.
---
---@param htmlTableRow table MediaWiki HTML table row entity
---@param buildingName string the name of the building
local function makeBuildingColumn(htmlTableRow, buildingName)

	htmlTableRow:tag("td")
			:attr(ATTR_SORT_VALUE, buildingName)
			:tag("div"):addClass(StyleUtils.CLASS_BUILDING_ICON)
				:wikitext(StyleUtils.BUILDING_LINK(buildingName, ICON_SIZE_LARGE))
			:done():newline()
		:done():newline()
end



---
--- Makes the content of the recipe efficiency column under the provided HTML
--- table row entity.
---
---@param htmlTableRow table MediaWiki HTML table row entity
---@param gradeStars number the stars of efficiency
---@param productionTime string the formatted for display time
local function makeEfficiencyColumn(htmlTableRow, gradeStars, productionTime)

	htmlTableRow:tag("td"):attr(ATTR_SORT_VALUE, gradeStars)
				:wikitext(StyleUtils.STARS[gradeStars]() .. StyleUtils.BR .. productionTime)
			:done():newline()
end



---
--- Pads the table row with empty cells depending on how many ingredient groups
--- are in the recipe from which this is called vs. the number of ingredient
--- columns in the table overall.
---
---@param htmlTableRow table MediaWiki HTML table row entity
---@param numIngredientInRecipe number of ingredient groups in the recipe
---@param numIngredientsTotal number of ingredient groups in the table overall
local function makeFillerBlankCells(htmlTableRow, numIngredientInRecipe, numIngredientsTotal)

	if numIngredientInRecipe < numIngredientsTotal then
		htmlTableRow:tag("td"):wikitext("—"):done()
		makeFillerBlankCells(htmlTableRow, numIngredientInRecipe + 1, numIngredientsTotal)
	end
end



---
--- Makes the row and fills it with content. Puts the row into the provided
--- HTML table entity.
---
--- Needs option information structured in a table, like this:
--- optionTable = {
--- 	[INDEX_OPTION_STACK_SIZE] = 9,
--- 	[INDEX_OPTION_GOOD_NAME] = "Display Name",
--- 	[INDEX_OPTION_GOOD_ICON] = "Icon_filename.png"
--- }
---
---@param htmlTable table MediaWiki HTML table entity
---@param optionTable table of one good's option data: stack size, name, icon
---@param numOptions number how many options total, for labeling the data
local function makeOptionRow(htmlTable, optionTable, numOptions)

	local row = htmlTable:tag("tr")
	row:tag("td")
			:wikitext(optionTable[INDEX_OPTION_STACK_SIZE])
			:done()

	local optionDiv = row:tag("td"):tag("div")
	if numOptions > 1 then
		optionDiv:addClass(StyleUtils.CLASS_OPTIONAL_INGREDIENT_ICON)
	else
		optionDiv:addClass(StyleUtils.CLASS_SINGLE_INGREDIENT_ICON)
	end

	optionDiv:wikitext(resourceLinkWithAnchor(optionTable[INDEX_OPTION_GOOD_NAME],
				optionTable[INDEX_OPTION_GOOD_ICON], StyleUtils.IMG_M(),
				LINK_ANCHOR_PRODUCT))
	:done()

	row:done():newline()
end



---
--- Makes the content of the result column under the provided HTML table row
--- entity. Agnostic of whether the result is a product or a service.
---
---@param htmlTableRow table MediaWiki HTML table row entity
---@param resultStackSize number of results provided by the recipe
---@param resultName string the display name of the result
---@param resultIcon string the filename of the result's icon
local function makeResultColumn(htmlTableRow, resultStackSize, resultName, resultIcon)

	if not resultStackSize or resultStackSize == 0 then
		resultStackSize = 1
	end

	local iconAndLinkPart = resourceLinkWithAnchor(resultName, resultIcon,
			StyleUtils.IMG_L(), LINK_ANCHOR_INGREDIENT )

	htmlTableRow:tag("td"):attr(ATTR_SORT_VALUE, resultName)
			:wikitext(resultStackSize)
			:tag("div"):addClass(StyleUtils.CLASS_PRODUCT_ICON):newline()
				:wikitext(iconAndLinkPart)
			:done():newline()
end

--endregion



--region Public methods

---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on ingredients at one building.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param ingredientName string the name of the ingredient this table is based on
---@param buildingName string the name of the building
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForIngredientAndBuilding(ingredientName, buildingName, displayOverride, numRecipes, numIngredients, finalColumnName)

	-- These should never be nil at runtime.
	if not ingredientName then
		error("Parameter is invalid for ingredient name.")
	end
	if not buildingName then
		error("Parameter is invalid for building name.")
	end
	-- Use generic defaults if they are not provided.
	if not numRecipes then
		numRecipes = "All"
	end

	local newNode
	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
		newNode = mw.html.create("ul")
	else
		newNode = startNewRecipeTable()
		newNode:tag("caption"):wikitext(numRecipes .. " recipes using " .. ingredientName .. " in the " .. buildingName .. "."):done():newline()

		makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
	end

	beginning = newNode
	-- Return value is not used by the Controller, but for debugging.
	return newNode
end



---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on ingredients.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param ingredientName string the name of the ingredient this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForIngredient(ingredientName, displayOverride, numRecipes, numIngredients, finalColumnName)

	-- These should never be nil at runtime.
	if not ingredientName then
		error("Parameter is invalid for ingredient name.")
	end
	-- Use generic defaults if they are not provided.
	if not numRecipes then
		numRecipes = "All"
	end

	local newNode
	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
		newNode = mw.html.create("ul")
	else
		newNode = startNewRecipeTable()
		newNode:tag("caption"):wikitext(numRecipes .. " recipes using " .. ingredientName .. "."):done():newline()

		makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
	end

	beginning = newNode
	-- Return value is not used by the Controller, but for debugging.
	return newNode
end



---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on a product.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param productName string the name of the product this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForProduct(productName, displayOverride, numRecipes, numIngredients, finalColumnName)

	-- These should never be nil at runtime.
	if not productName then
		error("Parameter is invalid for product name.")
	end
	-- Use generic defaults if they are not provided.
	if not numRecipes then
		numRecipes = "All"
	end

	local newNode
	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
		newNode = mw.html.create("ul")
	else
		newNode = startNewRecipeTable()
		newNode:tag("caption"):wikitext(numRecipes .. " recipes for " .. productName .. "."):done():newline()

		makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
	end

	beginning = newNode
	-- Return value is not used by the Controller, but for debugging.
	return newNode
end



---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on a product at one building.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param productName string the name of the product this table is based on
---@param buildingName string the name of the building this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForProductAndBuilding(productName, buildingName, displayOverride, numRecipes, numIngredients, finalColumnName)

	-- These should never be nil at runtime.
	if not productName then
		error("Parameter is invalid for product name.")
	end
	if not buildingName then
		error("Parameter is invalid for building name.")
	end
	-- Use generic defaults if they are not provided.
	if not numRecipes then
		numRecipes = "All"
	end

	local newNode
	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
		newNode = mw.html.create("ul")
	else
		newNode = startNewRecipeTable()
		newNode:tag("caption"):wikitext(numRecipes .. " recipe in the " .. buildingName .. " for " .. productName .. "."):done():newline()

		makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
	end

	beginning = newNode
	-- Return value is not used by the Controller, but for debugging.
	return newNode
end



---
--- Initializes the table, populates the caption, and writes and header row,
--- all appropriate for a table focusing on a building.
---
--- The default is a wikitable output. With the displayOverride flag, this
--- method will instead make a list.
---
---@param buildingName string the name of the building this table is based on
---@param displayOverride string a flag for the output type if not the default
---@param numRecipes number of recipes that will be in this table, if it's known
---@param numIngredients number of columns that will be written to this table, if known
---@param finalColumnName string the header label of the last column
function RecipeView.startViewForBuilding(buildingName, displayOverride, numRecipes, numIngredients, finalColumnName)

	-- These should never be nil at runtime.
	if not buildingName then
		error("Parameter is invalid for building name.")
	end
	-- Use generic defaults if they are not provided.
	if not numRecipes then
		numRecipes = "All"
	end

	local newNode
	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
		newNode = mw.html.create("ul")
	else
		newNode = startNewRecipeTable()
		newNode:tag("caption"):wikitext(numRecipes .. " recipes in the " .. buildingName .. "."):done():newline()

		makeStandardRecipeTableHeaderRow(newNode, numIngredients, finalColumnName)
	end

	beginning = newNode
	-- Return value is not used by the Controller, but for debugging.
	return newNode
end



---
--- Adds a new row to the content being built. This is typically called from a
--- Controller that knows how many rows to add. RecipeView maintains the output
--- in-progress while rows are added, so that layout and formatting can be
--- owned by this module instead of the Controller.
---
--- Expects a table-of-tables for ingredients information, like this:
--- ingredientsList = {
--- 	[1] = {
--- 		[1] = {
--- 			[INDEX_OPTION_STACK_SIZE] = 9,
--- 			[INDEX_OPTION_GOOD_NAME] = "Display Name",
--- 			[INDEX_OPTION_GOOD_ICON] = "Icon_filename.png"
--- 		},
--- 		[2] = { ... },
--- 		[3] = { ... },
--- 		... -- or fewer or up to six options
--- 	},
--- 	[2] = {
--- 		[1] = { ... },
--- 		[2] = { ... },
--- 		... -- or fewer or up to six options
--- 	},
--- 	[3] = { ... } -- or fewer ingredient option groups
--- }
---
---@param displayOverride string a flag for the output type if not the default
---@param buildingName string the name of the building of this recipe
---@param buildingIcon string the icon of the building
---@param gradeStars number of stars of efficiency of this recipe
---@param productionTime number of seconds to produce (if any--nil okay for none)
---@param ingredientsList table of ingredient subtables of options
---@param productStackSize number how many of the products are produced by this recipe (if any--nil okay for none)
---@param productName string the display name of the product
---@param productIcon string the icon of the product
function RecipeView.addRowForRecipe(displayOverride, numIngredients,
									buildingName, buildingIcon,
									gradeStars, productionTime, ingredientsList,
									productStackSize, productName, productIcon,
									requiredBuilding)

	local newNode
	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then
		-- List version. If the building is required, then show the product
		-- name with stars instead of showing the building name N times.
		if not requiredBuilding then
			newNode = mw.html.create("li")
					:wikitext(StyleUtils.BUILDING_LINK(buildingName, ICON_NONE)
					.. StyleUtils.NBSP .. StyleUtils.PAREN_STARS[gradeStars]())
		else

			local _, linkText = pcall(StyleUtils.RESOURCE_LINK, productName, ICON_SIZE_SMALL)
			-- If no good found, it might be a service instead.
			if string.find(linkText, "Lua error") then
				linkText = StyleUtils.SERVICE_LINK(productName, ICON_SIZE_SMALL)
			end
			-- If no service found, then return something more graceful
			if string.find(linkText, "^No service found") then
				linkText = "N/A"
			end

			newNode = mw.html.create("li")
					:wikitext(linkText .. StyleUtils.NBSP
							.. StyleUtils.PAREN_STARS[gradeStars]())
		end
	else
		-- Table version.
		newNode = mw.html.create("tr"):newline()
		makeBuildingColumn(newNode, buildingName, buildingIcon)
		makeEfficiencyColumn(newNode, gradeStars, formatTime(productionTime))

		-- Fill in with any needed blank cells.
		makeFillerBlankCells(newNode, #ingredientsList, numIngredients)

		for _, iList in ipairs(ingredientsList) do

			local numForSort = 0
			local optionsGrid = startNewIngredientsTable()
			for _, option in ipairs(iList) do
				makeOptionRow(optionsGrid, option, #iList)
				numForSort = math.max(numForSort, option[INDEX_OPTION_STACK_SIZE])
			end

			optionsGrid:done()
			newNode:tag("td"):attr(ATTR_SORT_VALUE, numForSort)
					:node(optionsGrid):done():newline()
		end

		makeResultColumn(newNode, productStackSize, productName, productIcon)
	end

	newNode:done()
	middle = middle .. tostring(newNode)

	return newNode
end



---
--- Wraps up anything outstanding for the view, then returns the entire
--- MediaWiki entity originally started in the respective method.
---
---@return string the HTML markup of the completed view
function RecipeView.endView(displayOverride)

	beginning:node(middle)
	beginning:done()

	middle = ""

	if displayOverride == PARAMETER_DISPLAY_OVERRIDE_LIST then

		return beginning
	else

		local divContainer = StyleUtils.styleRecipeContainer(mw.html.create("div"))
		divContainer:newline()
		divContainer:node(beginning)

		return divContainer
	end
end

--endregion

return RecipeView