Skip to main content

lax_markup/
format_text.rs

1use 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
10/// Formats the contents of an embedded block, like CSS in a style element or
11/// a script body. Receives the language hint (the element's `lang` or `type`
12/// attribute value, or `css`/`js` by element kind), the raw inner text, and
13/// the remaining print width. Returning `Ok(None)` keeps the contents
14/// verbatim.
15pub 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  // Astro files start with a frontmatter block whose body is TypeScript
29  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
44/// Splits a leading `---` fenced frontmatter block, returning its inner text
45/// and the remainder of the file.
46fn 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
61/// Strips the longest common leading whitespace prefix from every non empty
62/// line.
63fn 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  // exactly one trailing newline, so verbatim regions at the end of the
118  // file cannot accumulate blank lines across passes
119  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}