Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for external formatters #683

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/format_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::swc::ensure_no_specific_syntax_errors;

use super::configuration::Configuration;
use super::generation::generate;
pub use super::generation::ExternalFormatter;
use super::swc::parse_swc_ast;

/// Formats a file.
Expand Down Expand Up @@ -40,14 +41,34 @@ use super::swc::parse_swc_ast;
/// }
/// ```
pub fn format_text(file_path: &Path, file_extension: Option<&str>, file_text: String, config: &Configuration) -> Result<Option<String>> {
format_text_inner(file_path, file_extension, file_text, config, None)
}

pub fn format_text_with_external_formatter(
file_path: &Path,
file_extension: Option<&str>,
file_text: String,
config: &Configuration,
external_formatter: ExternalFormatter,
) -> Result<Option<String>> {
format_text_inner(file_path, file_extension, file_text, config, Some(external_formatter))
}

fn format_text_inner(
file_path: &Path,
file_extension: Option<&str>,
file_text: String,
config: &Configuration,
external_formatter: Option<ExternalFormatter>,
) -> Result<Option<String>> {
if super::utils::file_text_has_ignore_comment(&file_text, &config.ignore_file_comment_text) {
Ok(None)
} else {
let had_bom = file_text.starts_with("\u{FEFF}");
let file_text = if had_bom { file_text[3..].to_string() } else { file_text };
let file_text: Arc<str> = file_text.into();
let parsed_source = parse_swc_ast(file_path, file_extension, file_text)?;
match inner_format(&parsed_source, config)? {
match inner_format(&parsed_source, config, external_formatter)? {
Some(new_text) => Ok(Some(new_text)),
None => {
if had_bom {
Expand All @@ -66,15 +87,16 @@ pub fn format_parsed_source(source: &ParsedSource, config: &Configuration) -> Re
Ok(None)
} else {
ensure_no_specific_syntax_errors(source)?;
inner_format(source, config)
// TODO(bartlomieju): support external formatter
inner_format(source, config, None)
}
}

fn inner_format(parsed_source: &ParsedSource, config: &Configuration) -> Result<Option<String>> {
fn inner_format(parsed_source: &ParsedSource, config: &Configuration, external_formatter: Option<ExternalFormatter>) -> Result<Option<String>> {
let result = dprint_core::formatting::format(
|| {
#[allow(clippy::let_and_return)]
let print_items = generate(parsed_source, config);
let print_items = generate(parsed_source, config, external_formatter);
// println!("{}", print_items.get_as_text());
print_items
},
Expand All @@ -91,7 +113,7 @@ fn inner_format(parsed_source: &ParsedSource, config: &Configuration) -> Result<
pub fn trace_file(file_path: &Path, file_text: &str, config: &Configuration) -> dprint_core::formatting::TracingResult {
let parsed_source = parse_swc_ast(file_path, None, file_text.into()).unwrap();
ensure_no_specific_syntax_errors(&parsed_source).unwrap();
dprint_core::formatting::trace_printing(|| generate(&parsed_source, config), config_to_print_options(file_text, config))
dprint_core::formatting::trace_printing(|| generate(&parsed_source, config, None), config_to_print_options(file_text, config))
}

fn config_to_print_options(file_text: &str, config: &Configuration) -> PrintOptions {
Expand All @@ -111,7 +133,9 @@ mod test {
fn strips_bom() {
for input_text in ["\u{FEFF}const t = 5;\n", "\u{FEFF}const t = 5;"] {
let config = crate::configuration::ConfigurationBuilder::new().build();
let result = format_text(&std::path::PathBuf::from("test.ts"), None, input_text.into(), &config).unwrap().unwrap();
let result = format_text(&std::path::PathBuf::from("test.ts"), None, input_text.into(), &config)
.unwrap()
.unwrap();
assert_eq!(result, "const t = 5;\n");
}
}
Expand Down
40 changes: 39 additions & 1 deletion src/generation/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,41 @@ use super::*;
use crate::configuration::*;
use crate::utils::Stack;

/// A callback that will be called when encountering certain tagged templates.
///
/// Currently supports `css`, `html` and `sql` tagged templated.
///
/// Examples:
/// ```ignore
/// const styles = css`color: red;`;
///
/// const markup = html`<html>
/// <body>
/// <h1>Hello!<h1>
/// </body>
/// </html>`;
///
/// const query = sql`
/// SELECT
/// *
/// FROM
/// users
/// WHERE
/// active IS TRUE;
/// ```
///
/// External formatter should return `None` if it doesn't understand given `MediaType`, in such
/// cases the templates will be left as they are.
///
/// Only templates with no interpolation are supported.
pub type ExternalFormatter = Box<dyn Fn(MediaType, String) -> Option<String>>;

pub struct Context<'a> {
pub media_type: MediaType,
pub program: Program<'a>,
pub config: &'a Configuration,
pub comments: CommentTracker<'a>,
pub external_formatter: Option<ExternalFormatter>,
pub token_finder: TokenFinder<'a>,
pub current_node: Node<'a>,
pub parent_stack: Stack<Node<'a>>,
Expand All @@ -43,12 +73,20 @@ pub struct Context<'a> {
}

impl<'a> Context<'a> {
pub fn new(media_type: MediaType, tokens: &'a [TokenAndSpan], current_node: Node<'a>, program: Program<'a>, config: &'a Configuration) -> Context<'a> {
pub fn new(
media_type: MediaType,
tokens: &'a [TokenAndSpan],
current_node: Node<'a>,
program: Program<'a>,
config: &'a Configuration,
external_formatter: Option<ExternalFormatter>,
) -> Context<'a> {
Context {
media_type,
program,
config,
comments: CommentTracker::new(program, tokens),
external_formatter,
token_finder: TokenFinder::new(program),
current_node,
parent_stack: Default::default(),
Expand Down
52 changes: 50 additions & 2 deletions src/generation/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ use super::*;
use crate::configuration::*;
use crate::utils;

pub fn generate(parsed_source: &ParsedSource, config: &Configuration) -> PrintItems {
pub fn generate(parsed_source: &ParsedSource, config: &Configuration, external_formatter: Option<ExternalFormatter>) -> PrintItems {
// eprintln!("Leading: {:?}", parsed_source.comments().leading_map());
// eprintln!("Trailing: {:?}", parsed_source.comments().trailing_map());

parsed_source.with_view(|program| {
let program_node = program.into();
let mut context = Context::new(parsed_source.media_type(), parsed_source.tokens(), program_node, program, config);
let mut context = Context::new(
parsed_source.media_type(),
parsed_source.tokens(),
program_node,
program,
config,
external_formatter,
);
let mut items = gen_node(program_node, &mut context);
items.push_condition(if_true(
"endOfFileNewLine",
Expand Down Expand Up @@ -2996,6 +3003,40 @@ fn gen_spread_element<'a>(node: &SpreadElement<'a>, context: &mut Context<'a>) -
items
}

fn maybe_format_tagged_tpl_with_external_formatter<'a>(node: &TaggedTpl<'a>, external_formatter: &ExternalFormatter) -> Option<PrintItems> {
let Expr::Ident(ident) = node.tag else {
return None;
};

// TODO(bartlomieju): support `html` and `sql` template tags
let media_type = match ident.sym().as_str() {
"css" => MediaType::Css,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ => return None,
};

// TODO(bartlomieju): support formatting when there are multiplie quasis, but first need to figure
// out how to put valid "placeholders" for different languages
if node.tpl.quasis.len() != 1 {
return None;
}

let quasi = node.tpl.quasis[0];

let Some(formatted_tpl) = external_formatter(media_type, quasi.raw().to_string()) else {
return None;
};

let mut items = PrintItems::new();
// TODO(bartlomieju): might not be fully correct, need to better handle trailing newlines?
items.push_string("`".to_string());
for line in formatted_tpl.lines() {
items.push_string(line.to_string());
items.push_signal(Signal::NewLine);
}
items.push_string("`".to_string());
Some(items)
}

fn gen_tagged_tpl<'a>(node: &TaggedTpl<'a>, context: &mut Context<'a>) -> PrintItems {
let use_space = context.config.tagged_template_space_before_literal;
let mut items = gen_node(node.tag.into(), context);
Expand All @@ -3012,6 +3053,13 @@ fn gen_tagged_tpl<'a>(node: &TaggedTpl<'a>, context: &mut Context<'a>) -> PrintI
items.extend(generated_between_comments);
}

if let Some(external_formatter) = context.external_formatter.as_ref() {
if let Some(formatted_tpl) = maybe_format_tagged_tpl_with_external_formatter(node, external_formatter) {
items.push_condition(conditions::indent_if_start_of_line(formatted_tpl));
return items;
}
}

items.push_condition(conditions::indent_if_start_of_line(gen_node(node.tpl.into(), context)));
items
}
Expand Down
1 change: 1 addition & 0 deletions src/generation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ use context::*;
use generate_types::*;
use tokens::*;

pub use context::ExternalFormatter;
pub use generate::generate;
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ mod utils;

pub use format_text::format_parsed_source;
pub use format_text::format_text;
pub use format_text::format_text_with_external_formatter;
pub use format_text::ExternalFormatter;

#[cfg(feature = "tracing")]
pub use format_text::trace_file;
Expand Down
22 changes: 21 additions & 1 deletion tests/spec_test.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
use std::path::PathBuf;
use std::sync::Arc;

use deno_ast::MediaType;
use dprint_core::configuration::*;
use dprint_development::*;
use dprint_plugin_typescript::configuration::*;
use dprint_plugin_typescript::*;

fn external_formatter(media_type: MediaType, text: String) -> Option<String> {
assert_eq!(media_type, MediaType::Css);
// Put each rule on a separate line.
Some(
text
.split(';')
.filter_map(|val| {
let val = val.trim();
if val.is_empty() {
None
} else {
Some(format!("{};", val))
}
})
.collect::<Vec<_>>()
.join("\n"),
)
}

fn main() {
//debug_here!();
let global_config = GlobalConfiguration {
Expand All @@ -28,7 +48,7 @@ fn main() {
let config_result = resolve_config(spec_config, &global_config);
ensure_no_diagnostics(&config_result.diagnostics);

format_text(file_name, None, file_text.into(), &config_result.config)
format_text_with_external_formatter(file_name, None, file_text.into(), &config_result.config, Box::new(external_formatter))
})
},
Arc::new(move |_file_name, _file_text, _spec_config| {
Expand Down
21 changes: 21 additions & 0 deletions tests/specs/externalFormatter/css.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- file.js --
== should format CSS tagged template with a single quasi ==
import css from 'styled-components'

const Header = css`
height: 40px; padding: 0 15px;
display: flex; align-items: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
`;

[expect]
import css from "styled-components";

const Header = css`height: 40px;
padding: 0 15px;
display: flex;
align-items: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
`;
16 changes: 16 additions & 0 deletions tests/specs/externalFormatter/css_many_quasis.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- file.js --
== should not format CSS tagged template with multiple quasis ==
import css from 'styled-components'

const height = 50;
const Header = css`
height: ${height}px;
`;

[expect]
import css from "styled-components";

const height = 50;
const Header = css`
height: ${height}px;
`;
6 changes: 6 additions & 0 deletions tests/specs/externalFormatter/html.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- file.js --
== should not format HTML tagged template ==
const htmlString = html`<body>Hello there!</body>`;

[expect]
const htmlString = html`<body>Hello there!</body>`;
6 changes: 6 additions & 0 deletions tests/specs/externalFormatter/sql.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- file.js --
== should not format SQL tagged template ==
const query = sql`SELECT * FROM users;`;

[expect]
const query = sql`SELECT * FROM users;`;