Files
spsays-bff/pkg/shorturl/parse.go
T
2026-05-11 16:13:22 +05:30

190 lines
3.9 KiB
Go

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]
}