use badness::parser::{LatexFlavor, LexConfig, parse_with_flavor};
use badness::semantic::{DocKind, doc_associations};
use badness::syntax::{SyntaxKind, SyntaxNode};
fn parse_dtx(input: &str) -> SyntaxNode {
let config = LexConfig {
flavor: LatexFlavor::Document,
dtx: true,
};
let parsed = parse_with_flavor(input, config);
assert_eq!(
parsed.syntax().to_string(),
input,
"losslessness violated for {input:?}"
);
parsed.syntax()
}
fn count(root: &SyntaxNode, kind: SyntaxKind) -> usize {
root.descendants().filter(|n| n.kind() == kind).count()
}
fn count_token(root: &SyntaxNode, kind: SyntaxKind) -> usize {
root.descendants_with_tokens()
.filter_map(|e| e.into_token())
.filter(|t| t.kind() == kind)
.count()
}
fn tokens(root: &SyntaxNode) -> Vec<(SyntaxKind, String)> {
root.descendants_with_tokens()
.filter_map(|e| e.into_token())
.map(|t| (t.kind(), t.text().to_string()))
.collect()
}
#[test]
fn line_leading_percent_is_a_margin_mid_line_percent_is_a_comment() {
let root = parse_dtx("% doc text\nword % trailing\n");
let toks = tokens(&root);
let margins: Vec<_> = toks
.iter()
.filter(|(k, _)| *k == SyntaxKind::DOC_MARGIN)
.collect();
let comments: Vec<_> = toks
.iter()
.filter(|(k, _)| *k == SyntaxKind::COMMENT)
.collect();
assert_eq!(margins, vec![&(SyntaxKind::DOC_MARGIN, "%".to_string())]);
assert_eq!(
comments,
vec![&(SyntaxKind::COMMENT, "% trailing".to_string())]
);
assert!(toks.contains(&(SyntaxKind::WORD, "doc".to_string())));
}
#[test]
fn block_guards_are_guard_tokens_wrapping_ordinary_code() {
let root = parse_dtx("%<*driver>\n\\documentclass{article}\n%</driver>\n");
let toks = tokens(&root);
assert_eq!(count_token(&root, SyntaxKind::DOC_MARGIN), 0);
assert_eq!(count_token(&root, SyntaxKind::GUARD), 2);
assert!(toks.contains(&(SyntaxKind::GUARD, "%<*driver>".to_string())));
assert!(toks.contains(&(SyntaxKind::GUARD, "%</driver>".to_string())));
assert!(!toks.iter().any(|(k, _)| *k == SyntaxKind::COMMENT));
assert!(count(&root, SyntaxKind::COMMAND) >= 1);
}
#[test]
fn an_inline_guard_prefixes_parsed_code() {
let root = parse_dtx("%<plain>\\RequirePackage{xcolor}\n");
let toks = tokens(&root);
assert!(toks.contains(&(SyntaxKind::GUARD, "%<plain>".to_string())));
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\RequirePackage".to_string())));
assert!(toks.contains(&(SyntaxKind::WORD, "xcolor".to_string())));
assert_eq!(count(&root, SyntaxKind::COMMAND), 1);
}
#[test]
fn guards_punctuate_macrocode_bodies() {
let input =
"% \\begin{macrocode}\n%<*foo>\n\\def\\x{y}\n% note\n%</foo>\n% \\end{macrocode}\n";
let root = parse_dtx(input);
let toks = tokens(&root);
assert!(toks.contains(&(SyntaxKind::GUARD, "%<*foo>".to_string())));
assert!(toks.contains(&(SyntaxKind::GUARD, "%</foo>".to_string())));
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\def".to_string())));
assert!(toks.contains(&(SyntaxKind::COMMENT, "% note".to_string())));
}
#[test]
fn a_malformed_guard_falls_back_to_a_comment() {
let root = parse_dtx("%<unterminated\nword\n");
let toks = tokens(&root);
assert_eq!(count_token(&root, SyntaxKind::GUARD), 0);
assert!(toks.contains(&(SyntaxKind::COMMENT, "%<unterminated".to_string())));
}
#[test]
fn blank_margin_line_breaks_the_paragraph() {
let root = parse_dtx("% first paragraph\n%\n% second paragraph\n");
assert_eq!(count(&root, SyntaxKind::PARAGRAPH), 2);
}
#[test]
fn continuation_margins_keep_one_paragraph() {
let root = parse_dtx("% one two three\n% four five six\n");
assert_eq!(count(&root, SyntaxKind::PARAGRAPH), 1);
}
#[test]
fn macrocode_is_an_environment_whose_body_is_real_code() {
let input = "% \\begin{macrocode}\n\\def\\foo{\\bar}\n% \\end{macrocode}\n";
let root = parse_dtx(input);
assert_eq!(count(&root, SyntaxKind::ENVIRONMENT), 1);
assert!(count(&root, SyntaxKind::COMMAND) >= 1);
assert_eq!(count_token(&root, SyntaxKind::VERBATIM_BODY), 0);
assert_eq!(count_token(&root, SyntaxKind::DOC_MARGIN), 2);
}
#[test]
fn macrocode_body_lexes_under_the_package_regime() {
let inside = parse_dtx("% \\begin{macrocode}\n\\bar@baz\n% \\end{macrocode}\n");
assert!(tokens(&inside).contains(&(SyntaxKind::CONTROL_WORD, "\\bar@baz".to_string())));
let doc = parse_dtx("% \\bar@baz\n");
let dtoks = tokens(&doc);
assert!(dtoks.contains(&(SyntaxKind::CONTROL_WORD, "\\bar".to_string())));
assert!(
!dtoks
.iter()
.any(|(k, t)| *k == SyntaxKind::CONTROL_WORD && t == "\\bar@baz")
);
}
#[test]
fn a_stray_percent_line_inside_macrocode_is_a_code_comment() {
let input =
"% \\begin{macrocode}\n\\foo\n% an in-code comment\n\\bar\n% \\end{macrocode}\n";
let root = parse_dtx(input);
assert!(tokens(&root).contains(&(SyntaxKind::COMMENT, "% an in-code comment".to_string())));
assert_eq!(count_token(&root, SyntaxKind::DOC_MARGIN), 2);
}
#[test]
fn verbatim_command_defined_and_used_inside_macrocode_is_two_pass_stable() {
let input = "% \\begin{macrocode}\n\\newcommand\\shex[1]{\\@makeother\\$#1}\n\\shex{a_$b$}\n% \\end{macrocode}\n";
let root = parse_dtx(input);
assert_eq!(count_token(&root, SyntaxKind::VERB), 1);
}
#[test]
fn unterminated_macrocode_recovers_losslessly() {
let root = parse_dtx("% \\begin{macrocode}\n\\foo\n\\bar\n");
assert_eq!(count(&root, SyntaxKind::ENVIRONMENT), 1);
}
#[test]
fn doc_margins_never_form_a_doc_comment() {
let root = parse_dtx(
"% Some prose about \\foo.\n% \\begin{macrocode}\n\\foo\n% \\end{macrocode}\n",
);
assert_eq!(count(&root, SyntaxKind::DOC_COMMENT), 0);
}
#[test]
fn macrocode_frame_margins_sit_where_the_formatter_expects() {
let root = parse_dtx("% \\begin{macrocode}\n\\def\\foo{x}\n% \\end{macrocode}\n");
let env = root
.descendants()
.find(|n| n.kind() == SyntaxKind::ENVIRONMENT)
.expect("environment");
let begin = env
.children()
.find(|c| c.kind() == SyntaxKind::BEGIN)
.expect("begin");
let mut tok = begin.first_token().and_then(|t| t.prev_token());
while let Some(t) = &tok {
if t.kind() == SyntaxKind::WHITESPACE {
tok = t.prev_token();
} else {
break;
}
}
assert_eq!(
tok.map(|t| t.kind()),
Some(SyntaxKind::DOC_MARGIN),
"opening `\\begin` must be preceded by a DOC_MARGIN on its line"
);
let children: Vec<_> = env.children_with_tokens().collect();
let last_margin = children
.iter()
.rposition(|e| e.kind() == SyntaxKind::DOC_MARGIN)
.expect("a closing-frame DOC_MARGIN");
let end = children
.iter()
.position(|e| e.kind() == SyntaxKind::END)
.expect("END node");
assert!(
last_margin < end,
"the closing-frame DOC_MARGIN must precede the END node within the environment"
);
}
#[test]
fn macro_env_and_describe_parse_in_the_doc_layer() {
let root =
parse_dtx("% \\begin{macro}{\\foo}\n% \\DescribeMacro{\\foo} does foo.\n% \\end{macro}\n");
assert_eq!(count(&root, SyntaxKind::ENVIRONMENT), 1);
let toks = tokens(&root);
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\DescribeMacro".to_string())));
assert_eq!(count(&root, SyntaxKind::DOC_COMMENT), 0);
}
#[test]
fn doc_associations_pair_prose_with_nested_code() {
let src = "% \\begin{macro}{\\foo}\n% The \\foo macro.\n% \\begin{macrocode}\n\\def\\foo{x}\n% \\end{macrocode}\n% \\end{macro}\n% \\DescribeMacro\\bar is described.\n";
let root = parse_dtx(src);
let assocs = doc_associations(&root);
assert_eq!(assocs.len(), 2);
assert_eq!(assocs[0].name, "\\foo");
assert_eq!(assocs[0].kind, DocKind::Macro);
assert_eq!(assocs[0].code.len(), 1);
assert!(src[assocs[0].code[0]].contains("\\def\\foo{x}"));
assert_eq!(assocs[1].name, "\\bar");
assert_eq!(assocs[1].kind, DocKind::DescribeMacro);
assert!(assocs[1].code.is_empty());
}
#[test]
fn meta_comment_header_parses_as_documentation() {
let root = parse_dtx("% \\iffalse meta-comment\n% \\fi\n");
let toks = tokens(&root);
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\iffalse".to_string())));
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\fi".to_string())));
assert_eq!(count_token(&root, SyntaxKind::DOC_MARGIN), 2);
}
#[test]
fn full_self_extracting_dtx_needs_no_driver_machinery() {
let root = parse_dtx(
"% \\iffalse meta-comment\n\
%<*driver>\n\
\\documentclass{ltxdoc}\n\
\\DocInput{foo.dtx}\n\
%</driver>\n\
% \\fi\n\
% \\section{The \\foo macro}\n\
%<*package>\n\
% \\begin{macrocode}\n\
\\newcommand\\foo[1]{\\bar@baz{#1}}\n\
% \\end{macrocode}\n\
%</package>\n",
);
let toks = tokens(&root);
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\iffalse".to_string())));
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\fi".to_string())));
for guard in ["%<*driver>", "%</driver>", "%<*package>", "%</package>"] {
assert!(toks.contains(&(SyntaxKind::GUARD, guard.to_string())));
}
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\documentclass".to_string())));
assert_eq!(count(&root, SyntaxKind::ENVIRONMENT), 1);
assert!(toks.contains(&(SyntaxKind::CONTROL_WORD, "\\bar@baz".to_string())));
assert_eq!(count_token(&root, SyntaxKind::VERBATIM_BODY), 0);
}