Jump to content

Module:Recipe: Difference between revisions

From Apogea Wiki
Jayarrowz (talk | contribs)
Created page with "-- Module:Recipe -- Displays crafting recipes showing items combining into results -- Usage: {{#invoke:Recipe|display|input1=Item1|input2=Item2|output=Result}} local p = {} -- Format a single item slot local function formatSlot(itemName, quantity, spriteSize) if not itemName or itemName == "" then return '<div class="recipe-slot recipe-slot-empty"></div>' end local qty = tonumber(quantity) or 1 local qtyDisplay = "" if qty > 1 then..."
 
Jayarrowz (talk | contribs)
No edit summary
Line 5: Line 5:
local p = {}
local p = {}


-- Format a single item slot
-- CSS styles embedded in module
local function formatSlot(itemName, quantity, spriteSize)
local css = [[
     if not itemName or itemName == "" then
<style>
        return '<div class="recipe-slot recipe-slot-empty"></div>'
.recipe-list {
    background-color: #1a1a2e;
    border: 2px solid #4a4a6a;
    border-radius: 5px;
    padding: 10px;
    margin: 10px 0;
    overflow-x: auto;
}
 
.recipe-list-title {
    font-size: 18px;
    font-weight: bold;
    color: #ffd700;
    margin-bottom: 10px;
    padding-bottom: 5px;
    border-bottom: 1px solid #4a4a6a;
}
 
.recipe-table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}
 
.recipe-table-header th {
    text-align: left;
    padding: 8px;
    color: #aaa;
    border-bottom: 1px solid #4a4a6a;
}
 
.recipe-table-header th:nth-child(1) { width: 45%; }
.recipe-table-header th:nth-child(2) { width: 5%; }
.recipe-table-header th:nth-child(3) { width: 25%; }
.recipe-table-header th:nth-child(4) { width: 25%; }
 
.recipe-row td {
    padding: 8px;
    border-bottom: 1px solid #2a2a4e;
    vertical-align: middle;
}
 
.recipe-cell-inputs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
}
 
.recipe-cell-arrow {
    color: #9edc60;
    font-size: 20px;
    text-align: center;
}
 
.recipe-cell-output {
    color: #fff;
}
 
.recipe-cell-station {
    color: #9bddff;
}
 
.recipe-inline-item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
 
.recipe-plus-small {
    color: #888;
}
 
.recipe-container {
     display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 10px;
    background-color: #1a1a2e;
    border: 2px solid #4a4a6a;
    border-radius: 5px;
    margin: 10px 0;
}
 
.recipe-inputs {
    display: flex;
    align-items: center;
    gap: 5px;
}
 
.recipe-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 5px;
    background-color: #2a2a4e;
    border: 1px solid #3a3a5a;
    border-radius: 3px;
    position: relative;
}
 
.recipe-quantity {
    position: absolute;
    bottom: 2px;
    right: 2px;
    background-color: #000;
    color: #fff;
    font-size: 12px;
    padding: 1px 4px;
    border-radius: 2px;
}
 
.recipe-plus {
    font-size: 24px;
    color: #888;
    padding: 0 5px;
}
 
.recipe-arrow {
    font-size: 28px;
    color: #9edc60;
    padding: 0 10px;
}
 
.recipe-output {
    display: flex;
    align-items: center;
}
 
.recipe-station {
    font-size: 12px;
    color: #9bddff;
    margin-top: 5px;
    font-style: italic;
}
 
.recipe-inline {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background-color: #2a2a4e;
    padding: 2px 6px;
    border-radius: 3px;
}
</style>
]]
 
-- Helper to get args from either direct invoke or template
local function getArgs(frame)
    local args = {}
   
    local parent = frame:getParent()
    if parent and parent.args then
        for k, v in pairs(parent.args) do
            args[k] = v
        end
     end
     end
      
      
     local qty = tonumber(quantity) or 1
     if frame.args then
    local qtyDisplay = ""
        for k, v in pairs(frame.args) do
    if qty > 1 then
            args[k] = v
         qtyDisplay = string.format('<span class="recipe-quantity">%d</span>', qty)
         end
     end
     end
      
      
     return string.format(
     return args
        '<div class="recipe-slot">{{Sprite|%s|%s}}%s<span class="recipe-item-name">%s</span></div>',
        itemName, spriteSize, qtyDisplay, itemName
    )
end
end


-- Main display function for a single recipe
-- Main display function for a single recipe
function p.display(frame)
function p.display(frame)
     local args = frame.args
     local args = getArgs(frame)
     local spriteSize = args.spriteSize or "32"
     local spriteSize = args.spriteSize or "32"
    local showLabels = args.showLabels ~= "false"
      
      
     -- Collect inputs (support up to 6 inputs)
     -- Collect inputs (support up to 6 inputs)
Line 36: Line 188:
         if item and item ~= "" then
         if item and item ~= "" then
             table.insert(inputs, {name = item, quantity = qty})
             table.insert(inputs, {name = item, quantity = qty})
        end
    end
   
    -- Legacy support for simple input1, input2
    if #inputs == 0 then
        if args.input1 then
            table.insert(inputs, {name = args.input1, quantity = args.input1qty or "1"})
        end
        if args.input2 then
            table.insert(inputs, {name = args.input2, quantity = args.input2qty or "1"})
         end
         end
     end
     end
Line 58: Line 200:
     -- Build HTML
     -- Build HTML
     local html = {}
     local html = {}
   
    -- Add CSS
    table.insert(html, css)
      
      
     table.insert(html, '<div class="recipe-container">')
     table.insert(html, '<div class="recipe-container">')
Line 64: Line 209:
     table.insert(html, '<div class="recipe-inputs">')
     table.insert(html, '<div class="recipe-inputs">')
     for i, input in ipairs(inputs) do
     for i, input in ipairs(inputs) do
         table.insert(html, formatSlot(input.name, input.quantity, spriteSize))
        local qtyDisplay = ""
        if tonumber(input.quantity) > 1 then
            qtyDisplay = string.format('<span class="recipe-quantity">%s</span>', input.quantity)
        end
         table.insert(html, string.format(
            '<div class="recipe-slot">{{Sprite|%s|%s}}%s</div>',
            input.name, spriteSize, qtyDisplay
        ))
         if i < #inputs then
         if i < #inputs then
             table.insert(html, '<span class="recipe-plus">+</span>')
             table.insert(html, '<span class="recipe-plus">+</span>')
Line 75: Line 227:
      
      
     -- Output section
     -- Output section
    local outQtyDisplay = ""
    if tonumber(outputQty) > 1 then
        outQtyDisplay = string.format('<span class="recipe-quantity">%s</span>', outputQty)
    end
     table.insert(html, '<div class="recipe-output">')
     table.insert(html, '<div class="recipe-output">')
     table.insert(html, formatSlot(output, outputQty, spriteSize))
     table.insert(html, string.format(
        '<div class="recipe-slot">{{Sprite|%s|%s}}%s</div>',
        output, spriteSize, outQtyDisplay
    ))
     table.insert(html, '</div>')
     table.insert(html, '</div>')
      
      
Line 83: Line 242:
     -- Station requirement (if specified)
     -- Station requirement (if specified)
     if station ~= "" then
     if station ~= "" then
         table.insert(html, string.format('<div class="recipe-station">Requires: %s</div>', station))
         table.insert(html, string.format('<div class="recipe-station">Requires: {{Sprite|%s|20}} %s</div>', station, station))
     end
     end
      
      
Line 91: Line 250:
-- Display a list of multiple recipes
-- Display a list of multiple recipes
function p.list(frame)
function p.list(frame)
     local args = frame.args
     local args = getArgs(frame)
     local title = args.title or "Recipes"
     local title = args.title or "Recipes"
     local spriteSize = args.spriteSize or "32"
     local spriteSize = args.spriteSize or "24"
      
      
     local html = {}
     local html = {}
   
    -- Add CSS
    table.insert(html, css)
      
      
     table.insert(html, '<div class="recipe-list">')
     table.insert(html, '<div class="recipe-list">')
Line 117: Line 279:
                 output, station = outputPart:match("^(.+)@(.+)$")
                 output, station = outputPart:match("^(.+)@(.+)$")
             end
             end
           
            -- Trim whitespace from output
            output = output:match("^%s*(.-)%s*$")
              
              
             -- Parse inputs
             -- Parse inputs
Line 126: Line 291:
                     local qty, name = item:match("^(%d+)x(.+)$")
                     local qty, name = item:match("^(%d+)x(.+)$")
                     if qty then
                     if qty then
                         table.insert(inputItems, {name = name, quantity = qty})
                         table.insert(inputItems, {name = name:match("^%s*(.-)%s*$"), quantity = qty})
                     else
                     else
                         table.insert(inputItems, {name = item, quantity = "1"})
                         table.insert(inputItems, {name = item, quantity = "1"})
Line 138: Line 303:
                 outQty = "1"
                 outQty = "1"
                 outName = output
                 outName = output
            else
                outName = outName:match("^%s*(.-)%s*$")
             end
             end
              
              
Line 148: Line 315:
                 local qtyStr = ""
                 local qtyStr = ""
                 if tonumber(inp.quantity) > 1 then
                 if tonumber(inp.quantity) > 1 then
                     qtyStr = inp.quantity .. "x "
                     qtyStr = inp.quantity .. "× "
                 end
                 end
                 table.insert(html, string.format(
                 table.insert(html, string.format(
Line 155: Line 322:
                 ))
                 ))
                 if j < #inputItems then
                 if j < #inputItems then
                     table.insert(html, ' + ')
                     table.insert(html, '<span class="recipe-plus-small"> + </span>')
                 end
                 end
             end
             end
Line 166: Line 333:
             local outQtyStr = ""
             local outQtyStr = ""
             if tonumber(outQty) > 1 then
             if tonumber(outQty) > 1 then
                 outQtyStr = outQty .. "x "
                 outQtyStr = outQty .. "× "
             end
             end
             table.insert(html, string.format(
             table.insert(html, string.format(
Line 175: Line 342:
             -- Station cell
             -- Station cell
             if station ~= "" then
             if station ~= "" then
                station = station:match("^%s*(.-)%s*$") -- trim
                 table.insert(html, string.format(
                 table.insert(html, string.format(
                     '<td class="recipe-cell-station">{{Sprite|%s|%s}} %s</td>',
                     '<td class="recipe-cell-station"><span class="recipe-inline-item">{{Sprite|%s|%s}} %s</span></td>',
                     station, spriteSize, station
                     station, spriteSize, station
                 ))
                 ))
             else
             else
                 table.insert(html, '<td class="recipe-cell-station">-</td>')
                 table.insert(html, '<td class="recipe-cell-station"></td>')
             end
             end
              
              
Line 197: Line 365:
-- Simple inline recipe (for use within text)
-- Simple inline recipe (for use within text)
function p.inline(frame)
function p.inline(frame)
     local args = frame.args
     local args = getArgs(frame)
     local spriteSize = args.spriteSize or "16"
     local spriteSize = args.spriteSize or "16"
      
      
Line 204: Line 372:
     local output = args[3] or args.output or ""
     local output = args[3] or args.output or ""
      
      
     local html = string.format(
     local html = css .. string.format(
         '<span class="recipe-inline">{{Sprite|%s|%s}} + {{Sprite|%s|%s}} → {{Sprite|%s|%s}}</span>',
         '<span class="recipe-inline">{{Sprite|%s|%s}} + {{Sprite|%s|%s}} → {{Sprite|%s|%s}}</span>',
         input1, spriteSize, input2, spriteSize, output, spriteSize
         input1, spriteSize, input2, spriteSize, output, spriteSize

Revision as of 21:44, 27 January 2026

Module:Recipe Documentation

This module displays crafting recipes showing how items combine to create new items.

Single Recipe

Display a single crafting recipe:

{{#invoke:Recipe|display
|input1=Pumpkin_Slice
|input2=Empty_Bowl
|output=Pumpkin_Puree
}}

With quantities:

{{#invoke:Recipe|display
|input1=Pumpkin_Slice
|input1qty=3
|input2=Empty_Bowl
|input2qty=2
|output=Pumpkin_Puree
}}

Recipe Parameters

Parameter Description
input1, input2, ... input6 Input item names (up to 6 inputs)
input1qty, input2qty, etc. Quantity for each input (default: 1)
output The resulting item name
outputqty Quantity produced (default: 1)
station Required crafting station (optional)
spriteSize Size of item sprites (default: 32)

Recipe List

Display multiple recipes in a table:

{{#invoke:Recipe|list
|title=Weapon Recipes
|recipe1=Wood+Stone=Stone Axe
|recipe2=Wood+Iron Bar=Iron Sword@Anvil
|recipe3=2xLeather+Iron Bar=Leather Armor@Workbench
|recipe4=3xWood+2xRope=Raft
}}

Recipe String Format

Each recipe uses the format:

[qty]xItem1+[qty]xItem2=Output[@Station]

Examples:

  • Wood+Stone=Axe - Simple recipe
  • Wood+Iron Bar=Sword@Anvil - Recipe with station
  • 2xWood+3xStone=Wall - Recipe with quantities
  • 2xIron Bar=4xNails@Anvil - Multiple output

Inline Recipe

For showing a recipe within text:

Combine {{#invoke:Recipe|inline|Wood|Stone|Axe}} to make your first tool.

Examples

Cooking Recipes

{{#invoke:Recipe|list
|title=Cooking Recipes
|recipe1=Raw Meat+Salt=Cooked Meat@Campfire
|recipe2=Flour+Water+Egg=Dough@Kitchen
|recipe3=Dough+Blueberry=Blueberry Muffin@Oven
|recipe4=2xFish+Lemon=Fish Dinner@Campfire
}}

Weapon Crafting

{{#invoke:Recipe|list
|title=Weapons
|recipe1=2xWood+Silver Bar=Silver Dagger@Anvil
|recipe2=3xWood+2xString+Iron Bar=Crossbow@Workbench
|recipe3=Wood+4xIron Bar=Battle Axe@Anvil
|recipe4=2xWood+Iron Bar=Shovel@Workbench
}}
{{#invoke:Recipe|display
|input1=Silver Bar
|input1qty=2
|input2=Wood
|input3=Magic Gem
|output=Silver Dagger
|station=Enchanting Table
|spriteSize=48
}}

Required CSS

Add this to your wiki's Common.css:

/* Recipe Container */
.recipe-container {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px;
    background-color: #1a1a2e;
    border: 2px solid #4a4a6a;
    border-radius: 5px;
    margin: 10px 0;
    width: fit-content;
}

.recipe-inputs {
    display: flex;
    align-items: center;
    gap: 5px;
}

.recipe-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 5px;
    background-color: #2a2a4e;
    border: 1px solid #3a3a5a;
    border-radius: 3px;
    position: relative;
    min-width: 50px;
}

.recipe-slot-empty {
    background-color: #1a1a2e;
    min-width: 50px;
    min-height: 50px;
}

.recipe-quantity {
    position: absolute;
    bottom: 2px;
    right: 2px;
    background-color: #000;
    color: #fff;
    font-size: 12px;
    padding: 1px 4px;
    border-radius: 2px;
}

.recipe-item-name {
    font-size: 10px;
    color: #aaa;
    margin-top: 3px;
    text-align: center;
}

.recipe-plus {
    font-size: 24px;
    color: #888;
    padding: 0 5px;
}

.recipe-arrow {
    font-size: 28px;
    color: #9edc60;
    padding: 0 10px;
}

.recipe-output {
    display: flex;
    align-items: center;
}

.recipe-station {
    font-size: 12px;
    color: #9bddff;
    margin-top: 5px;
    font-style: italic;
}

/* Recipe List/Table */
.recipe-list {
    background-color: #1a1a2e;
    border: 2px solid #4a4a6a;
    border-radius: 5px;
    padding: 10px;
    margin: 10px 0;
}

.recipe-list-title {
    font-size: 18px;
    font-weight: bold;
    color: #ffd700;
    margin-bottom: 10px;
    padding-bottom: 5px;
    border-bottom: 1px solid #4a4a6a;
}

.recipe-table {
    width: 100%;
    border-collapse: collapse;
}

.recipe-table-header th {
    text-align: left;
    padding: 8px;
    color: #aaa;
    border-bottom: 1px solid #4a4a6a;
}

.recipe-row td {
    padding: 8px;
    border-bottom: 1px solid #2a2a4e;
}

.recipe-cell-arrow {
    color: #9edc60;
    font-size: 20px;
    text-align: center;
    width: 40px;
}

.recipe-cell-station {
    color: #9bddff;
}

.recipe-inline-item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

/* Inline recipe */
.recipe-inline {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background-color: #2a2a4e;
    padding: 2px 6px;
    border-radius: 3px;
}

-- Module:Recipe
-- Displays crafting recipes showing items combining into results
-- Usage: {{#invoke:Recipe|display|input1=Item1|input2=Item2|output=Result}}

local p = {}

-- CSS styles embedded in module
local css = [[
<style>
.recipe-list {
    background-color: #1a1a2e;
    border: 2px solid #4a4a6a;
    border-radius: 5px;
    padding: 10px;
    margin: 10px 0;
    overflow-x: auto;
}

.recipe-list-title {
    font-size: 18px;
    font-weight: bold;
    color: #ffd700;
    margin-bottom: 10px;
    padding-bottom: 5px;
    border-bottom: 1px solid #4a4a6a;
}

.recipe-table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

.recipe-table-header th {
    text-align: left;
    padding: 8px;
    color: #aaa;
    border-bottom: 1px solid #4a4a6a;
}

.recipe-table-header th:nth-child(1) { width: 45%; }
.recipe-table-header th:nth-child(2) { width: 5%; }
.recipe-table-header th:nth-child(3) { width: 25%; }
.recipe-table-header th:nth-child(4) { width: 25%; }

.recipe-row td {
    padding: 8px;
    border-bottom: 1px solid #2a2a4e;
    vertical-align: middle;
}

.recipe-cell-inputs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
}

.recipe-cell-arrow {
    color: #9edc60;
    font-size: 20px;
    text-align: center;
}

.recipe-cell-output {
    color: #fff;
}

.recipe-cell-station {
    color: #9bddff;
}

.recipe-inline-item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}

.recipe-plus-small {
    color: #888;
}

.recipe-container {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 10px;
    background-color: #1a1a2e;
    border: 2px solid #4a4a6a;
    border-radius: 5px;
    margin: 10px 0;
}

.recipe-inputs {
    display: flex;
    align-items: center;
    gap: 5px;
}

.recipe-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 5px;
    background-color: #2a2a4e;
    border: 1px solid #3a3a5a;
    border-radius: 3px;
    position: relative;
}

.recipe-quantity {
    position: absolute;
    bottom: 2px;
    right: 2px;
    background-color: #000;
    color: #fff;
    font-size: 12px;
    padding: 1px 4px;
    border-radius: 2px;
}

.recipe-plus {
    font-size: 24px;
    color: #888;
    padding: 0 5px;
}

.recipe-arrow {
    font-size: 28px;
    color: #9edc60;
    padding: 0 10px;
}

.recipe-output {
    display: flex;
    align-items: center;
}

.recipe-station {
    font-size: 12px;
    color: #9bddff;
    margin-top: 5px;
    font-style: italic;
}

.recipe-inline {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background-color: #2a2a4e;
    padding: 2px 6px;
    border-radius: 3px;
}
</style>
]]

-- Helper to get args from either direct invoke or template
local function getArgs(frame)
    local args = {}
    
    local parent = frame:getParent()
    if parent and parent.args then
        for k, v in pairs(parent.args) do
            args[k] = v
        end
    end
    
    if frame.args then
        for k, v in pairs(frame.args) do
            args[k] = v
        end
    end
    
    return args
end

-- Main display function for a single recipe
function p.display(frame)
    local args = getArgs(frame)
    local spriteSize = args.spriteSize or "32"
    
    -- Collect inputs (support up to 6 inputs)
    local inputs = {}
    for i = 1, 6 do
        local item = args["input" .. i] or args["in" .. i]
        local qty = args["input" .. i .. "qty"] or args["in" .. i .. "qty"] or "1"
        if item and item ~= "" then
            table.insert(inputs, {name = item, quantity = qty})
        end
    end
    
    -- Output
    local output = args.output or args.out or "Unknown"
    local outputQty = args.outputqty or args.outqty or "1"
    
    -- Station/workbench (optional)
    local station = args.station or args.workbench or ""
    
    -- Build HTML
    local html = {}
    
    -- Add CSS
    table.insert(html, css)
    
    table.insert(html, '<div class="recipe-container">')
    
    -- Inputs section
    table.insert(html, '<div class="recipe-inputs">')
    for i, input in ipairs(inputs) do
        local qtyDisplay = ""
        if tonumber(input.quantity) > 1 then
            qtyDisplay = string.format('<span class="recipe-quantity">%s</span>', input.quantity)
        end
        table.insert(html, string.format(
            '<div class="recipe-slot">{{Sprite|%s|%s}}%s</div>',
            input.name, spriteSize, qtyDisplay
        ))
        if i < #inputs then
            table.insert(html, '<span class="recipe-plus">+</span>')
        end
    end
    table.insert(html, '</div>')
    
    -- Arrow
    table.insert(html, '<div class="recipe-arrow">→</div>')
    
    -- Output section
    local outQtyDisplay = ""
    if tonumber(outputQty) > 1 then
        outQtyDisplay = string.format('<span class="recipe-quantity">%s</span>', outputQty)
    end
    table.insert(html, '<div class="recipe-output">')
    table.insert(html, string.format(
        '<div class="recipe-slot">{{Sprite|%s|%s}}%s</div>',
        output, spriteSize, outQtyDisplay
    ))
    table.insert(html, '</div>')
    
    table.insert(html, '</div>')
    
    -- Station requirement (if specified)
    if station ~= "" then
        table.insert(html, string.format('<div class="recipe-station">Requires: {{Sprite|%s|20}} %s</div>', station, station))
    end
    
    return frame:preprocess(table.concat(html, '\n'))
end

-- Display a list of multiple recipes
function p.list(frame)
    local args = getArgs(frame)
    local title = args.title or "Recipes"
    local spriteSize = args.spriteSize or "24"
    
    local html = {}
    
    -- Add CSS
    table.insert(html, css)
    
    table.insert(html, '<div class="recipe-list">')
    table.insert(html, string.format('<div class="recipe-list-title">%s</div>', title))
    table.insert(html, '<table class="recipe-table">')
    table.insert(html, '<tr class="recipe-table-header"><th>Ingredients</th><th></th><th>Result</th><th>Station</th></tr>')
    
    -- Parse recipes (format: input1+input2=output@station)
    local i = 1
    while args["recipe" .. i] or args["r" .. i] do
        local recipeStr = args["recipe" .. i] or args["r" .. i]
        
        -- Parse: "Item1+Item2=Output" or "Item1+Item2=Output@Station"
        local inputPart, outputPart = recipeStr:match("^(.+)=(.+)$")
        if inputPart and outputPart then
            local station = ""
            local output = outputPart
            
            -- Check for station
            if outputPart:find("@") then
                output, station = outputPart:match("^(.+)@(.+)$")
            end
            
            -- Trim whitespace from output
            output = output:match("^%s*(.-)%s*$")
            
            -- Parse inputs
            local inputItems = {}
            for item in inputPart:gmatch("[^+]+") do
                item = item:match("^%s*(.-)%s*$") -- trim
                if item ~= "" then
                    -- Check for quantity (e.g., "2xWood")
                    local qty, name = item:match("^(%d+)x(.+)$")
                    if qty then
                        table.insert(inputItems, {name = name:match("^%s*(.-)%s*$"), quantity = qty})
                    else
                        table.insert(inputItems, {name = item, quantity = "1"})
                    end
                end
            end
            
            -- Parse output quantity
            local outQty, outName = output:match("^(%d+)x(.+)$")
            if not outQty then
                outQty = "1"
                outName = output
            else
                outName = outName:match("^%s*(.-)%s*$")
            end
            
            -- Build table row
            table.insert(html, '<tr class="recipe-row">')
            
            -- Ingredients cell
            table.insert(html, '<td class="recipe-cell-inputs">')
            for j, inp in ipairs(inputItems) do
                local qtyStr = ""
                if tonumber(inp.quantity) > 1 then
                    qtyStr = inp.quantity .. "× "
                end
                table.insert(html, string.format(
                    '<span class="recipe-inline-item">{{Sprite|%s|%s}} %s%s</span>',
                    inp.name, spriteSize, qtyStr, inp.name
                ))
                if j < #inputItems then
                    table.insert(html, '<span class="recipe-plus-small"> + </span>')
                end
            end
            table.insert(html, '</td>')
            
            -- Arrow cell
            table.insert(html, '<td class="recipe-cell-arrow">→</td>')
            
            -- Output cell
            local outQtyStr = ""
            if tonumber(outQty) > 1 then
                outQtyStr = outQty .. "× "
            end
            table.insert(html, string.format(
                '<td class="recipe-cell-output"><span class="recipe-inline-item">{{Sprite|%s|%s}} %s%s</span></td>',
                outName, spriteSize, outQtyStr, outName
            ))
            
            -- Station cell
            if station ~= "" then
                station = station:match("^%s*(.-)%s*$") -- trim
                table.insert(html, string.format(
                    '<td class="recipe-cell-station"><span class="recipe-inline-item">{{Sprite|%s|%s}} %s</span></td>',
                    station, spriteSize, station
                ))
            else
                table.insert(html, '<td class="recipe-cell-station"></td>')
            end
            
            table.insert(html, '</tr>')
        end
        
        i = i + 1
    end
    
    table.insert(html, '</table>')
    table.insert(html, '</div>')
    
    return frame:preprocess(table.concat(html, '\n'))
end

-- Simple inline recipe (for use within text)
function p.inline(frame)
    local args = getArgs(frame)
    local spriteSize = args.spriteSize or "16"
    
    local input1 = args[1] or args.input1 or ""
    local input2 = args[2] or args.input2 or ""
    local output = args[3] or args.output or ""
    
    local html = css .. string.format(
        '<span class="recipe-inline">{{Sprite|%s|%s}} + {{Sprite|%s|%s}} → {{Sprite|%s|%s}}</span>',
        input1, spriteSize, input2, spriteSize, output, spriteSize
    )
    
    return frame:preprocess(html)
end

return p