• 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 (updates in comments; small change of function order)
m (small change in architecture - easier processing of InfoRow elements)
Line 276: Line 276:
local function normalize(text)
local function normalize(text)
   if isset(text) then
   if isset(text) then
     -- full-width question mark, space and tilde
     -- full-width characters to ASCII equivalents
     text = string.replace_char(text, {['~'] = '~', ['!'] = '!', ['?'] = '?', [' '] = ' '})
     text = string.replace_char(text, {['~'] = '~', ['!'] = '!', ['?'] = '?', [' '] = ' '})
     -- remove any HTML tags
     -- remove any HTML tags
Line 315: Line 315:


--[[ Search for "source: " and "original title: ", add a hover text to title and a "notes: " below. ]]
--[[ Search for "source: " and "original title: ", add a hover text to title and a "notes: " below. ]]
local function wrap_titles(frame, text, info_list)
local function wrap_titles(frame, elems, info_list)
  local elems = string.explode(text, '\n')
   local source = nil
   local source = nil
   local title,titlek,titlen = nil,nil,nil
   local title,titlek,titlen = nil,nil,nil
Line 363: Line 362:
       end
       end
     end
     end
    text = table.concat(elems, '\n')
   end
   end
end


   return text
--[[ Add links for staff where linking is possible. ]]
local function link_staff(frame, elems)
   -- nothing to do for now
end
end


Line 513: Line 514:
   end
   end


   ir[#ir+1] = wrap_titles(frame, table.concat(cc), info_list)
  local elems = string.explode(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
   -- just to make sure that everything stays in the cell
Line 693: Line 700:
   return table.concat(cats, '\n')
   return table.concat(cats, '\n')
end
end
 
 
-- == PUBLIC FUNCTIONS (see export table at bottom) ==
-- == PUBLIC FUNCTIONS (see export table at bottom) ==
--[[ Assembles the whole template for a Lyrics page. ]]
--[[ Assembles the whole template for a Lyrics page. ]]

Revision as of 17:36, 2 October 2012

Documentation for this module may be created at Module:Lyrics2/doc

Script error: Lua error at line 353: invalid escape sequence near ''['.

-- A port of [[Template:Lyrics]] to Lua
-- written by K
-- rewritten by DennouNeko
 
-- == HELPER FUNCTIONS ==
require("Module:Common")

common.romanizable = {'zh-hans', 'zh-hant', 'zh-cn', 'zh-tw', 'zh-hk', 'zh-sg', 'zh-mo', 'zh-my', 'ja', 'ko', 'vi'}

-- "Module:Common" candidate
--[[ Tries to get contents of a page with given name.
     If the function fails it returns nil (page doesn't exist or can't be loaded)
     On success it returns the contents of page (it can be be partially preprocessed, so watch out for parser markers). ]]
function mw.get_page(name)
  if not isset(name) then return nil end

  local frame = mw.getCurrentFrame()

  -- do a safe call to catch possible errors, like "template doesn't exist"
  local stat,page = pcall(frame.expandTemplate, frame, {title = ':' .. name})

  if not stat then
    -- TODO: 'page' contains the error message. Do some debugging?
    return nil
  end

  return page
end

-- "Module:Common" candidate
--[[ Sort table and remove repeating elements.
     Since tbl is passed as reference, any changes in it will affect the passed table. ]]
function table.trunk(tbl)
  table.sort(tbl)
  local last
  local redo
  repeat
    redo = false
    last = nil
    for k,v in pairs(tbl) do
      if v ~= last then
        last = v
      else
        table.remove(tbl, k)
        redo = true
        break
      end
    end
  until not redo
end

-- "Module:Common" candidate
--[[ simulates the (a ? b : c) notation of C/C++ and PHP languages.
     Similar to {{#if:{{{a|}}}|{{{b|}}}|{{{c|}}}}} from parser functions. ]]
function condval(a, b, c)
  if a then return b else return c end
end

-- "Module:Common" candidate
--[[ Checks if a given value is among the elements of a table and returns its index.
      Returns nil if it can't find it. ]]
function table.in_table(tbl, val)
  for k,v in pairs(tbl) do
    if v == val then return k end
  end
  return nil
end

-- "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 isset(options['class']) then ret[#ret+1] = ' class="' .. options['class'] .. '"' end
  if isset(options['style']) then ret[#ret+1] = ' style="' .. options['style'] .. '"' end
  if 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.mod(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.mod(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

-- "Module:Common" candidate
--[[Removes HTML tags from string. ]]
function string.strip_tags(text)
  if isset(text) then
    local tmp
    repeat
      tmp = text
      -- a pair of tags, like <td style="">...</td>
      text = string.gsub(text, '<%s*(%w+).->(.-)<%s*/%s*%1%s*>', '%2')
      -- closed tag, like <br/>
      text = string.gsub(text, '<%s*%w+%s*/%s*>', '')
    until tmp == text
  end
  return text
end

-- "Module:utf8" candidate (required by utf8explode)
--[[ Returns the position of current character and the (predicted) beginning of another one.
     Required by string.utf8explode ]]
function string.utf8iter(str)
  local i = 1
  local j
  local n = #str
  local char = nil
  return function()
      if i <= n then
        j = i
        char = str:byte(j)
        if char < 0x80 then
          -- 0xxxxxxx
          i = j + 1
        elseif char < 0xc0 then
          -- 10xxxxxx
          return nil -- error, we're in the middle of a character
        elseif char < 0xe0 then
          -- 110xxxxx 10xxxxxx
          i = j + 2
        elseif char < 0xf0 then
          -- 1110xxxx 10xxxxxx 10xxxxxx
          i = j + 3
        elseif char < 0xf8 then
          -- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
          i = j + 4
        elseif char < 0xfc then
          -- 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
          i = j + 5
        elseif char < 0xfe then
          -- 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
          i = j + 6
        else
          return nil -- 0xfe and 0xff are invalid UTF-8 values
        end
        
        -- TODO: parsing of a character?
        return j,(i-1),string.sub(str, j, (i-1))
      end
    end
end

-- "Module:utf8" candidate (requires utf8iter)
--[[ Splits an UTF-8 text (encoding used by Lua) into single character parts. ]]
function string.utf8explode(text)
  local pts = {}
  if text == nil or type(text) ~= 'string' then return pts end
  if #text > 0 then
    for s,e,v in string.utf8iter(text) do pts[#pts+1] = v end
  else
    -- technically there is one part - the empty string
    pts[#pts+1] = ''
  end
  return pts
end

-- "Module:utf8" candidate? (requires utf8explode)
--[[ Replaces whole non-ASCII characters.
     'reps' should be a table in format {['A'] = 'a', ['B'] = b, ...} ]]
function string.replace_char(text, reps)
  local parts

  parts = string.utf8explode(text)
  for k,v in pairs(parts) do
    for fnd,rep in pairs(reps) do
      if v == fnd then parts[k] = rep end
    end
  end

  return table.concat(parts)
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 = {}
  local albumList = {}

  for k, v in frame:argumentPairs() do
    s,e,t = string.find(k, '^album(%d+)$')
    if s ~= nil then
      idx[#idx+1] = tonumber(t)
      args[#idx] = v
    end
  end

  table.sort(idx)

  for k,v in pairs(idx) do
    albumList[#albumList + 1] = args[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 = string.explode(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 isset(text) then
    -- full-width characters to ASCII equivalents
    text = string.replace_char(text, {['~'] = '~', ['!'] = '!', ['?'] = '?', [' '] = ' '})
    -- remove any HTML tags
    text = string.strip_tags(text)
    -- any whitespace chain into single space
    text = string.gsub(text, '%s+', ' ')
    -- remove preceding and trailing spaces
    text = string.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 = mw.get_page(name)
  if tstr == nil then return ret end

  local ret = {}
  local s,e,t,t1,t2

  s,e,t = string.find(tstr, 'var%s+song_info%s-=%s-{(.-)};')
  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 = 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)
      end
    end
    if isset(source) and isset(titlen) then break end
  end

  if isset(source) and isset(titlen) then
    if info_list[source] ~= nil then
      if 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 isset(t1) then
          local lst = string.explode(t1, ';')
          if mw.exists(lst[#lst]) then
            lst[#lst] = '[\[' .. lst[#lst] .. ']]'
          end
          extra = table.concat(lst,'; ')
          elems[#elems+1] = '* notes: ' .. extra
        end
        
        if isset(t) then
          elems[titlek] = tt .. frame:expandTemplate{title = 'h:title', args = {title, t} }
        end
      end
    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}
  stanzaList = {}

  for k, v in frame:argumentPairs() do
    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
  
  -- sort indexes and remove repeating ones
  table.trunk(idx)

  for k,v in pairs(idx) do
    local tmp = {}
    tmp['eng'] = condval(isset(eng[v]), eng[v], '&nbsp;')
    tmp['rom'] = condval(isset(rom[v]), rom[v], '&nbsp;')
    tmp['kan'] = condval(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(isset(frame.args.titleen)) then
    sortkey = frame.args.titleen
  elseif(isset(frame.args.titlerom)) then
    sortkey = frame.args.titlerom
  elseif(isset(frame.args.titlejp)) then
    sortkey = frame.args.titlejp
  end

  return condval(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
  hr[#hr+1] = "'''" .. condval(frame.args.titleen, frame.args.titleen, frame.args.titlejp) .. "'''"
  if 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.
     Uses scanForAlbums(frame).]]
local function lyricsInfoRow(frame, cols, albumList, albumNames, info_list)
  local ir = {}
  local cc = {}

  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 isset(alist[v.idx]) then
        local link = v.name
        if 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 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 isset(frame.args.titleen) and isset(frame.args.titlejp) then
    cc[#cc+1] = frame:preprocess("\n*'''{{lang|ja|" .. frame.args.titlejp .. "}}'''")
  end
  if isset(frame.args.titlerom) then
    cc[#cc+1] = frame:preprocess("\n*''" .. frame.args.titlerom .. "''")
  end
  if isset(frame.args.length) then
    cc[#cc+1] = frame:preprocess("\n*length: " .. frame.args.length)
  end
  if isset(frame.args.arranger) then
    cc[#cc+1] = frame:preprocess("\n*arrangement: " .. frame.args.arranger)
  end
  if isset(frame.args.lyricist) then
    cc[#cc+1] = frame:preprocess("\n*lyrics: " .. frame.args.lyricist)
  end
  if isset(frame.args.vocalist) then
    cc[#cc+1] = frame:preprocess("\n*vocals: " .. frame.args.vocalist)
  end
  if isset(frame.args.other_staff) then
    cc[#cc+1] = frame:preprocess("\n" .. frame.args.other_staff)
  end
  if isset(frame.args.source) then
    cc[#cc+1] = frame:preprocess("\n" .. frame.args.source)
  end

  local elems = string.explode(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 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.
     Uses scanForStanzas(frame)]]
local function lyricsStanzaRows(frame, cols, slist)
  local srow = {}
  local lang = 'ja'
  if isset(frame.args['lang']) then
    lang = condval(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] = condval(cols['single'], '>English</th>', '>Translation</th>')
  end

  srow[#srow+1] = '\n</tr>'

  for i, stanza in pairs(slist) do
    csused = false
    srow[#srow+1] = '\n<!-- row:' .. i .. '-->\n<tr class="lyrics_row">'

    if cols['kan'] then
      srow[#srow+1] = '<td'
      if(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 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 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.
     Uses calculateDefaultSort(frame). ]]
local function lyricsCategories(frame, cols, albumNames)
  local cats = {''} -- so the first category won't disappear

  local romanizable = not isset(frame.args['lang']) or (table.in_table(common.romanizable, frame.args['lang']) ~= nil)
  local skip_rom = false
  if isset(frame.args['eng_only']) then skip_rom = true end
  if 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 isset(frame.args['group']) then
    -- extract the group name from links
    s,e,t = string.find(frame.args['group'], '%[%[.+|(.+)%]%]')

    -- in case of simple links
    if s == nil then s,e,t = string.find(frame.args['group'], '%[%[(.+)%]%]') end

    -- TODO: not sure if I can assume that group has category if the "group" param wasn't a link...
    -- if s == nil then t = frame.args['group'] end

    if isset(t) then cats[#cats+1] = '[\[Category:' .. t .. ']]' 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
    cats[#cats+1] = '[\[Category:Lyrics in English]]'
  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 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...
  table.trunk(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">'

  -- first prepare all the necessary data
  local albumList = scanForAlbums(frame)
  local albumNames = getAlbumNames(albumList)
  local slist,cols = scanForStanzas(frame)
  local info_list = load_info('MediaWiki:SongSource.js')

  -- allows to force a single-cell closing row
  cols['close_signle'] = 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 isset(text) then
    if isset(search) and isset(replace) then
      return string.replace_char(text, {[search] = replace})
    else
      local pts = {''}
      for s,e,v in string.utf8iter(text) do
        pts[#pts+1] = '* (' .. s .. ',' .. e .. ") = '" .. v .. "'"
      end
      return table.concat(pts, "\n")
    end
  end
  return ''
end

function lookup_song_info(frame)
  lst = load_info(frame.args['src'])
  for k,v in pairs(lst) do
      if k == normalize(frame.args['game']) then
        for k1,v1 in pairs(v) do
            if k1 == normalize(frame.args['name']) then
                local trimmed_info = v1
                local expinfo = string.explode(trimmed_info, "; ")
                local charname = string.explode(expinfo[#expinfo], "]")[1]
                if mw.exists(charname) then
                    return table.concat(table.slice(expinfo, 1, #expinfo-1), "; ") .. "; [[" .. charname .. "]]]"
                else
                    return v1
                end
            end
        end
    end
  end
end

return {
  ['outputLyrics'] = outputLyrics,
  ['outputLyricsFromTemplate'] = function(frame) return outputLyrics(frame:getParent()) end,
  ['dump_info'] = dump_info,
  ['unitest'] = uni_test,
  ['lookup_song_info'] = lookup_song_info,
}

-- [[Category:Lua Scripts|{{PAGENAME}}]]