Skip to main content

microcad_lang_format/
lib.rs

1// Copyright © 2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use microcad_lang_base::Diagnostics;
5use microcad_lang_parse::{Parse, ParseContext, ast};
6
7mod expression;
8mod extras;
9mod literal;
10mod node;
11mod statement;
12mod ty;
13
14pub(crate) use crate::node::{BreakMode, Node};
15
16#[derive(Debug, Clone)]
17pub struct FormatConfig {
18    pub max_width: usize,
19    pub indent_width: usize,
20}
21
22impl Default for FormatConfig {
23    fn default() -> Self {
24        Self {
25            max_width: 60,
26            indent_width: 4,
27        }
28    }
29}
30
31pub(crate) trait Format {
32    fn format(&self, f: &FormatConfig) -> Node;
33}
34
35impl<T> Format for T
36where
37    T: Into<Node> + Clone,
38{
39    fn format(&self, _f: &FormatConfig) -> Node {
40        self.clone().into()
41    }
42}
43
44// Blanket impl for Option
45impl<T: Format> Format for Option<T> {
46    fn format(&self, f: &FormatConfig) -> Node {
47        match self {
48            Some(inner) => inner.format(f),
49            None => Node::Nil, // Or whatever your "null" node is
50        }
51    }
52}
53
54impl Format for ast::Identifier {
55    fn format(&self, _: &FormatConfig) -> Node {
56        self.name.clone().into()
57    }
58}
59
60impl Format for ast::Comment {
61    fn format(&self, _: &FormatConfig) -> Node {
62        match &self.inner {
63            ast::CommentInner::SingleLine(line) => {
64                node!(Node::Softline Node::SingleLineComment(line.into()))
65            }
66            ast::CommentInner::MultiLine(line) => {
67                node!(Node::Softline "/* " Node::from(line.clone()) " */" Node::Softline)
68            }
69        }
70    }
71}
72
73impl Format for ast::DocBlock {
74    fn format(&self, _: &FormatConfig) -> Node {
75        Node::vlist(
76            self.lines.iter().cloned().map(|line| node!(line)),
77            Node::Nil,
78            0,
79        )
80    }
81}
82
83impl Format for ast::Program {
84    fn format(&self, f: &FormatConfig) -> Node {
85        self.statements.format(f)
86    }
87}
88
89/// node! macro for syntactic suger.
90#[macro_export]
91macro_rules! node {
92    // Single element: node!(x)
93    ($node:expr) => {
94        $crate::Node::from($node)
95    };
96    // Multiple elements: node!(begin, body, end)
97    ($($node:expr)*) => {
98        $crate::Node::from(vec![
99            $( $crate::Node::from($node) ),*
100        ])
101    };
102    // Multiple formatted elements with extras: node!(f => begin, body, end)
103    ($f:ident, $extras:expr => $($node:expr)*) => {
104        $crate::extras::with_extras(
105            &$extras,
106            $f,
107            $crate::Node::from(vec![
108                $( $node.format($f) ),*
109            ])
110
111        )
112    };
113    // Multiple formatted elements: node!(f => begin, body, end)
114    ($f:ident => $($node:expr)*) => {
115        $crate::Node::from(vec![
116            $( $node.format($f) ),*
117        ])
118    };
119}
120
121/// Format µcad program.
122pub fn format(program: &ast::Program, config: &FormatConfig) -> String {
123    program.format(config).to_string()
124}
125
126/// High-level API to format a &str containing µcad source code.
127pub fn format_str(source: &str, config: &FormatConfig) -> Result<String, Diagnostics> {
128    let parse_context = ParseContext::new(source);
129    let source =
130        ast::Source::parse(&parse_context).map_err(|err| err.to_diagnostics(&parse_context))?;
131    Ok(format(&source.ast, config))
132}
133
134/// Format a [`ast::Source`]
135pub fn format_source(
136    source: &ast::Source,
137    config: &FormatConfig,
138) -> Result<ast::Source, Diagnostics> {
139    let formatted = microcad_lang_base::Source::new(
140        source.url.clone(),
141        source.line_offset,
142        format(&source.ast, config),
143    );
144    let parse_context = ParseContext::from(&formatted);
145    ast::Source::parse(&parse_context).map_err(|err| err.to_diagnostics(&parse_context))
146}