Compare commits
	
		
			26 Commits
		
	
	
		
			57db4178d0
			...
			v1.0.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f3019ff7e8 | |||
| d6310ee8ee | |||
| a65920aa70 | |||
| 9ab48787d4 | |||
| be79a13d48 | |||
| 089c573aed | |||
| d8d0526a05 | |||
| 8fc6a10b0d | |||
| 0b70a8ee10 | |||
| da5d955b2d | |||
| ee72e91593 | |||
| ddf5b26a1e | |||
| cf131f0bcf | |||
| ae24db0c08 | |||
| 8ce7d54d00 | |||
| 764b143ff8 | |||
| bcf2532372 | |||
| b6b8970810 | |||
| c0f5306715 | |||
| 8a00759c4b | |||
| a49e853efb | |||
| e0384904b4 | |||
| e2986e70b1 | |||
| 4c38753ff7 | |||
| 7f30fd5411 | |||
| a7a6b5c711 | 
							
								
								
									
										278
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										278
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| An extensible Atom feed generator library that aims to be very close to RFC4287. | An extensible Atom feed generator library that aims to be very close to RFC4287. | ||||||
| It diligently checks for compliance with the standard and provides functions for | It diligently checks for compliance with the standard and provides functions for | ||||||
| easy creation and extension of elements. | easy creation, extension and deletion of elements. | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
| @@ -14,124 +14,228 @@ go get git.streifling.com/jason/atom@latest | |||||||
|  |  | ||||||
| ## Usage | ## Usage | ||||||
|  |  | ||||||
| This library provides convenient functions to safely create and extend elements | ### Basic Feed | ||||||
| and attributes of an Atom feed. This is because it can be hard to know all |  | ||||||
| pitfalls of RFC4287. The intended way of using atom is with these functions. | This library provides convenient functions to safely create, extend and delete | ||||||
|  | elements and attributes of Atom feeds. It also provides checks for all | ||||||
|  | constructs' adherence to RFC4287. | ||||||
|  |  | ||||||
| ```go | ```go | ||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  "fmt" |     "fmt" | ||||||
|  "log" |     "log" | ||||||
|  |  | ||||||
|  "git.streifling.com/jason/atom" |     "git.streifling.com/jason/atom" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|  feed, err := atom.NewFeed("Example Feed") |     feed := atom.NewFeed("Example Feed") | ||||||
|  if err != nil { |     if err := feed.Check(); err != nil { | ||||||
|   log.Fatalln(err) |         log.Fatalln(err) | ||||||
|  } |     } | ||||||
|  |  | ||||||
|  author := atom.NewPerson("John Doe") |     author := atom.NewPerson("John Doe") | ||||||
|  author.Email = "john.doe@example.com" |     author.Email = "john.doe@example.com" | ||||||
|  feed.AddAuthor(author) |     if err := author.Check(); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |     feed.AddAuthor(author) | ||||||
|  |  | ||||||
|  entry, err := atom.NewEntry("First Entry") |     entry := atom.NewEntry("First Entry") | ||||||
|  if err != nil { |     entry.Content = atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.") | ||||||
|   log.Fatalln(err) |     if err := entry.Check(); err != nil { | ||||||
|  } |         log.Fatalln(err) | ||||||
|  content, err := atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.") |     } | ||||||
|  if err != nil { |     feed.AddEntry(entry) | ||||||
|   log.Fatalln(err) |  | ||||||
|  } |  | ||||||
|  entry.Content = content |  | ||||||
|  feed.AddEntry(entry) |  | ||||||
|  |  | ||||||
|  if err := feed.Check(); err != nil { |     feedString, err := feed.ToXML("UTF-8") | ||||||
|   log.Fatalln(err) |     if err != nil { | ||||||
|  } |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  feedString, err := feed.ToXML("utf-8") |     fmt.Println(feedString) | ||||||
|  if err != nil { |  | ||||||
|   log.Fatalln(err) |  | ||||||
|  } |  | ||||||
|  fmt.Println(feedString) |  | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| It is also possible to use this library in a way similar to what other libraries | It is also possible to use this library in a way similar to what other libraries | ||||||
| would provide. This is, of course, making it easier to make mistakes. | provide. | ||||||
|  |  | ||||||
| ```go | ```go | ||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  "fmt" |     "fmt" | ||||||
|  "log" |     "log" | ||||||
|  "time" |     "time" | ||||||
|  |  | ||||||
|  "git.streifling.com/jason/atom" |     "git.streifling.com/jason/atom" | ||||||
|  |     "github.com/google/uuid" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|  now := time.Now() |     now := time.Now() | ||||||
|  |  | ||||||
|  feed := &atom.Feed{ |     feed := &atom.Feed{ | ||||||
|   Title: &atom.PlainText{ |         Title: &atom.PlainText{ | ||||||
|    Type: "text", |             Type: "text", | ||||||
|    Text: "Example Feed", |             Text: "Example Feed", | ||||||
|   }, |         }, | ||||||
|   ID:      &atom.ID{URI: "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"}, |         ID:      &atom.ID{URI: fmt.Sprint("urn:uuid:", uuid.New())}, | ||||||
|   Updated: &atom.Date{DateTime: atom.DateTime(now)}, |         Updated: &atom.Date{DateTime: atom.DateTime(now)}, | ||||||
|   Authors: []*atom.Person{ |         Authors: []*atom.Person{ | ||||||
|    { |             { | ||||||
|     Name:  "John Doe", |                 Name:  "John Doe", | ||||||
|     Email: "john.doe@example.com", |                 Email: "john.doe@example.com", | ||||||
|    }, |             }, | ||||||
|   }, |         }, | ||||||
|   Entries: []*atom.Entry{ |         Entries: []*atom.Entry{ | ||||||
|    { |             { | ||||||
|     Title: &atom.PlainText{ |                 Title: &atom.PlainText{ | ||||||
|      Type: "text", |                     Type: "text", | ||||||
|      Text: "First Entry", |                     Text: "First Entry", | ||||||
|     }, |                 }, | ||||||
|     ID:      &atom.ID{URI: "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"}, |                 ID:      &atom.ID{URI: fmt.Sprint("urn:uuid:", uuid.New())}, | ||||||
|     Updated: &atom.Date{DateTime: atom.DateTime(now)}, |                 Updated: &atom.Date{DateTime: atom.DateTime(now)}, | ||||||
|     Content: &atom.InlineTextContent{ |                 Content: &atom.InlineTextContent{ | ||||||
|      Type: "text", |                     Type: "text", | ||||||
|      Text: "This is the content of the first entry.", |                     Text: "This is the content of the first entry.", | ||||||
|     }, |                 }, | ||||||
|    }, |             }, | ||||||
|   }, |         }, | ||||||
|  } |     } | ||||||
|  |  | ||||||
|  feedString, err := feed.ToXML("utf-8") |     feedString, err := feed.ToXML("UTF-8") | ||||||
|  if err != nil { |     if err != nil { | ||||||
|   log.Fatalln(err) |         log.Fatalln(err) | ||||||
|  } |     } | ||||||
|  fmt.Println(feedString) |     fmt.Println(feedString) | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| The output of both ways of using it is an RFC4287 compliant Atom feed: | The output of both ways of using it is an RFC4287 compliant Atom feed. | ||||||
|  |  | ||||||
| ```xml | ```xml | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="UTF-8"?> | ||||||
| <feed xmlns="http://www.w3.org/2005/Atom"> | <feed xmlns="http://www.w3.org/2005/Atom"> | ||||||
|   <author> |     <author> | ||||||
|     <name>John Doe</name> |         <name>John Doe</name> | ||||||
|     <email>john.doe@example.com</email> |         <email>john.doe@example.com</email> | ||||||
|   </author> |     </author> | ||||||
|   <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> |     <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> | ||||||
|   <title type="text">Example Feed</title> |     <title type="text">Example Feed</title> | ||||||
|   <updated>2024-10-18T05:49:08+02:00</updated> |     <updated>2024-10-18T05:49:08+02:00</updated> | ||||||
|   <entry> |     <entry> | ||||||
|     <content type="text">This is the content of the first entry.</content> |         <content type="text">This is the content of the first entry.</content> | ||||||
|     <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> |         <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> | ||||||
|     <title type="text">First Entry</title> |         <title type="text">First Entry</title> | ||||||
|     <updated>2006-01-02T15:04:05+07:00</updated> |         <updated>2006-01-02T15:04:05+07:00</updated> | ||||||
|   </entry> |     </entry> | ||||||
|  | </feed> | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Compliance and Error Checking | ||||||
|  |  | ||||||
|  | All Atom constructs have their own ```construct.Check()``` method. It checks for | ||||||
|  | compliance with RFC4287 and errors. This allows one to be certain that only | ||||||
|  | compliant constructs are added to their respective parent construct. It also | ||||||
|  | means that not the entire feed has to be checked every time a new construct is | ||||||
|  | added. Instead one only checks the current construct and all of its | ||||||
|  | sub-constructs with a single ```construct.Check()``` call. | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  |     "fmt" | ||||||
|  |     "log" | ||||||
|  |  | ||||||
|  |     "git.streifling.com/jason/atom" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  |     feed := atom.NewFeed("Example Feed") | ||||||
|  |     if err := feed.Check(); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     entry := atom.NewEntry("One") | ||||||
|  |     entry.Content = atom.NewContent(atom.InlineText, "text", "Entry One") | ||||||
|  |     entry.AddAuthor(atom.NewPerson("John Doe")) | ||||||
|  |     if err := entry.Check(); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |     feed.AddEntry(entry) | ||||||
|  |  | ||||||
|  |     feedString, err := feed.ToXML("UTF-8") | ||||||
|  |     if err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |     fmt.Println(feedString) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Adding and Deleting Items | ||||||
|  |  | ||||||
|  | To add elements to any slice one calls the appropriate | ||||||
|  | ```someone.AddSomething(toBeAdded)``` method. It returns the item's index | ||||||
|  | number. To delete the item one calls the Delete method. | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  |     "fmt" | ||||||
|  |     "log" | ||||||
|  |  | ||||||
|  |     "git.streifling.com/jason/atom" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  |     feed := atom.NewFeed("Feed") | ||||||
|  |     if err := feed.Check(); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     e1 := atom.NewEntry("One") | ||||||
|  |     e1.Content = atom.NewContent(atom.InlineText, "text", "Entry One") | ||||||
|  |     if err := e1.Check(); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |     i1 := feed.AddEntry(e1) | ||||||
|  |  | ||||||
|  |     e2 := atom.NewEntry("Two") | ||||||
|  |     e2.Content = atom.NewContent(atom.InlineText, "text", "Entry Two") | ||||||
|  |     if err := e2.Check(); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |     feed.AddEntry(e2) | ||||||
|  |  | ||||||
|  |     if err := feed.DeleteEntry(i1); err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     feedString, err := feed.ToXML("UTF-8") | ||||||
|  |     if err != nil { | ||||||
|  |         log.Fatalln(err) | ||||||
|  |     } | ||||||
|  |     fmt.Println(feedString) | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The output of this example looks like this. It only shows the second entry. | ||||||
|  |  | ||||||
|  | ```xml | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <feed xmlns="http://www.w3.org/2005/Atom"> | ||||||
|  |     <id>urn:uuid:ae89d343-b535-447d-ac14-4b80d3e02a2f</id> | ||||||
|  |     <title type="text">Example Feed</title> | ||||||
|  |     <updated>2024-10-20T14:57:02+02:00</updated> | ||||||
|  |     <entry> | ||||||
|  |         <content type="text">Entry Two</content> | ||||||
|  |         <id>urn:uuid:620c6f73-ee1d-4c1e-be98-b0b1ad7a053f</id> | ||||||
|  |         <title type="text">Two</title> | ||||||
|  |         <updated>2024-10-20T14:57:02+02:00</updated> | ||||||
|  |     </entry> | ||||||
| </feed> | </feed> | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										88
									
								
								atom.go
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								atom.go
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| package atom | package atom | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/xml" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
| 	"mime" | 	"mime" | ||||||
| @@ -11,8 +12,31 @@ import ( | |||||||
| 	"golang.org/x/text/language" | 	"golang.org/x/text/language" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func Unescape(s string) string { | type Countable interface { | ||||||
| 	return html.UnescapeString(s) | 	*xml.Attr | *Person | *Category | *Link | *ExtensionElement | *Entry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // addToSlice adds a Countable countable to to a *[]Countable slice. It returns | ||||||
|  | // the index as an int. | ||||||
|  | func addToSlice[C Countable](slice *[]C, countable C) int { | ||||||
|  | 	if *slice == nil { | ||||||
|  | 		*slice = make([]C, 0) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	*slice = append(*slice, countable) | ||||||
|  | 	return len(*slice) - 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // deleteFromSlice deletes the Countable at index from the *[]Countable slice. | ||||||
|  | // It returns an error. | ||||||
|  | func deleteFromSlice[C Countable](slice *[]C, index int) error { | ||||||
|  | 	length := len(*slice) | ||||||
|  | 	if index > length { | ||||||
|  | 		return fmt.Errorf("id %v out of range %v", index, length) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	*slice = append((*slice)[:index], (*slice)[index+1:]...) | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // isValidIRI checks whether an IRI is valid or not. It returns a bool. | // isValidIRI checks whether an IRI is valid or not. It returns a bool. | ||||||
| @@ -22,16 +46,7 @@ func isValidIRI(iri string) bool { | |||||||
| 	return regexp.MustCompile(pattern).MatchString(iri) | 	return regexp.MustCompile(pattern).MatchString(iri) | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewIRI creates a new IRI. It returns a string and an error. | // isCorrectlyEscaped checks whether the text is correctly escaped as per | ||||||
| func NewIRI(iri string) (string, error) { |  | ||||||
| 	if !isValidIRI(iri) { |  | ||||||
| 		return "", fmt.Errorf("iri %v not correctly formatted", iri) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return iri, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // isCorrectlyEscaped checks whether a string is correctly escaped as per |  | ||||||
| // RFC4287. It returns a bool. | // RFC4287. It returns a bool. | ||||||
| func isCorrectlyEscaped(text string) bool { | func isCorrectlyEscaped(text string) bool { | ||||||
| 	relevantEntities := []string{"&", "<", ">", """, "'"} | 	relevantEntities := []string{"&", "<", ">", """, "'"} | ||||||
| @@ -45,8 +60,8 @@ func isCorrectlyEscaped(text string) bool { | |||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
| // isCompositeMediaType checks whether a string is a composite media type. It | // isCompositeMediaType checks whether the string m is a composite media type. | ||||||
| // returns a bool. | // It returns a bool. | ||||||
| func isCompositeMediaType(m string) bool { | func isCompositeMediaType(m string) bool { | ||||||
| 	mediaType, _, err := mime.ParseMediaType(m) | 	mediaType, _, err := mime.ParseMediaType(m) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -56,7 +71,7 @@ func isCompositeMediaType(m 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 | // isXMLMediaType checks whether the string m is an xml media type. It returns a | ||||||
| // bool. | // bool. | ||||||
| func isXMLMediaType(m string) bool { | func isXMLMediaType(m string) bool { | ||||||
| 	mediaType, _, err := mime.ParseMediaType(m) | 	mediaType, _, err := mime.ParseMediaType(m) | ||||||
| @@ -67,8 +82,8 @@ func isXMLMediaType(m 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 | // isValidMediaType checks whether the string m is a valid media type. It | ||||||
| // bool. | // returns a bool. | ||||||
| func isValidMediaType(m string) bool { | func isValidMediaType(m string) bool { | ||||||
| 	mediaType, _, err := mime.ParseMediaType(m) | 	mediaType, _, err := mime.ParseMediaType(m) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -76,45 +91,28 @@ func isValidMediaType(m string) bool { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	typeParts := strings.Split(mediaType, "/") | 	typeParts := strings.Split(mediaType, "/") | ||||||
| 	if len(typeParts) != 2 || typeParts[0] == "" || typeParts[1] == "" { | 	return len(typeParts) == 2 && typeParts[0] != "" && typeParts[1] != "" | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return true |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewMediaType creates a new MediaType. It returns a string and an error. | // isValidLanguageTag checks whether the string languageTag is valid. It returns | ||||||
| func NewMediaType(m string) (string, error) { | // a bool. | ||||||
| 	if !isValidMediaType(m) { |  | ||||||
| 		return "", fmt.Errorf("media type %v invalid", m) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	mediaType, _, _ := mime.ParseMediaType(m) |  | ||||||
| 	return mediaType, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool. |  | ||||||
| func isValidLanguageTag(languageTag string) bool { | func isValidLanguageTag(languageTag string) bool { | ||||||
| 	_, err := language.Parse(languageTag) | 	_, err := language.Parse(languageTag) | ||||||
| 	return err == nil | 	return err == nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewLanguageTag creates a new LanguageTag. It returns a string and an error. | // isValidAttribute checks whether the string attribute is valid. It returns a | ||||||
| func NewLanguageTag(l string) (string, error) { | // bool. | ||||||
| 	if !isValidLanguageTag(l) { |  | ||||||
| 		return "", fmt.Errorf("language tag %v invalid", l) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return l, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // isValidAttribute checks whether an Attribute is valid. It returns a bool. |  | ||||||
| func isValidAttribute(attribute string) bool { | func isValidAttribute(attribute string) bool { | ||||||
| 	regex := regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`) | 	return regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`).MatchString(attribute) | ||||||
| 	return regex.MatchString(attribute) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewURN generates an new valid IRI based on a UUID. It returns an IRI. | // NewURN generates an new valid IRI based on a UUID. It returns an IRI. | ||||||
| func NewURN() string { | func NewURN() string { | ||||||
| 	return fmt.Sprint("urn:uuid:", uuid.New()) | 	return fmt.Sprint("urn:uuid:", uuid.New()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Unescape unescapes the string s. It returns an IRI. | ||||||
|  | func Unescape(s string) string { | ||||||
|  | 	return html.UnescapeString(s) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								category.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								category.go
									
									
									
									
									
								
							| @@ -10,17 +10,16 @@ type Category struct { | |||||||
| 	*CommonAttributes | 	*CommonAttributes | ||||||
| 	Term   string `xml:"term,attr"` | 	Term   string `xml:"term,attr"` | ||||||
| 	Scheme string `xml:"scheme,attr,omitempty"` // IRI | 	Scheme string `xml:"scheme,attr,omitempty"` // IRI | ||||||
| 	Label  string `xml:"label,attr,omitempty"` | 	Label  string `xml:"label,attr,omitempty"`  // Must be unescaped | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewCategory creates a new Category. It returns a *Category. | // NewCategory creates a new Category. It takes in a string term and returns a | ||||||
|  | // *Category. | ||||||
| func NewCategory(term string) *Category { | func NewCategory(term string) *Category { | ||||||
| 	return &Category{Term: term} | 	return &Category{ | ||||||
| } | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Term:             term, | ||||||
| // SetLabel sets the Label attribute of the Category. | 	} | ||||||
| func (c *Category) SetLabel(label string) { |  | ||||||
| 	c.Label = Unescape(label) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Category for incompatibilities with RFC4287. It returns an | // Check checks the Category for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
| @@ -17,14 +17,19 @@ func NewCommonAttributes() *CommonAttributes { | |||||||
| 	return new(CommonAttributes) | 	return new(CommonAttributes) | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddAttribute adds the Attribute to the CommonAttributes. | // AddAttribute adds an attribute to the CommonAttributes. It takes in the | ||||||
| func (c *CommonAttributes) AddAttribute(name, value string) { | // strings name and value and returns the index as an int. | ||||||
| 	if c.UndefinedAttributes == nil { | func (c *CommonAttributes) AddAttribute(name, value string) int { | ||||||
| 		c.UndefinedAttributes = make([]*xml.Attr, 1) | 	return addToSlice(&c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value}) | ||||||
| 		c.UndefinedAttributes[0] = &xml.Attr{Name: xml.Name{Local: name}, Value: value} | } | ||||||
| 	} else { |  | ||||||
| 		c.UndefinedAttributes = append(c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value}) | // DeleteAttribute deletes the attribute at index from the CommonAttributes. It | ||||||
|  | // returns an error. | ||||||
|  | func (c *CommonAttributes) DeleteAttribute(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&c.UndefinedAttributes, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting undefined attribute %v from common attributes %v: %v", index, c, err) | ||||||
| 	} | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the CommonAttributes for incompatibilities with RFC4287. It | // Check checks the CommonAttributes for incompatibilities with RFC4287. It | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								content.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								content.go
									
									
									
									
									
								
							| @@ -14,17 +14,29 @@ type Content interface { | |||||||
| 	Check() error | 	Check() error | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewContent creates a new Content. It returns a Content and an error. | // NewContent creates a new Content. It takes in an int contentType, a string | ||||||
|  | // mediaType and an any content and returns a Content. | ||||||
|  | // | ||||||
|  | // If contentType is invalid, it returns nil. | ||||||
| func NewContent(contentType int, mediaType string, content any) Content { | func NewContent(contentType int, mediaType string, content any) Content { | ||||||
| 	switch contentType { | 	switch contentType { | ||||||
| 	case 0: | 	case 0: | ||||||
| 		return newInlineTextContent(mediaType, content.(string)) | 		if stringContent, ok := content.(string); ok { | ||||||
|  | 			return newInlineTextContent(mediaType, stringContent) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
| 	case 1: | 	case 1: | ||||||
| 		return newInlineXHTMLContent(mediaType, content.(*XHTMLDiv)) | 		if xhtmlDivContent, ok := content.(*XHTMLDiv); ok { | ||||||
|  | 			return newInlineXHTMLContent(mediaType, xhtmlDivContent) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
| 	case 2: | 	case 2: | ||||||
| 		return newInlineOtherContent(mediaType, content) | 		return newInlineOtherContent(mediaType, content) | ||||||
| 	case 3: | 	case 3: | ||||||
| 		return newOutOfLineContent(mediaType, content.(string)) | 		if stringContent, ok := content.(string); ok { | ||||||
|  | 			return newOutOfLineContent(mediaType, stringContent) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
| 	default: | 	default: | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								date.go
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								date.go
									
									
									
									
									
								
							| @@ -10,15 +10,18 @@ type Date struct { | |||||||
| 	DateTime string `xml:",chardata"` | 	DateTime string `xml:",chardata"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // DateTime formats a time.Time to string formated as defined by RFC3339. It | // DateTime formats the time.Time t to a string as defined by RFC3339. It | ||||||
| // returns a string. | // returns a string. | ||||||
| func DateTime(t time.Time) string { | func DateTime(t time.Time) string { | ||||||
| 	return t.Format(time.RFC3339) | 	return t.Format(time.RFC3339) | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewDate creates a new Date. It returns a *Date. | // NewDate creates a new Date. It takes in a time.Time t and returns a *Date. | ||||||
| func NewDate(t time.Time) *Date { | func NewDate(t time.Time) *Date { | ||||||
| 	return &Date{DateTime: DateTime(t)} | 	return &Date{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		DateTime:         DateTime(t), | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Date for incompatibilities with RFC4287. It returns an | // Check checks the Date for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
							
								
								
									
										187
									
								
								entry.go
									
									
									
									
									
								
							
							
						
						
									
										187
									
								
								entry.go
									
									
									
									
									
								
							| @@ -30,25 +30,26 @@ type Entry struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| // checkAuthors checks the entry's authors for incompatibilities with RFC4287. | // checkAuthors checks the entry's authors for incompatibilities with RFC4287. | ||||||
| // It returns an errors. | // It takes in a bool authorIsInFeed and 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(authorInFeed bool) error { | func (e *Entry) checkAuthors(authorIsInFeed bool) error { | ||||||
| 	if e.Authors == nil { | 	if e.Authors == nil { | ||||||
| 		if !authorInFeed { | 		if !authorIsInFeed { | ||||||
| 			if e.Source == nil { | 			if e.Source == nil { | ||||||
| 				return fmt.Errorf("no authors set in entry %v", e) | 				return fmt.Errorf("no authors set in entry %v", e.ID.URI) | ||||||
| 			} | 			} | ||||||
| 			if e.Source.Authors == nil { | 			if e.Source.Authors == nil { | ||||||
| 				return fmt.Errorf("no authors set in entry %v", e) | 				return fmt.Errorf("no authors set in entry %v", e.ID.URI) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		for i, a := range e.Authors { | 		for i, a := range e.Authors { | ||||||
| 			if err := a.Check(); err != nil { | 			if err := a.Check(); err != nil { | ||||||
| 				return fmt.Errorf("author element %v of entry %v: %v", i, e, err) | 				return fmt.Errorf("author element %v of entry %v: %v", i, e.ID.URI, err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -56,73 +57,110 @@ func (e *Entry) checkAuthors(authorInFeed bool) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewEntry creates a new Entry. It returns a *Entry. | // update sets the Updated time to time.Now. | ||||||
|  | func (e *Entry) update() { | ||||||
|  | 	if e.Updated == nil { | ||||||
|  | 		e.Updated = NewDate(time.Now()) | ||||||
|  | 	} else { | ||||||
|  | 		e.Updated.DateTime = DateTime(time.Now()) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewEntry creates a new Entry. It takes in a string title and returns a | ||||||
|  | // *Entry. | ||||||
| func NewEntry(title string) *Entry { | func NewEntry(title string) *Entry { | ||||||
| 	return &Entry{ | 	return &Entry{ | ||||||
| 		ID:      NewID(NewURN()), | 		CommonAttributes: NewCommonAttributes(), | ||||||
| 		Title:   NewText("text", title), | 		ID:               NewID(NewURN()), | ||||||
| 		Updated: NewDate(time.Now()), | 		Title:            NewText("text", title), | ||||||
|  | 		Updated:          NewDate(time.Now()), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddAuthor adds the Person as an author to the Entry. | // AddAuthor adds the Person a as an author to the Entry. It returns the index | ||||||
| func (e *Entry) AddAuthor(p *Person) { | // as an int. | ||||||
| 	if e.Authors == nil { | func (e *Entry) AddAuthor(a *Person) int { | ||||||
| 		e.Authors = make([]*Person, 1) | 	e.update() | ||||||
| 		e.Authors[0] = p | 	return addToSlice(&e.Authors, a) | ||||||
| 	} else { |  | ||||||
| 		e.Authors = append(e.Authors, p) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	e.Updated.DateTime = DateTime(time.Now()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddCategory adds the Category to the Entry. | // DeleteAuthor deletes the Person at index from the Entry. It returns an error. | ||||||
| func (e *Entry) AddCategory(c *Category) { | func (e *Entry) DeleteAuthor(index int) error { | ||||||
| 	if e.Categories == nil { | 	if err := deleteFromSlice(&e.Authors, index); err != nil { | ||||||
| 		e.Categories = make([]*Category, 1) | 		return fmt.Errorf("error deleting author %v from entry %v: %v", index, e.ID.URI, err) | ||||||
| 		e.Categories[0] = c |  | ||||||
| 	} else { |  | ||||||
| 		e.Categories = append(e.Categories, c) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e.Updated.DateTime = DateTime(time.Now()) | 	e.update() | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddContributor adds the Person as a contributor to the Entry. | // AddCategory adds the Category c to the Entry. It returns the index as an int. | ||||||
| func (e *Entry) AddContributor(c *Person) { | func (e *Entry) AddCategory(c *Category) int { | ||||||
| 	if e.Contributors == nil { | 	e.update() | ||||||
| 		e.Contributors = make([]*Person, 1) | 	return addToSlice(&e.Categories, c) | ||||||
| 		e.Contributors[0] = c |  | ||||||
| 	} else { |  | ||||||
| 		e.Contributors = append(e.Contributors, c) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	e.Updated.DateTime = DateTime(time.Now()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddLink adds the Link to the Entry. | // DeleteCategory deletes the Category at index from the Entry. It returns an | ||||||
| func (e *Entry) AddLink(l *Link) { | // error. | ||||||
| 	if e.Links == nil { | func (e *Entry) DeleteCategory(index int) error { | ||||||
| 		e.Links = make([]*Link, 1) | 	if err := deleteFromSlice(&e.Categories, index); err != nil { | ||||||
| 		e.Links[0] = l | 		return fmt.Errorf("error deleting category %v from entry %v: %v", index, e.ID.URI, err) | ||||||
| 	} else { |  | ||||||
| 		e.Links = append(e.Links, l) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e.Updated.DateTime = DateTime(time.Now()) | 	e.update() | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddExtension adds the ExtensionElement to the Entry. | // AddContributor adds the Person c as a contributor to the Entry. It returns | ||||||
| func (e *Entry) AddExtension(x *ExtensionElement) { | // the index as an int. | ||||||
| 	if e.Extensions == nil { | func (e *Entry) AddContributor(c *Person) int { | ||||||
| 		e.Extensions = make([]*ExtensionElement, 1) | 	e.update() | ||||||
| 		e.Extensions[0] = x | 	return addToSlice(&e.Contributors, c) | ||||||
| 	} else { | } | ||||||
| 		e.Extensions = append(e.Extensions, x) |  | ||||||
|  | // DeleteContributor deletes the Person at index from the Entry. It returns an | ||||||
|  | // error. | ||||||
|  | func (e *Entry) DeleteContributor(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&e.Contributors, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, e.ID.URI, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e.Updated.DateTime = DateTime(time.Now()) | 	e.update() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddLink adds the Link l to the Entry. It returns the index as an int. | ||||||
|  | func (e *Entry) AddLink(l *Link) int { | ||||||
|  | 	e.update() | ||||||
|  | 	return addToSlice(&e.Links, l) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteLink deletes the Link at index from the Entry. It returns an error. | ||||||
|  | func (e *Entry) DeleteLink(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&e.Links, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting link %v from entry %v: %v", index, e.ID.URI, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	e.update() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddExtension adds the ExtensionElement x to the Entry. It returns the index | ||||||
|  | // as an int. | ||||||
|  | func (e *Entry) AddExtension(x *ExtensionElement) int { | ||||||
|  | 	e.update() | ||||||
|  | 	return addToSlice(&e.Extensions, x) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteExtension deletes the Extension at index from the Entry. It returns an | ||||||
|  | // error. | ||||||
|  | func (e *Entry) DeleteExtension(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&e.Extensions, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting extension %v from entry %v: %v", index, e.ID.URI, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	e.update() | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Entry for incompatibilities with RFC4287. It returns an | // Check checks the Entry for incompatibilities with RFC4287. It returns an | ||||||
| @@ -137,64 +175,64 @@ func (e *Entry) Check() error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := e.checkAuthors(true); err != nil { | 	if err := e.checkAuthors(true); err != nil { | ||||||
| 		return fmt.Errorf("entry %v: %v", e, err) | 		return fmt.Errorf("entry %v: %v", e.ID.URI, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, c := range e.Categories { | 	for i, c := range e.Categories { | ||||||
| 		if err := c.Check(); err != nil { | 		if err := c.Check(); err != nil { | ||||||
| 			return fmt.Errorf("category element %v of entry %v: %v", i, e, err) | 			return fmt.Errorf("category element %v of entry %v: %v", i, e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Content != nil { | 	if e.Content != nil { | ||||||
| 		if err := e.Content.Check(); err != nil { | 		if err := e.Content.Check(); err != nil { | ||||||
| 			return fmt.Errorf("content element of entry %v: %v", e, err) | 			return fmt.Errorf("content element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		// atom:entry elements that contain no child atom:content element MUST | 		// atom:entry elements that contain no child atom:content element MUST | ||||||
| 		// contain at least one atom:link element with a rel attribute value of | 		// contain at least one atom:link element with a rel attribute value of | ||||||
| 		// "alternate". | 		// "alternate". | ||||||
| 		if !alternateRelExists(e.Links) { | 		if !alternateRelExists(e.Links) { | ||||||
| 			return fmt.Errorf("no content element of entry %v and no link element with rel \"alternate\"", e) | 			return fmt.Errorf("no content element of entry %v and no link element with rel \"alternate\"", e.ID.URI) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, c := range e.Contributors { | 	for i, c := range e.Contributors { | ||||||
| 		if err := c.Check(); err != nil { | 		if err := c.Check(); err != nil { | ||||||
| 			return fmt.Errorf("contributor element %v of entry %v: %v", i, e, err) | 			return fmt.Errorf("contributor element %v of entry %v: %v", i, e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, l := range e.Links { | 	for i, l := range e.Links { | ||||||
| 		if err := l.Check(); err != nil { | 		if err := l.Check(); err != nil { | ||||||
| 			return fmt.Errorf("link element %v of entry %v: %v", i, e, err) | 			return fmt.Errorf("link element %v of entry %v: %v", i, e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if hasAlternateDuplicateLinks(e.Links) { | 	if hasAlternateDuplicateLinks(e.Links) { | ||||||
| 		return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in entry %v", e) | 		return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in entry %v", e.ID.URI) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Published != nil { | 	if e.Published != nil { | ||||||
| 		if err := e.Published.Check(); err != nil { | 		if err := e.Published.Check(); err != nil { | ||||||
| 			return fmt.Errorf("published element of entry %v: %v", e, err) | 			return fmt.Errorf("published element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Rights != nil { | 	if e.Rights != nil { | ||||||
| 		if err := e.Rights.Check(); err != nil { | 		if err := e.Rights.Check(); err != nil { | ||||||
| 			return fmt.Errorf("rights element of entry %v: %v", e, err) | 			return fmt.Errorf("rights element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Source != nil { | 	if e.Source != nil { | ||||||
| 		if err := e.Source.Check(); err != nil { | 		if err := e.Source.Check(); err != nil { | ||||||
| 			return fmt.Errorf("source element of entry %v: %v", e, err) | 			return fmt.Errorf("source element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Summary != nil { | 	if e.Summary != nil { | ||||||
| 		if err := e.Summary.Check(); err != nil { | 		if err := e.Summary.Check(); err != nil { | ||||||
| 			return fmt.Errorf("summary element of entry %v: %v", e, err) | 			return fmt.Errorf("summary element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		// atom:entry elements MUST contain an atom:summary element in either | 		// atom:entry elements MUST contain an atom:summary element in either | ||||||
| @@ -202,7 +240,7 @@ func (e *Entry) Check() error { | |||||||
| 		// the atom:entry contains an atom:content that has a "src" attribute | 		// the atom:entry contains an atom:content that has a "src" attribute | ||||||
| 		// (and is thus empty). | 		// (and is thus empty). | ||||||
| 		if e.Content.hasSRC() { | 		if e.Content.hasSRC() { | ||||||
| 			return fmt.Errorf("no summary element of entry %v but content of type out of line content", e) | 			return fmt.Errorf("no summary element of entry %v but content of type out of line content", e.ID.URI) | ||||||
| 		} | 		} | ||||||
| 		// the atom:entry contains content that is encoded in Base64; i.e., the | 		// the atom:entry contains content that is encoded in Base64; i.e., the | ||||||
| 		// "type" attribute of atom:content is a MIME media type [MIMEREG], but | 		// "type" attribute of atom:content is a MIME media type [MIMEREG], but | ||||||
| @@ -210,40 +248,41 @@ func (e *Entry) Check() error { | |||||||
| 		// does not end with "/xml" or "+xml". | 		// does not end with "/xml" or "+xml". | ||||||
| 		mediaType := e.Content.getType() | 		mediaType := e.Content.getType() | ||||||
| 		if isValidMediaType(mediaType) && !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) | 			return fmt.Errorf("no summary element of entry %v but media type not xml", e.ID.URI) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Title == nil { | 	if e.Title == nil { | ||||||
| 		return fmt.Errorf("no title element of entry %v", e) | 		return fmt.Errorf("no title element of entry %v", e.ID.URI) | ||||||
| 	} else { | 	} else { | ||||||
| 		if err := e.Title.Check(); err != nil { | 		if err := e.Title.Check(); err != nil { | ||||||
| 			return fmt.Errorf("title element of entry %v: %v", e, err) | 			return fmt.Errorf("title element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e.Updated == nil { | 	if e.Updated == nil { | ||||||
| 		return fmt.Errorf("no updated element of entry %v", e) | 		return fmt.Errorf("no updated element of entry %v", e.ID.URI) | ||||||
| 	} else { | 	} else { | ||||||
| 		if err := e.Updated.Check(); err != nil { | 		if err := e.Updated.Check(); err != nil { | ||||||
| 			return fmt.Errorf("updated element of entry %v: %v", e, err) | 			return fmt.Errorf("updated element of entry %v: %v", e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, x := range e.Extensions { | 	for i, x := range e.Extensions { | ||||||
| 		if err := x.Check(); err != nil { | 		if err := x.Check(); err != nil { | ||||||
| 			return fmt.Errorf("extension element %v of entry %v: %v", i, e, err) | 			return fmt.Errorf("extension element %v of entry %v: %v", i, e.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToXML converts the Feed to XML. It returns a string and an error. | // ToXML converts the Feed to XML. It takes in a string encoding and returns a | ||||||
|  | // string and an error. | ||||||
| func (e *Entry) ToXML(encoding string) (string, error) { | func (e *Entry) ToXML(encoding string) (string, error) { | ||||||
| 	xml, err := xml.MarshalIndent(e, "", "  ") | 	xml, err := xml.MarshalIndent(e, "", "  ") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", fmt.Errorf("error xml encoding entry: %v", err) | 		return "", fmt.Errorf("error xml encoding entry %v: %v", e.ID.URI, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return fmt.Sprintln(`<?xml version="1.0" encoding="`+encoding+`"?>`) + string(xml), nil | 	return fmt.Sprintln(`<?xml version="1.0" encoding="`+encoding+`"?>`) + string(xml), nil | ||||||
|   | |||||||
| @@ -10,8 +10,8 @@ type ExtensionElement struct { | |||||||
| 	XMLName xml.Name | 	XMLName xml.Name | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewExtensionElement creates a new ExtensionElement. It returns a | // NewExtensionElement creates a new ExtensionElement. It takes in a string name | ||||||
| // *ExtensionElement. | // and any value and 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} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										218
									
								
								feed.go
									
									
									
									
									
								
							
							
						
						
									
										218
									
								
								feed.go
									
									
									
									
									
								
							| @@ -25,85 +25,151 @@ type Feed struct { | |||||||
| 	Entries      []*Entry            `xml:",omitempty"` | 	Entries      []*Entry            `xml:",omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewFeed creates a new Feed. It returns a *Feed. | // update sets the Updated time to time.Now. | ||||||
|  | func (f *Feed) update() { | ||||||
|  | 	if f.Updated == nil { | ||||||
|  | 		f.Updated = NewDate(time.Now()) | ||||||
|  | 	} else { | ||||||
|  | 		f.Updated.DateTime = DateTime(time.Now()) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewFeed creates a new Feed. It takes in a string title and returns a *Feed. | ||||||
| func NewFeed(title string) *Feed { | func NewFeed(title string) *Feed { | ||||||
| 	return &Feed{ | 	return &Feed{ | ||||||
| 		ID:      NewID(NewURN()), | 		CommonAttributes: NewCommonAttributes(), | ||||||
| 		Title:   NewText("text", title), | 		ID:               NewID(NewURN()), | ||||||
| 		Updated: NewDate(time.Now()), | 		Title:            NewText("text", title), | ||||||
|  | 		Updated:          NewDate(time.Now()), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddAuthor adds the Person as an author to the Feed. | // AddAuthor adds the Person a as an author to the Feed. It returns the index as | ||||||
| func (f *Feed) AddAuthor(p *Person) { | // an int. | ||||||
| 	if f.Authors == nil { | func (f *Feed) AddAuthor(a *Person) int { | ||||||
| 		f.Authors = make([]*Person, 1) | 	f.update() | ||||||
| 		f.Authors[0] = p | 	return addToSlice(&f.Authors, a) | ||||||
| 	} else { |  | ||||||
| 		f.Authors = append(f.Authors, p) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	f.Updated.DateTime = DateTime(time.Now()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddCategory adds the Category to the Feed. | // DeleteAuthor deletes the Person at index from the Feed. It returns an error. | ||||||
| func (f *Feed) AddCategory(c *Category) { | func (f *Feed) DeleteAuthor(index int) error { | ||||||
| 	if f.Categories == nil { | 	if err := deleteFromSlice(&f.Authors, index); err != nil { | ||||||
| 		f.Categories = make([]*Category, 1) | 		return fmt.Errorf("error deleting author %v from entry %v: %v", index, f.ID.URI, err) | ||||||
| 		f.Categories[0] = c |  | ||||||
| 	} else { |  | ||||||
| 		f.Categories = append(f.Categories, c) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	f.Updated.DateTime = DateTime(time.Now()) | 	f.update() | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddContributor adds the Person as a contributor to the Feed. | // AddCategory adds the Category c to the Feed. It returns the index as an int. | ||||||
| func (f *Feed) AddContributor(c *Person) { | func (f *Feed) AddCategory(c *Category) int { | ||||||
| 	if f.Contributors == nil { | 	f.update() | ||||||
| 		f.Contributors = make([]*Person, 1) | 	return addToSlice(&f.Categories, c) | ||||||
| 		f.Contributors[0] = c |  | ||||||
| 	} else { |  | ||||||
| 		f.Contributors = append(f.Contributors, c) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	f.Updated.DateTime = DateTime(time.Now()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddLink adds the Link to the Feed. There should be one Link with Rel "self". | // DeleteCategory deletes the Category at index from the Feed. It returns an | ||||||
| func (f *Feed) AddLink(l *Link) { | // error. | ||||||
| 	if f.Links == nil { | func (f *Feed) DeleteCategory(index int) error { | ||||||
| 		f.Links = make([]*Link, 1) | 	if err := deleteFromSlice(&f.Categories, index); err != nil { | ||||||
| 		f.Links[0] = l | 		return fmt.Errorf("error deleting category %v from entry %v: %v", index, f.ID.URI, err) | ||||||
| 	} else { |  | ||||||
| 		f.Links = append(f.Links, l) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	f.Updated.DateTime = DateTime(time.Now()) | 	f.update() | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddExtension adds the Extension to the Feed. | // AddContributor adds the Person c as a contributor to the Feed. It returns the | ||||||
| func (f *Feed) AddExtension(e *ExtensionElement) { | // index as an int. | ||||||
| 	if f.Extensions == nil { | func (f *Feed) AddContributor(c *Person) int { | ||||||
| 		f.Extensions = make([]*ExtensionElement, 1) | 	f.update() | ||||||
| 		f.Extensions[0] = e | 	return addToSlice(&f.Contributors, c) | ||||||
| 	} else { |  | ||||||
| 		f.Extensions = append(f.Extensions, e) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	f.Updated.DateTime = DateTime(time.Now()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddEntry adds the Entry to the Feed. | // DeleteContributor deletes the Person at index from the Feed. It returns an | ||||||
| func (f *Feed) AddEntry(e *Entry) { | // error. | ||||||
| 	if f.Entries == nil { | func (f *Feed) DeleteContributor(index int) error { | ||||||
| 		f.Entries = make([]*Entry, 1) | 	if err := deleteFromSlice(&f.Contributors, index); err != nil { | ||||||
| 		f.Entries[0] = e | 		return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, f.ID.URI, err) | ||||||
| 	} else { |  | ||||||
| 		f.Entries = append(f.Entries, e) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	f.Updated.DateTime = DateTime(time.Now()) | 	f.update() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddLink adds the Link l to the Feed. It returns the index as an int. | ||||||
|  | // | ||||||
|  | // There should be one Link with Rel "self". | ||||||
|  | func (f *Feed) AddLink(l *Link) int { | ||||||
|  | 	f.update() | ||||||
|  | 	return addToSlice(&f.Links, l) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteLink deletes the Link at index from the Feed. It returns an error. | ||||||
|  | func (f *Feed) DeleteLink(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&f.Links, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting link %v from entry %v: %v", index, f.ID.URI, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	f.update() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddExtension adds the Extension e to the Feed. It returns the index as an | ||||||
|  | // int. | ||||||
|  | func (f *Feed) AddExtension(e *ExtensionElement) int { | ||||||
|  | 	f.update() | ||||||
|  | 	return addToSlice(&f.Extensions, e) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteExtension deletes the Extension at index from the Feed. It returns an | ||||||
|  | // error. | ||||||
|  | func (f *Feed) DeleteExtension(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&f.Extensions, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting extension %v from entry %v: %v", index, f.ID.URI, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	f.update() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddEntry adds the Entry e to the Feed. It returns the index as an int. | ||||||
|  | func (f *Feed) AddEntry(e *Entry) int { | ||||||
|  | 	f.update() | ||||||
|  | 	return addToSlice(&f.Entries, e) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteEntry deletes the Entry at index from the Feed. It returns an error. | ||||||
|  | func (f *Feed) DeleteEntry(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&f.Entries, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting entry %v from entry %v: %v", index, f.ID.URI, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	f.update() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteEntryByURI deletes the Entry from the Feed. It takes in a string uri | ||||||
|  | // and returns an error. | ||||||
|  | func (f *Feed) DeleteEntryByURI(uri string) error { | ||||||
|  | 	if !isValidIRI(uri) { | ||||||
|  | 		return fmt.Errorf("error deleting entry from feed %v: uri %v invalid", f.ID.URI, uri) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	index := -1 | ||||||
|  | 	for i, e := range f.Entries { | ||||||
|  | 		if e.ID.URI == uri { | ||||||
|  | 			index = i | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if index < 0 { | ||||||
|  | 		return fmt.Errorf("error deleting entry from feed %v: id %v not found", f.ID.URI, uri) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	f.Entries = append(f.Entries[:index], f.Entries[index+1:]...) | ||||||
|  | 	f.update() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Feed for incompatibilities with RFC4287. It returns an | // Check checks the Feed for incompatibilities with RFC4287. It returns an | ||||||
| @@ -123,93 +189,93 @@ func (f *Feed) Check() error { | |||||||
| 	if f.Authors == nil { | 	if f.Authors == nil { | ||||||
| 		for _, e := range f.Entries { | 		for _, e := range f.Entries { | ||||||
| 			if err := e.checkAuthors(false); err != nil { | 			if err := e.checkAuthors(false); err != nil { | ||||||
| 				return fmt.Errorf("no authors set in feed %v: %v", f, err) | 				return fmt.Errorf("no authors set in feed %v: %v", f.ID.URI, err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		for i, a := range f.Authors { | 		for i, a := range f.Authors { | ||||||
| 			if err := a.Check(); err != nil { | 			if err := a.Check(); err != nil { | ||||||
| 				return fmt.Errorf("author element %v of feed %v: %v", i, f, err) | 				return fmt.Errorf("author element %v of feed %v: %v", i, f.ID.URI, err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, c := range f.Categories { | 	for i, c := range f.Categories { | ||||||
| 		if err := c.Check(); err != nil { | 		if err := c.Check(); err != nil { | ||||||
| 			return fmt.Errorf("category element %v of feed %v: %v", i, f, err) | 			return fmt.Errorf("category element %v of feed %v: %v", i, f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, c := range f.Contributors { | 	for i, c := range f.Contributors { | ||||||
| 		if err := c.Check(); err != nil { | 		if err := c.Check(); err != nil { | ||||||
| 			return fmt.Errorf("contributor element %v of feed %v: %v", i, f, err) | 			return fmt.Errorf("contributor element %v of feed %v: %v", i, f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Generator != nil { | 	if f.Generator != nil { | ||||||
| 		if err := f.Generator.Check(); err != nil { | 		if err := f.Generator.Check(); err != nil { | ||||||
| 			return fmt.Errorf("generator element of feed %v: %v", f, err) | 			return fmt.Errorf("generator element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Icon != nil { | 	if f.Icon != nil { | ||||||
| 		if err := f.Icon.Check(); err != nil { | 		if err := f.Icon.Check(); err != nil { | ||||||
| 			return fmt.Errorf("icon element of feed %v: %v", f, err) | 			return fmt.Errorf("icon element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, l := range f.Links { | 	for i, l := range f.Links { | ||||||
| 		if err := l.Check(); err != nil { | 		if err := l.Check(); err != nil { | ||||||
| 			return fmt.Errorf("link element %v of feed %v: %v", i, f, err) | 			return fmt.Errorf("link element %v of feed %v: %v", i, f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if hasAlternateDuplicateLinks(f.Links) { | 	if hasAlternateDuplicateLinks(f.Links) { | ||||||
| 		return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in feed %v", f) | 		return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in feed %v", f.ID.URI) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Logo != nil { | 	if f.Logo != nil { | ||||||
| 		if err := f.Logo.Check(); err != nil { | 		if err := f.Logo.Check(); err != nil { | ||||||
| 			return fmt.Errorf("logo element of feed %v: %v", f, err) | 			return fmt.Errorf("logo element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Rights != nil { | 	if f.Rights != nil { | ||||||
| 		if err := f.Rights.Check(); err != nil { | 		if err := f.Rights.Check(); err != nil { | ||||||
| 			return fmt.Errorf("rights element of feed %v: %v", f, err) | 			return fmt.Errorf("rights element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Subtitle != nil { | 	if f.Subtitle != nil { | ||||||
| 		if err := f.Subtitle.Check(); err != nil { | 		if err := f.Subtitle.Check(); err != nil { | ||||||
| 			return fmt.Errorf("subtitle element of feed %v: %v", f, err) | 			return fmt.Errorf("subtitle element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Title == nil { | 	if f.Title == nil { | ||||||
| 		return fmt.Errorf("no title element of feed %v", f) | 		return fmt.Errorf("no title element of feed %v", f.ID.URI) | ||||||
| 	} else { | 	} else { | ||||||
| 		if err := f.Title.Check(); err != nil { | 		if err := f.Title.Check(); err != nil { | ||||||
| 			return fmt.Errorf("title element of feed %v: %v", f, err) | 			return fmt.Errorf("title element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.Updated == nil { | 	if f.Updated == nil { | ||||||
| 		return fmt.Errorf("no updated element of feed %v", f) | 		return fmt.Errorf("no updated element of feed %v", f.ID.URI) | ||||||
| 	} else { | 	} else { | ||||||
| 		if err := f.Updated.Check(); err != nil { | 		if err := f.Updated.Check(); err != nil { | ||||||
| 			return fmt.Errorf("updated element of feed %v: %v", f, err) | 			return fmt.Errorf("updated element of feed %v: %v", f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, x := range f.Extensions { | 	for i, x := range f.Extensions { | ||||||
| 		if err := x.Check(); err != nil { | 		if err := x.Check(); err != nil { | ||||||
| 			return fmt.Errorf("extension element %v of feed %v: %v", i, f, err) | 			return fmt.Errorf("extension element %v of feed %v: %v", i, f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, n := range f.Entries { | 	for i, n := range f.Entries { | ||||||
| 		if err := n.Check(); err != nil { | 		if err := n.Check(); err != nil { | ||||||
| 			return fmt.Errorf("entry element %v of feed %v: %v", i, f, err) | 			return fmt.Errorf("entry element %v of feed %v: %v", i, f.ID.URI, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -220,7 +286,7 @@ func (f *Feed) Check() error { | |||||||
| 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 { | ||||||
| 		return "", fmt.Errorf("error xml encoding feed: %v", err) | 		return "", fmt.Errorf("error xml encoding feed %v: %v", f.ID.URI, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return fmt.Sprintln(`<?xml version="1.0" encoding="`+encoding+`"?>`) + string(xml), nil | 	return fmt.Sprintln(`<?xml version="1.0" encoding="`+encoding+`"?>`) + string(xml), nil | ||||||
|   | |||||||
| @@ -14,9 +14,13 @@ type Generator struct { | |||||||
| 	Text    string `xml:",chardata"` | 	Text    string `xml:",chardata"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewGenerator creates a new Generator. It returns a *Generator. | // NewGenerator creates a new Generator. It takes in a string text and returns a | ||||||
|  | // *Generator. | ||||||
| func NewGenerator(text string) *Generator { | func NewGenerator(text string) *Generator { | ||||||
| 	return &Generator{Text: html.UnescapeString(text)} | 	return &Generator{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Text:             html.UnescapeString(text), | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Generator for incompatibilities with RFC4287. It returns an | // Check checks the Generator for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| module git.streifling.com/jason/atom | module git.streifling.com/jason/atom | ||||||
|  |  | ||||||
| go 1.23.2 | go 1.24.0 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/google/uuid v1.6.0 | 	github.com/google/uuid v1.6.0 | ||||||
| 	golang.org/x/text v0.19.0 | 	golang.org/x/text v0.29.0 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,4 +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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= | ||||||
| golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								icon.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								icon.go
									
									
									
									
									
								
							| @@ -12,9 +12,12 @@ type Icon struct { | |||||||
| 	URI string `xml:",chardata"` // IRI | 	URI string `xml:",chardata"` // IRI | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewIcon creates a new Icon. It returns a *Icon. | // NewIcon creates a new Icon. It takes in a string uri and returns a *Icon. | ||||||
| func NewIcon(uri string) *Icon { | func NewIcon(uri string) *Icon { | ||||||
| 	return &Icon{URI: uri} | 	return &Icon{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		URI:              uri, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Icon for incompatibilities with RFC4287. It returns an | // Check checks the Icon for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								id.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								id.go
									
									
									
									
									
								
							| @@ -12,9 +12,12 @@ type ID struct { | |||||||
| 	URI string `xml:",chardata"` // IRI | 	URI string `xml:",chardata"` // IRI | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewID creates a new ID. It returns a *ID. | // NewID creates a new ID. It takes in a string uri and returns a *ID. | ||||||
| func NewID(uri string) *ID { | func NewID(uri string) *ID { | ||||||
| 	return &ID{URI: uri} | 	return &ID{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		URI:              uri, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the ID for incompatibilities with RFC4287. It returns an error. | // Check checks the ID for incompatibilities with RFC4287. It returns an error. | ||||||
|   | |||||||
| @@ -13,11 +13,16 @@ type InlineOtherContent struct { | |||||||
| 	Type       string `xml:"type,attr,omitempty"` // MediaType | 	Type       string `xml:"type,attr,omitempty"` // MediaType | ||||||
| } | } | ||||||
|  |  | ||||||
| // newInlineOtherContent creates a new InlineOtherContent. It returns a | // newInlineOtherContent creates a new InlineOtherContent. It takes in the string | ||||||
| // *InlineOtherContent and an error. | // mediaType and any content and returns a *InlineOtherContent and an error. | ||||||
| func newInlineOtherContent(mediaType string, content any) *InlineOtherContent { | func newInlineOtherContent(mediaType string, content any) *InlineOtherContent { | ||||||
| 	mediaType, _, _ = mime.ParseMediaType(mediaType) | 	mediaType, _, _ = mime.ParseMediaType(mediaType) | ||||||
| 	return &InlineOtherContent{Type: mediaType, AnyElement: content} |  | ||||||
|  | 	return &InlineOtherContent{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Type:             mediaType, | ||||||
|  | 		AnyElement:       content, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // isContent checks whether the InlineOtherContent is a Content. It returns a | // isContent checks whether the InlineOtherContent is a Content. It returns a | ||||||
|   | |||||||
| @@ -12,10 +12,14 @@ type InlineTextContent struct { | |||||||
| 	Text string `xml:",chardata"` | 	Text string `xml:",chardata"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // newInlineTextContent creates a new InlineTextContent. It returns a | // newInlineTextContent creates a new InlineTextContent. It takes in the strings | ||||||
| // *InlineTextContent. | // mediaType and text and returns a *InlineTextContent. | ||||||
| func newInlineTextContent(mediaType, text string) *InlineTextContent { | func newInlineTextContent(mediaType, text string) *InlineTextContent { | ||||||
| 	return &InlineTextContent{Type: mediaType, Text: text} | 	return &InlineTextContent{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Type:             mediaType, | ||||||
|  | 		Text:             text, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // isContent checks whether the InlineTextContent is a Content. It returns a | // isContent checks whether the InlineTextContent is a Content. It returns a | ||||||
|   | |||||||
| @@ -12,10 +12,14 @@ type InlineXHTMLContent struct { | |||||||
| 	Type     string `xml:"type,attr"` | 	Type     string `xml:"type,attr"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a | // newInlineXHTMLContent creates a new InlineXHTMLContent. It takes in the | ||||||
| // *InlineXHTMLContent. | // string mediaType and the XHTMLDiv div and returns a *InlineXHTMLContent. | ||||||
| func newInlineXHTMLContent(mediaType string, div *XHTMLDiv) *InlineXHTMLContent { | func newInlineXHTMLContent(mediaType string, div *XHTMLDiv) *InlineXHTMLContent { | ||||||
| 	return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: div} | 	return &InlineXHTMLContent{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Type:             mediaType, | ||||||
|  | 		XHTMLDiv:         div, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // isContent checks whether the InlineXHTMLContent is a Content. It returns a | // isContent checks whether the InlineXHTMLContent is a Content. It returns a | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								link.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								link.go
									
									
									
									
									
								
							| @@ -17,9 +17,12 @@ type Link struct { | |||||||
| 	Length   uint   `xml:"length,attr,omitempty"` | 	Length   uint   `xml:"length,attr,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewLink creates a new Link. It returns a *Link. | // NewLink creates a new Link. It takes in the string href and returns a *Link. | ||||||
| func NewLink(href string) *Link { | func NewLink(href string) *Link { | ||||||
| 	return &Link{Href: href} | 	return &Link{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Href:             href, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Link for incompatibilities with RFC4287. It returns an | // Check checks the Link for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								logo.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								logo.go
									
									
									
									
									
								
							| @@ -11,9 +11,12 @@ type Logo struct { | |||||||
| 	URI string `xml:",chardata"` // IRI | 	URI string `xml:",chardata"` // IRI | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewLogo creates a new Logo. It returns a *Logo. | // NewLogo creates a new Logo. It takes in a string uri and returns a *Logo. | ||||||
| func NewLogo(uri string) *Logo { | func NewLogo(uri string) *Logo { | ||||||
| 	return &Logo{URI: uri} | 	return &Logo{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		URI:              uri, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Logo for incompatibilities with RFC4287. It returns an | // Check checks the Logo for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
| @@ -13,11 +13,16 @@ type OutOfLineContent struct { | |||||||
| 	SRC  string `xml:"src,attr"`            // IRI | 	SRC  string `xml:"src,attr"`            // IRI | ||||||
| } | } | ||||||
|  |  | ||||||
| // newOutOfLineContent creates a new OutOfLineContent. It returns a | // newOutOfLineContent creates a new OutOfLineContent. It takes in the strings | ||||||
| // *OutOfLineContent. | // mediaType and src and returns a *OutOfLineContent. | ||||||
| func newOutOfLineContent(mediaType, src string) *OutOfLineContent { | func newOutOfLineContent(mediaType, src string) *OutOfLineContent { | ||||||
| 	mediaType, _, _ = mime.ParseMediaType(mediaType) | 	mediaType, _, _ = mime.ParseMediaType(mediaType) | ||||||
| 	return &OutOfLineContent{Type: mediaType, SRC: src} |  | ||||||
|  | 	return &OutOfLineContent{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Type:             mediaType, | ||||||
|  | 		SRC:              src, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // isContent checks whether the OutOfLineContent is a Content. It returns a | // isContent checks whether the OutOfLineContent is a Content. It returns a | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								person.go
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								person.go
									
									
									
									
									
								
							| @@ -13,19 +13,28 @@ type Person struct { | |||||||
| 	Extensions []*ExtensionElement `xml:",any,omitempty"` | 	Extensions []*ExtensionElement `xml:",any,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewPerson creates a new Person. It returns a *Person. | // NewPerson creates a new Person. It takes in a string name and returns a | ||||||
|  | // *Person. | ||||||
| func NewPerson(name string) *Person { | func NewPerson(name string) *Person { | ||||||
| 	return &Person{Name: name} | 	return &Person{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Name:             name, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddExtension adds the Extension to the Person. | // AddExtension adds the Extension e to the Person. It returns the index as an | ||||||
| func (p *Person) AddExtension(e *ExtensionElement) { | // int. | ||||||
| 	if p.Extensions == nil { | func (p *Person) AddExtension(e *ExtensionElement) int { | ||||||
| 		p.Extensions = make([]*ExtensionElement, 1) | 	return addToSlice(&p.Extensions, e) | ||||||
| 		p.Extensions[0] = e | } | ||||||
| 	} else { |  | ||||||
| 		p.Extensions = append(p.Extensions, e) | // DeleteExtension deletes the Extension at index from the Person. It returns an | ||||||
|  | // error. | ||||||
|  | func (p *Person) DeleteExtension(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&p.Extensions, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting extension %v from person %v: %v", index, p, err) | ||||||
| 	} | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Person for incompatibilities with RFC4287. It returns an | // Check checks the Person for incompatibilities with RFC4287. It returns an | ||||||
| @@ -47,11 +56,9 @@ func (p *Person) Check() error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if p.Extensions != nil { | 	for i, e := range p.Extensions { | ||||||
| 		for i, e := range p.Extensions { | 		if err := e.Check(); err != nil { | ||||||
| 			if err := e.Check(); err != nil { | 			return fmt.Errorf("extension element %v of person %v: %v", i, p, err) | ||||||
| 				return fmt.Errorf("extension element %v of person %v: %v", i, p, err) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								plainText.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								plainText.go
									
									
									
									
									
								
							| @@ -1,8 +1,6 @@ | |||||||
| package atom | package atom | ||||||
|  |  | ||||||
| import ( | import "fmt" | ||||||
| 	"fmt" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type PlainText struct { | type PlainText struct { | ||||||
| 	*CommonAttributes | 	*CommonAttributes | ||||||
| @@ -13,9 +11,14 @@ type PlainText struct { | |||||||
| // isText checks whether the PlainText is a Text. It returns a bool. | // isText checks whether the PlainText is a Text. It returns a bool. | ||||||
| func (p *PlainText) isText() bool { return true } | func (p *PlainText) isText() bool { return true } | ||||||
|  |  | ||||||
| // newPlainText creates a new PlainText. It returns a *PlainText. | // newPlainText creates a new PlainText. It takes in the strings textType and | ||||||
|  | // content and returns a *PlainText. | ||||||
| func newPlainText(textType, content string) *PlainText { | func newPlainText(textType, content string) *PlainText { | ||||||
| 	return &PlainText{Type: textType, Text: content} | 	return &PlainText{ | ||||||
|  | 		CommonAttributes: NewCommonAttributes(), | ||||||
|  | 		Type:             textType, | ||||||
|  | 		Text:             content, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the PlainText for incompatibilities with RFC4287. It returns an | // Check checks the PlainText for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								source.go
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								source.go
									
									
									
									
									
								
							| @@ -25,7 +25,79 @@ type Source struct { | |||||||
|  |  | ||||||
| // NewSource creates a new Source. It returns a *Source. | // NewSource creates a new Source. It returns a *Source. | ||||||
| func NewSource() *Source { | func NewSource() *Source { | ||||||
| 	return new(Source) | 	return &Source{CommonAttributes: NewCommonAttributes()} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddAuthor adds the Person a as an author to the Source. It returns the index | ||||||
|  | // as an int. | ||||||
|  | func (s *Source) AddAuthor(a *Person) int { | ||||||
|  | 	return addToSlice(&s.Authors, a) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteAuthor deletes the Person at index from the Source. It returns an | ||||||
|  | // error. | ||||||
|  | func (s *Source) DeleteAuthor(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&s.Authors, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting author %v from source %v: %v", index, s, err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddCategory adds the Category c to the Source. It returns the index as an int. | ||||||
|  | func (s *Source) AddCategory(c *Category) int { | ||||||
|  | 	return addToSlice(&s.Categories, c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteCategory deletes the Category at index from the Source. It returns an | ||||||
|  | // error. | ||||||
|  | func (s *Source) DeleteCategory(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&s.Categories, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting category %v from source %v: %v", index, s, err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddContributor adds the Person c as a contributor to the Source. It returns | ||||||
|  | // the index as an int. | ||||||
|  | func (s *Source) AddContributor(c *Person) int { | ||||||
|  | 	return addToSlice(&s.Contributors, c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteContributor deletes the Person at index from the Source. It returns an | ||||||
|  | // error. | ||||||
|  | func (s *Source) DeleteContributor(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&s.Contributors, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting contributor %v from source %v: %v", index, s, err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddLink adds the Link l to the Source. It returns the index as an int. | ||||||
|  | func (s *Source) AddLink(l *Link) int { | ||||||
|  | 	return addToSlice(&s.Links, l) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteLink deletes the Link at index from the Source. It returns an error. | ||||||
|  | func (s *Source) DeleteLink(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&s.Links, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting link %v from source %v: %v", index, s, err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddExtension adds the ExtensionElement e to the Source. It returns the index | ||||||
|  | // as an int. | ||||||
|  | func (s *Source) AddExtension(e *ExtensionElement) int { | ||||||
|  | 	return addToSlice(&s.Extensions, e) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteExtension deletes the Extension at index from the Source. It returns an | ||||||
|  | // error. | ||||||
|  | func (s *Source) DeleteExtension(index int) error { | ||||||
|  | 	if err := deleteFromSlice(&s.Extensions, index); err != nil { | ||||||
|  | 		return fmt.Errorf("error deleting extension %v from source %v: %v", index, s, err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check checks the Source for incompatibilities with RFC4287. It returns an | // Check checks the Source for incompatibilities with RFC4287. It returns an | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								text.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								text.go
									
									
									
									
									
								
							| @@ -7,7 +7,10 @@ type Text interface { | |||||||
| 	Check() error | 	Check() error | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewText creates a new Text. It returns a Text. | // NewText creates a new Text. It takes in the strings textType and content and | ||||||
|  | // returns a Text. | ||||||
|  | // | ||||||
|  | // If textType is invalid it returns nil. | ||||||
| func NewText(textType, content string) Text { | func NewText(textType, content string) Text { | ||||||
| 	switch textType { | 	switch textType { | ||||||
| 	case "text", "": | 	case "text", "": | ||||||
|   | |||||||
| @@ -11,7 +11,8 @@ type XHTMLDiv struct { | |||||||
| 	Content string   `xml:",innerxml"` | 	Content string   `xml:",innerxml"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewXHTMLDiv creates a new XHTMLDiv. It returns a *XHTMLDiv. | // NewXHTMLDiv creates a new XHTMLDiv. It takes in a string content and returns | ||||||
|  | // a *XHTMLDiv. | ||||||
| func NewXHTMLDiv(content string) *XHTMLDiv { | func NewXHTMLDiv(content string) *XHTMLDiv { | ||||||
| 	return &XHTMLDiv{ | 	return &XHTMLDiv{ | ||||||
| 		XMLNS:   "http://www.w3.org/1999/xhtml", | 		XMLNS:   "http://www.w3.org/1999/xhtml", | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								xhtmlText.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								xhtmlText.go
									
									
									
									
									
								
							| @@ -1,8 +1,6 @@ | |||||||
| package atom | package atom | ||||||
|  |  | ||||||
| import ( | import "fmt" | ||||||
| 	"fmt" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type XHTMLText struct { | type XHTMLText struct { | ||||||
| 	*CommonAttributes | 	*CommonAttributes | ||||||
| @@ -13,11 +11,13 @@ type XHTMLText struct { | |||||||
| // isText checks whether the XHTMLText is a Text. It returns a bool. | // isText checks whether the XHTMLText is a Text. It returns a bool. | ||||||
| func (x *XHTMLText) isText() bool { return true } | func (x *XHTMLText) isText() bool { return true } | ||||||
|  |  | ||||||
| // newPlainText creates a new PlainText. It returns a *PlainText. | // newPlainText creates a new PlainText. It takes in the strings textType and | ||||||
|  | // content and returns a *PlainText. | ||||||
| func newXHTMLText(textType, content string) *XHTMLText { | func newXHTMLText(textType, content string) *XHTMLText { | ||||||
| 	return &XHTMLText{ | 	return &XHTMLText{ | ||||||
| 		Type:     textType, | 		CommonAttributes: NewCommonAttributes(), | ||||||
| 		XHTMLDiv: NewXHTMLDiv(content), | 		Type:             textType, | ||||||
|  | 		XHTMLDiv:         NewXHTMLDiv(content), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user