package shorturl import ( "net/url" "strings" ) // Parsed contains analytics-friendly components extracted from a long URL. // Unrecognized paths produce ContentType = "unknown" with the other fields // left empty. type Parsed struct { Path string ContentType string BookCode string Division string Chapter string Verse string Slug string HasHighlight bool HighlightParam string } // ParsePath decomposes a long URL (path + query) into analytics columns. // It expects a same-site relative path; callers should run ValidatePath first. func ParsePath(longURL string) Parsed { u, err := url.Parse("http://x" + longURL) if err != nil { return Parsed{Path: longURL, ContentType: "unknown"} } hl := u.Query().Get("hl") out := Parsed{ Path: u.Path, HasHighlight: hl != "", HighlightParam: truncate(hl, 1024), ContentType: "unknown", } parts := splitPath(u.Path) if len(parts) == 0 { return out } switch parts[0] { case "books": out = parseBooks(parts, out) case "letters": if len(parts) >= 2 { out.ContentType = "letter" out.Slug = parts[1] } case "transcriptions": if len(parts) >= 2 { out.ContentType = "transcription" out.Slug = parts[1] } case "essays": if len(parts) >= 2 { out.ContentType = "essay" out.Slug = parts[1] } case "journals": if len(parts) >= 2 { out.ContentType = "journal" out.Slug = parts[1] } if len(parts) >= 3 { out.Chapter = parts[2] } case "magazines": if len(parts) >= 2 { out.ContentType = "magazine" out.Slug = parts[1] } if len(parts) >= 3 { out.Chapter = parts[2] } case "other-documents": if len(parts) >= 2 { out.ContentType = "other_document" out.Slug = parts[1] } if len(parts) >= 3 { out.Chapter = parts[2] } case "glossary": out.ContentType = "glossary" if len(parts) >= 2 { out.Slug = parts[1] } } return out } // verseBookCodes are the books whose URL pattern has a per-verse leaf. var verseBookCodes = map[string]bool{ "bg": true, "sb1962": true, "iso": true, "noi": true, "bs": true, "mm": true, "nbs": true, } // divisionVerseBookCodes have an extra division segment (canto for SB, līlā for CC). var divisionVerseBookCodes = map[string]bool{ "sb": true, "cc": true, } func parseBooks(parts []string, out Parsed) Parsed { if len(parts) < 2 { out.ContentType = "book" return out } code := parts[1] out.BookCode = code if divisionVerseBookCodes[code] { // /books/sb/{canto}/{chapter}/{verse} // /books/cc/{lila}/{chapter}/{verse} if len(parts) >= 5 { out.ContentType = "verse" out.Division = parts[2] out.Chapter = parts[3] out.Verse = parts[4] } else if len(parts) >= 4 { out.ContentType = "verse_index" out.Division = parts[2] out.Chapter = parts[3] } else { out.ContentType = "book" } return out } if verseBookCodes[code] { // /books/{code}/{chapter}/{verse} (BG, SB1962, BS, MM, NBS) // /books/{code}/{slug} (ISO, NOI — single-segment leaf) if len(parts) >= 4 { out.ContentType = "verse" out.Chapter = parts[2] out.Verse = parts[3] } else if len(parts) >= 3 { out.ContentType = "verse" out.Slug = parts[2] } else { out.ContentType = "book" } return out } // All other book codes are prose. KB has /kb/{volume}/{slug}; others /code/{slug}. if code == "kb" && len(parts) >= 4 { out.ContentType = "prose" out.Division = parts[2] // volume number out.Slug = parts[3] return out } if len(parts) >= 3 { out.ContentType = "prose" out.Slug = parts[2] return out } out.ContentType = "book" return out } func splitPath(p string) []string { p = strings.Trim(p, "/") if p == "" { return nil } parts := strings.Split(p, "/") for i, s := range parts { if decoded, err := url.PathUnescape(s); err == nil { parts[i] = decoded } } return parts } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] }