mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter_markdown): keep container columns as spaces under useTabs (#27277)
Added new IR builder `space_align()`. It is a similar one to `prefix_align()`, but `space` prefix never converted to TAB with `useTabs: true`. This is useful for cases like: ````md # JS-in-MD In markdown, structures are indented by SPACE. - Indented ```js // But here, may be TAB indented ``` ````
This commit is contained in:
1 parent
16860185da
commit
dd72fbf1e8
14 files changed
+340
-81
No files matched your search
@@ -349,43 +349,24 @@ fn convert_align<'a>(
|
||||
out.push(FormatElement::Tag(Tag::EndDedent(DedentMode::Root)));
|
||||
Ok(())
|
||||
}
|
||||
// A visible string align is a prefix on every line
|
||||
// (markdown's blockquote `"> "` is the only one Prettier's own printers emit):
|
||||
// intern here if a plugin brings another.
|
||||
Value::String(s) if s == "> " => {
|
||||
out.push(FormatElement::Tag(Tag::StartPrefix(Prefix::new(&"> "))));
|
||||
// A string align is a prefix on every line, never turned into a tab under `useTabs`:
|
||||
// markdown's blockquote `"> "` (the only visible one Prettier's own printers emit, intern here if a plugin brings another)
|
||||
// and its container columns `" ".repeat(n)` (`""` aligns nothing)
|
||||
Value::String(s) if s == "> " || s.bytes().all(|b| b == b' ') => {
|
||||
let prefixes: Vec<Prefix> = if s == "> " {
|
||||
vec![Prefix::new(&"> ")]
|
||||
} else {
|
||||
Prefix::spaces(s.len()).collect()
|
||||
};
|
||||
for &prefix in &prefixes {
|
||||
out.push(FormatElement::Tag(Tag::StartPrefix(prefix)));
|
||||
}
|
||||
if let Some(contents) = obj.get("contents") {
|
||||
convert_doc(contents, out, ctx)?;
|
||||
}
|
||||
out.push(FormatElement::Tag(Tag::EndPrefix));
|
||||
out.extend(prefixes.iter().map(|_| FormatElement::Tag(Tag::EndPrefix)));
|
||||
Ok(())
|
||||
}
|
||||
Value::String(s) if s.trim().is_empty() => {
|
||||
// Whitespace alignment (e.g., " " for markdown list continuation indent).
|
||||
// Prettier uses the string length as the number of spaces to align by.
|
||||
if s.is_empty() {
|
||||
// Empty string → no alignment, just render contents
|
||||
if let Some(contents) = obj.get("contents") {
|
||||
convert_doc(contents, out, ctx)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
debug_assert!(
|
||||
s.len() <= 255,
|
||||
"align string length {} exceeds NonZeroU8 range",
|
||||
s.len()
|
||||
);
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
if let Some(nz) = NonZeroU8::new(s.len() as u8) {
|
||||
out.push(FormatElement::Tag(Tag::StartAlign(Align::new(nz))));
|
||||
if let Some(contents) = obj.get("contents") {
|
||||
convert_doc(contents, out, ctx)?;
|
||||
}
|
||||
out.push(FormatElement::Tag(Tag::EndAlign));
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!("Unsupported align value: {n}"))
|
||||
}
|
||||
Value::Object(obj_val) => {
|
||||
// `align({type: "root"}, ...)` = Prettier's `markAsRoot()`:
|
||||
// records the current indent position so that literal lines and
|
||||
@@ -598,21 +579,46 @@ pub fn postprocess<'a>(ir: &mut ArenaVec<'a, FormatElement<'a>>, allocator: &'a
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_core::{Document, PrintWidth, PrinterOptions, UniqueGroupIdBuilder};
|
||||
use oxc_formatter_core::{
|
||||
Document, IndentStyle, PrintWidth, PrinterOptions, UniqueGroupIdBuilder,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{convert_envelope, postprocess};
|
||||
|
||||
fn print_doc(doc: &Value, print_width: u32) -> String {
|
||||
print_doc_with(
|
||||
doc,
|
||||
PrinterOptions::default().with_print_width(PrintWidth::new(print_width)),
|
||||
)
|
||||
}
|
||||
|
||||
fn print_doc_with(doc: &Value, options: PrinterOptions) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let group_id_builder = UniqueGroupIdBuilder::default();
|
||||
let (mut ir, _) =
|
||||
convert_envelope(json!([doc, {}]), &allocator, &group_id_builder).unwrap();
|
||||
postprocess(&mut ir, &allocator);
|
||||
Document::new(ir, vec![])
|
||||
.print(0, PrinterOptions::default().with_print_width(PrintWidth::new(print_width)))
|
||||
.unwrap()
|
||||
.into_code()
|
||||
Document::new(ir, vec![]).print(0, options).unwrap().into_code()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_string_align_stays_spaces_under_tabs() {
|
||||
let doc = json!([
|
||||
"a",
|
||||
{
|
||||
"type": "align",
|
||||
"n": " ",
|
||||
"contents": [
|
||||
{ "type": "line", "hard": true },
|
||||
"b",
|
||||
{ "type": "indent", "contents": [{ "type": "line", "hard": true }, "c"] }
|
||||
]
|
||||
}
|
||||
]);
|
||||
let options = PrinterOptions::default().with_indent_style(IndentStyle::Tab);
|
||||
// Prettier's markdown list item: the item's columns, then the child's tab
|
||||
assert_eq!(print_doc_with(&doc, options), "a\n b\n \tc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -29,7 +29,7 @@ Every Prettier doc primitive its own language printers emit has a counterpart he
|
||||
that manual wiring is `ifBreak({groupId})` there and `if_group_breaks(..).with_group_id(..)` here
|
||||
See `oxc_formatter_yaml`'s `mapping_item.rs` for the full pattern.
|
||||
Oxfmt's Doc→IR mechanical conversion maps `expandedStates` to the same `BestFitting` primitive.
|
||||
- `align` is `prefix_align()`
|
||||
- A string `align` is `prefix_align()` (a visible token) or `space_align()` (spaces, which stay spaces under `useTabs` where a number `align` becomes a tab)
|
||||
|
||||
### The printer never trims
|
||||
|
||||
|
||||
@@ -323,11 +323,16 @@ where
|
||||
}
|
||||
|
||||
StartPrefix(prefix) => {
|
||||
let (name, arg) = if prefix.is_spaces() {
|
||||
("space_align(", prefix.text().len().to_string())
|
||||
} else {
|
||||
("prefix_align(", format!("{:?}", prefix.text()))
|
||||
};
|
||||
w!(
|
||||
f,
|
||||
[
|
||||
token("prefix_align("),
|
||||
text(f.allocator().alloc_str(&format!("{:?}", prefix.0))),
|
||||
token(name),
|
||||
text(f.allocator().alloc_str(&arg)),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ pub enum Tag {
|
||||
EndAlign,
|
||||
|
||||
/// Prints a string after the indention on every line break inside the content
|
||||
/// (Prettier's `align("> ", doc)`), see [crate::builders::prefix_align].
|
||||
/// (Prettier's string `align("> ", doc)`), see [crate::builders::prefix_align] / [crate::builders::space_align].
|
||||
StartPrefix(Prefix),
|
||||
EndPrefix,
|
||||
|
||||
@@ -260,16 +260,48 @@ impl Align {
|
||||
|
||||
/// The string a [Tag::StartPrefix] prints after the indention on every new line.
|
||||
///
|
||||
/// Prefixes are syntax tokens (`"> "`, `" * "`), so `'static`;
|
||||
/// Prefixes are syntax tokens (`"> "`, `" * "`) or syntax columns ([Self::spaces]), so `'static`;
|
||||
/// the double reference keeps the payload one pointer wide (see the size assertion in `format_element/mod.rs`).
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Prefix(pub(crate) &'static &'static str);
|
||||
|
||||
/// The widest single space prefix, wider runs chain several (see [Prefix::spaces]).
|
||||
const MAX_SPACES: usize = 32;
|
||||
|
||||
/// `SPACE_RUNS[n]` is `n` spaces, the `'static` storage [Prefix::spaces] points into.
|
||||
static SPACE_RUNS: [&str; MAX_SPACES + 1] = {
|
||||
const SPACES: &str = " ";
|
||||
let mut runs = [""; MAX_SPACES + 1];
|
||||
let mut n = 0;
|
||||
while n <= MAX_SPACES {
|
||||
runs[n] = SPACES.split_at(n).0;
|
||||
n += 1;
|
||||
}
|
||||
runs
|
||||
};
|
||||
|
||||
impl Prefix {
|
||||
/// A visible token prefix (`"> "`); spaces alone are [Self::spaces].
|
||||
pub fn new(text: &'static &'static str) -> Self {
|
||||
debug_assert!(
|
||||
!text.trim().is_empty() && !text.contains(['\n', '\t']),
|
||||
"a prefix is a visible token on its line (spaces alone are `Prefix::spaces`)"
|
||||
);
|
||||
Self(text)
|
||||
}
|
||||
|
||||
/// `n` spaces that stay spaces under `useTabs`, unlike an `align` (Prettier's string align `" ".repeat(n)`),
|
||||
/// as the prefixes to nest, outermost first (none for `0`).
|
||||
pub fn spaces(n: usize) -> impl ExactSizeIterator<Item = Self> {
|
||||
(0..n.div_ceil(MAX_SPACES))
|
||||
.map(move |i| Self(&SPACE_RUNS[(n - i * MAX_SPACES).min(MAX_SPACES)]))
|
||||
}
|
||||
|
||||
/// Whether this is a [Self::spaces] prefix, which a blank line trims entirely.
|
||||
pub fn is_spaces(self) -> bool {
|
||||
self.0.bytes().all(|b| b == b' ')
|
||||
}
|
||||
|
||||
pub fn text(self) -> &'static str {
|
||||
self.0
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::{fmt::Debug, num::NonZeroU8};
|
||||
|
||||
use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintMode, PrintResult, TagKind};
|
||||
use crate::{
|
||||
IndentStyle, InvalidDocumentError, PrintError, PrintMode, PrintResult, TagKind,
|
||||
format_element::tag::Prefix,
|
||||
};
|
||||
|
||||
use super::{
|
||||
Indention, PrefixNode,
|
||||
@@ -248,12 +251,7 @@ pub(super) trait IndentStack {
|
||||
self.current_stack_mut().push(next_indent);
|
||||
}
|
||||
/// `Tag::StartPrefix`; `nodes` is the printer's prefix arena.
|
||||
fn prefix(
|
||||
&mut self,
|
||||
prefix: &'static str,
|
||||
nodes: &mut Vec<PrefixNode>,
|
||||
indent_style: IndentStyle,
|
||||
) {
|
||||
fn prefix(&mut self, prefix: Prefix, nodes: &mut Vec<PrefixNode>, indent_style: IndentStyle) {
|
||||
let next_indent = self.indention().set_prefix(prefix, nodes, indent_style);
|
||||
self.current_stack_mut().push(next_indent);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use unicode_width::UnicodeWidthChar;
|
||||
use crate::{
|
||||
ActualStart, BestFittingElement, Condition, DedentMode, FormatElement, GroupId, IndentStyle,
|
||||
IndentWidth, InvalidDocumentError, LineMode, PrintError, PrintMode, Tag, TagKind, TextWidth,
|
||||
format_element::tag::Prefix,
|
||||
};
|
||||
|
||||
use self::call_stack::{
|
||||
@@ -300,7 +301,7 @@ impl<'a> Printer<'a> {
|
||||
|
||||
FormatElement::Tag(StartPrefix(prefix)) => {
|
||||
indent_stack.prefix(
|
||||
prefix.0,
|
||||
*prefix,
|
||||
&mut self.state.prefix_nodes,
|
||||
self.options.indent_style(),
|
||||
);
|
||||
@@ -762,16 +763,24 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
|
||||
/// Writes `indention`'s prefix chain up to and including its last prefix
|
||||
/// (`trim_last`: that one without its trailing whitespace).
|
||||
/// (`trim_last`: up to its last visible one, without its trailing whitespace;
|
||||
/// the space prefixes after it are trailing whitespace too).
|
||||
/// Width is the caller's business.
|
||||
fn emit_prefix_chain(&mut self, indention: Indention, trim_last: bool) {
|
||||
let node =
|
||||
PrefixNode::get(&self.state.prefix_nodes, indention.prefix.expect("has a prefix"));
|
||||
if trim_last && node.prefix.is_spaces() {
|
||||
if node.before.prefix.is_some() {
|
||||
self.emit_prefix_chain(node.before, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if node.before.prefix.is_some() {
|
||||
self.emit_prefix_chain(node.before, false);
|
||||
}
|
||||
self.emit_level_and_align(node.before);
|
||||
self.state.buffer.print_str(if trim_last { node.prefix.trim_end() } else { node.prefix });
|
||||
let prefix = node.prefix.text();
|
||||
self.state.buffer.print_str(if trim_last { prefix.trim_end() } else { prefix });
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -787,7 +796,7 @@ impl<'a> Printer<'a> {
|
||||
|
||||
/// Ends the current line; `next` becomes the pending indention of the new one.
|
||||
///
|
||||
/// Ending an EMPTY line inside `prefix_align` still prints its prefixes, the last one trimmed:
|
||||
/// Ending an EMPTY line inside `prefix_align` still prints its prefixes, up to the last visible one trimmed:
|
||||
/// Prettier writes the whole indention at every newline and trims trailing whitespace at the next,
|
||||
/// so a blank line in a blockquote is `>`.
|
||||
/// The levels / aligns after the last prefix are that whitespace.
|
||||
@@ -992,7 +1001,7 @@ type PrefixId = std::num::NonZeroU32;
|
||||
struct PrefixNode {
|
||||
/// What prints before the prefix: the indention the prefix was added to (its own chain included).
|
||||
before: Indention,
|
||||
prefix: &'static str,
|
||||
prefix: Prefix,
|
||||
}
|
||||
|
||||
impl PrefixNode {
|
||||
@@ -1042,7 +1051,7 @@ impl Indention {
|
||||
/// and the new indention starts empty after it.
|
||||
fn set_prefix(
|
||||
self,
|
||||
prefix: &'static str,
|
||||
prefix: Prefix,
|
||||
nodes: &mut Vec<PrefixNode>,
|
||||
indent_style: IndentStyle,
|
||||
) -> Self {
|
||||
@@ -1070,7 +1079,7 @@ impl Indention {
|
||||
let own =
|
||||
node.before.level as usize * indent_width.value() as usize + node.before.align as usize;
|
||||
let before = node.before.prefix.map_or(0, |id| Self::chain_width(nodes, id, indent_width));
|
||||
before + own + TextWidth::from_text(node.prefix, indent_width).value() as usize
|
||||
before + own + TextWidth::from_text(node.prefix.text(), indent_width).value() as usize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1371,7 +1380,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
|
||||
FormatElement::Tag(StartPrefix(prefix)) => {
|
||||
self.indent_stack.prefix(
|
||||
prefix.0,
|
||||
*prefix,
|
||||
&mut self.printer.state.prefix_nodes,
|
||||
self.printer.options.indent_style(),
|
||||
);
|
||||
@@ -1656,7 +1665,7 @@ mod tests {
|
||||
align, block_indent, dedent_to_root, empty_line, exact_line_breaks, group,
|
||||
hard_line_break, if_group_breaks, if_group_fits_on_line, indent, line_suffix,
|
||||
literal_line_break, mark_as_root, prefix_align, soft_block_indent, soft_line_break,
|
||||
soft_line_break_or_space, space, text, token,
|
||||
soft_line_break_or_space, space, space_align, text, token,
|
||||
},
|
||||
format_args,
|
||||
printer::PrintWidth,
|
||||
@@ -1775,6 +1784,59 @@ mod tests {
|
||||
assert_eq!("a\n > b\na\n> b\na\n> > b", formatted.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_align_stays_spaces_under_tabs() {
|
||||
let allocator = Allocator::default();
|
||||
let inner = test_format_with(|f| {
|
||||
write!(f, [token("a"), indent(&format_args!(hard_line_break(), token("b")))]);
|
||||
});
|
||||
let quoted = test_format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token("> "),
|
||||
prefix_align(&"> ", &format_args!(token("q"), empty_line(), token("r")))
|
||||
]
|
||||
);
|
||||
});
|
||||
let options =
|
||||
PrinterOptions { indent_style: IndentStyle::Tab, ..PrinterOptions::default() };
|
||||
|
||||
// `align` before an `indent` becomes a tab, `space_align` keeps its columns
|
||||
let result = format_simple_with_options(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
align(2, &inner),
|
||||
hard_line_break(),
|
||||
space_align(2, &inner),
|
||||
hard_line_break(),
|
||||
space_align(2, &space_align(2, &inner))
|
||||
),
|
||||
options.clone(),
|
||||
);
|
||||
assert_eq!("a\n\t\tb\na\n \tb\na\n \tb", result.as_code());
|
||||
|
||||
// Before a prefix too; a blank line trims the spaces with the prefix
|
||||
let result = format_simple_with_options(
|
||||
&allocator,
|
||||
&format_args!(
|
||||
token("x"),
|
||||
space_align(2, &format_args!(hard_line_break(), quoted)),
|
||||
hard_line_break(),
|
||||
prefix_align(
|
||||
&"> ",
|
||||
&space_align(2, &format_args!(token("y"), empty_line(), token("z")))
|
||||
)
|
||||
),
|
||||
options.clone(),
|
||||
);
|
||||
assert_eq!("x\n > q\n >\n > r\ny\n>\n> z", result.as_code());
|
||||
|
||||
// Wider than one space run: the runs chain, no column is lost
|
||||
let result = format_simple_with_options(&allocator, &space_align(40, &inner), options);
|
||||
assert_eq!(format!("a\n{}\tb", " ".repeat(40)), result.as_code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_align_nests_and_survives_root_and_suffix_mechanics() {
|
||||
let allocator = Allocator::default();
|
||||
|
||||
@@ -653,7 +653,7 @@ impl<C> std::fmt::Debug for Align<'_, '_, C> {
|
||||
/// Inside the prefix, `indent` / `align` add to the right of it,
|
||||
/// so `prefix_align(&"> ", indent(..))` prints `> x`
|
||||
/// while `align(2, prefix_align(&"> ", ..))` prints ` > x`.
|
||||
/// A blank line inside prints the prefixes up to the last one with its trailing whitespace trimmed (`>`),
|
||||
/// A blank line inside prints the prefixes up to the last visible one with its trailing whitespace trimmed (`>`),
|
||||
/// as Prettier's end-of-line trimming leaves them.
|
||||
#[inline]
|
||||
pub fn prefix_align<'a, 'ast, C, Content>(
|
||||
@@ -663,24 +663,56 @@ pub fn prefix_align<'a, 'ast, C, Content>(
|
||||
where
|
||||
Content: Format<'ast, C>,
|
||||
{
|
||||
debug_assert!(
|
||||
!prefix.trim().is_empty() && !prefix.contains(['\n', '\t']),
|
||||
"a prefix is a visible token on its line (spaces alone are `align`)"
|
||||
);
|
||||
PrefixAlign { prefix, content: Argument::new(content) }
|
||||
PrefixAlign {
|
||||
prefix: PrefixKind::Token(tag::Prefix::new(prefix)),
|
||||
content: Argument::new(content),
|
||||
}
|
||||
}
|
||||
|
||||
/// Aligns `content` by `count` spaces that stay spaces under `useTabs`.
|
||||
///
|
||||
/// Unlike [align], whose spaces become a tab when an `indent` or a prefix follows them in tab mode
|
||||
/// (Prettier's number align), these columns are syntax:
|
||||
/// a markdown container's content column, whose width the next parse counts (a tab is up to 4 columns).
|
||||
/// An `indent` inside adds to the right of them (`␣␣\t`), like [prefix_align].
|
||||
/// `0` aligns nothing.
|
||||
#[inline]
|
||||
pub fn space_align<'a, 'ast, C, Content>(
|
||||
count: usize,
|
||||
content: &'a Content,
|
||||
) -> PrefixAlign<'a, 'ast, C>
|
||||
where
|
||||
Content: Format<'ast, C>,
|
||||
{
|
||||
PrefixAlign { prefix: PrefixKind::Spaces(count), content: Argument::new(content) }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PrefixKind {
|
||||
Token(tag::Prefix),
|
||||
Spaces(usize),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PrefixAlign<'a, 'ast, C> {
|
||||
prefix: &'static &'static str,
|
||||
prefix: PrefixKind,
|
||||
content: Argument<'a, 'ast, C>,
|
||||
}
|
||||
|
||||
impl<'ast, C> Format<'ast, C> for PrefixAlign<'_, 'ast, C> {
|
||||
fn fmt(&self, f: &mut Formatter<'_, 'ast, C>) {
|
||||
f.write_element(FormatElement::Tag(StartPrefix(tag::Prefix(self.prefix))));
|
||||
let prefixes: &mut dyn ExactSizeIterator<Item = tag::Prefix> = match self.prefix {
|
||||
PrefixKind::Token(prefix) => &mut std::iter::once(prefix),
|
||||
PrefixKind::Spaces(count) => &mut tag::Prefix::spaces(count),
|
||||
};
|
||||
let depth = prefixes.len();
|
||||
for prefix in prefixes {
|
||||
f.write_element(FormatElement::Tag(StartPrefix(prefix)));
|
||||
}
|
||||
Arguments::from(&self.content).fmt(f);
|
||||
f.write_element(FormatElement::Tag(EndPrefix));
|
||||
for _ in 0..depth {
|
||||
f.write_element(FormatElement::Tag(EndPrefix));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ Style facts (markers, fence kind, setext, break kind, list padding, ...) are AST
|
||||
so the printer never re-derives layout from the source text.
|
||||
Formatter policy (sentence splitting, CJK, aligned lists, escaping) stays here.
|
||||
|
||||
Container columns (list item, footnote, indented code) are `space_align`, never `align`:
|
||||
the next parse strips exactly those columns and a tab spans up to 4,
|
||||
while an `align` becomes a tab under `useTabs` once an `indent` or a blockquote prefix follows it.
|
||||
`tests/fixtures/markdown/use-tabs/` pins it.
|
||||
|
||||
### Escaping
|
||||
|
||||
Text is printed as written. Escapes are added in three places only:
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use oxc_formatter_core::{
|
||||
Buffer,
|
||||
builders::{
|
||||
align, exact_line_breaks, group, hard_line_break, literal_line_break, mark_as_root,
|
||||
soft_line_break_or_space, space, text, token,
|
||||
exact_line_breaks, group, hard_line_break, literal_line_break, mark_as_root,
|
||||
soft_line_break_or_space, space, space_align, text, token,
|
||||
},
|
||||
format_args,
|
||||
spec::{is_suppression_marker, parse_front_matter},
|
||||
@@ -351,7 +351,7 @@ fn write_footnote_definition<'a>(
|
||||
let parent = Parent::Container;
|
||||
write!(
|
||||
f,
|
||||
align(
|
||||
space_align(
|
||||
4,
|
||||
&mark_as_root(&format_with(|f| {
|
||||
let first = format_with(|f| write_block(&children[0], children, 0, parent, f));
|
||||
|
||||
@@ -5,7 +5,7 @@ use cow_utils::CowUtils;
|
||||
|
||||
use oxc_formatter_core::{
|
||||
Buffer, arena_cow_str,
|
||||
builders::{align, exact_line_breaks, hard_line_break, text, token},
|
||||
builders::{exact_line_breaks, hard_line_break, space_align, text, token},
|
||||
write,
|
||||
};
|
||||
use oxc_markdown_parser::{
|
||||
@@ -24,7 +24,7 @@ pub fn write_code_block<'a>(code: &'a CodeBlock<'a>, f: &mut MarkdownFormatter<'
|
||||
CodeBlockKind::Indented => {
|
||||
write!(
|
||||
f,
|
||||
align(
|
||||
space_align(
|
||||
4,
|
||||
&format_with(|f| {
|
||||
write!(f, token(" "));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use oxc_formatter_core::{
|
||||
Buffer,
|
||||
builders::{align, mark_as_root, text, token},
|
||||
builders::{mark_as_root, space_align, text, token},
|
||||
write,
|
||||
};
|
||||
use oxc_markdown_parser::{
|
||||
@@ -101,9 +101,10 @@ pub fn write_list<'a>(
|
||||
column_of(source, item.children[1].span().start) < column_of(source, content_column)
|
||||
};
|
||||
let body = format_with(|f| write_list_item(item, checkbox, prefix.len(), loose, f));
|
||||
let width = if skip_align { 0 } else { u8::try_from(prefix.len()).unwrap_or(u8::MAX) };
|
||||
// The item's content column is the root that verbatim continuation lines return to
|
||||
write!(f, align(width, &mark_as_root(&body)));
|
||||
let width = if skip_align { 0 } else { prefix.len() };
|
||||
// The item's content column is the root that verbatim continuation lines return to.
|
||||
// Its columns are syntax, spaces under `useTabs` too (see `space_align`).
|
||||
write!(f, space_align(width, &mark_as_root(&body)));
|
||||
}
|
||||
|
||||
f.context().lists().borrow_mut().pop();
|
||||
@@ -145,13 +146,11 @@ fn write_list_item<'a>(
|
||||
}
|
||||
// The first child (unless a list) sits right after the checkbox;
|
||||
// other children get the tab-width alignment.
|
||||
// `checkbox` is `[x] ` / `[ ] ` / empty, `alignment` is at most 3
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
let (checkbox_width, alignment_width) = (checkbox.len() as u8, alignment as u8);
|
||||
// `checkbox` is `[x] ` / `[ ] ` / empty, `alignment` is at most 3.
|
||||
if i == 0 && !matches!(child, Block::List(_)) {
|
||||
write!(f, align(checkbox_width, &body));
|
||||
write!(f, space_align(checkbox.len(), &body));
|
||||
} else {
|
||||
write!(f, [token(&" "[..alignment]), align(alignment_width, &body)]);
|
||||
write!(f, [token(&" "[..alignment]), space_align(alignment, &body)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
- a
|
||||
- b
|
||||
|
||||
> q1
|
||||
> q2
|
||||
|
||||
1. x
|
||||
|
||||
> q1
|
||||
>
|
||||
> q2
|
||||
|
||||
- [ ] task
|
||||
|
||||
> quoted
|
||||
|
||||
> - in quote
|
||||
>
|
||||
> > nested
|
||||
|
||||
Footnote[^1].
|
||||
|
||||
[^1]: Note.
|
||||
|
||||
> quoted in footnote
|
||||
> more
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
- a
|
||||
- b
|
||||
|
||||
> q1
|
||||
> q2
|
||||
|
||||
1. x
|
||||
|
||||
> q1
|
||||
>
|
||||
> q2
|
||||
|
||||
- [ ] task
|
||||
|
||||
> quoted
|
||||
|
||||
> - in quote
|
||||
>
|
||||
> > nested
|
||||
|
||||
Footnote[^1].
|
||||
|
||||
[^1]: Note.
|
||||
|
||||
> quoted in footnote
|
||||
> more
|
||||
|
||||
==================== Output ====================
|
||||
---------------------------------
|
||||
{ printWidth: 80, useTabs: true }
|
||||
---------------------------------
|
||||
- a
|
||||
- b
|
||||
|
||||
> q1
|
||||
> q2
|
||||
|
||||
1. x
|
||||
|
||||
> q1
|
||||
>
|
||||
> q2
|
||||
|
||||
- [ ] task
|
||||
|
||||
> quoted
|
||||
|
||||
> - in quote
|
||||
>
|
||||
> > nested
|
||||
|
||||
Footnote[^1].
|
||||
|
||||
[^1]: Note.
|
||||
|
||||
> quoted in footnote
|
||||
> more
|
||||
|
||||
----------------------------------
|
||||
{ printWidth: 100, useTabs: true }
|
||||
----------------------------------
|
||||
- a
|
||||
- b
|
||||
|
||||
> q1
|
||||
> q2
|
||||
|
||||
1. x
|
||||
|
||||
> q1
|
||||
>
|
||||
> q2
|
||||
|
||||
- [ ] task
|
||||
|
||||
> quoted
|
||||
|
||||
> - in quote
|
||||
>
|
||||
> > nested
|
||||
|
||||
Footnote[^1].
|
||||
|
||||
[^1]: Note.
|
||||
|
||||
> quoted in footnote
|
||||
> more
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1 @@
|
||||
[{ "useTabs": true }]
|
||||
Reference in new issue
Block a user