1use crate::cst::document::{Document, parse_stream_inner};
7use crate::cst::green::{GreenChild, GreenNode};
8use crate::cst::syntax::SyntaxKind;
9use crate::cst::{YAML_BLANK, is_yaml_blank};
10use crate::de::ParserConfig;
11use crate::error::{Error, Result};
12use crate::parser::ParseConfig;
13use crate::prelude::*;
14
15#[derive(Debug, Clone, Copy)]
17pub struct FormatConfig {
18 pub indent_size: usize,
20}
21
22impl Default for FormatConfig {
23 fn default() -> Self {
24 Self { indent_size: 2 }
25 }
26}
27
28pub fn format(input: &str) -> Result<String> {
44 format_with_config(input, &FormatConfig::default())
45}
46
47pub fn format_with_config(input: &str, config: &FormatConfig) -> Result<String> {
53 format_with_parser_config(input, config, &ParserConfig::default())
54}
55
56pub fn format_with_parser_config(
85 input: &str,
86 config: &FormatConfig,
87 parser: &ParserConfig,
88) -> Result<String> {
89 if is_yaml_blank(input) {
90 return Ok(String::new());
91 }
92 let parse_config = ParseConfig::from(parser);
93 let docs = parse_stream_inner(input, &parse_config, parser.max_stream_bytes)?;
94 let mut output = String::with_capacity(input.len());
95 for doc in &docs {
96 let mut formatter = Formatter::new(doc.source(), config);
97 formatter.format_node(doc.syntax(), 0)?;
98 output.push_str(&formatter.finish());
99 }
100 ensure_same_meaning(&docs, &output, &parse_config, parser.max_stream_bytes)?;
101 Ok(output)
102}
103
104fn ensure_same_meaning(
112 docs: &[Document],
113 output: &str,
114 config: &ParseConfig,
115 max_stream_bytes: usize,
116) -> Result<()> {
117 let refused = || {
118 Error::Parse(
119 "format: the input cannot be re-laid out without changing its meaning; \
120 it was left unformatted"
121 .into(),
122 )
123 };
124 let after = parse_stream_inner(output, config, max_stream_bytes).map_err(|_| refused())?;
125 let same = after.len() == docs.len()
126 && after
127 .iter()
128 .zip(docs)
129 .all(|(a, b)| *a.as_value() == *b.as_value());
130 if same { Ok(()) } else { Err(refused()) }
131}
132
133struct Formatter<'a> {
134 source: &'a str,
135 config: &'a FormatConfig,
136 out: String,
137 indent_level: usize,
138 at_line_start: bool,
139 last_was_space: bool,
141 last_token: Option<SyntaxKind>,
146}
147
148impl<'a> Formatter<'a> {
149 fn new(source: &'a str, config: &'a FormatConfig) -> Self {
150 Self {
151 source,
152 config,
153 out: String::with_capacity(source.len()),
154 indent_level: 0,
155 at_line_start: true,
156 last_was_space: false,
157 last_token: None,
158 }
159 }
160
161 fn finish(self) -> String {
162 let mut out = self.out;
163 if !out.ends_with('\n') && !out.is_empty() {
164 out.push('\n');
165 }
166 out
167 }
168
169 fn indent(&mut self) {
170 if self.at_line_start {
171 for _ in 0..(self.indent_level * self.config.indent_size) {
172 self.out.push(' ');
173 }
174 self.at_line_start = false;
175 self.last_was_space = self.indent_level > 0;
176 }
177 }
178
179 fn newline(&mut self) {
180 if self.at_line_start {
181 return;
182 }
183 let trimmed = self.out.trim_end_matches([' ', '\t']).len();
191 let line_is_only_space =
192 trimmed < self.out.len() && (trimmed == 0 || self.out.as_bytes()[trimmed - 1] == b'\n');
193 if line_is_only_space {
194 self.out.truncate(trimmed);
195 } else {
196 self.out.push('\n');
197 }
198 self.at_line_start = true;
199 self.last_was_space = false;
200 }
201
202 fn write_raw(&mut self, text: &str) {
203 if text.is_empty() {
204 return;
205 }
206 if !text.starts_with(['\n', '\r']) {
212 self.indent();
213 }
214 self.out.push_str(text);
215 if let Some(last_newline) = text.rfind('\n') {
216 let trailing = &text[last_newline + 1..];
217 self.at_line_start = trailing.is_empty();
218 self.last_was_space = trailing.ends_with(' ');
219 } else {
220 self.at_line_start = false;
221 self.last_was_space = text.ends_with(' ');
222 }
223 }
224
225 fn ensure_space(&mut self) {
226 if !self.at_line_start && !self.last_was_space {
227 self.out.push(' ');
228 self.last_was_space = true;
229 }
230 }
231
232 fn format_node(&mut self, node: &GreenNode, base: usize) -> Result<()> {
233 match node.kind() {
234 SyntaxKind::Document | SyntaxKind::Stream => {
235 self.format_children(node, base)?;
236 }
237 SyntaxKind::BlockMapping => {
238 self.format_block_mapping(node, base)?;
239 }
240 SyntaxKind::BlockSequence => {
241 self.format_block_sequence(node, base)?;
242 }
243 SyntaxKind::MappingEntry => {
244 self.format_mapping_entry(node, base)?;
245 }
246 SyntaxKind::SequenceItem => {
247 self.format_sequence_item(node, base)?;
248 }
249 SyntaxKind::FlowMapping | SyntaxKind::FlowSequence => {
250 self.write_verbatim(node, base);
251 }
252 _ => {
253 self.format_children(node, base)?;
254 }
255 }
256 Ok(())
257 }
258
259 fn format_children(&mut self, node: &GreenNode, base: usize) -> Result<()> {
260 let mut pos = base;
261 for child in node.children() {
262 match child {
263 GreenChild::Node(inner) => {
264 self.format_node(inner, pos)?;
265 }
266 GreenChild::Token { kind, len } => {
267 self.handle_token(*kind, &self.source[pos..pos + *len as usize]);
268 }
269 }
270 pos += child.text_len();
271 }
272 Ok(())
273 }
274
275 fn handle_token(&mut self, kind: SyntaxKind, text: &str) {
276 match kind {
277 SyntaxKind::Comment => {
278 self.ensure_space();
279 self.write_raw(text.trim_end_matches(YAML_BLANK));
280 self.newline();
281 }
282 SyntaxKind::Newline => {
283 self.newline();
284 }
285 SyntaxKind::Whitespace | SyntaxKind::Bom => {
286 }
288 SyntaxKind::DocStart | SyntaxKind::DocEnd => {
289 self.newline();
292 self.write_raw(if kind == SyntaxKind::DocStart {
293 "---"
294 } else {
295 "..."
296 });
297 self.newline();
298 }
299 SyntaxKind::ColonIndicator => {
300 if self.after_node_property() {
301 self.ensure_space();
302 }
303 self.write_raw(":");
304 }
305 SyntaxKind::DashIndicator => {
306 if !self.at_line_start {
307 self.newline();
308 }
309 self.write_raw("-");
310 }
311 _ if kind.is_token() => {
312 let trimmed = if matches!(kind, SyntaxKind::PlainScalar) {
313 text.trim_matches(YAML_BLANK)
314 } else {
315 text
316 };
317 self.separate_from_property();
318 self.write_raw(trimmed);
319 if trimmed.len() < text.len() && text.trim_end_matches([' ', '\t']).ends_with('\n')
322 {
323 self.newline();
324 }
325 }
326 _ => {}
327 }
328 if kind != SyntaxKind::Whitespace {
329 self.last_token = Some(kind);
330 }
331 }
332
333 fn after_node_property(&self) -> bool {
335 matches!(
336 self.last_token,
337 Some(SyntaxKind::TagMark | SyntaxKind::AnchorMark | SyntaxKind::AliasMark)
338 )
339 }
340
341 fn separate_from_property(&mut self) {
344 if matches!(
345 self.last_token,
346 Some(
347 SyntaxKind::TagMark
348 | SyntaxKind::AnchorMark
349 | SyntaxKind::ColonIndicator
350 | SyntaxKind::DashIndicator
351 | SyntaxKind::QuestionIndicator
352 )
353 ) {
354 self.ensure_space();
355 }
356 }
357
358 fn format_block_mapping(&mut self, node: &GreenNode, base: usize) -> Result<()> {
359 let mut pos = base;
360 for child in node.children() {
361 match child {
362 GreenChild::Node(inner) if inner.kind() == SyntaxKind::MappingEntry => {
363 self.format_mapping_entry(inner, pos)?;
364 }
365 GreenChild::Token { kind, len } => {
366 self.handle_token(*kind, &self.source[pos..pos + *len as usize]);
367 }
368 GreenChild::Node(inner) => {
369 self.format_node(inner, pos)?;
370 }
371 }
372 pos += child.text_len();
373 }
374 Ok(())
375 }
376
377 fn format_block_sequence(&mut self, node: &GreenNode, base: usize) -> Result<()> {
378 let mut pos = base;
379 for child in node.children() {
380 match child {
381 GreenChild::Node(inner) if inner.kind() == SyntaxKind::SequenceItem => {
382 self.format_sequence_item(inner, pos)?;
383 }
384 GreenChild::Token { kind, len } => {
385 self.handle_token(*kind, &self.source[pos..pos + *len as usize]);
386 }
387 GreenChild::Node(inner) => {
388 self.format_node(inner, pos)?;
389 }
390 }
391 pos += child.text_len();
392 }
393 Ok(())
394 }
395
396 fn format_mapping_entry(&mut self, node: &GreenNode, base: usize) -> Result<()> {
397 let children: Vec<&GreenChild> = node.children().collect();
403 let drop_newline_at = property_on_next_line(&children);
404 let mut state = EntryState::default();
405 let mut pos = base;
406 for (index, child) in children.iter().enumerate() {
407 match child {
408 GreenChild::Token { kind, len } if drop_newline_at != Some(index) => {
409 let text = &self.source[pos..pos + *len as usize];
410 self.entry_token(&mut state, *kind, text);
411 }
412 GreenChild::Token { .. } => {}
413 GreenChild::Node(inner) => self.entry_node(&mut state, inner, pos)?,
414 }
415 pos += child.text_len();
416 }
417 self.end_entry(&state);
418 Ok(())
419 }
420
421 fn entry_token(&mut self, state: &mut EntryState, kind: SyntaxKind, text: &str) {
423 if (state.saw_colon || state.saw_question) && !is_layout_token(kind) {
428 if state.saw_colon {
429 self.indent_value_on_next_line(state);
430 }
431 self.ensure_space();
432 }
433 match kind {
434 SyntaxKind::ColonIndicator => {
435 if state.saw_question && !self.at_line_start {
439 self.newline();
440 }
441 state.saw_colon = true;
442 state.saw_question = false;
443 }
444 SyntaxKind::QuestionIndicator => state.saw_question = true,
445 SyntaxKind::Newline | SyntaxKind::Comment if state.saw_colon => {
446 state.value_on_next_line = true;
447 }
448 _ => {}
449 }
450 self.handle_token(kind, text);
451 }
452
453 fn entry_node(&mut self, state: &mut EntryState, inner: &GreenNode, pos: usize) -> Result<()> {
455 let block = matches!(
456 inner.kind(),
457 SyntaxKind::BlockMapping | SyntaxKind::BlockSequence
458 );
459 if state.saw_question && !state.saw_colon {
460 self.ensure_space();
461 let nested = inner.kind() == SyntaxKind::BlockMapping;
468 return self.format_nested(inner, pos, nested);
469 }
470 if !state.saw_colon {
471 return self.format_node(inner, pos);
472 }
473 if block {
474 self.newline();
475 return self.format_nested(inner, pos, true);
476 }
477 self.indent_value_on_next_line(state);
478 self.ensure_space();
479 self.format_node(inner, pos)
480 }
481
482 fn indent_value_on_next_line(&mut self, state: &mut EntryState) {
486 if state.value_on_next_line && !state.value_indented {
487 self.indent_level += 1;
488 state.value_indented = true;
489 }
490 }
491
492 fn end_entry(&mut self, state: &EntryState) {
493 if state.value_indented {
494 self.indent_level -= 1;
495 }
496 self.newline();
497 }
498
499 fn format_nested(&mut self, inner: &GreenNode, pos: usize, deeper: bool) -> Result<()> {
501 if !deeper {
502 return self.format_node(inner, pos);
503 }
504 self.indent_level += 1;
505 let result = self.format_node(inner, pos);
506 self.indent_level -= 1;
507 result
508 }
509
510 fn format_sequence_item(&mut self, node: &GreenNode, base: usize) -> Result<()> {
511 let mut pos = base;
512 let mut state = EntryState::default();
513
514 for child in node.children() {
515 match child {
516 GreenChild::Token { kind, len } => {
517 let text = &self.source[pos..pos + *len as usize];
518 self.item_token(&mut state, *kind, text);
519 }
520 GreenChild::Node(inner) if state.saw_colon => {
521 let block = matches!(
523 inner.kind(),
524 SyntaxKind::BlockMapping | SyntaxKind::BlockSequence
525 );
526 if !block {
527 self.indent_value_on_next_line(&mut state);
528 }
529 self.ensure_space();
530 self.format_nested(inner, pos, block)?;
531 }
532 GreenChild::Node(inner) => self.format_node(inner, pos)?,
533 }
534 pos += child.text_len();
535 }
536 self.end_entry(&state);
537 Ok(())
538 }
539
540 fn item_token(&mut self, state: &mut EntryState, kind: SyntaxKind, text: &str) {
542 if state.saw_colon && !is_layout_token(kind) && kind != SyntaxKind::DashIndicator {
543 self.indent_value_on_next_line(state);
544 self.ensure_space();
545 }
546 match kind {
547 SyntaxKind::DashIndicator => state.saw_colon = true,
548 SyntaxKind::Newline | SyntaxKind::Comment if state.saw_colon => {
549 state.value_on_next_line = true;
550 }
551 _ => {}
552 }
553 self.handle_token(kind, text);
554 }
555
556 fn write_verbatim(&mut self, node: &GreenNode, base: usize) {
557 self.separate_from_property();
558 self.indent();
559 let text = node.text(&self.source[base..base + node.text_len()]);
560 self.out.push_str(&text);
561 self.at_line_start = text.ends_with('\n');
562 self.last_was_space = text.ends_with(' ');
563 self.last_token = Some(node.kind());
564 }
565}
566
567#[derive(Default)]
569struct EntryState {
570 saw_colon: bool,
573 saw_question: bool,
576 value_on_next_line: bool,
578 value_indented: bool,
580}
581
582fn is_layout_token(kind: SyntaxKind) -> bool {
584 matches!(
585 kind,
586 SyntaxKind::ColonIndicator
587 | SyntaxKind::Newline
588 | SyntaxKind::Whitespace
589 | SyntaxKind::Comment
590 )
591}
592
593fn property_on_next_line(children: &[&GreenChild]) -> Option<usize> {
596 let is = |c: &GreenChild, kinds: &[SyntaxKind]| matches!(c, GreenChild::Token { kind, .. } if kinds.contains(kind));
597 let colon = children
598 .iter()
599 .position(|c| is(c, &[SyntaxKind::ColonIndicator]))?;
600 let mut rest = children[colon + 1..]
601 .iter()
602 .enumerate()
603 .filter(|(_, c)| !is(c, &[SyntaxKind::Whitespace]));
604 let (offset, newline) = rest.next()?;
605 if !is(newline, &[SyntaxKind::Newline]) {
606 return None;
607 }
608 let (_, next) = rest.next()?;
609 is(next, &[SyntaxKind::TagMark, SyntaxKind::AnchorMark]).then_some(colon + 1 + offset)
610}