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

模块:并列图像

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

local template_name = '模板:并列图像'

-- 收集错误消息
local errors = list()
local function add_error(message)
    errors:append(require('Module:error')._main{ message, template_name })
end

local p = {}

-- 转换数字并检查是否合法
local function to_number(text)
    local num = nil
    if text and text ~= '' then
        num = tonumber(text)
        if not num or num <= 0 then
            add_error('错误:宽度必须是正数')
            return 200
        end
    end
    return num
end

-- 获取图片尺寸
local function get_dimension(file_name)
    -- 重定向以保证能获取正确的尺寸
    file_name = mw.getCurrentFrame()
        :callParserFunction('#redirect', 'File:' .. file_name)
    local title = mw.title.new(file_name)
    local file = title and title.file or {}
    return {
        width = tonumber(file.width) or 200,
        height = tonumber(file.height) or 200
    }
end

-- 生成每行图片数的列表
local function expand_perrow(perrow, image_count)
    local perrow_list = list(perrow:gmatch('[1-9]%d*')):map(tonumber)
    local len = perrow_list:len()
    if len > 1 then
        if perrow_list:sum() ~= image_count then
            add_error('警告:每行图片数之和与总图片数不相等')
        end
        return perrow_list
    elseif len == 0 then
        add_error('错误:perrow 必须是正整数')
    end

    perrow = perrow_list[1] or image_count
    if perrow > image_count then
        add_error('警告:第一行图片数大于总图片数')
    else
        perrow_list = math.floor(image_count / perrow) * list{perrow}
        if image_count % perrow > 0 then
            perrow_list:append(image_count % perrow)
        end
    end
    return perrow_list
end

-- title.exists 会增加高开销解析函数计数, 但 #ifexist 不会
local function title_exist(title)
    local result = mw.getCurrentFrame()
        :callParserFunction('#ifexist', title, '1', '0')
    return (result == '1') and true or false
end

-- 生成带分类的上传链接
local function generate_upload_link(file_name, category)
    local url = mw.uri.localUrl('Special:Upload', {
        wpDestFile = file_name,
        wpUploadDescription = category
    })
    local link = string.format('[%s 上传图片]', url.relativePath)

    local cell = mw.html.create('div')
        :addClass('multi-image-upload')

    cell:tag('span')
        :wikitext(file_name)

    cell:tag('span')
        :addClass('new')
        :wikitext(link)

    return tostring(cell)
end

-- 生成单个图片格子 (包含 caption)
local function render_image_cell(data)
    -- 里面的 thumbimage 有 1px 边框
    local cell_width = data.width + 2
    local cell = mw.html.create('div')
        :addClass('multi-image-item')
        :css('width', cell_width .. 'px')
        :css('max-width', cell_width .. 'px')

    local image = data.image
    local category = data.category or ''
    local content
    if category ~= '' and not title_exist('File:' .. image) then
        -- 提供分类且文件不存在时, 生成上传链接
        content = generate_upload_link(image, category)
    else
        local width = '|' .. data.width .. 'px'
        local class = data.class and ('|class=' .. data.class) or ''
        local link = data.link and ('|link=' .. data.link) or ''
        local alt = data.alt or ''
        if alt ~= '' then
            alt = '|alt=' .. alt
        end
        content = '[[File:' .. image .. width .. class .. link .. alt .. ']]'
    end
    local image_border = data.image_border or ''
    local image_background = data.image_background or ''
    local height = data.height
    cell:tag('div')
        :addClass('thumbimage')
        :wikitext(content)
        :css('border', (image_border ~= '') and image_border or nil)
        :css('background-color', (image_background ~= '') and image_background or nil)
        :css('height', height and (height .. 'px') or nil)
        :css('overflow', height and 'hidden' or nil)

    local caption = data.caption or ''
    local caption_align = data.caption_align or ''
    if caption ~= '' then
        cell:tag('div')
            :addClass('thumbcaption')
            :css('text-align', (caption_align ~= '') and caption_align or nil)
            :wikitext(caption)
    end

    return tostring(cell)
end

function p._main(args)
    -- 遍历参数得到最大的非空 image[n], 作为总图片数
    local image_count = list(pairs(args)):map(function(kv)
        return kv[2] ~= '' and tonumber(tostring(kv[1]):match('^image([1-9]%d*)$')) or 0
    end):max() or 0

    -- 图片不允许跳过编号
    for i = 1, image_count do
        local v = args['image' .. i] or ''
        if v == '' then
            add_error('警告:您跳过了图片 ' .. i)
        end
    end

    -- 处理 perrow 与 direction
    local perrow = args.perrow or ''
    if perrow == '' then
        local dir = args.direction or ''
        if dir == '' then
            dir = 'horizontal'
        end
        -- 未给定 perrow 时由 direction 决定每行图片数
        local dir_to_perrow = {
            horizontal = tostring(image_count),
            vertical = '1'
        }
        perrow = dir_to_perrow[dir]
        if not perrow then
            add_error('错误:direction 的值必须是 horizontal 或 vertical')
            perrow = tostring(image_count)
        end
    end
    local perrow_list = expand_perrow(perrow, image_count)

    -- 保证 image_gap ≥ 0
    local image_gap_raw = args.image_gap or ''
    local image_gap = tonumber(image_gap_raw)
    if not image_gap or image_gap < 0 then
        if image_gap_raw ~= '' then
            add_error('错误:image_gap 必须是非负数字')
        end
        image_gap = 0
    end

    -- 给定 total_width 时获取图片尺寸, 供后续计算使用
    local total_width = to_number(args.total_width)
    local i = 0
    local dims = perrow_list:map(function(row_size)
        return list.range(row_size):map(function()
            i = i + 1
            if total_width then
                -- 此行只有一张图片时不需要查询尺寸, 减小开销
                if row_size == 1 then
                    return {}
                end
                return get_dimension(args['image' .. i] or '')
            else
                return {
                    width = to_number(args['width' .. i]) or to_number(args.width) or 200
                }
            end
        end)
    end)

    -- 给定 total_width 时, 计算缩放到相同高度时的尺寸
    if total_width then
        dims = dims:map(function(row)
            local row_size = row:len()
            -- 计算纯图片部分的宽度. 图片默认间距 4px, 每行两侧各 6px 边距
            local content_width = total_width - (4 + image_gap) * (row_size - 1) - 12

            -- 单张图片直接返回可用宽度
            if row_size == 1 then
                return list{ { width = content_width } }
            end

            -- 宽高比列表
            local aspect_ratios = row:map(function(dim)
                return dim.width / dim.height
            end)

            -- (宽高比 1 + ··· + 宽高比 n) * 高度 = 总宽度
            local height = content_width / aspect_ratios:sum()

            -- 宽度四舍五入, 高度向下取整防止出现空白
            local row_new = aspect_ratios:map(function(aspect_ratio)
                return {
                    width = math.floor(aspect_ratio * height + 0.5),
                    height = math.floor(height)
                }
            end)

            return row_new
        end)
    end

    -- 无图片时直接返回空字符串
    if image_count == 0 then
        add_error('警告:未提供图片')
        return '', errors
    end

    -- 用最大行宽作为容器宽度
    local body_width = dims:map(function(row)
        local row_size = row:len()
        local width_sum = row:map(function(dim) return dim.width end):sum()
        return width_sum + (4 + image_gap) * (row_size - 1) + 12
    end):max()
    if body_width < 100 then
        add_error('警告:总宽度过小,至少应大于 100')
    end
    -- 减去 padding 与 border
    body_width = body_width - 8

    local align = args.align or ''
    if align == '' then
        align = 'right'
    end
    local thumb_class = {
        left = 'tleft',
        none = 'tnone',
        center = 'tnone center',
        right = 'tright'
    }
    local root_class = thumb_class[align]
    if not root_class then
        add_error('错误:align 的值必须是 left, center, right, none 之一')
    end
    -- 外层容器
    local root = mw.html.create('div')
        :addClass('thumb')
        :addClass('multi-image')
        :addClass(root_class or 'tright')
    -- 纵排时添加 noclear. 由于副作用比较大, 允许手动关闭
    if perrow == '1' then
        local h2_newline = args.h2_newline or ''
        if h2_newline == '' then
            root:addClass('noclear')
        end
    end

    local border = args.border or ''
    local background = args.background or ''
    -- 内层容器
    local inner = root:tag('div')
        :addClass('thumbinner')
        :addClass('multi-image-inner')
        :css('width', body_width .. 'px')
        :css('max-width', body_width .. 'px')
        :css('border', (border ~= '') and border or nil)
        :css('background-color', (background ~= '') and background or nil)
        :css('gap', (image_gap > 0) and (image_gap .. 'px') or nil)

    local header = args.header or ''
    if header ~= '' then
        local header_align = args.header_align or ''
        local header_background = args.header_background or ''
        inner:tag('div')
            :addClass('multi-image-row')
            :tag('div')
                :addClass('multi-image-header')
                :css('text-align', (header_align ~= '') and header_align or nil)
                :css('background-color', (header_background ~= '') and header_background or nil)
                :wikitext(header)
    end

    -- 生成图片行
    i = 0
    for _, row in ipairs(dims) do
        local images = row:map(function(dim)
            i = i + 1
            local category = args['category' .. i] or ''
            if category == '' then
                category = args.category
            end
            return render_image_cell{
                image = args['image' .. i] or '',
                width = dim.width,
                height = dim.height,
                class = args['class' .. i],
                link = args['link' .. i],
                alt = args['alt' .. i],
                category = category,
                image_border = args.image_border,
                image_background = args.image_background,
                caption = args['caption' .. i],
                caption_align = args.caption_align,
            }
        end)
        inner:tag('div')
            :addClass('multi-image-row')
            :wikitext(images:join(''))
            :css('gap', (image_gap > 0) and (image_gap .. 'px') or nil)
    end

    local footer = args.footer or ''
    if footer ~= '' then
        local footer_align = args.footer_align or ''
        local footer_background = args.footer_background or ''
        inner:tag('div')
            :addClass('multi-image-row')
            :tag('div')
                :addClass('thumbcaption')
                :addClass('multi-image-footer')
                :css('text-align', (footer_align ~= '') and footer_align or nil)
                :css('background-color', (footer_background ~= '') and footer_background or nil)
                :wikitext(footer)
    end

    return tostring(root), errors
end

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

    local text, messages = p._main(args)
    local message = args:make_message{ template_name, ignore_blank = 'all' }
    if message then
        messages:append(message)
    end

    return text .. messages:join('<br />')
end

return p