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