caddyfile 0.1.1

A library for working with Caddy's Caddyfile format
Documentation
// Copyright 2015 Matthew Holt and The Caddy Authors
// Copyright 2023 Aaron Dewes and The Nirvati Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Format formats the input Caddyfile to a standard, nice-looking
/// appearance. It works by reading each rune of the input and taking
/// control over all the bracing and whitespace that is written; otherwise,
/// words, comments, placeholders, and escaped characters are all treated
/// literally and written as they appear in the input.
pub fn format(input: &str) -> String {
    let input = input.trim();

    let mut out = String::new();

    let mut last: Option<char> = None; // the last character that was written to the result

    let mut space = true; // whether current/previous character was whitespace (beginning of input counts as space)
    let mut beginning_of_line = true; // whether we are at beginning of line

    let mut open_brace = false; // whether current word/token is or started with open curly brace
    let mut open_brace_written = false; // if openBrace, whether that brace was written or not
    let mut open_brace_space = false; // whether there was a non-newline space before open brace

    let mut new_lines: usize = 0; // count of newlines consumed

    let mut comment = false; // whether we're in a comment
    let mut quoted = false; // whether we're in a quoted segment
    let mut escaped = false; // whether current char is escaped

    let mut nesting = 0; // indentation level

    macro_rules! write {
        ($ch:expr) => {
            out.push($ch);
            #[allow(unused_assignments)]
            {
                last = Some($ch);
            }
        };
    }

    macro_rules! indent {
        () => {
            for _ in 0..nesting {
                write!('\t');
            }
        };
    }

    // A variant of the macro above that takes an optional argument: A Number of write() calls to do
    macro_rules! next_line {
        () => {
            write!('\n');
            beginning_of_line = true;
        };
        ($n:expr) => {
            for _ in 0..$n {
                write!('\n');
            }
            if $n > 0 {
                beginning_of_line = true;
            }
        };
    }

    for ch in input.chars() {
        if comment {
            if ch == '\n' {
                comment = false;
                space = true;
                next_line!();
                continue;
            } else {
                write!(ch);
                continue;
            }
        }

        if !escaped && ch == '\\' {
            if space {
                write!(' ');
                space = false;
            }
            write!(ch);
            escaped = true;
            continue;
        }

        if escaped {
            write!(ch);
            escaped = false;
            continue;
        }

        if quoted {
            if ch == '"' {
                quoted = false;
            }
            write!(ch);
            continue;
        }

        if space && ch == '"' {
            quoted = true
        }

        if ch.is_whitespace() {
            space = true;
            if ch == '\n' {
                new_lines += 1;
            }
            continue;
        };
        let space_prior = space;
        space = false;

        //////////////////////////////////////////////////////////
        // I find it helpful to think of the formatting loop in two
        // main sections; by the time we reach this point, we
        // know we are in a "regular" part of the file: we know
        // the character is not a space, not in a literal segment
        // like a comment or quoted, it's not escaped, etc.
        //////////////////////////////////////////////////////////

        if ch == '#' {
            comment = true
        }

        if open_brace && space_prior && !open_brace_written {
            if nesting == 0 && last == Some('}') {
                next_line!(2);
            }

            open_brace = false;
            if beginning_of_line {
                indent!();
            } else if !open_brace_space {
                write!(' ');
            }
            write!('{');
            open_brace_written = true;
            next_line!();
            new_lines = 0;
            // prevent infinite nesting from ridiculous inputs (Caddy issue #4169)
            if nesting < 10 {
                nesting += 1;
            }
        }

        match ch {
            '{' => {
                open_brace = true;
                open_brace_written = false;
                open_brace_space = space_prior && !beginning_of_line;
                if open_brace_space {
                    write!(' ');
                }
                continue;
            }

            '}' if space_prior || !open_brace => {
                if last != Some('\n') {
                    next_line!();
                }
                if nesting > 0 {
                    nesting -= 1;
                }
                indent!();
                write!('}');
                new_lines = 0;
                continue;
            }
            _ => {}
        }
        if new_lines > 2 {
            new_lines = 2
        }

        next_line!(new_lines);

        new_lines = 0;
        if beginning_of_line {
            indent!();
        }
        if nesting == 0 && last == Some('}') && beginning_of_line {
            next_line!(2);
        }

        if !beginning_of_line && space_prior {
            write!(' ');
        }

        if open_brace && !open_brace_written {
            write!('{');
            open_brace_written = true;
        }
        write!(ch);

        beginning_of_line = false;
    }

    // the Caddyfile does not need any leading or trailing spaces, but...
    let out = out.trim().to_string();

    // ...Caddyfiles should, however, end with a newline because
    // newlines are significant to the syntax of the file
    out + "\n"
}