use crate::cursor::LineCursor;
use crate::cursor::indent_columns;
use crate::cursor::indent_len;
use crate::syntax::Parsed;
use crate::syntax::SyntaxElement;
use crate::syntax::SyntaxKind;
use crate::syntax::SyntaxNode;
use crate::syntax::SyntaxToken;
use crate::text::TextRange;
pub(crate) fn convert_directives(parsed: &Parsed) -> Vec<crate::model::Directive> {
parsed
.root()
.nodes(SyntaxKind::DIRECTIVE)
.filter_map(|node| crate::parse::unified::Directive::cast(parsed, node))
.map(|d| crate::model::Directive {
name: d.name().text().to_owned(),
argument: d.argument().map(|t| t.text().to_owned()),
description: d.description().map(|t| convert_multiline_with_indentation(t.text())),
})
.collect()
}
pub(crate) fn directive_is_deprecated(parsed: &Parsed, node: &SyntaxNode) -> bool {
node.find_token(SyntaxKind::DIRECTIVE_NAME)
.is_some_and(|t| t.text(parsed.source()) == "deprecated")
}
pub(crate) fn build_text_block(kind: SyntaxKind, range: TextRange, source: &str) -> SyntaxNode {
let start = usize::from(range.start());
let end = usize::from(range.end());
let mut children = Vec::new();
let mut line_start = start;
for line in source[start..end].split_inclusive('\n') {
let content = line.trim();
if !content.is_empty() {
let lead = line.len() - line.trim_start().len();
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::TEXT_LINE,
TextRange::from_offset_len(line_start + lead, content.len()),
)));
}
line_start += line.len();
}
SyntaxNode::new(kind, range, children)
}
pub(crate) fn text_block_single(kind: SyntaxKind, range: TextRange) -> SyntaxNode {
let children = vec![SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TEXT_LINE, range))];
SyntaxNode::new(kind, range, children)
}
pub(crate) fn missing_text_block(kind: SyntaxKind, pos: crate::text::TextSize) -> SyntaxNode {
SyntaxNode::new(kind, TextRange::new(pos, pos), Vec::new())
}
pub(crate) fn build_paragraph(cursor: &LineCursor, first: usize, last: usize) -> SyntaxNode {
let first_line = cursor.line_text(first);
let first_col = indent_len(first_line);
let last_line = cursor.line_text(last);
let last_col = indent_len(last_line) + last_line.trim().len();
let range = cursor.make_range(first, first_col, last, last_col);
build_text_block(SyntaxKind::PARAGRAPH, range, cursor.source())
}
pub(crate) fn extend_text_block(block: &mut SyntaxNode, cont: TextRange) {
block.push_child(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TEXT_LINE, cont)));
block.extend_range_to(cont.end());
}
pub(crate) fn collect_description(cursor: &mut LineCursor, entry_indent_cols: usize) -> Option<TextRange> {
let mut first_content_line: Option<usize> = None;
let mut last_content_line = cursor.line;
while !cursor.is_eof() {
let line = cursor.current_line_text();
if !line.trim().is_empty() && indent_columns(line) <= entry_indent_cols {
break;
}
if !line.trim().is_empty() {
if first_content_line.is_none() {
first_content_line = Some(cursor.line);
}
last_content_line = cursor.line;
}
cursor.advance();
}
first_content_line.map(|first| {
let first_line = cursor.line_text(first);
let first_col = indent_len(first_line);
let last_line = cursor.line_text(last_content_line);
let last_col = indent_len(last_line) + last_line.trim().len();
cursor.make_range(first, first_col, last_content_line, last_col)
})
}
fn directive_name_len(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
if bytes.first().is_none_or(|b| !b.is_ascii_alphabetic()) {
return None;
}
Some(
bytes
.iter()
.take_while(|b| b.is_ascii_alphanumeric() || **b == b'-' || **b == b'_')
.count(),
)
}
pub(crate) fn try_parse_directive(cursor: &mut LineCursor) -> Option<SyntaxNode> {
let line = cursor.current_line_text();
let trimmed = line.trim();
let after_marker = trimmed.strip_prefix("..")?;
let name_ws = after_marker.len() - after_marker.trim_start().len();
if name_ws == 0 {
return None;
}
let after_ws = &after_marker[name_ws..];
let name_len = directive_name_len(after_ws)?;
let after_colons = after_ws[name_len..].strip_prefix("::")?;
let arg_ws = after_colons.len() - after_colons.trim_start().len();
let arg_str = after_colons.trim();
let col = cursor.current_indent();
let name_col = col + 2 + name_ws;
let colon_col = name_col + name_len;
let arg_col = colon_col + 2 + arg_ws;
let mut children: Vec<SyntaxElement> = Vec::new();
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DIRECTIVE_MARKER,
cursor.make_line_range(cursor.line, col, 2),
)));
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DIRECTIVE_NAME,
cursor.make_line_range(cursor.line, name_col, name_len),
)));
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DOUBLE_COLON,
cursor.make_line_range(cursor.line, colon_col, 2),
)));
if !arg_str.is_empty() {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::ARGUMENT,
cursor.make_line_range(cursor.line, arg_col, arg_str.len()),
)));
}
let start_line = cursor.line;
cursor.advance();
let desc_range = collect_description(cursor, indent_columns(line));
if let Some(desc) = desc_range {
children.push(SyntaxElement::Node(build_text_block(
SyntaxKind::DESCRIPTION,
desc,
cursor.source(),
)));
}
let (end_line, end_col) = match desc_range {
None => (start_line, col + trimmed.len()),
Some(d) => cursor.offset_to_line_col(d.end().raw() as usize),
};
let range = cursor.make_range(start_line, col, end_line, end_col);
Some(SyntaxNode::new(SyntaxKind::DIRECTIVE, range, children))
}
pub(crate) fn find_entry_colon(text: &str) -> Option<usize> {
let mut depth: u32 = 0;
for (i, b) in text.bytes().enumerate() {
match b {
b'(' | b'[' | b'{' | b'<' => depth += 1,
b')' | b']' | b'}' | b'>' => depth = depth.saturating_sub(1),
b':' if depth == 0 => return Some(i),
_ => {}
}
}
None
}
pub(crate) fn find_term_colon(text: &str) -> Option<usize> {
if text.trim_start().starts_with(':') {
return None;
}
let bytes = text.as_bytes();
let mut depth: u32 = 0;
let mut top_level_ws = false;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' | b'[' | b'{' | b'<' => depth += 1,
b')' | b']' | b'}' | b'>' => depth = depth.saturating_sub(1),
b':' if depth == 0 => {
let ws_before = i > 0 && bytes[i - 1].is_ascii_whitespace();
return if ws_before || !top_level_ws { Some(i) } else { None };
}
_ if depth == 0 && b.is_ascii_whitespace() => top_level_ws = true,
_ => {}
}
}
None
}
pub(crate) fn split_comma_parts(text: &str) -> Vec<(usize, &str)> {
let mut parts = Vec::new();
let mut depth: u32 = 0;
let mut start = 0;
for (i, b) in text.bytes().enumerate() {
match b {
b'(' | b'[' | b'{' | b'<' => depth += 1,
b')' | b']' | b'}' | b'>' => depth = depth.saturating_sub(1),
b',' if depth == 0 => {
parts.push((start, &text[start..i]));
start = i + 1;
}
_ => {}
}
}
parts.push((start, &text[start..]));
parts
}
pub(crate) fn separator_comma_offsets(text: &str, clean_len: usize) -> Vec<usize> {
split_comma_parts(text)
.iter()
.skip(1)
.map(|(seg_offset, _)| seg_offset - 1)
.filter(|&pos| pos >= clean_len)
.collect()
}
pub(crate) fn find_matching_close(s: &str, open_pos: usize) -> Option<usize> {
let bytes = s.as_bytes();
let open = bytes[open_pos];
let close = match open {
b'(' => b')',
b'[' => b']',
b'{' => b'}',
b'<' => b'>',
_ => return None,
};
let mut depth: u32 = 1;
for (i, &b) in bytes[open_pos + 1..].iter().enumerate() {
if b == open {
depth += 1;
} else if b == close {
depth -= 1;
if depth == 0 {
return Some(open_pos + 1 + i);
}
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MarkerSegment {
Optional {
offset: usize,
},
Default {
keyword: usize,
separator: Option<usize>,
value: Option<(usize, usize)>,
},
}
pub(crate) struct TypeMarkers<'a> {
pub clean_type: &'a str,
pub commas: Vec<usize>,
pub markers: Vec<MarkerSegment>,
}
pub(crate) fn scan_type_markers(type_content: &str) -> TypeMarkers<'_> {
let mut markers = Vec::new();
let mut type_end = 0;
for (seg_offset, seg_raw) in split_comma_parts(type_content) {
let seg = seg_raw.trim();
if seg.is_empty() {
continue;
}
let seg_off = seg_offset + (seg_raw.len() - seg_raw.trim_start().len());
if seg == "optional" {
markers.push(MarkerSegment::Optional { offset: seg_off });
} else if let Some(after_kw) = seg
.strip_prefix("default")
.filter(|rest| rest.is_empty() || rest.starts_with([' ', '\t', '=', ':']))
{
let rest = after_kw.trim_start();
let rest_off = seg_off + "default".len() + (after_kw.len() - rest.len());
let (separator, value) = if let Some(val) = rest.strip_prefix(['=', ':']) {
let val_trimmed = val.trim_start();
let value = if val_trimmed.is_empty() {
(rest_off + 1, 0)
} else {
(rest_off + 1 + (val.len() - val_trimmed.len()), val_trimmed.len())
};
(Some(rest_off), Some(value))
} else if !rest.is_empty() {
(None, Some((rest_off, rest.len())))
} else {
(None, None)
};
markers.push(MarkerSegment::Default {
keyword: seg_off,
separator,
value,
});
} else {
markers.clear();
type_end = seg_offset + seg_raw.trim_end().len();
}
}
let clean_type = if markers.is_empty() {
type_content
} else {
type_content[..type_end].trim_end_matches(',').trim_end()
};
TypeMarkers {
clean_type,
commas: separator_comma_offsets(type_content, clean_type.len()),
markers,
}
}
pub(crate) fn marker_syntax_elements(markers: &[MarkerSegment], base: usize) -> Vec<SyntaxElement> {
markers
.iter()
.map(|marker| match *marker {
MarkerSegment::Optional { offset } => SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::OPTIONAL,
TextRange::from_offset_len(base + offset, "optional".len()),
)),
MarkerSegment::Default {
keyword,
separator,
value,
} => {
let kw_range = TextRange::from_offset_len(base + keyword, "default".len());
let mut children = vec![SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DEFAULT_KEYWORD,
kw_range,
))];
let mut end = kw_range.end();
if let Some(sep) = separator {
let sep_range = TextRange::from_offset_len(base + sep, 1);
end = end.max(sep_range.end());
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DEFAULT_SEPARATOR,
sep_range,
)));
}
if let Some((off, len)) = value {
let val_range = TextRange::from_offset_len(base + off, len);
end = end.max(val_range.end());
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DEFAULT_VALUE,
val_range,
)));
}
SyntaxElement::Node(SyntaxNode::new(
SyntaxKind::DEFAULT,
TextRange::new(kw_range.start(), end),
children,
))
}
})
.collect()
}
pub(crate) struct BracketEntry<'a> {
pub name: &'a str,
pub open_bracket: usize,
pub close_bracket: usize,
pub clean_type: &'a str,
pub type_offset: usize,
pub commas: Vec<usize>,
pub markers: Vec<MarkerSegment>,
pub colon: Option<usize>,
pub description: Option<&'a str>,
pub description_offset: Option<usize>,
}
pub(crate) fn try_parse_bracket_entry(text: &str) -> Option<BracketEntry<'_>> {
let bracket_pos = text.bytes().enumerate().find_map(|(i, b)| {
if i > 0 && matches!(b, b'(' | b'[' | b'{' | b'<') {
Some(i)
} else {
None
}
})?;
if let Some(colon_pos) = find_entry_colon(text) {
if colon_pos < bracket_pos {
return None;
}
}
let close_pos = find_matching_close(text, bracket_pos)?;
let after_close = text[close_pos + 1..].trim_start();
if !after_close.is_empty() && !after_close.starts_with(':') {
return None;
}
let name = text[..bracket_pos].trim_end();
let (colon, description, description_offset) = if after_close.starts_with(':') {
let colon_byte = text[close_pos + 1..].find(':').unwrap() + close_pos + 1;
let after_colon = &text[colon_byte + 1..];
let desc = after_colon.trim();
if desc.is_empty() {
(Some(colon_byte), None, None)
} else {
let ws = after_colon.len() - after_colon.trim_start().len();
(Some(colon_byte), Some(desc), Some(colon_byte + 1 + ws))
}
} else {
(None, None, None)
};
let type_end = if let Some(c) = colon {
if c > bracket_pos && c < close_pos { c } else { close_pos }
} else {
close_pos
};
let type_raw = &text[bracket_pos + 1..type_end];
let type_trimmed = type_raw.trim();
let leading_ws = type_raw.len() - type_raw.trim_start().len();
let type_offset = bracket_pos + 1 + leading_ws;
let scanned = scan_type_markers(type_trimmed);
let commas: Vec<usize> = scanned.commas.into_iter().map(|rel| type_offset + rel).collect();
Some(BracketEntry {
name,
open_bracket: bracket_pos,
close_bracket: close_pos,
clean_type: scanned.clean_type,
type_offset,
commas,
markers: scanned.markers,
colon,
description,
description_offset,
})
}
pub(crate) fn find_colon_ignoring_parens(text: &str, start: usize) -> Option<usize> {
let mut depth: u32 = 0;
for (i, b) in text[start..].bytes().enumerate() {
match b {
b'[' | b'{' | b'<' => depth += 1,
b']' | b'}' | b'>' => depth = depth.saturating_sub(1),
b':' if depth == 0 => return Some(start + i),
_ => {}
}
}
None
}
pub(crate) fn find_entry_open_bracket(text: &str) -> Option<usize> {
let bracket_pos = text.bytes().enumerate().find_map(|(i, b)| {
if i > 0 && matches!(b, b'(' | b'[' | b'{' | b'<') {
Some(i)
} else {
None
}
})?;
if let Some(colon_pos) = find_entry_colon(text) {
if colon_pos < bracket_pos {
return None;
}
}
Some(bracket_pos)
}
pub(crate) fn convert_multiline_with_indentation(text: &str) -> String {
let description_indent = text
.lines()
.skip(1)
.filter_map(|line| {
let trimmed_len = line.trim_start().len();
if trimmed_len == 0 {
None
} else {
Some(line.len() - trimmed_len)
}
})
.min()
.unwrap_or(0);
let mut lines = text.lines();
if let Some(first_line) = lines.next() {
lines
.map(|line| {
if description_indent >= line.len() {
&line[0..0]
} else {
line[description_indent..].trim_end()
}
})
.fold(first_line.trim_end().to_owned(), |a, b| a + "\n" + b)
} else {
String::new()
}
}
fn build_reference_node_rst(
directive_marker: TextRange,
open_bracket: TextRange,
number: Option<TextRange>,
close_bracket: TextRange,
content: Option<TextRange>,
range: TextRange,
) -> SyntaxNode {
let mut children = Vec::new();
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DIRECTIVE_MARKER,
directive_marker,
)));
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::OPEN_BRACKET,
open_bracket,
)));
if let Some(n) = number {
children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::LABEL, n)));
}
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::CLOSE_BRACKET,
close_bracket,
)));
if let Some(c) = content {
children.push(SyntaxElement::Node(text_block_single(SyntaxKind::DESCRIPTION, c)));
}
SyntaxNode::new(SyntaxKind::CITATION, range, children)
}
fn build_reference_node_plain(content: TextRange, range: TextRange) -> SyntaxNode {
let children = vec![SyntaxElement::Node(text_block_single(SyntaxKind::DESCRIPTION, content))];
SyntaxNode::new(SyntaxKind::CITATION, range, children)
}
fn extend_last_ref_content(nodes: &mut [SyntaxElement], cont: TextRange) {
if let Some(SyntaxElement::Node(node)) = nodes.last_mut() {
let mut found_content = false;
for child in node.children_mut() {
if let SyntaxElement::Node(n) = child {
if n.kind() == SyntaxKind::DESCRIPTION {
extend_text_block(n, cont);
found_content = true;
break;
}
}
}
if !found_content {
node.push_child(SyntaxElement::Node(text_block_single(SyntaxKind::DESCRIPTION, cont)));
}
node.extend_range_to(cont.end());
}
}
pub(crate) fn process_reference_line(
cursor: &LineCursor,
nodes: &mut Vec<SyntaxElement>,
entry_indent: &mut Option<usize>,
) {
let indent_cols = cursor.current_indent_columns();
if let Some(base) = *entry_indent {
if indent_cols > base {
extend_last_ref_content(nodes, cursor.current_trimmed_range());
return;
}
}
if entry_indent.is_none() {
*entry_indent = Some(indent_cols);
}
let col = cursor.current_indent();
let trimmed = cursor.current_trimmed();
let is_directive = trimmed.starts_with("..") && trimmed[2..].trim_start().starts_with('[');
if is_directive {
let rel_open = trimmed.find('[').unwrap();
let abs_open = cursor.substr_offset(trimmed) + rel_open;
let line_end = cursor.substr_offset(cursor.current_line_text()) + cursor.current_line_text().len();
if let Some(abs_close) = find_matching_close(&cursor.source()[..line_end], abs_open) {
let directive_marker = cursor.make_line_range(cursor.line, col, 2);
let open_bracket = TextRange::from_offset_len(abs_open, 1);
let close_bracket = TextRange::from_offset_len(abs_close, 1);
let num_raw = &cursor.source()[abs_open + 1..abs_close];
let num_str = num_raw.trim();
let number = if !num_str.is_empty() {
let num_abs = cursor.substr_offset(num_str);
Some(TextRange::from_offset_len(num_abs, num_str.len()))
} else {
None
};
let line_end_offset = cursor.substr_offset(cursor.current_line_text()) + cursor.current_line_text().len();
let after_on_line = &cursor.source()[abs_close + 1..line_end_offset.min(cursor.source().len())];
let content_str = after_on_line.trim();
let content = if !content_str.is_empty() {
Some(TextRange::from_offset_len(
cursor.substr_offset(content_str),
content_str.len(),
))
} else {
None
};
nodes.push(SyntaxElement::Node(build_reference_node_rst(
directive_marker,
open_bracket,
number,
close_bracket,
content,
cursor.current_trimmed_range(),
)));
return;
}
}
nodes.push(SyntaxElement::Node(build_reference_node_plain(
cursor.current_trimmed_range(),
cursor.current_trimmed_range(),
)));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_entry_colon() {
assert_eq!(find_entry_colon("name: desc"), Some(4));
assert_eq!(find_entry_colon("name:desc"), Some(4));
assert_eq!(find_entry_colon("name:"), Some(4));
assert_eq!(find_entry_colon("name"), None);
assert_eq!(find_entry_colon("Dict[str, int]: desc"), Some(14));
assert_eq!(find_entry_colon("Tuple(a, b): desc"), Some(11));
assert_eq!(find_entry_colon("Dict[str, List[int]]: desc"), Some(20));
assert_eq!(find_entry_colon("Dict[k: v]"), None);
}
#[test]
fn test_find_term_colon() {
assert_eq!(find_term_colon("x : int"), Some(2));
assert_eq!(find_term_colon("x, y : int"), Some(5));
assert_eq!(find_term_colon("result:int"), Some(6));
assert_eq!(find_term_colon("func_a: Description"), Some(6));
assert_eq!(find_term_colon("Dict[str, int]: desc"), Some(14));
assert_eq!(find_term_colon("Description with attributes:"), None);
assert_eq!(find_term_colon(":attr:`~module.ClassName.attr1`"), None);
assert_eq!(find_term_colon("name"), None);
}
#[test]
fn test_split_comma_parts() {
let parts: Vec<_> = split_comma_parts("int, optional")
.iter()
.map(|(_, s)| s.trim())
.collect();
assert_eq!(parts, vec!["int", "optional"]);
let parts: Vec<_> = split_comma_parts("Dict[str, int], optional")
.iter()
.map(|(_, s)| s.trim())
.collect();
assert_eq!(parts, vec!["Dict[str, int]", "optional"]);
let parts = split_comma_parts("int, optional");
assert_eq!(parts[0].0, 0);
assert_eq!(parts[1].0, 4);
}
#[test]
fn test_separator_comma_offsets() {
assert_eq!(separator_comma_offsets("int, optional", 3), vec![3]);
assert_eq!(separator_comma_offsets("Dict[str, int], optional", 14), vec![14]);
assert_eq!(separator_comma_offsets("int, float", 10), Vec::<usize>::new());
assert_eq!(separator_comma_offsets("int", 3), Vec::<usize>::new());
assert_eq!(separator_comma_offsets("int, optional, default 5", 3), vec![3, 13]);
}
#[test]
fn test_find_matching_close_basic() {
assert_eq!(find_matching_close("(abc)", 0), Some(4));
}
#[test]
fn test_find_matching_close_nested_same() {
assert_eq!(find_matching_close("(a(b)c)", 0), Some(6));
}
#[test]
fn test_find_matching_close_nested_mixed() {
assert_eq!(find_matching_close("(a[b]c)", 0), Some(6));
}
#[test]
fn test_find_matching_close_mismatched_ignored() {
assert_eq!(find_matching_close("(a]b)", 0), Some(4));
}
#[test]
fn test_find_matching_close_no_match() {
assert_eq!(find_matching_close("(abc", 0), None);
}
#[test]
fn test_find_matching_close_angle_brackets() {
assert_eq!(find_matching_close("<int>", 0), Some(4));
}
fn optional_offsets(text: &str) -> Vec<usize> {
scan_type_markers(text)
.markers
.iter()
.filter_map(|m| match m {
MarkerSegment::Optional { offset } => Some(*offset),
_ => None,
})
.collect()
}
#[test]
fn test_scan_type_markers_optional() {
assert_eq!(scan_type_markers("int, optional").clean_type, "int");
assert_eq!(optional_offsets("int, optional"), vec![5]);
assert_eq!(scan_type_markers("int").clean_type, "int");
assert!(scan_type_markers("int").markers.is_empty());
assert_eq!(
scan_type_markers("Dict[str, int], optional").clean_type,
"Dict[str, int]"
);
assert_eq!(optional_offsets("Dict[str, int], optional"), vec![16]);
assert_eq!(scan_type_markers("optional").clean_type, "");
assert_eq!(optional_offsets("optional"), vec![0]);
assert_eq!(optional_offsets("int,optional"), vec![4]);
assert_eq!(optional_offsets("int, optional"), vec![6]);
assert_eq!(optional_offsets("int, optional "), vec![5]);
}
#[test]
fn test_scan_type_markers_default_forms() {
let m = scan_type_markers("int, default 5");
assert_eq!(m.clean_type, "int");
assert_eq!(
m.markers,
vec![MarkerSegment::Default {
keyword: 5,
separator: None,
value: Some((13, 1)),
}]
);
let m = scan_type_markers("int, default=5");
assert_eq!(
m.markers,
vec![MarkerSegment::Default {
keyword: 5,
separator: Some(12),
value: Some((13, 1)),
}]
);
let m = scan_type_markers("int, default: 5");
assert_eq!(
m.markers,
vec![MarkerSegment::Default {
keyword: 5,
separator: Some(12),
value: Some((14, 1)),
}]
);
let m = scan_type_markers("int, default=");
assert_eq!(
m.markers,
vec![MarkerSegment::Default {
keyword: 5,
separator: Some(12),
value: Some((13, 0)),
}]
);
let m = scan_type_markers("defaultdict");
assert_eq!(m.clean_type, "defaultdict");
assert!(m.markers.is_empty());
}
#[test]
fn test_scan_type_markers_repeated() {
let m = scan_type_markers("int, default 1, default 2");
assert_eq!(m.clean_type, "int");
assert_eq!(m.commas, vec![3, 14]);
assert_eq!(
m.markers,
vec![
MarkerSegment::Default {
keyword: 5,
separator: None,
value: Some((13, 1)),
},
MarkerSegment::Default {
keyword: 16,
separator: None,
value: Some((24, 1)),
},
]
);
let m = scan_type_markers("int, optional, optional");
assert_eq!(m.clean_type, "int");
assert_eq!(
m.markers,
vec![
MarkerSegment::Optional { offset: 5 },
MarkerSegment::Optional { offset: 15 },
]
);
}
#[test]
fn test_bracket_entry_basic() {
let e = try_parse_bracket_entry("name (int): desc").unwrap();
assert_eq!(e.name, "name");
assert_eq!(e.clean_type, "int");
assert_eq!(e.description, Some("desc"));
}
#[test]
fn test_bracket_entry_no_space() {
let e = try_parse_bracket_entry("name(int): desc").unwrap();
assert_eq!(e.name, "name");
assert_eq!(e.clean_type, "int");
}
#[test]
fn test_bracket_entry_optional() {
let e = try_parse_bracket_entry("name (int, optional): desc").unwrap();
assert_eq!(e.clean_type, "int");
assert_eq!(e.markers, vec![MarkerSegment::Optional { offset: 5 }]);
}
#[test]
fn test_bracket_entry_complex_type() {
let e = try_parse_bracket_entry("data (Dict[str, int]): values").unwrap();
assert_eq!(e.clean_type, "Dict[str, int]");
assert_eq!(e.description, Some("values"));
}
#[test]
fn test_bracket_entry_no_colon() {
let e = try_parse_bracket_entry("name (int)").unwrap();
assert_eq!(e.name, "name");
assert_eq!(e.clean_type, "int");
assert!(e.colon.is_none());
assert!(e.description.is_none());
}
#[test]
fn test_bracket_entry_empty_desc() {
let e = try_parse_bracket_entry("name (int):").unwrap();
assert_eq!(e.clean_type, "int");
assert!(e.colon.is_some());
assert!(e.description.is_none());
}
#[test]
fn test_bracket_entry_colon_before_bracket() {
assert!(try_parse_bracket_entry("name : (int)").is_none());
}
#[test]
fn test_bracket_entry_no_bracket() {
assert!(try_parse_bracket_entry("name : int").is_none());
}
#[test]
fn test_bracket_entry_text_after_bracket() {
assert!(try_parse_bracket_entry("name (int) not_colon").is_none());
}
#[test]
fn test_find_colon_ignoring_parens_basic() {
assert_eq!(find_colon_ignoring_parens("int : desc", 0), Some(4));
}
#[test]
fn test_find_colon_ignoring_parens_inside_brackets() {
assert_eq!(find_colon_ignoring_parens("Dict[k: v] : desc", 0), Some(11));
}
#[test]
fn test_find_colon_ignoring_parens_inside_parens() {
assert_eq!(find_colon_ignoring_parens("(int : desc", 1), Some(5));
}
#[test]
fn test_find_colon_ignoring_parens_none() {
assert_eq!(find_colon_ignoring_parens("int desc", 0), None);
}
#[test]
fn test_find_entry_open_bracket_basic() {
assert_eq!(find_entry_open_bracket("name (int)"), Some(5));
}
#[test]
fn test_find_entry_open_bracket_colon_first() {
assert_eq!(find_entry_open_bracket("name : (int)"), None);
}
#[test]
fn test_find_entry_open_bracket_no_bracket() {
assert_eq!(find_entry_open_bracket("name : int"), None);
}
#[test]
fn test_find_entry_open_bracket_at_start() {
assert_eq!(find_entry_open_bracket("(int)"), None);
}
#[test]
fn test_directive_name_len() {
assert_eq!(directive_name_len("deprecated::"), Some(10));
assert_eq!(directive_name_len("versionadded:: 2.0"), Some(12));
assert_eq!(directive_name_len("note::"), Some(4));
assert_eq!(directive_name_len("code-block:: python"), Some(10));
assert_eq!(directive_name_len("my_directive::"), Some(12));
assert_eq!(directive_name_len("math:: x"), Some(4));
assert_eq!(directive_name_len("1bad::"), None);
assert_eq!(directive_name_len("::"), None);
assert_eq!(directive_name_len(""), None);
assert_eq!(directive_name_len("[1]"), None);
assert_eq!(directive_name_len("_target:"), None);
assert_eq!(directive_name_len("|sub|"), None);
}
#[test]
fn test_convert_multiline_with_indentation() {
assert_eq!(
convert_multiline_with_indentation(
"First line.
Description line.
More description.
Blockquote.
Another.
Some text.
.. directive:: option
directive_option"
),
"First line.\n\nDescription line.\nMore description.\n\n Blockquote.\n Another.\n\nSome text.\n\n.. directive:: option\n directive_option"
);
}
}