138 lines
3.3 KiB
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
|
|
}
|