پرش به محتوا

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

از ویکی البرز
جزبدون خلاصۀ ویرایش
جزبدون خلاصۀ ویرایش
 
(یک نسخهٔ میانیِ ایجادشده توسط همین کاربر نشان داده نشد)
خط ۲: خط ۲:
-- Module:AlborzEducation
-- Module:AlborzEducation
-- Wiki Alborz Education Infobox Engine
-- Wiki Alborz Education Infobox Engine
-- پایه: نسخه‌ای که وبگاه و تلگرام+شماره درست کار می‌کند
-- نسخهٔ نهایی با تشخیص هوشمند و مقاوم layout برای آموزشگاه هنری
-- + اضافه شدن inherit و بهبودهای دیگر
-- + پشتیبانی از layout تخصصی آموزشگاه هنری
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local p = {}
local p = {}
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- CONFIGURATION
-- CONFIGURATION
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local INFOBOX_CLASS = "infobox education-infobox"
local INFOBOX_CLASS = "infobox education-infobox"
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- LOAD DATA
-- LOAD DATA
خط ۲۱: خط ۱۷:
local layouts = data.layouts
local layouts = data.layouts
local groups = data.groups
local groups = data.groups
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- BASIC UTILITIES
-- BASIC UTILITIES
خط ۷۱: خط ۶۶:
     return value:match("^{{") or value:match("^%[") or value:match("^%[%[")
     return value:match("^{{") or value:match("^%[") or value:match("^%[%[")
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- IMAGE HELPER
-- IMAGE HELPER
خط ۸۲: خط ۷۶:
     return string.format("[[پرونده:%s|%s|center]]", file, width)
     return string.format("[[پرونده:%s|%s|center]]", file, width)
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- URL & ID HELPERS (همان نسخهٔ قبلی که درست کار می‌کرد)
-- URL & ID HELPERS
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function normalizeUrl(url)
local function normalizeUrl(url)
خط ۱۱۱: خط ۱۰۴:
     return value
     return value
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- BOOLEAN
-- BOOLEAN
خط ۱۲۷: خط ۱۱۹:
     return "ندارد"
     return "ندارد"
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- LIST HELPER (اصلاح‌شده – فقط این بخش تغییر کرده)
-- LIST HELPER
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function formatList(value)
local function formatList(value)
خط ۱۳۵: خط ۱۲۶:
         return value
         return value
     end
     end
    -- اول ویرگول فارسی را به انگلیسی تبدیل کن تا gsplit درست کار کند
     value = value:gsub("،", ",")
     value = value:gsub("،", ",")
     local items = {}
     local items = {}
     for item in mw.text.gsplit(value, ",") do
     for item in mw.text.gsplit(value, ",") do
خط ۱۴۶: خط ۱۳۴:
         end
         end
     end
     end
     if #items == 0 then
     if #items == 0 then
         return value
         return value
خط ۱۵۳: خط ۱۴۰:
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- FORMATTERS (وبگاه و تلگرام دقیقاً مثل نسخهٔ قبلی)
-- FORMATTERS
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local formatters = {
local formatters = {
خط ۱۶۳: خط ۱۵۰:
         return string.format("[%s وبگاه رسمی]", url)
         return string.format("[%s وبگاه رسمی]", url)
     end,
     end,
     email = function(value)
     email = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۱۷۰: خط ۱۵۶:
         return string.format("[mailto:%s %s]", value, value)
         return string.format("[mailto:%s %s]", value, value)
     end,
     end,
     instagram = function(value)
     instagram = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۱۷۸: خط ۱۶۳:
         return string.format("[https://instagram.com/%s @%s]", id, id)
         return string.format("[https://instagram.com/%s @%s]", id, id)
     end,
     end,
     telegram = function(value)
     telegram = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۱۸۶: خط ۱۷۰:
         return string.format("[https://t.me/%s @%s]", id, id)
         return string.format("[https://t.me/%s @%s]", id, id)
     end,
     end,
     aparat = function(value)
     aparat = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۱۹۴: خط ۱۷۷:
         return string.format("[https://www.aparat.com/%s صفحهٔ آپارات]", id)
         return string.format("[https://www.aparat.com/%s صفحهٔ آپارات]", id)
     end,
     end,
     wikipedia = function(value)
     wikipedia = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۲۰۱: خط ۱۸۳:
         return string.format("[[%s]]", value)
         return string.format("[[%s]]", value)
     end,
     end,
     url = function(value)
     url = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۲۰۹: خط ۱۹۰:
         return string.format("[%s %s]", url, value)
         return string.format("[%s %s]", url, value)
     end,
     end,
     boolean = function(value)
     boolean = function(value)
         return formatBoolean(value)
         return formatBoolean(value)
     end,
     end,
     list = function(value)
     list = function(value)
         return formatList(value)
         return formatList(value)
     end,
     end,
     person = function(value)
     person = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۲۲۴: خط ۲۰۲:
         return string.format("[[%s]]", value)
         return string.format("[[%s]]", value)
     end,
     end,
     location = function(value)
     location = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۲۳۱: خط ۲۰۸:
         return string.format("[[%s]]", value)
         return string.format("[[%s]]", value)
     end,
     end,
     coordinates = function(value)
     coordinates = function(value)
         if isAlreadyFormatted(value) then
         if isAlreadyFormatted(value) then
خط ۲۴۴: خط ۲۲۰:
         return nil
         return nil
     end
     end
     local formatterName = (fieldDef and fieldDef.formatter) or fieldName
     local formatterName = (fieldDef and fieldDef.formatter) or fieldName
     if fieldDef and fieldDef.type and not formatters[formatterName] then
     if fieldDef and fieldDef.type and not formatters[formatterName] then
         if fieldDef.type == "boolean" then
         if fieldDef.type == "boolean" then
خط ۲۵۸: خط ۲۳۲:
         end
         end
     end
     end
     local formatter = formatters[formatterName]
     local formatter = formatters[formatterName]
     if formatter then
     if formatter then
         return formatter(value)
         return formatter(value)
     end
     end
     return value
     return value
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- FIELD RENDERING
-- FIELD RENDERING
خط ۲۷۵: خط ۲۴۶:
         return nil
         return nil
     end
     end
     if field.type == "image" then
     if field.type == "image" then
         local width = (fieldName == "logo") and "120px" or "250px"
         local width = (fieldName == "logo") and "120px" or "250px"
خط ۲۸۶: خط ۲۵۶:
                 :done()
                 :done()
     end
     end
     value = formatValue(fieldName, value, field)
     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("th"):wikitext(field.label or fieldName)
خط ۲۹۴: خط ۲۶۲:
     return row
     return row
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- TITLE & LOGO
-- TITLE & LOGO
خط ۳۰۱: خط ۲۶۸:
     local logo = getFieldValue(args, "logo")
     local logo = getFieldValue(args, "logo")
     local title = getFieldValue(args, "fullname") or getFieldValue(args, "name")
     local title = getFieldValue(args, "fullname") or getFieldValue(args, "name")
     if logo then
     if logo then
         box:tag("tr")
         box:tag("tr")
خط ۳۱۱: خط ۲۷۷:
                 :wikitext(imageMarkup(logo, "120px"))
                 :wikitext(imageMarkup(logo, "120px"))
     end
     end
     if title then
     if title then
         box:tag("tr")
         box:tag("tr")
خط ۳۲۴: خط ۲۸۹:
     end
     end
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- MAIN IMAGE + CAPTION
-- MAIN IMAGE + CAPTION
خط ۳۳۳: خط ۲۹۷:
         return
         return
     end
     end
     box:tag("tr")
     box:tag("tr")
         :tag("td")
         :tag("td")
خط ۳۴۰: خط ۳۰۳:
             :css("text-align", "center")
             :css("text-align", "center")
             :wikitext(imageMarkup(image, "250px"))
             :wikitext(imageMarkup(image, "250px"))
     local caption = getFieldValue(args, "caption")
     local caption = getFieldValue(args, "caption")
     if caption then
     if caption then
خط ۳۵۳: خط ۳۱۵:
     end
     end
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- GROUP
-- GROUP
خط ۳۶۸: خط ۳۲۹:
         return
         return
     end
     end
     local group = groups[groupName]
     local group = groups[groupName]
     if group then
     if group then
خط ۳۸۰: خط ۳۴۰:
                 :wikitext((group.icon or "") .. " " .. (group.label or groupName))
                 :wikitext((group.icon or "") .. " " .. (group.label or groupName))
     end
     end
     for _, field in ipairs(fieldList) do
     for _, field in ipairs(fieldList) do
         if not rendered[field] then
         if not rendered[field] then
خط ۳۹۴: خط ۳۵۳:
     end
     end
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- LAYOUT MAP
-- تشخیص هوشمند layout (نسخهٔ قوی و مقاوم)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local layoutMap = {
local function detectLayout(typeValue)
    ["دانشگاه"]         = "university",
    if not typeValue or typeValue == "" then
    ["university"]       = "university",
        return "school"
    ["حوزه علمیه"]      = "seminary",
    end
    ["seminary"]         = "seminary",
 
    ["آموزشگاه"]         = "training_center",
    -- نرمال‌سازی قوی
    ["training_center"] = "training_center",
    local t = mw.ustring.lower(typeValue)
    ["آموزشگاه هنری"]    = "art_school",
    t = t:gsub("‌", "")          -- حذف نیم‌فاصله
    ["آموزشگاه هنر"]    = "art_school",
    t = t:gsub("%s+", "")        -- حذف همه فاصله‌ها
    ["هنری"]             = "art_school",
    t = t:gsub("ي", "ی")          -- یکسان‌سازی ی
    ["art_school"]      = "art_school",
    t = t:gsub("ك", "ک")          -- یکسان‌سازی ک
    ["پژوهشکده"]         = "research_center",
 
    ["research_center"] = "research_center",
    -- مطابقت دقیق بعد از نرمال‌سازی
    ["کالج"]             = "college",
    local normalizedMap = {
    ["college"]         = "college",
        ["دانشگاه"]         = "university",
    ["مهدکودک"]         = "kindergarten",
        ["university"]     = "university",
    ["kindergarten"]     = "kindergarten",
        ["حوزهعلمیه"]      = "seminary",
    ["مدرسه"]           = "school",
        ["seminary"]       = "seminary",
    ["school"]           = "school",
        ["آموزشگاه"]       = "training_center",
}
        ["training_center"] = "training_center",
        ["آموزشگاههنری"]    = "art_school",
        ["آموزشگاههنر"]    = "art_school",
        ["هنری"]           = "art_school",
        ["art_school"]      = "art_school",
        ["آکادمیهنری"]      = "art_school",
        ["آکادمیهنر"]      = "art_school",
        ["پژوهشکده"]       = "research_center",
        ["research_center"] = "research_center",
        ["کالج"]           = "college",
        ["college"]         = "college",
        ["مهدکودک"]         = "kindergarten",
        ["kindergarten"]   = "kindergarten",
        ["مدرسه"]           = "school",
        ["school"]         = "school",
    }
 
    if normalizedMap[t] then
        return normalizedMap[t]
    end
 
    -- بررسی کلمات کلیدی
    if t:find("هنری") or t:find("هنر") or t:find("آکادمی") then
        return "art_school"
    end
    if t:find("دانشگاه") then
        return "university"
    end
    if t:find("حوزه") or t:find("علمیه") then
        return "seminary"
    end
    if t:find("پژوهش") then
        return "research_center"
    end
    if t:find("کالج") then
        return "college"
    end
    if t:find("مهد") or t:find("کودک") then
        return "kindergarten"
    end
    if t:find("آموزشگاه") or t:find("مرکزآموزشی") or t:find("آموزشی") then
        return "training_center"
    end
    if t:find("مدرسه") then
        return "school"
    end


    return "school"
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- RESOLVE INHERIT
-- RESOLVE INHERIT
خط ۴۲۵: خط ۴۳۰:
     local seen = {}
     local seen = {}
     local current = layoutName
     local current = layoutName
     while current do
     while current do
         if seen[current] then
         if seen[current] then
خط ۴۳۱: خط ۴۳۵:
         end
         end
         seen[current] = true
         seen[current] = true
         local layout = layouts[current]
         local layout = layouts[current]
         if not layout then
         if not layout then
             return layouts.school or {}
             return layouts.school or {}
         end
         end
         if layout.inherit then
         if layout.inherit then
             current = layout.inherit
             current = layout.inherit
خط ۴۴۳: خط ۴۴۵:
         end
         end
     end
     end
     return layouts.school or {}
     return layouts.school or {}
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- RENDER LAYOUT
-- RENDER LAYOUT
خط ۴۵۲: خط ۴۵۲:
local function renderLayout(layoutName, args)
local function renderLayout(layoutName, args)
     local layout = resolveLayout(layoutName)
     local layout = resolveLayout(layoutName)
     local box = mw.html.create("table")
     local box = mw.html.create("table")
         :addClass(INFOBOX_CLASS)
         :addClass(INFOBOX_CLASS)
         :addClass("education-" .. layoutName)
         :addClass("education-" .. layoutName)
     local rendered = {}
     local rendered = {}
     renderTitle(box, args)
     renderTitle(box, args)
     rendered.logo = true
     rendered.logo = true
     renderImage(box, args)
     renderImage(box, args)
     rendered.image = true
     rendered.image = true
     rendered.caption = 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, rendered)
     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 = detectLayout(args.type)
     local t = trim(args.type or "")
    local normalizedType = t and mw.ustring.lower(t) or ""
    local layout = layoutMap[normalizedType] or layoutMap[t] or "school"
 
     return renderLayout(layout, args)
     return renderLayout(layout, args)
end
end


return p
return p

نسخهٔ کنونی تا ‏۲۵ اوت ۲۰۲۶، ساعت ۲۱:۵۴

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

--------------------------------------------------------------------------------
-- Module:AlborzEducation
-- Wiki Alborz Education Infobox Engine
-- نسخهٔ نهایی با تشخیص هوشمند و مقاوم layout برای آموزشگاه هنری
--------------------------------------------------------------------------------
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
    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 (نسخهٔ قوی و مقاوم)
--------------------------------------------------------------------------------
local function detectLayout(typeValue)
    if not typeValue or typeValue == "" then
        return "school"
    end

    -- نرمال‌سازی قوی
    local t = mw.ustring.lower(typeValue)
    t = t:gsub("‌", "")          -- حذف نیم‌فاصله
    t = t:gsub("%s+", "")         -- حذف همه فاصله‌ها
    t = t:gsub("ي", "ی")          -- یکسان‌سازی ی
    t = t:gsub("ك", "ک")          -- یکسان‌سازی ک

    -- مطابقت دقیق بعد از نرمال‌سازی
    local normalizedMap = {
        ["دانشگاه"]         = "university",
        ["university"]      = "university",
        ["حوزهعلمیه"]       = "seminary",
        ["seminary"]        = "seminary",
        ["آموزشگاه"]        = "training_center",
        ["training_center"] = "training_center",
        ["آموزشگاههنری"]    = "art_school",
        ["آموزشگاههنر"]     = "art_school",
        ["هنری"]            = "art_school",
        ["art_school"]      = "art_school",
        ["آکادمیهنری"]      = "art_school",
        ["آکادمیهنر"]       = "art_school",
        ["پژوهشکده"]        = "research_center",
        ["research_center"] = "research_center",
        ["کالج"]            = "college",
        ["college"]         = "college",
        ["مهدکودک"]         = "kindergarten",
        ["kindergarten"]    = "kindergarten",
        ["مدرسه"]           = "school",
        ["school"]          = "school",
    }

    if normalizedMap[t] then
        return normalizedMap[t]
    end

    -- بررسی کلمات کلیدی
    if t:find("هنری") or t:find("هنر") or t:find("آکادمی") then
        return "art_school"
    end
    if t:find("دانشگاه") then
        return "university"
    end
    if t:find("حوزه") or t:find("علمیه") then
        return "seminary"
    end
    if t:find("پژوهش") then
        return "research_center"
    end
    if t:find("کالج") then
        return "college"
    end
    if t:find("مهد") or t:find("کودک") then
        return "kindergarten"
    end
    if t:find("آموزشگاه") or t:find("مرکزآموزشی") or t:find("آموزشی") then
        return "training_center"
    end
    if t:find("مدرسه") then
        return "school"
    end

    return "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
            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 layout = detectLayout(args.type)
    return renderLayout(layout, args)
end

return p