پرش به محتوا

پودمان:AlborzEducation: تفاوت میان نسخه‌ها

از ویکی البرز
جزبدون خلاصۀ ویرایش
جزبدون خلاصۀ ویرایش
 
(۱۱ نسخهٔ میانیِ ایجادشده توسط همین کاربر نشان داده نشد)
خط ۲: خط ۲:
-- Module:AlborzEducation
-- Module:AlborzEducation
-- Wiki Alborz Education Infobox Engine
-- Wiki Alborz Education Infobox Engine
--
-- پایه: نسخه‌ای که وبگاه و تلگرام+شماره درست کار می‌کند
-- Rendering engine for education information boxes.
-- + اضافه شدن inherit و بهبودهای دیگر
-- Configuration is stored in Module:AlborzEducation/Data
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local p = {}
local p = {}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- CSS CLASSES
-- CONFIGURATION
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local INFOBOX_CLASS = "infobox education-infobox"
local INFOBOX_CLASS = "infobox education-infobox"


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- LOAD DATA
-- LOAD DATA
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 
local data = mw.loadData("Module:AlborzEducation/Data")
local data = mw.loadData(
    "Module:AlborzEducation/Data"
)
 
 
 
--------------------------------------------------------------------------------
-- LOCAL REFERENCES
--------------------------------------------------------------------------------
 
local lookup = data.lookup
local lookup = data.lookup
local fields = data.fieldDefinitions
local fields = data.fieldDefinitions
local layouts = data.layouts
local layouts = data.layouts
local groups = data.groups
local groups = data.groups


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- BASIC UTILITIES
-- BASIC UTILITIES
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function trim(value)
local function trim(value)
 
     if value == nil then
     if not value then
         return nil
         return nil
     end
     end
 
     value = mw.text.trim(tostring(value))
 
    value = tostring(value)
 
     value = mw.text.trim(value)
 
 
     if value == "" then
     if value == "" then
         return nil
         return nil
     end
     end
     return value
     return value
end
end
--------------------------------------------------------------------------------
-- ARGUMENT HANDLING
--------------------------------------------------------------------------------


local function getArgs(frame)
local function getArgs(frame)
     local args = {}
     local args = {}
 
     for k, v in pairs(frame.args) do
 
         k = trim(k)
     for key, value in pairs(frame.args) do
         v = trim(v)
 
         if k and v then
         key = trim(key)
             args[k] = v
         value = trim(value)
 
 
         if key and value then
             args[key] = value
         end
         end
     end
     end
     return args
     return args
end
end
--------------------------------------------------------------------------------
-- NORMALIZE PARAMETERS
--------------------------------------------------------------------------------


local function normalizeArgs(args)
local function normalizeArgs(args)
     local result = {}
     local result = {}
     for key, value in pairs(args) do
     for key, value in pairs(args) do
 
         local newKey = lookup[key] or key
 
         result[newKey] = value
         local internalName = lookup[key]
 
 
        if internalName then
 
            result[internalName] = value
 
 
         else
 
            -- allow direct internal field names
            result[key] = value
 
        end
 
     end
     end
     return result
     return result
end


local function getFieldValue(args, name)
    if args[name] then
        return args[name]
    end
    local def = fields[name]
    if def and def.default then
        return def.default
    end
    return nil
end
end


 
local function isAlreadyFormatted(value)
    return value:match("^{{") or value:match("^%[") or value:match("^%[%[")
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- FIELD VALUE GETTER
-- IMAGE HELPER
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 
local function imageMarkup(file, width)
local function getFieldValue(args, fieldName)
     if not file then
 
         return nil
 
    local value = args[fieldName]
 
 
     if value then
         return value
     end
     end
    width = width or "250px"
    return string.format("[[پرونده:%s|%s|center]]", file, width)
end


    local definition = fields[fieldName]
    if definition and definition.default then
        return definition.default
    end
    return nil
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- FIELD RENDERING
-- URL & ID HELPERS (همان نسخهٔ قبلی که درست کار می‌کرد)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 
local function normalizeUrl(url)
local function renderField(fieldName, value)
     url = trim(url)
 
     if not url then
 
     local field = fields[fieldName]
 
 
     if not field then
         return nil
         return nil
     end
     end
    if not url:match("^https?://") then
        url = "https://" .. url
    end
    return url
end


 
local function extractId(value)
 
    value = trim(value)
     if not value then
     if not value then
         return nil
         return nil
     end
     end
    value = value:gsub("^https?://", "")
    value = value:gsub("^www%.", "")
    value = value:gsub("^instagram%.com/", "")
    value = value:gsub("^t%.me/", "")
    value = value:gsub("^aparat%.com/", "")
    value = value:gsub("^@", "")
    value = value:gsub("/$", "")
    return value
end


--------------------------------------------------------------------------------
-- BOOLEAN
--------------------------------------------------------------------------------
local function formatBoolean(value)
    local yes = {
        ["بله"] = true, ["بلی"] = true,
        ["yes"] = true, ["Yes"] = true, ["YES"] = true,
        ["true"] = true, ["True"] = true, ["TRUE"] = true,
        ["1"] = true
    }
    if yes[tostring(value)] then
        return "دارد"
    end
    return "ندارد"
end


--------------------------------------------------------------------------------
-- LIST HELPER (اصلاح‌شده – فقط این بخش تغییر کرده)
--------------------------------------------------------------------------------
local function formatList(value)
    if isAlreadyFormatted(value) then
        return value
    end


     local label = field.label
     -- اول ویرگول فارسی را به انگلیسی تبدیل کن تا gsplit درست کار کند
     local fieldType = field.type
     value = value:gsub("،", ",")


    local items = {}
    for item in mw.text.gsplit(value, ",") do
        item = trim(item)
        if item and item ~= "" then
            table.insert(items, "* " .. item)
        end
    end


    if #items == 0 then
        return value
    end
    return table.concat(items, "\n")
end
--------------------------------------------------------------------------------
-- FORMATTERS (وبگاه و تلگرام دقیقاً مثل نسخهٔ قبلی)
--------------------------------------------------------------------------------
local formatters = {
    website = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local url = normalizeUrl(value)
        return string.format("[%s وبگاه رسمی]", url)
    end,


     ----------------------------------------------------------------------------
     email = function(value)
    -- Image fields
        if isAlreadyFormatted(value) then
     ----------------------------------------------------------------------------
            return value
        end
        return string.format("[mailto:%s %s]", value, value)
     end,


     if fieldType == "image" then
     instagram = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local id = extractId(value)
        return string.format("[https://instagram.com/%s @%s]", id, id)
    end,


    telegram = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local id = extractId(value)
        return string.format("[https://t.me/%s @%s]", id, id)
    end,


         local row = mw.html.create("tr")
    aparat = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
         local id = extractId(value)
        return string.format("[https://www.aparat.com/%s صفحهٔ آپارات]", id)
    end,


    wikipedia = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[[%s]]", value)
    end,


        row
    url = function(value)
            :tag("td")
        if isAlreadyFormatted(value) then
            :attr("colspan", "2")
             return value
             :addClass("education-infobox-image")
        end
            :wikitext(
        local url = normalizeUrl(value)
                string.format(
        return string.format("[%s %s]", url, value)
                    "[[پرونده:%s|250px|center]]",
    end,
                    value
                )
            )
            :done()


    boolean = function(value)
        return formatBoolean(value)
    end,


         return row
    list = function(value)
         return formatList(value)
    end,


     end
     person = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[[%s]]", value)
    end,


    location = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[[%s]]", value)
    end,


    coordinates = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return value
    end,
}


     ----------------------------------------------------------------------------
local function formatValue(fieldName, value, fieldDef)
    -- Boolean fields
     if not value then
     ----------------------------------------------------------------------------
        return nil
     end


     if fieldType == "boolean" then
     local formatterName = (fieldDef and fieldDef.formatter) or fieldName
 
 
        if value == "yes"
            or value == "بله"
            or value == "true"
            or value == "1"
        then
 
            value = "دارد"
 
        else
 
            value = "ندارد"


    if fieldDef and fieldDef.type and not formatters[formatterName] then
        if fieldDef.type == "boolean" then
            formatterName = "boolean"
        elseif fieldDef.type == "email" then
            formatterName = "email"
        elseif fieldDef.type == "url" then
            formatterName = "url"
        elseif fieldDef.type == "list" then
            formatterName = "list"
         end
         end
     end
     end


 
     local formatter = formatters[formatterName]
 
     if formatter then
     ----------------------------------------------------------------------------
         return formatter(value)
    -- URL fields
    ----------------------------------------------------------------------------
 
     if fieldType == "url" then
 
 
         value = string.format(
            "[%s %s]",
            value,
            value
        )
 
 
     end
     end


    return value
end


    ----------------------------------------------------------------------------
    -- Normal text fields
    ----------------------------------------------------------------------------
    local row = mw.html.create("tr")
    row
        :tag("th")
        :wikitext(label)
        :done()
    row
        :tag("td")
        :wikitext(value)
        :done()
    return row
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- INFOBOX TITLE
-- FIELD RENDERING
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 
local function renderField(fieldName, value)
local function renderTitle(args)
     local field = fields[fieldName]
 
     if not field or not value then
 
     local title = getFieldValue(
        args,
        "name"
    )
 
 
     if not title then
         return nil
         return nil
     end
     end


    if field.type == "image" then
        local width = (fieldName == "logo") and "120px" or "250px"
        return mw.html.create("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-image")
                :css("text-align", "center")
                :wikitext(imageMarkup(value, width))
                :done()
    end


    value = formatValue(fieldName, value, field)


     local row = mw.html.create("tr")
     local row = mw.html.create("tr")
 
     row:tag("th"):wikitext(field.label or fieldName)
 
    row:tag("td"):wikitext(value)
     row
        :tag("th")
        :attr("colspan", "2")
        :addClass("education-infobox-title")
        :wikitext(title)
        :done()
 
 
 
     return row
     return row
end
end


--------------------------------------------------------------------------------
-- TITLE & LOGO
--------------------------------------------------------------------------------
local function renderTitle(box, args)
    local logo = getFieldValue(args, "logo")
    local title = getFieldValue(args, "fullname") or getFieldValue(args, "name")


    if logo then
        box:tag("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-logo")
                :css("text-align", "center")
                :css("padding", "8px")
                :wikitext(imageMarkup(logo, "120px"))
    end


    if title then
        box:tag("tr")
            :tag("th")
                :attr("colspan", "2")
                :addClass("education-infobox-title")
                :css("text-align", "center")
                :css("font-size", "120%")
                :css("font-weight", "bold")
                :css("padding", "8px")
                :wikitext(title)
    end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- IMAGE RENDERING
-- MAIN IMAGE + CAPTION
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 
local function renderImage(box, args)
local function renderImage(args)
     local image = getFieldValue(args, "image")
 
 
     local image = getFieldValue(
        args,
        "image"
    )
 
 
     if not image then
     if not image then
         return nil
         return
     end
     end


 
     box:tag("tr")
 
     local row = mw.html.create("tr")
 
 
    row
         :tag("td")
         :tag("td")
        :attr("colspan", "2")
            :attr("colspan", "2")
        :addClass("education-infobox-image")
            :addClass("education-infobox-image")
        :wikitext(
            :css("text-align", "center")
             string.format(
             :wikitext(imageMarkup(image, "250px"))
                "[[پرونده:%s|250px|center]]",
                image
            )
        )
        :done()
 
 
 
    return row


    local caption = getFieldValue(args, "caption")
    if caption then
        box:tag("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-caption")
                :css("text-align", "center")
                :css("font-size", "90%")
                :css("padding", "4px 8px")
                :wikitext(caption)
    end
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- GROUP RENDERING
-- GROUP
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 
local function renderGroup(box, groupName, fieldList, args, rendered)
local function renderGroup(groupName, fieldList, args)
    local hasData = false
 
    for _, field in ipairs(fieldList) do
        if getFieldValue(args, field) and not rendered[field] then
            hasData = true
            break
        end
    end
    if not hasData then
        return
    end


     local group = groups[groupName]
     local group = groups[groupName]
 
     if group then
 
         box:tag("tr")
     if not group then
            :tag("th")
         return nil
                :attr("colspan", "2")
                :addClass("education-infobox-group")
                :css("text-align", "center")
                :css("font-weight", "bold")
                :css("padding", "6px")
                :wikitext((group.icon or "") .. " " .. (group.label or groupName))
     end
     end


 
     for _, field in ipairs(fieldList) do
 
        if not rendered[field] then
    local sectionTable = mw.html.create("table")
            local value = getFieldValue(args, field)
        :addClass("infobox-section")
             if value then
 
                local row = renderField(field, value)
 
                if row then
 
                    box:node(row)
                    rendered[field] = true
 
                end
    local hasValue = false
 
 
 
    ----------------------------------------------------------------------------
    -- Fields
    ----------------------------------------------------------------------------
 
     for _, fieldName in ipairs(fieldList) do
 
 
        local value = getFieldValue(
            args,
             fieldName
        )
 
 
 
        if value then
 
 
            local row = renderField(
                fieldName,
                value
            )
 
 
 
            if row then
 
          sectionTable
              :node(row)
 
                hasValue = true
 
             end
             end
         end
         end
    end
end


--------------------------------------------------------------------------------
-- LAYOUT MAP
--------------------------------------------------------------------------------
local layoutMap = {
    ["دانشگاه"]          = "university",
    ["university"]      = "university",
    ["حوزه علمیه"]      = "seminary",
    ["seminary"]        = "seminary",
    ["آموزشگاه"]        = "training_center",
    ["training_center"]  = "training_center",
    ["پژوهشکده"]        = "research_center",
    ["research_center"]  = "research_center",
    ["کالج"]            = "college",
    ["college"]          = "college",
    ["مهدکودک"]          = "kindergarten",
    ["kindergarten"]    = "kindergarten",
    ["مدرسه"]            = "school",
    ["school"]          = "school",
}


     end
--------------------------------------------------------------------------------
-- RESOLVE INHERIT
--------------------------------------------------------------------------------
local function resolveLayout(layoutName)
     local seen = {}
    local current = layoutName


    while current do
        if seen[current] then
            return layouts.school or {}
        end
        seen[current] = true


        local layout = layouts[current]
        if not layout then
            return layouts.school or {}
        end


 
        if layout.inherit then
    if not hasValue then
            current = layout.inherit
         return nil
         else
            return layout
        end
     end
     end


    return layouts.school or {}
end


    ----------------------------------------------------------------------------
    -- Group Header
    ----------------------------------------------------------------------------
    local header = sectionTable
        :tag("tr")
    header
        :tag("th")
        :attr("colspan", "2")
        :wikitext(
            (group.icon or "")
            .. " "
            .. group.label
        )
        :done()
    return sectionTable
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- LAYOUT RENDERING
-- RENDER LAYOUT
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function renderLayout(layoutName, args)
local function renderLayout(layoutName, args)
 
     local layout = resolveLayout(layoutName)
 
     local layout = layouts[layoutName]
 
 
    if not layout then
        return nil
    end
 
 


     local box = mw.html.create("table")
     local box = mw.html.create("table")
         :addClass(INFOBOX_CLASS)
         :addClass(INFOBOX_CLASS)
        :addClass("education-" .. layoutName)


    local rendered = {}


    renderTitle(box, args)
    rendered.logo = true


     ----------------------------------------------------------------------------
     renderImage(box, args)
    -- Header
     rendered.image = true
    ----------------------------------------------------------------------------
     rendered.caption = true
 
local title = renderTitle(args)
 
if title then
 
     box
        :node(title)
 
end
 
 
 
local image = renderImage(args)
 
if image then
 
     box
        :node(image)
 
end
 
 
 
    ----------------------------------------------------------------------------
    -- Content Groups
    ----------------------------------------------------------------------------


     for _, section in ipairs(layout) do
     for _, section in ipairs(layout) do
 
         renderGroup(box, section.group, section.fields, args, rendered)
 
         local groupHtml = renderGroup(
            section.group,
            section.fields,
            args
        )
 
 
 
        if groupHtml then
 
            box
                :node(groupHtml)
 
        end
 
 
     end
     end


 
     return tostring(box)
 
 
     return box
 
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- MAIN FUNCTION
-- MAIN
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.main(frame)
function p.main(frame)
    local args = normalizeArgs(getArgs(frame))


    local t = (args.type or ""):lower()
    local layout = layoutMap[t] or layoutMap[args.type] or "school"


     local rawArgs = getArgs(frame)
     return renderLayout(layout, args)
 
 
 
    local args = normalizeArgs(
        rawArgs
    )
 
 
 
    local layoutName = "school"
 
 
 
    local result = renderLayout(
        layoutName,
        args
    )
 
 
 
    if result then
 
        return tostring(result)
 
    end
 
 
 
    return ""
 
end
end
--------------------------------------------------------------------------------
-- RETURN MODULE
--------------------------------------------------------------------------------


return p
return p

نسخهٔ کنونی تا ‏۲۹ ژوئیهٔ ۲۰۲۶، ساعت ۱۲:۵۳

توضیحات این پودمان می‌تواند در پودمان:AlborzEducation/توضیحات قرار گیرد.

--------------------------------------------------------------------------------
-- Module:AlborzEducation
-- Wiki Alborz Education Infobox Engine
-- پایه: نسخه‌ای که وبگاه و تلگرام+شماره درست کار می‌کند
-- + اضافه شدن inherit و بهبودهای دیگر
--------------------------------------------------------------------------------
local p = {}

--------------------------------------------------------------------------------
-- CONFIGURATION
--------------------------------------------------------------------------------
local INFOBOX_CLASS = "infobox education-infobox"

--------------------------------------------------------------------------------
-- LOAD DATA
--------------------------------------------------------------------------------
local data = mw.loadData("Module:AlborzEducation/Data")
local lookup = data.lookup
local fields = data.fieldDefinitions
local layouts = data.layouts
local groups = data.groups

--------------------------------------------------------------------------------
-- BASIC UTILITIES
--------------------------------------------------------------------------------
local function trim(value)
    if value == nil then
        return nil
    end
    value = mw.text.trim(tostring(value))
    if value == "" then
        return nil
    end
    return value
end

local function getArgs(frame)
    local args = {}
    for k, v in pairs(frame.args) do
        k = trim(k)
        v = trim(v)
        if k and v then
            args[k] = v
        end
    end
    return args
end

local function normalizeArgs(args)
    local result = {}
    for key, value in pairs(args) do
        local newKey = lookup[key] or key
        result[newKey] = value
    end
    return result
end

local function getFieldValue(args, name)
    if args[name] then
        return args[name]
    end
    local def = fields[name]
    if def and def.default then
        return def.default
    end
    return nil
end

local function isAlreadyFormatted(value)
    return value:match("^{{") or value:match("^%[") or value:match("^%[%[")
end

--------------------------------------------------------------------------------
-- IMAGE HELPER
--------------------------------------------------------------------------------
local function imageMarkup(file, width)
    if not file then
        return nil
    end
    width = width or "250px"
    return string.format("[[پرونده:%s|%s|center]]", file, width)
end

--------------------------------------------------------------------------------
-- URL & ID HELPERS (همان نسخهٔ قبلی که درست کار می‌کرد)
--------------------------------------------------------------------------------
local function normalizeUrl(url)
    url = trim(url)
    if not url then
        return nil
    end
    if not url:match("^https?://") then
        url = "https://" .. url
    end
    return url
end

local function extractId(value)
    value = trim(value)
    if not value then
        return nil
    end
    value = value:gsub("^https?://", "")
    value = value:gsub("^www%.", "")
    value = value:gsub("^instagram%.com/", "")
    value = value:gsub("^t%.me/", "")
    value = value:gsub("^aparat%.com/", "")
    value = value:gsub("^@", "")
    value = value:gsub("/$", "")
    return value
end

--------------------------------------------------------------------------------
-- BOOLEAN
--------------------------------------------------------------------------------
local function formatBoolean(value)
    local yes = {
        ["بله"] = true, ["بلی"] = true,
        ["yes"] = true, ["Yes"] = true, ["YES"] = true,
        ["true"] = true, ["True"] = true, ["TRUE"] = true,
        ["1"] = true
    }
    if yes[tostring(value)] then
        return "دارد"
    end
    return "ندارد"
end

--------------------------------------------------------------------------------
-- LIST HELPER (اصلاح‌شده – فقط این بخش تغییر کرده)
--------------------------------------------------------------------------------
local function formatList(value)
    if isAlreadyFormatted(value) then
        return value
    end

    -- اول ویرگول فارسی را به انگلیسی تبدیل کن تا gsplit درست کار کند
    value = value:gsub("،", ",")

    local items = {}
    for item in mw.text.gsplit(value, ",") do
        item = trim(item)
        if item and item ~= "" then
            table.insert(items, "* " .. item)
        end
    end

    if #items == 0 then
        return value
    end
    return table.concat(items, "\n")
end
--------------------------------------------------------------------------------
-- FORMATTERS (وبگاه و تلگرام دقیقاً مثل نسخهٔ قبلی)
--------------------------------------------------------------------------------
local formatters = {
    website = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local url = normalizeUrl(value)
        return string.format("[%s وبگاه رسمی]", url)
    end,

    email = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[mailto:%s %s]", value, value)
    end,

    instagram = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local id = extractId(value)
        return string.format("[https://instagram.com/%s @%s]", id, id)
    end,

    telegram = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local id = extractId(value)
        return string.format("[https://t.me/%s @%s]", id, id)
    end,

    aparat = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local id = extractId(value)
        return string.format("[https://www.aparat.com/%s صفحهٔ آپارات]", id)
    end,

    wikipedia = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[[%s]]", value)
    end,

    url = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local url = normalizeUrl(value)
        return string.format("[%s %s]", url, value)
    end,

    boolean = function(value)
        return formatBoolean(value)
    end,

    list = function(value)
        return formatList(value)
    end,

    person = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[[%s]]", value)
    end,

    location = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return string.format("[[%s]]", value)
    end,

    coordinates = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        return value
    end,
}

local function formatValue(fieldName, value, fieldDef)
    if not value then
        return nil
    end

    local formatterName = (fieldDef and fieldDef.formatter) or fieldName

    if fieldDef and fieldDef.type and not formatters[formatterName] then
        if fieldDef.type == "boolean" then
            formatterName = "boolean"
        elseif fieldDef.type == "email" then
            formatterName = "email"
        elseif fieldDef.type == "url" then
            formatterName = "url"
        elseif fieldDef.type == "list" then
            formatterName = "list"
        end
    end

    local formatter = formatters[formatterName]
    if formatter then
        return formatter(value)
    end

    return value
end

--------------------------------------------------------------------------------
-- FIELD RENDERING
--------------------------------------------------------------------------------
local function renderField(fieldName, value)
    local field = fields[fieldName]
    if not field or not value then
        return nil
    end

    if field.type == "image" then
        local width = (fieldName == "logo") and "120px" or "250px"
        return mw.html.create("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-image")
                :css("text-align", "center")
                :wikitext(imageMarkup(value, width))
                :done()
    end

    value = formatValue(fieldName, value, field)

    local row = mw.html.create("tr")
    row:tag("th"):wikitext(field.label or fieldName)
    row:tag("td"):wikitext(value)
    return row
end

--------------------------------------------------------------------------------
-- TITLE & LOGO
--------------------------------------------------------------------------------
local function renderTitle(box, args)
    local logo = getFieldValue(args, "logo")
    local title = getFieldValue(args, "fullname") or getFieldValue(args, "name")

    if logo then
        box:tag("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-logo")
                :css("text-align", "center")
                :css("padding", "8px")
                :wikitext(imageMarkup(logo, "120px"))
    end

    if title then
        box:tag("tr")
            :tag("th")
                :attr("colspan", "2")
                :addClass("education-infobox-title")
                :css("text-align", "center")
                :css("font-size", "120%")
                :css("font-weight", "bold")
                :css("padding", "8px")
                :wikitext(title)
    end
end

--------------------------------------------------------------------------------
-- MAIN IMAGE + CAPTION
--------------------------------------------------------------------------------
local function renderImage(box, args)
    local image = getFieldValue(args, "image")
    if not image then
        return
    end

    box:tag("tr")
        :tag("td")
            :attr("colspan", "2")
            :addClass("education-infobox-image")
            :css("text-align", "center")
            :wikitext(imageMarkup(image, "250px"))

    local caption = getFieldValue(args, "caption")
    if caption then
        box:tag("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-caption")
                :css("text-align", "center")
                :css("font-size", "90%")
                :css("padding", "4px 8px")
                :wikitext(caption)
    end
end

--------------------------------------------------------------------------------
-- GROUP
--------------------------------------------------------------------------------
local function renderGroup(box, groupName, fieldList, args, rendered)
    local hasData = false
    for _, field in ipairs(fieldList) do
        if getFieldValue(args, field) and not rendered[field] then
            hasData = true
            break
        end
    end
    if not hasData then
        return
    end

    local group = groups[groupName]
    if group then
        box:tag("tr")
            :tag("th")
                :attr("colspan", "2")
                :addClass("education-infobox-group")
                :css("text-align", "center")
                :css("font-weight", "bold")
                :css("padding", "6px")
                :wikitext((group.icon or "") .. " " .. (group.label or groupName))
    end

    for _, field in ipairs(fieldList) do
        if not rendered[field] then
            local value = getFieldValue(args, field)
            if value then
                local row = renderField(field, value)
                if row then
                    box:node(row)
                    rendered[field] = true
                end
            end
        end
    end
end

--------------------------------------------------------------------------------
-- LAYOUT MAP
--------------------------------------------------------------------------------
local layoutMap = {
    ["دانشگاه"]          = "university",
    ["university"]       = "university",
    ["حوزه علمیه"]       = "seminary",
    ["seminary"]         = "seminary",
    ["آموزشگاه"]         = "training_center",
    ["training_center"]  = "training_center",
    ["پژوهشکده"]         = "research_center",
    ["research_center"]  = "research_center",
    ["کالج"]             = "college",
    ["college"]          = "college",
    ["مهدکودک"]          = "kindergarten",
    ["kindergarten"]     = "kindergarten",
    ["مدرسه"]            = "school",
    ["school"]           = "school",
}

--------------------------------------------------------------------------------
-- RESOLVE INHERIT
--------------------------------------------------------------------------------
local function resolveLayout(layoutName)
    local seen = {}
    local current = layoutName

    while current do
        if seen[current] then
            return layouts.school or {}
        end
        seen[current] = true

        local layout = layouts[current]
        if not layout then
            return layouts.school or {}
        end

        if layout.inherit then
            current = layout.inherit
        else
            return layout
        end
    end

    return layouts.school or {}
end

--------------------------------------------------------------------------------
-- RENDER LAYOUT
--------------------------------------------------------------------------------
local function renderLayout(layoutName, args)
    local layout = resolveLayout(layoutName)

    local box = mw.html.create("table")
        :addClass(INFOBOX_CLASS)
        :addClass("education-" .. layoutName)

    local rendered = {}

    renderTitle(box, args)
    rendered.logo = true

    renderImage(box, args)
    rendered.image = true
    rendered.caption = true

    for _, section in ipairs(layout) do
        renderGroup(box, section.group, section.fields, args, rendered)
    end

    return tostring(box)
end

--------------------------------------------------------------------------------
-- MAIN
--------------------------------------------------------------------------------
function p.main(frame)
    local args = normalizeArgs(getArgs(frame))

    local t = (args.type or ""):lower()
    local layout = layoutMap[t] or layoutMap[args.type] or "school"

    return renderLayout(layout, args)
end

return p