پرش به محتوا

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

از ویکی البرز
جزبدون خلاصۀ ویرایش
جزبدون خلاصۀ ویرایش
خط ۱: خط ۱:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module:AlborzEducation
-- Module:AlborzEducation
-- Wiki Alborz Education Infobox Engine
-- Wiki Alborz Education Infobox Engine (Refactored)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local p = {}
local p = {}


خط ۹: خط ۸:
-- CONFIGURATION
-- 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 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 value == nil then
         return nil
         return nil
     end
     end
     value = mw.text.trim(tostring(value))
     value = mw.text.trim(tostring(value))
     if value == "" then
     if value == "" then
         return nil
         return nil
     end
     end
     return value
     return value
end
end
--------------------------------------------------------------------------------
-- ARGUMENTS
--------------------------------------------------------------------------------


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


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
         local newKey = lookup[key] or key
         result[newKey] = value
         result[newKey] = value
     end
     end
     return result
     return result
end
end


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


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


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- URL NORMALIZER
-- URL & ID HELPERS
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function normalizeUrl(url)
local function normalizeUrl(url)
     url = trim(url)
     url = trim(url)
     if not url then
     if not url then
         return nil
         return nil
     end
     end
     if not url:match("^https?://") then
     if not url:match("^https?://") then
         url = "https://" .. url
         url = "https://" .. url
     end
     end
     return url
     return url
end
end
--------------------------------------------------------------------------------
-- SOCIAL ID EXTRACTOR
--------------------------------------------------------------------------------


local function extractId(value)
local function extractId(value)
     value = trim(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("^https?://","")
     value = value:gsub("^www%.", "")
     value = value:gsub("^www%.","")
     value = value:gsub("^instagram%.com/", "")
     value = value:gsub("^instagram%.com/","")
     value = value:gsub("^t%.me/", "")
     value = value:gsub("^t%.me/","")
     value = value:gsub("^aparat%.com/", "")
     value = value:gsub("^aparat%.com/","")
     value = value:gsub("^@", "")
     value = value:gsub("^@","")
     value = value:gsub("/$", "")
     value = value:gsub("/$","")
 
     return value
     return value
end


-- اگر از قبل قالب یا لینک ویکی باشد، دست نزن
local function isAlreadyFormatted(value)
    return value:match("^{{") or value:match("^%[") or value:match("^%[%[")
end
end


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


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- FIELD RENDERING
-- FORMATTERS (قلب سیستم جدید)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local formatters = {
-- FIELD RENDERING
    website = function(value)
--------------------------------------------------------------------------------
        if isAlreadyFormatted(value) then
 
            return value
local function renderField(fieldName, value)
 
    local field = fields[fieldName]
 
    if not field or not value then
        return nil
    end
 
    ------------------------------------------------------------------------
    -- IMAGE
    ------------------------------------------------------------------------
 
    if field.type == "image" then
 
        local width = "250px"
 
        if fieldName == "logo" then
            width = "120px"
         end
         end
        return mw.html.create("tr")
            :tag("td")
                :attr("colspan", "2")
                :addClass("education-infobox-image")
                :css("text-align", "center")
                :wikitext(imageMarkup(value, width))
                :done()
    end
    ------------------------------------------------------------------------
    -- BOOLEAN
    ------------------------------------------------------------------------
    if field.type == "boolean" then
        value = formatBoolean(value)
    end
    ------------------------------------------------------------------------
    -- WEBSITE
    ------------------------------------------------------------------------
    if fieldName == "website" then
         local url = normalizeUrl(value)
         local url = normalizeUrl(value)
        return string.format("[%s وبگاه رسمی]", url)
    end,


         value = string.format(
    email = function(value)
            "[%s وبگاه رسمی]",
         if isAlreadyFormatted(value) then
            url
            return value
        )
        end
        return string.format("[mailto:%s %s]", value, value)
    end,


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


     elseif field.type == "email" then
     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,


        value = string.format(
    aparat = function(value)
            "[mailto:%s %s]",
         if isAlreadyFormatted(value) then
            value,
            return value
            value
        end
        )
        local id = extractId(value)
 
        return string.format("[https://www.aparat.com/%s صفحهٔ آپارات]", id)
    ------------------------------------------------------------------------
    end,
    -- INSTAGRAM
    ------------------------------------------------------------------------
 
    elseif fieldName == "instagram" then
 
         if not value:match("^{{") then
 
            local id = extractId(value)
 
            value = string.format(
                "[https://instagram.com/%s @%s]",
                id,
                id
            )


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


     ------------------------------------------------------------------------
     url = function(value)
    -- TELEGRAM
         if isAlreadyFormatted(value) then
    ------------------------------------------------------------------------
             return value
 
    elseif fieldName == "telegram" then
 
         if not value:match("^{{") then
 
             local id = extractId(value)
 
            value = string.format(
                "[https://t.me/%s @%s]",
                id,
                id
            )
 
         end
         end
        local url = normalizeUrl(value)
        return string.format("[%s %s]", url, value)
    end,


     ------------------------------------------------------------------------
     boolean = function(value)
    -- APARAT
        return formatBoolean(value)
    ------------------------------------------------------------------------
    end,
 
    elseif fieldName == "aparat" then
 
        if not value:match("^{{") then
 
            local id = extractId(value)
 
            value = string.format(
                "[https://www.aparat.com/%s صفحهٔ آپارات]",
                id
            )


    -- برای فیلدهای آینده کافی است formatter = "social" بگذارید
    social = function(value)
        if isAlreadyFormatted(value) then
            return value
         end
         end
        local id = extractId(value)
        return string.format("[https://%s @%s]", id, id) -- یا منطق اختصاصی
    end,
}


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


     elseif fieldName == "wikipedia" then
     -- اولویت با formatter تعریف‌شده در Data
    local formatterName = (fieldDef and fieldDef.formatter) or fieldName


         if not value:match("^%[%[") then
    -- اگر type مشخص باشد و formatter جداگانه نباشد
             value = string.format("[[%s]]", 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"
         end
         end
    end


     ------------------------------------------------------------------------
     local formatter = formatters[formatterName]
     -- OTHER URL FIELDS
     if formatter then
     ------------------------------------------------------------------------
        return formatter(value)
     end


     elseif field.type == "url" then
     return value
end


        local url = normalizeUrl(value)
--------------------------------------------------------------------------------
 
-- FIELD RENDERING
        value = string.format(
--------------------------------------------------------------------------------
            "[%s %s]",
local function renderField(fieldName, value)
            url,
    local field = fields[fieldName]
            value
    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
     end


     ------------------------------------------------------------------------
     -- فرمت مقدار
     -- NORMAL ROW
     value = formatValue(fieldName, value, field)
    ------------------------------------------------------------------------


    -- ردیف معمولی
     local row = mw.html.create("tr")
     local row = mw.html.create("tr")
 
     row:tag("th"):wikitext(field.label)
     row:tag("th")
     row:tag("td"):wikitext(value)
        :wikitext(field.label)
 
     row:tag("td")
        :wikitext(value)
 
     return row
     return row
end


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


     if logo then
     if logo then
         box:tag("tr")
         box:tag("tr")
             :tag("td")
             :tag("td")
خط ۳۸۵: خط ۲۷۱:
                 :css("padding", "8px")
                 :css("padding", "8px")
                 :wikitext(imageMarkup(logo, "120px"))
                 :wikitext(imageMarkup(logo, "120px"))
     end
     end
    ------------------------------------------------------------------------
    -- TITLE
    ------------------------------------------------------------------------


     if title then
     if title then
         box:tag("tr")
         box:tag("tr")
             :tag("th")
             :tag("th")
خط ۴۰۳: خط ۲۸۳:
                 :css("padding", "8px")
                 :css("padding", "8px")
                 :wikitext(title)
                 :wikitext(title)
     end
     end
end
end


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


     box:tag("tr")
     box:tag("tr")
خط ۴۲۹: خط ۳۰۲:
             :wikitext(imageMarkup(image, "250px"))
             :wikitext(imageMarkup(image, "250px"))


     ------------------------------------------------------------------------
     local caption = getFieldValue(args, "caption")
    -- IMAGE CAPTION
    ------------------------------------------------------------------------
 
     if caption then
     if caption then
         box:tag("tr")
         box:tag("tr")
             :tag("td")
             :tag("td")
خط ۴۴۳: خط ۳۱۲:
                 :css("padding", "4px 8px")
                 :css("padding", "4px 8px")
                 :wikitext(caption)
                 :wikitext(caption)
     end
     end
end


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


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


     for _, field in ipairs(fieldList) do
     for _, field in ipairs(fieldList) do
 
        if not rendered[field] then
        local value = getFieldValue(args, field)
            local value = getFieldValue(args, field)
 
            if value then
        if value then
                local row = renderField(field, value)
            local row = renderField(field, value)
                if row then
 
                    box:node(row)
            if row then
                    rendered[field] = true  -- جلوگیری از رندر تکراری
                box:node(row)
                end
             end
             end
         end
         end
     end
     end
end
end


خط ۴۹۶: خط ۳۵۹:
-- LAYOUT
-- LAYOUT
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
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",
}


local function renderLayout(layoutName, args)
local function renderLayout(layoutName, args)
 
     local layout = layouts[layoutName] or layouts.school
     local layout = layouts[layoutName]
 
    if not layout then
        layout = layouts.school
    end


     local box = mw.html.create("table")
     local box = mw.html.create("table")
         :addClass(INFOBOX_CLASS)
         :addClass(INFOBOX_CLASS)
    local rendered = {}  -- جلوگیری از نمایش دوباره فیلدها


     renderTitle(box, args)
     renderTitle(box, args)
    rendered.logo = true  -- لوگو دیگر در گروه‌ها تکرار نشود
     renderImage(box, args)
     renderImage(box, args)
    rendered.image = true
    rendered.caption = true


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


     return tostring(box)
     return tostring(box)
end
end


خط ۵۲۹: خط ۴۰۲:
-- MAIN
-- MAIN
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.main(frame)
function p.main(frame)
 
     local args = normalizeArgs(getArgs(frame))
     local args = normalizeArgs(
        getArgs(frame)
    )
 
    ------------------------------------------------------------------------
    -- انتخاب نوع چیدمان
    ------------------------------------------------------------------------
 
    local layout = "school"


     local t = (args.type or ""):lower()
     local t = (args.type or ""):lower()
 
     local layout = layoutMap[t] or layoutMap[args.type] or "school"
     if t == "دانشگاه" or t == "university" then
 
        layout = "university"
 
    elseif t == "حوزه علمیه" or t == "seminary" then
 
        layout = "seminary"
 
    elseif t == "آموزشگاه" or t == "training_center" then
 
        layout = "training_center"
 
    elseif t == "پژوهشکده" or t == "research_center" then
 
        layout = "research_center"
 
    elseif t == "کالج" or t == "college" then
 
        layout = "college"
 
    elseif t == "مهدکودک" or t == "kindergarten" then
 
        layout = "kindergarten"
 
    end


     return renderLayout(layout, args)
     return renderLayout(layout, args)
end
end
--------------------------------------------------------------------------------
-- RETURN
--------------------------------------------------------------------------------


return p
return p

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

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

--------------------------------------------------------------------------------
-- Module:AlborzEducation
-- Wiki Alborz Education Infobox Engine (Refactored)
--------------------------------------------------------------------------------
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

--------------------------------------------------------------------------------
-- 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

-- اگر از قبل قالب یا لینک ویکی باشد، دست نزن
local function isAlreadyFormatted(value)
    return value:match("^{{") or value:match("^%[") or value:match("^%[%[")
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

--------------------------------------------------------------------------------
-- 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,

    -- برای فیلدهای آینده کافی است formatter = "social" بگذارید
    social = function(value)
        if isAlreadyFormatted(value) then
            return value
        end
        local id = extractId(value)
        return string.format("[https://%s @%s]", id, id) -- یا منطق اختصاصی
    end,
}

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

    -- اولویت با formatter تعریف‌شده در Data
    local formatterName = (fieldDef and fieldDef.formatter) or fieldName

    -- اگر type مشخص باشد و formatter جداگانه نباشد
    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"
        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)
    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)
    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
--------------------------------------------------------------------------------
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",
}

local function renderLayout(layoutName, args)
    local layout = layouts[layoutName] or layouts.school

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

    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