Module:IPA vowel chart


local export = {}

local CURVATURE = 0.51

local function make_error(message, index)
	local specifier_index = math.floor((index - 1) / 4) + 1
	error(message .. " (argument #" .. index .. ", specifier #" .. specifier_index .. ")")
end

local function parse_fraction(arg, quality, index)
	local num, denom = mw.ustring.match(arg, "^([0-9]+)/([0-9]+)$")
	return num / denom
end

local function parse_scalar(arg, quality, index)
	arg = mw.text.trim(arg)
	local result
	if mw.ustring.match(arg, "^[0-9]+/[0-9]+$") then
		result = parse_fraction(arg, quality, index)
	else
		result = tonumber(arg)
	end
	if result == nil or result < 0 or result > 1 then
		return make_error(quality .. " must be a value within [0, 1]", index)
	end
	return result
end

local function parse_boolean(arg, quality, index)
	arg = mw.text.trim(arg)
	if arg == "0" then
		return 0
	elseif arg == "1" then
		return 1
	else
		return make_error(quality .. " must be a boolean", index)
	end
end

local function parse_vowel_specifier(args, index)
	local vowel = args[index]
	local openness = parse_scalar(args[index + 1], "openness", index + 1)
	local backness = parse_scalar(args[index + 2], "backness", index + 2)
	local roundness = parse_boolean(args[index + 3], "roundness", index + 3)
	return {
		vowel = vowel,
		openness = openness,
		backness = backness,
		roundness = roundness,
	}
end

local function curve(backness, openness, curvature)
	return backness * (CURVATURE + (1 - CURVATURE) * (1 - openness)) + openness * (1 - CURVATURE)
end

local function display_chart(vowels, args)
	local content = ""
	table.sort(vowels, function (a, b) return a.roundness < b.roundness end)

	local vowels_merged = {}
	local vowels_table = {}
	
	for _, vowel in ipairs(vowels) do
		local key = vowel.openness .. ":" .. vowel.backness
		if vowels_table[key] then
			table.insert(vowels_table[key].variants, vowel.vowel)
		else
			local new_table = {
				backness = vowel.backness,
				openness = vowel.openness,
				variants = {vowel.vowel}
			}
			vowels_table[key] = new_table
			table.insert(vowels_merged, new_table)
		end
	end

	vowels_table = nil
	for _, vowel in ipairs(vowels_merged) do
		local y = vowel.openness
		local x = curve(vowel.backness, y, CURVATURE)
		
		local parent = mw.html.create('div')
					:css('position', 'absolute')
					:css('top', (y * 100) .. "%")
					:css('left', (x * 100) .. "%")
					:css('font-size', '120%;')
					:tag('div')
						:css('position', 'relative')
						:css('left', '-2em')
						:css('top', '-1.25em')
						:css('width', '3em')
						:css('height', '2em')
						:css('padding', '0.5em')
						:css('text-align', 'center')
		
		for index, phoneme in ipairs(vowel.variants) do
			if index > 1 then parent:wikitext('&nbsp;&bull;&nbsp;') end
			parent = parent
					:tag('span')
							:attr('class', 'IPA')
							:wikitext(phoneme)
					:done()
		end
		
		content = content .. tostring(parent:allDone())
	end
	
	local style = args.style or ""
	
	local title_row
	if args.title then
		title_row = "\n|-\n! colspan=4 | " .. args.title .. "\n|-\n"
	else
		title_row = "\n"
	end
	
	return [[
	{| class="wikitable IPA-vowel-chart" style="]] .. style .. [["]] .. title_row..  [[
! !! class="backness-column" | Front !! class="backness-column" | Central !! class="backness-column" | Back
|-
! class="openness-row" | Close
| colspan="3" rowspan="7" | <div class="content-outer"><div class="content-inner"><div class="content-filler"><div class="trapezoid"></div></div>]] .. content .. [[</div></div>
|-
! class="openness-row" | Near-close
|-
! class="openness-row" | Close-mid
|-
! class="openness-row" | Mid
|-
! class="openness-row" | Open-mid
|-
! class="openness-row" | Near-open
|-
! class="openness-row" | Open
|}
]]
end

function export.show(frame)
	local args = frame:getParent().args
	local index = 1
	local vowels = {}
	
	while args[index] do
		if not args[index + 3] then
			error("Incomplete vowel specifier")
		end
		table.insert(vowels, parse_vowel_specifier(args, index))
		index = index + 4
	end
	
	return display_chart(vowels, { style = args["style"], title = args["title"] })
end

return export