cirru_parser 0.2.4

Parser for Cirru text syntax
Documentation
extern crate cirru_parser;

use cirru_parser::escape_cirru_leaf;

#[cfg(feature = "serde-json")]
mod json_write_test {
  use super::*;

  use cirru_parser::{Cirru, CirruWriterOptions, format, from_json_str};
  use std::fs;
  use std::io;

  #[test]
  fn write_demo() -> Result<(), String> {
    let writer_options = CirruWriterOptions { use_inline: false };

    match from_json_str(r#"[["a"], ["b"]]"#) {
      Ok(tree) => {
        if let Cirru::List(xs) = tree {
          assert_eq!("\na\n\nb\n", format(&xs, writer_options)?)
        } else {
          panic!("unexpected leaf here")
        }
      }
      Err(e) => {
        println!("file err: {e}");
        panic!("failed to load edn data from JSON");
      }
    };

    if let Cirru::List(xs) = from_json_str(r#"[["中文"], ["中文"]]"#).unwrap() {
      assert_eq!("\n\"中文\"\n\n\"中文\"\n", format(&xs, writer_options)?)
    } else {
      panic!("unexpected leaf here")
    }

    Ok(())
  }

  #[test]
  fn write_files() -> Result<(), io::Error> {
    let files = vec![
      "append-indent",
      "comma-indent",
      "cond-short",
      "cond",
      "demo",
      "double-nesting",
      "fold-vectors",
      "folding",
      // "html-inline",
      "html",
      "indent",
      "inline-let",
      // "inline-mode",
      "inline-simple",
      "line",
      "nested-2",
      "parentheses",
      "quote",
      "spaces",
      "unfolding",
      "list-match",
      "tag-match",
    ];
    for file in files {
      println!("testing file: {file}");
      let json_str = fs::read_to_string(format!("./tests/writer_data/{file}.json"))?;
      let cirru_str = fs::read_to_string(format!("./tests/writer_cirru/{file}.cirru"))?;

      let writer_options = CirruWriterOptions { use_inline: false };
      match from_json_str(&json_str) {
        Ok(tree) => {
          if let Cirru::List(xs) = tree {
            assert_eq!(cirru_str, format(&xs, writer_options).unwrap());
          } else {
            panic!("unexpected leaf here")
          }
        }
        Err(e) => {
          println!("{e:?}");
          panic!("failed to load edn data from json");
        }
      }
    }
    Ok(())
  }

  #[test]
  fn write_with_inline() -> Result<(), io::Error> {
    let files = vec!["html-inline", "inline-mode"];
    for file in files {
      println!("testing file: {file}");
      let json_str = fs::read_to_string(format!("./tests/writer_data/{file}.json"))?;
      let cirru_str = fs::read_to_string(format!("./tests/writer_cirru/{file}.cirru"))?;

      let writer_options = CirruWriterOptions { use_inline: true };
      match from_json_str(&json_str) {
        Ok(tree) => {
          if let Cirru::List(xs) = tree {
            assert_eq!(cirru_str, format(&xs, writer_options).unwrap());
          } else {
            panic!("unexpected literal here")
          }
        }
        Err(e) => {
          println!("file err: {e:?}");
          panic!("failed to load edn form data");
        }
      }
    }
    Ok(())
  }
}

#[test]
fn leaves_escapeing() {
  assert_eq!("\"a\"", escape_cirru_leaf("a"));
  assert_eq!("\"a b\"", escape_cirru_leaf("a b"));
  assert_eq!("\"a!+-b\"", escape_cirru_leaf("a!+-b"));
  assert_eq!("\"a\\nb\"", escape_cirru_leaf("a\nb"));

  assert_eq!("\"中文\"", escape_cirru_leaf("中文"));
  assert_eq!("\"中文\\n\"", escape_cirru_leaf("中文\n"));
}

#[test]
fn leaf_single_quote_without_quotes() -> Result<(), String> {
  use cirru_parser::{Cirru, CirruWriterOptions, format};

  let xs = vec![Cirru::List(vec![Cirru::leaf("foo'bar")])];
  let rendered = format(&xs, CirruWriterOptions::from(false))?;

  assert_eq!("\nfoo'bar\n", rendered);
  Ok(())
}

#[test]
fn leaf_single_quote_with_spaces_requires_quotes() -> Result<(), String> {
  use cirru_parser::{Cirru, CirruWriterOptions, format};

  let xs = vec![Cirru::List(vec![Cirru::leaf("foo bar'baz")])];
  let rendered = format(&xs, CirruWriterOptions::from(false))?;

  assert_eq!("\n\"foo bar'baz\"\n", rendered);
  Ok(())
}

#[test]
fn test_writer_options_from_bool() -> Result<(), String> {
  use cirru_parser::{Cirru, CirruWriterOptions, format};

  // Directly construct test data, not dependent on JSON
  // Create a structure with multiple nested lists so that inline mode has obvious differences
  let xs = vec![Cirru::List(vec![
    Cirru::leaf("a"),
    Cirru::List(vec![Cirru::leaf("c"), Cirru::leaf("b")]),
    Cirru::List(vec![Cirru::leaf("d"), Cirru::leaf("e")]),
    Cirru::List(vec![Cirru::leaf("g"), Cirru::leaf("h")]),
  ])];

  // 测试从 bool 转换为 CirruWriterOptions 并用于 format
  let inline_result = format(&xs, true.into())?;
  let non_inline_result = format(&xs, false.into())?;

  // 验证 inline 和 non-inline 模式产生不同的输出
  assert_ne!(inline_result, non_inline_result);
  assert!(non_inline_result.contains("\n"));

  // 测试使用 From::from 方法
  let inline_from_result = format(&xs, CirruWriterOptions::from(true))?;
  let non_inline_from_result = format(&xs, CirruWriterOptions::from(false))?;

  // 验证结果一致性
  assert_eq!(inline_result, inline_from_result);
  assert_eq!(non_inline_result, non_inline_from_result);

  Ok(())
}

#[cfg(feature = "serde-json")]
#[test]
fn test_dollar_sign_spacing() -> Result<(), String> {
  use cirru_parser::{Cirru, CirruWriterOptions, format, from_json_str};

  // Test case from user: tag-match with nested structures
  let json_str = r#"[
    [
      "tag-match",
      "self",
      [
        [
          ":plugin",
          "node",
          "cursor",
          "state"
        ],
        [
          "d!",
          "cursor",
          [
            "assoc",
            "state",
            ":show?",
            "false"
          ]
        ]
      ]
    ]
  ]"#;

  let writer_options = CirruWriterOptions { use_inline: false };

  match from_json_str(json_str) {
    Ok(tree) => {
      if let Cirru::List(xs) = tree {
        let result = format(&xs, writer_options)?;
        println!("Formatted result:\n{}", result);

        // Check that there's no "$ \n" pattern (dollar sign followed by space and newline)
        assert!(!result.contains("$ \n"), "Found unexpected '$ \\n' pattern in output");

        // The result should contain "$" followed directly by newline
        // when there are nested structures after the dollar sign
        Ok(())
      } else {
        panic!("unexpected leaf here")
      }
    }
    Err(e) => {
      println!("parse error: {e}");
      Err(format!("failed to parse JSON: {e}"))
    }
  }
}