feedparser-rs 0.4.7__tar.gz → 0.4.8__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.
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/Cargo.lock +3 -3
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/Cargo.toml +1 -1
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/PKG-INFO +1 -1
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/lib.rs +3 -3
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/mod.rs +21 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/src/namespace/slash.rs +117 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/src/namespace/threading.rs +264 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/atom.rs +323 -9
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/common.rs +40 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/json.rs +182 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/mod.rs +36 -20
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/namespace_detection.rs +6 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/rss.rs +134 -13
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/rss10.rs +34 -3
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/common.rs +32 -1
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/entry.rs +35 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/feed.rs +2 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/mod.rs +2 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/src/types/thread.rs +113 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/util/date.rs +45 -8
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/util/encoding.rs +25 -14
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/encoding_integration.rs +97 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/guid_entity_tests.rs +113 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/integration_tests.rs +592 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/test_adversarial_input.rs +120 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/test_georss_cc_namespaces.rs +261 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/test_parser_limits.rs +527 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/test_slash_wfw_namespaces.rs +153 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/test_thr_namespace.rs +59 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-core/tests/test_threading_namespace.rs +191 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/lib.rs +1 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/common.rs +16 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/entry.rs +89 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/feed_meta.rs +12 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/mod.rs +1 -0
- feedparser_rs-0.4.8/crates/feedparser-rs-py/src/types/thread.rs +103 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/tests/test_basic.py +120 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/pyproject.toml +1 -1
- feedparser_rs-0.4.7/crates/feedparser-rs-core/tests/integration_tests.rs +0 -296
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/README.md +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/Cargo.toml +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/README.md +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/benches/parsing.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/benches/types.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/error_handling.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/feeds/malformed_feed.xml +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/feeds/sample_atom.xml +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/feeds/sample_podcast.xml +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/feeds/sample_rss.xml +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/parse_file.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/parse_url.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/podcast_feed.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/examples/profile_memory.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/compat/mod.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/error.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/http/client.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/http/mod.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/http/response.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/http/validation.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/limits.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/cc.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/content.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/dublin_core.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/georss.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/media_rss.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/namespace/syndication.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/options.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/parser/detect.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/generics.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/podcast.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/types/version.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/util/base_url.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/util/mod.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/util/sanitize.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/src/util/text.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/http_integration.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/issue45_test.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/namespace_edge_cases.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/namespace_integration.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/test_podcast_namespace.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/test_rss10.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/test_url_resolution.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-core/tests/test_url_security.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/Cargo.toml +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/README.md +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/error.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/limits.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/compat.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/datetime.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/geo.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/media.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/parsed_feed.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/podcast.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/src/types/syndication.rs +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/tests/test_bindings.py +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/tests/test_compat.py +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/tests/test_guid_entities.py +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/tests/test_integration.py +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/tests/test_syndication.py +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/crates/feedparser-rs-py/uv.lock +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/python/feedparser_rs/__init__.py +0 -0
- {feedparser_rs-0.4.7 → feedparser_rs-0.4.8}/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.
|
|
603
|
+
version = "0.4.8"
|
|
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.
|
|
626
|
+
version = "0.4.8"
|
|
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.
|
|
636
|
+
version = "0.4.8"
|
|
637
637
|
dependencies = [
|
|
638
638
|
"chrono",
|
|
639
639
|
"feedparser-rs",
|
|
@@ -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,
|
|
192
|
-
ItunesEntryMeta, ItunesFeedMeta, ItunesOwner, LimitedCollectionExt, Link,
|
|
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,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/// Atom Threading Extensions (RFC 4685) namespace handler
|
|
2
|
+
///
|
|
3
|
+
/// Namespace URI: `http://purl.org/syndication/thread/1.0`
|
|
4
|
+
/// Prefix: `thr`
|
|
5
|
+
///
|
|
6
|
+
/// Handles two elements:
|
|
7
|
+
/// - `thr:in-reply-to` — attribute-only element referencing the parent entry
|
|
8
|
+
/// - `thr:total` — text element with total response count
|
|
9
|
+
use crate::types::{Entry, InReplyTo, MimeType, SmallString, Url};
|
|
10
|
+
|
|
11
|
+
/// Atom Threading Extensions namespace URI
|
|
12
|
+
pub const THREADING_NAMESPACE: &str = "http://purl.org/syndication/thread/1.0";
|
|
13
|
+
|
|
14
|
+
/// Normalize attribute value: empty string after trim becomes None
|
|
15
|
+
#[inline]
|
|
16
|
+
fn non_empty(s: &str) -> Option<&str> {
|
|
17
|
+
let trimmed = s.trim();
|
|
18
|
+
if trimmed.is_empty() {
|
|
19
|
+
None
|
|
20
|
+
} else {
|
|
21
|
+
Some(trimmed)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Build an `InReplyTo` from its four optional fields.
|
|
26
|
+
///
|
|
27
|
+
/// Returns `None` only if ALL fields are `None` (fully empty element).
|
|
28
|
+
#[inline]
|
|
29
|
+
fn build_in_reply_to(
|
|
30
|
+
ref_: Option<SmallString>,
|
|
31
|
+
href: Option<Url>,
|
|
32
|
+
type_: Option<MimeType>,
|
|
33
|
+
source: Option<Url>,
|
|
34
|
+
) -> Option<InReplyTo> {
|
|
35
|
+
if ref_.is_none() && href.is_none() && type_.is_none() && source.is_none() {
|
|
36
|
+
return None;
|
|
37
|
+
}
|
|
38
|
+
Some(InReplyTo {
|
|
39
|
+
ref_,
|
|
40
|
+
href,
|
|
41
|
+
type_,
|
|
42
|
+
source,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Parse `thr:in-reply-to` from a quick-xml attribute iterator (Atom and RSS 1.0 parser paths)
|
|
47
|
+
///
|
|
48
|
+
/// Returns `None` only if ALL fields are `None` after normalization (fully empty element).
|
|
49
|
+
/// Returns `Some(InReplyTo)` even if `ref` is missing, to tolerate malformed feeds.
|
|
50
|
+
///
|
|
51
|
+
/// # Arguments
|
|
52
|
+
///
|
|
53
|
+
/// * `attrs` - Iterator over quick-xml attributes (from `element.attributes().flatten()`)
|
|
54
|
+
/// * `max_attr_len` - Maximum attribute value length for `DoS` protection
|
|
55
|
+
pub fn parse_in_reply_to_from_attrs<'a>(
|
|
56
|
+
attrs: impl Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
|
|
57
|
+
max_attr_len: usize,
|
|
58
|
+
) -> Option<InReplyTo> {
|
|
59
|
+
let mut ref_ = None;
|
|
60
|
+
let mut href = None;
|
|
61
|
+
let mut type_ = None;
|
|
62
|
+
let mut source = None;
|
|
63
|
+
|
|
64
|
+
for attr in attrs {
|
|
65
|
+
if attr.value.len() > max_attr_len {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let Ok(value) = attr.unescape_value() else {
|
|
69
|
+
continue;
|
|
70
|
+
};
|
|
71
|
+
match attr.key.as_ref() {
|
|
72
|
+
b"ref" => ref_ = non_empty(&value).map(|s| s.to_string().into()),
|
|
73
|
+
b"href" => href = non_empty(&value).map(|s| s.to_string().into()),
|
|
74
|
+
b"type" => type_ = non_empty(&value).map(|s| s.to_string().into()),
|
|
75
|
+
b"source" => source = non_empty(&value).map(|s| s.to_string().into()),
|
|
76
|
+
_ => {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
build_in_reply_to(ref_, href, type_, source)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Parse `thr:in-reply-to` from collected attributes (RSS 2.0 parser path)
|
|
84
|
+
///
|
|
85
|
+
/// Returns `None` only if ALL fields are `None` after normalization.
|
|
86
|
+
///
|
|
87
|
+
/// # Arguments
|
|
88
|
+
///
|
|
89
|
+
/// * `attrs` - Slice of collected `(key_bytes, value_string)` attribute pairs
|
|
90
|
+
/// * `max_attr_len` - Maximum attribute value length for `DoS` protection
|
|
91
|
+
pub fn parse_in_reply_to_from_collected(
|
|
92
|
+
attrs: &[(Vec<u8>, String)],
|
|
93
|
+
max_attr_len: usize,
|
|
94
|
+
) -> Option<InReplyTo> {
|
|
95
|
+
let mut ref_ = None;
|
|
96
|
+
let mut href = None;
|
|
97
|
+
let mut type_ = None;
|
|
98
|
+
let mut source = None;
|
|
99
|
+
|
|
100
|
+
for (key, value) in attrs {
|
|
101
|
+
if value.len() > max_attr_len {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
match key.as_slice() {
|
|
105
|
+
b"ref" => ref_ = non_empty(value).map(|s| s.to_string().into()),
|
|
106
|
+
b"href" => href = non_empty(value).map(|s| s.to_string().into()),
|
|
107
|
+
b"type" => type_ = non_empty(value).map(|s| s.to_string().into()),
|
|
108
|
+
b"source" => source = non_empty(value).map(|s| s.to_string().into()),
|
|
109
|
+
_ => {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
build_in_reply_to(ref_, href, type_, source)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Handle `thr:total` text content
|
|
117
|
+
///
|
|
118
|
+
/// Parses non-negative integer from text. Silently ignores non-numeric, negative,
|
|
119
|
+
/// overflow, empty, and whitespace-only values (consistent with how `parse_duration`
|
|
120
|
+
/// handles invalid input throughout the codebase).
|
|
121
|
+
///
|
|
122
|
+
/// # Arguments
|
|
123
|
+
///
|
|
124
|
+
/// * `text` - Text content of the `thr:total` element
|
|
125
|
+
/// * `entry` - Entry to update with the parsed count
|
|
126
|
+
pub fn handle_total(text: &str, entry: &mut Entry) {
|
|
127
|
+
entry.thr_total = text.trim().parse::<u32>().ok();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[cfg(test)]
|
|
131
|
+
mod tests {
|
|
132
|
+
use super::*;
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_handle_total_valid() {
|
|
136
|
+
let mut entry = Entry::default();
|
|
137
|
+
handle_total("15", &mut entry);
|
|
138
|
+
assert_eq!(entry.thr_total, Some(15));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn test_handle_total_with_whitespace() {
|
|
143
|
+
let mut entry = Entry::default();
|
|
144
|
+
handle_total(" 42 ", &mut entry);
|
|
145
|
+
assert_eq!(entry.thr_total, Some(42));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn test_handle_total_zero() {
|
|
150
|
+
let mut entry = Entry::default();
|
|
151
|
+
handle_total("0", &mut entry);
|
|
152
|
+
assert_eq!(entry.thr_total, Some(0));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_handle_total_non_numeric() {
|
|
157
|
+
let mut entry = Entry::default();
|
|
158
|
+
handle_total("abc", &mut entry);
|
|
159
|
+
assert_eq!(entry.thr_total, None);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn test_handle_total_negative() {
|
|
164
|
+
let mut entry = Entry::default();
|
|
165
|
+
handle_total("-5", &mut entry);
|
|
166
|
+
assert_eq!(entry.thr_total, None);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
fn test_handle_total_overflow() {
|
|
171
|
+
let mut entry = Entry::default();
|
|
172
|
+
handle_total("99999999999999", &mut entry);
|
|
173
|
+
assert_eq!(entry.thr_total, None);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn test_handle_total_empty() {
|
|
178
|
+
let mut entry = Entry::default();
|
|
179
|
+
handle_total("", &mut entry);
|
|
180
|
+
assert_eq!(entry.thr_total, None);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn test_handle_total_whitespace_only() {
|
|
185
|
+
let mut entry = Entry::default();
|
|
186
|
+
handle_total(" ", &mut entry);
|
|
187
|
+
assert_eq!(entry.thr_total, None);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn test_parse_in_reply_to_from_collected_full() {
|
|
192
|
+
let attrs = vec![
|
|
193
|
+
(b"ref".to_vec(), "tag:example.com,2024:post/1".to_string()),
|
|
194
|
+
(b"href".to_vec(), "https://example.com/post/1".to_string()),
|
|
195
|
+
(b"type".to_vec(), "text/html".to_string()),
|
|
196
|
+
(
|
|
197
|
+
b"source".to_vec(),
|
|
198
|
+
"https://example.com/feed.xml".to_string(),
|
|
199
|
+
),
|
|
200
|
+
];
|
|
201
|
+
let result = parse_in_reply_to_from_collected(&attrs, 1024);
|
|
202
|
+
assert!(result.is_some());
|
|
203
|
+
let irt = result.unwrap();
|
|
204
|
+
assert_eq!(irt.ref_.as_deref(), Some("tag:example.com,2024:post/1"));
|
|
205
|
+
assert_eq!(irt.href.as_deref(), Some("https://example.com/post/1"));
|
|
206
|
+
assert_eq!(irt.type_.as_deref(), Some("text/html"));
|
|
207
|
+
assert_eq!(irt.source.as_deref(), Some("https://example.com/feed.xml"));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[test]
|
|
211
|
+
fn test_parse_in_reply_to_from_collected_empty_ref() {
|
|
212
|
+
let attrs = vec![
|
|
213
|
+
(b"ref".to_vec(), String::new()),
|
|
214
|
+
(b"href".to_vec(), "https://example.com/post/1".to_string()),
|
|
215
|
+
];
|
|
216
|
+
let result = parse_in_reply_to_from_collected(&attrs, 1024);
|
|
217
|
+
assert!(result.is_some());
|
|
218
|
+
let irt = result.unwrap();
|
|
219
|
+
// Empty ref should be normalized to None
|
|
220
|
+
assert!(irt.ref_.is_none());
|
|
221
|
+
assert_eq!(irt.href.as_deref(), Some("https://example.com/post/1"));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn test_parse_in_reply_to_from_collected_all_empty() {
|
|
226
|
+
let attrs = vec![
|
|
227
|
+
(b"ref".to_vec(), String::new()),
|
|
228
|
+
(b"href".to_vec(), String::new()),
|
|
229
|
+
(b"type".to_vec(), " ".to_string()),
|
|
230
|
+
(b"source".to_vec(), String::new()),
|
|
231
|
+
];
|
|
232
|
+
let result = parse_in_reply_to_from_collected(&attrs, 1024);
|
|
233
|
+
// All fields are None -> return None
|
|
234
|
+
assert!(result.is_none());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn test_parse_in_reply_to_from_collected_truncated_by_limit() {
|
|
239
|
+
let attrs = vec![(b"ref".to_vec(), "tag:example.com,2024:post/1".to_string())];
|
|
240
|
+
let result = parse_in_reply_to_from_collected(&attrs, 5);
|
|
241
|
+
// Value exceeds max_attr_len, should be skipped -> all None -> None
|
|
242
|
+
assert!(result.is_none());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#[test]
|
|
246
|
+
fn test_parse_in_reply_to_from_collected_only_ref() {
|
|
247
|
+
let attrs = vec![(b"ref".to_vec(), "tag:example.com,2024:post/1".to_string())];
|
|
248
|
+
let result = parse_in_reply_to_from_collected(&attrs, 1024);
|
|
249
|
+
assert!(result.is_some());
|
|
250
|
+
let irt = result.unwrap();
|
|
251
|
+
assert_eq!(irt.ref_.as_deref(), Some("tag:example.com,2024:post/1"));
|
|
252
|
+
assert!(irt.href.is_none());
|
|
253
|
+
assert!(irt.type_.is_none());
|
|
254
|
+
assert!(irt.source.is_none());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn test_non_empty_normalization() {
|
|
259
|
+
assert_eq!(non_empty(""), None);
|
|
260
|
+
assert_eq!(non_empty(" "), None);
|
|
261
|
+
assert_eq!(non_empty("hello"), Some("hello"));
|
|
262
|
+
assert_eq!(non_empty(" hello "), Some("hello"));
|
|
263
|
+
}
|
|
264
|
+
}
|