kreuzberg 4.0.0.pre.rc.6 → 4.0.0.rc1

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -6
  3. data/.rubocop.yaml +534 -1
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +11 -11
  6. data/README.md +5 -10
  7. data/examples/async_patterns.rb +0 -1
  8. data/ext/kreuzberg_rb/extconf.rb +0 -10
  9. data/ext/kreuzberg_rb/native/Cargo.toml +15 -23
  10. data/ext/kreuzberg_rb/native/build.rs +2 -0
  11. data/ext/kreuzberg_rb/native/include/ieeefp.h +1 -1
  12. data/ext/kreuzberg_rb/native/include/msvc_compat/strings.h +1 -1
  13. data/ext/kreuzberg_rb/native/include/strings.h +2 -2
  14. data/ext/kreuzberg_rb/native/include/unistd.h +1 -1
  15. data/ext/kreuzberg_rb/native/src/lib.rs +16 -75
  16. data/kreuzberg.gemspec +14 -57
  17. data/lib/kreuzberg/cache_api.rb +0 -1
  18. data/lib/kreuzberg/cli.rb +2 -2
  19. data/lib/kreuzberg/config.rb +2 -9
  20. data/lib/kreuzberg/errors.rb +7 -75
  21. data/lib/kreuzberg/extraction_api.rb +0 -1
  22. data/lib/kreuzberg/setup_lib_path.rb +0 -1
  23. data/lib/kreuzberg/version.rb +1 -1
  24. data/lib/kreuzberg.rb +0 -21
  25. data/pkg/kreuzberg-4.0.0.rc1.gem +0 -0
  26. data/sig/kreuzberg.rbs +3 -55
  27. data/spec/binding/cli_proxy_spec.rb +4 -2
  28. data/spec/binding/cli_spec.rb +11 -12
  29. data/spec/examples.txt +104 -0
  30. data/spec/fixtures/config.yaml +1 -0
  31. data/spec/spec_helper.rb +1 -1
  32. data/vendor/kreuzberg/Cargo.toml +42 -112
  33. data/vendor/kreuzberg/README.md +2 -2
  34. data/vendor/kreuzberg/build.rs +4 -18
  35. data/vendor/kreuzberg/src/bin/profile_extract.rs +455 -0
  36. data/vendor/kreuzberg/src/cache/mod.rs +3 -27
  37. data/vendor/kreuzberg/src/core/batch_mode.rs +0 -60
  38. data/vendor/kreuzberg/src/core/extractor.rs +81 -202
  39. data/vendor/kreuzberg/src/core/io.rs +2 -4
  40. data/vendor/kreuzberg/src/core/mime.rs +12 -2
  41. data/vendor/kreuzberg/src/core/mod.rs +1 -4
  42. data/vendor/kreuzberg/src/core/pipeline.rs +33 -111
  43. data/vendor/kreuzberg/src/embeddings.rs +16 -125
  44. data/vendor/kreuzberg/src/error.rs +1 -1
  45. data/vendor/kreuzberg/src/extraction/docx.rs +1 -1
  46. data/vendor/kreuzberg/src/extraction/image.rs +13 -13
  47. data/vendor/kreuzberg/src/extraction/libreoffice.rs +1 -0
  48. data/vendor/kreuzberg/src/extraction/mod.rs +5 -9
  49. data/vendor/kreuzberg/src/extraction/office_metadata/mod.rs +0 -2
  50. data/vendor/kreuzberg/src/extraction/pandoc/batch.rs +275 -0
  51. data/vendor/kreuzberg/src/extraction/pandoc/mime_types.rs +178 -0
  52. data/vendor/kreuzberg/src/extraction/pandoc/mod.rs +491 -0
  53. data/vendor/kreuzberg/src/extraction/pandoc/server.rs +496 -0
  54. data/vendor/kreuzberg/src/extraction/pandoc/subprocess.rs +1188 -0
  55. data/vendor/kreuzberg/src/extraction/pandoc/version.rs +162 -0
  56. data/vendor/kreuzberg/src/extractors/archive.rs +0 -21
  57. data/vendor/kreuzberg/src/extractors/docx.rs +128 -16
  58. data/vendor/kreuzberg/src/extractors/email.rs +0 -14
  59. data/vendor/kreuzberg/src/extractors/excel.rs +20 -19
  60. data/vendor/kreuzberg/src/extractors/html.rs +154 -137
  61. data/vendor/kreuzberg/src/extractors/image.rs +4 -7
  62. data/vendor/kreuzberg/src/extractors/mod.rs +9 -106
  63. data/vendor/kreuzberg/src/extractors/pandoc.rs +201 -0
  64. data/vendor/kreuzberg/src/extractors/pdf.rs +15 -12
  65. data/vendor/kreuzberg/src/extractors/pptx.rs +3 -17
  66. data/vendor/kreuzberg/src/extractors/structured.rs +0 -14
  67. data/vendor/kreuzberg/src/extractors/text.rs +5 -23
  68. data/vendor/kreuzberg/src/extractors/xml.rs +0 -7
  69. data/vendor/kreuzberg/src/keywords/rake.rs +1 -0
  70. data/vendor/kreuzberg/src/lib.rs +1 -4
  71. data/vendor/kreuzberg/src/mcp/mod.rs +1 -1
  72. data/vendor/kreuzberg/src/mcp/server.rs +3 -5
  73. data/vendor/kreuzberg/src/ocr/processor.rs +2 -18
  74. data/vendor/kreuzberg/src/pdf/error.rs +1 -1
  75. data/vendor/kreuzberg/src/pdf/table.rs +44 -17
  76. data/vendor/kreuzberg/src/pdf/text.rs +3 -0
  77. data/vendor/kreuzberg/src/plugins/extractor.rs +5 -8
  78. data/vendor/kreuzberg/src/plugins/ocr.rs +11 -2
  79. data/vendor/kreuzberg/src/plugins/processor.rs +1 -2
  80. data/vendor/kreuzberg/src/plugins/registry.rs +0 -13
  81. data/vendor/kreuzberg/src/plugins/validator.rs +8 -9
  82. data/vendor/kreuzberg/src/stopwords/mod.rs +2 -2
  83. data/vendor/kreuzberg/src/types.rs +12 -42
  84. data/vendor/kreuzberg/tests/batch_orchestration.rs +5 -19
  85. data/vendor/kreuzberg/tests/batch_processing.rs +3 -15
  86. data/vendor/kreuzberg/tests/chunking_offset_demo.rs +92 -0
  87. data/vendor/kreuzberg/tests/concurrency_stress.rs +1 -17
  88. data/vendor/kreuzberg/tests/config_features.rs +0 -18
  89. data/vendor/kreuzberg/tests/config_loading_tests.rs +39 -15
  90. data/vendor/kreuzberg/tests/core_integration.rs +7 -24
  91. data/vendor/kreuzberg/tests/csv_integration.rs +81 -71
  92. data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +25 -23
  93. data/vendor/kreuzberg/tests/pandoc_integration.rs +503 -0
  94. data/vendor/kreuzberg/tests/pipeline_integration.rs +1 -0
  95. data/vendor/kreuzberg/tests/plugin_postprocessor_test.rs +1 -0
  96. data/vendor/kreuzberg/tests/registry_integration_tests.rs +22 -1
  97. data/vendor/kreuzberg/tests/security_validation.rs +1 -12
  98. metadata +25 -90
  99. data/.rubocop.yml +0 -538
  100. data/ext/kreuzberg_rb/native/Cargo.lock +0 -6535
  101. data/lib/kreuzberg/error_context.rb +0 -32
  102. data/vendor/kreuzberg/benches/otel_overhead.rs +0 -48
  103. data/vendor/kreuzberg/src/extraction/markdown.rs +0 -213
  104. data/vendor/kreuzberg/src/extraction/office_metadata/odt_properties.rs +0 -287
  105. data/vendor/kreuzberg/src/extractors/bibtex.rs +0 -469
  106. data/vendor/kreuzberg/src/extractors/docbook.rs +0 -502
  107. data/vendor/kreuzberg/src/extractors/epub.rs +0 -707
  108. data/vendor/kreuzberg/src/extractors/fictionbook.rs +0 -491
  109. data/vendor/kreuzberg/src/extractors/fictionbook.rs.backup2 +0 -738
  110. data/vendor/kreuzberg/src/extractors/jats.rs +0 -1051
  111. data/vendor/kreuzberg/src/extractors/jupyter.rs +0 -367
  112. data/vendor/kreuzberg/src/extractors/latex.rs +0 -652
  113. data/vendor/kreuzberg/src/extractors/markdown.rs +0 -700
  114. data/vendor/kreuzberg/src/extractors/odt.rs +0 -628
  115. data/vendor/kreuzberg/src/extractors/opml.rs +0 -634
  116. data/vendor/kreuzberg/src/extractors/orgmode.rs +0 -528
  117. data/vendor/kreuzberg/src/extractors/rst.rs +0 -576
  118. data/vendor/kreuzberg/src/extractors/rtf.rs +0 -810
  119. data/vendor/kreuzberg/src/extractors/security.rs +0 -484
  120. data/vendor/kreuzberg/src/extractors/security_tests.rs +0 -367
  121. data/vendor/kreuzberg/src/extractors/typst.rs +0 -650
  122. data/vendor/kreuzberg/src/panic_context.rs +0 -154
  123. data/vendor/kreuzberg/tests/api_extract_multipart.rs +0 -52
  124. data/vendor/kreuzberg/tests/bibtex_parity_test.rs +0 -421
  125. data/vendor/kreuzberg/tests/docbook_extractor_tests.rs +0 -498
  126. data/vendor/kreuzberg/tests/docx_vs_pandoc_comparison.rs +0 -370
  127. data/vendor/kreuzberg/tests/epub_native_extractor_tests.rs +0 -275
  128. data/vendor/kreuzberg/tests/fictionbook_extractor_tests.rs +0 -228
  129. data/vendor/kreuzberg/tests/html_table_test.rs +0 -551
  130. data/vendor/kreuzberg/tests/instrumentation_test.rs +0 -139
  131. data/vendor/kreuzberg/tests/jats_extractor_tests.rs +0 -639
  132. data/vendor/kreuzberg/tests/jupyter_extractor_tests.rs +0 -704
  133. data/vendor/kreuzberg/tests/latex_extractor_tests.rs +0 -496
  134. data/vendor/kreuzberg/tests/markdown_extractor_tests.rs +0 -490
  135. data/vendor/kreuzberg/tests/odt_extractor_tests.rs +0 -695
  136. data/vendor/kreuzberg/tests/opml_extractor_tests.rs +0 -616
  137. data/vendor/kreuzberg/tests/orgmode_extractor_tests.rs +0 -822
  138. data/vendor/kreuzberg/tests/rst_extractor_tests.rs +0 -692
  139. data/vendor/kreuzberg/tests/rtf_extractor_tests.rs +0 -776
  140. data/vendor/kreuzberg/tests/typst_behavioral_tests.rs +0 -1259
  141. data/vendor/kreuzberg/tests/typst_extractor_tests.rs +0 -647
  142. data/vendor/rb-sys/.cargo-ok +0 -1
  143. data/vendor/rb-sys/.cargo_vcs_info.json +0 -6
  144. data/vendor/rb-sys/Cargo.lock +0 -393
  145. data/vendor/rb-sys/Cargo.toml +0 -70
  146. data/vendor/rb-sys/Cargo.toml.orig +0 -57
  147. data/vendor/rb-sys/LICENSE-APACHE +0 -190
  148. data/vendor/rb-sys/LICENSE-MIT +0 -21
  149. data/vendor/rb-sys/bin/release.sh +0 -21
  150. data/vendor/rb-sys/build/features.rs +0 -108
  151. data/vendor/rb-sys/build/main.rs +0 -246
  152. data/vendor/rb-sys/build/stable_api_config.rs +0 -153
  153. data/vendor/rb-sys/build/version.rs +0 -48
  154. data/vendor/rb-sys/readme.md +0 -36
  155. data/vendor/rb-sys/src/bindings.rs +0 -21
  156. data/vendor/rb-sys/src/hidden.rs +0 -11
  157. data/vendor/rb-sys/src/lib.rs +0 -34
  158. data/vendor/rb-sys/src/macros.rs +0 -371
  159. data/vendor/rb-sys/src/memory.rs +0 -53
  160. data/vendor/rb-sys/src/ruby_abi_version.rs +0 -38
  161. data/vendor/rb-sys/src/special_consts.rs +0 -31
  162. data/vendor/rb-sys/src/stable_api/compiled.c +0 -179
  163. data/vendor/rb-sys/src/stable_api/compiled.rs +0 -257
  164. data/vendor/rb-sys/src/stable_api/ruby_2_6.rs +0 -316
  165. data/vendor/rb-sys/src/stable_api/ruby_2_7.rs +0 -316
  166. data/vendor/rb-sys/src/stable_api/ruby_3_0.rs +0 -324
  167. data/vendor/rb-sys/src/stable_api/ruby_3_1.rs +0 -317
  168. data/vendor/rb-sys/src/stable_api/ruby_3_2.rs +0 -315
  169. data/vendor/rb-sys/src/stable_api/ruby_3_3.rs +0 -326
  170. data/vendor/rb-sys/src/stable_api/ruby_3_4.rs +0 -327
  171. data/vendor/rb-sys/src/stable_api.rs +0 -261
  172. data/vendor/rb-sys/src/symbol.rs +0 -31
  173. data/vendor/rb-sys/src/tracking_allocator.rs +0 -332
  174. data/vendor/rb-sys/src/utils.rs +0 -89
  175. data/vendor/rb-sys/src/value_type.rs +0 -7
@@ -1,1051 +0,0 @@
1
- //! JATS (Journal Article Tag Suite) document extractor.
2
- //!
3
- //! This extractor handles JATS XML documents, the standard format for scientific journal articles
4
- //! used by PubMed Central and major academic publishers.
5
- //!
6
- //! It extracts:
7
- //! - Rich metadata (title, subtitle, authors with affiliations, DOI, PII, keywords, dates)
8
- //! - Article abstract (regular and graphical)
9
- //! - Section hierarchy and content (intro, methods, results, discussion)
10
- //! - Paragraphs and text content
11
- //! - Tables with captions
12
- //! - Figures with captions
13
- //! - Citations and references
14
- //! - Supplementary material information
15
-
16
- use crate::Result;
17
- use crate::core::config::ExtractionConfig;
18
- use crate::extraction::cells_to_markdown;
19
- use crate::plugins::{DocumentExtractor, Plugin};
20
- use crate::types::{ExtractionResult, Metadata, Table};
21
- use async_trait::async_trait;
22
- use quick_xml::Reader;
23
- use quick_xml::events::Event;
24
- use std::path::Path;
25
-
26
- /// JATS document extractor.
27
- ///
28
- /// Supports JATS (Journal Article Tag Suite) XML documents in various versions,
29
- /// handling both the full article structure and minimal JATS subsets.
30
- pub struct JatsExtractor;
31
-
32
- impl Default for JatsExtractor {
33
- fn default() -> Self {
34
- Self::new()
35
- }
36
- }
37
-
38
- impl JatsExtractor {
39
- pub fn new() -> Self {
40
- Self
41
- }
42
- }
43
-
44
- /// Extract text content from a JATS element and its children.
45
- fn extract_text_content(reader: &mut Reader<&[u8]>) -> Result<String> {
46
- let mut text = String::new();
47
- let mut depth = 0;
48
-
49
- loop {
50
- match reader.read_event() {
51
- Ok(Event::Start(_)) => {
52
- depth += 1;
53
- }
54
- Ok(Event::End(_)) => {
55
- if depth == 0 {
56
- break;
57
- }
58
- depth -= 1;
59
- if !text.is_empty() && !text.ends_with('\n') {
60
- text.push(' ');
61
- }
62
- }
63
- Ok(Event::Text(t)) => {
64
- let decoded = String::from_utf8_lossy(t.as_ref()).to_string();
65
- if !decoded.trim().is_empty() {
66
- text.push_str(&decoded);
67
- text.push(' ');
68
- }
69
- }
70
- Ok(Event::CData(t)) => {
71
- let decoded = std::str::from_utf8(t.as_ref()).unwrap_or("").to_string();
72
- if !decoded.trim().is_empty() {
73
- text.push_str(&decoded);
74
- text.push('\n');
75
- }
76
- }
77
- Ok(Event::Eof) => break,
78
- Err(e) => {
79
- return Err(crate::error::KreuzbergError::parsing(format!(
80
- "XML parsing error: {}",
81
- e
82
- )));
83
- }
84
- _ => {}
85
- }
86
- }
87
-
88
- Ok(text.trim().to_string())
89
- }
90
-
91
- /// Structure to hold extracted JATS metadata.
92
- #[derive(Debug, Clone, Default)]
93
- struct JatsMetadataExtracted {
94
- title: String,
95
- subtitle: Option<String>,
96
- authors: Vec<String>,
97
- affiliations: Vec<String>,
98
- doi: Option<String>,
99
- pii: Option<String>,
100
- keywords: Vec<String>,
101
- publication_date: Option<String>,
102
- volume: Option<String>,
103
- issue: Option<String>,
104
- pages: Option<String>,
105
- journal_title: Option<String>,
106
- article_type: Option<String>,
107
- abstract_text: Option<String>,
108
- corresponding_author: Option<String>,
109
- }
110
-
111
- /// Extract all content in a single optimized pass.
112
- /// Combines metadata extraction, content parsing, and table extraction into one pass.
113
- fn extract_jats_all_in_one(content: &str) -> Result<(JatsMetadataExtracted, String, String, Vec<Table>)> {
114
- let mut reader = Reader::from_str(content);
115
- let mut metadata = JatsMetadataExtracted::default();
116
- let mut body_content = String::new();
117
- let mut title = String::new();
118
-
119
- let mut in_article_meta = false;
120
- let mut in_article_title = false;
121
- let mut in_subtitle = false;
122
- let mut in_contrib = false;
123
- let mut in_name = false;
124
- let mut in_aff = false;
125
- let mut in_abstract = false;
126
- let mut in_kwd_group = false;
127
- let mut in_kwd = false;
128
- let mut current_author = String::new();
129
- let mut current_aff = String::new();
130
- let mut abstract_content = String::new();
131
-
132
- let mut in_body = false;
133
- let mut in_section = false;
134
- let mut in_para = false;
135
-
136
- let mut in_table = false;
137
- let mut in_thead = false;
138
- let mut in_tbody = false;
139
- let mut in_row = false;
140
- let mut current_table: Vec<Vec<String>> = Vec::new();
141
- let mut current_row: Vec<String> = Vec::new();
142
- let mut tables = Vec::new();
143
- let mut table_index = 0;
144
-
145
- loop {
146
- match reader.read_event() {
147
- Ok(Event::Start(e)) => {
148
- let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
149
-
150
- match tag.as_str() {
151
- "article" => {
152
- for attr in e.attributes() {
153
- if let Ok(attr) = attr
154
- && String::from_utf8_lossy(attr.key.as_ref()) == "article-type"
155
- {
156
- metadata.article_type = Some(String::from_utf8_lossy(attr.value.as_ref()).to_string());
157
- }
158
- }
159
- }
160
- "article-meta" => {
161
- in_article_meta = true;
162
- }
163
- "article-title" if in_article_meta => {
164
- in_article_title = true;
165
- }
166
- "subtitle" if in_article_meta => {
167
- in_subtitle = true;
168
- }
169
- "contrib" if in_article_meta => {
170
- in_contrib = true;
171
- current_author.clear();
172
- }
173
- "name" if in_contrib => {
174
- in_name = true;
175
- }
176
- "aff" if in_article_meta => {
177
- in_aff = true;
178
- current_aff.clear();
179
- }
180
- "article-id" if in_article_meta => {
181
- let mut id_type = String::new();
182
- for attr in e.attributes() {
183
- if let Ok(attr) = attr
184
- && String::from_utf8_lossy(attr.key.as_ref()) == "pub-id-type"
185
- {
186
- id_type = String::from_utf8_lossy(attr.value.as_ref()).to_string();
187
- }
188
- }
189
-
190
- let id_text = extract_text_content(&mut reader)?;
191
- match id_type.as_str() {
192
- "doi" => metadata.doi = Some(id_text),
193
- "pii" => metadata.pii = Some(id_text),
194
- _ => {}
195
- }
196
- continue;
197
- }
198
- "volume" if in_article_meta => {
199
- let vol_text = extract_text_content(&mut reader)?;
200
- metadata.volume = Some(vol_text);
201
- continue;
202
- }
203
- "issue" if in_article_meta => {
204
- let issue_text = extract_text_content(&mut reader)?;
205
- metadata.issue = Some(issue_text);
206
- continue;
207
- }
208
- "fpage" | "lpage" if in_article_meta => {
209
- let page_text = extract_text_content(&mut reader)?;
210
- if let Some(pages) = &mut metadata.pages {
211
- pages.push('-');
212
- pages.push_str(&page_text);
213
- } else {
214
- metadata.pages = Some(page_text);
215
- }
216
- continue;
217
- }
218
- "pub-date" if in_article_meta => {
219
- let date_text = extract_text_content(&mut reader)?;
220
- if metadata.publication_date.is_none() {
221
- metadata.publication_date = Some(date_text);
222
- }
223
- continue;
224
- }
225
- "journal-title" if in_article_meta => {
226
- let journal_text = extract_text_content(&mut reader)?;
227
- if metadata.journal_title.is_none() {
228
- metadata.journal_title = Some(journal_text);
229
- }
230
- continue;
231
- }
232
- "abstract" if in_article_meta => {
233
- in_abstract = true;
234
- abstract_content.clear();
235
- }
236
- "kwd-group" if in_article_meta => {
237
- in_kwd_group = true;
238
- }
239
- "kwd" if in_kwd_group => {
240
- in_kwd = true;
241
- }
242
- "corresp" if in_article_meta => {
243
- let corresp_text = extract_text_content(&mut reader)?;
244
- metadata.corresponding_author = Some(corresp_text);
245
- continue;
246
- }
247
- "body" => {
248
- in_body = true;
249
- }
250
- "sec" if in_body => {
251
- in_section = true;
252
- }
253
- "title" if (in_section || in_body) && !in_article_title => {
254
- let section_title = extract_text_content(&mut reader)?;
255
- if !section_title.is_empty() {
256
- body_content.push_str("## ");
257
- body_content.push_str(&section_title);
258
- body_content.push_str("\n\n");
259
- }
260
- continue;
261
- }
262
- "p" if in_body || in_section => {
263
- in_para = true;
264
- }
265
- "table" => {
266
- in_table = true;
267
- current_table.clear();
268
- }
269
- "thead" if in_table => {
270
- in_thead = true;
271
- }
272
- "tbody" if in_table => {
273
- in_tbody = true;
274
- }
275
- "tr" if (in_thead || in_tbody) && in_table => {
276
- in_row = true;
277
- current_row.clear();
278
- }
279
- "td" | "th" if in_row => {
280
- let mut cell_text = String::new();
281
- let mut cell_depth = 0;
282
-
283
- loop {
284
- match reader.read_event() {
285
- Ok(Event::Start(_)) => {
286
- cell_depth += 1;
287
- }
288
- Ok(Event::End(e)) => {
289
- let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
290
- if (tag == "td" || tag == "th") && cell_depth == 0 {
291
- break;
292
- }
293
- if cell_depth > 0 {
294
- cell_depth -= 1;
295
- }
296
- }
297
- Ok(Event::Text(t)) => {
298
- let decoded = String::from_utf8_lossy(t.as_ref()).to_string();
299
- if !decoded.trim().is_empty() {
300
- if !cell_text.is_empty() {
301
- cell_text.push(' ');
302
- }
303
- cell_text.push_str(decoded.trim());
304
- }
305
- }
306
- Ok(Event::Eof) => break,
307
- Err(e) => {
308
- return Err(crate::error::KreuzbergError::parsing(format!(
309
- "XML parsing error: {}",
310
- e
311
- )));
312
- }
313
- _ => {}
314
- }
315
- }
316
-
317
- current_row.push(cell_text);
318
- }
319
- _ => {}
320
- }
321
- }
322
- Ok(Event::End(e)) => {
323
- let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
324
-
325
- match tag.as_str() {
326
- "article-meta" => {
327
- in_article_meta = false;
328
- }
329
- "article-title" if in_article_title => {
330
- in_article_title = false;
331
- }
332
- "subtitle" if in_subtitle => {
333
- in_subtitle = false;
334
- }
335
- "contrib" if in_contrib => {
336
- if !current_author.is_empty() {
337
- metadata.authors.push(current_author.clone());
338
- }
339
- in_contrib = false;
340
- current_author.clear();
341
- }
342
- "name" if in_name => {
343
- in_name = false;
344
- }
345
- "aff" if in_aff => {
346
- if !current_aff.is_empty() {
347
- metadata.affiliations.push(current_aff.clone());
348
- }
349
- in_aff = false;
350
- current_aff.clear();
351
- }
352
- "abstract" if in_abstract => {
353
- in_abstract = false;
354
- metadata.abstract_text = Some(abstract_content.trim().to_string());
355
- }
356
- "kwd-group" if in_kwd_group => {
357
- in_kwd_group = false;
358
- }
359
- "kwd" if in_kwd => {
360
- in_kwd = false;
361
- }
362
- "body" => {
363
- in_body = false;
364
- }
365
- "sec" if in_section => {
366
- in_section = false;
367
- }
368
- "p" if in_para => {
369
- in_para = false;
370
- }
371
- "table" if in_table => {
372
- if !current_table.is_empty() {
373
- let markdown = cells_to_markdown(&current_table);
374
- tables.push(Table {
375
- cells: current_table.clone(),
376
- markdown,
377
- page_number: table_index + 1,
378
- });
379
- table_index += 1;
380
- current_table.clear();
381
- }
382
- in_table = false;
383
- }
384
- "thead" if in_thead => {
385
- in_thead = false;
386
- }
387
- "tbody" if in_tbody => {
388
- in_tbody = false;
389
- }
390
- "tr" if in_row => {
391
- if !current_row.is_empty() {
392
- current_table.push(current_row.clone());
393
- current_row.clear();
394
- }
395
- in_row = false;
396
- }
397
- _ => {}
398
- }
399
- }
400
- Ok(Event::Text(t)) => {
401
- let decoded = String::from_utf8_lossy(t.as_ref()).to_string();
402
- let trimmed = decoded.trim();
403
-
404
- if !trimmed.is_empty() {
405
- if in_article_title && metadata.title.is_empty() {
406
- metadata.title.push_str(trimmed);
407
- } else if in_subtitle && metadata.subtitle.is_none() {
408
- metadata.subtitle = Some(trimmed.to_string());
409
- } else if in_name {
410
- if !current_author.is_empty() {
411
- current_author.push(' ');
412
- }
413
- current_author.push_str(trimmed);
414
- } else if in_aff {
415
- if !current_aff.is_empty() {
416
- current_aff.push(' ');
417
- }
418
- current_aff.push_str(trimmed);
419
- } else if in_abstract {
420
- if !abstract_content.is_empty() {
421
- abstract_content.push(' ');
422
- }
423
- abstract_content.push_str(trimmed);
424
- } else if in_kwd {
425
- metadata.keywords.push(trimmed.to_string());
426
- } else if in_para && in_body {
427
- body_content.push_str(trimmed);
428
- body_content.push_str("\n\n");
429
- }
430
- }
431
- }
432
- Ok(Event::Eof) => break,
433
- Err(e) => {
434
- return Err(crate::error::KreuzbergError::parsing(format!(
435
- "XML parsing error: {}",
436
- e
437
- )));
438
- }
439
- _ => {}
440
- }
441
- }
442
-
443
- let mut final_output = body_content;
444
- if !metadata.title.is_empty() {
445
- final_output = format!("# {}\n\n{}", metadata.title, final_output);
446
- title = metadata.title.clone();
447
- }
448
-
449
- Ok((metadata, final_output.trim().to_string(), title, tables))
450
- }
451
-
452
- impl Plugin for JatsExtractor {
453
- fn name(&self) -> &str {
454
- "jats-extractor"
455
- }
456
-
457
- fn version(&self) -> String {
458
- env!("CARGO_PKG_VERSION").to_string()
459
- }
460
-
461
- fn initialize(&self) -> Result<()> {
462
- Ok(())
463
- }
464
-
465
- fn shutdown(&self) -> Result<()> {
466
- Ok(())
467
- }
468
- }
469
-
470
- #[async_trait]
471
- impl DocumentExtractor for JatsExtractor {
472
- #[cfg_attr(
473
- feature = "otel",
474
- tracing::instrument(
475
- skip(self, content, config),
476
- fields(
477
- extractor.name = self.name(),
478
- content.size_bytes = content.len(),
479
- )
480
- )
481
- )]
482
- async fn extract_bytes(
483
- &self,
484
- content: &[u8],
485
- mime_type: &str,
486
- config: &ExtractionConfig,
487
- ) -> Result<ExtractionResult> {
488
- let _ = config;
489
- let jats_content = std::str::from_utf8(content)
490
- .map(|s| s.to_string())
491
- .unwrap_or_else(|_| String::from_utf8_lossy(content).to_string());
492
-
493
- let (jats_metadata, extracted_content, _title, tables) = extract_jats_all_in_one(&jats_content)?;
494
-
495
- let mut metadata = Metadata::default();
496
- let mut subject_parts = Vec::new();
497
-
498
- if !jats_metadata.title.is_empty() {
499
- metadata.subject = Some(jats_metadata.title.clone());
500
- subject_parts.push(format!("Title: {}", jats_metadata.title));
501
- }
502
-
503
- if let Some(subtitle) = &jats_metadata.subtitle {
504
- subject_parts.push(format!("Subtitle: {}", subtitle));
505
- }
506
-
507
- if !jats_metadata.authors.is_empty() {
508
- subject_parts.push(format!("Authors: {}", jats_metadata.authors.join("; ")));
509
- }
510
-
511
- if !jats_metadata.affiliations.is_empty() {
512
- subject_parts.push(format!("Affiliations: {}", jats_metadata.affiliations.join("; ")));
513
- }
514
-
515
- if let Some(doi) = &jats_metadata.doi {
516
- subject_parts.push(format!("DOI: {}", doi));
517
- }
518
-
519
- if let Some(pii) = &jats_metadata.pii {
520
- subject_parts.push(format!("PII: {}", pii));
521
- }
522
-
523
- if !jats_metadata.keywords.is_empty() {
524
- subject_parts.push(format!("Keywords: {}", jats_metadata.keywords.join("; ")));
525
- }
526
-
527
- if let Some(date) = &jats_metadata.publication_date {
528
- metadata.date = Some(date.clone());
529
- subject_parts.push(format!("Publication Date: {}", date));
530
- }
531
-
532
- if let Some(volume) = &jats_metadata.volume {
533
- subject_parts.push(format!("Volume: {}", volume));
534
- }
535
-
536
- if let Some(issue) = &jats_metadata.issue {
537
- subject_parts.push(format!("Issue: {}", issue));
538
- }
539
-
540
- if let Some(pages) = &jats_metadata.pages {
541
- subject_parts.push(format!("Pages: {}", pages));
542
- }
543
-
544
- if let Some(journal_title) = &jats_metadata.journal_title {
545
- subject_parts.push(format!("Journal: {}", journal_title));
546
- }
547
-
548
- if let Some(article_type) = &jats_metadata.article_type {
549
- subject_parts.push(format!("Article Type: {}", article_type));
550
- }
551
-
552
- if let Some(abstract_text) = &jats_metadata.abstract_text {
553
- subject_parts.push(format!("Abstract: {}", abstract_text));
554
- }
555
-
556
- if let Some(corresp_author) = &jats_metadata.corresponding_author {
557
- subject_parts.push(format!("Corresponding Author: {}", corresp_author));
558
- }
559
-
560
- if !subject_parts.is_empty() {
561
- metadata.subject = Some(subject_parts.join(" | "));
562
- }
563
-
564
- Ok(ExtractionResult {
565
- content: extracted_content,
566
- mime_type: mime_type.to_string(),
567
- metadata,
568
- tables,
569
- detected_languages: None,
570
- chunks: None,
571
- images: None,
572
- })
573
- }
574
-
575
- #[cfg(feature = "tokio-runtime")]
576
- #[cfg_attr(
577
- feature = "otel",
578
- tracing::instrument(
579
- skip(self, path, config),
580
- fields(
581
- extractor.name = self.name(),
582
- )
583
- )
584
- )]
585
- async fn extract_file(&self, path: &Path, mime_type: &str, config: &ExtractionConfig) -> Result<ExtractionResult> {
586
- let bytes = tokio::fs::read(path).await?;
587
- self.extract_bytes(&bytes, mime_type, config).await
588
- }
589
-
590
- fn supported_mime_types(&self) -> &[&str] {
591
- &["application/x-jats+xml", "text/jats"]
592
- }
593
-
594
- fn priority(&self) -> i32 {
595
- 50
596
- }
597
- }
598
-
599
- #[cfg(test)]
600
- mod tests {
601
- use super::*;
602
-
603
- #[test]
604
- fn test_jats_extractor_plugin_interface() {
605
- let extractor = JatsExtractor::new();
606
- assert_eq!(extractor.name(), "jats-extractor");
607
- assert!(extractor.initialize().is_ok());
608
- assert!(extractor.shutdown().is_ok());
609
- }
610
-
611
- #[test]
612
- fn test_jats_extractor_supported_mime_types() {
613
- let extractor = JatsExtractor::new();
614
- let mime_types = extractor.supported_mime_types();
615
- assert_eq!(mime_types.len(), 2);
616
- assert!(mime_types.contains(&"application/x-jats+xml"));
617
- assert!(mime_types.contains(&"text/jats"));
618
- }
619
-
620
- #[test]
621
- fn test_jats_extractor_priority() {
622
- let extractor = JatsExtractor::new();
623
- assert_eq!(extractor.priority(), 50);
624
- }
625
-
626
- #[test]
627
- fn test_parse_simple_jats_article() {
628
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
629
- <article>
630
- <front>
631
- <article-meta>
632
- <article-title>Test Article Title</article-title>
633
- </article-meta>
634
- </front>
635
- <body>
636
- <p>Test content paragraph.</p>
637
- </body>
638
- </article>"#;
639
-
640
- let (metadata, content, title, _tables) = extract_jats_all_in_one(jats).expect("Parse failed");
641
- assert_eq!(title, "Test Article Title");
642
- assert_eq!(metadata.title, "Test Article Title");
643
- assert!(content.contains("Test Article Title"));
644
- assert!(content.contains("Test content"));
645
- }
646
-
647
- #[test]
648
- fn test_extract_jats_title() {
649
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
650
- <article>
651
- <front>
652
- <article-meta>
653
- <article-title>Effects of Caffeine on Human Health</article-title>
654
- </article-meta>
655
- </front>
656
- </article>"#;
657
-
658
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
659
- assert_eq!(metadata.title, "Effects of Caffeine on Human Health");
660
- }
661
-
662
- #[test]
663
- fn test_extract_jats_subtitle() {
664
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
665
- <article>
666
- <front>
667
- <article-meta>
668
- <article-title>Main Title</article-title>
669
- <subtitle>A Systematic Review</subtitle>
670
- </article-meta>
671
- </front>
672
- </article>"#;
673
-
674
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
675
- assert_eq!(metadata.title, "Main Title");
676
- assert_eq!(metadata.subtitle, Some("A Systematic Review".to_string()));
677
- }
678
-
679
- #[test]
680
- fn test_extract_jats_authors() {
681
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
682
- <article>
683
- <front>
684
- <article-meta>
685
- <contrib-group>
686
- <contrib contrib-type="author">
687
- <name>
688
- <surname>Smith</surname>
689
- <given-names>John A.</given-names>
690
- </name>
691
- </contrib>
692
- <contrib contrib-type="author">
693
- <name>
694
- <surname>Johnson</surname>
695
- <given-names>Jane B.</given-names>
696
- </name>
697
- </contrib>
698
- </contrib-group>
699
- </article-meta>
700
- </front>
701
- </article>"#;
702
-
703
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
704
- assert_eq!(metadata.authors.len(), 2);
705
- assert!(metadata.authors[0].contains("Smith"));
706
- assert!(metadata.authors[1].contains("Johnson"));
707
- }
708
-
709
- #[test]
710
- fn test_extract_jats_affiliations() {
711
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
712
- <article>
713
- <front>
714
- <article-meta>
715
- <aff id="aff1">Department of Medicine, Harvard University, Cambridge, MA</aff>
716
- <aff id="aff2">Center for Health Research, Boston Medical Center, Boston, MA</aff>
717
- </article-meta>
718
- </front>
719
- </article>"#;
720
-
721
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
722
- assert_eq!(metadata.affiliations.len(), 2);
723
- assert!(metadata.affiliations[0].contains("Harvard"));
724
- assert!(metadata.affiliations[1].contains("Boston"));
725
- }
726
-
727
- #[test]
728
- fn test_extract_jats_doi_and_pii() {
729
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
730
- <article>
731
- <front>
732
- <article-meta>
733
- <article-id pub-id-type="doi">10.1371/journal.pmed.0020124</article-id>
734
- <article-id pub-id-type="pii">05-PLME-RA-0071R2</article-id>
735
- </article-meta>
736
- </front>
737
- </article>"#;
738
-
739
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
740
- assert_eq!(metadata.doi, Some("10.1371/journal.pmed.0020124".to_string()));
741
- assert_eq!(metadata.pii, Some("05-PLME-RA-0071R2".to_string()));
742
- }
743
-
744
- #[test]
745
- fn test_extract_jats_keywords() {
746
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
747
- <article>
748
- <front>
749
- <article-meta>
750
- <kwd-group>
751
- <kwd>caffeine</kwd>
752
- <kwd>meta-analysis</kwd>
753
- <kwd>systematic review</kwd>
754
- </kwd-group>
755
- </article-meta>
756
- </front>
757
- </article>"#;
758
-
759
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
760
- assert_eq!(metadata.keywords.len(), 3);
761
- assert!(metadata.keywords.contains(&"caffeine".to_string()));
762
- assert!(metadata.keywords.contains(&"meta-analysis".to_string()));
763
- }
764
-
765
- #[test]
766
- fn test_extract_jats_publication_info() {
767
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
768
- <article>
769
- <front>
770
- <article-meta>
771
- <pub-date pub-type="epub">
772
- <day>18</day>
773
- <month>04</month>
774
- <year>2005</year>
775
- </pub-date>
776
- <volume>2</volume>
777
- <issue>4</issue>
778
- <fpage>e124</fpage>
779
- <lpage>e132</lpage>
780
- </article-meta>
781
- </front>
782
- </article>"#;
783
-
784
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
785
- assert!(metadata.publication_date.is_some());
786
- assert_eq!(metadata.volume, Some("2".to_string()));
787
- assert_eq!(metadata.issue, Some("4".to_string()));
788
- assert!(metadata.pages.is_some());
789
- }
790
-
791
- #[test]
792
- fn test_extract_jats_abstract() {
793
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
794
- <article>
795
- <front>
796
- <article-meta>
797
- <abstract>
798
- <sec>
799
- <title>Background</title>
800
- <p>This is the background information of the study.</p>
801
- </sec>
802
- <sec>
803
- <title>Methods</title>
804
- <p>We used quantitative analysis to evaluate the hypothesis.</p>
805
- </sec>
806
- </abstract>
807
- </article-meta>
808
- </front>
809
- </article>"#;
810
-
811
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
812
- assert!(metadata.abstract_text.is_some());
813
- let abstract_text = metadata.abstract_text.unwrap();
814
- assert!(abstract_text.contains("background"));
815
- assert!(abstract_text.contains("quantitative"));
816
- }
817
-
818
- #[test]
819
- fn test_extract_jats_tables_basic() {
820
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
821
- <article>
822
- <body>
823
- <table-wrap id="tbl1">
824
- <table>
825
- <thead>
826
- <tr>
827
- <th>Study</th>
828
- <th>Year</th>
829
- </tr>
830
- </thead>
831
- <tbody>
832
- <tr>
833
- <td>Study A</td>
834
- <td>2003</td>
835
- </tr>
836
- <tr>
837
- <td>Study B</td>
838
- <td>2004</td>
839
- </tr>
840
- </tbody>
841
- </table>
842
- </table-wrap>
843
- </body>
844
- </article>"#;
845
-
846
- let (_metadata, _content, _title, tables) = extract_jats_all_in_one(jats).expect("Table extraction failed");
847
- assert_eq!(tables.len(), 1);
848
- assert_eq!(tables[0].cells.len(), 3);
849
- }
850
-
851
- #[test]
852
- fn test_extract_jats_corresponding_author() {
853
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
854
- <article>
855
- <front>
856
- <article-meta>
857
- <author-notes>
858
- <corresp id="cor1">To whom correspondence should be addressed. E-mail: rwilliams@yale.edu</corresp>
859
- </author-notes>
860
- </article-meta>
861
- </front>
862
- </article>"#;
863
-
864
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
865
- assert!(metadata.corresponding_author.is_some());
866
- let corresp = metadata.corresponding_author.unwrap();
867
- assert!(corresp.contains("rwilliams"));
868
- }
869
-
870
- #[test]
871
- fn test_extract_jats_section_hierarchy() {
872
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
873
- <article>
874
- <front>
875
- <article-meta>
876
- <article-title>Article Title</article-title>
877
- </article-meta>
878
- </front>
879
- <body>
880
- <sec id="s1">
881
- <title>Introduction</title>
882
- <p>Intro content.</p>
883
- </sec>
884
- <sec id="s2">
885
- <title>Methods</title>
886
- <p>Methods content.</p>
887
- </sec>
888
- <sec id="s3">
889
- <title>Results</title>
890
- <p>Results content.</p>
891
- </sec>
892
- </body>
893
- </article>"#;
894
-
895
- let (_metadata, content, _title, _tables) = extract_jats_all_in_one(jats).expect("Parse failed");
896
- assert!(content.contains("Introduction"));
897
- assert!(content.contains("Methods"));
898
- assert!(content.contains("Results"));
899
- }
900
-
901
- #[test]
902
- fn test_jats_extractor_full_metadata_extraction() {
903
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
904
- <article>
905
- <front>
906
- <article-meta>
907
- <article-title>Sample Article</article-title>
908
- <contrib-group>
909
- <contrib contrib-type="author">
910
- <name>
911
- <surname>Smith</surname>
912
- <given-names>John</given-names>
913
- </name>
914
- </contrib>
915
- </contrib-group>
916
- <article-id pub-id-type="doi">10.1234/test</article-id>
917
- <kwd-group>
918
- <kwd>test</kwd>
919
- </kwd-group>
920
- <abstract>
921
- <p>Test abstract.</p>
922
- </abstract>
923
- </article-meta>
924
- </front>
925
- <body>
926
- <p>Sample content.</p>
927
- </body>
928
- </article>"#;
929
-
930
- let (metadata_extracted, _content, _title, _tables) =
931
- extract_jats_all_in_one(jats).expect("Metadata extraction failed");
932
- assert_eq!(metadata_extracted.title, "Sample Article");
933
- assert_eq!(metadata_extracted.authors.len(), 1);
934
- assert_eq!(metadata_extracted.doi, Some("10.1234/test".to_string()));
935
- assert_eq!(metadata_extracted.keywords.len(), 1);
936
- assert!(metadata_extracted.abstract_text.is_some());
937
- }
938
-
939
- #[test]
940
- fn test_jats_extractor_empty_article() {
941
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
942
- <article>
943
- <front>
944
- <article-meta>
945
- </article-meta>
946
- </front>
947
- <body>
948
- </body>
949
- </article>"#;
950
-
951
- let (metadata, content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
952
- assert!(metadata.title.is_empty());
953
- assert!(content.is_empty() || content.trim().is_empty());
954
- }
955
-
956
- #[test]
957
- fn test_extract_jats_journal_title() {
958
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
959
- <article>
960
- <front>
961
- <article-meta>
962
- <article-title>Test Article</article-title>
963
- <journal-title>Nature Medicine</journal-title>
964
- </article-meta>
965
- </front>
966
- </article>"#;
967
-
968
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
969
- assert_eq!(metadata.journal_title, Some("Nature Medicine".to_string()));
970
- }
971
-
972
- #[test]
973
- fn test_extract_jats_article_type() {
974
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
975
- <article article-type="research-article">
976
- <front>
977
- <article-meta>
978
- <article-title>Test Article</article-title>
979
- </article-meta>
980
- </front>
981
- </article>"#;
982
-
983
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
984
- assert_eq!(metadata.article_type, Some("research-article".to_string()));
985
- }
986
-
987
- #[test]
988
- fn test_extract_all_13_metadata_fields() {
989
- let jats = r#"<?xml version="1.0" encoding="UTF-8"?>
990
- <article article-type="research-article">
991
- <front>
992
- <article-meta>
993
- <article-title>Full Metadata Test</article-title>
994
- <subtitle>A Complete Example</subtitle>
995
- <contrib-group>
996
- <contrib contrib-type="author">
997
- <name>
998
- <surname>Author</surname>
999
- <given-names>First</given-names>
1000
- </name>
1001
- </contrib>
1002
- </contrib-group>
1003
- <aff>Department of Testing, Test University</aff>
1004
- <article-id pub-id-type="doi">10.1234/full-test</article-id>
1005
- <article-id pub-id-type="pii">TEST-001</article-id>
1006
- <kwd-group>
1007
- <kwd>testing</kwd>
1008
- <kwd>metadata</kwd>
1009
- </kwd-group>
1010
- <pub-date pub-type="epub">
1011
- <year>2024</year>
1012
- </pub-date>
1013
- <volume>5</volume>
1014
- <issue>3</issue>
1015
- <fpage>100</fpage>
1016
- <lpage>110</lpage>
1017
- <journal-title>Test Journal</journal-title>
1018
- <abstract>
1019
- <p>This is a test abstract for all metadata fields.</p>
1020
- </abstract>
1021
- <author-notes>
1022
- <corresp>Correspondence: test@example.com</corresp>
1023
- </author-notes>
1024
- </article-meta>
1025
- </front>
1026
- <body>
1027
- <p>Test content.</p>
1028
- </body>
1029
- </article>"#;
1030
-
1031
- let (metadata, _content, _title, _tables) = extract_jats_all_in_one(jats).expect("Metadata extraction failed");
1032
-
1033
- assert_eq!(metadata.title, "Full Metadata Test");
1034
- assert_eq!(metadata.subtitle, Some("A Complete Example".to_string()));
1035
- assert_eq!(metadata.authors.len(), 1);
1036
- assert!(metadata.authors[0].contains("Author"));
1037
- assert_eq!(metadata.affiliations.len(), 1);
1038
- assert!(metadata.affiliations[0].contains("Testing"));
1039
- assert_eq!(metadata.doi, Some("10.1234/full-test".to_string()));
1040
- assert_eq!(metadata.pii, Some("TEST-001".to_string()));
1041
- assert_eq!(metadata.keywords.len(), 2);
1042
- assert!(metadata.publication_date.is_some());
1043
- assert_eq!(metadata.volume, Some("5".to_string()));
1044
- assert_eq!(metadata.issue, Some("3".to_string()));
1045
- assert!(metadata.pages.is_some());
1046
- assert_eq!(metadata.journal_title, Some("Test Journal".to_string()));
1047
- assert_eq!(metadata.article_type, Some("research-article".to_string()));
1048
- assert!(metadata.abstract_text.is_some());
1049
- assert!(metadata.corresponding_author.is_some());
1050
- }
1051
- }