feat: add url shortner
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user