tiny.css.token
Defined in tiny.css.
The CSS Syntax Level 3 tokenizer.
API (19)
Actions
Public operations.
Token.text: The whole token including any sigil, quotes, or unit.Token.unit: The dimension unit, empty for every other kind.Token.value: The consumer facing span described above.Tokenizer.initTokenizer.next: Consumes and returns the next token.consumeIdent: Consumes an identifier starting atindexand returns its end.hasEscape: Whethertextcarries an escape and therefore needsunescape.isIdent: Whetherbytemay continue an identifier.isIdentStart: Whetherbytemay start an identifier.isWhitespace: Whetherbyteis one of the five CSS whitespace bytes.startsIdent: Whether an identifier starts atindex, per section 4.3.9.startsNumber: Whether a number starts atindex, per section 4.3.10.unescape: Decodes CSS escapes fromtextintoout, dropping escaped newlines.validEscape: Whether a valid escape starts atindex, per section 4.3.8.
Types and contracts
Public types and contracts.
HashKind: Whether a hash token could name an identifier, which decides whether#ais an id selector or a hex color.Kind: The token kinds CSS Syntax Level 3 section 4 produces.NumericKind: Whether a numeric token was written without a fraction or an exponent.Token: One token as a span over the caller's source bytes.Tokenizer: A forward tokenizer over borrowed source bytes.
Source
Source: lib/css/src/token/scan.zig:35
/// Whether a hash token could name an identifier, which decides whether `#a`/// is an id selector or a hex color.pub const HashKind = enum(u8) { unrestricted, id,};Source: lib/css/src/token/scan.zig:5
/// The token kinds CSS Syntax Level 3 section 4 produces. Comments are/// consumed rather than emitted, which is what the parsing algorithms expect.pub const Kind = enum(u8) { eof, whitespace, ident, function, at_keyword, hash, string, bad_string, url, bad_url, delim, number, percentage, dimension, cdo, cdc, colon, semicolon, comma, left_square, right_square, left_paren, right_paren, left_curly, right_curly,};Source: lib/css/src/token/scan.zig:41
/// Whether a numeric token was written without a fraction or an exponent.pub const NumericKind = enum(u8) { integer, number,};Source: lib/css/src/token/scan.zig:49
/// One token as a span over the caller's source bytes. `value` narrows to the/// part a consumer reads: an identifier without its sigil, a string without/// its quotes, a dimension without its unit.pub const Token = struct { kind: Kind, start: u32, end: u32, value_start: u32, value_end: u32, unit_start: u32 = 0, unit_end: u32 = 0, number: f64 = 0, numeric: NumericKind = .integer, hash_kind: HashKind = .unrestricted, escaped: bool = false, /// The whole token including any sigil, quotes, or unit. pub fn text(self: Token, source: []const u8) []const u8 { return source[self.start..self.end]; } /// The consumer facing span described above. pub fn value(self: Token, source: []const u8) []const u8 { return source[self.value_start..self.value_end]; } /// The dimension unit, empty for every other kind. pub fn unit(self: Token, source: []const u8) []const u8 { return source[self.unit_start..self.unit_end]; }};Source: lib/css/src/token/scan.zig:80
/// A forward tokenizer over borrowed source bytes. It allocates nothing and/// every `next` either advances `index` or returns `eof`.pub const Tokenizer = struct { source: []const u8, index: u32 = 0, pub fn init(source: []const u8) Tokenizer { std.debug.assert(source.len <= std.math.maxInt(u32)); return .{ .source = source }; } /// Consumes and returns the next token. pub fn next(self: *Tokenizer) Token { self.skipComments(); const start = self.index; if (start >= self.source.len) return self.single(.eof, start); const byte = self.source[start]; if (isWhitespace(byte)) return self.whitespace(start); if (byte == '"' or byte == '\'') return self.string(start, byte); if (byte == '#') return self.hash(start); if (byte == '+' or byte == '.') { if (startsNumber(self.source, start)) return self.numeric(start); return self.delim(start); } if (byte == '-') return self.minus(start); if (byte == '<') return self.lessThan(start); if (byte == '@') return self.atKeyword(start); if (byte == '\\') { if (validEscape(self.source, start)) return self.identLike(start); return self.delim(start); } if (isDigit(byte)) return self.numeric(start); if (isIdentStart(byte)) return self.identLike(start); if (punctuation(byte)) |kind| return self.single(kind, start); return self.delim(start); } fn skipComments(self: *Tokenizer) void { var guard: usize = 0; while (guard <= self.source.len) : (guard += 1) { const index = self.index; if (index + 1 >= self.source.len) return; if (self.source[index] != '/' or self.source[index + 1] != '*') return; const rest = self.source[index + 2 ..]; const close = std.mem.indexOf(u8, rest, "*/") orelse { self.index = @intCast(self.source.len); return; }; self.index = index + 2 + @as(u32, @intCast(close)) + 2; } unreachable; } fn single(self: *Tokenizer, kind: Kind, start: u32) Token { const end = if (kind == .eof) start else start + 1; self.index = end; return .{ .kind = kind, .start = start, .end = end, .value_start = start, .value_end = end }; } fn delim(self: *Tokenizer, start: u32) Token { return self.single(.delim, start); } fn whitespace(self: *Tokenizer, start: u32) Token { var index = start; while (index < self.source.len and isWhitespace(self.source[index])) index += 1; self.index = index; return .{ .kind = .whitespace, .start = start, .end = index, .value_start = start, .value_end = index, }; } fn hash(self: *Tokenizer, start: u32) Token { const after = start + 1; if (after < self.source.len and (isIdent(self.source[after]) or validEscape(self.source, after))) { const kind: HashKind = if (startsIdent(self.source, after)) .id else .unrestricted; const end = consumeIdent(self.source, after); self.index = end; return .{ .kind = .hash, .start = start, .end = end, .value_start = after, .value_end = end, .hash_kind = kind, .escaped = hasEscape(self.source[after..end]), }; } return self.delim(start); } fn minus(self: *Tokenizer, start: u32) Token { if (startsNumber(self.source, start)) return self.numeric(start); if (start + 2 < self.source.len and std.mem.eql(u8, self.source[start..][0..3], "-->")) { self.index = start + 3; return .{ .kind = .cdc, .start = start, .end = start + 3, .value_start = start, .value_end = start + 3, }; } if (startsIdent(self.source, start)) return self.identLike(start); return self.delim(start); } fn lessThan(self: *Tokenizer, start: u32) Token { if (start + 3 < self.source.len and std.mem.eql(u8, self.source[start..][0..4], "<!--")) { self.index = start + 4; return .{ .kind = .cdo, .start = start, .end = start + 4, .value_start = start, .value_end = start + 4, }; } return self.delim(start); } fn atKeyword(self: *Tokenizer, start: u32) Token { const after = start + 1; if (!startsIdent(self.source, after)) return self.delim(start); const end = consumeIdent(self.source, after); self.index = end; return .{ .kind = .at_keyword, .start = start, .end = end, .value_start = after, .value_end = end, .escaped = hasEscape(self.source[after..end]), }; } fn identLike(self: *Tokenizer, start: u32) Token { const end = consumeIdent(self.source, start); const name = self.source[start..end]; if (end < self.source.len and self.source[end] == '(') { if (std.ascii.eqlIgnoreCase(name, "url") and !quotedUrl(self.source, end + 1)) { return self.url(start, end + 1); } self.index = end + 1; return .{ .kind = .function, .start = start, .end = end + 1, .value_start = start, .value_end = end, .escaped = hasEscape(name), }; } self.index = end; return .{ .kind = .ident, .start = start, .end = end, .value_start = start, .value_end = end, .escaped = hasEscape(name), }; } fn url(self: *Tokenizer, start: u32, body: u32) Token { var index = body; while (index < self.source.len and isWhitespace(self.source[index])) index += 1; const value_start = index; while (index < self.source.len and self.source[index] != ')') { if (self.source[index] == '\\' and index + 1 < self.source.len) { index += 2; continue; } index += 1; } const value_end = trimTrailingSpace(self.source, value_start, index); const closed = index < self.source.len; self.index = if (closed) index + 1 else index; return .{ .kind = if (closed) .url else .bad_url, .start = start, .end = self.index, .value_start = value_start, .value_end = value_end, .escaped = hasEscape(self.source[value_start..value_end]), }; } fn string(self: *Tokenizer, start: u32, quote: u8) Token { var index = start + 1; while (index < self.source.len) { const byte = self.source[index]; if (byte == quote) { self.index = index + 1; return .{ .kind = .string, .start = start, .end = index + 1, .value_start = start + 1, .value_end = index, .escaped = hasEscape(self.source[start + 1 .. index]), }; } if (byte == '\n') break; if (byte == '\\' and index + 1 < self.source.len) { index += 2; continue; } index += 1; } self.index = index; return .{ .kind = .bad_string, .start = start, .end = index, .value_start = start + 1, .value_end = index, }; } fn numeric(self: *Tokenizer, start: u32) Token { const scanned = consumeNumber(self.source, start); var token = Token{ .kind = .number, .start = start, .end = scanned.end, .value_start = start, .value_end = scanned.end, .number = std.fmt.parseFloat(f64, self.source[start..scanned.end]) catch 0, .numeric = scanned.kind, }; if (startsIdent(self.source, scanned.end)) { const unit_end = consumeIdent(self.source, scanned.end); token.kind = .dimension; token.unit_start = scanned.end; token.unit_end = unit_end; token.end = unit_end; } else if (scanned.end < self.source.len and self.source[scanned.end] == '%') { token.kind = .percentage; token.end = scanned.end + 1; } self.index = token.end; return token; }};Source: lib/css/src/token/scan.zig:474
/// Consumes an identifier starting at `index` and returns its end.pub fn consumeIdent(source: []const u8, index: u32) u32 { var cursor = index; var guard: usize = 0; while (cursor < source.len and guard <= source.len) : (guard += 1) { if (isIdent(source[cursor])) { cursor += 1; continue; } if (validEscape(source, cursor)) { cursor += 2; if (isHex(source[cursor - 1])) { var digits: usize = 1; while (cursor < source.len and digits < 6 and isHex(source[cursor])) : (digits += 1) { cursor += 1; } if (cursor < source.len and isWhitespace(source[cursor])) cursor += 1; } continue; } break; } return @min(cursor, @as(u32, @intCast(source.len)));}Source: lib/css/src/token/scan.zig:383
/// Whether `text` carries an escape and therefore needs `unescape`.pub fn hasEscape(text: []const u8) bool { return std.mem.indexOfScalar(u8, text, '\\') != null;}Source: lib/css/src/token/scan.zig:393
/// Whether `byte` may continue an identifier.pub fn isIdent(byte: u8) bool { return isIdentStart(byte) or std.ascii.isDigit(byte) or byte == '-';}Source: lib/css/src/token/scan.zig:388
/// Whether `byte` may start an identifier.pub fn isIdentStart(byte: u8) bool { return std.ascii.isAlphabetic(byte) or byte == '_' or byte >= 0x80;}Source: lib/css/src/token/scan.zig:398
/// Whether `byte` is one of the five CSS whitespace bytes.pub fn isWhitespace(byte: u8) bool { return byte == ' ' or byte == '\t' or byte == '\n' or byte == '\r' or byte == 0x0C;}Source: lib/css/src/token/scan.zig:443
/// Whether an identifier starts at `index`, per section 4.3.9.pub fn startsIdent(source: []const u8, index: u32) bool { if (index >= source.len) return false; const byte = source[index]; if (isIdentStart(byte)) return true; if (byte == '-') { if (index + 1 >= source.len) return false; const after = source[index + 1]; return isIdentStart(after) or after == '-' or validEscape(source, index + 1); } return validEscape(source, index);}Source: lib/css/src/token/scan.zig:456
/// Whether a number starts at `index`, per section 4.3.10.pub fn startsNumber(source: []const u8, index: u32) bool { if (index >= source.len) return false; const byte = source[index]; if (isDigit(byte)) return true; if (byte == '.') return index + 1 < source.len and isDigit(source[index + 1]); if (byte != '+' and byte != '-') return false; if (index + 1 >= source.len) return false; if (isDigit(source[index + 1])) return true; return source[index + 1] == '.' and index + 2 < source.len and isDigit(source[index + 2]);}Source: lib/css/src/token/scan.zig:337
/// Decodes CSS escapes from `text` into `out`, dropping escaped newlines./// Returns null when the decoded identifier does not fit.pub fn unescape(text: []const u8, out: []u8) ?[]const u8 { var read: usize = 0; var written: usize = 0; while (read < text.len) { const byte = text[read]; if (byte != '\\') { if (written >= out.len) return null; out[written] = byte; written += 1; read += 1; continue; } read += 1; if (read >= text.len) { if (written + 3 > out.len) return null; written += std.unicode.utf8Encode(0xFFFD, out[written..]) catch return null; break; } if (text[read] == '\n') { read += 1; continue; } if (!isHex(text[read])) { const start = read; read += std.unicode.utf8ByteSequenceLength(text[read]) catch 1; const end = @min(read, text.len); if (written + (end - start) > out.len) return null; @memcpy(out[written..][0 .. end - start], text[start..end]); written += end - start; continue; } var point: u32 = 0; var digits: usize = 0; while (read < text.len and digits < 6 and isHex(text[read])) : (digits += 1) { point = point * 16 + hexValue(text[read]); read += 1; } if (read < text.len and isWhitespace(text[read])) read += 1; const scalar = if (point == 0 or point > 0x10FFFF or (point >= 0xD800 and point <= 0xDFFF)) 0xFFFD else point; written += std.unicode.utf8Encode(@intCast(scalar), out[written..]) catch return null; } return out[0..written];}Source: lib/css/src/token/scan.zig:468
/// Whether a valid escape starts at `index`, per section 4.3.8.pub fn validEscape(source: []const u8, index: u32) bool { if (index >= source.len or source[index] != '\\') return false; return index + 1 < source.len and source[index + 1] != '\n';}Source: lib/css/src/root.zig:44
pub const token = @import("token/root.zig");Source: lib/css/src/token/root.zig
//! The CSS Syntax Level 3 tokenizer.//!//! Every selector, declaration value, and at-rule prelude in this package is//! read through `Tokenizer`, so escapes, strings, comments, and numeric units//! are handled once instead of at each byte scanning site.//!//! The tokenizer borrows the caller's source bytes and allocates nothing. A//! token is a pair of spans over that source, so a consumer that needs decoded//! text calls `unescape` with its own buffer.//!//! Identifiers need no Unicode tables. Section 4.2 makes every byte at or above//! 0x80 an identifier code point, so UTF-8 identifiers scan byte wise.const scan = @import("scan.zig");pub const HashKind = scan.HashKind;pub const Kind = scan.Kind;pub const NumericKind = scan.NumericKind;pub const Token = scan.Token;pub const Tokenizer = scan.Tokenizer;pub const consumeIdent = scan.consumeIdent;pub const hasEscape = scan.hasEscape;pub const isIdent = scan.isIdent;pub const isIdentStart = scan.isIdentStart;pub const isWhitespace = scan.isWhitespace;pub const startsIdent = scan.startsIdent;pub const startsNumber = scan.startsNumber;pub const unescape = scan.unescape;pub const validEscape = scan.validEscape;Complete caller list for token.Tokenizer.init
7 direct callers.
lib.css.src.token.scan.expectKinds[function] — private source atlib/css/src/token/scan.zig:522in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_a_hash_token_separates_an_identifier_from_a_hex_color[function] — test source atlib/css/src/token/scan.zig:566in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_a_string_keeps_its_interior_and_an_unterminated_one_goes_bad[function] — test source atlib/css/src/token/scan.zig:576in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_an_escaped_identifier_stays_one_identifier_token[function] — test source atlib/css/src/token/scan.zig:621in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_every_token_advances_the_cursor_and_an_empty_source_ends_at_once[function] — test source atlib/css/src/token/scan.zig:631in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_numeric_tokens_carry_their_value,_flag,_and_unit[function] — test source atlib/css/src/token/scan.zig:545in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_url_takes_an_unquoted_body_and_a_function_takes_a_quoted_one[function] — test source atlib/css/src/token/scan.zig:588in nearest public ownerlib.css.src.token.scan
Complete caller list for token.Tokenizer.next
7 direct callers.
lib.css.src.token.scan.expectKinds[function] — private source atlib/css/src/token/scan.zig:522in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_a_hash_token_separates_an_identifier_from_a_hex_color[function] — test source atlib/css/src/token/scan.zig:566in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_a_string_keeps_its_interior_and_an_unterminated_one_goes_bad[function] — test source atlib/css/src/token/scan.zig:576in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_an_escaped_identifier_stays_one_identifier_token[function] — test source atlib/css/src/token/scan.zig:621in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_every_token_advances_the_cursor_and_an_empty_source_ends_at_once[function] — test source atlib/css/src/token/scan.zig:631in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_numeric_tokens_carry_their_value,_flag,_and_unit[function] — test source atlib/css/src/token/scan.zig:545in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.test_url_takes_an_unquoted_body_and_a_function_takes_a_quoted_one[function] — test source atlib/css/src/token/scan.zig:588in nearest public ownerlib.css.src.token.scan
Complete call list for token.Tokenizer.next
17 direct calls.
lib.css.src.token.scan.Tokenizer.atKeyword[method] — private source atlib/css/src/token/scan.zig:205in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.delim[method] — private source atlib/css/src/token/scan.zig:137in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.hash[method] — private source atlib/css/src/token/scan.zig:154in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.identLike[method] — private source atlib/css/src/token/scan.zig:220in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.lessThan[method] — private source atlib/css/src/token/scan.zig:191in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.minus[method] — private source atlib/css/src/token/scan.zig:175in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.numeric[method] — private source atlib/css/src/token/scan.zig:304in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.single[method] — private source atlib/css/src/token/scan.zig:131in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.skipComments[method] — private source atlib/css/src/token/scan.zig:115in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.string[method] — private source atlib/css/src/token/scan.zig:272in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.whitespace[method] — private source atlib/css/src/token/scan.zig:141in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.isDigit[function] — private source atlib/css/src/token/scan.zig:402in nearest public ownerlib.css.src.token.scantiny.css.token.isIdentStart[function] atlib/css/src/token/scan.zig:388tiny.css.token.isWhitespace[function] atlib/css/src/token/scan.zig:398lib.css.src.token.scan.punctuation[function] — private source atlib/css/src/token/scan.zig:415in nearest public ownerlib.css.src.token.scantiny.css.token.startsNumber[function] atlib/css/src/token/scan.zig:456tiny.css.token.validEscape[function] atlib/css/src/token/scan.zig:468
Complete caller list for token.isWhitespace
7 direct callers.
tiny.css.token.Tokenizer.next[method] atlib/css/src/token/scan.zig:90lib.css.src.token.scan.Tokenizer.url[method] — private source atlib/css/src/token/scan.zig:248in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.Tokenizer.whitespace[method] — private source atlib/css/src/token/scan.zig:141in nearest public ownerlib.css.src.token.scantiny.css.token.consumeIdent[function] atlib/css/src/token/scan.zig:474lib.css.src.token.scan.quotedUrl[function] — private source atlib/css/src/token/scan.zig:430in nearest public ownerlib.css.src.token.scanlib.css.src.token.scan.trimTrailingSpace[function] — private source atlib/css/src/token/scan.zig:436in nearest public ownerlib.css.src.token.scantiny.css.token.unescape[function] atlib/css/src/token/scan.zig:337
Audit
| Definitions | 20 |
|---|---|
| Public names | 20 |
| Members | 42 |
| Version | 26.7.0 |
| Revision | daab053ee433 |