hyper_static_server/
support_markdown.rs

1
2use ::pulldown_cmark as cmark;
3use ::any_ascii;
4use ::serde;
5use ::serde_json;
6
7
8use ::std::{
9		
10		cell,
11		env,
12		ffi,
13		fmt,
14		fs,
15		io,
16		iter,
17		mem,
18		path,
19		rc,
20		
21		collections::BTreeSet,
22		iter::Iterator,
23		path::{Path, PathBuf},
24		
25		str::FromStr as _,
26		fmt::{Write as _},
27		io::{Write as _},
28		
29	};
30
31
32use crate::builder_errors::*;
33
34
35
36
37#[ derive (Debug, Clone) ]
38#[ derive (serde::Serialize, serde::Deserialize) ]
39pub struct MarkdownOptions {
40	
41	pub title_detect : bool,
42	pub headings_detect : bool,
43	pub headings_anchors : bool,
44	
45	pub enable_tables : bool,
46	pub enable_footnotes : bool,
47	pub enable_strikethrough : bool,
48	pub enable_tasklists : bool,
49	pub enable_headings_attributes : bool,
50	
51}
52
53
54impl Default for MarkdownOptions {
55	
56	fn default () -> Self {
57		Self {
58				
59				title_detect : true,
60				headings_detect : true,
61				headings_anchors : true,
62				
63				enable_tables : true,
64				enable_footnotes : true,
65				enable_strikethrough : true,
66				enable_tasklists : true,
67				enable_headings_attributes : false,
68				
69			}
70	}
71}
72
73
74
75
76#[ derive (Debug, Clone, Default) ]
77#[ derive (serde::Serialize, serde::Deserialize) ]
78pub struct MarkdownOutput {
79	pub body : String,
80	pub metadata : MarkdownMetadata,
81	pub frontmatter : Option<MarkdownFrontmatter>,
82}
83
84
85#[ derive (Debug, Clone, Default) ]
86#[ derive (serde::Serialize, serde::Deserialize) ]
87pub struct MarkdownMetadata {
88	pub title : Option<String>,
89	pub headings : Option<Vec<MarkdownHeading>>,
90}
91
92
93#[ derive (Debug, Clone, Default) ]
94#[ derive (serde::Serialize, serde::Deserialize) ]
95pub struct MarkdownHeading {
96	pub level : u8,
97	pub text : Option<String>,
98	pub anchor : Option<String>,
99}
100
101
102#[ derive (Debug, Clone, Default) ]
103#[ derive (serde::Serialize, serde::Deserialize) ]
104pub struct MarkdownFrontmatter {
105	pub encoding : String,
106	pub data : String,
107}
108
109
110
111
112pub fn compile_markdown_from_path (_source : &Path, _options : Option<&MarkdownOptions>) -> BuilderResult<MarkdownOutput> {
113	
114	let _source = fs::read_to_string (_source) ?;
115	
116	compile_markdown_from_data (_source.as_str (), _options)
117}
118
119
120
121
122pub fn compile_markdown_from_data (_source : &str, _options : Option<&MarkdownOptions>) -> BuilderResult<MarkdownOutput> {
123	
124	let mut _default_options = None;
125	let _options = if let Some (_options) = _options {
126		_options
127	} else {
128		_default_options = Some (MarkdownOptions::default ());
129		_default_options.as_ref () .infallible (0x1fe3a806)
130	};
131	
132	let mut _input : Vec<&str> = _source.lines () .skip_while (|_line| _line.is_empty ()) .collect ();
133	while let Some (_line) = _input.last () {
134		if _line.is_empty () {
135			_input.pop ();
136		} else {
137			break;
138		}
139	}
140	
141	if _input.is_empty () {
142		return Err (error_with_code (0x1fc18809));
143	}
144	
145	let (_input, _frontmatter) = {
146		let _detected = if let Some (_line) = _input.first () {
147			let _line_trimmed = _line.trim ();
148			match _line_trimmed {
149				"+++" =>
150					Some (("toml", "+++")),
151				"---" =>
152					Some (("yaml", "---")),
153				"{{{" =>
154					Some (("json", "}}}")),
155				_ =>
156					None,
157			}
158		} else {
159			None
160		};
161		if let Some ((_encoding, _marker)) = _detected {
162			let mut _input = _input.into_iter ();
163			let mut _frontmatter = Vec::new ();
164			let mut _frontmatter_is_empty = true;
165			_input.next ();
166			while let Some (_line) = _input.next () {
167				let _line_trimmed = _line.trim ();
168				if _line_trimmed == _marker {
169					break;
170				} else {
171					_frontmatter.push (_line);
172					if ! _line_trimmed.is_empty () {
173						_frontmatter_is_empty = false;
174					}
175				}
176			}
177			let _input : Vec<&str> = _input.collect ();
178			let _frontmatter = if ! _frontmatter_is_empty {
179				let _encoding = String::from (_encoding);
180				let _frontmatter = _frontmatter.join ("\n");
181				Some ((_encoding, _frontmatter))
182			} else {
183				None
184			};
185			(_input, _frontmatter)
186		} else {
187			(_input, None)
188		}
189	};
190	
191	let _input = _input.join ("\n");
192	
193	let mut _parser_options = cmark::Options::empty ();
194	if _options.enable_tables {
195		_parser_options.insert (cmark::Options::ENABLE_TABLES);
196	}
197	if _options.enable_footnotes {
198		_parser_options.insert (cmark::Options::ENABLE_FOOTNOTES);
199	}
200	if _options.enable_strikethrough {
201		_parser_options.insert (cmark::Options::ENABLE_STRIKETHROUGH);
202	}
203	if _options.enable_tasklists {
204		_parser_options.insert (cmark::Options::ENABLE_TASKLISTS);
205	}
206	if _options.enable_headings_attributes {
207		_parser_options.insert (cmark::Options::ENABLE_HEADING_ATTRIBUTES);
208	}
209	
210	let _parser = cmark::Parser::new_ext (&_input, _parser_options);
211	
212	let mut _events : Vec<_> = _parser.into_iter () .collect ();
213	
214	let mut _title = None;
215	if _options.title_detect {
216		let mut _capture_next = false;
217		for _event in _events.iter () {
218			match _event {
219				cmark::Event::Start (cmark::Tag::Heading (cmark::HeadingLevel::H1, _, _)) =>
220					_capture_next = true,
221				cmark::Event::End (cmark::Tag::Heading (_, _, _)) =>
222					if _capture_next {
223						break;
224					}
225				cmark::Event::Text (_text) =>
226					if _capture_next {
227						if ! _text.is_empty () {
228							_title = Some (_text.as_ref () .to_owned ());
229						}
230					}
231				_ =>
232					if _capture_next {
233						return Err (error_with_code (0xc36cbd17));
234					}
235			}
236		}
237	}
238	
239	let mut _headings_anchors = Vec::new ();
240	if _options.headings_anchors {
241		let mut _generate_next = false;
242		for (_index, _event) in _events.iter () .enumerate () {
243			match _event {
244				cmark::Event::Start (cmark::Tag::Heading (_, _anchor, _)) =>
245					if _anchor.is_none () {
246						_generate_next = true;
247					}
248				cmark::Event::End (cmark::Tag::Heading (_, _, _)) =>
249					if _generate_next {
250						_generate_next = false;
251					}
252				cmark::Event::Text (_text) =>
253					if _generate_next {
254						if ! _text.is_empty () {
255							let _anchor_id = build_markdown_anchor_from_text (_text.as_ref ());
256							if ! _anchor_id.is_empty () {
257								_headings_anchors.push ((_index - 1, _anchor_id));
258							}
259						}
260					}
261				_ =>
262					if _generate_next {
263						return Err (error_with_code (0xd9b3a175));
264					}
265			}
266		}
267		for (_index, _anchor_id) in _headings_anchors.iter () {
268			let _event = _events.get_mut (*_index) .infallible (0xf65facdb);
269			match _event {
270				cmark::Event::Start (cmark::Tag::Heading (_, ref mut _anchor, _)) =>
271					*_anchor = Some (_anchor_id),
272				_ =>
273					unreachable! ("[eddfdaf1]"),
274			}
275		}
276	}
277	
278	let mut _headings = None;
279	if _options.headings_detect {
280		let mut _headings_0 = Vec::new ();
281		let mut _capture_next = false;
282		let mut _capture_level = 0;
283		let mut _capture_anchor = String::new ();
284		for _event in _events.iter () {
285			match _event {
286				cmark::Event::Start (cmark::Tag::Heading (_level, _anchor, _)) => {
287					_capture_next = true;
288					if let Some (_anchor) = _anchor {
289						_capture_anchor = (*_anchor).to_owned ();
290					}
291					_capture_level = match _level {
292						cmark::HeadingLevel::H1 => 1,
293						cmark::HeadingLevel::H2 => 2,
294						cmark::HeadingLevel::H3 => 3,
295						cmark::HeadingLevel::H4 => 4,
296						cmark::HeadingLevel::H5 => 5,
297						cmark::HeadingLevel::H6 => 6,
298					}
299				}
300				cmark::Event::Text (_text) =>
301					if _capture_next {
302						let _heading = MarkdownHeading {
303								level : _capture_level,
304								text : Some (_text.as_ref () .to_owned ()),
305								anchor : if ! _capture_anchor.is_empty () { Some (_capture_anchor) } else { None },
306							};
307						_headings_0.push (_heading);
308						_capture_next = false;
309						_capture_anchor = String::new ();
310						_capture_level = 0;
311					}
312				_ =>
313					(),
314			}
315		}
316		if ! _headings_0.is_empty () {
317			_headings = Some (_headings_0);
318		}
319	}
320	
321	let mut _body = String::with_capacity (_input.len () * 2);
322	
323	cmark::html::push_html (&mut _body, _events.into_iter ());
324	
325	let _frontmatter = if let Some ((_encoding, _data)) = _frontmatter {
326		Some (MarkdownFrontmatter {
327				encoding : _encoding,
328				data : _data,
329			})
330	} else {
331		None
332	};
333	
334	let _metadata = MarkdownMetadata {
335			title : _title,
336			headings : _headings,
337		};
338	
339	let _output = MarkdownOutput {
340			body : _body,
341			metadata : _metadata,
342			frontmatter : _frontmatter,
343		};
344	
345	Ok (_output)
346}
347
348
349
350
351pub fn compile_markdown_from_path_to_paths (
352			_source_path : &Path,
353			_options : Option<&MarkdownOptions>,
354			_body_path : Option<&Path>,
355			_title_path : Option<&Path>,
356			_metadata_path : Option<&Path>,
357			_frontmatter_path : Option<&Path>,
358		) -> BuilderResult
359{
360	let _markdown = compile_markdown_from_path (_source_path, _options) ?;
361	
362	write_markdown_to_paths (_markdown, _body_path, _title_path, _metadata_path, _frontmatter_path)
363}
364
365
366
367
368pub fn write_markdown_to_paths (
369			_markdown : MarkdownOutput,
370			_body_path : Option<&Path>,
371			_title_path : Option<&Path>,
372			_metadata_path : Option<&Path>,
373			_frontmatter_path : Option<&Path>,
374		) -> BuilderResult
375{
376	let _body = _markdown.body;
377	let _metadata = _markdown.metadata;
378	let _frontmatter = _markdown.frontmatter;
379	
380	if let Some (_path) = _body_path {
381		let _data = _body;
382		let mut _file = fs::File::create (_path) ?;
383		_file.write_all (_data.as_bytes ()) ?;
384	}
385	
386	if let Some (_path) = _title_path {
387		let _data = if let Some (ref _title) = _metadata.title {
388			_title.as_str ()
389		} else {
390			""
391		};
392		let mut _file = fs::File::create (_path) ?;
393		_file.write_all (_data.as_bytes ()) ?;
394	}
395	
396	if let Some (_path) = _metadata_path {
397	}
398	
399	if let Some (_path) = _metadata_path {
400		let _data = serde_json::to_string_pretty (&_metadata) .or_wrap (0xa0176504) ?;
401		let mut _file = fs::File::create (_path) ?;
402		_file.write_all (_data.as_bytes ()) ?;
403	}
404	
405	if let Some (_path) = _frontmatter_path {
406		let _data = if let Some (_frontmatter) = _frontmatter {
407			match _frontmatter.encoding.as_str () {
408				"toml" => "## toml\n".to_owned () + &_frontmatter.data,
409				"yaml" => "## yaml\n".to_owned () + &_frontmatter.data,
410				"json" => _frontmatter.data,
411				_ =>
412					return Err (error_with_code (0xfc776131)),
413			}
414		} else {
415			String::new ()
416		};
417		let mut _file = fs::File::create (_path) ?;
418		_file.write_all (_data.as_bytes ()) ?;
419	}
420	
421	Ok (())
422}
423
424
425
426
427pub fn compile_markdown_html_from_path (_source : &Path, _header : Option<&Path>, _footer : Option<&Path>, _options : Option<&MarkdownOptions>) -> BuilderResult<String> {
428	
429	let _source = fs::read_to_string (_source) ?;
430	let _header = if let Some (_header) = _header { Some (fs::read_to_string (_header) ?) } else { None };
431	let _footer = if let Some (_footer) = _footer { Some (fs::read_to_string (_footer) ?) } else { None };
432	
433	let _source = _source.as_str ();
434	let _header = _header.as_ref () .map (String::as_str);
435	let _footer = _footer.as_ref () .map (String::as_str);
436	
437	compile_markdown_html_from_data (_source, _header, _footer, _options)
438}
439
440
441
442
443pub fn compile_markdown_html_from_data (_source : &str, _header : Option<&str>, _footer : Option<&str>, _options : Option<&MarkdownOptions>) -> BuilderResult<String> {
444	
445	let _output = compile_markdown_from_data (_source, _options) ?;
446	
447	let _body = _output.body;
448	let _title = _output.metadata.title;
449	let _frontmatter = _output.frontmatter;
450	
451	let _html = if _header.is_some () || _footer.is_some () {
452		
453		let _header = _header.unwrap_or ("");
454		let _footer = _footer.unwrap_or ("");
455		
456		let _title = if let Some (_title) = _title {
457			let mut _buffer = String::with_capacity (_title.len () * 3 / 2);
458			cmark::escape::escape_html (&mut _buffer, &_title) .infallible (0xef399d64);
459			_buffer
460		} else {
461			String::new ()
462		};
463		
464		let mut _buffer = String::with_capacity (_header.len () + _body.len () + _footer.len ());
465		_buffer.push_str (&_header.replace ("@@{{HSS::Markdown::Title}}", &_title));
466		_buffer.push_str (&_body);
467		_buffer.push_str (&_footer.replace ("@@{{HSS::Markdown::Title}}", &_title));
468		
469		_buffer
470		
471	} else {
472		
473		let mut _buffer = String::with_capacity (_body.len () + 1024);
474		
475		_buffer.push_str ("<!DOCTYPE html>\n");
476		_buffer.push_str ("<html>\n");
477		_buffer.push_str ("<head>\n");
478		
479		if let Some (_title) = _title {
480			_buffer.push_str ("<title>");
481			cmark::escape::escape_html (&mut _buffer, &_title) .infallible (0xdc5ea905);
482			_buffer.push_str ("</title>\n");
483		}
484		
485		_buffer.push_str (r#"<meta name="viewport" content="width=device-width, height=device-height" />"#);
486		_buffer.push_str ("\n");
487		_buffer.push_str ("</head>\n");
488		_buffer.push_str ("<body>\n");
489		
490		_buffer.push_str (&_body);
491		
492		_buffer.push_str ("</body>\n");
493		_buffer.push_str ("</html>\n");
494		
495		_buffer
496	};
497	
498	Ok (_html)
499}
500
501
502
503
504pub fn build_markdown_anchor_from_text (_text : &str) -> String {
505	
506	let mut _text = any_ascii::any_ascii (_text);
507	_text.make_ascii_lowercase ();
508	
509	let _max_length = std::cmp::max (_text.len (), 128);
510	let mut _id = String::with_capacity (_max_length);
511	
512	let mut _separator = false;
513	for _character in _text.chars () {
514		if _id.len () >= _max_length {
515			break;
516		}
517		if _character.is_ascii_alphabetic () || _character.is_ascii_digit () {
518			if _separator {
519				_id.push ('_');
520				_separator = false;
521			}
522			_id.push (_character);
523		} else {
524			_separator = true;
525		}
526	}
527	
528	return _id;
529}
530