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

-- 模块:谱师列表
-- 数据源: 模板:曲目列表.json (歌曲的 谱师 字段, 按难度) + 模板:谱师映射.json (显示名/简介/排序键)
-- 结构参照 Arcaea 中文维基 谱师列表: 按名字顺序分节, 每节 = 手工简介 + 谱面表
-- 表列: 曲名 / 所属曲包 / 难度 / 谱师名义 / 备注; 同一首歌的多个难度用 rowspan 合并
local p = {}

local DATA_TITLE = '模板:曲目列表.json'
local validator = require('模块:数据页校验')

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

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

-- 拆词与归一化统一由 模块:署名解析 提供(本模块不再自带一份,避免规则漂移)
local parse = require('模块:署名解析')
local norm = parse.norm

-- 曲包展示顺序 (与 模块:曲包列表 保持一致)
local PACK_ORDER = {
	'夏日序曲 第一章', '夏日序曲 第二章', '夏日序曲 第三章',
	'泡沫假日1号包', '数据突围1号包', '旅行伊始1号包', '神秘国度1号包',
	'冬日欢歌1号包', '新年快乐1号包', '闪音集结1号包', '五光十色1号包',
	'万象春来1号包', 'ChillMe!1号包', '时序织梦1号包',
	'中国BOY1号包', 'Nanatsukaze1号包',
	'免费单曲(商店)', '免费单曲', 'CORE基础卡',
}

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

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


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, err = validator.load(DATA_TITLE)
	if not data then return nil, err end
	return data.songs
end

-- ===== 主渲染 =====
-- ===== 自动排行榜(子页面用): {{#invoke:谱师列表|rank|min=3}} =====
-- 实时统计每位主名义制作的谱面数(曲目×难度);并列名次用 rowspan 合并;名字链到谱师列表对应小节
function p.rank(frame)
	local args = frame.args or {}
	local minN = tonumber(args['min']) or 3
	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 charts, songSet, display = {}, {}, {}
	for _, song in ipairs(data.songs or {}) do
		local st = song['谱师']
		if type(st) == 'table' then
			for _, d in ipairs(DIFFS) do
				local raw = st[d]
				if raw and raw ~= '' then
					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 {}
							display[key] = m['显示名'] or pt.base
							charts[key] = (charts[key] or 0) + 1
							songSet[key] = songSet[key] or {}
							songSet[key][song['曲名']] = true
						end
					end
				end
			end
		end
	end
	local byCount, nums = {}, {}
	for k, c in pairs(charts) 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"', '|+谱面数量排行榜', '!名次!!数量!!名字' }
	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)
	local songs, songsErr = loadSongs()
	if not songs then
		return songsErr
	end
	local map = loadJson(MAP_TITLE) or {}
	local lk = parse.buildLookup(map)
	local intros = loadIntros()

	local packIdx = {}
	for i, n in ipairs(PACK_ORDER) do packIdx[n] = i end

	-- 归集: key -> { display, intro, sort, entries = { {song, diff, raw, base, others} } }
	local artists, order = {}, {}
	for _, song in ipairs(songs) do
		local st = song['谱师']
		if type(st) == 'table' then
			for _, d in ipairs(DIFFS) do
				local raw = st[d]
				if raw and raw ~= '' then
					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
							if not artists[key] then
								local m = (lk and lk.extra[key]) or 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 '',
									unconfirmed = m['_未确认'] or false,
									entries = {},
								}
								table.insert(order, key)
							end
							-- 该难度的其他合作者(用于备注)
							local others = {}
							if pt.other then                     -- 新结构: 合作名义的成员表
								for _, q in ipairs(pt.other) do
									if parse.norm(q) ~= key then table.insert(others, q) end
								end
							else
								for _, q in ipairs(parts) do
									if norm(q.base) ~= key then table.insert(others, q.base) end
								end
							end
							table.insert(artists[key].entries, {
								song = song, diff = d, raw = raw,
								base = pt.base, ann = pt.ann, others = others,
							})
						end
					end
				end
			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 = {}
	if not (frame.args and frame.args['intro'] == '0') then
		table.insert(out, "'''谱师列表''':本列表按名字顺序整理了为游戏制作谱面的谱师及其谱面。")
		table.insert(out, '若一张谱面由多人合作完成,会分别列在每位谱师的条目下。'
			.. '曲目顺序依所属曲包(游戏内上架顺序)排序。\n')
	end

	for _, key in ipairs(order) do
			local a = artists[key]
			-- 曲目排序: 曲包顺序 → 曲名; 再把同一首歌的难度聚在一起
			table.sort(a.entries, function(x, y)
				local px = packIdx[x.song['曲包']] or (#PACK_ORDER + 1)
				local py = packIdx[y.song['曲包']] or (#PACK_ORDER + 1)
				if px ~= py then return px < py end
				if x.song['曲名'] ~= y.song['曲名'] then return x.song['曲名'] < y.song['曲名'] end
				local dx, dy = 0, 0
				for i, d in ipairs(DIFFS) do
					if d == x.diff then dx = i end
					if d == y.diff then dy = i end
				end
				return dx < dy
			end)

			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 table.insert(out, a.intro) end
		table.insert(out, '{| class="wikitable sortable" style="width:100%"')
		table.insert(out, '! 曲名 !! 所属曲包 !! 难度 !! 谱师名义 !! 备注')

		-- 按曲目分组计算 rowspan
		local i, n = 1, #a.entries
		while i <= n do
			local song = a.entries[i].song
			local j = i
			while j <= n and a.entries[j].song['曲名'] == song['曲名'] do j = j + 1 end
			local span = j - i
			for k = i, j - 1 do
				local e = a.entries[k]
				local note = ''
				if #e.others > 0 then note = '与 ' .. table.concat(e.others, '、') .. ' 合作' end
				if e.ann ~= '' and e.ann ~= nil then
					note = (note ~= '' and (note .. ';') or '') .. e.ann
				end
				table.insert(out, '|-')
				-- 单元格内容(不含前导竖线): 首格由行首的 '|' 引导, 后续用 ' || ' 分隔
				local cells = {}
				local songCell = '[[' .. '曲目:' .. esc(pageName(song)) .. '|' .. langWrap(song) .. ']]'
				local packCell = '[[' .. '曲包列表#' .. esc(song['曲包'] or '') .. '|'
					.. esc(song['曲包'] or '') .. ']]'
				if span > 1 and k == i then
					table.insert(cells, 'rowspan="' .. span .. '" | ' .. songCell)
					table.insert(cells, 'rowspan="' .. span .. '" | ' .. packCell)
				elseif span == 1 then
					table.insert(cells, songCell)
					table.insert(cells, packCell)
				end
				table.insert(cells, '{{难度|' .. e.diff .. '}}')
				table.insert(cells, esc(e.raw))
				table.insert(cells, esc(note))
				table.insert(out, '| ' .. table.concat(cells, ' || '))
			end
			i = j
		end
		table.insert(out, '|}')
	end

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

return p