1use build_html::Html;
2use build_html::HtmlContainer;
3use build_html::HtmlPage;
4use section::Section;
5use std::path::Path;
6use thiserror::Error;
7
8pub mod attribute;
10pub 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#[derive(Clone, Debug, PartialEq)]
41pub struct Page {
42 sections: Vec<Section>,
43}
44
45impl Page {
46 pub fn from_source(source: &str) -> Result<Self, PageParseError> {
48 Self::new(std::io::Cursor::new(source))
49 }
50
51 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 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 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 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
92pub(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 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, §ion)?);
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#[derive(Error, Debug)]
362pub enum PageParseError {
363 #[error("Page load error")]
365 IOError(
366 #[source]
367 #[from]
368 std::io::Error,
369 ),
370 #[error("Expected attribute, got '{0}'")]
372 ExpectedAttribute(String),
373 #[error("Expected section, got '{0}'")]
375 ExpectedSection(String),
376 #[error("Unknown section: '{0}'")]
378 UnknownSection(String),
379 #[error("Missing attribute argument in attribute '{0}'")]
381 MissingAttributeArgument(String),
382 #[error("Unexpected argument '{0}' in attribute '{1}', this attribute is ment to be used without arguments")]
384 UnexpectedArgument(String, String),
385 #[error("Wrong metadata format: {0}")]
387 WrongMetadataFormat(String),
388 #[error("Title/Subtitle section is empty!")]
390 EmptyTitle,
391 #[error("Expected image source")]
393 ExpectedImageSource,
394 #[error("Expected video ID")]
396 ExpectedVideoID,
397}
398
399#[derive(Error, Debug)]
401pub enum PageBuildError {
402 #[error("Failed to find relative path to project file from file '{0}'")]
404 RelativePathNotFound(String),
405}