use std::ops::Range;
use crate::docs::Line;
use crate::docs::links::blank_inline_code;
pub(super) fn spans(lines: &[Line<'_>]) -> Vec<Vec<Range<usize>>> {
let mut spans = vec![Vec::new(); lines.len()];
let mut index = 0;
while index < lines.len() {
if let Some(close) = block_comment(lines, index, &mut spans) {
index = close;
}
index += 1;
}
let mut joined = Vec::new();
let mut start = 0;
while start < lines.len() {
if !is_prose(&lines[start]) {
start += 1;
continue;
}
let end = (start..lines.len())
.find(|i| !is_prose(&lines[*i]))
.unwrap_or(lines.len());
paragraph(&lines[start..end], &mut spans[start..end], &mut joined);
start = end;
}
spans
}
fn is_prose(line: &Line<'_>) -> bool {
!line.in_code && !line.text.trim().is_empty()
}
fn paragraph(lines: &[Line<'_>], spans: &mut [Vec<Range<usize>>], joined: &mut Vec<char>) {
joined.clear();
let mut extents = Vec::with_capacity(lines.len());
for line in lines {
let start = joined.len();
joined.extend(line.text.chars());
extents.push(start..joined.len());
joined.push('\n');
}
blank_inline_code(joined);
let mut i = 0;
while i < joined.len() {
if joined[i] == '<'
&& let Some(end) = match_comment(joined, i).or_else(|| match_tag(joined, i))
{
record(joined, &extents, i..end + 1, spans);
i = end;
}
i += 1;
}
}
fn record(
joined: &[char],
extents: &[Range<usize>],
found: Range<usize>,
spans: &mut [Vec<Range<usize>>],
) {
let first = extents.partition_point(|extent| extent.start <= found.start) - 1;
let covered = joined[found.clone()].iter().filter(|c| **c == '\n').count() + 1;
for (extent, spans) in extents[first..first + covered]
.iter()
.zip(&mut spans[first..first + covered])
{
let lo = found.start.saturating_sub(extent.start);
let hi = found.end.min(extent.end) - extent.start;
spans.push(lo..hi);
}
}
fn block_comment(
lines: &[Line<'_>],
open: usize,
spans: &mut [Vec<Range<usize>>],
) -> Option<usize> {
let line = &lines[open];
if line.in_code {
return None;
}
let indent = line.text.bytes().take_while(|b| *b == b' ').count();
if indent > 3 || !line.text[indent..].starts_with("<!--") {
return None;
}
let chars: Vec<char> = line.text.chars().collect();
if match_comment(&chars, indent).is_some() {
return None;
}
let (close, after) = (open..lines.len()).find_map(|index| {
let text = lines[index].text;
text.find("-->")
.map(|byte| (index, text[..byte].chars().count() + "-->".len()))
})?;
for index in open..close {
spans[index].push(0..width(&lines[index]));
}
spans[close].push(0..after);
Some(close)
}
fn width(line: &Line<'_>) -> usize {
line.text.chars().count()
}
fn match_comment(chars: &[char], start: usize) -> Option<usize> {
const OPEN: [char; 4] = ['<', '!', '-', '-'];
const CLOSE: [char; 3] = ['-', '-', '>'];
if !chars[start..].starts_with(&OPEN) {
return None;
}
(start + 2..chars.len())
.find(|i| chars[*i..].starts_with(&CLOSE))
.map(|i| i + CLOSE.len() - 1)
}
fn match_tag(chars: &[char], start: usize) -> Option<usize> {
debug_assert_eq!(chars[start], '<');
let closing = chars.get(start + 1) == Some(&'/');
let name = start + 1 + usize::from(closing);
if !chars.get(name)?.is_ascii_alphabetic() {
return None;
}
let mut i = skip_while(chars, name, |c| c.is_ascii_alphanumeric() || c == '-');
if closing {
i = skip_while(chars, i, is_tag_space);
return (chars.get(i) == Some(&'>')).then_some(i);
}
loop {
let attributes = skip_while(chars, i, is_tag_space);
let separated = attributes > i;
match *chars.get(attributes)? {
'>' => return Some(attributes),
'/' => {
let close = attributes + 1;
return (chars.get(close) == Some(&'>')).then_some(close);
}
c if separated && (c.is_ascii_alphabetic() || matches!(c, '_' | ':')) => {
i = attribute_end(chars, attributes)?;
}
_ => return None,
}
}
}
fn attribute_end(chars: &[char], start: usize) -> Option<usize> {
let name_end = skip_while(chars, start, |c| {
c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '-')
});
let equals = skip_while(chars, name_end, is_tag_space);
if chars.get(equals) != Some(&'=') {
return Some(name_end);
}
let value = skip_while(chars, equals + 1, is_tag_space);
match *chars.get(value)? {
quote @ ('"' | '\'') => (value + 1..chars.len())
.find(|i| chars[*i] == quote)
.map(|close| close + 1),
_ => {
let end = skip_while(chars, value, |c| {
!is_tag_space(c) && !matches!(c, '"' | '\'' | '=' | '<' | '>' | '`')
});
(end > value).then_some(end)
}
}
}
fn is_tag_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n')
}
fn skip_while(chars: &[char], from: usize, keep: impl Fn(char) -> bool) -> usize {
(from..chars.len())
.find(|i| !keep(chars[*i]))
.unwrap_or(chars.len())
}