atom_common.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package atom // import "miniflux.app/v2/internal/reader/atom"
  4. import (
  5. "cmp"
  6. "slices"
  7. "strings"
  8. )
  9. // Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-3.2
  10. type AtomPerson struct {
  11. // The "atom:name" element's content conveys a human-readable name for the author.
  12. // It MAY be the name of a corporation or other entity no individual authors can be named.
  13. // Person constructs MUST contain exactly one "atom:name" element, whose content MUST be a string.
  14. Name string `xml:"name"`
  15. // The "atom:email" element's content conveys an e-mail address associated with the Person construct.
  16. // Person constructs MAY contain an atom:email element, but MUST NOT contain more than one.
  17. // Its content MUST be an e-mail address [RFC2822].
  18. // Ordering of the element children of Person constructs MUST NOT be considered significant.
  19. Email string `xml:"email"`
  20. }
  21. func (a *AtomPerson) PersonName() string {
  22. name := strings.TrimSpace(a.Name)
  23. if name != "" {
  24. return name
  25. }
  26. return strings.TrimSpace(a.Email)
  27. }
  28. type atomPersons []*AtomPerson
  29. // personNames returns sorted and deduplicated author names.
  30. func (a atomPersons) personNames() []string {
  31. return makeSorted((*AtomPerson).PersonName, a)
  32. }
  33. // Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.7
  34. type AtomLink struct {
  35. Href string `xml:"href,attr"`
  36. Type string `xml:"type,attr"`
  37. Rel string `xml:"rel,attr"`
  38. Length string `xml:"length,attr"`
  39. Title string `xml:"title,attr"`
  40. }
  41. type atomLinks []*AtomLink
  42. func (a atomLinks) originalLink() string {
  43. for _, link := range a {
  44. if strings.EqualFold(link.Rel, "alternate") {
  45. return strings.TrimSpace(link.Href)
  46. }
  47. if link.Rel == "" && (link.Type == "" || link.Type == "text/html") {
  48. return strings.TrimSpace(link.Href)
  49. }
  50. }
  51. return ""
  52. }
  53. func (a atomLinks) firstLinkWithRelation(relation string) string {
  54. for _, link := range a {
  55. if strings.EqualFold(link.Rel, relation) {
  56. return strings.TrimSpace(link.Href)
  57. }
  58. }
  59. return ""
  60. }
  61. func (a atomLinks) firstLinkWithRelationAndType(relation string, contentTypes ...string) string {
  62. for _, link := range a {
  63. if strings.EqualFold(link.Rel, relation) {
  64. for _, contentType := range contentTypes {
  65. if strings.EqualFold(link.Type, contentType) {
  66. return strings.TrimSpace(link.Href)
  67. }
  68. }
  69. }
  70. }
  71. return ""
  72. }
  73. func (a atomLinks) findAllLinksWithRelation(relation string) []*AtomLink {
  74. links := make([]*AtomLink, 0, len(a))
  75. for _, link := range a {
  76. if strings.EqualFold(link.Rel, relation) {
  77. link.Href = strings.TrimSpace(link.Href)
  78. if link.Href != "" {
  79. links = append(links, link)
  80. }
  81. }
  82. }
  83. return links
  84. }
  85. // The "atom:category" element conveys information about a category
  86. // associated with an entry or feed. This specification assigns no
  87. // meaning to the content (if any) of this element.
  88. //
  89. // Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.2
  90. type atomCategory struct {
  91. // The "term" attribute is a string that identifies the category to
  92. // which the entry or feed belongs. Category elements MUST have a
  93. // "term" attribute.
  94. Term string `xml:"term,attr"`
  95. // The "scheme" attribute is an IRI that identifies a categorization
  96. // scheme. Category elements MAY have a "scheme" attribute.
  97. Scheme string `xml:"scheme,attr"`
  98. // The "label" attribute provides a human-readable label for display in
  99. // end-user applications. The content of the "label" attribute is
  100. // Language-Sensitive. Entities such as "&" and "<" represent
  101. // their corresponding characters ("&" and "<", respectively), not
  102. // markup. Category elements MAY have a "label" attribute.
  103. Label string `xml:"label,attr"`
  104. }
  105. func (ac atomCategory) name() string {
  106. name := strings.TrimSpace(ac.Label)
  107. if name != "" {
  108. return name
  109. }
  110. name = strings.TrimSpace(ac.Term)
  111. if name != "" {
  112. return name
  113. }
  114. return ""
  115. }
  116. type atomCategories []atomCategory
  117. // CategoryNames returns sorted and deduplicated category names.
  118. func (ac atomCategories) CategoryNames() []string {
  119. return makeSorted(atomCategory.name, ac)
  120. }
  121. func makeSorted[I any, O cmp.Ordered](fn func(I) O, values []I) []O {
  122. var zero O
  123. sorted := make([]O, 0, len(values))
  124. for _, in := range values {
  125. out := fn(in)
  126. if out == zero {
  127. continue
  128. }
  129. where, found := slices.BinarySearch(sorted, out)
  130. if found {
  131. continue
  132. }
  133. // Insert sorted to avoid duplicates.
  134. sorted = slices.Insert(sorted, where, out)
  135. }
  136. return sorted
  137. }