use crate::{
ByteSpan, Comment, CommentKind, Edit, ExternalSpanError, Language, Layout, PreparedScanner,
ScanReport, SourceMap, TransformOptions, TransformPlan, TransformResult,
scanner::{
disposition, keep_yaml_structural_trails, lines_a_removal_must_swallow, scan,
unicode_line_terminator_width,
},
};
use unicode_width::UnicodeWidthChar;
pub fn transform(source: &[u8], language: Language, options: TransformOptions) -> TransformResult {
transform_plan(source, language, options).finish(source)
}
pub fn transform_plan(
source: &[u8],
language: Language,
options: TransformOptions,
) -> TransformPlan {
let force_invalid = options.scan.force_invalid;
let report = scan(source, language, options.scan);
plan_report(source, report, options.layout, force_invalid)
}
impl PreparedScanner {
pub fn transform_plan(
&self,
source: &[u8],
language: Language,
layout: Layout,
) -> TransformPlan {
let report = self.scan(source, language);
plan_report(source, report, layout, self.options().force_invalid)
}
pub fn transform(&self, source: &[u8], language: Language, layout: Layout) -> TransformResult {
self.transform_plan(source, language, layout).finish(source)
}
pub fn transform_spans_plan(
&self,
source: &[u8],
language: Language,
spans: &[(ByteSpan, CommentKind)],
layout: Layout,
) -> Result<TransformPlan, ExternalSpanError> {
let report = self.scan_spans(source, language, spans)?;
Ok(plan_report(
source,
report,
layout,
self.options().force_invalid,
))
}
pub fn scan_spans(
&self,
source: &[u8],
language: Language,
spans: &[(ByteSpan, CommentKind)],
) -> Result<ScanReport, ExternalSpanError> {
external_report(source, language, spans, self)
}
}
pub fn transform_spans(
source: &[u8],
language: Language,
spans: &[(ByteSpan, CommentKind)],
options: TransformOptions,
) -> Result<TransformResult, ExternalSpanError> {
let prepared = PreparedScanner::new(options.scan)
.map_err(|error| ExternalSpanError::InvalidPattern(error.to_string()))?;
Ok(prepared
.transform_spans_plan(source, language, spans, options.layout)?
.finish(source))
}
fn external_report(
source: &[u8],
language: Language,
spans: &[(ByteSpan, CommentKind)],
prepared: &PreparedScanner,
) -> Result<ScanReport, ExternalSpanError> {
let mut cursor = 0;
let mut comments = Vec::with_capacity(spans.len());
for (index, (span, kind)) in spans.iter().copied().enumerate() {
if span.start > span.end || span.end > source.len() {
return Err(ExternalSpanError::OutOfBounds {
index,
source_len: source.len(),
});
}
if span.is_empty() {
return Err(ExternalSpanError::Empty { index });
}
if index > 0 && span.start < cursor {
return Err(ExternalSpanError::OrderOrOverlap { index });
}
cursor = span.end;
comments.push(Comment {
span,
kind,
disposition: disposition(
kind,
prepared.options(),
&source[span.start..span.end],
&prepared.patterns,
),
});
}
keep_yaml_structural_trails(source, language, &mut comments);
Ok(ScanReport {
language,
comments,
diagnostics: Vec::new(),
valid: true,
})
}
pub(crate) fn transform_report(
source: &[u8],
report: crate::ScanReport,
options: TransformOptions,
) -> TransformResult {
plan_report(source, report, options.layout, options.scan.force_invalid).finish(source)
}
pub(crate) fn plan_report(
source: &[u8],
report: crate::ScanReport,
layout: Layout,
force_invalid: bool,
) -> TransformPlan {
let edits = if report.valid || force_invalid {
let swallow = lines_a_removal_must_swallow(source, report.language, &report.comments);
match layout {
Layout::Lines => line_edits(source, &report.comments, &swallow),
Layout::Columns => column_edits(source, &report.comments, &swallow),
Layout::Compact => compact_edits(source, &report.comments, &swallow),
}
} else {
Vec::new()
};
TransformPlan { edits, report }
}
impl TransformPlan {
pub fn finish(self, source: &[u8]) -> TransformResult {
let output = apply_edits(source, &self.edits);
let source_map = SourceMap::from_edits(source.len(), &self.edits);
TransformResult {
output,
edits: self.edits,
report: self.report,
source_map,
}
}
pub fn output(&self, source: &[u8]) -> Vec<u8> {
apply_edits(source, &self.edits)
}
pub fn source_map(&self, source_len: usize) -> SourceMap {
SourceMap::from_edits(source_len, &self.edits)
}
}
pub fn apply_edits(source: &[u8], edits: &[Edit]) -> Vec<u8> {
let mut cursor = 0;
let output_len = edits.iter().fold(source.len(), |length, edit| {
length
.saturating_sub(edit.span.len())
.saturating_add(edit.replacement.len())
});
let mut output = Vec::with_capacity(output_len);
for edit in edits {
assert!(
edit.span.start <= edit.span.end,
"edit has an inverted span"
);
assert!(edit.span.start >= cursor, "edits overlap or are not sorted");
assert!(edit.span.end <= source.len(), "edit is outside the source");
output.extend_from_slice(&source[cursor..edit.span.start]);
output.extend_from_slice(&edit.replacement);
cursor = edit.span.end;
}
output.extend_from_slice(&source[cursor..]);
output
}
fn line_edits(source: &[u8], comments: &[Comment], swallow: &[Option<ByteSpan>]) -> Vec<Edit> {
let mut edits = Vec::new();
let mut floor = 0usize;
for (index, comment) in comments.iter().enumerate() {
if !comment.disposition.is_remove() {
continue;
}
let edit = match swallow.get(index).copied().flatten() {
Some(line) => Edit {
span: ByteSpan::new(line.start.max(floor), line.end),
replacement: Vec::new(),
},
None => Edit {
span: comment.span,
replacement: if comment.kind == CommentKind::HtmlComment {
Vec::new()
} else {
line_replacement(source, comment.span)
},
},
};
floor = edit.span.end;
edits.push(edit);
}
edits
}
fn line_replacement(source: &[u8], span: ByteSpan) -> Vec<u8> {
let mut output = newline_sequence(&source[span.start..span.end]);
if output.is_empty() && has_non_whitespace_neighbors(source, span) {
output.push(b' ');
}
output
}
fn column_edits(source: &[u8], comments: &[Comment], swallow: &[Option<ByteSpan>]) -> Vec<Edit> {
let mut edits = Vec::new();
let mut cursor = 0usize;
let mut column = 0usize;
for (index, comment) in comments.iter().enumerate() {
if !comment.disposition.is_remove() {
continue;
}
if let Some(line) = swallow.get(index).copied().flatten() {
let span = ByteSpan::new(line.start.max(cursor), line.end);
cursor = span.end;
column = 0;
edits.push(Edit {
span,
replacement: Vec::new(),
});
continue;
}
column = advance_display_column(source, cursor, comment.span.start, column);
let (replacement, next) = if comment.kind == CommentKind::HtmlComment {
(Vec::new(), column)
} else {
column_replacement(source, comment.span, column)
};
cursor = comment.span.end;
column = next;
edits.push(Edit {
span: comment.span,
replacement,
});
}
edits
}
fn compact_edits(source: &[u8], comments: &[Comment], swallow: &[Option<ByteSpan>]) -> Vec<Edit> {
let mut edits = Vec::new();
let mut scan = 0usize;
let mut line_start = 0usize;
let mut floor = 0usize;
for (index, comment) in comments.iter().enumerate() {
if !comment.disposition.is_remove() {
continue;
}
if let Some(line) = swallow.get(index).copied().flatten() {
let span = ByteSpan::new(line.start.max(floor), line.end.max(floor));
floor = span.end;
scan = span.end;
line_start = span.end;
edits.push(Edit {
span,
replacement: Vec::new(),
});
continue;
}
while scan < comment.span.start {
match unicode_line_terminator_width(source, scan) {
Some(width) if scan + width <= comment.span.start => {
scan += width;
line_start = scan;
}
_ => scan += 1,
}
}
let ceiling = comments
.get(index + 1)
.map_or(source.len(), |next| next.span.start)
.max(comment.span.end);
let edit = compact_edit(source, comment, line_start, floor, ceiling);
floor = edit.span.end;
edits.push(edit);
}
edits
}
fn compact_edit(
source: &[u8],
comment: &Comment,
line_start: usize,
floor: usize,
ceiling: usize,
) -> Edit {
let span = comment.span;
let html = comment.kind == CommentKind::HtmlComment;
let interior = first_line_terminator(source, span);
let tail = line_tail(source, span.end);
let head_code = source[line_start..span.start]
.iter()
.any(|byte| !byte.is_ascii_whitespace());
let ends_the_line = tail.is_some() || (interior.is_some() && !html);
let start = if ends_the_line {
blank_start(source, span.start, floor.max(line_start))
} else {
span.start
};
let eats_the_terminator = if html {
!head_code
} else {
interior.is_some() || !head_code
};
let end = match tail {
Some((blanks, terminator)) => blanks + if eats_the_terminator { terminator } else { 0 },
None => span.end,
};
let replacement = if html {
Vec::new()
} else if !ends_the_line {
line_replacement(source, span)
} else if let Some(terminator) = interior.filter(|_| head_code) {
terminator.to_vec()
} else {
Vec::new()
};
Edit {
span: ByteSpan::new(start, end.min(ceiling)),
replacement,
}
}
fn first_line_terminator(source: &[u8], span: ByteSpan) -> Option<&[u8]> {
let mut index = span.start;
while index < span.end {
match unicode_line_terminator_width(source, index) {
Some(width) if index + width <= span.end => return Some(&source[index..index + width]),
_ => index += 1,
}
}
None
}
fn line_tail(source: &[u8], from: usize) -> Option<(usize, usize)> {
let mut index = from;
loop {
if let Some(width) = unicode_line_terminator_width(source, index) {
return Some((index, width));
}
match source.get(index) {
None => return Some((index, 0)),
Some(byte) if byte.is_ascii_whitespace() => index += 1,
Some(_) => return None,
}
}
}
fn blank_start(source: &[u8], at: usize, floor: usize) -> usize {
let mut index = at;
while index > floor
&& source[index - 1].is_ascii_whitespace()
&& unicode_line_terminator_width(source, index - 1).is_none()
{
index -= 1;
}
index
}
fn newline_sequence(bytes: &[u8]) -> Vec<u8> {
let mut output = Vec::new();
let mut index = 0;
while index < bytes.len() {
if let Some(width) = unicode_line_terminator_width(bytes, index) {
output.extend_from_slice(&bytes[index..index + width]);
index += width;
} else {
index += 1;
}
}
output
}
fn has_non_whitespace_neighbors(source: &[u8], span: ByteSpan) -> bool {
source
.get(span.start.wrapping_sub(1))
.is_some_and(|byte| !byte.is_ascii_whitespace())
&& source
.get(span.end)
.is_some_and(|byte| !byte.is_ascii_whitespace())
}
fn column_replacement(source: &[u8], span: ByteSpan, mut column: usize) -> (Vec<u8>, usize) {
let mut output = Vec::with_capacity(span.len());
let mut index = span.start;
while index < span.end {
if let Some(width) = unicode_line_terminator_width(source, index)
&& index + width <= span.end
{
output.extend_from_slice(&source[index..index + width]);
index += width;
column = 0;
continue;
}
match source[index] {
b'\t' => {
let width = 8 - (column % 8);
output.extend(std::iter::repeat_n(b' ', width));
column += width;
index += 1;
}
byte if byte.is_ascii() => {
output.push(b' ');
column += 1;
index += 1;
}
_ => {
if let Some((character, length)) = utf8_character(source, index, span.end) {
let width = character.width().unwrap_or(0);
output.extend(std::iter::repeat_n(b' ', width));
column += width;
index += length;
} else {
output.push(b' ');
column += 1;
index += 1;
}
}
}
}
(output, column)
}
fn advance_display_column(source: &[u8], mut index: usize, end: usize, mut column: usize) -> usize {
while index < end {
if let Some(width) = unicode_line_terminator_width(source, index)
&& index + width <= end
{
index += width;
column = 0;
continue;
}
if source[index] == b'\t' {
column += 8 - (column % 8);
index += 1;
} else if source[index].is_ascii() {
column += 1;
index += 1;
} else if let Some((character, length)) = utf8_character(source, index, end) {
column += character.width().unwrap_or(0);
index += length;
} else {
column += 1;
index += 1;
}
}
column
}
fn utf8_character(source: &[u8], index: usize, end: usize) -> Option<(char, usize)> {
let length = match *source.get(index)? {
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => return None,
};
let bytes = source.get(index..index.checked_add(length)?)?;
if index + length > end {
return None;
}
let text = std::str::from_utf8(bytes).ok()?;
Some((text.chars().next()?, length))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Policy, ScanOptions};
use proptest::prelude::*;
#[test]
fn preserves_crlf_and_separates_tokens() {
let result = transform(b"a/* x\r\ny */b", Language::C, TransformOptions::default());
assert_eq!(result.output, b"a\r\nb");
let joined = transform(b"a/*x*/b", Language::C, TransformOptions::default());
assert_eq!(joined.output, b"a b");
}
#[test]
fn invalid_input_is_not_edited_without_force() {
let result = transform(b"x /* no end", Language::C, TransformOptions::default());
assert!(!result.report.valid);
assert!(result.edits.is_empty());
assert_eq!(result.output, b"x /* no end");
}
#[test]
fn external_spans_use_the_normal_policy_and_validate_boundaries() {
let source = b"a/* ordinary */b/* directive */";
let result = transform_spans(
source,
Language::Unknown,
&[
(ByteSpan::new(1, 15), CommentKind::Block),
(ByteSpan::new(16, source.len()), CommentKind::Directive),
],
TransformOptions::default(),
)
.unwrap();
assert_eq!(result.output, b"a b/* directive */");
assert!(matches!(
transform_spans(
source,
Language::Unknown,
&[(ByteSpan::new(2, source.len() + 1), CommentKind::Block)],
TransformOptions::default(),
),
Err(ExternalSpanError::OutOfBounds { .. })
));
}
#[test]
fn source_map_prefers_the_following_segment_at_edit_boundaries() {
let source = b"ab/* remove */cd";
let result = transform(source, Language::C, TransformOptions::default());
let edit = &result.edits[0];
assert_eq!(result.source_map.original_to_output(0), Some(0));
assert_eq!(
result.source_map.original_to_output(edit.span.start),
Some(edit.span.start)
);
assert_eq!(
result.source_map.original_to_output(edit.span.end),
Some(edit.span.start + edit.replacement.len())
);
assert_eq!(
result.source_map.original_to_output(source.len()),
Some(result.output.len())
);
assert_eq!(
result.source_map.output_to_original(result.output.len()),
Some(source.len())
);
}
#[test]
fn html_is_byte_identical_in_safe_mode() {
let input = b"a<!-- visible\ncomment -->b";
assert_eq!(
transform(input, Language::Html, TransformOptions::default()).output,
input
);
let options = TransformOptions {
scan: ScanOptions {
policy: Policy::All,
..Default::default()
},
..Default::default()
};
assert_eq!(transform(input, Language::Html, options).output, b"ab");
}
proptest! {
#[test]
fn transform_is_idempotent(left in "[a-z ]{0,30}", body in "[a-z ]{0,30}", right in "[a-z ]{0,30}") {
let input = format!("{left}/*{body}*/{right}").into_bytes();
let first = transform(&input, Language::C, TransformOptions::default()).output;
let second = transform(&first, Language::C, TransformOptions::default()).output;
prop_assert_eq!(first, second);
}
}
}