Files
RenewCMS/internal/domain/article/articleDomain.go
T

141 lines
3.9 KiB
Go

package article
import (
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/slug"
"errors"
"net/url"
"slices"
"strconv"
"time"
"unicode/utf8"
)
var (
ErrInvalidTitle = errors.New("article's title length should be between 3 and 200 caracters")
ErrArticleNotFound = errors.New("article not found")
ErrArticleOffline = errors.New("article is offline")
ErrInvalidSlug = errors.New("invalid slug")
ErrSlugTaken = errors.New("another article on this site already uses that slug")
ErrInvalidCanonicalURL = errors.New("invalid canonical url")
)
type SlugTakenError struct {
Suggestion string
}
func (e *SlugTakenError) Error() string {
return ErrSlugTaken.Error()
}
func (e *SlugTakenError) Is(target error) bool {
return target == ErrSlugTaken
}
const (
MinTitleRunes = 3
MaxTitleRunes = 200
MaxCanonicalURLRunes = 2048
FallbackSlug = "article"
DeletedSlug = "#deleted-"
DismissTitleLong = "title_long"
DismissTitleEmpty = "title_empty"
DismissDescShort = "desc_short"
DismissDescLong = "desc_long"
DismissCoverAlt = "cover_alt"
DismissCoverNone = "cover_none"
DismissSlugLong = "slug_long"
DismissNoAuthor = "no_au"
DismissNoHeading = "no_h"
DismissBodyAlt = "body_alt"
DismissShort = "short"
)
var dismissCodes = []string{
DismissTitleLong, DismissTitleEmpty, DismissDescShort, DismissDescLong, DismissCoverAlt, DismissCoverNone,
DismissSlugLong, DismissNoAuthor, DismissNoHeading, DismissBodyAlt, DismissShort,
}
func DismissCodes() []string {
return slices.Clone(dismissCodes)
}
func NormalizeDismissed(codes []string) []string {
normalized := make([]string, 0, len(codes))
for _, code := range codes {
if !slices.Contains(normalized, code) {
normalized = append(normalized, code)
}
}
return normalized
}
func IsValidSlug(candidate string) bool {
return slug.IsValid(candidate)
}
func Tombstone(id uint32) string {
return DeletedSlug + strconv.FormatUint(uint64(id), 10)
}
func GenerateSlug(title string) string {
return slug.Generate(title, FallbackSlug)
}
func SlugWithSuffix(base string, attempt int) string {
return slug.WithSuffix(base, attempt)
}
func ValidateCanonicalURL(raw string) error {
if raw == "" {
return nil
}
if utf8.RuneCountInString(raw) > MaxCanonicalURLRunes {
return ErrInvalidCanonicalURL
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" {
return ErrInvalidCanonicalURL
}
return nil
}
func ValidateTitle(title string) error {
runeCount := utf8.RuneCountInString(title)
if runeCount < MinTitleRunes || runeCount > MaxTitleRunes {
return ErrInvalidTitle
}
return nil
}
type Article struct {
ID uint32 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
IsOnline bool `json:"is_online"`
SiteID uint32 `json:"site_id"`
Authors []author.Byline `json:"authors"`
Slug string `json:"slug"`
Excerpt string `json:"excerpt"`
SEOTitle string `json:"seo_title"`
SEODescription string `json:"seo_description"`
CoverImageID *uint32 `json:"cover_image_id"`
CoverImageAlt string `json:"cover_image_alt"`
SocialTitle string `json:"social_title"`
SocialDescription string `json:"social_description"`
SocialImageID *uint32 `json:"social_image_id"`
CanonicalURL string `json:"canonical_url"`
Noindex bool `json:"noindex"`
SEODismissed []string `json:"seo_dismissed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (a Article) UsesImage(imageID uint32) bool {
return a.CoverImageID != nil && *a.CoverImageID == imageID || a.SocialImageID != nil && *a.SocialImageID == imageID
}