pubmed_parser/pmc/parser/
mod.rs1use crate::common::{PmcId, PubMedId};
2use crate::error::Result;
3use crate::pmc::domain::{
4 Abstract, ArticleMeta, Back, Body, Front, License, Permissions, PmcArticle, TitleGroup,
5};
6
7pub(crate) mod author;
8pub(crate) mod metadata;
9pub(crate) mod reader_utils;
10pub(crate) mod reference;
11pub(crate) mod section;
12
13fn extract_section_slice<'a>(content: &'a str, start_tag: &str, end_tag: &str) -> Option<&'a str> {
18 let start = content.find(start_tag)?;
19 let end_offset = content[start..].find(end_tag)?;
20 Some(&content[start..start + end_offset + end_tag.len()])
21}
22
23pub fn parse_pmc_xml(xml_content: &str, pmcid: &str) -> Result<PmcArticle> {
29 let pmcid_typed = PmcId::parse(pmcid)?;
30
31 let front = extract_section_slice(xml_content, "<front>", "</front>").unwrap_or(xml_content);
34 let back = extract_section_slice(xml_content, "<back>", "</back>").unwrap_or("");
35
36 let title = metadata::extract_title(front);
38 let subtitle = metadata::extract_subtitle(front);
39 let journal = metadata::extract_journal_info(front);
40 let pub_dates = metadata::extract_pub_dates(front);
41 let volume = metadata::extract_volume(front);
42 let issue = metadata::extract_issue(front);
43 let doi = metadata::extract_doi(front);
44 let pmid_str = metadata::extract_pmid(front);
45 let pmid = pmid_str.as_deref().map(PubMedId::parse).transpose()?;
46 let keywords = metadata::extract_keywords(front);
47 let keyword_groups = metadata::extract_keyword_groups(front);
48 let subject_groups = metadata::extract_subject_groups(front);
49 let related_articles = metadata::extract_related_articles(front);
50 let author_notes = metadata::extract_author_notes(front);
51 let funding = metadata::extract_funding(front);
52
53 let abstract_text = metadata::extract_abstract(front);
55 let copyright = metadata::extract_copyright(front);
56 let license = metadata::extract_license(front);
57 let license_url = metadata::extract_license_url(front);
58 let history_dates = metadata::extract_history_dates(front);
59 let categories = metadata::extract_categories(front);
60 let fpage = metadata::extract_fpage(front);
61 let lpage = metadata::extract_lpage(front);
62 let elocation_id = metadata::extract_elocation_id(front);
63
64 let article_type = metadata::extract_article_type(xml_content);
66
67 let conflict_of_interest = metadata::extract_conflict_of_interest(back);
69 let acknowledgments = metadata::extract_acknowledgments(back);
70 let appendices = section::extract_appendices(back);
71
72 let data_availability = metadata::extract_data_availability(xml_content);
74 let supplementary_materials = metadata::extract_supplementary_materials(xml_content);
75
76 let authors = author::extract_authors(front)?;
78
79 let sections = section::extract_sections_enhanced(xml_content);
81
82 let references_scope = if back.is_empty() { xml_content } else { back };
86 let references = reference::extract_references_detailed(references_scope).unwrap_or_default();
87
88 let license = if license.is_some() || license_url.is_some() {
90 Some(License {
91 href: license_url,
92 text: license,
93 })
94 } else {
95 None
96 };
97 let permissions = if copyright.is_some() || license.is_some() {
98 Some(Permissions {
99 copyright_statement: copyright,
100 license,
101 })
102 } else {
103 None
104 };
105
106 let abstracts = abstract_text
108 .map(|text| {
109 vec![Abstract {
110 abstract_type: None,
111 text,
112 sections: Vec::new(),
113 }]
114 })
115 .unwrap_or_default();
116
117 let front = Front {
118 journal_meta: journal,
119 article_meta: ArticleMeta {
120 pmcid: pmcid_typed,
121 pmid,
122 doi,
123 categories,
124 title_group: TitleGroup {
125 article_title: title,
126 subtitle,
127 },
128 authors,
129 pub_dates,
130 volume,
131 issue,
132 fpage,
133 lpage,
134 elocation_id,
135 history: history_dates,
136 permissions,
137 abstracts,
138 keywords,
139 keyword_groups,
140 subject_groups,
141 related_articles,
142 author_notes,
143 funding,
144 },
145 };
146
147 let body = if xml_content.contains("<body") || !sections.is_empty() {
148 Some(Body { sections })
149 } else {
150 None
151 };
152
153 let has_back_content = acknowledgments.is_some()
154 || conflict_of_interest.is_some()
155 || !references.is_empty()
156 || !appendices.is_empty();
157 let back = if !back.is_empty() || has_back_content {
158 Some(Back {
159 acknowledgments,
160 conflict_of_interest,
161 references,
162 appendices,
163 glossary: Vec::new(),
164 })
165 } else {
166 None
167 };
168
169 Ok(PmcArticle {
170 article_type,
171 front,
172 body,
173 back,
174 supplementary_materials,
175 data_availability,
176 })
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_parse_basic_structure() {
185 let xml_content = r#"
187 <article xmlns:xlink="http://www.w3.org/1999/xlink" article-type="research-article">
188 <front>
189 <article-meta>
190 <article-id pub-id-type="pmc">PMC123456</article-id>
191 <article-id pub-id-type="doi">10.1234/test</article-id>
192 <title-group>
193 <article-title>Test Article Title</article-title>
194 </title-group>
195 <contrib-group>
196 <contrib>
197 <name>
198 <surname>Doe</surname>
199 <given-names>John</given-names>
200 </name>
201 </contrib>
202 </contrib-group>
203 <pub-date>
204 <year>2023</year>
205 <month>12</month>
206 <day>25</day>
207 </pub-date>
208 </article-meta>
209 </front>
210 <body>
211 <sec>
212 <title>Introduction</title>
213 <p>This is the introduction.</p>
214 </sec>
215 </body>
216 <back>
217 <ref-list>
218 <ref id="ref1">
219 <element-citation>
220 <article-title>Reference Title</article-title>
221 </element-citation>
222 </ref>
223 </ref-list>
224 </back>
225 </article>
226 "#;
227
228 let result = parse_pmc_xml(xml_content, "PMC123456");
229 assert!(result.is_ok());
230
231 let article = result.unwrap();
232 assert_eq!(article.pmcid().as_str(), "PMC123456");
233 assert_eq!(article.title(), Some("Test Article Title"));
234 assert!(!article.pub_dates().is_empty());
235 assert_eq!(article.pub_dates()[0].year, Some(2023));
236 assert_eq!(article.pub_dates()[0].month, Some(12));
237 assert_eq!(article.pub_dates()[0].day, Some(25));
238 assert!(!article.authors().is_empty());
239 assert!(!article.sections().is_empty());
240 assert!(!article.references().is_empty());
241 }
242
243 #[test]
244 fn test_parse_minimal_xml() {
245 let xml_content = r#"
247 <article>
248 <front>
249 <article-meta>
250 <title-group>
251 <article-title>Minimal Test</article-title>
252 </title-group>
253 </article-meta>
254 </front>
255 </article>
256 "#;
257
258 let result = parse_pmc_xml(xml_content, "PMC100000");
259 assert!(result.is_ok());
260
261 let article = result.unwrap();
262 assert_eq!(article.pmcid().as_str(), "PMC100000");
263 assert_eq!(article.title(), Some("Minimal Test"));
264 }
265
266 }