feat(utils): add parse_kind_prefix helper

Owns the 'character:Saber' user-input shortcut parser in one place.
Invoked by add_tag, bulk_add_tag, and the migration's sanity check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 09:24:21 -04:00
parent 7330a9f8f5
commit b9104f9caa
+43
View File
@@ -0,0 +1,43 @@
"""Parse the user-facing `kind:name` shortcut used by the add-tag input.
After the bare-name refactor, `Tag.name` no longer stores the `kind:` prefix.
The prefix lives only as an input convention in a handful of user-facing
places (modal add-tag input, bulk-add-tag form, import paths that receive
user-typed strings). This module owns that parser.
"""
from __future__ import annotations
# The set of tag kinds the app recognizes as prefixable at the input boundary.
# `user` is intentionally excluded — user-typed general tags never carry a
# prefix. Strings like "http://example" keep their colon as literal text
# because "http" is not in this set.
KNOWN_KINDS: frozenset[str] = frozenset({
'character',
'fandom',
'artist',
'series',
'post',
'archive',
'meta',
})
def parse_kind_prefix(raw: str) -> tuple[str | None, str]:
"""Split a raw user-typed tag string into (kind, name).
Returns:
(kind, name) where kind is one of KNOWN_KINDS, or
(None, raw.strip()) if no recognized prefix is present.
`name` is always whitespace-stripped.
Examples:
parse_kind_prefix('character:Saber') -> ('character', 'Saber')
parse_kind_prefix('sunset') -> (None, 'sunset')
parse_kind_prefix('http://example') -> (None, 'http://example')
parse_kind_prefix('artist: Eric ') -> ('artist', 'Eric')
"""
if ':' in raw:
prefix, rest = raw.split(':', 1)
if prefix in KNOWN_KINDS:
return prefix, rest.strip()
return None, raw.strip()