tstring-html-bindings 0.1.5__tar.gz → 0.1.7__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.
Files changed (16) hide show
  1. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/Cargo.lock +5 -5
  2. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/Cargo.toml +1 -1
  3. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/PKG-INFO +1 -1
  4. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/pyproject.toml +1 -1
  5. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-html-bindings/Cargo.toml +2 -2
  6. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-html-bindings/src/lib.rs +102 -10
  7. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-html-rs/src/lib.rs +1 -1
  8. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-thtml-rs/Cargo.toml +1 -1
  9. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-thtml-rs/src/lib.rs +2 -2
  10. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/README.md +0 -0
  11. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/python/tstring_html_bindings/__init__.py +0 -0
  12. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-format-doc-rs/Cargo.toml +0 -0
  13. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-format-doc-rs/src/lib.rs +0 -0
  14. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-html-bindings/README.md +0 -0
  15. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/tstring-html-rs/Cargo.toml +0 -0
  16. {tstring_html_bindings-0.1.5 → tstring_html_bindings-0.1.7}/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.5"
293
+ version = "0.1.7"
294
294
  dependencies = [
295
295
  "unicode-width",
296
296
  ]
297
297
 
298
298
  [[package]]
299
299
  name = "tstring-html"
300
- version = "0.1.5"
300
+ version = "0.1.7"
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.5"
308
+ version = "0.1.7"
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.5"
319
+ version = "0.1.7"
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.5"
338
+ version = "0.1.7"
339
339
  dependencies = [
340
340
  "tstring-html",
341
341
  "tstring-syntax",
@@ -9,7 +9,7 @@ homepage = "https://github.com/koxudaxi/tstring-html"
9
9
  license = "MIT"
10
10
  repository = "https://github.com/koxudaxi/tstring-html"
11
11
  rust-version = "1.94.0"
12
- version = "0.1.5"
12
+ version = "0.1.7"
13
13
 
14
14
  [workspace.dependencies]
15
15
  pyo3 = "0.27.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tstring-html-bindings
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tstring-html-bindings"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "Native Python bindings for tstring-html"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -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.5", path = "../tstring-html-rs" }
23
- tstring-thtml = { version = "0.1.5", path = "../tstring-thtml-rs" }
22
+ tstring-html = { version = "0.1.7", path = "../tstring-html-rs" }
23
+ tstring-thtml = { version = "0.1.7", path = "../tstring-thtml-rs" }
24
24
  tstring-syntax.workspace = true
25
25
 
26
26
  [dev-dependencies]
@@ -18,7 +18,8 @@ create_exception!(tstring_html_bindings, TemplateSemanticError, TemplateError);
18
18
  create_exception!(tstring_html_bindings, TemplateRuntimeError, TemplateError);
19
19
 
20
20
  const PARSE_CACHE_CAPACITY: usize = 256;
21
- const CONTRACT_VERSION: u32 = 1;
21
+ const CONTRACT_VERSION: u32 = 2;
22
+ const REGISTRY_TYPE_ERROR: &str = "registry= must be mapping-like.";
22
23
  const CONTRACT_SYMBOLS: &[&str] = &[
23
24
  "TemplateError",
24
25
  "TemplateParseError",
@@ -197,16 +198,30 @@ impl PyCompiledHtmlTemplate {
197
198
 
198
199
  #[pymethods]
199
200
  impl PyCompiledThtmlTemplate {
200
- #[pyo3(signature = (values, globals = None, locals = None))]
201
+ #[pyo3(signature = (values, globals = None, locals = None, registry = None))]
201
202
  fn render(
202
203
  &self,
203
204
  py: Python<'_>,
204
205
  values: Vec<Py<PyAny>>,
205
206
  globals: Option<&Bound<'_, PyDict>>,
206
207
  locals: Option<&Bound<'_, PyDict>>,
208
+ registry: Option<&Bound<'_, PyAny>>,
207
209
  ) -> PyResult<String> {
210
+ let (globals, locals) = normalize_scope_inputs(
211
+ py,
212
+ globals,
213
+ locals,
214
+ registry,
215
+ "CompiledThtmlTemplate.render",
216
+ )?;
208
217
  let context = runtime_context_from_values(py, &values)?;
209
- render_thtml_document(py, self.compiled.document(), &context, globals, locals)
218
+ render_thtml_document(
219
+ py,
220
+ self.compiled.document(),
221
+ &context,
222
+ globals.as_ref(),
223
+ locals.as_ref(),
224
+ )
210
225
  }
211
226
 
212
227
  fn __repr__(&self) -> String {
@@ -415,6 +430,42 @@ fn runtime_context_from_values(py: Python<'_>, values: &[Py<PyAny>]) -> PyResult
415
430
  })
416
431
  }
417
432
 
433
+ fn registry_to_scope_dict<'py>(
434
+ py: Python<'py>,
435
+ registry: &Bound<'py, PyAny>,
436
+ ) -> PyResult<Bound<'py, PyDict>> {
437
+ let builtins = py.import("builtins")?;
438
+ let dict = builtins
439
+ .getattr("dict")?
440
+ .call1((registry,))
441
+ .map_err(|_| PyTypeError::new_err(REGISTRY_TYPE_ERROR))?;
442
+ dict.cast_into::<PyDict>()
443
+ .map_err(|_| PyTypeError::new_err(REGISTRY_TYPE_ERROR))
444
+ }
445
+
446
+ fn normalize_scope_inputs<'py>(
447
+ py: Python<'py>,
448
+ globals: Option<&Bound<'py, PyDict>>,
449
+ locals: Option<&Bound<'py, PyDict>>,
450
+ registry: Option<&Bound<'py, PyAny>>,
451
+ api_name: &str,
452
+ ) -> PyResult<(Option<Bound<'py, PyDict>>, Option<Bound<'py, PyDict>>)> {
453
+ if registry.is_some() && (globals.is_some() || locals.is_some()) {
454
+ return Err(PyTypeError::new_err(format!(
455
+ "{api_name} does not allow combining registry= with globals= or locals=."
456
+ )));
457
+ }
458
+
459
+ if let Some(registry) = registry {
460
+ return Ok((
461
+ Some(registry_to_scope_dict(py, registry)?),
462
+ Some(PyDict::new(py)),
463
+ ));
464
+ }
465
+
466
+ Ok((globals.cloned(), locals.cloned()))
467
+ }
468
+
418
469
  fn render_thtml_document(
419
470
  py: Python<'_>,
420
471
  document: &Document,
@@ -607,7 +658,9 @@ fn render_thtml_node(
607
658
  for child in &element.children {
608
659
  match child {
609
660
  Node::Text(text) => out.push_str(&text.value),
610
- Node::Interpolation(interpolation) if element.name == "title" => {
661
+ Node::Interpolation(interpolation)
662
+ if element.name.eq_ignore_ascii_case("title") =>
663
+ {
611
664
  let Some(value) = context.values.get(interpolation.interpolation_index)
612
665
  else {
613
666
  return Err(runtime_error_to_py(
@@ -786,7 +839,7 @@ fn resolve_component<'py>(
786
839
  )));
787
840
  }
788
841
  Err(runtime_error_to_py(format!(
789
- "Unknown component '{name}'. Pass globals= or locals= explicitly."
842
+ "Unknown component '{name}'. Pass registry=, globals=, or locals= explicitly."
790
843
  )))
791
844
  }
792
845
 
@@ -797,7 +850,7 @@ fn default_scope_dict<'py>(py: Python<'py>, globals: bool) -> PyResult<Bound<'py
797
850
  .call1((1,)) // immediate caller only
798
851
  .map_err(|_| {
799
852
  runtime_error_to_py(
800
- "Caller-frame inspection failed. Pass globals= or locals= explicitly.",
853
+ "Caller-frame inspection failed. Pass registry=, globals=, or locals= explicitly.",
801
854
  )
802
855
  })?;
803
856
  let dict = if globals {
@@ -806,7 +859,9 @@ fn default_scope_dict<'py>(py: Python<'py>, globals: bool) -> PyResult<Bound<'py
806
859
  frame.getattr("f_locals")?
807
860
  };
808
861
  dict.cast_into::<PyDict>().map_err(|_| {
809
- runtime_error_to_py("Caller-frame inspection failed. Pass globals= or locals= explicitly.")
862
+ runtime_error_to_py(
863
+ "Caller-frame inspection failed. Pass registry=, globals=, or locals= explicitly.",
864
+ )
810
865
  })
811
866
  }
812
867
 
@@ -884,17 +939,26 @@ fn compile_thtml_template(
884
939
  })
885
940
  }
886
941
 
887
- #[pyfunction(signature = (template, globals = None, locals = None))]
942
+ #[pyfunction(signature = (template, globals = None, locals = None, registry = None))]
888
943
  fn render_thtml_template(
889
944
  py: Python<'_>,
890
945
  template: &Bound<'_, PyAny>,
891
946
  globals: Option<&Bound<'_, PyDict>>,
892
947
  locals: Option<&Bound<'_, PyDict>>,
948
+ registry: Option<&Bound<'_, PyAny>>,
893
949
  ) -> PyResult<String> {
894
950
  let bound = extract_template(py, template, "render_thtml_template")?;
951
+ let (globals, locals) =
952
+ normalize_scope_inputs(py, globals, locals, registry, "render_thtml_template")?;
895
953
  let compiled = compile_cached_thtml(&bound)?;
896
954
  let context = runtime_context_from_bound(py, &bound)?;
897
- render_thtml_document(py, compiled.document(), &context, globals, locals)
955
+ render_thtml_document(
956
+ py,
957
+ compiled.document(),
958
+ &context,
959
+ globals.as_ref(),
960
+ locals.as_ref(),
961
+ )
898
962
  }
899
963
 
900
964
  #[pymodule]
@@ -929,7 +993,9 @@ fn tstring_html_bindings(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResu
929
993
 
930
994
  #[cfg(test)]
931
995
  mod tests {
932
- use super::ParseCache;
996
+ use super::{ParseCache, render_thtml_node};
997
+ use pyo3::Python;
998
+ use tstring_html::{Node, RuntimeContext, RuntimeValue};
933
999
 
934
1000
  #[test]
935
1001
  fn parse_cache_reuses_entries() {
@@ -1020,4 +1086,30 @@ mod tests {
1020
1086
  assert_eq!(*value, 7);
1021
1087
  assert_eq!(attempts, 2);
1022
1088
  }
1089
+
1090
+ #[test]
1091
+ fn render_thtml_node_treats_uppercase_title_as_escaped_text() {
1092
+ Python::attach(|py| {
1093
+ let node = Node::RawTextElement(tstring_html::RawTextElementNode {
1094
+ name: "TITLE".to_string(),
1095
+ attributes: Vec::new(),
1096
+ children: vec![Node::Interpolation(tstring_html::InterpolationNode {
1097
+ interpolation_index: 0,
1098
+ expression: "title".to_string(),
1099
+ raw_source: Some("{title}".to_string()),
1100
+ conversion: None,
1101
+ format_spec: String::new(),
1102
+ span: None,
1103
+ })],
1104
+ span: None,
1105
+ });
1106
+ let context = RuntimeContext {
1107
+ values: vec![RuntimeValue::RawHtml("<safe>".to_string())],
1108
+ };
1109
+ let mut out = String::new();
1110
+ render_thtml_node(py, &node, &context, None, None, &mut out)
1111
+ .expect("render uppercase title");
1112
+ assert_eq!(out, "<TITLE>&lt;safe&gt;</TITLE>");
1113
+ });
1114
+ }
1023
1115
  }
@@ -830,7 +830,7 @@ pub fn is_raw_text_tag(name: &str) -> bool {
830
830
  }
831
831
 
832
832
  fn raw_text_allows_interpolation(name: &str) -> bool {
833
- name == "title"
833
+ name.eq_ignore_ascii_case("title")
834
834
  }
835
835
 
836
836
  fn validate_html_document(document: &Document) -> BackendResult<()> {
@@ -9,5 +9,5 @@ rust-version.workspace = true
9
9
  version.workspace = true
10
10
 
11
11
  [dependencies]
12
- tstring-html = { version = "0.1.5", path = "../tstring-html-rs" }
12
+ tstring-html = { version = "0.1.7", path = "../tstring-html-rs" }
13
13
  tstring-syntax.workspace = true
@@ -115,7 +115,7 @@ fn validate_raw_text_children(element: &tstring_html::RawTextElementNode) -> Bac
115
115
  for child in &element.children {
116
116
  match child {
117
117
  Node::Text(_) => {}
118
- Node::Interpolation(_) if element.name == "title" => {}
118
+ Node::Interpolation(_) if element.name.eq_ignore_ascii_case("title") => {}
119
119
  Node::Interpolation(interpolation) => {
120
120
  return Err(semantic_error(
121
121
  "html.semantic.raw_text_interpolation",
@@ -124,7 +124,7 @@ fn validate_raw_text_children(element: &tstring_html::RawTextElementNode) -> Bac
124
124
  ));
125
125
  }
126
126
  _ => {
127
- let message = if element.name == "title" {
127
+ let message = if element.name.eq_ignore_ascii_case("title") {
128
128
  format!(
129
129
  "Only text and interpolations are allowed inside <{}>.",
130
130
  element.name