rd-ast 0.0.1

Canonical, producer-agnostic AST for R's Rd (documentation) format
Documentation
use super::*;

fn path() -> RdPath {
    RdPath::new(vec![RdPathSegment::TopLevel(2), RdPathSegment::Child(1)])
}

#[test]
fn every_inline_tag_maps_and_preserves_body() {
    let cases = [
        (RdTag::Emph, RdInlineSpanKind::Emph),
        (RdTag::Strong, RdInlineSpanKind::Strong),
        (RdTag::Bold, RdInlineSpanKind::Bold),
        (RdTag::Code, RdInlineSpanKind::Code),
        (RdTag::Special, RdInlineSpanKind::Special),
        (RdTag::Verb, RdInlineSpanKind::Verb),
        (RdTag::Url, RdInlineSpanKind::Url),
        (RdTag::Email, RdInlineSpanKind::Email),
        (RdTag::File, RdInlineSpanKind::File),
        (RdTag::Pkg, RdInlineSpanKind::Pkg),
        (RdTag::Samp, RdInlineSpanKind::Samp),
        (RdTag::SQuote, RdInlineSpanKind::SQuote),
        (RdTag::DQuote, RdInlineSpanKind::DQuote),
        (RdTag::Kbd, RdInlineSpanKind::Kbd),
        (RdTag::Var, RdInlineSpanKind::Var),
        (RdTag::Env, RdInlineSpanKind::Env),
        (RdTag::Command, RdInlineSpanKind::Command),
        (RdTag::Option, RdInlineSpanKind::Option),
        (RdTag::Acronym, RdInlineSpanKind::Acronym),
        (RdTag::Abbr, RdInlineSpanKind::Abbr),
        (RdTag::Cite, RdInlineSpanKind::Cite),
        (RdTag::Dfn, RdInlineSpanKind::Dfn),
    ];
    let base = path();
    for (tag, kind) in cases {
        let body = vec![RdNode::Text("body".into()), RdNode::RCode("x".into())];
        let node = RdNode::tagged(tag, None, body.clone());
        let lossy = node.inline_span(&base).unwrap();
        let strict = node.inspect_inline_span(&base).unwrap().unwrap();
        for view in [lossy, strict] {
            assert_eq!(view.path(), &base);
            assert_eq!(view.kind(), kind);
            assert_eq!(view.body(), body.as_slice());
        }
    }
}

#[test]
fn inline_bodies_pass_through_including_empty_and_nested() {
    let base = path();
    let cases = [
        RdNode::tagged(RdTag::Emph, None, vec![RdNode::Text("text".into())]),
        RdNode::tagged(RdTag::Code, None, vec![RdNode::RCode("x()".into())]),
        RdNode::tagged(RdTag::Verb, None, vec![RdNode::Verb("literal".into())]),
        RdNode::tagged(
            RdTag::Cite,
            None,
            vec![RdNode::tagged(RdTag::Emph, None, vec![])],
        ),
        RdNode::tagged(RdTag::Kbd, None, vec![]),
    ];
    for node in cases {
        assert!(node.inline_span(&base).is_some());
        assert!(node.inspect_inline_span(&base).is_ok());
    }
}

#[test]
fn inline_options_and_raw_nodes_are_not_interpreted() {
    let base = path();
    for option in [Some(vec![]), Some(vec![RdNode::Text("option".into())])] {
        let node = RdNode::tagged(RdTag::Emph, option, vec![]);
        assert!(node.inline_span(&base).is_none());
        let error = node.inspect_inline_span(&base).unwrap_err();
        assert_eq!(error.path(), &base);
        assert_eq!(error.tag(), Some(&RdTag::Emph));
        assert!(matches!(error.kind(), RdShapeErrorKind::UnexpectedOption));
    }
    for spelling in [r"\emph", r"\code", r"\verb"] {
        let node = RdNode::Raw(crate::producer::raw_node(
            Some(spelling.into()),
            None,
            vec![RdNode::Text("opaque".into())],
            Some(crate::RawRdValue::Character(vec![Some("payload".into())])),
            vec![],
        ));
        assert!(node.inline_span(&base).is_none());
        let error = node.inspect_inline_span(&base).unwrap_err();
        assert_eq!(error.path(), &base);
        assert_eq!(error.tag(), Some(&RdTag::from_rd_tag(spelling)));
        assert!(matches!(
            error.kind(),
            RdShapeErrorKind::UnexpectedNode {
                expected: RdExpectedNode::Tagged,
                actual: RdNodeKind::Raw
            }
        ));
    }
}

#[test]
fn inline_unrelated_and_excluded_nodes_are_ignored() {
    let base = path();
    let nodes = [
        RdNode::tagged(RdTag::Title, None, vec![]),
        RdNode::Text("leaf".into()),
        RdNode::RCode("code".into()),
        RdNode::group(vec![]),
        RdNode::tagged(RdTag::Preformatted, None, vec![]),
        RdNode::tagged(RdTag::Out, None, vec![]),
        RdNode::tagged(RdTag::I, None, vec![]),
        RdNode::tagged(RdTag::Doi, None, vec![]),
        RdNode::tagged(RdTag::CranPkg, None, vec![]),
        RdNode::tagged(RdTag::LinkS4Class, None, vec![]),
        RdNode::tagged(RdTag::Sspace, None, vec![]),
    ];
    for node in nodes {
        assert!(node.inline_span(&base).is_none());
        assert!(node.inspect_inline_span(&base).unwrap().is_none());
    }
}

#[test]
fn text_symbols_preserve_kind_fallback_and_shape_errors() {
    let base = path();
    for (tag, kind, fallback) in [
        (RdTag::R, RdTextSymbolKind::R, "R"),
        (RdTag::Dots, RdTextSymbolKind::Dots, "..."),
        (RdTag::LDots, RdTextSymbolKind::LDots, "..."),
    ] {
        let node = RdNode::tagged(tag, None, vec![]);
        let view = node.text_symbol(&base).unwrap();
        assert_eq!(view.path(), &base);
        assert_eq!(view.kind(), kind);
        assert_eq!(view.fallback_text(), fallback);
        assert_eq!(node.inspect_text_symbol(&base).unwrap().unwrap(), view);
    }
    assert_ne!(RdTextSymbolKind::Dots, RdTextSymbolKind::LDots);
    assert_eq!(
        RdTextSymbolKind::Dots.fallback_text(),
        RdTextSymbolKind::LDots.fallback_text()
    );

    let option = RdNode::tagged(RdTag::R, Some(vec![]), vec![]);
    assert!(option.text_symbol(&base).is_none());
    assert!(matches!(
        option.inspect_text_symbol(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedOption
    ));

    for count in [1, 2] {
        let node = RdNode::tagged(RdTag::Dots, None, vec![RdNode::Text("x".into()); count]);
        assert!(node.text_symbol(&base).is_none());
        let error = node.inspect_text_symbol(&base).unwrap_err();
        assert_eq!(error.path(), &base);
        assert_eq!(error.tag(), Some(&RdTag::Dots));
        assert!(
            matches!(error.kind(), RdShapeErrorKind::WrongArity { expected: RdArity::Exactly(0), actual } if *actual == count)
        );
    }

    let raw = RdNode::Raw(crate::producer::raw_node(
        Some(r"\R".into()),
        None,
        vec![],
        None,
        vec![],
    ));
    assert!(raw.text_symbol(&base).is_none());
    assert!(matches!(
        raw.inspect_text_symbol(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedNode {
            expected: RdExpectedNode::Tagged,
            actual: RdNodeKind::Raw
        }
    ));
    let unrelated = RdNode::Raw(crate::producer::raw_node(
        Some(r"\other".into()),
        None,
        vec![],
        None,
        vec![],
    ));
    assert!(unrelated.inspect_text_symbol(&base).unwrap().is_none());
}