Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ members = [
resolver = "2"

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

# https://ohadravid.github.io/posts/2023-03-rusty-python
[profile.release]
debug = true # Debug symbols for profiler.
lto = true # Link-time optimization.
codegen-units = 1 # Slower compilation but faster code.
debug = true # Debug symbols for profiler.
lto = true # Link-time optimization.
codegen-units = 1 # Slower compilation but faster code.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ print(captured)
# }
```

## Architecture

This project uses a multi-crate Rust workspace structure to maintain clean separation of concerns:

### Crate structure

- **`djc-html-transformer`**: Pure Rust library for HTML transformation
- **`djc-template-parser`**: Pure Rust library for Django template parsing
- **`djc-core`**: Python bindings that combines all other libraries

### Design philosophy

To make sense of the code, the Python API and Rust logic are defined separately:

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.
2. The `djc-core` crate imports other crates
3. And it is only this `djc-core` where we define the Python API using PyO3.

## Development

1. Setup python env
Expand Down
1 change: 1 addition & 0 deletions crates/djc-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[package]
name = "djc-core"
description = "Singular Python API for Rust code used by django-components"
version = "1.1.0"
edition = "2021"

Expand Down
71 changes: 69 additions & 2 deletions crates/djc-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,76 @@
use djc_html_transformer::set_html_attributes;
use djc_html_transformer::{
set_html_attributes as set_html_attributes_rust, HtmlTransformerConfig,
};
use pyo3::exceptions::{PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};

/// A Python module implemented in Rust for high-performance transformations.
/// Singular Python API that brings togther all the other Rust crates.
#[pymodule]
fn djc_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
// HTML transformer
m.add_function(wrap_pyfunction!(set_html_attributes, m)?)?;
Ok(())
}

/// Transform HTML by adding attributes to the elements.
///
/// Args:
/// html (str): The HTML string to transform. Can be a fragment or full document.
/// root_attributes (List[str]): List of attribute names to add to root elements only.
/// all_attributes (List[str]): List of attribute names to add to all elements.
/// check_end_names (bool, optional): Whether to validate matching of end tags. Defaults to false.
/// watch_on_attribute (str, optional): If set, captures which attributes were added to elements with this attribute.
///
/// Returns:
/// Tuple[str, Dict[str, List[str]]]: A tuple containing:
/// - The transformed HTML string
/// - A dictionary mapping captured attribute values to lists of attributes that were added
/// to those elements. Only returned if watch_on_attribute is set, otherwise empty dict.
///
/// Example:
/// >>> html = '<div data-id="123"><p>Hello</p></div>'
/// >>> html, captured = set_html_attributes(html, ['data-root-id'], ['data-v-123'], watch_on_attribute='data-id')
/// >>> print(captured)
/// {'123': ['data-root-id', 'data-v-123']}
///
/// Raises:
/// ValueError: If the HTML is malformed or cannot be parsed.
#[pyfunction]
#[pyo3(signature = (html, root_attributes, all_attributes, check_end_names=None, watch_on_attribute=None))]
#[pyo3(
text_signature = "(html, root_attributes, all_attributes, *, check_end_names=False, watch_on_attribute=None)"
)]
pub fn set_html_attributes(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here is the Python definition of set_html_attributes() function.

To actually expose it to Python, this Python function is added to the module's scope on line 12:

    m.add_function(wrap_pyfunction!(set_html_attributes, m)?)?;

py: Python,
html: &str,
root_attributes: Vec<String>,
all_attributes: Vec<String>,
check_end_names: Option<bool>,
watch_on_attribute: Option<String>,
) -> PyResult<Py<PyAny>> {
let config = HtmlTransformerConfig::new(
root_attributes,
all_attributes,
check_end_names.unwrap_or(false),
watch_on_attribute,
);

match set_html_attributes_rust(html, &config) {
Ok((html, captured)) => {
// Convert captured attributes to a Python dictionary
let captured_dict = PyDict::new(py);
for (id, attrs) in captured {
captured_dict.set_item(id, attrs)?;
}

// Convert items to Bound<PyAny> for the tuple
use pyo3::types::PyString;
let html_obj = PyString::new(py, &html).as_any().clone();
let dict_obj = captured_dict.as_any().clone();
let result = PyTuple::new(py, vec![html_obj, dict_obj])?;
Ok(result.into_any().unbind())
}
Err(e) => Err(PyValueError::new_err(e.to_string())),
}
}
2 changes: 1 addition & 1 deletion crates/djc-html-transformer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "djc-html-transformer"
description = "Apply attributes to HTML in a single pass"
version = "1.0.3"
edition = "2021"

[dependencies]
pyo3 = { workspace = true }
quick-xml = { workspace = true }
Loading