Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 4 additions & 10 deletions api/export_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from utils.md_exporter import session_to_markdown
from utils.json_exporter import session_to_json
from utils.exclusion_rules import is_session_excluded
from utils.slugify import slugify

export_bp = Blueprint("export", __name__)

Expand Down Expand Up @@ -49,13 +50,6 @@ def get_export_state():
})


def _slugify(text: str) -> str:
import re
text = text.lower()
text = re.sub(r"[^a-z0-9]+", "-", text)
return text.strip("-")


@export_bp.route("/api/export", methods=["POST"])
def bulk_export():
body = request.get_json(silent=True) or {}
Expand Down Expand Up @@ -97,9 +91,9 @@ def bulk_export():

stats = compute_stats(session)
md = session_to_markdown(session, stats)
title_slug = _slugify(session["title"]) or "session"
title_slug = slugify(session["title"]) or "session"
short_id = sid[:8]
proj_slug = _slugify(project["name"])
proj_slug = slugify(project["name"])
ts = session["metadata"].get("first_timestamp", "")
ts_file = ts[:19].replace(":", "-") if ts else "0000-00-00T00-00-00"
rel_path = f"{proj_slug}/{ts_file}__{title_slug}__{short_id}.md"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -155,7 +149,7 @@ def export_session(project_name, session_id):
if is_session_excluded(rules, session, project_name):
return jsonify({"error": "Session not found"}), 404
stats = compute_stats(session)
title_slug = _slugify(session["title"]) or "session"
title_slug = slugify(session["title"]) or "session"

if fmt == "json":
content = session_to_json(session, stats)
Expand Down
19 changes: 4 additions & 15 deletions scripts/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
load_rules,
is_session_excluded,
)
from utils.slugify import slugify


STATE_DIR = os.path.join(os.path.expanduser("~"), ".claude-code-chat-browser")
Expand Down Expand Up @@ -366,9 +367,9 @@ def cmd_export(args):
meta["first_timestamp"] = ts
date_str = ts[:10]
ts_file = ts[:19].replace(":", "-") # 2026-02-10T01-46-15
title_slug = _slugify(session["title"])
title_slug = slugify(session["title"])
short_id = sid[:8]
project_slug = _slugify(project["name"])
project_slug = slugify(project["name"])

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if fmt in ("md", "both"):
md = session_to_markdown(session, stats)
Expand Down Expand Up @@ -444,7 +445,7 @@ def cmd_export(args):

def _export_single(session: dict, stats: dict, fmt: str, out_dir: str):
"""Write one session to disk as md, json, or both."""
title_slug = _slugify(session["title"])
title_slug = slugify(session["title"])
short_id = session["session_id"][:8]
ts = session["metadata"].get("first_timestamp", "")
ts_file = ts[:19].replace(":", "-") if ts else "0000-00-00T00-00-00"
Expand Down Expand Up @@ -609,18 +610,6 @@ def _save_state(sessions: dict, count: int, out_dir: str):
json.dump(state, f, indent=2)


def _slugify(text: str) -> str:
slug = ""
for c in text.lower():
if c.isalnum():
slug += c
elif c in " -_/.":
slug += "-"
while "--" in slug:
slug = slug.replace("--", "-")
return slug.strip("-")


def _die(msg: str):
print(f"Error: {msg}", file=sys.stderr)
sys.exit(1)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_slugify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Regression tests for utils.slugify (Issue #30 / CCC8).

Historically ``scripts/export.py`` used ``isalnum()`` (Unicode letters preserved)
while ``api/export_api.py`` used ASCII-only ``[^a-z0-9]+``. The canonical
implementation matches the API for portable zip / download filenames.
"""

from utils.slugify import slugify


def test_ascii_words_hyphenated():
assert slugify("Hello World") == "hello-world"


def test_punctuation_collapses_to_single_hyphen():
assert slugify("foo__bar") == "foo-bar"
assert slugify("a.b.c") == "a-b-c"


def test_unicode_letters_become_ascii_safe():
"""Old CLI kept Latin-1 letters (e.g. é); canonical slug strips to ASCII."""
assert slugify("Café noir") == "caf-noir"


def test_empty_after_strip():
assert slugify("!!!") == ""


def test_digits_preserved():
assert slugify("Issue 42 Fix") == "issue-42-fix"
Comment thread
clean6378-max-it marked this conversation as resolved.
18 changes: 18 additions & 0 deletions utils/slugify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Filesystem- and URL-safe slugs for export paths and download names.

Uses ASCII letters and digits only; other characters (including Unicode
letters and punctuation) become hyphen runs, then trimmed. Matches the
historical behavior of ``api/export_api.py`` and avoids platform-specific
issues with non-ASCII paths inside zip archives.
"""

from __future__ import annotations

import re


def slugify(text: str) -> str:
Comment thread
timon0305 marked this conversation as resolved.
Outdated
"""Lowercase *text* and replace each run of non-[a-z0-9] with a single hyphen."""
text = text.lower()
text = re.sub(r"[^a-z0-9]+", "-", text)
Comment thread
clean6378-max-it marked this conversation as resolved.
return text.strip("-")
Loading