1use crate::io::http::get;
3use crate::io::{read_file, uri_to_path, ApiResult};
4use crate::prelude::PathBuf;
5use crate::schema::pid::Identifier;
6use crate::util::MimeType;
7use crate::{Location, Scheme};
8use anydoc::Format;
9use bon::Builder;
10use color_eyre::eyre::eyre;
11use derive_more::Display;
12use serde::{Deserialize, Serialize};
13
14mod index;
15mod matching;
16
17pub(crate) use index::{DocumentEntry, DocumentParser};
18pub use index::{DocumentExcerpt, DocumentIndex, DocumentMatch, DocumentPath, DocumentPathSegment, DocumentPosition, DocumentQuery, DocumentSpan};
19
20#[derive(Clone, Copy, Debug, Deserialize, Display, Eq, PartialEq, Serialize)]
22pub enum DocumentFormat {
23 #[display("csv")]
25 Csv,
26 #[display("doc")]
28 Doc,
29 #[display("docx")]
31 Docx,
32 #[display("epub")]
34 Epub,
35 #[display("excel")]
37 Excel,
38 #[display("odp")]
40 Odp,
41 #[display("ods")]
43 Ods,
44 #[display("odt")]
46 Odt,
47 #[display("pdf")]
49 Pdf,
50 #[display("ppt")]
52 Ppt,
53 #[display("pptx")]
55 Pptx,
56 #[display("rtf")]
58 Rtf,
59}
60#[derive(Builder, Clone, Debug)]
62#[builder(start_fn = init, on(String, into))]
63pub struct SourceDocument {
64 pub content: String,
66 pub format: String,
68 pub source: String,
70}
71impl From<DocumentFormat> for Format {
72 fn from(value: DocumentFormat) -> Self {
73 match value {
74 | DocumentFormat::Csv => Self::Csv,
75 | DocumentFormat::Doc => Self::Doc,
76 | DocumentFormat::Docx => Self::Docx,
77 | DocumentFormat::Epub => Self::Epub,
78 | DocumentFormat::Excel => Self::Excel,
79 | DocumentFormat::Odp => Self::Odp,
80 | DocumentFormat::Ods => Self::Ods,
81 | DocumentFormat::Odt => Self::Odt,
82 | DocumentFormat::Pdf => Self::Pdf,
83 | DocumentFormat::Ppt => Self::Ppt,
84 | DocumentFormat::Pptx => Self::Pptx,
85 | DocumentFormat::Rtf => Self::Rtf,
86 }
87 }
88}
89impl From<Format> for DocumentFormat {
90 fn from(value: Format) -> Self {
91 match value {
92 | Format::Csv => Self::Csv,
93 | Format::Doc => Self::Doc,
94 | Format::Docx => Self::Docx,
95 | Format::Epub => Self::Epub,
96 | Format::Excel => Self::Excel,
97 | Format::Odp => Self::Odp,
98 | Format::Ods => Self::Ods,
99 | Format::Odt => Self::Odt,
100 | Format::Pdf => Self::Pdf,
101 | Format::Ppt => Self::Ppt,
102 | Format::Pptx => Self::Pptx,
103 | Format::Rtf => Self::Rtf,
104 }
105 }
106}
107impl From<DocumentFormat> for MimeType {
108 fn from(value: DocumentFormat) -> Self {
109 match value {
110 | DocumentFormat::Csv => Self::Csv,
111 | DocumentFormat::Doc => Self::Doc,
112 | DocumentFormat::Docx => Self::Docx,
113 | DocumentFormat::Epub => Self::Epub,
114 | DocumentFormat::Excel => Self::Excel,
115 | DocumentFormat::Odp => Self::Odp,
116 | DocumentFormat::Ods => Self::Ods,
117 | DocumentFormat::Odt => Self::Odt,
118 | DocumentFormat::Pdf => Self::Pdf,
119 | DocumentFormat::Ppt => Self::Ppt,
120 | DocumentFormat::Pptx => Self::Powerpoint,
121 | DocumentFormat::Rtf => Self::Rtf,
122 }
123 }
124}
125impl TryFrom<&MimeType> for DocumentFormat {
126 type Error = color_eyre::Report;
127 fn try_from(value: &MimeType) -> Result<Self, Self::Error> {
128 match value {
129 | MimeType::Csv => Ok(Self::Csv),
130 | MimeType::Doc => Ok(Self::Doc),
131 | MimeType::Docx => Ok(Self::Docx),
132 | MimeType::Epub => Ok(Self::Epub),
133 | MimeType::Excel => Ok(Self::Excel),
134 | MimeType::Odp => Ok(Self::Odp),
135 | MimeType::Ods => Ok(Self::Ods),
136 | MimeType::Odt => Ok(Self::Odt),
137 | MimeType::Pdf => Ok(Self::Pdf),
138 | MimeType::Ppt => Ok(Self::Ppt),
139 | MimeType::Powerpoint => Ok(Self::Pptx),
140 | MimeType::Rtf => Ok(Self::Rtf),
141 | _ => Err(eyre!("Unsupported Anydoc MIME type: {value}")),
142 }
143 }
144}
145impl SourceDocument {
146 pub(crate) fn is_markdown(&self) -> bool {
147 MimeType::from(self.source.as_str()) == MimeType::Markdown || MimeType::from(self.format.as_str()) == MimeType::Markdown
148 }
149 fn is_yaml(&self) -> bool {
150 let mime = MimeType::from(self.source.as_str());
151 mime.is_yaml() || mime.is_cff()
152 }
153 pub fn at(location: impl Into<PathBuf>) -> Self {
155 let source = location.into().display().to_string();
156 let format = MimeType::from(source.as_str()).file_type();
157 Self {
158 content: String::new(),
159 format,
160 source,
161 }
162 }
163 pub async fn load(value: impl Into<Location>, offline: bool) -> ApiResult<Self> {
165 let location = value.into();
166 let value: &str = (&location).into();
167 let persistent = !Identifier::find_all(value).is_empty();
168 let scheme = location.scheme();
169 let source = location.uri().unwrap_or_default();
170 match (persistent, scheme, offline) {
171 | (true, _, _) => Ok(Self::init().content(value).format("pid").source(value).build()),
172 | (false, Scheme::HTTP | Scheme::HTTPS, true) => Err(eyre!("Remote input is unavailable in offline mode: {value}")),
173 | (false, Scheme::HTTP | Scheme::HTTPS, false) => get(source.clone())
174 .send()
175 .await
176 .map(|response| response.body)
177 .and_then(|bytes| Self::from_bytes(&bytes, &source)),
178 | (false, Scheme::File | Scheme::Unsupported, _) => Self::from_path(uri_to_path(&source)),
179 }
180 }
181 pub fn from_bytes(bytes: &[u8], source: &str) -> ApiResult<Self> {
183 let mime = MimeType::from(source);
184 match DocumentFormat::try_from(&mime) {
185 | Ok(format) => to_markdown_bytes(bytes, Some(format)),
186 | Err(_) => String::from_utf8(bytes.to_vec()).map_err(color_eyre::Report::from),
187 }
188 .map(|content| Self::init().content(content).format(mime.file_type()).source(source).build())
189 }
190 pub fn from_path(path: impl Into<PathBuf>) -> ApiResult<Self> {
192 let path = path.into();
193 let source = path.display().to_string();
194 let mime = MimeType::from(source.as_str());
195 let content = match DocumentFormat::try_from(&mime) {
196 | Ok(_) => to_markdown(path),
197 | Err(_) => read_file(path),
198 };
199 content.map(|content| Self::init().content(content).format(mime.file_type()).source(source).build())
200 }
201 pub fn extract(&self) -> ApiResult<String> {
203 match self.content.is_empty() {
204 | true => Self::from_path(self.source.as_str()).map(|document| document.content),
205 | false => Ok(self.content.clone()),
206 }
207 }
208}
209pub fn to_markdown(path: impl Into<PathBuf>) -> ApiResult<String> {
211 let path = path.into();
212 anydoc::to_markdown(&path).map_err(|why| eyre!("Failed to convert document {} - {why}", path.display()))
213}
214pub fn to_markdown_bytes(bytes: &[u8], format: Option<DocumentFormat>) -> ApiResult<String> {
216 anydoc::to_markdown_bytes(bytes, format.map(Format::from)).map_err(|why| eyre!("Failed to convert document - {why}"))
217}
218
219#[cfg(test)]
220mod tests;