Module:Soroban

local p = {}

local MAX_RODS = 31

local function trim(s)
	return mw.text.trim(s or "")
end

local function split_csv(s)
	local t = {}

	for item in mw.text.gsplit(s or "", ",") do
		item = trim(item)

		if item ~= "" then
			table.insert(t, tonumber(item) or 0)
		end
	end

	return t
end

local function limit_rods(values, max_rods)
	local limited = {}

	for i = 1, math.min(#values, max_rods) do
		table.insert(limited, values[i])
	end

	return limited
end

local function px(n)
	return string.format("%.1fpx", n)
end

local function clamp(n, min, max)
	n = tonumber(n) or min

	if n < min then
		return min
	elseif n > max then
		return max
	end

	return n
end

local function clean_color(value, default)
	value = trim(value)

	if value == "" then
		return default
	end

	-- Simple safe CSS color values:
	-- #ccc, #cccccc, gray, black, white, rgb(220,220,220)
	if mw.ustring.match(value, "^[#%w%s%-%.%(%)%,]+$") then
		return value
	end

	return default
end

local function yesno(value, default)
	value = mw.ustring.lower(trim(value or ""))

	if value == "" then
		return default
	end

	if value == "no" or value == "false" or value == "0" or value == "off" then
		return false
	end

	if value == "yes" or value == "true" or value == "1" or value == "on" then
		return true
	end

	return default
end

local function has_dot(dots, index, dot_every)
	if dots[index] == 1 then
		return true
	end

	if dot_every and dot_every > 0 then
		return ((index - 1) % dot_every) == 0
	end

	return false
end

local function add_rect(parent, class, x, y, w, h, css)
	local node = parent:tag("div")
		:addClass(class)
		:css({
			position = "absolute",
			left = px(x),
			top = px(y),
			width = px(w),
			height = px(h),
			["box-sizing"] = "border-box"
		})

	if css then
		node:css(css)
	end

	return node
end

local function add_hex_bead(parent, x, y, w, h, active, size, bead_color, inactive_bead_color, bead_border_color, bead_line_color)
	local fill = active and bead_color or inactive_bead_color
	local outer_x = x - w / 2
	local outer_y = y - h / 2
	local border = 1.2 * size

	-- One solid bead shape.
	-- This avoids white vertical seams between triangle pieces.
	parent:tag("div")
		:addClass("soroban-bead")
		:css({
			position = "absolute",
			left = px(outer_x),
			top = px(outer_y),
			width = px(w),
			height = px(h),
			background = fill,
			border = px(border) .. " solid " .. bead_border_color,
			["box-sizing"] = "border-box",
			["clip-path"] = "polygon(28% 0%, 72% 0%, 100% 50%, 72% 100%, 28% 100%, 0% 50%)",
			["z-index"] = "4"
		})

	-- Middle line across bead, similar to pgf-soroban.
	add_rect(
		parent,
		"soroban-bead-middle-line",
		outer_x + 4 * size,
		y - 0.5 * size,
		w - 8 * size,
		1 * size,
		{
			background = bead_line_color,
			["z-index"] = "6",
			opacity = "0.65"
		}
	)
end

local function add_rod(parent, x, y, h, rod_width, rod_color)
	add_rect(
		parent,
		"soroban-rod",
		x - rod_width / 2,
		y,
		rod_width,
		h,
		{
			background = rod_color,
			["z-index"] = "1"
		}
	)
end

local function add_unit_dot(parent, x, y, dot_size, size, dot_color, dot_border_color)
	parent:tag("div")
		:addClass("soroban-unit-dot")
		:css({
			position = "absolute",
			left = px(x - dot_size / 2),
			top = px(y - dot_size / 2),
			width = px(dot_size),
			height = px(dot_size),
			["border-radius"] = "50%",
			background = dot_color,
			border = px(1 * size) .. " solid " .. dot_border_color,
			["box-sizing"] = "border-box",
			["z-index"] = "10"
		})
end

local function add_soroban_frame(parent, width, soroban_top, soroban_height, frame_thick, frame_color)
	-- Backing thin border
	add_rect(
		parent,
		"soroban-frame-background",
		0,
		soroban_top,
		width,
		soroban_height,
		{
			border = px(1) .. " solid " .. frame_color,
			background = "transparent",
			["z-index"] = "0"
		}
	)

	-- Top frame bar
	add_rect(
		parent,
		"soroban-frame-top",
		0,
		soroban_top,
		width,
		frame_thick,
		{
			background = frame_color,
			["z-index"] = "8"
		}
	)

	-- Bottom frame bar
	add_rect(
		parent,
		"soroban-frame-bottom",
		0,
		soroban_top + soroban_height - frame_thick,
		width,
		frame_thick,
		{
			background = frame_color,
			["z-index"] = "8"
		}
	)

	-- Left frame bar
	add_rect(
		parent,
		"soroban-frame-left",
		0,
		soroban_top,
		frame_thick,
		soroban_height,
		{
			background = frame_color,
			["z-index"] = "8"
		}
	)

	-- Right frame bar
	add_rect(
		parent,
		"soroban-frame-right",
		width - frame_thick,
		soroban_top,
		frame_thick,
		soroban_height,
		{
			background = frame_color,
			["z-index"] = "8"
		}
	)
end

function p.render(frame)
	local args = frame.args

	local values = split_csv(args.rods or "")
	local dots = split_csv(args.dots or "")

	-- Default: 13 rods, like a common soroban.
	if #values == 0 then
		values = split_csv("0,0,0,0,0,0,0,0,0,0,0,0,0")
	end

	values = limit_rods(values, MAX_RODS)
	dots = limit_rods(dots, MAX_RODS)

	local header = args.header or ""
	local footer = args.footer or ""
	local caption = args.caption or ""

	local align = mw.ustring.lower(trim(args.align or ""))

	if align ~= "right" and align ~= "left" and align ~= "center" then
		align = ""
	end

	local size = clamp(args.size or 1, 0.25, 4)

	local dot_every = tonumber(args["dot-every"] or "")

	if dot_every then
		dot_every = math.floor(dot_every)

		if dot_every < 1 then
			dot_every = nil
		end
	end

	-- Default colors matched to the reference image.
	local bead_color = clean_color(args["bead-color"], "#dedede")
	local inactive_bead_color = clean_color(args["inactive-bead-color"], "#dedede")
	local bead_border_color = clean_color(args["bead-border-color"], "#b3b3b3")
	local bead_line_color = clean_color(args["bead-line-color"], "#8f8f8f")

	local rod_color = clean_color(args["rod-color"], "#b3b3b3")
	local frame_color = clean_color(args["frame-color"], "#9f9f9f")

	local beam_color = clean_color(args["beam-color"], "#ffffff")
	local beam_line_color = clean_color(args["beam-line-color"], "#000000")

	local dot_color = clean_color(args["dot-color"], "#ffffff")
	local dot_border_color = clean_color(args["dot-border-color"], "#999999")
	local text_color = clean_color(args["text-color"], "inherit")

	-- New option:
	-- | frame = yes  shows the frame
	-- | frame = no   removes the frame
	local show_frame = yesno(args.frame, true)

	local rod_count = #values

	-- Main proportions.
	local soroban_height = 231 * size
	local rod_gap = 56.5 * size
	local margin = 45 * size
	local frame_thick = 10 * size

	local header_height = header ~= "" and 28 * size or 0
	local footer_height = footer ~= "" and 28 * size or 0
	local caption_height = caption ~= "" and 32 * size or 0

	local width = margin * 2 + rod_gap * (rod_count - 1)
	local drawing_height = header_height + soroban_height + footer_height

	local soroban_top = header_height
	local beam_y = soroban_top + 66 * size

	local rod_top = soroban_top + frame_thick
	local rod_height = soroban_height - frame_thick * 2
	local rod_width = 10 * size

	local bead_width = 50 * size
	local bead_height = 30 * size
	local bead_step = 30 * size

	local upper_inactive_y = soroban_top + 31 * size
	local upper_active_y = beam_y - 22 * size

	local lower_active_start_y = beam_y + 22 * size
	local lower_inactive_start_y = soroban_top + 114 * size

	local dot_size = 7 * size

	local outer = mw.html.create("div")
	outer:addClass("soroban-container")
		:css({
			width = px(width),
			["max-width"] = "100%",
			color = text_color,
			["font-family"] = "sans-serif",
			["box-sizing"] = "border-box"
		})

	if align == "right" then
		outer:css({
			float = "right",
			clear = "right",
			margin = "0 0 1em 1em"
		})
	elseif align == "left" then
		outer:css({
			float = "left",
			clear = "left",
			margin = "0 1em 1em 0"
		})
	elseif align == "center" then
		outer:css({
			margin = "0.5em auto"
		})
	else
		outer:css({
			margin = "0.5em 0"
		})
	end

	local root = outer:tag("div")
		:addClass("soroban-wrapper")
		:css({
			position = "relative",
			width = px(width),
			height = px(drawing_height),
			["box-sizing"] = "border-box"
		})

	-- Header
	if header ~= "" then
		root:tag("div")
			:addClass("soroban-header")
			:wikitext(mw.text.nowiki(header))
			:css({
				position = "absolute",
				left = "0",
				top = "0",
				width = "100%",
				height = px(24 * size),
				["line-height"] = px(24 * size),
				["text-align"] = "center",
				["font-size"] = px(14 * size),
				color = text_color
			})
	end

	-- Optional soroban frame
	if show_frame then
		add_soroban_frame(
			root,
			width,
			soroban_top,
			soroban_height,
			frame_thick,
			frame_color
		)
	end

	-- Rods
	for i, value in ipairs(values) do
		local x = margin + (i - 1) * rod_gap

		add_rod(
			root,
			x,
			rod_top,
			rod_height,
			rod_width,
			rod_color
		)
	end

	-- Beam: white center with black upper and lower lines.
	add_rect(
		root,
		"soroban-beam",
		frame_thick,
		beam_y - 5 * size,
		width - frame_thick * 2,
		10 * size,
		{
			background = beam_color,
			["z-index"] = "2"
		}
	)

	add_rect(
		root,
		"soroban-beam-top-line",
		frame_thick,
		beam_y - 5 * size,
		width - frame_thick * 2,
		1.2 * size,
		{
			background = beam_line_color,
			["z-index"] = "3"
		}
	)

	add_rect(
		root,
		"soroban-beam-bottom-line",
		frame_thick,
		beam_y + 4 * size,
		width - frame_thick * 2,
		1.2 * size,
		{
			background = beam_line_color,
			["z-index"] = "3"
		}
	)

	-- Beads and unit dots
	for i, value in ipairs(values) do
		value = tonumber(value) or 0
		value = clamp(value, 0, 9)

		local x = margin + (i - 1) * rod_gap

		-- Upper bead
		local upper_active = value >= 5
		local upper_y = upper_active and upper_active_y or upper_inactive_y

		add_hex_bead(
			root,
			x,
			upper_y,
			bead_width,
			bead_height,
			upper_active,
			size,
			bead_color,
			inactive_bead_color,
			bead_border_color,
			bead_line_color
		)

		-- Lower beads
		local lower_value = value % 5

		for b = 1, 4 do
			local active = b <= lower_value
			local y

			if active then
				y = lower_active_start_y + (b - 1) * bead_step
			else
				y = lower_inactive_start_y + (b - 1) * bead_step
			end

			add_hex_bead(
				root,
				x,
				y,
				bead_width,
				bead_height,
				active,
				size,
				bead_color,
				inactive_bead_color,
				bead_border_color,
				bead_line_color
			)
		end

		if has_dot(dots, i, dot_every) then
			add_unit_dot(
				root,
				x,
				beam_y,
				dot_size,
				size,
				dot_color,
				dot_border_color
			)
		end
	end

	-- Footer
	if footer ~= "" then
		root:tag("div")
			:addClass("soroban-footer")
			:wikitext(mw.text.nowiki(footer))
			:css({
				position = "absolute",
				left = "0",
				bottom = "0",
				width = "100%",
				height = px(24 * size),
				["line-height"] = px(24 * size),
				["text-align"] = "center",
				["font-size"] = px(12 * size),
				color = text_color
			})
	end

	-- Caption
	if caption ~= "" then
		outer:tag("div")
			:addClass("soroban-caption")
			:wikitext(mw.text.nowiki(caption))
			:css({
				width = "100%",
				["box-sizing"] = "border-box",
				["text-align"] = "center",
				["font-size"] = px(12 * size),
				["line-height"] = "1.35",
				["padding-top"] = px(4 * size),
				color = text_color
			})
	end

	return tostring(outer)
end

return p

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.