Skip to main content

strop_syntax/
spans.rs

1//! Resolve overlapping captures into disjoint byte intervals. Injected languages
2//! own their foreground while outer Markdown emphasis remains compositional.
3use super::{Class, Span};
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
6pub struct Emphasis {
7    pub bold: bool,
8    pub italic: bool,
9    pub underline: bool,
10    pub strikethrough: bool,
11}
12impl Emphasis {
13    pub(crate) fn from_capture(name: &str) -> Self {
14        Self {
15            bold: name.starts_with("markup.heading") || name == "markup.bold",
16            italic: name.starts_with("comment") || name == "markup.italic",
17            underline: name.starts_with("markup.link.url"),
18            strikethrough: name == "markup.strikethrough",
19        }
20    }
21    fn merge(&mut self, other: Self) {
22        self.bold |= other.bold;
23        self.italic |= other.italic;
24        self.underline |= other.underline;
25        self.strikethrough |= other.strikethrough;
26    }
27}
28#[derive(Clone, Copy)]
29pub(crate) struct CaptureStyle {
30    pub class: Class,
31    pub emphasis: Emphasis,
32}
33pub(crate) struct LayeredSpan {
34    pub span: Span,
35    pub injected: bool,
36}
37
38pub(crate) fn flatten(captures: Vec<LayeredSpan>, first: usize, last: usize) -> Vec<Span> {
39    let mut events = Vec::with_capacity(captures.len().saturating_mul(2));
40    for (index, capture) in captures.iter().enumerate() {
41        let start = capture.span.start.max(first);
42        let end = capture.span.end.min(last);
43        if start < end {
44            events.push((start, true, index));
45            events.push((end, false, index));
46        }
47    }
48    events.sort_unstable();
49    let mut active = Vec::new();
50    let mut result: Vec<Span> = Vec::new();
51    let mut at = 0;
52    while at < events.len() {
53        let start = events[at].0;
54        while at < events.len() && events[at].0 == start {
55            let (_, entering, index) = events[at];
56            if entering {
57                active.push(index);
58            } else {
59                active.retain(|&value| value != index);
60            }
61            at += 1;
62        }
63        let Some(&(end, _, _)) = events.get(at) else {
64            break;
65        };
66        let Some(&winner) = active.iter().max_by_key(|&&index| {
67            let capture = &captures[index];
68            (
69                capture.injected,
70                usize::MAX - (capture.span.end - capture.span.start),
71                index,
72            )
73        }) else {
74            continue;
75        };
76        let mut emphasis = Emphasis::default();
77        for &index in &active {
78            emphasis.merge(captures[index].span.emphasis);
79        }
80        let class = captures[winner].span.class;
81        if let Some(previous) = result
82            .last_mut()
83            .filter(|span| span.end == start && span.class == class && span.emphasis == emphasis)
84        {
85            previous.end = end;
86        } else {
87            result.push(Span {
88                start,
89                end,
90                class,
91                emphasis,
92            });
93        }
94    }
95    result
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    #[test]
102    fn nested_emphasis_survives_an_injected_foreground() {
103        let captures = vec![
104            LayeredSpan {
105                span: Span {
106                    start: 0,
107                    end: 10,
108                    class: Class::Variable,
109                    emphasis: Emphasis {
110                        bold: true,
111                        ..Emphasis::default()
112                    },
113                },
114                injected: false,
115            },
116            LayeredSpan {
117                span: Span {
118                    start: 2,
119                    end: 8,
120                    class: Class::Variable,
121                    emphasis: Emphasis {
122                        italic: true,
123                        ..Emphasis::default()
124                    },
125                },
126                injected: false,
127            },
128            LayeredSpan {
129                span: Span {
130                    start: 3,
131                    end: 5,
132                    class: Class::Keyword,
133                    emphasis: Emphasis::default(),
134                },
135                injected: true,
136            },
137        ];
138        let result = flatten(captures, 0, 10);
139        let keyword = result.iter().find(|span| span.start == 3).unwrap();
140        assert_eq!(keyword.class, Class::Keyword);
141        assert!(keyword.emphasis.bold && keyword.emphasis.italic);
142        assert_eq!(result.last().unwrap().end, 10);
143        assert!(result
144            .windows(2)
145            .all(|spans| spans[0].end <= spans[1].start));
146    }
147}