Module:Goban
This module reimplements the version of {{Goban}} as of 11:29, 4 September 2024. It accepts the same tile codes, but draws the board with CSS alone instead of one image per intersection: the grid lines, hoshi, stones and rings are styled spans from Module:Goban/style.css (loaded with TemplateStyles), and letters, numbers and the triangle mark are text. It also comes with a description of the position, hidden using {{Screen reader-only}}. It accepts arbitrary sizes and can implement the pre-existing variants ({{Goban 5x5}}, {{Goban 9x9}}, {{Goban 11x11}}, {{Goban 13x13}}, {{Goban 7x11}}).
Usage
[edit]The module should not be invoked directly from articles. Use one of the templates instead. If you need a new size, create a template and wrap this module like this:
<includeonly>{{#invoke:Goban|board|rows=13|cols=13|u=u|d=d|l=l|r=r}}</includeonly><noinclude>{{documentation}}</noinclude>
See Template:Goban/doc/instructions for the tile codes. {{#invoke:Goban|tile|bT|32}} renders a single tile at the given size, for documentation.
Rendering
[edit]The tile size argument becomes the font-size of the board and the stylesheet measures everything in em, so one intersection is exactly one em square. Each intersection is a span.goban-pt whose grid lines are two background gradients; the classes goban-u, goban-ul and so on drop the line towards a board edge. A hoshi is a ::before dot, a red ring a ::after border, and a stone a child span.goban-stone with a radial gradient. Marks are text: letters are shown lower-case italic, the triangle is U+25B3 and the square is a bordered pseudo-element.
Screen readers
[edit]The drawn board is marked aria-hidden, so a screen reader announces none of its spans. The submodule Module:Goban/accessibility instead puts a description wrapped in {{Screen reader-only}} at the start of the board. The text is generated from the cell codes:
Go board, 19 by 19.followed by any stones without a move order,Black stones: D4, Q16.,White stones: …, any moves,Moves: 1 black K10; 2 white D4.(from the00–99,000–999andb1–w9tiles, in move order), and any marksMarks: triangle on black stone at D4; letter A at E5; X at F6; circle at G7.Unrecognised codes are read out as, for example,zz tile at H8.- An empty board reads
Go board, 19 by 19, empty. - Coordinates use Go notation relative to the diagram: columns A–T from the left (skipping I), rows numbered from the bottom of the diagram.
-- Module:Goban
--
-- Renders a Go board diagram from per-intersection tile codes. The board is
-- drawn with CSS alone (see Module:Goban/style.css, loaded through
-- TemplateStyles): grid lines, stones and hoshi are styled spans, marks are
-- plain text. No images are used. A text description of the position for
-- screen readers is included, hidden with {{Screen reader-only}} (see
-- Module:Goban/accessibility).
--
-- Usage from a template:
-- {{#invoke:Goban|board|rows=19|cols=19|u=u|d=d|l=l|r=r|hoshi=4,4;4,10;...}}
--
-- Board configuration is read from the #invoke arguments; the cells, the tile
-- size and (unless fixed by the #invoke) the border flags are read from the
-- template's own arguments.
--
-- Template arguments:
-- 1 .. rows*cols tile code for each intersection, row by row from the top
-- left. Empty (or all-whitespace) cells fall back to the
-- board's default tile for that position.
-- rows*cols + 1 tile size in pixels (default 24).
-- u, d, l, r border codes, only honoured when the #invoke does not
-- fix them (this is how {{Goban 5x5}}, {{Goban 9x9}} and
-- {{Goban 11x11}} behave; {{Goban}}, {{Goban 13x13}} and
-- {{Goban 7x11}} fix their borders and ignore them).
-- alt optional text read out by screen readers instead of the
-- automatically generated description of the position.
--
-- #invoke configuration:
-- rows, cols board dimensions (default 19 x 19).
-- u, d, l, r border code used for the top, bottom, left and right
-- edges. Corners concatenate them (u..l, u..r, d..l, d..r),
-- giving "ul", "ur", "dl", "dr". When a flag is omitted
-- from the #invoke, the template argument of the same name
-- is used instead (default: no border).
-- hoshi star points as "row,col" pairs separated by ";" (or
-- whitespace), counted from the top-left, e.g. "4,4;4,10".
-- An empty cell on a star point defaults to the "-" tile.
--
-- Tile codes (the same as the former "Go <code>.svg" image set):
-- (empty) plain intersection; u d l r ul ur dl dr board edges
-- - or x hoshi point; add a trailing c (c, -c, uc, ulc, ...)
-- for a red ring on an empty point
-- b, w black and white stones; bT wT triangle, bS wS square,
-- b0-b9 w0-w9 numbered stones
-- 00-99, 000-999 numbered stones for game records (odd black, even white)
-- A-Z except X letter mark (shown in lower case); X an X mark
-- 0-9 a bare digit on the wood, with no grid lines
--
-- {{#invoke:Goban|tile|<code>|<size>}} renders a single tile, for
-- documentation.
local accessibility = require('Module:Goban/accessibility')
local p = {}
local STYLES = 'Module:Goban/style.css'
local DEFAULT_SIZE = 24
local TRIANGLE = '\226\150\179' -- U+25B3 WHITE UP-POINTING TRIANGLE
-- Strip leading and trailing whitespace.
local function trim(s)
if s == nil then
return nil
end
return (tostring(s):match('^%s*(.-)%s*$'))
end
-- Returns the trimmed value, or nil when the value is absent or blank.
local function nonBlank(s)
s = trim(s)
if s == '' then
return nil
end
return s
end
-- Escape characters that are significant in HTML or wikitext.
local function escape(s)
return (s:gsub("[<>%[%]{}|&'\"=*#:;]", function(ch)
return '&#' .. string.byte(ch) .. ';'
end))
end
-- Parse "r,c;r,c;..." into a set keyed by "r,c".
local function parseHoshi(spec)
local set = {}
if spec == nil then
return set
end
for r, c in tostring(spec):gmatch('(%d+)%s*,%s*(%d+)') do
set[tonumber(r) .. ',' .. tonumber(c)] = true
end
return set
end
-- A span, given its classes and (already escaped) content.
local function span(classes, content)
return '<span class="' .. classes .. '">' .. (content or '') .. '</span>'
end
-- HTML for one intersection. Every tile is a .goban-pt span; the code
-- selects modifier classes and, for stones and marks, a child span.
local function tile(code)
-- Empty points, with optional board edges and a red ring.
local edges, ring = code:match('^([udlr]*)(c?)$')
if edges then
local classes = 'goban-pt'
if edges ~= '' then
classes = classes .. ' goban-' .. edges
end
if ring == 'c' then
classes = classes .. ' goban-c'
end
return span(classes)
end
ring = code:match('^[%-x](c?)$')
if ring then
return span('goban-pt goban-hoshi' .. (ring == 'c' and ' goban-c' or ''))
end
-- Stones: b w bT wT bS wS b0-b9 w0-w9, and 00-99 / 000-999 for game
-- records.
local color, mark = code:match('^([bw])([ST%d]?)$')
if not color and code:match('^%d%d%d?$') then
color = (tonumber(code) % 2 == 0) and 'w' or 'b'
mark = code
end
if color then
local classes = 'goban-stone goban-' .. color
local text = mark
if mark == 'T' then
classes = classes .. ' goban-T'
text = TRIANGLE
elseif mark == 'S' then
classes = classes .. ' goban-S'
text = ''
elseif #mark == 3 then
classes = classes .. ' goban-3'
end
return span('goban-pt', span(classes, text))
end
-- Marks on an empty point and bare digits.
if code == 'X' then
return span('goban-pt', span('goban-mark goban-X', 'X'))
elseif code:match('^%u$') then
return span('goban-pt', span('goban-mark', code))
elseif code:match('^%d$') then
return span('goban-pt goban-bare', span('goban-mark goban-label', code))
end
-- Anything else is shown verbatim so that the mistake is visible.
return span('goban-pt', span('goban-mark goban-unknown', escape(code)))
end
-- The <templatestyles> tag; without a frame (unit tests) nothing is emitted.
local function styles(frame)
if frame and frame.extensionTag then
return frame:extensionTag{ name = 'templatestyles', args = { src = STYLES } }
end
return ''
end
-- Builds the board HTML.
-- cfg #invoke arguments (board configuration)
-- args template arguments (cells, size, border flags, alt)
-- frame the #invoke frame, used to expand {{Screen reader-only}} and
-- <templatestyles>
function p._board(cfg, args, frame)
cfg = cfg or {}
args = args or {}
local rows = tonumber(cfg.rows) or 19
local cols = tonumber(cfg.cols) or rows
local hoshi = parseHoshi(cfg.hoshi)
-- Border codes: fixed by the #invoke, otherwise taken from the template
-- call ({{{u|}}} etc.), otherwise empty.
local function border(name)
if cfg[name] ~= nil then
return trim(cfg[name])
end
return trim(args[name]) or ''
end
local up, down, left, right =
border('u'), border('d'), border('l'), border('r')
-- Tile size in pixels; it becomes the font-size of the board, and the
-- stylesheet measures everything in em.
local size = tonumber(nonBlank(args[rows * cols + 1])) or DEFAULT_SIZE
-- Default tile for an empty intersection.
local function defaultCode(r, c)
local v = ''
if r == 1 then
v = up
elseif r == rows then
v = down
end
local h = ''
if c == 1 then
h = left
elseif c == cols then
h = right
end
local code = v .. h
if code == '' and hoshi[r .. ',' .. c] then
return '-'
end
return code
end
local function cellCode(r, c)
return nonBlank(args[(r - 1) * cols + c]) or defaultCode(r, c)
end
local out = {
styles(frame),
'<div class="goban" style="font-size:' .. size .. 'px">',
-- Screen-reader description of the position.
accessibility.description(frame, rows, cols, cellCode, args.alt),
'<span class="goban-grid" aria-hidden="true">',
}
local n = #out
for r = 1, rows do
n = n + 1
out[n] = '<span class="goban-row">'
for c = 1, cols do
n = n + 1
out[n] = tile(cellCode(r, c))
end
n = n + 1
out[n] = '</span>'
end
out[n + 1] = '</span></div>'
return table.concat(out)
end
-- Entry point for {{#invoke:Goban|board|...}}.
function p.board(frame)
local parent = frame:getParent()
local args = parent and parent.args or frame.args
-- Allow direct use ({{#invoke:Goban|board|rows=5|cols=5|cell|cell|...}})
-- when the invoking page passed no positional cells of its own.
if args[1] == nil and frame.args[1] ~= nil then
args = frame.args
end
return p._board(frame.args, args, frame)
end
-- Entry point for {{#invoke:Goban|tile|<code>|<size>}}: a single tile, for
-- documentation. Screen readers get the code itself.
function p.tile(frame)
local code = trim(frame.args[1]) or ''
return p._board(
{ rows = 1, cols = 1, u = '', d = '', l = '', r = '' },
{ code, frame.args[2], alt = escape(code ~= '' and code or 'empty') },
frame)
end
return p