- Welcome to Touhou Wiki!
- Registering is temporarily disabled. Check in our Discord server to request an account and for assistance of any kind.
Module:Lyrics2: Difference between revisions
Jump to navigation
Jump to search
DennouNeko (talk | contribs) m (small width fix) |
DracoGideon (talk | contribs) (style the element) |
||
(20 intermediate revisions by 5 users not shown) | |||
Line 4: | Line 4: | ||
-- == HELPER FUNCTIONS == | -- == HELPER FUNCTIONS == | ||
require("Module:Common") | local common = require("Module:Common") | ||
require("Module:Lib UTF8") | local utf8 = require("Module:Lib UTF8") | ||
-- minor bugfixes, mw.text seem to be undefined at the time of this update | |||
mw.text2 = require("Module:MW.text") | |||
-- "Module:Common" candidate? | -- "Module:Common" candidate? | ||
Line 25: | Line 28: | ||
ret[#ret+1] = '<table' | ret[#ret+1] = '<table' | ||
if isset(options['class']) then ret[#ret+1] = ' class="' .. options['class'] .. '"' end | if common.isset(options['class']) then ret[#ret+1] = ' class="' .. options['class'] .. '"' end | ||
if isset(options['style']) then ret[#ret+1] = ' style="' .. options['style'] .. '"' end | if common.isset(options['style']) then ret[#ret+1] = ' style="' .. options['style'] .. '"' end | ||
if isset(options['spacing']) then ret[#ret+1] = ' cellspacing="' .. options['spacing'] .. '"' end | if common.isset(options['spacing']) then ret[#ret+1] = ' cellspacing="' .. options['spacing'] .. '"' end | ||
ret[#ret+1] = '>' | ret[#ret+1] = '>' | ||
Line 35: | Line 38: | ||
ret[#ret+1] = '<tr>' | ret[#ret+1] = '<tr>' | ||
for k,v in pairs(tbl) do | for k,v in pairs(tbl) do | ||
if colnum > 0 and math. | if colnum > 0 and math.fmod(colnum, cols) == 0 then | ||
ret[#ret+1] = '</tr><tr>' | ret[#ret+1] = '</tr><tr>' | ||
end | end | ||
Line 43: | Line 46: | ||
if cols ~= nil then | if cols ~= nil then | ||
while math. | while math.fmod(colnum, cols) ~= 0 do | ||
ret[#ret+1] = '<td></td>' | ret[#ret+1] = '<td></td>' | ||
colnum = colnum + 1 | colnum = colnum + 1 | ||
Line 63: | Line 66: | ||
local function scanForAlbums(frame) | local function scanForAlbums(frame) | ||
local idx = {} | local idx = {} | ||
local args = | local args = frame.args | ||
local albumList = {} | local albumList = {} | ||
for k | for k in pairs(args) do | ||
s,e,t = string.find(k, '^album(%d+)$') | s,e,t = string.find(k, '^album(%d+)$') | ||
if s ~= nil then | if s ~= nil then | ||
idx | table.insert(idx, t) | ||
end | end | ||
end | end | ||
Line 76: | Line 78: | ||
table.sort(idx) | table.sort(idx) | ||
for k | for k in pairs(idx) do | ||
albumList | table.insert (albumList, args["album"..k]) | ||
end | end | ||
Line 95: | Line 97: | ||
s,e,t = string.find(v, '%[%[(.+)%]%]') | s,e,t = string.find(v, '%[%[(.+)%]%]') | ||
if s ~= nil then | if s ~= nil then | ||
tl = | tl = mw.text2.split(t, '|') | ||
for k1,v1 in pairs(tl) do | for k1,v1 in pairs(tl) do | ||
s,e,t = string.find(v1, '^%s-link%s-=%s-(.+)%s-$') | s,e,t = string.find(v1, '^%s-link%s-=%s-(.+)%s-$') | ||
Line 126: | Line 128: | ||
--[[ Partial name normalization. ]] | --[[ Partial name normalization. ]] | ||
local function normalize(text) | local function normalize(text) | ||
if isset(text) then | if common.isset(text) then | ||
-- full-width characters to ASCII equivalents | -- full-width characters to ASCII equivalents | ||
text = | text = utf8.replace_char(text, common.normalization_table) | ||
-- remove any HTML tags | -- remove any HTML tags | ||
text = | text = common.stripTags(text) | ||
-- any whitespace chain into single space | -- any whitespace chain into single space | ||
text = string.gsub(text, '%s+', ' ') | text = string.gsub(text, '%s+', ' ') | ||
-- remove preceding and trailing spaces | -- remove preceding and trailing spaces | ||
text = | text = mw.text.trim(text) | ||
end | end | ||
return text | return text | ||
Line 141: | Line 143: | ||
--[[ Loads and parses a JavaScript table "song_info" from a script with given name. ]] | --[[ Loads and parses a JavaScript table "song_info" from a script with given name. ]] | ||
local function load_info(name) | local function load_info(name) | ||
local tstr = | local tstr = common.getPage(name) | ||
if tstr == nil then return ret end | if tstr == nil then return ret end | ||
Line 147: | Line 149: | ||
local s,e,t,t1,t2 | local s,e,t,t1,t2 | ||
s,e,t = string.find(tstr, ' | s,e,t = string.find(tstr, '{(.+)}') | ||
if s ~= nil then | if s ~= nil then | ||
for n,lst in string.gmatch(t, '"(.-[^\\])"%s-:%s-{(.-)}') do | for n,lst in string.gmatch(t, '"(.-[^\\])"%s-:%s-{(.-)}') do | ||
Line 189: | Line 191: | ||
end | end | ||
end | end | ||
if isset(source) and isset(titlen) then | if common.isset(source) and common.isset(titlen) then | ||
if info_list[source] ~= nil then | if common.isset(info_list) and info_list[source] ~= nil then | ||
if isset(info_list[source][titlen]) then | if common.isset(info_list[source][titlen]) then | ||
local extra = info_list[source][titlen] | local extra = info_list[source][titlen] | ||
s,e,t,t1 = string.find(extra, '^(.*)%s-%[(.+)%]') | s,e,t,t1 = string.find(extra, '^(.*)%s-%[(.+)%]') | ||
Line 197: | Line 199: | ||
if s == nil then t = extra end | if s == nil then t = extra end | ||
if s ~= nil and isset(t1) then | if s ~= nil and common.isset(t1) then | ||
local lst = | local lst = mw.text2.split(t1, ';') | ||
if | if common.exists(lst[#lst]) then | ||
lst[#lst] = '[ | lst[#lst] = '[[' .. lst[#lst] .. ']]' | ||
end | end | ||
extra = table.concat(lst,'; ') | extra = table.concat(lst,'; ') | ||
--if isset(extra) then table.insert(elems, sourcek+1, '* notes: ' .. extra) end | --if common.isset(extra) then table.insert(elems, sourcek+1, '* notes: ' .. extra) end | ||
if isset(extra) then elems[sourcek] = elems[sourcek] .. '<br/>' .. extra end | if common.isset(extra) then elems[sourcek] = elems[sourcek] .. '<br/>' .. extra end | ||
end | end | ||
if isset(t) then | if common.isset(t) then | ||
elems[titlek] = tt .. frame:expandTemplate{title = 'h:title', args = {title, t} } | elems[titlek] = tt .. frame:expandTemplate{title = 'h:title', args = {title, t} } | ||
end | end | ||
Line 235: | Line 237: | ||
for k, v in frame:argumentPairs() do | for k, v in frame:argumentPairs() do | ||
if isset(v) then | if common.isset(v) then | ||
s,e,t = string.find(k, '^eng(%d+)$') | s,e,t = string.find(k, '^eng(%d+)$') | ||
if s ~= nil then | if s ~= nil then | ||
Line 261: | Line 263: | ||
-- sort indexes and remove repeating ones | -- sort indexes and remove repeating ones | ||
common.trunkTable(idx) | |||
for k,v in pairs(idx) do | for k,v in pairs(idx) do | ||
local tmp = {} | local tmp = {} | ||
tmp['eng'] = | tmp['eng'] = common.cv(common.isset(eng[v]), eng[v], ' ') | ||
tmp['rom'] = | tmp['rom'] = common.cv(common.isset(rom[v]), rom[v], ' ') | ||
tmp['kan'] = | tmp['kan'] = common.cv(common.isset(kan[v]), kan[v], ' ') | ||
stanzaList[#stanzaList+1] = tmp | stanzaList[#stanzaList+1] = tmp | ||
end | end | ||
Line 288: | Line 290: | ||
local sortkey = "" | local sortkey = "" | ||
if(isset(frame.args.titleen)) then | if(common.isset(frame.args.titleen)) then | ||
sortkey = frame.args.titleen | sortkey = frame.args.titleen | ||
elseif(isset(frame.args.titlerom)) then | elseif(common.isset(frame.args.titlerom)) then | ||
sortkey = frame.args.titlerom | sortkey = frame.args.titlerom | ||
elseif(isset(frame.args.titlejp)) then | elseif(common.isset(frame.args.titlejp)) then | ||
sortkey = frame.args.titlejp | sortkey = frame.args.titlejp | ||
end | end | ||
return | return common.cv(common.isset(sortkey), "{{DEFAULTSORT:" .. string.gsub(string.lower(sortkey), '^%l', string.upper) .. "}}", "") | ||
end | end | ||
Line 308: | Line 310: | ||
-- display title | -- display title | ||
if isset(frame.args.titleen) or isset(frame.args.titlejp) then | if common.isset(frame.args.titleen) or common.isset(frame.args.titlejp) then | ||
hr[#hr+1] = "'''" .. | hr[#hr+1] = "'''" .. common.cv(frame.args.titleen, frame.args.titleen, frame.args.titlejp) .. "'''" | ||
end | end | ||
if isset(frame.args.group) then hr[#hr+1] = " by " .. frame.args.group end | if common.isset(frame.args.group) then hr[#hr+1] = " by " .. frame.args.group end | ||
hr[#hr+1] = '</th>\n</tr>' | hr[#hr+1] = '</th>\n</tr>' | ||
Line 333: | Line 335: | ||
local lnk | local lnk | ||
for k,v in pairs(albumNames) do | for k,v in pairs(albumNames) do | ||
if isset(alist[v.idx]) then | if common.isset(alist[v.idx]) then | ||
local link = v.name | local link = v.name | ||
if isset(v.link) then link = v.link .. '|' .. v.name end | if common.isset(v.link) then link = v.link .. '|' .. v.name end | ||
alist[v.idx] = alist[v.idx] .. '<br/>[ | alist[v.idx] = alist[v.idx] .. '<br/>[[' .. link .. ']]' | ||
end | end | ||
end | end | ||
ir[#ir+1] = '\n<div style="float: right; clear: right;">' | ir[#ir+1] = '\n<div class="lyrics_albums" style="float: right; clear: right;">' | ||
ir[#ir+1] = "Featured in: \n:" | ir[#ir+1] = "Featured in: \n:" | ||
ir[#ir+1] = table.build_table(alist, {cols = 3, spacing = 4, style="text-align: center;"}) | ir[#ir+1] = table.build_table(alist, {cols = 3, spacing = 4, style="text-align: center;"}) | ||
Line 347: | Line 349: | ||
-- yes, these newlines are required for the MW parser to correctly interpret start-of-line entities like * or # for lists | -- yes, these newlines are required for the MW parser to correctly interpret start-of-line entities like * or # for lists | ||
if isset(frame.args.titleen) and isset(frame.args.titlejp) then | if common.isset(frame.args.titleen) and common.isset(frame.args.titlejp) then | ||
s,e,t = string.find(frame.args.titlejp, '<span lang=') -- lang template output is already expanded | s,e,t = string.find(frame.args.titlejp, '<span lang=') -- lang template output is already expanded | ||
if s ~= nil then -- lang template already included | if s ~= nil then -- lang template already included | ||
Line 355: | Line 357: | ||
end | end | ||
end | end | ||
if isset(frame.args.titlerom) then | if common.isset(frame.args.titlerom) then | ||
cc[#cc+1] = frame:preprocess("\n*''" .. frame.args.titlerom .. "''") | cc[#cc+1] = frame:preprocess("\n*''" .. frame.args.titlerom .. "''") | ||
end | end | ||
if isset(frame.args.length) then | if common.isset(frame.args.length) then | ||
cc[#cc+1] = frame:preprocess("\n*length: " .. frame.args.length) | cc[#cc+1] = frame:preprocess("\n*length: " .. frame.args.length) | ||
end | end | ||
if isset(frame.args.arranger) then | if common.isset(frame.args.arranger) then | ||
cc[#cc+1] = frame:preprocess("\n*arrangement: " .. frame.args.arranger) | cc[#cc+1] = frame:preprocess("\n*arrangement: " .. frame.args.arranger) | ||
end | end | ||
if isset(frame.args.lyricist) then | if common.isset(frame.args.lyricist) then | ||
cc[#cc+1] = frame:preprocess("\n*lyrics: " .. frame.args.lyricist) | cc[#cc+1] = frame:preprocess("\n*lyrics: " .. frame.args.lyricist) | ||
end | end | ||
if isset(frame.args.vocalist) then | if common.isset(frame.args.vocalist) then | ||
cc[#cc+1] = frame:preprocess("\n*vocals: " .. frame.args.vocalist) | cc[#cc+1] = frame:preprocess("\n*vocals: " .. frame.args.vocalist) | ||
end | end | ||
if isset(frame.args.other_staff) then | if common.isset(frame.args.other_staff) then | ||
cc[#cc+1] = frame:preprocess("\n" .. frame.args.other_staff) | cc[#cc+1] = frame:preprocess("\n" .. frame.args.other_staff) | ||
end | end | ||
if isset(frame.args.source) then | if common.isset(frame.args.source) then | ||
cc[#cc+1] = frame:preprocess("\n" .. frame.args.source) | cc[#cc+1] = frame:preprocess("\n" .. frame.args.source) | ||
end | end | ||
local elems = | local elems = mw.text2.split(table.concat(cc), '\n') -- to make sure that it's a "one element per line" table | ||
-- process the elements | -- process the elements | ||
Line 398: | Line 400: | ||
local eir = {} | local eir = {} | ||
if isset(frame.args.extra_info) then | if common.isset(frame.args.extra_info) then | ||
eir[#eir+1] = '\n<tr>\n<th class="incell" colspan="' .. cols['colnum'] .. '">Additional Info</th>\n</tr>' | eir[#eir+1] = '\n<tr>\n<th class="incell" colspan="' .. cols['colnum'] .. '">Additional Info</th>\n</tr>' | ||
eir[#eir+1] = '\n<tr>\n<td style="text-align: justify" colspan="' .. cols['colnum'] .. '">' | eir[#eir+1] = '\n<tr>\n<td style="text-align: justify" colspan="' .. cols['colnum'] .. '">' | ||
Line 414: | Line 416: | ||
local srow = {} | local srow = {} | ||
local lang = 'ja' | local lang = 'ja' | ||
if isset(frame.args['lang']) then | if common.isset(frame.args['lang']) then | ||
lang = | lang = common.cv(frame.args['lang'] == 'none', '', frame.args['lang']) | ||
end | end | ||
Line 442: | Line 444: | ||
srow[#srow+1] = ' style="width: 35%"' | srow[#srow+1] = ' style="width: 35%"' | ||
-- end | -- end | ||
srow[#srow+1] = | srow[#srow+1] = common.cv(cols['single'], '>English</th>', '>Translation</th>') | ||
end | end | ||
Line 452: | Line 454: | ||
if cols['kan'] then | if cols['kan'] then | ||
srow[#srow+1] = '<td' | srow[#srow+1] = '<td' | ||
if(isset(lang)) then srow[#srow+1] = ' lang="' .. lang .. '" xml:lang="' .. lang .. '"' end | if(common.isset(lang)) then srow[#srow+1] = ' lang="' .. lang .. '" xml:lang="' .. lang .. '"' end | ||
srow[#srow+1] = '>\n' .. stanza['kan'] .. '\n</td>' | srow[#srow+1] = '>\n' .. stanza['kan'] .. '\n</td>' | ||
end | end | ||
Line 467: | Line 469: | ||
end | end | ||
if isset(frame.args['lyrics_source']) then | if common.isset(frame.args['lyrics_source']) then | ||
srow[#srow+1] = '<tr><td colspan="' .. cols['colnum'] .. '" style="text-align: center; font-size: 83%;">' | srow[#srow+1] = '<tr><td colspan="' .. cols['colnum'] .. '" style="text-align: center; font-size: 83%;">' | ||
srow[#srow+1] = "\n:''Lyrics source: " .. frame.args['lyrics_source'] .. "''" | srow[#srow+1] = "\n:''Lyrics source: " .. frame.args['lyrics_source'] .. "''" | ||
Line 480: | Line 482: | ||
local nrow = {} | local nrow = {} | ||
if isset(frame.args['notes']) then | if common.isset(frame.args['notes']) then | ||
nrow[#nrow+1] = '\n<tr><th class="incell" colspan="' .. cols['colnum'] .. '"></th></tr>' | nrow[#nrow+1] = '\n<tr><th class="incell" colspan="' .. cols['colnum'] .. '"></th></tr>' | ||
nrow[#nrow+1] = '\n<tr><td style="text-align: justify" colspan="' .. cols['colnum'] .. '">' | nrow[#nrow+1] = '\n<tr><td style="text-align: justify" colspan="' .. cols['colnum'] .. '">' | ||
Line 517: | Line 519: | ||
local function lyricsCategories(frame, cols, albumNames) | local function lyricsCategories(frame, cols, albumNames) | ||
local cats = {''} -- so the first category won't disappear | local cats = {''} -- so the first category won't disappear | ||
local group | |||
local romanizable = not isset(frame.args['lang']) or ( | local romanizable = not common.isset(frame.args['lang']) or (common.isInTable(common.romanizable, frame.args['lang']) ~= nil) | ||
local skip_rom = false | local skip_rom = false | ||
if isset(frame.args['eng_only']) then skip_rom = true end | if common.isset(frame.args['eng_only']) then skip_rom = true end | ||
if isset(frame.args['lang']) and not romanizable then skip_rom = true end | if common.isset(frame.args['lang']) and not romanizable then skip_rom = true end | ||
cats[#cats+1] = frame:preprocess(calculateDefaultSort(frame)) | cats[#cats+1] = frame:preprocess(calculateDefaultSort(frame)) | ||
-- escaping the second '[', so parser won't assume it's a category | -- escaping the second '[', so parser won't assume it's a category | ||
cats[#cats+1] = '[ | cats[#cats+1] = '[[Category:Lyrics]]' | ||
if isset(frame.args[' | if common.isset(frame.args['group_en']) then | ||
group = frame.args['group_en'] | |||
elseif common.isset(frame.args['group']) then | |||
group = frame.args['group'] | |||
end | |||
if common.isset(group) then | |||
local groups = {} | |||
local s1, s,e,t | |||
-- in case of simple links | -- extract the group names from links | ||
s1 = 0 | |||
while true do | |||
s,e,t = string.find(group, '%[%[.-|(.-)%]%]', s1 + 1) | |||
-- in case of simple links | |||
if s == nil then | |||
s,e,t = string.find(group, '%[%[(.-)%]%]', s1 + 1) | |||
end | |||
if s == nil then break end | |||
s1 = e | |||
groups[#groups+1] = t | |||
end | |||
-- | -- if group is not a link, assume it's plain name | ||
if #groups < 1 then groups[1] = group end | |||
for k,v in pairs(groups) do | |||
cats[#cats+1] = '[[Category:' .. v .. ']]' | |||
end | |||
end | end | ||
-- for k,v in pairs(albumNames) do | -- for k,v in pairs(albumNames) do | ||
-- TODO: scan album pages for categories? | -- TODO: scan album pages for categories? | ||
-- cats[#cats+1] = '[ | -- cats[#cats+1] = '[[Category:' .. v['name'] .. ' (Album)]]' | ||
-- end | -- end | ||
if cols['eng'] then | if cols['eng'] then | ||
if isset(frame.args['eng_only']) then cats[#cats+1] = '[ | if common.isset(frame.args['eng_only']) then cats[#cats+1] = '[[Category:Lyrics in English]]' end | ||
else | else | ||
cats[#cats+1] = '[ | cats[#cats+1] = '[[Category:Untranslated/Lyrics]]' | ||
end | end | ||
if cols['kan'] then | if cols['kan'] then | ||
if romanizable then cats[#cats+1] = '[ | if romanizable then cats[#cats+1] = '[[Category:Lyrics in Kanji]]' end | ||
elseif not isset(frame.args['eng_only']) then | elseif not common.isset(frame.args['eng_only']) then | ||
cats[#cats+1] = '[ | cats[#cats+1] = '[[Category:Untranscribed/Lyrics]]' | ||
end | end | ||
if not cols['rom'] and not skip_rom then | if not cols['rom'] and not skip_rom then | ||
cats[#cats+1] = '[ | cats[#cats+1] = '[[Category:Unromanized/Lyrics]]' | ||
end | end | ||
-- MediaWiki would ignore the repeating categories anyway, but just to be safe... | -- MediaWiki would ignore the repeating categories anyway, but just to be safe... | ||
common.trunkTable(cats) | |||
return table.concat(cats, '\n') | return table.concat(cats, '\n') | ||
Line 571: | Line 595: | ||
--[[ Assembles the whole template for a Lyrics page. ]] | --[[ Assembles the whole template for a Lyrics page. ]] | ||
local function outputLyrics(frame) | local function outputLyrics(frame) | ||
local tpl = { | local tpl = {} | ||
tpl[#tpl+1] = '<table class="template_lyrics outcell" style="width:98%;">' | tpl[#tpl+1] = '<table class="template_lyrics outcell" style="width:98%;">' | ||
Line 578: | Line 602: | ||
local albumNames = getAlbumNames(albumList) | local albumNames = getAlbumNames(albumList) | ||
local slist,cols = scanForStanzas(frame) | local slist,cols = scanForStanzas(frame) | ||
local info_list = load_info(' | local info_list = load_info('Touhou_Wiki:SongSource.json') | ||
-- allows to force a single-cell closing row | -- allows to force a single-cell closing row | ||
Line 615: | Line 639: | ||
local search = frame.args['search'] | local search = frame.args['search'] | ||
local replace = frame.args['replace'] | local replace = frame.args['replace'] | ||
if isset(text) then | if common.isset(text) then | ||
if isset(search) and isset(replace) then | if common.isset(search) and common.isset(replace) then | ||
return | return utf8.replace_char(text, {[search] = replace}) | ||
else | else | ||
local pts = {''} | local pts = {''} | ||
for s,e,v in | for s,e,v in utf8.iter(text) do | ||
pts[#pts+1] = '* (' .. s .. ',' .. e .. ") = '" .. v .. "'" | pts[#pts+1] = '* (' .. s .. ',' .. e .. ") = '" .. v .. "'" | ||
end | end | ||
Line 633: | Line 657: | ||
local search = frame.args['search'] | local search = frame.args['search'] | ||
local replace = frame.args['replace'] | local replace = frame.args['replace'] | ||
if isset(text) and isset(search) and isset(replace) then | if common.isset(text) and common.isset(search) and common.isset(replace) then | ||
return | return utf8.replace(text, { {search, replace} }) | ||
else | else | ||
return uni_test(frame) | return uni_test(frame) | ||
Line 645: | Line 669: | ||
local k2 = normalize(frame.args['name']) | local k2 = normalize(frame.args['name']) | ||
if isset(lst[k1]) and isset(lst[k1][k2]) then | if common.isset(lst[k1]) and common.isset(lst[k1][k2]) then | ||
local trimmed_info = lst[k1][k2] | local trimmed_info = lst[k1][k2] | ||
local s,e,title,info = string.find(trimmed_info, '^(.*)%s*%[(.+)%]') | local s,e,title,info = string.find(trimmed_info, '^(.*)%s*%[(.+)%]') | ||
if s ~= nil and isset(info) then | if s ~= nil and common.isset(info) then | ||
local expinfo = | local expinfo = mw.text2.split(info, ';') | ||
local charname = expinfo[#expinfo] -- last element of table | local charname = expinfo[#expinfo] -- last element of table | ||
if | if common.exists(charname) then | ||
expinfo[#expinfo] = '[ | expinfo[#expinfo] = '[[' .. charname .. ']]' | ||
end | end | ||
trimmed_info = title .. ' [' .. table.concat(expinfo, "; ") .. ']' | trimmed_info = title .. ' [' .. table.concat(expinfo, "; ") .. ']' | ||
Line 672: | Line 696: | ||
['lookup_song_info'] = lookup_song_info, | ['lookup_song_info'] = lookup_song_info, | ||
} | } | ||
Revision as of 16:38, 25 January 2023
Documentation for this module may be created at Module:Lyrics2/doc
-- A port of [[Template:Lyrics]] to Lua
-- written by K
-- rewritten by DennouNeko
-- == HELPER FUNCTIONS ==
local common = require("Module:Common")
local utf8 = require("Module:Lib UTF8")
-- minor bugfixes, mw.text seem to be undefined at the time of this update
mw.text2 = require("Module:MW.text")
-- "Module:Common" candidate?
--[[
Builds a simple HTML table from a 'tbl' elements with 'options' as a list of options for table.
Useful for quick tests or if a complex table is not needed.
Currently available options are:
* cols - max column count
* class - text containing classes that should be added to the table
* style - text that should be added to the table's style (like "empty-cells: hide;")
* spacing - separation of table cells
--]]
function table.build_table(tbl, options)
local cols = nil
local ret = {}
-- to avoid script crashing when skipping options in param list
if options == nil then options = {} end
if options['cols'] ~= nil then cols = tonumber(options['cols']) end
ret[#ret+1] = '<table'
if common.isset(options['class']) then ret[#ret+1] = ' class="' .. options['class'] .. '"' end
if common.isset(options['style']) then ret[#ret+1] = ' style="' .. options['style'] .. '"' end
if common.isset(options['spacing']) then ret[#ret+1] = ' cellspacing="' .. options['spacing'] .. '"' end
ret[#ret+1] = '>'
local colnum = 0
if cols ~= nil then
-- build a table with max 'cols' columns in row, content of cells are the values of 'tbl' elements
ret[#ret+1] = '<tr>'
for k,v in pairs(tbl) do
if colnum > 0 and math.fmod(colnum, cols) == 0 then
ret[#ret+1] = '</tr><tr>'
end
ret[#ret+1] = '<td><!--' .. k .. '-->\n' .. v .. '</td>'
colnum = colnum + 1
end
if cols ~= nil then
while math.fmod(colnum, cols) ~= 0 do
ret[#ret+1] = '<td></td>'
colnum = colnum + 1
end
end
ret[#ret+1] = '</tr>'
else
-- build a simple table with 'key | value' rows
for k,v in pairs(tbl) do
ret[#ret+1] = '<tr><td>\n' .. k .. '</td><td>\n' .. v .. '</td></tr>'
end
end
ret[#ret+1] = '</table>'
return table.concat(ret)
end
--[[ Search the list of template arguments for arguments matching the pattern
"album#", where # is a number, and then return them as an indexed, sorted table.]]
local function scanForAlbums(frame)
local idx = {}
local args = frame.args
local albumList = {}
for k in pairs(args) do
s,e,t = string.find(k, '^album(%d+)$')
if s ~= nil then
table.insert(idx, t)
end
end
table.sort(idx)
for k in pairs(idx) do
table.insert (albumList, args["album"..k])
end
return albumList
end
--[[ Extracts list of names from a albumList.
Indexes of found names match indexes in album table. ]]
local function getAlbumNames(albumList)
local cats = {}
local found
local link
for k,v in pairs(albumList) do
found = nil
link = nil
s,e,t = string.find(v, '%[%[(.+)%]%]')
if s ~= nil then
tl = mw.text2.split(t, '|')
for k1,v1 in pairs(tl) do
s,e,t = string.find(v1, '^%s-link%s-=%s-(.+)%s-$')
if s ~= nil then
-- found a image tag
found = t
break
end
end
if found == nil then
-- not an image link then assume it's usual link, so get the last part as album name, first part as link
if #tl > 0 then
found = tl[#tl]
if #tl > 1 then link = tl[1] end
end
end
else
-- TODO: not a link - assume it's a name?
-- found = t
end
if found ~= nil then
cats[#cats+1] = {name = string.gsub(found, '_', ' '), link = link, idx = k}
end
end
return cats
end
--[[ Partial name normalization. ]]
local function normalize(text)
if common.isset(text) then
-- full-width characters to ASCII equivalents
text = utf8.replace_char(text, common.normalization_table)
-- remove any HTML tags
text = common.stripTags(text)
-- any whitespace chain into single space
text = string.gsub(text, '%s+', ' ')
-- remove preceding and trailing spaces
text = mw.text.trim(text)
end
return text
end
--[[ Loads and parses a JavaScript table "song_info" from a script with given name. ]]
local function load_info(name)
local tstr = common.getPage(name)
if tstr == nil then return ret end
local ret = {}
local s,e,t,t1,t2
s,e,t = string.find(tstr, '{(.+)}')
if s ~= nil then
for n,lst in string.gmatch(t, '"(.-[^\\])"%s-:%s-{(.-)}') do
n = normalize(n)
ret[n] = {}
for t1,t2 in string.gmatch(lst, '"(.-[^\\])"%s-:%s-"(.-[^\\])"') do
t1 = normalize(t1)
-- unescape the string
ret[n][t1] = string.gsub(t2, '\\(.)', {['\\'] = '\\', ['n'] = '\n', ['"'] = '"'})
end
end
else
error('song_info not found!')
end
return ret
end
--[[ Search for "source: " and "original title: ", add a hover text to title and a "notes: " below. ]]
local function wrap_titles(frame, elems, info_list)
local source,sourcek = nil,nil
local title,titlek,titlen = nil,nil,nil
local s,e,t,t1,t2,tt
for k,v in pairs(elems) do
s,e,t1,t = string.find(v, '^(%*%s*original title:%s*)(.+)$')
if s ~= nil then
title = t
titlen = normalize(t)
titlek = k
tt = t1
else
s,e,t = string.find(v, '^%*%s*source:%s*(.*)$')
if s ~= nil then
s,e,t1 = string.find(t, '%[%[.+|(.+)%]%]')
if s == nil then s,e,t1 = string.find(t, '%[%[(.+)%]%]') end
if s ~= nil then t = t1 end
source = normalize(t)
sourcek = k
end
end
if common.isset(source) and common.isset(titlen) then
if common.isset(info_list) and info_list[source] ~= nil then
if common.isset(info_list[source][titlen]) then
local extra = info_list[source][titlen]
s,e,t,t1 = string.find(extra, '^(.*)%s-%[(.+)%]')
if s == nil then t = extra end
if s ~= nil and common.isset(t1) then
local lst = mw.text2.split(t1, ';')
if common.exists(lst[#lst]) then
lst[#lst] = '[[' .. lst[#lst] .. ']]'
end
extra = table.concat(lst,'; ')
--if common.isset(extra) then table.insert(elems, sourcek+1, '* notes: ' .. extra) end
if common.isset(extra) then elems[sourcek] = elems[sourcek] .. '<br/>' .. extra end
end
if common.isset(t) then
elems[titlek] = tt .. frame:expandTemplate{title = 'h:title', args = {title, t} }
end
end
end
source = nil
titlen = nil
end
end
end
--[[ Add links for staff where linking is possible. ]]
local function link_staff(frame, elems)
-- nothing to do for now
end
--[[ Search the list of template arguments for arguments matching the patterns
"eng#", "kan#", or "rom#", where # is a number. Entries are sorted and
grouped together into appropriate tables within an indexed table. ]]
local function scanForStanzas(frame)
local idx = {}
local eng = {}
local kan = {}
local rom = {}
local cols = {['colnum'] = 0, ['single'] = false, ['full'] = false, ['eng'] = false, ['rom'] = false, ['kan'] = false}
local stanzaList = {}
for k, v in frame:argumentPairs() do
if common.isset(v) then
s,e,t = string.find(k, '^eng(%d+)$')
if s ~= nil then
local i = tonumber(t)
idx[#idx+1] = i
eng[i] = v
cols['eng'] = true
end
s,e,t = string.find(k, '^kan(%d+)$')
if s ~= nil then
local i = tonumber(t)
idx[#idx+1] = i
kan[i] = v
cols['kan'] = true
end
s,e,t = string.find(k, '^rom(%d+)$')
if s ~= nil then
local i = tonumber(t)
idx[#idx+1] = i
rom[i] = v
cols['rom'] = true
end
end
end
-- sort indexes and remove repeating ones
common.trunkTable(idx)
for k,v in pairs(idx) do
local tmp = {}
tmp['eng'] = common.cv(common.isset(eng[v]), eng[v], ' ')
tmp['rom'] = common.cv(common.isset(rom[v]), rom[v], ' ')
tmp['kan'] = common.cv(common.isset(kan[v]), kan[v], ' ')
stanzaList[#stanzaList+1] = tmp
end
if cols['eng'] then cols['colnum'] = cols['colnum'] + 1 end
if cols['rom'] then cols['colnum'] = cols['colnum'] + 1 end
if cols['kan'] then cols['colnum'] = cols['colnum'] + 1 end
cols['single'] = (cols['colnum'] < 2)
cols['full'] = (cols['colnum'] > 2)
return stanzaList, cols
end
--[[ Creates the sort key used for DEFAULTSORT. A sort key is created by
taking the English title, romanized title, or Japanese title (takes the
first it can find), setting it to lowercase, and capitalizing the first
letter in the title. ]]
local function calculateDefaultSort(frame)
local sortkey = ""
if(common.isset(frame.args.titleen)) then
sortkey = frame.args.titleen
elseif(common.isset(frame.args.titlerom)) then
sortkey = frame.args.titlerom
elseif(common.isset(frame.args.titlejp)) then
sortkey = frame.args.titlejp
end
return common.cv(common.isset(sortkey), "{{DEFAULTSORT:" .. string.gsub(string.lower(sortkey), '^%l', string.upper) .. "}}", "")
end
-- == PAGE SECTIONS ==
--[[ Generates the header, which contains the title and group name. ]]
local function lyricsHeaderRow(frame, cols)
local hr = {}
hr[#hr+1] = '<tr>\n<th class="incell_top" style="font-weight: normal;" colspan="' .. cols['colnum'] .. '">'
-- display title
if common.isset(frame.args.titleen) or common.isset(frame.args.titlejp) then
hr[#hr+1] = "'''" .. common.cv(frame.args.titleen, frame.args.titleen, frame.args.titlejp) .. "'''"
end
if common.isset(frame.args.group) then hr[#hr+1] = " by " .. frame.args.group end
hr[#hr+1] = '</th>\n</tr>'
return table.concat(hr)
end
--[[ Generates the main info section, which contains song info as well as a
gallery of albums the song is featured on. ]]
local function lyricsInfoRow(frame, cols, albumList, albumNames, info_list)
local ir = {}
local cc = {}
local s,e,t
ir[#ir+1] = '<tr>\n<td colspan="' .. cols['colnum'] .. '">'
-- albums first, since it's a floating frame
if #albumList > 0 then
local alist = mw.clone(albumList)
local lnk
for k,v in pairs(albumNames) do
if common.isset(alist[v.idx]) then
local link = v.name
if common.isset(v.link) then link = v.link .. '|' .. v.name end
alist[v.idx] = alist[v.idx] .. '<br/>[[' .. link .. ']]'
end
end
ir[#ir+1] = '\n<div class="lyrics_albums" style="float: right; clear: right;">'
ir[#ir+1] = "Featured in: \n:"
ir[#ir+1] = table.build_table(alist, {cols = 3, spacing = 4, style="text-align: center;"})
ir[#ir+1] = '</div>'
end
-- yes, these newlines are required for the MW parser to correctly interpret start-of-line entities like * or # for lists
if common.isset(frame.args.titleen) and common.isset(frame.args.titlejp) then
s,e,t = string.find(frame.args.titlejp, '<span lang=') -- lang template output is already expanded
if s ~= nil then -- lang template already included
cc[#cc+1] = frame:preprocess("\n*'''" .. frame.args.titlejp .. "'''")
else
cc[#cc+1] = frame:preprocess("\n*'''{{lang|ja|" .. frame.args.titlejp .. "}}'''")
end
end
if common.isset(frame.args.titlerom) then
cc[#cc+1] = frame:preprocess("\n*''" .. frame.args.titlerom .. "''")
end
if common.isset(frame.args.length) then
cc[#cc+1] = frame:preprocess("\n*length: " .. frame.args.length)
end
if common.isset(frame.args.arranger) then
cc[#cc+1] = frame:preprocess("\n*arrangement: " .. frame.args.arranger)
end
if common.isset(frame.args.lyricist) then
cc[#cc+1] = frame:preprocess("\n*lyrics: " .. frame.args.lyricist)
end
if common.isset(frame.args.vocalist) then
cc[#cc+1] = frame:preprocess("\n*vocals: " .. frame.args.vocalist)
end
if common.isset(frame.args.other_staff) then
cc[#cc+1] = frame:preprocess("\n" .. frame.args.other_staff)
end
if common.isset(frame.args.source) then
cc[#cc+1] = frame:preprocess("\n" .. frame.args.source)
end
local elems = mw.text2.split(table.concat(cc), '\n') -- to make sure that it's a "one element per line" table
-- process the elements
link_staff(frame, elems)
wrap_titles(frame, elems, info_list)
ir[#ir+1] = table.concat(elems, '\n') -- assemble it back into a string
-- just to make sure that everything stays in the cell
ir[#ir+1] = '<div style="float: none; clear: both;></div>'
ir[#ir+1] = "</td>\n</tr>"
return table.concat(ir)
end
--[[ Generates the extra info section, which contains extra information as
specified in the extra_info argument. ]]
local function lyricsExtraInfoRow(frame, cols)
local eir = {}
if common.isset(frame.args.extra_info) then
eir[#eir+1] = '\n<tr>\n<th class="incell" colspan="' .. cols['colnum'] .. '">Additional Info</th>\n</tr>'
eir[#eir+1] = '\n<tr>\n<td style="text-align: justify" colspan="' .. cols['colnum'] .. '">'
eir[#eir+1] = frame:preprocess('\n' .. frame.args.extra_info)
eir[#eir+1] = '</td>\n</tr>'
end
return table.concat(eir)
end
--[[ Generates stanzas upon stanzas of lyrics. ]]
local function lyricsStanzaRows(frame, cols, slist)
local srow = {}
local lang = 'ja'
if common.isset(frame.args['lang']) then
lang = common.cv(frame.args['lang'] == 'none', '', frame.args['lang'])
end
-- header row for stanzas
srow[#srow+1] = '\n<tr>'
if cols['kan'] then
srow[#srow+1] = '\n<th class="incell"'
-- if not cols['single'] then
srow[#srow+1] = ' style="width: 30%"'
-- end
srow[#srow+1] = '>Original</th>'
end
if cols['rom'] then
srow[#srow+1] = '\n<th class="incell"'
-- if not cols['single'] then
srow[#srow+1] = ' style="width: 35%"'
-- end
srow[#srow+1] = '>Romanized</th>'
end
if cols['eng'] then
srow[#srow+1] = '\n<th class="incell"'
-- if not cols['single'] then
srow[#srow+1] = ' style="width: 35%"'
-- end
srow[#srow+1] = common.cv(cols['single'], '>English</th>', '>Translation</th>')
end
srow[#srow+1] = '\n</tr>'
for i, stanza in pairs(slist) do
srow[#srow+1] = '\n<!-- row:' .. i .. '-->\n<tr class="lyrics_row">'
if cols['kan'] then
srow[#srow+1] = '<td'
if(common.isset(lang)) then srow[#srow+1] = ' lang="' .. lang .. '" xml:lang="' .. lang .. '"' end
srow[#srow+1] = '>\n' .. stanza['kan'] .. '\n</td>'
end
if cols['rom'] then
srow[#srow+1] = '<td>\n' .. stanza['rom'] .. '\n</td>'
end
if cols['eng'] then
srow[#srow+1] = '<td>\n' .. stanza['eng'] .. '\n</td>'
end
srow[#srow+1] = '\n</tr>'
end
if common.isset(frame.args['lyrics_source']) then
srow[#srow+1] = '<tr><td colspan="' .. cols['colnum'] .. '" style="text-align: center; font-size: 83%;">'
srow[#srow+1] = "\n:''Lyrics source: " .. frame.args['lyrics_source'] .. "''"
srow[#srow+1] = '</td></tr>'
end
return table.concat(srow)
end
--[[ Generates the notes sections, based on the notes argument. ]]
local function lyricsNotesRow(frame, cols)
local nrow = {}
if common.isset(frame.args['notes']) then
nrow[#nrow+1] = '\n<tr><th class="incell" colspan="' .. cols['colnum'] .. '"></th></tr>'
nrow[#nrow+1] = '\n<tr><td style="text-align: justify" colspan="' .. cols['colnum'] .. '">'
nrow[#nrow+1] = frame:preprocess('\n' .. frame.args['notes'])
nrow[#nrow+1] = '\n</td></tr>'
cols['close_single'] = true
end
return table.concat(nrow)
end
--[[ Generates the shaded row at the bottom of the lyric sheet. ]]
local function lyricsClosingRow(frame, cols)
local crow = {}
crow[#crow+1] = '\n<tr>\n'
if cols['close_single'] then
crow[#crow+1] = '<td class="incell_bottom" colspan="' .. cols['colnum'] .. '"></td>'
else
if cols['single'] then
crow[#crow+1] = '<td class="incell_bottom"></td>'
else
crow[#crow+1] = '<td class="incell_bottomleft"></td>'
if cols['full'] then crow[#crow+1] = '<td class="incell"></td>' end
crow[#crow+1] = '<td class="incell_bottomright"></td>'
end
end
crow[#crow+1] = '\n</tr>'
return table.concat(crow)
end
--[[ Categorizes the article based on its transcription/translation statuses. ]]
local function lyricsCategories(frame, cols, albumNames)
local cats = {''} -- so the first category won't disappear
local group
local romanizable = not common.isset(frame.args['lang']) or (common.isInTable(common.romanizable, frame.args['lang']) ~= nil)
local skip_rom = false
if common.isset(frame.args['eng_only']) then skip_rom = true end
if common.isset(frame.args['lang']) and not romanizable then skip_rom = true end
cats[#cats+1] = frame:preprocess(calculateDefaultSort(frame))
-- escaping the second '[', so parser won't assume it's a category
cats[#cats+1] = '[[Category:Lyrics]]'
if common.isset(frame.args['group_en']) then
group = frame.args['group_en']
elseif common.isset(frame.args['group']) then
group = frame.args['group']
end
if common.isset(group) then
local groups = {}
local s1, s,e,t
-- extract the group names from links
s1 = 0
while true do
s,e,t = string.find(group, '%[%[.-|(.-)%]%]', s1 + 1)
-- in case of simple links
if s == nil then
s,e,t = string.find(group, '%[%[(.-)%]%]', s1 + 1)
end
if s == nil then break end
s1 = e
groups[#groups+1] = t
end
-- if group is not a link, assume it's plain name
if #groups < 1 then groups[1] = group end
for k,v in pairs(groups) do
cats[#cats+1] = '[[Category:' .. v .. ']]'
end
end
-- for k,v in pairs(albumNames) do
-- TODO: scan album pages for categories?
-- cats[#cats+1] = '[[Category:' .. v['name'] .. ' (Album)]]'
-- end
if cols['eng'] then
if common.isset(frame.args['eng_only']) then cats[#cats+1] = '[[Category:Lyrics in English]]' end
else
cats[#cats+1] = '[[Category:Untranslated/Lyrics]]'
end
if cols['kan'] then
if romanizable then cats[#cats+1] = '[[Category:Lyrics in Kanji]]' end
elseif not common.isset(frame.args['eng_only']) then
cats[#cats+1] = '[[Category:Untranscribed/Lyrics]]'
end
if not cols['rom'] and not skip_rom then
cats[#cats+1] = '[[Category:Unromanized/Lyrics]]'
end
-- MediaWiki would ignore the repeating categories anyway, but just to be safe...
common.trunkTable(cats)
return table.concat(cats, '\n')
end
-- == PUBLIC FUNCTIONS (see export table at bottom) ==
--[[ Assembles the whole template for a Lyrics page. ]]
local function outputLyrics(frame)
local tpl = {}
tpl[#tpl+1] = '<table class="template_lyrics outcell" style="width:98%;">'
-- first prepare all the necessary data
local albumList = scanForAlbums(frame)
local albumNames = getAlbumNames(albumList)
local slist,cols = scanForStanzas(frame)
local info_list = load_info('Touhou_Wiki:SongSource.json')
-- allows to force a single-cell closing row
cols['close_single'] = false
tpl[#tpl+1] = lyricsHeaderRow(frame, cols)
tpl[#tpl+1] = lyricsInfoRow(frame, cols, albumList, albumNames, info_list)
tpl[#tpl+1] = lyricsExtraInfoRow(frame, cols)
tpl[#tpl+1] = lyricsStanzaRows(frame, cols, slist)
tpl[#tpl+1] = lyricsNotesRow(frame, cols)
tpl[#tpl+1] = lyricsClosingRow(frame, cols)
tpl[#tpl+1] = '</table>'
tpl[#tpl+1] = lyricsCategories(frame, cols, albumNames)
return table.concat(tpl, '\n')
end
local function dump_info(frame)
local ret = {''}
lst = load_info(frame.args['name'])
ret[#ret+1] = '<table class="wikitable" style="width: 100%;">'
for k,v in pairs(lst) do
ret[#ret+1] = '<tr><th colspan="2" style="text-align: center;">' .. k .. '</th></tr>'
for k1,v1 in pairs(v) do
ret[#ret+1] = '<tr><td>' .. k1 .. '</td><td>' .. v1 .. '</td></tr>'
end
end
ret[#ret+1] = '</table>'
return table.concat(ret, '\n')
end
local function uni_test(frame)
local text = frame.args[1]
local search = frame.args['search']
local replace = frame.args['replace']
if common.isset(text) then
if common.isset(search) and common.isset(replace) then
return utf8.replace_char(text, {[search] = replace})
else
local pts = {''}
for s,e,v in utf8.iter(text) do
pts[#pts+1] = '* (' .. s .. ',' .. e .. ") = '" .. v .. "'"
end
return table.concat(pts, "\n")
end
end
return ''
end
local function uni_test2(frame)
local text = frame.args[1]
local search = frame.args['search']
local replace = frame.args['replace']
if common.isset(text) and common.isset(search) and common.isset(replace) then
return utf8.replace(text, { {search, replace} })
else
return uni_test(frame)
end
end
function lookup_song_info(frame)
local lst = load_info(frame.args['src'])
local k1 = normalize(frame.args['game'])
local k2 = normalize(frame.args['name'])
if common.isset(lst[k1]) and common.isset(lst[k1][k2]) then
local trimmed_info = lst[k1][k2]
local s,e,title,info = string.find(trimmed_info, '^(.*)%s*%[(.+)%]')
if s ~= nil and common.isset(info) then
local expinfo = mw.text2.split(info, ';')
local charname = expinfo[#expinfo] -- last element of table
if common.exists(charname) then
expinfo[#expinfo] = '[[' .. charname .. ']]'
end
trimmed_info = title .. ' [' .. table.concat(expinfo, "; ") .. ']'
end
return trimmed_info
end
return '' -- so we won't get a 'nil' as result if can't find a match
end
return {
['outputLyrics'] = outputLyrics,
['outputLyricsFromTemplate'] = function(frame) return outputLyrics(frame:getParent()) end,
['dump_info'] = dump_info,
['unitest'] = uni_test,
['unitest2'] = uni_test2,
['lookup_song_info'] = lookup_song_info,
}