Jump to content

Module:Ppoem

From Wikisource

--[=[
	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 cats = require('Module:Categories')
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
			cats:addCat("Pages using ppoem with custom stanza 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 rel_indent;
	line = line:gsub("^%+(%d*)>%s*", function(rel_indent_num)
		if rel_indent_num == "" then
			rel_indent = 1
		else
			rel_indent = tonumber(rel_indent_num)
		end
		return ""
	end, 1)

	local line_data = {
		type = 'line',
		align = alignment,
		content = line,
		line_num = line_num,
		verse_num = verse_num,
		rel_indent = rel_indent,
	}
	
	if #classes > 0 then
		line_data['classes'] = classes
		cats:addCat("Pages using ppoem with custom line 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 = "
\"" .. table.concat(classes, " ") .. "\" role=\"paragraph\">" 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'] if args['begin'] ~= nil then cats:addCat("Pages using ppoem with begin") 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. we put {} instead of {""} to not have a stray div -- split()/trim() throw when fed nil local lines = {} if args[1] ~= nil then lines = mw.text.split(mw.text.trim(args[1]), "\r?\n", false) end 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 = "
\"ws-poem-stanza\" role=\"paragraph\">" 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) local prev_lines_content = {} for k, v in pairs(lines) do local line_data = p.parse_line(v) if line_data['type'] == 'stanza' then prev_lines_content[k] = "" 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 if num_stanzas == 0 and num_lines == 0 and continued_stanza and not isPageNs then -- 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_classes_str = table.concat(line_classes, ' ') -- relative indentation based on previous lines local rel_indent = '' if line_data['rel_indent'] then if line_data['rel_indent'] >= k or line_data['rel_indent'] < 1 then cats:addCat("Pages using ppoem with invalid relative indents") else rel_indent = mw.html.create("span") :addClass("ws-poem-relative-indent") :wikitext(prev_lines_content[k-line_data['rel_indent']]) rel_indent = tostring(rel_indent) end end -- 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 = rel_indent .. opening_indent .. line_data['content'] prev_lines_content[k] = line -- 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 .. linebreak .. '' elseif is_unfinished_line then -- only open the line line = '\"' .. line_classes_str .. '\">' .. line else line = line .. linebreak 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) and #lines > 0 then s = s .. linebreak .. '
'
end if open then local div = mw.html.create('div') local container_classes = {'ws-poem', args['class']} if args['class'] ~= nil and args['class'] ~= "" then cats:addCat("Pages using ppoem with custom classes") end -- 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_line_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']}) div:cssText(args['style']) -- add contents div:wikitext(s) div = tostring(div) if not close then div = string.gsub(div, '%$', '') end s = div elseif close then s = s .. '
' end return s .. cats:getCats() end function p.ppoem(frame) local args = getArgs(frame) return p._ppoem(args) end return p