1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// Program-level printing for TypeScript
//
// Top-level orchestration: statement iteration with blank-line preservation,
// leading/trailing comment placement, and format-ignore raw emission.
use crate::ast;
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use super::Printer;
impl<'a> Printer<'a> {
/// Print a TypeScript program
///
/// Delegates to `build_program_doc` to build the doc tree, then renders it.
/// This is the same path used by Svelte's `<script>` formatting, ensuring
/// consistent behavior (e.g., trailing whitespace trimming in comments).
pub(crate) fn print_program(&mut self, program: &ast::Program<'_>) {
let doc = self.build_program_doc(program);
self.write_arena_doc(doc);
}
/// Build a DocId tree for a TypeScript program
///
/// Returns a DocId that can be wrapped with `indent()` and rendered.
/// Used both for standalone TS/JS formatting (via `print_program`) and
/// when embedding TypeScript in other formats like Svelte's `<script>`.
///
/// The Doc structure preserves:
/// - Statement separation with hardline
/// - Blank line preservation between statements using literalline
/// - Leading comments with proper spacing
/// - Trailing same-line comments using line_suffix
/// - Program trailing comments after the last statement
pub(crate) fn build_program_doc(&self, program: &ast::Program<'_>) -> DocId {
let d = self.d();
let (body, has_output) = self.build_program_body_doc(program);
// Trailing newline (only if there's content — empty files stay empty)
if has_output {
d.concat(&[body, d.hardline()])
} else {
body
}
}
/// [`Self::build_program_doc`] without the trailing newline, paired with whether the
/// program has any output — a statement or a comment — for that newline to end.
///
/// The shape an embedder wants when it places the body between delimiters of its own —
/// a `<script>` nested in Svelte markup, whose closing tag supplies the line break the
/// program would otherwise end with.
pub(crate) fn build_program_body_doc(&self, program: &ast::Program<'_>) -> (DocId, bool) {
let d = self.d();
let mut parts = d.pooled_docbuf();
// The shared statement-list walk — the same one every block, function body and
// `namespace` body takes. A program differs from those only in its BOUNDS and in
// having no opening delimiter: the body starts at byte 0 rather than past a `{`,
// it hoists nothing above the first statement (`has_leading: false`, which is what
// makes the walk's first-statement arm emit no separator — the program's own rule)
// and it pulls no comment onto a brace line (`delimiter_pull_pos: None`).
//
// Keeping a second copy of this loop here is what let the two drift: the copy's
// leading run answered the separator question from the ITEM rather than from the
// source after each `*/`, and its blank scan was emit-keyed where the shared one is
// in-source — so a glued run split and an owned comment's own newlines read as an
// author blank, at the top level only.
let tail = self.build_statement_list_docs_into(
&mut parts,
program.body,
Span::new(0, program.span.end),
false,
None,
true,
);
// Trailing program comments
let mut has_output = tail.last_stmt_end.is_some();
if self.push_program_trailing_comments(
&mut parts,
tail.prev_end,
tail.claims_trailing,
has_output,
) {
has_output = true;
}
(d.concat(&parts), has_output)
}
/// Build docs for trailing comments at the end of the program
///
/// Handles comments that appear after all statements but before end of file — the
/// `}`-less end-of-body run, so it is the shared
/// [`Printer::push_trailing_body_comments`] with the source length as its bound
/// (equivalent by construction to an unbounded scan: `self.comments` is already
/// island-local for an embedded `<script>`).
///
/// `claims_trailing` says this run owns the comments sharing `prev_end`'s source line —
/// set when the last statement deferred a line comment past its own `;`
/// (`terminator_defers_line_comment`), so it trailed nothing on that line and, with no
/// further statement to lead, this emitter is their last chance to be printed.
///
/// ⚠️ `has_output` is the program's own answer to "is there a previous item", the
/// question the shared emitter otherwise derives from `prev_end > 0`. That proxy holds
/// for every `{}` body — their cursor opens at the `{`, and a body's first item always
/// prints — but a dropped `EmptyStatement` at the head of a program advances the cursor
/// while printing NOTHING, and the run then breaks away from content that isn't there
/// (a blank first line, and a form the reparse doesn't reproduce). So with no output
/// the cursor is handed back as 0, the emitter's "no previous item at all" state.
/// Sound because nothing printed means nothing was skipped either — an orphan run that
/// finds a comment always emits it — so `[0, prev_end)` provably holds no comments the
/// widened scan could double-print.
fn push_program_trailing_comments(
&self,
parts: &mut DocBuf,
prev_end: u32,
claims_trailing: bool,
has_output: bool,
) -> bool {
let cursor = if has_output { prev_end } else { 0 };
self.push_trailing_body_comments(parts, cursor, self.source.len() as u32, claims_trailing)
}
}