use core::fmt;
use crate::model::FreeSectionKind;
use crate::model::SectionKind;
use crate::parse::Style;
use crate::parse::google::parse_google;
use crate::parse::numpy::parse_numpy;
use crate::parse::plain::parse_plain;
use crate::syntax::Parsed;
use crate::syntax::SyntaxElement;
use crate::syntax::SyntaxKind;
use crate::syntax::SyntaxNode;
use crate::syntax::SyntaxToken;
use crate::text::TextRange;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FragmentKind {
Entry,
Body,
Section,
Document,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetaVar {
name: String,
multi: bool,
site: MetaVarSite,
}
impl MetaVar {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_multi(&self) -> bool {
self.multi
}
pub fn site(&self) -> &MetaVarSite {
&self.site
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetaVarSite {
path: Vec<usize>,
kind: SyntaxKind,
parent_kind: SyntaxKind,
range: TextRange,
exact: bool,
}
impl MetaVarSite {
pub fn path(&self) -> &[usize] {
&self.path
}
pub fn kind(&self) -> SyntaxKind {
self.kind
}
pub fn parent_kind(&self) -> SyntaxKind {
self.parent_kind
}
pub fn range(&self) -> TextRange {
self.range
}
pub fn is_exact(&self) -> bool {
self.exact
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PatternError {
#[non_exhaustive]
Unparsable {
message: String,
},
}
impl fmt::Display for PatternError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PatternError::Unparsable { message } => write!(f, "unparsable pattern: {message}"),
}
}
}
impl std::error::Error for PatternError {}
#[derive(Debug, Clone)]
pub struct Reading {
parsed: Parsed,
fragment_kind: FragmentKind,
fragment_path: Vec<usize>,
section_kinds: Vec<SectionKind>,
metavars: Vec<MetaVar>,
}
impl Reading {
pub fn fragment_kind(&self) -> FragmentKind {
self.fragment_kind
}
pub fn section_kinds(&self) -> &[SectionKind] {
&self.section_kinds
}
pub fn fragment(&self) -> &SyntaxNode {
node_at(self.parsed.root(), &self.fragment_path)
}
pub fn metavars(&self) -> &[MetaVar] {
&self.metavars
}
pub fn parsed(&self) -> &Parsed {
&self.parsed
}
pub(crate) fn fragment_path(&self) -> &[usize] {
&self.fragment_path
}
}
#[derive(Debug, Clone)]
pub struct Pattern {
style: Style,
text: String,
readings: Vec<Reading>,
}
impl Pattern {
pub fn new(style: Style, text: &str) -> Result<Pattern, PatternError> {
let (substituted, occurrences) = substitute_metavars(text);
let doc_parsed = parse_for(style, &substituted);
let content: Vec<usize> = content_child_indices(doc_parsed.root());
if content.is_empty() {
return Err(PatternError::Unparsable {
message: "empty pattern: no content to match".to_owned(),
});
}
let mut readings: Vec<Reading> = Vec::new();
if matches!(style, Style::Google | Style::NumPy) {
if content.len() == 1 {
let mut entries: Vec<(Shape, Reading)> = Vec::new();
for kind in ENTRY_READING_ORDER {
if let Ok(analysis) = analyze_in_section(style, kind, &substituted, &occurrences) {
let shape = shape_of(&analysis);
if let Some((_, reading)) = entries.iter_mut().find(|(existing, _)| *existing == shape) {
reading.section_kinds.push(kind.clone());
} else {
entries.push((shape, analysis.into_reading(vec![kind.clone()])));
}
}
}
readings.extend(entries.into_iter().map(|(_, reading)| reading));
}
if let Ok(analysis) = analyze_in_section(
style,
&SectionKind::FreeText(FreeSectionKind::Notes),
&substituted,
&occurrences,
) {
readings.push(analysis.into_reading(free_text_kinds()));
}
}
if let [index] = content[..] {
if doc_parsed.root().children()[index].kind() == SyntaxKind::SECTION && check_coverage(&doc_parsed).is_ok()
{
let fragment_path = vec![index];
if let Ok(metavars) = locate_metavars(&doc_parsed, &occurrences, &fragment_path) {
readings.push(Reading {
parsed: doc_parsed.clone(),
fragment_kind: FragmentKind::Section,
fragment_path,
section_kinds: Vec::new(),
metavars,
});
}
}
}
if check_coverage(&doc_parsed).is_ok() {
if let Ok(metavars) = locate_metavars(&doc_parsed, &occurrences, &[]) {
readings.push(Reading {
parsed: doc_parsed,
fragment_kind: FragmentKind::Document,
fragment_path: Vec::new(),
section_kinds: Vec::new(),
metavars,
});
}
}
if readings.is_empty() {
return Err(PatternError::Unparsable {
message: "no valid reading: every metavariable must land on a bindable site (a whole token/node, \
or inside prose) in at least one grammar that accepts the text"
.to_owned(),
});
}
Ok(Pattern {
style,
text: text.to_owned(),
readings,
})
}
pub fn style(&self) -> Style {
self.style
}
pub fn text(&self) -> &str {
&self.text
}
pub fn readings(&self) -> &[Reading] {
&self.readings
}
pub fn reading_for(&self, kind: &SectionKind) -> Option<&Reading> {
self.readings.iter().find(|r| r.section_kinds.contains(kind))
}
pub fn fragment_kind(&self) -> FragmentKind {
self.readings[0].fragment_kind()
}
pub fn fragment(&self) -> &SyntaxNode {
self.readings[0].fragment()
}
pub fn metavars(&self) -> &[MetaVar] {
self.readings[0].metavars()
}
pub fn parsed(&self) -> &Parsed {
self.readings[0].parsed()
}
}
struct Occurrence {
name: String,
multi: bool,
placeholder: String,
}
fn is_ident_start(b: u8) -> bool {
b.is_ascii_uppercase()
}
fn is_ident_continue(b: u8) -> bool {
b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'
}
fn is_word_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'$'
}
fn placeholder_stem(text: &str) -> String {
let mut stem = String::from("PYDOCMV");
while text.contains(&stem) {
stem.push('Q');
}
stem
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MetaVarToken<'a> {
Literal(&'a str),
Var { name: &'a str, multi: bool },
}
pub(crate) fn lex_metavars(text: &str) -> Vec<MetaVarToken<'_>> {
let bytes = text.as_bytes();
let mut tokens: Vec<MetaVarToken<'_>> = Vec::new();
let mut run_start = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' && (i == 0 || !is_word_byte(bytes[i - 1])) {
let (sigil_len, multi) = if text[i..].starts_with("$$$") {
(3, true)
} else {
(1, false)
};
let ident_start = i + sigil_len;
if ident_start < bytes.len() && is_ident_start(bytes[ident_start]) {
let mut end = ident_start + 1;
while end < bytes.len() && is_ident_continue(bytes[end]) {
end += 1;
}
if run_start < i {
tokens.push(MetaVarToken::Literal(&text[run_start..i]));
}
tokens.push(MetaVarToken::Var {
name: &text[ident_start..end],
multi,
});
run_start = end;
i = end;
continue;
}
}
i += 1;
}
if run_start < text.len() {
tokens.push(MetaVarToken::Literal(&text[run_start..]));
}
tokens
}
fn substitute_metavars(text: &str) -> (String, Vec<Occurrence>) {
let stem = placeholder_stem(text);
let mut out = String::with_capacity(text.len());
let mut occurrences: Vec<Occurrence> = Vec::new();
for token in lex_metavars(text) {
match token {
MetaVarToken::Literal(literal) => out.push_str(literal),
MetaVarToken::Var { name, multi } => {
let placeholder = format!("{stem}{}X", occurrences.len());
out.push_str(&placeholder);
occurrences.push(Occurrence {
name: name.to_owned(),
multi,
placeholder,
});
}
}
}
(out, occurrences)
}
const ENTRY_READING_ORDER: &[SectionKind] = &[
SectionKind::Parameters,
SectionKind::KeywordParameters,
SectionKind::OtherParameters,
SectionKind::Receives,
SectionKind::Returns,
SectionKind::Yields,
SectionKind::Raises,
SectionKind::Warns,
SectionKind::Attributes,
SectionKind::Methods,
SectionKind::SeeAlso,
SectionKind::References,
];
fn free_text_kinds() -> Vec<SectionKind> {
[
FreeSectionKind::Notes,
FreeSectionKind::Examples,
FreeSectionKind::Warnings,
FreeSectionKind::Todo,
FreeSectionKind::Attention,
FreeSectionKind::Caution,
FreeSectionKind::Danger,
FreeSectionKind::Error,
FreeSectionKind::Hint,
FreeSectionKind::Important,
FreeSectionKind::Tip,
]
.into_iter()
.map(SectionKind::FreeText)
.collect()
}
fn header_name(style: Style, kind: &SectionKind) -> Option<String> {
let name = match (style, kind) {
(Style::Google, SectionKind::Parameters) => "Args",
(Style::Google, SectionKind::KeywordParameters) => "Keyword Args",
(Style::NumPy, SectionKind::Parameters) => "Parameters",
(Style::NumPy, SectionKind::KeywordParameters) => "Keyword Parameters",
(Style::Google | Style::NumPy, SectionKind::OtherParameters) => "Other Parameters",
(Style::Google | Style::NumPy, SectionKind::Receives) => "Receives",
(Style::Google | Style::NumPy, SectionKind::Returns) => "Returns",
(Style::Google | Style::NumPy, SectionKind::Yields) => "Yields",
(Style::Google | Style::NumPy, SectionKind::Raises) => "Raises",
(Style::Google | Style::NumPy, SectionKind::Warns) => "Warns",
(Style::Google | Style::NumPy, SectionKind::Attributes) => "Attributes",
(Style::Google | Style::NumPy, SectionKind::Methods) => "Methods",
(Style::Google | Style::NumPy, SectionKind::SeeAlso) => "See Also",
(Style::Google | Style::NumPy, SectionKind::References) => "References",
(Style::Google | Style::NumPy, SectionKind::FreeText(free)) => return free_header_name(free),
_ => return None,
};
Some(name.to_owned())
}
fn free_header_name(kind: &FreeSectionKind) -> Option<String> {
let name = match kind {
FreeSectionKind::Notes => "Notes",
FreeSectionKind::Examples => "Examples",
FreeSectionKind::Warnings => "Warnings",
FreeSectionKind::Todo => "Todo",
FreeSectionKind::Attention => "Attention",
FreeSectionKind::Caution => "Caution",
FreeSectionKind::Danger => "Danger",
FreeSectionKind::Error => "Error",
FreeSectionKind::Hint => "Hint",
FreeSectionKind::Important => "Important",
FreeSectionKind::Tip => "Tip",
FreeSectionKind::Unknown(name) => {
let trimmed = name.trim();
return (!trimmed.is_empty() && !trimmed.contains('\n')).then(|| trimmed.to_owned());
}
};
Some(name.to_owned())
}
fn wrap_section_body(style: Style, header: &str, fragment: &str) -> String {
let fragment = fragment.strip_suffix('\n').unwrap_or(fragment);
let mut out = String::new();
match style {
Style::Google => {
out.push_str(header);
out.push_str(":\n");
for line in fragment.split('\n') {
if !line.trim().is_empty() {
out.push_str(" ");
out.push_str(line);
}
out.push('\n');
}
}
_ => {
out.push_str(header);
out.push('\n');
for _ in 0..header.len() {
out.push('-');
}
out.push('\n');
for line in fragment.split('\n') {
out.push_str(line);
out.push('\n');
}
}
}
out
}
fn parse_for(style: Style, text: &str) -> Parsed {
match style {
Style::NumPy => parse_numpy(text),
Style::Google => parse_google(text),
_ => parse_plain(text),
}
}
struct ChainLink {
path: Vec<usize>,
kind: SyntaxKind,
range: TextRange,
is_token: bool,
}
fn landing_chain(root: &SyntaxNode, ph: TextRange) -> Vec<ChainLink> {
let mut chain = vec![ChainLink {
path: Vec::new(),
kind: root.kind(),
range: *root.range(),
is_token: false,
}];
let mut path = Vec::new();
let mut cur = root;
'descend: loop {
for (i, child) in cur.children().iter().enumerate() {
let r = *child.range();
if !r.is_empty() && r.start() <= ph.start() && ph.end() <= r.end() {
path.push(i);
match child {
SyntaxElement::Node(n) => {
chain.push(ChainLink {
path: path.clone(),
kind: n.kind(),
range: r,
is_token: false,
});
cur = n;
continue 'descend;
}
SyntaxElement::Token(t) => {
chain.push(ChainLink {
path: path.clone(),
kind: t.kind(),
range: r,
is_token: true,
});
break 'descend;
}
}
}
}
break;
}
chain
}
fn choose_site(
chain: &[ChainLink],
sigil: &str,
name: &str,
ph: TextRange,
multi: bool,
) -> Result<MetaVarSite, String> {
let exact: Vec<usize> = (1..chain.len()).filter(|&i| chain[i].range == ph).collect();
let chosen = if let Some(&i) = if multi { exact.first() } else { exact.last() } {
i
} else {
let last = chain.len() - 1;
if last == 0 || !chain[last].is_token {
return Err(format!("metavariable {sigil}{name} did not land on a single token"));
}
if chain[last].kind != SyntaxKind::TEXT_LINE {
return Err(format!(
"metavariable {sigil}{name} lands inside a {} token mixed with literal text and cannot bind a whole node",
chain[last].kind
));
}
last
};
Ok(MetaVarSite {
path: chain[chosen].path.clone(),
kind: chain[chosen].kind,
parent_kind: chain[chosen - 1].kind,
range: ph,
exact: chain[chosen].range == ph,
})
}
fn locate_metavars(
parsed: &Parsed,
occurrences: &[Occurrence],
required_prefix: &[usize],
) -> Result<Vec<MetaVar>, String> {
let mut metavars = Vec::with_capacity(occurrences.len());
for occ in occurrences {
let sigil = if occ.multi { "$$$" } else { "$" };
let offset = parsed
.source()
.find(&occ.placeholder)
.ok_or_else(|| format!("metavariable {sigil}{} was lost during parsing", occ.name))?;
let ph = TextRange::from_offset_len(offset, occ.placeholder.len());
let chain = landing_chain(parsed.root(), ph);
let site = choose_site(&chain, sigil, &occ.name, ph, occ.multi)?;
if !site.path.starts_with(required_prefix) {
return Err(format!("metavariable {sigil}{} landed outside the fragment", occ.name));
}
metavars.push(MetaVar {
name: occ.name.clone(),
multi: occ.multi,
site,
});
}
Ok(metavars)
}
fn content_child_indices(node: &SyntaxNode) -> Vec<usize> {
node.children()
.iter()
.enumerate()
.filter(|(_, c)| !c.kind().is_trivia())
.map(|(i, _)| i)
.collect()
}
fn node_at<'a>(root: &'a SyntaxNode, path: &[usize]) -> &'a SyntaxNode {
let mut cur = root;
for &i in path {
match &cur.children()[i] {
SyntaxElement::Node(n) => cur = n,
SyntaxElement::Token(_) => unreachable!("fragment path points at a token"),
}
}
cur
}
fn check_coverage(parsed: &Parsed) -> Result<(), String> {
fn collect<'a>(node: &'a SyntaxNode, out: &mut Vec<&'a SyntaxToken>) {
for child in node.children() {
match child {
SyntaxElement::Node(n) => collect(n, out),
SyntaxElement::Token(t) => out.push(t),
}
}
}
let mut tokens = Vec::new();
collect(parsed.root(), &mut tokens);
tokens.sort_by_key(|t| (t.range().start(), t.range().end()));
let mut pos = 0usize;
for token in tokens {
let (start, end) = (usize::from(token.range().start()), usize::from(token.range().end()));
if start > pos {
return Err(format!("pattern parse lost bytes at {pos}..{start}"));
}
pos = pos.max(end);
}
if pos != parsed.source().len() {
return Err(format!("pattern parse lost trailing bytes at {pos}.."));
}
Ok(())
}
struct InSectionAnalysis {
parsed: Parsed,
fragment_kind: FragmentKind,
fragment_path: Vec<usize>,
metavars: Vec<MetaVar>,
}
impl InSectionAnalysis {
fn into_reading(self, section_kinds: Vec<SectionKind>) -> Reading {
Reading {
parsed: self.parsed,
fragment_kind: self.fragment_kind,
fragment_path: self.fragment_path,
section_kinds,
metavars: self.metavars,
}
}
}
fn analyze_in_section(
style: Style,
kind: &SectionKind,
substituted: &str,
occurrences: &[Occurrence],
) -> Result<InSectionAnalysis, String> {
let header =
header_name(style, kind).ok_or_else(|| format!("section kind {kind:?} cannot be spelled as a header"))?;
let wrapped = wrap_section_body(style, &header, substituted);
let parsed = parse_for(style, &wrapped);
check_coverage(&parsed)?;
let root_content = content_child_indices(parsed.root());
let [section_index] = root_content[..] else {
return Err(format!(
"expected a single section, found {} top-level items",
root_content.len()
));
};
let SyntaxElement::Node(section) = &parsed.root().children()[section_index] else {
return Err("expected a section node".to_owned());
};
if section.kind() != SyntaxKind::SECTION {
return Err(format!("expected a SECTION, found {}", section.kind()));
}
let expected_body_kind = match kind {
SectionKind::References => SyntaxKind::CITATION,
SectionKind::FreeText(_) => SyntaxKind::DESCRIPTION,
_ => SyntaxKind::ENTRY,
};
let section_content = content_child_indices(section);
if section_content.len() != 2 {
return Err(format!(
"expected exactly one {expected_body_kind} in the section body, found {}",
section_content.len().saturating_sub(1)
));
}
if section.children()[section_content[0]].kind() != SyntaxKind::SECTION_HEADER {
return Err("section header missing from the wrapped parse".to_owned());
}
let body_index = section_content[1];
let body_kind = section.children()[body_index].kind();
if body_kind != expected_body_kind {
return Err(format!(
"expected the section body to be one {expected_body_kind}, found {body_kind}"
));
}
let fragment_path = vec![section_index, body_index];
let metavars = locate_metavars(&parsed, occurrences, &fragment_path)?;
let fragment_kind = if expected_body_kind == SyntaxKind::DESCRIPTION {
FragmentKind::Body
} else {
FragmentKind::Entry
};
Ok(InSectionAnalysis {
parsed,
fragment_kind,
fragment_path,
metavars,
})
}
#[derive(PartialEq, Eq)]
struct Shape {
tree: Vec<(SyntaxKind, bool, u32, u32)>,
vars: Vec<VarShape>,
}
#[derive(PartialEq, Eq)]
struct VarShape {
name: String,
multi: bool,
kind: SyntaxKind,
parent_kind: SyntaxKind,
exact: bool,
start: u32,
end: u32,
rel_path: Vec<usize>,
}
fn shape_of(analysis: &InSectionAnalysis) -> Shape {
fn dfs(node: &SyntaxNode, base: u32, out: &mut Vec<(SyntaxKind, bool, u32, u32)>) {
out.push((
node.kind(),
true,
node.range().start().raw() - base,
node.range().end().raw() - base,
));
for child in node.children() {
match child {
SyntaxElement::Node(n) => dfs(n, base, out),
SyntaxElement::Token(t) => out.push((
t.kind(),
false,
t.range().start().raw() - base,
t.range().end().raw() - base,
)),
}
}
}
let fragment = node_at(analysis.parsed.root(), &analysis.fragment_path);
let base = fragment.range().start().raw();
let mut tree = Vec::new();
dfs(fragment, base, &mut tree);
let vars = analysis
.metavars
.iter()
.map(|m| VarShape {
name: m.name.clone(),
multi: m.multi,
kind: m.site.kind,
parent_kind: m.site.parent_kind,
exact: m.site.exact,
start: m.site.range.start().raw() - base,
end: m.site.range.end().raw() - base,
rel_path: m.site.path[analysis.fragment_path.len()..].to_vec(),
})
.collect();
Shape { tree, vars }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_substitute_single_and_multi() {
let (out, occs) = substitute_metavars("$NAME ($TYPE): $$$REST");
assert_eq!(occs.len(), 3);
assert_eq!(occs[0].name, "NAME");
assert!(!occs[0].multi);
assert_eq!(occs[2].name, "REST");
assert!(occs[2].multi);
assert_eq!(out, "PYDOCMV0X (PYDOCMV1X): PYDOCMV2X");
}
#[test]
fn test_substitute_literals() {
for text in ["$x", "$3", "a$B", "$$B", "cost: $5", "US$ 3"] {
let (out, occs) = substitute_metavars(text);
assert!(occs.is_empty(), "{text:?} should have no metavariables");
assert_eq!(out, text);
}
}
#[test]
fn test_substitute_identifier_charset() {
let (_, occs) = substitute_metavars("$A_2B $Aa");
assert_eq!(occs.len(), 2);
assert_eq!(occs[0].name, "A_2B");
assert_eq!(occs[1].name, "A");
}
#[test]
fn test_placeholder_stem_probing() {
assert_eq!(placeholder_stem("no collision"), "PYDOCMV");
assert_eq!(placeholder_stem("contains PYDOCMV literal"), "PYDOCMVQ");
assert_eq!(placeholder_stem("PYDOCMV and PYDOCMVQ"), "PYDOCMVQQ");
}
#[test]
fn test_wrap_section_body_google_indents_and_keeps_blank_lines() {
let wrapped = wrap_section_body(Style::Google, "Args", "x: d\n\n more\n");
assert_eq!(wrapped, "Args:\n x: d\n\n more\n");
}
#[test]
fn test_wrap_section_body_numpy_underline_matches_header() {
let wrapped = wrap_section_body(Style::NumPy, "Keyword Parameters", "x : int");
assert_eq!(wrapped, "Keyword Parameters\n------------------\nx : int\n");
}
}