mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter_css): keep a same-line line comment on its line (#26321)
Consistent output with other language formatters.
This commit is contained in:
1 parent
cafaf32de1
commit
172ddb67dc
10 files changed
+674
-77
No files matched your search
@@ -104,6 +104,8 @@ The invariants (placement only; losing a comment is the lossless contract under
|
||||
- Terminator vs separator: a terminator cannot be replaced by another token (`;` after a JS statement); a separator can (`,` / `;` between TS interface members)
|
||||
- The replaceability test only separates these two
|
||||
- Comments may move behind a terminator (per-language compat tables decide when); they always stay before a separator
|
||||
- Except a same-line line comment: it rides a `line_suffix` and lands just past the separator (`a // c\n, b` -> `a, // c`), the separator cannot follow it on the line;
|
||||
- an own-line comment leads what follows the separator instead
|
||||
- Grammar-fixed DELIMITER (braces, a head's parens) is neither: it bounds a region and stays user content, never crossed
|
||||
- (JS/TS) Redundant expression parentheses are NOT delimiters: the formatter drops them and re-derives parens by its own rules, so any paren in the output is formatter-owned
|
||||
- Trailing comment inside the dropped pair moves behind the terminator, even across a re-printed pair
|
||||
|
||||
@@ -79,6 +79,10 @@ The shared invariants (FORMATTER_POLICY.md "Comment placement invariants") apply
|
||||
- Line-boundary rule in CSS terms: `//` comments force a hardline after;
|
||||
- a `//` on a list `,`'s line stays there (`1, // c`, like JS), together with the block comments glued before it on that line;
|
||||
Prettier's CSS moves it below as the next element's leading comment (DIVERGENCES.md "line-comment-after-comma")
|
||||
- a `//` before a list `,` rides past it (`a // c\n, b` -> `a, // c`), an own-line comment there leads the next element;
|
||||
a `//` glued to a prelude's end stays there and `{` starts the next line (DIVERGENCES.md "line-comment-before-block")
|
||||
- a structured prelude printer places its own comments, word by word (`value::write_with_comments`: own-line ones lead the word, glued ones trail it),
|
||||
and `write_at_rule` writes whatever is still pending before the `;` / `{`, so a prelude comment is never lost
|
||||
- own-line comments stay own-line at statement and trailing level, but a value-level own-line BLOCK comment is a plain fill item (joins the line when it fits):
|
||||
- it carries no line-based semantics, and freezing it own-line would pin a wrapped layout (= not idempotent)
|
||||
|
||||
|
||||
@@ -563,11 +563,16 @@ Its JS printer follows the option (`[1, 2\n // own]`), and so do all our formatt
|
||||
);
|
||||
```
|
||||
|
||||
An own-line trailing comment before a closing `)` keeps its own line:
|
||||
in maps and `@use`/`@forward with (...)` configs (any comment kind),
|
||||
in map-item lists (any kind, the body is already one item per line),
|
||||
and in paren lists, call/`@include` arguments and `@mixin` parameters
|
||||
(`//` only: an own-line block comment in a fill body is a fill item, see AGENTS.md).
|
||||
An own-line trailing comment before a closing `)` keeps its own line
|
||||
(so does an own-line comment before a list comma: it leads the next element, `a,\n // c\n b`, pinned in `line-comment-before-comma.scss`):
|
||||
- maps
|
||||
- `@use`/`@forward with (...)` configs (any comment kind)
|
||||
- map-item lists (any kind, the body is already one item per line)
|
||||
- paren lists
|
||||
- call/`@include` arguments
|
||||
- `@mixin` parameters
|
||||
|
||||
`//` only: an own-line block comment in a fill body is a fill item, see AGENTS.md.
|
||||
|
||||
Prettier's output changes line, own-line to the last item's line, a `lineSuffix` artifact of its comma-group printing.
|
||||
Same-line trailing comments still glue (matching Prettier); moving an own-line comment up would destroy the author's visual grouping.
|
||||
@@ -699,6 +704,66 @@ Applies at every comma site: values, function and `@include` arguments, maps, pa
|
||||
`@import` paths and modifiers, `@layer`, `@custom-selector`, `@media` query lists and `selector()` lists.
|
||||
For `@media` Prettier's move also swallows the next query (`@media a, // c b {`), a semantics bug on its side.
|
||||
|
||||
## line-comment-before-block
|
||||
|
||||
- Why: invariant
|
||||
- Pin: `tests/fixtures/format/scss/at-rule-comment-before-block.scss`
|
||||
|
||||
```scss
|
||||
/* input */
|
||||
@supports (a: b) // c
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ours */
|
||||
@supports (a: b) // c
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* prettier */
|
||||
@supports (a: b) { // c
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
A `//` glued to the end of an at-rule prelude stays on that line and the `{` starts the next;
|
||||
Prettier moves it past the `{` as the block's first comment for `@media` / `@supports` / `@mixin` / `@include`
|
||||
(and keeps it before the `{` for `@page` / `@keyframes` / `@font-face` / `@if`):
|
||||
the comment crosses the `{`, a grammar-fixed delimiter.
|
||||
For `@media screen // c {` and `@else // c` Prettier's move also swallows the `{` / the comment text, bugs on its side.
|
||||
|
||||
## line-comment-before-comma-fill-head
|
||||
|
||||
- Why: uniform-rule (same construct, same output: `@each $k in a, // c` with the `//` after the comma)
|
||||
- Pin: `tests/fixtures/format/scss/line-comment-before-comma.scss`
|
||||
|
||||
```scss
|
||||
/* input */
|
||||
@each $k in a // c
|
||||
, b {
|
||||
}
|
||||
|
||||
/* ours */
|
||||
@each $k in a, // c
|
||||
b
|
||||
{
|
||||
}
|
||||
|
||||
/* prettier */
|
||||
@each $k
|
||||
in a, // c
|
||||
b
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
A `//` before a list comma rides past the comma and ends the line there; the fill entries before it stay on their line,
|
||||
exactly as when the `//` follows the comma in the source.
|
||||
Prettier attaches a `breakParent` to the deferred comment, which its fill measures as never fitting,
|
||||
so the entry BEFORE the comment (`in a,`) breaks away from `$k` too; the same source with the `//` after the comma keeps `$k in a,`.
|
||||
|
||||
## semiless-custom-property-block
|
||||
|
||||
- Why: cost
|
||||
|
||||
@@ -195,18 +195,11 @@ pub(super) fn write_at_rule<'a>(at_rule: &AtRule<'a>, f: &mut CssFormatter<'_, '
|
||||
// Whatever the prelude printer left pending (before `region_end`: the `{` or the `;`)
|
||||
// is written here, so nothing inside the prelude is lost to the statement loop's cursor guard.
|
||||
if let Some(block) = &at_rule.block {
|
||||
// An inline comment between the prelude and `{` keeps its own line
|
||||
// (Prettier's `lastLineHasInlineComment` → line instead of space).
|
||||
if f.context().comments().peek().is_some_and(|c| c.inline && c.span.end <= region_end) {
|
||||
write!(f, hard_line_break());
|
||||
for &comment in f.context().comments().take_before(region_end) {
|
||||
write!(f, FormatCommentBeforeContent::new(comment, BlockCommentAfter::HardLine));
|
||||
}
|
||||
} else {
|
||||
value::flush_trailing_value_comments(region_end, f);
|
||||
if !is_control_directive {
|
||||
write!(f, space());
|
||||
}
|
||||
let prelude_end =
|
||||
at_rule.prelude.as_ref().map_or(name_span.end, |prelude| to_span(prelude.span()).end);
|
||||
value::write_comments_before_block(prelude_end, region_end, f);
|
||||
if !is_control_directive {
|
||||
write!(f, space());
|
||||
}
|
||||
statement::write_block(block, f);
|
||||
} else {
|
||||
@@ -786,8 +779,10 @@ fn write_at_rule_prelude<'a>(prelude: &AtRulePrelude<'a>, f: &mut CssFormatter<'
|
||||
let body = format_with(move |f: &mut CssFormatter<'_, 'a>| {
|
||||
if let Some(prefix) = &namespace.prefix {
|
||||
let span = to_span(prefix.span());
|
||||
write!(f, text(source.text_for(&span)));
|
||||
value::flush_glued_comments(span.end, to_span(namespace.span()).end, f);
|
||||
let prelude_end = to_span(namespace.span()).end;
|
||||
value::write_with_comments(span, prelude_end, f, |f| {
|
||||
write!(f, text(source.text_for(&span)));
|
||||
});
|
||||
write!(f, space());
|
||||
value::flush_value_comments(to_span(namespace.uri.span()).start, f);
|
||||
}
|
||||
@@ -1473,18 +1468,15 @@ fn write_import_prelude_inner<'a>(import: &ImportPrelude<'a>, f: &mut CssFormatt
|
||||
}
|
||||
// A `//` between the parts breaks the prelude either way
|
||||
let prelude_end = to_span(import.span()).end;
|
||||
let write_part = |span: oxc_span::Span,
|
||||
f: &mut CssFormatter<'_, 'a>,
|
||||
write: &dyn Fn(&mut CssFormatter<'_, 'a>)| {
|
||||
value::flush_value_comments(span.start, f);
|
||||
write(f);
|
||||
value::flush_glued_comments(span.end, prelude_end, f);
|
||||
};
|
||||
write_part(to_span(import.href.span()), f, &|f| write_import_href(&import.href, f));
|
||||
value::write_with_comments(to_span(import.href.span()), prelude_end, f, |f| {
|
||||
write_import_href(&import.href, f);
|
||||
});
|
||||
if let Some(layer) = &import.layer {
|
||||
write!(f, space());
|
||||
let span = to_span(layer.span());
|
||||
write_part(span, f, &|f| write!(f, text(source.text_for(&span))));
|
||||
value::write_with_comments(span, prelude_end, f, |f| {
|
||||
write!(f, text(source.text_for(&span)));
|
||||
});
|
||||
}
|
||||
if let Some(supports) = &import.supports {
|
||||
// `@import ... supports(<cond>)`.
|
||||
@@ -1496,7 +1488,7 @@ fn write_import_prelude_inner<'a>(import: &ImportPrelude<'a>, f: &mut CssFormatt
|
||||
// plus one of our own (a width-overflowing condition with no trailing media breaks INSIDE the parens, not before `supports`).
|
||||
// Empty `supports()` was the prior data-loss stub.
|
||||
write!(f, space());
|
||||
write_part(to_span(supports.span()), f, &|f| {
|
||||
value::write_with_comments(to_span(supports.span()), prelude_end, f, |f| {
|
||||
write!(f, "supports(");
|
||||
match &supports.kind {
|
||||
// `supports(not (display: inline-grid))`, `supports(font-format(woff2))`
|
||||
@@ -1649,10 +1641,12 @@ fn write_media_query_list<'a>(list: &MediaQueryList<'a>, f: &mut CssFormatter<'_
|
||||
let body = format_with(move |f: &mut CssFormatter<'_, 'a>| {
|
||||
write_media_query_list_inner(list, f);
|
||||
});
|
||||
// The indent only governs the inter-query `,`-breaks; a single query never breaks internally,
|
||||
// so dropping it avoids leaking a level into an embedded `${}` placeholder break
|
||||
// (a media value is flat text, like Prettier).
|
||||
if list.queries.len() > 1 {
|
||||
// The indent governs the inter-query `,`-breaks and a `//` break inside a query;
|
||||
// without either a single query never breaks, and dropping the indent avoids
|
||||
// leaking a level into an embedded `${}` placeholder break (a media value is flat text, like Prettier).
|
||||
let breaks_inside =
|
||||
f.context().comments().iter_before(to_span(list.span()).end).any(|c| c.inline);
|
||||
if list.queries.len() > 1 || breaks_inside {
|
||||
write!(f, group(&indent(&body)));
|
||||
} else {
|
||||
write!(f, group(&body));
|
||||
@@ -1676,18 +1670,23 @@ fn write_media_query<'a>(query: &MediaQuery<'a>, f: &mut CssFormatter<'_, 'a>) {
|
||||
match query {
|
||||
MediaQuery::ConditionOnly(condition) => write_media_condition(condition, f),
|
||||
MediaQuery::WithType(with_type) => {
|
||||
// A `//` among the words breaks the query
|
||||
let query_end = to_span(query.span()).end;
|
||||
let write_word = |span: oxc_span::Span, f: &mut CssFormatter<'_, 'a>| {
|
||||
value::write_with_comments(span, query_end, f, |f| {
|
||||
write_maybe_lowercase(source.text_for(&span), f);
|
||||
});
|
||||
};
|
||||
if let Some(modifier) = &with_type.modifier {
|
||||
let span = to_span(modifier.span());
|
||||
write_maybe_lowercase(source.text_for(&span), f);
|
||||
write_word(to_span(modifier.span()), f);
|
||||
write!(f, space());
|
||||
}
|
||||
let span = to_span(with_type.media_type.span());
|
||||
write_maybe_lowercase(source.text_for(&span), f);
|
||||
write_word(to_span(with_type.media_type.span()), f);
|
||||
if let Some(condition) = &with_type.condition {
|
||||
write!(f, space());
|
||||
let and_span = to_span(condition.and.span());
|
||||
write_maybe_lowercase(source.text_for(&and_span), f);
|
||||
write_word(to_span(condition.and.span()), f);
|
||||
write!(f, space());
|
||||
value::flush_value_comments(to_span(condition.condition.span()).start, f);
|
||||
write_media_condition(&condition.condition, f);
|
||||
}
|
||||
}
|
||||
@@ -1700,11 +1699,12 @@ fn write_media_query<'a>(query: &MediaQuery<'a>, f: &mut CssFormatter<'_, 'a>) {
|
||||
|
||||
fn write_media_condition<'a>(condition: &MediaCondition<'a>, f: &mut CssFormatter<'_, 'a>) {
|
||||
let source = f.context().source_text();
|
||||
let condition_end = to_span(condition.span()).end;
|
||||
for (i, kind) in condition.conditions.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, space());
|
||||
}
|
||||
match kind {
|
||||
value::write_with_comments(to_span(kind.span()), condition_end, f, |f| match kind {
|
||||
MediaConditionKind::MediaInParens(in_parens) => {
|
||||
write_media_in_parens(in_parens, f);
|
||||
}
|
||||
@@ -1726,7 +1726,7 @@ fn write_media_condition<'a>(condition: &MediaCondition<'a>, f: &mut CssFormatte
|
||||
write!(f, space());
|
||||
write_media_in_parens(¬.media_in_parens, f);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1866,6 +1866,7 @@ fn write_supports_condition<'a>(condition: &SupportsCondition<'a>, f: &mut CssFo
|
||||
// A fill of keywords and parenthesized terms:
|
||||
// a long condition breaks AFTER `and`/`or`, one indent in
|
||||
// (`postcss-values` prints the params as a value group, so each word/paren is its own fill entry).
|
||||
let condition_end = to_span(condition.span()).end;
|
||||
let body = format_with(move |f: &mut CssFormatter<'_, 'a>| {
|
||||
let mut filler = f.fill();
|
||||
for kind in &condition.conditions {
|
||||
@@ -1875,15 +1876,20 @@ fn write_supports_condition<'a>(condition: &SupportsCondition<'a>, f: &mut CssFo
|
||||
SupportsConditionKind::Or(or) => (Some(&or.keyword), &or.condition),
|
||||
SupportsConditionKind::Not(not) => (Some(¬.keyword), ¬.condition),
|
||||
};
|
||||
// A `//` among the entries breaks the condition
|
||||
if let Some(keyword) = keyword {
|
||||
let kw = format_with(move |f: &mut CssFormatter<'_, 'a>| {
|
||||
let span = to_span(keyword.span());
|
||||
write_maybe_lowercase(f.context().source_text().text_for(&span), f);
|
||||
value::write_with_comments(span, condition_end, f, |f| {
|
||||
write_maybe_lowercase(f.context().source_text().text_for(&span), f);
|
||||
});
|
||||
});
|
||||
filler.entry(&soft_line_break_or_space(), &kw);
|
||||
}
|
||||
let term = format_with(move |f: &mut CssFormatter<'_, 'a>| {
|
||||
write_supports_in_parens(in_parens, f);
|
||||
value::write_with_comments(to_span(in_parens.span()), condition_end, f, |f| {
|
||||
write_supports_in_parens(in_parens, f);
|
||||
});
|
||||
});
|
||||
filler.entry(&soft_line_break_or_space(), &term);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use oxc_css_parser::ast::{
|
||||
ComponentValue, InterpolableStr, SassEach, SassFor, SassForBoundaryKind, SassForward,
|
||||
SassForwardVisibilityModifierKind, SassFunction, SassIfAtRule, SassInclude, SassList, SassMap,
|
||||
SassMixin, SassModuleConfig, SassParameters, SassUnaryOperatorKind, SassUse,
|
||||
SassUseNamespaceKind, SassVariableDeclaration,
|
||||
SassUseNamespaceKind, SassVariableDeclaration, SimpleBlock,
|
||||
};
|
||||
use oxc_formatter_core::{
|
||||
Buffer,
|
||||
@@ -681,7 +681,7 @@ pub(super) fn write_sass_include<'a>(include: &SassInclude<'a>, f: &mut CssForma
|
||||
pub(super) fn write_sass_if_at_rule<'a>(if_rule: &SassIfAtRule<'a>, f: &mut CssFormatter<'_, 'a>) {
|
||||
write!(f, ["@if", space()]);
|
||||
write_control_condition(&if_rule.if_clause.condition, f);
|
||||
statement::write_block(&if_rule.if_clause.block, f);
|
||||
write_block_after(to_span(if_rule.if_clause.condition.span()).end, &if_rule.if_clause.block, f);
|
||||
for (clause, else_span) in if_rule.else_if_clauses.iter().zip(&if_rule.else_spans) {
|
||||
write_else_join(to_span(else_span).start, f);
|
||||
// `if` is a value word in postcss, so the condition may break after it
|
||||
@@ -691,18 +691,23 @@ pub(super) fn write_sass_if_at_rule<'a>(if_rule: &SassIfAtRule<'a>, f: &mut CssF
|
||||
} else {
|
||||
write_condition_chain(Some("if"), &clause.condition, f);
|
||||
}
|
||||
statement::write_block(&clause.block, f);
|
||||
write_block_after(to_span(clause.condition.span()).end, &clause.block, f);
|
||||
}
|
||||
if let Some(else_block) = &if_rule.else_clause {
|
||||
let else_start = if_rule
|
||||
.else_spans
|
||||
.last()
|
||||
.map_or_else(|| to_span(&else_block.span).start, |sp| to_span(sp).start);
|
||||
write_else_join(else_start, f);
|
||||
statement::write_block(else_block, f);
|
||||
let block_start = to_span(&else_block.span).start;
|
||||
let else_span = if_rule.else_spans.last().map_or(Span::empty(block_start), to_span);
|
||||
write_else_join(else_span.start, f);
|
||||
write_block_after(else_span.end, else_block, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// A clause's block, with the comments between its head (ending at `head_end`)
|
||||
/// and `{` placed first (`@if $a // c\n{`), so they do not fall into the block as its first leading comment.
|
||||
fn write_block_after<'a>(head_end: u32, block: &SimpleBlock<'a>, f: &mut CssFormatter<'_, 'a>) {
|
||||
value::write_comments_before_block(head_end, to_span(&block.span).start, f);
|
||||
statement::write_block(block, f);
|
||||
}
|
||||
|
||||
/// `} @else`: comments between them break the join; each keeps its line
|
||||
/// (`} /* c */` stays on the brace line, an own-line comment stays own-line).
|
||||
/// `else_start` (the keyword) bounds the take, so a comment after it leads the condition
|
||||
@@ -904,10 +909,10 @@ pub(super) fn write_sass_forward<'a>(forward: &SassForward<'a>, f: &mut CssForma
|
||||
let entry = format_with(move |f: &mut CssFormatter<'_, 'a>| {
|
||||
value::write_text_with_leading_comments(to_span(member.span()), f);
|
||||
if i + 1 < members.len() {
|
||||
let comma = to_span(&visibility.comma_spans[i]).start;
|
||||
write_same_line_trailing_comments(comma, f);
|
||||
write!(f, ",");
|
||||
value::flush_line_comment_after_comma(comma, f);
|
||||
value::write_group_comma(
|
||||
Some(to_span(&visibility.comma_spans[i]).start),
|
||||
f,
|
||||
);
|
||||
}
|
||||
});
|
||||
filler.entry(&soft_line_break_or_space(), &entry);
|
||||
@@ -990,12 +995,7 @@ fn write_sass_module_config<'a>(config: &SassModuleConfig<'a>, f: &mut CssFormat
|
||||
write!(f, text(source.text_for(&span)));
|
||||
}
|
||||
if i + 1 < config.items.len() {
|
||||
// An own-line comment before the comma stays pending and
|
||||
// leads the next item instead.
|
||||
let comma = to_span(&config.comma_spans[i]).start;
|
||||
write_same_line_trailing_comments(comma, f);
|
||||
write!(f, ",");
|
||||
value::flush_line_comment_after_comma(comma, f);
|
||||
value::write_group_comma(Some(to_span(&config.comma_spans[i]).start), f);
|
||||
} else {
|
||||
// Comments before `)` (past a trailing comma, which is dropped):
|
||||
// same-line ones glue to the last item, own-line ones keep their line.
|
||||
|
||||
@@ -37,7 +37,7 @@ use oxc_span::Span;
|
||||
|
||||
use crate::{
|
||||
CssFormatOptions,
|
||||
comments::{self, BlockCommentAfter, FormatCommentBeforeContent},
|
||||
comments::{self, BlockCommentAfter, FormatCommentBeforeContent, FormatLineCommentSuffix},
|
||||
format::to_span,
|
||||
print::{CssFormatter, format_with, less, scss, statement},
|
||||
};
|
||||
@@ -596,17 +596,45 @@ pub(super) fn comma_group_is_multi(group: &[ComponentValue<'_>], is_first: bool)
|
||||
}
|
||||
|
||||
/// A group's separator comma:
|
||||
/// comments between the group and its comma stay before the comma (`a /* c */, b`);
|
||||
/// a same-line `//` right after it stays on its line (`a, // c`, the line-boundary invariant),
|
||||
/// only comments past those lead the next group.
|
||||
/// a block comment between the group and its comma stays before the comma (`a /* c */, b`),
|
||||
/// a same-line `//` there rides past it (`a // c\n,` -> `a, // c`: the comma cannot follow a `//`),
|
||||
/// a same-line `//` right after it stays on its line (`a, // c`);
|
||||
/// own-line comments and anything past those lead the next group (the line-boundary invariant).
|
||||
pub(super) fn write_group_comma(comma_start: Option<u32>, f: &mut CssFormatter<'_, '_>) {
|
||||
let Some(comma) = comma_start else {
|
||||
write!(f, ",");
|
||||
return;
|
||||
};
|
||||
flush_trailing_value_comments(comma, f);
|
||||
let line_comment_deferred = flush_comments_before_separator(comma, f);
|
||||
write!(f, ",");
|
||||
flush_line_comment_after_comma(comma, f);
|
||||
if line_comment_deferred {
|
||||
// Flushes the deferred `//` right behind the comma and ends the line.
|
||||
// A hardline, not `expand_parent`:
|
||||
// inside a `fill` the latter would also break the separator BEFORE this entry (`@each $k\n in a, // c`),
|
||||
// the former keeps it (`@each $k in a, // c`).
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
}
|
||||
|
||||
/// The same-line comments before a separator at `separator_start`:
|
||||
/// block comments glue before it, a `//` rides a `line_suffix` past it (its line ends after the separator).
|
||||
/// Own-line comments stay pending, they lead what follows the separator.
|
||||
/// Returns `true` when a `//` was deferred: the caller must end the line right after the separator.
|
||||
fn flush_comments_before_separator(separator_start: u32, f: &mut CssFormatter<'_, '_>) -> bool {
|
||||
flush_trailing_comments_while(0, separator_start, LineCommentAs::Suffix, f, |_, c, source| {
|
||||
!comment_is_own_line(c, source)
|
||||
})
|
||||
}
|
||||
|
||||
/// How a trailing `//` is emitted by [`flush_trailing_comments_while`].
|
||||
#[derive(Clone, Copy)]
|
||||
enum LineCommentAs {
|
||||
/// In place, ending the line (`FormatCommentBeforeContent`)
|
||||
InPlace,
|
||||
/// A `line_suffix`: the token written next (a separator) precedes it on the line;
|
||||
/// the caller ends the line after that token
|
||||
Suffix,
|
||||
}
|
||||
|
||||
/// A `//` on the line of the comma just written (`a, // c`, `a, /* b */ // c`) stays on that line,
|
||||
@@ -626,37 +654,83 @@ pub(super) fn flush_line_comment_after_comma(comma_start: u32, f: &mut CssFormat
|
||||
/// as ` /* c */` / ` // c`, stopping after a `//` (it ends the line).
|
||||
/// Unlike [`flush_same_line_comments`] a comment further along the line, past other tokens, stays pending.
|
||||
pub(super) fn flush_glued_comments(prev_end: u32, upper_bound: u32, f: &mut CssFormatter<'_, '_>) {
|
||||
flush_trailing_comments_while(prev_end, upper_bound, f, |prev_end, comment, source| {
|
||||
source.all_bytes_match(prev_end, comment.span.start, |b| matches!(b, b' ' | b'\t'))
|
||||
});
|
||||
flush_trailing_comments_while(
|
||||
prev_end,
|
||||
upper_bound,
|
||||
LineCommentAs::InPlace,
|
||||
f,
|
||||
|prev_end, comment, source| {
|
||||
source.all_bytes_match(prev_end, comment.span.start, |b| matches!(b, b' ' | b'\t'))
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// A word with the comments around it: own-line ones before it lead it (`flush_value_comments`),
|
||||
/// the run glued after it trails it (`flush_glued_comments`, bounded by `upper_bound`).
|
||||
/// The shape of every structured prelude word (`@import` parts, `@media` words, `@supports` terms, ...).
|
||||
pub(super) fn write_with_comments<'a>(
|
||||
span: Span,
|
||||
upper_bound: u32,
|
||||
f: &mut CssFormatter<'_, 'a>,
|
||||
write: impl FnOnce(&mut CssFormatter<'_, 'a>),
|
||||
) {
|
||||
flush_value_comments(span.start, f);
|
||||
write(f);
|
||||
flush_glued_comments(span.end, upper_bound, f);
|
||||
}
|
||||
|
||||
/// Emits pending comments before `upper_bound` as ` /* c */` / ` // c` while `keep` accepts them
|
||||
/// (`prev_end` is the previous comment's end from the second one on), stopping after a `//`.
|
||||
/// Returns `true` when it stopped at a `//`.
|
||||
fn flush_trailing_comments_while(
|
||||
mut prev_end: u32,
|
||||
upper_bound: u32,
|
||||
line_comment_as: LineCommentAs,
|
||||
f: &mut CssFormatter<'_, '_>,
|
||||
keep: impl Fn(u32, comments::CssComment, SourceText<'_>) -> bool,
|
||||
) {
|
||||
) -> bool {
|
||||
loop {
|
||||
let Some(comment) = f.context().comments().peek() else { return };
|
||||
let Some(comment) = f.context().comments().peek() else { return false };
|
||||
let source = f.context().source_text();
|
||||
if comment.span.start < prev_end
|
||||
|| comment.span.end > upper_bound
|
||||
|| !keep(prev_end, comment, source)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
f.context().comments().take_before(comment.span.end);
|
||||
if comment.inline && matches!(line_comment_as, LineCommentAs::Suffix) {
|
||||
write!(f, FormatLineCommentSuffix::new(comment).with_leading_space());
|
||||
return true;
|
||||
}
|
||||
write!(f, [space(), FormatCommentBeforeContent::new(comment, BlockCommentAfter::None)]);
|
||||
if comment.inline {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
prev_end = comment.span.end;
|
||||
}
|
||||
}
|
||||
|
||||
/// Comments between a prelude ending at `prelude_end` and its `{` at `block_start`:
|
||||
/// a run glued to the prelude stays on its line (`@page :first // c`, the `//` ends it so `{` starts the next),
|
||||
/// own-line ones keep their line before the `{`.
|
||||
/// Either way the caller's space before `{` is safe: the printer drops it at the start of a line.
|
||||
pub(super) fn write_comments_before_block(
|
||||
prelude_end: u32,
|
||||
block_start: u32,
|
||||
f: &mut CssFormatter<'_, '_>,
|
||||
) {
|
||||
flush_glued_comments(prelude_end, block_start, f);
|
||||
let own_line = f.context().comments().take_before(block_start);
|
||||
if own_line.is_empty() {
|
||||
return;
|
||||
}
|
||||
write!(f, hard_line_break());
|
||||
for &comment in own_line {
|
||||
write!(f, FormatCommentBeforeContent::new(comment, BlockCommentAfter::HardLine));
|
||||
}
|
||||
}
|
||||
|
||||
/// End of the comment run glued right after `prev_end` (only spaces/tabs between) that ends in a `//`;
|
||||
/// `None` when no `//` is glued there (a lone block comment leads the next element instead).
|
||||
/// `comments` are the pending comments in source order.
|
||||
@@ -922,9 +996,13 @@ pub(super) fn flush_same_line_comments(
|
||||
upper_bound: u32,
|
||||
f: &mut CssFormatter<'_, '_>,
|
||||
) {
|
||||
flush_trailing_comments_while(prev_end, upper_bound, f, |_, comment, source| {
|
||||
!comment_is_own_line(comment, source)
|
||||
});
|
||||
flush_trailing_comments_while(
|
||||
prev_end,
|
||||
upper_bound,
|
||||
LineCommentAs::InPlace,
|
||||
f,
|
||||
|_, c, source| !comment_is_own_line(c, source),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emits pending comments before `upper_bound` as ` /* c */` suffixes
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
|
||||
// so `{` starts the next one. Own-line comments keep their line before the `{`.
|
||||
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
|
||||
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
|
||||
@page :first // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* b */ // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* c */ {
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first
|
||||
// c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@font-face // c
|
||||
{
|
||||
a: b;
|
||||
}
|
||||
@media screen // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@supports (a: b) // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@mixin m($a) // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
.a {
|
||||
@include m // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@if $a // c
|
||||
{
|
||||
b: c;
|
||||
} @else // d
|
||||
{
|
||||
d: e;
|
||||
}
|
||||
@each $i in a, b // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
|
||||
@media screen // c
|
||||
and (min-width: 1px) {
|
||||
}
|
||||
@supports (display: grid) // c
|
||||
and (gap: 1px) {
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
---
|
||||
source: crates/oxc_formatter_css/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
|
||||
// so `{` starts the next one. Own-line comments keep their line before the `{`.
|
||||
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
|
||||
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
|
||||
@page :first // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* b */ // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* c */ {
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first
|
||||
// c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@font-face // c
|
||||
{
|
||||
a: b;
|
||||
}
|
||||
@media screen // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@supports (a: b) // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@mixin m($a) // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
.a {
|
||||
@include m // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@if $a // c
|
||||
{
|
||||
b: c;
|
||||
} @else // d
|
||||
{
|
||||
d: e;
|
||||
}
|
||||
@each $i in a, b // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
|
||||
@media screen // c
|
||||
and (min-width: 1px) {
|
||||
}
|
||||
@supports (display: grid) // c
|
||||
and (gap: 1px) {
|
||||
}
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
|
||||
// so `{` starts the next one. Own-line comments keep their line before the `{`.
|
||||
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
|
||||
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
|
||||
@page :first // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* b */ // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* c */ {
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first
|
||||
// c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@font-face // c
|
||||
{
|
||||
a: b;
|
||||
}
|
||||
@media screen // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@supports (a: b) // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@mixin m($a) // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
.a {
|
||||
@include m // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@if $a // c
|
||||
{
|
||||
b: c;
|
||||
} @else // d
|
||||
{
|
||||
d: e;
|
||||
}
|
||||
@each $i in a, b // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
|
||||
@media screen // c
|
||||
and (min-width: 1px) {
|
||||
}
|
||||
@supports (display: grid) // c
|
||||
and (gap: 1px) {
|
||||
}
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
// A comment glued to the end of a prelude stays on that line; a `//` there ends the line,
|
||||
// so `{` starts the next one. Own-line comments keep their line before the `{`.
|
||||
// Prettier agrees for `@page` / `@keyframes` / `@font-face` / `@if`, but moves the `//`
|
||||
// past the `{` for `@media` / `@supports` / `@mixin` / `@include` (`{ // c`, DIVERGENCES.md "line-comment-before-block").
|
||||
@page :first // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* b */ // c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first /* c */ {
|
||||
margin: 1px;
|
||||
}
|
||||
@page :first
|
||||
// c
|
||||
{
|
||||
margin: 1px;
|
||||
}
|
||||
@font-face // c
|
||||
{
|
||||
a: b;
|
||||
}
|
||||
@media screen // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@supports (a: b) // c
|
||||
{
|
||||
a {
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@mixin m($a) // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
.a {
|
||||
@include m // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
}
|
||||
@if $a // c
|
||||
{
|
||||
b: c;
|
||||
} @else // d
|
||||
{
|
||||
d: e;
|
||||
}
|
||||
@each $i in a, b // c
|
||||
{
|
||||
b: c;
|
||||
}
|
||||
// Inside a query / condition: a glued `//` trails its word, the rest continues one level in.
|
||||
@media screen // c
|
||||
and (min-width: 1px) {
|
||||
}
|
||||
@supports (display: grid) // c
|
||||
and (gap: 1px) {
|
||||
}
|
||||
|
||||
===================== End =====================
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
|
||||
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
|
||||
// A block comment stays before the comma; an own-line comment leads the next element
|
||||
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
|
||||
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
|
||||
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
|
||||
a {
|
||||
transition: a 1s // c
|
||||
, b 2s;
|
||||
transition: a 1s /* b */ // c
|
||||
, b 2s;
|
||||
transition: a 1s /* c */, b 2s;
|
||||
transition: a 1s
|
||||
// c
|
||||
, b 2s;
|
||||
b: f(a // c
|
||||
, b);
|
||||
@include m(a // c
|
||||
, b);
|
||||
}
|
||||
$m: (a: 1 // c
|
||||
, b: 2);
|
||||
$l: (a // c
|
||||
, b);
|
||||
@each $k in a // c
|
||||
, b {
|
||||
}
|
||||
@use "a" with ($a: 1 // c
|
||||
, $b: 2);
|
||||
@forward "b" show a // c
|
||||
, b;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
---
|
||||
source: crates/oxc_formatter_css/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
|
||||
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
|
||||
// A block comment stays before the comma; an own-line comment leads the next element
|
||||
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
|
||||
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
|
||||
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
|
||||
a {
|
||||
transition: a 1s // c
|
||||
, b 2s;
|
||||
transition: a 1s /* b */ // c
|
||||
, b 2s;
|
||||
transition: a 1s /* c */, b 2s;
|
||||
transition: a 1s
|
||||
// c
|
||||
, b 2s;
|
||||
b: f(a // c
|
||||
, b);
|
||||
@include m(a // c
|
||||
, b);
|
||||
}
|
||||
$m: (a: 1 // c
|
||||
, b: 2);
|
||||
$l: (a // c
|
||||
, b);
|
||||
@each $k in a // c
|
||||
, b {
|
||||
}
|
||||
@use "a" with ($a: 1 // c
|
||||
, $b: 2);
|
||||
@forward "b" show a // c
|
||||
, b;
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
|
||||
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
|
||||
// A block comment stays before the comma; an own-line comment leads the next element
|
||||
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
|
||||
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
|
||||
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
|
||||
a {
|
||||
transition:
|
||||
a 1s, // c
|
||||
b 2s;
|
||||
transition:
|
||||
a 1s /* b */, // c
|
||||
b 2s;
|
||||
transition:
|
||||
a 1s /* c */,
|
||||
b 2s;
|
||||
transition:
|
||||
a 1s,
|
||||
// c
|
||||
b 2s;
|
||||
b: f(
|
||||
a, // c
|
||||
b
|
||||
);
|
||||
@include m(
|
||||
a, // c
|
||||
b
|
||||
);
|
||||
}
|
||||
$m: (
|
||||
a: 1, // c
|
||||
b: 2,
|
||||
);
|
||||
$l: (
|
||||
a, // c
|
||||
b
|
||||
);
|
||||
@each $k in a, // c
|
||||
b
|
||||
{
|
||||
}
|
||||
@use "a" with (
|
||||
$a: 1, // c
|
||||
$b: 2
|
||||
);
|
||||
@forward "b" show a, // c
|
||||
b;
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
// A same-line `//` before a list comma rides past the comma (`a // c\n,` -> `a, // c`):
|
||||
// the comma cannot follow a `//`, and the comment keeps its line (JS prints the same).
|
||||
// A block comment stays before the comma; an own-line comment leads the next element
|
||||
// (Prettier pulls it up to the comma line, DIVERGENCES.md "own-line-trailing-comment-keeps-line").
|
||||
// In a fill (`@each`, `@forward`) the entries before the comment keep their line
|
||||
// (Prettier breaks `$k` / `in a,` apart, DIVERGENCES.md "line-comment-before-comma-fill-head").
|
||||
a {
|
||||
transition:
|
||||
a 1s, // c
|
||||
b 2s;
|
||||
transition:
|
||||
a 1s /* b */, // c
|
||||
b 2s;
|
||||
transition:
|
||||
a 1s /* c */,
|
||||
b 2s;
|
||||
transition:
|
||||
a 1s,
|
||||
// c
|
||||
b 2s;
|
||||
b: f(
|
||||
a, // c
|
||||
b
|
||||
);
|
||||
@include m(
|
||||
a, // c
|
||||
b
|
||||
);
|
||||
}
|
||||
$m: (
|
||||
a: 1, // c
|
||||
b: 2,
|
||||
);
|
||||
$l: (
|
||||
a, // c
|
||||
b
|
||||
);
|
||||
@each $k in a, // c
|
||||
b
|
||||
{
|
||||
}
|
||||
@use "a" with (
|
||||
$a: 1, // c
|
||||
$b: 2
|
||||
);
|
||||
@forward "b" show a, // c
|
||||
b;
|
||||
|
||||
===================== End =====================
|
||||
Reference in new issue
Block a user