跳转到内容

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

来自Bleap Wiki
Admin​(留言 | 贡献)
JSON 流水线更新曲包渲染模块
 
Admin​(留言 | 贡献)
输出加 __NOCONTENTCONVERT__/__NOTITLECONVERT__:署名与曲名保持 UTF-8 原字,不做繁简变体转换(无需给名字加包裹,无 DOM 开销)
 
(未显示3个用户的8个中间版本)
第1行: 第1行:
local p = {}
local p = {}


-- 数据页: 曲包列表 (json 内容模型)
-- 单一数据源: 曲目列表.json (曲包信息由 songs 的 "曲包" 字段派生)
local DATA_TITLE = '模板:曲包列表.json'
local DATA_TITLE = '模板:曲目列表.json'
-- lang 标注单一来源: 曲目列表数据页的 lang 字段
local validator = require('模块:数据页校验')
local SONG_DATA_TITLE = '模板:曲目列表.json'
 
-- 曲包展示顺序 (游戏内上架顺序), 不在表中的曲包按名称排序追加到末尾
local PACK_ORDER = {
'夏日序曲 第一章', '夏日序曲 第二章', '夏日序曲 第三章',
'泡沫假日1号包', '数据突围1号包', '旅行伊始1号包', '神秘国度1号包',
'冬日欢歌1号包', '新年快乐1号包', '闪音集结1号包', '五光十色1号包',
'万象春来1号包', 'ChillMe!1号包', '时序织梦1号包',
'中国BOY1号包', 'Nanatsukaze1号包',
'免费单曲(商店)', '免费单曲', 'CORE基础卡',
}


local function esc(s)
local function esc(s)
第10行: 第19行:
end
end


local function loadData(title)
-- 链接目标: # 是 MediaWiki 的锚点分隔符, 标题里一律换成全角 #
local t = mw.title.new(title)
-- 优先用「链接名」字段; 没有就回退到曲名并自动替换 (双保险)
if not t or not t.exists then
-- 注意: 光用 <nowiki>&#35;</nowiki> 之类的转义救不了 —— # 在标题解析阶段就被切走了
return nil, '数据页不存在: [[' .. title .. ']]'
local function pageName(song, suffix)
local n = song['链接名'] or song['曲名']
if suffix then
n = n .. '(' .. suffix .. ')'
end
end
local ok, data = pcall(mw.text.jsonDecode, t:getContent())
return (n:gsub('#', '#'))
if not ok or type(data) ~= 'table' then
end
return nil, '数据页 JSON 解析失败: [[' .. title .. ']]'
 
end
local function norm(s)
return data
-- 图片文件名规范: 空格 -> 下划线
return (s:gsub(' ', '_'))
end
end


local function buildLangMap()
local function langWrap(song)
local langBy = {}
local lang = song['lang']
local songData = loadData(SONG_DATA_TITLE)
if lang == 'ja' then
if songData then
return '{{lang|ja|' .. esc(song['曲名']) .. '}}'
for _, s in ipairs(songData.songs or {}) do
elseif lang == 'zh-Hant' then
if s['lang'] then langBy[s['曲名']] = s['lang'] end
return '{{lang|zh-Hant|' .. esc(song['曲名']) .. '}}'
end
for _, s in ipairs(songData.special or {}) do
if s['lang'] then langBy[s['曲名']] = s['lang'] end
end
end
end
return langBy
return esc(song['曲名'])
end
end


function p.render(frame)
function p.render(frame)
local demomode = frame and frame.args['demo']
local demomode = frame and frame.args['demo']
local data, err = loadData(DATA_TITLE)
local data, dataErr = validator.load(DATA_TITLE)
if not data then
if not data then
return '<span class="error">' .. tostring(err) .. '</span>'
return dataErr
end
end


local langBy = buildLangMap()
-- 按曲包分组 (保持 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


local function disp(name)
-- 排序: PACK_ORDER 优先, 未知曲包按名称追加
local lang = langBy[name]
local orderMap = {}
if lang == 'ja' then
for i, name in ipairs(PACK_ORDER) do
return '{{lang|ja|' .. esc(name) .. '}}'
orderMap[name] = i
elseif lang == 'zh-Hant' then
end
return '{{lang|zh-Hant|' .. esc(name) .. '}}'
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
end
return esc(name)
return a < b
end
end)


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

2026年10月3日 (六) 21:57的最新版本

此模块的文档可以在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('|', '&#124;'))
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