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

196 lines
5.5 KiB
Go

package classes
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
// ErrRobotsValue is the refusal of a malformed robots value.
var ErrRobotsValue = errors.New("golem15.seo: malformed robots value")
// Robots is a parsed robots value (UI-SPEC C5, D-08): comma-separated
// directives in the fixed order index|noindex, follow|nofollow, noarchive,
// nosnippet, noimageindex, max-snippet:N, max-image-preview:none|standard|
// large, max-video-preview:N, unavailable_after:YYYY-MM-DD. An empty string
// inherits everything; an unset directive inherits too.
type Robots struct {
Index string // "index", "noindex" or "" (inherit)
Follow string // "follow", "nofollow" or "" (inherit)
NoArchive bool
NoSnippet bool
NoImageIndex bool
MaxSnippet *int
MaxImagePreview string
MaxVideoPreview *int
UnavailableAfter string
}
var robotsDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
// ParseRobots reads a robots value; spaces around directives and their
// case are ignored. A directive outside the grammar, a repeated directive,
// two contradicting directives or a malformed number or date is refused.
func ParseRobots(value string) (Robots, error) {
var r Robots
value = strings.TrimSpace(value)
if value == "" {
return r, nil
}
seen := map[string]bool{}
for _, part := range strings.Split(value, ",") {
d := strings.ToLower(strings.TrimSpace(part))
if d == "" {
return Robots{}, fmt.Errorf("%w: empty directive", ErrRobotsValue)
}
name, arg, hasArg := strings.Cut(d, ":")
name, arg = strings.TrimSpace(name), strings.TrimSpace(arg)
group := name
switch name {
case "index", "noindex":
group = "index"
case "follow", "nofollow":
group = "follow"
}
if seen[group] {
return Robots{}, fmt.Errorf("%w: %s given twice", ErrRobotsValue, group)
}
seen[group] = true
switch name {
case "index", "noindex":
if hasArg {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
r.Index = name
case "follow", "nofollow":
if hasArg {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
r.Follow = name
case "noarchive", "nosnippet", "noimageindex":
if hasArg {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
switch name {
case "noarchive":
r.NoArchive = true
case "nosnippet":
r.NoSnippet = true
default:
r.NoImageIndex = true
}
case "max-snippet", "max-video-preview":
n, err := strconv.Atoi(arg)
if !hasArg || err != nil || n < -1 {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
if name == "max-snippet" {
r.MaxSnippet = &n
} else {
r.MaxVideoPreview = &n
}
case "max-image-preview":
if arg != "none" && arg != "standard" && arg != "large" {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
r.MaxImagePreview = arg
case "unavailable_after":
if !robotsDate.MatchString(arg) {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
if _, err := time.Parse("2006-01-02", arg); err != nil {
return Robots{}, fmt.Errorf("%w: %s", ErrRobotsValue, d)
}
r.UnavailableAfter = arg
default:
return Robots{}, fmt.Errorf("%w: unknown directive %s", ErrRobotsValue, name)
}
}
return r, nil
}
// directives lists the set directives in the fixed order.
func (r Robots) directives() []string {
var out []string
if r.Index != "" {
out = append(out, r.Index)
}
if r.Follow != "" {
out = append(out, r.Follow)
}
if r.NoArchive {
out = append(out, "noarchive")
}
if r.NoSnippet {
out = append(out, "nosnippet")
}
if r.NoImageIndex {
out = append(out, "noimageindex")
}
if r.MaxSnippet != nil {
out = append(out, "max-snippet:"+strconv.Itoa(*r.MaxSnippet))
}
if r.MaxImagePreview != "" {
out = append(out, "max-image-preview:"+r.MaxImagePreview)
}
if r.MaxVideoPreview != nil {
out = append(out, "max-video-preview:"+strconv.Itoa(*r.MaxVideoPreview))
}
if r.UnavailableAfter != "" {
out = append(out, "unavailable_after:"+r.UnavailableAfter)
}
return out
}
// String serialises the stored form: directives in the fixed order joined
// by ",". The empty value inherits everything.
func (r Robots) String() string { return strings.Join(r.directives(), ",") }
// Header serialises the value for a meta robots tag and the X-Robots-Tag
// header: the same directives joined by ", ".
func (r Robots) Header() string { return strings.Join(r.directives(), ", ") }
// Empty reports whether no directive is set.
func (r Robots) Empty() bool { return len(r.directives()) == 0 }
// Inherit fills every directive r leaves unset from lower (the next layer
// of the precedence chain); a directive r sets wins.
func (r Robots) Inherit(lower Robots) Robots {
out := r
if out.Index == "" {
out.Index = lower.Index
}
if out.Follow == "" {
out.Follow = lower.Follow
}
out.NoArchive = out.NoArchive || lower.NoArchive
out.NoSnippet = out.NoSnippet || lower.NoSnippet
out.NoImageIndex = out.NoImageIndex || lower.NoImageIndex
if out.MaxSnippet == nil {
out.MaxSnippet = lower.MaxSnippet
}
if out.MaxImagePreview == "" {
out.MaxImagePreview = lower.MaxImagePreview
}
if out.MaxVideoPreview == nil {
out.MaxVideoPreview = lower.MaxVideoPreview
}
if out.UnavailableAfter == "" {
out.UnavailableAfter = lower.UnavailableAfter
}
return out
}
// NormalizeRobots parses and re-serialises a robots value in the fixed
// order; a malformed value is an error.
func NormalizeRobots(value string) (string, error) {
r, err := ParseRobots(value)
if err != nil {
return "", err
}
return r.String(), nil
}