Module:RecipeView

--- --- 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 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 = "" local linkPart = "" .. 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 stackSizePart = resultStackSize .. StyleUtils.NBSP local iconAndLinkPart = resourceLinkWithAnchor(resultName, resultIcon,			StyleUtils.IMG_L, LINK_ANCHOR_INGREDIENT )

htmlTableRow:tag("td"):attr(ATTR_SORT_VALUE, resultName) :wikitext(stackSizePart):newline :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 that require " .. 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 that require " .. 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 that produce " .. 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 .. " that produces " .. 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_SIZE_SMALL)					.. StyleUtils.NBSP .. StyleUtils.PAREN_STARS[gradeStars]) else newNode = mw.html.create("li") :wikitext(StyleUtils.RESOURCE_LINK(productName, ICON_SIZE_SMALL)					.. 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