mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
refactor(formatter_core): make embedded dispatch parent-to-child only (#28159)
Remove `child_context`, every usecases are handled only parent -> child direction.
This commit is contained in:
1 parent
014e3d2676
commit
f525b34636
10 files changed
+112
-165
No files matched your search
@@ -226,17 +226,17 @@ impl ResolvedDispatchConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fallback invoked for [`Route::Prettier`] languages.
|
/// Fallback invoked for [`Route::Prettier`] languages.
|
||||||
/// Same shape as `FormatDispatcher` minus the request envelope
|
/// Same shape as `FormatDispatcher`, plus the routed language
|
||||||
/// (the Doc path consumes neither `input_kind` nor `parent_context` today;
|
/// (the Doc path consumes `parent_context` only for html(angular)-in-js's [`oxc_formatter::HtmlInJsTemplate`], not `input_kind`;
|
||||||
/// re-examine if it ever serves envelope-bearing inputs).
|
/// re-examine if it ever serves envelope-bearing inputs).
|
||||||
///
|
///
|
||||||
/// Assembled only in napi builds ([`super::prettier_doc`]);
|
/// Assembled only in napi builds ([`super::prettier_doc`]);
|
||||||
/// the pure Rust build passes `None` and these languages are deliberately preserved as-is.
|
/// the pure Rust build passes `None` and these languages are deliberately preserved as-is.
|
||||||
pub type PrettierDocFallback = Arc<
|
pub type PrettierDocFallback = Arc<
|
||||||
dyn for<'a> Fn(
|
dyn for<'a, 'r> Fn(
|
||||||
&FormatSession<'a>,
|
&FormatSession<'a>,
|
||||||
PrettierLanguage,
|
PrettierLanguage,
|
||||||
&str,
|
&DispatchRequest<'r>,
|
||||||
) -> Result<DispatchResponse<'a>, String>
|
) -> Result<DispatchResponse<'a>, String>
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync,
|
+ Sync,
|
||||||
@@ -323,7 +323,7 @@ pub fn build_dispatcher(
|
|||||||
// deliberate skip otherwise (pure build).
|
// deliberate skip otherwise (pure build).
|
||||||
Route::Prettier(language) => {
|
Route::Prettier(language) => {
|
||||||
if let Some(fallback) = &fallback {
|
if let Some(fallback) = &fallback {
|
||||||
fallback(session, language, text)
|
fallback(session, language, &request)
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"No fallback for Prettier language '{}' in this build, part stays as-is",
|
"No fallback for Prettier language '{}' in this build, part stays as-is",
|
||||||
@@ -349,7 +349,7 @@ fn format_native<'a, E: std::fmt::Display>(
|
|||||||
format_to_ir: impl FnOnce() -> Result<EmbeddedIr<'a>, E>,
|
format_to_ir: impl FnOnce() -> Result<EmbeddedIr<'a>, E>,
|
||||||
) -> DispatchResponse<'a> {
|
) -> DispatchResponse<'a> {
|
||||||
debug_span!("oxfmt::embed::format_to_ir", language).in_scope(|| match format_to_ir() {
|
debug_span!("oxfmt::embed::format_to_ir", language).in_scope(|| match format_to_ir() {
|
||||||
Ok(embedded) => DispatchResponse::Formatted(embedded.into()),
|
Ok(embedded) => DispatchResponse::Formatted(embedded),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
debug!("native '{language}' format_to_ir failed, part stays as-is: {err}");
|
debug!("native '{language}' format_to_ir failed, part stays as-is: {err}");
|
||||||
DispatchResponse::PreserveOriginal
|
DispatchResponse::PreserveOriginal
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ use serde::Deserialize;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tracing::{debug, debug_span};
|
use tracing::{debug, debug_span};
|
||||||
|
|
||||||
use oxc_formatter::HtmlEmbedMeta;
|
use oxc_allocator::ArenaVec;
|
||||||
use oxc_formatter_core::{DispatchPayload, DispatchResponse, FormatSession};
|
use oxc_formatter::HtmlInJsTemplate;
|
||||||
|
use oxc_formatter_core::{
|
||||||
|
DispatchRequest, DispatchResponse, EmbeddedIr, FormatElement, FormatSession, Tag,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{
|
core::{
|
||||||
@@ -31,46 +34,60 @@ pub fn build_prettier_fallback(
|
|||||||
dispatch_config: Arc<ResolvedDispatchConfig>,
|
dispatch_config: Arc<ResolvedDispatchConfig>,
|
||||||
format_embedded_doc: FormatEmbeddedDocWithConfigCallback,
|
format_embedded_doc: FormatEmbeddedDocWithConfigCallback,
|
||||||
) -> PrettierDocFallback {
|
) -> PrettierDocFallback {
|
||||||
Arc::new(move |session: &FormatSession<'_>, language: PrettierLanguage, text: &str| {
|
Arc::new(
|
||||||
let parser_name = language.parser();
|
move |session: &FormatSession<'_>,
|
||||||
let Some(options) = dispatch_config.prettier_options_for(language) else {
|
language: PrettierLanguage,
|
||||||
debug!("The plugin for parser '{parser_name}' is not enabled, part stays as-is");
|
request: &DispatchRequest<'_>| {
|
||||||
return Ok(DispatchResponse::PreserveOriginal);
|
let text = request.text;
|
||||||
};
|
let parser_name = language.parser();
|
||||||
debug_span!("oxfmt::external::format_embedded_doc", parser = parser_name)
|
let Some(options) = dispatch_config.prettier_options_for(language) else {
|
||||||
.in_scope(|| {
|
debug!("The plugin for parser '{parser_name}' is not enabled, part stays as-is");
|
||||||
let doc_json_str = (format_embedded_doc)(options, text).map_err(|err| {
|
return Ok(DispatchResponse::PreserveOriginal);
|
||||||
format!("Failed to get Doc for embedded code (parser '{parser_name}'): {err}")
|
};
|
||||||
})?;
|
debug_span!("oxfmt::external::format_embedded_doc", parser = parser_name)
|
||||||
// Prettier's Doc can produce deeply nested arrays
|
.in_scope(|| {
|
||||||
// (e.g., md-in-js with `proseWrap: preserve`, which nests each word in `[[[prev, " "], word], " "]`).
|
let doc_json_str = (format_embedded_doc)(options, text).map_err(|err| {
|
||||||
// The default recursion limit of 128 is not enough for long paragraphs.
|
format!(
|
||||||
// This only affects this deserialization call;
|
"Failed to get Doc for embedded code (parser '{parser_name}'): {err}"
|
||||||
// other `serde_json` usage in the codebase keeps the default limit.
|
)
|
||||||
let mut de = serde_json::Deserializer::from_str(&doc_json_str);
|
})?;
|
||||||
de.disable_recursion_limit();
|
// Prettier's Doc can produce deeply nested arrays
|
||||||
let doc_json = serde_json::Value::deserialize(&mut de)
|
// (e.g., md-in-js with `proseWrap: preserve`, which nests each word in `[[[prev, " "], word], " "]`).
|
||||||
.map_err(|e| format!("Failed to parse Doc JSON: {e}"))?;
|
// The default recursion limit of 128 is not enough for long paragraphs.
|
||||||
|
// This only affects this deserialization call;
|
||||||
|
// other `serde_json` usage in the codebase keeps the default limit.
|
||||||
|
let mut de = serde_json::Deserializer::from_str(&doc_json_str);
|
||||||
|
de.disable_recursion_limit();
|
||||||
|
let doc_json = serde_json::Value::deserialize(&mut de)
|
||||||
|
.map_err(|e| format!("Failed to parse Doc JSON: {e}"))?;
|
||||||
|
|
||||||
let allocator = session.allocator();
|
let allocator = session.allocator();
|
||||||
let (mut ir, metadata) = from_prettier_doc::convert_envelope(
|
let (mut ir, metadata) = from_prettier_doc::convert_envelope(
|
||||||
doc_json,
|
doc_json,
|
||||||
allocator,
|
allocator,
|
||||||
session.group_id_builder(),
|
session.group_id_builder(),
|
||||||
)?;
|
)?;
|
||||||
from_prettier_doc::postprocess(&mut ir, allocator);
|
from_prettier_doc::postprocess(&mut ir, allocator);
|
||||||
// HTML/Angular additionally surface `HtmlEmbedMeta` to the embed site.
|
// html(angular)-in-js: indent the content when more than one node prints at the root
|
||||||
let child_context = language.wants_html_meta().then(|| {
|
let indent = request
|
||||||
Box::new(HtmlEmbedMeta {
|
.parent_context_as::<HtmlInJsTemplate>()
|
||||||
has_multiple_root_elements: metadata
|
.is_some_and(|c| c.indent_multiple_roots)
|
||||||
|
&& metadata
|
||||||
.get("htmlHasMultipleRootElements")
|
.get("htmlHasMultipleRootElements")
|
||||||
.and_then(Value::as_bool),
|
.and_then(Value::as_bool)
|
||||||
}) as Box<dyn std::any::Any>
|
.unwrap_or(true);
|
||||||
});
|
if indent {
|
||||||
Ok(DispatchResponse::Formatted(DispatchPayload { doc: ir, child_context }))
|
let mut indented = ArenaVec::with_capacity_in(ir.len() + 2, &allocator);
|
||||||
})
|
indented.push(FormatElement::Tag(Tag::StartIndent));
|
||||||
.inspect_err(|err| {
|
indented.extend(ir);
|
||||||
debug!("Failed to format embedded doc for parser '{parser_name}': {err}");
|
indented.push(FormatElement::Tag(Tag::EndIndent));
|
||||||
})
|
ir = indented;
|
||||||
})
|
}
|
||||||
|
Ok(DispatchResponse::Formatted(EmbeddedIr { ir }))
|
||||||
|
})
|
||||||
|
.inspect_err(|err| {
|
||||||
|
debug!("Failed to format embedded doc for parser '{parser_name}': {err}");
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -199,12 +199,6 @@ impl PrettierLanguage {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the Doc→IR conversion must surface `HtmlEmbedMeta`
|
|
||||||
/// (`htmlHasMultipleRootElements`) to the embed site.
|
|
||||||
pub fn wants_html_meta(self) -> bool {
|
|
||||||
matches!(self, Self::Html | Self::Angular)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ pub struct CssInJsTemplate;
|
|||||||
/// which would need escaping in the template.
|
/// which would need escaping in the template.
|
||||||
pub struct MarkdownInJsTemplate;
|
pub struct MarkdownInJsTemplate;
|
||||||
|
|
||||||
/// Child→parent pair context for HTML/Angular formatted as an embedded child (html-in-js).
|
/// Parent→child context for HTML/Angular dispatched from a JS template literal (html-in-js).
|
||||||
pub struct HtmlEmbedMeta {
|
pub struct HtmlInJsTemplate {
|
||||||
/// Whether the parsed HTML has more than one root element.
|
/// The backticks hug the content (no whitespace on both ends, whitespace sensitivity not `ignore`):
|
||||||
/// Used to decide whether to `indent` the template content.
|
/// the child indents its content when more than one node prints at the root.
|
||||||
pub has_multiple_root_elements: Option<bool>,
|
pub indent_multiple_roots: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a dispatched JS/TS program is embedded in, for the print rules that depend on it.
|
/// What a dispatched JS/TS program is embedded in, for the print rules that depend on it.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use oxc_span::SourceType;
|
|||||||
// or the special-purpose AST-in `format_program`.
|
// or the special-purpose AST-in `format_program`.
|
||||||
pub(crate) use crate::ast_nodes::{AstNode, AstNodes};
|
pub(crate) use crate::ast_nodes::{AstNode, AstNodes};
|
||||||
pub use crate::embed_context::{
|
pub use crate::embed_context::{
|
||||||
CssInJsTemplate, HtmlEmbedMeta, JsEmbeddedIn, MarkdownInJsTemplate,
|
CssInJsTemplate, HtmlInJsTemplate, JsEmbeddedIn, MarkdownInJsTemplate,
|
||||||
};
|
};
|
||||||
// `JsFormatContext` is public solely as the type parameter of the returned `Formatted`;
|
// `JsFormatContext` is public solely as the type parameter of the returned `Formatted`;
|
||||||
// its methods are not part of the public contract.
|
// its methods are not part of the public contract.
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use oxc_allocator::ArenaStringBuilder;
|
use oxc_allocator::ArenaStringBuilder;
|
||||||
use oxc_ast::ast::*;
|
use oxc_ast::ast::*;
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
DispatchRequest, DispatchResponse, FormatElement, InputKind, map_text_in_ir,
|
FormatElement, dispatch_fragment_ir, map_text_in_ir, push_text_with_literal_lines,
|
||||||
push_text_with_literal_lines,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast_nodes::AstNode,
|
ast_nodes::AstNode,
|
||||||
embed_context::HtmlEmbedMeta,
|
embed_context::HtmlInJsTemplate,
|
||||||
format_args,
|
format_args,
|
||||||
formatter::prelude::*,
|
formatter::prelude::*,
|
||||||
print::template::{
|
print::template::{
|
||||||
@@ -52,39 +51,19 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
|
|
||||||
let has_leading_ws = cooked.starts_with(|c: char| c.is_ascii_whitespace());
|
let has_leading_ws = cooked.starts_with(|c: char| c.is_ascii_whitespace());
|
||||||
let has_trailing_ws = cooked.ends_with(|c: char| c.is_ascii_whitespace());
|
let has_trailing_ws = cooked.ends_with(|c: char| c.is_ascii_whitespace());
|
||||||
|
let ws_ignore = f.options().html_whitespace_sensitivity_ignore;
|
||||||
|
|
||||||
let Ok(DispatchResponse::Formatted(result)) = f.session().dispatch(DispatchRequest {
|
let context = html_in_js_template(has_leading_ws, has_trailing_ws, ws_ignore);
|
||||||
language: embedded_language,
|
let Some(ir) = dispatch_fragment_ir(f, embedded_language, cooked, Some(&context)) else {
|
||||||
text: cooked,
|
|
||||||
input_kind: InputKind::Fragment,
|
|
||||||
parent_context: None,
|
|
||||||
}) else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(html_has_multiple_root_elements) = result
|
|
||||||
.child_context
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|context| context.downcast_ref::<HtmlEmbedMeta>())
|
|
||||||
.map(|meta| meta.has_multiple_root_elements)
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let ir = result.doc;
|
|
||||||
|
|
||||||
// Re-escape template chars in `Text` runs:
|
// Re-escape template chars in `Text` runs:
|
||||||
// the IR is reinserted into a JS template literal built from `.cooked` values.
|
// the IR is reinserted into a JS template literal built from `.cooked` values.
|
||||||
let ir = super::escape_template_chars_in_ir(ir, f);
|
let ir = super::escape_template_chars_in_ir(ir, f);
|
||||||
|
|
||||||
let content = format_once(|f| f.write_elements(ir));
|
let content = format_once(|f| f.write_elements(ir));
|
||||||
let ws_ignore = f.options().html_whitespace_sensitivity_ignore;
|
write_html_template(f, &content, has_leading_ws, has_trailing_ws, ws_ignore);
|
||||||
write_html_template(
|
|
||||||
f,
|
|
||||||
&content,
|
|
||||||
has_leading_ws,
|
|
||||||
has_trailing_ws,
|
|
||||||
html_has_multiple_root_elements.unwrap_or(true),
|
|
||||||
ws_ignore,
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,27 +91,14 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
|
|
||||||
let has_leading_ws = joined.starts_with(|c: char| c.is_ascii_whitespace());
|
let has_leading_ws = joined.starts_with(|c: char| c.is_ascii_whitespace());
|
||||||
let has_trailing_ws = joined.ends_with(|c: char| c.is_ascii_whitespace());
|
let has_trailing_ws = joined.ends_with(|c: char| c.is_ascii_whitespace());
|
||||||
|
let ws_ignore = f.options().html_whitespace_sensitivity_ignore;
|
||||||
|
|
||||||
// Phase 2: Format via the dispatcher (IR path)
|
// Phase 2: Format via the dispatcher (IR path)
|
||||||
let Ok(DispatchResponse::Formatted(result)) = f.session().dispatch(DispatchRequest {
|
let context = html_in_js_template(has_leading_ws, has_trailing_ws, ws_ignore);
|
||||||
language: embedded_language,
|
let Some(ir) = dispatch_fragment_ir(f, embedded_language, joined, Some(&context)) else {
|
||||||
text: joined,
|
|
||||||
input_kind: InputKind::Fragment,
|
|
||||||
parent_context: None,
|
|
||||||
}) else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(html_has_multiple_root_elements) = result
|
|
||||||
.child_context
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|context| context.downcast_ref::<HtmlEmbedMeta>())
|
|
||||||
.map(|meta| meta.has_multiple_root_elements)
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let ir = result.doc;
|
|
||||||
|
|
||||||
// Validate before formatting any expression.
|
// Validate before formatting any expression.
|
||||||
// Formatting consumes comment state,
|
// Formatting consumes comment state,
|
||||||
// so discovering an unusable placeholder layout afterwards would make the verbatim fallback unsafe.
|
// so discovering an unusable placeholder layout afterwards would make the verbatim fallback unsafe.
|
||||||
@@ -188,19 +154,19 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
true
|
true
|
||||||
});
|
});
|
||||||
let format_content = format_once(move |f: &mut JsFormatter<'_, 'a>| f.write_elements(ir));
|
let format_content = format_once(move |f: &mut JsFormatter<'_, 'a>| f.write_elements(ir));
|
||||||
|
write_html_template(f, &format_content, has_leading_ws, has_trailing_ws, ws_ignore);
|
||||||
let ws_ignore = f.options().html_whitespace_sensitivity_ignore;
|
|
||||||
write_html_template(
|
|
||||||
f,
|
|
||||||
&format_content,
|
|
||||||
has_leading_ws,
|
|
||||||
has_trailing_ws,
|
|
||||||
html_has_multiple_root_elements.unwrap_or(true),
|
|
||||||
ws_ignore,
|
|
||||||
);
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The child indents its content only where the backticks hug it (the last case of [`write_html_template`]).
|
||||||
|
fn html_in_js_template(
|
||||||
|
has_leading_ws: bool,
|
||||||
|
has_trailing_ws: bool,
|
||||||
|
ws_ignore: bool,
|
||||||
|
) -> HtmlInJsTemplate {
|
||||||
|
HtmlInJsTemplate { indent_multiple_roots: !(ws_ignore || has_leading_ws && has_trailing_ws) }
|
||||||
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
/// Write the HTML template with appropriate wrapping based on whitespace and top-level count.
|
/// Write the HTML template with appropriate wrapping based on whitespace and top-level count.
|
||||||
@@ -214,12 +180,12 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
/// - Otherwise: `group(["`", leadingWS?, maybeIndent(group(content)), trailingWS?, "`"])`
|
/// - Otherwise: `group(["`", leadingWS?, maybeIndent(group(content)), trailingWS?, "`"])`
|
||||||
/// → content hugs the backtick directly
|
/// → content hugs the backtick directly
|
||||||
/// → multiple root elements wraps with `indent`, single does not
|
/// → multiple root elements wraps with `indent`, single does not
|
||||||
|
/// → the child does the `indent` on request ([`HtmlInJsTemplate`])
|
||||||
fn write_html_template<'a>(
|
fn write_html_template<'a>(
|
||||||
f: &mut JsFormatter<'_, 'a>,
|
f: &mut JsFormatter<'_, 'a>,
|
||||||
content: &impl Format<'a, JsFormatContext<'a>>,
|
content: &impl Format<'a, JsFormatContext<'a>>,
|
||||||
has_leading_ws: bool,
|
has_leading_ws: bool,
|
||||||
has_trailing_ws: bool,
|
has_trailing_ws: bool,
|
||||||
has_multiple_root_elements: bool,
|
|
||||||
ws_ignore: bool,
|
ws_ignore: bool,
|
||||||
) {
|
) {
|
||||||
if ws_ignore {
|
if ws_ignore {
|
||||||
@@ -249,11 +215,7 @@ fn write_html_template<'a>(
|
|||||||
// group(["`", leadingWS?, maybeIndent(group(content)), trailingWS?, "`"])
|
// group(["`", leadingWS?, maybeIndent(group(content)), trailingWS?, "`"])
|
||||||
let leading = if has_leading_ws { " " } else { "" };
|
let leading = if has_leading_ws { " " } else { "" };
|
||||||
let trailing = if has_trailing_ws { " " } else { "" };
|
let trailing = if has_trailing_ws { " " } else { "" };
|
||||||
if has_multiple_root_elements {
|
write!(f, [group(&format_args!("`", leading, group(content), trailing, "`"))]);
|
||||||
write!(f, [group(&format_args!("`", leading, indent(&group(content)), trailing, "`"))]);
|
|
||||||
} else {
|
|
||||||
write!(f, [group(&format_args!("`", leading, group(content), trailing, "`"))]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ The core is parameterized over a consumer-supplied context so it stays language-
|
|||||||
|
|
||||||
### Embedded-language infrastructure (`session/`)
|
### Embedded-language infrastructure (`session/`)
|
||||||
|
|
||||||
`FormatSession` / `FormatDispatcher` / `DispatchRequest` / `DispatchPayload` let one formatter's IR be built inside another's document (e.g. graphql-in-js):
|
`FormatSession` / `FormatDispatcher` / `DispatchRequest` / `EmbeddedIr` let one formatter's IR be built inside another's document (e.g. graphql-in-js):
|
||||||
|
|
||||||
- The orchestrator (oxfmt) assembles the dispatcher, mapping language names to formatter implementations (or a Prettier fallback)
|
- The orchestrator (oxfmt) assembles the dispatcher, mapping language names to formatter implementations (or a Prettier fallback)
|
||||||
- Formatter crates only invoke it via `FormatSession::dispatch`
|
- Formatter crates only invoke it via `FormatSession::dispatch`
|
||||||
@@ -86,8 +86,9 @@ The core is parameterized over a consumer-supplied context so it stays language-
|
|||||||
- A child adds classes straight into the parent's index space, so its doc is written as-is (no remap)
|
- A child adds classes straight into the parent's index space, so its doc is written as-is (no remap)
|
||||||
- A root formatter opens a fresh scope (`with_new_tailwind_scope`) and sorts it when finalizing its `Document` (`take_sorted_tailwind_classes`)
|
- A root formatter opens a fresh scope (`with_new_tailwind_scope`) and sorts it when finalizing its `Document` (`take_sorted_tailwind_classes`)
|
||||||
- A language crate's `format_to_ir` entry returns `EmbeddedIr`, one shape for every child language, no per-crate tuples
|
- A language crate's `format_to_ir` entry returns `EmbeddedIr`, one shape for every child language, no per-crate tuples
|
||||||
|
- The dispatcher answers with it as-is (`DispatchResponse::Formatted`): data flows parent→child only, a child reports nothing back
|
||||||
- Only language-pair specific data crosses as `dyn Any`, core never learns concrete languages:
|
- Only language-pair specific data crosses as `dyn Any`, core never learns concrete languages:
|
||||||
- When the parent picks the child language, it passes the pair's own data (css-in-js's `CssInJsTemplate`, html-in-js's `has_multiple_root_elements`)
|
- When the parent picks the child language, it passes the pair's own data (css-in-js's `CssInJsTemplate`, html-in-js's `HtmlInJsTemplate`)
|
||||||
- When it cannot (a Markdown code block's name resolves in the host), it passes a fact about itself to every child,
|
- When it cannot (a Markdown code block's name resolves in the host), it passes a fact about itself to every child,
|
||||||
and the host translates it into the pair's data once the child language is known (`XxxInMarkdownCodeBlock` → `JsEmbeddedIn`)
|
and the host translates it into the pair's data once the child language is known (`XxxInMarkdownCodeBlock` → `JsEmbeddedIn`)
|
||||||
- `FormatSession` (`session/mod.rs`) is the execution unit:
|
- `FormatSession` (`session/mod.rs`) is the execution unit:
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ pub use printer::{PrintResult, PrintWidth, Printed, PrinterOptions};
|
|||||||
// which is what makes print-without-finalize unrepresentable outside core.
|
// which is what makes print-without-finalize unrepresentable outside core.
|
||||||
pub(crate) use printer::Printer;
|
pub(crate) use printer::Printer;
|
||||||
pub use session::embedded::{
|
pub use session::embedded::{
|
||||||
DispatchPayload, DispatchRequest, DispatchResponse, EmbeddedIr, FormatDispatcher,
|
DispatchRequest, DispatchResponse, EmbeddedIr, FormatDispatcher, dispatch_fragment_ir,
|
||||||
dispatch_fragment_ir, dispatch_ir, map_text_in_ir, push_text_with_literal_lines,
|
dispatch_ir, map_text_in_ir, push_text_with_literal_lines,
|
||||||
};
|
};
|
||||||
pub use session::{FormatSession, InputKind, SessionServices, StringEmbedder, TailwindSorter};
|
pub use session::{FormatSession, InputKind, SessionServices, StringEmbedder, TailwindSorter};
|
||||||
pub use source::{SourceText, SpanCursor, rebase_labels, restore_crlf_labels};
|
pub use source::{SourceText, SpanCursor, rebase_labels, restore_crlf_labels};
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ pub struct DispatchRequest<'r> {
|
|||||||
pub input_kind: InputKind,
|
pub input_kind: InputKind,
|
||||||
/// Parent→child language-pair specific data,
|
/// Parent→child language-pair specific data,
|
||||||
/// downcast by the implementation (`None` for most pairs; e.g. JS's `CssInJsTemplate`).
|
/// downcast by the implementation (`None` for most pairs; e.g. JS's `CssInJsTemplate`).
|
||||||
/// The borrowed counterpart of [`DispatchPayload::child_context`]
|
/// Only parent→child: a child formats what the parent asks for, it reports nothing back.
|
||||||
/// (borrowed because the parent outlives the dispatch call).
|
|
||||||
pub parent_context: Option<&'r dyn Any>,
|
pub parent_context: Option<&'r dyn Any>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +54,8 @@ impl DispatchRequest<'_> {
|
|||||||
/// optional-embed callers degrade the same way for both,
|
/// optional-embed callers degrade the same way for both,
|
||||||
/// but the two must never be conflated at the source.
|
/// but the two must never be conflated at the source.
|
||||||
pub enum DispatchResponse<'a> {
|
pub enum DispatchResponse<'a> {
|
||||||
/// The child formatted the request; consume [`DispatchPayload`].
|
/// The child formatted the request; the parent writes its IR as-is.
|
||||||
Formatted(DispatchPayload<'a>),
|
Formatted(EmbeddedIr<'a>),
|
||||||
/// Deliberately not formatted; keep the original source untouched.
|
/// Deliberately not formatted; keep the original source untouched.
|
||||||
PreserveOriginal,
|
PreserveOriginal,
|
||||||
}
|
}
|
||||||
@@ -77,34 +76,16 @@ pub type FormatDispatcher = Arc<
|
|||||||
+ Sync,
|
+ Sync,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/// IR built by a language crate's embedded entry point (`format_to_ir`) for one input text.
|
/// IR built by a language crate's embedded entry point (`format_to_ir`) for one input text,
|
||||||
/// The orchestrator's dispatcher wraps it into a [`DispatchPayload`].
|
/// carried by [`DispatchResponse::Formatted`].
|
||||||
///
|
///
|
||||||
/// Every language crate's `format_to_ir` returns this shape,
|
/// Every language crate's `format_to_ir` returns this shape,
|
||||||
/// so a new child language only has to fill in the fields (no per-crate tuple conventions).
|
/// so a new child language only has to fill in the fields (no per-crate tuple conventions).
|
||||||
pub struct EmbeddedIr<'a> {
|
pub struct EmbeddedIr<'a> {
|
||||||
/// The formatter IR, arena-allocated alongside its elements.
|
/// The formatter IR, arena-allocated alongside its elements.
|
||||||
/// Its `FormatElement::TailwindClass` indices point into the session's class scope.
|
|
||||||
pub ir: ArenaVec<'a, FormatElement<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<EmbeddedIr<'a>> for DispatchPayload<'a> {
|
|
||||||
fn from(embedded: EmbeddedIr<'a>) -> Self {
|
|
||||||
Self { doc: embedded.ir, child_context: None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The child's formatted product, carried by [`DispatchResponse::Formatted`].
|
|
||||||
pub struct DispatchPayload<'a> {
|
|
||||||
/// The formatted IR, arena-allocated alongside its elements.
|
|
||||||
/// Its `FormatElement::TailwindClass` indices already point into the parent's class scope
|
/// Its `FormatElement::TailwindClass` indices already point into the parent's class scope
|
||||||
/// (the child session shares it), so the parent writes it as-is.
|
/// (the child session shares it), so the parent writes it as-is.
|
||||||
pub doc: ArenaVec<'a, FormatElement<'a>>,
|
pub ir: ArenaVec<'a, FormatElement<'a>>,
|
||||||
/// Child→parent language-pair specific data,
|
|
||||||
/// downcast by the parent (`None` for most pairs; e.g. HTML's `has_multiple_root_elements`).
|
|
||||||
/// The owned counterpart of [`DispatchRequest::parent_context`]
|
|
||||||
/// (owned because it outlives the child's stack frame).
|
|
||||||
pub child_context: Option<Box<dyn Any>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatches one embedded fragment and hands out its doc:
|
/// Dispatches one embedded fragment and hands out its doc:
|
||||||
@@ -125,7 +106,6 @@ pub fn dispatch_fragment_ir<'a, C: FormatContext>(
|
|||||||
///
|
///
|
||||||
/// `None` covers [`DispatchResponse::PreserveOriginal`] and operational errors alike;
|
/// `None` covers [`DispatchResponse::PreserveOriginal`] and operational errors alike;
|
||||||
/// the caller keeps its original source.
|
/// the caller keeps its original source.
|
||||||
/// Embed sites that must inspect [`DispatchPayload::child_context`] first stay manual.
|
|
||||||
pub fn dispatch_ir<'a, C: FormatContext>(
|
pub fn dispatch_ir<'a, C: FormatContext>(
|
||||||
f: &Formatter<'_, 'a, C>,
|
f: &Formatter<'_, 'a, C>,
|
||||||
request: DispatchRequest<'_>,
|
request: DispatchRequest<'_>,
|
||||||
@@ -133,7 +113,7 @@ pub fn dispatch_ir<'a, C: FormatContext>(
|
|||||||
let Ok(DispatchResponse::Formatted(result)) = f.session().dispatch(request) else {
|
let Ok(DispatchResponse::Formatted(result)) = f.session().dispatch(request) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(result.doc)
|
Some(result.ir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuild an embedded IR, descending into BestFitting variants and interned content.
|
/// Rebuild an embedded IR, descending into BestFitting variants and interned content.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{cell::RefCell, rc::Rc, sync::Arc};
|
|||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
DispatchPayload, DispatchRequest, DispatchResponse, Document, FormatDispatcher, LineEnding,
|
DispatchRequest, DispatchResponse, Document, EmbeddedIr, FormatDispatcher, LineEnding,
|
||||||
PrinterOptions, UniqueGroupIdBuilder,
|
PrinterOptions, UniqueGroupIdBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,8 +200,7 @@ impl<'a> FormatSession<'a> {
|
|||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
let text_len = request.text.len();
|
let text_len = request.text.len();
|
||||||
let scoped = self.with_new_tailwind_scope();
|
let scoped = self.with_new_tailwind_scope();
|
||||||
let DispatchResponse::Formatted(DispatchPayload { doc, .. }) = scoped.dispatch(request)?
|
let DispatchResponse::Formatted(EmbeddedIr { ir: doc }) = scoped.dispatch(request)? else {
|
||||||
else {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -282,8 +281,8 @@ mod tests {
|
|||||||
|
|
||||||
use super::{FormatSession, InputKind, MAX_DISPATCH_DEPTH, SessionServices, TailwindSorter};
|
use super::{FormatSession, InputKind, MAX_DISPATCH_DEPTH, SessionServices, TailwindSorter};
|
||||||
use crate::{
|
use crate::{
|
||||||
DispatchPayload, DispatchRequest, DispatchResponse, Document, FormatDispatcher,
|
DispatchRequest, DispatchResponse, Document, EmbeddedIr, FormatDispatcher, FormatElement,
|
||||||
FormatElement, FormatState, Interned, PrinterOptions,
|
FormatState, Interned, PrinterOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn request<'r>() -> DispatchRequest<'r> {
|
fn request<'r>() -> DispatchRequest<'r> {
|
||||||
@@ -307,10 +306,7 @@ mod tests {
|
|||||||
fn dispatch_preserves_a_bom_headed_text() {
|
fn dispatch_preserves_a_bom_headed_text() {
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let dispatcher: FormatDispatcher = Arc::new(|ctx, _request| {
|
let dispatcher: FormatDispatcher = Arc::new(|ctx, _request| {
|
||||||
Ok(DispatchResponse::Formatted(DispatchPayload {
|
Ok(DispatchResponse::Formatted(EmbeddedIr { ir: ArenaVec::new_in(&ctx.allocator()) }))
|
||||||
doc: ArenaVec::new_in(&ctx.allocator()),
|
|
||||||
child_context: None,
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
let session = FormatSession::with_services(
|
let session = FormatSession::with_services(
|
||||||
&allocator,
|
&allocator,
|
||||||
@@ -329,10 +325,7 @@ mod tests {
|
|||||||
fn dispatch_stops_at_the_depth_limit() {
|
fn dispatch_stops_at_the_depth_limit() {
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let dispatcher: FormatDispatcher = Arc::new(|ctx, _request| {
|
let dispatcher: FormatDispatcher = Arc::new(|ctx, _request| {
|
||||||
Ok(DispatchResponse::Formatted(DispatchPayload {
|
Ok(DispatchResponse::Formatted(EmbeddedIr { ir: ArenaVec::new_in(&ctx.allocator()) }))
|
||||||
doc: ArenaVec::new_in(&ctx.allocator()),
|
|
||||||
child_context: None,
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
let mut session = FormatSession::with_services(
|
let mut session = FormatSession::with_services(
|
||||||
&allocator,
|
&allocator,
|
||||||
@@ -405,7 +398,7 @@ mod tests {
|
|||||||
[FormatElement::Interned(Interned::new(inner))],
|
[FormatElement::Interned(Interned::new(inner))],
|
||||||
&ctx.allocator(),
|
&ctx.allocator(),
|
||||||
);
|
);
|
||||||
Ok(DispatchResponse::Formatted(DispatchPayload { doc, child_context: None }))
|
Ok(DispatchResponse::Formatted(EmbeddedIr { ir: doc }))
|
||||||
});
|
});
|
||||||
let session = FormatSession::with_services(
|
let session = FormatSession::with_services(
|
||||||
&allocator,
|
&allocator,
|
||||||
@@ -426,7 +419,7 @@ mod tests {
|
|||||||
[FormatElement::TailwindClass(parent_index), FormatElement::Space],
|
[FormatElement::TailwindClass(parent_index), FormatElement::Space],
|
||||||
&&allocator,
|
&&allocator,
|
||||||
);
|
);
|
||||||
elements.extend(payload.doc);
|
elements.extend(payload.ir);
|
||||||
|
|
||||||
let printed = Document::new(elements, session.take_sorted_tailwind_classes())
|
let printed = Document::new(elements, session.take_sorted_tailwind_classes())
|
||||||
.print(0, PrinterOptions::default())
|
.print(0, PrinterOptions::default())
|
||||||
|
|||||||
Reference in new issue
Block a user