Module:Ppoem/sandbox
Appearance
This is the module sandbox page for Module:Ppoem (diff). See also the companion subpage for test cases (run). |
This module depends on the following other modules: |
The module that provides the logic for {{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
local yesno = require('Module:Yesno')
local text_direction = require('Module:Lang')._text_direction
-- 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(next_char)
if 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 = mw.html.create('span')
:addClass('ws-poem-break')
:tag('br')
:allDone()
linebreak = tostring(linebreak)
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_classes_str = table.concat(line_classes, ' ')
-- get indentation (REVIEW: do this with CSS?)
local opening_indent = ''
if line_data['indent'] then
if line_data['indent']['em'] then
opening_indent = construct_fixed_width(line_data['indent']['em'])
elseif line_data['indent']['nbsp'] then
local chr = " "
opening_indent = chr:rep(line_data['indent']['nbsp'])
end
end
-- start with the line content
local line = opening_indent .. line_data['content']
-- check whether line is already opened on previous page
local is_continuing_line = not open and k == 1 and args['start'] == 'same-line'
-- check whether line will continue on the next page
local is_unfinished_line = not close and k == #lines and args['end'] == 'same-line'
if is_continuing_line and is_unfinished_line then
-- don't open or close the line
elseif is_continuing_line then
-- only close the line
line = line .. tostring(linebreak) .. '</span>'
elseif is_unfinished_line then
-- only open the line
line = '<span class=\"' .. line_classes_str .. '\">' .. line
else
line = mw.html.create('span'):addClass(line_classes_str):wikitext(line)
end
s = s .. tostring(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 = mw.html.create('div')
local container_classes = {'ws-poem', args['class']}
-- hanging indentation is the default
if not yesno(args['no_hi']) then
table.insert(container_classes, '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
table.insert(container_classes, 'ws-poem-left-gutter')
end
if have_verse_num or args['gutter'] == 'right' or args['gutter'] == 'both' then
table.insert(container_classes, 'ws-poem-right-gutter')
end
div:addClass(table.concat(container_classes, ' '))
-- add HTML and XML lang attributes if needed
if args['lang'] ~= nil then
div:attr('lang', args['lang'])
div:attr('xml:lang', args['lang'])
div:attr('dir', text_direction(args['lang']))
end
-- set up the CSS style if needed
div:css({['text-align'] = args['align'], ['style'] = args['style']})
-- add contents
div:wikitext(s)
div = tostring(div)
if not close then
div = string.gsub(div, '%</div%>$', '')
end
s = div
elseif close then
s = s .. '</div>'
end
return s
end
function p.ppoem(frame)
local args = getArgs(frame)
return p._ppoem(args)
end
return p