190 lines
3.9 KiB
Go
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]
|
|
}
|