dprint-plugin-pug 0.1.5

A super minimal Pug formatter plugin for dprint.
Documentation
mod support;

pub use support::{ast, config, formatter, lexer, parser};

use std::fs;

use ast::{InlineTextKind, Node, StatementHead};
use config::Configuration;
use support::format_source;

#[test]
fn normalizes_space_tab_and_newline_separated_attributes() {
    let source = "input(type = 'text'  disabled\tdata-count = items.length)\n";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(
        formatted,
        "input(type=\"text\" disabled data-count=items.length)\n"
    );

    let multiline = "input(\n  type = 'text'\n\tdisabled\n  data-count = items.length\n)\n";
    let formatted_multiline = format_source(multiline, &Configuration::default());

    assert_eq!(
        formatted_multiline,
        "input(type=\"text\" disabled data-count=items.length)\n"
    );
}

#[test]
fn restores_a_missing_separator_between_attributes_and_inline_text() {
    let source = "a(href='/docs')Docs\n";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(formatted, "a(href=\"/docs\") Docs\n");
}

#[test]
fn restores_a_missing_separator_between_quoted_attributes() {
    let source = "input(type='text'disabled data-count=items.length)\n";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(
        formatted,
        "input(type=\"text\" disabled data-count=items.length)\n"
    );
}

#[test]
fn trims_structural_trailing_spaces_but_preserves_inline_text_whitespace() {
    let source = "ul \np   hello world\n";
    let lexed = lexer::lex(source);
    let document = parser::parse(&lexed);

    assert!(matches!(
        &document.children[0],
        Node::Statement(statement)
            if matches!(
                &statement.head,
                StatementHead::Tag(head)
                    if head.inline_space.is_none()
                        && head.inline_text.is_none()
            )
    ));

    assert!(matches!(
        &document.children[1],
        Node::Statement(statement)
            if matches!(
                &statement.head,
                StatementHead::Tag(head)
                    if head.inline_space.as_deref() == Some(" ")
                        && head.inline_text.as_ref().is_some_and(|text|
                            text.kind == InlineTextKind::Plain
                                && text.content == "  hello world"
                        )
            )
    ));

    assert_eq!(
        format_source(source, &Configuration::default()),
        "ul\np   hello world\n"
    );
}

#[test]
fn canonicalizes_tab_separators_without_dropping_significant_whitespace() {
    let source = "p\tHello\np\t\tHello\n";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(formatted, "p Hello\np \tHello\n");
}

#[test]
fn trims_structural_trailing_spaces_in_supported_statement_heads() {
    let source = "\
doctype html   
include partials/nav   
if condition   
div(class='hero')   
";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(
        formatted,
        "\
doctype html
include partials/nav
if condition
div(class=\"hero\")
"
    );
}

#[test]
fn compacts_repeated_spaces_in_non_tag_statement_heads() {
    let source = "\
doctype   html
include    partials/nav
extends    layouts/base
block   append   scripts
if   user.admin
else if   user.editor
case   kind
when   'admin'
default   
each   item,   index   in   items
while   remaining > 0
mixin   button(label)
-   const enabled = true
=   user.name
!=   html
";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(
        formatted,
        "\
doctype html
include partials/nav
extends layouts/base
block append scripts
if user.admin
else if user.editor
case kind
when 'admin'
default
each item, index in items
while remaining > 0
mixin button(label)
- const enabled = true
= user.name
!= html
"
    );
}

#[test]
fn compacts_tab_separators_in_non_tag_statement_heads() {
    let source = "\
doctype\thtml
include\t\tpartials/nav
block\tappend\tscripts
if\t\tuser.admin
else if\t\tuser.editor
each\titem,\tindex\tin\titems
mixin\t\tbutton(label)
-\t\tconst enabled = true
=\t\tuser.name
!=\t\thtml
";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(
        formatted,
        "\
doctype html
include partials/nav
block append scripts
if user.admin
else if user.editor
each item, index in items
mixin button(label)
- const enabled = true
= user.name
!= html
"
    );
}

#[test]
fn collapses_trailing_blank_lines_at_end_of_file_all_at_once() {
    let source = "p.\n  line one\n  \n  \n";
    let formatted = format_source(source, &Configuration::default());

    assert_eq!(formatted, "p.\n  line one\n");
    assert_eq!(
        format_source(&formatted, &Configuration::default()),
        formatted
    );
}

#[test]
fn keeps_plain_text_doc_canonical_at_end_of_file() {
    let path = support::docs_dir().join("plain-text.pug");
    let source = fs::read_to_string(&path).expect("plain-text.pug should read");
    let formatted = format_source(&source, &Configuration::default());

    let source_trailing = source.chars().rev().take_while(|ch| *ch == '\n').count();
    let formatted_trailing = formatted.chars().rev().take_while(|ch| *ch == '\n').count();

    assert_eq!(
        source_trailing,
        1,
        "unexpected source tail in {}",
        path.display()
    );
    assert_eq!(
        formatted_trailing,
        1,
        "formatter should keep the canonical EOF newline shape for {}",
        path.display()
    );
    assert_eq!(formatted, source);
}

#[test]
fn collapses_augmented_plain_text_doc_eof_blank_lines_in_one_pass() {
    let path = support::docs_dir().join("plain-text.pug");
    let source = fs::read_to_string(&path).expect("plain-text.pug should read");
    let augmented = format!("{source}\n\n\n\n\n\n");
    let formatted = format_source(&augmented, &Configuration::default());
    let reformatted = format_source(&formatted, &Configuration::default());

    let formatted_trailing = formatted.chars().rev().take_while(|ch| *ch == '\n').count();

    assert_eq!(
        formatted_trailing,
        1,
        "formatter should collapse augmented EOF blank lines in one pass for {}",
        path.display()
    );
    assert_eq!(reformatted, formatted);
}