use crate::printer::Printer;
use tsv_lang::Span;
use tsv_lang::doc::arena::DocId;
const DELIMITER_LINE_FILLER: [char; 3] = ['\t', '\x0c', ' '];
const RENDERED_LINE_TRAILING_WS: [char; 2] = [' ', '\t'];
fn strip_one_terminator_front(s: &str) -> Option<&str> {
s.strip_prefix("\r\n")
.or_else(|| s.strip_prefix('\n'))
.or_else(|| s.strip_prefix('\r'))
}
fn strip_one_terminator_back(s: &str) -> Option<&str> {
s.strip_suffix("\r\n")
.or_else(|| s.strip_suffix('\n'))
.or_else(|| s.strip_suffix('\r'))
}
pub(in crate::printer) fn frozen_body_span(source: &str, content: Span) -> Span {
let raw = content.extract(source);
let rest = raw.trim_start_matches(DELIMITER_LINE_FILLER);
let start = match strip_one_terminator_front(rest) {
Some(after) => raw.len() - after.len(),
None => 0,
};
let head = raw[start..].trim_end_matches(DELIMITER_LINE_FILLER);
let end = match strip_one_terminator_back(head) {
Some(shorter) => start + shorter.len(),
None => raw.len(),
};
Span::new(content.start + start as u32, content.start + end as u32)
}
impl<'a> Printer<'a> {
pub(in crate::printer) fn write_frozen_section(&mut self, tag: &str, content: Span) {
if content.start != content.end {
let body = frozen_body_span(self.source, content);
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::record_verbatim_range(self.source, body.start, body.end);
let bytes = body.extract(self.source);
self.write("\n");
self.write(bytes.trim_end_matches(RENDERED_LINE_TRAILING_WS));
self.write("\n");
}
self.write("</");
self.write(tag);
self.write(">\n");
}
pub(in crate::printer) fn frozen_body_doc(&self, content: Span) -> DocId {
let d = self.d();
let body = frozen_body_span(self.source, content);
if body.start == body.end {
return d.concat(&[d.literalline(), d.hardline()]);
}
d.concat(&[
d.literalline(),
self.source_span_covering_comments_doc(body),
d.hardline(),
])
}
}
#[cfg(test)]
mod tests {
use super::{RENDERED_LINE_TRAILING_WS, frozen_body_span};
use tsv_lang::Span;
fn body(raw: &str) -> &str {
frozen_body_span(raw, Span::new(0, raw.len() as u32)).extract(raw)
}
#[test]
fn body_bounds_drop_one_delimiter_newline_per_side() {
assert_eq!(body("\nh1 text\n"), "h1 text");
assert_eq!(body(" \nh1 text\n\t"), "h1 text");
assert_eq!(body("\n\nh1 text\n\n"), "\nh1 text\n");
assert_eq!(body("h1 text"), "h1 text");
assert_eq!(body("\nh1 text "), "h1 text ");
}
#[test]
fn body_bounds_are_absolute_in_the_host_source() {
let src = "<style lang=\"less\">\na\n</style>";
let content = Span::new(19, 22);
assert_eq!(content.extract(src), "\na\n");
assert_eq!(frozen_body_span(src, content).extract(src), "a");
}
#[test]
fn body_bounds_are_empty_when_the_two_delimiter_lines_are_all_there_is() {
assert_eq!(body("\n"), "");
assert_eq!(body("\n\n"), "");
assert_eq!(body(" \n "), " ");
assert_eq!(body("\u{c}\nh1 text\u{c}\n\u{c}"), "h1 text\u{c}");
}
#[test]
fn body_bounds_drop_the_delimiters_own_terminator_however_it_is_spelled() {
assert_eq!(body("\r\nh1 text\r\n"), "h1 text");
assert_eq!(body("\r\na\r\nb\r\n"), "a\r\nb");
assert_eq!(body("\rh1 text\r"), "h1 text");
assert_eq!(body("\ra\rb\r"), "a\rb");
}
#[test]
fn a_frozen_bodys_last_line_owes_the_rendered_line_trim() {
fn trim(raw: &str) -> &str {
body(raw).trim_end_matches(RENDERED_LINE_TRAILING_WS)
}
assert_eq!(trim(" \n "), "");
assert_eq!(trim("\nh1 text "), "h1 text");
assert_eq!(trim("\na \nb \n"), "a \nb");
assert_eq!(trim("\r\nh1 text\r\n"), "h1 text");
}
}