adoctra-core 0.0.2

Core parser and document model for the adoctra AsciiDoc toolchain
Documentation
use adoctra_core::{
    parse_resource, BlockKind, ParseOptions, Resource, ResourceError, ResourceErrorKind,
    ResourceProvider, ResourceRequest, ResourceResult, SafeMode, SourceId, SourceMap, Span,
};
use std::cell::RefCell;

#[derive(Default)]
struct Host {
    events: RefCell<Vec<String>>,
    missing: bool,
}

impl ResourceProvider for Host {
    fn enable_uri_cache(&self) {
        self.events.borrow_mut().push("enable".into());
    }

    fn load(&self, request: ResourceRequest<'_>) -> ResourceResult<Resource> {
        self.events.borrow_mut().push(request.target.into());
        if self.missing {
            return Err(ResourceError::new(
                ResourceErrorKind::NotFound,
                request.target,
                "missing",
            ));
        }
        Ok(Resource {
            name: request.target.into(),
            text: "*remote*\n".into(),
        })
    }
}

#[test]
fn cache_activation_follows_uri_permission_depth_and_lazy_attributes() {
    for (source, allow, safe, depth, expected) in [
        (":cache-uri:\n\ninclude::http://host/a.adoc[]", true, SafeMode::Safe, "64", vec!["enable", "http://host/a.adoc"]),
        (":cache-uri:\n\ninclude::http://host/a.adoc[]", false, SafeMode::Safe, "64", vec![]),
        (":cache-uri:\n\ninclude::http://host/a.adoc[]", true, SafeMode::Secure, "64", vec![]),
        (":cache-uri:\n\ninclude::http://host/a.adoc[]", true, SafeMode::Safe, "0", vec![]),
        (":cache-uri:\n\ninclude::local.adoc[]", true, SafeMode::Safe, "64", vec!["local.adoc"]),
        (":cache-uri:\n:cache-uri!:\n\ninclude::http://host/a.adoc[]", true, SafeMode::Safe, "64", vec!["http://host/a.adoc"]),
        ("ifdef::absent[]\ninclude::http://host/a.adoc[]\nendif::[]", true, SafeMode::Safe, "64", vec![]),
        ("include::http://host/a.adoc[]\n\n:cache-uri:\n\ninclude::http://host/a.adoc[]\n\n:cache-uri!:\n\ninclude::http://host/a.adoc[]", true, SafeMode::Server, "64", vec!["http://host/a.adoc", "enable", "http://host/a.adoc", "http://host/a.adoc"]),
    ] {
        let host = Host::default();
        let mut sources = SourceMap::new();
        let primary = sources.add("main.adoc", source);
        let mut options = ParseOptions { source_id: primary, safe, ..Default::default() };
        options.attributes.set("max-include-depth", depth);
        if allow { options.attributes.set("allow-uri-read", ""); }
        let result = parse_resource(&mut sources, &options, &host).unwrap();
        assert!(result.diagnostics.is_empty(), "{source}: {:?}", result.diagnostics);
        assert_eq!(*host.events.borrow(), expected, "{source}");
        if expected.contains(&"http://host/a.adoc") {
            let block = &result.document.blocks[0];
            assert!(matches!(block.kind, BlockKind::Paragraph { .. }));
            assert_eq!(block.span.source_id, SourceId(1));
            assert_eq!(sources.slice(Span::in_source(SourceId(1), 0, 8)), Some("*remote*"));
        }
    }
}

#[test]
fn optional_uri_errors_keep_the_directive_cursor_and_resolved_uri() {
    for optional in ["", "opts=optional"] {
        let source = format!(":cache-uri:\n\ninclude::http://host/missing.adoc[{optional}]\n");
        let host = Host {
            missing: true,
            ..Default::default()
        };
        let mut sources = SourceMap::new();
        let primary = sources.add("main.adoc", source.clone());
        let mut options = ParseOptions {
            source_id: primary,
            ..Default::default()
        };
        options.attributes.set("allow-uri-read", "");
        let result = parse_resource(&mut sources, &options, &host).unwrap();
        assert_eq!(result.diagnostics.len(), 1);
        let diagnostic = &result.diagnostics[0];
        assert_eq!(
            diagnostic.report.message.as_deref(),
            Some("include uri not readable: http://host/missing.adoc")
        );
        assert_eq!(diagnostic.span.source_id, primary);
        assert_eq!(
            sources
                .get(primary)
                .unwrap()
                .position(diagnostic.span.start)
                .unwrap()
                .line,
            3
        );
        assert_eq!(
            sources.slice(diagnostic.span),
            Some(source.lines().nth(2).unwrap())
        );
        assert!(sources.get(SourceId(1)).is_none());
        assert_eq!(
            *host.events.borrow(),
            ["enable", "http://host/missing.adoc"]
        );
    }
}