1use tstring_html::{
2 format_template_syntax, parse_template, runtime_error, AttributeLike, CompiledHtmlTemplate,
3 Document, Node, RuntimeContext, ValuePart, RenderedFragment,
4};
5use tstring_syntax::{BackendError, BackendResult, SourceSpan, TemplateInput};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct CompiledThtmlTemplate {
9 document: Document,
10}
11
12pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
13 prepare_template(template).map(|_| ())
14}
15
16pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
17 let document = format_template_syntax(template)?;
18 validate_thtml_document(&document)?;
19 Ok(format_document(&document))
20}
21
22pub fn compile_template(template: &TemplateInput) -> BackendResult<CompiledThtmlTemplate> {
23 let document = prepare_template(template)?;
24 Ok(CompiledThtmlTemplate { document })
25}
26
27pub fn render_html(
28 compiled: &CompiledThtmlTemplate,
29 context: &RuntimeContext,
30) -> BackendResult<String> {
31 if contains_components(&compiled.document) {
32 return Err(runtime_error(
33 "thtml.runtime.component_resolution",
34 "Component rendering requires the bindings layer runtime context.",
35 None,
36 ));
37 }
38 let html_compiled = CompiledHtmlTemplate::from_document(compiled.document.clone());
39 tstring_html::render_html(&html_compiled, context)
40}
41
42pub fn render_fragment(
43 compiled: &CompiledThtmlTemplate,
44 context: &RuntimeContext,
45) -> BackendResult<RenderedFragment> {
46 Ok(RenderedFragment {
47 html: render_html(compiled, context)?,
48 })
49}
50
51impl CompiledThtmlTemplate {
52 #[must_use]
53 pub fn document(&self) -> &Document {
54 &self.document
55 }
56
57 #[must_use]
58 pub fn from_document(document: Document) -> Self {
59 Self { document }
60 }
61}
62
63pub fn prepare_template(template: &TemplateInput) -> BackendResult<Document> {
64 let document = parse_template(template)?;
65 validate_thtml_document(&document)?;
66 Ok(document)
67}
68
69fn validate_thtml_document(document: &Document) -> BackendResult<()> {
70 for child in &document.children {
71 validate_thtml_node(child)?;
72 }
73 Ok(())
74}
75
76fn validate_thtml_node(node: &Node) -> BackendResult<()> {
77 match node {
78 Node::Element(element) => {
79 validate_attributes(&element.attributes)?;
80 for child in &element.children {
81 validate_thtml_node(child)?;
82 }
83 Ok(())
84 }
85 Node::RawTextElement(element) => {
86 validate_attributes(&element.attributes)?;
87 for child in &element.children {
88 match child {
89 Node::Interpolation(interpolation) => {
90 return Err(semantic_error(
91 "html.semantic.raw_text_interpolation",
92 format!("Interpolations are not allowed inside <{}>.", element.name),
93 interpolation.span.clone(),
94 ));
95 }
96 Node::Text(_) => {}
97 _ => {
98 return Err(semantic_error(
99 "html.semantic.raw_text_content",
100 format!("Only text is allowed inside <{}>.", element.name),
101 element.span.clone(),
102 ));
103 }
104 }
105 }
106 Ok(())
107 }
108 Node::ComponentTag(component) => {
109 validate_attributes(&component.attributes)?;
110 for child in &component.children {
111 validate_thtml_node(child)?;
112 }
113 Ok(())
114 }
115 Node::Fragment(fragment) => {
116 for child in &fragment.children {
117 validate_thtml_node(child)?;
118 }
119 Ok(())
120 }
121 _ => Ok(()),
122 }
123}
124
125fn validate_attributes(attributes: &[AttributeLike]) -> BackendResult<()> {
126 for attribute in attributes {
127 match attribute {
128 AttributeLike::Attribute(attribute) => {
129 if let Some(value) = &attribute.value {
130 if !value.quoted
131 && value
132 .parts
133 .iter()
134 .any(|part| matches!(part, ValuePart::Interpolation(_)))
135 {
136 return Err(semantic_error(
137 "html.semantic.unquoted_dynamic_attr",
138 format!(
139 "Dynamic attribute value for '{}' must be quoted.",
140 attribute.name
141 ),
142 attribute.span.clone(),
143 ));
144 }
145 }
146 }
147 AttributeLike::SpreadAttribute(_) => {}
148 }
149 }
150 Ok(())
151}
152
153fn semantic_error(
154 code: impl Into<String>,
155 message: impl Into<String>,
156 span: Option<SourceSpan>,
157) -> BackendError {
158 BackendError::semantic_at(code, message, span)
159}
160
161fn contains_components(document: &Document) -> bool {
162 document.children.iter().any(node_contains_component)
163}
164
165fn node_contains_component(node: &Node) -> bool {
166 match node {
167 Node::ComponentTag(_) => true,
168 Node::Element(element) => element.children.iter().any(node_contains_component),
169 Node::RawTextElement(element) => element.children.iter().any(node_contains_component),
170 Node::Fragment(fragment) => fragment.children.iter().any(node_contains_component),
171 _ => false,
172 }
173}
174
175fn format_document(document: &Document) -> String {
176 let mut out = String::new();
177 for node in &document.children {
178 format_node(node, &mut out);
179 }
180 out
181}
182
183fn format_node(node: &Node, out: &mut String) {
184 match node {
185 Node::Fragment(fragment) => {
186 for child in &fragment.children {
187 format_node(child, out);
188 }
189 }
190 Node::Element(element) => {
191 format_tag_like(&element.name, &element.attributes, &element.children, element.self_closing, out);
192 }
193 Node::ComponentTag(component) => {
194 format_tag_like(
195 &component.name,
196 &component.attributes,
197 &component.children,
198 component.self_closing,
199 out,
200 );
201 }
202 Node::RawTextElement(element) => {
203 format_tag_like(&element.name, &element.attributes, &element.children, false, out);
204 }
205 Node::Text(text) => out.push_str(&text.value),
206 Node::Interpolation(interpolation) => {
207 if let Some(raw_source) = &interpolation.raw_source {
208 out.push_str(raw_source);
209 }
210 }
211 Node::Comment(comment) => {
212 out.push_str("<!--");
213 out.push_str(&comment.value);
214 out.push_str("-->");
215 }
216 Node::Doctype(doctype) => {
217 out.push_str("<!DOCTYPE ");
218 out.push_str(&doctype.value);
219 out.push('>');
220 }
221 }
222}
223
224fn format_tag_like(
225 name: &str,
226 attributes: &[tstring_html::AttributeLike],
227 children: &[Node],
228 self_closing: bool,
229 out: &mut String,
230) {
231 out.push('<');
232 out.push_str(name);
233 for attribute in attributes {
234 out.push(' ');
235 match attribute {
236 tstring_html::AttributeLike::Attribute(attribute) => {
237 out.push_str(&attribute.name);
238 if let Some(value) = &attribute.value {
239 out.push('=');
240 if value.quoted {
241 out.push('"');
242 }
243 for part in &value.parts {
244 match part {
245 tstring_html::ValuePart::Text(text) => out.push_str(text),
246 tstring_html::ValuePart::Interpolation(interpolation) => {
247 if let Some(raw_source) = &interpolation.raw_source {
248 out.push_str(raw_source);
249 }
250 }
251 }
252 }
253 if value.quoted {
254 out.push('"');
255 }
256 }
257 }
258 tstring_html::AttributeLike::SpreadAttribute(attribute) => {
259 if let Some(raw_source) = &attribute.interpolation.raw_source {
260 out.push_str(raw_source);
261 }
262 }
263 }
264 }
265 if self_closing {
266 out.push_str(" />");
267 return;
268 }
269 out.push('>');
270 for child in children {
271 format_node(child, out);
272 }
273 out.push_str("</");
274 out.push_str(name);
275 out.push('>');
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use tstring_syntax::TemplateSegment;
282
283 #[test]
284 fn thtml_accepts_component_tags() {
285 let input = TemplateInput::from_segments(vec![TemplateSegment::StaticText(
286 "<Button disabled />".to_string(),
287 )]);
288 check_template(&input).expect("thtml should allow component tags");
289 }
290
291 #[test]
292 fn thtml_runtime_without_bindings_rejects_components() {
293 let input =
294 TemplateInput::from_segments(vec![TemplateSegment::StaticText("<Button />".to_string())]);
295 let compiled = compile_template(&input).expect("compile thtml");
296 let err = render_html(&compiled, &RuntimeContext::default()).expect_err("must fail");
297 assert_eq!(err.message, "Component rendering requires the bindings layer runtime context.");
298 }
299}