Files
sm-seo-plugin/classes/urls.go

138 lines
3.3 KiB
Go

package classes
import (
"net/url"
"regexp"
"sort"
"strings"
"sync"
"git.golem15.com/golem15/sm-seo-plugin/models"
)
// globCache keeps compiled glob patterns.
var globCache sync.Map
// globRegexp compiles a pattern where * matches any text and everything
// else is literal, anchored at both ends.
func globRegexp(pattern string) *regexp.Regexp {
if re, ok := globCache.Load(pattern); ok {
return re.(*regexp.Regexp)
}
parts := strings.Split(pattern, "*")
for i, p := range parts {
parts[i] = regexp.QuoteMeta(p)
}
re := regexp.MustCompile(`^` + strings.Join(parts, `.*`) + `$`)
globCache.Store(pattern, re)
return re
}
// MatchGlob reports whether s matches pattern (* matches any text).
func MatchGlob(pattern, s string) bool {
if !strings.Contains(pattern, "*") {
return pattern == s
}
return globRegexp(pattern).MatchString(s)
}
// MatchPathList reports whether path matches one pattern of a list given
// one per line (empty lines ignored).
func MatchPathList(list, path string) bool {
for _, line := range strings.Split(list, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if MatchGlob(line, path) {
return true
}
}
return false
}
// paramKind returns the kind of query parameter name on path from the D-07
// list: a row limited to a path that matches wins over a site-wide row; a
// parameter the list does not name is "" (dropped from the canonical,
// without noindex).
func paramKind(params []models.QueryParam, name, path string) string {
kind, scoped := "", false
for _, p := range params {
pn := strings.TrimSpace(p.Name)
if pn == "" {
continue
}
matches := pn == name || (strings.HasSuffix(pn, "*") && strings.HasPrefix(name, strings.TrimSuffix(pn, "*")))
if !matches {
continue
}
if pp := strings.TrimSpace(p.Path); pp != "" {
if MatchGlob(pp, path) {
kind, scoped = p.Kind, true
}
continue
}
if !scoped {
kind = p.Kind
}
}
return kind
}
// splitQuery applies the D-07 list to a page's query: kept holds the
// content parameters (sorted, for a stable canonical) and filtered is true
// when a filter or sort parameter is present.
func splitQuery(params []models.QueryParam, path string, query url.Values) (kept url.Values, filtered bool) {
kept = url.Values{}
for name, values := range query {
switch paramKind(params, name, path) {
case models.ParamContent:
kept[name] = values
case models.ParamFilter:
filtered = true
}
}
return kept, filtered
}
// encodeQuery encodes kept parameters in name order; "" when none.
func encodeQuery(q url.Values) string {
if len(q) == 0 {
return ""
}
names := make([]string, 0, len(q))
for name := range q {
names = append(names, name)
}
sort.Strings(names)
var b strings.Builder
for _, name := range names {
for _, v := range q[name] {
if b.Len() > 0 {
b.WriteByte('&')
}
b.WriteString(url.QueryEscape(name))
b.WriteByte('=')
b.WriteString(url.QueryEscape(v))
}
}
return b.String()
}
// AbsoluteURL makes a canonical override absolute: an http(s) URL stays
// as it is (it may point to another domain), a path is joined to the site
// URL.
func AbsoluteURL(site, ref string) string {
ref = strings.TrimSpace(ref)
if ref == "" {
return ""
}
if u, err := url.Parse(ref); err == nil && u.IsAbs() {
return ref
}
if !strings.HasPrefix(ref, "/") {
ref = "/" + ref
}
return site + ref
}