mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter): format xxx-in-js inside JSDoc js fence (#25414)
Such as CSS-in-JS-in-JSDoc_Markdown-in-JS. 😇
This commit is contained in:
1 parent
1a2c64a356
commit
ab52a593f3
8 files changed
+193
-227
No files matched your search
@@ -225,6 +225,36 @@ describe("JSDoc", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should format embedded languages inside a fenced js block", async () => {
|
||||
// The JS snippet formats on the parent session, so xxx-in-js inside the fence dispatch like anywhere else
|
||||
// (upstream formats these too: formatCode runs prettier.format, whose embed pass applies).
|
||||
const source = `
|
||||
/**
|
||||
* \`\`\`js
|
||||
* const s = css\`a{color: red}\`;
|
||||
* \`\`\`
|
||||
*/
|
||||
export const a = 1;
|
||||
`.trim();
|
||||
|
||||
const result = await format("a.ts", source, { jsdoc: {} });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(
|
||||
`
|
||||
/**
|
||||
* \`\`\`js
|
||||
* const s = css\`
|
||||
* a {
|
||||
* color: red;
|
||||
* }
|
||||
* \`;
|
||||
* \`\`\`
|
||||
*/
|
||||
export const a = 1;
|
||||
`.trimStart(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should keep fenced code verbatim for languages outside the registry", async () => {
|
||||
const source = `
|
||||
/**
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_core::LineWidth;
|
||||
use oxc_formatter_core::{FormatSession, LineWidth};
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::options::TrailingCommas;
|
||||
use crate::{JsFormatOptions, format_program, parse_for_format};
|
||||
use crate::{JsFormatOptions, format_with_session};
|
||||
|
||||
/// Helper for:
|
||||
/// - Parse a snippet
|
||||
/// - Parse a snippet ([`format_with_session`]'s gate: any parse diagnostic rejects)
|
||||
/// - and format it to a string with trailing whitespace trimmed
|
||||
/// - (no string embedder)
|
||||
///
|
||||
/// Returns `None` if parsing fails (panic or any error), the gate every embedded site uses.
|
||||
/// Formats on the parent session (same arena; embeds inside the snippet
|
||||
/// dispatch through the parent's services).
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the formatted IR is invalid or unbalanced, indicating a formatter bug.
|
||||
fn parse_and_build<'a>(
|
||||
allocator: &'a Allocator,
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &'a str,
|
||||
source_type: SourceType,
|
||||
options: JsFormatOptions,
|
||||
) -> Option<String> {
|
||||
let ret = parse_for_format(allocator, source_text, source_type);
|
||||
if ret.panicked || !ret.diagnostics.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut code = format_program(allocator, &ret.program, options).print().unwrap().into_code();
|
||||
let formatted = format_with_session(session, source_text, source_type, options).ok()?;
|
||||
let mut code = formatted.print().unwrap().into_code();
|
||||
code.truncate(code.trim_end().len());
|
||||
Some(code)
|
||||
}
|
||||
@@ -91,10 +87,15 @@ pub(super) fn update_template_depth(line: &str, mut depth: u32) -> u32 {
|
||||
|
||||
/// Format a JS/TS/JSX snippet extracted from a JSDoc comment.
|
||||
///
|
||||
/// For now, this is JSDoc-specific helper, NOT a generic embedded-language formatter.
|
||||
/// JSDoc snippets are standalone comment fragments rather than parent-integrated IR,
|
||||
/// so they go through `format_program` directly (no orchestrator round trip)
|
||||
/// and return a string for line-by-line splicing into the surrounding comment.
|
||||
/// This is a JSDoc-specific helper, NOT a generic embedded-language formatter.
|
||||
/// JSDoc snippets are standalone comment fragments rather than parent-integrated IR:
|
||||
/// the snippet itself formats in-crate on the parent session (no dispatcher round trip)
|
||||
/// and returns a string for line-by-line splicing into the surrounding comment,
|
||||
/// while embeds INSIDE the snippet (css-in-js, …) dispatch through the session normally.
|
||||
///
|
||||
/// The snippet inherits the parent session's `InputKind` and dispatch depth AS-IS
|
||||
/// (the fence boundary is not a dispatch; the same interim shape as the string
|
||||
/// channel's fresh-session reset on the oxfmt side).
|
||||
///
|
||||
/// JSDoc-specific behaviour (inline comments below give the full rationale):
|
||||
/// - object-literal wrap for `{`-leading snippets
|
||||
@@ -106,7 +107,7 @@ pub(super) fn format_jsdoc_js_snippet(
|
||||
code: &str,
|
||||
print_width: usize,
|
||||
format_options: &JsFormatOptions,
|
||||
allocator: &Allocator,
|
||||
session: &FormatSession<'_>,
|
||||
) -> Option<String> {
|
||||
let width = u16::try_from(print_width).unwrap_or(80).clamp(1, 320);
|
||||
let line_width = LineWidth::try_from(width).unwrap();
|
||||
@@ -121,7 +122,7 @@ pub(super) fn format_jsdoc_js_snippet(
|
||||
|
||||
// Try to parse and format with the given source type
|
||||
let try_format = |code: &str, source_type: SourceType| {
|
||||
parse_and_build(allocator, code, source_type, base_options.clone())
|
||||
parse_and_build(session, code, source_type, base_options.clone())
|
||||
};
|
||||
|
||||
// If code starts with `{`, try expression wrapping FIRST to handle object
|
||||
@@ -129,14 +130,14 @@ pub(super) fn format_jsdoc_js_snippet(
|
||||
// with labels. The upstream plugin uses `parser: "json"` for this case.
|
||||
let trimmed = code.trim();
|
||||
if trimmed.starts_with('{') {
|
||||
let wrapped = allocator.alloc_concat_strs_array(["(", trimmed, ")"]);
|
||||
let wrapped = session.allocator().alloc_concat_strs_array(["(", trimmed, ")"]);
|
||||
// Use TrailingCommas::None for object literals since JSON-like code
|
||||
// shouldn't have trailing commas
|
||||
let obj_options =
|
||||
JsFormatOptions { trailing_commas: TrailingCommas::None, ..base_options.clone() };
|
||||
|
||||
let try_format_obj = |code: &str, source_type: SourceType| -> Option<String> {
|
||||
let formatted = parse_and_build(allocator, code, source_type, obj_options.clone())?;
|
||||
let formatted = parse_and_build(session, code, source_type, obj_options.clone())?;
|
||||
// Remove the wrapping parens and trailing semicolon
|
||||
if let Some(inner) = formatted.strip_prefix('(')
|
||||
&& let Some(inner) = inner.strip_suffix(");")
|
||||
@@ -203,7 +204,7 @@ pub(super) fn format_jsdoc_js_snippet(
|
||||
pub(super) fn format_type_via_formatter(
|
||||
type_str: &str,
|
||||
type_options: &JsFormatOptions,
|
||||
allocator: &Allocator,
|
||||
session: &FormatSession<'_>,
|
||||
) -> Option<String> {
|
||||
if type_str.is_empty() {
|
||||
return None;
|
||||
@@ -216,8 +217,8 @@ pub(super) fn format_type_via_formatter(
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let wrapped = allocator.alloc_concat_strs_array(["(", rest, ")[]"]);
|
||||
let formatted = format_type_via_formatter(wrapped, type_options, allocator)?;
|
||||
let wrapped = session.allocator().alloc_concat_strs_array(["(", rest, ")[]"]);
|
||||
let formatted = format_type_via_formatter(wrapped, type_options, session)?;
|
||||
let inner = formatted.strip_suffix("[]")?;
|
||||
let mut result = String::with_capacity(inner.len() + 3);
|
||||
result.push_str("...");
|
||||
@@ -236,9 +237,9 @@ pub(super) fn format_type_via_formatter(
|
||||
// Let the TS formatter handle quote conversion naturally, matching Prettier's
|
||||
// TS parser which uses the configured quote style (double by default).
|
||||
// No string literal protection needed — the formatter should convert quotes.
|
||||
let input = allocator.alloc_concat_strs_array(["type __t = ", type_str, ";"]);
|
||||
let input = session.allocator().alloc_concat_strs_array(["type __t = ", type_str, ";"]);
|
||||
|
||||
let formatted = parse_and_build(allocator, input, SourceType::tsx(), type_options.clone())?;
|
||||
let formatted = parse_and_build(session, input, SourceType::tsx(), type_options.clone())?;
|
||||
|
||||
// Strip the `type __t = ` prefix (11 chars) using slice, matching upstream's
|
||||
// `pretty.slice(TYPE_START.length)` approach. This handles both same-line and
|
||||
|
||||
@@ -7,8 +7,7 @@ use std::borrow::Cow;
|
||||
|
||||
use markdown::{Constructs, ParseOptions, to_mdast};
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_core::StringEmbedder;
|
||||
use oxc_formatter_core::FormatSession;
|
||||
|
||||
use crate::JsFormatOptions;
|
||||
|
||||
@@ -33,15 +32,14 @@ pub fn format_description_mdast(
|
||||
max_width: usize,
|
||||
tag_string_length: usize,
|
||||
capitalize: bool,
|
||||
format_options: Option<&JsFormatOptions>,
|
||||
allocator: Option<&Allocator>,
|
||||
string_embedder: Option<&StringEmbedder>,
|
||||
format_options: &JsFormatOptions,
|
||||
session: &FormatSession<'_>,
|
||||
) -> String {
|
||||
if text.trim().is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let jsdoc_opts = format_options.and_then(|opts| opts.jsdoc.as_ref());
|
||||
let jsdoc_opts = format_options.jsdoc.as_ref();
|
||||
let description_with_dot = jsdoc_opts.is_some_and(|o| o.description_with_dot);
|
||||
let prefer_code_fences = jsdoc_opts.is_some_and(|o| o.prefer_code_fences);
|
||||
let line_wrapping_style =
|
||||
@@ -143,8 +141,7 @@ pub fn format_description_mdast(
|
||||
line_wrapping_style,
|
||||
source: &protected,
|
||||
format_options,
|
||||
allocator,
|
||||
string_embedder,
|
||||
session,
|
||||
};
|
||||
serialize_children(&root, 0, opts.tag_string_length, &opts, &mut lines);
|
||||
|
||||
@@ -160,7 +157,6 @@ pub(super) struct SerializeOptions<'a> {
|
||||
pub(super) prefer_code_fences: bool,
|
||||
pub(super) line_wrapping_style: crate::LineWrappingStyle,
|
||||
pub(super) source: &'a str,
|
||||
pub(super) format_options: Option<&'a JsFormatOptions>,
|
||||
pub(super) allocator: Option<&'a Allocator>,
|
||||
pub(super) string_embedder: Option<&'a StringEmbedder>,
|
||||
pub(super) format_options: &'a JsFormatOptions,
|
||||
pub(super) session: &'a FormatSession<'a>,
|
||||
}
|
||||
@@ -759,22 +759,21 @@ fn format_code_value<'a>(
|
||||
width: usize,
|
||||
opts: &SerializeOptions<'_>,
|
||||
) -> Cow<'a, str> {
|
||||
if let (Some(format_options), Some(allocator)) = (opts.format_options, opts.allocator) {
|
||||
// For fenced code with an explicit non-JS/TS lang, try external formatter (CSS, HTML, etc.)
|
||||
if let Some(l) = lang
|
||||
&& !is_js_ts_lang(l)
|
||||
// For fenced code with an explicit non-JS/TS lang, try external formatter (CSS, HTML, etc.)
|
||||
if let Some(l) = lang
|
||||
&& !is_js_ts_lang(l)
|
||||
{
|
||||
if let Some(embed) = opts.session.string_embedder()
|
||||
&& let Ok(formatted) = embed(l, code, width)
|
||||
{
|
||||
if let Some(embed) = opts.string_embedder
|
||||
&& let Ok(formatted) = embed(l, code, width)
|
||||
{
|
||||
return Cow::Owned(formatted);
|
||||
}
|
||||
return Cow::Borrowed(code);
|
||||
}
|
||||
// Fenced JS/TS, fenced with no lang, or indented code: try formatting
|
||||
if let Some(formatted) = format_jsdoc_js_snippet(code, width, format_options, allocator) {
|
||||
return Cow::Owned(formatted);
|
||||
}
|
||||
return Cow::Borrowed(code);
|
||||
}
|
||||
// Fenced JS/TS, fenced with no lang, or indented code: try formatting
|
||||
if let Some(formatted) = format_jsdoc_js_snippet(code, width, opts.format_options, opts.session)
|
||||
{
|
||||
return Cow::Owned(formatted);
|
||||
}
|
||||
Cow::Borrowed(code)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::Comment;
|
||||
use oxc_formatter_core::{LineWidth, StringEmbedder};
|
||||
use oxc_formatter_core::{FormatSession, LineWidth};
|
||||
use oxc_jsdoc::JSDoc;
|
||||
use oxc_span::Span;
|
||||
|
||||
@@ -54,14 +53,15 @@ const LINE_PREFIX_LEN: usize = 3;
|
||||
/// Holds the shared per-comment state for JSDoc formatting,
|
||||
/// reducing parameter passing across formatting functions.
|
||||
///
|
||||
/// Uses two lifetimes: `'a` for the allocator (tied to output strings)
|
||||
/// and `'o` for options (only need to live as long as the formatter).
|
||||
/// Uses two lifetimes: `'a` for the arena (tied to output strings)
|
||||
/// and `'o` for the options / session references (only need to live as long as the formatter).
|
||||
pub(super) struct JsdocFormatter<'a, 'o> {
|
||||
pub(super) options: &'o JsdocOptions,
|
||||
pub(super) format_options: &'o JsFormatOptions,
|
||||
pub(super) type_format_options: JsFormatOptions,
|
||||
pub(super) allocator: &'a Allocator,
|
||||
pub(super) string_embedder: Option<&'o StringEmbedder>,
|
||||
/// The parent JS run's session: arena, string embedder, and (for fenced
|
||||
/// JS/TS snippets) the dispatcher for embeds inside the snippet.
|
||||
pub(super) session: &'o FormatSession<'a>,
|
||||
pub(super) wrap_width: usize,
|
||||
pub(super) content_lines: LineBuffer,
|
||||
}
|
||||
@@ -70,9 +70,8 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
|
||||
fn new(
|
||||
options: &'o JsdocOptions,
|
||||
format_options: &'o JsFormatOptions,
|
||||
allocator: &'a Allocator,
|
||||
session: &'o FormatSession<'a>,
|
||||
available_width: usize,
|
||||
string_embedder: Option<&'o StringEmbedder>,
|
||||
) -> Self {
|
||||
let wrap_width = available_width.saturating_sub(LINE_PREFIX_LEN);
|
||||
// Use commentContentPrintWidth (= wrap_width) as the line width for type
|
||||
@@ -92,8 +91,7 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
|
||||
options,
|
||||
format_options,
|
||||
type_format_options,
|
||||
allocator,
|
||||
string_embedder,
|
||||
session,
|
||||
wrap_width,
|
||||
content_lines: LineBuffer::new(),
|
||||
}
|
||||
@@ -177,9 +175,8 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
|
||||
self.wrap_width,
|
||||
0,
|
||||
self.options.capitalize_descriptions,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
if self.options.description_tag {
|
||||
// Emit as @description tag
|
||||
@@ -380,7 +377,7 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
|
||||
if tmp == content {
|
||||
return None;
|
||||
}
|
||||
let alloc_first = self.allocator.alloc_str(first);
|
||||
let alloc_first = self.session.allocator().alloc_str(first);
|
||||
return Some(FormattedJsdoc::SingleLine(alloc_first));
|
||||
}
|
||||
|
||||
@@ -408,7 +405,7 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
|
||||
}
|
||||
|
||||
// Arena-allocate only the inner content (without /** */ wrapper)
|
||||
let alloc_content = self.allocator.alloc_str(content_str);
|
||||
let alloc_content = self.session.allocator().alloc_str(content_str);
|
||||
Some(FormattedJsdoc::MultiLine(alloc_content))
|
||||
}
|
||||
|
||||
@@ -1069,9 +1066,7 @@ pub fn format_jsdoc_comment<'a>(
|
||||
available_width: usize,
|
||||
f: &JsFormatter<'_, 'a>,
|
||||
) -> Option<FormattedJsdoc<'a>> {
|
||||
let string_embedder = f.session().string_embedder();
|
||||
let fmt =
|
||||
JsdocFormatter::new(options, f.options(), f.allocator(), available_width, string_embedder);
|
||||
let fmt = JsdocFormatter::new(options, f.options(), f.session(), available_width);
|
||||
fmt.format(comment, source_text)
|
||||
}
|
||||
|
||||
@@ -1160,10 +1155,15 @@ mod tests {
|
||||
assert!(!should_remove_empty_tag("abstract"));
|
||||
}
|
||||
|
||||
fn fmt_type(type_str: &str) -> Option<String> {
|
||||
fn fmt_type_with_opts(type_str: &str, opts: &JsFormatOptions) -> Option<String> {
|
||||
use crate::formatter::jsdoc::embedded::format_type_via_formatter;
|
||||
let allocator = oxc_allocator::Allocator::default();
|
||||
format_type_via_formatter(type_str, &JsFormatOptions::default(), &allocator)
|
||||
let session = FormatSession::new(&allocator, oxc_formatter_core::InputKind::Fragment);
|
||||
format_type_via_formatter(type_str, opts, &session)
|
||||
}
|
||||
|
||||
fn fmt_type(type_str: &str) -> Option<String> {
|
||||
fmt_type_with_opts(type_str, &JsFormatOptions::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1182,8 +1182,6 @@ mod tests {
|
||||
}
|
||||
|
||||
fn fmt_type_width(type_str: &str, width: u16) -> Option<String> {
|
||||
use crate::formatter::jsdoc::embedded::format_type_via_formatter;
|
||||
let allocator = oxc_allocator::Allocator::default();
|
||||
let opts = JsFormatOptions {
|
||||
line_width: LineWidth::try_from(width).unwrap(),
|
||||
jsdoc: None,
|
||||
@@ -1191,7 +1189,7 @@ mod tests {
|
||||
sort_tailwindcss: None,
|
||||
..JsFormatOptions::default()
|
||||
};
|
||||
format_type_via_formatter(type_str, &opts, &allocator)
|
||||
fmt_type_with_opts(type_str, &opts)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,6 +6,7 @@ use super::{
|
||||
embedded::{
|
||||
format_jsdoc_js_snippet, format_type_via_formatter, is_js_ts_lang, update_template_depth,
|
||||
},
|
||||
mdast_serialize::format_description_mdast,
|
||||
normalize::{
|
||||
capitalize_first, normalize_markdown_emphasis, normalize_type,
|
||||
normalize_type_preserve_quotes, normalize_type_return, strip_jsdoc_stars_preserve_newlines,
|
||||
@@ -16,7 +17,7 @@ use super::{
|
||||
should_preserve_description_verbatim, should_skip_description_formatting,
|
||||
strip_default_is_suffix,
|
||||
},
|
||||
wrap::{str_width, wrap_text},
|
||||
wrap::str_width,
|
||||
};
|
||||
|
||||
/// Replace standalone JSDoc `*` type syntax with `any` in a multiline type string.
|
||||
@@ -163,7 +164,7 @@ impl JsdocFormatter<'_, '_> {
|
||||
// wrap_width minus the code indent width.
|
||||
let effective_width = self.wrap_width.saturating_sub(self.code_indent_width());
|
||||
if let Some(formatted) =
|
||||
format_jsdoc_js_snippet(code, effective_width, self.format_options, self.allocator)
|
||||
format_jsdoc_js_snippet(code, effective_width, self.format_options, self.session)
|
||||
.filter(|f| {
|
||||
// Reject pseudo-code that parses as valid JS but produces
|
||||
// structurally different output (e.g., `{undefined}('popup', 'options')`
|
||||
@@ -208,7 +209,7 @@ impl JsdocFormatter<'_, '_> {
|
||||
inner_code,
|
||||
effective_width,
|
||||
self.format_options,
|
||||
self.allocator,
|
||||
self.session,
|
||||
) {
|
||||
self.push_formatted_code_lines(&formatted, indent);
|
||||
} else {
|
||||
@@ -295,7 +296,7 @@ impl JsdocFormatter<'_, '_> {
|
||||
if let Some(formatted) = format_type_via_formatter(
|
||||
&formatter_input,
|
||||
&self.type_format_options,
|
||||
self.allocator,
|
||||
self.session,
|
||||
) {
|
||||
// If the formatter collapsed a multi-line type to single
|
||||
// line, verify it fits within the available width. The tag
|
||||
@@ -339,17 +340,20 @@ impl JsdocFormatter<'_, '_> {
|
||||
if let Some(np) = &name_part {
|
||||
let name_raw = np.raw();
|
||||
if is_type_optional && !name_raw.starts_with('[') {
|
||||
name_str = self.allocator.alloc_concat_strs_array(["[", name_raw, "]"]);
|
||||
name_str = self.session.allocator().alloc_concat_strs_array(["[", name_raw, "]"]);
|
||||
} else if name_raw.starts_with('[') && name_raw.ends_with(']') {
|
||||
if let Some(eq_pos) = name_raw.find('=') {
|
||||
let name_part_inner = &name_raw[1..eq_pos];
|
||||
let val = name_raw[eq_pos + 1..name_raw.len() - 1].trim();
|
||||
if val.is_empty() {
|
||||
name_str =
|
||||
self.allocator.alloc_concat_strs_array(["[", name_part_inner, "]"]);
|
||||
name_str = self.session.allocator().alloc_concat_strs_array([
|
||||
"[",
|
||||
name_part_inner,
|
||||
"]",
|
||||
]);
|
||||
} else {
|
||||
default_value = Some(val);
|
||||
name_str = self.allocator.alloc_concat_strs_array([
|
||||
name_str = self.session.allocator().alloc_concat_strs_array([
|
||||
"[",
|
||||
name_part_inner,
|
||||
"=",
|
||||
@@ -408,14 +412,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
if !desc_raw.is_empty() {
|
||||
let indent = self.continuation_indent();
|
||||
let indent_width = self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
desc_raw,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
}
|
||||
@@ -457,14 +460,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
self.content_lines.push_empty();
|
||||
let indent = self.continuation_indent();
|
||||
let indent_width = self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
desc_raw,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
return;
|
||||
@@ -483,14 +485,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
self.content_lines.push_empty();
|
||||
let indent = self.continuation_indent();
|
||||
let indent_width = self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let mut desc = wrap_text(
|
||||
let mut desc = format_description_mdast(
|
||||
desc_raw,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
// Skip leading blank line from wrap_text since we already added one
|
||||
if desc.starts_with('\n') {
|
||||
@@ -607,14 +608,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
let s = self.content_lines.begin_line();
|
||||
s.push_str(&tag_line);
|
||||
s.push_str(" -");
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&remaining_desc,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
return;
|
||||
@@ -657,26 +657,24 @@ impl JsdocFormatter<'_, '_> {
|
||||
line.push_str(" -");
|
||||
}
|
||||
self.content_lines.push(line);
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&full_desc,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
} else {
|
||||
// Append description inline with tag_string_length offset
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&full_desc,
|
||||
indent_width,
|
||||
tag_str_len,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
let mut iter = desc.split('\n');
|
||||
if let Some(first) = iter.next() {
|
||||
@@ -748,7 +746,7 @@ impl JsdocFormatter<'_, '_> {
|
||||
if let Some(formatted) = format_type_via_formatter(
|
||||
&formatter_input,
|
||||
&self.type_format_options,
|
||||
self.allocator,
|
||||
self.session,
|
||||
) {
|
||||
if was_multiline && !formatted.contains('\n') {
|
||||
normalized_type_str = Cow::Owned(star_stripped.unwrap());
|
||||
@@ -807,7 +805,8 @@ impl JsdocFormatter<'_, '_> {
|
||||
// Append single-token description to the last line of the tag
|
||||
let mut lines: Vec<&str> = tag_line.split('\n').collect();
|
||||
if let Some(last) = lines.last_mut() {
|
||||
let combined = self.allocator.alloc_concat_strs_array([last, " ", &desc_text]);
|
||||
let combined =
|
||||
self.session.allocator().alloc_concat_strs_array([last, " ", &desc_text]);
|
||||
for line in &lines[..lines.len() - 1] {
|
||||
self.content_lines.push(*line);
|
||||
}
|
||||
@@ -823,14 +822,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
for line in lines_iter {
|
||||
self.content_lines.push(line);
|
||||
}
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&desc_text,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
}
|
||||
@@ -872,25 +870,23 @@ impl JsdocFormatter<'_, '_> {
|
||||
let first_word_w = desc_text_no_dash.split_whitespace().next().map_or(0, str_width);
|
||||
if prefix_len + first_word_w > self.wrap_width {
|
||||
self.content_lines.push(tag_line);
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&desc_text_no_dash,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
} else {
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&desc_text_no_dash,
|
||||
indent_width,
|
||||
tag_str_len,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
let mut iter = desc.split('\n');
|
||||
if let Some(first) = iter.next() {
|
||||
@@ -1020,14 +1016,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
}
|
||||
} else if skip_fmt {
|
||||
// Single-line skip-formatting: wrap at full width, no continuation indent.
|
||||
let mut desc = wrap_text(
|
||||
let mut desc = format_description_mdast(
|
||||
raw_ws_desc,
|
||||
self.wrap_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
if desc.starts_with('\n') {
|
||||
desc.remove(0);
|
||||
@@ -1036,14 +1031,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
} else {
|
||||
let indent = self.continuation_indent();
|
||||
let indent_width = self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let mut desc = wrap_text(
|
||||
let mut desc = format_description_mdast(
|
||||
&desc_text,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
// Skip leading blank line from wrap_text since we already added one
|
||||
if desc.starts_with('\n') {
|
||||
@@ -1060,14 +1054,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
self.content_lines.push(tag_line);
|
||||
let indent = self.continuation_indent();
|
||||
let indent_width = self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&desc_text,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
return;
|
||||
@@ -1091,14 +1084,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
let indent = self.continuation_indent();
|
||||
let indent_width =
|
||||
self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
desc_part,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
return;
|
||||
@@ -1179,14 +1171,13 @@ impl JsdocFormatter<'_, '_> {
|
||||
let indent_width =
|
||||
self.wrap_width.saturating_sub(self.continuation_indent_width());
|
||||
let tag_str_len = prefix_len.saturating_sub(self.continuation_indent_width());
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
raw_ws_desc,
|
||||
indent_width,
|
||||
tag_str_len,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
let mut iter = desc.split('\n');
|
||||
if let Some(first) = iter.next() {
|
||||
@@ -1220,25 +1211,23 @@ impl JsdocFormatter<'_, '_> {
|
||||
let first_word_w = desc_text.split_whitespace().next().map_or(0, str_width);
|
||||
if prefix_len + first_word_w > self.wrap_width {
|
||||
self.content_lines.push(tag_line);
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&desc_text,
|
||||
indent_width,
|
||||
0,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
self.push_indented_desc(indent, desc);
|
||||
} else {
|
||||
let desc = wrap_text(
|
||||
let desc = format_description_mdast(
|
||||
&desc_text,
|
||||
indent_width,
|
||||
tag_str_len,
|
||||
false,
|
||||
Some(self.format_options),
|
||||
Some(self.allocator),
|
||||
self.string_embedder,
|
||||
self.format_options,
|
||||
self.session,
|
||||
);
|
||||
let mut iter = desc.split('\n');
|
||||
if let Some(first) = iter.next() {
|
||||
|
||||
@@ -454,37 +454,33 @@ fn flush_paragraph_balance(
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap text into lines, preserving structured content (lists, code blocks, tables, etc.)
|
||||
/// and wrapping plain paragraphs to the given max width.
|
||||
///
|
||||
/// Delegates to `format_description_mdast` for full markdown-aware formatting.
|
||||
pub fn wrap_text(
|
||||
text: &str,
|
||||
max_width: usize,
|
||||
tag_string_length: usize,
|
||||
capitalize: bool,
|
||||
format_options: Option<&crate::JsFormatOptions>,
|
||||
allocator: Option<&oxc_allocator::Allocator>,
|
||||
string_embedder: Option<&oxc_formatter_core::StringEmbedder>,
|
||||
) -> String {
|
||||
if text.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
super::mdast_serialize::format_description_mdast(
|
||||
text,
|
||||
max_width,
|
||||
tag_string_length,
|
||||
capitalize,
|
||||
format_options,
|
||||
allocator,
|
||||
string_embedder,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Thin harness over `format_description_mdast` with default options
|
||||
/// on a service-less `Fragment` session.
|
||||
fn wrap_text(
|
||||
text: &str,
|
||||
max_width: usize,
|
||||
tag_string_length: usize,
|
||||
capitalize: bool,
|
||||
) -> String {
|
||||
let allocator = oxc_allocator::Allocator::default();
|
||||
let session = oxc_formatter_core::FormatSession::new(
|
||||
&allocator,
|
||||
oxc_formatter_core::InputKind::Fragment,
|
||||
);
|
||||
crate::formatter::jsdoc::mdast_serialize::format_description_mdast(
|
||||
text,
|
||||
max_width,
|
||||
tag_string_length,
|
||||
capitalize,
|
||||
&crate::JsFormatOptions::default(),
|
||||
&session,
|
||||
)
|
||||
}
|
||||
|
||||
// Pins the JS-`.length` semantics (see `str_width` doc):
|
||||
// the jsdoc conformance still passes even with `unicode_width`-based width.
|
||||
#[test]
|
||||
@@ -499,7 +495,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_wrap_simple_text() {
|
||||
let result = wrap_text("This is a short line", 80, 0, false, None, None, None);
|
||||
let result = wrap_text("This is a short line", 80, 0, false);
|
||||
assert_eq!(result, "This is a short line");
|
||||
}
|
||||
|
||||
@@ -510,9 +506,6 @@ mod tests {
|
||||
40,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -524,8 +517,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_wrap_preserves_markdown_list() {
|
||||
let result =
|
||||
wrap_text("- item one\n- item two\n- item three", 80, 0, false, None, None, None);
|
||||
let result = wrap_text("- item one\n- item two\n- item three", 80, 0, false);
|
||||
assert_eq!(result, "- item one\n- item two\n- item three");
|
||||
}
|
||||
|
||||
@@ -536,9 +528,6 @@ mod tests {
|
||||
40,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -548,42 +537,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_wrap_converts_code_fence_to_indented() {
|
||||
let result = wrap_text(
|
||||
"Some text\n```\ncode here\n indented\n```\nMore text",
|
||||
80,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let result =
|
||||
wrap_text("Some text\n```\ncode here\n indented\n```\nMore text", 80, 0, false);
|
||||
// Fenced code without language tag is converted to indented code block.
|
||||
assert_eq!(result, "Some text\n\n code here\n indented\n\nMore text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_preserves_code_fence_with_language() {
|
||||
let result = wrap_text(
|
||||
"Some text\n```js\nconst x = 1;\n```\nMore text",
|
||||
80,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let result = wrap_text("Some text\n```js\nconst x = 1;\n```\nMore text", 80, 0, false);
|
||||
assert_eq!(result, "Some text\n\n```js\nconst x = 1;\n```\n\nMore text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_empty_lines() {
|
||||
let result = wrap_text("Paragraph one\n\nParagraph two", 80, 0, false, None, None, None);
|
||||
let result = wrap_text("Paragraph one\n\nParagraph two", 80, 0, false);
|
||||
assert_eq!(result, "Paragraph one\n\nParagraph two");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_empty_text() {
|
||||
let result = wrap_text("", 80, 0, false, None, None, None);
|
||||
let result = wrap_text("", 80, 0, false);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
@@ -591,8 +565,7 @@ mod tests {
|
||||
fn test_numbered_spread_list_collapses_blank_lines() {
|
||||
// Upstream prettier-plugin-jsdoc removes blank lines between list items
|
||||
// even when the source has them (spread lists).
|
||||
let result =
|
||||
wrap_text("1. Thing 1\n\n2. Thing 2\n\n3. Thing 3", 80, 0, false, None, None, None);
|
||||
let result = wrap_text("1. Thing 1\n\n2. Thing 2\n\n3. Thing 3", 80, 0, false);
|
||||
assert_eq!(result, "1. Thing 1\n2. Thing 2\n3. Thing 3");
|
||||
}
|
||||
|
||||
@@ -603,9 +576,6 @@ mod tests {
|
||||
77,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -621,9 +591,6 @@ mod tests {
|
||||
77,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -639,9 +606,6 @@ mod tests {
|
||||
97,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -660,9 +624,6 @@ mod tests {
|
||||
77,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -675,9 +636,6 @@ mod tests {
|
||||
77,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
@@ -690,9 +648,6 @@ mod tests {
|
||||
77,
|
||||
0,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
result.contains('\n'),
|
||||
@@ -704,8 +659,7 @@ mod tests {
|
||||
fn test_spread_list_collapses_blank_lines() {
|
||||
// Upstream prettier-plugin-jsdoc removes blank lines between list items
|
||||
// even when the source has them (spread lists).
|
||||
let result =
|
||||
wrap_text("- item one\n\n- item two\n\n- item three", 80, 0, false, None, None, None);
|
||||
let result = wrap_text("- item one\n\n- item two\n\n- item three", 80, 0, false);
|
||||
assert_eq!(result, "- item one\n- item two\n- item three");
|
||||
}
|
||||
|
||||
@@ -715,7 +669,7 @@ mod tests {
|
||||
let input =
|
||||
"The options object:\n\n const result = process(options);\n console.log(result);";
|
||||
// Simulate @param {object} options: tag_str_len=22, indent_width=78
|
||||
let result = wrap_text(input, 78, 22, false, None, None, None);
|
||||
let result = wrap_text(input, 78, 22, false);
|
||||
// The indented code block should be preserved with 4-space indent
|
||||
assert!(
|
||||
result.contains(" const result = process(options);"),
|
||||
|
||||
@@ -199,8 +199,7 @@ impl<'a> FormatSession<'a> {
|
||||
}
|
||||
|
||||
/// The string channel service ([`StringEmbedder`]), when this run installs one.
|
||||
/// (A handle rather than an invoking method: the JSDoc serializer threads it
|
||||
/// through session-less layers; see the alias for the `Err`-means-verbatim contract.)
|
||||
/// (A handle rather than an invoking method; see the alias for the `Err`-means-verbatim contract.)
|
||||
pub fn string_embedder(&self) -> Option<&StringEmbedder> {
|
||||
self.services.string_embedder.as_ref()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user