use super::text::has_authored_blank_line;
use super::{HoistedComments, HoistedSection, Printer, push_char_reference, root_sections};
use crate::ast::{self, FragmentNode};
use smallvec::SmallVec;
use tsv_lang::Span;
struct LiftedRun {
section: HoistedSection,
section_span: Span,
extent: Span,
pieces: SmallVec<[Span; 4]>,
between: bool,
in_range: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Separation {
Glued,
Space,
Line,
Blank,
}
impl Separation {
fn of(segment: &str, edge: bool) -> Self {
debug_assert!(
!edge || ast::is_collapsible_ws_str(segment),
"an edge segment is collapsible whitespace"
);
if edge && has_authored_blank_line(segment) {
Self::Blank
} else if segment.contains('\n') {
Self::Line
} else if segment.is_empty() {
Self::Glued
} else {
Self::Space
}
}
fn spelling(self) -> &'static str {
match self {
Self::Blank => "\n\n",
Self::Line => "\n",
Self::Space => " ",
Self::Glued => "",
}
}
}
fn group_end<T>(items: &[T], i: usize, joined: impl Fn(&T, &T) -> bool) -> usize {
let mut j = i;
while j + 1 < items.len() && joined(&items[j], &items[j + 1]) {
j += 1;
}
j
}
fn ws_edges(source: &str, floor: u32, first: u32, last: u32) -> (Span, Span) {
let before_len = ast::collapsible_ws_suffix_len(&source[floor as usize..first as usize]);
let after_len = ast::collapsible_ws_prefix_len(&source[last as usize..]);
(
Span::new(first - before_len as u32, first),
Span::new(last, last + after_len as u32),
)
}
fn ws_only_between(source: &str, a: Span, b: Span) -> bool {
ast::is_collapsible_ws_str(&source[a.end as usize..b.start as usize])
}
fn continues_reference(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'#'
}
fn open_reference(text: &str) -> &str {
match text.bytes().rposition(|b| !continues_reference(b)) {
Some(at) if text.as_bytes()[at] == b'&' => &text[at..],
_ => "",
}
}
fn extend_open_reference(open: &mut String, piece: &str) {
if piece.bytes().all(continues_reference) {
if !open.is_empty() {
open.push_str(piece);
}
} else {
open.clear();
open.push_str(open_reference(piece));
}
}
fn glued_seam_respell(left: &str, right: &str) -> Option<char> {
let open = open_reference(left);
if open.is_empty() {
return None;
}
let continuation_len = right
.bytes()
.position(|b| !(continues_reference(b) || b == b';'))
.unwrap_or(right.len());
if continuation_len == 0 {
return None;
}
let continuation = &right[..continuation_len];
fn decode(text: &str) -> std::borrow::Cow<'_, str> {
crate::html::decode_character_references(text, crate::html::ReferenceContext::Text)
}
let apart = decode(open).into_owned() + &decode(continuation);
(decode(&format!("{open}{continuation}")) != apart).then(|| char::from(right.as_bytes()[0]))
}
pub(super) struct RangeCut {
pub(super) removed: Span,
pub(super) seam: RangeSeam,
pub(super) respell: Option<char>,
}
pub(super) enum RangeSeam {
Kept(Span),
Space,
Glued,
}
impl RangeCut {
pub(super) fn resume(&self) -> u32 {
self.removed.end + self.respell.map_or(0, |ch| ch.len_utf8() as u32)
}
}
impl RangeSeam {
pub(super) fn text<'s>(&self, source: &'s str) -> &'s str {
match self {
Self::Kept(span) => span.extract(source),
Self::Space => " ",
Self::Glued => "",
}
}
}
pub(super) fn range_cuts(source: &str, floor: u32, sections: &[Span]) -> SmallVec<[RangeCut; 2]> {
let mut cuts = SmallVec::new();
let mut floor = floor;
let mut open = String::new();
let mut piece_start = floor;
let mut i = 0;
while i < sections.len() {
let j = group_end(sections, i, |&a, &b| ws_only_between(source, a, b));
let (before, after) = ws_edges(source, floor, sections[i].start, sections[j].end);
let edge_seam = |run: Span, sep: Separation| {
if sep == Separation::Glued {
RangeSeam::Glued
} else {
RangeSeam::Kept(run)
}
};
let mut sep = Separation::of(after.extract(source), true);
let mut seam = edge_seam(after, sep);
let before_sep = Separation::of(before.extract(source), true);
if before_sep > sep {
sep = before_sep;
seam = edge_seam(before, sep);
}
for pair in sections[i..=j].windows(2) {
let between = Span::new(pair[0].end, pair[1].start);
let text = between.extract(source);
let between_sep = Separation::of(text, false);
if between_sep > sep {
sep = between_sep;
seam = match text.bytes().position(|b| b == b'\n') {
Some(at) => {
let at = between.start + at as u32;
RangeSeam::Kept(Span::new(at, at + 1))
}
None => RangeSeam::Space,
};
}
}
extend_open_reference(
&mut open,
&source[piece_start as usize..before.start as usize],
);
let respell = match seam {
RangeSeam::Glued => glued_seam_respell(&open, &source[after.end as usize..]),
RangeSeam::Kept(_) | RangeSeam::Space => None,
};
if !matches!(seam, RangeSeam::Glued) || respell.is_some() {
open.clear();
}
let cut = RangeCut {
removed: Span::new(before.start, after.end),
seam,
respell,
};
piece_start = cut.resume();
cuts.push(cut);
floor = after.end;
i = j + 1;
}
cuts
}
impl Printer<'_> {
pub(super) fn lifted_runs_source(
&self,
root: &ast::Root<'_>,
sections: &HoistedComments,
) -> Option<String> {
let mut runs = self.section_runs(root, sections);
if !runs.iter().any(|run| run.between) {
return None;
}
let nodes = &root.fragment.nodes;
let ranges: SmallVec<[Span; 2]> = self
.ignore_ranges(&root.fragment)
.pairs
.iter()
.map(|&(start, end)| Span::new(nodes[start].span().start, nodes[end].span().end))
.collect();
for run in &mut runs {
run.in_range = ranges.iter().any(|range| range.contains(run.section_span));
}
if !runs.iter().any(|run| run.between && !run.in_range) {
return None;
}
runs.sort_unstable_by_key(|run| run.extent.start);
let source = self.source;
let bom_len = if source.starts_with('\u{feff}') {
'\u{feff}'.len_utf8()
} else {
0
};
let mut out = String::with_capacity(source.len() + runs.len() * 2 + 2);
out.push_str(&source[..bom_len]);
let mut heads: SmallVec<[&LiftedRun; 4]> = runs
.iter()
.filter(|run| run.section != HoistedSection::Style)
.collect();
heads.sort_by_key(|run| run.section.slot());
for run in heads {
out.push_str(run.extent.extract(source));
out.push_str("\n\n");
}
let mut template = String::with_capacity(source.len());
let mut cursor = bom_len;
let mut i = 0;
while i < runs.len() {
if runs[i].in_range {
let j = group_end(&runs, i, |_, b| b.in_range);
let sections: SmallVec<[Span; 4]> = runs[i..=j]
.iter()
.map(|run| {
debug_assert_eq!(
run.extent, run.section_span,
"a section in a range travels with no comments"
);
run.section_span
})
.collect();
for cut in range_cuts(source, cursor as u32, §ions) {
template.push_str(&source[cursor..cut.removed.start as usize]);
template.push_str(cut.seam.text(source));
if let Some(ch) = cut.respell {
push_char_reference(&mut template, ch);
}
cursor = cut.resume() as usize;
}
i = j + 1;
continue;
}
let j = group_end(&runs, i, |a, b| {
!b.in_range && ws_only_between(source, a.extent, b.extent)
});
let group = &runs[i..=j];
let first = group[0].extent.start;
let last = group[group.len() - 1].extent.end;
if group[0].between {
let (before, after) = ws_edges(source, cursor as u32, first, last);
let mut sep = Separation::of(before.extract(source), true)
.max(Separation::of(after.extract(source), true));
for (k, run) in group.iter().enumerate() {
for pair in run.pieces.windows(2) {
let inside = Span::new(pair[0].end, pair[1].start);
sep = sep.max(Separation::of(inside.extract(source), false));
}
if let Some(next) = group.get(k + 1) {
let between = Span::new(run.extent.end, next.extent.start);
sep = sep.max(Separation::of(between.extract(source), false));
}
}
template.push_str(&source[cursor..before.start as usize]);
template.push_str(sep.spelling());
cursor = after.end as usize;
if sep == Separation::Glued
&& let Some(ch) = glued_seam_respell(&template, &source[cursor..])
{
push_char_reference(&mut template, ch);
cursor += ch.len_utf8();
}
} else {
template.push_str(&source[cursor..first as usize]);
cursor = last as usize;
}
i = j + 1;
}
let kept_end = tsv_lang::trim_end_js_whitespace(source).len();
template.push_str(&source[cursor..kept_end.max(cursor)]);
match runs.iter().find(|run| run.section == HoistedSection::Style) {
Some(style) => {
out.push_str(&template);
out.push_str("\n\n");
out.push_str(style.extent.extract(source));
out.push('\n');
}
None => {
let content = ast::trim_end_collapsible_ws(&template);
match content.chars().next_back() {
Some(ch) if crate::whitespace::is_end_trimmed_content(ch) => {
let at = content.len() - ch.len_utf8();
out.push_str(&template[..at]);
push_char_reference(&mut out, ch);
out.push_str(&template[at + ch.len_utf8()..]);
}
_ => out.push_str(&template),
}
}
}
Some(out)
}
fn section_runs(
&self,
root: &ast::Root<'_>,
sections: &HoistedComments,
) -> SmallVec<[LiftedRun; 4]> {
let nodes = &root.fragment.nodes;
let travels = sections.travelling(nodes.len());
let is_content =
|i: usize, n: &FragmentNode<'_>| !(n.is_whitespace_only_text() || travels[i]);
let content = nodes
.iter()
.enumerate()
.position(|(i, n)| is_content(i, n))
.map(|first| {
let last = nodes
.iter()
.enumerate()
.rposition(|(i, n)| is_content(i, n))
.unwrap_or(first);
(nodes[first].span().start, nodes[last].span().end)
});
let mut runs = SmallVec::new();
for (span, section) in root_sections(root) {
let Some(span) = span else { continue };
let mut run = self.lifted_run(span, section, sections, &root.fragment);
run.between = content
.is_some_and(|(start, end)| start < run.extent.start && run.extent.end < end);
runs.push(run);
}
runs
}
fn lifted_run(
&self,
section_span: Span,
section: HoistedSection,
sections: &HoistedComments,
fragment: &ast::Fragment<'_>,
) -> LiftedRun {
let comments = §ions[section];
let comment_span = |i: usize| Self::fragment_comment(fragment, i).map(|c| c.span);
let mut pieces: SmallVec<[Span; 4]> = comments
.leading
.iter()
.filter_map(|&i| comment_span(i))
.collect();
pieces.push(section_span);
pieces.extend(comments.trail.and_then(comment_span));
let extent = Span::new(pieces[0].start, pieces[pieces.len() - 1].end);
LiftedRun {
section,
section_span,
extent,
pieces,
between: false,
in_range: false,
}
}
}