Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:Goban/accessibility

From Wikipedia, the free encyclopedia
--------------------------------------------------------------------------------
-- Module:Goban/accessibility
--
-- Screen-reader support for [[Module:Goban]]: builds a plain-language
-- description of a Go board position from its tile codes, and wraps text in
-- {{Screen reader-only}} so it is voiced but not displayed.
--
--   describe(rows, cols, cells)  ->  description string
--       cells is a function(r, c) returning the tile code shown at row r,
--       column c (counted from the top left, 1-based).
--   hide(frame, text)            ->  wikitext hiding text visually
--   description(frame, rows, cols, cells, alt)
--                                ->  hidden wikitext describing the board,
--                                    using alt when given, else describe()
--------------------------------------------------------------------------------

local p = {}

-- Template that hides text visually while leaving it for screen readers.
local SR_ONLY_TEMPLATE = 'Screen reader-only'

-- Column letters in the usual Go notation, which skips "I".
local COLUMN_LETTERS = 'ABCDEFGHJKLMNOPQRSTUVWXYZ'

local COLOR_NAME = { b = 'black', w = 'white' }
local MARK_NAME = { S = 'square', T = 'triangle' }

-- Escape wikitext-significant characters in text that came from a cell code.
local function escape(s)
	return (s:gsub("[<>%[%]{}|&'=*#]", function(ch)
		return '&#' .. string.byte(ch) .. ';'
	end))
end

-- Coordinate of a cell in Go notation: column letter (A-T, no I) and row
-- number counted from the bottom of the diagram.
local function coordinate(r, c, rows)
	local letter = COLUMN_LETTERS:sub(c, c)
	if letter == '' then
		letter = 'column ' .. c .. ' '
	end
	return letter .. (rows - r + 1)
end

-- Build the plain-language description of the position.
function p.describe(rows, cols, cells)
	local black, white, moves, marks = {}, {}, {}, {}
	for r = 1, rows do
		for c = 1, cols do
			local code = cells(r, c)
			local at = coordinate(r, c, rows)
			local color, suffix = code:match('^([bw])([ST]?)$')
			local mcolor, mnumber = code:match('^([bw])(%d)$')
			if color then
				local list = (color == 'b') and black or white
				list[#list + 1] = at
				if suffix ~= '' then
					marks[#marks + 1] = MARK_NAME[suffix] .. ' on '
						.. COLOR_NAME[color] .. ' stone at ' .. at
				end
			elseif mcolor then
				moves[#moves + 1] = {
					n = tonumber(mnumber), color = COLOR_NAME[mcolor], at = at,
				}
			elseif code:match('^%d%d%d?$') then
				local n = tonumber(code)
				moves[#moves + 1] = {
					n = n, color = (n % 2 == 0) and 'white' or 'black', at = at,
				}
			elseif code == 'X' then
				marks[#marks + 1] = 'X at ' .. at
			elseif code:match('^%u$') then
				marks[#marks + 1] = 'letter ' .. code .. ' at ' .. at
			elseif code:match('^[udlr%-]*c$') then
				marks[#marks + 1] = 'circle at ' .. at
			elseif code == '' or code == '-' or code == 'x'
				or code:match('^[udlr]+$') then
				-- empty board: nothing to say
			else
				marks[#marks + 1] = escape(code) .. ' tile at ' .. at
			end
		end
	end
	table.sort(moves, function(a, b)
		if a.n ~= b.n then
			return a.n < b.n
		end
		return a.color < b.color
	end)

	local parts = {}
	if #black > 0 then
		parts[#parts + 1] = 'Black stones: ' .. table.concat(black, ', ') .. '.'
	end
	if #white > 0 then
		parts[#parts + 1] = 'White stones: ' .. table.concat(white, ', ') .. '.'
	end
	if #moves > 0 then
		local list = {}
		for i, m in ipairs(moves) do
			list[i] = m.n .. ' ' .. m.color .. ' ' .. m.at
		end
		parts[#parts + 1] = 'Moves: ' .. table.concat(list, '; ') .. '.'
	end
	if #marks > 0 then
		parts[#parts + 1] = 'Marks: ' .. table.concat(marks, '; ') .. '.'
	end
	local text = 'Go board, ' .. rows .. ' by ' .. cols
	if #parts == 0 then
		return text .. ', empty.'
	end
	return text .. '. ' .. table.concat(parts, ' ')
end

-- Wikitext that hides text visually but leaves it for screen readers.
-- Without a frame (unit tests) a bare span with the same class is returned.
function p.hide(frame, text)
	if frame and frame.expandTemplate then
		return frame:expandTemplate{ title = SR_ONLY_TEMPLATE, args = { text } }
	end
	return '<span class="sr-only">' .. text .. '</span>'
end

-- Hidden description of the board.  A non-blank alt replaces the generated
-- description.
function p.description(frame, rows, cols, cells, alt)
	local text = alt
	if text == nil or text:match('^%s*$') then
		text = p.describe(rows, cols, cells)
	else
		text = text:match('^%s*(.-)%s*$')
	end
	return p.hide(frame, text)
end

return p