Module:曲目列表:修订间差异
外观
名义提示判定:只在真·马甲/组合标签/带注释时显示(排除纯写法差异) |
输出加 __NOCONTENTCONVERT__/__NOTITLECONVERT__:署名与曲名保持 UTF-8 原字,不做繁简变体转换(无需给名字加包裹,无 DOM 开销) |
||
| (未显示同一用户的3个中间版本) | |||
| 第36行: | 第36行: | ||
local function creditLinks(raw, lk, known) | local function creditLinks(raw, lk, known) | ||
if not raw or raw == '' then return '' end | if not raw or raw == '' then return '' end | ||
local linked = parse.linkify(raw, lk, { page = '曲师列表', known = known }) | |||
if linked then return linked end | |||
local parts = parse.attribute(lk, raw) or parse.parts(raw) | local parts = parse.attribute(lk, raw) or parse.parts(raw) | ||
local out = {} | local out = {} | ||
for _, pt in ipairs(parts) do | for _, pt in ipairs(parts) do | ||
local text = pt.rawPart or pt.base | |||
if known and not known[pt.base] then | if known and not known[pt.base] then | ||
out[#out + 1] = esc( | out[#out + 1] = esc(text) | ||
else | else | ||
out[#out + 1] = '[[曲师列表#' .. esc(pt.base) .. '|' .. esc( | out[#out + 1] = '[[曲师列表#' .. esc(pt.base) .. '|' .. esc(text) .. ']]' | ||
end | end | ||
end | end | ||
-- | -- 回退分支: | ||
local | -- ① 解析出多个主名义(标签式合作名义,如 rank.x / Kevin.B / ↑=192.333KB/s) | ||
if | -- → 「合作名义<br>主名义A链接 / 主名义B链接」 | ||
-- ② 单个主名义 → 直接显示(原名与主名义不同时补「(名义:X)」) | |||
local joined = table.concat(out, ' / ') | |||
if #parts > 1 then | |||
return esc(raw) .. '<br>' .. joined | |||
end | end | ||
if parse.creditNote(raw, lk) and parse.norm(raw) ~= parse.norm(parts[1].rawPart or parts[1].base) then | |||
return joined .. ' <span style="font-size:85%;color:#666">(名义:' .. esc(raw) .. ')</span>' | |||
end | |||
return joined | |||
end | end | ||
| 第150行: | 第159行: | ||
end | end | ||
-- Scribunto 返回文本中的 {{模板}} 不会被自动展开, 必须显式 preprocess | -- Scribunto 返回文本中的 {{模板}} 不会被自动展开, 必须显式 preprocess | ||
return frame:preprocess(table.concat(out, '\n')) | return frame:preprocess('__NOCONTENTCONVERT__\n__NOTITLECONVERT__\n' .. table.concat(out, '\n')) | ||
end | end | ||
return p | return p | ||
2026年10月3日 (六) 21:57的最新版本
此模块的文档可以在Module:曲目列表/doc创建
local p = {}
-- 数据页: 存 JSON 原文 (json 内容模型)
local DATA_TITLE = '模板:曲目列表.json'
local validator = require('模块:数据页校验')
local parse = require('模块:署名解析') -- 署名→主名义(马甲/合作名义归并)
local ARTIST_MAP = '模板:曲师映射.json'
local function esc(s)
-- 防御 wikitext 特殊字符 (当前数据无, 以防未来出现)
return (s:gsub('|', '|'))
end
local function loadJson(title)
local t = mw.title.new(title)
if not t or not t.exists then return nil end
local ok, data = pcall(mw.text.jsonDecode, t:getContent())
if not ok then return nil end
return data
end
-- 曲师列表里实际有小节的曲师(= songs/special 的曲师;教程/训练场的独占曲师不在其中)
local function knownArtists(data, lk)
local known = {}
local function eat(list)
for _, s in ipairs(list or {}) do
local parts = parse.attribute(lk, s['曲师']) or parse.parts(s['曲师'])
for _, pt in ipairs(parts) do known[pt.base] = true end
end
end
eat(data.songs); eat(data.special)
return known
end
-- 曲师署名 → 链到 [[曲师列表#主名义]](马甲/合作名义自动归并;无小节则纯文本)
local function creditLinks(raw, lk, known)
if not raw or raw == '' then return '' end
local linked = parse.linkify(raw, lk, { page = '曲师列表', known = known })
if linked then return linked end
local parts = parse.attribute(lk, raw) or parse.parts(raw)
local out = {}
for _, pt in ipairs(parts) do
local text = pt.rawPart or pt.base
if known and not known[pt.base] then
out[#out + 1] = esc(text)
else
out[#out + 1] = '[[曲师列表#' .. esc(pt.base) .. '|' .. esc(text) .. ']]'
end
end
-- 回退分支:
-- ① 解析出多个主名义(标签式合作名义,如 rank.x / Kevin.B / ↑=192.333KB/s)
-- → 「合作名义<br>主名义A链接 / 主名义B链接」
-- ② 单个主名义 → 直接显示(原名与主名义不同时补「(名义:X)」)
local joined = table.concat(out, ' / ')
if #parts > 1 then
return esc(raw) .. '<br>' .. joined
end
if parse.creditNote(raw, lk) and parse.norm(raw) ~= parse.norm(parts[1].rawPart or parts[1].base) then
return joined .. ' <span style="font-size:85%;color:#666">(名义:' .. esc(raw) .. ')</span>'
end
return joined
end
-- 链接目标: # 是 MediaWiki 的锚点分隔符, 标题里一律换成全角 #
-- 优先用「链接名」字段; 没有就回退到曲名并自动替换 (双保险)
-- 注意: 光用 <nowiki>#</nowiki> 之类的转义救不了 —— # 在标题解析阶段就被切走了
local function pageName(song, suffix)
local n = song['链接名'] or song['曲名']
if suffix then
n = n .. '(' .. suffix .. ')'
end
return (n:gsub('#', '#'))
end
local function norm(s)
-- 曲绘文件名规范: 空格 -> 下划线
return (s:gsub(' ', '_'))
end
local function langWrap(song)
local lang = song['lang']
if lang == 'ja' then
return '{{lang|ja|' .. esc(song['曲名']) .. '}}'
elseif lang == 'zh-Hant' then
return '{{lang|zh-Hant|' .. esc(song['曲名']) .. '}}'
end
return esc(song['曲名'])
end
local DIFF_CLASS = { CASUAL = 'cas', TOUGH = 'tou', INSANE = 'ins', PRO = 'pro' }
local function diffsCell(diffs)
if #diffs == 1 and diffs[1] == '?' then
return '? ? ?'
end
local out = {}
for _, d in ipairs(diffs) do
table.insert(out, '{{难度|' .. d .. '}}')
end
return table.concat(out, '<br>')
end
local function songRow(song, suffix, lk, known)
local art = song['曲绘'] or ''
if suffix then
art = art:gsub('(' .. suffix .. ')$', '') .. '_' .. suffix
end
local link = esc(pageName(song, suffix))
local artist = creditLinks(song['曲师'], lk, known)
return '|-\n| [[File:曲绘-' .. norm(art) .. '.png|96px|link=曲目:' .. link
.. ']] || [[' .. '曲目:' .. link .. '|' .. langWrap(song) .. ']] || '
.. artist .. ' || style="text-align:center" | ' .. diffsCell(song['难度'])
end
local function tableOpen()
return '{| class="wikitable sortable" style="width:100%"\n! 曲绘 !! 曲名 !! 曲师 !! style="text-align:center" | 难度'
end
local function loadData()
-- 结构校验集中在 模块:数据页校验(缺 songs 时显红字 + 分类,不再静默空表)
return validator.load(DATA_TITLE)
end
function p.render(frame)
local demomode = frame and frame.args['demo']
local data, err = loadData()
if not data then
return tostring(err)
end
local out = { tableOpen() }
local lk = parse.buildLookup(loadJson(ARTIST_MAP))
local known = knownArtists(data, lk)
local function section(title, intro, list, suffix)
if not list or #list == 0 then return end
table.insert(out, '\n== ' .. title .. ' ==')
if intro and intro ~= '' then table.insert(out, intro .. '\n') end
table.insert(out, tableOpen())
for _, song in ipairs(list) do
table.insert(out, songRow(song, suffix, lk, known))
end
table.insert(out, '|}')
end
for _, song in ipairs(data.songs) do
table.insert(out, songRow(song, nil, lk, known))
end
table.insert(out, '|}')
section('教程', '游戏内新手/进阶教学关卡。', data.tutorial)
local story = data.story or {}
section('剧情模式 · 剧情歌曲', '剧情章节中出现的曲目。', story.songs)
section('剧情模式 · 训练场', '剧情模式中的练习关卡(不计入定数与榜单)。', story.training)
section('石墩子模式', '愚人节限定的特殊谱面,正常游玩时不会出现。石墩子模式中,会将游戏角色强行锁定为石墩子:', data.special, '石墩子模式')
if not demomode then
table.insert(out, '\n[[Category:曲目]]')
end
-- Scribunto 返回文本中的 {{模板}} 不会被自动展开, 必须显式 preprocess
return frame:preprocess('__NOCONTENTCONVERT__\n__NOTITLECONVERT__\n' .. table.concat(out, '\n'))
end
return p