use crate::prelude::*;
use beet_core::prelude::*;
use beet_dom::prelude::*;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use proc_macro2_diagnostics::Diagnostic;
use proc_macro2_diagnostics::Level;
use rstml::Parser;
use rstml::ParserConfig;
use rstml::node::KVAttributeValue;
use rstml::node::KeyedAttributeValue;
use rstml::node::Node;
use rstml::node::NodeAttribute;
use rstml::node::NodeBlock;
use rstml::node::NodeElement;
use rstml::node::NodeName;
use send_wrapper::SendWrapper;
use syn::Expr;
use syn::ExprLit;
use syn::spanned::Spanned;
pub(super) type RstmlCustomNode = rstml::Infallible;
#[derive(Debug, Clone, Deref, Component)]
#[require(SnippetRoot)]
pub struct RstmlTokens(SendWrapper<TokenStream>);
impl RstmlTokens {
pub fn new(tokens: TokenStream) -> Self { Self(SendWrapper::new(tokens)) }
pub fn take(self) -> TokenStream { self.0.take() }
}
#[derive(Debug, Deref, DerefMut, Component)]
pub struct TokensDiagnostics(pub SendWrapper<Vec<Diagnostic>>);
impl TokensDiagnostics {
pub fn new(value: Vec<Diagnostic>) -> Self { Self(SendWrapper::new(value)) }
pub fn take(self) -> Vec<Diagnostic> { self.0.take() }
pub fn into_tokens(self) -> Vec<TokenStream> {
self.take()
.into_iter()
.map(|d| d.emit_as_expr_tokens())
.collect()
}
}
pub fn create_rstml_parser(
constants: &HtmlConstants,
) -> Parser<RstmlCustomNode> {
Parser::new(
ParserConfig::new()
.recover_block(true)
.always_self_closed_elements(
constants.self_closing_elements.clone(),
)
.raw_text_elements(constants.raw_text_elements.clone())
.macro_call_pattern(quote::quote!(rsx! {%%}))
.custom_node::<RstmlCustomNode>(),
)
}
pub(super) fn parse_rstml_tokens(
_: TempNonSendMarker,
mut commands: Commands,
constants: Res<HtmlConstants>,
parser: NonSend<Parser<RstmlCustomNode>>,
query: Populated<(Entity, &SnippetRoot, &RstmlTokens), Added<RstmlTokens>>,
) -> Result {
for (entity, snippet_root, handle) in query.iter() {
let tokens = handle.clone().take();
let (nodes, mut diagnostics) =
parser.parse_recoverable(tokens).split_vec();
let mut collected_elements = CollectedElements::default();
let children = RstmlToWorld {
constants: &constants,
file_path: &snippet_root.file,
collected_elements: &mut collected_elements,
diagnostics: &mut diagnostics,
commands: &mut commands,
expr_idx: ExprIdxBuilder::new(),
}
.spawn_nodes(ParentContext::default(), nodes);
commands
.entity(entity)
.remove::<RstmlTokens>()
.insert((collected_elements, TokensDiagnostics::new(diagnostics)))
.add_children(&children);
}
Ok(())
}
struct RstmlToWorld<'w, 's, 'a> {
file_path: &'a WsPathBuf,
constants: &'a HtmlConstants,
collected_elements: &'a mut CollectedElements,
diagnostics: &'a mut Vec<Diagnostic>,
commands: &'a mut Commands<'w, 's>,
expr_idx: ExprIdxBuilder,
}
#[derive(Default, Copy, Clone, PartialEq, Eq)]
enum ParentContext {
#[default]
None,
StyleTag,
}
impl<'w, 's, 'a> RstmlToWorld<'w, 's, 'a> {
pub fn spawn_nodes(
&mut self,
parent_cx: ParentContext,
nodes: Vec<Node<RstmlCustomNode>>,
) -> Vec<Entity> {
nodes
.into_iter()
.map(|node| {
let entity = self.commands.spawn_empty().id();
self.insert_node(parent_cx, entity, node);
entity
})
.collect()
}
fn insert_node(
&mut self,
parent_cx: ParentContext,
entity: Entity,
node: Node<RstmlCustomNode>,
) {
let node_span = node.span();
let file_span = FileSpan::new_from_span(self.file_path.clone(), &node);
match node {
Node::Doctype(_) => {
self.commands.entity(entity).insert((
DoctypeNode,
FileSpanOf::<DoctypeNode>::new(file_span),
SpanOf::<DoctypeNode>::new(node_span),
));
}
Node::Comment(node) => {
self.commands.entity(entity).insert((
CommentNode(node.value.value()),
FileSpanOf::<CommentNode>::new(file_span),
SpanOf::<CommentNode>::new(node_span),
));
}
Node::Text(node) => {
self.commands.entity(entity).insert((
TextNode::new(node.value.value()),
FileSpanOf::<TextNode>::new(file_span),
SpanOf::<TextNode>::new(node_span),
));
}
Node::RawText(node) => {
let mut text = node.to_string_best();
if parent_cx == ParentContext::StyleTag
&& self.constants.mend_raw_text_style_tags
{
text = self.mend_style_raw_text(&text);
}
self.commands.entity(entity).insert((
TextNode::new(text),
FileSpanOf::<TextNode>::new(file_span),
SpanOf::<TextNode>::new(node_span),
));
}
Node::Fragment(fragment) => {
let children = self.spawn_nodes(parent_cx, fragment.children);
self.commands
.entity(entity)
.insert((
FragmentNode,
FileSpanOf::<FragmentNode>::new(file_span),
SpanOf::<FragmentNode>::new(node_span),
))
.add_children(&children);
}
Node::Block(NodeBlock::ValidBlock(block)) => {
self.commands.entity(entity).insert((
BlockNode,
self.expr_idx.next(),
FileSpanOf::<BlockNode>::new(file_span),
SpanOf::<BlockNode>::new(node_span),
NodeExpr::new_block(block),
));
}
Node::Block(NodeBlock::Invalid(invalid)) => {
self.diagnostics.push(Diagnostic::spanned(
invalid.span(),
Level::Error,
"Invalid block",
));
self.commands.entity(entity).insert((
BlockNode,
FileSpanOf::<BlockNode>::new(file_span),
SpanOf::<BlockNode>::new(node_span),
));
}
Node::Element(el) => {
self.check_self_closing_children(&el);
let NodeElement {
open_tag,
children,
close_tag,
} = el;
let (tag_str, tag_file_span, tag_span) =
self.parse_node_name(&open_tag.name);
self.collected_elements.push((
tag_str.clone(),
send_wrapper::SendWrapper::new(tag_span.clone()),
));
let self_closing = close_tag.is_none();
if let Some(close_tag) = close_tag.as_ref() {
let (close_tag_name, _, close_tag_span) =
self.parse_node_name(&close_tag.name);
self.collected_elements.push((
close_tag_name,
send_wrapper::SendWrapper::new(close_tag_span),
));
}
let mut entity = self.commands.entity(entity);
entity.insert((
NodeTag(tag_str.clone()),
FileSpanOf::<NodeTag>::new(tag_file_span),
SpanOf::<NodeTag>::new(tag_span),
));
if tag_str.starts_with(|c: char| c.is_uppercase()) {
entity.insert((
TemplateNode,
self.expr_idx.next(),
FileSpanOf::<TemplateNode>::new(file_span),
SpanOf::<TemplateNode>::new(node_span),
));
} else {
entity.insert((
ElementNode { self_closing },
FileSpanOf::<ElementNode>::new(file_span),
SpanOf::<ElementNode>::new(node_span),
));
}
let entity = entity.id();
open_tag
.attributes
.into_iter()
.for_each(|attr| self.spawn_attribute(entity, attr));
let parent_cx = match tag_str.as_str() {
"style" => ParentContext::StyleTag,
_ => ParentContext::None,
};
let children = self.spawn_nodes(parent_cx, children);
self.commands.entity(entity).add_children(&children);
}
Node::Custom(_) => {
self.diagnostics.push(Diagnostic::spanned(
node.span(),
Level::Error,
"Unhandled custom node",
));
}
};
}
fn spawn_attribute(&mut self, parent: Entity, attr: NodeAttribute) {
match attr {
NodeAttribute::Block(NodeBlock::Invalid(block)) => {
self.diagnostics.push(Diagnostic::spanned(
block.span(),
Level::Error,
"Invalid block",
));
}
NodeAttribute::Block(NodeBlock::ValidBlock(block)) => {
let block_file_span =
FileSpan::new_from_span(self.file_path.clone(), &block);
self.commands.spawn((
AttributeOf::new(parent),
FileSpanOf::<NodeExpr>::new(block_file_span),
SpanOf::<NodeExpr>::new(block.span()),
NodeExpr::new_block(block),
));
self.commands.entity(parent).insert(self.expr_idx.next());
}
NodeAttribute::Attribute(attr) => {
let key = match &attr.key {
NodeName::Block(block) => {
self.diagnostics.push(Diagnostic::spanned(
block.span(),
Level::Error,
"Block tag names are not supported as attribute keys",
));
"block-key".to_string()
}
key => key.to_string(),
};
let mut entity = self.commands.spawn((
AttributeOf::new(parent),
AttributeKey::new(key),
FileSpanOf::<AttributeKey>::new(FileSpan::new_from_span(
self.file_path.clone(),
&attr.key,
)),
SpanOf::<AttributeKey>::new(attr.key.span()),
));
let val_expr_file_span = FileSpan::new_from_span(
self.file_path.clone(),
&attr.possible_value,
);
let val_expr_span = attr.possible_value.span();
match attr.possible_value {
KeyedAttributeValue::Value(value) => match value.value {
KVAttributeValue::Expr(val_expr) => {
if let Expr::Lit(ExprLit { lit, attrs: _ }) =
&val_expr
{
insert_lit(&mut entity, lit);
} else {
entity.insert(self.expr_idx.next());
}
entity.insert((
NodeExpr::new(val_expr),
FileSpanOf::<NodeExpr>::new(val_expr_file_span),
SpanOf::<NodeExpr>::new(val_expr_span),
));
}
KVAttributeValue::InvalidBraced(invalid) => {
self.diagnostics.push(Diagnostic::spanned(
invalid.span(),
Level::Error,
"Invalid block",
));
}
},
_ => {
}
};
}
}
}
fn parse_node_name(&mut self, name: &NodeName) -> (String, FileSpan, Span) {
match name {
NodeName::Block(block) => {
self.diagnostics.push(Diagnostic::spanned(
block.span(),
Level::Error,
"Block tag names are not supported",
));
}
_ => {}
}
let key_str = name.to_string();
(
key_str,
FileSpan::new_from_span(self.file_path.clone(), name),
name.span(),
)
}
fn mend_style_raw_text(&self, str: &str) -> String {
{
let replaced = str
.replace(".em", "em")
.replace(". em", "em");
let regex = regex::Regex::new(r"([A-Za-z]) - ([A-Za-z])").unwrap();
regex.replace_all(&replaced, "$1-$2").to_string()
}
}
fn check_self_closing_children(
&mut self,
element: &NodeElement<RstmlCustomNode>,
) {
if self
.constants
.self_closing_elements
.contains(element.open_tag.name.to_string().as_str())
&& !element.children.is_empty()
{
self.diagnostics.push(Diagnostic::spanned(
element.open_tag.name.span(),
Level::Warning,
"Self closing elements cannot have children",
));
}
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
use beet_dom::prelude::*;
use proc_macro2::TokenStream;
use quote::quote;
fn parse(tokens: TokenStream) -> (World, Entity) {
let mut app = App::new();
app.add_plugins(ParseRsxTokensPlugin);
let mut world = std::mem::take(app.world_mut());
let entity = world.spawn(RstmlTokens::new(tokens)).id();
world.run_schedule(ParseRsxTokens);
(world, entity)
}
#[test]
fn works() {
let (mut app, _) = parse(quote! {
<span>
<MyComponent client:load />
<div/>
</span>
});
app.query_once::<&NodeTag>().len().xpect_eq(3);
app.query_once::<&ClientLoadDirective>().len().xpect_eq(1);
}
#[test]
fn attribute_expr() {
let (mut app, _) = parse(quote! {<div foo={7} bar="baz"/>});
app.query_once::<&ExprIdx>().len().xpect_eq(1);
}
#[test]
fn style_tags() {
let (mut app, _) = parse(quote! {
<style scope:global>
body{
font-size: 1.em;
}
</style>
});
#[cfg(feature = "css")]
app.query_once::<&InnerText>()[0]
.xpect_eq(InnerText("body {\n font-size: 1 em;\n}\n".to_string()));
#[cfg(not(feature = "css"))]
app.query_once::<&InnerText>()[0]
.xpect_eq(InnerText("body { font-size : 1 em ; }".to_string()));
}
}