Module:曲师列表

Admin​(留言 | 贡献)2026年9月11日 (五) 00:01的版本 (JSON 流水线更新渲染模块)

此模块的文档可以在Module:曲师列表/doc创建

-- 模块:曲师列表
-- 数据源: 模板:曲目列表.json (歌曲的 曲师 字段) + 模板:曲师映射.json (显示名/简介/排序键)
-- 结构参照 Arcaea 中文维基 曲师列表: 按名字顺序分节, 每节 = 手工简介 + 作品表
-- 排序: 分组为 符号 → 拉丁字母 → 假名 → 汉字, 组内按名字; 可用映射表 "排序键" 覆盖
local p = {}

local DATA_TITLE = '模板:曲目列表.json'
local MAP_TITLE = '模板:曲师映射.json'
local INTRO_TITLE = '模板:曲师简介'   -- 手工简介数据页(可编辑 wikitext)

local DIFFS = { 'CASUAL', 'TOUGH', 'INSANE', 'PRO' }

-- 含分隔符但其实是"单人名义"的写法, 不拆分 (键为 norm() 后的值)
local NO_SPLIT = {
	['w/csθ'] = true,              -- 单人名, 含斜杠
	['↑=192.333kb/s'] = true,      -- 单人名, 含斜杠
	['t+pazolite'] = true,         -- 单人名, 含加号
}

-- ===== 工具 =====
local function esc(s)
	return (tostring(s or ''):gsub('|', '|'))
end

local function norm(s)
	return (mw.ustring.lower(tostring(s or '')):gsub('%s+', ''))
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=其他
local function groupRank(name)
	local cp = mw.ustring.codepoint(name, 1, 1)
	if not cp then return 9 end
	if (cp >= 0x30 and cp <= 0x39) or cp < 0x30 then return 0 end
	if (cp >= 0x41 and cp <= 0x5A) or (cp >= 0x61 and cp <= 0x7A) then return 1 end
	if (cp >= 0x3040 and cp <= 0x30FF) then return 2 end
	if (cp >= 0x3400 and cp <= 0x9FFF) then return 3 end
	return 4
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 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

-- 解析简介数据页: 每个 == 名字 == 小节为该名字的介绍(wikitext 原样输出)
local function loadIntros()
	local t = mw.title.new(INTRO_TITLE)
	if not t or not t.exists then return {} end
	local content = (t:getContent() or ''):gsub('<noinclude>.-</noinclude>', '')
	local map, cur = {}, nil
	for line in mw.text.gsplit(content, '\n') do
		local h = line:match('^==%s*(.-)%s*==%s*$')
		if h then
			cur = norm(h)
			map[cur] = ''
		elseif cur then
			map[cur] = map[cur] .. line .. '\n'
		end
	end
	for k, v in pairs(map) do
		v = mw.text.trim(v)
		v = v:gsub('<!%-%-.-%-%->', '')   -- 去掉占位注释
		map[k] = mw.text.trim(v)
	end
	return map
end

local function loadSongs()
	local data = loadJson(DATA_TITLE)
	if not data then return nil end
	if data.songs then return data.songs end
	return data
end

-- ===== 主渲染 =====
function p.render(frame)
	local songs = loadSongs()
	if not songs then
		return '<span class="error">数据页不存在: [[' .. DATA_TITLE .. ']]</span>'
	end
	local map = loadJson(MAP_TITLE) or {}
	local intros = loadIntros()

	-- 归集: key -> { display, intro, sort, songs = { {song=..., ann=...} } }
	local artists, order = {}, {}
	for _, song in ipairs(songs) do
		local raw = song['曲师']
		if type(raw) == 'table' then raw = raw[1] end
		local parts = NO_SPLIT[norm(raw)] and { { base = raw, ann = '' } } or splitRaw(raw)
		local seen = {}
		for _, pt in ipairs(parts) do
			local key = norm(pt.base)
			if key ~= '' and not seen[key] then
				seen[key] = true
				if not artists[key] then
					local m = map[key] or {}
					artists[key] = {
						display = m['显示名'] or pt.base,
						intro = (m['简介'] and m['简介'] ~= '') and m['简介']
							or intros[key] or intros[norm(m['显示名'] or pt.base)] or '',
						sort = m['排序键'] or '',
						songs = {},
					}
					table.insert(order, key)
				end
				table.insert(artists[key].songs, { song = song, ann = pt.ann, base = pt.base })
			end
		end
	end

	local function sortKey(key)
		local a = artists[key]
		if a.sort ~= '' then return '0' .. a.sort end
		return string.format('%d_%s', groupRank(a.display), mw.ustring.lower(a.display))
	end
	table.sort(order, function(x, y) return sortKey(x) < sortKey(y) end)

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

	for _, key in ipairs(order) do
		local a = artists[key]
		table.insert(out, '\n==' .. esc(a.display) .. '==')
		if a.intro ~= '' then
			table.insert(out, a.intro)
		end
		table.insert(out, '{| class="wikitable sortable" style="width:100%"')
		table.insert(out, '! 曲目名称 !! 时长 !! BPM !! 曲包 !! 备注')
		for _, item in ipairs(a.songs) do
			local s = item.song
			-- 备注: 合作者 + 括号注释
			local notes = {}
			local rawS = type(s['曲师']) == 'table' and s['曲师'][1] or s['曲师']
			local allParts = NO_SPLIT[norm(rawS)] and { { base = rawS, ann = '' } } or splitRaw(rawS)
			for _, pt in ipairs(allParts) do
				if norm(pt.base) ~= key then table.insert(notes, pt.base) end
			end
			local note = ''
			if #notes > 0 then note = '与 ' .. table.concat(notes, '、') .. ' 合作' end
			if item.ann ~= '' and item.ann ~= nil then
				note = (note ~= '' and (note .. ';') or '') .. item.ann
			end
			table.insert(out, '|-')
			table.insert(out, '| [[' .. '曲目:' .. esc(s['曲名']) .. '|' .. langWrap(s) .. ']] || '
				.. esc(s['时长'] or '') .. ' || ' .. esc(s['BPM'] or '') .. ' || '
				.. '[[' .. '曲包列表#' .. esc(s['曲包'] or '') .. '|' .. esc(s['曲包'] or '') .. ']] || '
				.. esc(note))
		end
		table.insert(out, '|}')
	end

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

return p