Module:曲目列表:修订间差异
JSON 流水线更新渲染模块 |
输出加 __NOCONTENTCONVERT__/__NOTITLECONVERT__:署名与曲名保持 UTF-8 原字,不做繁简变体转换(无需给名字加包裹,无 DOM 开销) |
||
| (未显示3个用户的14个中间版本) | |||
| 第3行: | 第3行: | ||
-- 数据页: 存 JSON 原文 (json 内容模型) | -- 数据页: 存 JSON 原文 (json 内容模型) | ||
local DATA_TITLE = '模板:曲目列表.json' | local DATA_TITLE = '模板:曲目列表.json' | ||
local validator = require('模块:数据页校验') | |||
local parse = require('模块:署名解析') -- 署名→主名义(马甲/合作名义归并) | |||
local ARTIST_MAP = '模板:曲师映射.json' | |||
local function esc(s) | local function esc(s) | ||
-- 防御 wikitext 特殊字符 (当前数据无, 以防未来出现) | -- 防御 wikitext 特殊字符 (当前数据无, 以防未来出现) | ||
return (s:gsub('|', '|')) | 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 | end | ||
| 第37行: | 第101行: | ||
end | end | ||
local function songRow(song, suffix) | local function songRow(song, suffix, lk, known) | ||
local art = song['曲绘'] or '' | local art = song['曲绘'] or '' | ||
if suffix then | if suffix then | ||
art = art:gsub('(' .. suffix .. ')$', '') .. '_' .. suffix | art = art:gsub('(' .. suffix .. ')$', '') .. '_' .. suffix | ||
end | end | ||
local link = esc(song | local link = esc(pageName(song, suffix)) | ||
local artist = | local artist = creditLinks(song['曲师'], lk, known) | ||
return '|-\n| [[File:曲绘-' .. norm(art) .. '.png|96px|link=曲目:' .. link | return '|-\n| [[File:曲绘-' .. norm(art) .. '.png|96px|link=曲目:' .. link | ||
.. ']] || [[' .. '曲目:' .. link .. '|' .. langWrap(song) .. ']] || ' | .. ']] || [[' .. '曲目:' .. link .. '|' .. langWrap(song) .. ']] || ' | ||
| 第54行: | 第118行: | ||
local function loadData() | local function loadData() | ||
-- 结构校验集中在 模块:数据页校验(缺 songs 时显红字 + 分类,不再静默空表) | |||
return validator.load(DATA_TITLE) | |||
end | end | ||
| 第69行: | 第126行: | ||
local data, err = loadData() | local data, err = loadData() | ||
if not data then | if not data then | ||
return | return tostring(err) | ||
end | end | ||
local out = { tableOpen() } | 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 | for _, song in ipairs(data.songs) do | ||
table.insert(out, songRow(song)) | table.insert(out, songRow(song, nil, lk, known)) | ||
end | end | ||
table.insert(out, '|}') | table.insert(out, '|}') | ||
section('教程', '游戏内新手/进阶教学关卡。', data.tutorial) | |||
local story = data.story or {} | |||
section('剧情模式 · 剧情歌曲', '剧情章节中出现的曲目。', story.songs) | |||
section('剧情模式 · 训练场', '剧情模式中的练习关卡(不计入定数与榜单)。', story.training) | |||
section('石墩子模式', '愚人节限定的特殊谱面,正常游玩时不会出现。石墩子模式中,会将游戏角色强行锁定为石墩子:', data.special, '石墩子模式') | |||
if not demomode then | if not demomode then | ||
| 第90行: | 第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 | ||