logicaffeine_language/lgx/
css.rs1pub const GROUP_AT_RULES: &[&str] = &[
23 "media",
24 "supports",
25 "container",
26 "layer",
27 "scope",
28 "starting-style",
29];
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum CssErrorKind {
33 UnbalancedBraces,
35 UnterminatedString,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct CssError {
41 pub kind: CssErrorKind,
42 pub at: usize,
44}
45
46impl CssError {
47 pub fn message(&self) -> String {
48 match self.kind {
49 CssErrorKind::UnbalancedBraces => "The braces in this stylesheet do not balance: every rule is `selector { declarations }` and every `{` needs its `}`.".into(),
50 CssErrorKind::UnterminatedString => "A string or `(` in this stylesheet never closes.".into(),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum CssItem {
58 Rule { prelude: String, body: String },
60 AtRule {
65 name: String,
66 prelude: String,
67 body: Option<String>,
68 body_at: usize,
69 },
70}
71
72pub fn parse_items(css: &str) -> Result<Vec<CssItem>, CssError> {
74 parse_items_at(css, 0)
75}
76
77fn parse_items_at(css: &str, base: usize) -> Result<Vec<CssItem>, CssError> {
80 let at = |e: CssError| CssError {
81 kind: e.kind,
82 at: e.at + base,
83 };
84 let items = parse_blanked(&blank_comments(css).map_err(at)?, base).map_err(at)?;
85 Ok(items.into_iter().map(|item| unblank(item)).collect())
86}
87
88fn parse_blanked(stripped: &str, base: usize) -> Result<Vec<CssItem>, CssError> {
90 let stripped = stripped.to_string();
91 let b = stripped.as_bytes();
92 let mut items = Vec::new();
93 let mut i = 0;
94 while i < b.len() {
95 while i < b.len() && (b[i].is_ascii_whitespace() || b[i] == BLANK as u8) {
96 i += 1;
97 }
98 if i >= b.len() {
99 break;
100 }
101 if b[i] == b'}' {
102 return Err(CssError {
103 kind: CssErrorKind::UnbalancedBraces,
104 at: i,
105 });
106 }
107 let prelude_start = i;
109 let mut depth_paren = 0usize;
110 loop {
111 if i >= b.len() {
112 let text = trim_blank(&stripped[prelude_start..]);
114 if text.is_empty() {
115 break;
116 }
117 if let Some(rest) = text.strip_prefix('@') {
118 let (name, prelude) = split_at_name(rest);
119 items.push(CssItem::AtRule {
120 name,
121 prelude,
122 body: None,
123 body_at: base + i,
124 });
125 break;
126 }
127 return Err(CssError {
128 kind: CssErrorKind::UnbalancedBraces,
129 at: prelude_start,
130 });
131 }
132 match b[i] {
133 b'"' | b'\'' => i = skip_string(&stripped, i)?,
134 b'(' => {
135 depth_paren += 1;
136 i += 1;
137 }
138 b')' => {
139 depth_paren = depth_paren.saturating_sub(1);
140 i += 1;
141 }
142 b';' if depth_paren == 0 => {
143 let text = trim_blank(&stripped[prelude_start..i]);
144 if let Some(rest) = text.strip_prefix('@') {
145 let (name, prelude) = split_at_name(rest);
146 items.push(CssItem::AtRule {
147 name,
148 prelude,
149 body: None,
150 body_at: base + i,
151 });
152 }
153 i += 1;
155 break;
156 }
157 b'{' if depth_paren == 0 => {
158 let prelude = trim_blank(&stripped[prelude_start..i]).to_string();
159 let body_start = i + 1;
160 let body_end = matching_brace(&stripped, i)?;
161 let body = stripped[body_start..body_end].to_string();
162 if let Some(rest) = prelude.strip_prefix('@') {
163 let (name, prelude) = split_at_name(rest);
164 items.push(CssItem::AtRule {
165 name,
166 prelude,
167 body: Some(body),
168 body_at: base + body_start,
169 });
170 } else {
171 items.push(CssItem::Rule { prelude, body });
172 }
173 i = body_end + 1;
174 break;
175 }
176 _ => i += 1,
177 }
178 }
179 }
180 Ok(items)
181}
182
183fn push_char_at(src: &str, at: usize, out: &mut String) -> usize {
195 match src[at..].chars().next() {
196 Some(c) => {
197 out.push(c);
198 at + c.len_utf8()
199 }
200 None => at + 1,
201 }
202}
203
204pub fn scope(css: &str, class: &str) -> Result<String, CssError> {
205 let items = parse_items(css)?;
206 Ok(emit_items(&items, class)?.join(" "))
207}
208
209fn emit_items(items: &[CssItem], class: &str) -> Result<Vec<String>, CssError> {
210 let mut out = Vec::new();
211 for item in items {
212 match item {
213 CssItem::Rule { prelude, body } => {
214 let selectors = split_top_level(prelude, ',');
215 let scoped: Vec<String> = selectors
216 .iter()
217 .map(|s| scope_selector(s.trim(), class))
218 .collect();
219 out.push(format!("{} {{{}}}", scoped.join(", "), body));
220 }
221 CssItem::AtRule {
222 name,
223 prelude,
224 body: None,
225 ..
226 } => {
227 out.push(if prelude.is_empty() {
228 format!("@{name};")
229 } else {
230 format!("@{name} {prelude};")
231 });
232 }
233 CssItem::AtRule {
234 name,
235 prelude,
236 body: Some(body),
237 body_at,
238 } => {
239 let head = if prelude.is_empty() {
240 format!("@{name}")
241 } else {
242 format!("@{name} {prelude}")
243 };
244 if GROUP_AT_RULES.contains(&name.to_ascii_lowercase().as_str()) {
245 let inner = emit_items(&parse_items_at(body, *body_at)?, class)?;
246 out.push(format!("{head} {{ {} }}", inner.join(" ")));
247 } else {
248 out.push(format!("{head} {{{}}}", body.replace(BLANK, "")));
249 }
250 }
251 }
252 }
253 Ok(out)
254}
255
256pub fn scope_selector(selector: &str, class: &str) -> String {
259 let parts = split_compounds(selector);
260 let subject = parts.iter().rposition(|(compound, _)| !is_global(compound));
262 let mut out = String::new();
263 for (idx, (compound, combinator)) in parts.iter().enumerate() {
264 let text = if is_global(compound) {
265 unwrap_global(compound)
266 } else if Some(idx) == subject {
267 stamp(compound, class)
268 } else {
269 compound.clone()
270 };
271 out.push_str(&text);
272 if let Some(c) = combinator {
273 if c.trim().is_empty() {
274 out.push(' ');
275 } else {
276 out.push(' ');
277 out.push_str(c.trim());
278 out.push(' ');
279 }
280 }
281 }
282 out
283}
284
285fn is_global(compound: &str) -> bool {
286 compound.starts_with(":global(")
287}
288
289fn unwrap_global(compound: &str) -> String {
290 let inner = &compound[":global(".len()..];
291 match inner.rfind(')') {
292 Some(k) => format!("{}{}", &inner[..k], &inner[k + 1..]),
293 None => inner.to_string(),
294 }
295}
296
297fn stamp(compound: &str, class: &str) -> String {
299 if compound == "*" {
300 return format!(".{class}");
301 }
302 let b = compound.as_bytes();
303 let mut depth = 0i32;
304 let mut i = 0;
305 while i < b.len() {
306 match b[i] {
307 b'(' | b'[' => depth += 1,
308 b')' | b']' => depth -= 1,
309 b':' if depth == 0 => {
310 let (head, tail) = compound.split_at(i);
311 return format!("{head}.{class}{tail}");
312 }
313 _ => {}
314 }
315 i += 1;
316 }
317 format!("{compound}.{class}")
318}
319
320fn split_compounds(selector: &str) -> Vec<(String, Option<String>)> {
323 let b = selector.as_bytes();
324 let mut parts: Vec<(String, Option<String>)> = Vec::new();
325 let mut current = String::new();
326 let mut depth = 0i32;
327 let mut i = 0;
328 while i < b.len() {
329 let c = b[i];
330 if depth == 0
331 && (c == b' ' || c == b'\t' || c == b'\n' || c == b'>' || c == b'+' || c == b'~')
332 {
333 let start = i;
335 while i < b.len()
336 && (b[i] == b' '
337 || b[i] == b'\t'
338 || b[i] == b'\n'
339 || b[i] == b'>'
340 || b[i] == b'+'
341 || b[i] == b'~')
342 {
343 i += 1;
344 }
345 let comb = selector[start..i].to_string();
346 if current.is_empty() {
347 continue;
349 }
350 if i >= b.len() {
351 break;
353 }
354 parts.push((std::mem::take(&mut current), Some(comb)));
355 continue;
356 }
357 match c {
358 b'(' | b'[' => depth += 1,
359 b')' | b']' => depth -= 1,
360 b'"' | b'\'' => {
361 let q = c;
362 current.push(c as char);
363 i += 1;
364 while i < b.len() && b[i] != q {
365 i = push_char_at(selector, i, &mut current);
366 }
367 }
368 _ => {}
369 }
370 if i < b.len() {
371 i = push_char_at(selector, i, &mut current);
372 } else {
373 i += 1;
374 }
375 }
376 if !current.is_empty() {
377 parts.push((current, None));
378 }
379 parts
380}
381
382fn split_top_level(s: &str, delim: char) -> Vec<String> {
384 let mut out = Vec::new();
385 let mut current = String::new();
386 let mut depth = 0i32;
387 let mut chars = s.chars().peekable();
388 while let Some(c) = chars.next() {
389 match c {
390 '(' | '[' => {
391 depth += 1;
392 current.push(c);
393 }
394 ')' | ']' => {
395 depth -= 1;
396 current.push(c);
397 }
398 '"' | '\'' => {
399 current.push(c);
400 for n in chars.by_ref() {
401 current.push(n);
402 if n == c {
403 break;
404 }
405 }
406 }
407 _ if c == delim && depth == 0 => out.push(std::mem::take(&mut current)),
408 _ => current.push(c),
409 }
410 }
411 if !current.trim().is_empty() || out.is_empty() {
412 out.push(current);
413 }
414 out.into_iter().filter(|s| !s.trim().is_empty()).collect()
415}
416
417fn split_at_name(rest: &str) -> (String, String) {
418 let rest = rest.trim();
419 let end = rest
420 .find(|c: char| c.is_ascii_whitespace() || c == '(')
421 .unwrap_or(rest.len());
422 (rest[..end].to_string(), rest[end..].trim().to_string())
423}
424
425const BLANK: char = '\u{0}';
429
430fn blank_comments(css: &str) -> Result<String, CssError> {
433 let mut out = String::with_capacity(css.len());
434 let b = css.as_bytes();
435 let mut i = 0;
436 while i < b.len() {
437 if css[i..].starts_with("/*") {
438 let end = css[i + 2..].find("*/").map_or(b.len(), |k| i + 2 + k + 2);
439 out.extend(std::iter::repeat(BLANK).take(end - i));
440 i = end;
441 continue;
442 }
443 if b[i] == b'"' || b[i] == b'\'' {
444 let end = skip_string(css, i)?;
445 out.push_str(&css[i..end]);
446 i = end;
447 continue;
448 }
449 let ch = css[i..].chars().next().unwrap();
450 out.push(ch);
451 i += ch.len_utf8();
452 }
453 Ok(out)
454}
455
456fn trim_blank(s: &str) -> &str {
458 s.trim_matches(|c: char| c.is_whitespace() || c == BLANK)
459}
460
461fn unblank(item: CssItem) -> CssItem {
463 let clean = |s: String| {
464 if s.contains(BLANK) {
465 s.replace(BLANK, "")
466 } else {
467 s
468 }
469 };
470 match item {
471 CssItem::Rule { prelude, body } => CssItem::Rule {
472 prelude: clean(prelude).trim().to_string(),
473 body: clean(body),
474 },
475 CssItem::AtRule {
476 name,
477 prelude,
478 body,
479 body_at,
480 } => CssItem::AtRule {
481 name: clean(name),
482 prelude: clean(prelude).trim().to_string(),
483 body,
484 body_at,
485 },
486 }
487}
488
489fn skip_string(s: &str, at: usize) -> Result<usize, CssError> {
491 let b = s.as_bytes();
492 let q = b[at];
493 let mut i = at + 1;
494 while i < b.len() {
495 if b[i] == b'\\' {
496 i += 2;
497 continue;
498 }
499 if b[i] == q {
500 return Ok(i + 1);
501 }
502 if b[i] == b'\n' {
503 break;
504 }
505 i += 1;
506 }
507 Err(CssError {
508 kind: CssErrorKind::UnterminatedString,
509 at,
510 })
511}
512
513fn matching_brace(s: &str, open: usize) -> Result<usize, CssError> {
515 let b = s.as_bytes();
516 let mut depth = 0usize;
517 let mut i = open;
518 while i < b.len() {
519 match b[i] {
520 b'"' | b'\'' => {
521 i = skip_string(s, i)?;
522 continue;
523 }
524 b'{' => depth += 1,
525 b'}' => {
526 depth -= 1;
527 if depth == 0 {
528 return Ok(i);
529 }
530 }
531 _ => {}
532 }
533 i += 1;
534 }
535 Err(CssError {
536 kind: CssErrorKind::UnbalancedBraces,
537 at: open,
538 })
539}