mod attributes;
mod classification;
mod frozen_body;
#[cfg(test)]
mod fused_spans_tests;
mod helpers;
mod lifted_runs;
mod nodes;
mod script_style;
mod text;
use crate::ast::{self, FragmentNode};
use nodes::AttrGaps;
use smallvec::SmallVec;
use std::cell::{Cell, OnceCell, RefCell};
use std::fmt::Write as _;
use std::ops::{Index, IndexMut};
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::{DocArena, DocId, GroupId};
use tsv_lang::printing::LineBreaks;
use tsv_lang::{
Comment, CommentFreeWindow, EmbedContext, INDENT, LayoutMode, OutputBuffer, Span, TAB_WIDTH,
comments_in_source_from, comments_on_page_from, comments_to_emit_from,
has_comments_on_page_from, has_comments_to_emit_from, is_format_ignore_directive,
is_format_ignore_range_end, is_format_ignore_range_start, is_honored_format_ignore,
is_region_end_marker,
};
use tsv_lang::{FxHashMap, FxHashSet};
use tsv_ts::ast::{Expression, ExpressionKind};
pub(in crate::printer) type CommentRun<'a> = SmallVec<[&'a Comment; 8]>;
#[derive(Debug, Clone, Copy, PartialEq)]
enum HoistedSection {
Options,
ModuleScript,
InstanceScript,
Style,
}
impl HoistedSection {
const ALL: [Self; 4] = [
Self::Options,
Self::ModuleScript,
Self::InstanceScript,
Self::Style,
];
const fn slot(self) -> usize {
match self {
Self::Options => 0,
Self::ModuleScript => 1,
Self::InstanceScript => 2,
Self::Style => 3,
}
}
fn span_in(self, root: &ast::Root<'_>) -> Option<Span> {
match self {
Self::Options => root.options.as_ref().map(|s| s.span),
Self::ModuleScript => root.module.as_ref().map(|s| s.span),
Self::InstanceScript => root.instance.as_ref().map(|s| s.span),
Self::Style => root.css.as_ref().map(|s| s.span),
}
}
}
const _: () = {
let mut k = 0;
while k < HoistedSection::ALL.len() {
assert!(
HoistedSection::ALL[k].slot() == k,
"`HoistedSection::slot` disagrees with `ALL`"
);
k += 1;
}
};
#[derive(Default)]
struct SectionComments {
leading: Vec<usize>,
trail: Option<usize>,
}
#[derive(Default)]
struct HoistedComments([SectionComments; 4]);
impl Index<HoistedSection> for HoistedComments {
type Output = SectionComments;
fn index(&self, section: HoistedSection) -> &SectionComments {
&self.0[section.slot()]
}
}
impl IndexMut<HoistedSection> for HoistedComments {
fn index_mut(&mut self, section: HoistedSection) -> &mut SectionComments {
&mut self.0[section.slot()]
}
}
impl HoistedComments {
fn travelling(&self, len: usize) -> Vec<bool> {
let mut travels = vec![false; len];
for section in &self.0 {
for &i in section.leading.iter().chain(§ion.trail) {
travels[i] = true;
}
}
travels
}
}
fn push_char_reference(out: &mut String, ch: char) {
let _ = write!(out, "&#x{:X};", u32::from(ch));
}
type FrozenSections = [bool; 4];
fn root_sections(root: &ast::Root<'_>) -> [(Option<Span>, HoistedSection); 4] {
HoistedSection::ALL.map(|section| (section.span_in(root), section))
}
struct IgnoreRanges {
markers: Vec<Option<RangeMarker>>,
pairs: SmallVec<[(usize, usize); 2]>,
nodes_addr: usize,
}
impl IgnoreRanges {
fn new(nodes: &[FragmentNode<'_>], source: &str) -> Self {
let markers: Vec<Option<RangeMarker>> = nodes
.iter()
.map(|n| match n {
FragmentNode::Comment(c) => range_marker(c, source),
_ => None,
})
.collect();
let mut pairs = SmallVec::new();
let mut open = None;
for (i, marker) in markers.iter().enumerate() {
match (marker, open) {
(Some(RangeMarker::Start), None) => open = Some(i),
(Some(RangeMarker::End), Some(start)) => {
pairs.push((start, i));
open = None;
}
_ => {}
}
}
Self {
markers,
pairs,
nodes_addr: nodes.as_ptr() as usize,
}
}
fn end_of(&self, i: usize) -> Option<usize> {
self.pairs
.binary_search_by_key(&i, |&(start, _)| start)
.ok()
.map(|k| self.pairs[k].1)
}
fn contains(&self, i: usize) -> bool {
let k = self.pairs.partition_point(|&(start, _)| start < i);
k > 0 && i <= self.pairs[k - 1].1
}
}
struct CommentScan<'r> {
ranges: &'r IgnoreRanges,
next_barrier: Vec<Option<u32>>,
}
impl<'r> CommentScan<'r> {
fn new(fragment: &ast::Fragment<'_>, ranges: &'r IgnoreRanges) -> Self {
let nodes = &fragment.nodes;
let mut next_barrier = vec![None; nodes.len()];
let mut barrier = None;
for i in (0..nodes.len()).rev() {
next_barrier[i] = barrier;
let pins = match &nodes[i] {
FragmentNode::Text(t) => !t.is_collapsible_ws_only,
FragmentNode::Comment(_) => ranges.markers[i].is_some(),
_ => true,
};
if pins {
barrier = Some(nodes[i].span().start);
}
}
Self {
ranges,
next_barrier,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RangeMarker {
Start,
End,
}
fn range_marker(comment: &ast::HtmlComment, source: &str) -> Option<RangeMarker> {
let content = comment.content(source);
if is_format_ignore_range_start(content) {
Some(RangeMarker::Start)
} else if is_format_ignore_range_end(content) {
Some(RangeMarker::End)
} else {
None
}
}
#[derive(Clone, Copy)]
pub(in crate::printer) struct HeadExpr {
pub(in crate::printer) doc: DocId,
pub(in crate::printer) layout: HeadLayout,
pub(in crate::printer) ends_with_line_comment: bool,
pub(in crate::printer) owes_continuation_indent: bool,
}
pub(in crate::printer) struct ValueTrailing {
pub(in crate::printer) docs: DocBuf,
pub(in crate::printer) gap_run: bool,
pub(in crate::printer) ends_with_line_comment: bool,
pub(in crate::printer) embed_end: Option<GroupId>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(in crate::printer) enum HeadLayout {
Inline,
HangsAfterOpen,
OpensOwnLine,
}
impl HeadLayout {
pub(in crate::printer) const fn opens_own_line(self) -> bool {
matches!(self, Self::OpensOwnLine)
}
pub(in crate::printer) const fn indents_content(self) -> bool {
!matches!(self, Self::Inline)
}
}
pub(crate) struct FragmentScope<'p> {
depth: &'p Cell<usize>,
outer: usize,
}
impl Drop for FragmentScope<'_> {
fn drop(&mut self) {
self.depth.set(self.outer);
}
}
pub(crate) struct Printer<'a> {
buffer: OutputBuffer,
pub(crate) indent_level: usize,
embed: EmbedContext,
pub(crate) arena: &'a DocArena,
pub(crate) source: &'a str,
comments: &'a [Comment],
has_owned_comments: bool,
has_format_ignore: bool,
line_breaks: LineBreaks<'a>,
block_dangle_allowed: Cell<bool>,
pub(crate) nested_raw_text_nesting: Cell<u32>,
in_textarea_content: Cell<bool>,
fragment_depth: Cell<usize>,
css_host_scan_cache: Cell<Option<tsv_css::__private::HostBoundaryScan>>,
root_inline_run_block_starts: RefCell<FxHashSet<u32>>,
comment_free_gap: CommentFreeWindow<'a>,
fragment_edges: RefCell<std::collections::BTreeMap<usize, nodes::IndexedFragment>>,
fragment_glue_cache: RefCell<FxHashMap<(usize, usize), std::rc::Rc<nodes::FragmentGlue>>>,
ignore_ranges: OnceCell<IgnoreRanges>,
}
#[cfg(test)]
thread_local! {
static FUSED_SOURCE_SPANS_OFF: Cell<bool> = const { Cell::new(false) };
}
#[inline]
pub(in crate::printer) fn fuses_source_spans() -> bool {
#[cfg(test)]
{
!FUSED_SOURCE_SPANS_OFF.get()
}
#[cfg(not(test))]
{
true
}
}
#[cfg(test)]
pub(crate) fn without_fused_source_spans<R>(f: impl FnOnce() -> R) -> R {
struct Restore(bool);
impl Drop for Restore {
fn drop(&mut self) {
FUSED_SOURCE_SPANS_OFF.set(self.0);
}
}
let _restore = Restore(FUSED_SOURCE_SPANS_OFF.replace(true));
f()
}
fn plain_reference_span(id: &tsv_ts::ast::Identifier<'_>) -> Option<Span> {
(id.name_plain_ascii
&& id.escaped_name.is_none()
&& !id.optional
&& id.extra.is_none()
&& id.span.end - id.span.start == u32::from(id.name_len))
.then_some(id.span)
}
pub(in crate::printer) fn plain_path_span(expr: &Expression<'_>) -> Option<Span> {
let path = match &expr.kind {
ExpressionKind::Identifier(id) => plain_reference_span(id)?,
ExpressionKind::MemberExpression(member) if !member.computed && !member.optional => {
let (ExpressionKind::Identifier(object), ExpressionKind::Identifier(property)) =
(&member.object.kind, &member.property.kind)
else {
return None;
};
let object = plain_reference_span(object)?;
let property = plain_reference_span(property)?;
if object.end + 1 != property.start {
return None;
}
Span::new(object.start, property.end)
}
_ => return None,
};
(path == expr.span).then_some(path)
}
impl<'a> Printer<'a> {
#[cfg(test)]
pub(crate) fn new(arena: &'a DocArena, source: &'a str, comments: &'a [Comment]) -> Self {
let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
Self::with_line_breaks(
arena,
source,
comments,
EmbedContext::default(),
line_breaks,
)
}
pub(crate) fn with_line_breaks(
arena: &'a DocArena,
source: &'a str,
comments: &'a [Comment],
embed: EmbedContext,
line_breaks: LineBreaks<'a>,
) -> Self {
let tsv_ts::__private::PrinterInputs {
has_owned_comments,
has_format_ignore,
..
} = tsv_ts::__private::PrinterInputs::for_document(source, comments, line_breaks.table());
Self {
buffer: OutputBuffer::with_capacity(source.len()),
indent_level: 0,
embed,
arena,
source,
comments,
has_owned_comments,
has_format_ignore,
line_breaks,
block_dangle_allowed: Cell::new(true),
nested_raw_text_nesting: Cell::new(0),
in_textarea_content: Cell::new(false),
fragment_depth: Cell::new(0),
css_host_scan_cache: Cell::new(None),
root_inline_run_block_starts: RefCell::new(FxHashSet::default()),
comment_free_gap: CommentFreeWindow::new(comments),
fragment_edges: RefCell::new(std::collections::BTreeMap::new()),
fragment_glue_cache: RefCell::new(FxHashMap::default()),
ignore_ranges: OnceCell::new(),
}
}
pub(crate) fn is_root_inline_run_block(&self, node: &FragmentNode<'_>) -> bool {
self.root_inline_run_block_starts
.borrow()
.contains(&node.span().start)
}
#[inline]
pub(crate) fn d(&self) -> &DocArena {
self.arena
}
#[inline]
pub(crate) fn block_dangle_allowed(&self) -> bool {
self.block_dangle_allowed.get()
}
#[inline]
pub(crate) fn set_block_dangle_allowed(&self, allowed: bool) -> bool {
self.block_dangle_allowed.replace(allowed)
}
#[inline]
pub(crate) fn in_textarea_content(&self) -> bool {
self.in_textarea_content.get()
}
pub(crate) fn enter_fragment(&self) -> FragmentScope<'_> {
let outer = self.fragment_depth.get();
self.fragment_depth.set(outer + 1);
FragmentScope {
depth: &self.fragment_depth,
outer,
}
}
pub(crate) fn body_indent_level(&self) -> usize {
self.indent_level + self.fragment_depth.get()
}
pub(crate) fn css_host_scan(&self) -> tsv_css::__private::HostBoundaryScan {
if let Some(scan) = self.css_host_scan_cache.get() {
return scan;
}
let scan = tsv_css::__private::HostBoundaryScan::of(self.source);
self.css_host_scan_cache.set(Some(scan));
scan
}
pub(crate) fn write(&mut self, s: &str) {
self.buffer.write(s);
}
pub(crate) fn write_verbatim_span(&mut self, span: Span) {
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::record_verbatim_range(self.source, span.start, span.end);
self.write(span.extract(self.source));
}
pub(crate) fn verbatim_source_doc(&self, span: Span) -> DocId {
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::record_verbatim_range(self.source, span.start, span.end);
self.d().verbatim_source_span(span, self.source)
}
pub(crate) fn source_span_covering_comments_doc(&self, span: Span) -> DocId {
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::record_verbatim_range(self.source, span.start, span.end);
self.d().source_span(span, self.source)
}
pub(in crate::printer) fn build_frozen_node_doc(&self, span: Span) -> DocId {
self.claim_owned_leading_comment(self.verbatim_source_doc(span), span.start)
}
pub(in crate::printer) fn claim_owned_leading_comment(&self, doc: DocId, start: u32) -> DocId {
if !self.has_owned_comments {
return doc;
}
let Some(comment) = tsv_lang::owned_leading_comment_at(self.source, self.comments, start)
else {
return doc;
};
let d = self.d();
let comment_doc = tsv_ts::__private::build_comment_doc(d, comment, &self.ts_inputs());
let separator = if tsv_lang::printing::has_newline_between_scan(
self.source.as_bytes(),
self.line_table(),
comment.span.end,
start,
) {
d.hardline()
} else {
d.text(" ")
};
d.concat(&[comment_doc, separator, doc])
}
pub(in crate::printer) fn build_head_value_doc(
&self,
expr: &Expression<'_>,
frozen: bool,
embed: &EmbedContext,
) -> DocId {
if frozen {
self.build_frozen_node_doc(expr.span())
} else if let Some(path) = self.fused_plain_path(expr) {
self.plain_source_span_doc(path)
} else {
tsv_ts::__private::build_expression_doc(self.d(), expr, &self.ts_inputs(), *embed)
}
}
pub(in crate::printer) fn fused_plain_path(&self, expr: &Expression<'_>) -> Option<Span> {
plain_path_span(expr)
.filter(|path| !self.owns_leading_comment_at(path.start) && fuses_source_spans())
}
pub(in crate::printer) fn plain_source_span_doc(&self, span: Span) -> DocId {
debug_assert!(
!span
.extract(self.source)
.bytes()
.any(tsv_lang::printing::is_width_relevant),
"a fused span holds a byte its width depends on"
);
self.d().source_span_plain(span)
}
fn owns_leading_comment_at(&self, start: u32) -> bool {
self.has_owned_comments
&& tsv_lang::owned_leading_comment_at(self.source, self.comments, start).is_some()
}
pub(in crate::printer) fn build_assignment_value_doc(
&self,
expr: &Expression<'_>,
frozen: bool,
embed: &EmbedContext,
value_gap_start: Option<u32>,
) -> DocId {
if frozen {
self.build_frozen_node_doc(expr.span())
} else {
tsv_ts::__private::build_assignment_value_expression_doc(
self.d(),
expr,
&self.ts_inputs(),
*embed,
value_gap_start,
)
}
}
pub(in crate::printer) fn head_embed(&self) -> EmbedContext {
EmbedContext {
mode: LayoutMode::Embedded,
jsdoc_cast_cannot_hang: true,
..self.embed
}
}
pub(in crate::printer) fn head_gap_end(&self, value: &Expression<'_>) -> u32 {
tsv_ts::__private::expression_printed_start(self.d(), value, &self.ts_inputs())
}
pub(in crate::printer) fn head_layout(
&self,
gap_start: u32,
value_start: u32,
frozen: bool,
) -> HeadLayout {
if frozen {
return HeadLayout::OpensOwnLine;
}
let mut run = self
.comments_to_emit_between(gap_start, value_start)
.peekable();
let Some(first) = run.peek() else {
return HeadLayout::Inline;
};
let first_own_line =
tsv_lang::source_scan::has_newline_before_position(self.source, first.span.start);
if !run.any(|c| !c.is_block) {
return HeadLayout::Inline;
}
if first_own_line {
HeadLayout::OpensOwnLine
} else {
HeadLayout::HangsAfterOpen
}
}
pub(in crate::printer) fn wrap_value_clarity_parens(
&self,
expr: &Expression<'_>,
value_doc: DocId,
) -> DocId {
if Self::value_takes_clarity_parens(expr) {
self.d().parens(value_doc)
} else {
value_doc
}
}
pub(in crate::printer) fn value_takes_clarity_parens(expr: &Expression<'_>) -> bool {
matches!(expr.kind, ExpressionKind::AssignmentExpression(_))
}
pub(in crate::printer) fn indent_own_line_head(&self, content: DocId) -> DocId {
let d = self.d();
d.indent_hardline(content)
}
pub(in crate::printer) fn indent_head_content(
&self,
content: DocId,
layout: HeadLayout,
) -> DocId {
match layout {
HeadLayout::OpensOwnLine => self.indent_own_line_head(content),
HeadLayout::HangsAfterOpen => self.d().indent(content),
HeadLayout::Inline => content,
}
}
pub(in crate::printer) fn value_trailing_docs(
&self,
value_doc: DocId,
value_span: Span,
from: u32,
to: u32,
closer_owns_break: bool,
) -> ValueTrailing {
let (mut docs, ends_with_line_comment) =
self.trailing_comment_docs(from, to, closer_owns_break);
let d = self.d();
let run_empty = docs.is_empty();
if self.gap_known_comment_free(value_span.start, value_span.end)
|| !d.holds_line_suffix(value_doc)
{
return ValueTrailing {
docs,
gap_run: !run_empty,
ends_with_line_comment,
embed_end: None,
};
}
let pending = d.pending_run_at_end(value_doc, self.source);
let end = d.embed_end(pending, run_empty && closer_owns_break);
if let Some(run) = pending
&& run.certain
&& run.has_line_comment
{
docs.insert(0, d.break_parent());
}
docs.insert(0, end.doc());
ValueTrailing {
docs,
gap_run: !run_empty,
ends_with_line_comment,
embed_end: Some(end),
}
}
pub(in crate::printer) fn assemble_head_expr(
&self,
value_doc: DocId,
gap_start: u32,
value: &Expression<'_>,
content_end: u32,
frozen: bool,
) -> HeadExpr {
let span = value.span();
let layout = self.head_layout(gap_start, self.head_gap_end(value), frozen);
let leading_docs = self.leading_comment_docs(gap_start, span.start);
let trailing = self.value_trailing_docs(
value_doc,
span,
span.end,
content_end,
layout.indents_content(),
);
let body = self.concat_with_surrounding_comments(leading_docs, value_doc, trailing.docs);
HeadExpr {
doc: self.indent_head_content(body, layout),
layout,
ends_with_line_comment: trailing.ends_with_line_comment,
owes_continuation_indent: false,
}
}
pub(in crate::printer) fn head_open_doc(
&self,
open: &'static str,
opens_own_line: bool,
) -> DocId {
self.d().text(if opens_own_line {
open.trim_end()
} else {
open
})
}
pub(in crate::printer) fn build_prefixed_head_doc(
&self,
open: &'static str,
head: HeadExpr,
close: DocId,
) -> DocId {
let d = self.d();
if !head.layout.indents_content() {
return d.concat(&[d.text(open), head.doc, close]);
}
let open_doc = self.head_open_doc(open, head.layout.opens_own_line());
if head.ends_with_line_comment {
return d.concat(&[open_doc, head.doc, close]);
}
d.concat(&[open_doc, head.doc, d.hardline(), close])
}
pub(in crate::printer) fn hug_head_content(&self, head: HeadExpr) -> DocId {
if head.owes_continuation_indent {
self.d().indent(head.doc)
} else {
head.doc
}
}
pub(crate) fn source(&self) -> &str {
self.source
}
pub(crate) fn line_table(&self) -> tsv_lang::printing::LineTable<'_> {
self.line_breaks.table()
}
pub(crate) fn ts_inputs(&self) -> tsv_ts::__private::PrinterInputs<'_> {
tsv_ts::__private::PrinterInputs {
source: self.source,
comments: self.comments,
line_table: self.line_table(),
has_owned_comments: self.has_owned_comments,
has_format_ignore: self.has_format_ignore,
comment_free_window: Some(&self.comment_free_gap),
}
}
#[inline]
pub(in crate::printer) fn gap_known_comment_free(&self, start: u32, end: u32) -> bool {
self.comment_free_gap.known_comment_free(start, end)
}
#[inline]
fn first_index_between(&self, start: u32, end: u32) -> usize {
if self.gap_known_comment_free(start, end) {
self.comment_free_gap.comments().len()
} else {
self.first_index_between_wide(start)
}
}
#[inline(never)]
fn first_index_between_wide(&self, start: u32) -> usize {
self.comment_free_gap.search_from(start)
}
#[inline]
pub(crate) fn comments_to_emit_between(
&self,
start: u32,
end: u32,
) -> impl Iterator<Item = &'a Comment> + use<'a> {
comments_to_emit_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
#[inline]
pub(crate) fn comments_on_page_between(
&self,
start: u32,
end: u32,
) -> impl Iterator<Item = &'a Comment> + use<'a> {
comments_on_page_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
#[inline]
pub(crate) fn comments_in_source_between(
&self,
start: u32,
end: u32,
) -> impl Iterator<Item = &'a Comment> + use<'a> {
comments_in_source_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
#[inline]
pub(crate) fn has_comments_to_emit_between(&self, start: u32, end: u32) -> bool {
has_comments_to_emit_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
#[inline]
pub(crate) fn has_comments_on_page_between(&self, start: u32, end: u32) -> bool {
has_comments_on_page_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
pub(crate) fn write_indent(&mut self) {
tsv_lang::write_indent(&mut self.buffer, self.indent_level, INDENT);
}
pub(crate) fn into_string(self) -> String {
self.arena
.park_line_breaks_scratch(self.line_breaks.into_scratch());
self.buffer.into_string()
}
pub(crate) fn render_doc_immediate(&mut self, d: DocId) {
let col = self.buffer.current_column(TAB_WIDTH);
let mut output = self.arena.take_render_scratch();
tsv_lang::doc::arena_print_doc_with_indent_resolved_preserve_whitespace_into(
self.arena,
d,
&self.embed,
col,
self.indent_level,
self.source,
&mut output,
);
self.write(&output);
self.arena.park_render_scratch(output);
}
}
impl<'a> Printer<'a> {
pub(crate) fn build_ts_expression_doc(&self, expr: &Expression<'_>) -> DocId {
self.build_head_value_doc(expr, false, &self.embed)
}
pub(crate) fn build_ts_expression_doc_cannot_hang(&self, expr: &Expression<'_>) -> DocId {
self.build_head_value_doc(expr, false, &self.cannot_hang_embed())
}
pub(in crate::printer) fn cannot_hang_embed(&self) -> EmbedContext {
EmbedContext {
jsdoc_cast_cannot_hang: true,
..self.embed
}
}
}
pub(crate) fn format_svelte(root: &ast::Root<'_>, source: &str) -> String {
let arena = DocArena::for_source(source);
format_svelte_in(root, source, &arena)
}
pub(crate) fn format_svelte_in(root: &ast::Root<'_>, source: &str, arena: &DocArena) -> String {
let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
format_root(
root,
source,
line_breaks,
arena,
TailRespell::Allow,
LiftRuns::Allow,
Known::default(),
)
}
pub(crate) fn format_svelte_folded_in(
root: &ast::Root<'_>,
folded: &tsv_lang::printing::FoldedSource<'_>,
arena: &DocArena,
) -> String {
let line_breaks = LineBreaks::of_folded(folded, arena.take_line_breaks_scratch());
format_root(
root,
folded.text(),
line_breaks,
arena,
TailRespell::Allow,
LiftRuns::Allow,
Known::default(),
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TailRespell {
Allow,
Respelled,
Declined,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LiftRuns {
Allow,
Lifted,
Declined,
}
#[derive(Default)]
struct Known {
sections: Option<HoistedComments>,
frozen: Option<FrozenSections>,
}
impl Known {
fn frozen(frozen: FrozenSections) -> Self {
Self {
sections: None,
frozen: Some(frozen),
}
}
}
fn format_root<'a>(
root: &ast::Root<'_>,
source: &'a str,
line_breaks: LineBreaks<'a>,
arena: &'a DocArena,
tail: TailRespell,
lift: LiftRuns,
known: Known,
) -> String {
debug_assert!(
!source.contains('\r'),
"a format entry point takes CR-folded text (`tsv_lang::printing::normalize_carriage_returns`)"
);
let comments = tsv_lang::merge_nestled_block_comments(source, &root.comments);
let printer = Printer::with_line_breaks(
arena,
source,
&comments,
EmbedContext::default(),
line_breaks,
);
printer
.nested_raw_text_nesting
.set(root.nested_raw_text_nesting);
printer.index_fragment_edges(root);
let sections = known
.sections
.unwrap_or_else(|| printer.hoisted_comments(root));
let frozen = known
.frozen
.unwrap_or_else(|| printer.frozen_sections(root));
match lift {
LiftRuns::Allow => {
if let Some(lifted) = printer.lifted_runs_source(root, §ions) {
drop(printer.into_string()); return format_lifted_runs(root, source, &lifted, arena, tail, sections, frozen);
}
}
LiftRuns::Lifted => debug_assert!(
printer.lifted_runs_source(root, §ions).is_none(),
"a lifted source must not ask to be lifted again"
),
LiftRuns::Declined => {}
}
match tail {
TailRespell::Allow => {
if let Some((at, ch)) = printer.template_tail_respell(root, §ions) {
drop(printer.into_string()); let lift = match lift {
LiftRuns::Declined => LiftRuns::Declined,
LiftRuns::Allow | LiftRuns::Lifted => LiftRuns::Lifted,
};
return format_respelled_tail(root, source, at, ch, arena, lift, frozen);
}
}
TailRespell::Respelled => debug_assert!(
printer.template_tail_respell(root, §ions).is_none(),
"a respelled template tail must not ask to be respelled again"
),
TailRespell::Declined => {}
}
#[cfg(feature = "_internal_comment_check")]
{
tsv_lang::comment_ledger::register_parsed(source, &comments);
let mut html_comment_spans = Vec::new();
collect_html_comment_spans(&root.fragment, &mut html_comment_spans);
tsv_lang::comment_ledger::register_parsed_spans(source, html_comment_spans);
}
print_document(printer, root, §ions, frozen)
}
fn print_document(
mut printer: Printer<'_>,
root: &ast::Root<'_>,
sections: &HoistedComments,
frozen: FrozenSections,
) -> String {
printer.print_root(root, sections, frozen);
tsv_lang::printing::encode_leading_zwnbsp(printer.into_string())
}
fn format_respelled_tail(
root: &ast::Root<'_>,
source: &str,
at: usize,
ch: char,
arena: &DocArena,
lift: LiftRuns,
frozen: FrozenSections,
) -> String {
let mut respelled = String::with_capacity(source.len() + 10);
respelled.push_str(&source[..at]);
push_char_reference(&mut respelled, ch);
respelled.push_str(&source[at + ch.len_utf8()..]);
let bump = bumpalo::Bump::new();
match crate::__private::parse(&respelled, &bump) {
Ok(respelled_root) => {
let line_breaks = LineBreaks::new(&respelled, arena.take_line_breaks_scratch());
format_root(
&respelled_root,
&respelled,
line_breaks,
arena,
TailRespell::Respelled,
lift,
Known::frozen(frozen),
)
}
Err(_) => {
let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
format_root(
root,
source,
line_breaks,
arena,
TailRespell::Declined,
lift,
Known::frozen(frozen),
)
}
}
}
fn format_lifted_runs(
root: &ast::Root<'_>,
source: &str,
lifted: &str,
arena: &DocArena,
tail: TailRespell,
sections: HoistedComments,
frozen: FrozenSections,
) -> String {
let bump = bumpalo::Bump::new();
match crate::__private::parse(lifted, &bump) {
Ok(lifted_root) => {
let line_breaks = LineBreaks::new(lifted, arena.take_line_breaks_scratch());
format_root(
&lifted_root,
lifted,
line_breaks,
arena,
tail,
LiftRuns::Lifted,
Known::frozen(frozen),
)
}
Err(_) => {
let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
format_root(
root,
source,
line_breaks,
arena,
tail,
LiftRuns::Declined,
Known {
sections: Some(sections),
frozen: Some(frozen),
},
)
}
}
}
#[cfg(feature = "_internal_comment_check")]
fn collect_html_comment_spans(fragment: &ast::Fragment<'_>, out: &mut Vec<Span>) {
for node in fragment.nodes {
match node {
FragmentNode::Comment(comment) => out.push(comment.span),
FragmentNode::Element(el) => collect_html_comment_spans(&el.fragment, out),
FragmentNode::SpecialElement(el) => collect_html_comment_spans(&el.fragment, out),
FragmentNode::IfBlock(block) => {
collect_html_comment_spans(&block.consequent, out);
if let Some(alternate) = &block.alternate {
collect_html_comment_spans(alternate, out);
}
}
FragmentNode::EachBlock(block) => {
collect_html_comment_spans(&block.body, out);
if let Some(fallback) = &block.fallback {
collect_html_comment_spans(fallback, out);
}
}
FragmentNode::AwaitBlock(block) => {
if let Some(pending) = &block.pending {
collect_html_comment_spans(pending, out);
}
if let Some(then) = &block.then {
collect_html_comment_spans(then, out);
}
if let Some(catch) = &block.catch {
collect_html_comment_spans(catch, out);
}
}
FragmentNode::KeyBlock(block) => collect_html_comment_spans(&block.fragment, out),
FragmentNode::SnippetBlock(block) => collect_html_comment_spans(&block.body, out),
_ => {}
}
}
}
impl<'a> Printer<'a> {
pub(in crate::printer) fn honored_directive_in_gap(&self, start: u32, end: u32) -> bool {
self.has_format_ignore
&& self
.comments_in_source_between(start, end)
.any(|c| self.is_honored_directive(c))
}
pub(in crate::printer) fn is_honored_directive(&self, c: &Comment) -> bool {
self.has_format_ignore && is_honored_format_ignore(self.source, c, 0)
}
fn section_is_frozen(
&self,
fragment: &ast::Fragment<'_>,
section: Span,
hoisted: &[Span],
) -> bool {
let nodes = &fragment.nodes;
let above = nodes.partition_point(|n| n.span().end <= section.start);
let Some(FragmentNode::Comment(comment)) = nodes[..above]
.iter()
.rev()
.find(|n| !n.is_whitespace_only_text())
else {
return false;
};
is_format_ignore_directive(comment.content(self.source))
&& !hoisted
.iter()
.any(|h| comment.span.end <= h.start && h.end <= section.start)
}
fn frozen_sections(&self, root: &ast::Root<'_>) -> FrozenSections {
let hoisted: SmallVec<[Span; 4]> = root_sections(root)
.into_iter()
.filter_map(|(span, _)| span)
.collect();
root_sections(root).map(|(span, section)| {
section != HoistedSection::Options
&& span.is_some_and(|span| self.section_is_frozen(&root.fragment, span, &hoisted))
})
}
fn ignore_ranges(&self, root_fragment: &ast::Fragment<'_>) -> &IgnoreRanges {
let ranges = self
.ignore_ranges
.get_or_init(|| IgnoreRanges::new(root_fragment.nodes, self.source));
debug_assert_eq!(
ranges.nodes_addr,
root_fragment.nodes.as_ptr() as usize,
"the ignore ranges pair one root fragment per printer"
);
ranges
}
fn classify_fragment_comment(
comment: &ast::HtmlComment,
comment_idx: usize,
root: &ast::Root<'_>,
scan: &CommentScan<'_>,
) -> Option<HoistedSection> {
if scan.ranges.markers[comment_idx].is_some() {
return None;
}
if scan.ranges.contains(comment_idx) {
return None;
}
let comment_end = comment.span.end;
let mut nearest: Option<(u32, Option<HoistedSection>)> =
scan.next_barrier[comment_idx].map(|barrier| (barrier, None));
for (span, section) in root_sections(root) {
let Some(start) = span.map(|s| s.start) else {
continue;
};
if start >= comment_end && nearest.as_ref().is_none_or(|(p, _)| start < *p) {
nearest = Some((start, Some(section)));
}
}
nearest.and_then(|(_, section)| section)
}
fn region_end_trails(&self, root: &ast::Root<'_>) -> [Option<usize>; 4] {
let nodes = &root.fragment.nodes;
let bytes = self.source.as_bytes();
root_sections(root).map(|(span, _)| {
let end = span?.end as usize;
let first = nodes.partition_point(|n| (n.span().start as usize) < end);
let (idx, node) = nodes[first..]
.iter()
.enumerate()
.find(|(_, n)| !n.is_whitespace_only_text())
.map(|(k, n)| (first + k, n))?;
let FragmentNode::Comment(comment) = node else {
return None;
};
let start = comment.span.start as usize;
(bytes[end..start].iter().all(|&b| ast::is_collapsible_ws(b))
&& is_region_end_marker(comment.content(self.source))
&& range_marker(comment, self.source).is_none()
&& !self.ignore_ranges(&root.fragment).contains(idx))
.then_some(idx)
})
}
fn fragment_comment<'f>(
fragment: &'f ast::Fragment<'_>,
idx: usize,
) -> Option<&'f ast::HtmlComment> {
let comment = match &fragment.nodes[idx] {
FragmentNode::Comment(comment) => Some(comment),
_ => None,
};
debug_assert!(comment.is_some(), "only comment indices are classified");
comment
}
fn print_comment_line(&mut self, comment: &ast::HtmlComment, prev_end: Option<u32>) {
if let Some(end) = prev_end
&& text::has_authored_blank_line(
&self.source[end as usize..comment.span.start as usize],
)
{
self.write("\n");
}
self.print_comment(comment);
self.write("\n");
}
fn print_comment_run(
&mut self,
comment_indices: &[usize],
fragment: &ast::Fragment<'_>,
) -> Option<u32> {
let mut prev_end: Option<u32> = None;
for &i in comment_indices {
let Some(comment) = Self::fragment_comment(fragment, i) else {
continue;
};
self.print_comment_line(comment, prev_end);
prev_end = Some(comment.span.end);
}
prev_end
}
fn print_region_end_trail(
&mut self,
trail: Option<usize>,
fragment: &ast::Fragment<'_>,
section_end: u32,
) {
if let Some(idx) = trail
&& let Some(comment) = Self::fragment_comment(fragment, idx)
{
self.print_comment_line(comment, Some(section_end));
}
}
fn print_section_comments(
&mut self,
comment_indices: &[usize],
fragment: &ast::Fragment<'_>,
section_start: u32,
) -> bool {
let Some(last_end) = self.print_comment_run(comment_indices, fragment) else {
return false;
};
let between = &self.source[last_end as usize..section_start as usize];
if text::has_authored_blank_line(between) {
self.write("\n");
}
true
}
fn hoisted_comments(&self, root: &ast::Root<'_>) -> HoistedComments {
let trails = self.region_end_trails(root);
let mut sections = HoistedComments::default();
let scan = CommentScan::new(&root.fragment, self.ignore_ranges(&root.fragment));
for (i, node) in root.fragment.nodes.iter().enumerate() {
if let FragmentNode::Comment(comment) = node {
let section = Self::classify_fragment_comment(comment, i, root, &scan);
if matches!(section, None | Some(HoistedSection::Options))
&& let Some(k) = trails.iter().position(|&trail| trail == Some(i))
{
debug_assert!(sections.0[k].trail.is_none());
sections.0[k].trail = Some(i);
continue;
}
if let Some(section) = section {
sections[section].leading.push(i);
}
}
}
sections
}
fn template_tail_respell(
&self,
root: &ast::Root<'_>,
sections: &HoistedComments,
) -> Option<(usize, char)> {
if root.css.is_some() {
return None;
}
let nodes = &root.fragment.nodes;
let mut tail = None;
for (i, node) in nodes.iter().enumerate().rev() {
match node {
FragmentNode::Text(t) if t.is_collapsible_ws_only => {}
FragmentNode::Comment(_) => {}
FragmentNode::Text(t) => {
tail = Some((i, t));
break;
}
_ => return None,
}
}
let (idx, text) = tail?;
let content = ast::trim_end_collapsible_ws(text.raw(self.source));
let ch = content.chars().next_back()?;
if !crate::whitespace::is_end_trimmed_content(ch) {
return None;
}
let is_comment = |i: usize| matches!(nodes[i], FragmentNode::Comment(_));
if (idx + 1..nodes.len()).any(is_comment) {
let travels = sections.travelling(nodes.len());
if (idx + 1..nodes.len()).any(|i| is_comment(i) && !travels[i]) {
return None;
}
}
Some((
text.raw_span.start as usize + content.len() - ch.len_utf8(),
ch,
))
}
fn style_trailing_comments(
&self,
root: &ast::Root<'_>,
sections: &HoistedComments,
) -> Vec<usize> {
let mut run: Vec<usize> = Vec::new();
if root.css.is_none() {
return run;
}
let nodes = &root.fragment.nodes;
let travels = sections.travelling(nodes.len());
for (i, node) in nodes.iter().enumerate().rev() {
match node {
FragmentNode::Text(t) if t.is_collapsible_ws_only => {}
FragmentNode::Comment(_) if sections.0.iter().any(|s| s.trail == Some(i)) => {}
FragmentNode::Comment(_) if !travels[i] => {
let ranges = self.ignore_ranges(&root.fragment);
if ranges.markers[i].is_some() || ranges.contains(i) {
break;
}
run.push(i);
}
_ => break,
}
}
run.reverse();
run
}
fn print_root(
&mut self,
root: &ast::Root<'_>,
sections: &HoistedComments,
frozen: FrozenSections,
) {
let hoisted: SmallVec<[Span; 4]> = root_sections(root)
.into_iter()
.filter_map(|(span, _)| span)
.collect();
let mut printed_comment_indices: Vec<usize> = Vec::new();
for section in §ions.0 {
printed_comment_indices.extend(§ion.leading);
printed_comment_indices.extend(section.trail);
}
let style_trailing = self.style_trailing_comments(root, sections);
printed_comment_indices.extend(&style_trailing);
let mut has_previous_section = false;
if let Some(options) = &root.options {
let comments = §ions[HoistedSection::Options];
self.print_section_comments(&comments.leading, &root.fragment, options.span.start);
self.print_svelte_options(options);
self.print_region_end_trail(comments.trail, &root.fragment, options.span.end);
has_previous_section = true;
}
for (script, section) in [
(root.module.as_ref(), HoistedSection::ModuleScript),
(root.instance.as_ref(), HoistedSection::InstanceScript),
] {
let comments = §ions[section];
if let Some(script) = script {
if has_previous_section {
self.write("\n"); }
self.print_section_comments(&comments.leading, &root.fragment, script.span.start);
if frozen[section.slot()] {
self.write_verbatim_span(script.span);
self.write("\n");
} else {
self.print_script(script);
}
self.print_region_end_trail(comments.trail, &root.fragment, script.span.end);
has_previous_section = true;
}
}
let has_content = root.fragment.nodes.iter().enumerate().any(|(i, node)| {
if printed_comment_indices.contains(&i) {
return false;
}
!matches!(node, FragmentNode::Text(text) if text.is_collapsible_ws_only)
});
if has_content {
if has_previous_section {
self.write("\n"); }
self.print_root_fragment(&root.fragment, &printed_comment_indices, &hoisted);
self.write("\n"); has_previous_section = true;
}
if let Some(style) = &root.css {
let ignore_style = frozen[HoistedSection::Style.slot()];
if has_previous_section {
self.write("\n"); }
if self
.print_comment_run(&style_trailing, &root.fragment)
.is_some()
{
self.write("\n");
}
let comments = §ions[HoistedSection::Style];
self.print_section_comments(&comments.leading, &root.fragment, style.span.start);
if ignore_style {
self.write_verbatim_span(style.span);
self.write("\n");
} else {
self.print_style(style);
}
self.print_region_end_trail(comments.trail, &root.fragment, style.span.end);
}
}
fn print_svelte_options(&mut self, options: &ast::SvelteOptions<'_>) {
let d = self.d();
let mut parts: DocBuf = DocBuf::with_capacity(options.attributes.len() * 2);
self.push_attrs_with_comments(
&mut parts,
options.attributes,
d.line(),
AttrGaps {
first_range_start: options.name_end,
open_tag_end: options.open_tag_end,
claimed: None,
},
false,
);
if parts.is_empty() {
self.write("<svelte:options />\n");
return;
}
let attrs = d.concat(&parts);
let has_multiline = d.will_break(attrs);
let attr_indent = d.indent(attrs);
let line = d.line();
let inner = d.concat(&[d.text("<svelte:options"), attr_indent, line, d.text("/>")]);
let group = if has_multiline {
d.group_break(inner)
} else {
d.group(inner)
};
self.render_doc_immediate(group);
self.write("\n");
}
fn print_root_fragment(
&mut self,
fragment: &ast::Fragment<'_>,
skip_indices: &[usize],
hoisted: &[Span],
) {
let skippable = |i: usize, n: &FragmentNode<'_>| {
skip_indices.contains(&i)
|| matches!(n, FragmentNode::Text(t) if t.is_collapsible_ws_only)
};
let Some(start) = fragment
.nodes
.iter()
.enumerate()
.position(|(i, n)| !skippable(i, n))
else {
return;
};
let end = fragment
.nodes
.iter()
.enumerate()
.rposition(|(i, n)| !skippable(i, n))
.unwrap_or(start);
let nodes = &fragment.nodes[start..=end];
self.mark_root_inline_run_blocks(nodes);
let ranges = self.ignore_ranges(fragment);
debug_assert!(
skip_indices.iter().all(|&k| ranges.markers[k].is_none()),
"a range marker never travels with a section"
);
let mut out: DocBuf = DocBuf::new();
let mut seg_start = 0;
let mut seg_is_range_tail = false;
let mut i = 0;
while i < nodes.len() {
if skip_indices.contains(&(start + i)) {
if nodes[seg_start..i]
.iter()
.any(|n| !n.is_whitespace_only_text())
{
out.push(self.build_segment_doc(&nodes[seg_start..i], seg_is_range_tail));
if let Some(sep) = self.hoisted_comment_separator(nodes, i) {
out.push(sep);
}
}
seg_is_range_tail = false;
seg_start = i + 1;
i += 1;
continue;
}
if let Some(range_end) = ranges.end_of(start + i).map(|end| end - start) {
out.push(self.build_segment_doc(&nodes[seg_start..=i], seg_is_range_tail));
let raw_start = nodes[i].span().end;
let raw_end = nodes[range_end].span().start;
out.push(self.build_ignore_range_doc(Span::new(raw_start, raw_end), hoisted));
seg_start = range_end;
seg_is_range_tail = true;
i = range_end + 1;
continue;
}
i += 1;
}
if seg_start < nodes.len() {
out.push(self.build_segment_doc(&nodes[seg_start..], seg_is_range_tail));
}
if !out.is_empty() {
let doc = self.d().concat(&out);
self.render_doc_immediate(doc);
}
}
fn build_segment_doc(&self, nodes: &[FragmentNode<'_>], range_tail: bool) -> DocId {
if range_tail {
self.build_range_tail_doc(nodes)
} else {
self.build_nodes_doc_multiline(nodes)
}
}
fn build_ignore_range_doc(&self, span: Span, hoisted: &[Span]) -> DocId {
let mut sections: SmallVec<[Span; 4]> = hoisted
.iter()
.copied()
.filter(|h| span.contains(*h))
.collect();
if sections.is_empty() {
return self.source_span_covering_comments_doc(span);
}
sections.sort_unstable_by_key(|s| s.start);
let mut parts: DocBuf = DocBuf::new();
let mut cursor = span.start;
for cut in lifted_runs::range_cuts(self.source, span.start, §ions) {
if cut.removed.start > cursor {
parts.push(
self.source_span_covering_comments_doc(Span::new(cursor, cut.removed.start)),
);
}
match cut.seam {
lifted_runs::RangeSeam::Kept(seam) => {
parts.push(self.source_span_covering_comments_doc(seam));
}
lifted_runs::RangeSeam::Space => parts.push(self.d().text(" ")),
lifted_runs::RangeSeam::Glued => {}
}
if let Some(ch) = cut.respell {
let mut reference = String::new();
push_char_reference(&mut reference, ch);
parts.push(self.d().text_pooled(&reference));
}
cursor = cut.resume();
}
if cursor < span.end {
parts.push(self.source_span_covering_comments_doc(Span::new(cursor, span.end)));
}
self.d().concat(&parts)
}
fn mark_root_inline_run_blocks(&self, nodes: &[FragmentNode<'_>]) {
let mut marks = self.root_inline_run_block_starts.borrow_mut();
marks.clear();
let mut i = 0;
while i < nodes.len() {
if let Some(run_end) = self.detect_root_inline_run(nodes, i) {
let run = &nodes[i..=run_end];
let multiline = run
.iter()
.any(|n| matches!(n, FragmentNode::Text(t) if t.has_newline()));
if !multiline {
for n in run {
if nodes::is_control_flow_block(n) {
marks.insert(n.span().start);
}
}
}
i = run_end + 1;
} else {
i += 1;
}
}
}
fn hoisted_comment_separator(
&self,
nodes: &[FragmentNode<'_>],
comment_idx: usize,
) -> Option<DocId> {
if comment_idx + 1 >= nodes.len() {
return None;
}
let d = self.d();
let blank = matches!(
&nodes[comment_idx + 1],
FragmentNode::Text(t) if text::has_leading_blank_line(t.raw(self.source))
);
Some(if blank {
d.concat(&[d.literalline(), d.hardline()])
} else {
d.hardline()
})
}
fn detect_root_inline_run(
&self,
nodes: &[FragmentNode<'_>],
start_idx: usize,
) -> Option<usize> {
let is_content_text =
|n: &FragmentNode<'_>| matches!(n, FragmentNode::Text(t) if !t.is_collapsible_ws_only);
let start = &nodes[start_idx];
if !(is_content_text(start)
|| self.is_inline_run_node(start)
|| nodes::is_control_flow_block(start))
{
return None;
}
let mut last_idx = start_idx;
let mut has_control_flow = nodes::is_control_flow_block(start);
let mut has_content_or_inline = is_content_text(start) || self.is_inline_run_node(start);
for j in (start_idx + 1)..nodes.len() {
tsv_lang::fanout_probe::predicate();
if nodes[j - 1].span().end != nodes[j].span().start {
break;
}
let node = &nodes[j];
if let FragmentNode::Text(text) = node {
if text.is_collapsible_ws_only {
break;
}
last_idx = j;
has_content_or_inline = true;
} else if nodes::is_control_flow_block(node) {
last_idx = j;
has_control_flow = true;
} else if self.is_inline_run_node(node) {
last_idx = j;
has_content_or_inline = true;
} else {
break;
}
}
(has_control_flow && has_content_or_inline && last_idx > start_idx).then_some(last_idx)
}
fn is_inline_run_node(&self, node: &FragmentNode<'_>) -> bool {
match node {
FragmentNode::ExpressionTag(_)
| FragmentNode::HtmlTag(_)
| FragmentNode::RenderTag(_) => true,
FragmentNode::Element(el) => !self.is_block_element(el),
FragmentNode::SpecialElement(el) => !el.kind.is_block(),
_ => false,
}
}
fn print_comment(&mut self, comment: &ast::HtmlComment) {
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::record_emitted(self.source, comment.span);
self.write(comment.span.extract(self.source));
}
}