Actions

Module

DataTableParserV2: Difference between revisions

From Dune Awakening DB

Created page with "-- Module:DataTableParser -- Handles display and formatting of building data for Templates -- Updated to use External Data instead of parsing a wiki table page local p = {} -- Function to generate icon file reference for a resource local function getResourceIcon(resourceName) if not resourceName or resourceName == "" then return "" end local fileName = resourceName:gsub("%s+", "_") .. "_-_Icon.png" local fileTitle = mw.title.new("File:" .. fileN..."
 
mNo edit summary
 
(24 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:DataTableParser
-- Module:DataTableParserV2
-- Handles display and formatting of building data for Templates
-- Handles display and formatting of building data for Templates using External Data.
-- Updated to use External Data instead of parsing a wiki table page
-- Data is fetched from your SQL tables (data_buildings and data_refining_recipes)
-- via the ExternalData extension.


local p = {}
local p = {}


-- Function to generate icon file reference for a resource
--------------------------------------------------
-- Helper: Get icon file reference for a resource
--------------------------------------------------
local function getResourceIcon(resourceName)
local function getResourceIcon(resourceName)
     if not resourceName or resourceName == "" then
     if not resourceName or resourceName == "" then
Line 19: Line 22:
end
end


-- Function to add icons to resource links in text
---------------------------------------------
-- Function: iconize
-- Adds icons to resource links in text
---------------------------------------------
function p.iconize(frame)
function p.iconize(frame)
     local text = frame.args[1] or ""
     local text = frame.args[1] or ""
Line 33: Line 39:
end
end


----------------------------------------------------------------------
------------------------------------------------------
-- Updated: Helper function to load building data from ExternalData
-- Function: formatComponent
----------------------------------------------------------------------
-- Formats a recipe component by adding icons to resource links.
local function loadBuildingWikiTable(frame)
------------------------------------------------------
     -- Build the externaldata query string.
function p.formatComponent(text)
     local edQuery = [[
    if not text or text == "" then
{{#get_external_data: source=externaldb
        return ""
|from=data_buildings
    end
|data=Name=name,Description=description,JourneyRequirement=journey_requirement,Health=health,PowerCost=power_cost,GeneratesPower=generates_power,StorageSlots=storage_slots,StorageVolume=storage_capacity,RecipeToBuild=recipe_to_build,PlacedWith=placed_with,ImageFile=image_file,AdditionalNotes=additional_notes
    local components = mw.text.split(text, ";")
|cache=yes
    local formatted = {}
}}
    for i, component in ipairs(components) do
{| class="wikitable"
        component = mw.text.trim(component)
! Name
        local itemName, quantity = component:match("%[%[([^%]]+)%]%] x (%d+)")
! Description
        if itemName and quantity then
! JourneyRequirement
            local icon = getResourceIcon(itemName)
! Health
            table.insert(formatted, icon .. " [[" .. itemName .. "]] x " .. quantity)
! PowerCost
        else
! GeneratesPower
            table.insert(formatted, component)
! StorageSlots
        end
! StorageVolume
    end
! RecipeToBuild
    return table.concat(formatted, "<br>")
! PlacedWith
end
! ImageFile
 
! AdditionalNotes
--------------------------------------------------------------------------------
{{#for_external_table:|
-- Function: loadBuildingData
{{!}}-
-- Uses ExternalData (via the parser function) to fetch building data from data_buildings.
{{!}} {{{Name}}}
--------------------------------------------------------------------------------
{{!}} {{{Description}}}
local function splitBySeparator(input, sep)
{{!}} {{{JourneyRequirement}}}
    local result = {}
{{!}} {{{Health}}}
    local pos = 1
{{!}} {{{PowerCost}}}
    while true do
{{!}} {{{GeneratesPower}}}
        local i, j = input:find(sep, pos, true)
{{!}} {{{StorageSlots}}}
        if not i then
{{!}} {{{StorageVolume}}}
            table.insert(result, mw.text.trim(input:sub(pos)))
{{!}} {{{RecipeToBuild}}}
            break
{{!}} {{{PlacedWith}}}
        end
{{!}} {{{ImageFile}}}
        table.insert(result, mw.text.trim(input:sub(pos, i - 1)))
{{!}} {{{AdditionalNotes}}}
        pos = j + 1
}}
    end
|}
    return result
]]
end
     local content = frame:preprocess(edQuery)
 
local function loadBuildingData(frame)
     local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
     local edQuery = string.format([[
        {{#get_external_data: source=externaldb
        |from=data_buildings
        |data=ID=id,BuildingType=building_type,Name=name,Description=description,PowerCost=power_cost,GeneratesPower=generates_power,StorageSlots=storage_slots,StorageCapacity=storage_capacity,SchematicRequirement=schematic_requirement,JourneyRequirement=journey_requirement,Health=health,PlacedWith=placed_with,AdditionalNotes=additional_notes,RecipeToBuild=recipe_to_build,ImageFile=image_file,IconFile=icon_file,Category1=category_1,Category2=category_2,Category3=category_3,Gallery1=gallery_1,Gallery2=gallery_2,Gallery3=gallery_3,Gallery4=gallery_4,YoutubeVideoLink=youtube_video_link
        |cache=yes
        |where=name='%s'
        |limit=1
        |format=plain
        |separator=||
        }}
    ]], buildingName)
   
    local rawOutput = mw.text.trim(frame:preprocess(edQuery))
    if rawOutput == "" then
        return nil, "No data found for building: " .. buildingName
    end
 
    local fields = {
        "ID", "BuildingType", "Name", "Description", "PowerCost", "GeneratesPower",
        "StorageSlots", "StorageCapacity", "SchematicRequirement", "JourneyRequirement",
        "Health", "PlacedWith", "AdditionalNotes", "RecipeToBuild", "ImageFile",
        "IconFile", "Category1", "Category2", "Category3", "Gallery1", "Gallery2",
        "Gallery3", "Gallery4", "YoutubeVideoLink"
    }
 
    local values = splitBySeparator(rawOutput, "||")
    if #values < #fields then
        return nil, "Incomplete data returned for building: " .. buildingName
    end
 
    local colMap = {}
    for i, field in ipairs(fields) do
        colMap[field] = i
    end
 
    local buildingData = { colMap = colMap, rows = { values } }
    return buildingData, nil
end
 
 
--------------------------------------------------------------------------------
-- Function: loadRefiningData
-- Uses ExternalData (via parser function) to fetch refining recipes from data_refining_recipes.
--------------------------------------------------------------------------------
local function loadRefiningData(frame)
    local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
    local edQuery = string.format([[
        {{#get_external_data: source=externaldb
        |from=data_refining_recipes
        |data=Refiner=Refiner,Output=Output,Ingredients=Ingredients,Time=Time,Recipe=Recipe
        |cache=yes
        |where=Refiner='%s'
        }}
    ]], buildingName)
   
    local result = frame:preprocess(edQuery)
   
    local refiningData = {}
    local colMap = {}
    local rows = {}
    local fields = {"Refiner", "Output", "Ingredients", "Time", "Recipe"}
   
    local firstRow = {}
    for i, field in ipairs(fields) do
        colMap[field] = i
        local value = frame:preprocess("{{{" .. field .. "}}}")
        if value and value ~= "" and value ~= "{{{" .. field .. "}}}" then
            firstRow[i] = value
        else
            firstRow[i] = ""
        end
    end
   
     local hasFirstRow = false
    for _, v in pairs(firstRow) do
        if v ~= "" then
            hasFirstRow = true
            break
        end
    end
      
      
     if not content or content == "" then
     if hasFirstRow then
         return nil, "Error: No building data returned from external source!"
         table.insert(rows, firstRow)
     end
     end
      
      
     local lines = mw.text.split(content, "\n")
     -- Check for additional rows
    local filtered = {}
    for i = 1, 50 do
    for _, line in ipairs(lines) do
        local rowData = {}
        line = mw.text.trim(line)
        local hasData = false
        if line ~= "" and not line:match("^__") and not line:match("<noindex>") and not line:match("</noindex>") then
        for j, field in ipairs(fields) do
             table.insert(filtered, line)
            local value = frame:preprocess("{{{" .. field .. "_" .. i .. "}}}")
            if value and value ~= "" and value ~= "{{{" .. field .. "_" .. i .. "}}}" then
                rowData[j] = value
                hasData = true
            else
                rowData[j] = ""
            end
        end
        if hasData then
             table.insert(rows, rowData)
        else
            break
         end
         end
     end
     end
     if #filtered < 2 then
   
         return nil, "Error: No valid building data found!"
     if #rows == 0 then
         return nil, "No refining recipes found for building: " .. buildingName
     end
     end
   
    refiningData.colMap = colMap
    refiningData.rows = rows
   
    return refiningData, nil
end


    local inTable = false
--------------------------------------------------------------------------------
    local headers = {}  -- table headers
-- Function: getBuildingData
    local colMap = {}  -- header name -> column index
-- Returns the row (and colMap) for a given building name.
     local data = {}     -- array of row arrays
--------------------------------------------------------------------------------
     local currentRow = {}
function p.getBuildingData(buildingName, frame)
     local readingHeader = true
     local buildingData, err = loadBuildingData(frame)
     if not buildingData or #buildingData.rows == 0 then
        return nil, err or "Building not found"
     end
     return buildingData.rows[1], buildingData.colMap
end


    for _, line in ipairs(filtered) do
--------------------------------------------------------------------------------
        if line:match("^{|") then
-- Function: getRefiningRecipes
            inTable = true
-- Returns a table of refining recipes matching the building name.
        elseif line:match("^|%-") then
--------------------------------------------------------------------------------
            if not readingHeader and #currentRow > 0 then
function p.getRefiningRecipes(frame)
                table.insert(data, currentRow)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
            end
    local refiningData, err = loadRefiningData(frame)
            currentRow = {}
   
            readingHeader = false
    if not refiningData then
         elseif line:match("^!") then
        return '<tr><td colspan="3" style="text-align:center;">No refining recipes found for this building.</td></tr>'
            local header = line:gsub("^!+%s*", "")
    end
            header = mw.text.trim(header)
   
            table.insert(headers, header)
    local rows = refiningData.rows
        elseif line:match("^|%+") then
    local colMap = refiningData.colMap
            -- Skip caption
   
        elseif line:match("^|}") then
    if #rows == 0 then
            if not readingHeader and #currentRow > 0 then
         return '<tr><td colspan="3" style="text-align:center;">No refining recipes found for this building.</td></tr>'
                table.insert(data, currentRow)
    end
            end
   
            inTable = false
    local output = "<tr>\n" ..
         elseif inTable and line:match("^|[^%-+}]") then
                  "<th style=\"text-align:left;\">Output</th>\n" ..
             local cell = line:gsub("^|+%s*", "")
                  "<th style=\"text-align:left;\">Ingredients</th>\n" ..
            cell = mw.text.trim(cell)
                  "<th style=\"text-align:left;\">Craft Time</th>\n" ..
             if not readingHeader then
                  "</tr>\n"
                 table.insert(currentRow, cell)
                 
    for _, recipe in ipairs(rows) do
        local outputItem = recipe[colMap["Output"]] or ""
        local ingredients = recipe[colMap["Ingredients"]] or ""
         local time = recipe[colMap["Time"]] or ""
        local recipeQty = recipe[colMap["Recipe"]] or ""
       
        local qty = "1"
        if recipeQty ~= "" then
             local extractQty = recipeQty:match("x%s*(%d+)")
             if extractQty then
                 qty = extractQty
             end
             end
         end
         end
       
        local formattedOutput = p.iconize({args = {[1] = outputItem}})
        if qty ~= "1" then
            formattedOutput = formattedOutput .. " × " .. qty
        end
       
        local formattedIngredients = p.formatRecipeList(ingredients)
       
        output = output .. "<tr>\n" ..
                "<td style=\"text-align:left;\">" .. formattedOutput .. "</td>\n" ..
                "<td style=\"text-align:left;\">" .. formattedIngredients .. "</td>\n" ..
                "<td style=\"text-align:left;\">" .. time .. "</td>\n" ..
                "</tr>\n"
    end
   
    return output
end
--------------------------------------------------------------------------------
-- Function: formatBuilding
-- Formats building data as a template call to BuildingRefinerDisplayV2.
--------------------------------------------------------------------------------
function p.formatBuilding(frame)
    local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
    if not buildingName then
        return "Error: No building name provided"
    end
    if frame.args.debug then
        return p.debugBuildingData(frame)
    end
    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return "Error: " .. (err or "Building '" .. buildingName .. "' not found")
    end
   
    local colMap = buildingData.colMap
    local building = buildingData.rows[1]
   
    if not building then
        return "Error: No data row found for building '" .. buildingName .. "'"
    end
    local params = {
        Name = building[colMap["Name"]] or buildingName,
        Tier = building[colMap["BuildingType"]] or "Unknown",
        Description = building[colMap["Description"]] or "",
        JourneyRequirement = building[colMap["JourneyRequirement"]] or "",
        Health = building[colMap["Health"]] or "0",
        EnergyConsumption = building[colMap["PowerCost"]] or "0",
        GeneratesPower = building[colMap["GeneratesPower"]] or "0",
        StorageSlots = building[colMap["StorageSlots"]] or "0",
        StorageVolume = building[colMap["StorageCapacity"]] or "0",
        Components = building[colMap["RecipeToBuild"]] or "",
        PlacedWith = building[colMap["PlacedWith"]] or "",
        AdditionalNotes = building[colMap["AdditionalNotes"]] or "",
        ImageFile = building[colMap["ImageFile"]] or "",
        PrimarySource = "Crafting"
    }
   
    if params.Description ~= "" then
        params.Description = p.iconize({args = {[1] = params.Description}})
    end
   
    if params.JourneyRequirement ~= "" then
        params.JourneyRequirement = p.iconize({args = {[1] = params.JourneyRequirement}})
    end
   
    if params.Components ~= "" then
        params.Components = p.formatComponent(params.Components)
    end
   
    if params.PlacedWith ~= "" then
        params.PlacedWith = p.iconize({args = {[1] = params.PlacedWith}})
    end
   
    if params.AdditionalNotes ~= "" then
        params.AdditionalNotes = p.iconize({args = {[1] = params.AdditionalNotes}})
    end
    if colMap["YoutubeVideoLink"] and building[colMap["YoutubeVideoLink"]] and building[colMap["YoutubeVideoLink"]] ~= "" then
        params.YoutubeEmbed = p.getYoutubeEmbed(frame)
    else
        params.YoutubeEmbed = "Coming Soon"
    end
   
    params.RefiningRecipes = p.getRefiningRecipes(frame)
   
    if colMap["Category3"] and building[colMap["Category3"]] and building[colMap["Category3"]] ~= "" then
        params.Category3 = building[colMap["Category3"]]
    else
        params.Category3 = "Buildings"
    end
   
    params.RelatedBuildings = p.relatedBuildings(frame)
   
    local templateCall = "{{BuildingRefinerDisplayV2"
    for key, value in pairs(params) do
        if value and value ~= "" then
            templateCall = templateCall .. "\n|" .. key .. "=" .. value
        end
    end
    templateCall = templateCall .. "\n}}"
   
    return frame:preprocess(templateCall)
end
--------------------------------------------------------------------------------
-- Function: getYoutubeEmbed
-- Returns a processed YouTube embed tag.
--------------------------------------------------------------------------------
function p.getYoutubeEmbed(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local width = frame.args.width or 400
    local height = frame.args.height or 300
    local dimensions = width .. "x" .. height
    local building, colMap = p.getBuildingData(buildingName, frame)
    if not building then
        return "Error loading data"
    end
    local youtubeUrl = building[colMap["YoutubeVideoLink"]] or ""
    if youtubeUrl == "" or youtubeUrl == "-" then
        return "Coming Soon"
     end
     end


     for i, h in ipairs(headers) do
     local videoId = youtubeUrl:match("v=([%w-_]+)")
         colMap[mw.text.trim(h)] = i
    if not videoId then
         return "Coming Soon"
     end
     end


     return { rows = data, colMap = colMap }, nil
     local ytMarkup = string.format('<youtube dimensions="%s" alignment="center">%s</youtube>', dimensions, videoId)
    return frame:preprocess(ytMarkup)
end
end


----------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Updated: Helper function to load refining data from ExternalData
-- Function: relatedBuildings
----------------------------------------------------------------------
-- Finds and formats related buildings based on Category3.
local function loadRefiningWikiTable(frame)
--------------------------------------------------------------------------------
     local edQuery = [[
function p.relatedBuildings(frame)
{{#get_external_data: source=externaldb
     local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
|from=data_refining_recipes
    local building, colMap = p.getBuildingData(buildingName, frame)
|data=Refiner=Refiner,Output=Output,Ingredients=Ingredients,Time=Time,Recipe=Recipe
    if not building then
|cache=yes
        return "Error loading building data"
}}
    end
{| class="wikitable"
    local targetCategory = building[colMap["Category3"]] or ""
! Refiner
    if targetCategory == "-" or targetCategory == "" then
! Output
        return "No related buildings found"
! Ingredients
    end
! Time
    if targetCategory:find("%[%[") then
! Recipe
        targetCategory = targetCategory:match("%[%[(.-)%]%]")
{{#for_external_table:|
    end
{{!}}-
    targetCategory = mw.text.trim(targetCategory)
{{!}} {{{Refiner}}}
 
{{!}} {{{Output}}}
     local allData, err = loadBuildingData(frame)
{{!}} {{{Ingredients}}}
     if not allData then
{{!}} {{{Time}}}
         return "Error loading building data"
{{!}} {{{Recipe}}}
}}
|}
]]
     local content = frame:preprocess(edQuery)
     if not content or content == "" then
         return nil, "Error: No refining data returned from external source!"
     end
     end
    local rows = allData.rows


     local lines = mw.text.split(content, "\n")
     local relatedBuildings = {}
    local filtered = {}
     for _, row in ipairs(rows) do
     for _, line in ipairs(lines) do
         if row[colMap["Name"]] ~= buildingName then
         line = mw.text.trim(line)
            local category = row[colMap["Category3"]] or ""
        if line ~= "" and not line:match("^__") and not line:match("<noindex>") and not line:match("</noindex>") then
            if category ~= "-" and category ~= "" then
            table.insert(filtered, line)
                if category:find("%[%[") then
                    category = category:match("%[%[(.-)%]%]")
                end
                category = mw.text.trim(category)
                if category == targetCategory then
                    table.insert(relatedBuildings, row)
                end
            end
         end
         end
     end
     end
     if #filtered < 2 then
 
         return nil, "Error: No valid refining data found!"
     if #relatedBuildings == 0 then
         return "No other " .. targetCategory .. " found"
     end
     end


     local inTable = false
     local output = '<table class="infobox-dune" style="width:100%">\n'
     local headers = {}
    output = output .. "<tr>\n"
    local colMap = {}
    output = output .. "<th style=\"text-align:left;\">Name</th>\n"
    local data = {}
    output = output .. "<th style=\"text-align:left;\">Tier</th>\n"
    local currentRow = {}
    output = output .. "<th style=\"text-align:left;\">Description</th>\n"
     local readingHeader = true
    output = output .. "</tr>\n"
     for _, b in ipairs(relatedBuildings) do
        local name = b[colMap["Name"]] or ""
        local tier = b[colMap["BuildingType"]] or ""
        local description = b[colMap["Description"]] or ""
        if tier == "-" then tier = "" end
        if description == "-" then description = "" end
        local nameWithIcon = "[[" .. name .. "]]"
        local iconFile = b[colMap["ImageFile"]] or ""
        if iconFile ~= "-" and iconFile ~= "" then
            nameWithIcon = "[[File:" .. iconFile .. "|20px]] [[" .. name .. "]]"
        end
        output = output .. "<tr>\n"
        output = output .. "<td>" .. nameWithIcon .. "</td>\n"
        output = output .. "<td>" .. tier .. "</td>\n"
        output = output .. "<td>" .. description .. "</td>\n"
        output = output .. "</tr>\n"
    end
     output = output .. "</table>"
    return output
end


    for _, line in ipairs(filtered) do
--------------------------------------------------------------------------------
        if line:match("^{|") then
-- Function: debugBuildingData
            inTable = true
-- Outputs debugging information.
        elseif line:match("^|%-") then
--------------------------------------------------------------------------------
            if not readingHeader and #currentRow > 0 then
function p.debugBuildingData(frame)
                table.insert(data, currentRow)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
            end
    local buildingData, err = loadBuildingData(frame)
            currentRow = {}
   
            readingHeader = false
    if not buildingData then
        elseif line:match("^!") then
        return "Error: " .. (err or "Unknown error")
            local header = line:gsub("^!+%s*", "")
    end
            header = mw.text.trim(header)
   
            table.insert(headers, header)
    local output = "Debug information for '" .. buildingName .. "':\n\n"
         elseif line:match("^|%+") then
    output = output .. "Raw data from database:\n"
            -- Skip caption
    output = output .. "<pre>"
        elseif line:match("^|}") then
   
            if not readingHeader and #currentRow > 0 then
    output = output .. "Column mapping:\n"
                table.insert(data, currentRow)
    for name, idx in pairs(buildingData.colMap) do
            end
         output = output .. name .. " => " .. idx .. "\n"
            inTable = false
    end
         elseif inTable and line:match("^|[^%-+}]") then
   
             local cell = line:gsub("^|+%s*", "")
    output = output .. "\nRows (" .. #buildingData.rows .. " found):\n"
             cell = mw.text.trim(cell)
    for i, row in ipairs(buildingData.rows) do
            if not readingHeader then
        output = output .. "Row " .. i .. ":\n"
                 table.insert(currentRow, cell)
         for j, cell in ipairs(row) do
             local colName = "Unknown"
             for name, idx in pairs(buildingData.colMap) do
                if idx == j then
                    colName = name
                    break
                 end
             end
             end
            output = output .. "  " .. colName .. " (" .. j .. "): '" .. cell .. "'\n"
         end
         end
    end
   
    output = output .. "</pre>"
    return output
end
--------------------------------------------------------------------------------
-- Function: formatRecipeList
-- Outputs format for multiple recipe lines
--------------------------------------------------------------------------------
function p.formatRecipeList(frameOrText)
    local text = ""
    -- Handle both direct string or frame.args
    if type(frameOrText) == "string" then
        text = frameOrText
    elseif type(frameOrText) == "table" then
        text = frameOrText.args and frameOrText.args[1] or ""
     end
     end


     for i, h in ipairs(headers) do
     if not text or text == "" then
         colMap[mw.text.trim(h)] = i
         return ""
     end
     end


     if #data == 0 then
     local components = mw.text.split(text, ";")
         return nil, "Error: No data rows found in refining data!"
    local formatted = {}
 
    for _, raw in ipairs(components) do
        local line = mw.text.trim(raw)
         if line ~= "" then
            local withIcon = p.iconize({ args = { line } })
            table.insert(formatted, withIcon)
        end
     end
     end


     return { rows = data, colMap = colMap }, nil
     return table.concat(formatted, "<br>")
end
end


-- [Rest of your module functions remain unchanged]
 
-- For example, p.iconize, p.formatComponent, p.getBuildingData, etc.
 
-- They will use the table data returned from loadBuildingWikiTable() or loadRefiningWikiTable(frame)
 
 
 
 
 
 
 


return p
return p

Latest revision as of 01:49, 1 April 2025

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

-- Module:DataTableParserV2
-- Handles display and formatting of building data for Templates using External Data.
-- Data is fetched from your SQL tables (data_buildings and data_refining_recipes)
-- via the ExternalData extension.

local p = {}

--------------------------------------------------
-- Helper: Get icon file reference for a resource
--------------------------------------------------
local function getResourceIcon(resourceName)
    if not resourceName or resourceName == "" then
        return ""
    end
    local fileName = resourceName:gsub("%s+", "_") .. "_-_Icon.png"
    local fileTitle = mw.title.new("File:" .. fileName)
    if fileTitle and fileTitle.exists then
        return "[[File:" .. fileName .. "|20px]]"
    else
        return ""
    end
end

---------------------------------------------
-- Function: iconize
-- Adds icons to resource links in text
---------------------------------------------
function p.iconize(frame)
    local text = frame.args[1] or ""
    text = text:gsub("%[%[([^%]]+)%]%]", function(resourceName)
        local icon = getResourceIcon(resourceName)
        if icon ~= "" then
            return icon .. " [[" .. resourceName .. "]]"
        else
            return "[[" .. resourceName .. "]]"
        end
    end)
    return text
end

------------------------------------------------------
-- Function: formatComponent
-- Formats a recipe component by adding icons to resource links.
------------------------------------------------------
function p.formatComponent(text)
    if not text or text == "" then
        return ""
    end
    local components = mw.text.split(text, ";")
    local formatted = {}
    for i, component in ipairs(components) do
        component = mw.text.trim(component)
        local itemName, quantity = component:match("%[%[([^%]]+)%]%] x (%d+)")
        if itemName and quantity then
            local icon = getResourceIcon(itemName)
            table.insert(formatted, icon .. " [[" .. itemName .. "]] x " .. quantity)
        else
            table.insert(formatted, component)
        end
    end
    return table.concat(formatted, "<br>")
end

--------------------------------------------------------------------------------
-- Function: loadBuildingData
-- Uses ExternalData (via the parser function) to fetch building data from data_buildings.
--------------------------------------------------------------------------------
local function splitBySeparator(input, sep)
    local result = {}
    local pos = 1
    while true do
        local i, j = input:find(sep, pos, true)
        if not i then
            table.insert(result, mw.text.trim(input:sub(pos)))
            break
        end
        table.insert(result, mw.text.trim(input:sub(pos, i - 1)))
        pos = j + 1
    end
    return result
end

local function loadBuildingData(frame)
    local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
    local edQuery = string.format([[
        {{#get_external_data: source=externaldb
         |from=data_buildings
         |data=ID=id,BuildingType=building_type,Name=name,Description=description,PowerCost=power_cost,GeneratesPower=generates_power,StorageSlots=storage_slots,StorageCapacity=storage_capacity,SchematicRequirement=schematic_requirement,JourneyRequirement=journey_requirement,Health=health,PlacedWith=placed_with,AdditionalNotes=additional_notes,RecipeToBuild=recipe_to_build,ImageFile=image_file,IconFile=icon_file,Category1=category_1,Category2=category_2,Category3=category_3,Gallery1=gallery_1,Gallery2=gallery_2,Gallery3=gallery_3,Gallery4=gallery_4,YoutubeVideoLink=youtube_video_link
         |cache=yes
         |where=name='%s'
         |limit=1
         |format=plain
         |separator=|| 
        }}
    ]], buildingName)
    
    local rawOutput = mw.text.trim(frame:preprocess(edQuery))
    if rawOutput == "" then
        return nil, "No data found for building: " .. buildingName
    end

    local fields = {
        "ID", "BuildingType", "Name", "Description", "PowerCost", "GeneratesPower",
        "StorageSlots", "StorageCapacity", "SchematicRequirement", "JourneyRequirement",
        "Health", "PlacedWith", "AdditionalNotes", "RecipeToBuild", "ImageFile",
        "IconFile", "Category1", "Category2", "Category3", "Gallery1", "Gallery2",
        "Gallery3", "Gallery4", "YoutubeVideoLink"
    }

    local values = splitBySeparator(rawOutput, "||")
    if #values < #fields then
        return nil, "Incomplete data returned for building: " .. buildingName
    end

    local colMap = {}
    for i, field in ipairs(fields) do
        colMap[field] = i
    end

    local buildingData = { colMap = colMap, rows = { values } }
    return buildingData, nil
end


--------------------------------------------------------------------------------
-- Function: loadRefiningData
-- Uses ExternalData (via parser function) to fetch refining recipes from data_refining_recipes.
--------------------------------------------------------------------------------
local function loadRefiningData(frame)
    local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
    local edQuery = string.format([[
        {{#get_external_data: source=externaldb
         |from=data_refining_recipes
         |data=Refiner=Refiner,Output=Output,Ingredients=Ingredients,Time=Time,Recipe=Recipe
         |cache=yes
         |where=Refiner='%s'
        }}
    ]], buildingName)
    
    local result = frame:preprocess(edQuery)
    
    local refiningData = {}
    local colMap = {}
    local rows = {}
    local fields = {"Refiner", "Output", "Ingredients", "Time", "Recipe"}
    
    local firstRow = {}
    for i, field in ipairs(fields) do
        colMap[field] = i
        local value = frame:preprocess("{{{" .. field .. "}}}")
        if value and value ~= "" and value ~= "{{{" .. field .. "}}}" then
            firstRow[i] = value
        else
            firstRow[i] = ""
        end
    end
    
    local hasFirstRow = false
    for _, v in pairs(firstRow) do
        if v ~= "" then
            hasFirstRow = true
            break
        end
    end
    
    if hasFirstRow then
        table.insert(rows, firstRow)
    end
    
    -- Check for additional rows
    for i = 1, 50 do
        local rowData = {}
        local hasData = false
        for j, field in ipairs(fields) do
            local value = frame:preprocess("{{{" .. field .. "_" .. i .. "}}}")
            if value and value ~= "" and value ~= "{{{" .. field .. "_" .. i .. "}}}" then
                rowData[j] = value
                hasData = true
            else
                rowData[j] = ""
            end
        end
        if hasData then
            table.insert(rows, rowData)
        else
            break
        end
    end
    
    if #rows == 0 then
        return nil, "No refining recipes found for building: " .. buildingName
    end
    
    refiningData.colMap = colMap
    refiningData.rows = rows
    
    return refiningData, nil
end

--------------------------------------------------------------------------------
-- Function: getBuildingData
-- Returns the row (and colMap) for a given building name.
--------------------------------------------------------------------------------
function p.getBuildingData(buildingName, frame)
    local buildingData, err = loadBuildingData(frame)
    if not buildingData or #buildingData.rows == 0 then
        return nil, err or "Building not found"
    end
    return buildingData.rows[1], buildingData.colMap
end

--------------------------------------------------------------------------------
-- Function: getRefiningRecipes
-- Returns a table of refining recipes matching the building name.
--------------------------------------------------------------------------------
function p.getRefiningRecipes(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local refiningData, err = loadRefiningData(frame)
    
    if not refiningData then
        return '<tr><td colspan="3" style="text-align:center;">No refining recipes found for this building.</td></tr>'
    end
    
    local rows = refiningData.rows
    local colMap = refiningData.colMap
    
    if #rows == 0 then
        return '<tr><td colspan="3" style="text-align:center;">No refining recipes found for this building.</td></tr>'
    end
    
    local output = "<tr>\n" ..
                   "<th style=\"text-align:left;\">Output</th>\n" ..
                   "<th style=\"text-align:left;\">Ingredients</th>\n" ..
                   "<th style=\"text-align:left;\">Craft Time</th>\n" ..
                   "</tr>\n"
                   
    for _, recipe in ipairs(rows) do
        local outputItem = recipe[colMap["Output"]] or ""
        local ingredients = recipe[colMap["Ingredients"]] or ""
        local time = recipe[colMap["Time"]] or ""
        local recipeQty = recipe[colMap["Recipe"]] or ""
        
        local qty = "1"
        if recipeQty ~= "" then
            local extractQty = recipeQty:match("x%s*(%d+)")
            if extractQty then
                qty = extractQty
            end
        end
        
        local formattedOutput = p.iconize({args = {[1] = outputItem}})
        if qty ~= "1" then
            formattedOutput = formattedOutput .. " × " .. qty
        end
        
        local formattedIngredients = p.formatRecipeList(ingredients)

        
        output = output .. "<tr>\n" ..
                 "<td style=\"text-align:left;\">" .. formattedOutput .. "</td>\n" ..
                 "<td style=\"text-align:left;\">" .. formattedIngredients .. "</td>\n" ..
                 "<td style=\"text-align:left;\">" .. time .. "</td>\n" ..
                 "</tr>\n"
    end
    
    return output
end

--------------------------------------------------------------------------------
-- Function: formatBuilding
-- Formats building data as a template call to BuildingRefinerDisplayV2.
--------------------------------------------------------------------------------
function p.formatBuilding(frame)
    local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
    if not buildingName then
        return "Error: No building name provided"
    end

    if frame.args.debug then
        return p.debugBuildingData(frame)
    end

    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return "Error: " .. (err or "Building '" .. buildingName .. "' not found")
    end
    
    local colMap = buildingData.colMap
    local building = buildingData.rows[1]
    
    if not building then
        return "Error: No data row found for building '" .. buildingName .. "'"
    end

    local params = {
        Name = building[colMap["Name"]] or buildingName,
        Tier = building[colMap["BuildingType"]] or "Unknown",
        Description = building[colMap["Description"]] or "",
        JourneyRequirement = building[colMap["JourneyRequirement"]] or "",
        Health = building[colMap["Health"]] or "0",
        EnergyConsumption = building[colMap["PowerCost"]] or "0",
        GeneratesPower = building[colMap["GeneratesPower"]] or "0",
        StorageSlots = building[colMap["StorageSlots"]] or "0",
        StorageVolume = building[colMap["StorageCapacity"]] or "0",
        Components = building[colMap["RecipeToBuild"]] or "",
        PlacedWith = building[colMap["PlacedWith"]] or "",
        AdditionalNotes = building[colMap["AdditionalNotes"]] or "",
        ImageFile = building[colMap["ImageFile"]] or "",
        PrimarySource = "Crafting"
    }
    
    if params.Description ~= "" then
        params.Description = p.iconize({args = {[1] = params.Description}})
    end
    
    if params.JourneyRequirement ~= "" then
        params.JourneyRequirement = p.iconize({args = {[1] = params.JourneyRequirement}})
    end
    
    if params.Components ~= "" then
        params.Components = p.formatComponent(params.Components)
    end
    
    if params.PlacedWith ~= "" then
        params.PlacedWith = p.iconize({args = {[1] = params.PlacedWith}})
    end
    
    if params.AdditionalNotes ~= "" then
        params.AdditionalNotes = p.iconize({args = {[1] = params.AdditionalNotes}})
    end

    if colMap["YoutubeVideoLink"] and building[colMap["YoutubeVideoLink"]] and building[colMap["YoutubeVideoLink"]] ~= "" then
        params.YoutubeEmbed = p.getYoutubeEmbed(frame)
    else
        params.YoutubeEmbed = "Coming Soon"
    end
    
    params.RefiningRecipes = p.getRefiningRecipes(frame)
    
    if colMap["Category3"] and building[colMap["Category3"]] and building[colMap["Category3"]] ~= "" then
        params.Category3 = building[colMap["Category3"]]
    else
        params.Category3 = "Buildings"
    end
    
    params.RelatedBuildings = p.relatedBuildings(frame)
    
    local templateCall = "{{BuildingRefinerDisplayV2"
    for key, value in pairs(params) do
        if value and value ~= "" then
            templateCall = templateCall .. "\n|" .. key .. "=" .. value
        end
    end
    templateCall = templateCall .. "\n}}"
    
    return frame:preprocess(templateCall)
end

--------------------------------------------------------------------------------
-- Function: getYoutubeEmbed
-- Returns a processed YouTube embed tag.
--------------------------------------------------------------------------------
function p.getYoutubeEmbed(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local width = frame.args.width or 400
    local height = frame.args.height or 300
    local dimensions = width .. "x" .. height

    local building, colMap = p.getBuildingData(buildingName, frame)
    if not building then
        return "Error loading data"
    end

    local youtubeUrl = building[colMap["YoutubeVideoLink"]] or ""
    if youtubeUrl == "" or youtubeUrl == "-" then
        return "Coming Soon"
    end

    local videoId = youtubeUrl:match("v=([%w-_]+)")
    if not videoId then
        return "Coming Soon"
    end

    local ytMarkup = string.format('<youtube dimensions="%s" alignment="center">%s</youtube>', dimensions, videoId)
    return frame:preprocess(ytMarkup)
end

--------------------------------------------------------------------------------
-- Function: relatedBuildings
-- Finds and formats related buildings based on Category3.
--------------------------------------------------------------------------------
function p.relatedBuildings(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local building, colMap = p.getBuildingData(buildingName, frame)
    if not building then
        return "Error loading building data"
    end
    local targetCategory = building[colMap["Category3"]] or ""
    if targetCategory == "-" or targetCategory == "" then
        return "No related buildings found"
    end
    if targetCategory:find("%[%[") then
        targetCategory = targetCategory:match("%[%[(.-)%]%]")
    end
    targetCategory = mw.text.trim(targetCategory)

    local allData, err = loadBuildingData(frame)
    if not allData then
        return "Error loading building data"
    end
    local rows = allData.rows

    local relatedBuildings = {}
    for _, row in ipairs(rows) do
        if row[colMap["Name"]] ~= buildingName then
            local category = row[colMap["Category3"]] or ""
            if category ~= "-" and category ~= "" then
                if category:find("%[%[") then
                    category = category:match("%[%[(.-)%]%]")
                end
                category = mw.text.trim(category)
                if category == targetCategory then
                    table.insert(relatedBuildings, row)
                end
            end
        end
    end

    if #relatedBuildings == 0 then
        return "No other " .. targetCategory .. " found"
    end

    local output = '<table class="infobox-dune" style="width:100%">\n'
    output = output .. "<tr>\n"
    output = output .. "<th style=\"text-align:left;\">Name</th>\n"
    output = output .. "<th style=\"text-align:left;\">Tier</th>\n"
    output = output .. "<th style=\"text-align:left;\">Description</th>\n"
    output = output .. "</tr>\n"
    for _, b in ipairs(relatedBuildings) do
        local name = b[colMap["Name"]] or ""
        local tier = b[colMap["BuildingType"]] or ""
        local description = b[colMap["Description"]] or ""
        if tier == "-" then tier = "" end
        if description == "-" then description = "" end
        local nameWithIcon = "[[" .. name .. "]]"
        local iconFile = b[colMap["ImageFile"]] or ""
        if iconFile ~= "-" and iconFile ~= "" then
            nameWithIcon = "[[File:" .. iconFile .. "|20px]] [[" .. name .. "]]"
        end
        output = output .. "<tr>\n"
        output = output .. "<td>" .. nameWithIcon .. "</td>\n"
        output = output .. "<td>" .. tier .. "</td>\n"
        output = output .. "<td>" .. description .. "</td>\n"
        output = output .. "</tr>\n"
    end
    output = output .. "</table>"
    return output
end

--------------------------------------------------------------------------------
-- Function: debugBuildingData
-- Outputs debugging information.
--------------------------------------------------------------------------------
function p.debugBuildingData(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local buildingData, err = loadBuildingData(frame)
    
    if not buildingData then
        return "Error: " .. (err or "Unknown error")
    end
    
    local output = "Debug information for '" .. buildingName .. "':\n\n"
    output = output .. "Raw data from database:\n"
    output = output .. "<pre>"
    
    output = output .. "Column mapping:\n"
    for name, idx in pairs(buildingData.colMap) do
        output = output .. name .. " => " .. idx .. "\n"
    end
    
    output = output .. "\nRows (" .. #buildingData.rows .. " found):\n"
    for i, row in ipairs(buildingData.rows) do
        output = output .. "Row " .. i .. ":\n"
        for j, cell in ipairs(row) do
            local colName = "Unknown"
            for name, idx in pairs(buildingData.colMap) do
                if idx == j then
                    colName = name
                    break
                end
            end
            output = output .. "  " .. colName .. " (" .. j .. "): '" .. cell .. "'\n"
        end
    end
    
    output = output .. "</pre>"
    return output
end


--------------------------------------------------------------------------------
-- Function: formatRecipeList
-- Outputs format for multiple recipe lines
--------------------------------------------------------------------------------
function p.formatRecipeList(frameOrText)
    local text = ""

    -- Handle both direct string or frame.args
    if type(frameOrText) == "string" then
        text = frameOrText
    elseif type(frameOrText) == "table" then
        text = frameOrText.args and frameOrText.args[1] or ""
    end

    if not text or text == "" then
        return ""
    end

    local components = mw.text.split(text, ";")
    local formatted = {}

    for _, raw in ipairs(components) do
        local line = mw.text.trim(raw)
        if line ~= "" then
            local withIcon = p.iconize({ args = { line } })
            table.insert(formatted, withIcon)
        end
    end

    return table.concat(formatted, "<br>")
end











return p