oxc/crates/oxc_prettier/src/doc.rs

342 lines
11 KiB
Rust

//! Prettier IR
//!
//! References:
//! * <https://github.com/prettier/prettier/blob/main/commands.md>
use oxc_allocator::{Allocator, Box, String, Vec};
use std::fmt;
use crate::{array, line, ss, GroupId};
#[derive(Debug)]
pub enum Doc<'a> {
Str(&'a str),
// perf: can we use &[Doc] here?
Array(Vec<'a, Doc<'a>>),
/// Increase the level of indentation.
Indent(Vec<'a, Doc<'a>>),
IndentIfBreak(IndentIfBreak<'a>),
/// Mark a group of items which the printer should try to fit on one line.
/// This is the basic command to tell the printer when to break.
/// Groups are usually nested, and the printer will try to fit everything on one line,
/// but if it doesn't fit it will break the outermost group first and try again.
/// It will continue breaking groups until everything fits (or there are no more groups to break).
Group(Group<'a>),
/// Specify a line break.
/// If an expression fits on one line, the line break will be replaced with a space.
/// Line breaks always indent the next line with the current level of indentation.
Line(Line),
/// This is used to implement trailing comments.
/// It's not practical to constantly check where the line ends to avoid accidentally printing some code at the end of a comment.
/// `lineSuffix` buffers docs passed to it and flushes them before any new line.
LineSuffix(Vec<'a, Doc<'a>>),
/// Print something if the current `group` or the current element of `fill` breaks and something else if it doesn't.
IfBreak(IfBreak<'a>),
/// This is an alternative type of group which behaves like text layout:
/// it's going to add a break whenever the next element doesn't fit in the line anymore.
/// The difference with `group` is that it's not going to break all the separators, just the ones that are at the end of lines.
Fill(Fill<'a>),
/// Include this anywhere to force all parent groups to break.
BreakParent,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Line {
pub hard: bool,
pub soft: bool,
pub literal: bool,
}
impl Line {
/// Specify a line break.
/// The difference from line is that if the expression fits on one line, it will be replaced with nothing.
pub fn softline() -> Self {
Self { soft: true, ..Self::default() }
}
/// Specify a line break that is **always** included in the output,
/// no matter if the expression fits on one line or not.
pub fn hardline() -> Self {
Self { hard: true, ..Self::default() }
}
pub fn literal_line() -> Self {
Self { literal: true, ..Self::default() }
}
pub fn hardline_without_break_parent() -> Self {
Self { hard: true, ..Self::default() }
}
pub fn literal_line_without_break_parent() -> Self {
Self { hard: true, literal: true, ..Self::default() }
}
}
#[derive(Debug)]
pub struct Group<'a> {
pub contents: Vec<'a, Doc<'a>>,
pub should_break: bool,
pub expanded_states: Option<Vec<'a, Doc<'a>>>,
pub id: Option<GroupId>,
}
impl<'a> Group<'a> {
pub fn new(contents: Vec<'a, Doc<'a>>) -> Self {
Self { contents, should_break: false, id: None, expanded_states: None }
}
pub fn new_conditional_group(
contents: Vec<'a, Doc<'a>>,
expanded_states: Vec<'a, Doc<'a>>,
) -> Self {
Self { contents, should_break: false, id: None, expanded_states: Some(expanded_states) }
}
pub fn with_break(mut self, yes: bool) -> Self {
self.should_break = yes;
self
}
pub fn with_id(mut self, id: GroupId) -> Self {
self.id = Some(id);
self
}
}
#[derive(Debug)]
pub struct IndentIfBreak<'a> {
pub contents: Vec<'a, Doc<'a>>,
pub group_id: Option<GroupId>,
}
impl<'a> IndentIfBreak<'a> {
pub fn new(contents: Vec<'a, Doc<'a>>) -> Self {
Self { contents, group_id: None }
}
pub fn with_id(mut self, id: GroupId) -> Self {
self.group_id = Some(id);
self
}
}
#[derive(Debug)]
pub struct Fill<'a> {
pub parts: Vec<'a, Doc<'a>>,
}
impl<'a> Fill<'a> {
pub fn new(docs: Vec<'a, Doc<'a>>) -> Self {
Self { parts: docs }
}
pub fn drain_out_pair(&mut self) -> (Option<Doc<'a>>, Option<Doc<'a>>) {
let content = if self.parts.len() > 0 { Some(self.parts.remove(0)) } else { None };
let whitespace = if self.parts.len() > 0 { Some(self.parts.remove(0)) } else { None };
(content, whitespace)
}
pub fn dequeue(&mut self) -> Option<Doc<'a>> {
if self.parts.len() > 0 {
Some(self.parts.remove(0))
} else {
None
}
}
pub fn enqueue(&mut self, doc: Doc<'a>) {
self.parts.insert(0, doc);
}
pub fn parts(&self) -> &[Doc<'a>] {
&self.parts
}
pub fn take_parts(self) -> Vec<'a, Doc<'a>> {
self.parts
}
}
#[derive(Debug)]
pub struct IfBreak<'a> {
pub break_contents: Box<'a, Doc<'a>>,
pub flat_content: Box<'a, Doc<'a>>,
pub group_id: Option<GroupId>,
}
#[derive(Clone, Copy)]
#[allow(unused)]
pub enum Separator {
Softline,
Hardline,
CommaLine, // [",", line]
}
/// Doc Builder
pub trait DocBuilder<'a> {
fn allocator(&self) -> &'a Allocator;
#[inline]
fn vec<T>(&self) -> Vec<'a, T> {
Vec::new_in(self.allocator())
}
fn vec_single<T>(&self, value: T) -> Vec<'a, T> {
let mut vec = Vec::with_capacity_in(1, self.allocator());
vec.push(value);
vec
}
#[inline]
fn str(&self, s: &str) -> Doc<'a> {
Doc::Str(String::from_str_in(s, self.allocator()).into_bump_str())
}
#[inline]
fn boxed(&self, doc: Doc<'a>) -> Box<'a, Doc<'a>> {
Box(self.allocator().alloc(doc))
}
#[allow(unused)]
fn join(&self, separator: Separator, docs: std::vec::Vec<Doc<'a>>) -> Vec<'a, Doc<'a>> {
let mut parts = self.vec();
for (i, doc) in docs.into_iter().enumerate() {
if i != 0 {
parts.push(match separator {
Separator::Softline => Doc::Line(Line::softline()),
Separator::Hardline => Doc::Line(Line::hardline()),
Separator::CommaLine => array![self, ss!(","), line!()],
});
}
parts.push(doc);
}
parts
}
}
impl<'a> fmt::Display for Doc<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", print_doc_to_debug(self))
}
}
// https://github.com/prettier/prettier/blob/main/src/document/debug.js
fn print_doc_to_debug(doc: &Doc<'_>) -> std::string::String {
use std::string::String;
let mut string = String::new();
match doc {
Doc::Str(s) => {
string.push('"');
string.push_str(s);
string.push('"');
}
Doc::Array(docs) => {
string.push_str("[\n");
for (idx, doc) in docs.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != docs.len() - 1 {
string.push_str(", ");
}
}
string.push_str("]\n");
}
Doc::Indent(contents) => {
string.push_str("indent([");
for (idx, doc) in contents.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != contents.len() - 1 {
string.push_str(", ");
}
}
string.push_str("])");
}
Doc::IndentIfBreak(indent_if_break) => {
string.push_str("indentIfBreak(");
string.push_str("[\n");
for (idx, doc) in indent_if_break.contents.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != indent_if_break.contents.len() - 1 {
string.push_str(", ");
}
}
if let Some(id) = indent_if_break.group_id {
string.push_str(&format!(", {{id: {id}}}"));
}
string.push_str("])");
}
Doc::Group(group) => {
if group.expanded_states.is_some() {
string.push_str("conditionalGroup([\n");
}
string.push_str("group([\n");
for (idx, doc) in group.contents.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != group.contents.len() - 1 {
string.push_str(", ");
}
}
string.push_str("], { shouldBreak: ");
string.push_str(&group.should_break.to_string());
if let Some(id) = group.id {
string.push_str(&format!(", id: {id}"));
}
string.push_str(" })");
if let Some(expanded_states) = &group.expanded_states {
string.push_str(",\n");
for (idx, doc) in expanded_states.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != expanded_states.len() - 1 {
string.push_str(", ");
}
}
string.push_str("])");
}
}
Doc::Line(Line { soft, hard, .. }) => {
if *soft {
string.push_str("softline");
} else if *hard {
string.push_str("hardline");
} else {
string.push_str("line");
}
}
Doc::IfBreak(if_break) => {
string.push_str(&format!(
"ifBreak({}, {}",
print_doc_to_debug(&if_break.break_contents),
print_doc_to_debug(&if_break.flat_content)
));
if let Some(group_id) = if_break.group_id {
string.push_str(&format!(", {{ groupId: {group_id} }}"));
}
string.push(')');
}
Doc::Fill(fill) => {
string.push_str("fill([\n");
let parts = fill.parts();
for (idx, doc) in parts.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != parts.len() - 1 {
string.push_str(", ");
}
}
string.push_str("])");
}
Doc::LineSuffix(docs) => {
string.push_str("lineSuffix(");
for (idx, doc) in docs.iter().enumerate() {
string.push_str(&print_doc_to_debug(doc));
if idx != docs.len() - 1 {
string.push_str(", ");
}
}
string.push(')');
}
Doc::BreakParent => {
string.push_str("BreakParent");
}
}
string
}