splitFilenameID identifies the ID portion and remaining slug from a filename stem. Returns ("", "") if no ID pattern matches.
(name string)
| 127 | // splitFilenameID identifies the ID portion and remaining slug from a filename stem. |
| 128 | // Returns ("", "") if no ID pattern matches. |
| 129 | func splitFilenameID(name string) (id, slug string) { |
| 130 | // Pattern: Full UUID — 8-4-4-4-12 hex (e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479-slug") |
| 131 | if uid, rest, ok := matchFullUUID(name); ok { |
| 132 | return uid, rest |
| 133 | } |
| 134 | |
| 135 | parts := strings.SplitN(name, "-", 2) |
| 136 | |
| 137 | // Pattern 1: Sequential — starts with digit (e.g. "009-add-feature") |
| 138 | if name[0] >= '0' && name[0] <= '9' { |
| 139 | slug := "" |
| 140 | if len(parts) == 2 { |
| 141 | slug = parts[1] |
| 142 | } |
| 143 | return parts[0], slug |
| 144 | } |
| 145 | |
| 146 | if len(parts) < 2 { |
| 147 | return "", "" |
| 148 | } |
| 149 | |
| 150 | // Pattern 2: Prefixed — alpha prefix + hyphen + digits (e.g. "dr-001-fix-login") |
| 151 | if isAlpha(parts[0]) { |
| 152 | restParts := strings.SplitN(parts[1], "-", 2) |
| 153 | if isNumeric(restParts[0]) { |
| 154 | slug := "" |
| 155 | if len(restParts) == 2 { |
| 156 | slug = restParts[1] |
| 157 | } |
| 158 | return parts[0] + "-" + restParts[0], slug |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | // Pattern 3: Random — 3-8 lowercase alphanumeric with at least one digit |
| 163 | if isAlphanumericID(parts[0]) { |
| 164 | return parts[0], parts[1] |
| 165 | } |
| 166 | |
| 167 | // Pattern 4: Hex ID — 9-32 hex chars (truncated UUID longer than random range) |
| 168 | if isHexID(parts[0]) { |
| 169 | return parts[0], parts[1] |
| 170 | } |
| 171 | |
| 172 | return "", "" |
| 173 | } |
| 174 | |
| 175 | // isNumeric returns true if s is non-empty and all characters are digits. |
| 176 | func isNumeric(s string) bool { |