#[derive(Debug, Clone, Copy)]
pub struct LogicalLine<'a> {
pub content: &'a str,
pub content_start: u32,
pub is_content_empty: bool,
}
impl<'a> LogicalLine<'a> {
#[inline]
#[must_use]
pub fn content_end(&self) -> u32 {
self.content_start + self.content.len() as u32
}
}
#[derive(Debug, Clone, Copy)]
pub struct MarginInfo<'a> {
pub initial: &'a str,
pub delimiter: &'a str,
pub post_delimiter: &'a str,
pub line_end: &'a str,
pub is_content_empty: bool,
}
pub struct ScanResult<'a> {
pub lines: Vec<LogicalLine<'a>>,
pub margins: Vec<MarginInfo<'a>>,
}
#[must_use]
pub fn is_jsdoc_block(source_text: &str) -> bool {
source_text.trim_start().starts_with("/**")
}
#[must_use]
pub fn has_closing_block(source_text: &str) -> bool {
source_text.trim_end().ends_with("*/")
}
#[must_use]
pub fn body_range(source_text: &str) -> Option<(usize, usize)> {
if !is_jsdoc_block(source_text) || !has_closing_block(source_text) || source_text.len() < 5 {
return None;
}
let leading = source_text.len() - source_text.trim_start().len();
let trailing = source_text.len() - source_text.trim_end().len();
Some((leading + 3, source_text.len() - trailing - 2))
}
#[must_use]
pub fn logical_lines(source_text: &str, base_offset: u32) -> ScanResult<'_> {
let Some((body_start, body_end)) = body_range(source_text) else {
return ScanResult {
lines: Vec::new(),
margins: Vec::new(),
};
};
let body = &source_text[body_start..body_end];
let body_bytes = body.as_bytes();
let body_len = body_bytes.len();
let estimated_lines = memchr::memchr_iter(b'\n', body_bytes).count() + 1;
let mut lines = Vec::with_capacity(estimated_lines);
let mut margins = Vec::with_capacity(estimated_lines);
let mut cursor = 0usize;
loop {
let line_end = match memchr::memchr(b'\n', &body_bytes[cursor..]) {
Some(off) => cursor + off,
None => body_len,
};
let raw_line = &body[cursor..line_end];
let raw_start = body_start + cursor;
let mut pos = 0usize;
let bytes = raw_line.as_bytes();
let initial_start = pos;
while pos < bytes.len() && matches!(bytes[pos], b' ' | b'\t') {
pos += 1;
}
let initial = &raw_line[initial_start..pos];
let delimiter_start = pos;
if bytes.get(pos) == Some(&b'*') {
pos += 1;
}
let delimiter = &raw_line[delimiter_start..pos];
let post_delim_start = pos;
if !delimiter.is_empty() && matches!(bytes.get(pos), Some(b' ' | b'\t')) {
pos += 1;
}
let post_delimiter = &raw_line[post_delim_start..pos];
let content_start = pos;
let content = &raw_line[content_start..];
let is_content_empty = content.bytes().all(|b| b == b' ' || b == b'\t');
let line_end_str = if line_end < body_len {
if line_end > 0 && body_bytes[line_end - 1] == b'\r' {
"\r\n"
} else {
"\n"
}
} else {
""
};
let absolute_content_start = base_offset + (raw_start + content_start) as u32;
lines.push(LogicalLine {
content,
content_start: absolute_content_start,
is_content_empty,
});
margins.push(MarginInfo {
initial,
delimiter,
post_delimiter,
line_end: line_end_str,
is_content_empty,
});
if line_end == body_len {
break;
}
cursor = line_end + 1;
}
ScanResult { lines, margins }
}
#[cfg(test)]
mod tests {
use super::{body_range, has_closing_block, is_jsdoc_block, logical_lines};
#[test]
fn recognizes_only_closed_jsdoc_blocks() {
assert!(is_jsdoc_block("/** ok */"));
assert!(!is_jsdoc_block("/* plain */"));
assert!(has_closing_block("/** ok */"));
assert!(!has_closing_block("/** unclosed"));
assert_eq!(body_range("/** ok */"), Some((3, 7)));
assert_eq!(body_range("/* plain */"), None);
}
#[test]
fn strips_jsdoc_margin_and_keeps_absolute_offsets() {
let source = "/**\n * Find a user.\n * @param {string} id\n */";
let result = logical_lines(source, 100);
assert_eq!(result.lines.len(), 4);
assert_eq!(result.lines[1].content, "Find a user.");
assert_eq!(result.lines[1].content_start, 107);
assert_eq!(result.margins[1].delimiter, "*");
}
}