Box-Shadow Generator — Zig source
Design layered CSS box-shadows with offset, blur, spread, color, and inset. Live preview and copy-ready CSS.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// box-shadow-generator — Zig polyglot showcase port.
//
// Pure CSS box-shadow builder. Formats one or more shadow layers and joins
// them into a single CSS box-shadow value. Deterministic, dependency-free,
// and never panics on bad input: invalid colors quietly fall back to a
// neutral translucent black, so a single bad color never breaks the whole
// stack.
//
// This is the Zig sibling of src/lib/boxShadow.ts (the canonical TypeScript
// that powers the live tool). The public surface mirrors the TS: a
// ShadowLayer struct plus parseColor, formatLayer, and buildBoxShadow
// (Zig convention keeps snake_case, matching the TS names directly). Zig
// ships no regex engine, so the color-shape checks are written by hand
// exactly like the Rust sibling — they accept what the TS regexes do,
// nothing more. Functions that build strings take an allocator; on
// allocation failure they degrade to a static fallback rather than
// bubbling an error, keeping the "total over arbitrary input" contract.
//
// Language: Zig 0.13 (standard library only).
// Ported from src/lib/boxShadow.ts.
// Source: CosmoDev polyglot showcase port.
// License: display source — part of CosmoDev's polyglot tool pages.
const std = @import("std");
/// A single layer in a CSS box-shadow stack.
pub const ShadowLayer = struct {
inset: bool = false, // draw the shadow inside the box
offset_x: f64 = 0, // horizontal offset in px
offset_y: f64 = 0, // vertical offset in px
blur: f64 = 0, // blur radius in px
spread: f64 = 0, // spread distance in px
color: []const u8 = "", // any CSS color (named, hex, rgb(), hsl(), ...)
};
/// Outcome of validating a color string. `err` is null when `ok` is true,
/// mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
/// When non-null it points at the allocator-owned message (or a static
/// fallback if allocation failed).
pub const ColorResult = struct {
ok: bool,
err: ?[]const u8,
};
/// CSS named colors accepted without further inspection. parseColor()
/// lower-cases its input first, so the comparison is effectively
/// case-insensitive.
const named_colors = [_][]const u8{
"transparent", "black", "white", "red", "green", "blue", "yellow",
"orange", "purple", "pink", "gray", "grey", "brown", "cyan", "magenta",
};
fn isNamedColor(c: []const u8) bool {
for (named_colors) |name| {
if (std.mem.eql(u8, c, name)) return true;
}
return false;
}
/// Matches the TS `^#[0-9a-f]{3}([0-9a-f]{3})?$` and `^#[0-9a-f]{8}$` rules:
/// a leading '#' followed by exactly 3, 6, or 8 lower-case hex digits.
/// `s` must already be lower-cased, as in the TS original.
fn isHexColor(s: []const u8) bool {
if (s.len < 2 or s[0] != '#') return false;
const n = s.len - 1;
if (n != 3 and n != 6 and n != 8) return false;
for (s[1..]) |c| {
const digit = c >= '0' and c <= '9';
const lower_hex = c >= 'a' and c <= 'f';
if (!digit and !lower_hex) return false;
}
return true;
}
/// Matches the TS `^rgba?\([^)]+\)$` / `^hsla?\([^)]+\)$` rules: a function
/// name, an opening paren, one or more characters that are not ')', and a
/// closing paren at the very end. The inner contents are otherwise
/// unchecked, matching the live tool's permissive behavior. `prefixes`
/// should list the longer form first (e.g. "rgba" before "rgb") so each is
/// tried independently.
fn isFunctionColor(s: []const u8, prefixes: []const []const u8) bool {
for (prefixes) |prefix| {
if (!std.mem.startsWith(u8, s, prefix)) continue;
const rest = s[prefix.len..];
// Need '(' ... ')' with at least one char strictly between. The TS
// `[^)]+` forbids any ')' inside; combined with the trailing `)$`
// anchor, the closing paren must be the final character.
if (rest.len >= 3 and rest[0] == '(' and rest[rest.len - 1] == ')') {
const inside = rest[1 .. rest.len - 1];
if (inside.len > 0 and std.mem.indexOfScalar(u8, inside, ')') == null) {
return true;
}
}
}
return false;
}
/// The whitespace set Python's `str.strip()` removes (ASCII whitespace).
fn isSpace(c: u8) bool {
return c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == 0x0b or c == 0x0c;
}
/// Copy `s` trimmed of whitespace and lower-cased into `buf`, returning the
/// canonical slice — the `.strip().lower()` parseColor applies once up front.
fn lowerTrim(buf: []u8, s: []const u8) []const u8 {
var start: usize = 0;
var end: usize = s.len;
while (start < end and isSpace(s[start])) start += 1;
while (end > start and isSpace(s[end - 1])) end -= 1;
var n: usize = 0;
for (s[start..end]) |c| {
if (n == buf.len) break;
buf[n] = std.ascii.toLower(c);
n += 1;
}
return buf[0..n];
}
/// The shape checks shared by parseColor (for its result) and normalizeColor
/// (for its fallback decision). `c` must already be lower-cased and trimmed.
fn colorMatches(c: []const u8) bool {
if (isNamedColor(c)) return true;
if (isHexColor(c)) return true;
if (isFunctionColor(c, &[_][]const u8{ "rgba", "rgb" })) return true;
if (isFunctionColor(c, &[_][]const u8{ "hsla", "hsl" })) return true;
return false;
}
/// Validate a CSS color string. Accepts the curated named-color set plus
/// hex (#rgb, #rrggbb, #rrggbbaa), rgb()/rgba(), and hsl()/hsla() forms.
/// The functional notations are checked for well-formed wrappers only, not
/// their numeric contents. `allocator` is used only to build the error
/// message; if that allocation fails the static text "invalid color" is
/// returned in its place.
pub fn parseColor(allocator: std.mem.Allocator, color: []const u8) ColorResult {
// Lower-case + trim once so every shape check below sees a canonical form.
var buf: [256]u8 = undefined;
const c = lowerTrim(&buf, color);
if (c.len == 0) return .{ .ok = false, .err = "empty color" };
if (colorMatches(c)) return .{ .ok = true, .err = null };
const msg = std.fmt.allocPrint(
allocator,
"invalid color: {s}",
.{color},
) catch "invalid color";
return .{ .ok = false, .err = msg };
}
/// Keep a color when it parses and otherwise substitute a neutral
/// translucent black. This is what makes buildBoxShadow total over
/// arbitrary input. No allocation: the trimmed slice borrows from `color`.
fn normalizeColor(color: []const u8) []const u8 {
var buf: [256]u8 = undefined;
const valid = blk: {
const c = lowerTrim(&buf, color);
break :blk colorMatches(c);
};
const ws = " \t\n\r\x0b\x0c";
return if (valid) std.mem.trim(u8, color, ws) else "rgba(0,0,0,0.5)";
}
/// Render an f64 the way JavaScript's template literals would: whole
/// numbers drop the decimal point (5, not 5.0); `{d}` keeps the short
/// decimal form for fractions (5.5 -> "5.5").
fn formatNumber(allocator: std.mem.Allocator, v: f64) []const u8 {
if (v == @trunc(v) and std.math.fabs(v) < 1e15) {
const whole: i64 = @intFromFloat(v);
return std.fmt.allocPrint(allocator, "{d}", .{whole}) catch "0";
}
return std.fmt.allocPrint(allocator, "{d}", .{v}) catch "0";
}
/// Render one shadow layer as its CSS fragment, e.g.
/// "inset 4px 8px 16px 0px #1a2b3c" or "0px 2px 4px 0px rgba(0,0,0,0.5)".
/// The returned slice is owned by `allocator`.
pub fn formatLayer(allocator: std.mem.Allocator, layer: ShadowLayer) []const u8 {
return std.fmt.allocPrint(allocator, "{s}{d}px {d}px {d}px {d}px {s}", .{
if (layer.inset) "inset " else "",
layer.offset_x,
layer.offset_y,
layer.blur,
layer.spread,
normalizeColor(layer.color),
}) catch "";
}
/// Compose a full CSS box-shadow declaration from an ordered list of layers
/// (the first layer renders on top). An empty slice yields the CSS keyword
/// "none", matching the property's default value. The returned slice is
/// owned by `allocator`.
pub fn buildBoxShadow(allocator: std.mem.Allocator, layers: []const ShadowLayer) []const u8 {
if (layers.len == 0) return "none";
var out = std.ArrayList(u8).init(allocator);
for (layers, 0..) |layer, i| {
if (i > 0) out.appendSlice(", ") catch return "";
out.appendSlice(formatLayer(allocator, layer)) catch return "";
}
return out.toOwnedSlice() catch "";
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →