2023年政策修订增补工作正在进行中,欢迎参与!
  • Moegirl.ICU:萌娘百科流亡社群 581077156(QQ),欢迎对萌娘百科运营感到失望的编辑者加入
  • Moegirl.ICU:账号认领正在试运行,有意者请参照账号认领流程

Module:ModulePageSystem/util

萌娘百科,万物皆可萌的百科全书!转载请标注来源页面的网页链接,并声明引自萌娘百科。内容不可商用。
跳转到导航 跳转到搜索
Template-info.svg 模块文档  [创建] [刷新]
local util = {}

util.PARENTNODE = ".." -- 一个常量,表示上级节点的路径。
util.CURRENTNODE = "." -- 一个常量,表示同级节点的路径。

util.FUNC_HASVALUE_TRUE = function() return true end
util.FUNC_HASVALUE_FALSE = function() return false end
util.FUNC_VALUE_NIL = function() return nil end
--定义一个常量,表示该节点值不存在。
util.NOVALUE = {
    hasValue = util.FUNC_HASVALUE_FALSE,
    value = util.FUNC_VALUE_NIL
}
util.FUNC__NOTSUPPORTED = function() util.error("不支持本函数。", util.ELID_ERROR) end
util.KEYCOMPARER_NGROUP = function(k1, k2, params)
    local key1, ngpage1 = util.getNGroup(k1)
    local key2, ngpage2 = util.getNGroup(k2)

    local loadNGData = function(path)
        local title = mw.title.new(path)
        if title.exists then
            local success, data = pcall(mw.loadData, path)
            if success then
                return { path = path, data = data }
            else
                util.error(mw.ustring.format("加载页面“%s”时发生错误:%s", path, data), util.ELID_WARNING)
                return { path = path, data = {} } -- 设为空Lua表。
            end
        else
            util.error(mw.ustring.format("不存在页面“%s”", ngpage), util.ELID_WARNING)
            return { path = ngpage, data = {} }
        end
    end

    if key1 == nil and key2 == nil then -- 两个键均不为名称组。
        return util.normalize(k1) == util.normalize(k2)
    else
        local customizedngpage = nil -- 自定义的名称组页面路径。
        local customizedngdata = nil -- 自定义的名称组数据。
        if params ~= nil then
            -- 从params参数中获取自定义的名称组页面路径。
            if type(params.ngpage) == "string" then
                customizedngpage = params.ngpage
            elseif type(params.ngpage) == "table" then
                customizedngpage = params.ngpage.path
                customizedngdata = params.ngpage.data
            end
        end

        -- 第二个键为名称组,且传入了params参数,该行为应当视为异常。
        if key2 ~= nil and params ~= nil then util.error(mw.ustring.format("试图在查找一个名称组“%s”。", k2), util.ELID_ERROR) end 

        if key1 ~= nil and key2 ~= nil then -- 两个键均为名称组。
            return key1 == key2 and (ngpage1 or customizedngpage) == (ngpage2 or customizedngpage)
        elseif key1 ~= nil then -- 第一个键为名称组,第二个键不为名称组。
            if ngpage1 ~= nil and ngpage1 ~= customizedngpage then -- 节点中设置了名称组所属页面路径,且与params参数中的页面路径不同。
                customizedngpage = ngpage1
                -- 加载节点中设置的名称组所属页面路径。
                customizedngdata = loadNGData(customizedngpage).data
            elseif type(params.ngpage) == "string" then -- 数据未加载。
                -- 加载节点中设置的名称组所属页面路径。
                customizedngdata = loadNGData(customizedngpage).data

                -- 将加载的数据保存入params参数中,以后不必二次耗时加载。
                if type(params.ngpage) == "table" then
                    params.ngpage.data = customizedngdata
                else
                    params.ngpage = { path = customizedngpage, data = customizedngdata }
                end
            end

            if customizedngdata == nil or customizedngdata[key1] == nil then -- 无法获取名称组列表,或列表中没有key1这个名称组的键。
                return false
            else -- 在列表中搜索。
                for _, ngKey in ipairs(customizedngdata[key1]) do
                    if util.normalize(ngKey) == util.normalize(k2) then return true end -- 组内只要有一个名称与第二个名称相等。
                end
                return false
            end
        elseif key2 ~= nil then -- 第一个键不为名称组,第二个键为名称组。
            return false
        end
    end
end

util.EL_FATAL = "fatal" -- 一个常量,表示错误级别【致命错误】。
util.ELID_FATAL = 1 -- 一个常量,表示错误级别ID【致命错误】。
util.EL_ERROR = "error" -- 一个常量,表示错误级别【错误】。
util.ELID_ERROR = 2 -- 一个常量,表示错误级别ID【错误】。
util.EL_WARNING = "warning" -- 一个常量,表示错误级别【警告】。
util.ELID_WARNING = 3 -- 一个常量,表示错误级别ID【警告】

util.errorlevel = "error" -- 全局的错误级别,可人为修改。

local getErrorLevelID = function(errorlevel)
    if type(errorlevel) == "string" then
        if errorlevel == util.EL_WARNING then
            return util.ELID_WARNING
        elseif errorlevel == util.EL_ERROR then
            return util.ELID_ERROR
        elseif errorlevel == util.EL_FATAL then
            return util.ELID_FATAL
        else
            local elid = tonumber(errorlevel)
            if elid == nil then
                return util.ELID_ERROR
            else
                return elid
            end
        end
    elseif type(errorlevel) == "number" then
        return errorlevel
    else
        return util.ELID_ERROR
    end
end

--[[
    根据指定的错误级别抛出一个错误。
    -----
    message - 错误信息
    errorlevel - 错误级别
--]]
function util.error(message, errorlevel)
    if getErrorLevelID(errorlevel) <= getErrorLevelID(util.errorlevel) then
        error(message)
    end
end

--[[
    标准化名称格式。
    1. 下划线字符视为空白字符。
    2. 多个空白字符视为一个下划线字符。
    3. 首字母为$字符表示名称组。
    4. 名称组的方括号内为其所在的页面地址。
    -----
    name - 要被标准化的名称
    -----
    返回:标准化的名称
    -----
    错误 - 名称中出现无效字符。:名称里出现了中括号、正反斜杠等不被允许的字符。
    错误 - 名称中的页面路径中出现无效字符。:名称中的页面路径里出现了中括号、正反斜杠等不被允许的字符。
--]]
function util.normalize(name)
    if name == nil then return nil
    elseif type(name) ~= "string" then return util.error(mw.ustring.format("在normalize中name的值类型错误。(应为%s,实际为%s)", "string", type(name)), util.ELID_FATAL)
    end

    local normalizeKey = function(key)
        -- 首字符$表示名称组,若需要在键中使用$字符则需使用“%$”。
        -- 获取转义后的键。
        key = mw.ustring.gsub(key, "%%[%%$]", function(m) -- 替换掉
            if m == "%%" then return "%"
            elseif m == "%$" then return "$"
            else return ""
            end
        end)
        key = mw.ustring.gsub(key, "[_%s]+", "_") -- 替换多个空白字符为一个空白字符。

        local v1, v2 = mw.ustring.find(key, "[%[%]/\\]") -- 检测无效字符。
        if v1 ~= nil then util.error(mw.ustring.format("名称“%s”中出现无效字符“%s”", key, mw.ustring.sub(key, v1, v2)), util.ELID_FATAL) end

        return key
    end

    if mw.ustring.sub(name, 1, 1)  == "$" then -- 若第一个字符是$号,则表示是名称组。
    	name = mw.ustring.sub(name, 2)
        local page, ngroup = mw.ustring.match(name, "^%[([^%[%]]*)%]([^%[%]/\\]+)$")
        if page ~= nil then -- 含有页面地址的名称组格式正确。
            if page ~= "" then
                local title = mw.title.new(page)
                if title == nil then
                    util.error(mw.ustring.format("名称中的页面路径“%s”中出现无效字符。", page), util.ELID_FATAL)
                else
                    page = title.prefixedText -- 获取自动转换的标准页面路径。
                end
            end

            ngroup = normalizeKey(ngroup) -- 标准化键名称。

            return mw.ustring.format("$[%s]%s", page, ngroup)
        end

        ngroup = mw.ustring.match(name, "^[^%[%]/\\]+$")
        if ngroup ~= nil then -- 名称组格式正确。
            ngroup = normalizeKey(ngroup) -- 标准化键名称。

            return mw.ustring.format("$%s", ngroup)
        end

        util.error(mw.ustring.format("名称组“%s”格式不正确", mw.ustring.sub(name, 1, 1)), util.ELID_FATAL)
    else
        name = normalizeKey(name)

        if name == "" then util.error(mw.ustring.format("名称“%s”为空。", name), util.ELID_WARNING) end

        return name
    end

    return nil
end

--[[
    返回名称组的键及定义页面路径。
    -----
    name - 要检查的名称。
    -----
    返回:ngroup, page
        ngroup - 名称组的键。
        page - 定义名组的页面路径。未定义时返回nil。
--]]
function util.getNGroup(name)
    name = util.normalize(name) -- 获取标准化名称。
    if name == nil then return nil end -- 获取失败。

    local page, ngroup = mw.ustring.match(name, "%$%[([^%]]*)%](.+)$")
    if page == nil then
    	ngroup = mw.ustring.match(name, "%$(.+)$")
    end
    
    if page == "" then page = nil end -- 空页面视为nil。

    return ngroup, page
end

--[[
    检查名称是否为名称组。
    -----
    name - 要检查的名称。
    -----
    返回:名称是否为名称组。
--]]
function util.IsNGroup(name)
    return util.getNGroup(name) == nil
end

--[[
    合并一系列路径。函数将会调用util.normalize标准化路径片段,并整理逻辑层级。
    -----
    seg1 - (至少有一个)路径片段
    [匿名参数列表] - 其余的路径片段
    -----
    返回:合并后的路径
--]]
function util.pathCombine(seg1, ...)
    local path = {}
    if seg1 ~= nil then table.insert(path, seg1) end
    for i = 1, select("#", ...) do
        local seg = select(i, ...)
        if seg ~= nil then table.insert(path, seg) end
    end

    local result = util.pathSplit(path)
    return table.concat(result, " ")
end

--[[
    将路径切割成为多个片段。每个片段会被util.normalize标准化。
    -----
    path - 要被切割的路径。
    -----
    返回:路径片段数组。
--]]
function util.pathSplit(path)
    if path == nil then return {}
    else
        local sort = {}
        local splitPathString = function(pathStr)
            local pos = 1
            while pos <= mw.ustring.len(pathStr) + 1 do
                -- 对字符串路径,按斜杠、反斜杠及空格进行拆分,剔除空名称。
                local v1, v2 = mw.ustring.find(pathStr, "^%[[^%[%]]*%][^/\\%s]*", pos)
                local v3, v4 = mw.ustring.find(pathStr, "^[^/\\%s]*", pos)
                if v1 == nil and v3 == nil then break
                elseif v1 ~= nil and (v3 == nil or v1 <= v3) then
                    table.insert(sort, util.normalize(mw.ustring.sub(pathStr, v1, v2)))
                    pos = v2 + 2
                elseif v3 ~= nil and (v1 == nil or v1 > v3) then
                    table.insert(sort, util.normalize(mw.ustring.sub(pathStr, v3, v4)))
                    pos = v4 + 2
                else -- 硬编码错误:必然是BUG的例外情况,理论上应当永远不执行。
                    error("意料之外的错误。")
                end
            end
        end

        -- 对路径中的每个元素进行格式整理。
        if type(path) == "string" then
            splitPathString(path)
        elseif type(path) == "table" then
            -- 对Lua表表示的路径,递归拆解Lua表,剔除空名称和非字符串的值。
            local function unpackInnerTable(_tab)
                for _, item in ipairs(_tab) do
                    if type(item) == "string" then
                        splitPathString(item)
                    elseif type(item) == "table" then
                        unpackInnerTable(item)
                    end
                end
            end
            unpackInnerTable(path)
        end

        local result = {}
        -- 对整理结果进行逻辑整理。
        -- 去除“..”(返回上级目录)、“.”(同级目录)等控制目录名称。
        for index, item in ipairs(sort) do
            if item == util.CURRENTNODE then -- 同级目录)
                -- 不添加也不删除目录层次。
            elseif item == util.PARENTNODE then -- 上级目录
                if #result == 0 then util.error(mw.ustring.format("不能获取\"%s\"的上级目录,因为已经是根目录。", table.concat(sort, "/", 1, index - 1)), util.ELID_ERROR)
                else
                    table.remove(result, #result) -- 删除最后一个层次。
                end
            else -- 下级目录
                table.insert(result, item) -- 在最后添加一个层次。
            end
        end

        return result
    end
end

return util