Skip to content

Commit 28472c0

Browse files
authored
Merge pull request #13 from django-components/jo-refactor-move-rust-python-api-to-djc-core
2 parents 03de1ab + 3a16642 commit 28472c0

File tree

10 files changed

+550
-501
lines changed

10 files changed

+550
-501
lines changed

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ members = [
66
resolver = "2"
77

88
[workspace.dependencies]
9-
pyo3 = { version = "0.27.0", features = ["extension-module"] }
9+
pyo3 = { version = "0.27.1", features = ["extension-module"] }
1010
quick-xml = "0.38.3"
1111

1212
# https://ohadravid.github.io/posts/2023-03-rusty-python
1313
[profile.release]
14-
debug = true # Debug symbols for profiler.
15-
lto = true # Link-time optimization.
16-
codegen-units = 1 # Slower compilation but faster code.
14+
debug = true # Debug symbols for profiler.
15+
lto = true # Link-time optimization.
16+
codegen-units = 1 # Slower compilation but faster code.

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ print(captured)
7272
# }
7373
```
7474

75+
## Architecture
76+
77+
This project uses a multi-crate Rust workspace structure to maintain clean separation of concerns:
78+
79+
### Crate structure
80+
81+
- **`djc-html-transformer`**: Pure Rust library for HTML transformation
82+
- **`djc-template-parser`**: Pure Rust library for Django template parsing
83+
- **`djc-core`**: Python bindings that combines all other libraries
84+
85+
### Design philosophy
86+
87+
To make sense of the code, the Python API and Rust logic are defined separately:
88+
89+
1. Each crate (AKA Rust package) has `lib.rs` (which is like Python's `__init__.py`). These files do not define the main logic, but only the public API of the crate. So the API that's to be used by other crates.
90+
2. The `djc-core` crate imports other crates
91+
3. And it is only this `djc-core` where we define the Python API using PyO3.
92+
7593
## Development
7694

7795
1. Setup python env

crates/djc-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[package]
22
name = "djc-core"
3+
description = "Singular Python API for Rust code used by django-components"
34
version = "1.1.0"
45
edition = "2021"
56

crates/djc-core/src/lib.rs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,76 @@
1-
use djc_html_transformer::set_html_attributes;
1+
use djc_html_transformer::{
2+
set_html_attributes as set_html_attributes_rust, HtmlTransformerConfig,
3+
};
4+
use pyo3::exceptions::{PyValueError};
25
use pyo3::prelude::*;
6+
use pyo3::types::{PyDict, PyTuple};
37

4-
/// A Python module implemented in Rust for high-performance transformations.
8+
/// Singular Python API that brings togther all the other Rust crates.
59
#[pymodule]
610
fn djc_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
11+
// HTML transformer
712
m.add_function(wrap_pyfunction!(set_html_attributes, m)?)?;
813
Ok(())
914
}
15+
16+
/// Transform HTML by adding attributes to the elements.
17+
///
18+
/// Args:
19+
/// html (str): The HTML string to transform. Can be a fragment or full document.
20+
/// root_attributes (List[str]): List of attribute names to add to root elements only.
21+
/// all_attributes (List[str]): List of attribute names to add to all elements.
22+
/// check_end_names (bool, optional): Whether to validate matching of end tags. Defaults to false.
23+
/// watch_on_attribute (str, optional): If set, captures which attributes were added to elements with this attribute.
24+
///
25+
/// Returns:
26+
/// Tuple[str, Dict[str, List[str]]]: A tuple containing:
27+
/// - The transformed HTML string
28+
/// - A dictionary mapping captured attribute values to lists of attributes that were added
29+
/// to those elements. Only returned if watch_on_attribute is set, otherwise empty dict.
30+
///
31+
/// Example:
32+
/// >>> html = '<div data-id="123"><p>Hello</p></div>'
33+
/// >>> html, captured = set_html_attributes(html, ['data-root-id'], ['data-v-123'], watch_on_attribute='data-id')
34+
/// >>> print(captured)
35+
/// {'123': ['data-root-id', 'data-v-123']}
36+
///
37+
/// Raises:
38+
/// ValueError: If the HTML is malformed or cannot be parsed.
39+
#[pyfunction]
40+
#[pyo3(signature = (html, root_attributes, all_attributes, check_end_names=None, watch_on_attribute=None))]
41+
#[pyo3(
42+
text_signature = "(html, root_attributes, all_attributes, *, check_end_names=False, watch_on_attribute=None)"
43+
)]
44+
pub fn set_html_attributes(
45+
py: Python,
46+
html: &str,
47+
root_attributes: Vec<String>,
48+
all_attributes: Vec<String>,
49+
check_end_names: Option<bool>,
50+
watch_on_attribute: Option<String>,
51+
) -> PyResult<Py<PyAny>> {
52+
let config = HtmlTransformerConfig::new(
53+
root_attributes,
54+
all_attributes,
55+
check_end_names.unwrap_or(false),
56+
watch_on_attribute,
57+
);
58+
59+
match set_html_attributes_rust(html, &config) {
60+
Ok((html, captured)) => {
61+
// Convert captured attributes to a Python dictionary
62+
let captured_dict = PyDict::new(py);
63+
for (id, attrs) in captured {
64+
captured_dict.set_item(id, attrs)?;
65+
}
66+
67+
// Convert items to Bound<PyAny> for the tuple
68+
use pyo3::types::PyString;
69+
let html_obj = PyString::new(py, &html).as_any().clone();
70+
let dict_obj = captured_dict.as_any().clone();
71+
let result = PyTuple::new(py, vec![html_obj, dict_obj])?;
72+
Ok(result.into_any().unbind())
73+
}
74+
Err(e) => Err(PyValueError::new_err(e.to_string())),
75+
}
76+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[package]
22
name = "djc-html-transformer"
3+
description = "Apply attributes to HTML in a single pass"
34
version = "1.0.3"
45
edition = "2021"
56

67
[dependencies]
7-
pyo3 = { workspace = true }
88
quick-xml = { workspace = true }

0 commit comments

Comments
 (0)