mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-06 17:34:25 +00:00
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:
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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user