use crate::options::SsrCompilerOptions;
use vize_atelier_core::ast::{
CommentNode, ElementNode, ElementType, ForNode, IfNode, InterpolationNode, RootNode,
RuntimeHelper, TemplateChildNode, TextNode,
};
use vize_carton::{Bump, FxHashSet};
#[derive(Debug, Default)]
pub struct SsrCodegenResult {
pub code: String,
pub preamble: String,
}
pub struct SsrCodegenContext<'a> {
#[allow(dead_code)]
allocator: &'a Bump,
options: &'a SsrCompilerOptions,
code: Vec<u8>,
indent_level: u32,
ssr_helpers: FxHashSet<RuntimeHelper>,
core_helpers: FxHashSet<RuntimeHelper>,
current_template_parts: Vec<TemplatePart>,
#[allow(dead_code)]
has_open_push: bool,
#[allow(dead_code)]
with_slot_scope_id: bool,
}
#[derive(Debug)]
enum TemplatePart {
Static(String),
Dynamic(String),
}
impl<'a> SsrCodegenContext<'a> {
pub fn new(allocator: &'a Bump, options: &'a SsrCompilerOptions) -> Self {
Self {
allocator,
options,
code: Vec::with_capacity(1024),
indent_level: 0,
ssr_helpers: FxHashSet::default(),
core_helpers: FxHashSet::default(),
current_template_parts: Vec::new(),
has_open_push: false,
with_slot_scope_id: false,
}
}
pub fn generate(mut self, root: &RootNode) -> SsrCodegenResult {
let is_fragment = root.children.len() > 1
&& root
.children
.iter()
.any(|c| !matches!(c, TemplateChildNode::Text(_)));
self.push("function ssrRender(_ctx, _push, _parent, _attrs");
if self.options.scope_id.is_some() {
self.push(", _scopeId");
}
self.push(") {\n");
self.indent_level += 1;
if let Some(css_vars) = &self.options.ssr_css_vars {
self.push_indent();
self.push("const _cssVars = { style: ");
self.push(css_vars);
self.push(" }\n");
}
self.process_children(&root.children, is_fragment, false, false);
self.flush_push();
self.indent_level -= 1;
self.push("}\n");
let preamble = self.build_preamble();
SsrCodegenResult {
code: String::from_utf8(self.code).unwrap_or_default(),
preamble,
}
}
fn process_children(
&mut self,
children: &[TemplateChildNode],
as_fragment: bool,
disable_nested_fragments: bool,
disable_comment: bool,
) {
if as_fragment {
self.push_string_part_static("<!--[-->");
}
for child in children {
self.process_child(child, disable_nested_fragments, disable_comment);
}
if as_fragment {
self.push_string_part_static("<!--]-->");
}
}
fn process_child(
&mut self,
child: &TemplateChildNode,
disable_nested_fragments: bool,
disable_comment: bool,
) {
match child {
TemplateChildNode::Element(el) => {
self.process_element(el, disable_nested_fragments);
}
TemplateChildNode::Text(text) => {
self.process_text(text);
}
TemplateChildNode::Comment(comment) => {
if !disable_comment {
self.process_comment(comment);
}
}
TemplateChildNode::Interpolation(interp) => {
self.process_interpolation(interp);
}
TemplateChildNode::If(if_node) => {
self.process_if(if_node, disable_nested_fragments, disable_comment);
}
TemplateChildNode::For(for_node) => {
self.process_for(for_node, disable_nested_fragments);
}
TemplateChildNode::IfBranch(_) => {
}
TemplateChildNode::TextCall(_) | TemplateChildNode::CompoundExpression(_) => {
}
TemplateChildNode::Hoisted(_) => {
}
}
}
fn process_element(&mut self, el: &ElementNode, disable_nested_fragments: bool) {
match el.tag_type {
ElementType::Element => {
self.process_plain_element(el);
}
ElementType::Component => {
self.process_component(el, disable_nested_fragments);
}
ElementType::Slot => {
self.process_slot_outlet(el);
}
ElementType::Template => {
self.process_children(&el.children, false, disable_nested_fragments, false);
}
}
}
fn process_plain_element(&mut self, el: &ElementNode) {
let tag = &el.tag;
self.push_string_part_static("<");
self.push_string_part_static(tag);
self.process_element_attrs(el);
if let Some(scope_id) = &self.options.scope_id {
self.push_string_part_static(" ");
self.push_string_part_static(scope_id);
}
if vize_carton::is_void_tag(tag) {
self.push_string_part_static(">");
return;
}
self.push_string_part_static(">");
self.process_children(&el.children, false, false, false);
self.push_string_part_static("</");
self.push_string_part_static(tag);
self.push_string_part_static(">");
}
fn process_element_attrs(&mut self, el: &ElementNode) {
use vize_atelier_core::ast::PropNode;
for prop in &el.props {
match prop {
PropNode::Attribute(attr) => {
self.push_string_part_static(" ");
self.push_string_part_static(&attr.name);
if let Some(value) = &attr.value {
self.push_string_part_static("=\"");
self.push_string_part_static(&escape_html_attr(&value.content));
self.push_string_part_static("\"");
}
}
PropNode::Directive(dir) => {
self.process_directive_on_element(el, dir);
}
}
}
}
fn process_directive_on_element(
&mut self,
el: &ElementNode,
dir: &vize_atelier_core::ast::DirectiveNode,
) {
match dir.name.as_str() {
"bind" => {
self.process_v_bind_on_element(el, dir);
}
"on" => {
}
"model" => {
self.process_v_model_on_element(el, dir);
}
"show" => {
self.process_v_show_on_element(el, dir);
}
"html" => {
}
"text" => {
}
_ => {
self.process_custom_directive(el, dir);
}
}
}
fn process_v_bind_on_element(
&mut self,
_el: &ElementNode,
dir: &vize_atelier_core::ast::DirectiveNode,
) {
use vize_atelier_core::ast::ExpressionNode;
let arg_name = match &dir.arg {
Some(ExpressionNode::Simple(simple)) if simple.is_static => {
Some(simple.content.clone())
}
_ => None,
};
let exp = match &dir.exp {
Some(ExpressionNode::Simple(simple)) => simple.content.as_str(),
Some(ExpressionNode::Compound(_)) => {
"_ctx.value"
}
None => return,
};
match arg_name.as_deref() {
Some("class") => {
self.use_ssr_helper(RuntimeHelper::SsrRenderClass);
self.push_string_part_dynamic(&format!("_ssrRenderClass({})", exp));
}
Some("style") => {
self.use_ssr_helper(RuntimeHelper::SsrRenderStyle);
self.push_string_part_static(" style=\"");
self.push_string_part_dynamic(&format!("_ssrRenderStyle({})", exp));
self.push_string_part_static("\"");
}
Some(name) => {
self.use_ssr_helper(RuntimeHelper::SsrRenderAttr);
self.push_string_part_dynamic(&format!("_ssrRenderAttr(\"{}\", {})", name, exp));
}
None => {
self.use_ssr_helper(RuntimeHelper::SsrRenderAttrs);
self.push_string_part_dynamic(&format!("_ssrRenderAttrs({})", exp));
}
}
}
fn process_v_model_on_element(
&mut self,
el: &ElementNode,
dir: &vize_atelier_core::ast::DirectiveNode,
) {
use vize_atelier_core::ast::ExpressionNode;
let exp = match &dir.exp {
Some(ExpressionNode::Simple(simple)) => simple.content.as_str(),
_ => return,
};
let tag = el.tag.as_str();
match tag {
"input" => {
let input_type = self.get_element_attr_value(el, "type");
match input_type.as_deref() {
Some("checkbox") => {
self.use_ssr_helper(RuntimeHelper::SsrIncludeBooleanAttr);
self.use_ssr_helper(RuntimeHelper::SsrLooseContain);
self.push_string_part_dynamic(&format!(
"(_ssrIncludeBooleanAttr(Array.isArray({}) ? _ssrLooseContain({}, null) : {})) ? \" checked\" : \"\"",
exp, exp, exp
));
}
Some("radio") => {
self.use_ssr_helper(RuntimeHelper::SsrIncludeBooleanAttr);
self.use_ssr_helper(RuntimeHelper::SsrLooseEqual);
let value = self.get_element_attr_value(el, "value");
let value_exp = value.as_deref().unwrap_or("null");
self.push_string_part_dynamic(&format!(
"(_ssrIncludeBooleanAttr(_ssrLooseEqual({}, {}))) ? \" checked\" : \"\"",
exp, value_exp
));
}
_ => {
self.use_ssr_helper(RuntimeHelper::SsrRenderAttr);
self.push_string_part_dynamic(&format!(
"_ssrRenderAttr(\"value\", {})",
exp
));
}
}
}
"textarea" => {
self.use_ssr_helper(RuntimeHelper::SsrInterpolate);
}
"select" => {
}
_ => {}
}
}
fn process_v_show_on_element(
&mut self,
_el: &ElementNode,
dir: &vize_atelier_core::ast::DirectiveNode,
) {
use vize_atelier_core::ast::ExpressionNode;
let exp = match &dir.exp {
Some(ExpressionNode::Simple(simple)) => simple.content.as_str(),
_ => return,
};
self.push_string_part_dynamic(&format!(
"(({}) ? \"\" : \" style=\\\"display: none;\\\"\")",
exp
));
}
fn process_custom_directive(
&mut self,
_el: &ElementNode,
dir: &vize_atelier_core::ast::DirectiveNode,
) {
self.use_ssr_helper(RuntimeHelper::SsrGetDirectiveProps);
self.push_string_part_dynamic(&format!(
"_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directives, \"{}\"))",
dir.name
));
}
fn get_element_attr_value(&self, el: &ElementNode, name: &str) -> Option<String> {
use vize_atelier_core::ast::PropNode;
for prop in &el.props {
if let PropNode::Attribute(attr) = prop {
if attr.name == name {
return attr.value.as_ref().map(|v| v.content.to_string());
}
}
}
None
}
fn process_component(&mut self, el: &ElementNode, _disable_nested_fragments: bool) {
self.flush_push();
self.use_ssr_helper(RuntimeHelper::SsrRenderComponent);
self.use_core_helper(RuntimeHelper::ResolveComponent);
let tag = &el.tag;
self.push_indent();
self.push("_push(_ssrRenderComponent(_component_");
self.push(tag);
self.push(", _attrs, ");
if el.children.is_empty() {
self.push("null");
} else {
self.push("{\n");
self.indent_level += 1;
self.push_indent();
self.push("default: _withCtx(() => [\n");
self.indent_level += 1;
let old_parts = std::mem::take(&mut self.current_template_parts);
self.process_children(&el.children, false, false, false);
self.flush_push();
self.current_template_parts = old_parts;
self.indent_level -= 1;
self.push_indent();
self.push("]),\n");
self.indent_level -= 1;
self.push_indent();
self.push("_: 1\n");
self.push_indent();
self.push("}");
}
self.push(", _parent))\n");
}
fn process_slot_outlet(&mut self, el: &ElementNode) {
self.flush_push();
self.use_ssr_helper(RuntimeHelper::SsrRenderSlot);
self.push_indent();
self.push("_ssrRenderSlot(_ctx.$slots, ");
let slot_name = self.get_slot_name(el);
self.push("\"");
self.push(&slot_name);
self.push("\", ");
self.push("{}, ");
if el.children.is_empty() {
self.push("null");
} else {
self.push("() => {\n");
self.indent_level += 1;
let old_parts = std::mem::take(&mut self.current_template_parts);
self.process_children(&el.children, false, false, false);
self.flush_push();
self.current_template_parts = old_parts;
self.indent_level -= 1;
self.push_indent();
self.push("}");
}
self.push(", _push, _parent");
if self.options.scope_id.is_some() {
self.push(", _scopeId");
}
self.push(")\n");
}
fn get_slot_name(&self, el: &ElementNode) -> String {
use vize_atelier_core::ast::{ExpressionNode, PropNode};
for prop in &el.props {
if let PropNode::Directive(dir) = prop {
if dir.name == "bind" {
if let Some(ExpressionNode::Simple(arg)) = &dir.arg {
if arg.content == "name" {
if let Some(ExpressionNode::Simple(exp)) = &dir.exp {
return exp.content.to_string();
}
}
}
}
} else if let PropNode::Attribute(attr) = prop {
if attr.name == "name" {
if let Some(value) = &attr.value {
return value.content.to_string();
}
}
}
}
"default".to_string()
}
fn process_text(&mut self, text: &TextNode) {
self.push_string_part_static(&escape_html(&text.content));
}
fn process_comment(&mut self, comment: &CommentNode) {
self.push_string_part_static("<!--");
self.push_string_part_static(&comment.content);
self.push_string_part_static("-->");
}
fn process_interpolation(&mut self, interp: &InterpolationNode) {
use vize_atelier_core::ast::ExpressionNode;
self.use_ssr_helper(RuntimeHelper::SsrInterpolate);
let exp = match &interp.content {
ExpressionNode::Simple(simple) => simple.content.as_str(),
ExpressionNode::Compound(_) => "_ctx.value", };
self.push_string_part_dynamic(&format!("_ssrInterpolate({})", exp));
}
fn process_if(
&mut self,
if_node: &IfNode,
disable_nested_fragments: bool,
disable_comment: bool,
) {
self.flush_push();
for (i, branch) in if_node.branches.iter().enumerate() {
self.push_indent();
if i == 0 {
self.push("if (");
if let Some(condition) = &branch.condition {
self.push_expression(condition);
}
self.push(") {\n");
} else if branch.condition.is_some() {
self.push("} else if (");
if let Some(condition) = &branch.condition {
self.push_expression(condition);
}
self.push(") {\n");
} else {
self.push("} else {\n");
}
self.indent_level += 1;
let needs_fragment = !disable_nested_fragments && branch.children.len() > 1;
self.process_children(
&branch.children,
needs_fragment,
disable_nested_fragments,
disable_comment,
);
self.flush_push();
self.indent_level -= 1;
}
if if_node.branches.iter().all(|b| b.condition.is_some()) {
self.push_indent();
self.push("} else {\n");
self.indent_level += 1;
self.push_string_part_static("<!---->");
self.flush_push();
self.indent_level -= 1;
}
self.push_indent();
self.push("}\n");
}
fn process_for(&mut self, for_node: &ForNode, disable_nested_fragments: bool) {
self.flush_push();
self.use_ssr_helper(RuntimeHelper::SsrRenderList);
if !disable_nested_fragments {
self.push_indent();
self.push("_push(`<!--[-->`)\n");
}
self.push_indent();
self.push("_ssrRenderList(");
self.push_expression(&for_node.source);
self.push(", (");
if let Some(value) = &for_node.value_alias {
self.push_expression(value);
}
if let Some(key) = &for_node.key_alias {
self.push(", ");
self.push_expression(key);
}
if let Some(index) = &for_node.object_index_alias {
self.push(", ");
self.push_expression(index);
}
self.push(") => {\n");
self.indent_level += 1;
let needs_fragment = !disable_nested_fragments && for_node.children.len() > 1;
self.process_children(&for_node.children, needs_fragment, true, false);
self.flush_push();
self.indent_level -= 1;
self.push_indent();
self.push("})\n");
if !disable_nested_fragments {
self.push_indent();
self.push("_push(`<!--]-->`)\n");
}
}
fn push_expression(&mut self, expr: &vize_atelier_core::ast::ExpressionNode) {
use vize_atelier_core::ast::ExpressionNode;
match expr {
ExpressionNode::Simple(simple) => {
self.push(&simple.content);
}
ExpressionNode::Compound(compound) => {
for child in &compound.children {
use vize_atelier_core::ast::CompoundExpressionChild;
match child {
CompoundExpressionChild::Simple(s) => self.push(&s.content),
CompoundExpressionChild::String(s) => self.push(s),
CompoundExpressionChild::Symbol(helper) => {
self.push("_");
self.push(helper.name());
}
_ => {}
}
}
}
}
}
fn push_string_part_static(&mut self, s: &str) {
if let Some(TemplatePart::Static(last)) = self.current_template_parts.last_mut() {
last.push_str(s);
} else {
self.current_template_parts
.push(TemplatePart::Static(s.to_string()));
}
}
fn push_string_part_dynamic(&mut self, expr: &str) {
self.current_template_parts
.push(TemplatePart::Dynamic(expr.to_string()));
}
fn flush_push(&mut self) {
if self.current_template_parts.is_empty() {
return;
}
let parts = std::mem::take(&mut self.current_template_parts);
self.push_indent();
self.push("_push(`");
for part in &parts {
match part {
TemplatePart::Static(s) => {
let escaped = s.replace('`', "\\`").replace("${", "\\${");
self.push(&escaped);
}
TemplatePart::Dynamic(expr) => {
self.push("${");
self.push(expr);
self.push("}");
}
}
}
self.push("`)\n");
}
fn use_ssr_helper(&mut self, helper: RuntimeHelper) {
self.ssr_helpers.insert(helper);
}
fn use_core_helper(&mut self, helper: RuntimeHelper) {
self.core_helpers.insert(helper);
}
fn push(&mut self, s: &str) {
self.code.extend_from_slice(s.as_bytes());
}
fn push_indent(&mut self) {
for _ in 0..self.indent_level {
self.code.extend_from_slice(b" ");
}
}
fn build_preamble(&self) -> String {
let mut preamble = String::new();
if !self.ssr_helpers.is_empty() {
preamble.push_str("import { ");
let helpers: Vec<_> = self
.ssr_helpers
.iter()
.map(|h| format!("{} as _{}", h.name(), h.name()))
.collect();
preamble.push_str(&helpers.join(", "));
preamble.push_str(" } from \"@vue/server-renderer\"\n");
}
if !self.core_helpers.is_empty() {
preamble.push_str("import { ");
let helpers: Vec<_> = self
.core_helpers
.iter()
.map(|h| format!("{} as _{}", h.name(), h.name()))
.collect();
preamble.push_str(&helpers.join(", "));
preamble.push_str(" } from \"vue\"\n");
}
preamble
}
}
fn escape_html(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
_ => result.push(c),
}
}
result
}
fn escape_html_attr(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => result.push_str("&"),
'"' => result.push_str("""),
_ => result.push(c),
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_html() {
assert_eq!(escape_html("<div>"), "<div>");
assert_eq!(escape_html("a & b"), "a & b");
assert_eq!(escape_html("\"hello\""), ""hello"");
}
#[test]
fn test_escape_html_all_special_chars() {
assert_eq!(escape_html("&"), "&");
assert_eq!(escape_html("<"), "<");
assert_eq!(escape_html(">"), ">");
assert_eq!(escape_html("\""), """);
assert_eq!(escape_html("'"), "'");
}
#[test]
fn test_escape_html_no_special() {
assert_eq!(escape_html("hello world"), "hello world");
assert_eq!(escape_html("abc123"), "abc123");
}
#[test]
fn test_escape_html_attr() {
assert_eq!(escape_html_attr("hello\"world"), "hello"world");
assert_eq!(escape_html_attr("a & b"), "a & b");
}
#[test]
fn test_escape_html_attr_preserves_angle_brackets() {
assert_eq!(escape_html_attr("<foo>"), "<foo>");
assert_eq!(escape_html_attr("a > b"), "a > b");
}
#[test]
fn test_escape_html_attr_no_special() {
assert_eq!(escape_html_attr("hello"), "hello");
}
#[test]
fn test_ssr_codegen_result_default() {
let result = SsrCodegenResult::default();
assert!(result.code.is_empty());
assert!(result.preamble.is_empty());
}
}