1use mos_core::{DiagnosticAnnotation, Suggestion, codes};
2
3use crate::Item;
4use crate::parser::Parser;
5use crate::support::{locate_label, strip_leading_label, strip_trailing_label};
6
7impl Parser<'_> {
8 pub(crate) fn parse_heading(&mut self) {
9 let (line_start, content_end, line_end) = self.current_line_bounds();
10 let bytes = self.src.as_bytes();
11 let mut level: u8 = 0;
12 let mut i = line_start;
13 while i < content_end && bytes[i] == b'=' {
14 level = level.saturating_add(1);
15 i += 1;
16 }
17 if i >= content_end || !bytes[i].is_ascii_whitespace() {
18 self.parse_paragraph();
19 return;
20 }
21 while i < content_end && (bytes[i] == b' ' || bytes[i] == b'\t') {
22 i += 1;
23 }
24 let (text_end, parsed_label) = strip_trailing_label(self.src, i, content_end);
25 if parsed_label.is_none() {
26 self.flag_misplaced_heading_label(i, content_end);
27 }
28 let label_span = parsed_label
29 .as_ref()
30 .map(|label| self.span(label.start, label.end));
31 let label = parsed_label.map(|label| label.text);
32 let content = &self.src[i..text_end];
33 let inlines = self.parse_inlines(content, i);
34 let span = self.span(line_start, content_end);
35 self.items.push(Item::Heading {
36 level,
37 inlines,
38 label,
39 label_span,
40 span,
41 });
42 self.pos = line_end;
43 }
44
45 fn flag_misplaced_heading_label(&mut self, start: usize, content_end: usize) {
51 let Some((label, after)) = locate_label(self.src, start, content_end) else {
52 return;
53 };
54 let trailing = self.src[after..content_end].trim();
57 if trailing.is_empty() {
58 return;
59 }
60 let Some(label_open) = label.start.checked_sub(1).filter(|open| *open >= start) else {
61 return;
62 };
63 let before = self.src[start..label_open].trim();
64 let mut fixed = String::new();
65 for part in [before, trailing] {
66 if part.is_empty() {
67 continue;
68 }
69 if !fixed.is_empty() {
70 fixed.push(' ');
71 }
72 fixed.push_str(part);
73 }
74 if !fixed.is_empty() {
75 fixed.push(' ');
76 }
77 fixed.push('<');
78 fixed.push_str(&label.text);
79 fixed.push('>');
80 let message = format!(
81 "heading label `<{}>` must be the last element on the line",
82 label.text
83 );
84 let diagnostic = self
85 .warn(&codes::MOS0048, &message, start, content_end)
86 .with_suggestion(Suggestion::new(self.span(start, content_end), fixed))
87 .with_suggestion(Suggestion::new(self.span(label_open, label.start), "\\<"))
88 .with_annotation(DiagnosticAnnotation::Hint(format!(
89 "if `<{label}>` is literal text (e.g. an HTML tag), escape the `<` as `\\<{label}>`",
90 label = label.text
91 )));
92 self.diagnostics.push(diagnostic);
93 }
94
95 pub(crate) fn parse_paragraph(&mut self) {
96 let bytes = self.src.as_bytes();
97 let para_start = self.pos;
98 let mut para_end = self.pos;
99 let mut text_start: Option<usize> = None;
100 loop {
101 if self.pos >= bytes.len() || self.at_blank_line() {
102 break;
103 }
104 if self.starts_with("=") && self.heading_level_of_current_line().is_some() {
105 break;
106 }
107 if self.at_directive_keyword().is_some() || self.at_list_marker() {
108 break;
109 }
110 let (line_start, content_end, line_end) = self.current_line_bounds();
111 if text_start.is_none() {
112 text_start = Some(line_start);
113 }
114 para_end = content_end;
115 self.pos = line_end;
116 }
117 if let Some(start) = text_start {
118 let (body_start, parsed_label) = strip_leading_label(self.src, start, para_end);
119 let label_span = parsed_label
120 .as_ref()
121 .map(|label| self.span(label.start, label.end));
122 let label = parsed_label.map(|label| label.text);
123 let slice = &self.src[body_start..para_end];
124 let mut inlines = self.parse_inlines(slice, body_start);
125 for inline in &mut inlines {
126 if inline.text.contains("\r\n") {
127 inline.text = inline.text.replace("\r\n", "\n");
128 }
129 }
130 let span = self.span(para_start, para_end);
131 self.items.push(Item::Paragraph {
132 inlines,
133 label,
134 label_span,
135 span,
136 });
137 }
138 }
139
140 fn heading_level_of_current_line(&self) -> Option<u8> {
143 let (start, content_end, _) = self.current_line_bounds();
144 let bytes = self.src.as_bytes();
145 let mut i = start;
146 let mut level: u8 = 0;
147 while i < content_end && bytes[i] == b'=' {
148 level = level.saturating_add(1);
149 i += 1;
150 }
151 if level == 0 {
152 return None;
153 }
154 if i < content_end && bytes[i].is_ascii_whitespace() {
155 Some(level)
156 } else {
157 None
158 }
159 }
160}