• 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

From Touhou Wiki
Jump to navigation Jump to search
m (Aha, is this our chance?)
(style the element)
(26 intermediate revisions by 7 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.mod(colnum, cols) == 0 then
       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.mod(colnum, cols) ~= 0 do
       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, v in frame:argumentPairs() do
   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[#idx+1] = tonumber(t)
       table.insert(idx, t)
      args[#idx] = v
     end
     end
   end
   end
Line 76: Line 78:
   table.sort(idx)
   table.sort(idx)


   for k,v in pairs(idx) do
   for k in pairs(idx) do
     albumList[#albumList + 1] = args[k]
     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 = string.explode(t, '|')
       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 = string.utf8replace_char(text, common.normalization_table)
     text = utf8.replace_char(text, common.normalization_table)
     -- remove any HTML tags
     -- remove any HTML tags
     text = string.strip_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 = string.trim(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 = mw.get_page(name)
   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, 'var%s+song_info%s-=%s-{(.-)};')
   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 = string.explode(t1, ';')
             local lst = mw.text2.split(t1, ';')
             if mw.exists(lst[#lst]) then
             if common.exists(lst[#lst]) then
               lst[#lst] = '[\[' .. 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
   table.trunk(idx)
   common.trunkTable(idx)


   for k,v in pairs(idx) do
   for k,v in pairs(idx) do
     local tmp = {}
     local tmp = {}
     tmp['eng'] = condval(isset(eng[v]), eng[v], '&nbsp;')
     tmp['eng'] = common.cv(common.isset(eng[v]), eng[v], '&nbsp;')
     tmp['rom'] = condval(isset(rom[v]), rom[v], '&nbsp;')
     tmp['rom'] = common.cv(common.isset(rom[v]), rom[v], '&nbsp;')
     tmp['kan'] = condval(isset(kan[v]), kan[v], '&nbsp;')
     tmp['kan'] = common.cv(common.isset(kan[v]), kan[v], '&nbsp;')
     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 condval(isset(sortkey), "{\{DEFAULTSORT:" .. string.gsub(string.lower(sortkey), '^%l', string.upper) .. "}}", "")
   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] = "'''" .. condval(frame.args.titleen, frame.args.titleen, frame.args.titlejp) .. "'''"
     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/>[\[' .. link .. ']]'
         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, '{{lang|', 1, true)
     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
         cc[#cc+1] = frame:preprocess("\n*'''" .. frame.args.titlejp .. "'''")
         cc[#cc+1] = frame:preprocess("\n*'''" .. frame.args.titlejp .. "'''")
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 = string.explode(table.concat(cc), '\n') -- to make sure that it's a "one element per line" table
   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 = condval(frame.args['lang'] == 'none', '', frame.args['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] = condval(cols['single'], '>English</th>', '>Translation</th>')
     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 (table.in_table(common.romanizable, frame.args['lang']) ~= nil)
   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] = '[\[Category:Lyrics]]'
   cats[#cats+1] = '[[Category:Lyrics]]'


   if isset(frame.args['group']) then
   if common.isset(frame.args['group_en']) then
    -- extract the group name from links
      group = frame.args['group_en']
    s,e,t = string.find(frame.args['group'], '%[%[.+|(.+)%]%]')
  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
    if s == nil then s,e,t = string.find(frame.args['group'], '%[%[(.+)%]%]') end
    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


     -- TODO: not sure if I can assume that group has category if the "group" param wasn't a link...
     -- if group is not a link, assume it's plain name
     -- if s == nil then t = frame.args['group'] end
     if #groups < 1 then groups[1] = group end


     if isset(t) then cats[#cats+1] = '[\[Category:' .. t .. ']]' 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] = '[\[Category:' .. v['name'] .. ' (Album)]]'
     -- cats[#cats+1] = '[[Category:' .. v['name'] .. ' (Album)]]'
   -- end
   -- end


   if cols['eng'] then
   if cols['eng'] then
     cats[#cats+1] = '[\[Category:Lyrics in English]]'
     if common.isset(frame.args['eng_only']) then cats[#cats+1] = '[[Category:Lyrics in English]]' end
   else
   else
     cats[#cats+1] = '[\[Category:Untranslated/Lyrics]]'
     cats[#cats+1] = '[[Category:Untranslated/Lyrics]]'
   end
   end


   if cols['kan'] then
   if cols['kan'] then
     if romanizable then cats[#cats+1] = '[\[Category:Lyrics in Kanji]]' end
     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] = '[\[Category:Untranscribed/Lyrics]]'
     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] = '[\[Category:Unromanized/Lyrics]]'
     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...
   table.trunk(cats)
   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">'
   tpl[#tpl+1] = '<table class="template_lyrics outcell" style="width:98%;">'


   -- first prepare all the necessary data
   -- first prepare all the necessary data
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('MediaWiki:SongSource.js')
   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 string.utf8replace_char(text, {[search] = replace})
       return utf8.replace_char(text, {[search] = replace})
     else
     else
       local pts = {''}
       local pts = {''}
       for s,e,v in string.utf8iter(text) do
       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 string.utf8replace(text, { {search, replace} })
     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 = string.explode(info, ';')
       local expinfo = mw.text2.split(info, ';')
       local charname = expinfo[#expinfo] -- last element of table
       local charname = expinfo[#expinfo] -- last element of table
       if mw.exists(charname) then
       if common.exists(charname) then
         expinfo[#expinfo] = '[\[' .. charname .. ']]'
         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,
}
}
-- [[Category:Lua Scripts|{{PAGENAME}}]]

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], '&nbsp;')
    tmp['rom'] = common.cv(common.isset(rom[v]), rom[v], '&nbsp;')
    tmp['kan'] = common.cv(common.isset(kan[v]), kan[v], '&nbsp;')
    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,
}