extern crate nom;
use nom::branch::alt;
use nom::bytes::complete::{take_until, take_until1};
use nom::combinator::fail;
use nom::error::{ErrorKind, ParseError};
use nom::multi::many0;
use nom::sequence::{delimited, preceded};
use nom::{bytes::complete::tag, sequence::tuple, IResult};
use std::str;
use swc_core::common::DUMMY_SP;
use swc_core::ecma::ast::Module;
use super::attributes::parse_attributes;
use super::ecma::{parse_js, parse_js_module};
use super::html_utils::{classify_element_kind, html_name, space0, TagKind};
use fervid_core::{
fervid_atom, AttributeOrBinding, ElementKind, ElementNode, FervidAtom, Interpolation, Node,
SfcCustomBlock, SfcDescriptor, SfcScriptBlock, SfcScriptLang, SfcStyleBlock, SfcTemplateBlock,
StartingTag,
};
pub fn parse_sfc(mut input: &str) -> IResult<&str, SfcDescriptor> {
let mut result = SfcDescriptor::default();
loop {
let len = input.len();
match parse_root_block(input, &mut result) {
Err(nom::Err::Error(_)) => return Ok((input, result)),
Err(e) => return Err(e),
Ok(new_input) => {
if new_input.len() == len {
return Err(nom::Err::Error(ParseError::from_error_kind(
input,
ErrorKind::Many0,
)));
}
input = new_input;
}
}
}
}
fn parse_root_block<'a>(
input: &'a str,
out: &mut SfcDescriptor,
) -> Result<&'a str, nom::Err<nom::error::Error<&'a str>>> {
let input = input.trim_start();
let (input, (starting_tag, is_self_closing)) = parse_element_starting_tag(input)?;
let is_script = starting_tag.tag_name == fervid_atom!("script");
let is_template = !is_script && starting_tag.tag_name == fervid_atom!("template");
let is_style = !is_template && starting_tag.tag_name == fervid_atom!("style");
let read_raw_text = |input: &'a str| {
parse_text_node(input).map_or_else(
move |_| (input, "".into()),
move |v| {
let Node::Text(content, _) = v.1 else {
unreachable!("parse_text_node always returns a Node::TextNode")
};
(v.0, content.into())
},
)
};
if !is_script && !is_template && !is_style {
if is_self_closing {
out.custom_blocks.push(SfcCustomBlock {
starting_tag,
content: "".into(),
span: DUMMY_SP,
});
return Ok(input);
}
let (input, content) = read_raw_text(input);
let (input, _end_tag) = parse_element_end_tag(input)?;
out.custom_blocks.push(SfcCustomBlock {
starting_tag,
content,
span: DUMMY_SP,
});
return Ok(input);
}
let lang = starting_tag.attributes.iter().find_map(|attr| match attr {
AttributeOrBinding::RegularAttribute { name, value, .. } if name == "lang" => {
Some(value.to_owned())
}
_ => None,
});
if is_template {
let lang = lang.unwrap_or(fervid_atom!("html"));
if is_self_closing {
out.template = Some(SfcTemplateBlock {
lang,
roots: Vec::new(),
span: DUMMY_SP, });
return Ok(input);
}
let (input, children) = parse_node_children(input)?;
let (input, _end_tag) = parse_element_end_tag(input)?;
out.template = Some(SfcTemplateBlock {
lang,
roots: children,
span: DUMMY_SP, });
return Ok(input);
}
if is_script {
let lang = if matches!(lang.as_deref(), Some("ts")) {
SfcScriptLang::Typescript
} else {
SfcScriptLang::Es
};
let is_setup = starting_tag.attributes.iter().any(|attr| {
matches!(
attr,
AttributeOrBinding::RegularAttribute { name, .. } if name.eq("setup")
)
});
macro_rules! add_script {
($content: expr) => {
if is_setup {
out.script_setup = Some(SfcScriptBlock {
content: $content,
lang,
is_setup,
span: DUMMY_SP,
});
} else {
out.script_legacy = Some(SfcScriptBlock {
content: $content,
lang,
is_setup,
span: DUMMY_SP,
})
}
};
}
if is_self_closing {
add_script!(Box::new(Module {
span: DUMMY_SP,
body: vec![],
shebang: None,
}));
return Ok(input);
}
let (input, content) = read_raw_text(input);
let (input, _end_tag) = parse_element_end_tag(input)?;
let content = parse_js_module(&content, 0, 0);
match content {
Ok(content_module) => {
add_script!(Box::new(content_module));
return Ok(input);
}
Err(_) => {
return Err(nom::Err::Error(nom::error::Error {
input,
code: ErrorKind::Fail,
}));
}
}
}
let lang = lang.unwrap_or_else(|| FervidAtom::from("css"));
let is_scoped = starting_tag.attributes.iter().any(|attr| {
matches!(
attr,
AttributeOrBinding::RegularAttribute { name, .. } if name.eq("scoped")
)
});
let is_module = starting_tag.attributes.iter().any(|attr| {
matches!(
attr,
AttributeOrBinding::RegularAttribute { name, .. } if name.eq("module")
)
});
if is_self_closing {
return Ok(input);
}
let (input, content) = read_raw_text(input);
let (input, _end_tag) = parse_element_end_tag(input)?;
out.styles.push(SfcStyleBlock {
lang: lang.into(),
content: content.into(),
is_scoped,
is_module,
span: DUMMY_SP
});
Ok(input)
}
fn parse_element_starting_tag(input: &str) -> IResult<&str, (StartingTag, bool)> {
let (input, (_, tag_name, (attributes, directives), _, ending_bracket)) = tuple((
tag("<"),
html_name,
parse_attributes,
space0,
alt((tag(">"), tag("/>"))),
))(input)?;
#[cfg(dbg_print)]
{
println!("Tag name: {:?}", tag_name);
println!("Attributes: {:?}", attributes);
}
Ok((
input,
(
StartingTag {
tag_name: FervidAtom::from(tag_name),
attributes,
directives,
},
ending_bracket == "/>",
),
))
}
fn parse_element_end_tag(input: &str) -> IResult<&str, &str> {
delimited(tag("</"), html_name, preceded(space0, tag(">")))(input)
}
fn parse_interpolation_node(input: &str) -> IResult<&str, Node> {
let (input, raw_expression) = parse_dynamic_expression(input)?;
let parsed = match parse_js(raw_expression, 0, 0) {
Ok(parsed) => parsed,
Err(_) => return fail(input), };
Ok((
input,
Node::Interpolation(Interpolation {
value: parsed,
template_scope: 0,
patch_flag: false,
span: DUMMY_SP
}),
))
}
pub fn parse_element_node(input: &str) -> IResult<&str, Node> {
let (input, (starting_tag, is_self_closing)) = parse_element_starting_tag(input)?;
let element_kind = classify_element_kind(&starting_tag.tag_name);
let early_return = matches!(element_kind, TagKind::Void) || is_self_closing;
if early_return {
return Ok((
input,
Node::Element(ElementNode {
starting_tag,
children: vec![],
template_scope: 0,
kind: ElementKind::Element,
patch_hints: Default::default(),
span: DUMMY_SP, }),
));
}
let (input, children) = parse_node_children(input)?;
let (input, end_tag) = parse_element_end_tag(input)?;
if !starting_tag.tag_name.eq(end_tag) {
println!(
"End tag does not match start tag: <{}> </{}>",
&starting_tag.tag_name, &end_tag
);
}
Ok((
input,
Node::Element(ElementNode {
starting_tag,
children,
template_scope: 0,
kind: ElementKind::Element,
patch_hints: Default::default(),
span: DUMMY_SP, }),
))
}
fn parse_dynamic_expression(input: &str) -> IResult<&str, &str> {
delimited(tag("{{"), take_until1("}}"), tag("}}"))(input)
}
fn parse_text_node(input: &str) -> IResult<&str, Node> {
let mut iter = input.chars().peekable();
let mut bytes_taken = 0;
while let Some(ch) = iter.next() {
if ch == '<' {
break;
};
if let ('{', Some('{')) = (ch, iter.peek()) {
break;
};
bytes_taken += ch.len_utf8();
}
if bytes_taken == 0 {
return fail(input);
}
let (text, input) = input.split_at(bytes_taken);
Ok((input, Node::Text(text.into(), DUMMY_SP)))
}
fn parse_comment_node(input: &str) -> IResult<&str, Node> {
let (input, comment) = delimited(tag("<!--"), take_until("-->"), tag("-->"))(input)?;
Ok((input, Node::Comment(comment.into(), DUMMY_SP)))
}
fn parse_node_children(input: &str) -> IResult<&str, Vec<Node>> {
many0(alt((
parse_interpolation_node,
parse_comment_node,
parse_element_node,
parse_text_node,
)))(input)
}