bfom_lib/
lib.rs

1mod line_type;
2pub mod utilities;
3
4use line_type::{block_line_type, HtmlType, LineType};
5
6use std::{
7  collections::HashMap,
8  fs::File,
9  io::{BufRead, BufReader, Error},
10  path::Path,
11};
12
13// for testing
14
15// for converting
16#[derive(Debug, Clone)]
17struct NodeData {
18  // h1/p/pre/etc
19  // plain text is none
20  tag: String,
21
22  // any misc data, depends on teh tag
23  misc: Option<String>,
24  // the raw data inside this node
25  text: Option<String>,
26
27  contents: Vec<NodeData>,
28}
29
30#[derive(Debug, Clone)]
31struct ReferenceLink {
32  url: String,
33  title: Option<String>,
34}
35
36#[derive(Debug)]
37pub struct Converter {
38  html_void: Vec<String>,
39  references: HashMap<String, ReferenceLink>,
40  indentation: usize,
41}
42
43#[derive(Debug)]
44pub struct FrontMatter {
45  pub date: Option<String>,
46  pub description: Option<String>,
47  pub title: Option<String>,
48  pub slug: Option<String>,
49  pub author: Option<String>,
50  pub draft: bool,
51  pub slides: bool,
52  pub skip_index: bool,
53}
54
55// takes the link provided, converts it
56impl Converter {
57  pub fn new(html_void: Vec<String>, indentation: usize) -> Self {
58    Converter {
59      html_void,
60      references: Default::default(),
61      indentation,
62    }
63  }
64
65  // this manages the conversion
66  pub fn convert_file(&mut self, input: &Path, depth: usize) -> Result<(String, FrontMatter), Error> {
67    // read file into memory
68    let lines = self.file_read(input)?;
69
70    // grab the reference lines out of it
71    let lines_processed_references = self.reference_link_get(lines);
72
73    // pull out teh fm data
74    let (lines_processed_fm, fm) = self.fm_get(lines_processed_references);
75
76    // get teh block elements
77    // frontmatter is only valid at a top level one
78    let blocks = self.block_process(lines_processed_fm)?;
79
80    // merge it all together
81    let processed = self.block_merge(blocks, depth, fm.slides);
82
83    // clean up references
84    self.references = Default::default();
85
86    Ok((processed, fm))
87  }
88
89  // load the file into working memory
90  fn file_read(&self, path: &Path) -> Result<Vec<String>, Error> {
91    // read file into memory
92    //println!("In file {}", path);
93    // read the file into memory
94    let input = File::open(path)?;
95    let buffered = BufReader::new(input);
96
97    let lines_all = buffered
98      .lines()
99      .map(|l| {
100        let unwrapped = l.expect("Could not parse line");
101        unwrapped.replace('\u{0000}', "\u{FFFD}")
102      })
103      .collect::<Vec<String>>();
104
105    Ok(lines_all)
106  }
107
108  fn reference_link_get(&mut self, lines: Vec<String>) -> Vec<String> {
109    let mut result: Vec<String> = vec![];
110
111    // iterate through the lines
112    // if one matches the format it is not added to the result and added to teh link hash, if its not already there
113
114    let mut references: HashMap<String, ReferenceLink> = HashMap::new();
115
116    'outer: for line in lines {
117      // only lines that start with [ are considered reference, number of spaces is irrelevant
118      if !line.trim_start().starts_with('[') {
119        result.push(line);
120        continue;
121      }
122
123      // now check of the rest of the line matches
124
125      // form manging position
126      let characters: Vec<char> = line.trim_start().chars().collect();
127      // starts at 1 to skip the first [
128      let mut index_char = 1;
129
130      // for getting the identifier
131      let mut reference_vec: Vec<char> = vec![];
132      loop {
133        if index_char >= characters.len() {
134          break;
135        }
136
137        let character = characters[index_char];
138
139        match character {
140          ']' => {
141            // check if last character is a backslash
142            if characters[index_char - 1] == '\\' {
143              // remove teh slash
144              // add the bracket
145              reference_vec.pop();
146              reference_vec.push(character);
147            } else {
148              // increment to account for this
149              index_char += 1;
150              break;
151            }
152          }
153          _ => reference_vec.push(character),
154        }
155        index_char += 1;
156      }
157
158      // need to have an identifier
159      if reference_vec.is_empty() {
160        result.push(line);
161        continue 'outer;
162      }
163
164      // check if next character is ':' (required)
165      if index_char >= characters.len() {
166        result.push(line);
167        continue 'outer;
168      }
169      if characters[index_char] != ':' {
170        // add to normal output
171        result.push(line);
172        continue 'outer;
173      } else {
174        // if ir is :
175        index_char += 1;
176      }
177
178      // check if next character is whitespace, skip past these
179      loop {
180        if index_char >= characters.len() {
181          break;
182        }
183        let character = characters[index_char];
184
185        if character == ' ' || character == '\t' {
186          index_char += 1;
187        } else {
188          break;
189        }
190      }
191
192      // now get teh url
193      let mut url_vec: Vec<char> = vec![];
194      // (start, end)
195      let mut angle_brackets = (false, false);
196      loop {
197        if index_char >= characters.len() {
198          break;
199        }
200        let character = characters[index_char];
201
202        match character {
203          '<' => {
204            // opening is only allowed on teh first chr
205            if !angle_brackets.0 {
206              angle_brackets = (true, false);
207            } else {
208              url_vec.push(character)
209            }
210          }
211          '>' => {
212            // check if it was opened
213            if angle_brackets.0 {
214              // then this closes it out
215
216              // check if its preceded by backslash, its action is ignored
217              if characters[index_char - 1] == '\\' {
218                // remove teh slash
219                url_vec.pop();
220                url_vec.push(character);
221              } else {
222                // actually closes it out
223                angle_brackets = (true, true);
224
225                index_char += 1;
226                break;
227              }
228            } else {
229              url_vec.push(character);
230            }
231          }
232          ' ' | '\t' => {
233            // space breaks the url, unless its in an angled bracket
234
235            if angle_brackets.0 {
236              url_vec.push(character)
237            } else {
238              break;
239            }
240          }
241
242          _ => url_vec.push(character),
243        }
244
245        index_char += 1;
246      }
247
248      if url_vec.is_empty() {
249        // check if its angle brackets
250        if !angle_brackets.1 {
251          result.push(line);
252          continue 'outer;
253        }
254      }
255
256      // if angle brackets were opened but never closed
257      if angle_brackets.0 && !angle_brackets.1 {
258        result.push(line);
259        continue 'outer;
260      }
261
262      // check for whitespace again, but this time it must be at least 1 if its followed by an open quotation
263      let mut whitespace = 0;
264      loop {
265        if index_char >= characters.len() {
266          break;
267        }
268        let character = characters[index_char];
269
270        match character {
271          ' ' | '\t' => {
272            index_char += 1;
273            whitespace += 1;
274          }
275          '\'' | '"' => {
276            if whitespace == 0 {
277              result.push(line);
278              continue 'outer;
279            } else {
280              break;
281            }
282          }
283          _ => break,
284        }
285      }
286
287      // find the optional title next
288
289      // first char must be either ' or "
290
291      let mut title_quote: Option<char> = None;
292      if index_char < characters.len() {
293        let character = characters[index_char];
294        match character {
295          '\'' | '"' => title_quote = Some(character),
296          _ => {}
297        }
298        index_char += 1;
299      }
300
301      let mut title_vec: Vec<char> = vec![];
302
303      if let Some(delimiter) = title_quote {
304        // starts off being opened
305        let mut title_closed = false;
306
307        loop {
308          if index_char >= characters.len() {
309            break;
310          }
311          let character = characters[index_char];
312
313          if character == delimiter {
314            // check if preceding was backslash
315
316            if characters[index_char - 1] == '\\' {
317              // remove teh slash
318              // add the bracket
319              title_vec.pop();
320              title_vec.push(character);
321            } else {
322              title_closed = true;
323
324              // increment to account for this
325              //index_char += 1;
326              break;
327            }
328          } else {
329            title_vec.push(character)
330          }
331
332          index_char += 1;
333        }
334
335        // check if it was closed properly
336        if !title_closed {
337          // title is optional so just clear it instead of revoking teh line
338          title_vec = vec![];
339        }
340      }
341
342      // tidy up
343      let reference: String = reference_vec.iter().collect();
344      let url: String = url_vec.iter().collect();
345
346      let title: Option<String> = if title_vec.is_empty() { None } else { Some(title_vec.iter().collect()) };
347
348      let reference_data = ReferenceLink {
349        url,
350        title,
351      };
352
353      references.insert(reference, reference_data);
354    }
355
356    self.references = references;
357
358    result
359  }
360
361  fn fm_get(&mut self, lines: Vec<String>) -> (Vec<String>, FrontMatter) {
362    let mut fm = FrontMatter {
363      date: None,
364      description: None,
365      title: None,
366      slug: None,
367      author: None,
368      draft: false,
369      slides: false,
370      skip_index: false,
371    };
372
373    let mut result: Vec<String> = vec![];
374
375    let mut fm_processed = false;
376    let mut fm_in_progress = false;
377
378    // get the language
379    let mut closer: Vec<char> = vec![];
380    let mut pre_closer = String::default();
381    for line in lines {
382      if block_line_type(&line, &[]) == LineType::FrontMatter && !fm_in_progress && !fm_processed {
383        fm_in_progress = true;
384      }
385      if !fm_in_progress {
386        result.push(line);
387        continue;
388      }
389
390      /*
391      All front matter blocks are fenced.
392      They start and end with at least 3 plusses (+++).
393
394      Everything inside can have further md applied, though its returned as a String.
395      */
396
397      if closer.is_empty() {
398        for character in line.trim().chars() {
399          match character {
400            '+' => closer.push(character),
401            _ => {
402              break;
403            }
404          }
405        }
406        pre_closer = closer.iter().collect::<String>().trim().to_string();
407
408        // nothing more left to do on this line
409        continue;
410      }
411
412      let trimmed = line.trim();
413
414      if trimmed == pre_closer.as_str() {
415        fm_in_progress = false;
416        fm_processed = true;
417        continue;
418      }
419
420      // start off with default value
421      let mut stripped = String::default();
422
423      for substring in ["date", "slug", "title", "description", "author", "draft", "slides", "skip_index"] {
424        if trimmed.starts_with(substring) {
425          stripped = trimmed.replace(substring, "")
426        }
427      }
428
429      // extract data from teh line, each line can has the format of
430      // attribute = value
431      // spaces
432      let mut characters = vec![];
433      let mut equals = false;
434      for c in stripped.chars() {
435        match c {
436          '=' => {
437            if !equals {
438              equals = true;
439            } else {
440              characters.push(c);
441            }
442          }
443          _ => {
444            if equals {
445              characters.push(c);
446            }
447          }
448        }
449      }
450
451      let tmp = characters.iter().collect::<String>();
452      let mut value_tmp = tmp.trim();
453      if (value_tmp.starts_with('\'') && value_tmp.ends_with('\'')) || (value_tmp.starts_with('"') && value_tmp.ends_with('"')) {
454        value_tmp = &value_tmp[1..value_tmp.len() - 1];
455      }
456      let value = value_tmp.trim().to_owned();
457      let formatted = self.inline_process(value.clone(), 0);
458
459      // description
460      if trimmed.starts_with("date") {
461        fm.date = Some(formatted);
462      } else if trimmed.starts_with("slug") {
463        fm.slug = Some(value);
464      } else if trimmed.starts_with("title") {
465        fm.title = Some(formatted);
466      } else if trimmed.starts_with("description") {
467        fm.description = Some(formatted);
468      } else if trimmed.starts_with("author") {
469        fm.author = Some(formatted);
470      } else if trimmed.starts_with("draft") && value.to_ascii_lowercase().as_str() == "true" {
471        fm.draft = true;
472      } else if trimmed.starts_with("slides") && value.to_ascii_lowercase().as_str() == "true" {
473        fm.slides = true;
474      } else if trimmed.starts_with("skip_index") && value.to_ascii_lowercase().as_str() == "true" {
475        fm.skip_index = true;
476      };
477    }
478
479    (result, fm)
480  }
481
482  fn block_process(&self, lines: Vec<String>) -> Result<Vec<NodeData>, Error> {
483    // this gets most of the structure done
484
485    // what gets returned
486    let mut node_data: Vec<NodeData> = vec![];
487
488    // these handle dealing with teh current lines
489    let mut row: isize = 0;
490
491    // These keep track of info about the current block
492    let mut block_excluded: Vec<String> = vec![];
493
494    // (row, col)
495    let mut html_end_col = 0;
496
497    loop {
498      // ensure that row is in bounds
499      if row >= lines.len() as isize {
500        break;
501      }
502
503      let line = if html_end_col != 0 { &lines[row as usize][html_end_col..] } else { lines[row as usize].as_str() };
504
505      //println!("Start:  Top: {}. Row {} of {}. Block: {:?}, Current Line: {}. Line: {}. End?: {}", _top_layer, row, lines.len(), block_type, line_current_type.0, line, line_current_is_end);
506
507      // an array of this line and all the remaining lines to be sent off to the sub functions
508      let lines_remaining = &lines[(row as usize)..];
509
510      // partial is for paragraphs and html
511      let mut lines_remaining_partial = vec![line.to_string()];
512
513      if ((row as usize) + 1) < lines.len() {
514        let remainder = &lines[((row as usize) + 1)..];
515        lines_remaining_partial.extend(remainder.to_vec());
516      }
517
518      let mut clean_up = false;
519      // now process based on what type of line it is
520      match block_line_type(line, &block_excluded) {
521        LineType::Header => {
522          node_data.push(self.block_process_headers(line));
523
524          // go straight to the next row
525          row += 1;
526
527          clean_up = true;
528        }
529        LineType::HorizontalRule => {
530          /*
531              hr are simple, no content or anything.
532          */
533
534          node_data.push(NodeData {
535            tag: "hr".to_string(),
536            text: None,
537            misc: None,
538            contents: vec![],
539          });
540
541          // go straight to the next row
542          row += 1;
543
544          clean_up = true;
545        }
546
547        // blocks from here on out
548        LineType::Paragraph => {
549          if line.is_empty() {
550            row += 1;
551            continue;
552          }
553
554          if let Some((node, offset)) = self.block_process_paragraph(&lines_remaining_partial, 0, &block_excluded) {
555            node_data.push(node);
556
557            row += offset;
558
559            clean_up = true;
560          } else {
561            block_excluded.push("p".to_string());
562          }
563        }
564        LineType::Preformatted => {
565          if let Some((node, offset)) = self.block_process_pre(lines_remaining) {
566            node_data.push(node);
567
568            // +1 as to not include the closing fence
569            row += offset + 1;
570
571            clean_up = true;
572          } else {
573            block_excluded.push("pre".to_string());
574          }
575        }
576
577        LineType::FrontMatter => {
578          // no longer handled here
579        }
580
581        LineType::BlockQuote => {
582          if let Some((node, offset)) = self.block_process_blockquote(lines_remaining) {
583            node_data.push(node);
584
585            row += offset;
586
587            clean_up = true;
588          } else {
589            block_excluded.push("blockquote".to_string());
590          }
591        }
592
593        LineType::UL(_, _) | LineType::OL(_, _) => {
594          if let Some((node, offset)) = self.block_process_lists(lines_remaining) {
595            node_data.push(node);
596
597            row += offset;
598
599            clean_up = true;
600          } else {
601            block_excluded.push("ul".to_string());
602          }
603        }
604
605        LineType::Html(html_type) => {
606          if let Some((node, offset, col)) = self.block_process_html(&lines_remaining_partial, html_type) {
607            node_data.push(node);
608
609            if col > 0 {
610              // repeat teh current line
611              row += offset - 1;
612              html_end_col = col;
613            } else {
614              row += offset;
615              html_end_col = 0;
616            }
617
618            // clear the excluded blocks because this completed successfully
619            block_excluded = vec![];
620          } else {
621            match html_type {
622              HtmlType::Normal => {
623                block_excluded.push("html".to_string());
624              }
625              HtmlType::Comment => {
626                block_excluded.push("html_comment".to_string());
627              }
628              HtmlType::CData => {
629                block_excluded.push("html_cdata".to_string());
630              }
631            }
632          }
633          // either way continue to the next/same row
634          continue;
635        }
636
637        LineType::Table => {
638          if let Some((node, offset)) = self.block_process_table(lines_remaining) {
639            node_data.push(node);
640
641            row += offset;
642
643            clean_up = true;
644          } else {
645            block_excluded.push("table".to_string());
646          }
647        }
648      }
649
650      // this is the clean up for everything but html
651      if clean_up {
652        // clean up
653        block_excluded = vec![];
654        html_end_col = 0;
655
656        continue;
657      }
658
659      // this is to catch anything that somehow slipped through
660      row += 1;
661    }
662    Ok(node_data)
663  }
664
665  fn block_process_headers(&self, line: &str) -> NodeData {
666    /*
667        Headers are pretty easy to process.
668        Strip leading whitespace.
669        Count how many #'s there are at the start
670        This either caps at 6 or when there is a non # character
671        Anything after the last #] is counted as content
672    */
673
674    let mut count = 0;
675    let mut broken = false;
676    let mut content_array: Vec<char> = vec![];
677    for character in line.trim().chars() {
678      match character {
679        '#' => {
680          if !broken && count < 6 {
681            count += 1
682          } else {
683            content_array.push(character);
684          }
685        }
686        _ => {
687          broken = true;
688          content_array.push(character);
689        }
690      }
691    }
692
693    // for this the tag is depending on the number of #'s at the beginning
694    let tag = format!("h{}", &count);
695
696    // content is everything after the break
697    let content_string = content_array.to_vec().iter().collect::<String>();
698    let text = Some(content_string.as_str().trim().to_string());
699
700    NodeData {
701      tag,
702      text,
703      misc: None,
704      contents: vec![],
705    }
706  }
707
708  fn block_process_pre(&self, lines: &[String]) -> Option<(NodeData, isize)> {
709    /*
710        All pre blocks are fenced.
711        They start with at least 3 backticks (`)and an optional language identifier.
712        They close out with an equal number of backticks as the opener
713
714        Opening square brackets '<' are replaced with '&lt;'.
715
716        Everything inside is preformatted text and no further processing is done.
717    */
718
719    let line_first = &lines[0];
720
721    // get the language
722    let mut pre_language_tmp: Vec<char> = vec![];
723    let mut closer: Vec<char> = vec![];
724    let mut space = false;
725    for character in line_first.trim().chars() {
726      match character {
727        // skip this one
728        '`' => {
729          if !space {
730            closer.push(character)
731          }
732
733          continue;
734        }
735        ' ' | '\t' => {
736          // first one mark it as a space
737          if !space {
738            space = true;
739          } else {
740            pre_language_tmp.push(character);
741          }
742        }
743        _ => {
744          pre_language_tmp.push(character);
745        }
746      }
747    }
748
749    let pre_language = if pre_language_tmp.is_empty() {
750      None
751    } else {
752      let language = pre_language_tmp.iter().collect::<String>().trim().to_string();
753      if language.is_empty() {
754        None
755      } else {
756        Some(language)
757      }
758    };
759
760    // set the closer it is looking for
761    let pre_closer = closer.iter().collect::<String>().trim().to_string();
762
763    // start on the second row
764    let mut row = 1;
765
766    let mut block_lines = vec![];
767    let mut finished = false;
768    loop {
769      if row >= lines.len() as isize {
770        break;
771      }
772      let line = &lines[row as usize];
773
774      if line.trim() == pre_closer.as_str() {
775        // end of block, now tidy up
776        finished = true;
777        break;
778      } else {
779        // add the lines to teh block
780        block_lines.push(line.clone());
781
782        // if its the last line then its caught alter down
783      }
784      row += 1;
785    }
786
787    // if successful it delivers teh NodeData and the offset, failure is None
788
789    if finished {
790      let text = block_lines.join("\n").replace('<', "&lt;").replace('>', "&gt;");
791      let node = NodeData {
792        tag: "pre".to_string(),
793        text: Some(text),
794        misc: pre_language,
795        contents: vec![],
796      };
797      Some((node, row))
798    } else {
799      None
800    }
801  }
802
803  fn block_process_blockquote(&self, lines: &[String]) -> Option<(NodeData, isize)> {
804    /*
805        Like Paragraphs, Blockquotes dont have a failure condition.
806        Also it is one of the few blocks that can have other blocks inside itself
807        This is because lazy continuation is not allowed each like starts with >
808
809        The first > is stripped off of each line and the result is added to an array.
810        On the final row with a > the contents are recursively added back into the block_process function.
811    */
812
813    let mut row = 0;
814
815    let mut block_lines = vec![];
816    loop {
817      if row >= lines.len() as isize {
818        break;
819      }
820      let line = &lines[row as usize];
821
822      // > indicates that it is a blockquote, >! indicates a spoiler
823      if line.starts_with('>') && !line.starts_with(">!") {
824        let cleaned = line.replacen('>', "", 1).replacen(' ', "", 1);
825        block_lines.push(cleaned);
826      } else {
827        break;
828      }
829
830      row += 1;
831    }
832
833    if !block_lines.is_empty() {
834      let contents = self
835        .block_process(block_lines)
836        // if it errors then fail fairly gracefully
837        .unwrap_or_default();
838
839      let node = NodeData {
840        tag: "blockquote".to_string(),
841        text: None,
842        misc: None,
843        contents,
844      };
845
846      Some((node, row))
847    } else {
848      None
849    }
850  }
851
852  fn block_process_table(&self, lines: &[String]) -> Option<(NodeData, isize)> {
853    /*
854        Mostly taken from https://github.github.com/gfm/#tables-extension- with a few changes:
855        First character on a line is always |, while a trailing | isn't explicitly required it can look better.
856
857        The delimiter line can occur anywhere, even before the header.
858        This is because it is used to set the alignment.
859    */
860
861    let mut table_rows = vec![];
862    let mut table_alignment: HashMap<i32, String> = HashMap::new();
863    let mut table_header_length = 0;
864
865    let mut row = 0;
866    loop {
867      if row >= lines.len() as isize {
868        break;
869      }
870      let line = &lines[row as usize];
871
872      // break early if its not a table
873      if !line.trim().starts_with('|') {
874        break;
875      }
876
877      // deal with alignment rows first
878      if line.replace(['|', '-', ':'], "").trim() == "" {
879        // will use https://developer.mozilla.org/en-US/docs/Web/CSS/text-align as the way github does it is no longer valid
880
881        // alignment will impact subsequent lines, unless overwritten
882        table_alignment = self.block_process_table_alignment(line);
883
884        row += 1;
885        continue;
886      }
887
888      let mut row_contents: Vec<NodeData> = vec![];
889      let mut content: Vec<char> = vec![];
890      let mut last_character: Option<char> = None;
891      let mut current_col = 0;
892
893      // first row has cols with th, every other row is td
894      let tag_col = if table_rows.is_empty() { "th".to_string() } else { "td".to_string() };
895
896      let tag_row = "tr".to_string();
897
898      for character in line.trim_start().trim_end_matches('|').chars() {
899        match character {
900          '|' => {
901            if let Some(x) = last_character {
902              if x == '\\' {
903                // remove the backslash
904                content.pop();
905                content.push(character);
906                last_character = Some(character);
907                continue;
908              }
909            }
910
911            if current_col > 0 {
912              let text: String = content.iter().collect();
913              let alignment = table_alignment.get(&{ current_col }).cloned();
914
915              // add current
916              row_contents.push(NodeData {
917                tag: tag_col.clone(),
918                misc: alignment,
919                text: Some(text),
920                contents: vec![],
921              });
922            }
923
924            // clear teh existing data (since its previously used
925            content = vec![];
926
927            // mark it as opening another col
928            current_col += 1;
929
930            if table_rows.is_empty() {
931              table_header_length = current_col;
932            } else if current_col > table_header_length {
933              // if it isn't the header check if the new number exceeds what teh header gave
934              break;
935            }
936          }
937          _ => content.push(character),
938        }
939        last_character = Some(character);
940      }
941
942      // tidy up any trailing content
943      if !content.is_empty() {
944        let text: String = content.iter().collect();
945        let alignment = table_alignment.get(&{ current_col }).cloned();
946
947        // add current
948        row_contents.push(NodeData {
949          tag: tag_col.clone(),
950          misc: alignment,
951          text: Some(text),
952          contents: vec![],
953        });
954      }
955
956      table_rows.push(NodeData {
957        tag: tag_row,
958        misc: None,
959        text: None,
960        contents: row_contents,
961      });
962
963      row += 1;
964    }
965
966    if !table_rows.is_empty() {
967      let table_header = vec![table_rows[0].clone()];
968
969      let table_body = if table_rows.len() > 1 { table_rows[1..].to_vec() } else { vec![] };
970
971      let table = NodeData {
972        tag: "table".to_string(),
973        misc: None,
974        text: None,
975        contents: vec![
976          NodeData {
977            tag: "thead".to_string(),
978            misc: None,
979            text: None,
980            contents: table_header,
981          },
982          NodeData {
983            tag: "tbody".to_string(),
984            misc: None,
985            text: None,
986            contents: table_body,
987          },
988        ],
989      };
990
991      Some((table, row))
992    } else {
993      None
994    }
995  }
996
997  fn block_process_lists(&self, lines: &[String]) -> Option<(NodeData, isize)> {
998    /*
999        All types of lists are comprised of li elements.
1000        These li can contain other blocks.
1001        Because of this it makes it a tad trickier to process compared to other blocks.
1002
1003        Thankfully since there is no lazy continuation there are two ways to see if a line is part of a li.
1004            1. It is the first line of a li.
1005            2. It has an indent specified by the first line of the li.
1006
1007        Like blockquotes the li contents are recursively proceed in the block_process function.
1008
1009    */
1010
1011    // manage teh current li
1012    let mut li_active = false;
1013    let mut li_number: Option<String> = None;
1014    let mut li_type = LineType::Paragraph;
1015    let mut li_indent = 0;
1016    // current lines in teh list item, list items can have other blocks inside them
1017    let mut li_lines = vec![];
1018    let mut li_finished = false;
1019
1020    let mut li_array = vec![];
1021
1022    let mut row = 0;
1023    loop {
1024      let mut finished = false;
1025
1026      if row >= lines.len() as isize {
1027        break;
1028      }
1029      let line = &lines[row as usize];
1030
1031      let list_type = block_line_type(line, &[]);
1032
1033      if !li_active {
1034        // deal with first line of a li
1035
1036        match list_type {
1037          LineType::OL(indent, number) => {
1038            li_indent = indent;
1039            li_number = Some(number.to_string());
1040          }
1041          LineType::UL(indent, _) => {
1042            li_indent = indent;
1043          }
1044          _ => {}
1045        };
1046
1047        let vec_chars: Vec<char> = line.chars().collect();
1048        // remove lists_indent's worth ofd characters
1049        let trimmed = &vec_chars[li_indent..];
1050        let cleaned = trimmed.iter().collect::<String>();
1051
1052        li_lines.push(cleaned);
1053        li_type = list_type;
1054
1055        // next round will not be on this branch of the if
1056        li_active = true;
1057      } else {
1058        // second line of the lists
1059
1060        let leading_spaces = String::from_utf8(vec![b' '; li_indent]).unwrap_or_default();
1061
1062        if line.starts_with(&leading_spaces) {
1063          // if it starts with lists_indent spaces its staying in teh same li
1064          let trimmed = line.replacen(' ', "", li_indent);
1065          li_lines.push(trimmed);
1066        } else if line.is_empty() {
1067          // close out teh existing list item
1068          li_finished = true;
1069          finished = true;
1070        } else if compare_lists(list_type, li_type) {
1071          // get lists done properly, dont close out too fast
1072          li_finished = true;
1073
1074          // repeat the current line next time
1075          row -= 1;
1076        } else {
1077          // line is not part of the ul or li
1078          // close out teh li and ul
1079          li_finished = true;
1080          finished = true;
1081        }
1082      }
1083
1084      if (row + 1) >= lines.len() as isize {
1085        li_finished = true;
1086      }
1087
1088      if li_finished {
1089        if li_lines.len() == 1 {
1090          li_array.push(NodeData {
1091            tag: "li".to_string(),
1092            text: Some(li_lines[0].clone()),
1093            misc: li_number.clone(),
1094            contents: vec![],
1095          });
1096        } else {
1097          let contents = self
1098            .block_process(li_lines.clone())
1099            // if it errors then fail fairly gracefully
1100            .unwrap_or_default();
1101
1102          li_array.push(NodeData {
1103            tag: "li".to_string(),
1104            text: None,
1105            misc: li_number.clone(),
1106            contents,
1107          });
1108        }
1109
1110        // reset for next round
1111        li_finished = false;
1112        li_active = false;
1113        li_lines = vec![];
1114      }
1115
1116      if finished {
1117        break;
1118      }
1119
1120      row += 1;
1121    }
1122
1123    if !li_array.is_empty() {
1124      let list_type = match li_number {
1125        None => "ul".to_string(),
1126        Some(_) => "ol".to_string(),
1127      };
1128      let node = NodeData {
1129        tag: list_type,
1130        text: None,
1131        misc: None,
1132        contents: li_array,
1133      };
1134
1135      Some((node, row))
1136    } else {
1137      None
1138    }
1139  }
1140
1141  fn block_process_paragraph(&self, lines: &[String], row_start: isize, block_excluded: &[String]) -> Option<(NodeData, isize)> {
1142    /*
1143        Paragraphs are the catch all.
1144        If something does not fit in anything else its a paragraph.
1145
1146        A paragraph ends on a blank like or the start of a new block.
1147
1148        The extra parameters is due to the fact that some html can be considered part of a paragraph and to facilitate passthroughs.
1149        For example:
1150            <a href="">link</a> in paragraph
1151
1152        Paragraphs have the advantage of having no failure, they don't need to search for a closer
1153    */
1154
1155    let mut block_lines = lines[..(row_start as usize)].to_vec();
1156
1157    let mut row = row_start;
1158    let mut first_run = true;
1159    loop {
1160      if row >= lines.len() as isize {
1161        break;
1162      }
1163
1164      let line = &lines[row as usize];
1165
1166      // // a line gap is instant break
1167      if line.is_empty() {
1168        break;
1169      }
1170
1171      if block_lines.is_empty() {
1172        let line_to_push = if block_excluded.contains(&"html".to_string()) || block_excluded.contains(&"html_comment".to_string()) || block_excluded.contains(&"html_cdata".to_string()) {
1173          // strip first < from it, &gt;
1174          line.replacen('<', "&gt;", 1).to_string()
1175        } else {
1176          line.to_string()
1177        };
1178
1179        // add it to the tmp array
1180        block_lines.push(line_to_push);
1181      } else {
1182        match block_line_type(line, &[]) {
1183          LineType::Paragraph => {}
1184          _ => {
1185            // if its not a paragraph and if its not the first run
1186
1187            if !first_run {
1188              break;
1189            }
1190          }
1191        }
1192
1193        block_lines.push(line.to_string());
1194      }
1195
1196      first_run = false;
1197      row += 1;
1198    }
1199
1200    if !block_lines.is_empty() {
1201      let node = NodeData {
1202        tag: 'p'.to_string(),
1203        text: Some(block_lines.join("\n")),
1204        misc: None,
1205        contents: vec![],
1206      };
1207
1208      Some((node, row))
1209    } else {
1210      None
1211    }
1212  }
1213
1214  fn block_process_html(&self, lines: &[String], html_type: HtmlType) -> Option<(NodeData, isize, usize)> {
1215    /*
1216        HTML blocks are identified by a line starting with:
1217            * <!--
1218            * <![CDATA[
1219            * < followed by any alphanumeric character
1220        Based on the identifier an opener and a closer are set.
1221
1222        Both are set in order to handle nestled html:
1223            <div><div>inner content</div></div>
1224
1225
1226        Blocks can also end mid way through a line so keeping track of the last col is important for them:
1227            <div>first</div><div>second</div>
1228
1229
1230    */
1231
1232    let opener;
1233    let closer;
1234    let tag;
1235
1236    if !lines.is_empty() {
1237      if let Some((inner_tag, inner_opener, inner_closer)) = self.block_process_html_tags(&lines[0], html_type) {
1238        tag = inner_tag;
1239        opener = inner_opener;
1240        closer = inner_closer;
1241      } else {
1242        return None;
1243      }
1244    } else {
1245      return None;
1246    };
1247
1248    let mut block_lines = vec![];
1249
1250    let mut html_depth = 0;
1251
1252    let mut row = 0;
1253    let mut index = 0;
1254    let mut html_end_col = 0;
1255    let mut finished = false;
1256    loop {
1257      if row >= lines.len() as isize {
1258        break;
1259      }
1260      let line = &lines[row as usize];
1261
1262      // to get html blocks each row has to be scanned for both openers and closers
1263
1264      // set the flags for this line
1265      let mut valid_chars_closing = 0;
1266      let mut valid_chars_opening = 0;
1267      // reset the index
1268      index = 0;
1269
1270      let line_chars: Vec<char> = line.chars().collect();
1271
1272      loop {
1273        if closer.is_empty() {
1274          break;
1275        }
1276        if opener.is_empty() {
1277          break;
1278        }
1279        if index >= line_chars.len() {
1280          break;
1281        }
1282
1283        let character = line_chars[index];
1284
1285        // set it to be used next round
1286        index += 1;
1287
1288        if opener[valid_chars_opening] == character {
1289          // character is valid,
1290          valid_chars_opening += 1;
1291
1292          if valid_chars_opening == opener.len() {
1293            html_depth += 1;
1294            valid_chars_opening = 0;
1295          }
1296        } else {
1297          // reset
1298          valid_chars_opening = 0;
1299        }
1300
1301        if closer[valid_chars_closing] == character {
1302          // character is valid,
1303          valid_chars_closing += 1;
1304
1305          if valid_chars_closing == closer.len() {
1306            html_depth -= 1;
1307            if html_depth == 0 {
1308              finished = true;
1309              break;
1310            }
1311            // reset it
1312            valid_chars_closing = 0;
1313          }
1314        } else {
1315          // reset
1316          valid_chars_closing = 0;
1317        }
1318      }
1319
1320      if finished {
1321        break;
1322      } else {
1323        block_lines.push(line.to_string())
1324      }
1325
1326      row += 1;
1327    }
1328
1329    // its marked finished if its closed out properly
1330    if finished {
1331      let mut partial_line = false;
1332      let mut permitted_in_p = false;
1333      let forbidden_tag = false; // how handling forbidden tags would be
1334
1335      let line = &lines[row as usize];
1336
1337      // check if there is anything left on teh line
1338      if line[index..].trim() != "" {
1339        partial_line = true;
1340
1341        // check if the tag is permitted in paragraphs
1342        // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#phrasing_content
1343        // https://www.w3.org/TR/html52/dom.html#phrasing-content
1344        let allowed_tags_p = vec![
1345          // pure html stuff
1346          "abbr",
1347          "audio",
1348          "b",
1349          "button",
1350          "canvas",
1351          "cite",
1352          "code",
1353          "data",
1354          "datalist",
1355          "dfn",
1356          "em",
1357          "embed",
1358          "i",
1359          "iframe",
1360          "img",
1361          "input",
1362          "label",
1363          "mark",
1364          "math",
1365          "meter",
1366          "noscript",
1367          "object",
1368          "output",
1369          "picture",
1370          "progress",
1371          "q",
1372          "ruby",
1373          "samp",
1374          "script",
1375          "select",
1376          "small",
1377          "span",
1378          "string",
1379          "sub",
1380          "sup",
1381          "svg",
1382          "textarea",
1383          "time",
1384          "u",
1385          "var",
1386          "video",
1387          "wbr",
1388          // according to dn these are not always applicable
1389          "a",
1390          "del",
1391          "ins",
1392          "map",
1393          // these require an itemprop attribute, however not looking for these
1394          // "link", "mata",
1395
1396          // these are the "custom" tags for misc html stuff
1397          "custom_comment",
1398          "custom_cdata",
1399        ];
1400
1401        if allowed_tags_p.contains(&tag.as_str()) {
1402          permitted_in_p = true;
1403        }
1404      }
1405
1406      // add list of forbidden tags here
1407      if forbidden_tag {
1408        // forbidden
1409      }
1410
1411      // tidy up the closing of html here
1412      if partial_line {
1413        if permitted_in_p {
1414          // change the type to paragraph
1415
1416          // switch over to paragraph, return that result
1417          return if let Some((node, offset)) = self.block_process_paragraph(lines, row, &[]) {
1418            Some((node, offset, 0))
1419          } else {
1420            // was not a code block, exclude it and try the same line again
1421            None
1422          };
1423        } else {
1424          // the first part is always added to teh tmp array
1425          let tmp_line = &line[..index];
1426          block_lines.push(tmp_line.to_string());
1427
1428          // decide what to do with teh rest
1429
1430          // get the type of the remaining line
1431          // if its html then its processed
1432          // else it is completely skipped
1433          let remaining = &line[index..];
1434
1435          if let LineType::Html(_) = block_line_type(remaining, &[]) {
1436            html_end_col = index;
1437          }
1438        }
1439      } else {
1440        // add whole line to teh array
1441        block_lines.push(line.to_string());
1442      }
1443
1444      // close up the html tag
1445
1446      let node = NodeData {
1447        tag: "html".to_string(),
1448        // gonna do no more processing on this, just going to pass it straight through
1449        text: Some(block_lines.join("\n")),
1450        misc: None,
1451        contents: vec![],
1452      };
1453
1454      // +1 to include the last row
1455      Some((node, row + 1, html_end_col))
1456    } else {
1457      None
1458    }
1459  }
1460
1461  fn block_process_html_tags(&self, line: &str, html_type: HtmlType) -> Option<(String, Vec<char>, Vec<char>)> {
1462    let mut opener;
1463    let mut closer;
1464    let tag;
1465
1466    match html_type {
1467      HtmlType::Normal => {
1468        // type of tag isn't known here
1469
1470        // this first line is to find the tag name
1471        let mut tag_vec: Vec<char> = vec![];
1472
1473        // 1 to skip the opening < in teh first run
1474        let mut index = 1;
1475        let line_chars: Vec<char> = line.trim_start().chars().collect();
1476        loop {
1477          if index >= line_chars.len() {
1478            break;
1479          }
1480
1481          let character = line_chars[index];
1482
1483          // set it to be used next round
1484          index += 1;
1485          match character {
1486            'a'..='z' | 'A'..='Z' | '0'..='9' => {
1487              tag_vec.push(character);
1488            }
1489            _ => {
1490              // not a valid tag character
1491              break;
1492            }
1493          }
1494        }
1495
1496        // not a valid tag
1497        if tag_vec.is_empty() {
1498          // repeat teh current line, but not as a html tag
1499          return None;
1500        }
1501
1502        // use html_void to see what type of closing it has
1503        // if its in html_void then its />
1504        // else its </tag>
1505
1506        tag = tag_vec.iter().collect();
1507
1508        // check for nestled html
1509        // <div>1<div>2</div></div>
1510        //html_depth += 1;
1511
1512        opener = vec!['<'];
1513        opener.extend(&tag_vec);
1514
1515        // these are the self-closing tags
1516        if self.html_void.contains(&tag) {
1517          closer = vec!['/', '>'];
1518        } else {
1519          closer = vec!['<', '/'];
1520          closer.extend(&tag_vec);
1521          closer.push('>');
1522        }
1523      }
1524      HtmlType::Comment => {
1525        // -->
1526        opener = vec!['<', '!', '-', '-'];
1527        closer = vec!['-', '-', '>'];
1528        tag = "custom_comment".to_string();
1529      }
1530      HtmlType::CData => {
1531        // ]]>
1532        opener = vec!['<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['];
1533        closer = vec![']', ']', '>'];
1534        tag = "custom_cdata".to_string();
1535      }
1536    }
1537
1538    Some((tag, opener, closer))
1539  }
1540
1541  fn block_process_table_alignment(&self, line: &str) -> HashMap<i32, String> {
1542    let mut alignment: HashMap<i32, String> = HashMap::new();
1543
1544    let mut last_character: Option<char> = None;
1545    let mut start = false;
1546    let mut end = false;
1547    let mut counter = 0;
1548    for character in line.trim_end_matches('|').chars() {
1549      match character {
1550        '|' => {
1551          // skip first character
1552          if last_character.is_none() {
1553            // dont forget to set it here
1554            last_character = Some(character);
1555            continue;
1556          }
1557
1558          counter += 1;
1559
1560          if start && end {
1561            alignment.insert(counter, "center".to_string());
1562          } else if start {
1563            alignment.insert(counter, "left".to_string());
1564          } else if end {
1565            alignment.insert(counter, "right".to_string());
1566          } else {
1567            // dont insert
1568          }
1569
1570          // reset for next one
1571          start = false;
1572          end = false;
1573        }
1574        ':' => {
1575          // if preceded by a space or bar then its a start
1576
1577          // if preceded by - then its a end
1578
1579          if let Some(last) = last_character {
1580            if last == ' ' || last == '|' {
1581              start = true;
1582            }
1583            if last == '-' {
1584              end = true;
1585            }
1586          }
1587        }
1588        _ => {
1589          // nothing should happen here
1590        }
1591      }
1592
1593      last_character = Some(character);
1594    }
1595
1596    counter += 1;
1597    // tidy up the trailing
1598    if start && end {
1599      alignment.insert(counter, "center".to_string());
1600    } else if start {
1601      alignment.insert(counter, "left".to_string());
1602    } else if end {
1603      alignment.insert(counter, "right".to_string());
1604    } else {
1605      // dont insert
1606    }
1607
1608    alignment
1609  }
1610
1611  fn block_merge(&self, nodes: Vec<NodeData>, mut depth: usize, slides: bool) -> String {
1612    let mut result: Vec<String> = vec![];
1613
1614    let mut slide_number = 0;
1615
1616    if slides {
1617      // let indented = format!("{:indent$}{}", "", x_opening, indent = (depth * self.indentation));
1618      result.push(format!("{:indent$}<section class='slide' id='{}' style='display:none'>", "", slide_number, indent = (depth * self.indentation)));
1619
1620      depth += 1;
1621    }
1622
1623    for node in nodes {
1624      let tag = node.tag.as_str();
1625      let mut slide = false;
1626      let (opening, content, closing) = match tag {
1627        // html just gets passed straight through
1628        "html" => {
1629          if let Some(text) = node.text {
1630            result.push(text);
1631          }
1632          (None, None, None)
1633        }
1634        // simple stuff first, no recursion
1635
1636        "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
1637          if let Some(text) = node.text {
1638            let processed = self.inline_process(text, 0);
1639
1640            // no attributes or anything
1641            let formatted = format!("<{}>{}</{}>", tag, processed, tag);
1642
1643            (Some(formatted), None, None)
1644          } else {
1645            (None, None, None)
1646          }
1647        }
1648
1649        "p" => {
1650          if let Some(text) = node.text {
1651            let processed = self.inline_process(text, depth + 1);
1652
1653            // no attributes or anything
1654            let open = format!("<{}>", tag);
1655            let close = format!("</{}>", tag);
1656
1657            (Some(open), Some(processed), Some(close))
1658          } else {
1659            (None, None, None)
1660          }
1661        }
1662
1663        "hr" => {
1664          if !slides {
1665            // simplest of them all
1666            (Some("<hr />".to_string()), None, None)
1667          } else {
1668            // starting teh next slide
1669            slide_number += 1;
1670            slide = true;
1671            (Some("</section>".to_string()), None, Some(format!("<section class='slide' id='{}' style='display:none'>", slide_number)))
1672          }
1673        }
1674
1675        "pre" => {
1676          if let Some(contents) = node.text {
1677
1678            // misc for fenced code blocks is used for the language
1679            let formatted = if let Some(language) = node.misc {
1680              // using class="language-{language}" is a pseudo standard
1681              format!("<pre><code class=\"language-{}\">\n{}\n</code></pre>", language, contents)
1682            } else {
1683              format!("<pre><code>\n{}\n</code></pre>", contents)
1684            };
1685
1686            result.push(formatted);
1687          }
1688          (None, None, None)
1689        }
1690
1691        ///////////////////////////
1692        // recursive beyond this //
1693        ///////////////////////////
1694
1695        // table
1696        "table" | "thead" | "tbody" | "tr" |
1697        // blockquote always contains other stuff
1698        "blockquote" |
1699        // lists
1700        "ul" | "ol"
1701        => {
1702          let processed = self.block_merge(node.contents, depth + 1, false);
1703          let open = format!("<{}>", tag);
1704          let close = format!("</{}>", tag);
1705          (Some(open), Some(processed), Some(close))
1706        }
1707
1708        "th" | "td" => {
1709          let processed = if let Some(text) = node.text {
1710            self.inline_process(text, depth + 1)
1711          } else {
1712            "".to_string()
1713          };
1714
1715          let open = if let Some(alignment) = node.misc {
1716            // text-align: alignment;
1717            // https://developer.mozilla.org/en-US/docs/Web/CSS/text-align
1718            format!("<{} style=\"text-align: {};\">", tag, alignment)
1719          } else {
1720            format!("<{}>", tag)
1721          };
1722
1723          let close = format!("</{}>", tag);
1724
1725          (Some(open), Some(processed), Some(close))
1726        }
1727
1728        "li" => {
1729          // this handles the contents for both ordered and unordered lists
1730
1731          let processed = if !node.contents.is_empty() {
1732            self.block_merge(node.contents, depth + 1, false)
1733          } else if let Some(text) = node.text {
1734            self.inline_process(text, depth + 1)
1735          } else {
1736            "".to_string()
1737          };
1738
1739
1740          let open = if let Some(value) = node.misc {
1741            format!("<{} value=\"{}\">", tag, value)
1742          } else {
1743            format!("<{}>", tag)
1744          };
1745
1746          let close = format!("</{}>", tag);
1747
1748          (Some(open), Some(processed), Some(close))
1749        }
1750
1751        _ => {
1752          (None, None, None)
1753        }
1754      };
1755
1756      if slide {
1757        depth -= 1;
1758      }
1759      if let Some(x_opening) = opening {
1760        let indented = format!("{:indent$}{}", "", x_opening, indent = (depth * self.indentation));
1761        result.push(indented)
1762      }
1763      if let Some(x_content) = content {
1764        let indented = format!("{:indent$}{}", "", x_content, indent = 0);
1765        // already indented in its section, specifically text content
1766        result.push(indented)
1767      }
1768      if let Some(x_closing) = closing {
1769        let indented = format!("{:indent$}{}", "", x_closing, indent = (depth * self.indentation));
1770        result.push(indented)
1771      }
1772      if slide {
1773        depth += 1;
1774      }
1775    }
1776
1777    if slides {
1778      // let indented = format!("{:indent$}{}", "", x_opening, indent = (depth * self.indentation));
1779      result.push(format!("{:indent$}</section>", "", indent = ((depth - 1) * self.indentation)));
1780    }
1781
1782    result.join("\n")
1783  }
1784
1785  fn inline_process(&self, input: String, depth: usize) -> String {
1786    // convert it into nodes
1787    let nodes = self.inline_spans(input);
1788
1789    // merge them together according to the type of node
1790    let merged = Converter::inline_merge(nodes);
1791
1792    // add line breaks if they are required
1793    // replace "  \n" with "\n<br />\n"
1794    let line_breaks_added = merged.replace("  \n", "\n<br />\n");
1795
1796    // add the appropriate indentation to make it readable as raw html
1797    self.inline_indention(line_breaks_added, depth)
1798  }
1799
1800  // this breaks down teh spans
1801  fn inline_spans(&self, input: String) -> Vec<NodeData> {
1802    /*
1803
1804        //em//
1805        **strong**
1806        __underline__
1807        ~~strikethrough~~
1808        >!spoiler text!< class=md-spoiler-text , hiding it is handled in css, using teh format from Reddit
1809        ``code``
1810        <<autolink>>
1811
1812        yes these are breaking changes with commonmark right back to daringfireball's one.
1813        main reason is conformity.
1814        All of these have a specific opening and closing tag (em/strong from previous implementations is tricky)
1815        identifiers dont do multiple jobs, _ was used in place of * in previous versions
1816
1817
1818        contents of html are checked for markdown
1819
1820
1821        [link][]
1822        ![image]
1823
1824        Valid:
1825            Normal:
1826                []()
1827                [](<>)
1828                [text](url "title")
1829                [text](<url with spaces>)
1830
1831            Reference:
1832                [identifier]: /url "title"
1833                [text][identifier]
1834
1835    text is subject to markdown
1836        ye some breaking changes here
1837
1838
1839
1840        >! spoilered  **strong spoilered //emp strong spoilered// __not underlined** !< __
1841        >!![]!<
1842
1843     */
1844
1845    let mut node_data: Vec<NodeData> = vec![];
1846
1847    let mut characters: Vec<char> = input.chars().collect();
1848
1849    let mut index = 0;
1850
1851    let mut span_closer: Vec<char> = vec![];
1852    let mut span_start = 0;
1853
1854    /*
1855    normal = stuff created by the delimiters
1856    html is any html blocks
1857    url is anything that matches a url [
1858    image is like the url but starts with ![
1859    */
1860
1861    // marks the start of a span
1862    let mut span_active = false;
1863    let mut span_finished = false;
1864    let mut span_reset = false;
1865
1866    // for allocating where items go
1867    let mut span_text = true; // defaults to this,
1868    let mut span_normal = false;
1869    let mut span_html = false;
1870
1871    let mut span_normal_type: Option<String> = None;
1872
1873    let mut html_tag_vec = vec![];
1874    let mut html_tag_complete = false;
1875
1876    loop {
1877      if index >= characters.len() {
1878        break;
1879      }
1880
1881      let character = characters[index];
1882      let character_next = if (index + 1) < characters.len() { Some(characters[index + 1]) } else { None };
1883      let character_last_escape = if (index as isize - 1) > 0 { characters[index - 1] == '\\' } else { false };
1884
1885      // this gets teh first section of a span
1886      if !span_active {
1887        match character {
1888          // em
1889          '/' => {
1890            // /
1891            if let Some(next) = character_next {
1892              if next == '/' {
1893                if character_last_escape {
1894                  // remove the backslash
1895                  characters.remove(index - 1);
1896
1897                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
1898                  // the -1 here will cancel out the +1 later
1899                  index -= 1;
1900                } else {
1901                  span_normal_type = Some("em".to_string());
1902                  span_closer = vec!['/', '/'];
1903                  span_active = true;
1904                  span_normal = true;
1905                  span_text = false;
1906                }
1907              }
1908            }
1909          }
1910          // strong
1911          '*' => {
1912            // *
1913            if let Some(next) = character_next {
1914              if next == '*' {
1915                if character_last_escape {
1916                  // remove the backslash
1917                  characters.remove(index - 1);
1918
1919                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
1920                  // the -1 here will cancel out the +1 later
1921                  index -= 1;
1922                } else {
1923                  span_normal_type = Some("strong".to_string());
1924                  span_closer = vec!['*', '*'];
1925                  span_active = true;
1926                  span_normal = true;
1927                  span_text = false;
1928                }
1929              }
1930            }
1931          }
1932          // underline
1933          '_' => {
1934            // _
1935            if let Some(next) = character_next {
1936              if next == '_' {
1937                if character_last_escape {
1938                  // remove the backslash
1939                  characters.remove(index - 1);
1940
1941                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
1942                  // the -1 here will cancel out the +1 later
1943                  index -= 1;
1944                } else {
1945                  span_normal_type = Some("u".to_string());
1946                  span_closer = vec!['_', '_'];
1947                  span_active = true;
1948                  span_normal = true;
1949                  span_text = false;
1950                }
1951              }
1952            }
1953          }
1954          // strikethrough
1955          '~' => {
1956            // ~
1957            if let Some(next) = character_next {
1958              if next == '~' {
1959                if character_last_escape {
1960                  // remove the backslash
1961                  characters.remove(index - 1);
1962
1963                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
1964                  // the -1 here will cancel out the +1 later
1965                  index -= 1;
1966                } else {
1967                  span_normal_type = Some("s".to_string());
1968                  span_closer = vec!['~', '~'];
1969                  span_active = true;
1970                  span_normal = true;
1971                  span_text = false;
1972                }
1973              }
1974            }
1975          }
1976          // code
1977          '`' => {
1978            // `
1979            if let Some(next) = character_next {
1980              if next == '`' {
1981                if character_last_escape {
1982                  // remove the backslash
1983                  characters.remove(index - 1);
1984
1985                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
1986                  // the -1 here will cancel out the +1 later
1987                  index -= 1;
1988                } else {
1989                  span_normal_type = Some("code".to_string());
1990                  span_closer = vec!['`', '`'];
1991                  span_active = true;
1992                  span_normal = true;
1993                  span_text = false;
1994                }
1995              }
1996            }
1997          }
1998          '>' => {
1999            // >
2000            // only one that breaks teh double mould
2001            if let Some(next) = character_next {
2002              if next == '!' {
2003                if character_last_escape {
2004                  // remove the backslash
2005                  characters.remove(index - 1);
2006
2007                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
2008                  // the -1 here will cancel out the +1 later
2009                  index -= 1;
2010                } else {
2011                  span_normal_type = Some("spoiler".to_string());
2012                  span_closer = vec!['!', '<'];
2013                  span_active = true;
2014                  span_normal = true;
2015                  span_text = false;
2016                }
2017              }
2018            }
2019          }
2020          '<' => {
2021            // set html first
2022            span_active = true;
2023            span_html = true;
2024            span_text = false;
2025
2026            if let Some(next) = character_next {
2027              // check if autolink
2028              match next {
2029                '<' => {
2030                  // undo the html flag set above
2031                  span_html = false;
2032
2033                  // set info for the closing
2034                  span_normal_type = Some("autolink".to_string());
2035                  span_closer = vec!['>', '>'];
2036                  span_normal = true;
2037                }
2038                ' ' | '\t' | '/' | '>' => {
2039                  // not a html tag
2040                  span_active = false;
2041                  span_html = false;
2042                  span_text = true;
2043                }
2044                _ => {}
2045              }
2046            }
2047
2048            if span_active && character_last_escape {
2049              // reset teh flags
2050              span_active = false;
2051              span_html = false;
2052              span_text = true;
2053              span_normal_type = None;
2054              span_closer = vec![];
2055              span_normal = false;
2056
2057              characters.remove(index - 1);
2058
2059              // since everything moved to the left so staying still is teh same as the normal +1 for the index
2060              // the -1 here will cancel out the +1 later
2061              index -= 1;
2062            }
2063          }
2064          '[' => {
2065            if character_last_escape {
2066              // remove the backslash
2067              characters.remove(index - 1);
2068
2069              // since everything moved to the left so staying still is teh same as the normal +1 for the index
2070              // the -1 here will cancel out the +1 later
2071              index -= 1;
2072            } else if let Some((node, offset)) = self.inline_spans_links(&characters[index..], "a".to_string()) {
2073              // set teh stuff before to be a text
2074              let text: String = characters[span_start..index].iter().collect();
2075              node_data.push(NodeData {
2076                tag: "text".to_string(),
2077                misc: None,
2078                text: Some(text),
2079                contents: vec![],
2080              });
2081
2082              // add the data to teh array
2083              node_data.push(node);
2084              index += offset;
2085
2086              // set the enw span start
2087              span_start = index + 1;
2088            }
2089          }
2090          '!' => {
2091            if let Some(next) = character_next {
2092              if next == '[' {
2093                if character_last_escape {
2094                  // remove the backslash
2095                  characters.remove(index - 1);
2096
2097                  // since everything moved to the left so staying still is teh same as the normal +1 for the index
2098                  // the -1 here will cancel out the +1 later
2099                  index -= 1;
2100                } else {
2101                  // images require a +1 to the offsets to take into account teh !
2102                  if let Some((node, offset)) = self.inline_spans_links(&characters[(index + 1)..], "img".to_string()) {
2103                    // set teh stuff before to be a text
2104                    let text: String = characters[span_start..index].iter().collect();
2105                    node_data.push(NodeData {
2106                      tag: "text".to_string(),
2107                      misc: None,
2108                      text: Some(text),
2109                      contents: vec![],
2110                    });
2111
2112                    // add the data to teh array
2113                    node_data.push(node);
2114                    index += offset + 1;
2115
2116                    // set the new span start
2117                    span_start = index + 2;
2118                  }
2119                }
2120              }
2121            }
2122          }
2123
2124          _ => {
2125            // do nothing
2126          }
2127        }
2128
2129        if span_active {
2130          // tidy up teh last text span here
2131          // bundle up everything between span_start and index into a string
2132          let text: String = characters[span_start..index].iter().collect();
2133          node_data.push(NodeData {
2134            tag: "text".to_string(),
2135            misc: None,
2136            text: Some(text),
2137            contents: vec![],
2138          });
2139
2140          span_start = index;
2141
2142          // skip to the next character if html, two if its
2143          if span_normal {
2144            index += 2;
2145          } else {
2146            index += 1;
2147          }
2148
2149          continue;
2150        }
2151      }
2152
2153      // this captures the content of the spans
2154      if span_active {
2155        if span_normal {
2156          // this aught to be pretty easy, just find the tail end of the span, bundle it up into NodeData
2157
2158          if span_closer[0] == character {
2159            if character_last_escape {
2160              characters.remove(index - 1);
2161
2162              // since everything moved to the left so staying still is teh same as the normal +1 for the index
2163              // the -1 here will cancel out the +1 later
2164              index -= 1;
2165            } else if let Some(next) = character_next {
2166              if span_closer[1] == next {
2167                // mark it as finished
2168                span_finished = true;
2169              }
2170            }
2171          }
2172        }
2173
2174        if span_html {
2175          // of the tag isn't complete then we need to find it
2176          if !html_tag_complete {
2177            match character {
2178              ' ' | '\t' => {
2179                html_tag_complete = true;
2180              }
2181              '/' | '>' => {
2182                // check last character, if it was a backslash then remove the backslash and add the current character
2183                if characters[index - 1] == '\\' {
2184                  // remove teh slash
2185                  // add the bracket
2186                  html_tag_vec.pop();
2187                  html_tag_vec.push(character);
2188                } else {
2189                  html_tag_complete = true;
2190                }
2191              }
2192              _ => {
2193                // as long as there are no spaces
2194                html_tag_vec.push(character);
2195              }
2196            }
2197          }
2198          if html_tag_complete {
2199            if span_closer.is_empty() {
2200              // sets teh
2201              let tag: String = html_tag_vec.iter().collect();
2202
2203              if self.html_void.contains(&tag) {
2204                span_closer = vec!['/', '>'];
2205              } else {
2206                span_closer = vec!['<', '/'];
2207                span_closer.extend(&html_tag_vec);
2208                span_closer.push('>');
2209              }
2210              // reverse it so it can be used easier down below
2211              span_closer.reverse();
2212            }
2213            // using tag_closing find the end of the html span
2214            // check if this is the
2215
2216            // starts off true
2217            let mut matches = true;
2218            for (position, closing_char) in span_closer.iter().enumerate() {
2219              let index_new: isize = (index as isize) - (position as isize);
2220              if index_new < 0 {
2221                span_reset = true;
2222                matches = false;
2223                break;
2224              }
2225
2226              if &characters[index_new as usize] != closing_char {
2227                matches = false;
2228                break;
2229              }
2230            }
2231            if matches {
2232              span_finished = true;
2233            }
2234          }
2235        }
2236      }
2237
2238      if character_next.is_none() && !span_finished {
2239        if span_text {
2240          span_finished = true;
2241        } else {
2242          span_reset = true;
2243        }
2244      }
2245
2246      if span_reset {
2247        // if the first character is < then replace it with &lt;
2248        if characters[span_start] == '<' {
2249          let replacement_char = ['&', 'l', 't', ';'];
2250          characters.splice(span_start..=span_start, replacement_char.iter().cloned());
2251        }
2252
2253        span_closer = vec![];
2254
2255        span_active = false;
2256        span_finished = false;
2257        span_reset = false; // reset this flag
2258
2259        span_normal = false;
2260        span_html = false;
2261        span_text = true;
2262
2263        span_normal_type = None;
2264
2265        html_tag_vec = vec![];
2266        html_tag_complete = false;
2267
2268        // reset back to the start of the span
2269        index = span_start;
2270        // skip to next character so it wont re-analyse the same set of characters
2271        index += 1;
2272
2273        continue;
2274      }
2275
2276      if span_finished {
2277        if span_text {
2278          let text: String = characters[span_start..=index].iter().collect();
2279          node_data.push(NodeData {
2280            tag: "text".to_string(),
2281            misc: None,
2282            text: Some(text),
2283            contents: vec![],
2284          });
2285
2286          span_start = index;
2287
2288          // skip to the next character if html, two if its
2289          if span_normal {
2290            index += 1;
2291          } else {
2292            index += 0;
2293          }
2294        }
2295
2296        if span_normal {
2297          // the offset of 2 is to exclude teh delimiter
2298          let text_raw: String = characters[(span_start + 2)..=(index - 1)].iter().collect();
2299
2300          let span_type = if let Some(span) = span_normal_type { span } else { "text".to_string() };
2301
2302          let (text, contents) = match span_type.as_str() {
2303            "text" => (Some(text_raw), vec![]),
2304            "code" => (Some(text_raw), vec![]),
2305            "autolink" => (Some(text_raw), vec![]),
2306            _ => (None, self.inline_spans(text_raw)),
2307          };
2308
2309          node_data.push(NodeData {
2310            tag: span_type,
2311            misc: None,
2312            text,
2313            contents,
2314          });
2315
2316          // skip to after the current delimiter
2317          span_start = index + 2;
2318
2319          // skip to the next character if html, two if its
2320
2321          index += 1;
2322        }
2323
2324        if span_html {
2325          let text: String = characters[span_start..=index].iter().collect();
2326
2327          node_data.push(NodeData {
2328            tag: "html".to_string(),
2329            misc: None,
2330            text: Some(text),
2331            contents: vec![],
2332          });
2333
2334          span_start = index + 1;
2335        }
2336
2337        span_closer = vec![];
2338
2339        span_active = false;
2340        span_finished = false;
2341
2342        span_normal = false;
2343        span_html = false;
2344        span_text = true;
2345
2346        span_normal_type = None;
2347
2348        html_tag_vec = vec![];
2349        html_tag_complete = false;
2350      }
2351
2352      index += 1;
2353    }
2354
2355    // catch anything at the end
2356    if characters.is_empty() || span_start < (characters.len() - 1) {
2357      let text: String = characters[span_start..].iter().collect();
2358      node_data.push(NodeData {
2359        tag: "text".to_string(),
2360        misc: None,
2361        text: Some(text),
2362        contents: vec![],
2363      });
2364    }
2365
2366    node_data
2367  }
2368
2369  fn inline_spans_links(&self, characters: &[char], tag: String) -> Option<(NodeData, usize)> {
2370    let references = self.references.clone();
2371
2372    // index starts at 1 as first character is always the opener [
2373    let mut index = 1;
2374
2375    let mut block_first = vec![];
2376
2377    let mut link_type = None;
2378    loop {
2379      if index >= characters.len() {
2380        break;
2381      }
2382
2383      let character = characters[index];
2384      let character_next = if (index + 1) < characters.len() { Some(characters[index + 1]) } else { None };
2385      let character_last_escape = if (index as isize - 1) > 0 { characters[index - 1] == '\\' } else { false };
2386
2387      match character {
2388        ']' => {
2389          if character_last_escape {
2390            block_first.pop();
2391            block_first.push(character)
2392          } else {
2393            // find the type of link
2394            if let Some(next) = character_next {
2395              match next {
2396                '(' => {
2397                  link_type = Some("classic".to_string());
2398                  // going to need to take a look at the contents
2399                  index += 2;
2400                }
2401                '[' => {
2402                  link_type = Some("reference_named".to_string());
2403                  index += 2;
2404                }
2405                _ => {
2406                  // check if the link_text is the same as the reference links in self.references
2407                  link_type = Some("reference_anon".to_string());
2408                }
2409              }
2410            } else {
2411              // set up to test if its teh anon type
2412              link_type = Some("reference_anon".to_string());
2413            }
2414
2415            // regardless break it here
2416            break;
2417          }
2418        }
2419
2420        _ => block_first.push(character),
2421      }
2422
2423      index += 1;
2424    }
2425
2426    // deal with teh anon references
2427    if let Some(type_) = &link_type {
2428      // only concerned with teh anon_test one this time
2429      if let "reference_anon" = type_.as_str() {
2430        let reference: String = block_first.iter().collect();
2431
2432        return match references.get(&reference) {
2433          Some(link_data) => {
2434            let url = link_data.url.clone();
2435
2436            let contents = if let Some(title) = &link_data.title {
2437              // if it has a title then it will override the reference for teh contents
2438              self.inline_spans(title.clone())
2439            } else {
2440              // reference us used as the contents if its an anon one
2441              self.inline_spans(reference)
2442            };
2443
2444            let node = NodeData {
2445              tag,
2446              misc: link_data.title.clone(),
2447              // text here is used as the url
2448              text: Some(url),
2449              contents,
2450            };
2451
2452            Some((node, index))
2453          }
2454          None => {
2455            // not a link
2456            None
2457          }
2458        };
2459      }
2460    }
2461
2462    let mut block_second = vec![];
2463    let mut angle_brackets = false;
2464    loop {
2465      if index >= characters.len() {
2466        break;
2467      }
2468
2469      let character = characters[index];
2470      let character_last_escape = if (index as isize - 1) > 0 { characters[index - 1] == '\\' } else { false };
2471
2472      // do stuff here
2473
2474      if let Some(type_) = &link_type {
2475        match type_.as_str() {
2476          "classic" => {
2477            match character {
2478              '<' => {
2479                if block_second.is_empty() {
2480                  angle_brackets = true;
2481                }
2482
2483                // add it regardless, can be easily removed later
2484                block_second.push(character)
2485              }
2486              ' ' | '\t' => {
2487                if angle_brackets {
2488                  block_second.push(character)
2489                } else if block_second.is_empty() {
2490                  // do nothing,
2491                } else {
2492                  // next loop will start on the next character
2493                  index += 1;
2494                  // its teh end of the url
2495                  break;
2496                }
2497              }
2498              ')' => {
2499                if angle_brackets {
2500                  block_second.push(character)
2501                } else if character_last_escape {
2502                  block_second.pop();
2503                  block_second.push(character);
2504                } else {
2505                  // this marks the classic link as finished
2506                  // so make it and finish early
2507
2508                  let text_raw: String = block_first.iter().collect();
2509                  let contents = self.inline_spans(text_raw);
2510                  let url: String = block_second.iter().collect();
2511                  let node = NodeData {
2512                    tag,
2513                    misc: None,
2514                    text: Some(url),
2515                    contents,
2516                  };
2517
2518                  return Some((node, index));
2519                }
2520              }
2521              '\n' => {
2522                // all inline links must be on the one line, not split over multiple
2523                // saves a lot of complexity
2524                return None;
2525              }
2526              '>' => {
2527                if angle_brackets {
2528                  // marks teh end of the span
2529                  // however if last char is a backslash its ignored
2530                  if character_last_escape {
2531                    block_second.pop();
2532                    block_second.push(character);
2533                  } else {
2534                    // remove the first char which is a <
2535                    block_second.remove(0);
2536                    break;
2537                  }
2538                } else {
2539                  block_second.push(character)
2540                }
2541              }
2542              _ => block_second.push(character),
2543            }
2544          }
2545          "reference_named" => match character {
2546            ']' => {
2547              if character_last_escape {
2548                block_second.pop();
2549                block_second.push(character);
2550              } else {
2551                break;
2552              }
2553            }
2554            _ => block_second.push(character),
2555          },
2556          _ => {
2557            // only classic and reference_named should show up
2558          }
2559        }
2560      }
2561
2562      index += 1;
2563    }
2564
2565    // tidy up the references
2566    if let Some(type_) = &link_type {
2567      // not for classic
2568      if let "reference_named" = type_.as_str() {
2569        let reference: String = block_second.iter().collect();
2570
2571        return match references.get(&reference) {
2572          Some(link_data) => {
2573            let text_raw: String = block_first.iter().collect();
2574            let contents = self.inline_spans(text_raw);
2575
2576            let node = NodeData {
2577              tag,
2578              misc: link_data.title.clone(),
2579              // text here is used as the url
2580              text: Some(link_data.url.clone()),
2581              contents,
2582            };
2583
2584            Some((node, index))
2585          }
2586          None => {
2587            // not a link
2588            None
2589          }
2590        };
2591      }
2592    }
2593
2594    //
2595    /*
2596    now just left with classic
2597
2598    // these are the three general patterns to find
2599    [](<>)
2600    [](/url )
2601    [](/url "title")
2602     */
2603
2604    // this handles the title
2605    let mut block_third = vec![];
2606    let mut delimiter = (' ', false, false);
2607    loop {
2608      if index >= characters.len() {
2609        break;
2610      }
2611
2612      let character = characters[index];
2613      let character_last_escape = if (index as isize - 1) > 0 { characters[index - 1] == '\\' } else { false };
2614
2615      match character {
2616        // manage delimiter
2617        '\'' | '"' => {
2618          if !delimiter.1 {
2619            // set the delimiter
2620            delimiter = (character, true, false);
2621          } else if delimiter.0 == character {
2622            // check if last character was an escape
2623            if character_last_escape {
2624              block_third.pop();
2625              block_third.push(character);
2626            } else {
2627              // mark it closed
2628              delimiter = (character, true, true);
2629            }
2630          } else {
2631            block_third.push(character);
2632          }
2633        }
2634
2635        //
2636        ')' => {
2637          if delimiter.1 && !delimiter.2 {
2638            // if delimiter is open then add it to teh array
2639            block_third.push(character);
2640          } else {
2641            // else check if its escaped
2642            if character_last_escape {
2643              block_third.pop();
2644              block_third.push(character);
2645            } else {
2646              // this marks the classic link as finished
2647              // so make it and finish early
2648
2649              let text_raw: String = block_first.iter().collect();
2650              let contents = self.inline_spans(text_raw);
2651
2652              let url: String = block_second.iter().collect();
2653
2654              let title = if block_third.is_empty() {
2655                None
2656              } else {
2657                let title_tmp: String = block_third.iter().collect();
2658                Some(title_tmp)
2659              };
2660
2661              let node = NodeData {
2662                tag,
2663                misc: title,
2664                text: Some(url),
2665                contents,
2666              };
2667
2668              return Some((node, index));
2669            }
2670          }
2671        }
2672
2673        ' ' | '\t' => {
2674          if delimiter.1 && !delimiter.2 {
2675            // if delimiter is open then add it to teh array
2676            block_third.push(character);
2677          } else {
2678            // do nothing
2679          }
2680        }
2681        _ => {
2682          block_third.push(character);
2683        }
2684      }
2685
2686      index += 1;
2687    }
2688
2689    // if its gone to teh end without being closed out then its invalid
2690
2691    None
2692  }
2693
2694  fn inline_merge(nodes: Vec<NodeData>) -> String {
2695    let mut result: Vec<String> = vec![];
2696
2697    for node in nodes {
2698      let tag = node.tag.as_str();
2699      let (opening, content, closing) = match tag {
2700        // html  and text just gets passed straight through
2701        "html" | "text" => {
2702          if let Some(text) = node.text {
2703            result.push(text);
2704          }
2705          (None, None, None)
2706        }
2707        // treat this basically the same as html above
2708        "autolink" => {
2709          if let Some(text) = node.text {
2710            let (cleaned, mail, tel) = if text.starts_with("mailto:") {
2711              (text.replacen("mailto:", "", 1), true, false)
2712            } else if text.starts_with("MAILTO:") {
2713              (text.replacen("MAILTO:", "", 1), true, false)
2714            } else if text.starts_with("tel:") {
2715              (text.replacen("tel:", "", 1), false, true)
2716            } else if text.starts_with("TEL:") {
2717              (text.replacen("TEL:", "", 1), false, true)
2718            } else {
2719              (text, false, false)
2720            };
2721
2722            // check if it contains @
2723            let formatted = if cleaned.contains('@') || mail {
2724              format!("<a target='_blank' rel='noopener noreferrer' href='mailto:{}'>{}</a>", &cleaned, &cleaned)
2725            } else if tel {
2726              format!("<a target='_blank' rel='noopener noreferrer' href='tel:{}'>{}</a>", &cleaned, &cleaned)
2727            } else {
2728              format!("<a target='_blank' rel='noopener noreferrer' href='{}'>{}</a>", &cleaned, &cleaned)
2729            };
2730
2731            result.push(formatted);
2732          }
2733          (None, None, None)
2734        }
2735
2736        // set tags and no recursion
2737        "code" => {
2738          if let Some(text) = node.text {
2739            // no attributes or anything recursive
2740            let open = format!("<{}>", tag);
2741            let close = format!("</{}>", tag);
2742
2743            let cleaned = text.replace('<', "&lt;").replace('>', "&gt;");
2744
2745            (Some(open), Some(cleaned), Some(close))
2746          } else {
2747            (None, None, None)
2748          }
2749        }
2750
2751        // recursive
2752        "em" | "strong" | "u" | "s" => {
2753          let processed = Converter::inline_merge(node.contents);
2754          let open = format!("<{}>", tag);
2755          let close = format!("</{}>", tag);
2756          (Some(open), Some(processed), Some(close))
2757        }
2758
2759        // spoiler is a span with a class of class="md-spoiler" on it as there is no html element for spoilers and must be done using css
2760        "spoiler" => {
2761          let processed = Converter::inline_merge(node.contents);
2762          let open = "<span class='md-spoiler'>".to_string();
2763          let close = "</span>".to_string();
2764          (Some(open), Some(processed), Some(close))
2765        }
2766
2767        "a" => {
2768          let url = node.text.unwrap_or_default();
2769          let open = if let Some(title) = node.misc {
2770            format!("<a target='_blank' rel='noopener noreferrer' href='{}' title='{}'>", url, title)
2771          } else {
2772            format!("<a target='_blank' rel='noopener noreferrer' href='{}'>", url)
2773          };
2774
2775          let processed = Converter::inline_merge(node.contents);
2776          let close = "</a>".to_string();
2777
2778          (Some(open), Some(processed), Some(close))
2779        }
2780
2781        "img" => {
2782          let url = node.text.unwrap_or_default();
2783          let alt = Converter::inline_merge(node.contents);
2784          let open = if let Some(title) = node.misc {
2785            format!("<img src='{}' alt='{}' title='{}' />", url, alt, title)
2786          } else {
2787            format!("<img src='{}' alt='{}' />", url, alt)
2788          };
2789
2790          (Some(open), None, None)
2791        }
2792
2793        _ => (None, None, None),
2794      };
2795
2796      if let Some(data) = opening {
2797        result.push(data)
2798      }
2799      if let Some(data) = content {
2800        result.push(data)
2801      }
2802      if let Some(data) = closing {
2803        result.push(data)
2804      }
2805    }
2806
2807    result.join("")
2808  }
2809
2810  fn inline_indention(&self, input: String, depth: usize) -> String {
2811    let mut result: Vec<String> = vec![];
2812
2813    let lines = input.split('\n').collect::<Vec<_>>();
2814    for line in lines {
2815      let indented = format!("{:indent$}{}", "", line, indent = (depth * self.indentation));
2816      result.push(indented);
2817    }
2818
2819    result.join("\n")
2820  }
2821}
2822//
2823
2824fn compare_lists(list_type: LineType, li_type: LineType) -> bool {
2825  if let LineType::OL(_, _) = list_type {
2826    if let LineType::OL(_, _) = li_type {
2827      return true;
2828    }
2829  }
2830
2831  if let LineType::UL(_, _) = list_type {
2832    if let LineType::UL(_, _) = li_type {
2833      return true;
2834    }
2835  }
2836
2837  false
2838}