lax_markup/
format_text.rs1use std::path::Path;
2
3use anyhow::Result;
4use dprint_core::configuration::resolve_new_line_kind;
5use dprint_core::formatting::PrintOptions;
6
7use crate::configuration::Configuration;
8use crate::generation;
9
10pub type ExternalFormatter<'a> = dyn Fn(&str, &str, u32) -> Result<Option<String>> + 'a;
16
17pub fn format_text(_path: &Path, text: &str, config: &Configuration) -> Result<Option<String>> {
18 let result = format_text_inner(text, config, None)?;
19 if result == text { Ok(None) } else { Ok(Some(result)) }
20}
21
22pub fn format_text_with_external(
23 path: &Path,
24 text: &str,
25 config: &Configuration,
26 external: &ExternalFormatter,
27) -> Result<Option<String>> {
28 if path.extension().and_then(|e| e.to_str()) == Some("astro")
30 && let Some((frontmatter, rest)) = split_frontmatter(text)
31 {
32 let body = match external("ts", &dedent(frontmatter), config.line_width)? {
33 Some(formatted) => formatted.trim_end().to_string(),
34 None => dedent(frontmatter).trim().to_string(),
35 };
36 let rest_formatted = format_text_inner(rest, config, Some(external))?;
37 let result = format!("---\n{}\n---\n{}", body, rest_formatted);
38 return if result == text { Ok(None) } else { Ok(Some(result)) };
39 }
40 let result = format_text_inner(text, config, Some(external))?;
41 if result == text { Ok(None) } else { Ok(Some(result)) }
42}
43
44fn split_frontmatter(text: &str) -> Option<(&str, &str)> {
47 let trimmed = text.trim_start();
48 let rest = trimmed.strip_prefix("---")?;
49 let rest = rest.strip_prefix('\n').or_else(|| rest.strip_prefix("\r\n"))?;
50 let mut search_from = 0;
51 loop {
52 let line_end = rest[search_from..].find('\n').map(|i| search_from + i)?;
53 let line = &rest[search_from..line_end];
54 if line.trim_end() == "---" {
55 return Some((&rest[..search_from], &rest[line_end + 1..]));
56 }
57 search_from = line_end + 1;
58 }
59}
60
61fn dedent(text: &str) -> String {
64 let mut common: Option<&str> = None;
65 for line in text.split('\n') {
66 if line.trim().is_empty() {
67 continue;
68 }
69 let leading = &line[..line.len() - line.trim_start().len()];
70 common = Some(match common {
71 None => leading,
72 Some(prev) => {
73 let len = prev
74 .as_bytes()
75 .iter()
76 .zip(leading.as_bytes())
77 .take_while(|(a, b)| a == b)
78 .count();
79 &prev[..len]
80 }
81 });
82 }
83 let common = common.unwrap_or("");
84 if common.is_empty() {
85 return text.to_string();
86 }
87 text
88 .split('\n')
89 .map(|line| line.strip_prefix(common).unwrap_or(line))
90 .collect::<Vec<_>>()
91 .join("\n")
92}
93
94fn format_text_inner(text: &str, config: &Configuration, external: Option<&ExternalFormatter>) -> Result<String> {
95 let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
96 let events = generation::tokenize(text);
97 if has_ignore_file_comment(&events, &config.ignore_file_comment_text) {
98 return Ok(text.to_string());
99 }
100 let nodes = generation::parse(events);
101 if nodes.is_empty() {
102 return Ok(String::new());
103 }
104 let external_error = std::cell::RefCell::new(None);
105 let formatted = dprint_core::formatting::format(
106 || generation::generate(&nodes, text, config, external, &external_error),
107 PrintOptions {
108 indent_width: config.indent_width,
109 max_width: config.line_width,
110 use_tabs: config.use_tabs,
111 new_line_text: resolve_new_line_kind(text, config.new_line_kind),
112 },
113 );
114 if let Some(error) = external_error.into_inner() {
115 return Err(error);
116 }
117 Ok(format!("{}\n", formatted.trim_end()))
120}
121
122fn has_ignore_file_comment(events: &[generation::Event], directive: &str) -> bool {
123 lax_core::has_ignore_file_comment(
124 events.iter().map(|event| match &event.kind {
125 generation::EventKind::Whitespace { newlines } => lax_core::HeaderToken::Whitespace { newlines: *newlines },
126 generation::EventKind::Comment { text } => lax_core::HeaderToken::Comment(text),
127 _ => lax_core::HeaderToken::Other,
128 }),
129 directive,
130 )
131}