use super::writer::Writer;
use super::{Backend, EmitContext, EmitError};
use crate::markup::*;
use crate::resolve::Resolution;
use crate::roblox;
const MERGE_PROPS: &str = crate::imports::MERGE_HELPER;
const READ: &str = crate::imports::READ_HELPER;
pub struct Vide;
impl Backend for Vide {
fn name(&self) -> &'static str {
"vide"
}
fn emit(&self, node: &Node, context: &EmitContext<'_>) -> Result<String, EmitError> {
let mut writer = Writer::new(context, node.span().start);
emit_node(node, context, &mut writer)?;
Ok(writer.finish())
}
}
enum Entry<'a> {
Pair { offset: usize, text: String },
Node { offset: usize, node: &'a Node },
Expression { offset: usize, expression: &'a str },
Comment { offset: usize, luau: &'a str },
}
impl Entry<'_> {
fn offset(&self) -> usize {
match self {
Entry::Pair { offset, .. }
| Entry::Node { offset, .. }
| Entry::Expression { offset, .. }
| Entry::Comment { offset, .. } => *offset,
}
}
}
fn emit_node(
node: &Node,
context: &EmitContext<'_>,
writer: &mut Writer<'_>,
) -> Result<(), EmitError> {
match node {
Node::Element(element) => emit_element(element, context, writer),
Node::Fragment(fragment) => {
let entries = child_entries(&fragment.children, false);
emit_table(&entries, fragment.span, context, writer)
}
}
}
fn emit_element(
element: &Element,
context: &EmitContext<'_>,
writer: &mut Writer<'_>,
) -> Result<(), EmitError> {
let (intrinsic, resolved) = match context.resolve(&element.name, element.span.start) {
Resolution::Intrinsic(class) => (Some(class), true),
Resolution::Component => (None, true),
Resolution::Unresolved(written) => (Some(written), false),
};
let plan = match resolved {
true => plan_text(element, intrinsic.as_deref(), context)?,
false => TextPlan::default(),
};
match &intrinsic {
Some(class) => {
context.used_create();
writer.push(&format!("{}(\"{class}\")(", context.create()));
}
None => writer.push(&format!("{}(", element.name.as_written())),
}
let mut groups: Vec<Vec<Entry>> = vec![Vec::new()];
let mut spreads: Vec<(usize, &str)> = Vec::new();
let mut order: Vec<Group> = Vec::new();
for attribute in &element.attributes {
match attribute {
Attribute::Spread { expression, span } => {
if !groups.last().expect("a group").is_empty() {
order.push(Group::Table(groups.len() - 1));
groups.push(Vec::new());
}
order.push(Group::Spread(spreads.len()));
spreads.push((span.start, expression));
}
Attribute::Named { name, value, span } => {
if plan.text.is_some() && name == "Text" {
continue;
}
let key = match (&intrinsic, resolved) {
(Some(class), true) => context.resolve_attribute(class, name, span.start),
_ => name.clone(),
};
groups.last_mut().expect("a group").push(Entry::Pair {
offset: span.start,
text: format!("{key} = {}", attribute_value(value)),
});
}
}
}
let last = groups.len() - 1;
if let Some(text) = &plan.text {
groups[last].push(Entry::Pair {
offset: plan.offset,
text: format!("Text = {text}"),
});
}
groups[last].extend(child_entries(&element.children, plan.consumed_expressions));
if !groups[last].is_empty() || order.is_empty() {
order.push(Group::Table(last));
}
let uses_merge = !spreads.is_empty();
if uses_merge {
context.used_merge_props();
writer.push(&format!("{MERGE_PROPS}("));
}
for (index, group) in order.iter().enumerate() {
if index > 0 {
writer.push(",");
match group {
Group::Spread(spread) => writer.break_or_space(spreads[*spread].0),
Group::Table(table) => {
let offset = groups[*table]
.first()
.map(Entry::offset)
.unwrap_or(element.span.start);
writer.break_or_space(offset);
}
}
} else if let Group::Spread(spread) = group {
writer.to(spreads[*spread].0);
}
match group {
Group::Spread(spread) => writer.push(spreads[*spread].1),
Group::Table(table) => emit_table(&groups[*table], element.span, context, writer)?,
}
}
writer.to(element.span.end.saturating_sub(1));
if uses_merge {
writer.push(")");
}
writer.push(")");
Ok(())
}
enum Group {
Table(usize),
Spread(usize),
}
fn emit_table(
entries: &[Entry<'_>],
span: Span,
context: &EmitContext<'_>,
writer: &mut Writer<'_>,
) -> Result<(), EmitError> {
if entries.is_empty() {
let close = span.end.saturating_sub(1);
if writer.will_break(close) {
writer.push("{");
writer.to(close);
writer.push("}");
} else {
writer.push("{}");
}
return Ok(());
}
writer.push("{");
let last_value = entries
.iter()
.rposition(|entry| !matches!(entry, Entry::Comment { .. }));
for (index, entry) in entries.iter().enumerate() {
if let Entry::Comment { offset, luau } = entry {
writer.break_or_space(*offset);
writer.push(luau);
continue;
}
writer.break_or_space(entry.offset());
match entry {
Entry::Comment { .. } => unreachable!("handled above"),
Entry::Pair { text, .. } => writer.push(text),
Entry::Node { node, .. } => emit_node(node, context, writer)?,
Entry::Expression { expression, .. } => writer.push(expression),
}
if Some(index) != last_value {
writer.push(",");
}
}
let close = span.end.saturating_sub(1);
if writer.will_break(close) {
if last_value.is_some() {
writer.push(",");
}
writer.to(close);
} else {
writer.push(" ");
}
writer.push("}");
Ok(())
}
fn attribute_value(value: &AttributeValue) -> String {
match value {
AttributeValue::Expression(expression) => expression.clone(),
AttributeValue::StringLiteral(literal) => literal.clone(),
AttributeValue::Boolean => "true".to_string(),
}
}
fn child_entries<'a>(children: &'a [Child], expressions_are_text: bool) -> Vec<Entry<'a>> {
let mut entries = Vec::new();
for child in children {
match child {
Child::Node(node) => entries.push(Entry::Node {
offset: node.span().start,
node,
}),
Child::Text { .. } => {}
Child::Comment { luau, span } => entries.push(Entry::Comment {
offset: span.start,
luau,
}),
Child::Expression { .. } if expressions_are_text => {}
Child::Expression { expression, span } => entries.push(Entry::Expression {
offset: span.start,
expression,
}),
}
}
entries
}
#[derive(Default)]
struct TextPlan {
text: Option<String>,
consumed_expressions: bool,
offset: usize,
}
fn plan_text(
element: &Element,
intrinsic: Option<&str>,
context: &EmitContext<'_>,
) -> Result<TextPlan, EmitError> {
let has_text_literal = element
.children
.iter()
.any(|child| matches!(child, Child::Text { .. }));
let has_nodes = element
.children
.iter()
.any(|child| matches!(child, Child::Node(_)));
let has_expressions = element
.children
.iter()
.any(|child| matches!(child, Child::Expression { .. }));
if !has_text_literal && !has_expressions {
return Ok(TextPlan::default());
}
let Some(class) = intrinsic else {
if has_text_literal {
return Err(EmitError::new(
format!(
"<{}> is a component, so it cannot take bare text",
element.name.as_written()
),
element.span.start,
element.name.as_written().len() + 1,
)
.with_help("pass the text as a prop instead"));
}
return Ok(TextPlan::default());
};
if !roblox::has_text_property(class) {
if has_text_literal {
return Err(EmitError::new(
format!("<{class}> has no Text property"),
element.span.start,
class.len() + 1,
)
.with_help("wrap the text in a <TextLabel>"));
}
return Ok(TextPlan::default());
}
if has_expressions && has_nodes {
return Err(EmitError::new(
format!(
"<{class}> has both an expression child and element children, so it is unclear \
whether the expression is text or a child"
),
element.span.start,
class.len() + 1,
)
.with_help("write it as Text={...} instead"));
}
let mut parts = Vec::new();
let mut offset = element.span.start;
for (index, child) in element.children.iter().enumerate() {
match child {
Child::Text { text, span } => {
if index == 0 || parts.is_empty() {
offset = span.start;
}
parts.push(TextPart::Literal(text.clone()));
}
Child::Expression { expression, span } => {
if parts.is_empty() {
offset = span.start;
}
parts.push(TextPart::Expression(expression.clone()));
}
Child::Node(_) | Child::Comment { .. } => {}
}
}
let text = encode_text(&parts);
if text.contains(&format!("{READ}(")) {
context.used_read();
}
Ok(TextPlan {
text: Some(text),
consumed_expressions: has_expressions,
offset,
})
}
enum TextPart {
Literal(String),
Expression(String),
}
fn encode_text(parts: &[TextPart]) -> String {
let expressions = parts
.iter()
.filter(|part| matches!(part, TextPart::Expression(_)))
.count();
if expressions == 0 {
return encode_plain(parts);
}
if let [TextPart::Expression(expression)] = parts {
return expression.clone();
}
format!("function() return {} end", encode_interpolated(parts))
}
fn encode_plain(parts: &[TextPart]) -> String {
let mut out = String::from("\"");
for part in parts {
if let TextPart::Literal(text) = part {
for character in text.chars() {
match character {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
_ => out.push(character),
}
}
}
}
out.push('"');
out
}
fn encode_interpolated(parts: &[TextPart]) -> String {
let mut out = String::from("`");
for part in parts {
match part {
TextPart::Literal(text) => {
for character in text.chars() {
match character {
'\\' => out.push_str("\\\\"),
'`' => out.push_str("\\`"),
'{' => out.push_str("\\{"),
'}' => out.push_str("\\}"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
_ => out.push(character),
}
}
}
TextPart::Expression(expression) => {
out.push_str(&format!("{{{READ}({expression})}}"));
}
}
}
out.push('`');
out
}