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
2 changes: 2 additions & 0 deletions nh3.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Cleaner:
allowed_classes: Optional[Mapping[str, AbstractSet[str]]] = None,
filter_style_properties: Optional[AbstractSet[str]] = None,
url_relative: Optional[UrlRelative] = None,
id_prefix: Optional[str] = None,
) -> None: ...
def clean(self, html: str) -> str: ...

Expand All @@ -56,6 +57,7 @@ def clean(
allowed_classes: Optional[Mapping[str, AbstractSet[str]]] = None,
filter_style_properties: Optional[AbstractSet[str]] = None,
url_relative: Optional[UrlRelative] = None,
id_prefix: Optional[str] = None,
) -> str: ...
def clean_text(html: str, tags: Optional[AbstractSet[str]] = None) -> str: ...
def escape(html: str, tags: Optional[AbstractSet[str]] = None) -> str: ...
Expand Down
32 changes: 30 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct Config {
allowed_classes: Option<HashMap<String, HashSet<String>>>,
filter_style_properties: Option<HashSet<String>>,
url_relative: Option<UrlRelativeConfig>,
id_prefix: Option<String>,
}

impl Default for Config {
Expand All @@ -51,6 +52,7 @@ impl Default for Config {
allowed_classes: None,
filter_style_properties: None,
url_relative: None,
id_prefix: None,
}
}
}
Expand Down Expand Up @@ -203,6 +205,11 @@ struct Inner {
/// returns a non-string, non-``None`` value) strips the URL, and the error
/// is reported via ``sys.unraisablehook``.
/// :type url_relative: ``str | tuple | Callable[[str], str | None]``, optional
/// :param id_prefix: Prepends the given string to every allowed ``id`` attribute value,
/// which helps avoid collisions with ``id``\ s already present on the host page.
/// The tag and the ``id`` attribute must still be whitelisted (via ``attributes``);
/// values that already start with the prefix are left unchanged. Defaults to ``None``.
/// :type id_prefix: ``str``, optional
///
/// Example usage:
///
Expand Down Expand Up @@ -418,6 +425,9 @@ impl Cleaner {
};
builder.url_relative(value);
}
if let Some(id_prefix) = config.id_prefix.as_ref() {
builder.id_prefix(Some(id_prefix.as_str()));
}

builder
}
Expand All @@ -444,7 +454,8 @@ impl Cleaner {
url_schemes = None,
allowed_classes = None,
filter_style_properties = None,
url_relative = None
url_relative = None,
id_prefix = None
))]
fn py_new(
py: Python,
Expand All @@ -461,6 +472,7 @@ impl Cleaner {
allowed_classes: Option<HashMap<String, HashSet<String>>>,
filter_style_properties: Option<HashSet<String>>,
url_relative: Option<Py<PyAny>>,
id_prefix: Option<String>,
) -> PyResult<Self> {
if let Some(callback) = attribute_filter.as_ref() {
if !callback.bind(py).is_callable() {
Expand Down Expand Up @@ -522,6 +534,7 @@ impl Cleaner {
allowed_classes,
filter_style_properties,
url_relative,
id_prefix,
};
Ok(Self::new(config))
}
Expand Down Expand Up @@ -678,6 +691,18 @@ impl Cleaner {
/// ... url_relative=lambda url: f"https://cdn.example.com{url}",
/// ... )
/// '<img src="https://cdn.example.com/a.png">'
///
/// ``id_prefix`` namespaces ``id`` attributes (which must be whitelisted) so they
/// cannot collide with ``id``\ s on the surrounding page:
///
/// .. code-block:: pycon
///
/// >>> nh3.clean(
/// ... '<b id="x">hi</b>',
/// ... attributes={"b": {"id"}},
/// ... id_prefix="user-content-",
/// ... )
/// '<b id="user-content-x">hi</b>'

#[pyfunction(signature = (
html,
Expand All @@ -693,7 +718,8 @@ impl Cleaner {
url_schemes = None,
allowed_classes = None,
filter_style_properties = None,
url_relative = None
url_relative = None,
id_prefix = None
))]
#[allow(clippy::too_many_arguments)]
fn clean(
Expand All @@ -712,6 +738,7 @@ fn clean(
allowed_classes: Option<HashMap<String, HashSet<String>>>,
filter_style_properties: Option<HashSet<String>>,
url_relative: Option<Py<PyAny>>,
id_prefix: Option<String>,
) -> PyResult<String> {
let cleaner = Cleaner::py_new(
py,
Expand All @@ -728,6 +755,7 @@ fn clean(
allowed_classes,
filter_style_properties,
url_relative,
id_prefix,
)?;
Ok(py.detach(|| cleaner.clean(html)))
}
Expand Down
27 changes: 27 additions & 0 deletions tests/test_nh3.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,33 @@ def test_cleaner_url_relative_reusable():
)


def test_clean_id_prefix():
# id_prefix prepends the given string to every allowed `id` value.
assert (
nh3.clean("<b id='a'>x</b>", attributes={"b": {"id"}}, id_prefix="safe-")
== '<b id="safe-a">x</b>'
)
# Values already carrying the prefix are left untouched (no double prefix).
assert (
nh3.clean("<b id='safe-a'>x</b>", attributes={"b": {"id"}}, id_prefix="safe-")
== '<b id="safe-a">x</b>'
)
# The `id` attribute must still be whitelisted; otherwise it is stripped and
# the prefix is irrelevant.
assert nh3.clean("<b id='a'>x</b>", id_prefix="safe-") == "<b>x</b>"
# Omitting id_prefix keeps `id` values unchanged (ammonia default).
assert (
nh3.clean("<b id='a'>x</b>", attributes={"b": {"id"}})
== '<b id="a">x</b>'
)


def test_cleaner_id_prefix_reusable():
cleaner = nh3.Cleaner(attributes={"b": {"id"}}, id_prefix="safe-")
assert cleaner.clean("<b id='a'>x</b>") == '<b id="safe-a">x</b>'
assert cleaner.clean("<b id='b'>y</b>") == '<b id="safe-b">y</b>'


def test_is_html():
assert not nh3.is_html("plain text")
assert nh3.is_html("<p>html!</p>")
Expand Down
Loading