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(&scss)
}
struct Raw {
line: usize,
indent: usize,
indent_str: String,
content: String,
}
struct Transpiler {
lines: Vec<Raw>,
idx: usize,
out: String,
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(),
in_css_callable: false,
}
}
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;
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)?;
if logical.starts_with('\\') {
logical.remove(0);
self.out.push_str(&logical);
if !self.parse_child_into_braces(indent)? {
self.out.push_str(" {}\n");
}
return Ok(());
}
if let Some(rest) = logical.strip_prefix('=') {
logical = format!("@mixin {}", rest.trim_start());
} else if matches!(logical.strip_prefix('+'), Some(r) if r.starts_with(|c: char| is_ident_char(c) || c == '#' || c == '\\'))
{
let rest = logical[1..].trim_start();
logical = format!("@include {rest}");
}
self.extend_directive_prelude(&mut logical, indent)?;
if directive_name(&logical).as_deref() == Some("import") {
logical = quote_import_urls(&logical);
}
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(&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.trim_start_matches("/*");
if !first.trim().is_empty() {
content_lines.push(Some((3, first.trim_start().to_string())));
}
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;
}
}
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(" */");
}
}
}
for (i, line) in content_lines.iter().enumerate() {
match line {
Some((col, text)) => {
let pad = col.max(&3) - 2;
if i == 0 {
self.out.push_str("/*");
} else {
self.out.push_str(" *");
}
for _ in 0..pad {
self.out.push(' ');
}
self.out.push_str(text);
}
None => self.out.push_str(" *"),
}
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_silent_comment(self.lines[start].content.trim_start());
self.idx = start + 1;
let comma_continues =
!logical.starts_with('@') && !logical.starts_with('$') && find_decl_colon(&logical).is_none();
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;
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.pop();
if st.in_string {
logical.push_str(&next_indent_str);
logical.push_str(&joined);
} else {
logical.push(' ');
logical.push_str(joined.trim_start());
}
} 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, _indent: 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());
self.idx = i + 1;
if piece.is_empty() {
continue;
}
if !logical.is_empty() && !logical.ends_with(char::is_whitespace) {
logical.push(' ');
}
logical.push_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());
self.idx = j + 1;
logical.push(' ');
logical.push_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)?;
self.out.push_str("}\n");
Ok(true)
}
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 after = trim_trailing_loud_comments(logical[semi + 1..].trim());
if !after.is_empty() {
let col = logical[..semi].chars().count();
return Err(Error::at(
"multiple statements on one line are not supported in the indented syntax.".to_string(),
Pos {
line: line_no,
col: indent + col + 1,
},
));
}
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(logical);
let css_callable = matches!(directive_name(logical).as_deref(), Some("function" | "mixin"))
&& logical
.trim_start()
.trim_start_matches('@')
.trim_start_matches(|c: char| is_ident_char(c))
.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..].trim_start().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(name);
self.out.push_str(": ");
self.out.push_str(value.trim_end());
self.out.push_str(";\n");
Ok(Some(()))
}
}
fn directive_name(logical: &str) -> Option<String> {
let t = logical.trim_start();
let rest = t.strip_prefix('@')?;
let name: String = rest.chars().take_while(|c| is_ident_char(*c)).collect();
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;
}
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_at = &t[1..]; let prelude = after_at.strip_prefix(name.as_str()).unwrap_or(after_at).trim();
if ends_with_pending_operator(prelude) {
return true;
}
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 let Some(rest) = t.strip_prefix('@') {
let name: String = rest
.chars()
.take_while(|c| is_ident_char(*c))
.collect::<String>()
.to_ascii_lowercase();
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(',')
}
fn quote_import_urls(logical: &str) -> String {
let Some(rest) = logical.trim_start().strip_prefix("@import") else {
return logical.to_string();
};
let mut out = String::from("@import ");
let mut first = true;
for part in split_top_level_commas(rest.trim()) {
if !first {
out.push_str(", ");
}
first = false;
let part = part.trim();
let token_end = part.find(char::is_whitespace).unwrap_or(part.len());
let (token, modifiers) = part.split_at(token_end);
let bare = !token.is_empty()
&& !token.starts_with('"')
&& !token.starts_with('\'')
&& !token.contains("#{")
&& !token.to_ascii_lowercase().starts_with("url(");
if bare {
out.push('"');
out.push_str(token);
out.push('"');
} else {
out.push_str(token);
}
out.push_str(modifiers);
}
out
}
fn split_top_level_commas(s: &str) -> Vec<String> {
let cs: Vec<char> = s.chars().collect();
let mut parts = Vec::new();
let mut cur = String::new();
let mut depth = 0i32;
let mut i = 0;
while i < cs.len() {
let c = cs[i];
match c {
'"' | '\'' => {
cur.push(c);
i += 1;
while i < cs.len() {
cur.push(cs[i]);
if cs[i] == '\\' && i + 1 < cs.len() {
i += 1;
cur.push(cs[i]);
} else if cs[i] == c {
break;
}
i += 1;
}
}
'(' | '[' => {
depth += 1;
cur.push(c);
}
')' | ']' => {
depth -= 1;
cur.push(c);
}
',' if depth == 0 => {
parts.push(std::mem::take(&mut cur));
}
_ => cur.push(c),
}
i += 1;
}
parts.push(cur);
parts
}
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 !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 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] {
'{' => d += 1,
'}' => d -= 1,
_ => {}
}
byte += cs[i].len_utf8();
i += 1;
}
continue;
}
_ => {}
}
byte += c.len_utf8();
i += 1;
}
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,
}
fn custom_value_open(s: &str) -> bool {
let cs: Vec<char> = s.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,
_ => {}
}
i += 1;
}
depth > 0
}
fn interp_open_anywhere(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 scan_state(s: &str) -> ScanState {
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];
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 ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: false,
in_string: 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 ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: true,
in_string: 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;
}
ScanState {
bracket_depth: depth,
in_interp: interp_depth > 0,
in_loud_comment: false,
in_string: false,
}
}
fn find_decl_colon(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] {
'{' => 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(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 && is_ident_char(cs[i - 1]);
let word: String = cs[i..].iter().take_while(|c| 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(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] {
'{' => 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
}