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

From Wikipedia, the free encyclopedia

-- 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