feedparser-rs 0.5.1__tar.gz → 0.5.2__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 (111) hide show
  1. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/Cargo.lock +11 -11
  2. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/Cargo.toml +1 -1
  3. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/PKG-INFO +1 -1
  4. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/limits.rs +48 -0
  5. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/dublin_core.rs +11 -3
  6. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/georss.rs +240 -18
  7. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/atom.rs +160 -38
  8. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/common.rs +187 -24
  9. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/json.rs +188 -2
  10. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/rss.rs +2248 -1296
  11. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/rss10.rs +109 -59
  12. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/common.rs +6 -0
  13. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/mod.rs +5 -3
  14. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/podcast.rs +269 -0
  15. feedparser_rs-0.5.2/crates/feedparser-rs-core/tests/fixtures/rss10/rss10_channel_selfclosing_refs.xml +26 -0
  16. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_georss_cc_namespaces.rs +51 -0
  17. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_rss10.rs +25 -1
  18. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/lib.rs +2 -0
  19. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/limits.rs +15 -9
  20. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/common.rs +1 -0
  21. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/geo.rs +21 -3
  22. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/podcast.rs +126 -3
  23. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/pyproject.toml +1 -1
  24. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/README.md +0 -0
  25. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/Cargo.toml +0 -0
  26. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/README.md +0 -0
  27. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/benches/parsing.rs +0 -0
  28. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/benches/types.rs +0 -0
  29. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/error_handling.rs +0 -0
  30. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/feeds/malformed_feed.xml +0 -0
  31. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/feeds/sample_atom.xml +0 -0
  32. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/feeds/sample_podcast.xml +0 -0
  33. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/feeds/sample_rss.xml +0 -0
  34. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/parse_file.rs +0 -0
  35. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/parse_url.rs +0 -0
  36. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/podcast_feed.rs +0 -0
  37. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/examples/profile_memory.rs +0 -0
  38. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/compat/mod.rs +0 -0
  39. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/error.rs +0 -0
  40. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/http/client.rs +0 -0
  41. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/http/mod.rs +0 -0
  42. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/http/response.rs +0 -0
  43. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/http/validation.rs +0 -0
  44. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/lib.rs +0 -0
  45. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/cc.rs +0 -0
  46. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/content.rs +0 -0
  47. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/media_rss.rs +0 -0
  48. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/mod.rs +0 -0
  49. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/slash.rs +0 -0
  50. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/syndication.rs +0 -0
  51. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/namespace/threading.rs +0 -0
  52. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/options.rs +0 -0
  53. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/detect.rs +0 -0
  54. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/mod.rs +0 -0
  55. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/parser/namespace_detection.rs +0 -0
  56. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/entry.rs +0 -0
  57. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/feed.rs +0 -0
  58. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/generics.rs +0 -0
  59. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/thread.rs +0 -0
  60. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/types/version.rs +0 -0
  61. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/util/base_url.rs +0 -0
  62. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/util/date.rs +0 -0
  63. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/util/encoding.rs +0 -0
  64. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/util/mod.rs +0 -0
  65. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/util/sanitize.rs +0 -0
  66. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/src/util/text.rs +0 -0
  67. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/encoding_integration.rs +0 -0
  68. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/guid_entity_tests.rs +0 -0
  69. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/http_integration.rs +0 -0
  70. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/integration_tests.rs +0 -0
  71. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/issue45_test.rs +0 -0
  72. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/namespace_edge_cases.rs +0 -0
  73. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/namespace_integration.rs +0 -0
  74. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_adversarial_input.rs +0 -0
  75. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_author_detail.rs +0 -0
  76. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_entry_author_precedence.rs +0 -0
  77. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_media_namespace.rs +0 -0
  78. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_parser_limits.rs +0 -0
  79. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_podcast_namespace.rs +0 -0
  80. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_rss_optional_elements.rs +0 -0
  81. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_slash_wfw_namespaces.rs +0 -0
  82. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_thr_namespace.rs +0 -0
  83. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_threading_namespace.rs +0 -0
  84. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_timezone_preservation.rs +0 -0
  85. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_truncated_feeds.rs +0 -0
  86. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_url_resolution.rs +0 -0
  87. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-core/tests/test_url_security.rs +0 -0
  88. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/Cargo.toml +0 -0
  89. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/README.md +0 -0
  90. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/error.rs +0 -0
  91. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/compat.rs +0 -0
  92. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/datetime.rs +0 -0
  93. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/entry.rs +0 -0
  94. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/feed_meta.rs +0 -0
  95. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/media.rs +0 -0
  96. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/mod.rs +0 -0
  97. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/parsed_feed.rs +0 -0
  98. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/syndication.rs +0 -0
  99. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/src/types/thread.rs +0 -0
  100. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_basic.py +0 -0
  101. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_bindings.py +0 -0
  102. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_compat.py +0 -0
  103. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_dict_methods.py +0 -0
  104. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_guid_entities.py +0 -0
  105. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_integration.py +0 -0
  106. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_itunes_flat_keys.py +0 -0
  107. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_subscriptable.py +0 -0
  108. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/tests/test_syndication.py +0 -0
  109. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/crates/feedparser-rs-py/uv.lock +0 -0
  110. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/python/feedparser_rs/__init__.py +0 -0
  111. {feedparser_rs-0.5.1 → feedparser_rs-0.5.2}/python/feedparser_rs/py.typed +0 -0
@@ -500,9 +500,9 @@ dependencies = [
500
500
 
501
501
  [[package]]
502
502
  name = "ctor"
503
- version = "0.6.3"
503
+ version = "0.8.0"
504
504
  source = "registry+https://github.com/rust-lang/crates.io-index"
505
- checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
505
+ checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
506
506
  dependencies = [
507
507
  "ctor-proc-macro",
508
508
  "dtor",
@@ -558,9 +558,9 @@ dependencies = [
558
558
 
559
559
  [[package]]
560
560
  name = "dtor"
561
- version = "0.1.1"
561
+ version = "0.3.0"
562
562
  source = "registry+https://github.com/rust-lang/crates.io-index"
563
- checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301"
563
+ checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
564
564
  dependencies = [
565
565
  "dtor-proc-macro",
566
566
  ]
@@ -600,7 +600,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
600
600
 
601
601
  [[package]]
602
602
  name = "feedparser-rs"
603
- version = "0.5.1"
603
+ version = "0.5.2"
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.5.1"
626
+ version = "0.5.2"
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.5.1"
636
+ version = "0.5.2"
637
637
  dependencies = [
638
638
  "chrono",
639
639
  "feedparser-rs",
@@ -1330,9 +1330,9 @@ dependencies = [
1330
1330
 
1331
1331
  [[package]]
1332
1332
  name = "napi"
1333
- version = "3.8.3"
1333
+ version = "3.8.4"
1334
1334
  source = "registry+https://github.com/rust-lang/crates.io-index"
1335
- checksum = "e6944d0bf100571cd6e1a98a316cdca262deb6fccf8d93f5ae1502ca3fc88bd3"
1335
+ checksum = "fb7848c221fb7bb789e02f01875287ebb1e078b92a6566a34de01ef8806e7c2b"
1336
1336
  dependencies = [
1337
1337
  "anyhow",
1338
1338
  "bitflags",
@@ -1352,9 +1352,9 @@ checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1"
1352
1352
 
1353
1353
  [[package]]
1354
1354
  name = "napi-derive"
1355
- version = "3.5.2"
1355
+ version = "3.5.3"
1356
1356
  source = "registry+https://github.com/rust-lang/crates.io-index"
1357
- checksum = "2c914b5e420182bfb73504e0607592cdb8e2e21437d450883077669fb72a114d"
1357
+ checksum = "60867ff9a6f76e82350e0c3420cb0736f5866091b61d7d8a024baa54b0ec17dd"
1358
1358
  dependencies = [
1359
1359
  "convert_case",
1360
1360
  "ctor",
@@ -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.5.1"
6
+ version = "0.5.2"
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.5.1
3
+ Version: 0.5.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -143,6 +143,36 @@ pub struct ParserLimits {
143
143
  ///
144
144
  /// Default: 20 recipients
145
145
  pub max_value_recipients: usize,
146
+
147
+ /// Maximum number of alternate enclosures per entry
148
+ ///
149
+ /// Default: 20
150
+ pub max_podcast_alternate_enclosures: usize,
151
+
152
+ /// Maximum number of sources per alternate enclosure
153
+ ///
154
+ /// Default: 10
155
+ pub max_podcast_alternate_enclosure_sources: usize,
156
+
157
+ /// Maximum number of podroll entries per feed
158
+ ///
159
+ /// Default: 50
160
+ pub max_podcast_podroll: usize,
161
+
162
+ /// Maximum number of socialInteract elements per entry
163
+ ///
164
+ /// Default: 20
165
+ pub max_podcast_social_interact: usize,
166
+
167
+ /// Maximum number of txt records per feed or entry
168
+ ///
169
+ /// Default: 20
170
+ pub max_podcast_txt: usize,
171
+
172
+ /// Maximum number of follow links per feed or entry
173
+ ///
174
+ /// Default: 20
175
+ pub max_podcast_follow: usize,
146
176
  }
147
177
 
148
178
  impl Default for ParserLimits {
@@ -170,6 +200,12 @@ impl Default for ParserLimits {
170
200
  max_podcast_funding: 20,
171
201
  max_podcast_persons: 50,
172
202
  max_value_recipients: 20,
203
+ max_podcast_alternate_enclosures: 20,
204
+ max_podcast_alternate_enclosure_sources: 10,
205
+ max_podcast_podroll: 50,
206
+ max_podcast_social_interact: 20,
207
+ max_podcast_txt: 20,
208
+ max_podcast_follow: 20,
173
209
  }
174
210
  }
175
211
  }
@@ -209,6 +245,12 @@ impl ParserLimits {
209
245
  max_podcast_funding: 5,
210
246
  max_podcast_persons: 10,
211
247
  max_value_recipients: 5,
248
+ max_podcast_alternate_enclosures: 5,
249
+ max_podcast_alternate_enclosure_sources: 3,
250
+ max_podcast_podroll: 10,
251
+ max_podcast_social_interact: 5,
252
+ max_podcast_txt: 5,
253
+ max_podcast_follow: 5,
212
254
  }
213
255
  }
214
256
 
@@ -246,6 +288,12 @@ impl ParserLimits {
246
288
  max_podcast_funding: 50,
247
289
  max_podcast_persons: 200,
248
290
  max_value_recipients: 50,
291
+ max_podcast_alternate_enclosures: 100,
292
+ max_podcast_alternate_enclosure_sources: 50,
293
+ max_podcast_podroll: 200,
294
+ max_podcast_social_interact: 100,
295
+ max_podcast_txt: 100,
296
+ max_podcast_follow: 100,
249
297
  }
250
298
  }
251
299
 
@@ -40,8 +40,12 @@ pub fn handle_feed_element(element: &str, text: &str, feed: &mut FeedMeta) {
40
40
  feed.authors.push(Person::from_name(text));
41
41
  }
42
42
  "date" => {
43
- // dc:date → updated (always overrides pubDate promotion)
44
43
  if let Some(dt) = parse_date(text) {
44
+ if feed.published.is_none() {
45
+ feed.published = Some(dt);
46
+ feed.published_str = Some(text.to_string());
47
+ }
48
+ // dc:date → updated (always overrides pubDate promotion)
45
49
  feed.updated = Some(dt);
46
50
  feed.updated_str = Some(text.to_string());
47
51
  }
@@ -115,6 +119,10 @@ pub fn handle_entry_element(element: &str, text: &str, entry: &mut Entry) {
115
119
  "date" => {
116
120
  if let Some(dt) = parse_date(text) {
117
121
  entry.dc_date = Some(dt);
122
+ if entry.published.is_none() {
123
+ entry.published = Some(dt);
124
+ entry.published_str = Some(text.to_string());
125
+ }
118
126
  // dc:date is more authoritative than pubDate for updated; always override
119
127
  entry.updated = Some(dt);
120
128
  entry.updated_str = Some(text.to_string());
@@ -236,12 +244,12 @@ mod tests {
236
244
  }
237
245
 
238
246
  #[test]
239
- fn test_entry_updated_from_dc_date() {
247
+ fn test_entry_updated_and_published_from_dc_date() {
240
248
  let mut entry = Entry::default();
241
249
  handle_entry_element("date", "2024-01-15T10:30:00Z", &mut entry);
242
250
 
243
251
  assert!(entry.updated.is_some());
244
- assert!(entry.published.is_none());
252
+ assert!(entry.published.is_some());
245
253
  }
246
254
 
247
255
  #[test]
@@ -51,6 +51,14 @@ pub struct GeoLocation {
51
51
  ///
52
52
  /// Default is WGS84 (latitude/longitude) if not specified
53
53
  pub srs_name: Option<String>,
54
+ /// Elevation in meters (from `georss:elev`)
55
+ pub elev: Option<f64>,
56
+ /// Feature type classification (from `georss:featuretypetag`)
57
+ pub feature_type_tag: Option<String>,
58
+ /// Human-readable place name (from `georss:featurename`)
59
+ pub feature_name: Option<String>,
60
+ /// Relationship type (from `georss:relationshiptag`)
61
+ pub relationship_tag: Option<String>,
54
62
  }
55
63
 
56
64
  impl GeoLocation {
@@ -74,7 +82,7 @@ impl GeoLocation {
74
82
  Self {
75
83
  geo_type: GeoType::Point,
76
84
  coordinates: vec![(lat, lon)],
77
- srs_name: None,
85
+ ..Default::default()
78
86
  }
79
87
  }
80
88
 
@@ -94,11 +102,11 @@ impl GeoLocation {
94
102
  /// assert_eq!(loc.coordinates.len(), 2);
95
103
  /// ```
96
104
  #[must_use]
97
- pub const fn line(coords: Vec<(f64, f64)>) -> Self {
105
+ pub fn line(coords: Vec<(f64, f64)>) -> Self {
98
106
  Self {
99
107
  geo_type: GeoType::Line,
100
108
  coordinates: coords,
101
- srs_name: None,
109
+ ..Default::default()
102
110
  }
103
111
  }
104
112
 
@@ -122,11 +130,11 @@ impl GeoLocation {
122
130
  /// let loc = GeoLocation::polygon(coords);
123
131
  /// ```
124
132
  #[must_use]
125
- pub const fn polygon(coords: Vec<(f64, f64)>) -> Self {
133
+ pub fn polygon(coords: Vec<(f64, f64)>) -> Self {
126
134
  Self {
127
135
  geo_type: GeoType::Polygon,
128
136
  coordinates: coords,
129
- srs_name: None,
137
+ ..Default::default()
130
138
  }
131
139
  }
132
140
 
@@ -152,7 +160,7 @@ impl GeoLocation {
152
160
  Self {
153
161
  geo_type: GeoType::Box,
154
162
  coordinates: vec![(lower_lat, lower_lon), (upper_lat, upper_lon)],
155
- srs_name: None,
163
+ ..Default::default()
156
164
  }
157
165
  }
158
166
  }
@@ -258,28 +266,80 @@ pub fn handle_entry_element(
258
266
  match tag {
259
267
  b"point" => {
260
268
  if let Some(loc) = parse_point(text) {
261
- entry.r#where = Some(Box::new(loc));
269
+ let existing = entry
270
+ .r#where
271
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
272
+ existing.geo_type = loc.geo_type;
273
+ existing.coordinates = loc.coordinates;
274
+ existing.srs_name = loc.srs_name;
262
275
  }
263
276
  true
264
277
  }
265
278
  b"line" => {
266
279
  if let Some(loc) = parse_line(text) {
267
- entry.r#where = Some(Box::new(loc));
280
+ let existing = entry
281
+ .r#where
282
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
283
+ existing.geo_type = loc.geo_type;
284
+ existing.coordinates = loc.coordinates;
285
+ existing.srs_name = loc.srs_name;
268
286
  }
269
287
  true
270
288
  }
271
289
  b"polygon" => {
272
290
  if let Some(loc) = parse_polygon(text) {
273
- entry.r#where = Some(Box::new(loc));
291
+ let existing = entry
292
+ .r#where
293
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
294
+ existing.geo_type = loc.geo_type;
295
+ existing.coordinates = loc.coordinates;
296
+ existing.srs_name = loc.srs_name;
274
297
  }
275
298
  true
276
299
  }
277
300
  b"box" => {
278
301
  if let Some(loc) = parse_box(text) {
279
- entry.r#where = Some(Box::new(loc));
302
+ let existing = entry
303
+ .r#where
304
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
305
+ existing.geo_type = loc.geo_type;
306
+ existing.coordinates = loc.coordinates;
307
+ existing.srs_name = loc.srs_name;
280
308
  }
281
309
  true
282
310
  }
311
+ b"elev" => {
312
+ if let Ok(v) = text.trim().parse::<f64>()
313
+ && v.is_finite()
314
+ {
315
+ entry
316
+ .r#where
317
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
318
+ .elev = Some(v);
319
+ }
320
+ true
321
+ }
322
+ b"featuretypetag" => {
323
+ entry
324
+ .r#where
325
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
326
+ .feature_type_tag = Some(text.to_string());
327
+ true
328
+ }
329
+ b"featurename" => {
330
+ entry
331
+ .r#where
332
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
333
+ .feature_name = Some(text.to_string());
334
+ true
335
+ }
336
+ b"relationshiptag" => {
337
+ entry
338
+ .r#where
339
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
340
+ .relationship_tag = Some(text.to_string());
341
+ true
342
+ }
283
343
  _ => false,
284
344
  }
285
345
  }
@@ -305,28 +365,76 @@ pub fn handle_feed_element(
305
365
  match tag {
306
366
  b"point" => {
307
367
  if let Some(loc) = parse_point(text) {
308
- feed.r#where = Some(Box::new(loc));
368
+ let existing = feed
369
+ .r#where
370
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
371
+ existing.geo_type = loc.geo_type;
372
+ existing.coordinates = loc.coordinates;
373
+ existing.srs_name = loc.srs_name;
309
374
  }
310
375
  true
311
376
  }
312
377
  b"line" => {
313
378
  if let Some(loc) = parse_line(text) {
314
- feed.r#where = Some(Box::new(loc));
379
+ let existing = feed
380
+ .r#where
381
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
382
+ existing.geo_type = loc.geo_type;
383
+ existing.coordinates = loc.coordinates;
384
+ existing.srs_name = loc.srs_name;
315
385
  }
316
386
  true
317
387
  }
318
388
  b"polygon" => {
319
389
  if let Some(loc) = parse_polygon(text) {
320
- feed.r#where = Some(Box::new(loc));
390
+ let existing = feed
391
+ .r#where
392
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
393
+ existing.geo_type = loc.geo_type;
394
+ existing.coordinates = loc.coordinates;
395
+ existing.srs_name = loc.srs_name;
321
396
  }
322
397
  true
323
398
  }
324
399
  b"box" => {
325
400
  if let Some(loc) = parse_box(text) {
326
- feed.r#where = Some(Box::new(loc));
401
+ let existing = feed
402
+ .r#where
403
+ .get_or_insert_with(|| Box::new(GeoLocation::default()));
404
+ existing.geo_type = loc.geo_type;
405
+ existing.coordinates = loc.coordinates;
406
+ existing.srs_name = loc.srs_name;
407
+ }
408
+ true
409
+ }
410
+ b"elev" => {
411
+ if let Ok(v) = text.trim().parse::<f64>()
412
+ && v.is_finite()
413
+ {
414
+ feed.r#where
415
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
416
+ .elev = Some(v);
327
417
  }
328
418
  true
329
419
  }
420
+ b"featuretypetag" => {
421
+ feed.r#where
422
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
423
+ .feature_type_tag = Some(text.to_string());
424
+ true
425
+ }
426
+ b"featurename" => {
427
+ feed.r#where
428
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
429
+ .feature_name = Some(text.to_string());
430
+ true
431
+ }
432
+ b"relationshiptag" => {
433
+ feed.r#where
434
+ .get_or_insert_with(|| Box::new(GeoLocation::default()))
435
+ .relationship_tag = Some(text.to_string());
436
+ true
437
+ }
330
438
  _ => false,
331
439
  }
332
440
  }
@@ -341,7 +449,7 @@ fn parse_point(text: &str) -> Option<GeoLocation> {
341
449
  Some(GeoLocation {
342
450
  geo_type: GeoType::Point,
343
451
  coordinates: coords,
344
- srs_name: None,
452
+ ..Default::default()
345
453
  })
346
454
  } else {
347
455
  None
@@ -358,7 +466,7 @@ fn parse_line(text: &str) -> Option<GeoLocation> {
358
466
  Some(GeoLocation {
359
467
  geo_type: GeoType::Line,
360
468
  coordinates: coords,
361
- srs_name: None,
469
+ ..Default::default()
362
470
  })
363
471
  } else {
364
472
  None
@@ -375,7 +483,7 @@ fn parse_polygon(text: &str) -> Option<GeoLocation> {
375
483
  Some(GeoLocation {
376
484
  geo_type: GeoType::Polygon,
377
485
  coordinates: coords,
378
- srs_name: None,
486
+ ..Default::default()
379
487
  })
380
488
  } else {
381
489
  None
@@ -392,7 +500,7 @@ fn parse_box(text: &str) -> Option<GeoLocation> {
392
500
  Some(GeoLocation {
393
501
  geo_type: GeoType::Box,
394
502
  coordinates: coords,
395
- srs_name: None,
503
+ ..Default::default()
396
504
  })
397
505
  } else {
398
506
  None
@@ -631,4 +739,118 @@ mod tests {
631
739
  assert!(handled);
632
740
  assert!(feed.r#where.is_none());
633
741
  }
742
+
743
+ #[test]
744
+ fn test_handle_entry_element_elev() {
745
+ let mut entry = Entry::default();
746
+ let limits = ParserLimits::default();
747
+
748
+ let handled = handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
749
+ assert!(handled);
750
+ let geo = entry.r#where.as_ref().unwrap();
751
+ assert_eq!(geo.elev, Some(1337.5));
752
+ }
753
+
754
+ #[test]
755
+ fn test_handle_entry_element_feature_name() {
756
+ let mut entry = Entry::default();
757
+ let limits = ParserLimits::default();
758
+
759
+ let handled = handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
760
+ assert!(handled);
761
+ let geo = entry.r#where.as_ref().unwrap();
762
+ assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
763
+ }
764
+
765
+ #[test]
766
+ fn test_handle_entry_element_feature_type_tag() {
767
+ let mut entry = Entry::default();
768
+ let limits = ParserLimits::default();
769
+
770
+ let handled = handle_entry_element(b"featuretypetag", "mountain", &mut entry, &limits);
771
+ assert!(handled);
772
+ let geo = entry.r#where.as_ref().unwrap();
773
+ assert_eq!(geo.feature_type_tag.as_deref(), Some("mountain"));
774
+ }
775
+
776
+ #[test]
777
+ fn test_handle_entry_element_relationship_tag() {
778
+ let mut entry = Entry::default();
779
+ let limits = ParserLimits::default();
780
+
781
+ let handled =
782
+ handle_entry_element(b"relationshiptag", "is-located-at", &mut entry, &limits);
783
+ assert!(handled);
784
+ let geo = entry.r#where.as_ref().unwrap();
785
+ assert_eq!(geo.relationship_tag.as_deref(), Some("is-located-at"));
786
+ }
787
+
788
+ #[test]
789
+ fn test_extended_attrs_without_geometry() {
790
+ let mut entry = Entry::default();
791
+ let limits = ParserLimits::default();
792
+
793
+ handle_entry_element(b"featurename", "Unknown Location", &mut entry, &limits);
794
+ let geo = entry.r#where.as_ref().unwrap();
795
+ assert_eq!(geo.feature_name.as_deref(), Some("Unknown Location"));
796
+ assert!(geo.coordinates.is_empty());
797
+ }
798
+
799
+ #[test]
800
+ fn test_extended_attrs_invalid_elev() {
801
+ let mut entry = Entry::default();
802
+ let limits = ParserLimits::default();
803
+
804
+ let handled = handle_entry_element(b"elev", "not-a-number", &mut entry, &limits);
805
+ assert!(handled);
806
+ // GeoLocation not created because elev parse failed
807
+ assert!(entry.r#where.is_none());
808
+ }
809
+
810
+ #[test]
811
+ fn test_extended_attrs_elev_non_finite_ignored() {
812
+ let limits = ParserLimits::default();
813
+
814
+ for value in ["NaN", "Infinity", "-Infinity"] {
815
+ let mut entry = Entry::default();
816
+ let handled = handle_entry_element(b"elev", value, &mut entry, &limits);
817
+ assert!(handled, "element must be recognized for value {value}");
818
+ assert!(
819
+ entry.r#where.is_none(),
820
+ "non-finite elev '{value}' must not create GeoLocation"
821
+ );
822
+ }
823
+ }
824
+
825
+ #[test]
826
+ fn test_extended_attrs_before_geometry() {
827
+ let mut entry = Entry::default();
828
+ let limits = ParserLimits::default();
829
+
830
+ handle_entry_element(b"featurename", "Reverse Order", &mut entry, &limits);
831
+ handle_entry_element(b"elev", "500.0", &mut entry, &limits);
832
+ handle_entry_element(b"point", "40.0 -74.0", &mut entry, &limits);
833
+
834
+ let geo = entry.r#where.as_ref().unwrap();
835
+ assert_eq!(geo.geo_type, GeoType::Point);
836
+ assert_eq!(geo.coordinates[0], (40.0, -74.0));
837
+ assert_eq!(geo.feature_name.as_deref(), Some("Reverse Order"));
838
+ assert_eq!(geo.elev, Some(500.0));
839
+ }
840
+
841
+ #[test]
842
+ fn test_extended_attrs_after_geometry() {
843
+ let mut entry = Entry::default();
844
+ let limits = ParserLimits::default();
845
+
846
+ handle_entry_element(b"point", "45.256 -71.92", &mut entry, &limits);
847
+ handle_entry_element(b"featurename", "Mont Mégantic", &mut entry, &limits);
848
+ handle_entry_element(b"elev", "1337.5", &mut entry, &limits);
849
+
850
+ let geo = entry.r#where.as_ref().unwrap();
851
+ assert_eq!(geo.geo_type, GeoType::Point);
852
+ assert_eq!(geo.coordinates[0], (45.256, -71.92));
853
+ assert_eq!(geo.feature_name.as_deref(), Some("Mont Mégantic"));
854
+ assert_eq!(geo.elev, Some(1337.5));
855
+ }
634
856
  }