• 欢迎来到THBWiki!如果您是第一次来到这里,请点击右上角注册一个帐户
  • 有任何意见、建议、求助、反馈都可以在 讨论板 提出
  • THBWiki以专业性和准确性为目标,如果你发现了任何确定的错误或疏漏,可在登录后直接进行改正

模块:技能信息

来自THBWiki
跳到导航 跳到搜索
Lua-Logo.svg 模块文档[创建]
local list = require('Module:list')
local tttools = require('Module:tttools')
local ArgsTracker = require('Module:checkargs').ArgsTracker

local inner_template_name = '模板:技能信息'
local frame_template_name = '模板:技能信息框架'

local function make_errmsg(name)
    return function(msg)
        return require('Module:error')._main{ msg, name }
    end
end

-- 为两个模板创建不同的错误消息函数
local err_inner = make_errmsg(inner_template_name)
local err_frame = make_errmsg(frame_template_name)

-- 单个技能信息的最大行数/列数
local size_limit = 10

local align_types = {
    ['左'] = 'left',
    ['中'] = 'center',
    ['右'] = 'right',
}

local bold_classes = {
    ['是'] = 'bold',
    ['否'] = 'nobold',
}

local p = {}

-- 解析宽度并检查是否合法
local function parse_width(text)
    if text and text ~= '' then
        local num = tonumber(text)
        if num and num > 0 then
            return num .. 'px'
        end
        return nil, '宽度必须是正数'
    end
    return nil
end

-- 解析对齐方式并检查是否合法
local function parse_align(text)
    if text and text ~= '' then
        local align = align_types[text]
        if align then
            return align
        end
        return nil, '<code>对齐</code> 的值必须是 <code>左/中/右</code>'
    end
    return nil
end

-- 解析加粗参数并检查是否合法
local function parse_bold(text)
    if text and text ~= '' then
        local bold = bold_classes[text]
        if bold then
            return bold
        end
        return nil, '<code>加粗</code> 的值必须是 <code>是/否</code>'
    end
    return nil
end

-- 带缓存功能的翻译表获取器
local function make_fetcher(title)
    local cache = {}
    return function(section, num)
        if not cache[section] then
            cache[section] = tttools.get_content{ title, section }
        end

        local sec = cache[section]
        if not num then
            return sec
        end

        num = num:gsub('(%d+)%-(%d+)', function(a, b)
            a, b = tonumber(a), tonumber(b)
            b = math.min(b, #sec)
            return list.range(a, b):join(',')
        end)
        local num_list = list(num:gmatch('[1-9]%d*'))
            :set()
            :map(tonumber)
            :sorted()

        return num_list:map(function(i) return sec[i] end)
    end
end

-- 生成单一语言的表格
local function render_table(args, lang)
    local data = args.data
    local align = args.align
    local style = args.style
    local max_col = args.max_col

    -- 语言代号
    local normal = lang
    local header = lang .. 'h'

    local blocks = data:map(function(item)
        local row_align = item.align
        local row_style = item.style
        local trs = item.rows:map(function(row)
            local tds = row:map(function(cell)
                if cell.title then
                    local th = mw.html.create('th')
                        :attr('colspan', max_col)
                        :wikitext(cell.title)

                    return tostring(th)
                end

                if not cell.is_filled then
                    local td = mw.html.create('td')
                        :wikitext(err_inner('缺少内容'))

                    return tostring(td)
                end

                if not cell.has_content then
                    return ''
                end

                local tt_content = cell.tt_content
                local content = cell.content or ''
                if tt_content then
                    local divs = tt_content:map(function(tt)
                        -- 找出翻译表类型
                        local tt_type = (tt[normal] and normal)
                            or (tt[header] and header)
                        if not tt_type then
                            return ''
                        end

                        local text = tt[tt_type]
                            :gsub('%s+$', '')
                            :gsub('\n', '<br />')
                        local div = mw.html.create('div')
                            :addClass('tt-' .. tt_type)
                            :wikitext(text)

                        return tostring(div)
                    end)
                    local text = divs:join('')
                    if text == '' then
                        text = err_inner('引用的翻译表不存在')
                    end
                    content = text .. content
                end

                local td = mw.html.create('td')
                    :addClass(cell.bold)
                    :attr('rowspan', cell.rowspan)
                    :attr('colspan', cell.colspan)
                    :css('width', cell.width)
                    :css('text-align', cell.align)
                    :cssText(cell.style)
                    :wikitext(content)

                return tostring(td)
            end)

            local tr = mw.html.create('tr')
                :css('text-align', row_align)
                :cssText(row_style)
                :wikitext(tds:join(''))

            return tostring(tr)
        end)

        -- 单独生成一行显示错误消息
        if item.message then
            local tr = mw.html.create('tr')
            tr:tag('td')
                :attr('colspan', max_col)
                :wikitext(item.message)
            trs:insert(1, tostring(tr))
        end

        return trs:join('')
    end)

    local body = mw.html.create('table')
        :addClass('wikitable')
        :addClass('skillinfo')
        :attr('lang', lang)
        :css('text-align', align)
        :cssText(style)
        :wikitext(blocks:join(''))

    return tostring(body)
end

-- 将内层模板的输入参数整理为结构化数据
function p._preprocess(args)
    local align = args['对齐']
    local style = args.style

    -- 标题行不需要其他参数, 提前返回
    local title = args['标题'] or ''
    if title ~= '' then
        local cell = { title = title }
        return {
            rows = list{ list{ cell } },
            size = {1, 1},
            align = align,
            style = style,
        }
    end

    -- 遍历参数名求出表格尺寸
    local indices = list.fromkeys(args):map(function(key)
        key = tostring(key)

        local i, j = key:match('^([1-9]%d*)行([1-9]%d*)列')
        if i then
            return { tonumber(i), tonumber(j) }
        end

        j = key:match('^第([1-9]%d*)格')
        if j then
            return { 1, tonumber(j) }
        end

        return {0, 0}
    end)

    local max_row = indices
        :map(function(index) return index[1] end)
        :max() or 0
    local max_col = indices
        :map(function(index) return index[2] end)
        :max() or 0

    max_row = math.min(max_row, size_limit)
    max_col = math.min(max_col, size_limit)

    if max_row == 0 then
        return {
            rows = list{},
            size = {0, 0},
            message = err_inner('缺少内容'),
        }
    end

    -- 只有一行时允许使用别名
    if max_row == 1 then
        local row = list.range(max_col):map(function(j)
            local prefix = ('1行%d列'):format(j)
            local prefix_short = ('第%d格'):format(j)
            return {
                ttt = args[prefix .. 'ttt'] or args[prefix_short .. 'ttt'],
                content = args[prefix .. '内容'] or args[prefix_short .. '内容'],
                width = args[prefix .. '宽度'] or args[prefix_short .. '宽度'],
                align = args[prefix .. '对齐'] or args[prefix_short .. '对齐'],
                bold = args[prefix .. '加粗'] or args[prefix_short .. '加粗'],
                style = args[prefix .. 'style'] or args[prefix_short .. 'style'],
            }
        end)
        return {
            rows = list{ row },
            size = { 1, max_col },
            align = align,
            style = style,
        }
    end

    local rows = list.range(max_row):map(function(i)
        return list.range(max_col):map(function(j)
            local prefix = ('%d行%d列'):format(i, j)
            return {
                ttt = args[prefix .. 'ttt'],
                content = args[prefix .. '内容'],
                width = args[prefix .. '宽度'],
                align = args[prefix .. '对齐'],
                bold = args[prefix .. '加粗'],
                style = args[prefix .. 'style'],
            }
        end)
    end)
    return {
        rows = rows,
        size = { max_row, max_col },
        align = align,
        style = style,
    }
end

function p._main(args)
    local page = args['中日对照页面'] or ''
    if page == '' then
        return err_frame('缺少参数 <code>中日对照页面</code>')
    end
    local fetch = make_fetcher(page)

    local align, message = parse_align(args['对齐'])
    if message then
        return err_frame(message)
    end
    local style = args.style

    local lang = args['语言'] or ''
    if lang == '' then
        lang = '中日'
    end

    local data = args.data
    if not data then
        error('没有提供 data')
    end

    data = list(data)

    -- 计算最大列数
    local max_col = data
        :map(function(item) return item.size[2] end)
        :max() or 0

    -- 处理数据
    for item in data:iter() do
        item.align, message = parse_align(item.align)
        if message then
            item.message = err_inner(message)
        end

        -- 将行和列转换为列表
        local rows = list(item.rows):map(list)
        item.rows = rows

        -- 补充缺少的格子
        local len = max_col - item.size[2]
        if len > 0 then
            for row in rows:iter() do
                for i = 1, len do
                    row:append({})
                end
            end
        end

        local max_row = rows:len()

        -- 遍历表格, 将有内容的单元格向右扩展
        -- 算出 colspan, 标出本身有内容或被填充的单元格
        -- 顺便解析格式参数和获取翻译表内容
        for i = 1, max_row do
            local prev  -- 本行上一个有填写内容的单元格
            for j = 1, max_col do
                local cell = rows[i][j]
                if cell.ttt or cell.content or cell.title then
                    cell.has_content = true  -- 表示本身有内容
                    cell.is_filled = true  -- 本身有内容或被其他单元格填充
                    prev = cell
                elseif prev then
                    -- 如果这一格无内容, 且左方有非空单元格, 就将那一格的宽度加 1
                    prev.colspan = (prev.colspan or 1) + 1
                    cell.is_filled = true
                end

                -- 解析格式参数
                cell.align, message = parse_align(cell.align)
                if message then
                    cell.content = err_inner(message)
                end
                cell.width, message = parse_width(cell.width)
                if message then
                    cell.content = err_inner(message)
                end
                cell.bold, message = parse_bold(cell.bold)
                if message then
                    cell.content = err_inner(message)
                end

                -- 获取翻译表内容
                local ttt = cell.ttt or ''
                if ttt ~= '' then
                    local section, num = ttt:match('([^,]+),(.+)')
                    if section then
                        cell.tt_content = fetch(section, num)
                    else
                        cell.content = err_inner('参数 <code>ttt</code> 格式不正确, 必须是 <code>章节名,序号</code>')
                    end
                end
            end
        end

        -- 再次遍历表格, 将有内容的单元格向下扩展 (如果可能)
        -- 算出 rowspan, 标出被填充的单元格
        for j = 1, max_col do
            local prev  -- 本列上一个有填写内容且能往下扩展的单元格
            for i = 1, max_row do
                local cell = rows[i][j]
                if cell.has_content then
                    prev = cell
                elseif prev then
                    -- 如果这一格没有填写内容, 且上方有非空单元格, 就尝试将那一格的高度加 1
                    local width = prev.colspan or 1
                    -- 切片出将被覆盖的单元格
                    local covered = rows[i]{j, j + width - 1}
                    if covered:any(function(c) return c.is_filled end) then
                        -- 如果其中任何单元格已被填充, 就不能再往下扩展
                        prev = nil
                    else
                        prev.rowspan = (prev.rowspan or 1) + 1
                        for c in covered:iter() do
                            c.is_filled = true
                        end
                    end
                end
            end
        end
    end

    -- 生成表格
    local render_args = {
        data = data,
        align = align,
        style = style,
        max_col = max_col,
    }
    if lang == '中日' then
        local table_zh = render_table(render_args, 'zh')
        local table_ja = render_table(render_args, 'ja')
        local tabber_text = ('|-| 中文 = %s |-| 日文 = %s'):format(table_zh, table_ja)
        return mw.getCurrentFrame():extensionTag('tabber', tabber_text)

    elseif lang == '中文' then
        return render_table(render_args, 'zh')

    elseif lang == '日文' then
        return render_table(render_args, 'ja')

    else
        return err_frame('<code>语言</code> 的值必须是 <code>中日/中文/日文</code>')
    end
end

function p.preprocess(frame)
    -- 追踪参数使用情况
    local args = ArgsTracker(frame:getParent().args)

    local item = p._preprocess(args)
    item.message = args:make_message{ inner_template_name }

    -- 返回 json 给外层模板统一处理
    return mw.text.jsonEncode(item) .. ','
end

function p.main(frame)
    local args = frame:getParent().args

    local raw = args['内容'] or ''
    if raw == '' then
        return err_frame('缺少参数 <code>内容</code>')
    end
    -- 防止解析出错
    local json = '[' .. raw:gsub(',$', '') .. ']'
    local success, data = pcall(mw.text.jsonDecode, json)
    if not success then
        return err_frame('请勿在参数 <code>内容</code> 中添加 [[' .. inner_template_name .. ']] 以外的内容')
    end
    args.data = data

    -- 追踪参数使用情况
    args = ArgsTracker(args)
    -- 访问一次记为已使用
    local _ = args['内容']

    local text = p._main(args)
    local message = args:make_message{ frame_template_name } or ''

    return message .. text
end

return p