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:
leaysgur committed 2026-10-06 06:37:47 +00:00
1 parent 6e573f926e
commit c2e6d5fad2
23 files changed
+402 -64

No files matched your search

+1
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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).
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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 -3
View File
@@ -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,
+4 -4
View File
@@ -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 } => {
+3 -3
View File
@@ -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()
}
}
+8 -6
View File
@@ -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"
);
}
+6 -5
View File
@@ -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 () => {
+51
View File
@@ -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" });
+51
View File
@@ -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/);
});
});
-2
View File
@@ -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);
}
+48
View File
@@ -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`).
+28
View File
@@ -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
+51
View File
@@ -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)
}
+9
View File
@@ -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},
};
+69
View File
@@ -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()
}
}
}
+1
View File
@@ -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",
]