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.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a403fdfeeed1610f

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