Repository: pypi/warehouse

Issue:
Invalid classifier IDs on search results page
Each classifier on the search results page displays in the sidebar like so:

```html
<li>
    <input name="c" type="checkbox" id="Framework :: AsyncIO" class="-js-form-submit-trigger checkbox-tree__checkbox" value="Framework :: AsyncIO">
    <label class="checkbox-tree__label" for="Framework :: AsyncIO">AsyncIO</label>
 </li>
```

The ID here e.g. (`"Framework :: AsyncIO"`) is invalid HTML as it contains spaces.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id

We should also check that we are following MDNs recommendation:

> Note: Using characters except ASCII letters, digits, '_', '-' and '.' may cause compatibility problems, as they weren't allowed in HTML 4. Though this restriction has been lifted in HTML5, an ID should start with a letter for compatibility.


Discussion:

Resolving patch:
diff --git a/warehouse/config.py b/warehouse/config.py
--- a/warehouse/config.py
+++ b/warehouse/config.py
@@ -284,6 +284,7 @@ def configure(settings=None):
     # We'll want to configure some filters for Jinja2 as well.
     filters = config.get_settings().setdefault("jinja2.filters", {})
     filters.setdefault("format_classifiers", "warehouse.filters:format_classifiers")
+    filters.setdefault("classifier_id", "warehouse.filters:classifier_id")
     filters.setdefault("format_tags", "warehouse.filters:format_tags")
     filters.setdefault("json", "warehouse.filters:tojson")
     filters.setdefault("camoify", "warehouse.filters:camoify")
diff --git a/warehouse/filters.py b/warehouse/filters.py
--- a/warehouse/filters.py
+++ b/warehouse/filters.py
@@ -144,6 +144,10 @@ def format_classifiers(classifiers):
     return structured
 
 
+def classifier_id(classifier):
+    return classifier.replace(" ", "_").replace("::", ".")
+
+
 def contains_valid_uris(items):
     """Returns boolean representing whether the input list contains any valid
     URIs


Test patch:
diff --git a/tests/functional/test_templates.py b/tests/functional/test_templates.py
--- a/tests/functional/test_templates.py
+++ b/tests/functional/test_templates.py
@@ -40,6 +40,7 @@ def test_templates_for_empty_titles():
             "format_rfc822_datetime": "warehouse.i18n.filters:format_rfc822_datetime",
             "format_number": "warehouse.i18n.filters:format_number",
             "format_classifiers": "warehouse.filters:format_classifiers",
+            "classifier_id": "warehouse.filters:classifier_id",
             "format_tags": "warehouse.filters:format_tags",
             "json": "warehouse.filters:tojson",
             "camoify": "warehouse.filters:camoify",
diff --git a/tests/unit/test_filters.py b/tests/unit/test_filters.py
--- a/tests/unit/test_filters.py
+++ b/tests/unit/test_filters.py
@@ -152,6 +152,13 @@ def test_format_classifiers(inp, expected):
     assert list(filters.format_classifiers(inp).items()) == expected
 
 
+@pytest.mark.parametrize(
+    ("inp", "expected"), [("Foo", "Foo"), ("Foo :: Foo", "Foo_._Foo")]
+)
+def test_classifier_id(inp, expected):
+    assert filters.classifier_id(inp) == expected
+
+
 @pytest.mark.parametrize(
     ("inp", "expected"),
     [