Jump to content

Module:Ppoem/sandbox

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 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 = "&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 = '&nbsp;'
			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 = "&nbsp;"
					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