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.
- checksums.yaml +4 -4
- data/.gitignore +0 -6
- data/.rubocop.yaml +534 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +11 -11
- data/README.md +5 -10
- data/examples/async_patterns.rb +0 -1
- data/ext/kreuzberg_rb/extconf.rb +0 -10
- data/ext/kreuzberg_rb/native/Cargo.toml +15 -23
- data/ext/kreuzberg_rb/native/build.rs +2 -0
- data/ext/kreuzberg_rb/native/include/ieeefp.h +1 -1
- data/ext/kreuzberg_rb/native/include/msvc_compat/strings.h +1 -1
- data/ext/kreuzberg_rb/native/include/strings.h +2 -2
- data/ext/kreuzberg_rb/native/include/unistd.h +1 -1
- data/ext/kreuzberg_rb/native/src/lib.rs +16 -75
- data/kreuzberg.gemspec +14 -57
- data/lib/kreuzberg/cache_api.rb +0 -1
- data/lib/kreuzberg/cli.rb +2 -2
- data/lib/kreuzberg/config.rb +2 -9
- data/lib/kreuzberg/errors.rb +7 -75
- data/lib/kreuzberg/extraction_api.rb +0 -1
- data/lib/kreuzberg/setup_lib_path.rb +0 -1
- data/lib/kreuzberg/version.rb +1 -1
- data/lib/kreuzberg.rb +0 -21
- data/pkg/kreuzberg-4.0.0.rc1.gem +0 -0
- data/sig/kreuzberg.rbs +3 -55
- data/spec/binding/cli_proxy_spec.rb +4 -2
- data/spec/binding/cli_spec.rb +11 -12
- data/spec/examples.txt +104 -0
- data/spec/fixtures/config.yaml +1 -0
- data/spec/spec_helper.rb +1 -1
- data/vendor/kreuzberg/Cargo.toml +42 -112
- data/vendor/kreuzberg/README.md +2 -2
- data/vendor/kreuzberg/build.rs +4 -18
- data/vendor/kreuzberg/src/bin/profile_extract.rs +455 -0
- data/vendor/kreuzberg/src/cache/mod.rs +3 -27
- data/vendor/kreuzberg/src/core/batch_mode.rs +0 -60
- data/vendor/kreuzberg/src/core/extractor.rs +81 -202
- data/vendor/kreuzberg/src/core/io.rs +2 -4
- data/vendor/kreuzberg/src/core/mime.rs +12 -2
- data/vendor/kreuzberg/src/core/mod.rs +1 -4
- data/vendor/kreuzberg/src/core/pipeline.rs +33 -111
- data/vendor/kreuzberg/src/embeddings.rs +16 -125
- data/vendor/kreuzberg/src/error.rs +1 -1
- data/vendor/kreuzberg/src/extraction/docx.rs +1 -1
- data/vendor/kreuzberg/src/extraction/image.rs +13 -13
- data/vendor/kreuzberg/src/extraction/libreoffice.rs +1 -0
- data/vendor/kreuzberg/src/extraction/mod.rs +5 -9
- data/vendor/kreuzberg/src/extraction/office_metadata/mod.rs +0 -2
- data/vendor/kreuzberg/src/extraction/pandoc/batch.rs +275 -0
- data/vendor/kreuzberg/src/extraction/pandoc/mime_types.rs +178 -0
- data/vendor/kreuzberg/src/extraction/pandoc/mod.rs +491 -0
- data/vendor/kreuzberg/src/extraction/pandoc/server.rs +496 -0
- data/vendor/kreuzberg/src/extraction/pandoc/subprocess.rs +1188 -0
- data/vendor/kreuzberg/src/extraction/pandoc/version.rs +162 -0
- data/vendor/kreuzberg/src/extractors/archive.rs +0 -21
- data/vendor/kreuzberg/src/extractors/docx.rs +128 -16
- data/vendor/kreuzberg/src/extractors/email.rs +0 -14
- data/vendor/kreuzberg/src/extractors/excel.rs +20 -19
- data/vendor/kreuzberg/src/extractors/html.rs +154 -137
- data/vendor/kreuzberg/src/extractors/image.rs +4 -7
- data/vendor/kreuzberg/src/extractors/mod.rs +9 -106
- data/vendor/kreuzberg/src/extractors/pandoc.rs +201 -0
- data/vendor/kreuzberg/src/extractors/pdf.rs +15 -12
- data/vendor/kreuzberg/src/extractors/pptx.rs +3 -17
- data/vendor/kreuzberg/src/extractors/structured.rs +0 -14
- data/vendor/kreuzberg/src/extractors/text.rs +5 -23
- data/vendor/kreuzberg/src/extractors/xml.rs +0 -7
- data/vendor/kreuzberg/src/keywords/rake.rs +1 -0
- data/vendor/kreuzberg/src/lib.rs +1 -4
- data/vendor/kreuzberg/src/mcp/mod.rs +1 -1
- data/vendor/kreuzberg/src/mcp/server.rs +3 -5
- data/vendor/kreuzberg/src/ocr/processor.rs +2 -18
- data/vendor/kreuzberg/src/pdf/error.rs +1 -1
- data/vendor/kreuzberg/src/pdf/table.rs +44 -17
- data/vendor/kreuzberg/src/pdf/text.rs +3 -0
- data/vendor/kreuzberg/src/plugins/extractor.rs +5 -8
- data/vendor/kreuzberg/src/plugins/ocr.rs +11 -2
- data/vendor/kreuzberg/src/plugins/processor.rs +1 -2
- data/vendor/kreuzberg/src/plugins/registry.rs +0 -13
- data/vendor/kreuzberg/src/plugins/validator.rs +8 -9
- data/vendor/kreuzberg/src/stopwords/mod.rs +2 -2
- data/vendor/kreuzberg/src/types.rs +12 -42
- data/vendor/kreuzberg/tests/batch_orchestration.rs +5 -19
- data/vendor/kreuzberg/tests/batch_processing.rs +3 -15
- data/vendor/kreuzberg/tests/chunking_offset_demo.rs +92 -0
- data/vendor/kreuzberg/tests/concurrency_stress.rs +1 -17
- data/vendor/kreuzberg/tests/config_features.rs +0 -18
- data/vendor/kreuzberg/tests/config_loading_tests.rs +39 -15
- data/vendor/kreuzberg/tests/core_integration.rs +7 -24
- data/vendor/kreuzberg/tests/csv_integration.rs +81 -71
- data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +25 -23
- data/vendor/kreuzberg/tests/pandoc_integration.rs +503 -0
- data/vendor/kreuzberg/tests/pipeline_integration.rs +1 -0
- data/vendor/kreuzberg/tests/plugin_postprocessor_test.rs +1 -0
- data/vendor/kreuzberg/tests/registry_integration_tests.rs +22 -1
- data/vendor/kreuzberg/tests/security_validation.rs +1 -12
- metadata +25 -90
- data/.rubocop.yml +0 -538
- data/ext/kreuzberg_rb/native/Cargo.lock +0 -6535
- data/lib/kreuzberg/error_context.rb +0 -32
- data/vendor/kreuzberg/benches/otel_overhead.rs +0 -48
- data/vendor/kreuzberg/src/extraction/markdown.rs +0 -213
- data/vendor/kreuzberg/src/extraction/office_metadata/odt_properties.rs +0 -287
- data/vendor/kreuzberg/src/extractors/bibtex.rs +0 -469
- data/vendor/kreuzberg/src/extractors/docbook.rs +0 -502
- data/vendor/kreuzberg/src/extractors/epub.rs +0 -707
- data/vendor/kreuzberg/src/extractors/fictionbook.rs +0 -491
- data/vendor/kreuzberg/src/extractors/fictionbook.rs.backup2 +0 -738
- data/vendor/kreuzberg/src/extractors/jats.rs +0 -1051
- data/vendor/kreuzberg/src/extractors/jupyter.rs +0 -367
- data/vendor/kreuzberg/src/extractors/latex.rs +0 -652
- data/vendor/kreuzberg/src/extractors/markdown.rs +0 -700
- data/vendor/kreuzberg/src/extractors/odt.rs +0 -628
- data/vendor/kreuzberg/src/extractors/opml.rs +0 -634
- data/vendor/kreuzberg/src/extractors/orgmode.rs +0 -528
- data/vendor/kreuzberg/src/extractors/rst.rs +0 -576
- data/vendor/kreuzberg/src/extractors/rtf.rs +0 -810
- data/vendor/kreuzberg/src/extractors/security.rs +0 -484
- data/vendor/kreuzberg/src/extractors/security_tests.rs +0 -367
- data/vendor/kreuzberg/src/extractors/typst.rs +0 -650
- data/vendor/kreuzberg/src/panic_context.rs +0 -154
- data/vendor/kreuzberg/tests/api_extract_multipart.rs +0 -52
- data/vendor/kreuzberg/tests/bibtex_parity_test.rs +0 -421
- data/vendor/kreuzberg/tests/docbook_extractor_tests.rs +0 -498
- data/vendor/kreuzberg/tests/docx_vs_pandoc_comparison.rs +0 -370
- data/vendor/kreuzberg/tests/epub_native_extractor_tests.rs +0 -275
- data/vendor/kreuzberg/tests/fictionbook_extractor_tests.rs +0 -228
- data/vendor/kreuzberg/tests/html_table_test.rs +0 -551
- data/vendor/kreuzberg/tests/instrumentation_test.rs +0 -139
- data/vendor/kreuzberg/tests/jats_extractor_tests.rs +0 -639
- data/vendor/kreuzberg/tests/jupyter_extractor_tests.rs +0 -704
- data/vendor/kreuzberg/tests/latex_extractor_tests.rs +0 -496
- data/vendor/kreuzberg/tests/markdown_extractor_tests.rs +0 -490
- data/vendor/kreuzberg/tests/odt_extractor_tests.rs +0 -695
- data/vendor/kreuzberg/tests/opml_extractor_tests.rs +0 -616
- data/vendor/kreuzberg/tests/orgmode_extractor_tests.rs +0 -822
- data/vendor/kreuzberg/tests/rst_extractor_tests.rs +0 -692
- data/vendor/kreuzberg/tests/rtf_extractor_tests.rs +0 -776
- data/vendor/kreuzberg/tests/typst_behavioral_tests.rs +0 -1259
- data/vendor/kreuzberg/tests/typst_extractor_tests.rs +0 -647
- data/vendor/rb-sys/.cargo-ok +0 -1
- data/vendor/rb-sys/.cargo_vcs_info.json +0 -6
- data/vendor/rb-sys/Cargo.lock +0 -393
- data/vendor/rb-sys/Cargo.toml +0 -70
- data/vendor/rb-sys/Cargo.toml.orig +0 -57
- data/vendor/rb-sys/LICENSE-APACHE +0 -190
- data/vendor/rb-sys/LICENSE-MIT +0 -21
- data/vendor/rb-sys/bin/release.sh +0 -21
- data/vendor/rb-sys/build/features.rs +0 -108
- data/vendor/rb-sys/build/main.rs +0 -246
- data/vendor/rb-sys/build/stable_api_config.rs +0 -153
- data/vendor/rb-sys/build/version.rs +0 -48
- data/vendor/rb-sys/readme.md +0 -36
- data/vendor/rb-sys/src/bindings.rs +0 -21
- data/vendor/rb-sys/src/hidden.rs +0 -11
- data/vendor/rb-sys/src/lib.rs +0 -34
- data/vendor/rb-sys/src/macros.rs +0 -371
- data/vendor/rb-sys/src/memory.rs +0 -53
- data/vendor/rb-sys/src/ruby_abi_version.rs +0 -38
- data/vendor/rb-sys/src/special_consts.rs +0 -31
- data/vendor/rb-sys/src/stable_api/compiled.c +0 -179
- data/vendor/rb-sys/src/stable_api/compiled.rs +0 -257
- data/vendor/rb-sys/src/stable_api/ruby_2_6.rs +0 -316
- data/vendor/rb-sys/src/stable_api/ruby_2_7.rs +0 -316
- data/vendor/rb-sys/src/stable_api/ruby_3_0.rs +0 -324
- data/vendor/rb-sys/src/stable_api/ruby_3_1.rs +0 -317
- data/vendor/rb-sys/src/stable_api/ruby_3_2.rs +0 -315
- data/vendor/rb-sys/src/stable_api/ruby_3_3.rs +0 -326
- data/vendor/rb-sys/src/stable_api/ruby_3_4.rs +0 -327
- data/vendor/rb-sys/src/stable_api.rs +0 -261
- data/vendor/rb-sys/src/symbol.rs +0 -31
- data/vendor/rb-sys/src/tracking_allocator.rs +0 -332
- data/vendor/rb-sys/src/utils.rs +0 -89
- 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(§ion_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(¤t_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
|
-
}
|