Repository: geopandas/geopandas

Issue:
Escape special characters to avoid TemplateSyntaxError in explore
The following code has the error `TemplateSyntaxError: expected token ':', got 'a'BUG:`,

```python
import pandas as pd
import geopandas as gpd
data = [{"tweet":"I didnÃ¢â‚¬â„¢t want to do it. but I did it anyway. I looked at the Town Halls. {{{what a mess}}} they are so different. #Biden is a gentle caterpillar while #Trump is a rabid fox Ã°Å¸Â¥Âº #TownHalls #trustYourInstinct  #election2020 #holdontight #bumpyride",
"lat":40.7127281,"lon":-74.0060152}]
df = pd.DataFrame(data)
gdf = gpd.GeoDataFrame(df,geometry=gpd.points_from_xy(df.lon,df.lat),crs="epsg:4326")
gdf.explore()
```

I guess because of the {} brakets in `gdf`:

> I didnâ€™t want to do it. but I did it anyway. I looked at the Town Halls. {{{what a mess}}} they are so different. #Biden is a gentle caterpillar while #Trump is a rabid fox ðŸ¥º #TownHalls #trustYourInstinct #election2020 #holdontight #bumpyride

The bug can be reduplicated[ at colab](https://colab.research.google.com/drive/1fVtTDl1SkE1DzmPd_VvQqRe56vwmGz5Q?usp=sharing)





Discussion:
wybert: Hi, I just find out one check is failed,
```bash
FAILED geopandas/tests/test_dissolve.py::test_mean_dissolve - TypeError: Could not convert Staten IslandQueensBrooklyn to numeric
FAILED geopandas/tests/test_dissolve.py::test_dissolve_none_mean - TypeError: Could not convert Staten IslandQueensBrooklynManhattanBronx to numeric
FAILED geopandas/tests/test_extension_array.py::TestGroupby::test_in_numeric_groupby - AssertionError: Regex pattern did not match.
 Regex: 'does not support'
 Input: 'cannot perform sum with type geometry'
FAILED geopandas/tests/test_op_output_types.py::test_constructor_sliced_in_pandas_methods - TypeError: '<' not supported between instances of 'Point' and 'Point'
FAILED geopandas/tests/test_pandas_methods.py::test_numerical_operations - TypeError: cannot perform sum with type geometry
FAILED geopandas/tests/test_pandas_methods.py::test_groupby - TypeError: cannot perform sum with type geometry
= 6 failed, 1793 passed, 264 skipped, 11 xfailed, 2 xpassed, 2294 warnings in 97.43s (0:01:37) =
Error: Process completed with exit code 1.
```
Dose anyone know how it come?
martinfleis: The errors are not related to this PR (the same are on main) and can be ignored here.
martinfleis: Hey, thanks for the fix. Could you also add a test for this to `test_explore.py`?
wybert: > Hey, thanks for the fix. Could you also add a test for this to `test_explore.py`?

I just added [a test](https://github.com/wybert/geopandas/blob/3b5fb3f1486153b5d9021283d89c60d0da50e690/geopandas/tests/test_explore.py#L468) for it. 

martinfleis: Thanks! looks good. 2 more things 
- can you remove `oryx-build-commands.txt` that is added in this PR? 
- can you add a changelog note to the Changelog.md?
wybert: > 

I just delete that file and add a changelog note to the Changelog.md
jorisvandenbossche: (FYI the failures you will see on the dev branch are unrelated: https://github.com/scipy/scipy/issues/17811)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04eef7b5..d3b37828 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Bug fixes:
 - Ensure that GeoDataFrame created from DataFrame is a copy, not a view (#2667)
 - Fix mismatch between geometries and colors in ``plot()`` if an empty or missing
   geometry is present (#2224)
+- Escape special characters to avoid TemplateSyntaxError in ``explore()`` (#2657)
 - Fix `to_parquet`/`to_feather` to not write an invalid bbox (with NaNs) in the
   metadata in case of an empty GeoDataFrame (#2653)
 
diff --git a/geopandas/explore.py b/geopandas/explore.py
index 4fa68200..21b93d06 100644
--- a/geopandas/explore.py
+++ b/geopandas/explore.py
@@ -278,6 +278,7 @@ GON (((180.00000 -16.06713, 180.00000...
     try:
         import branca as bc
         import folium
+        import re
         import matplotlib
         import matplotlib.colors as colors
         import matplotlib.pyplot as plt
@@ -618,10 +619,21 @@ GON (((180.00000 -16.06713, 180.00000...
     else:
         tooltip = None
         popup = None
+    # escape the curly braces {{}} for jinja2 templates
+    feature_collection = gdf.__geo_interface__
+    for feature in feature_collection["features"]:
+        for k in feature["properties"]:
+            # escape the curly braces in values
+            if type(feature["properties"][k]) == str:
+                feature["properties"][k] = re.sub(
+                    r"\{{2,}",
+                    lambda x: "{% raw %}" + x.group(0) + "{% endraw %}",
+                    feature["properties"][k],
+                )
 
     # add dataframe to map
     folium.GeoJson(
-        gdf.__geo_interface__,
+        feature_collection,
         tooltip=tooltip,
         popup=popup,
         marker=marker,


Test patch:
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 285cc909..3e7c05d9 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -466,6 +466,15 @@ class TestExplore:
         out_str = self._fetch_map_string(m)
         assert "BoroName" in out_str
 
+    def test_escape_special_characters(self):
+        # check if special characters are escaped
+        gdf = self.world.copy()
+        gdf["name"] = """{{{what a mess}}} they are so different."""
+        m = gdf.explore()
+        out_str = self._fetch_map_string(m)
+        assert """{{{""" in out_str
+        assert """}}}""" in out_str
+
     def test_default_markers(self):
         # check overridden default for points
         m = self.cities.explore()