feat(formatter_core): add literal line and root indention primitives (#24051)

IR primitives required for future YAML and Markdown support.

At the same time, this has resolved the workarounds used in embedded such as markdown-in-js / js-in-vue, etc.
This commit is contained in:
leaysgur committed 2026-07-02 08:14:03 +00:00
1 parent 213a96b21d
commit 89ec3d9f4b
12 files changed
+430 -75

No files matched your search

@@ -99,7 +99,7 @@ fn convert_doc<'a>(
};
match doc_type {
"line" => {
convert_line(obj, out, ctx);
convert_line(obj, out);
Ok(())
}
"group" => convert_group(obj, out, ctx),
@@ -136,17 +136,17 @@ fn convert_doc<'a>(
fn convert_line<'a>(
obj: &serde_json::Map<String, Value>,
out: &mut ArenaVec<'a, FormatElement<'a>>,
ctx: &FmtCtx<'a, '_>,
) {
let hard = obj.get("hard").and_then(Value::as_bool).unwrap_or(false);
let soft = obj.get("soft").and_then(Value::as_bool).unwrap_or(false);
let literal = obj.get("literal").and_then(Value::as_bool).unwrap_or(false);
if hard && literal {
let arena_text = ctx.allocator.alloc_str("\n");
let width = TextWidth::multiline(0);
out.push(FormatElement::Text { text: arena_text, width });
out.push(FormatElement::ExpandParent);
// NOTE: inherits the core printer's known divergence — a hard line directly
// after a COLUMN-0 literal line is absorbed (Prettier prints both newlines).
// This mechanical conversion cannot apply the `empty_line()` workaround;
// see `hard_line_after_column_zero_literal_line_is_absorbed` in `oxc_formatter_core`.
out.push(FormatElement::Line(LineMode::Literal));
} else if hard {
out.push(FormatElement::Line(LineMode::Hard));
} else if soft {
@@ -263,22 +263,15 @@ fn convert_align<'a>(
Err(format!("Unsupported align value: {n}"))
}
Value::Object(obj_val) => {
// `align({type: "root"}, ...)` = Prettier's `markAsRoot()`.
// In Prettier, `markAsRoot` records the current indent position
// so that a later `dedentToRoot` can return to it.
// However, `oxc_formatter`'s `DedentMode::Root` always resets to absolute level 0
// and has no way to store a custom root position.
// Skipping the root capture is safe because
// embedded language Docs are processed in their own context starting near level 0,
// so `dedentToRoot` to absolute 0 produces the same result.
//
// NOTE: `markAsRoot` is used in Prettier for other cases.
// e.g. JS comment printer, YAML block printer, and front-matter embed.
// But none of those go through this Doc→IR path.
// `align({type: "root"}, ...)` = Prettier's `markAsRoot()`:
// records the current indent position so that literal lines and
// a later `dedentToRoot` return to it.
if obj_val.get("type").and_then(Value::as_str) == Some("root") {
out.push(FormatElement::Tag(Tag::StartMarkAsRoot));
if let Some(contents) = obj.get("contents") {
convert_doc(contents, out, ctx)?;
}
out.push(FormatElement::Tag(Tag::EndMarkAsRoot));
return Ok(());
}
Err(format!("Unsupported align value: {n}"))
@@ -78,6 +78,7 @@ enum StartTagInfo {
Entry,
LineSuffix,
Labelled,
MarkAsRoot,
}
/// Simulates a subset of the `oxc_formatter` printer's runtime state.
@@ -187,6 +188,13 @@ fn convert_elements(
push_line(current_children_mut(&mut stack)?, *mode);
}
}
LineMode::Literal => {
// A literal line always prints (no `last_was_hardline` dedup) and
// preserves the pending space (the printer never trims it away),
// matching a `\n` embedded in `FormatElement::Text` below.
printer.flush_pending_space(&mut stack)?;
push_line(current_children_mut(&mut stack)?, *mode);
}
}
printer.last_was_hardline = matches!(mode, LineMode::Hard | LineMode::Empty);
}
@@ -420,6 +428,9 @@ fn push_line(children: &mut Vec<Value>, mode: LineMode) {
children.push(json!({"type": "line", "hard": true}));
children.push(json!({"type": "break-parent"}));
}
LineMode::Literal => {
push_literal_line(children);
}
}
}
@@ -445,6 +456,7 @@ fn extract_start_tag_info(tag: &Tag) -> StartTagInfo {
Tag::StartEntry => StartTagInfo::Entry,
Tag::StartLineSuffix => StartTagInfo::LineSuffix,
Tag::StartLabelled(_) => StartTagInfo::Labelled,
Tag::StartMarkAsRoot => StartTagInfo::MarkAsRoot,
_ => unreachable!("end tags should not be passed to `extract_start_tag_info()`"),
}
}
@@ -468,6 +480,10 @@ fn build_doc(start_info: Option<&StartTagInfo>, children: Vec<Value>) -> Value {
StartTagInfo::Align(count) => {
json!({"type": "align", "n": *count, "contents": normalize_array(children)})
}
StartTagInfo::MarkAsRoot => {
// Prettier's `markAsRoot()` = `align({type: "root"}, ...)`
json!({"type": "align", "n": {"type": "root"}, "contents": normalize_array(children)})
}
StartTagInfo::Dedent(mode) => {
let n: Value = match mode {
DedentMode::Level => Value::from(-1),
@@ -56,8 +56,7 @@ pub(super) fn try_embed_markdown<'a>(
if has_indent {
write!(f, ["`", indent(&format_args!(soft_line_break(), content)), soft_line_break(), "`"]);
} else {
let literalline = format_with(|f| super::write_literalline(f, allocator));
write!(f, ["`", literalline, dedent_to_root(&content), soft_line_break(), "`"]);
write!(f, ["`", literal_line_break(), dedent_to_root(&content), soft_line_break(), "`"]);
}
true
@@ -10,6 +10,7 @@ use oxc_formatter_core::IndentWidth;
use crate::{
ast_nodes::{AstNode, AstNodes},
formatter::{FormatElement, format_element::TextWidth, prelude::*},
write,
};
/// Try to format a tagged template with the embedded formatter if supported.
@@ -266,7 +267,7 @@ fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&
/// Emit text with newlines converted to literal line breaks (`replaceEndOfLine()` equivalent).
///
/// Uses [`write_literalline`] instead of `hard_line_break()` to avoid adding indentation.
/// Uses [`literal_line_break`] instead of `hard_line_break()` to avoid adding indentation.
///
/// The external formatter has already computed proper indentation in the text content,
/// so we must not add extra indent from the surrounding `block_indent`.
@@ -280,7 +281,7 @@ fn write_text_with_line_breaks<'a>(
// Splitting on `\n` is safe because `Doc` only contains normalized linebreaks.
for line in text.split('\n') {
if !first {
write_literalline(f, allocator);
write!(f, [literal_line_break()]);
}
first = false;
if !line.is_empty() {
@@ -291,16 +292,6 @@ fn write_text_with_line_breaks<'a>(
}
}
/// Emit Prettier's `literalline` equivalent,
/// which newline that preserves indentation from the source.
fn write_literalline<'a>(f: &mut JsFormatter<'_, 'a>, allocator: &'a Allocator) {
f.write_element(FormatElement::Text {
text: allocator.alloc_str("\n"),
width: TextWidth::multiline(0),
});
f.write_element(FormatElement::ExpandParent);
}
// ---
/// Re-escape template-literal characters (`` ` ``, `${`, `\`) in every `Text` element of an embedded IR.
+10
View File
@@ -16,6 +16,16 @@ Formatting is two stages:
Key IR pieces are all exported from the crate root.
The semantics of each building block live in the `builders.rs` rustdocs.
e.g. the three mechanisms for verbatim multi-line content
(`literal_line_break()`, multiline `text()`, `text(..).without_expand_parent()`, and `mark_as_root` / `dedent_to_root`),
with the non-obvious behaviors pinned by printer tests verified against Prettier's `printDocToString`.
Prettier doc primitives are ported on demand; still missing:
- `hardlineWithoutBreakParent` (markdown tables)
- and the `trim` doc
### Generic context design
The core is parameterized over a consumer-supplied context so it stays language-agnostic:
+56 -1
View File
@@ -52,6 +52,27 @@ pub const fn soft_line_break_or_space() -> Line {
Line::new(LineMode::SoftOrSpace)
}
/// A forced line break that starts the next line at the marked root indention (Prettier's `literalline`).
///
/// Unlike [hard_line_break]:
/// - trailing whitespace on the current line is preserved (never trimmed)
/// - the newline always prints, even on an empty line
/// - the next line starts at the [mark_as_root] indention (column 0 when unmarked)
/// instead of the current indention
///
/// Used for verbatim multi-line content whose line structure is built element by element
/// (e.g. YAML block scalars). For verbatim content held as ONE string,
/// a multiline [text] already prints its embedded newlines with these semantics.
///
/// Known divergence from Prettier:
/// a [hard_line_break] directly after a COLUMN-0 literal line is absorbed
/// by the printer's "only print a newline if the line isn't already empty" rule (Prettier prints both newlines).
/// Use [empty_line] when the extra structural newline is required, it prints exactly one newline in this state.
#[inline]
pub const fn literal_line_break() -> Line {
Line::new(LineMode::Literal)
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Line {
mode: LineMode,
@@ -429,7 +450,8 @@ where
Dedent { content: Argument::new(content), mode: DedentMode::Level }
}
/// Resets the indent document so that the content will be printed at the start of the line.
/// Resets the indention so that the content prints at the [mark_as_root] indention,
/// or at the start of the line when no `mark_as_root` is active (Prettier's `dedentToRoot`).
#[inline]
pub fn dedent_to_root<'ast, C, Content>(content: &Content) -> Dedent<'_, 'ast, C>
where
@@ -438,6 +460,39 @@ where
Dedent { content: Argument::new(content), mode: DedentMode::Root }
}
/// Marks the current indention as the root that [literal_line_break] and [dedent_to_root]
/// inside `content` return to (Prettier's `markAsRoot`).
///
/// Without an enclosing `mark_as_root`, the root is column 0.
/// e.g. YAML block scalars wrap each line boundary in `mark_as_root(&literal_line_break())`
/// so continuation lines keep the block's base indention.
#[inline]
pub fn mark_as_root<'ast, C, Content>(content: &Content) -> MarkAsRoot<'_, 'ast, C>
where
Content: Format<'ast, C>,
{
MarkAsRoot { content: Argument::new(content) }
}
#[derive(Copy, Clone)]
pub struct MarkAsRoot<'a, 'ast, C> {
content: Argument<'a, 'ast, C>,
}
impl<'ast, C> Format<'ast, C> for MarkAsRoot<'_, 'ast, C> {
fn fmt(&self, f: &mut Formatter<'_, 'ast, C>) {
f.write_element(FormatElement::Tag(Tag::StartMarkAsRoot));
Arguments::from(&self.content).fmt(f);
f.write_element(FormatElement::Tag(Tag::EndMarkAsRoot));
}
}
impl<C> std::fmt::Debug for MarkAsRoot<'_, '_, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("MarkAsRoot").field(&"{{content}}").finish()
}
}
#[derive(Copy, Clone)]
pub struct Dedent<'a, 'ast, C> {
content: Argument<'a, 'ast, C>,
@@ -92,9 +92,9 @@ where
fn fmt(&self, f: &mut Formatter<'_, 'a, C>) {
use Tag::{
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
};
w!(f, [ContentArrayStart]);
@@ -170,6 +170,9 @@ where
LineMode::Empty => {
w!(f, [token("empty_line")]);
}
LineMode::Literal => {
w!(f, [token("literal_line_break")]);
}
},
FormatElement::ExpandParent => {
w!(f, [token("expand_parent")]);
@@ -395,6 +398,10 @@ where
w!(f, [token("fill(")]);
}
StartMarkAsRoot => {
w!(f, [token("mark_as_root(")]);
}
StartEntry => {
// handled after the match for all start tags
}
@@ -408,6 +415,7 @@ where
| EndIndent
| EndGroup
| EndLineSuffix
| EndMarkAsRoot
| EndDedent(_) => {
w!(f, [ContentArrayEnd, token(")")]);
}
@@ -5,7 +5,7 @@ use oxc_allocator::ArenaVec;
use rustc_hash::FxHashMap;
use super::{
FormatElement, FormatElements, Interned, LineMode,
FormatElement, FormatElements, Interned,
tag::{self, LabelId, Tag, TagKind},
};
@@ -54,7 +54,7 @@ impl<'a> Document<'a> {
impl Document<'_> {
/// Sets [`expand`](tag::Group::expand) to [`GroupMode::Propagated`] if the group contains any of:
/// * a group with [`expand`](tag::Group::expand) set to [GroupMode::Propagated] or [GroupMode::Expand].
/// * a non-soft [line break](FormatElement::Line) with mode [LineMode::Hard] or [LineMode::Empty].
/// * a non-soft [line break](FormatElement::Line) whose [`LineMode::will_break()`](super::LineMode::will_break) is true.
/// * a multiline [FormatElement::Text] whose `TextWidth` is not marked `without_expand_parent`.
/// * a [FormatElement::ExpandParent]
///
@@ -148,8 +148,8 @@ impl Document<'_> {
}
// `FormatElement::Token` cannot contain line breaks
FormatElement::Text { text: _, width } => width.propagates_expand(),
FormatElement::ExpandParent
| FormatElement::Line(LineMode::Hard | LineMode::Empty) => true,
FormatElement::ExpandParent => true,
FormatElement::Line(mode) => mode.will_break(),
_ => false,
};
@@ -134,6 +134,8 @@ pub enum LineMode {
Hard,
/// See [crate::builders::empty_line] for documentation.
Empty,
/// See [crate::builders::literal_line_break] for documentation.
Literal,
}
impl LineMode {
@@ -142,7 +144,7 @@ impl LineMode {
}
pub const fn will_break(self) -> bool {
matches!(self, LineMode::Hard | LineMode::Empty)
matches!(self, LineMode::Hard | LineMode::Empty | LineMode::Literal)
}
}
@@ -64,6 +64,12 @@ pub enum Tag {
/// See [crate::builders::labelled] for documentation.
StartLabelled(LabelId),
EndLabelled,
/// Marks the current indention as the root that [LineMode::Literal](super::LineMode::Literal)
/// line breaks and [DedentMode::Root] dedents return to.
/// See [crate::builders::mark_as_root] for documentation.
StartMarkAsRoot,
EndMarkAsRoot,
}
impl Tag {
@@ -81,6 +87,7 @@ impl Tag {
| Tag::StartEntry
| Tag::StartLineSuffix
| Tag::StartLabelled(_)
| Tag::StartMarkAsRoot
)
}
@@ -92,9 +99,9 @@ impl Tag {
pub const fn kind(&self) -> TagKind {
use Tag::{
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
};
match self {
@@ -108,6 +115,7 @@ impl Tag {
StartEntry | EndEntry => TagKind::Entry,
StartLineSuffix | EndLineSuffix => TagKind::LineSuffix,
StartLabelled(_) | EndLabelled => TagKind::Labelled,
StartMarkAsRoot | EndMarkAsRoot => TagKind::MarkAsRoot,
}
}
}
@@ -128,6 +136,7 @@ pub enum TagKind {
LineSuffix,
Labelled,
TailwindClass,
MarkAsRoot,
}
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)]
@@ -202,6 +202,8 @@ pub(super) trait IndentStack {
fn current_stack_mut(&mut self) -> &mut Self::Stack;
fn history_stack_mut(&mut self) -> &mut Self::HistoryStack;
fn root_stack(&self) -> &Self::Stack;
fn root_stack_mut(&mut self) -> &mut Self::Stack;
fn start_dedent(&mut self) {
if let Some(indent) = self.current_stack_mut().pop() {
@@ -219,8 +221,23 @@ pub(super) trait IndentStack {
fn indention(&self) -> Indention {
self.current_stack().top().copied().unwrap_or_default()
}
/// `Tag::StartMarkAsRoot`: the current indention becomes the root that
/// [Self::reset_indent] and literal line breaks return to.
fn mark_root(&mut self) {
let root = self.indention();
self.root_stack_mut().push(root);
}
/// `Tag::EndMarkAsRoot`.
fn end_mark_root(&mut self) {
self.root_stack_mut().pop();
}
/// The marked root indention; zero-indent when no `mark_as_root` is active.
fn root(&self) -> Indention {
self.root_stack().top().copied().unwrap_or_default()
}
fn reset_indent(&mut self) {
self.current_stack_mut().push(Indention::default());
let root = self.root();
self.current_stack_mut().push(root);
}
fn indent(&mut self, indent_style: IndentStyle) {
let next_indent = self.indention().increment_level(indent_style);
@@ -238,6 +255,7 @@ pub(super) struct PrintIndentStack {
indentions: Vec<Indention>,
history_indentions: Vec<Indention>,
suffix_indentions: Vec<Indention>,
root_indentions: Vec<Indention>,
}
impl PrintIndentStack {
@@ -246,6 +264,7 @@ impl PrintIndentStack {
indentions: vec![indention],
history_indentions: Vec::new(),
suffix_indentions: Vec::new(),
root_indentions: Vec::new(),
}
}
@@ -268,6 +287,14 @@ impl IndentStack for PrintIndentStack {
fn history_stack_mut(&mut self) -> &mut Self::HistoryStack {
&mut self.history_indentions
}
fn root_stack(&self) -> &Self::Stack {
&self.root_indentions
}
fn root_stack_mut(&mut self) -> &mut Self::Stack {
&mut self.root_indentions
}
}
impl SuffixStack for PrintIndentStack {
type SuffixStack = Vec<Indention>;
@@ -282,6 +309,7 @@ impl SuffixStack for PrintIndentStack {
pub(super) struct FitsIndentStack<'print> {
indentions: StackedStack<'print, Indention>,
history_indentions: StackedStack<'print, Indention>,
root_indentions: StackedStack<'print, Indention>,
}
impl<'print> FitsIndentStack<'print> {
@@ -289,16 +317,23 @@ impl<'print> FitsIndentStack<'print> {
print: &'print PrintIndentStack,
saved_indent_stack: Vec<Indention>,
saved_history_indent_stack: Vec<Indention>,
saved_root_indent_stack: Vec<Indention>,
) -> Self {
let indentions = StackedStack::with_vec(&print.indentions, saved_indent_stack);
let history_indentions =
StackedStack::with_vec(&print.history_indentions, saved_history_indent_stack);
let root_indentions =
StackedStack::with_vec(&print.root_indentions, saved_root_indent_stack);
Self { indentions, history_indentions }
Self { indentions, history_indentions, root_indentions }
}
pub(super) fn finish(self) -> (Vec<Indention>, Vec<Indention>) {
(self.indentions.into_vec(), self.history_indentions.into_vec())
pub(super) fn finish(self) -> (Vec<Indention>, Vec<Indention>, Vec<Indention>) {
(
self.indentions.into_vec(),
self.history_indentions.into_vec(),
self.root_indentions.into_vec(),
)
}
}
@@ -317,4 +352,12 @@ impl<'a> IndentStack for FitsIndentStack<'a> {
fn history_stack_mut(&mut self) -> &mut Self::HistoryStack {
&mut self.history_indentions
}
fn root_stack(&self) -> &Self::Stack {
&self.root_indentions
}
fn root_stack_mut(&mut self) -> &mut Self::Stack {
&mut self.root_indentions
}
}
+258 -29
View File
@@ -138,9 +138,9 @@ impl<'a> Printer<'a> {
) -> PrintResult<()> {
use Tag::{
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
};
let args = stack.top();
@@ -164,7 +164,7 @@ impl<'a> Printer<'a> {
}
return Ok(());
}
LineMode::Hard | LineMode::Empty => {
LineMode::Hard | LineMode::Empty | LineMode::Literal => {
self.state.measured_group_fits = false;
}
}
@@ -175,6 +175,20 @@ impl<'a> Printer<'a> {
return Ok(());
}
if line_mode == &LineMode::Literal {
// Prettier's literal line (`doc.literal` in `printDocToString`):
// pending indention/space materialize as-is (a literal line never trims),
// the newline always prints (even on an empty line),
// and the next line starts at the marked root indention,
// written eagerly (Prettier writes `indent.root.value` together with the newline)
// so that a following hard line still sees a non-empty line to trim and break.
self.flush_pending_indention_and_space();
self.print_char('\n');
self.print_indention(indent_stack.root());
self.state.has_empty_line = false;
return Ok(());
}
// Only print a newline if the current line isn't already empty
if self.state.line_width > 0 {
self.state.buffer.trim_trailing_ascii_whitespace();
@@ -268,6 +282,16 @@ impl<'a> Printer<'a> {
stack.push(TagKind::Align, args);
}
FormatElement::Tag(StartMarkAsRoot) => {
indent_stack.mark_root();
stack.push(TagKind::MarkAsRoot, args);
}
FormatElement::Tag(tag @ EndMarkAsRoot) => {
stack.pop(tag.kind())?;
indent_stack.end_mark_root();
}
FormatElement::Tag(StartConditionalContent(Condition { mode, group_id })) => {
let group_mode = match group_id {
None => args.mode(),
@@ -666,25 +690,33 @@ impl<'a> Printer<'a> {
invalid_end_tag(TagKind::Entry, stack.top_kind())
}
fn print_text(&mut self, text: Text) {
if !self.state.pending_indent.is_empty() {
let indent = std::mem::take(&mut self.state.pending_indent);
let level = indent.level() as usize;
self.state.buffer.print_indent(level);
self.state.line_width += level * self.options.indent_width().value() as usize;
let align_count = indent.align() as usize;
for _ in 0..align_count {
// SAFETY: `' '` is an valid ASCII character
unsafe {
self.state.buffer.print_byte_unchecked(b' ');
}
}
self.state.line_width += align_count;
/// Prints `indention` (indent levels + align spaces) and adds its width to the current line.
#[inline]
fn print_indention(&mut self, indention: Indention) {
if indention.is_empty() {
return;
}
// Print pending spaces
let level = indention.level() as usize;
self.state.buffer.print_indent(level);
self.state.line_width += level * self.options.indent_width().value() as usize;
let align_count = indention.align() as usize;
for _ in 0..align_count {
// SAFETY: `' '` is an valid ASCII character
unsafe {
self.state.buffer.print_byte_unchecked(b' ');
}
}
self.state.line_width += align_count;
}
/// Materializes the pending indention and pending space before printing visible content.
#[inline]
fn flush_pending_indention_and_space(&mut self) {
let pending_indent = std::mem::take(&mut self.state.pending_indent);
self.print_indention(pending_indent);
if self.state.pending_space {
// SAFETY: `' '` is an valid ASCII character
unsafe {
@@ -693,6 +725,10 @@ impl<'a> Printer<'a> {
self.state.pending_space = false;
self.state.line_width += 1;
}
}
fn print_text(&mut self, text: Text) {
self.flush_pending_indention_and_space();
match text {
Text::Token(text) => {
@@ -782,6 +818,7 @@ struct PrinterState<'a> {
fits_stack: Vec<StackFrame>,
fits_indent_stack: Vec<Indention>,
fits_stack_tem_indent: Vec<Indention>,
fits_root_indent_stack: Vec<Indention>,
fits_queue: Vec<&'a [FormatElement<'a>]>,
/// Sorted Tailwind CSS classes for lookup during printing
sorted_tailwind_classes: &'a [String],
@@ -938,16 +975,22 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
let saved_queue = std::mem::take(&mut printer.state.fits_queue);
let saved_indent_stack = std::mem::take(&mut printer.state.fits_indent_stack);
let saved_stack_tem_indent = std::mem::take(&mut printer.state.fits_stack_tem_indent);
let saved_root_indent_stack = std::mem::take(&mut printer.state.fits_root_indent_stack);
debug_assert!(saved_stack.is_empty());
debug_assert!(saved_queue.is_empty());
debug_assert!(saved_indent_stack.is_empty());
debug_assert!(saved_stack_tem_indent.is_empty());
debug_assert!(saved_root_indent_stack.is_empty());
let fits_queue = FitsQueue::new(print_queue, saved_queue);
let fits_stack = FitsCallStack::new(print_stack, saved_stack);
let fits_indent_stack =
FitsIndentStack::new(print_indent_stack, saved_indent_stack, saved_stack_tem_indent);
let fits_indent_stack = FitsIndentStack::new(
print_indent_stack,
saved_indent_stack,
saved_stack_tem_indent,
saved_root_indent_stack,
);
let fits_state = FitsState {
pending_indent: printer.state.pending_indent,
@@ -1034,9 +1077,9 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
fn fits_element(&mut self, element: &'a FormatElement) -> PrintResult<Fits> {
use Tag::{
EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign,
EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix,
StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
};
let args = self.stack.top();
@@ -1054,7 +1097,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
self.state.pending_space = true;
}
LineMode::Soft => {}
LineMode::Hard | LineMode::Empty => {
LineMode::Hard | LineMode::Empty | LineMode::Literal => {
// Even in flat mode, content that _directly_ contains a hard or empty
// line is considered to fit when a hard break is reached, since that
// break is always going to exist, regardless of the print mode.
@@ -1245,6 +1288,14 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
self.stack.pop(tag.kind())?;
self.indent_stack.pop();
}
FormatElement::Tag(StartMarkAsRoot) => {
self.indent_stack.mark_root();
self.stack.push(TagKind::MarkAsRoot, args);
}
FormatElement::Tag(tag @ EndMarkAsRoot) => {
self.stack.pop(tag.kind())?;
self.indent_stack.end_mark_root();
}
FormatElement::Tag(tag @ EndDedent(mode)) => {
if matches!(mode, DedentMode::Level) {
self.indent_stack.end_dedent();
@@ -1312,11 +1363,14 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
stack.clear();
self.printer.state.fits_stack = stack;
let (mut indent_stack, mut history_stack) = self.indent_stack.finish();
let (mut indent_stack, mut history_stack, mut root_indent_stack) =
self.indent_stack.finish();
indent_stack.clear();
self.printer.state.fits_indent_stack = indent_stack;
history_stack.clear();
self.printer.state.fits_stack_tem_indent = history_stack;
root_indent_stack.clear();
self.printer.state.fits_root_indent_stack = root_indent_stack;
}
fn options(&self) -> &PrinterOptions {
@@ -1404,9 +1458,10 @@ mod tests {
Argument, Arguments, Buffer, Document, Format, FormatState, IndentStyle, LineEnding,
Printed, Printer, PrinterOptions, SimpleFormatContext, VecBuffer,
builders::{
block_indent, empty_line, group, hard_line_break, if_group_breaks,
if_group_fits_on_line, line_suffix, soft_block_indent, soft_line_break,
soft_line_break_or_space, space, text, token,
align, block_indent, dedent_to_root, empty_line, group, hard_line_break,
if_group_breaks, if_group_fits_on_line, indent, line_suffix, literal_line_break,
mark_as_root, soft_block_indent, soft_line_break, soft_line_break_or_space, space,
text, token,
},
format_args,
printer::PrintWidth,
@@ -1499,6 +1554,180 @@ a",
);
}
// The expected outputs of the `literal_line_break` / `mark_as_root` tests below are
// verified against Prettier's doc printer (`printDocToString`) with the equivalent
// `literallineWithoutBreakParent` / `markAsRoot` / `dedentToRoot` documents.
#[test]
fn literal_line_break_prints_at_column_zero_without_trimming() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(
token("a"),
indent(&format_args!(
hard_line_break(),
token("b "),
literal_line_break(),
token("c")
)),
hard_line_break(),
token("d")
),
);
// The trailing space after `b` is preserved and `c` starts at column 0.
assert_eq!("a\n b \nc\nd", result.as_code());
}
#[test]
fn literal_line_break_always_prints_the_newline() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(token("a"), literal_line_break(), literal_line_break(), token("b")),
);
// Unlike `hard_line_break`, consecutive literal lines are not collapsed.
assert_eq!("a\n\nb", result.as_code());
}
#[test]
fn literal_line_break_returns_to_marked_root() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(
token("k:"),
align(
2,
&format_args!(
hard_line_break(),
token("line1"),
mark_as_root(&literal_line_break()),
token("line2")
)
),
hard_line_break(),
token("z")
),
);
// Without `mark_as_root`, `line2` would start at column 0.
assert_eq!("k:\n line1\n line2\nz", result.as_code());
}
#[test]
fn literal_line_break_expands_the_enclosing_group() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&group(&format_args!(
token("<"),
soft_line_break(),
token("a"),
literal_line_break(),
token("b"),
soft_line_break(),
token(">")
)),
);
assert_eq!("<\na\nb\n>", result.as_code());
}
#[test]
fn literal_line_break_flushes_line_suffixes() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(
token("a"),
line_suffix(&token(" // c")),
literal_line_break(),
token("b")
),
);
assert_eq!("a // c\nb", result.as_code());
}
#[test]
fn dedent_to_root_returns_to_marked_root() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(
token("k:"),
align(
2,
&mark_as_root(&format_args!(
hard_line_break(),
token("a"),
indent(&format_args!(
hard_line_break(),
token("deep"),
dedent_to_root(&format_args!(hard_line_break(), token("back")))
))
))
)
),
);
// `dedent_to_root` returns to the `mark_as_root` indention (column 2), not column 0.
assert_eq!("k:\n a\n deep\n back", result.as_code());
}
/// Composes the document shape of Prettier's YAML block scalar printing
/// (`language-yaml/print/block.js`): content indented via `align`,
/// line boundaries as `mark_as_root(&literal_line_break())` so continuation lines
/// keep the block's base indention, an empty content line as a hard line,
/// and the keep-chomping (`|+`) trailing newline as `dedent_to_root(&literal_line_break())`.
#[test]
fn yaml_block_scalar_shaped_document() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(
token("k: |+"),
align(
2,
&format_args!(
hard_line_break(),
token("hello world"),
mark_as_root(&literal_line_break()),
hard_line_break(),
token("next"),
dedent_to_root(&literal_line_break())
)
)
),
);
assert_eq!("k: |+\n hello world\n\n next\n", result.as_code());
}
/// Known divergence from Prettier: a hard line directly after a column-0 literal line
/// is absorbed by the "only print a newline if the line isn't already empty" rule
/// (Prettier prints both newlines). When a consumer needs the extra structural newline
/// after a trailing literal line (e.g. after a keep-chomping YAML block scalar),
/// use `empty_line()` which prints exactly one newline in this state.
#[test]
fn hard_line_after_column_zero_literal_line_is_absorbed() {
let allocator = Allocator::default();
let result = format_simple(
&allocator,
&format_args!(token("a"), literal_line_break(), hard_line_break(), token("b")),
);
assert_eq!("a\nb", result.as_code());
let result = format_simple(
&allocator,
&format_args!(token("a"), literal_line_break(), empty_line(), token("b")),
);
assert_eq!("a\n\nb", result.as_code());
}
#[test]
fn it_converts_line_endings() {
let allocator = Allocator::default();