diff --git a/tests/test_convey_icons.py b/tests/test_convey_icons.py index c18f7216e..2ffbc3e0c 100644 --- a/tests/test_convey_icons.py +++ b/tests/test_convey_icons.py @@ -27,6 +27,7 @@ CONVERTED_FILES = ( ROOT / "solstone" / "apps" / "search" / "workspace.html", ROOT / "solstone" / "apps" / "sol" / "workspace.html", ROOT / "solstone" / "apps" / "import" / "workspace.html", + ROOT / "solstone" / "apps" / "settings" / "workspace.html", ROOT / "solstone" / "apps" / "support" / "workspace.html", ROOT / "solstone" / "apps" / "support" / "static" / "support.js", ROOT / "solstone" / "apps" / "stats" / "workspace.html", @@ -58,6 +59,16 @@ L2_ICON_SLOT_CLASSES = ( "col-activity", "activity-item", "summary-item", + "import-source-card-icon", + "import-source-inline-icon", + "import-content-summary-icon", + "filter-icon", + "result-agent-icon", + "occ-activity-icon", + "act-icon", + "activity-detail-icon", + "activity-icon", + "activity-chip-icon", ) ICON_SLOT_CLASSES = ( "surface-state-icon", @@ -74,6 +85,10 @@ ICON_SLOT_OPEN_RE = re.compile( re.IGNORECASE, ) EMPTY_ICONS_MAP_RE = re.compile(r"const\s+emptyIcons\s*=\s*\{[\s\S]*?\n\s*\};") +UNICODE_ESCAPE_RE = re.compile( + r"\\U(?P[0-9a-fA-F]{8})|\\u(?P[0-9a-fA-F]{4})" +) +STYLE_BLOCK_RE = re.compile(r"]*>(?P[\s\S]*?)", re.IGNORECASE) L2_ICON_SLOT_CSS_FILES = { "icon-slot": ROOT / "solstone" / "convey" / "static" / "app.css", "entity-delete-btn": ROOT / "solstone" / "apps" / "entities" / "workspace.html", @@ -86,6 +101,28 @@ L2_ICON_SLOT_CSS_FILES = { "col-activity": ROOT / "solstone" / "apps" / "sol" / "workspace.html", "activity-item": ROOT / "solstone" / "apps" / "sol" / "workspace.html", "summary-item": ROOT / "solstone" / "apps" / "sol" / "workspace.html", + "import-source-card-icon": ROOT / "solstone" / "apps" / "import" / "workspace.html", + "import-source-inline-icon": ROOT + / "solstone" + / "apps" + / "import" + / "workspace.html", + "import-content-summary-icon": ROOT + / "solstone" + / "apps" + / "import" + / "workspace.html", + "filter-icon": ROOT / "solstone" / "apps" / "search" / "workspace.html", + "result-agent-icon": ROOT / "solstone" / "apps" / "search" / "workspace.html", + "occ-activity-icon": ROOT / "solstone" / "apps" / "activities" / "workspace.html", + "act-icon": ROOT / "solstone" / "apps" / "activities" / "workspace.html", + "activity-detail-icon": ROOT + / "solstone" + / "apps" + / "activities" + / "workspace.html", + "activity-icon": ROOT / "solstone" / "apps" / "settings" / "workspace.html", + "activity-chip-icon": ROOT / "solstone" / "apps" / "settings" / "workspace.html", } CONVERTED_GLYPH_RESIDUE = { @@ -107,6 +144,12 @@ CONVERTED_GLYPH_RESIDUE = { "solstone/apps/import/workspace.html": ( '
📥
', '
🔍
', + "source.emoji", + "source_emoji", + "sourceEmojiByName", + "import-source-card-emoji", + "import-source-inline-emoji", + '

⚡ quick import

', ), "solstone/apps/support/static/support.js": ( '
🛟
', @@ -116,6 +159,13 @@ CONVERTED_GLYPH_RESIDUE = { "solstone/apps/support/workspace.html": ( '
⚠️
', '
⋯
', + '

💬 Share your impressions, ideas, or anything on your mind. Your feedback shapes the product.

', + "

🛟 getting help

", + "

🔍 search the knowledge base

", + "

🩺 run diagnostics

", + "

📢 announcements

", + "

🔒 privacy

", + "const icons = {'known-issue': '⚠️', 'maintenance': '🔧', 'info': '📢'};", ), "solstone/apps/stats/static/dashboard.js": ( "['📊']", @@ -200,14 +250,6 @@ OUT_OF_SCOPE_GLYPHS = { "solstone/convey/static/shell.html": ( '', ), - "solstone/apps/support/workspace.html": ( - "

🛟 getting help

", - "

🔍 search the knowledge base

", - "const icons = {'known-issue': '⚠️', 'maintenance': '🔧', 'info': '📢'};", - ), - "solstone/apps/import/workspace.html": ( - '

⚡ quick import

', - ), } @@ -256,6 +298,37 @@ def _converted_source() -> str: return "\n".join(path.read_text(encoding="utf-8") for path in CONVERTED_FILES) +def _combine_surrogate_pairs(text: str) -> str: + chars: list[str] = [] + index = 0 + while index < len(text): + codepoint = ord(text[index]) + if 0xD800 <= codepoint <= 0xDBFF and index + 1 < len(text): + next_codepoint = ord(text[index + 1]) + if 0xDC00 <= next_codepoint <= 0xDFFF: + chars.append( + chr( + 0x10000 + + ((codepoint - 0xD800) << 10) + + (next_codepoint - 0xDC00) + ) + ) + index += 2 + continue + chars.append(text[index]) + index += 1 + return "".join(chars) + + +def _glyph_scan_source(text: str) -> str: + def decode_match(match: re.Match[str]) -> str: + raw_codepoint = match.group("long") or match.group("short") + return chr(int(raw_codepoint, 16)) + + decoded = UNICODE_ESCAPE_RE.sub(decode_match, text) + return text + "\n" + _combine_surrogate_pairs(decoded) + + def requested_icon_names(source: str) -> set[str]: return {match.group("name") for match in CALL_RE.finditer(source)} @@ -267,18 +340,24 @@ def _inline_slot_icon_names(path: Path, svg_to_name: Mapping[str, str]) -> set[s def _rule_bodies_for_svg_class(source: str, class_name: str) -> list[str]: class_ref = re.escape(class_name) bodies: list[str] = [] - for match in re.finditer(r"(?P[^{}]+)\{(?P[^{}]+)\}", source): - selectors = [ - selector.strip() for selector in match.group("selectors").split(",") - ] - if any( - re.search( - rf"(?[^{}]+)\{(?P[^{}]+)\}", style_source ): - bodies.append(match.group("body")) + selectors = [ + selector.strip() for selector in match.group("selectors").split(",") + ] + if any( + re.search( + rf"(? None: assert resolve_icon_svg("brain", "📚") == lucide_svg("brain") assert resolve_icon_svg(None, "📚") == lucide_svg("library") assert resolve_icon_svg("", "📚") == lucide_svg("library") - assert resolve_icon_svg("definitely-not-an-icon", "📚") == lucide_svg( - "library" - ) + assert resolve_icon_svg("definitely-not-an-icon", "📚") == lucide_svg("library") assert resolve_icon_svg("coins", "🪮") == lucide_svg("coins") +def test_data_driven_l3_icon_names_resolve() -> None: + import importlib + + import_routes = importlib.import_module("solstone.apps.import.routes") + from solstone.apps.search.routes import AGENT_ICON_FALLBACK, AGENT_ICONS + from solstone.think.activities import DEFAULT_ACTIVITIES + + import_icon_names = [source["icon"] for source in import_routes.SOURCE_METADATA] + search_icon_names = [*AGENT_ICONS.values(), AGENT_ICON_FALLBACK] + activity_icon_names = [activity["icon"] for activity in DEFAULT_ACTIVITIES] + + for icon_name in import_icon_names: + assert lucide_svg(icon_name), f"import icon not found: {icon_name}" + for icon_name in search_icon_names: + assert lucide_svg(icon_name), f"search icon not found: {icon_name}" + for icon_name in activity_icon_names: + assert lucide_svg(icon_name), f"activity icon not found: {icon_name}" + + +def test_activity_icon_overrides_use_explicit_lucide_names() -> None: + from solstone.think.activities import DEFAULT_ACTIVITIES + + by_id = {activity["id"]: activity for activity in DEFAULT_ACTIVITIES} + + appointment = by_id["appointment"] + assert resolve_icon_svg(appointment["icon"], appointment["emoji"]) == lucide_svg( + "pin" + ) + assert resolve_icon_svg(appointment["icon"], appointment["emoji"]) != lucide_svg( + "map-pin" + ) + + event = by_id["event"] + assert resolve_icon_svg(event["icon"], event["emoji"]) == lucide_svg("ticket") + assert resolve_icon_svg(event["icon"], event["emoji"]) != lucide_svg("tags") + + def test_search_lucide_icons_lock_matches_name_or_tag() -> None: tags = _lucide_tags() results = search_lucide_icons("lock")