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