vize_l1 0.591.0

L1 - the lossless Vue-template surface tree (codename Sinopia, Davinci P2-7)
Documentation
//! Descriptor-selected original template ownership, without File completion.

use super::{NativeChildren, NativeComponent};
use crate::container::vue::{AdmittedDescriptor, ScriptRole, ScriptView};
use crate::{embed::Lang, markup::ComponentSourceError};
use vize_l0::{Allocator, SourceBlock};

/// Intrinsic first-family grammar, selected by the authentic Descriptor.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeTemplateGrammar {
    JavaScriptModule,
    TypeScriptModule,
}

#[derive(Debug, Clone, Copy)]
struct Selection<'a> {
    index: usize,
    block: SourceBlock<'a>,
    lang: Lang,
    role: ScriptRole,
}
impl<'a> Selection<'a> {
    fn original(view: ScriptView<'_, 'a>) -> Self {
        Self {
            index: view.container_index(),
            block: view.block(),
            lang: view.lang(),
            role: view.role(),
        }
    }
}

/// Original Component parsed once from an authentic selected template.
/// The short Descriptor borrow ends before this owner is stored or moved.
/// No caller-selected block, language, role or raw Component can mint it.
/// This owner does not certify clean grammar or a completed native File.
///
/// ```compile_fail
/// use vize_l1::markup::NativeTemplateComponent;
/// fn requires_clone<T: Clone>() {}
/// requires_clone::<NativeTemplateComponent<'static>>();
/// ```
#[derive(Debug)]
pub struct NativeTemplateComponent<'a> {
    component: NativeComponent<'a>,
    index: usize,
    grammar: NativeTemplateGrammar,
    has_styles: bool,
    ordinary: Option<Selection<'a>>,
    setup: Option<Selection<'a>>,
}

impl<'a> NativeTemplateComponent<'a> {
    /// Missing selected template returns `None` without parsing another block.
    pub fn parse_in(
        allocator: &'a Allocator,
        descriptor: AdmittedDescriptor<'_, 'a>,
    ) -> Result<Option<Self>, ComponentSourceError> {
        let Some(template) = descriptor.template() else {
            return Ok(None);
        };
        let grammar = match descriptor.template_lang() {
            Lang::Js => NativeTemplateGrammar::JavaScriptModule,
            Lang::Ts => NativeTemplateGrammar::TypeScriptModule,
        };
        Ok(Some(Self {
            component: NativeComponent::parse_selected_in(allocator, template.block())?,
            index: template.container_index(),
            grammar,
            has_styles: descriptor.styles().len() != 0,
            ordinary: descriptor.ordinary().map(Selection::original),
            setup: descriptor.setup().map(Selection::original),
        }))
    }
    #[must_use]
    pub fn component(&self) -> &NativeComponent<'a> {
        &self.component
    }
    #[must_use]
    pub fn template_index(&self) -> usize {
        self.index
    }
    #[must_use]
    pub fn grammar(&self) -> NativeTemplateGrammar {
        self.grammar
    }
    /// Authentic Descriptor style presence, retained without a second parse.
    #[must_use]
    pub fn has_styles(&self) -> bool {
        self.has_styles
    }
    #[must_use]
    pub fn children(&self) -> NativeChildren<'_, 'a> {
        self.component.children()
    }
    #[must_use]
    pub fn ordinary(&self) -> Option<NativeScriptSelection<'_, 'a>> {
        self.ordinary.map(|selected| NativeScriptSelection {
            owner: self,
            selected,
        })
    }
    #[must_use]
    pub fn setup(&self) -> Option<NativeScriptSelection<'_, 'a>> {
        self.setup.map(|selected| NativeScriptSelection {
            owner: self,
            selected,
        })
    }
    /// Consuming transfer drops Descriptor selection authority.
    #[must_use]
    pub fn into_component(self) -> NativeComponent<'a> {
        self.component
    }
}

/// Readonly original script selection borrowing its selected template owner.
#[derive(Debug)]
pub struct NativeScriptSelection<'o, 'a> {
    owner: &'o NativeTemplateComponent<'a>,
    selected: Selection<'a>,
}
impl<'o, 'a> NativeScriptSelection<'o, 'a> {
    #[must_use]
    pub fn owner(&self) -> &'o NativeTemplateComponent<'a> {
        self.owner
    }
    #[must_use]
    pub fn container_index(&self) -> usize {
        self.selected.index
    }
    #[must_use]
    pub fn block(&self) -> SourceBlock<'a> {
        self.selected.block
    }
    #[must_use]
    pub fn lang(&self) -> Lang {
        self.selected.lang
    }
    #[must_use]
    pub fn role(&self) -> ScriptRole {
        self.selected.role
    }
}

#[cfg(test)]
mod tests {
    use super::{NativeTemplateComponent, NativeTemplateGrammar};
    use crate::container::Vue;
    use crate::container::vue::{DescriptorOptions, ScriptRole};
    use crate::{embed::Lang, markup::SurfaceParseOptions};
    use vize_l0::{
        Allocator,
        config::{VueDialect, VueVersion},
    };

    fn options() -> DescriptorOptions {
        DescriptorOptions {
            version: VueVersion::V3,
            dialect: VueDialect::Vue,
            template: SurfaceParseOptions::default(),
        }
    }

    #[test]
    fn selected_template_does_not_promote_script_or_comment_markup() {
        let arena = Allocator::default();
        let source = "<!-- <template><i/></template> --><script lang=ts>const fake='<div/>'</script><template><p a='full value'/></template><script setup lang=ts>const msg=1</script>";
        let descriptor = Vue.observe_descriptor(&arena, source, options());
        let admitted = descriptor.admitted().unwrap();
        let selected = admitted.template().unwrap();
        let original = selected.block();
        let owner = NativeTemplateComponent::parse_in(&arena, admitted)
            .unwrap()
            .unwrap();
        assert_eq!(owner.template_index(), selected.container_index());
        assert_eq!(owner.component().block(), original);
        assert!(core::ptr::eq(
            owner.component().block().root_source(),
            source
        ));
        assert_eq!(owner.grammar(), NativeTemplateGrammar::TypeScriptModule);
        let p = owner.children().next().unwrap().into_element().unwrap();
        assert_eq!(p.surface().tag(), "p");
        assert_eq!(
            p.attributes()
                .next()
                .unwrap()
                .surface()
                .value
                .as_ref()
                .unwrap()
                .content
                .text,
            "full value"
        );
        let ordinary = owner.ordinary().unwrap();
        let setup = owner.setup().unwrap();
        assert!(core::ptr::eq(ordinary.owner(), &owner));
        assert!(core::ptr::eq(setup.owner(), &owner));
        assert_eq!(
            ordinary.container_index(),
            admitted.ordinary().unwrap().container_index()
        );
        assert_eq!(
            setup.container_index(),
            admitted.setup().unwrap().container_index()
        );
        assert_eq!(ordinary.block(), admitted.ordinary().unwrap().block());
        assert_eq!(setup.block(), admitted.setup().unwrap().block());
        assert_eq!(ordinary.role(), ScriptRole::Ordinary);
        assert_eq!(setup.role(), ScriptRole::Setup);
        assert_eq!(ordinary.lang(), Lang::Ts);
        assert_eq!(setup.lang(), Lang::Ts);
    }

    #[test]
    fn scriptless_empty_template_survives_descriptor_drop_and_owner_move() {
        let arena = Allocator::default();
        let source = "<template></template>";
        let owner = {
            let descriptor = Vue.observe_descriptor(&arena, source, options());
            NativeTemplateComponent::parse_in(&arena, descriptor.admitted().unwrap())
                .unwrap()
                .unwrap()
        };
        let moved = core::hint::black_box(owner);
        assert_eq!(moved.grammar(), NativeTemplateGrammar::JavaScriptModule);
        assert!(moved.ordinary().is_none());
        assert!(moved.setup().is_none());
        assert_eq!(moved.children().len(), 0);
        assert_eq!(moved.component().block().source(), "");
        assert!(core::ptr::eq(
            moved.component().block().root_source(),
            source
        ));
        assert_eq!(moved.into_component().into_carrier().tree.children.len(), 0);
    }

    #[test]
    fn absent_template_is_distinct_from_selected_empty_template() {
        let arena = Allocator::default();
        let source = "<script>const fake='<div/>'</script>";
        let descriptor = Vue.observe_descriptor(&arena, source, options());
        let admitted = descriptor.admitted().unwrap();
        assert!(admitted.template().is_none());
        assert!(
            NativeTemplateComponent::parse_in(&arena, admitted)
                .unwrap()
                .is_none()
        );
    }

    #[test]
    fn authentic_style_presence_survives_descriptor_drop_and_owner_move() {
        let arena = Allocator::default();
        for (source, has_styles) in [
            ("<template><div/></template>", false),
            ("<!-- <style scoped/> --><template><div/></template>", false),
            (
                "<script>const fake='<style/>'</script><template><div/></template>",
                false,
            ),
            ("<template><div/></template><style></style>", true),
            (
                "<style scoped>div{color:red}</style><template><div/></template>",
                true,
            ),
            (
                "<template><div/></template><style lang=scss>$x:red;</style>",
                true,
            ),
        ] {
            let selected = {
                let descriptor = Vue.observe_descriptor(&arena, source, options());
                let admitted = descriptor.admitted().unwrap();
                assert_eq!(admitted.styles().len() != 0, has_styles);
                NativeTemplateComponent::parse_in(&arena, admitted)
                    .unwrap()
                    .unwrap()
            };
            let moved = core::hint::black_box(selected);
            assert_eq!(moved.has_styles(), has_styles);
            assert!(core::ptr::eq(
                moved.component().block().root_source(),
                source
            ));
        }
    }

    #[test]
    fn setup_only_typescript_grammar_is_derived_from_original_role() {
        let arena = Allocator::default();
        let source =
            "<template><span/></template><script setup lang=ts>const msg: string='x'</script>";
        let descriptor = Vue.observe_descriptor(&arena, source, options());
        let owner = NativeTemplateComponent::parse_in(&arena, descriptor.admitted().unwrap())
            .unwrap()
            .unwrap();
        assert_eq!(owner.grammar(), NativeTemplateGrammar::TypeScriptModule);
        assert!(owner.ordinary().is_none());
        let setup = owner.setup().unwrap();
        assert_eq!(setup.role(), ScriptRole::Setup);
        assert_eq!(setup.lang(), Lang::Ts);
        assert_eq!(setup.block().source(), "const msg: string='x'");
    }

    #[test]
    fn selected_owner_retains_original_recovery_without_certifying_clean_grammar() {
        let arena = Allocator::default();
        let descriptor = Vue.observe_descriptor(&arena, "<template><div></template>", options());
        let owner = NativeTemplateComponent::parse_in(&arena, descriptor.admitted().unwrap())
            .unwrap()
            .unwrap();
        let div = owner.children().next().unwrap().into_element().unwrap();
        assert_eq!(div.surface().tag(), "div");
        assert!(matches!(
            div.surface().close,
            crate::markup::ElementClose::Missing
        ));
        assert_eq!(owner.component().block().source(), "<div>");
    }

    #[test]
    fn ambiguous_or_unsupported_descriptors_cannot_supply_selected_admission() {
        let arena = Allocator::default();
        for source in [
            "<template/><script lang=js></script><script setup lang=ts></script>",
            "<template/><template/>",
            "<template/><script lang='t&#115;'></script>",
            "<template lang=pug>div</template>",
        ] {
            let descriptor = Vue.observe_descriptor(&arena, source, options());
            assert!(descriptor.admitted().is_err(), "{source}");
        }
    }
}