1use tstring_html::{
2 AttributeLike, CompiledHtmlTemplate, Document, FormatOptions, Node, RenderedFragment,
3 RuntimeContext, ValuePart, format_document_as_thtml_with_options, format_template_syntax,
4 parse_template, runtime_error,
5};
6use tstring_syntax::{BackendError, BackendResult, SourceSpan, TemplateInput};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct CompiledThtmlTemplate {
10 document: Document,
11}
12
13pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
14 prepare_template(template).map(|_| ())
15}
16
17pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
18 format_template_with_options(template, &FormatOptions::default())
19}
20
21pub fn format_template_with_options(
22 template: &TemplateInput,
23 options: &FormatOptions,
24) -> BackendResult<String> {
25 let document = format_template_syntax(template)?;
26 validate_thtml_document(&document)?;
27 Ok(format_document_as_thtml_with_options(&document, options))
28}
29
30pub fn compile_template(template: &TemplateInput) -> BackendResult<CompiledThtmlTemplate> {
31 let document = prepare_template(template)?;
32 Ok(CompiledThtmlTemplate { document })
33}
34
35pub fn render_html(
36 compiled: &CompiledThtmlTemplate,
37 context: &RuntimeContext,
38) -> BackendResult<String> {
39 if contains_components(&compiled.document) {
40 return Err(runtime_error(
41 "thtml.runtime.component_resolution",
42 "Component rendering requires the bindings layer runtime context.",
43 None,
44 ));
45 }
46 let html_compiled = CompiledHtmlTemplate::from_document(compiled.document.clone());
47 tstring_html::render_html(&html_compiled, context)
48}
49
50pub fn render_fragment(
51 compiled: &CompiledThtmlTemplate,
52 context: &RuntimeContext,
53) -> BackendResult<RenderedFragment> {
54 Ok(RenderedFragment {
55 html: render_html(compiled, context)?,
56 })
57}
58
59impl CompiledThtmlTemplate {
60 #[must_use]
61 pub fn document(&self) -> &Document {
62 &self.document
63 }
64
65 #[must_use]
66 pub fn from_document(document: Document) -> Self {
67 Self { document }
68 }
69}
70
71pub fn prepare_template(template: &TemplateInput) -> BackendResult<Document> {
72 let document = parse_template(template)?;
73 validate_thtml_document(&document)?;
74 Ok(document)
75}
76
77fn validate_thtml_document(document: &Document) -> BackendResult<()> {
78 for child in &document.children {
79 validate_thtml_node(child)?;
80 }
81 Ok(())
82}
83
84fn validate_thtml_node(node: &Node) -> BackendResult<()> {
85 match node {
86 Node::Element(element) => {
87 validate_attributes(&element.attributes)?;
88 for child in &element.children {
89 validate_thtml_node(child)?;
90 }
91 Ok(())
92 }
93 Node::RawTextElement(element) => {
94 validate_attributes(&element.attributes)?;
95 for child in &element.children {
96 match child {
97 Node::Interpolation(interpolation) => {
98 return Err(semantic_error(
99 "html.semantic.raw_text_interpolation",
100 format!("Interpolations are not allowed inside <{}>.", element.name),
101 interpolation.span.clone(),
102 ));
103 }
104 Node::Text(_) => {}
105 _ => {
106 return Err(semantic_error(
107 "html.semantic.raw_text_content",
108 format!("Only text is allowed inside <{}>.", element.name),
109 element.span.clone(),
110 ));
111 }
112 }
113 }
114 Ok(())
115 }
116 Node::ComponentTag(component) => {
117 validate_attributes(&component.attributes)?;
118 for child in &component.children {
119 validate_thtml_node(child)?;
120 }
121 Ok(())
122 }
123 Node::Fragment(fragment) => {
124 for child in &fragment.children {
125 validate_thtml_node(child)?;
126 }
127 Ok(())
128 }
129 _ => Ok(()),
130 }
131}
132
133fn validate_attributes(attributes: &[AttributeLike]) -> BackendResult<()> {
134 for attribute in attributes {
135 match attribute {
136 AttributeLike::Attribute(attribute) => {
137 if let Some(value) = &attribute.value {
138 if !value.quoted
139 && value
140 .parts
141 .iter()
142 .any(|part| matches!(part, ValuePart::Interpolation(_)))
143 {
144 return Err(semantic_error(
145 "html.semantic.unquoted_dynamic_attr",
146 format!(
147 "Dynamic attribute value for '{}' must be quoted.",
148 attribute.name
149 ),
150 attribute.span.clone(),
151 ));
152 }
153 }
154 }
155 AttributeLike::SpreadAttribute(_) => {}
156 }
157 }
158 Ok(())
159}
160
161fn semantic_error(
162 code: impl Into<String>,
163 message: impl Into<String>,
164 span: Option<SourceSpan>,
165) -> BackendError {
166 BackendError::semantic_at(code, message, span)
167}
168
169fn contains_components(document: &Document) -> bool {
170 document.children.iter().any(node_contains_component)
171}
172
173fn node_contains_component(node: &Node) -> bool {
174 match node {
175 Node::ComponentTag(_) => true,
176 Node::Element(element) => element.children.iter().any(node_contains_component),
177 Node::RawTextElement(element) => element.children.iter().any(node_contains_component),
178 Node::Fragment(fragment) => fragment.children.iter().any(node_contains_component),
179 _ => false,
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use tstring_syntax::TemplateSegment;
187
188 #[test]
189 fn thtml_accepts_component_tags() {
190 let input = TemplateInput::from_segments(vec![TemplateSegment::StaticText(
191 "<Button disabled />".to_string(),
192 )]);
193 check_template(&input).expect("thtml should allow component tags");
194 }
195
196 #[test]
197 fn thtml_runtime_without_bindings_rejects_components() {
198 let input = TemplateInput::from_segments(vec![TemplateSegment::StaticText(
199 "<Button />".to_string(),
200 )]);
201 let compiled = compile_template(&input).expect("compile thtml");
202 let err = render_html(&compiled, &RuntimeContext::default()).expect_err("must fail");
203 assert_eq!(
204 err.message,
205 "Component rendering requires the bindings layer runtime context."
206 );
207 }
208}