1use std::{io::Read, path::Path};
2
3use flams_ontology::{
4 content::modules::OpenModule, narration::documents::UncheckedDocument, DocumentRange, Unchecked,
5};
6pub use flams_utils::global;
7use flams_utils::CSS;
8
9use crate::{
10 backend::AnyBackend,
11 building::{BuildArtifact, BuildResult, BuildTask},
12};
13
14global! {SER SourceFormat {name,
15 description: &'static str,
16 file_exts: &'static [&'static str],
17 targets: &'static [BuildTargetId],
18 dependencies:fn(&AnyBackend,task:&BuildTask)
19}}
20
21#[macro_export]
22macro_rules! source_format {
23 ($name:ident[$($ft:literal),+] [$($tgt:expr)=>*] @ $desc:literal = $deps:expr) => {
24 $crate::formats::global!{NEW {$crate::formats}SourceFormat; $name [
25 $desc,&[$($ft),+],&[$($tgt),*],$deps
26 ]
27 }
28 }
29}
30
31global! {SER BuildTarget {name,
32 description: &'static str,
33 dependencies: &'static [BuildArtifactTypeId],
34 yields: &'static [BuildArtifactTypeId],
35 run: fn(&AnyBackend,task:&BuildTask) -> BuildResult
36}}
37
38#[macro_export]
39macro_rules! build_target {
40 ($name:ident [$($dep:expr),*] => [$($yield:expr),*] @ $desc:literal = $f:expr) => {
41 $crate::formats::global!{NEW {$crate::formats}BuildTarget; $name [
42 $desc,&[$($dep),*],&[$($yield),*],$f
43 ]
44 }
45 }
46}
47
48build_target!(check [UNCHECKED_OMDOC] => [OMDOC]
49 @ "Resolve OMDoc dependencies and type check"
50 = |_,_| { BuildResult::empty() }
51);
52
53global! {SER BuildArtifactType {name,
54 description: &'static str
55}}
56
57#[macro_export]
58macro_rules! build_result {
59 ($name:ident @ $desc:literal) => {
60 $crate::formats::global! {NEW {$crate::formats}BuildArtifactType; $name [$desc]
61 }
62 };
63}
64
65build_result!(unchecked_omdoc @ "OMDoc ready to be checked");
66build_result!(omdoc @ "OMDoc document and modules; fully checked");
67build_result!(pdf @ "PDF document (semantically opaque)");
68
69pub struct OMDocResult {
70 pub document: UncheckedDocument,
71 pub html: HTMLData,
72 pub modules: Vec<OpenModule<Unchecked>>,
73}
74
75#[derive(Debug)]
76pub struct HTMLData {
77 pub html: String,
78 pub css: Vec<CSS>,
79 pub body: DocumentRange,
80 pub inner_offset: usize,
81 pub refs: Vec<u8>,
82}
83
84impl OMDocResult {
85 pub(crate) fn load_html_body(path: &Path, full: bool) -> Option<(Vec<CSS>, String)> {
86 use std::io::{Seek, SeekFrom};
87 let file = std::fs::File::open(path).ok()?;
88 let mut file = std::io::BufReader::new(file);
89 let mut buf = [0; 20];
90 file.read_exact(&mut buf).ok()?;
91 let css_offset = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
92 let css_len = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
93 let body_start = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
94 let body_start = 20 + css_offset + css_len + body_start;
95 let body_len = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
96 let inner_offset = u32::from_be_bytes([buf[16], buf[17], buf[18], buf[19]]);
97 let mut css = vec![0; css_len as usize];
98 file.seek(SeekFrom::Start(u64::from(css_offset + 20)))
99 .ok()?;
100 file.read_exact(&mut css).ok()?;
101 let css = bincode::serde::decode_from_slice(&css, bincode::config::standard())
102 .ok()?
103 .0;
104 if full {
105 file.seek(SeekFrom::Start(u64::from(body_start))).ok()?;
106 let mut html = vec![0; body_len as usize];
107 file.read_exact(&mut html).ok()?;
108 String::from_utf8(html).ok().map(|html| (css, html))
109 } else {
110 let inner_offset_real = body_start + inner_offset;
111 file.seek(SeekFrom::Start(u64::from(inner_offset_real)))
112 .ok()?;
113 let mut html = vec![0; ((body_len - inner_offset) as usize) - "</body>".len()];
114 file.read_exact(&mut html).ok()?;
115 String::from_utf8(html).ok().map(|html| (css, html))
116 }
117 }
118
119 #[cfg(feature = "tokio")]
120 pub(crate) async fn load_html_body_async(
121 path: std::path::PathBuf,
122 full: bool,
123 ) -> Option<(Vec<CSS>, String)> {
124 use std::io::SeekFrom;
125 use tokio::io::{AsyncReadExt, AsyncSeekExt};
126 let file = tokio::fs::File::open(path).await.ok()?;
128 let mut file = tokio::io::BufReader::new(file);
129 let mut buf = [0; 20];
130 file.read_exact(&mut buf).await.ok()?;
131 let css_offset = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
132 let css_len = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
133 let body_start = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
134 let body_start = 20 + css_offset + css_len + body_start;
135 let body_len = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
136 let inner_offset = u32::from_be_bytes([buf[16], buf[17], buf[18], buf[19]]);
137 let mut css = vec![0; css_len as usize];
139 file.seek(SeekFrom::Start(u64::from(css_offset + 20)))
140 .await
141 .ok()?;
142 file.read_exact(&mut css).await.ok()?;
143 let css = bincode::serde::decode_from_slice(&css, bincode::config::standard())
144 .ok()?
145 .0;
146 if full {
147 file.seek(SeekFrom::Start(u64::from(body_start)))
148 .await
149 .ok()?;
150 let mut html = vec![0; body_len as usize];
151 file.read_exact(&mut html).await.ok()?;
152 String::from_utf8(html).ok().map(|html| (css, html))
153 } else {
154 let inner_offset_real = body_start + inner_offset;
155 file.seek(SeekFrom::Start(u64::from(inner_offset_real)))
157 .await
158 .ok()?;
159 let mut html = vec![0; ((body_len - inner_offset) as usize) - "</body>".len()];
160 file.read_exact(&mut html).await.ok()?;
162 String::from_utf8(html).ok().map(|html| (css, html))
164 }
165 }
166
167 #[cfg(feature = "tokio")]
168 pub(crate) async fn load_html_full_async(path: std::path::PathBuf) -> Option<String> {
169 use std::io::SeekFrom;
170 use tokio::io::{AsyncReadExt, AsyncSeekExt};
171 let file = tokio::fs::File::open(path).await.ok()?;
173 let mut file = tokio::io::BufReader::new(file);
174 let mut buf = [0; 20];
175 file.read_exact(&mut buf).await.ok()?;
176 let css_offset = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
177 let css_len = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
178 let html_start = 20 + css_offset + css_len;
179
180 file.seek(SeekFrom::Start(u64::from(html_start)))
181 .await
182 .ok()?;
183 let mut ret = String::new();
184 file.read_to_string(&mut ret).await.ok()?;
185 Some(ret)
186 }
187
188 pub(crate) fn load_html_full(path: std::path::PathBuf) -> Option<String> {
189 use std::io::{Seek, SeekFrom};
190 let file = std::fs::File::open(path).ok()?;
191 let mut file = std::io::BufReader::new(file);
192 let mut buf = [0; 20];
193 file.read_exact(&mut buf).ok()?;
194 let css_offset = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
195 let css_len = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
196 let html_start = 20 + css_offset + css_len;
197
198 file.seek(SeekFrom::Start(u64::from(html_start))).ok()?;
199 let mut ret = String::new();
200 file.read_to_string(&mut ret).ok()?;
201 Some(ret)
202 }
203
204 pub(crate) fn load_html_fragment(
205 path: &Path,
206 range: DocumentRange,
207 ) -> Option<(Vec<CSS>, String)> {
208 use std::io::{Seek, SeekFrom};
209 let file = std::fs::File::open(path).ok()?;
210 let mut file = std::io::BufReader::new(file);
211 let mut buf = [0; 20];
212 file.read_exact(&mut buf).ok()?;
213 let css_offset = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
214 let css_len = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
215 let html_start = 20 + css_offset + css_len;
217 let mut css = vec![0; css_len as usize];
220 file.seek(SeekFrom::Start(u64::from(css_offset + 20)))
221 .ok()?;
222 file.read_exact(&mut css).ok()?;
223 let css = bincode::serde::decode_from_slice(&css, bincode::config::standard())
224 .ok()?
225 .0;
226 file.seek(SeekFrom::Start(u64::from(html_start) + range.start as u64))
227 .ok()?;
228 let mut html = vec![0; range.end - range.start];
229 file.read_exact(&mut html).ok()?;
230 String::from_utf8(html).ok().map(|html| (css, html))
231 }
232
233 #[allow(clippy::cast_possible_wrap)]
234 pub(crate) fn load_reference<T: flams_ontology::Resourcable>(
235 path: &Path,
236 range: DocumentRange,
237 ) -> eyre::Result<T> {
238 use std::io::{Seek, SeekFrom};
239 let file = std::fs::File::open(path)?;
240 let mut file = std::io::BufReader::new(file);
241 let mut buf = [0; 20];
242 file.read_exact(&mut buf)?;
243 file.seek(SeekFrom::Current(range.start as i64))?;
244 let len = range.end - range.start;
245 let mut bytes = vec![0; len];
246 file.read_exact(&mut bytes)?;
247 let r = bincode::serde::decode_from_slice(&bytes, bincode::config::standard())?;
248 Ok(r.0)
249 }
250
251 #[cfg(feature = "tokio")]
252 pub(crate) async fn load_html_fragment_async(
253 path: std::path::PathBuf,
254 range: DocumentRange,
255 ) -> Option<(Vec<CSS>, String)> {
256 use std::io::SeekFrom;
257 use tokio::io::{AsyncReadExt, AsyncSeekExt};
258 let file = tokio::fs::File::open(path).await.ok()?;
259 let mut file = tokio::io::BufReader::new(file);
260 let mut buf = [0; 20];
261 file.read_exact(&mut buf).await.ok()?;
262 let css_offset = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
263 let css_len = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
264 let html_start = 20 + css_offset + css_len;
265 let mut css = vec![0; css_len as usize];
270 file.seek(SeekFrom::Start(u64::from(css_offset + 20)))
271 .await
272 .ok()?;
273 file.read_exact(&mut css).await.ok()?;
274 let css = bincode::serde::decode_from_slice(&css, bincode::config::standard())
275 .ok()?
276 .0;
277 file.seek(SeekFrom::Start(u64::from(html_start) + range.start as u64))
278 .await
279 .ok()?;
280 let mut html = vec![0; range.end - range.start];
281 file.read_exact(&mut html).await.ok()?;
282 String::from_utf8(html).ok().map(|html| (css, html))
283 }
284
285 #[allow(clippy::cast_possible_truncation)]
286 #[allow(clippy::cognitive_complexity)]
287 pub(crate) fn write(&self, path: &Path) {
288 use std::io::Write;
289 macro_rules! err {
290 ($e:expr) => {
291 match $e {
292 Ok(r) => r,
293 Err(e) => {
294 tracing::error!("Failed to save {}: {}", path.display(), e);
295 return;
296 }
297 }
298 };
299 }
300 macro_rules! er {
301 ($e:expr) => {
302 if let Err(e) = $e {
303 tracing::error!("Failed to save {}: {}", path.display(), e);
304 return;
305 }
306 };
307 }
308 let file = err!(std::fs::File::create(path));
309 let mut buf = std::io::BufWriter::new(file);
310 let Self {
311 html:
312 HTMLData {
313 css,
314 body,
315 inner_offset,
316 refs,
317 html,
318 },
319 ..
320 } = self;
321 let css_offset = (refs.len() as u32).to_be_bytes();
322 let css = err!(bincode::serde::encode_to_vec(
323 css,
324 bincode::config::standard()
325 ));
326 let css_len = (css.len() as u32).to_be_bytes();
327 let body_start = (body.start as u32).to_be_bytes();
328 let body_len = ((body.end - body.start) as u32).to_be_bytes();
329 let inner_offset = (*inner_offset as u32).to_be_bytes();
330 er!(buf.write_all(&css_offset));
331 er!(buf.write_all(&css_len));
332 er!(buf.write_all(&body_start));
333 er!(buf.write_all(&body_len));
334 er!(buf.write_all(&inner_offset));
335 er!(buf.write_all(refs));
336 er!(buf.write_all(&css));
337 er!(buf.write_all(html.as_bytes()));
338 er!(buf.flush());
339 }
340}
341
342impl BuildArtifact for OMDocResult {
343 #[inline]
344 fn get_type(&self) -> BuildArtifactTypeId {
345 UNCHECKED_OMDOC
346 }
347 #[inline]
348 fn get_type_id() -> BuildArtifactTypeId
349 where
350 Self: Sized,
351 {
352 UNCHECKED_OMDOC
353 }
354 fn write(&self, _path: &std::path::Path) -> Result<(), std::io::Error> {
355 unreachable!()
356 }
357 fn load(_p: &std::path::Path) -> Result<Self, std::io::Error>
358 where
359 Self: Sized,
360 {
361 unreachable!()
362 }
363 #[inline]
364 fn as_any(&self) -> &dyn std::any::Any {
365 self
366 }
367}
368
369global! {FLAMSExtension {name,
370 on_start: fn()
371}}
372
373#[macro_export]
374macro_rules! flams_extension {
375 ($name:ident = $f:expr) => {
376 $crate::formats::global! {NEW {$crate::formats} FLAMSExtension; $name [$f]}
377 };
378}
379
380impl FLAMSExtension {
381 pub fn initialize() {
382 for e in Self::all() {
383 let f = move || {
384 tracing::info_span!("Initializing", extension = e.name())
385 .in_scope(|| (e.on_start())());
386 };
387 #[cfg(feature = "tokio")]
388 flams_utils::background(f);
389 #[cfg(not(feature = "tokio"))]
390 f();
391 }
392 }
393}
394
395#[derive(Copy, Clone, Debug, PartialEq, Eq)]
396pub enum FormatOrTargets<'a> {
397 Format(SourceFormatId),
398 Targets(&'a [BuildTargetId]),
399}