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.

193 lines
5.5KB

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