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 &emsp; 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 = "" return s end

-- construct a fixed width span for use in indenting local function construct_fixed_width(ems) local emsp = "&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 = "" 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 = ' '

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 .. ' '	   			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 .. "" 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 .. ' '           end s = s .. line num_lines = num_lines + 1 end end

if args['end'] == "stanza" or close then s = s .. linebreak .. ' '   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 .. " "   end

return s end

function p.ppoem(frame) local args = getArgs(frame) return p._ppoem(args) end

return p