51 Commits

Author SHA1 Message Date
3172a4865a Create needed functions for common attributes and their extensions 2024-10-17 20:48:32 +02:00
3b3f1f7e41 Change title 2024-10-17 20:47:37 +02:00
9c38048bd2 Properly use xml names 2024-10-17 20:10:18 +02:00
26e0c99150 Use *XHTMLDiv 2024-10-17 19:44:27 +02:00
6117876a59 Check uris before applying them 2024-10-17 19:28:09 +02:00
cb61d90cae Make NewLogo more flexible 2024-10-17 19:19:22 +02:00
30623fdfe0 More ",chardata" 2024-10-17 19:14:37 +02:00
4ae8cecb17 More ",chardata" 2024-10-17 19:11:33 +02:00
8e226b48ef isValidXML is no longer of any use 2024-10-17 19:05:23 +02:00
478d679985 Small bug fix 2024-10-17 18:59:24 +02:00
d11b229691 No more undefined content in link and category
It seems that undefined content is only mentioned in RFC4287 because
link and category are elements and those usually need some content.
2024-10-17 18:57:43 +02:00
e73b78ef30 Content can also be empty 2024-10-17 18:40:52 +02:00
5a82f1799f Force undefined content of category and link to be empty or valid xml 2024-10-17 18:39:57 +02:00
73624eadd8 More ",chardata" 2024-10-17 18:15:41 +02:00
17aa4b1a15 Streamline type check in content constructs 2024-10-17 18:12:56 +02:00
040d7a6b7b Create xhtmlDiv.go 2024-10-17 18:12:23 +02:00
705b651f08 Make undefined content in category and link type string 2024-10-17 18:11:06 +02:00
4b97bf7fdc Simplify inline text content 2024-10-17 17:33:33 +02:00
86785be588 More ",chardata" 2024-10-17 17:19:43 +02:00
28f5616f76 Make uri in id and text in plain text xml type ",chardata" 2024-10-17 17:14:10 +02:00
657624cbd6 Small bug fix 2024-10-17 16:56:46 +02:00
b496ac3691 Allow the user to generate a valid iri based on a uuid 2024-10-17 16:52:03 +02:00
13e7a16178 Let the user define their own iri 2024-10-17 16:51:40 +02:00
3734a3eb3d More complete check within isValidMediaType 2024-10-17 16:46:40 +02:00
e0902d9ba4 Fix source check in entry 2024-10-17 05:34:32 +02:00
434783165b Add needed checks for link if attributes are empty 2024-10-17 05:18:02 +02:00
46138d302b Fix bug in link check 2024-10-17 05:13:17 +02:00
6b9d5be734 Act as if there is an author in the feed when only checking the entry 2024-10-17 05:05:14 +02:00
2ef8d8c9df Fix nil dereference bug in entry checkAuthors 2024-10-17 04:55:28 +02:00
97fe20f364 Add missing methods to entry 2024-10-16 21:35:24 +02:00
0c24f80d09 Rename package to atom 2024-10-16 21:28:04 +02:00
c5a6e0c44f Change module description in go.mod 2024-10-16 21:08:37 +02:00
3774f41f1b Changed README 2024-10-16 20:35:44 +02:00
7a2e79d205 Corrected LICENSE 2024-10-16 20:30:02 +02:00
e12cd132d9 Small comment clean up 2024-10-16 20:24:43 +02:00
03ec0d2ddf Remove unused stuff 2024-10-16 20:23:52 +02:00
4220a2acc6 Added and corrected comments 2024-10-16 19:59:28 +02:00
6a76bdc0c3 Small changes 2024-10-16 18:41:42 +02:00
31cc803440 Create NewLogo and add checks 2024-10-16 18:33:23 +02:00
f11dee630d Make isText private 2024-10-16 18:31:24 +02:00
f8c36a7045 Change title to string type and add checks 2024-10-16 18:31:13 +02:00
4fe133a394 Create isValidMediaType and isValidLanguageTag 2024-10-16 18:30:22 +02:00
39bd0776c5 Correct AddExtension for entry and person 2024-10-16 17:38:03 +02:00
082e71a698 Change URI to IRI after finding validation pattern 2024-10-16 17:33:25 +02:00
b76e529ca3 Created NewLink 2024-10-16 17:17:41 +02:00
bc9fd49d18 Add checks to id 2024-10-16 17:07:57 +02:00
a65aa0a740 Create NewIcon and appropriate checks 2024-10-16 17:03:23 +02:00
566227773e Create NewGenerator and checks for the generator construct 2024-10-16 16:58:58 +02:00
92d71fdbde Fix wordings 2024-10-16 16:54:26 +02:00
f27116930a Correctly escape strings if needed and check for it 2024-10-16 16:51:39 +02:00
c200d5bf73 Create isValidURN and isValidURI and use isValidURI everywhere where it is needed 2024-10-16 16:48:44 +02:00
28 changed files with 573 additions and 238 deletions

View File

@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
atom-feed atom
Copyright (C) 2024 jason Copyright (C) 2024 Jason Streifling
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
atom-feed Copyright (C) 2024 jason atom Copyright (C) 2024 Jason Streifling
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View File

@ -1,3 +1,3 @@
# atom-feed # atom
An extensible implementation of an Atom feed. An extensible implementation of an Atom feed that aims to be very close to RFC4287.

63
atom.go
View File

@ -1,23 +1,45 @@
package atomfeed package atom
import ( import (
"fmt"
"mime" "mime"
"net/url" "regexp"
"strings" "strings"
"github.com/google/uuid"
"golang.org/x/text/language"
) )
type ( type (
EmailAddress string EmailAddress string
LanguageTag string LanguageTag string
MediaType string MediaType string
URI string IRI string
) )
func isValidURL(testURL URI) bool { // isValidIRI checks whether an IRI is valid or not. It returns a bool.
_, err := url.ParseRequestURI(string(testURL)) // https://www.w3.org/2011/04/XMLSchema/TypeLibrary-IRI-RFC3987.xsd
return err == nil func isValidIRI(iri IRI) bool {
pattern := `((([A-Za-z])[A-Za-z0-9+\-\.]*):((//(((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽!$&'()*+,;=:]|(%[0-9A-Fa-f][0-9A-Fa-f]))*@))?((\[((((([0-9A-Fa-f]{0,4}:)){6}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|(::(([0-9A-Fa-f]{0,4}:)){5}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|(([0-9A-Fa-f]{0,4})?::(([0-9A-Fa-f]{0,4}:)){4}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:))?[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:)){3}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,2}[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:)){2}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,3}[0-9A-Fa-f]{0,4}))?::[0-9A-Fa-f]{0,4}:(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,4}[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,5}[0-9A-Fa-f]{0,4}))?::[0-9A-Fa-f]{0,4})|((((([0-9A-Fa-f]{0,4}:)){0,6}[0-9A-Fa-f]{0,4}))?::))|(v[0-9A-Fa-f]+\.[A-Za-z0-9\-\._~!$&'()*+,;=:]+))\])|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))|(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=]))*)((:[0-9]*))?)((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*)|(/(((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))+((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*))?)|((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))+((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*)|)((\?(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@])|[-󰀀-󿿽􀀀-􏿽/?])*))?((#((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@])|/|\?))*))?)`
return regexp.MustCompile(pattern).MatchString(string(iri))
} }
// isCorrectlyEscaped checks whether a string is correctly escaped as per
// RFC4287. It returns a bool.
func isCorrectlyEscaped(text string) bool {
relevantEntities := []string{"&", "<", ">", """, "'"}
for _, entity := range relevantEntities {
if strings.Contains(text, entity) {
return false
}
}
return true
}
// isCompositeMediaType checks whether a string is a composite media type. It
// returns a bool.
func isCompositeMediaType(mediaType string) bool { func isCompositeMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType) mediaType, _, err := mime.ParseMediaType(mediaType)
if err != nil { if err != nil {
@ -27,6 +49,8 @@ func isCompositeMediaType(mediaType string) bool {
return strings.HasPrefix(mediaType, "multipart/") || strings.HasPrefix(mediaType, "message/") return strings.HasPrefix(mediaType, "multipart/") || strings.HasPrefix(mediaType, "message/")
} }
// isXMLMediaType checks whether a string is an xml media type. It returns a
// bool.
func isXMLMediaType(mediaType string) bool { func isXMLMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType) mediaType, _, err := mime.ParseMediaType(mediaType)
if err != nil { if err != nil {
@ -35,3 +59,30 @@ func isXMLMediaType(mediaType string) bool {
return strings.HasSuffix(mediaType, "/xml") || strings.HasSuffix(mediaType, "+xml") return strings.HasSuffix(mediaType, "/xml") || strings.HasSuffix(mediaType, "+xml")
} }
// isValidMediaType checks whether a string is a valid media type. It returns a
// bool.
func isValidMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType)
if err != nil {
return false
}
typeParts := strings.Split(mediaType, "/")
if len(typeParts) != 2 || typeParts[0] == "" || typeParts[1] == "" {
return false
}
return true
}
// isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool.
func isValidLanguageTag(tag LanguageTag) bool {
_, err := language.Parse(string(tag))
return err == nil
}
// NewURN generates an new valid IRI based on a UUID. It returns an IRI.
func NewURN() IRI {
return IRI(fmt.Sprint("urn:uuid:", uuid.New()))
}

View File

@ -1,39 +1,46 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"html"
) )
type Category struct { type Category struct {
XMLName xml.Name `xml:"category"`
*CommonAttributes *CommonAttributes
Content Content `xml:"content"` // should this even exist in here? Term string `xml:"term,attr"`
Term string `xml:"term,attr"` Scheme IRI `xml:"scheme,attr,omitempty"`
Scheme URI `xml:"scheme,attr,omitempty"` Label string `xml:"label,attr,omitempty"`
Label string `xml:"label,attr,omitempty"`
} }
func NewCategory(term string) (*Category, error) { // NewCategory creates a new Category. It returns a *Category.
content, err := NewContent(InlineText, "", "") func NewCategory(term string) *Category {
if err != nil { return &Category{Term: term}
return nil, fmt.Errorf("error creating content element: %v", err)
}
return &Category{Term: term, Content: content}, nil
} }
// SetLabel sets the label of the Category.
func (c *Category) SetLabel(label string) {
c.Label = html.UnescapeString(label)
}
// Check checks the Category for incompatibilities with RFC4287. It returns an
// error.
func (c *Category) Check() error { func (c *Category) Check() error {
if c.Term == "" { if c.Term == "" {
return errors.New("term attribute of category empty") return errors.New("term attribute of category empty")
} }
if c.Content == nil { if c.Scheme != "" {
return errors.New("no content element of category") if !isValidIRI(c.Scheme) {
} else { return fmt.Errorf("scheme attribute %v of category not correctly formatted", c.Scheme)
if err := c.Content.Check(); err != nil {
return fmt.Errorf("content element of category: %v", err)
} }
} }
if !isCorrectlyEscaped(c.Label) {
return fmt.Errorf("label attribute %v of category not correctly escaped", c.Label)
}
return nil return nil
} }

View File

@ -1,13 +1,31 @@
package atomfeed package atom
import "fmt" import "fmt"
type CommonAttributes struct { type CommonAttributes struct {
Base URI `xml:"base,attr,omitempty"` Base IRI `xml:"base,attr,omitempty"`
Lang LanguageTag `xml:"lang,attr,omitempty"` Lang LanguageTag `xml:"lang,attr,omitempty"`
UndefinedAttributes []*ExtensionAttribute `xml:",any"` UndefinedAttributes []*ExtensionAttribute `xml:",any"`
} }
// NewCommonAttributes creates a new set of CommonAttributes. It returns a
// *CommonAttributes.
func NewCommonAttributes() *CommonAttributes {
return new(CommonAttributes)
}
// AddExtensionAttribute adds the ExtensionAttribute to the CommonAttributes.
func (c *CommonAttributes) AddExtensionAttribute(e *ExtensionAttribute) {
if c.UndefinedAttributes == nil {
c.UndefinedAttributes = make([]*ExtensionAttribute, 1)
c.UndefinedAttributes[0] = e
} else {
c.UndefinedAttributes = append(c.UndefinedAttributes, e)
}
}
// Check checks the CommonAttributes for incompatibilities with RFC4287. It
// returns an error.
func (c *CommonAttributes) Check() error { func (c *CommonAttributes) Check() error {
for i, e := range c.UndefinedAttributes { for i, e := range c.UndefinedAttributes {
if err := e.Check(); err != nil { if err := e.Check(); err != nil {

View File

@ -1,4 +1,4 @@
package atomfeed package atom
import ( import (
"fmt" "fmt"
@ -18,6 +18,7 @@ type Content interface {
Check() error Check() error
} }
// NewContent creates a new Content. It returns a Content and an error.
func NewContent(contentType int, mediaType string, content any) (Content, error) { func NewContent(contentType int, mediaType string, content any) (Content, error) {
switch contentType { switch contentType {
case 0: case 0:

View File

@ -1,4 +1,4 @@
package atomfeed package atom
import ( import (
"errors" "errors"
@ -7,17 +7,22 @@ import (
type Date struct { type Date struct {
*CommonAttributes *CommonAttributes
DateTime string DateTime string `xml:",chardata"`
} }
// DateTime formats a time.Time to string formated as defined by RFC3339. It
// returns a string.
func DateTime(t time.Time) string { func DateTime(t time.Time) string {
return string(t.Format(time.RFC3339)) return string(t.Format(time.RFC3339))
} }
// NewDate creates a new Date. It returns a *Date.
func NewDate(t time.Time) *Date { func NewDate(t time.Time) *Date {
return &Date{DateTime: DateTime(t)} return &Date{DateTime: DateTime(t)}
} }
// Check checks the Date for incompatibilities with RFC4287. It returns an
// error.
func (d *Date) Check() error { func (d *Date) Check() error {
if d.DateTime == "" { if d.DateTime == "" {
return errors.New("date time element of date is empty") return errors.New("date time element of date is empty")

118
entry.go
View File

@ -1,10 +1,11 @@
package atomfeed package atom
import ( import (
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
) )
// It is advisable that each atom:entry element contain a non-empty atom:title // It is advisable that each atom:entry element contain a non-empty atom:title
@ -12,30 +13,38 @@ import (
// a non-empty atom:summary element when the entry contains no atom:content // a non-empty atom:summary element when the entry contains no atom:content
// element. // element.
type Entry struct { type Entry struct {
XMLName xml.Name `xml:"entry"`
*CommonAttributes *CommonAttributes
Authors []*Person `xml:"author,omitempty"` Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"` Categories []*Category `xml:",omitempty"`
Content Content `xml:"content,omitempty"` Content Content `xml:",omitempty"`
Contributors []*Person `xml:"contributors,omitempty"` Contributors []*Person `xml:"contributors,omitempty"`
ID *ID `xml:"id"` ID *ID
Links []*Link `xml:"link,omitempty"` Links []*Link `xml:",omitempty"`
Published *Date `xml:"published,omitempty"` Published *Date `xml:"published,omitempty"`
Rights Text `xml:"rights,omitempty"` Rights Text `xml:"rights,omitempty"`
Source *Source `xml:"source,omitempty"` Source *Source `xml:",omitempty"`
Summary Text `xml:"summary,omitempty"` Summary Text `xml:"summary,omitempty"`
Title Text `xml:"title"` Title Text `xml:"title"`
Updated *Date `xml:"updated"` Updated *Date `xml:"updated"`
Extensions []*ExtensionElement `xml:",any,omitempty"` Extensions []*ExtensionElement `xml:",any,omitempty"`
} }
// checkAuthors checks the entry's authors for incompatibilities with RFC4287.
// It returns an errors.
// atom:entry elements MUST contain one or more atom:author elements, unless // atom:entry elements MUST contain one or more atom:author elements, unless
// the atom:entry contains an atom:source element that contains an atom:author // the atom:entry contains an atom:source element that contains an atom:author
// element or, in an Atom Feed Document, the atom:feed element contains an // element or, in an Atom Feed Document, the atom:feed element contains an
// atom:author element itself. // atom:author element itself.
func (e *Entry) checkAuthors() error { func (e *Entry) checkAuthors(authorInFeed bool) error {
if e.Authors == nil { if e.Authors == nil {
if e.Source.Authors == nil { if !authorInFeed {
return errors.New("no authors set in entry") if e.Source == nil {
return errors.New("no authors set in entry")
}
if e.Source.Authors == nil {
return errors.New("no authors set in entry")
}
} }
} else { } else {
for i, a := range e.Authors { for i, a := range e.Authors {
@ -48,20 +57,87 @@ func (e *Entry) checkAuthors() error {
return nil return nil
} }
func alternateRelExists(l []*Link) bool { // NewEntry creates a new Entry. It returns a *Entry and an error.
for _, link := range l { func NewEntry(title string) (*Entry, error) {
if link.Rel == "alternate" { text, err := NewText("text", title)
return true if err != nil {
} return nil, fmt.Errorf("error creating new entry: %v", err)
} }
return false id, err := NewID(NewURN())
if err != nil {
return nil, fmt.Errorf("error creating new entry: %v", err)
}
return &Entry{
ID: id,
Title: text,
Updated: NewDate(time.Now()),
}, nil
} }
func (e *Entry) AddExtension(name string, value any) { // AddAuthor adds the Person as an author to the Entry.
e.Extensions = append(e.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}) func (e *Entry) AddAuthor(p *Person) {
if e.Authors == nil {
e.Authors = make([]*Person, 1)
e.Authors[0] = p
} else {
e.Authors = append(e.Authors, p)
}
e.Updated.DateTime = DateTime(time.Now())
} }
// AddCategory adds the Category to the Entry.
func (e *Entry) AddCategory(c *Category) {
if e.Categories == nil {
e.Categories = make([]*Category, 1)
e.Categories[0] = c
} else {
e.Categories = append(e.Categories, c)
}
e.Updated.DateTime = DateTime(time.Now())
}
// AddContributor adds the Person as a contributor to the Entry.
func (e *Entry) AddContributor(c *Person) {
if e.Contributors == nil {
e.Contributors = make([]*Person, 1)
e.Contributors[0] = c
} else {
e.Contributors = append(e.Contributors, c)
}
e.Updated.DateTime = DateTime(time.Now())
}
// AddLink adds the Link to the Entry.
func (e *Entry) AddLink(l *Link) {
if e.Links == nil {
e.Links = make([]*Link, 1)
e.Links[0] = l
} else {
e.Links = append(e.Links, l)
}
e.Updated.DateTime = DateTime(time.Now())
}
// AddExtension adds the ExtensionElement to the Entry.
func (e *Entry) AddExtension(x *ExtensionElement) {
if e.Extensions == nil {
e.Extensions = make([]*ExtensionElement, 1)
e.Extensions[0] = x
} else {
e.Extensions = append(e.Extensions, x)
}
e.Updated.DateTime = DateTime(time.Now())
}
// Check checks the Entry for incompatibilities with RFC4287. It returns an
// error.
func (e *Entry) Check() error { func (e *Entry) Check() error {
if e.ID == nil { if e.ID == nil {
return errors.New("no id element of entry") return errors.New("no id element of entry")
@ -71,7 +147,7 @@ func (e *Entry) Check() error {
} }
} }
if err := e.checkAuthors(); err != nil { if err := e.checkAuthors(true); err != nil {
return fmt.Errorf("entry %v: %v", e.ID.URI, err) return fmt.Errorf("entry %v: %v", e.ID.URI, err)
} }
@ -144,7 +220,7 @@ func (e *Entry) Check() error {
// is not an XML media type [RFC3023], does not begin with "text/", and // is not an XML media type [RFC3023], does not begin with "text/", and
// does not end with "/xml" or "+xml". // does not end with "/xml" or "+xml".
mediaType := e.Content.getType() mediaType := e.Content.getType()
if !isXMLMediaType(mediaType) && !strings.HasPrefix(mediaType, "text/") { if isValidMediaType(mediaType) && !isXMLMediaType(mediaType) && !strings.HasPrefix(mediaType, "text/") {
return fmt.Errorf("no summary element of entry %v but media type not xml", e.ID.URI) return fmt.Errorf("no summary element of entry %v but media type not xml", e.ID.URI)
} }
} }

View File

@ -1,17 +1,24 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt"
) )
type ExtensionAttribute struct { type ExtensionAttribute struct {
Value any `xml:",attr"` Attr string `xml:",attr"`
XMLName xml.Name
} }
// NewExtensionAttribute creates a new ExtensionAttribute. It returns a
// *ExtensionAttribute.
func NewExtensionAttribute(name, value string) *ExtensionAttribute {
return &ExtensionAttribute{Attr: fmt.Sprint(name, `="`, value, `"`)}
}
// Check checks the ExtensionAttribute for incompatibilities with RFC4287. It
// returns an error.
func (e *ExtensionAttribute) Check() error { func (e *ExtensionAttribute) Check() error {
if e.Value == nil { if e.Attr == "" {
return errors.New("value element of extension attribute empty") return errors.New("value element of extension attribute empty")
} }

View File

@ -1,4 +1,4 @@
package atomfeed package atom
import ( import (
"encoding/xml" "encoding/xml"
@ -10,10 +10,14 @@ type ExtensionElement struct {
XMLName xml.Name XMLName xml.Name
} }
// NewExtensionElement creates a new ExtensionElement. It returns a
// *ExtensionElement.
func NewExtensionElement(name string, value any) *ExtensionElement { func NewExtensionElement(name string, value any) *ExtensionElement {
return &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value} return &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}
} }
// Check checks the ExtensionElement for incompatibilities with RFC4287. It
// returns an error.
func (e *ExtensionElement) Check() error { func (e *ExtensionElement) Check() error {
if e.Value == nil { if e.Value == nil {
return errors.New("value element of extension element empty") return errors.New("value element of extension element empty")

68
feed.go
View File

@ -1,4 +1,4 @@
package atomfeed package atom
import ( import (
"encoding/xml" "encoding/xml"
@ -10,37 +10,42 @@ import (
type Feed struct { type Feed struct {
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
*CommonAttributes *CommonAttributes
Authors []*Person `xml:"author,omitempty"` Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"` Categories []*Category `xml:",omitempty"`
Contributors []*Person `xml:"contributor,omitempty"` Contributors []*Person `xml:"contributor,omitempty"`
Generator *Generator `xml:"generator,omitempty"` Generator *Generator `xml:",omitempty"`
Icon *Icon `xml:"icon,omitempty"` Icon *Icon `xml:",omitempty"`
ID *ID `xml:"id"` ID *ID
Links []*Link `xml:"link,omitempty"` Links []*Link `xml:",omitempty"`
Logo *Logo `xml:"logo,omitempty"` Logo *Logo `xml:",omitempty"`
Rights Text `xml:"rights,omitempty"` Rights Text `xml:"rights,omitempty"`
Subtitle Text `xml:"subtitle,omitempty"` Subtitle Text `xml:"subtitle,omitempty"`
Title Text `xml:"title"` Title Text `xml:"title"`
Updated *Date `xml:"updated"` Updated *Date `xml:"updated"`
Extensions []*ExtensionElement `xml:",any,omitempty"` Extensions []*ExtensionElement `xml:",any,omitempty"`
Entries []*Entry `xml:"entry,omitempty"` Entries []*Entry `xml:",omitempty"`
} }
// NewFeed creates a new feed. // NewFeed creates a new Feed. It returns a *Feed and an error.
func NewFeed(title string) (*Feed, error) { func NewFeed(title string) (*Feed, error) {
text, err := NewText("text", title) text, err := NewText("text", title)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating new feed: %v", err) return nil, fmt.Errorf("error creating new feed: %v", err)
} }
id, err := NewID(NewURN())
if err != nil {
return nil, fmt.Errorf("error creating new feed: %v", err)
}
return &Feed{ return &Feed{
ID: NewID(), ID: id,
Title: text, Title: text,
Updated: NewDate(time.Now()), Updated: NewDate(time.Now()),
}, nil }, nil
} }
// AddAuthor adds the person as an author to the feed. // AddAuthor adds the Person as an author to the Feed.
func (f *Feed) AddAuthor(p *Person) { func (f *Feed) AddAuthor(p *Person) {
if f.Authors == nil { if f.Authors == nil {
f.Authors = make([]*Person, 1) f.Authors = make([]*Person, 1)
@ -52,7 +57,7 @@ func (f *Feed) AddAuthor(p *Person) {
f.Updated.DateTime = DateTime(time.Now()) f.Updated.DateTime = DateTime(time.Now())
} }
// AddCategory adds the category to the feed. // AddCategory adds the Category to the Feed.
func (f *Feed) AddCategory(c *Category) { func (f *Feed) AddCategory(c *Category) {
if f.Categories == nil { if f.Categories == nil {
f.Categories = make([]*Category, 1) f.Categories = make([]*Category, 1)
@ -64,7 +69,7 @@ func (f *Feed) AddCategory(c *Category) {
f.Updated.DateTime = DateTime(time.Now()) f.Updated.DateTime = DateTime(time.Now())
} }
// AddContributor adds the contributor to the feed. // AddContributor adds the Person as a contributor to the Feed.
func (f *Feed) AddContributor(c *Person) { func (f *Feed) AddContributor(c *Person) {
if f.Contributors == nil { if f.Contributors == nil {
f.Contributors = make([]*Person, 1) f.Contributors = make([]*Person, 1)
@ -76,8 +81,7 @@ func (f *Feed) AddContributor(c *Person) {
f.Updated.DateTime = DateTime(time.Now()) f.Updated.DateTime = DateTime(time.Now())
} }
// AddLink adds the link to the feed. // AddLink adds the Link to the Feed. There should be one Link with Rel "self".
// There should be one link with rel "self".
func (f *Feed) AddLink(l *Link) { func (f *Feed) AddLink(l *Link) {
if f.Links == nil { if f.Links == nil {
f.Links = make([]*Link, 1) f.Links = make([]*Link, 1)
@ -89,7 +93,7 @@ func (f *Feed) AddLink(l *Link) {
f.Updated.DateTime = DateTime(time.Now()) f.Updated.DateTime = DateTime(time.Now())
} }
// AddExtension adds the extension to the feed. // AddExtension adds the Extension to the Feed.
func (f *Feed) AddExtension(e *ExtensionElement) { func (f *Feed) AddExtension(e *ExtensionElement) {
if f.Extensions == nil { if f.Extensions == nil {
f.Extensions = make([]*ExtensionElement, 1) f.Extensions = make([]*ExtensionElement, 1)
@ -101,7 +105,7 @@ func (f *Feed) AddExtension(e *ExtensionElement) {
f.Updated.DateTime = DateTime(time.Now()) f.Updated.DateTime = DateTime(time.Now())
} }
// AddEntry adds the entry to the feed. // AddEntry adds the Entry to the Feed.
func (f *Feed) AddEntry(e *Entry) { func (f *Feed) AddEntry(e *Entry) {
if f.Entries == nil { if f.Entries == nil {
f.Entries = make([]*Entry, 1) f.Entries = make([]*Entry, 1)
@ -113,7 +117,8 @@ func (f *Feed) AddEntry(e *Entry) {
f.Updated.DateTime = DateTime(time.Now()) f.Updated.DateTime = DateTime(time.Now())
} }
// Check checks the feed for incompatibilities with RFC4287. // Check checks the Feed for incompatibilities with RFC4287. It returns an
// error.
func (f *Feed) Check() error { func (f *Feed) Check() error {
if f.ID == nil { if f.ID == nil {
return errors.New("no id element of feed") return errors.New("no id element of feed")
@ -128,7 +133,7 @@ func (f *Feed) Check() error {
// least one atom:author element. // least one atom:author element.
if f.Authors == nil { if f.Authors == nil {
for _, e := range f.Entries { for _, e := range f.Entries {
if err := e.checkAuthors(); err != nil { if err := e.checkAuthors(false); err != nil {
return fmt.Errorf("no authors set in feed %v: %v", f.ID.URI, err) return fmt.Errorf("no authors set in feed %v: %v", f.ID.URI, err)
} }
} }
@ -222,26 +227,7 @@ func (f *Feed) Check() error {
return nil return nil
} }
// TODO: Create complete link or delete // ToXML converts the Feed to XML. It returns a string and an error.
func (f *Feed) Standardize() {
if f.Links == nil {
f.Links = make([]*Link, 1)
f.Links[0] = &Link{Rel: "self"}
} else {
selfExists := false
for _, l := range f.Links {
if l.Rel == "self" {
selfExists = true
break
}
}
if !selfExists {
f.Links = append(f.Links, &Link{Rel: "self"})
}
}
}
// ToXML converts the feed to XML.
func (f *Feed) ToXML(encoding string) (string, error) { func (f *Feed) ToXML(encoding string) (string, error) {
xml, err := xml.MarshalIndent(f, "", " ") xml, err := xml.MarshalIndent(f, "", " ")
if err != nil { if err != nil {

View File

@ -1,18 +1,41 @@
package atomfeed package atom
import "errors" import (
"encoding/xml"
"errors"
"fmt"
"html"
)
type Generator struct { type Generator struct {
XMLName xml.Name `xml:"generator"`
*CommonAttributes *CommonAttributes
URI URI `xml:"uri,attr,omitempty"` URI IRI `xml:"uri,attr,omitempty"`
Version string `xml:"version,attr,omitempty"` Version string `xml:"version,attr,omitempty"`
Text string `xml:"text"` Text string `xml:",chardata"`
} }
// NewGenerator creates a new Generator. It returns a *Generator.
func NewGenerator(text string) *Generator {
return &Generator{Text: html.UnescapeString(text)}
}
// Check checks the Generator for incompatibilities with RFC4287. It returns an
// error.
func (g *Generator) Check() error { func (g *Generator) Check() error {
if g.URI != "" {
if !isValidIRI(g.URI) {
return fmt.Errorf("uri attribute %v of generator not correctly formatted", g.URI)
}
}
if g.Text == "" { if g.Text == "" {
return errors.New("text element of generator empty") return errors.New("text element of generator empty")
} }
if !isCorrectlyEscaped(g.Text) {
return fmt.Errorf("text element %v of generator not correctly escaped", g.Text)
}
return nil return nil
} }

7
go.mod
View File

@ -1,5 +1,8 @@
module streifling.com/jason/atom-feed module git.streifling.com/jason/atom
go 1.23.2 go 1.23.2
require github.com/google/uuid v1.6.0 require (
github.com/google/uuid v1.6.0
golang.org/x/text v0.19.0
)

2
go.sum
View File

@ -1,2 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=

26
icon.go
View File

@ -1,15 +1,35 @@
package atomfeed package atom
import "errors" import (
"encoding/xml"
"errors"
"fmt"
)
type Icon struct { type Icon struct {
XMLName xml.Name `xml:"icon"`
*CommonAttributes *CommonAttributes
URI URI `xml:"uri"` URI IRI `xml:",chardata"`
} }
// NewIcon creates a new Icon. It returns a *Icon and an error.
func NewIcon(uri IRI) (*Icon, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
}
return &Icon{URI: uri}, nil
}
// Check checks the Icon for incompatibilities with RFC4287. It returns an
// error.
func (i *Icon) Check() error { func (i *Icon) Check() error {
if i.URI == "" { if i.URI == "" {
return errors.New("uri element of icon empty") return errors.New("uri element of icon empty")
} else {
if !isValidIRI(i.URI) {
return fmt.Errorf("uri attribute %v of icon not correctly formatted", i.URI)
}
} }
return nil return nil

22
id.go
View File

@ -1,24 +1,34 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
) )
type ID struct { type ID struct {
XMLName xml.Name `xml:"id"`
*CommonAttributes *CommonAttributes
URI URI `xml:"uri"` URI IRI `xml:",chardata"`
} }
func NewID() *ID { // NewID creates a new ID. It returns a *ID and an error.
return &ID{URI: URI(fmt.Sprint("urn:uuid:", uuid.New()))} func NewID(uri IRI) (*ID, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
}
return &ID{URI: IRI(uri)}, nil
} }
// Check checks the ID for incompatibilities with RFC4287. It returns an error.
func (i *ID) Check() error { func (i *ID) Check() error {
if i.URI == "" { if i.URI == "" {
return errors.New("uri element of id empty") return errors.New("uri element of id empty")
} else {
if !isValidIRI(i.URI) {
return fmt.Errorf("uri element %v of id not correctly formatted", i.URI)
}
} }
return nil return nil

View File

@ -1,17 +1,21 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"mime" "mime"
) )
type InlineOtherContent struct { type InlineOtherContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
AnyElement any `xml:"anyelement,omitempty"` AnyElement any `xml:",chardata"`
Type MediaType `xml:"type,attr,omitempty"` Type MediaType `xml:"type,attr,omitempty"`
} }
// newInlineOtherContent creates a new InlineOtherContent. It returns a
// *InlineOtherContent and an error.
func newInlineOtherContent(mediaType string, content any) (*InlineOtherContent, error) { func newInlineOtherContent(mediaType string, content any) (*InlineOtherContent, error) {
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil { if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return nil, fmt.Errorf("media type %v incompatible with inline other content", mediaType) return nil, fmt.Errorf("media type %v incompatible with inline other content", mediaType)
@ -20,12 +24,19 @@ func newInlineOtherContent(mediaType string, content any) (*InlineOtherContent,
return &InlineOtherContent{Type: MediaType(mediaType), AnyElement: content}, nil return &InlineOtherContent{Type: MediaType(mediaType), AnyElement: content}, nil
} }
// isContent checks whether the InlineOtherContent is a Content. It returns a
// bool.
func (i *InlineOtherContent) isContent() bool { return true } func (i *InlineOtherContent) isContent() bool { return true }
// hasSRC checks whether the InlineOtherContent has a SRC attribute. It returns
// a bool.
func (i *InlineOtherContent) hasSRC() bool { return false } func (i *InlineOtherContent) hasSRC() bool { return false }
// getType returns the Type of the InlineOtherContent as a string.
func (i *InlineOtherContent) getType() string { return string(i.Type) } func (i *InlineOtherContent) getType() string { return string(i.Type) }
// Check checks the InlineOtherContent for incompatibilities with RFC4287. It
// returns an error.
func (i *InlineOtherContent) Check() error { func (i *InlineOtherContent) Check() error {
mediaType := i.getType() mediaType := i.getType()

View File

@ -1,47 +1,46 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"reflect"
) )
type InlineTextContent struct { type InlineTextContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
Type string `xml:"type,attr,omitempty"` // Must be text or html Type string `xml:"type,attr,omitempty"` // Must be text or html
Texts []string `xml:"texts,omitempty"` Text string `xml:",chardata"`
} }
// newInlineTextContent creates a new InlineTextContent. It returns a
// *InlineTextContent and an error.
func newInlineTextContent(mediaType string, content any) (*InlineTextContent, error) { func newInlineTextContent(mediaType string, content any) (*InlineTextContent, error) {
if mediaType != "text" && mediaType != "html" && mediaType != "" { if mediaType != "text" && mediaType != "html" && mediaType != "" {
return nil, fmt.Errorf("media type %v incompatible with inline text content", mediaType) return nil, fmt.Errorf("media type %v incompatible with inline text content", mediaType)
} }
texts := make([]string, 0) text, ok := content.(string)
if !ok {
t := reflect.TypeOf(content)
switch t.Kind() {
case reflect.Slice:
if t.Elem().Kind() == reflect.String {
for _, t := range content.([]string) {
texts = append(texts, t)
}
}
case reflect.String:
texts = append(texts, content.(string))
default:
return nil, fmt.Errorf("content type %T incompatible with inline text content", content) return nil, fmt.Errorf("content type %T incompatible with inline text content", content)
} }
return &InlineTextContent{Type: mediaType, Texts: texts}, nil return &InlineTextContent{Type: mediaType, Text: text}, nil
} }
// isContent checks whether the InlineTextContent is a Content. It returns a
// bool.
func (i *InlineTextContent) isContent() bool { return true } func (i *InlineTextContent) isContent() bool { return true }
// hasSRC checks whether the InlineTextContent has a SRC attribute. It returns
// a bool.
func (i *InlineTextContent) hasSRC() bool { return false } func (i *InlineTextContent) hasSRC() bool { return false }
// getType returns the Type of the InlineTextContent as a string.
func (i *InlineTextContent) getType() string { return i.Type } func (i *InlineTextContent) getType() string { return i.Type }
// Check checks the InlineTextContent for incompatibilities with RFC4287. It
// returns an error.
func (i *InlineTextContent) Check() error { func (i *InlineTextContent) Check() error {
if i.Type != "" && i.Type != "text" && i.Type != "html" { if i.Type != "" && i.Type != "text" && i.Type != "html" {
return errors.New("type attribute of inline text content must be text or html if not omitted") return errors.New("type attribute of inline text content must be text or html if not omitted")

View File

@ -1,42 +1,53 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"reflect"
) )
type InlineXHTMLContent struct { type InlineXHTMLContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
XHTMLDiv *XHTMLDiv
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
XHTMLDiv string `xml:"xhtmldiv"`
} }
// newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a
// *InlineXHTMLContent and an error.
func newInlineXHTMLContent(mediaType string, content any) (*InlineXHTMLContent, error) { func newInlineXHTMLContent(mediaType string, content any) (*InlineXHTMLContent, error) {
if mediaType != "xhtml" { if mediaType != "xhtml" {
return nil, fmt.Errorf("media type %v incompatible with inline xhtml content", mediaType) return nil, fmt.Errorf("media type %v incompatible with inline xhtml content", mediaType)
} }
if reflect.TypeOf(content).Kind() != reflect.String { xhtmlDiv, ok := content.(*XHTMLDiv)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with inline xhtml content", content) return nil, fmt.Errorf("content type %T incompatible with inline xhtml content", content)
} }
return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: content.(string)}, nil return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: xhtmlDiv}, nil
} }
// isContent checks whether the InlineXHTMLContent is a Content. It returns a
// bool.
func (i *InlineXHTMLContent) isContent() bool { return true } func (i *InlineXHTMLContent) isContent() bool { return true }
// hasSRC checks whether the InlineXHTMLContent has a SRC attribute. It returns
// a bool.
func (i *InlineXHTMLContent) hasSRC() bool { return false } func (i *InlineXHTMLContent) hasSRC() bool { return false }
// getType returns the Type of the InlineXHTMLContent as a string.
func (i *InlineXHTMLContent) getType() string { return i.Type } func (i *InlineXHTMLContent) getType() string { return i.Type }
// Check checks the InlineXHTMLContent for incompatibilities with RFC4287. It
// returns an error.
func (i *InlineXHTMLContent) Check() error { func (i *InlineXHTMLContent) Check() error {
if i.Type != "xhtml" { if i.Type != "xhtml" {
return errors.New("type attribute of inline xhtml content must be xhtml") return errors.New("type attribute of inline xhtml content must be xhtml")
} }
if i.XHTMLDiv == "" { if err := i.XHTMLDiv.Check(); err != nil {
return errors.New("xhtmlDiv element of inline xhtml content empty") return fmt.Errorf("xhtml div element %v of inline xhtml content %v: %v", i.XHTMLDiv, i, err)
} }
return nil return nil

59
link.go
View File

@ -1,43 +1,62 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"strings"
) )
type Link struct { type Link struct {
XMLName xml.Name `xml:"link"`
*CommonAttributes *CommonAttributes
Title *Text `xml:"title,attr,omitempty"` Title string `xml:"title,attr,omitempty"`
Content *Content `xml:"content"` Href IRI `xml:"href,attr"`
Href URI `xml:"href,attr"`
Rel string `xml:"rel,attr,omitempty"` Rel string `xml:"rel,attr,omitempty"`
Type MediaType `xml:"type,attr,omitempty"` Type MediaType `xml:"type,attr,omitempty"`
HrefLang LanguageTag `xml:"hreflang,attr,omitempty"` HrefLang LanguageTag `xml:"hreflang,attr,omitempty"`
Length uint `xml:"length,attr,omitempty"` Length uint `xml:"length,attr,omitempty"`
} }
// NewLink creates a new Link. It returns a *Link.
func NewLink(href string) *Link {
return &Link{Href: IRI(href)}
}
// Check checks the Link for incompatibilities with RFC4287. It returns an
// error.
func (l *Link) Check() error { func (l *Link) Check() error {
if l.Href == "" { if l.Href == "" {
return errors.New("href attribute of link empty") return errors.New("href attribute of link empty")
} } else {
if !isValidIRI(l.Href) {
if l.Title != nil { return fmt.Errorf("href attribute %v of link not correctly formatted", l.Href)
if err := (*l.Title).Check(); err != nil {
return fmt.Errorf("title attribute of link %v: %v", l.Href, err)
} }
} }
if l.Content == nil { if l.Rel != "" {
return fmt.Errorf("no content element of link %v", l.Href) if strings.Contains(l.Rel, ":") && !isValidIRI(IRI(l.Rel)) {
} else { return fmt.Errorf("rel attribute %v of link %v not correctly formatted", l.Rel, l.Href)
if err := (*l.Content).Check(); err != nil { }
return fmt.Errorf("content element of link %v: %v", l.Href, err) }
if l.Type != "" {
if !isValidMediaType(string(l.Type)) {
return fmt.Errorf("type attribute %v of link %v invalid media type", l.Type, l.Href)
}
}
if l.HrefLang != "" {
if !isValidLanguageTag(l.HrefLang) {
return fmt.Errorf("hreflang attribute %v of link %v invalid language tag", l.Type, l.HrefLang)
} }
} }
return nil return nil
} }
// hasAlternateDuplicateLinks checks whether multiple Links with Rel
// "alternate" also have Type and HrefLang in common. It returns a bool.
// atom:feed/entry elements MUST NOT contain more than one atom:link element // atom:feed/entry elements MUST NOT contain more than one atom:link element
// with a rel attribute value of "alternate" that has the same combination of // with a rel attribute value of "alternate" that has the same combination of
// type and hreflang attribute values. // type and hreflang attribute values.
@ -56,3 +75,15 @@ func hasAlternateDuplicateLinks(l []*Link) bool {
return false return false
} }
// alternateRelExists checks whether multiple Links with Rel "alternate" exist.
// It returns a bool.
func alternateRelExists(l []*Link) bool {
for _, link := range l {
if link.Rel == "alternate" {
return true
}
}
return false
}

26
logo.go
View File

@ -1,15 +1,35 @@
package atomfeed package atom
import "errors" import (
"encoding/xml"
"errors"
"fmt"
)
type Logo struct { type Logo struct {
XMLName xml.Name `xml:"logo"`
*CommonAttributes *CommonAttributes
URI URI `xml:"uri"` URI IRI `xml:",chardata"`
} }
// NewLogo creates a new Logo. It returns a *Logo.
func NewLogo(uri IRI) (*Logo, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
}
return &Logo{URI: uri}, nil
}
// Check checks the Logo for incompatibilities with RFC4287. It returns an
// error.
func (l *Logo) Check() error { func (l *Logo) Check() error {
if l.URI == "" { if l.URI == "" {
return errors.New("uri element of logo empty") return errors.New("uri element of logo empty")
} else {
if !isValidIRI(l.URI) {
return fmt.Errorf("uri element %v of logo not correctly formatted", l.URI)
}
} }
return nil return nil

View File

@ -1,40 +1,51 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"mime" "mime"
"reflect"
) )
type OutOfLineContent struct { type OutOfLineContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
Type MediaType `xml:"type,attr,omitempty"` Type MediaType `xml:"type,attr,omitempty"`
SRC URI `xml:"src,attr"` SRC IRI `xml:"src,attr"`
} }
// newOutOfLineContent creates a new OutOfLineContent. It returns a
// *OutOfLineContent and an error.
func newOutOfLineContent(mediaType string, content any) (*OutOfLineContent, error) { func newOutOfLineContent(mediaType string, content any) (*OutOfLineContent, error) {
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil { if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return nil, fmt.Errorf("media type %v incompatible with out of line content", mediaType) return nil, fmt.Errorf("media type %v incompatible with out of line content", mediaType)
} }
if reflect.TypeOf(content).Kind() != reflect.String { iri, ok := content.(IRI)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with out of line content", content) return nil, fmt.Errorf("content type %T incompatible with out of line content", content)
} }
if !isValidURL(content.(URI)) { if !isValidIRI(iri) {
return nil, errors.New("content not a valid uri") return nil, errors.New("content not a valid uri")
} }
return &OutOfLineContent{Type: MediaType(mediaType), SRC: content.(URI)}, nil return &OutOfLineContent{Type: MediaType(mediaType), SRC: iri}, nil
} }
// isContent checks whether the OutOfLineContent is a Content. It returns a
// bool.
func (o *OutOfLineContent) isContent() bool { return true } func (o *OutOfLineContent) isContent() bool { return true }
// hasSRC checks whether the OutOfLineContent has a SRC attribute. It returns a
// bool.
func (o *OutOfLineContent) hasSRC() bool { return true } func (o *OutOfLineContent) hasSRC() bool { return true }
// getType returns the Type of the OutOfLineContent as a string.
func (o *OutOfLineContent) getType() string { return string(o.Type) } func (o *OutOfLineContent) getType() string { return string(o.Type) }
// Check checks the OutOfLineContent for incompatibilities with RFC4287. It
// returns an error.
func (o *OutOfLineContent) Check() error { func (o *OutOfLineContent) Check() error {
mediaType := o.getType() mediaType := o.getType()

View File

@ -1,7 +1,6 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"net/mail" "net/mail"
@ -10,26 +9,35 @@ import (
type Person struct { type Person struct {
*CommonAttributes *CommonAttributes
Name string `xml:"name"` Name string `xml:"name"`
URI URI `xml:"uri,omitempty"` URI IRI `xml:"uri,omitempty"`
Email EmailAddress `xml:"email,omitempty"` Email EmailAddress `xml:"email,omitempty"`
Extensions []*ExtensionElement `xml:",any,omitempty"` Extensions []*ExtensionElement `xml:",any,omitempty"`
} }
// NewPerson creates a new Person. It returns a *Person.
func NewPerson(name string) *Person { func NewPerson(name string) *Person {
return &Person{Name: name} return &Person{Name: name}
} }
func (p *Person) AddExtension(name string, value any) { // AddExtension adds the Extension to the Person.
p.Extensions = append(p.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}) func (p *Person) AddExtension(e *ExtensionElement) {
if p.Extensions == nil {
p.Extensions = make([]*ExtensionElement, 1)
p.Extensions[0] = e
} else {
p.Extensions = append(p.Extensions, e)
}
} }
// Check checks the Person for incompatibilities with RFC4287. It returns an
// error.
func (p *Person) Check() error { func (p *Person) Check() error {
if p.Name == "" { if p.Name == "" {
return errors.New("name element of person element empty") return errors.New("name element of person element empty")
} }
if p.URI != "" { if p.URI != "" {
if !isValidURL(p.URI) { if !isValidIRI(p.URI) {
return fmt.Errorf("uri element of person %v not correctly formatted", p.Name) return fmt.Errorf("uri element of person %v not correctly formatted", p.Name)
} }
} }

View File

@ -1,20 +1,27 @@
package atomfeed package atom
import "errors" import "errors"
type PlainText struct { type PlainText struct {
*CommonAttributes *CommonAttributes
Type string `xml:"type,attr,omitempty"` // Must be text or html Type string `xml:"type,attr,omitempty"` // Must be text or html
Text string `xml:"text"` Text string `xml:",chardata"`
} }
func (p *PlainText) IsText() bool { return true } // isText checks whether the PlainText is a Text. It returns a bool.
func (p *PlainText) isText() bool { return true }
// Check checks the PlainText for incompatibilities with RFC4287. It returns an
// error.
func (p *PlainText) Check() error { func (p *PlainText) Check() error {
if p.Type != "" && p.Type != "text" && p.Type != "html" { if p.Type != "" && p.Type != "text" && p.Type != "html" {
return errors.New("type attribute of plain text must be text or html if not omitted") return errors.New("type attribute of plain text must be text or html if not omitted")
} }
if p.Type == "html" && !isCorrectlyEscaped(p.Text) {
return errors.New("text element of plain text not correctly escaped")
}
if p.Text == "" { if p.Text == "" {
return errors.New("text element of plain text empty") return errors.New("text element of plain text empty")
} }

View File

@ -1,110 +1,106 @@
package atomfeed package atom
import "fmt" import (
"encoding/xml"
"fmt"
)
type Source struct { type Source struct {
XMLName xml.Name `xml:"source"`
*CommonAttributes *CommonAttributes
Authors []*Person `xml:"author,omitempty"` Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"` Categories []*Category `xml:",omitempty"`
Contributors []*Person `xml:"contributor,omitempty"` Contributors []*Person `xml:"contributor,omitempty"`
Generator *Generator `xml:"generator,omitempty"` Generator *Generator `xml:",omitempty"`
Icon *Icon `xml:"icon,omitempty"` Icon *Icon `xml:",omitempty"`
ID *ID `xml:"id,omitempty"` ID *ID `xml:",omitempty"`
Links []*Link `xml:"link,omitempty"` Links []*Link `xml:",omitempty"`
Logo *Logo `xml:"logo,omitempty"` Logo *Logo `xml:",omitempty"`
Rights *Text `xml:"rights,omitempty"` Rights Text `xml:"rights,omitempty"`
Subtitle *Text `xml:"subtitle,omitempty"` Subtitle Text `xml:"subtitle,omitempty"`
Title *Text `xml:"title,omitempty"` Title Text `xml:"title,omitempty"`
Updated *Date `xml:"updated,omitempty"` Updated *Date `xml:"updated,omitempty"`
Extensions []*ExtensionElement `xml:",any,omitempty"` Extensions []*ExtensionElement `xml:",any,omitempty"`
} }
// Check checks the Source for incompatibilities with RFC4287. It returns an
// error.
func (s *Source) Check() error { func (s *Source) Check() error {
if s.Authors != nil { for i, a := range s.Authors {
for i, a := range s.Authors { if err := a.Check(); err != nil {
if err := a.Check(); err != nil { return fmt.Errorf("author element %v of source %v: %v", i, s.ID.URI, err)
return fmt.Errorf("author element %v of source: %v", i, err)
}
} }
} }
if s.Categories != nil { for i, c := range s.Categories {
for i, c := range s.Categories { if err := c.Check(); err != nil {
if err := c.Check(); err != nil { return fmt.Errorf("category element %v of source %v: %v", i, s.ID.URI, err)
return fmt.Errorf("category element %v of source: %v", i, err)
}
} }
} }
if s.Contributors != nil { for i, c := range s.Contributors {
for i, c := range s.Contributors { if err := c.Check(); err != nil {
if err := c.Check(); err != nil { return fmt.Errorf("contributor element %v of source %v: %v", i, s.ID.URI, err)
return fmt.Errorf("contributor element %v of source: %v", i, err)
}
} }
} }
if s.Generator != nil { if s.Generator != nil {
if err := s.Generator.Check(); err != nil { if err := s.Generator.Check(); err != nil {
return fmt.Errorf("generator element of source: %v", err) return fmt.Errorf("generator element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Icon != nil { if s.Icon != nil {
if err := s.Icon.Check(); err != nil { if err := s.Icon.Check(); err != nil {
return fmt.Errorf("icon element of source: %v", err) return fmt.Errorf("icon element of source %v: %v", s.ID.URI, err)
} }
} }
if s.ID != nil { if s.ID != nil {
if err := s.ID.Check(); err != nil { if err := s.ID.Check(); err != nil {
return fmt.Errorf("id element of source: %v", err) return fmt.Errorf("id element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Links != nil { for i, l := range s.Links {
for i, l := range s.Links { if err := l.Check(); err != nil {
if err := l.Check(); err != nil { return fmt.Errorf("link element %v of source %v: %v", i, s.ID.URI, err)
return fmt.Errorf("link element %v of source: %v", i, err)
}
} }
} }
if s.Logo != nil { if s.Logo != nil {
if err := s.Logo.Check(); err != nil { if err := s.Logo.Check(); err != nil {
return fmt.Errorf("logo element of source: %v", err) return fmt.Errorf("logo element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Rights != nil { if s.Rights != nil {
if err := (*s.Rights).Check(); err != nil { if err := s.Rights.Check(); err != nil {
return fmt.Errorf("rights element of source: %v", err) return fmt.Errorf("rights element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Subtitle != nil { if s.Subtitle != nil {
if err := (*s.Subtitle).Check(); err != nil { if err := s.Subtitle.Check(); err != nil {
return fmt.Errorf("subtitle element of source: %v", err) return fmt.Errorf("subtitle element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Title != nil { if s.Title != nil {
if err := (*s.Title).Check(); err != nil { if err := s.Title.Check(); err != nil {
return fmt.Errorf("title element of source: %v", err) return fmt.Errorf("title element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Updated != nil { if s.Updated != nil {
if err := s.Updated.Check(); err != nil { if err := s.Updated.Check(); err != nil {
return fmt.Errorf("updated element of source: %v", err) return fmt.Errorf("updated element of source %v: %v", s.ID.URI, err)
} }
} }
if s.Extensions != nil { for i, e := range s.Extensions {
for i, e := range s.Extensions { if err := e.Check(); err != nil {
if err := e.Check(); err != nil { return fmt.Errorf("extension element %v of source %v: %v", i, s.ID.URI, err)
return fmt.Errorf("extension element %v of source: %v", i, err)
}
} }
} }

View File

@ -1,4 +1,4 @@
package atomfeed package atom
import ( import (
"fmt" "fmt"
@ -6,20 +6,21 @@ import (
) )
type Text interface { type Text interface {
isText() bool
Check() error Check() error
IsText() bool
} }
// NewText creates a new Text. It returns a Text and an error.
func NewText(textType, content string) (Text, error) { func NewText(textType, content string) (Text, error) {
switch textType { switch textType {
case "text", "": case "text", "":
return &PlainText{Type: textType, Text: content}, nil return &PlainText{Type: textType, Text: content}, nil
case "html": case "html":
return &PlainText{Type: textType, Text: html.EscapeString(content)}, nil return &PlainText{Type: textType, Text: html.UnescapeString(content)}, nil
case "xhtml": case "xhtml":
return &XHTMLText{ return &XHTMLText{
Type: textType, Type: textType,
XHTMLDiv: XHTMLDiv{ XHTMLDiv: &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml", XMLNS: "http://www.w3.org/1999/xhtml",
Content: content, Content: content,
}, },

30
xhtmlDiv.go Normal file
View File

@ -0,0 +1,30 @@
package atom
import (
"encoding/xml"
"errors"
)
type XHTMLDiv struct {
XMLName xml.Name `xml:"div"`
XMLNS string `xml:"xmlns,attr"`
Content string `xml:",innerxml"`
}
// NewXHTMLDiv creates a new XHTMLDiv. It returns a *XHTMLDiv.
func NewXHTMLDiv(content string) *XHTMLDiv {
return &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml",
Content: content,
}
}
// Check checks the XHTMLDiv for incompatibilities with RFC4287. It returns an
// error.
func (x *XHTMLDiv) Check() error {
if x.XMLNS != "http://www.w3.org/1999/xhtml" {
return errors.New("xmlns attribute of xhtml text must be http://www.w3.org/1999/xhtml")
}
return nil
}

View File

@ -1,31 +1,28 @@
package atomfeed package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt"
) )
type XHTMLDiv struct {
XMLName xml.Name `xml:"div"`
XMLNS string `xml:"xmlns,attr"`
Content string `xml:",innerxml"`
}
type XHTMLText struct { type XHTMLText struct {
*CommonAttributes *CommonAttributes
XHTMLDiv *XHTMLDiv
Type string `xml:"type,attr"` // Must be xhtml Type string `xml:"type,attr"` // Must be xhtml
XHTMLDiv XHTMLDiv
} }
func (x *XHTMLText) IsText() bool { return true } // isText checks whether the XHTMLText is a Text. It returns a bool.
func (x *XHTMLText) isText() bool { return true }
// Check checks the XHTMLText for incompatibilities with RFC4287. It returns an
// error.
func (x *XHTMLText) Check() error { func (x *XHTMLText) Check() error {
if x.Type != "xhtml" { if x.Type != "xhtml" {
return errors.New("type attribute of xhtml text must be xhtml") return errors.New("type attribute of xhtml text must be xhtml")
} }
if x.XHTMLDiv.XMLNS != "http://www.w3.org/1999/xhtml" { if err := x.XHTMLDiv.Check(); err != nil {
return errors.New("xmlns attribute of xhtml text must be http://www.w3.org/1999/xhtml") return fmt.Errorf("xhtml div element %v of xhtml text %v: %v", x.XHTMLDiv, x, err)
} }
return nil return nil