You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
5.5KB

  1. use tera::{Tera, Context as TeraContext};
  2. use front_matter::InsertAnchor;
  3. #[derive(Debug, PartialEq, Clone, Serialize)]
  4. pub struct Header {
  5. #[serde(skip_serializing)]
  6. pub level: i32,
  7. pub id: String,
  8. pub title: String,
  9. pub permalink: String,
  10. pub children: Vec<Header>,
  11. }
  12. impl Header {
  13. pub fn from_temp_header(tmp: &TempHeader, children: Vec<Header>) -> Header {
  14. Header {
  15. level: tmp.level,
  16. id: tmp.id.clone(),
  17. title: tmp.title.clone(),
  18. permalink: tmp.permalink.clone(),
  19. children,
  20. }
  21. }
  22. }
  23. /// Populated while receiving events from the markdown parser
  24. #[derive(Debug, PartialEq, Clone)]
  25. pub struct TempHeader {
  26. pub level: i32,
  27. pub id: String,
  28. pub permalink: String,
  29. pub title: String,
  30. }
  31. impl TempHeader {
  32. pub fn new(level: i32) -> TempHeader {
  33. TempHeader {
  34. level,
  35. id: String::new(),
  36. permalink: String::new(),
  37. title: String::new(),
  38. }
  39. }
  40. pub fn push(&mut self, val: &str) {
  41. self.title += val;
  42. }
  43. /// Transform all the information we have about this header into the HTML string for it
  44. pub fn to_string(&self, tera: &Tera, insert_anchor: InsertAnchor) -> String {
  45. let anchor_link = if insert_anchor != InsertAnchor::None {
  46. let mut c = TeraContext::new();
  47. c.add("id", &self.id);
  48. tera.render("anchor-link.html", &c).unwrap()
  49. } else {
  50. String::new()
  51. };
  52. match insert_anchor {
  53. InsertAnchor::None => format!("<h{lvl} id=\"{id}\">{t}</h{lvl}>\n", lvl = self.level, t = self.title, id = self.id),
  54. InsertAnchor::Left => format!("<h{lvl} id=\"{id}\">{a}{t}</h{lvl}>\n", lvl = self.level, a = anchor_link, t = self.title, id = self.id),
  55. InsertAnchor::Right => format!("<h{lvl} id=\"{id}\">{t}{a}</h{lvl}>\n", lvl = self.level, a = anchor_link, t = self.title, id = self.id),
  56. }
  57. }
  58. }
  59. impl Default for TempHeader {
  60. fn default() -> Self {
  61. TempHeader::new(0)
  62. }
  63. }
  64. /// Recursively finds children of a header
  65. fn find_children(parent_level: i32, start_at: usize, temp_headers: &[TempHeader]) -> (usize, Vec<Header>) {
  66. let mut headers = vec![];
  67. let mut start_at = start_at;
  68. // If we have children, we will need to skip some headers since they are already inserted
  69. let mut to_skip = 0;
  70. for h in &temp_headers[start_at..] {
  71. // stop when we encounter a title at the same level or higher
  72. // than the parent one. Here a lower integer is considered higher as we are talking about
  73. // HTML headers: h1, h2, h3, h4, h5 and h6
  74. if h.level <= parent_level {
  75. return (start_at, headers);
  76. }
  77. // Do we need to skip some headers?
  78. if to_skip > 0 {
  79. to_skip -= 1;
  80. continue;
  81. }
  82. let (end, children) = find_children(h.level, start_at + 1, temp_headers);
  83. headers.push(Header::from_temp_header(h, children));
  84. // we didn't find any children
  85. if end == start_at {
  86. start_at += 1;
  87. to_skip = 0;
  88. } else {
  89. // calculates how many we need to skip. Since the find_children start_at starts at 1,
  90. // we need to remove 1 to ensure correctness
  91. to_skip = end - start_at - 1;
  92. start_at = end;
  93. }
  94. // we don't want to index out of bounds
  95. if start_at + 1 > temp_headers.len() {
  96. return (start_at, headers);
  97. }
  98. }
  99. (start_at, headers)
  100. }
  101. /// Converts the flat temp headers into a nested set of headers
  102. /// representing the hierarchy
  103. pub fn make_table_of_contents(temp_headers: &[TempHeader]) -> Vec<Header> {
  104. let mut toc = vec![];
  105. let mut start_idx = 0;
  106. for (i, h) in temp_headers.iter().enumerate() {
  107. if i < start_idx {
  108. continue;
  109. }
  110. let (end_idx, children) = find_children(h.level, start_idx + 1, temp_headers);
  111. start_idx = end_idx;
  112. toc.push(Header::from_temp_header(h, children));
  113. }
  114. toc
  115. }
  116. #[cfg(test)]
  117. mod tests {
  118. use super::*;
  119. #[test]
  120. fn can_make_basic_toc() {
  121. let input = vec![
  122. TempHeader::new(1),
  123. TempHeader::new(1),
  124. TempHeader::new(1),
  125. ];
  126. let toc = make_table_of_contents(&input);
  127. assert_eq!(toc.len(), 3);
  128. }
  129. #[test]
  130. fn can_make_more_complex_toc() {
  131. let input = vec![
  132. TempHeader::new(1),
  133. TempHeader::new(2),
  134. TempHeader::new(2),
  135. TempHeader::new(3),
  136. TempHeader::new(2),
  137. TempHeader::new(1),
  138. TempHeader::new(2),
  139. TempHeader::new(3),
  140. TempHeader::new(3),
  141. ];
  142. let toc = make_table_of_contents(&input);
  143. assert_eq!(toc.len(), 2);
  144. assert_eq!(toc[0].children.len(), 3);
  145. assert_eq!(toc[1].children.len(), 1);
  146. assert_eq!(toc[0].children[1].children.len(), 1);
  147. assert_eq!(toc[1].children[0].children.len(), 2);
  148. }
  149. #[test]
  150. fn can_make_messy_toc() {
  151. let input = vec![
  152. TempHeader::new(3),
  153. TempHeader::new(2),
  154. TempHeader::new(2),
  155. TempHeader::new(3),
  156. TempHeader::new(2),
  157. TempHeader::new(1),
  158. TempHeader::new(4),
  159. ];
  160. let toc = make_table_of_contents(&input);
  161. assert_eq!(toc.len(), 5);
  162. assert_eq!(toc[2].children.len(), 1);
  163. assert_eq!(toc[4].children.len(), 1);
  164. }
  165. }