mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(formatter_toml)!: add oxc_formatter_toml crate wrapping oxc-toml (#27365)
- Before: Oxfmt directly calls `oxc-toml` - After: Oxfmt calls `oxc_formatter_toml` as like others - `oxc_formatter_toml` is wrapper for `oxc-toml` but aligned behavior with other formatters - Also embedded TOML (frontmatter, toml-in-markdown) are now formatted - Formatting logic still lives in `oxc-toml` side, not yet ported And by bumping `oxc-toml`, TOML 1.1 syntax is supported.
This commit is contained in:
1 parent
6e573f926e
commit
c2e6d5fad2
23 files changed
+402
-64
No files matched your search
@@ -34,6 +34,7 @@ pnpm-lock.yaml
|
||||
/crates/oxc_formatter_css @leaysgur
|
||||
/crates/oxc_formatter_yaml @leaysgur
|
||||
/crates/oxc_formatter_markdown @leaysgur
|
||||
/crates/oxc_formatter_toml @leaysgur
|
||||
/crates/oxc_formatter_tests @leaysgur
|
||||
/crates/oxc_jsdoc @leaysgur
|
||||
/crates/oxc_regular_expression @leaysgur
|
||||
|
||||
@@ -303,13 +303,13 @@ jobs:
|
||||
- uses: ./.github/actions/check-changes
|
||||
id: filter
|
||||
with:
|
||||
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
||||
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_toml/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
- uses: ./.github/actions/check-changes
|
||||
id: filter-conformance
|
||||
with:
|
||||
paths: apps/oxfmt/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,pnpm-lock.yaml,package.json
|
||||
paths: apps/oxfmt/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_toml/,pnpm-lock.yaml,package.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
|
||||
@@ -457,7 +457,7 @@ jobs:
|
||||
- uses: ./.github/actions/check-changes
|
||||
id: filter
|
||||
with:
|
||||
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
||||
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_toml/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
- uses: samypr100/setup-dev-drive@562171fe8df7401fdf582d766cd3b6ab80d5b1a0 # v4.1.0
|
||||
|
||||
Generated
+14
-3
@@ -1728,9 +1728,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc-toml"
|
||||
version = "0.14.6"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71bca8630e33e93a38d8dceb0daba6f562f29215b47098a7f05e8d295227ed94"
|
||||
checksum = "7456e981b4eef3e7c557a302720d1d2f8308ef8cd49ff51f6c00b45637e5bc08"
|
||||
|
||||
[[package]]
|
||||
name = "oxc-yaml-parser"
|
||||
@@ -2172,6 +2172,17 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_formatter_toml"
|
||||
version = "0.72.0"
|
||||
dependencies = [
|
||||
"oxc-toml",
|
||||
"oxc_allocator",
|
||||
"oxc_diagnostics",
|
||||
"oxc_formatter_core",
|
||||
"oxc_span",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_formatter_yaml"
|
||||
version = "0.72.0"
|
||||
@@ -2880,7 +2891,6 @@ dependencies = [
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"oxc-schemars",
|
||||
"oxc-toml",
|
||||
"oxc_allocator",
|
||||
"oxc_config",
|
||||
"oxc_diagnostics",
|
||||
@@ -2890,6 +2900,7 @@ dependencies = [
|
||||
"oxc_formatter_graphql",
|
||||
"oxc_formatter_json",
|
||||
"oxc_formatter_markdown",
|
||||
"oxc_formatter_toml",
|
||||
"oxc_formatter_yaml",
|
||||
"oxc_language_server",
|
||||
"oxc_napi",
|
||||
|
||||
+2
-1
@@ -167,6 +167,7 @@ oxc_formatter_graphql = { path = "crates/oxc_formatter_graphql" } # GraphQL form
|
||||
oxc_formatter_json = { path = "crates/oxc_formatter_json" } # JSON formatter
|
||||
oxc_formatter_markdown = { path = "crates/oxc_formatter_markdown" } # Markdown formatter
|
||||
oxc_formatter_tests = { path = "crates/oxc_formatter_tests" } # Formatter test infrastructure (fixture codegen + harness)
|
||||
oxc_formatter_toml = { path = "crates/oxc_formatter_toml" } # TOML formatter
|
||||
oxc_formatter_yaml = { path = "crates/oxc_formatter_yaml" } # YAML formatter
|
||||
oxc_language_server = { path = "crates/oxc_language_server", default-features = false } # Language server
|
||||
oxc_lexer = { path = "crates/oxc_lexer" } # JS/TS lexer (incubating; x86_64 SIMD core, scalar fallback)
|
||||
@@ -242,7 +243,7 @@ num-traits = "0.2.19" # Numeric traits
|
||||
oxc-css-parser = "0.0.16" # CSS/SCSS/Less parser (raffia 0.12.3 fork: adds `template_placeholder` typed placeholders for css-in-js + valid-syntax coverage fixes (see crates/oxc_formatter_css/AGENTS.md))
|
||||
oxc-graphql-parser = "0.0.9" # GraphQL parser (direct AST; apollo-parser 0.8.6 fork aligned with graphql-js 17 syntax)
|
||||
oxc-markdown-parser = "0.0.3" # CommonMark + GFM parser (span-faithful, style-preserving typed AST)
|
||||
oxc-toml = "0.14.5" # TOML formatter
|
||||
oxc-toml = "0.15.0" # TOML parser and formatter
|
||||
oxc-yaml-parser = "0.0.6" # YAML 1.2 parser (comment-preserving typed AST, node naming from `yaml-unist-parser`)
|
||||
papaya = "0.2.4" # Concurrent hash map
|
||||
percent-encoding = "2.3.2" # URL encoding
|
||||
|
||||
@@ -68,7 +68,7 @@ The stable distinction is:
|
||||
Oxfmt utilizes different implementations depending on the file extension and filename:
|
||||
|
||||
- Tier 1: Rust implementations using `oxc_formatter`, `oxc_formatter_json`, etc found in this repository
|
||||
- Tier 2: Rust implementations using external libraries like `oxc_toml`
|
||||
- Tier 2: Rust implementations wrapping external libraries like `oxc_formatter_toml` (`oxc-toml`)
|
||||
- Tier 3: Delegations to Prettier via NAPI-JS calls (e.g., for Vue or MDX)
|
||||
- Tier 4: Delegations to Prettier that require additional Prettier plugins (e.g., for Svelte)
|
||||
|
||||
@@ -95,7 +95,7 @@ Embedded languages (e.g. css-in-js, CSS front matter YAML) go through the `Forma
|
||||
|
||||
Routing is ONE table (`dispatcher::route`):
|
||||
|
||||
- `Native` languages (js/ts/css/graphql/yaml/json/markdown/...) get a Rust branch
|
||||
- `Native` languages (js/ts/css/graphql/yaml/json/markdown/toml/...) get a Rust branch
|
||||
- `Prettier` set (html/angular/vue/svelte/handlebars/mdx) goes to the Prettier Doc→IR channel
|
||||
- `embed/prettier_doc.rs`, napi only; svelte only with the `svelte` config
|
||||
- everything else is deliberately preserved
|
||||
@@ -103,7 +103,7 @@ Routing is ONE table (`dispatcher::route`):
|
||||
Language-pair data crosses as `DispatchRequest::parent_context` markers the dispatcher translates for the child, so no language crate depends on another.
|
||||
|
||||
Vocabulary: "fallback" = the dispatcher's optional `PrettierDocFallback` slot (a build/root may not install one).
|
||||
The pure Rust build runs fallback-less, so non-native embeds (html-in-js, TOML/custom front matter) deliberately stay verbatim.
|
||||
The pure Rust build runs fallback-less, so non-native embeds (html-in-js, custom front matter) deliberately stay verbatim.
|
||||
|
||||
The roots install `SessionServices`, all via `embed/services.rs::for_root` (one definition per build; the napi one takes the `ExternalServices` transport, and adds the Prettier fallback / string embedder / Tailwind sorter to the registry dispatcher): the JS/TS, CSS and Markdown file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte `<script>` root (`api/text_to_doc_api.rs`, `VirtualDocument` session, napi only).
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ oxc_formatter_css = { workspace = true }
|
||||
oxc_formatter_graphql = { workspace = true }
|
||||
oxc_formatter_json = { workspace = true }
|
||||
oxc_formatter_markdown = { workspace = true }
|
||||
oxc_formatter_toml = { workspace = true }
|
||||
oxc_formatter_yaml = { workspace = true }
|
||||
oxc_language_server = { workspace = true }
|
||||
oxc_napi = { workspace = true }
|
||||
@@ -55,7 +56,6 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["unbounded_depth"] }
|
||||
simdutf8 = { workspace = true }
|
||||
sort-package-json = { workspace = true }
|
||||
oxc-toml = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature
|
||||
|
||||
@@ -544,7 +544,8 @@ mod tests_slow_path_validation {
|
||||
#[test]
|
||||
fn fast_path_resolve_succeeds() {
|
||||
let resolver = resolver_from_json(serde_json::json!({ "printWidth": 80 }));
|
||||
let kind = FileKind::OxfmtToml { path: Arc::from(PathBuf::from("Cargo.toml").as_path()) };
|
||||
let kind =
|
||||
FileKind::OxcFormatterToml { path: Arc::from(PathBuf::from("Cargo.toml").as_path()) };
|
||||
assert!(resolver.resolve(kind).is_ok());
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,15 @@ use oxc_formatter_css::{CssFormatOptions, CssVariant};
|
||||
use oxc_formatter_graphql::GraphqlFormatOptions;
|
||||
use oxc_formatter_json::{JsonFormatOptions, JsonVariant};
|
||||
use oxc_formatter_markdown::{MarkdownFormatOptions, XxxInMarkdownCodeBlock};
|
||||
use oxc_formatter_toml::TomlFormatOptions;
|
||||
use oxc_formatter_yaml::YamlFormatOptions;
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::core::{
|
||||
options::{
|
||||
ValidatedOptions, to_oxc_formatter, to_oxc_formatter_css, to_oxc_formatter_graphql,
|
||||
to_oxc_formatter_json, to_oxc_formatter_markdown, to_oxc_formatter_yaml,
|
||||
to_oxc_formatter_json, to_oxc_formatter_markdown, to_oxc_formatter_toml,
|
||||
to_oxc_formatter_yaml,
|
||||
},
|
||||
oxfmtrc::FormatConfig,
|
||||
};
|
||||
@@ -39,6 +41,7 @@ pub enum NativeLanguage {
|
||||
Yaml,
|
||||
Json(JsonVariant),
|
||||
Markdown,
|
||||
Toml,
|
||||
}
|
||||
|
||||
/// Languages Prettier still formats for us (no Rust formatter yet).
|
||||
@@ -119,6 +122,7 @@ pub fn route(language: &str) -> Route {
|
||||
"handlebars" | "hbs" => Route::Prettier(PrettierLanguage::Handlebars),
|
||||
"mdx" => Route::Prettier(PrettierLanguage::Mdx),
|
||||
"markdown" | "md" => Route::Native(NativeLanguage::Markdown),
|
||||
"toml" => Route::Native(NativeLanguage::Toml),
|
||||
// JS / TS by file extension, which carries the module kind and JSX.
|
||||
// A component's inline template is found from its decorator, not from the fence (`angular-ts`).
|
||||
_ => {
|
||||
@@ -158,6 +162,7 @@ pub struct ResolvedDispatchConfig {
|
||||
/// One cell per fence-reachable [`JsonVariant`] (json / jsonc / json5; `JsonStringify` is `package.json`-only).
|
||||
json: [OnceLock<JsonFormatOptions>; 3],
|
||||
markdown: OnceLock<MarkdownFormatOptions>,
|
||||
toml: OnceLock<TomlFormatOptions>,
|
||||
/// The options handed to Prettier; see [`PrettierOptions`].
|
||||
#[cfg(feature = "napi")]
|
||||
prettier: PrettierOptions,
|
||||
@@ -191,6 +196,7 @@ impl ResolvedDispatchConfig {
|
||||
yaml: OnceLock::new(),
|
||||
json: [OnceLock::new(), OnceLock::new(), OnceLock::new()],
|
||||
markdown: OnceLock::new(),
|
||||
toml: OnceLock::new(),
|
||||
#[cfg(feature = "napi")]
|
||||
prettier: PrettierOptions::default(),
|
||||
}
|
||||
@@ -278,6 +284,10 @@ impl ResolvedDispatchConfig {
|
||||
*self.markdown.get_or_init(|| to_oxc_formatter_markdown(&self.config, self.validated.core))
|
||||
}
|
||||
|
||||
pub fn toml_options(&self) -> TomlFormatOptions {
|
||||
*self.toml.get_or_init(|| to_oxc_formatter_toml(&self.config, self.validated.core))
|
||||
}
|
||||
|
||||
/// Printer options from the shared resolved core bundle;
|
||||
/// the fence adapter ([`super::jsdoc_fence`]) derives its per-fence options from these
|
||||
/// (width overridden to the fence's effective width).
|
||||
@@ -420,6 +430,9 @@ pub fn build_dispatcher(
|
||||
)
|
||||
}))
|
||||
}
|
||||
Route::Native(NativeLanguage::Toml) => Ok(format_native("toml", || {
|
||||
oxc_formatter_toml::format_to_ir(session, text, dispatch_config.toml_options())
|
||||
})),
|
||||
|
||||
// Prettier-served languages: Doc→IR fallback when available (napi),
|
||||
// deliberate skip otherwise (pure build).
|
||||
@@ -435,7 +448,7 @@ pub fn build_dispatcher(
|
||||
}
|
||||
}
|
||||
|
||||
// A language without a formatter is a deliberate skip, in every build.
|
||||
// A language without a formatter is a deliberate skip, in every build
|
||||
Route::Unsupported => {
|
||||
debug!("No formatter for language '{}', part stays as-is", request.language);
|
||||
Ok(DispatchResponse::PreserveOriginal)
|
||||
@@ -519,6 +532,7 @@ mod tests {
|
||||
"json5",
|
||||
"markdown",
|
||||
"md",
|
||||
"toml",
|
||||
] {
|
||||
let text = match language {
|
||||
"graphql" | "gql" => "{ a }",
|
||||
@@ -577,7 +591,7 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
for (language, text) in [("html", "<div></div>"), ("toml", "a = 1")] {
|
||||
for (language, text) in [("html", "<div></div>"), ("ini", "a = 1")] {
|
||||
let response = session.dispatch(DispatchRequest {
|
||||
language,
|
||||
text,
|
||||
|
||||
@@ -16,8 +16,8 @@ use super::options::{
|
||||
use super::{
|
||||
embed::dispatcher::ResolvedDispatchConfig,
|
||||
options::{
|
||||
ValidatedOptions, to_oxc_formatter_graphql, to_oxc_formatter_json, to_oxc_formatter_yaml,
|
||||
to_oxc_toml, to_sort_package_json,
|
||||
ValidatedOptions, to_oxc_formatter_graphql, to_oxc_formatter_json, to_oxc_formatter_toml,
|
||||
to_oxc_formatter_yaml, to_sort_package_json,
|
||||
},
|
||||
oxfmtrc::FormatConfig,
|
||||
support::FileKind,
|
||||
@@ -152,8 +152,8 @@ impl SourceFormatter {
|
||||
oxc_formatter_markdown::format_with_session(&session, source_text, options)
|
||||
.and_then(|formatted| print(formatted, &path))
|
||||
}
|
||||
FileKind::OxfmtToml { .. } => {
|
||||
Ok(oxc_toml::format(source_text, to_oxc_toml(&config, core)))
|
||||
FileKind::OxcFormatterToml { .. } => {
|
||||
oxc_formatter_toml::format(source_text, to_oxc_formatter_toml(&config, core))
|
||||
}
|
||||
#[cfg(feature = "napi")]
|
||||
FileKind::Prettier { path, parser_name } => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! - [`to_oxc_formatter_graphql()`]: `oxc_formatter_graphql::GraphqlFormatOptions` for GraphQL formatting
|
||||
//! - [`to_oxc_formatter_yaml()`]: `oxc_formatter_yaml::YamlFormatOptions` for YAML formatting
|
||||
//! - [`to_oxc_formatter_markdown()`]: `oxc_formatter_markdown::MarkdownFormatOptions` for Markdown formatting
|
||||
//! - [`to_oxc_toml()`]: `oxc_toml::Options` for TOML formatting
|
||||
//! - [`to_oxc_formatter_toml()`]: `TomlFormatOptions` for `oxc_formatter_toml`
|
||||
//! - `to_prettier`(NAPI-only): Prettier-compatible JSON, plus `inject_*` helpers for
|
||||
//! layering in `parser` / `filepath` / plugin payloads at the format step
|
||||
//! - [`validate()`]: the validation gate runs every fallible conversion once
|
||||
@@ -21,8 +21,8 @@ mod to_oxc_formatter_css;
|
||||
mod to_oxc_formatter_graphql;
|
||||
mod to_oxc_formatter_json;
|
||||
mod to_oxc_formatter_markdown;
|
||||
mod to_oxc_formatter_toml;
|
||||
mod to_oxc_formatter_yaml;
|
||||
mod to_oxc_toml;
|
||||
#[cfg(feature = "napi")]
|
||||
mod to_prettier;
|
||||
mod validate;
|
||||
@@ -32,8 +32,8 @@ pub use to_oxc_formatter_css::to_oxc_formatter_css;
|
||||
pub use to_oxc_formatter_graphql::to_oxc_formatter_graphql;
|
||||
pub use to_oxc_formatter_json::{to_oxc_formatter_json, to_sort_package_json};
|
||||
pub use to_oxc_formatter_markdown::to_oxc_formatter_markdown;
|
||||
pub use to_oxc_formatter_toml::to_oxc_formatter_toml;
|
||||
pub use to_oxc_formatter_yaml::to_oxc_formatter_yaml;
|
||||
pub use to_oxc_toml::to_oxc_toml;
|
||||
#[cfg(feature = "napi")]
|
||||
pub use to_prettier::{
|
||||
build_prettier_options, inject_filepath, inject_oxfmt_plugin_payload, inject_parser,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use oxc_formatter_core::{CoreFormatOptions, FormatOptions};
|
||||
use oxc_formatter_toml::{TomlFormatOptions, TrailingCommas};
|
||||
|
||||
use super::super::oxfmtrc::{FormatConfig, TrailingCommaConfig};
|
||||
|
||||
/// Convert `FormatConfig` into `TomlFormatOptions` for `oxc_formatter_toml`.
|
||||
///
|
||||
/// Currently, `oxfmtrc` does not have dedicated TOML-specific options.
|
||||
/// So it mirrors Prettier's `prettier-plugin-toml` conventions.
|
||||
///
|
||||
/// NOTE: Pure field translation:
|
||||
/// `core` comes pre-validated from the config-resolution gate (`validate()`), so this cannot fail.
|
||||
pub fn to_oxc_formatter_toml(
|
||||
config: &FormatConfig,
|
||||
core_options: CoreFormatOptions,
|
||||
) -> TomlFormatOptions {
|
||||
let mut options = TomlFormatOptions::default();
|
||||
options.apply_core(core_options);
|
||||
|
||||
// [Prettier] trailingComma: "all" | "es5" | "none"
|
||||
// `all`/`es5` are indistinguishable for TOML (arrays only check "not none")
|
||||
if let Some(trailing_comma) = config.trailing_comma {
|
||||
options.trailing_commas = match trailing_comma {
|
||||
TrailingCommaConfig::All | TrailingCommaConfig::Es5 => TrailingCommas::Always,
|
||||
TrailingCommaConfig::None => TrailingCommas::Never,
|
||||
};
|
||||
}
|
||||
|
||||
options
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use oxc_formatter_core::CoreFormatOptions;
|
||||
use oxc_toml::Options as TomlFormatterOptions;
|
||||
|
||||
use super::super::oxfmtrc::{FormatConfig, TrailingCommaConfig};
|
||||
|
||||
/// Convert `FormatConfig` into `TomlFormatterOptions` for `oxc_toml`.
|
||||
///
|
||||
/// Currently, `oxfmtrc` does not have dedicated TOML-specific options.
|
||||
/// So it mirrors Prettier's `prettier-plugin-toml` conventions.
|
||||
/// <https://github.com/un-ts/prettier/blob/7a4346d5dbf6b63987c0f81228fc46bb12f8692f/packages/toml/src/index.ts#L27-L31>
|
||||
///
|
||||
/// NOTE: Pure field translation:
|
||||
/// `core` comes pre-validated from the config-resolution gate (`validate()`), so this cannot fail.
|
||||
pub fn to_oxc_toml(config: &FormatConfig, core_options: CoreFormatOptions) -> TomlFormatterOptions {
|
||||
TomlFormatterOptions {
|
||||
column_width: core_options.line_width.value() as usize,
|
||||
indent_string: if core_options.indent_style.is_tab() {
|
||||
"\t".to_string()
|
||||
} else {
|
||||
" ".repeat(core_options.indent_width.value() as usize)
|
||||
},
|
||||
crlf: core_options.line_ending.is_carriage_return_line_feed(),
|
||||
// [Prettier] trailingComma: "all" | "es5" | "none"
|
||||
array_trailing_comma: !matches!(config.trailing_comma, Some(TrailingCommaConfig::None)),
|
||||
// NOTE: This is needed to `oxfmtrc.insertFinalNewline` work
|
||||
trailing_newline: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ pub fn classify_file_kind(path: Arc<Path>) -> Option<FileKind> {
|
||||
return Some(FileKind::OxcFormatter { path, source_type: SourceType::default() });
|
||||
}
|
||||
if TOML_FILENAMES.contains(file_name) || ext == "toml" || file_name.ends_with(".toml.example") {
|
||||
return Some(FileKind::OxfmtToml { path });
|
||||
return Some(FileKind::OxcFormatterToml { path });
|
||||
}
|
||||
if file_name == "package.json" {
|
||||
return Some(FileKind::OxcFormatterJsonPackageJson { path });
|
||||
@@ -68,6 +68,8 @@ pub fn classify_file_kind(path: Arc<Path>) -> Option<FileKind> {
|
||||
///
|
||||
/// Consumed by the resolver to construct a [`super::FormatStrategy`] with the resolved config.
|
||||
#[derive(Debug)]
|
||||
// Every variant is `OxcFormatter*` without the napi-only `Prettier`
|
||||
#[cfg_attr(not(feature = "napi"), expect(clippy::enum_variant_names))]
|
||||
pub enum FileKind {
|
||||
/// JS/TS files formatted by `oxc_formatter`.
|
||||
OxcFormatter { path: Arc<Path>, source_type: SourceType },
|
||||
@@ -87,8 +89,8 @@ pub enum FileKind {
|
||||
OxcFormatterYamlRc { path: Arc<Path> },
|
||||
/// Markdown files formatted by `oxc_formatter_markdown`.
|
||||
OxcFormatterMarkdown { path: Arc<Path> },
|
||||
/// TOML files formatted by `oxc_toml`.
|
||||
OxfmtToml { path: Arc<Path> },
|
||||
/// TOML files formatted by `oxc_formatter_toml`.
|
||||
OxcFormatterToml { path: Arc<Path> },
|
||||
/// Files formatted by delegating to Prettier (Tier 3/4).
|
||||
#[cfg(feature = "napi")]
|
||||
Prettier { path: Arc<Path>, parser_name: &'static str },
|
||||
@@ -105,7 +107,7 @@ impl FileKind {
|
||||
| Self::OxcFormatterYaml { path }
|
||||
| Self::OxcFormatterYamlRc { path }
|
||||
| Self::OxcFormatterMarkdown { path }
|
||||
| Self::OxfmtToml { path } => path,
|
||||
| Self::OxcFormatterToml { path } => path,
|
||||
#[cfg(feature = "napi")]
|
||||
Self::Prettier { path, .. } => path,
|
||||
}
|
||||
@@ -122,7 +124,7 @@ impl FileKind {
|
||||
Self::OxcFormatterYaml { .. } => "yaml",
|
||||
Self::OxcFormatterYamlRc { .. } => "yaml_rc",
|
||||
Self::OxcFormatterMarkdown { .. } => "markdown",
|
||||
Self::OxfmtToml { .. } => "toml",
|
||||
Self::OxcFormatterToml { .. } => "toml",
|
||||
#[cfg(feature = "napi")]
|
||||
Self::Prettier { parser_name, .. } => parser_name,
|
||||
}
|
||||
@@ -639,7 +641,7 @@ mod tests {
|
||||
for file_name in toml_files {
|
||||
let result = classify_file_kind(Arc::from(Path::new(file_name)));
|
||||
assert!(
|
||||
matches!(result, Some(FileKind::OxfmtToml { .. })),
|
||||
matches!(result, Some(FileKind::OxcFormatterToml { .. })),
|
||||
"`{file_name}` should be detected as TOML"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,16 +44,17 @@ describe("CSS front matter", () => {
|
||||
expect(result.code).toBe("---\na: 1\n---\n");
|
||||
});
|
||||
|
||||
it("keeps non-YAML blocks verbatim", async () => {
|
||||
it("keeps custom language blocks verbatim", async () => {
|
||||
// Custom language: never dispatched (Prettier keeps it raw too).
|
||||
const custom = await format("a.css", "---mycustomparser\na: 1\n---\nb {}\n");
|
||||
expect(custom.errors).toStrictEqual([]);
|
||||
expect(custom.code).toBe("---mycustomparser\na: 1\n---\n\nb {\n}\n");
|
||||
});
|
||||
|
||||
// TOML: dispatched but no native formatter, degrades to verbatim.
|
||||
const toml = await format("a.css", "+++\na = 1\n+++\nb {}\n");
|
||||
expect(toml.errors).toStrictEqual([]);
|
||||
expect(toml.code).toBe("+++\na = 1\n+++\n\nb {\n}\n");
|
||||
it("formats a TOML block", async () => {
|
||||
const result = await format("a.css", "+++\na = 1\n+++\nb {}\n");
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe("+++\na = 1\n+++\n\nb {\n}\n");
|
||||
});
|
||||
|
||||
it("keeps the block verbatim when the YAML does not parse", async () => {
|
||||
|
||||
@@ -83,6 +83,57 @@ describe("Markdown", () => {
|
||||
expect(result.code).toBe(source);
|
||||
});
|
||||
|
||||
it("formats TOML front matter and code blocks", async () => {
|
||||
const source =
|
||||
'+++\ntitle="x"\n+++\n\n- a\n\n ```toml\n arr=[1,\n 2]\n s = """\n x\n y"""\n ```\n\n```toml\nbad = = 1\n```\n';
|
||||
const result = await format("a.md", source);
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"+++
|
||||
title = "x"
|
||||
+++
|
||||
|
||||
- a
|
||||
|
||||
\`\`\`toml
|
||||
arr = [1, 2]
|
||||
s = """
|
||||
x
|
||||
y"""
|
||||
\`\`\`
|
||||
|
||||
\`\`\`toml
|
||||
bad = = 1
|
||||
\`\`\`
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
// Pinned divergence: TOML is formatted at the full `printWidth` regardless of its column,
|
||||
// while other languages count from it (see `crates/oxc_formatter_toml/AGENTS.md`)
|
||||
it("formats TOML code blocks without counting the embedding column", async () => {
|
||||
const source =
|
||||
'- a\n\n ```toml\n a = ["aaaa", "bbbb"]\n ```\n\n ```yaml\n a: [aaaa, bbbbbbbbb]\n ```\n';
|
||||
const result = await format("a.md", source, { printWidth: 20 });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"- a
|
||||
|
||||
\`\`\`toml
|
||||
a = ["aaaa", "bbbb"]
|
||||
\`\`\`
|
||||
|
||||
\`\`\`yaml
|
||||
a:
|
||||
[
|
||||
aaaa,
|
||||
bbbbbbbbb,
|
||||
]
|
||||
\`\`\`
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("keeps front matter and code blocks verbatim under embeddedLanguageFormatting: off", async () => {
|
||||
const source = "---\ntitle: Home\n---\n\n```css\na{color:red}\n```\n";
|
||||
const result = await format("a.md", source, { embeddedLanguageFormatting: "off" });
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { format } from "../../dist/index.js";
|
||||
|
||||
// Formatting itself is tested in `oxc-toml`; these pin the oxfmt wiring.
|
||||
// File names, `useTabs` and `insertFinalNewline` are covered by `test/cli`.
|
||||
describe("TOML files (oxc_formatter_toml)", () => {
|
||||
it("should map tabWidth, printWidth and trailingComma", async () => {
|
||||
const source = `a = ["aaaa", "bbbb", "cccc"]\n`;
|
||||
const result = await format("a.toml", source, {
|
||||
tabWidth: 4,
|
||||
printWidth: 20,
|
||||
trailingComma: "none",
|
||||
});
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"a = [
|
||||
"aaaa",
|
||||
"bbbb",
|
||||
"cccc"
|
||||
]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should map endOfLine", async () => {
|
||||
const result = await format("a.toml", "a=1\nb=2\n", { endOfLine: "crlf" });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe("a = 1\r\nb = 2\r\n");
|
||||
});
|
||||
|
||||
it("should accept TOML 1.1 syntax", async () => {
|
||||
const source = `esc="\\e[0m"\ntime=07:32\ninline={ a=1,\n b=2, }\n`;
|
||||
const result = await format("a.toml", source);
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"esc = "\\e[0m"
|
||||
time = 07:32
|
||||
inline = { a = 1, b = 2 }
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should report a diagnostic for broken input", async () => {
|
||||
// `oxc-toml` alone would format around the invalid range
|
||||
const source = "a = 1\nb = = 1\n";
|
||||
const result = await format("broken.toml", source);
|
||||
expect(result.code).toBe(source);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].message).toMatch(/Syntax error/);
|
||||
});
|
||||
});
|
||||
@@ -273,8 +273,6 @@ pub fn to_span(span: &oxc_css_parser::Span) -> Span {
|
||||
}
|
||||
|
||||
fn write_front_matter<'a>(fm: &FrontMatter<'a>, f: &mut CssFormatter<'_, 'a>) {
|
||||
// NOTE: TOML currently has no IR-capable formatter, so it degrades to verbatim through `PreserveOriginal`.
|
||||
// Still need to specify here since blank TOML frontmatter will be normalized.
|
||||
oxc_formatter_core::write_front_matter(fm, &["yaml", "toml"], f);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Coding agent guides for `crates/oxc_formatter_toml`
|
||||
|
||||
## Overview
|
||||
|
||||
TOML formatter (`oxfmt`'s Tier 2 backend), a thin wrapper around [`oxc-toml`](https://github.com/oxc-project/oxc-toml).
|
||||
|
||||
- `oxc-toml` is a formatter-only fork of Taplo, maintained in its own repository
|
||||
- Formatting logic and its tests live there, not here
|
||||
- It prints a string, it does NOT build the `oxc_formatter_core` IR
|
||||
- Prettier has no TOML support; `prettier-plugin-toml` runs Taplo, so `oxc-toml`'s output is the reference as-is
|
||||
- Entry points:
|
||||
- `format()`: standalone files
|
||||
- `format_to_ir()`: embedded use via the dispatcher (TOML front matter, Markdown code blocks)
|
||||
|
||||
### Divergences and the IR goal
|
||||
|
||||
All divergences below stem from `oxc-toml` printing a string instead of building the IR.
|
||||
The goal is to move the formatting logic into this crate and build the `oxc_formatter_core` IR like the other `oxc_formatter_*` crates;
|
||||
`oxc-toml` would then shrink to a parser.
|
||||
|
||||
Not done yet, since no case needs it today:
|
||||
|
||||
- The output already IS the reference (`prettier-plugin-toml` runs Taplo), so a rewrite risks diverging from it
|
||||
- Every embedding host places the child at a root (front matter at column 0, Markdown code blocks under `mark_as_root`),
|
||||
where the string with literal lines prints the same as an IR would
|
||||
|
||||
Strong motivations, since these break rules every other language follows:
|
||||
|
||||
- Width: the child is formatted at the full `printWidth` wherever it sits
|
||||
- Other languages' IR is printed by the parent's printer, so their width counts from the embedding column (e.g. inside a list item)
|
||||
- Prettier with the plugin behaves the same as now, but consistency across our languages comes first
|
||||
- Pinned in `apps/oxfmt/test/api/markdown.test.ts`
|
||||
- No BOM split, which the other crates do
|
||||
|
||||
Known limits of the string embedding (text with literal lines, the same as Prettier's `replaceEndOfLine()` on the plugin's string):
|
||||
|
||||
- Blank lines inside an indented root (list item, blockquote) keep the root's prefix as trailing whitespace, as Prettier does
|
||||
- Every line resumes at the parent's root, so lines would not follow an enclosing `indent()`
|
||||
- Once a host does that (e.g. an indented Vue custom block), structural lines must follow the indent but multi-line string contents must not
|
||||
- A string cannot tell them apart (the same reason js-in-vue moved off string embedding)
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
cargo c -p oxc_formatter_toml
|
||||
```
|
||||
|
||||
Embedded behavior is covered by `apps/oxfmt` tests (`test/api/front_matter.test.ts`, `test/api/markdown.test.ts`).
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "oxc_formatter_toml"
|
||||
version = "0.72.0"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
include = ["/src"]
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
oxc-toml = { workspace = true }
|
||||
oxc_allocator = { workspace = true }
|
||||
oxc_diagnostics = { workspace = true }
|
||||
oxc_formatter_core = { workspace = true }
|
||||
oxc_span = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
@@ -0,0 +1,51 @@
|
||||
use oxc_allocator::ArenaVec;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_formatter_core::{EmbeddedIr, FormatSession, push_text_with_literal_lines};
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::options::TomlFormatOptions;
|
||||
|
||||
/// Format a standalone TOML file.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`OxcDiagnostic`] when the parse produces any error.
|
||||
pub fn format(source_text: &str, options: TomlFormatOptions) -> Result<String, OxcDiagnostic> {
|
||||
let parse = parse_for_format(source_text)?;
|
||||
Ok(oxc_toml::format(&parse, options.to_oxc_toml()))
|
||||
}
|
||||
|
||||
/// Format `source_text` into IR for embedding into another formatter's document
|
||||
/// (e.g. TOML front matter, Markdown code blocks).
|
||||
///
|
||||
/// The formatted string crosses as text with literal lines (see AGENTS.md "Divergences and the IR goal").
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`OxcDiagnostic`] when the parse produces any error.
|
||||
pub fn format_to_ir<'a>(
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &str,
|
||||
options: TomlFormatOptions,
|
||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||
let parse = parse_for_format(source_text)?;
|
||||
|
||||
// The IR only carries `\n`, the printer applies the line ending, and the parent owns the trailing newline
|
||||
let toml_options =
|
||||
oxc_toml::Options { crlf: false, trailing_newline: false, ..options.to_oxc_toml() };
|
||||
let formatted = oxc_toml::format(&parse, toml_options);
|
||||
|
||||
let allocator = session.allocator();
|
||||
let mut ir = ArenaVec::new_in(&allocator);
|
||||
push_text_with_literal_lines(&mut ir, allocator.alloc_str(&formatted), options.indent_width);
|
||||
Ok(EmbeddedIr { ir })
|
||||
}
|
||||
|
||||
/// `oxc_toml::format()` keeps invalid ranges as-is instead of failing,
|
||||
/// so bail out on any parse error here.
|
||||
fn parse_for_format(source_text: &str) -> Result<oxc_toml::Parse<'_>, OxcDiagnostic> {
|
||||
let parse = oxc_toml::parse(source_text);
|
||||
if let Some(error) = parse.errors.first() {
|
||||
return Err(OxcDiagnostic::error(format!("Syntax error: {}", error.message))
|
||||
.with_label(Span::new(error.range.start, error.range.end)));
|
||||
}
|
||||
Ok(parse)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! TOML formatter wrapping [oxc-toml](https://docs.rs/oxc-toml), a formatter-only fork of Taplo.
|
||||
|
||||
mod format;
|
||||
mod options;
|
||||
|
||||
pub use crate::{
|
||||
format::{format, format_to_ir},
|
||||
options::{TomlFormatOptions, TrailingCommas},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
use oxc_formatter_core::{
|
||||
CoreFormatOptions, FormatOptions, IndentStyle, IndentWidth, LineEnding, LineWidth,
|
||||
};
|
||||
|
||||
/// Format options for TOML.
|
||||
///
|
||||
/// Mirrors `prettier-plugin-toml`, which maps the shared layout options and `trailingComma` only.
|
||||
/// <https://github.com/un-ts/prettier/blob/7a4346d5dbf6b63987c0f81228fc46bb12f8692f/packages/toml/src/index.ts#L27-L31>
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct TomlFormatOptions {
|
||||
pub indent_style: IndentStyle,
|
||||
pub indent_width: IndentWidth,
|
||||
pub line_width: LineWidth,
|
||||
pub line_ending: LineEnding,
|
||||
pub trailing_commas: TrailingCommas,
|
||||
}
|
||||
|
||||
/// Whether a multi-line array gets a trailing comma (TOML 1.0 allows none in inline tables).
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum TrailingCommas {
|
||||
/// Prettier `trailingComma: "es5" | "all"` (default).
|
||||
#[default]
|
||||
Always,
|
||||
/// Prettier `trailingComma: "none"`.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl FormatOptions for TomlFormatOptions {
|
||||
fn indent_style(&self) -> IndentStyle {
|
||||
self.indent_style
|
||||
}
|
||||
|
||||
fn indent_width(&self) -> IndentWidth {
|
||||
self.indent_width
|
||||
}
|
||||
|
||||
fn line_width(&self) -> LineWidth {
|
||||
self.line_width
|
||||
}
|
||||
|
||||
fn line_ending(&self) -> LineEnding {
|
||||
self.line_ending
|
||||
}
|
||||
|
||||
fn apply_core(&mut self, core: CoreFormatOptions) {
|
||||
self.indent_style = core.indent_style;
|
||||
self.indent_width = core.indent_width;
|
||||
self.line_width = core.line_width;
|
||||
self.line_ending = core.line_ending;
|
||||
}
|
||||
}
|
||||
|
||||
impl TomlFormatOptions {
|
||||
pub(crate) fn to_oxc_toml(self) -> oxc_toml::Options {
|
||||
oxc_toml::Options {
|
||||
column_width: self.line_width.value() as usize,
|
||||
indent_string: if self.indent_style.is_tab() {
|
||||
"\t".to_string()
|
||||
} else {
|
||||
" ".repeat(self.indent_width.value() as usize)
|
||||
},
|
||||
crlf: self.line_ending.is_carriage_return_line_feed(),
|
||||
array_trailing_comma: matches!(self.trailing_commas, TrailingCommas::Always),
|
||||
// Callers trim it for `insertFinalNewline: false`
|
||||
trailing_newline: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,5 +41,6 @@ versioned_files = [
|
||||
"crates/oxc_formatter_css/Cargo.toml",
|
||||
"crates/oxc_formatter_yaml/Cargo.toml",
|
||||
"crates/oxc_formatter_markdown/Cargo.toml",
|
||||
"crates/oxc_formatter_toml/Cargo.toml",
|
||||
"npm/oxfmt/package.json",
|
||||
]
|
||||
Reference in new issue
Block a user