rd-ast 0.0.1

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

#[test]
fn link_views_follow_r_destination_rules() {
    let base = RdPath::new(vec![RdPathSegment::TopLevel(2), RdPathSegment::Child(5)]);
    let inspect = |option: Option<&str>, children: Vec<RdNode>| -> String {
        let option = option.map(|value| vec![RdNode::Text(value.into())]);
        let node = RdTagged::new(RdTag::Link, option, children);
        match node.inspect_link(&base).unwrap().destination() {
            RdLinkDestination::DisplayText { .. } => "display".into(),
            RdLinkDestination::Explicit { topic } => format!("={topic}"),
            RdLinkDestination::Package { package, topic } => match topic {
                RdLinkTopic::DisplayText(_) => format!("{package}:display"),
                RdLinkTopic::Explicit(topic) => format!("{package}:{topic}"),
            },
        }
    };
    assert_eq!(inspect(None, vec![]), "display");
    assert_eq!(
        inspect(Some("pkg"), vec![RdNode::Text("x".into())]),
        "pkg:display"
    );
    assert_eq!(
        inspect(
            Some("pkg:topic"),
            vec![RdNode::tagged(RdTag::Code, None, vec![])]
        ),
        "pkg:topic"
    );
    assert_eq!(
        inspect(
            Some("=dest"),
            vec![RdNode::tagged(RdTag::Code, None, vec![])]
        ),
        "=dest"
    );
    assert_eq!(inspect(Some("=a:b"), vec![]), "=a:b");
    assert_eq!(inspect(Some("a:b:c"), vec![]), "a:b:c");
    assert_eq!(inspect(Some("="), vec![]), "=");
    assert_eq!(inspect(Some(":topic"), vec![]), ":topic");
    assert_eq!(inspect(Some("pkg:"), vec![]), "pkg:");

    let bad = RdTagged::new(
        RdTag::Link,
        None,
        vec![RdNode::tagged(RdTag::Code, None, vec![])],
    );
    let error = bad.inspect_link(&base).unwrap_err();
    assert_eq!(error.path(), &base.with_child(0));
    assert!(matches!(
        error.kind(),
        RdShapeErrorKind::UnexpectedContent {
            actual: RdNodeKind::Tagged
        }
    ));
    let bad = RdTagged::new(
        RdTag::Link,
        Some(vec![RdNode::Text("pkg".into())]),
        vec![RdNode::tagged(RdTag::Code, None, vec![])],
    );
    let error = bad.inspect_link(&base).unwrap_err();
    assert_eq!(error.path(), &base.with_child(0));
    assert!(matches!(
        error.kind(),
        RdShapeErrorKind::UnexpectedContent {
            actual: RdNodeKind::Tagged
        }
    ));
    let malformed = RdTagged::new(
        RdTag::Link,
        Some(vec![RdNode::Text("a".into()), RdNode::Text("b".into())]),
        vec![],
    );
    assert!(matches!(
        malformed.inspect_link(&base).unwrap_err().kind(),
        RdShapeErrorKind::WrongArity { .. }
    ));
    let malformed = RdTagged::new(RdTag::Link, Some(vec![RdNode::group(vec![])]), vec![]);
    assert!(matches!(
        malformed.inspect_link(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedContent {
            actual: RdNodeKind::Group
        }
    ));
}

#[test]
fn href_view_validates_groups_and_paths() {
    let base = RdPath::new(vec![RdPathSegment::TopLevel(2), RdPathSegment::Child(5)]);
    let node = RdTagged::new(
        RdTag::Href,
        None,
        vec![
            RdNode::group(vec![RdNode::Verb("URL".into())]),
            RdNode::group(vec![RdNode::tagged(RdTag::Code, None, vec![])]),
        ],
    );
    let href = node.inspect_href(&base).unwrap();
    assert!(matches!(href.url(), [RdNode::Verb(value)] if value == "URL"));
    assert_eq!(href.path(), &base);
    let option = RdTagged::new(RdTag::Href, Some(vec![]), vec![]);
    assert!(matches!(
        option.inspect_href(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedOption
    ));
    let arity = RdTagged::new(RdTag::Href, None, vec![]);
    assert!(matches!(
        arity.inspect_href(&base).unwrap_err().kind(),
        RdShapeErrorKind::WrongArity {
            expected: RdArity::Exactly(2),
            actual: 0
        }
    ));
    let child = RdTagged::new(
        RdTag::Href,
        None,
        vec![RdNode::Text("bad".into()), RdNode::group(vec![])],
    );
    let error = child.inspect_href(&base).unwrap_err();
    assert_eq!(error.path(), &base.with_child(0));
    assert!(matches!(
        error.kind(),
        RdShapeErrorKind::UnexpectedNode {
            expected: RdExpectedNode::Group,
            actual: RdNodeKind::Text
        }
    ));
    let wrong = RdTagged::new(RdTag::Link, None, vec![]);
    assert!(matches!(
        wrong.inspect_href(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedNode {
            expected: RdExpectedNode::Href,
            ..
        }
    ));
    assert_eq!(
        wrong.inspect_href(&base).unwrap_err().to_string(),
        r"expected \href node, found tagged node for \link at top-level[2] / child[5]"
    );
    let wrong = RdTagged::new(RdTag::Href, None, vec![]);
    assert!(matches!(
        wrong.inspect_link(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedNode {
            expected: RdExpectedNode::Link,
            ..
        }
    ));
}

#[test]
fn s4_class_links_preserve_class_and_package_contents() {
    let base = RdPath::new(vec![RdPathSegment::TopLevel(1)]);
    let node = RdNode::tagged(
        RdTag::LinkS4Class,
        None,
        vec![RdNode::Text("myClass".into())],
    );
    let view = node.inspect_s4_class_link(&base).unwrap().unwrap();
    assert_eq!(view.class_text(), Some("myClass".into()));
    assert_eq!(view.package(), None);
    assert_eq!(view.package_text(), None);
    let empty = RdNode::tagged(RdTag::LinkS4Class, Some(vec![]), vec![]);
    let empty = empty.s4_class_link(&base).unwrap();
    assert_eq!(empty.package(), Some([].as_slice()));
    assert_eq!(empty.package_text(), Some(String::new()));

    let class_child = RdNode::tagged(RdTag::Emph, None, vec![]);
    let option_child = RdNode::tagged(RdTag::Code, None, vec![]);
    let node = RdNode::tagged(
        RdTag::LinkS4Class,
        Some(vec![RdNode::Text("methods".into()), option_child.clone()]),
        vec![RdNode::Text("my".into()), class_child.clone()],
    );
    let view = node.s4_class_link(&base).unwrap();
    assert_eq!(view.class(), node.as_tagged().unwrap().children());
    assert_eq!(view.package(), node.as_tagged().unwrap().option());
    assert_eq!(view.class_text(), None);
    assert_eq!(view.package_text(), None);
    let plain = RdNode::tagged(
        RdTag::LinkS4Class,
        Some(vec![RdNode::Text("methods".into())]),
        vec![RdNode::Text("myClass".into())],
    );
    assert_eq!(
        plain.s4_class_link(&base).unwrap().package_text(),
        Some("methods".into())
    );
    assert_eq!(
        plain.s4_class_link(&base).unwrap().class_text(),
        Some("myClass".into())
    );
}

#[test]
fn s4_class_links_only_reject_matching_raw() {
    let base = RdPath::new(vec![RdPathSegment::TopLevel(1)]);
    let raw = RdNode::Raw(producer::raw_node(
        Some(r"\linkS4class".into()),
        None,
        vec![],
        None,
        vec![],
    ));
    assert!(matches!(
        raw.inspect_s4_class_link(&base).unwrap_err().kind(),
        RdShapeErrorKind::UnexpectedNode {
            expected: RdExpectedNode::Tagged,
            actual: RdNodeKind::Raw
        }
    ));
    for tag in [RdTag::Link, RdTag::Href] {
        assert_eq!(
            RdNode::tagged(tag, None, vec![])
                .inspect_s4_class_link(&base)
                .unwrap(),
            None
        );
    }
}