Module:Ppoem
-- This is an module to implement "ppoem" (a.k.a. proper poem)
-- The aim here is to provide a poem syntax that's simple, but semantically
-- correct and able to handle things like export and line wrapping.
require('strict')
local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
-- return true if an item is in a given list
local function check_in_list(x, list)
for k, v in pairs(list) do
if x == v then
return true
end
end
return false
end
-- Error if the args[name] is not in the given list of values
local function check_arg_in_list(args, name, list, allowNil)
if args[name] == nil then
if allowNil then
return
else
error("Argument '" .. name .. "' may not be empty")
end
end
local inlist = check_in_list(args[name], list)
if not inlist then
error("Unknown argument value: '" .. name .. "=" .. args[name] .. "'. Expected one of: " .. table.concat(list, ", "))
end
end
--[=[
Decompose a single line into a data structure containing all relevant information
]=]
function p.parse_line(line)
-- do indents first
local nbsps = 0
local ems = 0
line = line:gsub("^ +", function(spaces)
nbsps = spaces:len()
return ""
end, 1)
if nbsps == 0 then
-- replace leading colons with  
line = line:gsub("^(:+)%s*", function(colons)
ems = colons:len()
return ""
end, 1)
end
-- for all lines, classes come next
local classes = {}
line = line:gsub("^{(.-)}%s*", function(classes_match)
for class_name in string.gmatch(classes_match, "%S+") do
table.insert(classes, "ws-poem-" .. class_name)
end
return ""
end, 1)
local alignment
line = line:gsub("^>>(%s*)(.?)", function(whitespace, next_char)
if whitespace == '' and next_char == '>' then
-- this is a >>>, which is handled later,
-- so return nil so nothing is replaced
return nil
end
-- Otherwise, set alignment and replace the angle brackets with the
-- char following them (in effect, delete the angle brackets).
alignment = "r"
return next_char
end, 1)
line = line:gsub("^<>%s*", function()
alignment = "c"
return ""
end, 1)
-- nothing left - this is a stanza break line
if line == "" then
local stanza = {
type = 'stanza',
align = alignment
}
if #classes > 0 then
stanza['classes'] = classes
end
return stanza
end
-- at this point this must be a content line
local line_num;
line = line:gsub("%s*>>>%s*(.+)$", function(line_num_str)
line_num = line_num_str;
return ""
end, 1)
local verse_num;
line = line:gsub("^(.-)%s*<<<%s*", function(verse_num_str)
verse_num = verse_num_str;
return ""
end, 1)
local line_data = {
type = 'line',
align = alignment,
content = line,
line_num = line_num,
verse_num = verse_num,
}
if #classes > 0 then
line_data['classes'] = classes
end
if nbsps > 0 then
line_data['indent'] = { nbsp = nbsps }
elseif ems > 0 then
line_data['indent'] = { em = ems }
end
return line_data
end
local function construct_stanza(stanza)
local classes = { 'ws-poem-stanza' }
if stanza['classes'] then
for k, v in pairs(stanza['classes']) do
table.insert(classes, v)
end
end
if stanza['align'] == 'r' then
table.insert(classes, 'ws-poem-right')
elseif stanza['align'] == 'c' then
table.insert(classes, 'ws-poem-center')
end
local s = "<div class=\"" .. table.concat(classes, " ") .. "\">"
return s
end
-- construct a fixed width span for use in indenting
local function construct_fixed_width(ems)
local emsp = " "
local s = mw.html.create("span")
:addClass("ws-poem-indent")
:css({
width = ems .. "em",
})
:wikitext(emsp:rep(ems))
return tostring(s)
end
--[=[
Construct a "proper poem"
]=]
function p._ppoem(args)
check_arg_in_list(args, 'start', {"open", "stanza", "follow", "same-line"}, true)
check_arg_in_list(args, 'end', {"close", "stanza", "follow", "same-line"}, true)
local open = args['start'] == "open" or not args['start']
local close = args['end'] == "close" or not args['end']
local isPageNs = mw.title.getCurrentTitle():inNamespace(104)
-- in Page namespace, we always open a fresh environment and close it at the end
if isPageNs then
open = true
close = true
end
-- Try not to blow up if called without an argument
-- split()/trim() handle empty strings fine, but throw when fed nil
local input = ""
if args[1] ~= nil then
input = args[1]
end
local lines = mw.text.split(mw.text.trim(input), "\r?\n", false)
local s = ""
local pending_stanza
-- start a new stanza
-- this can be overridden later by an explict stanza line like '{stanza class}'
if open or args['start'] == "stanza" then
pending_stanza = "<div class=\"ws-poem-stanza\">"
end
local have_line_num = false
local have_verse_num = false
local num_stanzas = 0
local num_lines = 0
-- we inherited an open stanza
local continued_stanza = not (args['start'] == "stanza" or open)
-- hide the BR in a span so we can manipulate it with CSS cross-browser
local linebreak = '<span class="ws-poem-break"><br/></span>'
for k,v in pairs(lines) do
local line_data = p.parse_line(v)
if line_data['type'] == 'stanza' then
-- mw.logObject(line_data)
pending_stanza = tostring(construct_stanza(line_data))
else
-- it's a line
-- we have to put something on the line to make sure it has height
if mw.text.trim( line_data.content ) == '' then
line_data.content = ' '
end
-- first start any pending stanza
if pending_stanza then
-- mw.log("pending: " .. pending_stanza, num_stanzas, num_lines)
if num_stanzas == 0 and num_lines == 0 and continued_stanza and not isPageNs then
-- mw.log("Skip stanza")
-- the stanza config in this case is just to set up the stanza in page NS
-- otherwise we continue the one from the last template
else
-- either we have our own stanzas to close, or we inherited one
if num_stanzas > 0 or continued_stanza then
-- add an extra BR for copy-paste
s = s .. linebreak .. '</div>'
end
-- and now open the pending stanza
s = s .. pending_stanza
end
pending_stanza = nil
num_stanzas = num_stanzas + 1
end
if line_data['line_num'] then
have_line_num = true
local ln = mw.html.create("span")
:addClass("ws-poem-linenum")
:wikitext(line_data['line_num'])
s = s .. tostring(ln)
end
if line_data['verse_num'] then
have_verse_num = true
local vn = mw.html.create("span")
:addClass("ws-poem-versenum")
:wikitext(line_data['verse_num'] .. " ")
s = s .. tostring(vn)
end
-- open the line tag
local line_classes = line_data['classes'] or {}
table.insert(line_classes, 'ws-poem-line')
if line_data['align'] == 'r' then
table.insert(line_classes, 'ws-poem-right')
elseif line_data['align'] == 'c' then
table.insert(line_classes, 'ws-poem-center')
end
local line = ""
if not open and k == 1 and args['start'] == 'same-line' then
-- line is already opened on previous page
else
line = line .. "<span class=\"" .. table.concat(line_classes, " ") .. "\">"
end
-- add indentation (REVIEW: do this with CSS?)
if line_data['indent'] then
if line_data['indent']['em'] then
line = line .. construct_fixed_width(line_data['indent']['em'])
elseif line_data['indent']['nbsp'] then
local chr = " "
line = line .. chr:rep(line_data['indent']['nbsp'])
end
end
-- ...add the line content
line = line .. line_data['content']
if not close and k == #lines and args['end'] == 'same-line' then
-- don't close the line, it'll continue on the next page
else
line = line .. linebreak .. '</span>'
end
s = s .. line
num_lines = num_lines + 1
end
end
if args['end'] == "stanza" or close then
s = s .. linebreak .. '</div>'
end
if open then
local div = "<div class=\"ws-poem"
if args['class'] ~= nil then
div = div .. " " .. args['class']
end
-- hanging indentation is the default
if args['no_hi'] == nil then
div = div .. " ws-poem-hi"
end
-- add gutters if we see a line/verse number or the user tells us they want them
if have_verse_num or args["gutter"] == "left" or args["gutter"] == "both" then
div = div .. " ws-poem-left-gutter"
end
if have_line_num or args["gutter"] == "right" or args["gutter"] == "both" then
div = div .. " ws-poem-right-gutter"
end
div = div .. "\""
-- add an HTML and XML lang attributes if needed
if args['lang'] ~= nil then
div = div .. " lang=\"" .. args['lang'] .. "\""
div = div .. " xml:lang=\"" .. args['lang'] .. "\""
end
-- set up the CSS style if needed
local style = ""
if args['align'] ~= nil and args['align'] ~= "" then
style = style .. 'text-align:' .. args['align'] .. ';'
end
if args['style'] ~= nil and args['style'] ~= "" then
style = style .. args['style']
end
if style ~= "" then
div = div .. " style=\"" .. style .. "\""
end
div = div .. ">"
s = div .. s
end
if close then
s = s .. "</div>"
end
return s
end
function p.ppoem(frame)
local args = getArgs(frame)
return p._ppoem(args)
end
return p