| name | whatwg |
| description | Comprehensive guidance for implementing WHATWG specifications in Zig |
| license | MIT |
WHATWG Specification Implementation Skill
Purpose
This skill provides comprehensive guidance for implementing WHATWG specifications in Zig: from reading the spec to writing spec-compliant Zig code.
When to Use This Skill
Load this skill automatically when:
- Implementing features from any WHATWG specification
- Looking up specific algorithm steps or sections
- Mapping WHATWG spec concepts to Zig implementations
- Understanding data structures, state machines, or parsers
- Verifying correct implementation behavior
- Checking interface definitions and method signatures
What This Skill Provides
Integrated workflow for WHATWG spec implementation:
- Spec Navigation - Find and read WHATWG algorithms
- Context Detection - Automatically identify which spec you're working on
- Type Mapping - Map spec types to idiomatic Zig types
- Implementation Patterns - Complete examples with numbered steps matching spec
- Cross-Spec Dependencies - Handle dependencies between specs
- Spec Compliance - Ensure implementation matches specification exactly
Part 1: Reading WHATWG Specifications
Critical Rule: Always Load Complete Spec Sections
NEVER rely on grep fragments or partial algorithm text.
When implementing any WHATWG feature:
- Detect context - Identify which spec you're implementing
- Load the complete algorithm from the relevant spec file
- Read the full algorithm with all steps, context, and cross-references
- Check cross-spec dependencies - Algorithms often reference other WHATWG specs
- Understand edge cases - Every algorithm has corner cases and error conditions
Context Detection
This skill automatically detects which spec you're working on from:
File Path Detection
src/url/parser.zig → URL Standard
src/encoding/decoder.zig → Encoding Standard
src/streams/readable.zig → Streams Standard
src/infra/strings.zig → Infra Standard
src/console/logger.zig → Console Standard
src/mimesniff/sniff.zig → MIME Sniffing Standard
src/webidl/types.zig → WebIDL
Import Statement Detection
@import("url") → URL Standard
@import("encoding") → Encoding Standard
@import("infra") → Infra Standard
@import("webidl") → WebIDL
@import("streams") → Streams Standard
Manual Context
If context cannot be detected, the user will specify which spec they're implementing.
Available Specifications
Complete specifications are in specs/ directory:
| Spec | File | Status | Key Features |
|---|---|---|---|
| URL | specs/url.md |
✅ Implemented | Parsing, serialization, host parsing |
| Encoding | specs/encoding.md |
✅ Implemented | UTF-8, legacy encodings |
| Console | specs/console.md |
✅ Implemented | Logging APIs |
| MIME Sniff | specs/mimesniff.md |
✅ Implemented | MIME type detection |
| WebIDL | specs/webidl.md |
✅ Implemented | Type system, conversions |
| Infra | specs/infra.md |
✅ Implemented | Primitives (lists, strings, bytes) |
| Streams | specs/streams.md |
✅ Implemented | ReadableStream, WritableStream |
| Fetch | specs/fetch.md |
Available | HTTP requests |
| DOM | specs/dom.md |
Available | DOM APIs |
| XHR | specs/xhr.md |
Available | XMLHttpRequest |
Workflow: Spec to Implementation
Step 1: Identify Context
Automatic detection:
- Working in
src/url/→ URL Standard - Working in
src/encoding/→ Encoding Standard - Working in
src/streams/→ Streams Standard
Manual context:
- User says "I'm implementing URL parsing" → URL Standard
- User says "I need to decode UTF-8" → Encoding Standard
Step 2: Locate the Spec File
Pattern: specs/[spec-name].md
# URL Standard
specs/url.md
# Encoding Standard
specs/encoding.md
# Streams Standard
specs/streams.md
# Infra Standard (used by almost all specs)
specs/infra.md
Step 3: Find the Algorithm
Use ripgrep (rg) to find algorithms:
# Find URL parsing algorithm
rg -n "basic URL parser" specs/url.md
# Find encoding decoder
rg -n "decode" specs/encoding.md
# Find stream reading
rg -n "read from a readable stream" specs/streams.md
# Find Infra list operations
rg -n "append to a list" specs/infra.md
Common algorithm patterns:
- "To [operation name]" - Algorithm definitions
- "The [operation name] algorithm" - Formal algorithm sections
- "[Type] object's [operation]" - Object method algorithms
Step 4: Load Complete Section
Read the file with the read tool:
read("specs/url.md", offset=<start_line>, limit=<line_count>)
Find section boundaries:
- Load from algorithm header to the next algorithm or section
- Include all numbered steps, notes, and examples
- Don't stop at the first step - read the ENTIRE algorithm
Step 5: Understand Cross-Spec References
WHATWG specs frequently reference each other:
Common patterns:
- "Let result be an empty list" → Infra Standard (§4 Lists)
- "Let url be a new URL" → URL Standard
- "Encode using UTF-8" → Encoding Standard
- "Return a new promise" → WebIDL
When you see cross-spec references:
- Note the referenced spec
- Use
monorepo_navigationskill to find implementation insrc/ - If not implemented, use
dependency_mockingskill to create temporary mock
Spec Terminology
| Spec Term | Meaning |
|---|---|
| Let | Variable assignment |
| Set | Modify existing variable or property |
| Return | Return value from algorithm |
| Throw | Raise an exception |
| Assert | Invariant that should always be true |
| If...then...otherwise | Conditional logic |
| For each...in | Loop over collection |
| While | Loop with condition |
| Abort these steps | Early return (stop algorithm) |
| Continue | Skip to next iteration |
Part 2: WHATWG Type Mapping to Zig
Core Principle
Map WHATWG spec types to idiomatic Zig types that preserve spec semantics while enabling efficient implementation.
General Mapping Patterns
| Spec Concept | Zig Type | Notes |
|---|---|---|
| String | []const u8 |
UTF-8 encoded |
| Byte sequence | []const u8 |
Raw bytes |
| List | std.ArrayList(T) |
Dynamic array |
| Ordered map | Custom struct | Preserves insertion order |
| Boolean | bool |
Direct mapping |
| Integer | i32, u32, usize |
Context-dependent |
| Float | f64 |
Usually f64 for spec compliance |
| Enum/State | enum |
For spec-defined states |
| Record/Struct | struct |
For spec-defined types |
| Optional | ?T |
For "X or null" |
Context-Specific Type Mapping
URL Standard Types
| URL Spec Type | Zig Type | Example |
|---|---|---|
| URL record | struct |
pub const URL = struct { ... } |
| Host | union(enum) |
domain, IPv4, IPv6, opaque |
| Scheme | []const u8 |
"https", "http", "file" |
| Port | ?u16 |
Optional port (0-65535 or null) |
| Path | []const u8 |
"/path/to/resource" |
Example:
/// URL record from URL Standard §4.
pub const URL = struct {
scheme: []const u8,
username: []const u8,
password: []const u8,
host: ?Host,
port: ?u16,
path: []const u8,
query: ?[]const u8,
fragment: ?[]const u8,
allocator: Allocator,
pub fn deinit(self: *URL) void {
self.allocator.free(self.scheme);
self.allocator.free(self.username);
self.allocator.free(self.password);
if (self.host) |host| host.deinit(self.allocator);
self.allocator.free(self.path);
if (self.query) |q| self.allocator.free(q);
if (self.fragment) |f| self.allocator.free(f);
}
};
/// Host from URL Standard §4.2.
pub const Host = union(enum) {
domain: []const u8,
ipv4: u32,
ipv6: [8]u16,
opaque: []const u8,
pub fn deinit(self: Host, allocator: Allocator) void {
switch (self) {
.domain, .opaque => |s| allocator.free(s),
.ipv4, .ipv6 => {},
}
}
};
Encoding Standard Types
| Encoding Spec Type | Zig Type | Example |
|---|---|---|
| Decoder | struct |
Stateful decoder |
| Encoder | struct |
Stateful encoder |
| Code point | u21 |
Unicode code point (0-0x10FFFF) |
| Byte | u8 |
Single byte |
Example:
/// Decoder from Encoding Standard.
pub const Decoder = struct {
encoding: Encoding,
state: DecoderState,
allocator: Allocator,
pub fn decode(self: *Decoder, input: []const u8) ![]const u8 {
var output = std.ArrayList(u8).init(self.allocator);
errdefer output.deinit();
for (input) |byte| {
const result = try self.processByte(byte);
if (result.output) |code_point| {
try appendCodePoint(&output, code_point);
}
}
return output.toOwnedSlice();
}
};
Streams Standard Types
| Streams Spec Type | Zig Type | Example |
|---|---|---|
| ReadableStream | struct |
Stream state + queue |
| WritableStream | struct |
Stream state + queue |
| Stream state | enum |
readable/closed/errored |
| Reader/Writer | struct |
Locks the stream |
Example:
/// ReadableStream from Streams Standard §3.
pub const ReadableStream = struct {
state: State,
reader: ?*Reader,
queue: std.ArrayList([]const u8),
allocator: Allocator,
pub const State = enum {
readable,
closed,
errored,
};
pub fn getReader(self: *ReadableStream) !*Reader {
if (self.isLocked()) {
return error.TypeError;
}
const reader = try self.allocator.create(Reader);
errdefer self.allocator.destroy(reader);
reader.* = Reader{
.stream = self,
.allocator = self.allocator,
};
self.reader = reader;
return reader;
}
};
Infra Standard Types
| Infra Spec Type | Zig Type | Example |
|---|---|---|
| List | std.ArrayList(T) |
Dynamic array |
| Ordered map | Custom struct | Insertion-order map |
| Byte sequence | []const u8 |
Raw bytes |
| Code point | u21 |
Unicode code point |
| String | []const u8 |
UTF-8 encoded |
Example:
/// Ordered map from Infra Standard §7.
pub fn OrderedMap(comptime K: type, comptime V: type) type {
return struct {
entries: std.ArrayList(Entry),
allocator: Allocator,
const Entry = struct {
key: K,
value: V,
};
const Self = @This();
pub fn get(self: Self, key: K) ?V {
for (self.entries.items) |entry| {
if (std.meta.eql(entry.key, key)) {
return entry.value;
}
}
return null;
}
pub fn set(self: *Self, key: K, value: V) !void {
for (self.entries.items, 0..) |entry, i| {
if (std.meta.eql(entry.key, key)) {
self.entries.items[i].value = value;
return;
}
}
try self.entries.append(.{ .key = key, .value = value });
}
};
}
Part 3: Algorithm Implementation
Step-by-Step Implementation Pattern
Spec Algorithm:
To parse a URL, given a string input:
1. Let url be a new URL.
2. Let state be scheme start state.
3. Set url's scheme to the empty string.
4. For each code point c in input:
a. If state is scheme start state:
i. If c is an ASCII alpha, ...
ii. Otherwise, ...
5. Return url.
Zig Implementation:
/// Parse a URL string into a URL record.
///
/// Implements WHATWG URL "parse a URL" per §4.1.
///
/// ## Spec Reference
/// https://url.spec.whatwg.org/#concept-url-parser
///
/// ## Algorithm (URL §4.1)
/// 1. Let url be a new URL.
/// 2. Let state be scheme start state.
/// 3. Set url's scheme to the empty string.
/// 4. For each code point c in input...
/// 5. Return url.
///
/// ## Parameters
/// - `allocator`: Memory allocator
/// - `input`: URL string to parse
///
/// ## Returns
/// Parsed URL record, or error if invalid.
pub fn parse(allocator: Allocator, input: []const u8) !URL {
// 1. Let url be a new URL
var url = URL.init(allocator);
errdefer url.deinit();
// 2. Let state be scheme start state
var state = State.scheme_start;
// 3. Set url's scheme to the empty string
url.scheme = "";
// 4. For each code point c in input
var iter = CodePointIterator.init(input);
while (iter.next()) |c| {
// a. If state is scheme start state
if (state == .scheme_start) {
// i. If c is an ASCII alpha
if (isAsciiAlpha(c)) {
// ... (implementation)
}
// ii. Otherwise
else {
// ... (implementation)
}
}
}
// 5. Return url
return url;
}
Key Pattern:
- Numbered comments match spec steps exactly
- Preserve spec structure (nested if statements, loops)
- Use spec terminology in variable names
- Document with spec reference
State Machine Implementation
Many WHATWG specs use state machines (URL parsing, encoding, stream operations).
Pattern:
pub const State = enum {
scheme_start,
scheme,
no_scheme,
special_relative_or_authority,
// ... (all states from spec)
};
pub fn parse(allocator: Allocator, input: []const u8) !URL {
var url = URL.init(allocator);
errdefer url.deinit();
var state = State.scheme_start;
var buffer = std.ArrayList(u8).init(allocator);
defer buffer.deinit();
// Main state machine loop
var iter = CodePointIterator.init(input);
while (iter.next()) |c| {
switch (state) {
.scheme_start => {
if (isAsciiAlpha(c)) {
try buffer.append(toLower(c));
state = .scheme;
} else {
state = .no_scheme;
iter.rewind();
}
},
.scheme => {
if (isAsciiAlphanumeric(c) or c == '+' or c == '-' or c == '.') {
try buffer.append(toLower(c));
} else if (c == ':') {
url.scheme = try buffer.toOwnedSlice();
state = .special_relative_or_authority;
} else {
return error.InvalidScheme;
}
},
// ... (all other states)
}
}
return url;
}
Documentation Pattern for Spec Implementations
Every public function implementing a spec algorithm MUST have:
/// [Brief one-line description].
///
/// Implements WHATWG [Spec Name] "[algorithm name]" per §X.Y.
///
/// [Detailed explanation]
///
/// ## Spec Reference
///
/// https://[spec].spec.whatwg.org/#[anchor]
///
/// ## Algorithm ([Spec Name] §X.Y)
///
/// [Paste the complete algorithm from the spec, or summarize steps]
///
/// ## Parameters
///
/// - `param1`: Description
/// - `param2`: Description
///
/// ## Returns
///
/// Description of return value
///
/// ## Errors
///
/// - `error.ErrorName`: When this error occurs
///
/// ## Example
///
/// ```zig
/// const result = try functionName(allocator, input);
/// defer result.deinit();
/// ```
pub fn functionName(allocator: Allocator, input: []const u8) !Result {
// Implementation with numbered comments matching spec
}
Part 4: Spec-Specific Guidance
URL Standard
Key sections:
- §4 URL parsing - The basic URL parser (state machine)
- §4.3 Host parsing - IPv4, IPv6, domain, opaque host
- §4.5 URL serialization - Converting URL to string
Common algorithms:
- "basic URL parser" - Main parsing algorithm
- "host parser" - Parse host component
- "URL serializer" - Convert URL to string
Search patterns:
rg -n "basic URL parser" specs/url.md
rg -n "host parser" specs/url.md
rg -n "URL serializer" specs/url.md
Encoding Standard
Key sections:
- §4 Encodings - UTF-8, legacy encodings
- §5 API - Decoder and encoder interfaces
- §8 Legacy encodings - ISO-8859-1, Shift_JIS, etc.
Common algorithms:
- "decode" - Convert bytes to string
- "encode" - Convert string to bytes
- "UTF-8 decode" - UTF-8 decoding algorithm
Search patterns:
rg -n "UTF-8 decode" specs/encoding.md
rg -n "encode" specs/encoding.md
Streams Standard
Key sections:
- §3 Readable streams - ReadableStream, readers, controllers
- §4 Writable streams - WritableStream, writers, controllers
- §5 Transform streams - TransformStream
- §6 Queuing strategies - Buffering and backpressure
Common algorithms:
- "read from a readable stream" - Reading chunks
- "write to a writable stream" - Writing chunks
- "pipe to" - Connecting streams
Search patterns:
rg -n "read from a readable stream" specs/streams.md
rg -n "write to a writable stream" specs/streams.md
rg -n "pipe to" specs/streams.md
Infra Standard
Key sections:
- §2 Bytes - Byte sequences and operations
- §3 Code points - Unicode code point operations
- §4 Lists - List/array operations
- §5 Strings - String operations
- §7 Ordered maps - Key-value maps with insertion order
Common algorithms:
- "append to a list" - Add item to list
- "ASCII lowercase" - Lowercase ASCII string
- "split a string" - String splitting
Search patterns:
rg -n "append to a list" specs/infra.md
rg -n "ASCII lowercase" specs/infra.md
rg -n "split a string" specs/infra.md
Part 5: Cross-Spec Dependencies
Common Dependency Patterns
Infra - Used by nearly all specs:
- String operations (ASCII lowercase, split, strip)
- Byte operations
- List operations (append, prepend, remove)
- Ordered map (insertion-order key-value map)
WebIDL - Used by specs with Web APIs:
- Type conversions (DOMString, USVString, etc.)
- Interface definitions
URL - Used by Fetch, DOM, and many others:
- URL parsing
- URL serialization
- Host parsing
Encoding - Used by text processing specs:
- UTF-8 encoding/decoding
- Legacy encoding support
Streams - Used by Fetch, File API, etc.:
- ReadableStream
- WritableStream
- TransformStream
Handling Dependencies
When spec references another spec:
- Identify dependency - Note which spec is referenced
- Check if implemented - Use
ls src/[spec-name]/ - Import if available -
@import("spec-name") - Mock if unavailable - Use
dependency_mockingskill
Example:
// Spec says: "Let result be an empty list"
// This references Infra Standard
const infra = @import("infra");
pub fn parseURL(allocator: Allocator, input: []const u8) !URL {
// "Let segments be an empty list"
var segments = std.ArrayList([]const u8).init(allocator);
defer segments.deinit();
// Use Infra operations if needed
// try infra.append(&segments, segment);
}
Part 6: WebIDL Interface Naming Conventions
Critical Rules for WebIDL Interfaces
When implementing WebIDL interfaces, namespaces, mixins, or dictionaries:
File Naming: PascalCase
All WebIDL interface files MUST use PascalCase naming:
❌ WRONG:
webidl/src/encoding/text_decoder.zig
webidl/src/console/console.zig
webidl/src/dom/abort_controller.zig
✅ CORRECT:
webidl/src/encoding/TextDecoder.zig
webidl/src/console/Console.zig
webidl/src/dom/AbortController.zig
This applies to:
- Interfaces:
TextDecoder.zig,ReadableStream.zig,EventTarget.zig - Namespaces:
Console.zig - Mixins:
TextDecoderCommon.zig,EventTargetMixin.zig - Dictionaries:
TextDecoderOptions.zig,ReadableStreamReadResult.zig
Rationale: File names should match the type they define for clarity and consistency.
Attribute Naming: camelCase
All WebIDL interface attributes MUST use camelCase:
❌ WRONG:
pub const TextDecoder = webidl.interface(struct {
encoding_name: []const u8, // ❌ snake_case
do_not_flush: bool, // ❌ snake_case
ignore_bom: bool, // ❌ snake_case
reusable_utf16_buf: ?[]u16, // ❌ snake_case
});
✅ CORRECT:
pub const TextDecoder = webidl.interface(struct {
encodingName: []const u8, // ✅ camelCase
doNotFlush: bool, // ✅ camelCase
ignoreBOM: bool, // ✅ camelCase
reusableUtf16Buffer: ?webidl.DOMString, // ✅ camelCase + WebIDL type
});
Rationale: WebIDL attributes use camelCase per JavaScript naming conventions.
Method Naming: camelCase (No Prefixes)
WebIDL methods MUST use camelCase without prefixes:
❌ WRONG:
pub fn call_decode(...) ![]const u8 { } // ❌ call_ prefix
pub fn call_encode(...) ![]const u8 { } // ❌ call_ prefix
pub fn get_encoding() []const u8 { } // ❌ get_ prefix for getter
pub fn call_log(...) void { } // ❌ call_ prefix
✅ CORRECT:
pub fn decode(...) ![]const u8 { } // ✅ No prefix
pub fn encode(...) ![]const u8 { } // ✅ No prefix
pub fn encoding() []const u8 { } // ✅ No prefix (readonly attribute)
pub fn log(...) void { } // ✅ No prefix
Exception for getters: Readonly attributes may use method names matching the attribute:
readonly attribute DOMString encoding→pub fn encoding() []const u8readonly attribute boolean fatal→pub fn getFatal() bool(or just expose field)
Rationale: WebIDL methods map directly to JavaScript methods, which use camelCase without prefixes.
Type Usage: WebIDL/Infra Types
ALWAYS use WebIDL/Infra type aliases instead of raw Zig primitives:
❌ WRONG (raw Zig types):
pub const TextDecoder = webidl.interface(struct {
fatal: bool, // ❌ Use webidl.boolean
reusable_buf: ?[]u16, // ❌ Use webidl.DOMString
encoding_name: []const u8, // ❌ For WebIDL string, consider DOMString
});
✅ CORRECT (WebIDL types):
pub const TextDecoder = webidl.interface(struct {
fatal: webidl.boolean, // ✅ WebIDL type
reusableBuffer: ?webidl.DOMString, // ✅ WebIDL type (UTF-16)
encodingName: []const u8, // ✅ OK for internal UTF-8
});
Available WebIDL types (from src/webidl/root.zig):
// Primitives
webidl.boolean // bool
webidl.long // i32
webidl.@"unsigned long" // u32
webidl.double // f64
webidl.any // JSValue
// Strings
webidl.DOMString // UTF-16 string (infra.String)
webidl.USVString // UTF-16 scalar values (infra.String)
webidl.ByteString // Latin-1 string ([]const u8)
// Complex types
webidl.JSValue // JavaScript value union
webidl.JSObject // JavaScript object
webidl.Nullable(T) // T or null
webidl.Sequence(T) // Array of T
webidl.Record(K, V) // Map/dictionary
When to use WebIDL types vs Zig primitives:
| Use Case | Type Choice | Example |
|---|---|---|
| WebIDL interface parameter | WebIDL type | fn decode(input: webidl.ByteString) |
| WebIDL interface attribute | WebIDL type | fatal: webidl.boolean |
| WebIDL interface return | WebIDL type | fn encode() !webidl.ByteString |
| Internal implementation | Zig primitive OK | var index: usize = 0; |
| Spec algorithm variable | Zig primitive OK | var found: bool = false; |
Key principle: Use WebIDL types at API boundaries, Zig primitives for internal implementation.
Complete Example
BEFORE (incorrect naming):
// ❌ webidl/src/encoding/text_decoder.zig
const TextDecoderOptions = @import("text_decoder_options.zig").TextDecoderOptions;
pub const TextDecoder = webidl.interface(struct {
encoding_name: []const u8, // ❌ snake_case
do_not_flush: bool, // ❌ snake_case, raw type
ignore_bom: bool, // ❌ snake_case, raw type
pub fn get_encoding(self: *const TextDecoder) []const u8 { // ❌ get_ prefix
return self.encoding_name;
}
pub fn call_decode( // ❌ call_ prefix
self: *TextDecoder,
input: []const u8,
options: TextDecodeOptions,
) ![]const u8 {
// ...
}
});
AFTER (correct naming):
// ✅ webidl/src/encoding/TextDecoder.zig
const TextDecoderOptions = @import("TextDecoderOptions.zig").TextDecoderOptions;
pub const TextDecoder = webidl.interface(struct {
encodingName: []const u8, // ✅ camelCase
doNotFlush: webidl.boolean, // ✅ camelCase + WebIDL type
ignoreBOM: webidl.boolean, // ✅ camelCase + WebIDL type
pub fn encoding(self: *const TextDecoder) []const u8 { // ✅ No prefix
return self.encodingName;
}
pub fn decode( // ✅ No prefix
self: *TextDecoder,
input: []const u8,
options: TextDecodeOptions,
) ![]const u8 {
// ...
}
});
Import Statements
Update imports to use PascalCase filenames:
❌ WRONG:
const TextDecoder = @import("text_decoder.zig").TextDecoder;
const TextDecoderOptions = @import("text_decoder_options.zig").TextDecoderOptions;
✅ CORRECT:
const TextDecoder = @import("TextDecoder.zig").TextDecoder;
const TextDecoderOptions = @import("TextDecoderOptions.zig").TextDecoderOptions;
WebIDL Mixins
Interface mixins define reusable member bundles that can be included in multiple interfaces using composition.
What are Mixins?
From WebIDL spec:
An interface mixin is a definition (matching
InterfaceMixin) that declares a set of members that can be included by one or more interfaces.
Mixins are used to share common attributes and methods across multiple interfaces without inheritance.
Critical Rule: Always Use webidl.mixin()
ALL mixin definitions MUST use webidl.mixin() wrapper:
// IDL:
// interface mixin TextDecoderCommon {
// readonly attribute DOMString encoding;
// readonly attribute boolean fatal;
// readonly attribute boolean ignoreBOM;
// };
// ✅ CORRECT: Use webidl.mixin()
pub const TextDecoderCommon = webidl.mixin(struct {
encoding: []const u8,
fatal: webidl.boolean,
ignoreBOM: webidl.boolean,
});
// ❌ WRONG: Don't use plain struct or webidl.interface()
pub const TextDecoderCommon = struct { ... }; // ❌ Missing webidl.mixin()
pub const TextDecoderCommon = webidl.interface(struct { ... }); // ❌ Wrong wrapper
Key differences from webidl.interface():
- Mixins cannot be instantiated directly
- Mixins define shared members that are included in other interfaces
- Mixins do not have constructors
- Mixins are included using the WebIDL
includesstatement - MUST be wrapped with
webidl.mixin()
Mixin Composition Pattern (REQUIRED)
When an interface includes a mixin, use composition by embedding the mixin as a field:
// IDL:
// interface TextDecoder {
// constructor(...);
// // ... TextDecoder-specific members
// };
// TextDecoder includes TextDecoderCommon;
// ✅ CORRECT: Embed mixin as a field
pub const TextDecoder = webidl.interface(struct {
mixin: TextDecoderCommon, // ✅ Embed mixin as field
allocator: std.mem.Allocator,
enc: *const Encoding,
doNotFlush: bool,
bomSeen: bool,
// ... other interface-specific fields
pub fn init(allocator: std.mem.Allocator, label: []const u8, options: TextDecoderOptions) !TextDecoder {
const enc = try getEncoding(label);
return .{
.mixin = .{ // ✅ Initialize mixin
.encoding = enc.name,
.fatal = options.fatal,
.ignoreBOM = options.ignoreBOM,
},
.allocator = allocator,
.enc = enc,
.doNotFlush = false,
.bomSeen = false,
};
}
// ✅ Delegate getters to mixin
pub inline fn encoding(self: *const TextDecoder) []const u8 {
return self.mixin.encoding;
}
pub inline fn getFatal(self: *const TextDecoder) webidl.boolean {
return self.mixin.fatal;
}
pub inline fn getIgnoreBOM(self: *const TextDecoder) webidl.boolean {
return self.mixin.ignoreBOM;
}
});
❌ WRONG: Direct field duplication (DO NOT DO THIS):
// ❌ Don't duplicate mixin fields directly in the interface
pub const TextDecoder = webidl.interface(struct {
// ❌ These should be in a mixin field, not duplicated
encoding: []const u8,
fatal: webidl.boolean,
ignoreBOM: webidl.boolean,
allocator: std.mem.Allocator,
// ...
});
Multiple Mixin Composition
When an interface includes multiple mixins, use descriptive field names:
// IDL:
// interface TextDecoderStream {
// constructor(...);
// };
// TextDecoderStream includes TextDecoderCommon;
// TextDecoderStream includes GenericTransformStream;
pub const TextDecoderStream = webidl.interface(struct {
decoderMixin: TextDecoderCommon, // ✅ First mixin
transformMixin: GenericTransformStream, // ✅ Second mixin
allocator: std.mem.Allocator,
// ... other fields
pub fn init(...) !TextDecoderStream {
return .{
.decoderMixin = .{
.encoding = enc.name,
.fatal = options.fatal,
.ignoreBOM = options.ignoreBOM,
},
.transformMixin = .{
.transform = transform,
},
// ... other fields
};
}
// Delegate to TextDecoderCommon mixin
pub inline fn encoding(self: *const TextDecoderStream) []const u8 {
return self.decoderMixin.encoding;
}
// Delegate to GenericTransformStream mixin
pub inline fn readable(self: *const TextDecoderStream) *ReadableStream {
return self.transformMixin.readable();
}
});
File Naming for Mixins
Mixin files follow the same PascalCase naming as interfaces:
❌ WRONG: text_decoder_common.zig
✅ CORRECT: TextDecoderCommon.zig
Verifying Mixin Usage in Specs
When implementing an interface, ALWAYS check the spec for includes statements:
// In specs/encoding.md:
TextDecoder includes TextDecoderCommon;
TextDecoderStream includes TextDecoderCommon;
Steps to implement:
- Find all
includesstatements for the interface in the spec - Verify mixin definitions exist and use
webidl.mixin() - Embed each mixin as a field in the interface
- Delegate all mixin methods/getters to the mixin fields
- Never duplicate mixin fields directly in the interface
Common WebIDL Mixins (Verified)
From the WHATWG specs:
- TextDecoderCommon (
webidl/src/encoding/TextDecoderCommon.zig): Shared byTextDecoderandTextDecoderStream✅ - TextEncoderCommon (
webidl/src/encoding/TextEncoderCommon.zig): Shared byTextEncoderandTextEncoderStream✅ - GenericTransformStream (
webidl/src/streams/GenericTransformStream.zig): Shared by transform stream classes ✅ - ReadableStreamGenericReader (
webidl/src/streams/ReadableStreamGenericReader.zig): Shared by reader classes ✅ - WindowOrWorkerGlobalScope: Shared by
WindowandWorkerGlobalScope - Body: Shared by
RequestandResponse(Fetch API)
Complete Mixin Example
Mixin Definition (TextEncoderCommon.zig):
const webidl = @import("webidl");
pub const TextEncoderCommon = webidl.mixin(struct { // ✅ MUST use webidl.mixin()
/// The encoding name (always "utf-8")
encoding: []const u8,
});
Interface Including Mixin (TextEncoder.zig):
const webidl = @import("webidl");
const TextEncoderCommon = @import("TextEncoderCommon.zig").TextEncoderCommon;
pub const TextEncoder = webidl.interface(struct {
mixin: TextEncoderCommon, // ✅ Embed mixin as field
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) TextEncoder {
return .{
.mixin = .{ // ✅ Initialize mixin
.encoding = "utf-8",
},
.allocator = allocator,
};
}
pub fn deinit(self: *TextEncoder) void {
_ = self;
}
// ✅ Delegate getter to mixin
pub inline fn encoding(self: *const TextEncoder) []const u8 {
return self.mixin.encoding;
}
pub fn encode(self: *TextEncoder, input: []const u8) ![]const u8 {
// Implementation
}
});
Key Principles for Mixin Implementation
✅ DO:
- Always use
webidl.mixin()for mixin definitions - Embed mixins as fields in interfaces that include them
- Use descriptive field names for multiple mixins (
decoderMixin,transformMixin) - Delegate all getters from interface to mixin fields
- Initialize mixin fields in constructor
- Check spec for
includesstatements to find all required mixins - Never duplicate mixin fields directly in the interface struct
❌ DON'T:
- Don't use plain
structfor mixins (must usewebidl.mixin()) - Don't use
webidl.interface()for mixins (wrong wrapper) - Don't duplicate mixin fields directly in interface
- Don't skip delegation - always delegate to mixin
- Don't use generic names like
mixin1,mixin2for multiple mixins - Don't bypass mixin fields with direct access
Checklist for WebIDL Interfaces and Mixins
When creating or modifying WebIDL interfaces or mixins:
General Requirements:
- Identify the WebIDL type:
- Use
webidl.interface()for IDLinterfacedefinitions - Use
webidl.namespace()for IDLnamespacedefinitions - Use
webidl.mixin()for IDLinterface mixindefinitions (REQUIRED)
- Use
- File named in PascalCase (e.g.,
TextDecoder.zig, nottext_decoder.zig) - All attributes use camelCase (e.g.,
encodingName, notencoding_name) - All methods use camelCase without prefixes (e.g.,
decode(), notcall_decode()) - Use WebIDL type aliases (e.g.,
webidl.boolean, notboolfor attributes) - Use WebIDL string types appropriately (e.g.,
webidl.DOMStringfor UTF-16) - Import statements use PascalCase filenames
- Tests updated to use camelCase method names
- Documentation uses correct naming
For Mixin Definitions:
- MUST use
webidl.mixin()wrapper (not plain struct or webidl.interface) - Define only shared attributes (no constructors, no instance-specific fields)
- Use camelCase for mixin fields
- Document which interfaces include this mixin
For Interfaces Including Mixins:
- Check spec for ALL
includesstatements for this interface - Embed each mixin as a field (e.g.,
mixin: TextDecoderCommon) - Use descriptive field names for multiple mixins (e.g.,
decoderMixin,transformMixin) - Initialize mixin fields in constructor
- Add delegation methods for all mixin getters (e.g.,
pub inline fn encoding(self) { return self.mixin.encoding; }) - Never duplicate mixin fields directly in the interface struct
- Update all internal references to use
self.mixin.fieldinstead ofself.field
Quick Reference
Workflow Summary
- Identify context - Which spec am I implementing?
- Find algorithm -
rg -n "algorithm name" specs/[spec].md - Load complete section - Read entire algorithm with context
- Map types to Zig - Use type mapping tables above
- Implement with numbered comments - Match spec steps exactly
- Document with spec reference - Include URL and algorithm
- Test - Verify against spec behavior
Common Searches
# URL
rg -n "basic URL parser" specs/url.md
rg -n "host parser" specs/url.md
# Encoding
rg -n "UTF-8 decode" specs/encoding.md
rg -n "encode" specs/encoding.md
# Streams
rg -n "read from a readable stream" specs/streams.md
rg -n "write to a writable stream" specs/streams.md
# Infra
rg -n "append to a list" specs/infra.md
rg -n "ASCII lowercase" specs/infra.md
Type Mapping Quick Lookup
String → []const u8
Byte sequence → []const u8
List<T> → std.ArrayList(T)
Ordered map → Custom struct (Infra pattern)
Boolean → bool
Integer → i32, u32, usize (context-dependent)
Optional<T> → ?T
Enum/State → enum
Record/Struct → struct
Integration with Other Skills
This skill works with:
- zig - Provides Zig idioms for implementing algorithms
- monorepo_navigation - Finds cross-spec dependencies
- dependency_mocking - Creates mocks for unimplemented dependencies
Remember: Read complete spec sections, map types precisely, implement with numbered comments matching spec steps, and always reference the specification in documentation.