#[derive(Debug, Clone, Copy)]
pub struct LanguageSyntax {
pub line_comments: &'static [&'static str],
pub block_comments: &'static [(&'static str, &'static str)],
pub string_delimiters: &'static [&'static str],
pub backslash_escapes: bool,
}
impl LanguageSyntax {
fn string_delimiter_at(&self, bytes: &[u8], index: usize) -> Option<&'static str> {
let rest = bytes.get(index..)?;
self.string_delimiters
.iter()
.filter(|delimiter| !delimiter.is_empty() && rest.starts_with(delimiter.as_bytes()))
.max_by_key(|delimiter| delimiter.len())
.copied()
}
}
impl Default for LanguageSyntax {
fn default() -> Self {
Self {
line_comments: &[],
block_comments: &[],
string_delimiters: &["\""],
backslash_escapes: true,
}
}
}
#[must_use]
pub fn code_mask(source: &str, syntax: &LanguageSyntax) -> Vec<bool> {
let bytes = source.as_bytes();
let mut mask = vec![true; bytes.len()];
let mut index = 0usize;
while index < bytes.len() {
if let Some(end) = match_line_comment(bytes, index, syntax) {
mark(&mut mask, index, end);
index = end;
continue;
}
if let Some(end) = match_block_comment(bytes, index, syntax) {
mark(&mut mask, index, end);
index = end;
continue;
}
if let Some(end) = match_string(bytes, index, syntax) {
mark(&mut mask, index, end);
index = end;
continue;
}
index += 1;
}
mask
}
fn mark(mask: &mut [bool], start: usize, end: usize) {
let Some(range) = mask.get_mut(start..end.min(mask.len())) else {
return;
};
for slot in range {
*slot = false;
}
}
fn match_line_comment(bytes: &[u8], index: usize, syntax: &LanguageSyntax) -> Option<usize> {
let rest = bytes.get(index..)?;
for prefix in syntax.line_comments {
if prefix.is_empty() {
continue;
}
if rest.starts_with(prefix.as_bytes()) {
return Some(
rest.iter()
.position(|byte| *byte == b'\n')
.map_or(bytes.len(), |offset| index + offset),
);
}
}
None
}
fn match_block_comment(bytes: &[u8], index: usize, syntax: &LanguageSyntax) -> Option<usize> {
let rest = bytes.get(index..)?;
for (open, close) in syntax.block_comments {
if open.is_empty() || close.is_empty() {
continue;
}
if !rest.starts_with(open.as_bytes()) {
continue;
}
let body_start = index + open.len();
return Some(
match bytes
.get(body_start..)
.unwrap_or_default()
.windows(close.len())
.position(|window| window == close.as_bytes())
{
Some(offset) => body_start + offset + close.len(),
None => bytes.len(),
},
);
}
None
}
fn match_string(bytes: &[u8], index: usize, syntax: &LanguageSyntax) -> Option<usize> {
let delimiter = syntax.string_delimiter_at(bytes, index)?;
let body_start = index + delimiter.len();
let mut cursor = body_start;
while let Some(byte) = bytes.get(cursor) {
if syntax.backslash_escapes && *byte == b'\\' {
cursor = (cursor + 2).min(bytes.len());
continue;
}
if bytes.get(cursor..).unwrap_or_default().starts_with(delimiter.as_bytes()) {
return Some(cursor + delimiter.len());
}
if delimiter.len() == 1 && *byte == b'\n' {
return Some(cursor);
}
cursor += 1;
}
Some(bytes.len())
}
#[cfg(test)]
mod tests {
use super::{LanguageSyntax, code_mask};
const RUST: LanguageSyntax = LanguageSyntax {
line_comments: &["//"],
block_comments: &[("/*", "*/")],
string_delimiters: &["\""],
backslash_escapes: true,
};
fn code_at(source: &str, needle: &str) -> bool {
let mask = code_mask(source, &RUST);
let start = source.find(needle).expect("needle present");
mask[start]
}
#[test]
fn identifier_occurrences_are_code() {
assert!(code_at("let Widget = 1;", "Widget"));
}
#[test]
fn string_contents_are_not_code() {
assert!(!code_at("let label = \"Widget\";", "Widget"));
}
#[test]
fn comment_contents_are_not_code() {
assert!(!code_at("// Widget comment", "Widget"));
assert!(!code_at("/* Widget */", "Widget"));
}
#[test]
fn escaped_quotes_do_not_end_the_string_early() {
let source = "let label = \"a\\\"Widget\"; let x = Widget;";
assert!(!code_at(source, "Widget\""));
assert!(code_at(source, "Widget;"));
}
#[test]
fn block_comments_span_lines() {
let source = "/* start\nWidget\nend */ let y = Widget;";
let mask = code_mask(source, &RUST);
let widget = source.find("Widget").expect("needle present");
assert!(!mask[widget], "the commented occurrence is not code");
let last = source.rfind("Widget").expect("second occurrence");
assert!(mask[last], "the code occurrence is code");
}
#[test]
fn languages_without_comments_treat_everything_as_code() {
let syntax = LanguageSyntax { line_comments: &[], ..LanguageSyntax::default() };
let source = "// Widget";
assert!(code_mask(source, &syntax)[source.find("Widget").expect("needle present")]);
}
#[test]
fn python_style_comments_are_recognised() {
let syntax = LanguageSyntax {
line_comments: &["#"],
block_comments: &[],
string_delimiters: &["\"", "'"],
backslash_escapes: true,
};
let source = "# Widget\nx = 'Widget'\ny = Widget";
let mask = code_mask(source, &syntax);
let comment = source.find("Widget").expect("needle present");
let string = source.find("'Widget'").expect("string present") + 1;
let code = source.rfind("Widget").expect("needle present");
assert!(!mask[comment], "a comment mention is not code");
assert!(!mask[string], "a string literal is not code");
assert!(mask[code], "a bare identifier is code");
}
}