feedparser-rs 0.4.7__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/Cargo.lock +3 -3
  2. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/Cargo.toml +1 -1
  3. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/PKG-INFO +1 -1
  4. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/parse_file.rs +1 -1
  5. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/podcast_feed.rs +8 -4
  6. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/lib.rs +3 -3
  7. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/dublin_core.rs +39 -15
  8. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/georss.rs +23 -23
  9. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/media_rss.rs +3 -3
  10. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/mod.rs +21 -0
  11. feedparser_rs-0.5.0/crates/feedparser-rs-core/src/namespace/slash.rs +117 -0
  12. feedparser_rs-0.5.0/crates/feedparser-rs-core/src/namespace/threading.rs +264 -0
  13. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/atom.rs +879 -40
  14. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/common.rs +380 -26
  15. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/json.rs +204 -10
  16. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/mod.rs +36 -20
  17. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/namespace_detection.rs +6 -0
  18. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/rss.rs +1124 -87
  19. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/rss10.rs +141 -9
  20. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/common.rs +114 -32
  21. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/entry.rs +67 -12
  22. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/feed.rs +27 -18
  23. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/mod.rs +2 -0
  24. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/podcast.rs +8 -0
  25. feedparser_rs-0.5.0/crates/feedparser-rs-core/src/types/thread.rs +113 -0
  26. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/util/date.rs +99 -8
  27. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/util/encoding.rs +25 -14
  28. feedparser_rs-0.5.0/crates/feedparser-rs-core/src/util/text.rs +174 -0
  29. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/encoding_integration.rs +97 -0
  30. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/guid_entity_tests.rs +113 -0
  31. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/integration_tests.rs +844 -0
  32. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/issue45_test.rs +1 -1
  33. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/namespace_edge_cases.rs +19 -18
  34. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/namespace_integration.rs +150 -16
  35. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_adversarial_input.rs +120 -0
  36. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_author_detail.rs +122 -0
  37. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_entry_author_precedence.rs +65 -0
  38. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_georss_cc_namespaces.rs +261 -0
  39. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_parser_limits.rs +527 -0
  40. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/test_rss10.rs +11 -10
  41. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_slash_wfw_namespaces.rs +153 -0
  42. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_thr_namespace.rs +59 -0
  43. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_threading_namespace.rs +191 -0
  44. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_timezone_preservation.rs +239 -0
  45. feedparser_rs-0.5.0/crates/feedparser-rs-core/tests/test_truncated_feeds.rs +77 -0
  46. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/lib.rs +1 -0
  47. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/common.rs +120 -18
  48. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/entry.rs +360 -41
  49. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/feed_meta.rs +199 -40
  50. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/media.rs +58 -10
  51. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/mod.rs +1 -0
  52. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/parsed_feed.rs +14 -0
  53. feedparser_rs-0.5.0/crates/feedparser-rs-py/src/types/thread.rs +103 -0
  54. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/tests/test_basic.py +120 -0
  55. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/tests/test_bindings.py +129 -42
  56. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/tests/test_compat.py +2 -2
  57. feedparser_rs-0.5.0/crates/feedparser-rs-py/tests/test_dict_methods.py +191 -0
  58. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/tests/test_integration.py +1 -1
  59. feedparser_rs-0.5.0/crates/feedparser-rs-py/tests/test_itunes_flat_keys.py +84 -0
  60. feedparser_rs-0.5.0/crates/feedparser-rs-py/tests/test_subscriptable.py +196 -0
  61. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/uv.lock +1 -1
  62. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/pyproject.toml +1 -1
  63. feedparser_rs-0.4.7/crates/feedparser-rs-core/src/util/text.rs +0 -53
  64. feedparser_rs-0.4.7/crates/feedparser-rs-core/tests/integration_tests.rs +0 -296
  65. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/README.md +0 -0
  66. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/Cargo.toml +0 -0
  67. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/README.md +0 -0
  68. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/benches/parsing.rs +0 -0
  69. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/benches/types.rs +0 -0
  70. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/error_handling.rs +0 -0
  71. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/feeds/malformed_feed.xml +0 -0
  72. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/feeds/sample_atom.xml +0 -0
  73. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/feeds/sample_podcast.xml +0 -0
  74. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/feeds/sample_rss.xml +0 -0
  75. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/parse_url.rs +0 -0
  76. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/examples/profile_memory.rs +0 -0
  77. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/compat/mod.rs +0 -0
  78. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/error.rs +0 -0
  79. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/http/client.rs +0 -0
  80. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/http/mod.rs +0 -0
  81. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/http/response.rs +0 -0
  82. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/http/validation.rs +0 -0
  83. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/limits.rs +0 -0
  84. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/cc.rs +0 -0
  85. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/content.rs +0 -0
  86. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/namespace/syndication.rs +0 -0
  87. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/options.rs +0 -0
  88. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/parser/detect.rs +0 -0
  89. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/generics.rs +0 -0
  90. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/types/version.rs +0 -0
  91. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/util/base_url.rs +0 -0
  92. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/util/mod.rs +0 -0
  93. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/src/util/sanitize.rs +0 -0
  94. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/http_integration.rs +0 -0
  95. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/test_podcast_namespace.rs +0 -0
  96. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/test_url_resolution.rs +0 -0
  97. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-core/tests/test_url_security.rs +0 -0
  98. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/Cargo.toml +0 -0
  99. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/README.md +0 -0
  100. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/error.rs +0 -0
  101. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/limits.rs +0 -0
  102. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/compat.rs +0 -0
  103. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/datetime.rs +0 -0
  104. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/geo.rs +0 -0
  105. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/podcast.rs +0 -0
  106. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/src/types/syndication.rs +0 -0
  107. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/tests/test_guid_entities.py +0 -0
  108. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/crates/feedparser-rs-py/tests/test_syndication.py +0 -0
  109. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/python/feedparser_rs/__init__.py +0 -0
  110. {feedparser_rs-0.4.7 → feedparser_rs-0.5.0}/python/feedparser_rs/py.typed +0 -0
@@ -600,7 +600,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
600
600
 
601
601
  [[package]]
602
602
  name = "feedparser-rs"
603
- version = "0.4.7"
603
+ version = "0.5.0"
604
604
  dependencies = [
605
605
  "ammonia",
606
606
  "chrono",
@@ -623,7 +623,7 @@ dependencies = [
623
623
 
624
624
  [[package]]
625
625
  name = "feedparser-rs-node"
626
- version = "0.4.7"
626
+ version = "0.5.0"
627
627
  dependencies = [
628
628
  "feedparser-rs",
629
629
  "napi",
@@ -633,7 +633,7 @@ dependencies = [
633
633
 
634
634
  [[package]]
635
635
  name = "feedparser-rs-py"
636
- version = "0.4.7"
636
+ version = "0.5.0"
637
637
  dependencies = [
638
638
  "chrono",
639
639
  "feedparser-rs",
@@ -3,7 +3,7 @@ members = ["crates/feedparser-rs-core", "crates/feedparser-rs-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.4.7"
6
+ version = "0.5.0"
7
7
  edition = "2024"
8
8
  rust-version = "1.88.0"
9
9
  authors = ["bug-ops"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feedparser-rs
3
- Version: 0.4.7
3
+ Version: 0.5.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -109,7 +109,7 @@ fn parse_rss_example() -> Result<(), Box<dyn std::error::Error>> {
109
109
  if let Some(enclosure_type) = &enc.enclosure_type {
110
110
  println!(" Type: {enclosure_type}");
111
111
  }
112
- if let Some(length) = enc.length {
112
+ if let Some(length) = &enc.length {
113
113
  println!(" Size: {length} bytes");
114
114
  }
115
115
  }
@@ -176,10 +176,14 @@ fn display_episodes(feed: &feedparser_rs::ParsedFeed) {
176
176
  if let Some(enclosure_type) = &enc.enclosure_type {
177
177
  println!(" Type: {enclosure_type}");
178
178
  }
179
- if let Some(length) = enc.length {
180
- #[allow(clippy::cast_precision_loss)]
181
- let mb = length as f64 / 1_048_576.0;
182
- println!(" Size: {mb:.2} MB ({length} bytes)");
179
+ if let Some(length) = &enc.length {
180
+ if let Ok(bytes) = length.parse::<u64>() {
181
+ #[allow(clippy::cast_precision_loss)]
182
+ let mb = bytes as f64 / 1_048_576.0;
183
+ println!(" Size: {mb:.2} MB ({bytes} bytes)");
184
+ } else {
185
+ println!(" Size: {length} bytes");
186
+ }
183
187
  }
184
188
  }
185
189
  }
@@ -188,9 +188,9 @@ pub use limits::{LimitError, ParserLimits};
188
188
  pub use options::ParseOptions;
189
189
  pub use parser::{detect_format, parse, parse_with_limits};
190
190
  pub use types::{
191
- Content, Email, Enclosure, Entry, FeedMeta, FeedVersion, Generator, Image, ItunesCategory,
192
- ItunesEntryMeta, ItunesFeedMeta, ItunesOwner, LimitedCollectionExt, Link, MediaContent,
193
- MediaThumbnail, MimeType, ParsedFeed, Person, PodcastChapters, PodcastEntryMeta,
191
+ Content, Email, Enclosure, Entry, FeedMeta, FeedVersion, Generator, Image, InReplyTo,
192
+ ItunesCategory, ItunesEntryMeta, ItunesFeedMeta, ItunesOwner, LimitedCollectionExt, Link,
193
+ MediaContent, MediaThumbnail, MimeType, ParsedFeed, Person, PodcastChapters, PodcastEntryMeta,
194
194
  PodcastFunding, PodcastMeta, PodcastPerson, PodcastSoundbite, PodcastTranscript, PodcastValue,
195
195
  PodcastValueRecipient, Source, Tag, TextConstruct, TextType, Url, parse_duration,
196
196
  parse_explicit,
@@ -32,10 +32,8 @@ pub const DC_NAMESPACE: &str = "http://purl.org/dc/elements/1.1/";
32
32
  pub fn handle_feed_element(element: &str, text: &str, feed: &mut FeedMeta) {
33
33
  match element {
34
34
  "creator" => {
35
- // dc:creator → author (if not already set)
36
- if feed.author.is_none() {
37
- feed.author = Some(text.into());
38
- }
35
+ // dc:creator → author (always overrides)
36
+ feed.author = Some(text.into());
39
37
  // Store in dc_creator field
40
38
  feed.dc_creator = Some(text.into());
41
39
  // Also add to authors list
@@ -47,6 +45,7 @@ pub fn handle_feed_element(element: &str, text: &str, feed: &mut FeedMeta) {
47
45
  && feed.updated.is_none()
48
46
  {
49
47
  feed.updated = Some(dt);
48
+ feed.updated_str = Some(text.to_string());
50
49
  }
51
50
  }
52
51
  "subject" => {
@@ -111,18 +110,16 @@ pub fn handle_feed_element(element: &str, text: &str, feed: &mut FeedMeta) {
111
110
  pub fn handle_entry_element(element: &str, text: &str, entry: &mut Entry) {
112
111
  match element {
113
112
  "creator" => {
114
- if entry.author.is_none() {
115
- entry.author = Some(text.into());
116
- }
113
+ // dc:creator takes precedence over <author>; reconciled at </item>
117
114
  entry.dc_creator = Some(text.into());
118
115
  entry.authors.push(Person::from_name(text));
119
116
  }
120
117
  "date" => {
121
118
  if let Some(dt) = parse_date(text) {
122
119
  entry.dc_date = Some(dt);
123
- // Prefer published over updated for entries
124
- if entry.published.is_none() {
125
- entry.published = Some(dt);
120
+ if entry.updated.is_none() {
121
+ entry.updated = Some(dt);
122
+ entry.updated_str = Some(text.to_string());
126
123
  }
127
124
  }
128
125
  }
@@ -149,6 +146,9 @@ pub fn handle_entry_element(element: &str, text: &str, entry: &mut Entry) {
149
146
  entry.contributors.push(Person::from_name(text));
150
147
  }
151
148
  "rights" => {
149
+ if entry.rights.is_none() {
150
+ entry.rights = Some(text.to_string());
151
+ }
152
152
  entry.dc_rights = Some(text.to_string());
153
153
  }
154
154
  _ => {
@@ -177,8 +177,8 @@ mod tests {
177
177
  handle_feed_element("creator", "Alice", &mut feed);
178
178
  handle_feed_element("creator", "Bob", &mut feed);
179
179
 
180
- // First creator becomes primary author
181
- assert_eq!(feed.author.as_deref(), Some("Alice"));
180
+ // Last dc:creator wins (always overrides)
181
+ assert_eq!(feed.author.as_deref(), Some("Bob"));
182
182
  // Both are in authors list
183
183
  assert_eq!(feed.authors.len(), 2);
184
184
  }
@@ -232,16 +232,40 @@ mod tests {
232
232
  handle_entry_element("subject", "Tech", &mut entry);
233
233
  handle_entry_element("description", "Entry summary", &mut entry);
234
234
 
235
- assert_eq!(entry.author.as_deref(), Some("Jane Doe"));
235
+ // dc:creator is stored in dc_creator; entry.author is reconciled at </item> in rss.rs
236
+ assert_eq!(entry.dc_creator.as_deref(), Some("Jane Doe"));
236
237
  assert_eq!(entry.tags.len(), 1);
237
238
  assert_eq!(entry.summary.as_deref(), Some("Entry summary"));
238
239
  }
239
240
 
240
241
  #[test]
241
- fn test_entry_published_from_dc_date() {
242
+ fn test_entry_updated_from_dc_date() {
242
243
  let mut entry = Entry::default();
243
244
  handle_entry_element("date", "2024-01-15T10:30:00Z", &mut entry);
244
245
 
245
- assert!(entry.published.is_some());
246
+ assert!(entry.updated.is_some());
247
+ assert!(entry.published.is_none());
248
+ }
249
+
250
+ #[test]
251
+ fn test_entry_dc_rights_sets_both_fields() {
252
+ let mut entry = Entry::default();
253
+ handle_entry_element("rights", "Copyright 2024 Example", &mut entry);
254
+
255
+ assert_eq!(entry.rights.as_deref(), Some("Copyright 2024 Example"));
256
+ assert_eq!(entry.dc_rights.as_deref(), Some("Copyright 2024 Example"));
257
+ }
258
+
259
+ #[test]
260
+ fn test_entry_dc_rights_does_not_overwrite_rights() {
261
+ let mut entry = Entry {
262
+ rights: Some("Atom rights".to_string()),
263
+ ..Default::default()
264
+ };
265
+ handle_entry_element("rights", "DC rights", &mut entry);
266
+
267
+ // dc:rights should not overwrite rights already set (e.g. by Atom <rights>)
268
+ assert_eq!(entry.rights.as_deref(), Some("Atom rights"));
269
+ assert_eq!(entry.dc_rights.as_deref(), Some("DC rights"));
246
270
  }
247
271
  }
@@ -178,25 +178,25 @@ pub fn handle_entry_element(
178
178
  match tag {
179
179
  b"point" => {
180
180
  if let Some(loc) = parse_point(text) {
181
- entry.geo = Some(Box::new(loc));
181
+ entry.r#where = Some(Box::new(loc));
182
182
  }
183
183
  true
184
184
  }
185
185
  b"line" => {
186
186
  if let Some(loc) = parse_line(text) {
187
- entry.geo = Some(Box::new(loc));
187
+ entry.r#where = Some(Box::new(loc));
188
188
  }
189
189
  true
190
190
  }
191
191
  b"polygon" => {
192
192
  if let Some(loc) = parse_polygon(text) {
193
- entry.geo = Some(Box::new(loc));
193
+ entry.r#where = Some(Box::new(loc));
194
194
  }
195
195
  true
196
196
  }
197
197
  b"box" => {
198
198
  if let Some(loc) = parse_box(text) {
199
- entry.geo = Some(Box::new(loc));
199
+ entry.r#where = Some(Box::new(loc));
200
200
  }
201
201
  true
202
202
  }
@@ -225,25 +225,25 @@ pub fn handle_feed_element(
225
225
  match tag {
226
226
  b"point" => {
227
227
  if let Some(loc) = parse_point(text) {
228
- feed.geo = Some(Box::new(loc));
228
+ feed.r#where = Some(Box::new(loc));
229
229
  }
230
230
  true
231
231
  }
232
232
  b"line" => {
233
233
  if let Some(loc) = parse_line(text) {
234
- feed.geo = Some(Box::new(loc));
234
+ feed.r#where = Some(Box::new(loc));
235
235
  }
236
236
  true
237
237
  }
238
238
  b"polygon" => {
239
239
  if let Some(loc) = parse_polygon(text) {
240
- feed.geo = Some(Box::new(loc));
240
+ feed.r#where = Some(Box::new(loc));
241
241
  }
242
242
  true
243
243
  }
244
244
  b"box" => {
245
245
  if let Some(loc) = parse_box(text) {
246
- feed.geo = Some(Box::new(loc));
246
+ feed.r#where = Some(Box::new(loc));
247
247
  }
248
248
  true
249
249
  }
@@ -426,9 +426,9 @@ mod tests {
426
426
 
427
427
  let handled = handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
428
428
  assert!(handled);
429
- assert!(entry.geo.is_some());
429
+ assert!(entry.r#where.is_some());
430
430
 
431
- let geo = entry.geo.as_ref().unwrap();
431
+ let geo = entry.r#where.as_ref().unwrap();
432
432
  assert_eq!(geo.geo_type, GeoType::Point);
433
433
  assert_eq!(geo.coordinates[0], (45.256, -71.92));
434
434
  }
@@ -441,8 +441,8 @@ mod tests {
441
441
  let handled =
442
442
  handle_entry_element(b"line", "45.256 -71.92 46.0 -72.0", &mut entry, &limits);
443
443
  assert!(handled);
444
- assert!(entry.geo.is_some());
445
- assert_eq!(entry.geo.as_ref().unwrap().geo_type, GeoType::Line);
444
+ assert!(entry.r#where.is_some());
445
+ assert_eq!(entry.r#where.as_ref().unwrap().geo_type, GeoType::Line);
446
446
  }
447
447
 
448
448
  #[test]
@@ -452,7 +452,7 @@ mod tests {
452
452
 
453
453
  let handled = handle_entry_element(b"unknown", "data", &mut entry, &limits);
454
454
  assert!(!handled);
455
- assert!(entry.geo.is_none());
455
+ assert!(entry.r#where.is_none());
456
456
  }
457
457
 
458
458
  #[test]
@@ -487,9 +487,9 @@ mod tests {
487
487
 
488
488
  let handled = handle_feed_element(b"point", "45.256 -71.92", &mut feed, &limits);
489
489
  assert!(handled);
490
- assert!(feed.geo.is_some());
490
+ assert!(feed.r#where.is_some());
491
491
 
492
- let geo = feed.geo.as_ref().unwrap();
492
+ let geo = feed.r#where.as_ref().unwrap();
493
493
  assert_eq!(geo.geo_type, GeoType::Point);
494
494
  assert_eq!(geo.coordinates[0], (45.256, -71.92));
495
495
  }
@@ -501,8 +501,8 @@ mod tests {
501
501
 
502
502
  let handled = handle_feed_element(b"line", "45.256 -71.92 46.0 -72.0", &mut feed, &limits);
503
503
  assert!(handled);
504
- assert!(feed.geo.is_some());
505
- assert_eq!(feed.geo.as_ref().unwrap().geo_type, GeoType::Line);
504
+ assert!(feed.r#where.is_some());
505
+ assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Line);
506
506
  }
507
507
 
508
508
  #[test]
@@ -517,8 +517,8 @@ mod tests {
517
517
  &limits,
518
518
  );
519
519
  assert!(handled);
520
- assert!(feed.geo.is_some());
521
- assert_eq!(feed.geo.as_ref().unwrap().geo_type, GeoType::Polygon);
520
+ assert!(feed.r#where.is_some());
521
+ assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Polygon);
522
522
  }
523
523
 
524
524
  #[test]
@@ -528,8 +528,8 @@ mod tests {
528
528
 
529
529
  let handled = handle_feed_element(b"box", "45.0 -72.0 46.0 -71.0", &mut feed, &limits);
530
530
  assert!(handled);
531
- assert!(feed.geo.is_some());
532
- assert_eq!(feed.geo.as_ref().unwrap().geo_type, GeoType::Box);
531
+ assert!(feed.r#where.is_some());
532
+ assert_eq!(feed.r#where.as_ref().unwrap().geo_type, GeoType::Box);
533
533
  }
534
534
 
535
535
  #[test]
@@ -539,7 +539,7 @@ mod tests {
539
539
 
540
540
  let handled = handle_feed_element(b"unknown", "data", &mut feed, &limits);
541
541
  assert!(!handled);
542
- assert!(feed.geo.is_none());
542
+ assert!(feed.r#where.is_none());
543
543
  }
544
544
 
545
545
  #[test]
@@ -549,6 +549,6 @@ mod tests {
549
549
 
550
550
  let handled = handle_feed_element(b"point", "invalid data", &mut feed, &limits);
551
551
  assert!(handled);
552
- assert!(feed.geo.is_none());
552
+ assert!(feed.r#where.is_none());
553
553
  }
554
554
  }
@@ -198,13 +198,13 @@ pub fn handle_entry_element(element: &str, text: &str, entry: &mut Entry) {
198
198
  /// let enclosure = media_content_to_enclosure(&content);
199
199
  /// assert_eq!(enclosure.url, "https://example.com/video.mp4");
200
200
  /// assert_eq!(enclosure.enclosure_type.as_deref(), Some("video/mp4"));
201
- /// assert_eq!(enclosure.length, Some(1_024_000));
201
+ /// assert_eq!(enclosure.length.as_deref(), Some("1024000"));
202
202
  /// ```
203
203
  pub fn media_content_to_enclosure(content: &MediaContent) -> Enclosure {
204
204
  Enclosure {
205
205
  url: content.url.clone().into(),
206
206
  enclosure_type: content.type_.as_ref().map(|t| t.clone().into()),
207
- length: content.file_size,
207
+ length: content.file_size.map(|v| v.to_string()),
208
208
  }
209
209
  }
210
210
 
@@ -412,7 +412,7 @@ mod tests {
412
412
 
413
413
  assert_eq!(enclosure.url, "https://example.com/video.mp4");
414
414
  assert_eq!(enclosure.enclosure_type.as_deref(), Some("video/mp4"));
415
- assert_eq!(enclosure.length, Some(1_024_000));
415
+ assert_eq!(enclosure.length.as_deref(), Some("1024000"));
416
416
  }
417
417
 
418
418
  #[test]
@@ -8,6 +8,8 @@
8
8
  /// - **Media RSS** (`media:`) - Multimedia content
9
9
  /// - **GeoRSS** (`georss:`) - Geographic location data
10
10
  /// - **Creative Commons** (`cc:`) - License information
11
+ /// - **Slash** (`slash:`) - Comment count
12
+ /// - **WFW** (`wfw:`) - Comment RSS URL
11
13
  ///
12
14
  /// # Usage
13
15
  ///
@@ -35,8 +37,12 @@ pub mod dublin_core;
35
37
  pub mod georss;
36
38
  /// Media RSS specification
37
39
  pub mod media_rss;
40
+ /// Slash and WFW namespace (comment count + comment RSS URL)
41
+ pub mod slash;
38
42
  /// Syndication Module for RSS 1.0
39
43
  pub mod syndication;
44
+ /// Atom Threading Extensions (RFC 4685)
45
+ pub mod threading;
40
46
 
41
47
  /// Common namespace URIs used in feeds
42
48
  pub mod namespaces {
@@ -75,6 +81,15 @@ pub mod namespaces {
75
81
 
76
82
  /// Creative Commons (legacy Userland)
77
83
  pub const CREATIVE_COMMONS: &str = "http://backend.userland.com/creativeCommonsRssModule";
84
+
85
+ /// Atom Threading Extensions (RFC 4685)
86
+ pub const THREADING: &str = "http://purl.org/syndication/thread/1.0";
87
+
88
+ /// Slash namespace
89
+ pub const SLASH: &str = "http://purl.org/rss/1.0/modules/slash/";
90
+
91
+ /// WFW (Well-Formed Web) namespace
92
+ pub const WFW: &str = "http://wellformedweb.org/CommentAPI/";
78
93
  }
79
94
 
80
95
  /// Get namespace URI for a common prefix
@@ -99,6 +114,9 @@ pub fn get_namespace_uri(prefix: &str) -> Option<&'static str> {
99
114
  "georss" => Some(namespaces::GEORSS),
100
115
  "cc" => Some(namespaces::CC),
101
116
  "creativeCommons" => Some(namespaces::CREATIVE_COMMONS),
117
+ "thr" => Some(namespaces::THREADING),
118
+ "slash" => Some(namespaces::SLASH),
119
+ "wfw" => Some(namespaces::WFW),
102
120
  _ => None,
103
121
  }
104
122
  }
@@ -125,6 +143,9 @@ pub fn get_namespace_prefix(uri: &str) -> Option<&'static str> {
125
143
  namespaces::GEORSS => Some("georss"),
126
144
  namespaces::CC => Some("cc"),
127
145
  namespaces::CREATIVE_COMMONS => Some("creativeCommons"),
146
+ namespaces::THREADING => Some("thr"),
147
+ namespaces::SLASH => Some("slash"),
148
+ namespaces::WFW => Some("wfw"),
128
149
  _ => None,
129
150
  }
130
151
  }
@@ -0,0 +1,117 @@
1
+ /// Slash namespace — `http://purl.org/rss/1.0/modules/slash/`
2
+ /// WFW (Well-Formed Web) namespace — `http://wellformedweb.org/CommentAPI/`
3
+ ///
4
+ /// These namespaces are commonly used together in `WordPress`, `Blogger`, and
5
+ /// `Movable Type` feeds to expose comment metadata.
6
+ ///
7
+ /// - `slash:comments` — integer count of comments on an entry
8
+ /// - `wfw:commentRss` — URL of the entry's comment RSS feed
9
+ use crate::types::Entry;
10
+
11
+ /// Slash namespace URI
12
+ pub const SLASH_NAMESPACE: &str = "http://purl.org/rss/1.0/modules/slash/";
13
+
14
+ /// WFW namespace URI
15
+ pub const WFW_NAMESPACE: &str = "http://wellformedweb.org/CommentAPI/";
16
+
17
+ /// Handle a Slash namespace element at entry level
18
+ ///
19
+ /// # Arguments
20
+ ///
21
+ /// * `element` - Local name of the element (without namespace prefix)
22
+ /// * `text` - Text content of the element
23
+ /// * `entry` - Entry to update
24
+ pub fn handle_slash_entry_element(element: &str, text: &str, entry: &mut Entry) {
25
+ if element == "comments" {
26
+ // Ignore non-integer values silently (bozo pattern: keep parsing)
27
+ entry.slash_comments = text.trim().parse::<u32>().ok();
28
+ }
29
+ }
30
+
31
+ /// Handle a WFW namespace element at entry level
32
+ ///
33
+ /// # Arguments
34
+ ///
35
+ /// * `element` - Local name of the element (without namespace prefix)
36
+ /// * `text` - Text content of the element
37
+ /// * `entry` - Entry to update
38
+ pub fn handle_wfw_entry_element(element: &str, text: &str, entry: &mut Entry) {
39
+ if element == "commentRss" || element == "commentrss" {
40
+ entry.wfw_comment_rss = Some(text.trim().to_string());
41
+ }
42
+ }
43
+
44
+ #[cfg(test)]
45
+ mod tests {
46
+ use super::*;
47
+
48
+ #[test]
49
+ fn test_slash_comments_integer() {
50
+ let mut entry = Entry::default();
51
+ handle_slash_entry_element("comments", "42", &mut entry);
52
+ assert_eq!(entry.slash_comments, Some(42));
53
+ }
54
+
55
+ #[test]
56
+ fn test_slash_comments_with_whitespace() {
57
+ let mut entry = Entry::default();
58
+ handle_slash_entry_element("comments", " 7 ", &mut entry);
59
+ assert_eq!(entry.slash_comments, Some(7));
60
+ }
61
+
62
+ #[test]
63
+ fn test_slash_comments_zero() {
64
+ let mut entry = Entry::default();
65
+ handle_slash_entry_element("comments", "0", &mut entry);
66
+ assert_eq!(entry.slash_comments, Some(0));
67
+ }
68
+
69
+ #[test]
70
+ fn test_slash_comments_invalid_is_ignored() {
71
+ let mut entry = Entry::default();
72
+ handle_slash_entry_element("comments", "not-a-number", &mut entry);
73
+ assert_eq!(entry.slash_comments, None);
74
+ }
75
+
76
+ #[test]
77
+ fn test_slash_unknown_element_ignored() {
78
+ let mut entry = Entry::default();
79
+ handle_slash_entry_element("section", "tech", &mut entry);
80
+ assert_eq!(entry.slash_comments, None);
81
+ }
82
+
83
+ #[test]
84
+ fn test_wfw_comment_rss() {
85
+ let mut entry = Entry::default();
86
+ handle_wfw_entry_element(
87
+ "commentRss",
88
+ "https://example.com/post/comments/feed/",
89
+ &mut entry,
90
+ );
91
+ assert_eq!(
92
+ entry.wfw_comment_rss.as_deref(),
93
+ Some("https://example.com/post/comments/feed/")
94
+ );
95
+ }
96
+
97
+ #[test]
98
+ fn test_wfw_comment_rss_lowercase_variant() {
99
+ let mut entry = Entry::default();
100
+ handle_wfw_entry_element(
101
+ "commentrss",
102
+ "https://example.com/post/comments/feed/",
103
+ &mut entry,
104
+ );
105
+ assert_eq!(
106
+ entry.wfw_comment_rss.as_deref(),
107
+ Some("https://example.com/post/comments/feed/")
108
+ );
109
+ }
110
+
111
+ #[test]
112
+ fn test_wfw_unknown_element_ignored() {
113
+ let mut entry = Entry::default();
114
+ handle_wfw_entry_element("other", "value", &mut entry);
115
+ assert_eq!(entry.wfw_comment_rss, None);
116
+ }
117
+ }