Module:Lyrics2

-- from touhouwiki -- 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: -- 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
 * 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

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] = ' ' for k,v in pairs(tbl) do     if colnum > 0 and math.fmod(colnum, cols) == 0 then ret[#ret+1] = ' ' end ret[#ret+1] = ' \n' .. v .. ' '     colnum = colnum + 1 end

if cols ~= nil then while math.fmod(colnum, cols) ~= 0 do       ret[#ret+1] = '  ' colnum = colnum + 1 end end ret[#ret+1] = ' ' else -- build a simple table with 'key | value' rows for k,v in pairs(tbl) do     ret[#ret+1] = '  \n' .. k .. ' \n' .. v .. ' '    end end ret[#ret+1] = ' ' return table.concat(ret) end

-- Search the list of template arguments for arguments matching the pattern     "album 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[' 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] .. ' ' .. 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 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), "", "") end

-- == PAGE SECTIONS == -- Generates the header, which contains the title and group name. local function lyricsHeaderRow(frame, cols) local hr = {}

hr[#hr+1] = ' \n'

-- 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] = ' \n '

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] = ' \n'

-- 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] .. ' ' .. link .. '' end end

ir[#ir+1] = '\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] = ' ' 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*") 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.vocalist) then cc[#cc+1] = frame:preprocess("\n*vocals: " .. frame.args.vocalist) end if common.isset(frame.args.lyricist) then cc[#cc+1] = frame:preprocess("\n*lyrics: " .. frame.args.lyricist) end if common.isset(frame.args.composer) then cc[#cc+1] = frame:preprocess("\n*music: " .. frame.args.composer) end if common.isset(frame.args.arranger) then cc[#cc+1] = frame:preprocess("\n*arrangement: " .. frame.args.arranger) 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] = ' '

ir[#ir+1] = " \n "

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 \nAdditional Info \n ' eir[#eir+1] = '\n \n'

eir[#eir+1] = frame:preprocess('\n' .. frame.args.extra_info)

eir[#eir+1] = ' \n ' 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 '

if cols['kan'] then srow[#srow+1] = '\nOriginal ' end

if cols['rom'] then srow[#srow+1] = '\nRomanized ' end

if cols['eng'] then srow[#srow+1] = '\nEnglish ', '>Translation ') end

srow[#srow+1] = '\n '

for i, stanza in pairs(slist) do   srow[#srow+1] = '\n\n'

if cols['kan'] then srow[#srow+1] = '\n' .. stanza['kan'] .. '\n ' end

if cols['rom'] then srow[#srow+1] = ' \n' .. stanza['rom'] .. '\n ' end

if cols['eng'] then srow[#srow+1] = ' \n' .. stanza['eng'] .. '\n ' end

srow[#srow+1] = '\n ' end

if common.isset(frame.args['lyrics_source']) then srow[#srow+1] = ' ' srow[#srow+1] = "\n:''Lyrics source: " .. frame.args['lyrics_source'] .. "''"   srow[#srow+1] = '  ' 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  ' nrow[#nrow+1] = '\n ' nrow[#nrow+1] = frame:preprocess('\n' .. frame.args['notes']) nrow[#nrow+1] = '\n ' 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 \n'

if cols['close_single'] then crow[#crow+1] = ' ' else if cols['single'] then crow[#crow+1] = ' ' else crow[#crow+1] = ' ' if cols['full'] then crow[#crow+1] = ' ' end crow[#crow+1] = '<td class="incell_bottomright"> ' end end

crow[#crow+1] = '\n '

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] = ''

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] = '' end end

-- for k,v in pairs(albumNames) do   -- TODO: scan album pages for categories? -- cats[#cats+1] = '' -- end

if cols['eng'] then if common.isset(frame.args['eng_only']) then cats[#cats+1] = '' end else cats[#cats+1] = '' end

if cols['kan'] then if romanizable then cats[#cats+1] = '' end elseif not common.isset(frame.args['eng_only']) then cats[#cats+1] = '' end

if not cols['rom'] and not skip_rom then cats[#cats+1] = '' 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] = ' '

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] = ' ' 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, }