diff --git a/docs/formatting.md b/docs/formatting.md
index f7212079..3a9ac302 100644
--- a/docs/formatting.md
+++ b/docs/formatting.md
@@ -63,6 +63,12 @@ Conversion specifiers allow to *convert* the value to a different form or type.
{foo!C} |
Foo Bar |
+
+ g |
+ Slugify a value |
+ {foo!g} |
+ foo-bar |
+
j |
Serialize value to a JSON formatted string |
diff --git a/gallery_dl/formatter.py b/gallery_dl/formatter.py
index 192950bc..dd32b8a5 100644
--- a/gallery_dl/formatter.py
+++ b/gallery_dl/formatter.py
@@ -381,6 +381,7 @@ _CONVERSIONS = {
"T": util.datetime_to_timestamp_string,
"d": text.parse_timestamp,
"U": text.unescape,
+ "g": text.slugify,
"S": util.to_string,
"s": str,
"r": repr,
diff --git a/gallery_dl/text.py b/gallery_dl/text.py
index 97ef3acb..79cf016e 100644
--- a/gallery_dl/text.py
+++ b/gallery_dl/text.py
@@ -39,6 +39,16 @@ def split_html(txt):
return []
+def slugify(value):
+ """Convert a string to a URL slug
+
+ Adapted from:
+ https://github.com/django/django/blob/master/django/utils/text.py
+ """
+ value = re.sub(r"[^\w\s-]", "", str(value).lower())
+ return re.sub(r"[-\s]+", "-", value).strip("-_")
+
+
def ensure_http_scheme(url, scheme="https://"):
"""Prepend 'scheme' to 'url' if it doesn't have one"""
if url and not url.startswith(("https://", "http://")):
diff --git a/test/test_formatter.py b/test/test_formatter.py
index 41fe2e21..b3353326 100644
--- a/test/test_formatter.py
+++ b/test/test_formatter.py
@@ -58,6 +58,7 @@ class TestFormatter(unittest.TestCase):
self._run_test("{dt!T}", "1262304000")
self._run_test("{l!j}", '["a", "b", "c"]')
self._run_test("{dt!j}", '"2010-01-01 00:00:00"')
+ self._run_test("{a!g}", "hello-world")
with self.assertRaises(KeyError):
self._run_test("{a!q}", "hello world")
diff --git a/test/test_text.py b/test/test_text.py
index ffed7267..0ac77671 100644
--- a/test/test_text.py
+++ b/test/test_text.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright 2015-2021 Mike Fährmann
+# Copyright 2015-2022 Mike Fährmann
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
@@ -75,6 +75,23 @@ class TestText(unittest.TestCase):
for value in INVALID:
self.assertEqual(f(value), empty)
+ def test_slugify(self, f=text.slugify):
+ self.assertEqual(f("Hello World"), "hello-world")
+ self.assertEqual(f("-HeLLo---World-"), "hello-world")
+ self.assertEqual(f("_-H#e:l#l:o+\t+W?o!rl=d-_"), "hello-world")
+ self.assertEqual(f("_Hello_World_"), "hello_world")
+
+ self.assertEqual(f(""), "")
+ self.assertEqual(f("-"), "")
+ self.assertEqual(f("--"), "")
+
+ self.assertEqual(f(()), "")
+ self.assertEqual(f([]), "")
+ self.assertEqual(f({}), "")
+ self.assertEqual(f(None), "none")
+ self.assertEqual(f(1), "1")
+ self.assertEqual(f(2.3), "23")
+
def test_ensure_http_scheme(self, f=text.ensure_http_scheme):
result = "https://example.org/filename.ext"