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

local p = {}

-- 单一数据源: 曲目列表.json (曲包信息由 songs 的 "曲包" 字段派生)
local DATA_TITLE = '模板:曲目列表.json'
local validator = require('模块:数据页校验')

-- 曲包展示顺序 (游戏内上架顺序), 不在表中的曲包按名称排序追加到末尾
local PACK_ORDER = {
	'夏日序曲 第一章', '夏日序曲 第二章', '夏日序曲 第三章',
	'泡沫假日1号包', '数据突围1号包', '旅行伊始1号包', '神秘国度1号包',
	'冬日欢歌1号包', '新年快乐1号包', '闪音集结1号包', '五光十色1号包',
	'万象春来1号包', 'ChillMe!1号包', '时序织梦1号包',
	'中国BOY1号包', 'Nanatsukaze1号包',
	'免费单曲(商店)', '免费单曲', 'CORE基础卡',
}

local function esc(s)
	return (s:gsub('|', '|'))
end

-- 链接目标: # 是 MediaWiki 的锚点分隔符, 标题里一律换成全角 #
-- 优先用「链接名」字段; 没有就回退到曲名并自动替换 (双保险)
-- 注意: 光用 <nowiki>&#35;</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

function p.render(frame)
	local demomode = frame and frame.args['demo']
	local data, dataErr = validator.load(DATA_TITLE)
	if not data then
		return dataErr
	end

	-- 按曲包分组 (保持 songs 数组顺序)
	local packs = {}
	for _, song in ipairs(data.songs or {}) do
		local pk = song['曲包']
		if pk and pk ~= '' then
			packs[pk] = packs[pk] or {}
			table.insert(packs[pk], song)
		end
	end

	-- 排序: PACK_ORDER 优先, 未知曲包按名称追加
	local orderMap = {}
	for i, name in ipairs(PACK_ORDER) do
		orderMap[name] = i
	end
	local keys = {}
	for pk in pairs(packs) do
		table.insert(keys, pk)
	end
	table.sort(keys, function(a, b)
		local ra = orderMap[a] or (#PACK_ORDER + 1)
		local rb = orderMap[b] or (#PACK_ORDER + 1)
		if ra ~= rb then
			return ra < rb
		end
		return a < b
	end)

	local out = {}
	table.insert(out, '{| class="wikitable sortable" style="width:100%"')
	table.insert(out, '! 图片 !! 曲包 !! 曲目数 !! 包含曲目')
	for _, pk in ipairs(keys) do
		local songs = packs[pk]
		-- 图片: 曲包物品-<曲包名>.png, 不存在则显示 —
		local imgFile = '曲包物品-' .. norm(pk) .. '.png'
		local imgTitle = mw.title.new('File:' .. imgFile)
		local imgCell
		if imgTitle and imgTitle.exists then
			imgCell = '[[File:' .. esc(imgFile) .. '|80px]]'
		else
			imgCell = '—'
		end
		local links = {}
		for _, song in ipairs(songs) do
            table.insert(links, '[[' .. '曲目:' .. esc(pageName(song)) .. '|' .. langWrap(song) .. ']]')
		end
		-- 锚点: 其他页面可 [[曲包列表#<曲包名>]] 跳转到对应行
		table.insert(out, '|-\n| id=\"' .. esc(pk) .. '\" | ' .. imgCell .. ' || ' .. esc(pk)
			.. ' || ' .. tostring(#songs)
			.. ' || ' .. table.concat(links, ' / '))
	end
	table.insert(out, '|}')

	-- 特殊曲包: 教程 / 剧情模式(剧情歌曲+训练场) / 石墩子模式 —— 单一表格
	local story = data.story or {}
	local specialRows = {
		{ name = '教程', list = data.tutorial or {}, note = '教学关卡' },
		{ name = '剧情模式', list = story.songs or {}, note = '剧情章节曲目' },
		{ name = '训练场', list = story.training or {}, note = '剧情模式中的练习关卡' },
		{ name = '石墩子模式', list = data.special or {}, note = '愚人节限定谱面' },
	}
	local anyRow = false
	for _, r in ipairs(specialRows) do if #r.list > 0 then anyRow = true end end
	if anyRow then
		local function plainName(song)
			return song['链接名'] or song['曲名']
		end
		table.insert(out, '\n== 特殊曲包 ==')
		table.insert(out, '以下内容不属于常规曲包,单独列出:\n')
		table.insert(out, '{| class="wikitable sortable" style="width:100%"')
		table.insert(out, '! 图片 !! 曲包 !! 曲目数 !! 包含曲目')
		for _, r in ipairs(specialRows) do
			if #r.list > 0 then
				local imgFile = '曲包物品-' .. norm(r.name) .. '.png'
				local imgTitle = mw.title.new('File:' .. imgFile)
				local imgCell = (imgTitle and imgTitle.exists) and ('[[File:' .. esc(imgFile) .. '|80px]]') or '—'
				local links = {}
				for _, song in ipairs(r.list) do
					table.insert(links, '[[' .. '曲目:' .. esc(pageName(song)) .. '|' .. langWrap(song) .. ']]')
				end
				table.insert(out, '|-\n| id="' .. esc(r.name) .. '" | ' .. imgCell .. ' || ' .. esc(r.name)
					.. ' || ' .. tostring(#r.list) .. ' || ' .. table.concat(links, ' / '))
			end
		end
		table.insert(out, '|}')
	end

	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