模块:沙盒/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