use super::{NativeChildren, NativeComponent};
use crate::container::vue::{AdmittedDescriptor, ScriptRole, ScriptView};
use crate::{embed::Lang, markup::ComponentSourceError};
use vize_l0::{Allocator, SourceBlock};
#[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(),
}
}
}
#[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> {
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
}
#[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,
})
}
#[must_use]
pub fn into_component(self) -> NativeComponent<'a> {
self.component
}
}
#[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::{SurfaceParseOptions, embed::Lang};
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::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='ts'></script>",
"<template lang=pug>div</template>",
] {
let descriptor = Vue.observe_descriptor(&arena, source, options());
assert!(descriptor.admitted().is_err(), "{source}");
}
}
}