模块:沙盒/PexEric/TopicList

维基百科,自由的百科全书
跳转到导航 跳转到搜索

local p = {}
local title_obj = mw.title.getCurrentTitle()

-- ============================================================================
-- 配置部分
-- ============================================================================

local CONFIG = {
    colors = {
        ['1h'] = 'var(--background-color-success-subtle, #efe)', 
        ['1d'] = 'var(--background-color-progressive-subtle, #eef)', 
        ['old'] = 'var(--background-color-neutral-subtle, #ddd)',
        ['very_old'] = 'var(--background-color-disabled-subtle, #bbb)',
        ['archived'] = 'var(--background-color-disabled-subtle, #ddd)', 
        ['warn'] = 'var(--background-color-error-subtle, #fcc)', 
        ['hot'] = 'var(--background-color-warning-subtle, #ffe)' 
    },
    
    DATEPATTERN = '(%d-)年(%d-)月(%d-)日 %([一二三四五六日]-%) (%d-):(%d-) %(UTC%)',
    
    USERPREFIX = {
        'user:', '用戶:', '用户:', '使用者:', 'u:', 'U:',
        'user talk:', 'user_talk:', '用戶討論:', '用户讨论:', '使用者討論:', 'ut:','UT:','Ut:',
        'special:用户贡献/', 'special:用戶貢獻/', 'special:使用者貢獻/', '特殊:用户贡献/', '特殊:用戶貢獻/', '特殊:使用者貢獻/', 
        'special:contributions/', '特殊:contributions/', 'special:contribs/', '特殊:contribs/' 
    },

    TEMPLATES = {
        moved = {
            ['移動至'] = true, ['movedto'] = true, ['moveto'] = true
        },
        archive_top = {
            ['archive top'] = true, ['archive top'] = true, ['archiveh'] = true, ['存档顶'] = true, 
            ['discussion top'] = true
        },
        archive_bottom = {
            ['archive bottom'] = true, ['archive bottom'] = true, ['closed rfc bottom'] = true, 
            ['archivef'] = true, ['存档底'] = true, ['rm bottom'] = true, ['discussion bottom'] = true
        }
    }
}

-- ============================================================================
-- 核心工具函数
-- ============================================================================

local function clean_title_text(text)
    if not text then return "" end
    text = string.gsub(text, "%[%[[^|%]]-|([^%]]-)%]%]", "%1")
    text = string.gsub(text, "%[%[:([^%]]-)%]%]", "%1")
    text = string.gsub(text, "%[%[([^%]]-)%]%]", "%1")
    text = string.gsub(text, "[%[%]]", "")
    return text
end

local function rfind(s, pattern, init)
    local x, y
    local i = init or #s
    local len = #s
    x, y = string.find(string.reverse(s), string.reverse(pattern), len-i+1, true)
    if x then return len-y+1, len-x+1 end
end

local function date_to_timestamp(date_str)
    local Y, M, D, h, m = string.match(date_str, CONFIG.DATEPATTERN)
    if Y and M and D and h and m then
        return os.time({
            year = tonumber(Y), month = tonumber(M), day = tonumber(D),
            hour = tonumber(h), min = tonumber(m), sec = 0
        })
    end
    return 0
end

local function format_user_link(name)
    if not name or name == "Unknown" then return "Unknown" end
    if string.match(name, '^%d+%.%d+%.%d+%.%d+$') or string.match(name, '^[%x:]+$') then
        return string.format('[[Special:用户贡献/%s|%s]]', name, name)
    else
        return string.format('[[User:%s|%s]]', name, name)
    end
end

local function check_template_at_start(text, template_set)
    local t_name = string.match(text, "^%s*{{%s*([^{}|]+)")
    if t_name then
        t_name = mw.text.trim(string.lower(t_name))
        t_name = string.gsub(t_name, "_", " ")
        if template_set[t_name] then return true end
    end
    return false
end

local function check_template_at_end(text, template_set)
    local rev_text = string.reverse(text)
    local rev_match = string.match(rev_text, "^%s*}}%s*([^}|{]+)%s*{{")
    if rev_match then
        local t_name = string.reverse(rev_match)
        t_name = mw.text.trim(string.lower(t_name))
        t_name = string.gsub(t_name, "_", " ")
        if template_set[t_name] then return true end
    end
    return false
end

-- ============================================================================
-- 章节解析逻辑
-- ============================================================================

local function analyze_section_text(text)
    local stats = {
        reply_count = 0,
        participants = {},
        participant_count = 0,
        last_time = 0,
        last_user = nil,
        status = 'normal'
    }

    if check_template_at_start(text, CONFIG.TEMPLATES.moved) then
        stats.status = 'moved'
    elseif check_template_at_start(text, CONFIG.TEMPLATES.archive_top) and 
           check_template_at_end(text, CONFIG.TEMPLATES.archive_bottom) then
        stats.status = 'archived'
    end

    local lowertext = string.lower(text)
    local x, y = 1, 1
    
    while true do
        x, y = string.find(text, CONFIG.DATEPATTERN, y + 1)
        if not x then break end

        local date_str = string.sub(text, x, y)
        local ts = date_to_timestamp(date_str)
        
        stats.reply_count = stats.reply_count + 1

        local t1, _ = rfind(text, '\n', x)
        t1 = t1 or 1
        local curtalk_segment = string.sub(lowertext, t1, x - 1)
        local found_user = nil
        
        for _, prefix in ipairs(CONFIG.USERPREFIX) do
            local u_start, u_end = rfind(curtalk_segment, '[[' .. prefix)
            if u_end then
                local name_end_marker = string.find(curtalk_segment, '[/#|%]]', u_end + 1)
                if name_end_marker then
                    found_user = string.sub(text, t1 + u_end, t1 + name_end_marker - 2)
                    local first = string.byte(found_user, 1, 1)
                    if first >= 97 and first <= 122 then
                        found_user = string.upper(string.char(first)) .. string.sub(found_user, 2)
                    end
                    break
                end
            end
        end
        
        found_user = found_user or "Unknown"

        if not stats.participants[found_user] then
            stats.participants[found_user] = true
            stats.participant_count = stats.participant_count + 1
        end

        if ts > stats.last_time then
            stats.last_time = ts
            stats.last_user = found_user
        end
    end

    return stats
end

-- ============================================================================
-- 主程序
-- ============================================================================

function p.main(frame)
    local args = frame.args
    local page_name = args.page or args[1]
    
    local content
    if page_name then
        local t = mw.title.new(page_name)
        if t then content = t:getContent() end
    else
        content = title_obj:getContent()
    end

    if not content then return "无法读取页面内容。" end
    content = "\n" .. content

    -- 解析章节
    local sections_data = {}
    local pos = 1
    local matches = {}
    
    while true do
        local s, e, title = string.find(content, "\n==%s*([^=].-[^=]?)%s*==[ \t]*\n", pos)
        if not s then break end
        table.insert(matches, {start_idx = s, end_idx = e, title = title})
        pos = e
    end

    for i, m in ipairs(matches) do
        local text_start = m.end_idx + 1
        local text_end = matches[i+1] and (matches[i+1].start_idx - 1) or #content
        local section_text = string.sub(content, text_start, text_end)
        table.insert(sections_data, {header = m.title, text = section_text})
    end

    if #sections_data == 0 then return "未发现二级标题讨论话题。" end

    -- 生成HTML
    local output = '\n'
    
    local root = mw.html.create('table')
    root:addClass('wikitable sortable mw-collapsible')
    
    -- 表头
    local header = root:tag('tr')
    
    -- #
    header:tag('th')
        :attr('data-sort-type', 'number')
        :css('font-weight', 'normal')
        :wikitext('<small>#</small>')
    
    -- 话题
    header:tag('th'):wikitext('💭 話題')
    
    -- 回复数
    header:tag('th')
        :wikitext('<span title="發言數/發言人次 (實際上為計算簽名數)">💬</span>')
        
    -- 参与人数
    header:tag('th')
        :wikitext('<span title="參與討論人數/發言人數">👥</span>')
        
    -- 最新发言 (用户)
    header:tag('th'):wikitext('🙋 最新發言')
    
    -- 最后更新 (时间)
    header:tag('th')
        :attr('data-sort-type', 'isoDate')
        :wikitext('<span title="最後更新">🕒 <small>(UTC+8)</small></span>')
        :css('white-space', 'nowrap')

    local index_counter = 0
    local now = os.time()

    for _, sec in ipairs(sections_data) do
        index_counter = index_counter + 1
        local stats = analyze_section_text(sec.text)
        
        local time_diff = now - stats.last_time
        local is_closed = (stats.status == 'archived' or stats.status == 'moved')
        local is_single_reply = (stats.reply_count == 1 and stats.participant_count == 1)
        
        -- 背景色逻辑
        local bg_color = CONFIG.colors['old'] -- 默认为灰色
        
        if is_closed then
            bg_color = CONFIG.colors['archived']
        elseif time_diff < 3600 then
            bg_color = CONFIG.colors['1h']
        elseif time_diff < 86400 then
            bg_color = CONFIG.colors['1d']
        elseif time_diff > 2592000 then
            bg_color = CONFIG.colors['very_old']
        end

        local row = root:tag('tr')
        if is_closed then
            row:css('text-decoration', 'line-through')
        end

        -- 1. 序号
        row:tag('td'):css('text-align', 'right'):wikitext(index_counter)

        -- 2. 话题
        local clean_display = clean_title_text(sec.header)
        local link_text = string.format("[[#%s|%s]]", clean_display, clean_display)
        
        local title_cell = row:tag('td'):css('max-width', '24em')
        -- 字数超过20变小
        if mw.ustring.len(clean_display) > 20 then
            title_cell:wikitext('<small>' .. link_text .. '</small>')
        else
            title_cell:wikitext(link_text)
        end

        -- 3. 回复数
        local reply_cell = row:tag('td'):css('text-align', 'right'):wikitext(stats.reply_count)
        if stats.reply_count >= 10 then
            reply_cell:css('background-color', CONFIG.colors['hot'])
        elseif is_single_reply then
            reply_cell:css('background-color', CONFIG.colors['warn'])
        end

        -- 4. 参与人数
        local part_cell = row:tag('td'):css('text-align', 'right'):wikitext(stats.participant_count)
        if is_single_reply then
            part_cell:css('background-color', CONFIG.colors['warn'])
        end

        -- 5. 最新发言人
        row:tag('td')
            :css('background-color', bg_color)
            :wikitext(format_user_link(stats.last_user))

        -- 6. 最后更新时间
        local time_cell = row:tag('td')
            :css('background-color', bg_color)
            :attr('data-sort-type', 'isoDate')
            
        if stats.last_time > 0 then
            local iso_val = os.date("!%Y-%m-%dT%H:%M:%S.000Z", stats.last_time)
            time_cell:attr('data-sort-value', iso_val)
            
            local tz_offset = 8 * 3600
            local local_ts = stats.last_time + tz_offset
            
            local date_part = os.date("!%Y-%m-%d", local_ts)
            local time_part = os.date("!%H:%M", local_ts)
            
            -- 时间颜色适配深色模式
            time_cell:wikitext(string.format('%s <span style="color: var(--color-progressive, #36c);">%s</span>', date_part, time_part))
        else
            time_cell:wikitext('-')
        end
    end
    
    output = output .. tostring(root)

    -- ========================================================================
    -- 图例生成
    -- ========================================================================
    
    local legend = mw.html.create('table')
    legend:addClass('wikitable mw-collapsible mw-collapsed')
    legend:css('float', 'left')
    legend:css('margin-left', '.5em')
    
    if args.no_time_legend then
        legend:css('display', 'none')
    end

    -- 表头1
    legend:tag('tr')
        :tag('th')
        :attr('title', 'From the latest bot edit')
        :wikitext('發言更新圖例')

    -- 辅助函数
    local function add_legend_row(color, text)
        local td = legend:tag('tr'):tag('td')
        if color then td:css('background-color', color) end
        td:wikitext('* ' .. text)
    end

    -- 添加各时间段 (使用 CONFIG 变量以适配深色模式)
    add_legend_row(CONFIG.colors['1h'],       '最近一小時內')
    add_legend_row(CONFIG.colors['1d'],       '最近一日內')
    add_legend_row(nil,                       '一週內')
    add_legend_row(CONFIG.colors['old'],      '一個月內')
    add_legend_row(CONFIG.colors['very_old'], '逾一個月')

    -- 特殊状态
    legend:tag('tr'):tag('th'):wikitext('特殊狀態')
    legend:tag('tr')
        :tag('td')
        :css('text-decoration', 'line-through')
        :wikitext('已移動至其他頁面<br />或完成討論之議題')

    return output .. tostring(legend)
end

return p