oreneo/page/
mod.rs

1use build_html::Html;
2use build_html::HtmlContainer;
3use build_html::HtmlPage;
4use section::Section;
5use std::path::Path;
6use thiserror::Error;
7
8/// Different attributes, like --hide or --id
9pub mod attribute;
10/// A section, like --title or --html
11pub mod section;
12
13use self::attribute::Attribute;
14
15fn has_section_prefix(line: &str) -> bool {
16    line.starts_with("--") || line.starts_with("```") || line.starts_with('#')
17}
18
19fn strip_section_prefix(line: &str) -> Option<&str> {
20    line.strip_prefix("--")
21        .or_else(|| {
22            if line.starts_with("```") {
23                Some(line)
24            } else {
25                None
26            }
27        })
28        .map(|line| line.trim())
29}
30
31fn has_attr_prefix(line: &str) -> bool {
32    line.starts_with("--")
33}
34
35fn strip_attr_prefix(line: &str) -> Option<&str> {
36    line.strip_prefix("--").map(|line| line.trim())
37}
38
39/// A page
40#[derive(Clone, Debug, PartialEq)]
41pub struct Page {
42    sections: Vec<Section>,
43}
44
45impl Page {
46    /// Generate a page from source
47    pub fn from_source(source: &str) -> Result<Self, PageParseError> {
48        Self::new(std::io::Cursor::new(source))
49    }
50
51    /// Read a page from a file
52    pub fn load<P: AsRef<std::path::Path>>(path: P) -> Result<Self, PageParseError> {
53        Self::new(std::io::BufReader::new(std::fs::File::open(path)?))
54    }
55}
56
57impl Page {
58    /// Read a page from a reader
59    pub fn new<R: std::io::BufRead>(source: R) -> Result<Self, PageParseError> {
60        Ok(Self {
61            sections: Reader::new(source).next_sections(None)?,
62        })
63    }
64
65    /// Convert a page to [build_html::html_page::HtmlPage]
66    pub fn to_html(&self, project_root: &Path) -> Result<HtmlPage, PageBuildError> {
67        let mut page = HtmlPage::new();
68        page.add_head_link(
69            "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css",
70            "stylesheet",
71        );
72        page.add_script_link(
73            "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js",
74        );
75        page.add_head_link(
76            project_root.join("global.css").to_string_lossy().as_ref(),
77            "stylesheet",
78        );
79        page.add_script_literal("hljs.highlightAll();");
80        for section in &self.sections {
81            page.add_html(section.to_html(project_root)?);
82        }
83        Ok(page)
84    }
85
86    /// Convert a page to a string, containing HTML for it
87    pub fn to_html_string(&self, page_path: &Path) -> Result<String, PageBuildError> {
88        Ok(self.to_html(page_path)?.to_html_string())
89    }
90}
91
92// * ------------------------------------ Reader ------------------------------------ * //
93pub(super) struct Reader<R> {
94    lines: std::io::Lines<R>,
95    peek: Option<String>,
96}
97
98impl<R: std::io::BufRead> Reader<R> {
99    pub(super) fn new(reader: R) -> Self {
100        Self {
101            lines: reader.lines(),
102            peek: None,
103        }
104    }
105
106    pub(super) fn peek_line(&mut self) -> Result<Option<&String>, PageParseError> {
107        if self.peek.is_none() {
108            if let Some(line) = self.lines.next() {
109                self.peek = Some(line?)
110            }
111        }
112        Ok(self.peek.as_ref())
113    }
114
115    pub(super) fn next_line(&mut self) -> Result<Option<String>, PageParseError> {
116        self.peek_line()?;
117        Ok(self.peek.take())
118    }
119
120    pub(super) fn next_line_if(
121        &mut self,
122        pred: impl FnOnce(&str) -> bool,
123    ) -> Result<Option<String>, PageParseError> {
124        if let Some(line) = self.peek_line()? {
125            if pred(line) {
126                return Ok(self.peek.take());
127            }
128        }
129        Ok(None)
130    }
131
132    pub(super) fn next_line_if_map(
133        &mut self,
134        map: impl FnOnce(&str) -> Option<&str>,
135    ) -> Result<Option<String>, PageParseError> {
136        if let Some(line) = self.peek_line()? {
137            if let Some(line) = map(line) {
138                let line = line.to_owned();
139                self.peek = None;
140                return Ok(Some(line));
141            }
142        }
143        Ok(None)
144    }
145
146    pub(super) fn skip_blank(&mut self) -> Result<bool, PageParseError> {
147        Ok(self.next_line_if(|line| line.trim().is_empty())?.is_some())
148    }
149
150    pub(super) fn skip_blanks(&mut self) -> Result<(), PageParseError> {
151        while self.skip_blank()? {}
152        Ok(())
153    }
154
155    fn next_text(
156        &mut self,
157        mut filter_map: impl FnMut(&str) -> Option<&str>,
158        raw: bool,
159    ) -> Result<String, PageParseError> {
160        self.skip_blanks()?;
161        let mut text = String::new();
162        loop {
163            let line = if let Some(line) = self.next_line_if_map(&mut filter_map)? {
164                line
165            } else {
166                break;
167            };
168
169            #[allow(clippy::collapsible_else_if)]
170            if raw {
171                text.push_str(&line);
172                text.push('\n');
173            } else {
174                if line.trim().is_empty() {
175                    text.push('\n');
176                } else {
177                    if !text.ends_with('\n') {
178                        text.push(' ');
179                    }
180                    text.push_str(&line);
181                }
182            }
183        }
184        if raw {
185            Ok(text.trim_end().to_owned())
186        } else {
187            Ok(text.trim().to_owned())
188        }
189    }
190
191    fn next_text_until(
192        &mut self,
193        mut until: impl FnMut(&str) -> bool,
194        raw: bool,
195    ) -> Result<String, PageParseError> {
196        self.next_text(|line| if until(line) { None } else { Some(line) }, raw)
197    }
198
199    fn next_text_until_tag(&mut self, tag: &str, raw: bool) -> Result<String, PageParseError> {
200        let text = self.next_text_until(
201            |line| {
202                if tag == "```" && line == tag {
203                    return true;
204                }
205                if let Some(section) = strip_section_prefix(line) {
206                    if let Some(section_tag) = section.strip_prefix('/') {
207                        if section_tag == tag {
208                            return true;
209                        }
210                    }
211                }
212                false
213            },
214            raw,
215        )?;
216        self.next_line()?;
217        Ok(text)
218    }
219
220    fn next_text_prefixed(&mut self, prefix: &str, raw: bool) -> Result<String, PageParseError> {
221        self.next_text(
222            |line| {
223                if line.trim().is_empty() && raw {
224                    None
225                } else {
226                    line.strip_prefix(prefix)
227                }
228            },
229            raw,
230        )
231    }
232
233    fn next_text_until_section(&mut self, raw: bool) -> Result<String, PageParseError> {
234        self.next_text_until(has_section_prefix, raw)
235    }
236
237    // * ----------------------------------- Specials ----------------------------------- * //
238    pub(super) fn next_attr(&mut self) -> Result<Option<Attribute>, PageParseError> {
239        if let Some(line) = self.next_line_if(has_attr_prefix)? {
240            if let Some(attr) = strip_attr_prefix(&line) {
241                if let Some(attr) = Attribute::parse(attr)? {
242                    return Ok(Some(attr));
243                } else {
244                    self.peek = Some(line);
245                }
246            }
247        }
248        Ok(None)
249    }
250
251    pub(super) fn next_attrs(&mut self) -> Result<Vec<Attribute>, PageParseError> {
252        let mut attrs = Vec::new();
253        while let Some(attr) = self.next_attr()? {
254            attrs.push(attr);
255        }
256        Ok(attrs)
257    }
258
259    pub(super) fn next_list(
260        &mut self,
261        filter: impl Fn(&str) -> bool,
262    ) -> Result<Vec<String>, PageParseError> {
263        self.skip_blanks()?;
264        let mut list = Vec::new();
265        while let Some(line) = self.peek_line()? {
266            if !filter(line) {
267                break;
268            }
269            let mut fist_line = true;
270            let entry = self.next_text_until(
271                |line| {
272                    if fist_line {
273                        fist_line = false;
274                        return false;
275                    }
276                    has_section_prefix(line) || filter(line)
277                },
278                false,
279            )?;
280            list.push(entry);
281            self.skip_blanks()?;
282        }
283        Ok(list)
284    }
285
286    pub(super) fn next_list_prefixed(
287        &mut self,
288        prefix: &str,
289    ) -> Result<Vec<String>, PageParseError> {
290        Ok(self
291            .next_list(|line| line.starts_with(prefix))?
292            .iter()
293            .map(|entry| entry.strip_prefix(prefix).unwrap().to_owned())
294            .collect())
295    }
296
297    pub(super) fn next_sections(
298        &mut self,
299        end_tag: Option<&str>,
300    ) -> Result<Vec<Section>, PageParseError> {
301        let mut sections = Vec::new();
302        loop {
303            self.skip_blanks()?;
304            let line = if let Some(line) = self.peek_line()? {
305                if let Some(end_tag) = end_tag {
306                    if let Some(section) = strip_section_prefix(line) {
307                        if let Some(tag) = section.strip_prefix('/') {
308                            if tag == end_tag {
309                                self.next_line()?;
310                                break;
311                            }
312                        }
313                    }
314                }
315                line
316            } else {
317                break;
318            };
319
320            if line.starts_with('#') {
321                let prefix = line.chars().take_while(|&c| c == '#').collect::<String>();
322                sections.push(Section::Text {
323                    tag: format!("h{}", prefix.len()),
324                    class: None,
325                    attributes: Vec::new(),
326                    content: self.next_text(
327                        |line| {
328                            if line.trim().is_empty() {
329                                Some(line)
330                            } else {
331                                let line = line.strip_prefix(&prefix)?;
332                                if line.starts_with('#') {
333                                    None
334                                } else {
335                                    Some(line)
336                                }
337                            }
338                        },
339                        false,
340                    )?,
341                });
342            } else if let Some(section) = strip_section_prefix(line) {
343                let section = section.to_owned();
344                self.next_line()?;
345                sections.push(Section::parse(self, &section)?);
346            } else {
347                sections.push(Section::Text {
348                    tag: String::from("p"),
349                    class: None,
350                    attributes: Vec::new(),
351                    content: self.next_text_until(has_section_prefix, false)?,
352                });
353            }
354        }
355        Ok(sections)
356    }
357}
358
359// * ------------------------------------- Error ------------------------------------ * //
360/// An error types
361#[derive(Error, Debug)]
362pub enum PageParseError {
363    /// IO error from the reader
364    #[error("Page load error")]
365    IOError(
366        #[source]
367        #[from]
368        std::io::Error,
369    ),
370    /// Expected attribute
371    #[error("Expected attribute, got '{0}'")]
372    ExpectedAttribute(String),
373    /// Expected section
374    #[error("Expected section, got '{0}'")]
375    ExpectedSection(String),
376    /// Unknown section
377    #[error("Unknown section: '{0}'")]
378    UnknownSection(String),
379    /// Missing attribute argument
380    #[error("Missing attribute argument in attribute '{0}'")]
381    MissingAttributeArgument(String),
382    /// Unexpected argument
383    #[error("Unexpected argument '{0}' in attribute '{1}', this attribute is ment to be used without arguments")]
384    UnexpectedArgument(String, String),
385    /// Wrong metadata format
386    #[error("Wrong metadata format: {0}")]
387    WrongMetadataFormat(String),
388    /// Title/Subtitle section is empty
389    #[error("Title/Subtitle section is empty!")]
390    EmptyTitle,
391    /// Expected image source
392    #[error("Expected image source")]
393    ExpectedImageSource,
394    /// Expected video ID
395    #[error("Expected video ID")]
396    ExpectedVideoID,
397}
398
399/// An error occured while building a page
400#[derive(Error, Debug)]
401pub enum PageBuildError {
402    /// Failed to find relative path to project file
403    #[error("Failed to find relative path to project file from file '{0}'")]
404    RelativePathNotFound(String),
405}