use super::fragment_doc::LeadingEdge;
use crate::ast::{self, FragmentNode, is_collapsible_ws_char};
use crate::printer::Printer;
use crate::printer::classification::element::has_raw_content;
use smallvec::{SmallVec, smallvec};
use tsv_lang::Span;
use tsv_lang::doc::{DocBuf, arena::DocId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum BoundaryMode {
Hard,
Soft,
}
#[derive(Debug, Clone, Copy)]
pub(super) enum ElementLayout {
Void,
SelfClosing,
Empty,
WithContent(BoundaryMode),
}
impl ElementLayout {
pub(super) fn is_soft(self) -> bool {
matches!(self, Self::WithContent(BoundaryMode::Soft))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ElementKind {
Component,
Block,
Inline,
}
impl ElementKind {
pub(super) fn is_component(self) -> bool {
matches!(self, ElementKind::Component)
}
pub(super) fn is_block(self) -> bool {
matches!(self, ElementKind::Block)
}
pub(super) fn is_inline(self) -> bool {
matches!(self, ElementKind::Inline)
}
}
#[derive(Clone, Copy)]
pub(super) struct ElementParts<'arena> {
pub(super) name: DocId,
pub(super) kind: ElementKind,
pub(super) is_void: bool,
pub(super) can_self_close: bool,
pub(super) collapses_child_ws: bool,
pub(super) nodes: &'arena [FragmentNode<'arena>],
pub(super) span: Span,
}
#[expect(clippy::struct_excessive_bools)]
#[derive(Clone, Copy)]
pub(super) struct TagClass {
pub(super) kind: ElementKind,
pub(super) is_void: bool,
pub(super) is_foreign: bool,
pub(super) is_namespaced: bool,
pub(super) raw_text: Option<RawTextKind>,
pub(super) is_ws_sensitive: bool,
pub(super) collapses_child_ws: bool,
pub(super) is_declaration: bool,
}
#[derive(Clone, Copy)]
pub(in crate::printer) struct AttrGaps {
pub(in crate::printer) first_range_start: u32,
pub(in crate::printer) open_tag_end: u32,
pub(in crate::printer) claimed: Option<ThisClaim>,
}
pub(super) struct ElementAttrsDoc {
pub(super) docs: DocBuf,
pub(super) emission: AttrListEmission,
}
#[derive(Clone, Copy, Default)]
pub(in crate::printer) struct AttrListEmission {
pub(in crate::printer) ends_with_line_comment: bool,
}
#[derive(Clone, Copy, Default)]
struct AttrCommentRun {
next_on_new_line: bool,
ends_with_line_comment: bool,
}
#[derive(Clone, Copy)]
pub(in crate::printer) struct ThisClaim {
value: Span,
head_gap_end: u32,
prev_end: u32,
}
impl ThisClaim {
pub(in crate::printer) fn new(
name_end: u32,
value: Span,
attrs: &[ast::AttributeNode<'_>],
) -> Self {
let head_gap_end = attrs
.iter()
.map(|a| a.span().start)
.min()
.map_or(value.start, |first| first.min(value.start));
let prev_end = attrs
.iter()
.map(|a| a.span().end)
.filter(|&end| end <= value.start)
.max()
.unwrap_or(name_end);
Self {
value,
head_gap_end,
prev_end,
}
}
pub(in crate::printer) fn value_start(&self) -> u32 {
self.value.start
}
pub(in crate::printer) fn claims(&self, p: &Printer<'_>, comment: &tsv_lang::Comment) -> bool {
if comment.span.start >= self.value.start {
return comment.span.end <= self.value.end;
}
if p.comment_starts_its_own_line(comment) {
self.prev_end <= comment.span.start
} else {
comment.span.end <= self.head_gap_end
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum MultilineCause {
None,
Structural,
SourceBreaks,
}
impl MultilineCause {
pub(super) fn is_multiline(self) -> bool {
self != Self::None
}
}
pub(super) struct ElementContext {
pub(super) is_self_closing: bool,
pub(super) is_empty: bool,
pub(super) multiline: MultilineCause,
pub(super) has_multiline_attr: bool,
}
pub(super) struct PreparedElement<'e> {
pub(super) element: &'e ast::Element<'e>,
pub(super) class: TagClass,
pub(super) parts: ElementParts<'e>,
pub(super) ctx: ElementContext,
pub(super) attr_docs: DocBuf,
pub(super) layout: ElementLayout,
}
impl PreparedElement<'_> {
pub(super) fn gt_dangle_boundary(&self) -> Option<BoundaryMode> {
match self.layout {
ElementLayout::WithContent(BoundaryMode::Soft) => Some(BoundaryMode::Soft),
ElementLayout::WithContent(BoundaryMode::Hard)
if self.ctx.multiline == MultilineCause::SourceBreaks =>
{
Some(BoundaryMode::Hard)
}
_ => None,
}
}
fn is_soft(&self) -> bool {
self.layout.is_soft()
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum RawTextKind {
Style,
Script,
}
impl RawTextKind {
fn lang(self) -> ast::EmbeddedLang {
match self {
Self::Style => ast::EmbeddedLang::Style,
Self::Script => ast::EmbeddedLang::Script,
}
}
}
impl<'a> Printer<'a> {
#[inline]
pub(super) fn start_tag(&self, name: DocId) -> DocId {
let d = self.d();
d.concat(&[d.text("<"), name, d.text(">")])
}
#[inline]
pub(super) fn end_tag(&self, name: DocId) -> DocId {
let d = self.d();
d.concat(&[d.text("</"), name, d.text(">")])
}
pub(super) fn classify_tag(&self, element: &ast::Element<'_>) -> TagClass {
let facts = element.facts;
let kind = if facts.is_component_name() {
ElementKind::Component
} else if facts.is_block() {
ElementKind::Block
} else {
ElementKind::Inline
};
TagClass {
kind,
is_void: facts.is_void(),
is_foreign: facts.is_foreign(),
is_namespaced: facts.is_namespaced(),
raw_text: facts.is_raw_text().then(|| {
if facts.is_style() {
RawTextKind::Style
} else {
RawTextKind::Script
}
}),
is_ws_sensitive: facts.is_ws_sensitive(),
collapses_child_ws: facts.collapses_child_whitespace(),
is_declaration: facts.is_declaration(),
}
}
pub(super) fn element_parts<'e>(
&self,
element: &'e ast::Element<'e>,
class: TagClass,
) -> ElementParts<'e> {
ElementParts {
name: self.d().source_span(element.name_span, self.source),
kind: class.kind,
is_void: class.is_void,
can_self_close: class.kind.is_component() || class.is_foreign || class.is_namespaced,
collapses_child_ws: class.collapses_child_ws,
nodes: element.fragment.nodes,
span: element.span,
}
}
pub(super) fn build_element_doc(&self, element: &ast::Element<'_>) -> DocId {
let class = self.classify_tag(element);
let is_html = element.kind == ast::ElementKind::Html;
let attrs = self.build_element_attrs_doc(
element.attributes,
self.d().line(),
element.name_span.end,
element.open_tag_end,
is_html,
);
if let Some(kind) = class.raw_text {
return self.build_raw_content_element_doc(kind, element, attrs.docs);
}
if element.is_frozen_template(self.source) {
return self.build_frozen_template_doc(element);
}
if class.is_ws_sensitive {
return self.build_whitespace_sensitive_element_doc(element, attrs);
}
let attr_docs = attrs.docs;
let parts = self.element_parts(element, class);
let ctx = self.analyze_element(&parts, &attr_docs);
let layout = self.compute_element_layout(&parts, &ctx);
self.build_element_layout_doc(element, class, &parts, &ctx, &attr_docs, layout)
}
fn prepare_element<'e>(
&self,
element: &'e ast::Element<'e>,
class: TagClass,
attr_docs: DocBuf,
) -> PreparedElement<'e> {
let parts = self.element_parts(element, class);
let ctx = self.analyze_element(&parts, &attr_docs);
let layout = self.compute_element_layout(&parts, &ctx);
PreparedElement {
element,
class,
parts,
ctx,
attr_docs,
layout,
}
}
pub(super) fn build_prepared_element_doc(&self, prepared: &PreparedElement<'_>) -> DocId {
let PreparedElement {
element,
class,
parts,
ctx,
attr_docs,
layout,
} = prepared;
self.build_element_layout_doc(element, *class, parts, ctx, attr_docs, *layout)
}
fn build_element_layout_doc(
&self,
element: &ast::Element<'_>,
class: TagClass,
parts: &ElementParts<'_>,
ctx: &ElementContext,
attr_docs: &[DocId],
layout: ElementLayout,
) -> DocId {
match layout {
ElementLayout::Void | ElementLayout::SelfClosing => {
self.build_void_element_doc(parts, attr_docs, class.is_declaration)
}
ElementLayout::Empty => {
let opening_tag =
self.build_opening_tag(parts.name, attr_docs, ctx.has_multiline_attr);
self.build_empty_element_doc(
element,
opening_tag,
!attr_docs.is_empty(),
class.kind,
)
}
ElementLayout::WithContent(boundary) => {
self.build_content_element_doc(parts, ctx, attr_docs, boundary, false, None)
}
}
}
pub(super) fn build_gt_dangle_element_doc(
&self,
prepared: &PreparedElement<'_>,
boundary: BoundaryMode,
sheds_gt: bool,
gt_prefix: Option<DocId>,
) -> DocId {
self.build_content_element_doc(
&prepared.parts,
&prepared.ctx,
&prepared.attr_docs,
boundary,
sheds_gt,
gt_prefix,
)
}
pub(super) fn prepare_sibling_element<'e>(
&self,
element: &'e ast::Element<'e>,
) -> Option<PreparedElement<'e>> {
let class = self.classify_tag(element);
if class.raw_text.is_some()
|| class.is_ws_sensitive
|| element.is_frozen_template(self.source)
{
return None;
}
let is_html = element.kind == ast::ElementKind::Html;
let attr_docs = self
.build_element_attrs_doc(
element.attributes,
self.d().line(),
element.name_span.end,
element.open_tag_end,
is_html,
)
.docs;
Some(self.prepare_element(element, class, attr_docs))
}
pub(super) fn build_inline_element_close_gt_dangle(
&self,
element: &ast::Element<'_>,
) -> Option<DocId> {
let prepared = self
.prepare_sibling_element(element)
.filter(PreparedElement::is_soft)?;
Some(self.build_close_gt_dangle_doc(&prepared.parts, &prepared.ctx, &prepared.attr_docs))
}
pub(super) fn build_close_gt_dangle_doc(
&self,
parts: &ElementParts<'_>,
ctx: &ElementContext,
attr_docs: &[DocId],
) -> DocId {
let children_doc = self.build_content_children_doc(parts, ctx, BoundaryMode::Soft);
let block_state =
self.build_collapsible_element_doc(parts, ctx, attr_docs, children_doc, false, None);
let d = self.d();
if d.will_break(children_doc) || ctx.has_multiline_attr {
return block_state;
}
let name = parts.name;
let opening = self.build_opening_tag(name, attr_docs, ctx.has_multiline_attr);
let head = d.concat(&[opening, d.text(">"), children_doc, d.text("</"), name]);
let inline_state = d.concat(&[head, d.text(">")]);
let dangle_state = d.concat(&[head, d.hardline(), d.text(">")]);
d.conditional_group(&[inline_state, dangle_state, block_state])
}
pub(super) fn build_void_element_doc(
&self,
parts: &ElementParts<'_>,
attr_docs: &[DocId],
is_declaration: bool,
) -> DocId {
let d = self.d();
let name = parts.name;
if attr_docs.is_empty() {
if is_declaration {
d.concat(&[d.text("<"), name, d.text(">")])
} else {
d.concat(&[d.text("<"), name, d.text(" />")])
}
} else if is_declaration {
let attr_concat = d.concat(attr_docs);
let attr_indent = d.indent(attr_concat);
let inner = d.concat(&[d.text("<"), name, attr_indent, d.softline(), d.text(">")]);
d.group(inner)
} else {
let has_multiline = attr_docs.iter().any(|&doc| d.will_break(doc));
let attr_concat = d.concat(attr_docs);
let attr_indent = d.indent(attr_concat);
let inner = d.concat(&[d.text("<"), name, attr_indent, d.line(), d.text("/>")]);
if has_multiline {
d.group_break(inner)
} else {
d.group(inner)
}
}
}
pub(super) fn build_opening_tag(
&self,
name: DocId,
attr_docs: &[DocId],
force_break: bool,
) -> DocId {
let d = self.d();
if attr_docs.is_empty() {
d.concat(&[d.text("<"), name])
} else {
let sl = d.softline();
let inner = d.concat(&[d.concat(attr_docs), d.dedent(sl)]);
let attr_group = if force_break {
d.group_break(inner)
} else {
d.group(inner)
};
let indented = d.indent(attr_group);
d.concat(&[d.text("<"), name, indented])
}
}
fn build_opening_tag_with_gt_prefix(
&self,
name: DocId,
attr_docs: &[DocId],
force_break: bool,
gt: DocId,
) -> DocId {
let d = self.d();
if attr_docs.is_empty() {
return d.concat(&[gt, d.text("<"), name]);
}
let sl = d.softline();
let attrs_body = d.indent(d.concat(&[d.concat(attr_docs), d.dedent(sl)]));
let prefix = d.if_break(d.concat(&[d.hardline(), gt]), gt);
let whole = d.concat(&[prefix, d.text("<"), name, attrs_body]);
if force_break {
d.group_break(whole)
} else {
d.group(whole)
}
}
pub(super) fn build_content_element_doc(
&self,
parts: &ElementParts<'_>,
ctx: &ElementContext,
attr_docs: &[DocId],
boundary: BoundaryMode,
sheds_gt: bool,
gt_prefix: Option<DocId>,
) -> DocId {
let children_doc = self.build_content_children_doc(parts, ctx, boundary);
if boundary == BoundaryMode::Soft {
return self.build_collapsible_element_doc(
parts,
ctx,
attr_docs,
children_doc,
sheds_gt,
gt_prefix,
);
}
self.build_multiline_element_doc(parts, ctx, attr_docs, children_doc, sheds_gt, gt_prefix)
}
fn build_content_children_doc(
&self,
parts: &ElementParts<'_>,
ctx: &ElementContext,
boundary: BoundaryMode,
) -> DocId {
if parts.collapses_child_ws {
self.build_container_content_doc(parts.nodes)
} else {
let multiline = boundary == BoundaryMode::Hard && ctx.multiline.is_multiline();
self.build_nodes_doc_trimmed(parts.nodes, multiline, LeadingEdge::Trimmed)
}
}
fn build_multiline_element_doc(
&self,
parts: &ElementParts<'_>,
ctx: &ElementContext,
attr_docs: &[DocId],
children_doc: DocId,
sheds_gt: bool,
gt_prefix: Option<DocId>,
) -> DocId {
let d = self.d();
d.concat(&[
self.build_content_opening_tag(parts, ctx, attr_docs, gt_prefix),
d.text(">"),
d.indent_hardline(children_doc),
d.hardline(),
if sheds_gt {
d.concat(&[d.text("</"), parts.name])
} else {
self.end_tag(parts.name)
},
])
}
fn build_content_opening_tag(
&self,
parts: &ElementParts<'_>,
ctx: &ElementContext,
attr_docs: &[DocId],
gt_prefix: Option<DocId>,
) -> DocId {
match gt_prefix {
Some(gt) => self.build_opening_tag_with_gt_prefix(
parts.name,
attr_docs,
ctx.has_multiline_attr,
gt,
),
None => self.build_opening_tag(parts.name, attr_docs, ctx.has_multiline_attr),
}
}
fn build_collapsible_element_doc(
&self,
parts: &ElementParts<'_>,
ctx: &ElementContext,
attr_docs: &[DocId],
children_doc: DocId,
sheds_gt: bool,
gt_prefix: Option<DocId>,
) -> DocId {
let d = self.d();
let opening = self.build_content_opening_tag(parts, ctx, attr_docs, gt_prefix);
let close_gt = if sheds_gt { d.empty() } else { d.text(">") };
let body = d.indent(d.concat(&[d.softline(), children_doc]));
d.group(d.concat(&[
opening,
d.text(">"),
body,
d.softline(),
d.text("</"),
parts.name,
close_gt,
]))
}
fn build_empty_element_doc(
&self,
element: &ast::Element<'_>,
opening_tag: DocId,
has_attrs: bool,
kind: ElementKind,
) -> DocId {
let d = self.d();
let name_doc = d.source_span(element.name_span, self.source);
let is_html = element.kind == ast::ElementKind::Html;
let closing = d.concat(&[d.text("></"), name_doc, d.text(">")]);
if has_attrs && (kind.is_inline() || kind.is_component()) {
let inline_state = d.concat(&[opening_tag, closing]);
let hug_attrs = self
.build_element_attrs_doc(
element.attributes,
self.d().text(" "),
element.name_span.end,
element.open_tag_end,
is_html,
)
.docs;
let hug_state = d.concat(&[
d.text("<"),
name_doc,
d.concat(&hug_attrs),
d.hardline(),
closing,
]);
let multiline_attrs = self
.build_element_attrs_doc(
element.attributes,
self.d().line(),
element.name_span.end,
element.open_tag_end,
is_html,
)
.docs;
let multiline_concat = d.concat(&multiline_attrs);
let multiline_indent = d.indent(multiline_concat);
let multiline_state = d.concat(&[
d.text("<"),
name_doc,
multiline_indent,
d.hardline(),
closing,
]);
d.conditional_group(&[inline_state, hug_state, multiline_state])
} else {
d.group(d.concat(&[opening_tag, closing]))
}
}
fn build_frozen_template_doc(&self, element: &ast::Element<'_>) -> DocId {
let d = self.d();
let name_doc = d.source_span(element.name_span, self.source);
let attr_docs = self
.build_element_attrs_doc(
element.attributes,
d.line(),
element.name_span.end,
element.open_tag_end,
true,
)
.docs;
let mut parts: DocBuf = smallvec![self.build_opening_tag(name_doc, &attr_docs, false)];
parts.push(d.text(">"));
let nodes = element.fragment.nodes;
if let (Some(first), Some(last)) = (nodes.first(), nodes.last()) {
parts.push(self.frozen_body_doc(Span::new(first.span().start, last.span().end)));
}
parts.push(self.end_tag(name_doc));
d.concat(&parts)
}
fn build_raw_content_element_doc(
&self,
kind: RawTextKind,
element: &ast::Element<'_>,
attr_docs: DocBuf,
) -> DocId {
let d = self.d();
let name_doc = d.source_span(element.name_span, self.source);
let opening_tag = d.group(d.concat(&[
self.build_opening_tag(name_doc, &attr_docs, false),
d.text(">"),
]));
let closing_tag = self.end_tag(name_doc);
let text = match element.fragment.nodes.first() {
Some(FragmentNode::Text(text)) if has_raw_content(element) => text,
_ => return d.concat(&[opening_tag, closing_tag]),
};
if kind.lang().is_frozen(element.attributes, self.source) {
return d.concat(&[
opening_tag,
self.frozen_body_doc(text.raw_span),
closing_tag,
]);
}
let content = text.data(self.source);
if content.trim().is_empty() {
return d.concat(&[opening_tag, d.hardline(), closing_tag]);
}
let arena =
bumpalo::Bump::with_capacity(tsv_lang::estimated_ast_arena_capacity(content.len()));
let base_offset = text.raw_span.start as usize;
let host_nesting = self.nested_raw_text_nesting.get();
let body = match kind {
RawTextKind::Script => {
tsv_ts::__private::parse_embedded(&content, base_offset, host_nesting, &arena)
.ok()
.and_then(|program| self.build_nested_script_body_doc(&program))
}
RawTextKind::Style => {
tsv_css::__private::parse_embedded(&content, base_offset, host_nesting, &arena)
.ok()
.and_then(|stylesheet| self.build_nested_style_body_doc(&stylesheet))
}
};
match body {
Some(body) => d.concat(&[
opening_tag,
d.indent(d.concat(&[d.hardline(), body])),
d.hardline(),
closing_tag,
]),
None => {
d.concat(&[opening_tag, d.text_pooled(&content), closing_tag])
}
}
}
fn build_nested_script_body_doc(&self, program: &tsv_ts::ast::Program<'_>) -> Option<DocId> {
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::register_parsed(
self.source,
&tsv_lang::merge_nestled_block_comments(self.source, program.comments),
);
tsv_ts::__private::build_program_body_doc(
self.d(),
program,
self.source,
self.line_table(),
tsv_lang::EmbedContext::line_owning(),
)
}
fn build_nested_style_body_doc(
&self,
stylesheet: &tsv_css::ast::CssStyleSheet<'_>,
) -> Option<DocId> {
let level = self.body_indent_level();
let format_at = |level: usize| {
let embed = tsv_lang::EmbedContext {
base_indent_offset: level,
..tsv_lang::EmbedContext::default()
};
tsv_css::__private::format_embedded_in(
stylesheet,
self.source,
self.line_table(),
embed,
self.css_host_scan(),
self.d(),
)
};
let formatted = format_at(level);
let formatted = formatted.trim_end();
if formatted.trim_start().is_empty() {
return None;
}
let d = self.d();
let deeper = {
#[cfg(feature = "_internal_comment_check")]
let _measuring = tsv_lang::comment_ledger::SuppressEmits::new();
format_at(level + 1)
};
let deeper = deeper.trim_end();
let lines: Vec<&str> = formatted.split('\n').collect();
let deeper_lines: Vec<&str> = deeper.split('\n').collect();
if lines.len() != deeper_lines.len() {
return Some(d.multiline_text_literal(formatted.trim_start_matches('\t')));
}
let mut parts: SmallVec<[DocId; 16]> = SmallVec::new();
parts.push(d.text_pooled(lines[0].trim_start_matches('\t')));
for (line, deeper_line) in lines.iter().zip(&deeper_lines).skip(1) {
let owned = deeper_line.strip_prefix('\t') == Some(*line)
&& line.len() - line.trim_start_matches('\t').len() >= level;
if owned {
parts.push(d.hardline());
parts.push(d.text_pooled(&line[level..]));
} else {
parts.push(d.literalline());
parts.push(d.text_pooled(line));
}
}
Some(d.concat(&parts))
}
pub(super) fn build_element_attrs_doc(
&self,
attrs: &[ast::AttributeNode<'_>],
separator: DocId,
name_end: u32,
open_tag_end: u32,
is_html: bool,
) -> ElementAttrsDoc {
let mut docs: DocBuf = DocBuf::with_capacity(attrs.len() * 2);
let emission = self.push_attrs_with_comments(
&mut docs,
attrs,
separator,
AttrGaps {
first_range_start: name_end,
open_tag_end,
claimed: None,
},
is_html,
);
ElementAttrsDoc { docs, emission }
}
pub(in crate::printer) fn push_attrs_with_comments(
&self,
docs: &mut DocBuf,
attrs: &[ast::AttributeNode<'_>],
separator: DocId,
gaps: AttrGaps,
is_html: bool,
) -> AttrListEmission {
let AttrGaps {
first_range_start,
open_tag_end,
claimed,
} = gaps;
let gap_comments = |start: u32, end: u32| {
self.comments_to_emit_between(start, end)
.filter(move |c| !claimed.is_some_and(|cl| cl.claims(self, c)))
};
let has_gap_comments = |start: u32, end: u32| gap_comments(start, end).next().is_some();
let gap_start = |i: usize| {
i.checked_sub(1)
.map_or(first_range_start, |prev| attrs[prev].span().end)
};
if !has_gap_comments(first_range_start, open_tag_end) {
for attr in attrs {
docs.push(separator);
docs.push(self.build_attribute_node_doc(attr, is_html));
}
return AttrListEmission::default();
}
for (i, attr) in attrs.iter().enumerate() {
self.push_attr_item_with_leading_comments(
docs,
separator,
gap_comments(gap_start(i), attr.span().start),
self.build_attribute_node_doc(attr, is_html),
);
}
let trailing =
self.push_attr_comment_docs(docs, gap_comments(gap_start(attrs.len()), open_tag_end));
AttrListEmission {
ends_with_line_comment: trailing.ends_with_line_comment,
}
}
pub(super) fn push_attr_item_with_leading_comments<'c>(
&self,
docs: &mut DocBuf,
separator: DocId,
comments: impl IntoIterator<Item = &'c tsv_lang::Comment>,
item: DocId,
) {
let d = self.d();
let mut comments = comments.into_iter().peekable();
if comments.peek().is_none() {
docs.push(separator);
} else {
let tail = self.push_attr_comment_docs(docs, comments);
docs.push(if tail.next_on_new_line {
d.hardline()
} else {
separator
});
}
docs.push(item);
}
fn comment_starts_its_own_line(&self, comment: &tsv_lang::Comment) -> bool {
self.source[..comment.span.start as usize]
.chars()
.rev()
.take_while(|&c| is_collapsible_ws_char(c))
.any(|c| c == '\n')
}
fn push_attr_comment_docs<'c>(
&self,
docs: &mut DocBuf,
comments: impl IntoIterator<Item = &'c tsv_lang::Comment>,
) -> AttrCommentRun {
let d = self.d();
let mut tail = AttrCommentRun::default();
for comment in comments {
let is_own_line = self.comment_starts_its_own_line(comment);
if is_own_line {
docs.push(d.hardline());
} else {
docs.push(d.text(" "));
}
docs.push(self.build_attr_js_comment_doc(comment));
if !comment.is_block {
docs.push(d.break_parent());
}
tail = AttrCommentRun {
next_on_new_line: is_own_line || !comment.is_block,
ends_with_line_comment: !comment.is_block,
};
}
tail
}
fn build_attr_js_comment_doc(&self, comment: &tsv_lang::Comment) -> DocId {
let doc = self.js_comment_text_doc(comment);
#[cfg(feature = "_internal_comment_check")]
self.d().tag_comment_doc(doc, comment.span, self.source);
doc
}
pub(super) fn span_was_self_closing(&self, span: Span) -> bool {
span.extract(self.source).trim_end().ends_with("/>")
}
}