Module:Goban/accessibility
Appearance
--------------------------------------------------------------------------------
-- 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