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 }