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 }