lib/css/src/scan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! Byte level CSS scanning shared by the sheet parser and the media reader.
2 //!
3 //! These helpers respect strings, comments, and bracket nesting, which is what
4 //! keeps a declaration delimiter inside a data URL or a comma inside a media
5 //! query from ending the construct that holds it.
6
7 const std = @import("std");
8
9 pub fn findTopLevelByte(source: []const u8, start: usize, needle: u8) ?usize {
10 var index = start;
11 var square_depth: usize = 0;
12 var paren_depth: usize = 0;
13 while (index < source.len) : (index += 1) {
14 const byte = source[index];
15 if (byte == '"' or byte == '\'') {
16 index = skipString(source, index);
17 continue;
18 }
19 switch (byte) {
20 '[' => square_depth += 1,
21 ']' => {
22 if (square_depth > 0) square_depth -= 1;
23 },
24 '(' => paren_depth += 1,
25 ')' => {
26 if (paren_depth > 0) paren_depth -= 1;
27 },
28 else => {},
29 }
30 if (byte == needle and square_depth == 0 and paren_depth == 0) return index;
31 }
32 return null;
33 }
34
35 pub fn findBlockEnd(source: []const u8, open: usize) ?usize {
36 var index = open + 1;
37 var depth: usize = 1;
38 while (index < source.len) : (index += 1) {
39 const byte = source[index];
40 if (byte == '"' or byte == '\'') {
41 index = skipString(source, index);
42 continue;
43 }
44 if (byte == '/' and index + 1 < source.len and source[index + 1] == '*') {
45 const end = std.mem.indexOf(u8, source[index + 2 ..], "*/") orelse return null;
46 index += end + 3;
47 continue;
48 }
49 if (byte == '{') {
50 depth += 1;
51 continue;
52 }
53 if (byte == '}') {
54 depth -= 1;
55 if (depth == 0) return index;
56 }
57 }
58 return null;
59 }
60
61 pub fn skipString(source: []const u8, quote: usize) usize {
62 var index = quote + 1;
63 while (index < source.len) : (index += 1) {
64 if (source[index] == '\\') {
65 if (index + 1 < source.len) index += 1;
66 continue;
67 }
68 if (source[index] == source[quote]) return index;
69 }
70 return source.len;
71 }
72
73 pub fn findDeclarationEnd(source: []const u8, start: usize) usize {
74 return findTopLevelByte(source, start, ';') orelse source.len;
75 }
76
77 pub fn findParenEnd(source: []const u8, open: usize) ?usize {
78 var index = open + 1;
79 var depth: usize = 1;
80 while (index < source.len) : (index += 1) {
81 const byte = source[index];
82 if (byte == '"' or byte == '\'') {
83 index = skipString(source, index);
84 continue;
85 }
86 if (byte == '/' and index + 1 < source.len and source[index + 1] == '*') {
87 const end = std.mem.indexOf(u8, source[index + 2 ..], "*/") orelse return null;
88 index += end + 3;
89 continue;
90 }
91 if (byte == '(') {
92 depth += 1;
93 continue;
94 }
95 if (byte == ')') {
96 depth -= 1;
97 if (depth == 0) return index;
98 }
99 }
100 return null;
101 }
102
103 pub fn containsTopLevelLogical(source: []const u8) bool {
104 var index: usize = 0;
105 var paren_depth: usize = 0;
106 while (index < source.len) : (index += 1) {
107 const byte = source[index];
108 if (byte == '"' or byte == '\'') {
109 index = skipString(source, index);
110 continue;
111 }
112 switch (byte) {
113 '(' => paren_depth += 1,
114 ')' => {
115 if (paren_depth > 0) paren_depth -= 1;
116 },
117 else => {},
118 }
119 if (paren_depth == 0 and identByte(byte)) {
120 const end = readIdent(source, index);
121 const word = source[index..end];
122 if (std.ascii.eqlIgnoreCase(word, "and") or std.ascii.eqlIgnoreCase(word, "or")) return true;
123 index = end - 1;
124 }
125 }
126 return false;
127 }
128
129 pub fn consumePrefixIgnoreCase(source: []const u8, prefix: []const u8) ?[]const u8 {
130 if (source.len < prefix.len) return null;
131 if (!std.ascii.eqlIgnoreCase(source[0..prefix.len], prefix)) return null;
132 return source[prefix.len..];
133 }
134
135 pub fn endsWithIgnoreCase(source: []const u8, suffix: []const u8) bool {
136 if (source.len < suffix.len) return false;
137 return std.ascii.eqlIgnoreCase(source[source.len - suffix.len ..], suffix);
138 }
139
140 pub fn consumeWord(source: []const u8, index: *usize, word: []const u8) bool {
141 index.* = skipCssSpace(source, index.*);
142 const end = readIdent(source, index.*);
143 if (end == index.* or !std.ascii.eqlIgnoreCase(source[index.*..end], word)) return false;
144 index.* = end;
145 return true;
146 }
147
148 pub fn skipCssSpace(source: []const u8, start: usize) usize {
149 var index = start;
150 while (index < source.len and std.ascii.isWhitespace(source[index])) index += 1;
151 return index;
152 }
153
154 pub fn readIdent(source: []const u8, start: usize) usize {
155 var index = start;
156 while (index < source.len and identByte(source[index])) index += 1;
157 return index;
158 }
159
160 pub fn identByte(byte: u8) bool {
161 return std.ascii.isAlphanumeric(byte) or byte == '_' or byte == '-';
162 }
163
164 pub fn skipSpaceAndComments(source: []const u8, start: usize) usize {
165 var index = start;
166 while (index < source.len) {
167 while (index < source.len and std.ascii.isWhitespace(source[index])) index += 1;
168 if (index + 1 < source.len and source[index] == '/' and source[index + 1] == '*') {
169 const end = std.mem.indexOf(u8, source[index + 2 ..], "*/") orelse return source.len;
170 index += end + 4;
171 continue;
172 }
173 return index;
174 }
175 return index;
176 }
177
178 pub const AtRuleBoundaryKind = enum {
179 semicolon,
180 block,
181 };
182
183 pub const AtRuleBoundary = struct {
184 kind: AtRuleBoundaryKind,
185 index: usize,
186 };
187
188 pub fn findAtRuleBoundary(source: []const u8, start: usize) ?AtRuleBoundary {
189 var index = start;
190 var square_depth: usize = 0;
191 var paren_depth: usize = 0;
192 while (index < source.len) : (index += 1) {
193 const byte = source[index];
194 if (byte == '"' or byte == '\'') {
195 index = skipString(source, index);
196 continue;
197 }
198 switch (byte) {
199 '[' => square_depth += 1,
200 ']' => {
201 if (square_depth > 0) square_depth -= 1;
202 },
203 '(' => paren_depth += 1,
204 ')' => {
205 if (paren_depth > 0) paren_depth -= 1;
206 },
207 ';' => if (square_depth == 0 and paren_depth == 0) return .{ .kind = .semicolon, .index = index },
208 '{' => if (square_depth == 0 and paren_depth == 0) return .{ .kind = .block, .index = index },
209 else => {},
210 }
211 }
212 return null;
213 }