Skip to main content

mos_parse/
block.rs

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    /// Emit `MOS0048` when a heading carries a `<label>` token that is not the
46    /// trailing element, so [`strip_trailing_label`] left it unrecognised and
47    /// it would otherwise be swallowed into the heading text. The attached
48    /// suggestion moves the label to the end of the line, where it registers as
49    /// a real declaration.
50    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        // Only a label with real content after it is misplaced; trailing
55        // whitespace alone is harmless and not the author's mistake.
56        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    /// Returns `Some(level)` if the current line is a well-formed
141    /// heading of `=`+ followed by ASCII whitespace.
142    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}