tstring-html-bindings 0.1.4__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/Cargo.lock +5 -5
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/Cargo.toml +1 -1
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/PKG-INFO +1 -1
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/pyproject.toml +1 -1
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-bindings/Cargo.toml +2 -2
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-bindings/src/lib.rs +77 -1
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-rs/Cargo.toml +1 -1
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-rs/src/lib.rs +131 -42
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-thtml-rs/Cargo.toml +1 -1
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-thtml-rs/src/lib.rs +53 -20
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/README.md +0 -0
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/python/tstring_html_bindings/__init__.py +0 -0
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-format-doc-rs/Cargo.toml +0 -0
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-format-doc-rs/src/lib.rs +0 -0
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-bindings/README.md +0 -0
- {tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-rs/src/formatter.rs +0 -0
|
@@ -290,14 +290,14 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|
|
290
290
|
|
|
291
291
|
[[package]]
|
|
292
292
|
name = "tstring-format-doc"
|
|
293
|
-
version = "0.1.
|
|
293
|
+
version = "0.1.6"
|
|
294
294
|
dependencies = [
|
|
295
295
|
"unicode-width",
|
|
296
296
|
]
|
|
297
297
|
|
|
298
298
|
[[package]]
|
|
299
299
|
name = "tstring-html"
|
|
300
|
-
version = "0.1.
|
|
300
|
+
version = "0.1.6"
|
|
301
301
|
dependencies = [
|
|
302
302
|
"tstring-format-doc",
|
|
303
303
|
"tstring-syntax",
|
|
@@ -305,7 +305,7 @@ dependencies = [
|
|
|
305
305
|
|
|
306
306
|
[[package]]
|
|
307
307
|
name = "tstring-html-backend-e2e-tests"
|
|
308
|
-
version = "0.1.
|
|
308
|
+
version = "0.1.6"
|
|
309
309
|
dependencies = [
|
|
310
310
|
"serde",
|
|
311
311
|
"toml",
|
|
@@ -316,7 +316,7 @@ dependencies = [
|
|
|
316
316
|
|
|
317
317
|
[[package]]
|
|
318
318
|
name = "tstring-html-bindings"
|
|
319
|
-
version = "0.1.
|
|
319
|
+
version = "0.1.6"
|
|
320
320
|
dependencies = [
|
|
321
321
|
"pyo3",
|
|
322
322
|
"tstring-html",
|
|
@@ -335,7 +335,7 @@ dependencies = [
|
|
|
335
335
|
|
|
336
336
|
[[package]]
|
|
337
337
|
name = "tstring-thtml"
|
|
338
|
-
version = "0.1.
|
|
338
|
+
version = "0.1.6"
|
|
339
339
|
dependencies = [
|
|
340
340
|
"tstring-html",
|
|
341
341
|
"tstring-syntax",
|
{tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-bindings/Cargo.toml
RENAMED
|
@@ -19,8 +19,8 @@ extension-module = ["pyo3/extension-module"]
|
|
|
19
19
|
|
|
20
20
|
[dependencies]
|
|
21
21
|
pyo3 = { workspace = true, features = ["abi3-py314"] }
|
|
22
|
-
tstring-html = { version = "0.1.
|
|
23
|
-
tstring-thtml = { version = "0.1.
|
|
22
|
+
tstring-html = { version = "0.1.6", path = "../tstring-html-rs" }
|
|
23
|
+
tstring-thtml = { version = "0.1.6", path = "../tstring-thtml-rs" }
|
|
24
24
|
tstring-syntax.workspace = true
|
|
25
25
|
|
|
26
26
|
[dev-dependencies]
|
{tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-bindings/src/lib.rs
RENAMED
|
@@ -607,6 +607,17 @@ fn render_thtml_node(
|
|
|
607
607
|
for child in &element.children {
|
|
608
608
|
match child {
|
|
609
609
|
Node::Text(text) => out.push_str(&text.value),
|
|
610
|
+
Node::Interpolation(interpolation)
|
|
611
|
+
if element.name.eq_ignore_ascii_case("title") =>
|
|
612
|
+
{
|
|
613
|
+
let Some(value) = context.values.get(interpolation.interpolation_index)
|
|
614
|
+
else {
|
|
615
|
+
return Err(runtime_error_to_py(
|
|
616
|
+
"Missing runtime value for interpolation.",
|
|
617
|
+
));
|
|
618
|
+
};
|
|
619
|
+
render_escaped_text_value(value, out).map_err(backend_error_to_py)?;
|
|
620
|
+
}
|
|
610
621
|
_ => {
|
|
611
622
|
return Err(runtime_error_to_py(
|
|
612
623
|
"Invalid raw-text content in T-HTML render path.",
|
|
@@ -646,6 +657,43 @@ fn render_thtml_node(
|
|
|
646
657
|
Ok(())
|
|
647
658
|
}
|
|
648
659
|
|
|
660
|
+
fn render_escaped_text_value(value: &RuntimeValue, out: &mut String) -> Result<(), BackendError> {
|
|
661
|
+
match value {
|
|
662
|
+
RuntimeValue::Null => {}
|
|
663
|
+
RuntimeValue::Bool(value) => out.push_str(&escape_html_text(&value.to_string())),
|
|
664
|
+
RuntimeValue::Int(value) => out.push_str(&escape_html_text(&value.to_string())),
|
|
665
|
+
RuntimeValue::Float(value) => out.push_str(&escape_html_text(&value.to_string())),
|
|
666
|
+
RuntimeValue::String(value) => out.push_str(&escape_html_text(value)),
|
|
667
|
+
RuntimeValue::RawHtml(value) => out.push_str(&escape_html_text(value)),
|
|
668
|
+
RuntimeValue::Fragment(values) | RuntimeValue::Sequence(values) => {
|
|
669
|
+
for value in values {
|
|
670
|
+
render_escaped_text_value(value, out)?;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
RuntimeValue::Attributes(_) => {
|
|
674
|
+
return Err(tstring_html::runtime_error(
|
|
675
|
+
"html.runtime.child_type",
|
|
676
|
+
"Mapping-like values cannot be rendered as children.",
|
|
677
|
+
None,
|
|
678
|
+
));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
Ok(())
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
fn escape_html_text(value: &str) -> String {
|
|
685
|
+
let mut escaped = String::with_capacity(value.len());
|
|
686
|
+
for ch in value.chars() {
|
|
687
|
+
match ch {
|
|
688
|
+
'&' => escaped.push_str("&"),
|
|
689
|
+
'<' => escaped.push_str("<"),
|
|
690
|
+
'>' => escaped.push_str(">"),
|
|
691
|
+
_ => escaped.push(ch),
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
escaped
|
|
695
|
+
}
|
|
696
|
+
|
|
649
697
|
fn render_attribute_value_for_component(
|
|
650
698
|
py: Python<'_>,
|
|
651
699
|
value: &tstring_html::AttributeValue,
|
|
@@ -883,7 +931,9 @@ fn tstring_html_bindings(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResu
|
|
|
883
931
|
|
|
884
932
|
#[cfg(test)]
|
|
885
933
|
mod tests {
|
|
886
|
-
use super::ParseCache;
|
|
934
|
+
use super::{ParseCache, render_thtml_node};
|
|
935
|
+
use pyo3::Python;
|
|
936
|
+
use tstring_html::{Node, RuntimeContext, RuntimeValue};
|
|
887
937
|
|
|
888
938
|
#[test]
|
|
889
939
|
fn parse_cache_reuses_entries() {
|
|
@@ -974,4 +1024,30 @@ mod tests {
|
|
|
974
1024
|
assert_eq!(*value, 7);
|
|
975
1025
|
assert_eq!(attempts, 2);
|
|
976
1026
|
}
|
|
1027
|
+
|
|
1028
|
+
#[test]
|
|
1029
|
+
fn render_thtml_node_treats_uppercase_title_as_escaped_text() {
|
|
1030
|
+
Python::attach(|py| {
|
|
1031
|
+
let node = Node::RawTextElement(tstring_html::RawTextElementNode {
|
|
1032
|
+
name: "TITLE".to_string(),
|
|
1033
|
+
attributes: Vec::new(),
|
|
1034
|
+
children: vec![Node::Interpolation(tstring_html::InterpolationNode {
|
|
1035
|
+
interpolation_index: 0,
|
|
1036
|
+
expression: "title".to_string(),
|
|
1037
|
+
raw_source: Some("{title}".to_string()),
|
|
1038
|
+
conversion: None,
|
|
1039
|
+
format_spec: String::new(),
|
|
1040
|
+
span: None,
|
|
1041
|
+
})],
|
|
1042
|
+
span: None,
|
|
1043
|
+
});
|
|
1044
|
+
let context = RuntimeContext {
|
|
1045
|
+
values: vec![RuntimeValue::RawHtml("<safe>".to_string())],
|
|
1046
|
+
};
|
|
1047
|
+
let mut out = String::new();
|
|
1048
|
+
render_thtml_node(py, &node, &context, None, None, &mut out)
|
|
1049
|
+
.expect("render uppercase title");
|
|
1050
|
+
assert_eq!(out, "<TITLE><safe></TITLE>");
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
977
1053
|
}
|
|
@@ -9,5 +9,5 @@ rust-version.workspace = true
|
|
|
9
9
|
version.workspace = true
|
|
10
10
|
|
|
11
11
|
[dependencies]
|
|
12
|
-
tstring-format-doc = { version = "0.1.
|
|
12
|
+
tstring-format-doc = { version = "0.1.5", path = "../tstring-format-doc-rs" }
|
|
13
13
|
tstring-syntax.workspace = true
|
|
@@ -829,6 +829,10 @@ pub fn is_raw_text_tag(name: &str) -> bool {
|
|
|
829
829
|
matches!(name, "script" | "style" | "title" | "textarea")
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
fn raw_text_allows_interpolation(name: &str) -> bool {
|
|
833
|
+
name.eq_ignore_ascii_case("title")
|
|
834
|
+
}
|
|
835
|
+
|
|
832
836
|
fn validate_html_document(document: &Document) -> BackendResult<()> {
|
|
833
837
|
for child in &document.children {
|
|
834
838
|
validate_html_node(child)?;
|
|
@@ -913,32 +917,45 @@ fn validate_html_node(node: &Node) -> BackendResult<()> {
|
|
|
913
917
|
}
|
|
914
918
|
Node::RawTextElement(element) => {
|
|
915
919
|
validate_attributes(&element.attributes)?;
|
|
916
|
-
|
|
917
|
-
match child {
|
|
918
|
-
Node::Interpolation(interpolation) => {
|
|
919
|
-
return Err(semantic_error(
|
|
920
|
-
"html.semantic.raw_text_interpolation",
|
|
921
|
-
format!("Interpolations are not allowed inside <{}>.", element.name),
|
|
922
|
-
interpolation.span.clone(),
|
|
923
|
-
));
|
|
924
|
-
}
|
|
925
|
-
Node::Text(_) => {}
|
|
926
|
-
_ => {
|
|
927
|
-
return Err(semantic_error(
|
|
928
|
-
"html.semantic.raw_text_content",
|
|
929
|
-
format!("Only text is allowed inside <{}>.", element.name),
|
|
930
|
-
element.span.clone(),
|
|
931
|
-
));
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
Ok(())
|
|
920
|
+
validate_raw_text_children(element)
|
|
936
921
|
}
|
|
937
922
|
Node::Fragment(fragment) => validate_children(&fragment.children),
|
|
938
923
|
_ => Ok(()),
|
|
939
924
|
}
|
|
940
925
|
}
|
|
941
926
|
|
|
927
|
+
fn validate_raw_text_children(element: &RawTextElementNode) -> BackendResult<()> {
|
|
928
|
+
for child in &element.children {
|
|
929
|
+
match child {
|
|
930
|
+
Node::Text(_) => {}
|
|
931
|
+
Node::Interpolation(_) if raw_text_allows_interpolation(&element.name) => {}
|
|
932
|
+
Node::Interpolation(interpolation) => {
|
|
933
|
+
return Err(semantic_error(
|
|
934
|
+
"html.semantic.raw_text_interpolation",
|
|
935
|
+
format!("Interpolations are not allowed inside <{}>.", element.name),
|
|
936
|
+
interpolation.span.clone(),
|
|
937
|
+
));
|
|
938
|
+
}
|
|
939
|
+
_ => {
|
|
940
|
+
let message = if raw_text_allows_interpolation(&element.name) {
|
|
941
|
+
format!(
|
|
942
|
+
"Only text and interpolations are allowed inside <{}>.",
|
|
943
|
+
element.name
|
|
944
|
+
)
|
|
945
|
+
} else {
|
|
946
|
+
format!("Only text is allowed inside <{}>.", element.name)
|
|
947
|
+
};
|
|
948
|
+
return Err(semantic_error(
|
|
949
|
+
"html.semantic.raw_text_content",
|
|
950
|
+
message,
|
|
951
|
+
element.span.clone(),
|
|
952
|
+
));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
Ok(())
|
|
957
|
+
}
|
|
958
|
+
|
|
942
959
|
fn validate_children(children: &[Node]) -> BackendResult<()> {
|
|
943
960
|
for child in children {
|
|
944
961
|
validate_html_node(child)?;
|
|
@@ -1030,28 +1047,7 @@ fn render_node(node: &Node, context: &RuntimeContext, out: &mut String) -> Backe
|
|
|
1030
1047
|
context,
|
|
1031
1048
|
out,
|
|
1032
1049
|
)?,
|
|
1033
|
-
Node::RawTextElement(element) =>
|
|
1034
|
-
out.push('<');
|
|
1035
|
-
out.push_str(&element.name);
|
|
1036
|
-
let normalized = normalize_attributes(&element.attributes, context)?;
|
|
1037
|
-
write_attributes(&normalized, out);
|
|
1038
|
-
out.push('>');
|
|
1039
|
-
for child in &element.children {
|
|
1040
|
-
match child {
|
|
1041
|
-
Node::Text(text) => out.push_str(&text.value),
|
|
1042
|
-
_ => {
|
|
1043
|
-
return Err(semantic_error(
|
|
1044
|
-
"html.semantic.raw_text_render",
|
|
1045
|
-
format!("Only text can be rendered inside <{}>.", element.name),
|
|
1046
|
-
element.span.clone(),
|
|
1047
|
-
));
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
out.push_str("</");
|
|
1052
|
-
out.push_str(&element.name);
|
|
1053
|
-
out.push('>');
|
|
1054
|
-
}
|
|
1050
|
+
Node::RawTextElement(element) => render_raw_text_element(element, context, out)?,
|
|
1055
1051
|
Node::ComponentTag(component) => {
|
|
1056
1052
|
return Err(semantic_error(
|
|
1057
1053
|
"html.semantic.component_render",
|
|
@@ -1066,6 +1062,45 @@ fn render_node(node: &Node, context: &RuntimeContext, out: &mut String) -> Backe
|
|
|
1066
1062
|
Ok(())
|
|
1067
1063
|
}
|
|
1068
1064
|
|
|
1065
|
+
fn render_raw_text_element(
|
|
1066
|
+
element: &RawTextElementNode,
|
|
1067
|
+
context: &RuntimeContext,
|
|
1068
|
+
out: &mut String,
|
|
1069
|
+
) -> BackendResult<()> {
|
|
1070
|
+
out.push('<');
|
|
1071
|
+
out.push_str(&element.name);
|
|
1072
|
+
let normalized = normalize_attributes(&element.attributes, context)?;
|
|
1073
|
+
write_attributes(&normalized, out);
|
|
1074
|
+
out.push('>');
|
|
1075
|
+
for child in &element.children {
|
|
1076
|
+
match child {
|
|
1077
|
+
Node::Text(text) => out.push_str(&text.value),
|
|
1078
|
+
Node::Interpolation(interpolation) if raw_text_allows_interpolation(&element.name) => {
|
|
1079
|
+
render_escaped_text_value(value_for_interpolation(context, interpolation)?, out)?;
|
|
1080
|
+
}
|
|
1081
|
+
_ => {
|
|
1082
|
+
let message = if raw_text_allows_interpolation(&element.name) {
|
|
1083
|
+
format!(
|
|
1084
|
+
"Only text and interpolations can be rendered inside <{}>.",
|
|
1085
|
+
element.name
|
|
1086
|
+
)
|
|
1087
|
+
} else {
|
|
1088
|
+
format!("Only text can be rendered inside <{}>.", element.name)
|
|
1089
|
+
};
|
|
1090
|
+
return Err(semantic_error(
|
|
1091
|
+
"html.semantic.raw_text_render",
|
|
1092
|
+
message,
|
|
1093
|
+
element.span.clone(),
|
|
1094
|
+
));
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
out.push_str("</");
|
|
1099
|
+
out.push_str(&element.name);
|
|
1100
|
+
out.push('>');
|
|
1101
|
+
Ok(())
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1069
1104
|
fn render_html_element(
|
|
1070
1105
|
name: &str,
|
|
1071
1106
|
attributes: &[AttributeLike],
|
|
@@ -1267,6 +1302,30 @@ pub fn render_child_value(value: &RuntimeValue, out: &mut String) -> BackendResu
|
|
|
1267
1302
|
Ok(())
|
|
1268
1303
|
}
|
|
1269
1304
|
|
|
1305
|
+
fn render_escaped_text_value(value: &RuntimeValue, out: &mut String) -> BackendResult<()> {
|
|
1306
|
+
match value {
|
|
1307
|
+
RuntimeValue::Null => {}
|
|
1308
|
+
RuntimeValue::Bool(value) => out.push_str(&escape_html_text(&value.to_string())),
|
|
1309
|
+
RuntimeValue::Int(value) => out.push_str(&escape_html_text(&value.to_string())),
|
|
1310
|
+
RuntimeValue::Float(value) => out.push_str(&escape_html_text(&value.to_string())),
|
|
1311
|
+
RuntimeValue::String(value) => out.push_str(&escape_html_text(value)),
|
|
1312
|
+
RuntimeValue::RawHtml(value) => out.push_str(&escape_html_text(value)),
|
|
1313
|
+
RuntimeValue::Fragment(values) | RuntimeValue::Sequence(values) => {
|
|
1314
|
+
for value in values {
|
|
1315
|
+
render_escaped_text_value(value, out)?;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
RuntimeValue::Attributes(_) => {
|
|
1319
|
+
return Err(runtime_error(
|
|
1320
|
+
"html.runtime.child_type",
|
|
1321
|
+
"Mapping-like values cannot be rendered as children.",
|
|
1322
|
+
None,
|
|
1323
|
+
));
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
Ok(())
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1270
1329
|
fn render_attribute_value_string(
|
|
1271
1330
|
value: &AttributeValue,
|
|
1272
1331
|
context: &RuntimeContext,
|
|
@@ -1664,4 +1723,34 @@ mod tests {
|
|
|
1664
1723
|
.expect("normalize class");
|
|
1665
1724
|
assert_eq!(values, vec!["foo", "bar", "baz"]);
|
|
1666
1725
|
}
|
|
1726
|
+
|
|
1727
|
+
#[test]
|
|
1728
|
+
fn title_interpolation_is_allowed_and_escaped_on_render() {
|
|
1729
|
+
let input = TemplateInput::from_segments(vec![
|
|
1730
|
+
TemplateSegment::StaticText("<title>".to_string()),
|
|
1731
|
+
interpolation(0, "title", Some("{title}")),
|
|
1732
|
+
TemplateSegment::StaticText("</title>".to_string()),
|
|
1733
|
+
]);
|
|
1734
|
+
let compiled = compile_template(&input).expect("compile title template");
|
|
1735
|
+
let rendered = render_html(
|
|
1736
|
+
&compiled,
|
|
1737
|
+
&RuntimeContext {
|
|
1738
|
+
values: vec![RuntimeValue::RawHtml("<safe>".to_string())],
|
|
1739
|
+
},
|
|
1740
|
+
)
|
|
1741
|
+
.expect("render title");
|
|
1742
|
+
assert_eq!(rendered, "<title><safe></title>");
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
#[test]
|
|
1746
|
+
fn script_interpolation_is_still_rejected() {
|
|
1747
|
+
let input = TemplateInput::from_segments(vec![
|
|
1748
|
+
TemplateSegment::StaticText("<script>".to_string()),
|
|
1749
|
+
interpolation(0, "script", Some("{script}")),
|
|
1750
|
+
TemplateSegment::StaticText("</script>".to_string()),
|
|
1751
|
+
]);
|
|
1752
|
+
let err = check_template(&input).expect_err("script must still fail");
|
|
1753
|
+
assert_eq!(err.kind, ErrorKind::Semantic);
|
|
1754
|
+
assert!(err.message.contains("<script>"));
|
|
1755
|
+
}
|
|
1667
1756
|
}
|
|
@@ -92,26 +92,7 @@ fn validate_thtml_node(node: &Node) -> BackendResult<()> {
|
|
|
92
92
|
}
|
|
93
93
|
Node::RawTextElement(element) => {
|
|
94
94
|
validate_attributes(&element.attributes)?;
|
|
95
|
-
|
|
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(())
|
|
95
|
+
validate_raw_text_children(element)
|
|
115
96
|
}
|
|
116
97
|
Node::ComponentTag(component) => {
|
|
117
98
|
validate_attributes(&component.attributes)?;
|
|
@@ -130,6 +111,38 @@ fn validate_thtml_node(node: &Node) -> BackendResult<()> {
|
|
|
130
111
|
}
|
|
131
112
|
}
|
|
132
113
|
|
|
114
|
+
fn validate_raw_text_children(element: &tstring_html::RawTextElementNode) -> BackendResult<()> {
|
|
115
|
+
for child in &element.children {
|
|
116
|
+
match child {
|
|
117
|
+
Node::Text(_) => {}
|
|
118
|
+
Node::Interpolation(_) if element.name.eq_ignore_ascii_case("title") => {}
|
|
119
|
+
Node::Interpolation(interpolation) => {
|
|
120
|
+
return Err(semantic_error(
|
|
121
|
+
"html.semantic.raw_text_interpolation",
|
|
122
|
+
format!("Interpolations are not allowed inside <{}>.", element.name),
|
|
123
|
+
interpolation.span.clone(),
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
_ => {
|
|
127
|
+
let message = if element.name.eq_ignore_ascii_case("title") {
|
|
128
|
+
format!(
|
|
129
|
+
"Only text and interpolations are allowed inside <{}>.",
|
|
130
|
+
element.name
|
|
131
|
+
)
|
|
132
|
+
} else {
|
|
133
|
+
format!("Only text is allowed inside <{}>.", element.name)
|
|
134
|
+
};
|
|
135
|
+
return Err(semantic_error(
|
|
136
|
+
"html.semantic.raw_text_content",
|
|
137
|
+
message,
|
|
138
|
+
element.span.clone(),
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
Ok(())
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
fn validate_attributes(attributes: &[AttributeLike]) -> BackendResult<()> {
|
|
134
147
|
for attribute in attributes {
|
|
135
148
|
match attribute {
|
|
@@ -205,4 +218,24 @@ mod tests {
|
|
|
205
218
|
"Component rendering requires the bindings layer runtime context."
|
|
206
219
|
);
|
|
207
220
|
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn thtml_accepts_title_interpolation() {
|
|
224
|
+
let input = TemplateInput::from_segments(vec![
|
|
225
|
+
TemplateSegment::StaticText("<title>".to_string()),
|
|
226
|
+
TemplateSegment::Interpolation(tstring_syntax::TemplateInterpolation {
|
|
227
|
+
expression: "title".to_string(),
|
|
228
|
+
conversion: None,
|
|
229
|
+
format_spec: String::new(),
|
|
230
|
+
interpolation_index: 0,
|
|
231
|
+
raw_source: Some("{title}".to_string()),
|
|
232
|
+
}),
|
|
233
|
+
TemplateSegment::StaticText("</title>".to_string()),
|
|
234
|
+
]);
|
|
235
|
+
check_template(&input).expect("title interpolation should be allowed");
|
|
236
|
+
assert_eq!(
|
|
237
|
+
format_template(&input).expect("format title"),
|
|
238
|
+
"<title>{title}</title>"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
208
241
|
}
|
|
File without changes
|
{tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/python/tstring_html_bindings/__init__.py
RENAMED
|
File without changes
|
{tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-format-doc-rs/Cargo.toml
RENAMED
|
File without changes
|
{tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-format-doc-rs/src/lib.rs
RENAMED
|
File without changes
|
|
File without changes
|
{tstring_html_bindings-0.1.4 → tstring_html_bindings-0.1.6}/tstring-html-rs/src/formatter.rs
RENAMED
|
File without changes
|