use crate::ast::Stylesheet;
use crate::error::Error;
use crate::scanner::Pos;
pub(crate) fn parse(src: &str) -> Result<Stylesheet, Error> {
let scss = Transpiler::new(src).run()?;
crate::parser::parse_indented(&scss)
}
struct Raw {
line: usize,
indent: usize,
indent_str: String,
content: String,
}
struct Transpiler {
lines: Vec<Raw>,
idx: usize,
out: String,
out_lines: usize,
scanned: usize,
in_css_callable: bool,
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || (c as u32) >= 0x80
}
impl Transpiler {
fn new(src: &str) -> Self {
let normalized = src.replace("\r\n", "\n").replace(['\r', '\u{c}'], "\n");
let mut lines = Vec::new();
for (i, physical) in normalized.split('\n').enumerate() {
let mut indent = 0usize;
let mut indent_str = String::new();
let mut rest = physical;
for (b, ch) in physical.char_indices() {
if ch == ' ' || ch == '\t' {
indent += 1;
indent_str.push(ch);
} else {
rest = &physical[b..];
break;
}
rest = &physical[b + ch.len_utf8()..];
}
lines.push(Raw {
line: i + 1,
indent,
indent_str,
content: rest.to_string(),
});
}
Transpiler {
lines,
idx: 0,
out: String::new(),
out_lines: 0,
scanned: 0,
in_css_callable: false,
}
}
fn pad_to(&mut self, line_no: usize) {
self.out_lines += self.out[self.scanned..].bytes().filter(|&b| b == b'\n').count();
self.scanned = self.out.len();
while self.out_lines + 1 < line_no {
self.out.push('\n');
self.out_lines += 1;
}
self.scanned = self.out.len();
}
fn is_blank(&self, i: usize) -> bool {
self.lines
.get(i)
.map(|l| l.content.trim().is_empty())
.unwrap_or(true)
}
fn run(mut self) -> Result<String, Error> {
self.parse_block(0, 0)?;
Ok(self.out)
}
fn next_nonblank(&self, from: usize) -> Option<usize> {
let mut i = from;
while i < self.lines.len() {
if !self.is_blank(i) {
return Some(i);
}
i += 1;
}
None
}
fn parse_block(&mut self, block_indent: usize, _parent_indent: usize) -> Result<(), Error> {
loop {
let Some(i) = self.next_nonblank(self.idx) else {
self.idx = self.lines.len();
break;
};
self.idx = i;
let indent = self.lines[i].indent;
if indent < block_indent {
break;
}
if indent > block_indent {
return Err(Error::at(
"This line was indented unexpectedly.".to_string(),
Pos {
line: self.lines[i].line,
col: indent + 1,
},
));
}
self.parse_statement(block_indent)?;
}
Ok(())
}
fn parse_statement(&mut self, indent: usize) -> Result<(), Error> {
let start = self.idx;
let line_no = self.lines[start].line;
self.pad_to(line_no);
let trimmed = self.lines[start].content.trim_start().to_string();
if trimmed.starts_with("//") {
self.idx += 1;
self.consume_child_block(indent);
return Ok(());
}
if trimmed.starts_with("/*") {
return self.parse_loud_comment(indent);
}
if trimmed.starts_with("--") {
if let Some(()) = self.try_parse_custom_property_stmt(indent, line_no)? {
return Ok(());
}
}
let (mut logical, child_indent) = self.assemble_logical_line(indent)?;
self.extend_directive_prelude(&mut logical, line_no)?;
self.emit_statement(&logical, child_indent, indent, line_no)
}
fn consume_child_block(&mut self, indent: usize) {
while let Some(i) = self.next_nonblank(self.idx) {
if self.lines[i].indent <= indent {
break;
}
self.idx = i + 1;
}
}
fn parse_loud_comment(&mut self, indent: usize) -> Result<(), Error> {
let start = self.idx;
let line_no = self.lines[start].line;
let mut content = self.lines[start].content.trim_start().to_string();
let mut first_line_end = start + 1;
while interp_open_anywhere(&content) && first_line_end < self.lines.len() {
content.push('\n');
content.push_str(&self.lines[first_line_end].indent_str);
content.push_str(&self.lines[first_line_end].content);
first_line_end += 1;
}
if let Some(end) = content.find("*/") {
let after = content[end + 2..].trim_start();
if !after.is_empty() && !after.starts_with("//") && !after.starts_with("/*") {
return Err(Error::at(
"expected expression.".to_string(),
Pos {
line: line_no,
col: indent + end + 3,
},
));
}
self.out.push_str(&self.lines[start].indent_str);
self.out.push_str(&content[..end + 2]);
self.out.push('\n');
self.idx = first_line_end;
if let Some(i) = self.next_nonblank(self.idx) {
if self.lines[i].indent > indent {
return Err(Error::at(
"This line was indented unexpectedly.".to_string(),
Pos {
line: self.lines[i].line,
col: self.lines[i].indent + 1,
},
));
}
}
return Ok(());
}
let mut content_lines: Vec<Option<(usize, String)>> = Vec::new();
let first = content.strip_prefix("/*").unwrap_or(&content).to_string();
if !first.trim().is_empty() {
content_lines.push(Some((usize::MAX, first)));
}
self.idx = first_line_end;
let mut closed = false;
let mut pending_blanks = 0usize;
while self.idx < self.lines.len() {
let l = &self.lines[self.idx];
if l.content.trim().is_empty() {
pending_blanks += 1;
self.idx += 1;
continue;
}
if l.indent <= indent {
break;
}
for _ in 0..pending_blanks {
content_lines.push(None);
}
pending_blanks = 0;
let body = l.content.to_string();
content_lines.push(Some((l.indent, body.clone())));
self.idx += 1;
if body.contains("*/") {
closed = true;
break;
}
}
while matches!(content_lines.first(), Some(None)) {
content_lines.remove(0);
}
if content_lines.is_empty() {
content_lines.push(Some((3, String::new())));
}
if !closed {
if let Some(Some((_, last))) = content_lines.last_mut() {
if last.is_empty() {
*last = "*/".to_string();
} else {
last.push_str(" */");
}
}
}
let gutter = if indent == 0 { " * " } else { "* " };
for (i, line) in content_lines.iter().enumerate() {
if i == 0 {
self.out.push_str(&self.lines[start].indent_str);
}
match line {
Some((col, text)) if *col == usize::MAX => {
self.out.push_str("/*");
self.out.push_str(text);
}
Some((col, text)) => {
let pad = col.saturating_sub(indent).saturating_sub(3);
if i == 0 {
self.out.push_str("/* ");
} else {
self.out.push_str(gutter);
}
for _ in 0..pad {
self.out.push(' ');
}
self.out.push_str(text);
}
None => self.out.push_str(gutter.trim_end()),
}
self.out.push('\n');
}
Ok(())
}
fn assemble_logical_line(&mut self, indent: usize) -> Result<(String, usize), Error> {
let start = self.idx;
let mut logical = strip_statement_comment(self.lines[start].content.trim_start());
self.idx = start + 1;
let comma_continues = !logical.starts_with('@')
&& !logical.starts_with('$')
&& match find_decl_colon(&logical) {
None => true,
Some(c) => !logical[c + 1..].starts_with(char::is_whitespace),
};
let op_continues =
!logical.starts_with('@') && !logical.starts_with('$') && find_decl_colon(&logical).is_some();
loop {
if continuation_pending(&logical, comma_continues)
|| (op_continues && ends_with_value_operator(&logical))
{
if self.idx >= self.lines.len() {
break;
}
let next_content = self.lines[self.idx].content.clone();
let next_indent_str = self.lines[self.idx].indent_str.clone();
let st = scan_state(&logical);
let verbatim = st.in_interp || st.in_loud_comment || st.in_string || st.in_url;
if next_content.trim().is_empty() {
if st.bracket_depth > 0 || verbatim {
logical.push('\n');
self.idx += 1;
continue;
}
break;
}
let joined = if verbatim {
next_content.clone()
} else {
strip_silent_comment(&next_content)
};
if logical.ends_with('\\') {
logical.push('\n');
logical.push_str(&next_indent_str);
logical.push_str(&joined);
} else {
logical.push('\n');
logical.push_str(&next_indent_str);
logical.push_str(&joined);
}
self.idx += 1;
continue;
}
break;
}
Ok((logical, indent))
}
fn extend_directive_prelude(&mut self, logical: &mut String, start_line: usize) -> Result<(), Error> {
if !prelude_can_span(logical) {
return Ok(());
}
while prelude_incomplete(logical) {
let Some(i) = self.next_nonblank(self.idx) else {
break;
};
let piece = strip_silent_comment(self.lines[i].content.trim_start());
let (line, indent_str) = (self.lines[i].line, self.lines[i].indent_str.clone());
self.idx = i + 1;
if piece.is_empty() {
continue;
}
join_continuation(logical, start_line, line, &indent_str, &piece);
let comma_continues = matches!(directive_name(logical).as_deref(), Some("use" | "forward"));
while continuation_pending(logical, comma_continues) {
let Some(j) = self.next_nonblank(self.idx) else {
break;
};
let cont = strip_silent_comment(self.lines[j].content.trim_start());
let (line, indent_str) = (self.lines[j].line, self.lines[j].indent_str.clone());
self.idx = j + 1;
join_continuation(logical, start_line, line, &indent_str, &cont);
}
}
Ok(())
}
fn parse_child_into_braces(&mut self, indent: usize) -> Result<bool, Error> {
let Some(i) = self.next_nonblank(self.idx) else {
return Ok(false);
};
let child_indent = self.lines[i].indent;
if child_indent <= indent {
return Ok(false);
}
self.out.push_str(" {\n");
self.idx = i;
self.parse_block(child_indent, indent)?;
if self.out.ends_with('\n') {
self.out.pop();
if self.scanned > self.out.len() {
self.out_lines -= 1;
self.scanned = self.out.len();
}
}
self.out.push_str(" }\n");
Ok(true)
}
fn indent_str_for(&self, line_no: usize) -> String {
self.lines
.get(line_no.saturating_sub(1))
.map(|l| l.indent_str.clone())
.unwrap_or_default()
}
fn emit_statement(
&mut self,
logical: &str,
_child_indent: usize,
indent: usize,
line_no: usize,
) -> Result<(), Error> {
let mut logical = logical.trim();
let mut explicit_semicolon = false;
if let Some(semi) = find_top_level_semicolon(logical) {
let rest = &logical[semi + 1..];
let after = trim_trailing_loud_comments(rest.trim());
if !after.is_empty() {
let skipped = rest.len() - rest.trim_start().len();
let prefix = &logical[..semi + 1 + skipped];
let (line, col) = match prefix.rfind('\n') {
Some(nl) => (
line_no + prefix.matches('\n').count(),
prefix[nl + 1..].chars().count() + 1,
),
None => (line_no, indent + prefix.chars().count() + 1),
};
return Err(Error::at(
"multiple statements on one line are not supported in the indented syntax.".to_string(),
Pos { line, col },
));
}
logical = logical[..semi].trim_end();
explicit_semicolon = true;
}
if let Some(name) = forbids_indented_child(logical) {
if let Some(i) = self.next_nonblank(self.idx) {
if self.lines[i].indent > indent {
return Err(Error::at(
format!("Nothing may be indented beneath a @{name} rule."),
Pos {
line: self.lines[i].line,
col: self.lines[i].indent + 1,
},
));
}
}
}
let is_decl_with_value = !logical.starts_with('@')
&& !logical.starts_with('$')
&& !logical.starts_with("--")
&& find_decl_colon(logical).is_some_and(|c| {
let after = &logical[c + 1..];
after.starts_with(char::is_whitespace) && !after.trim().is_empty()
});
if is_decl_with_value {
if let Some(i) = self.next_nonblank(self.idx) {
if self.lines[i].indent > indent {
let child_indent = self.lines[i].indent;
let mut j = i;
while j < self.lines.len() {
let l = &self.lines[j];
if l.content.trim().is_empty() {
j += 1;
continue;
}
if l.indent < child_indent {
break;
}
if l.indent == child_indent {
let t = l.content.trim_start();
if !t.starts_with("//") && !t.starts_with("/*") && find_decl_colon(t).is_none() {
return Err(Error::at(
"expected \":\".".to_string(),
Pos {
line: l.line,
col: l.content.trim_end().chars().count() + 1,
},
));
}
}
j += 1;
}
}
}
}
if self.in_css_callable && !logical.contains("#{") {
if let Some(colon) = find_decl_colon(logical) {
if logical[..colon].trim().eq_ignore_ascii_case("result") {
if let Some(i) = self.next_nonblank(self.idx) {
if self.lines[i].indent > indent {
return Err(Error::at(
"Nothing may be indented beneath a @function result.".to_string(),
Pos {
line: self.lines[i].line,
col: self.lines[i].indent + 1,
},
));
}
}
}
}
}
self.out.push_str(&self.indent_str_for(line_no));
self.out.push_str(logical);
let css_callable = matches!(directive_name(logical).as_deref(), Some("function" | "mixin")) && {
let after_sigil: Vec<char> = logical
.trim_start()
.trim_start_matches(['@', '='])
.chars()
.collect();
let (_, end) = crate::parser::decode_ident(&after_sigil, 0);
after_sigil[end..]
.iter()
.collect::<String>()
.trim_start()
.starts_with("--")
};
let saved_callable = self.in_css_callable;
if css_callable {
self.in_css_callable = true;
}
let had_block = self.parse_child_into_braces(indent)?;
self.in_css_callable = saved_callable;
if !had_block {
let form = if explicit_semicolon {
EmptyForm::Semicolon
} else {
empty_form(logical)
};
match form {
EmptyForm::Braces => self.out.push_str(" {}\n"),
EmptyForm::Semicolon => self.out.push_str(";\n"),
}
}
Ok(())
}
fn try_parse_custom_property_stmt(
&mut self,
indent: usize,
_line_no: usize,
) -> Result<Option<()>, Error> {
let start = self.idx;
let raw = self.lines[start].content.trim_start().to_string();
let Some(colon) = find_decl_colon(&raw) else {
return Ok(None);
};
let name = raw[..colon].trim_end();
if !name.starts_with("--") || name.len() < 2 || name.contains(char::is_whitespace) {
return Ok(None);
}
self.idx = start + 1;
let mut value = raw[colon + 1..].to_string();
while custom_value_open(&value) && self.idx < self.lines.len() {
let l = &self.lines[self.idx];
value.push('\n');
value.push_str(&l.indent_str);
value.push_str(&l.content);
self.idx += 1;
}
if let Some(i) = self.next_nonblank(self.idx) {
if self.lines[i].indent > indent {
return Err(Error::at(
"Nothing may be indented beneath a custom property.".to_string(),
Pos {
line: self.lines[i].line,
col: self.lines[i].indent + 1,
},
));
}
}
self.out.push_str(&self.lines[start].indent_str);
self.out.push_str(&raw[..colon]);
self.out.push(':');
self.out.push_str(value.trim_end());
self.out.push_str(";\n");
Ok(Some(()))
}
}
fn join_continuation(logical: &mut String, start_line: usize, line: usize, indent_str: &str, piece: &str) {
let end_line = start_line + logical.bytes().filter(|&b| b == b'\n').count();
if line > end_line {
for _ in end_line..line {
logical.push('\n');
}
logical.push_str(indent_str);
} else if !logical.is_empty() && !logical.ends_with(char::is_whitespace) {
logical.push(' ');
}
logical.push_str(piece);
}
fn trailing_backslashes(s: &str) -> usize {
s.chars().rev().take_while(|&c| c == '\\').count()
}
fn plain_name_is_url(cs: &[char]) -> bool {
fn is_url(cs: &[char]) -> bool {
cs.len() == 3
&& cs[0].eq_ignore_ascii_case(&'u')
&& cs[1].eq_ignore_ascii_case(&'r')
&& cs[2].eq_ignore_ascii_case(&'l')
}
if is_url(cs) {
return true;
}
if cs.first() == Some(&'-') {
if let Some(rel) = cs[1..].iter().position(|&c| c == '-') {
if rel >= 1 {
return is_url(&cs[1 + rel + 1..]);
}
}
}
false
}
fn directive_name(logical: &str) -> Option<String> {
let t = logical.trim_start();
if t.starts_with('=') {
return Some("mixin".to_string());
}
if let Some(after) = t.strip_prefix('+') {
return after
.starts_with(|c: char| is_ident_char(c) || c == '#' || c == '\\')
.then(|| "include".to_string());
}
let rest = t.strip_prefix('@')?;
let cs: Vec<char> = rest.chars().collect();
let (name, _) = crate::parser::decode_ident(&cs, 0);
if name.is_empty() {
None
} else {
Some(name.to_ascii_lowercase())
}
}
fn prelude_can_span(logical: &str) -> bool {
if logical.trim_start().starts_with('$') {
return true;
}
matches!(
directive_name(logical).as_deref(),
Some(
"for"
| "each"
| "if"
| "else"
| "while"
| "function"
| "mixin"
| "include"
| "return"
| "warn"
| "debug"
| "error"
| "extend"
| "use"
| "forward"
| "at-root"
| "content",
)
)
}
fn forbids_indented_child(logical: &str) -> Option<String> {
match directive_name(logical).as_deref() {
Some(
name @ ("import" | "charset" | "return" | "extend" | "error" | "warn" | "debug" | "content"
| "use" | "forward"),
) => Some(name.to_string()),
_ => None,
}
}
fn ends_with_pending_operator(t: &str) -> bool {
let t = t.trim_end();
if t.ends_with('\\') {
return true;
}
if t.ends_with("as *") {
return false;
}
for op in ["+", "-", "*", "/", "%", "<", ">", "=", ":"] {
if t.ends_with(op) {
return true;
}
}
let last_word: String = t
.chars()
.rev()
.take_while(|c| is_ident_char(*c))
.collect::<String>()
.chars()
.rev()
.collect();
matches!(
last_word.to_ascii_lowercase().as_str(),
"from"
| "through"
| "to"
| "in"
| "and"
| "or"
| "not"
| "using"
| "as"
| "with"
| "show"
| "hide"
| "if"
)
}
fn prelude_incomplete(logical: &str) -> bool {
if bracket_depth(logical) > 0 {
return true;
}
let t = logical.trim_start();
if t.starts_with('$') {
if ends_with_pending_operator(t) {
return true;
}
return match find_decl_colon(t) {
Some(colon) => t[colon + 1..].trim().is_empty(),
None => true,
};
}
let Some(name) = directive_name(logical) else {
return false;
};
let t = logical.trim_start();
let after_sigil = &t[1..]; let owned;
let prelude = if t.starts_with(['=', '+']) {
after_sigil.trim()
} else {
let cs: Vec<char> = after_sigil.chars().collect();
let (_, end) = crate::parser::decode_ident(&cs, 0);
owned = cs[end..].iter().collect::<String>();
owned.trim()
};
if ends_with_pending_operator(prelude) {
return true;
}
let prelude: String = if prelude.bytes().any(|b| b == b'\n') {
prelude.split_whitespace().collect::<Vec<_>>().join(" ")
} else {
prelude.to_string()
};
let prelude = prelude.as_str();
match name.as_str() {
"for" => {
let lower = prelude.to_ascii_lowercase();
if !lower.contains(" from ") && !lower.ends_with(" from") {
return !lower.contains("from");
}
let has_bound = lower.contains(" through ") || lower.contains(" to ");
!has_bound
}
"each" => {
let lower = prelude.to_ascii_lowercase();
!(lower.contains(" in ") || lower.ends_with(" in"))
}
"if" | "while" => prelude.is_empty(),
"else" => {
let lower = prelude.to_ascii_lowercase();
lower == "if" || lower.ends_with(" if")
}
"function" | "mixin" => prelude.is_empty(),
"return" | "warn" | "debug" | "error" | "extend" => prelude.is_empty(),
"include" => prelude.is_empty(),
"use" | "forward" if prelude.ends_with(',') => true,
"use" | "forward" | "import" => prelude.is_empty(),
_ => false,
}
}
enum EmptyForm {
Braces,
Semicolon,
}
fn empty_form(logical: &str) -> EmptyForm {
let t = logical.trim_start();
if t.starts_with('@') || directive_name(t).is_some() {
let name = directive_name(t).unwrap_or_default();
return match name.as_str() {
"function" | "mixin" | "if" | "else" | "each" | "for" | "while" | "media" | "supports"
| "at-root" | "keyframes" | "-webkit-keyframes" | "-moz-keyframes" | "-o-keyframes"
| "-ms-keyframes" => EmptyForm::Braces,
"include" if has_top_level_using(t) => EmptyForm::Braces,
"include" | "return" | "import" | "use" | "forward" | "extend" | "content" | "warn" | "debug"
| "error" | "charset" => EmptyForm::Semicolon,
_ => EmptyForm::Semicolon,
};
}
if t.starts_with('$') {
return EmptyForm::Semicolon;
}
if let Some(colon) = find_decl_colon(t) {
let value = t[colon + 1..].trim();
let after = &t[colon + 1..];
let ws_after = after.starts_with(char::is_whitespace) || after.is_empty();
if value.is_empty() {
return EmptyForm::Semicolon;
}
if ws_after {
return EmptyForm::Semicolon;
}
return EmptyForm::Braces;
}
EmptyForm::Braces
}
fn continuation_pending(s: &str, comma_continues: bool) -> bool {
let st = scan_state(s);
if st.bracket_depth > 0 || st.in_interp || st.in_loud_comment {
return true;
}
let t = trim_trailing_loud_comments(s);
if t.ends_with('\\') {
return true;
}
if t.ends_with('!') {
return true;
}
comma_continues && t.ends_with(',')
}
struct LineScanner {
cs: Vec<char>,
i: usize,
byte: usize,
ascii: bool,
}
impl LineScanner {
fn new(s: &str) -> Self {
LineScanner {
cs: s.chars().collect(),
i: 0,
byte: 0,
ascii: s.is_ascii(),
}
}
#[inline]
fn done(&self) -> bool {
self.i >= self.cs.len()
}
#[inline]
fn cur(&self) -> char {
self.cs[self.i]
}
#[inline]
fn peek(&self, k: usize) -> Option<char> {
self.cs.get(self.i + k).copied()
}
#[inline]
fn offset(&self) -> usize {
if self.ascii {
self.i
} else {
self.byte
}
}
#[inline]
fn bump(&mut self) {
if !self.ascii {
self.byte += self.cs[self.i].len_utf8();
}
self.i += 1;
}
#[inline]
fn skip_quoted(&mut self) -> bool {
let q = self.cur();
self.bump(); while self.i < self.cs.len() && self.cs[self.i] != q {
if self.cs[self.i] == '\\' && self.i + 1 < self.cs.len() {
self.bump();
}
self.bump();
}
if self.i < self.cs.len() {
self.bump(); true
} else {
false
}
}
#[inline]
fn skip_interp(&mut self) {
self.bump(); self.bump(); let mut depth = 1;
while self.i < self.cs.len() && depth > 0 {
match self.cs[self.i] {
'"' | '\'' => {
self.skip_quoted();
continue;
}
'\\' => {
self.bump();
if !self.done() {
self.bump();
}
continue;
}
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
self.bump();
}
}
fn url_func_open(&self) -> Option<usize> {
let cur = *self.cs.get(self.i)?;
if !(is_ident_char(cur) || cur == '\\') {
return None;
}
if self.i > 0 && (is_ident_char(self.cs[self.i - 1]) || self.cs[self.i - 1] == '\\') {
return None;
}
let mut k = self.i;
let mut escaped = false;
while let Some(&c) = self.cs.get(k) {
if c == '\\' {
escaped = true;
break;
}
if is_ident_char(c) {
k += 1;
continue;
}
break;
}
if !escaped {
return (self.cs.get(k) == Some(&'(') && plain_name_is_url(&self.cs[self.i..k]))
.then(|| k + 1 - self.i);
}
let (name, k) = crate::parser::decode_ident(&self.cs, self.i);
if self.cs.get(k) != Some(&'(') {
return None;
}
crate::parser::is_url_function(&name).then(|| k + 1 - self.i)
}
fn skip_url(&mut self, open: usize) -> bool {
for _ in 0..open {
self.bump(); }
let mut depth = 1i32;
while !self.done() {
match self.cur() {
'"' | '\'' => {
self.skip_quoted();
}
'\\' => {
self.bump();
if !self.done() {
self.bump();
}
}
'(' => {
depth += 1;
self.bump();
}
')' => {
depth -= 1;
self.bump();
if depth == 0 {
return true;
}
}
_ => self.bump(),
}
}
false
}
#[inline]
fn skip_loud_comment(&mut self) -> bool {
self.bump(); self.bump(); while self.i + 1 < self.cs.len() && !(self.cs[self.i] == '*' && self.cs[self.i + 1] == '/') {
self.bump();
}
if self.i + 1 < self.cs.len() {
self.bump(); self.bump(); true
} else {
false
}
}
}
fn ends_with_value_operator(s: &str) -> bool {
let t = trim_trailing_loud_comments(s);
if t.ends_with("==") || t.ends_with("<=") || t.ends_with(">=") || t.ends_with("!=") {
return true;
}
if let Some(rest) = t.strip_suffix('-') {
let prev = rest.chars().next_back();
return !matches!(prev, Some(p) if is_ident_char(p));
}
if let Some(rest) = t.strip_suffix('%') {
let prev = rest.chars().next_back();
return matches!(prev, Some(p) if p.is_whitespace());
}
if let Some(c) = t.chars().next_back() {
if matches!(c, '+' | '*' | '/' | '<' | '>') {
return true;
}
}
let last_word: String = t
.chars()
.rev()
.take_while(|c| is_ident_char(*c))
.collect::<String>()
.chars()
.rev()
.collect();
if last_word.len() < t.trim_end().len() {
let before = &t[..t.len() - last_word.len()];
if !before.ends_with(char::is_whitespace) {
return false;
}
}
matches!(last_word.to_ascii_lowercase().as_str(), "and" | "or" | "not")
}
fn trim_trailing_loud_comments(s: &str) -> &str {
let mut t = s.trim_end();
while t.ends_with("*/") {
let Some(open) = t.rfind("/*") else {
break;
};
if open + 2 > t.len() - 2 {
break;
}
if !t[open..].ends_with("*/") || t[open + 2..t.len() - 2].contains("/*") {
break;
}
t = t[..open].trim_end();
}
t
}
fn strip_silent_comment(s: &str) -> String {
let mut sc = LineScanner::new(s);
while !sc.done() {
if let Some(open) = sc.url_func_open() {
let _closed = sc.skip_url(open);
continue;
}
match sc.cur() {
'"' | '\'' => {
sc.skip_quoted();
}
'/' if sc.peek(1) == Some('*') => {
sc.skip_loud_comment();
}
'/' if sc.peek(1) == Some('/') => {
return s[..sc.offset()].trim_end().to_string();
}
'#' if sc.peek(1) == Some('{') => {
sc.skip_interp();
}
_ => sc.bump(),
}
}
s.trim_end().to_string()
}
fn strip_statement_comment(s: &str) -> String {
if directive_name(s).as_deref() != Some("import") {
return strip_silent_comment(s);
}
let mut sc = LineScanner::new(s);
sc.bump();
let (_, end) = crate::parser::decode_ident(&sc.cs, sc.i);
while sc.i < end {
sc.bump();
}
loop {
while !sc.done() && sc.cur().is_whitespace() {
sc.bump();
}
if sc.done() {
break;
}
let quoted = matches!(sc.cur(), '"' | '\'');
if quoted {
sc.skip_quoted();
}
let mut depth = 0i32;
while !sc.done() {
if let Some(open) = sc.url_func_open() {
let _closed = sc.skip_url(open);
continue;
}
match sc.cur() {
'"' | '\'' => {
sc.skip_quoted();
}
'#' if sc.peek(1) == Some('{') => sc.skip_interp(),
'/' if quoted && sc.peek(1) == Some('*') => {
sc.skip_loud_comment();
}
'/' if quoted && sc.peek(1) == Some('/') => {
return s[..sc.offset()].trim_end().to_string();
}
'(' | '[' => {
depth += 1;
sc.bump();
}
')' | ']' => {
depth -= 1;
sc.bump();
}
';' if depth == 0 => {
let head = &s[..sc.offset()];
let tail = strip_silent_comment(&s[sc.offset()..]);
let mut out = String::with_capacity(head.len() + tail.len());
out.push_str(head);
out.push_str(&tail);
return out.trim_end().to_string();
}
',' if depth == 0 => {
sc.bump();
break;
}
_ => sc.bump(),
}
}
}
s.trim_end().to_string()
}
fn bracket_depth(s: &str) -> i32 {
scan_state(s).bracket_depth
}
struct ScanState {
bracket_depth: i32,
in_interp: bool,
in_loud_comment: bool,
in_string: bool,
in_url: bool,
}
fn custom_value_open(s: &str) -> bool {
if trailing_backslashes(s) % 2 == 1 {
return true;
}
let mut sc = LineScanner::new(s);
let mut brackets: Vec<char> = Vec::new();
while !sc.done() {
match sc.cur() {
'"' | '\'' => {
sc.skip_quoted();
}
'\\' => {
sc.bump();
if !sc.done() {
sc.bump();
}
}
'(' | '[' | '{' => {
brackets.push(match sc.cur() {
'(' => ')',
'[' => ']',
_ => '}',
});
sc.bump();
}
')' | ']' | '}' => {
match brackets.last().copied() {
None => return false,
Some(expected) if expected != sc.cur() => return true,
Some(_) => {
brackets.pop();
}
}
sc.bump();
}
_ => sc.bump(),
}
}
!brackets.is_empty()
}
fn interp_open_anywhere(s: &str) -> bool {
let mut sc = LineScanner::new(s);
let mut interp = 0i32;
while !sc.done() {
match sc.cur() {
'"' | '\'' if interp > 0 => {
sc.skip_quoted();
}
'#' if sc.peek(1) == Some('{') => {
interp += 1;
sc.bump();
sc.bump();
}
'{' if interp > 0 => {
interp += 1;
sc.bump();
}
'}' if interp > 0 => {
interp -= 1;
sc.bump();
}
_ => sc.bump(),
}
}
interp > 0
}
fn scan_state(s: &str) -> ScanState {
let mut sc = LineScanner::new(s);
let mut depth = 0i32;
let mut interp_depth = 0i32;
while !sc.done() {
if let Some(open) = sc.url_func_open() {
if !sc.skip_url(open) {
depth += 1;
return ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: false,
in_string: false,
in_url: true,
};
}
continue;
}
match sc.cur() {
'"' | '\'' => {
if !sc.skip_quoted() {
return ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: false,
in_string: true,
in_url: false,
};
}
}
'/' if sc.peek(1) == Some('/') => break,
'/' if sc.peek(1) == Some('*') => {
if !sc.skip_loud_comment() {
return ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: true,
in_string: false,
in_url: false,
};
}
}
'#' if sc.peek(1) == Some('{') => {
interp_depth += 1;
sc.bump();
sc.bump();
}
'{' if interp_depth > 0 => {
interp_depth += 1;
sc.bump();
}
'}' if interp_depth > 0 => {
interp_depth -= 1;
sc.bump();
}
'(' | '[' => {
depth += 1;
sc.bump();
}
')' | ']' => {
depth -= 1;
sc.bump();
}
_ => sc.bump(),
}
}
ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: false,
in_string: false,
in_url: false,
}
}
fn find_decl_colon(logical: &str) -> Option<usize> {
let mut sc = LineScanner::new(logical);
let mut paren = 0i32;
let mut bracket = 0i32;
while !sc.done() {
match sc.cur() {
'"' | '\'' => {
sc.skip_quoted();
}
'#' if sc.peek(1) == Some('{') => {
sc.skip_interp();
}
'(' => {
paren += 1;
sc.bump();
}
')' => {
paren -= 1;
sc.bump();
}
'[' => {
bracket += 1;
sc.bump();
}
']' => {
bracket -= 1;
sc.bump();
}
':' if paren == 0 && bracket == 0 => return Some(sc.offset()),
_ => sc.bump(),
}
}
None
}
fn has_top_level_using(logical: &str) -> bool {
let mut sc = LineScanner::new(logical);
let mut depth = 0i32;
while !sc.done() {
match sc.cur() {
'"' | '\'' => {
sc.skip_quoted();
}
'(' | '[' => {
depth += 1;
sc.bump();
}
')' | ']' => {
depth -= 1;
sc.bump();
}
'u' | 'U' if depth == 0 => {
let prev_ident = sc.i > 0 && is_ident_char(sc.cs[sc.i - 1]);
let word: String = sc.cs[sc.i..].iter().take_while(|c| is_ident_char(**c)).collect();
if !prev_ident && word.eq_ignore_ascii_case("using") {
return true;
}
for _ in 0..word.len().max(1) {
sc.bump();
}
}
_ => sc.bump(),
}
}
false
}
fn find_top_level_semicolon(logical: &str) -> Option<usize> {
let mut sc = LineScanner::new(logical);
let mut paren = 0i32;
let mut bracket = 0i32;
while !sc.done() {
if let Some(open) = sc.url_func_open() {
let _closed = sc.skip_url(open);
continue;
}
match sc.cur() {
'"' | '\'' => {
sc.skip_quoted();
}
'/' if sc.peek(1) == Some('/') => break,
'/' if sc.peek(1) == Some('*') => {
sc.skip_loud_comment();
}
'#' if sc.peek(1) == Some('{') => {
sc.skip_interp();
}
'(' => {
paren += 1;
sc.bump();
}
')' => {
paren -= 1;
sc.bump();
}
'[' => {
bracket += 1;
sc.bump();
}
']' => {
bracket -= 1;
sc.bump();
}
';' if paren == 0 && bracket == 0 => return Some(sc.offset()),
_ => sc.bump(),
}
}
None
}
#[cfg(test)]
mod line_scanner_parity {
fn strip_silent_comment_ref(s: &str) -> String {
let cs: Vec<char> = s.chars().collect();
let mut i = 0;
let mut byte = 0usize;
while i < cs.len() {
let c = cs[i];
match c {
'"' | '\'' => {
let q = c;
byte += c.len_utf8();
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' && i + 1 < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
byte += cs[i].len_utf8();
i += 1;
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'/' if cs.get(i + 1) == Some(&'*') => {
byte += 2;
i += 2;
while i + 1 < cs.len() && !(cs[i] == '*' && cs[i + 1] == '/') {
byte += cs[i].len_utf8();
i += 1;
}
if i + 1 < cs.len() {
byte += 2;
i += 2;
}
continue;
}
'/' if cs.get(i + 1) == Some(&'/') => {
return s[..byte].trim_end().to_string();
}
'#' if cs.get(i + 1) == Some(&'{') => {
byte += c.len_utf8() + '{'.len_utf8();
i += 2;
let mut d = 1;
while i < cs.len() && d > 0 {
match cs[i] {
q @ ('"' | '\'') => {
byte += cs[i].len_utf8();
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' && i + 1 < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
byte += cs[i].len_utf8();
i += 1;
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'\\' => {
byte += cs[i].len_utf8();
i += 1;
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'{' => d += 1,
'}' => d -= 1,
_ => {}
}
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
_ => {}
}
byte += c.len_utf8();
i += 1;
}
s.trim_end().to_string()
}
fn custom_value_open_ref(s: &str) -> bool {
let cs: Vec<char> = s.chars().collect();
if cs.iter().rev().take_while(|&&c| c == '\\').count() % 2 == 1 {
return true;
}
let mut brackets: Vec<char> = Vec::new();
let mut i = 0;
while i < cs.len() {
match cs[i] {
'"' | '\'' => {
let q = cs[i];
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' {
i += 1;
}
i += 1;
}
}
'\\' => i += 1,
'(' => brackets.push(')'),
'[' => brackets.push(']'),
'{' => brackets.push('}'),
')' | ']' | '}' => match brackets.last().copied() {
None => return false,
Some(expected) if expected != cs[i] => return true,
Some(_) => {
brackets.pop();
}
},
_ => {}
}
i += 1;
}
!brackets.is_empty()
}
fn interp_open_anywhere_ref(s: &str) -> bool {
let cs: Vec<char> = s.chars().collect();
let mut interp = 0i32;
let mut i = 0;
while i < cs.len() {
match cs[i] {
'"' | '\'' if interp > 0 => {
let q = cs[i];
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' {
i += 1;
}
i += 1;
}
}
'#' if cs.get(i + 1) == Some(&'{') => {
interp += 1;
i += 2;
continue;
}
'{' if interp > 0 => interp += 1,
'}' if interp > 0 => interp -= 1,
_ => {}
}
i += 1;
}
interp > 0
}
fn at_url_func_ref(cs: &[char], i: usize) -> bool {
let want = ['u', 'r', 'l', '('];
if !(0..4).all(|k| cs.get(i + k).is_some_and(|c| c.eq_ignore_ascii_case(&want[k]))) {
return false;
}
let mut start = i;
while start > 0 && super::is_ident_char(cs[start - 1]) {
start -= 1;
}
if start == i {
return true;
}
let name: String = cs[start..i + 3].iter().collect();
crate::parser::is_url_function(&name)
}
fn skip_url_ref(cs: &[char], mut i: usize) -> (usize, bool) {
i += 4;
let mut depth = 1i32;
while i < cs.len() {
match cs[i] {
'"' | '\'' => {
let q = cs[i];
i += 1;
while i < cs.len() {
if cs[i] == '\\' {
i += 2;
continue;
}
if cs[i] == q {
i += 1;
break;
}
i += 1;
}
}
'\\' => i += 2,
'(' => {
depth += 1;
i += 1;
}
')' => {
depth -= 1;
i += 1;
if depth == 0 {
return (i, true);
}
}
_ => i += 1,
}
}
(i, false)
}
fn scan_state_ref(s: &str) -> (i32, bool, bool, bool) {
let cs: Vec<char> = s.chars().collect();
let mut depth = 0i32;
let mut interp_depth = 0i32;
let mut i = 0;
while i < cs.len() {
let c = cs[i];
if (c == 'u' || c == 'U') && at_url_func_ref(&cs, i) {
let (next, closed) = skip_url_ref(&cs, i);
i = next;
if !closed {
depth += 1;
}
continue;
}
match c {
'"' | '\'' => {
let q = c;
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' {
i += 1;
}
i += 1;
}
if i >= cs.len() {
return (depth, interp_depth > 0, false, true);
}
}
'/' if cs.get(i + 1) == Some(&'/') => break,
'/' if cs.get(i + 1) == Some(&'*') => {
i += 2;
while i + 1 < cs.len() && !(cs[i] == '*' && cs[i + 1] == '/') {
i += 1;
}
if i + 1 >= cs.len() && !(cs.get(i) == Some(&'*') && cs.get(i + 1) == Some(&'/')) {
return (depth, interp_depth > 0, true, false);
}
i += 2;
continue;
}
'#' if cs.get(i + 1) == Some(&'{') => {
i += 2;
interp_depth += 1;
continue;
}
'{' if interp_depth > 0 => interp_depth += 1,
'}' if interp_depth > 0 => interp_depth -= 1,
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
_ => {}
}
i += 1;
}
(depth, interp_depth > 0, false, false)
}
fn find_decl_colon_ref(logical: &str) -> Option<usize> {
let cs: Vec<char> = logical.chars().collect();
let mut byte = 0usize;
let mut paren = 0i32;
let mut bracket = 0i32;
let mut i = 0;
while i < cs.len() {
let c = cs[i];
match c {
'"' | '\'' => {
let q = c;
byte += c.len_utf8();
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' {
byte += cs[i].len_utf8();
i += 1;
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'#' if cs.get(i + 1) == Some(&'{') => {
byte += c.len_utf8() + '{'.len_utf8();
i += 2;
let mut d = 1;
while i < cs.len() && d > 0 {
match cs[i] {
q @ ('"' | '\'') => {
byte += cs[i].len_utf8();
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' && i + 1 < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
byte += cs[i].len_utf8();
i += 1;
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'\\' => {
byte += cs[i].len_utf8();
i += 1;
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'{' => d += 1,
'}' => d -= 1,
_ => {}
}
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'(' => paren += 1,
')' => paren -= 1,
'[' => bracket += 1,
']' => bracket -= 1,
':' if paren == 0 && bracket == 0 => return Some(byte),
_ => {}
}
byte += c.len_utf8();
i += 1;
}
None
}
fn has_top_level_using_ref(logical: &str) -> bool {
let cs: Vec<char> = logical.chars().collect();
let mut depth = 0i32;
let mut i = 0;
while i < cs.len() {
match cs[i] {
'"' | '\'' => {
let q = cs[i];
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' {
i += 1;
}
i += 1;
}
}
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
'u' | 'U' if depth == 0 => {
let prev_ident = i > 0 && super::is_ident_char(cs[i - 1]);
let word: String = cs[i..].iter().take_while(|c| super::is_ident_char(**c)).collect();
if !prev_ident && word.eq_ignore_ascii_case("using") {
return true;
}
i += word.len().max(1);
continue;
}
_ => {}
}
i += 1;
}
false
}
fn find_top_level_semicolon_ref(logical: &str) -> Option<usize> {
let cs: Vec<char> = logical.chars().collect();
let mut paren = 0i32;
let mut bracket = 0i32;
let mut byte = 0usize;
let mut i = 0;
while i < cs.len() {
let c = cs[i];
match c {
'"' | '\'' => {
let q = c;
byte += c.len_utf8();
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' && i + 1 < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
byte += cs[i].len_utf8();
i += 1;
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'/' if cs.get(i + 1) == Some(&'/') => break,
'/' if cs.get(i + 1) == Some(&'*') => {
byte += 2;
i += 2;
while i + 1 < cs.len() && !(cs[i] == '*' && cs[i + 1] == '/') {
byte += cs[i].len_utf8();
i += 1;
}
if i + 1 < cs.len() {
byte += 2;
i += 2;
}
continue;
}
'#' if cs.get(i + 1) == Some(&'{') => {
byte += c.len_utf8() + '{'.len_utf8();
i += 2;
let mut d = 1;
while i < cs.len() && d > 0 {
match cs[i] {
q @ ('"' | '\'') => {
byte += cs[i].len_utf8();
i += 1;
while i < cs.len() && cs[i] != q {
if cs[i] == '\\' && i + 1 < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
byte += cs[i].len_utf8();
i += 1;
}
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'\\' => {
byte += cs[i].len_utf8();
i += 1;
if i < cs.len() {
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'{' => d += 1,
'}' => d -= 1,
_ => {}
}
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
'(' => paren += 1,
')' => paren -= 1,
'[' => bracket += 1,
']' => bracket -= 1,
';' if paren == 0 && bracket == 0 => return Some(byte),
_ => {}
}
byte += c.len_utf8();
i += 1;
}
None
}
fn corpus() -> Vec<String> {
let fragments = [
"",
"a, b, c",
"color: red",
"foo: bar; baz: qux",
"@include thing($a, $b) using ($c)",
"@include thing(using: 1)",
"url(a, b)",
"\"a, b\", c",
"'a; b': c",
"a: \"unterminated",
"a: 'half \\' quote'",
"a: \"esc \\\" still in\", b",
"trailing backslash \\",
"trailing backslash in str \"x\\",
"map: (a: 1, b: 2)",
"list: [1, 2, 3]",
"nested: (a: (b: c), d: [e, f])",
"interp: #{1 + 2}px",
"interp colon #{$a: b}",
"unclosed interp #{1 + 2",
"nested interp #{ #{x} }",
"interp with brace #{ {a} }",
"interp with quoted brace #{ \"}\" }",
"interp with quoted comment #{\"} // no\"}",
"interp with quoted semicolon #{\"; \"}: v",
"interp with escaped brace #{ \\} }",
"custom escaped brace --x: \\{",
"custom escaped quote --x: \\\"",
"custom escaped semicolon --x: a\\;b",
"interp in comment /* #{x} */",
"// just a comment",
"value // trailing comment",
"value /* loud */ more",
"value /* unclosed loud",
"a: b /* c */ // d",
"/* #{x} */ : y",
"weird: a:b:c",
"深: 値; другой: значение",
"emoji 😀: 🎉, x",
"multi 字\\符 byte",
"semi inside (a; b); real",
"colon inside [a:b]: outside",
"@media screen",
"$var: value",
"url(\"x; y\")",
"a\\:escaped colon: v",
"#{} : after empty interp",
"} stray close",
") stray paren",
"using",
" using ",
"foousing: 1",
"x using y",
"\"\"",
"''",
"()",
"#{",
"/*",
"*/",
"//",
];
fragments.iter().map(|s| s.to_string()).collect()
}
#[test]
fn line_scanner_matches_reference_implementations() {
for s in corpus() {
let s = s.as_str();
assert_eq!(
super::strip_silent_comment(s),
strip_silent_comment_ref(s),
"strip_silent_comment diverged on {s:?}"
);
assert_eq!(
super::custom_value_open(s),
custom_value_open_ref(s),
"custom_value_open diverged on {s:?}"
);
assert_eq!(
super::interp_open_anywhere(s),
interp_open_anywhere_ref(s),
"interp_open_anywhere diverged on {s:?}"
);
let st = super::scan_state(s);
assert_eq!(
(st.bracket_depth, st.in_interp, st.in_loud_comment, st.in_string),
scan_state_ref(s),
"scan_state diverged on {s:?}"
);
assert_eq!(
super::find_decl_colon(s),
find_decl_colon_ref(s),
"find_decl_colon diverged on {s:?}"
);
assert_eq!(
super::has_top_level_using(s),
has_top_level_using_ref(s),
"has_top_level_using diverged on {s:?}"
);
assert_eq!(
super::find_top_level_semicolon(s),
find_top_level_semicolon_ref(s),
"find_top_level_semicolon diverged on {s:?}"
);
}
}
#[test]
fn returned_byte_offsets_are_char_boundaries() {
for s in corpus() {
let s = s.as_str();
if let Some(b) = super::find_decl_colon(s) {
assert!(
s.is_char_boundary(b),
"find_decl_colon offset {b} not a boundary in {s:?}"
);
}
if let Some(b) = super::find_top_level_semicolon(s) {
assert!(
s.is_char_boundary(b),
"find_top_level_semicolon offset {b} not a boundary in {s:?}"
);
}
}
}
#[test]
fn overlapping_slash_star_slash_does_not_panic() {
for s in [
"/*/",
"a /*/",
"0\"0/*/",
"\u{e9}__ '%/*/",
", in %+as *\t@\n'*/*/",
] {
let _ = super::trim_trailing_loud_comments(s);
}
}
#[test]
fn well_formed_trailing_comments_still_trim() {
assert_eq!(super::trim_trailing_loud_comments("a /* c */"), "a");
assert_eq!(super::trim_trailing_loud_comments("a /* c */ /* d */"), "a");
assert_eq!(super::trim_trailing_loud_comments("a /**/"), "a");
assert_eq!(super::trim_trailing_loud_comments("a"), "a");
assert_eq!(super::trim_trailing_loud_comments("a /* c"), "a /* c");
assert_eq!(super::trim_trailing_loud_comments("a /*/"), "a /*/");
}
}