Skip to main content

all: add a text sub-package with SmartyPants formatting

ID
6aad8e3
date
2026-03-29 20:37:20+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
7386d03
message
all: add a `text` sub-package with SmartyPants formatting
changed files
8 files, 75 additions, 4 deletions

Changed files

CHANGELOG.md (3523) → CHANGELOG.md (3706)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e35ddb7..336df4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # CHANGELOG
 
+## v29 - 2026-03-29
+
+Add a `chives.text` package which exports a `smartify()` function for applying SmartyPants-style formatting to a string -- adding curly quotes and smart dashes.
+
 ## v28 - 2026-03-28
 
 Remove the httpx dependency in `chives.urls`.

dev_requirements.in (85) → dev_requirements.in (90)

diff --git a/dev_requirements.in b/dev_requirements.in
index 735adba..99e47b4 100644
--- a/dev_requirements.in
+++ b/dev_requirements.in
@@ -1,4 +1,4 @@
--e file:.[media,static_site_tests,urls]
+-e file:.[media,static_site_tests,text,urls]
 
 build
 mypy

dev_requirements.txt (2259) → dev_requirements.txt (2306)

diff --git a/dev_requirements.txt b/dev_requirements.txt
index 1e90bc9..274e85e 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -28,7 +28,7 @@ iniconfig==2.3.0
     # via pytest
 jaraco-classes==3.4.0
     # via keyring
-jaraco-context==6.1.1
+jaraco-context==6.1.2
     # via keyring
 jaraco-functools==4.4.0
     # via keyring
@@ -79,7 +79,7 @@ pytest==9.0.2
     #   alexwlchan-chives
     #   pytest-cov
     #   pytest-vcr
-pytest-cov==7.0.0
+pytest-cov==7.1.0
     # via -r dev_requirements.in
 pytest-vcr==1.0.2
     # via -r dev_requirements.in
@@ -101,6 +101,8 @@ rich==14.3.3
     # via twine
 ruff==0.15.7
     # via -r dev_requirements.in
+smartypants==2.0.2
+    # via alexwlchan-chives
 twine==6.2.0
     # via -r dev_requirements.in
 typing-extensions==4.15.0

pyproject.toml (1297) → pyproject.toml (1320)

diff --git a/pyproject.toml b/pyproject.toml
index 355cb94..5f44086 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,7 @@ license = "MIT"
 [project.optional-dependencies]
 media = ["Pillow"]
 static_site_tests = ["playwright", "pytest", "rapidfuzz"]
+text = ["smartypants"]
 urls = ["certifi", "hyperlink"]
 
 [project.urls]

src/chives/__init__.py (391) → src/chives/__init__.py (391)

diff --git a/src/chives/__init__.py b/src/chives/__init__.py
index 65fd082..1401076 100644
--- a/src/chives/__init__.py
+++ b/src/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
 
 """
 
-__version__ = "28"
+__version__ = "29"

src/chives/text.py (0) → src/chives/text.py (713)

diff --git a/src/chives/text.py b/src/chives/text.py
new file mode 100644
index 0000000..bc3617d
--- /dev/null
+++ b/src/chives/text.py
@@ -0,0 +1,32 @@
+"""
+Functions for dealing with text.
+"""
+
+import functools
+
+import smartypants
+
+
+@functools.cache
+def smartify(text: str) -> str:
+    """
+    Add curly quotes and smart dashes to a string.
+    """
+    # Undo some escaping from Mistune.
+    text = text.replace("&quot;", '"')
+
+    attrs = (
+        # normal quotes (" and ') to curly ones
+        smartypants.Attr.q
+        |
+        # typewriter dashes (--) to en-dashes and dashes (---) to em-dashes
+        smartypants.Attr.D
+        |
+        # dashes (...) to ellipses
+        smartypants.Attr.e
+        |
+        # output Unicode chars instead of numeric character references
+        smartypants.Attr.u
+    )
+
+    return smartypants.smartypants(text, attrs)

tests/stubs/smartypants.pyi (0) → tests/stubs/smartypants.pyi (108)

diff --git a/tests/stubs/smartypants.pyi b/tests/stubs/smartypants.pyi
new file mode 100644
index 0000000..273b48b
--- /dev/null
+++ b/tests/stubs/smartypants.pyi
@@ -0,0 +1,7 @@
+class Attr:
+    q: int
+    D: int
+    e: int
+    u: int
+
+def smartypants(text: str, attrs: int) -> str: ...

tests/test_text.py (0) → tests/test_text.py (674)

diff --git a/tests/test_text.py b/tests/test_text.py
new file mode 100644
index 0000000..22e137e
--- /dev/null
+++ b/tests/test_text.py
@@ -0,0 +1,25 @@
+"""
+Tests for `chives.text`.
+"""
+
+import pytest
+
+from chives.text import smartify
+
+
+@pytest.mark.parametrize(
+    "text, expected",
+    [
+        ("Isn't it delightful -- she said", "Isn’t it delightful – she said"),
+        ("Are you ... sure?", "Are you … sure?"),
+        ("<h2>Isn't it delightful?</h2>", "<h2>Isn’t it delightful?</h2>"),
+        ("<li>Isn't it delightful?</li>", "<li>Isn’t it delightful?</li>"),
+        ("<p>&quot;It's nice&quot;, he said</p>", "<p>“It’s nice”, he said</p>"),
+    ],
+)
+def test_smartify(text: str, expected: str) -> None:
+    """
+    Test smartify().
+    """
+    actual = smartify(text)
+    assert actual == expected