Module:曲师列表:修订间差异

Admin​(留言 | 贡献)
JSON 流水线更新渲染模块
标签:已被回退
Admin​(留言 | 贡献)
输出加 __NOCONTENTCONVERT__/__NOTITLECONVERT__:署名与曲名保持 UTF-8 原字,不做繁简变体转换(无需给名字加包裹,无 DOM 开销)
 
(未显示同一用户的16个中间版本)
第6行: 第6行:


local DATA_TITLE = '模板:曲目列表.json'
local DATA_TITLE = '模板:曲目列表.json'
local validator = require('模块:数据页校验')
local MAP_TITLE = '模板:曲师映射.json'
local MAP_TITLE = '模板:曲师映射.json'
local INTRO_TITLE = '模板:曲师简介'  -- 手工简介数据页(可编辑 wikitext)
local INTRO_TITLE = '模板:曲师简介'  -- 手工简介数据页(可编辑 wikitext)
第11行: 第13行:
local DIFFS = { 'CASUAL', 'TOUGH', 'INSANE', 'PRO' }
local DIFFS = { 'CASUAL', 'TOUGH', 'INSANE', 'PRO' }


-- 含分隔符但其实是"单人名义"的写法, 不拆分 (键为 norm() 后的值)
-- 拆词与归一化统一由 模块:署名解析 提供(本模块不再自带一份,避免规则漂移)
local NO_SPLIT = {
local parse = require('模块:署名解析')
['w/csθ'] = true,              -- 单人名, 含斜杠
local norm = parse.norm
['↑=192.333kb/s'] = true,      -- 单人名, 含斜杠
['t+pazolite'] = true,        -- 单人名, 含加号
}


-- ===== 工具 =====
-- ===== 工具 =====
第23行: 第22行:
end
end


local function norm(s)
-- 链接目标: # 是 MediaWiki 的锚点分隔符, 标题里一律换成全角 #
return (mw.ustring.lower(tostring(s or '')):gsub('%s+', ''))
-- 优先用「链接名」字段; 没有就回退到曲名并自动替换 (与 模块:曲目列表 保持一致)
local function pageName(song, suffix)
local n = song['链接名'] or song['曲名']
if suffix then
n = n .. '(' .. suffix .. ')'
end
return (n:gsub('#', '#'))
end
end


-- 拆分合作者: 先归一化 vs./feat./x 为 '/', 再按分隔符切, 并抽出括号注释
local function splitRaw(raw)
local out = {}
raw = tostring(raw or '')
raw = raw:gsub('%s+[Vv][Ss]%.?%s+', '/'):gsub('%s+[Ff][Ee][Aa][Tt]%.?%s*', '/')
raw = raw:gsub('%s+[Xx]%s+', '/')
for part in mw.text.gsplit(raw, '[/、,,&&%+]') do
part = mw.text.trim(part)
if part ~= '' then
local base, ann = mw.ustring.match(part, '^(.-)%s*[((]([^))]*)[))]')
if base then
table.insert(out, { base = mw.text.trim(base), ann = ann })
else
table.insert(out, { base = part, ann = '' })
end
end
end
return out
end


-- 排序分组: 0=符号/数字 1=拉丁 2=假名 3=汉字 4=其他
-- 排序分组: 0=符号/数字 1=拉丁 2=假名 3=汉字 4=其他
第100行: 第86行:


local function loadSongs()
local function loadSongs()
local data = loadJson(DATA_TITLE)
local data, err = validator.load(DATA_TITLE)
if not data then return nil end
if not data then return nil, err end
if data.songs then return data.songs end
local all = {}
return data
for _, s in ipairs(data.songs or {}) do all[#all + 1] = s end
for _, s in ipairs(data.special or {}) do all[#all + 1] = s end   -- 站长要求: special(石墩子)也计入曲师列表
return all
end
end


-- ===== 主渲染 =====
-- ===== 主渲染 =====
-- ===== 自动排行榜(子页面用): {{#invoke:曲师列表|rank|min=3}} =====
-- 实时统计每位主名义收录的曲目数;并列名次用 rowspan 合并;名字链到曲师列表对应小节
function p.rank(frame)
local args = frame.args or {}
local minN = tonumber(args['min']) or 3
local mode = args['mode'] or '署名'
local data = validator.load(DATA_TITLE)
if not data then return '<span class="error">数据页错误</span>' end
local map = loadJson(MAP_TITLE) or {}
local lk = parse.buildLookup(map)
local roles = loadJson('模板:曲师参与.json') or {}
local sets, display = {}, {}
local function touch(key, name)
if not sets[key] then sets[key] = {}; display[key] = name end
end
for _, song in ipairs(data.songs or {}) do
local raw = song['曲师']
if type(raw) == 'table' then raw = raw[1] end
local parts = parse.attribute(lk, raw) or parse.parts(raw)
local seen = {}
for _, pt in ipairs(parts) do
local key = norm(pt.base)
if key ~= '' and not seen[key] then
seen[key] = true
local m = (lk and lk.extra[key]) or map[key] or {}
touch(key, m['显示名'] or pt.base)
sets[key][song['曲名']] = true              -- 署名归属
end
end
if mode == '参与' then
for ak, songs in pairs(roles) do
if ak ~= '_说明' and type(songs) == 'table' and songs[song['曲名']] then
local m = (lk and lk.extra[ak]) or map[ak] or {}
touch(ak, m['显示名'] or ak)
sets[ak][song['曲名']] = true          -- 参与记录
end
end
end
end
local counts = {}
for k, set in pairs(sets) do
local n = 0
for _ in pairs(set) do n = n + 1 end
counts[k] = n
end
local byCount, nums = {}, {}
for k, c in pairs(counts) do
if c >= minN then
byCount[c] = byCount[c] or {}
table.insert(byCount[c], k)
end
end
for c in pairs(byCount) do table.insert(nums, c) end
table.sort(nums, function(a, b) return a > b end)
local out = { '{| class="wikitable"', '|+收录曲数量排行榜(' .. mode .. ')', '!名次!!数量!!名字' }
local emitted = 0
for _, c in ipairs(nums) do
local list = byCount[c]
table.sort(list, function(a, b)
return mw.ustring.lower(display[a]) < mw.ustring.lower(display[b])
end)
local r = emitted + 1
for i, k in ipairs(list) do
table.insert(out, '|-')
if i == 1 then
table.insert(out, '| rowspan="' .. #list .. '" |' .. r)
table.insert(out, '| rowspan="' .. #list .. '" |' .. c)
end
table.insert(out, '| [[' .. '曲师列表#' .. esc(display[k]) .. '|' .. esc(display[k]) .. ']]')
end
emitted = emitted + #list
end
table.insert(out, '|}')
if emitted == 0 then return '<span class="error">没有达到门槛(' .. minN .. ')的曲师</span>' end
return frame:preprocess('__NOCONTENTCONVERT__\n__NOTITLECONVERT__\n' .. table.concat(out, '\n'))
end
function p.render(frame)
function p.render(frame)
local songs = loadSongs()
local songs, songsErr = loadSongs()
if not songs then
if not songs then
return '<span class="error">数据页不存在: [[' .. DATA_TITLE .. ']]</span>'
return songsErr
end
end
local map = loadJson(MAP_TITLE) or {}
local map = loadJson(MAP_TITLE) or {}
local roles = loadJson('模板:曲师参与.json') or {}  -- 参与(作曲/作词/人声…):人工知识,并入备注
local lk = parse.buildLookup(map)
local intros = loadIntros()
local intros = loadIntros()


第120行: 第187行:
local raw = song['曲师']
local raw = song['曲师']
if type(raw) == 'table' then raw = raw[1] end
if type(raw) == 'table' then raw = raw[1] end
local parts = NO_SPLIT[norm(raw)] and { { base = raw, ann = '' } } or splitRaw(raw)
local parts = parse.attribute(lk, raw) or parse.parts(raw)
local seen = {}
local seen = {}
for _, pt in ipairs(parts) do
for _, pt in ipairs(parts) do
第127行: 第194行:
seen[key] = true
seen[key] = true
if not artists[key] then
if not artists[key] then
local m = map[key] or {}
local m = (lk and lk.extra[key]) or map[key] or {}
artists[key] = {
artists[key] = {
display = m['显示名'] or pt.base,
display = m['显示名'] or pt.base,
第133行: 第200行:
or intros[key] or intros[norm(m['显示名'] or pt.base)] or '',
or intros[key] or intros[norm(m['显示名'] or pt.base)] or '',
sort = m['排序键'] or '',
sort = m['排序键'] or '',
unconfirmed = m['_未确认'] or false,
songs = {},
songs = {},
}
}
table.insert(order, key)
table.insert(order, key)
end
end
table.insert(artists[key].songs, { song = song, ann = pt.ann, base = pt.base })
table.insert(artists[key].songs, { song = song, ann = pt.ann, base = pt.base, raw = raw, other = pt.other })
end
end
end
end
第150行: 第218行:


local out = {}
local out = {}
table.insert(out, "'''曲师列表''':本列表按名字顺序整理了参与游戏曲目制作的曲师及其作品。")
if not (frame.args and frame.args['intro'] == '0') then
table.insert(out, '若某首歌曲由多人合作完成,会分别列在每位曲师的条目下;'
table.insert(out, "'''曲师列表''':本列表按名字顺序整理了参与游戏曲目制作的曲师及其作品。")
.. '同一人的不同写法(含马甲)统一列在最常见的名称之下。')
table.insert(out, '若某首歌曲由多人合作完成,会分别列在每位曲师的条目下;'
table.insert(out, '曲包与曲目一览见[[曲目列表]]与[[曲包列表]]。\n')
.. '同一人的不同写法(含马甲)统一列在最常见的名称之下。')
-- 编辑指引: 仅登录编辑者可见(CSS: .mw-maint-box 默认隐藏, body.mw-editable 显示)
table.insert(out, '曲包与曲目一览见[[曲目列表]]与[[曲包列表]]。\n')
table.insert(out, '<div class="mw-maint-box mw-maint-todo"><b>编辑提示</b><br/>'
end
.. '本列表由[[模块:曲师列表]]自动生成,请勿直接编辑本页。'
.. '需要修改时请前往:显示名 / 排序 / 参与 → [[模板:曲师映射.json]];人物简介 → [[模板:曲师简介]]。</div>')


for _, key in ipairs(order) do
for _, key in ipairs(order) do
local a = artists[key]
local a = artists[key]
table.insert(out, '\n==' .. esc(a.display) .. '==')
table.insert(out, '\n==' .. esc(a.display) .. '==')
if a.unconfirmed then
table.insert(out, '<div style="font-size:92%;color:#b45309">主名义待确认</div>')
end
if a.intro ~= '' then
if a.intro ~= '' then
table.insert(out, a.intro)
table.insert(out, a.intro)
第169行: 第238行:
for _, item in ipairs(a.songs) do
for _, item in ipairs(a.songs) do
local s = item.song
local s = item.song
-- 备注: 合作者 + 括号注释
-- 备注: 参与(来自 模板:曲师参与.json) ;与…合作 ;名义:X
local rawS = type(s['曲师']) == 'table' and s['曲师'][1] or s['曲师']
local notes = {}
local notes = {}
local rawS = type(s['曲师']) == 'table' and s['曲师'][1] or s['曲师']
local r = roles[key]                      -- 参与数据以「艺术家」为键(= 该行归属的主名义)
local allParts = NO_SPLIT[norm(rawS)] and { { base = rawS, ann = '' } } or splitRaw(rawS)
if r and type(r[s['曲名']]) == 'table' then
for _, pt in ipairs(allParts) do
local rl = {}
if norm(pt.base) ~= key then table.insert(notes, pt.base) end
for _, x in ipairs(r[s['曲名']]) do rl[#rl + 1] = x end
if #rl > 0 then table.insert(notes, table.concat(rl, '、')) end
end
end
local note = ''
local others = {}
if #notes > 0 then note = '与 ' .. table.concat(notes, '、') .. ' 合作' end
if item.other then
if item.ann ~= '' and item.ann ~= nil then
for _, q in ipairs(item.other) do
note = (note ~= '' and (note .. ';') or '') .. item.ann
if parse.norm(q) ~= key then table.insert(others, q) end
end
else
for _, pt in ipairs(parse.parts(rawS)) do
if norm(pt.base) ~= key then table.insert(others, pt.base) end
end
end
end
if #others > 0 then table.insert(notes, '与 ' .. table.concat(others, '、') .. ' 合作') end
if item.ann ~= '' and item.ann ~= nil then table.insert(notes, item.ann) end
local note = table.concat(notes, ';')
table.insert(out, '|-')
table.insert(out, '|-')
table.insert(out, '| [[' .. '曲目:' .. esc(s['曲名']) .. '|' .. langWrap(s) .. ']] || '
table.insert(out, '| [[' .. '曲目:' .. esc(pageName(s)) .. '|' .. langWrap(s) .. ']] || '
.. esc(s['时长'] or '') .. ' || ' .. esc(s['BPM'] or '') .. ' || '
.. esc(s['时长'] or '') .. ' || ' .. esc(s['BPM'] or '') .. ' || '
.. '[[' .. '曲包列表#' .. esc(s['曲包'] or '') .. '|' .. esc(s['曲包'] or '') .. ']] || '
.. '[[' .. '曲包列表#' .. esc(s['曲包'] or '') .. '|' .. esc(s['曲包'] or '') .. ']] || '
第191行: 第270行:


table.insert(out, '\n[[Category:曲师]]')
table.insert(out, '\n[[Category:曲师]]')
return frame:preprocess(table.concat(out, '\n'))
return frame:preprocess('__NOCONTENTCONVERT__\n__NOTITLECONVERT__\n' .. table.concat(out, '\n'))
end
end


return p
return p