pydocstring 0.1.11

A zero-dependency Rust parser for Python docstrings (Google and NumPy styles) with a unified syntax tree and byte-precise source locations
Documentation
//! Plain docstring parser (SyntaxNode-based).
//!
//! Parses docstrings that contain no NumPy or Google style section markers.
//! Produces a [`Parsed`] with a [`SyntaxKind::PLAIN_DOCSTRING`] root that may
//! contain a [`SyntaxKind::SUMMARY`] token and an
//! [`SyntaxKind::EXTENDED_SUMMARY`] token.

use crate::cursor::{LineCursor, indent_len};
use crate::syntax::{Parsed, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
use crate::text::TextRange;

// =============================================================================
// Parser
// =============================================================================

/// Build the TextRange covering `first..=last` content lines (trimmed).
fn build_content_range(cursor: &LineCursor, first: Option<usize>, last: usize) -> Option<TextRange> {
    first.map(|f| {
        let first_line = cursor.line_text(f);
        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();
        cursor.make_range(f, first_col, last, last_col)
    })
}

/// Parse a plain docstring (no NumPy or Google section markers).
///
/// The returned [`Parsed`] has a [`SyntaxKind::PLAIN_DOCSTRING`] root that
/// contains at most one `SUMMARY` token and one `EXTENDED_SUMMARY` token.
/// Unrecognised styles (e.g. Sphinx) are also parsed this way.
///
/// # Example
///
/// ```rust
/// use pydocstring::parse::plain::{parse_plain, nodes::PlainDocstring};
/// use pydocstring::syntax::SyntaxKind;
///
/// let result = parse_plain("Summary.\n\nMore details here.");
/// assert_eq!(result.root().kind(), SyntaxKind::PLAIN_DOCSTRING);
///
/// let doc = PlainDocstring::cast(result.root()).unwrap();
/// assert_eq!(doc.summary().unwrap().text(result.source()), "Summary.");
/// assert_eq!(doc.extended_summary().unwrap().text(result.source()), "More details here.");
/// ```
pub fn parse_plain(input: &str) -> Parsed {
    let mut line_cursor = LineCursor::new(input);
    let mut root_children: Vec<SyntaxElement> = Vec::new();

    line_cursor.skip_blanks();
    if line_cursor.is_eof() {
        let root = SyntaxNode::new(SyntaxKind::PLAIN_DOCSTRING, line_cursor.full_range(), root_children);
        return Parsed::new(input.to_string(), root);
    }

    let mut summary_done = false;
    let mut summary_first: Option<usize> = None;
    let mut summary_last: usize = 0;
    let mut ext_first: Option<usize> = None;
    let mut ext_last: usize = 0;

    while !line_cursor.is_eof() {
        if line_cursor.current_trimmed().is_empty() {
            // Blank line: flush summary if not done yet.
            if !summary_done && summary_first.is_some() {
                root_children.push(SyntaxElement::Token(SyntaxToken::new(
                    SyntaxKind::SUMMARY,
                    build_content_range(&line_cursor, summary_first, summary_last).unwrap(),
                )));
                summary_done = true;
            }
            line_cursor.advance();
            continue;
        }

        if !summary_done {
            if summary_first.is_none() {
                summary_first = Some(line_cursor.line);
            }
            summary_last = line_cursor.line;
        } else {
            if ext_first.is_none() {
                ext_first = Some(line_cursor.line);
            }
            ext_last = line_cursor.line;
        }

        line_cursor.advance();
    }

    // Finalise at EOF.
    if !summary_done && summary_first.is_some() {
        root_children.push(SyntaxElement::Token(SyntaxToken::new(
            SyntaxKind::SUMMARY,
            build_content_range(&line_cursor, summary_first, summary_last).unwrap(),
        )));
    }
    if ext_first.is_some() {
        root_children.push(SyntaxElement::Token(SyntaxToken::new(
            SyntaxKind::EXTENDED_SUMMARY,
            build_content_range(&line_cursor, ext_first, ext_last).unwrap(),
        )));
    }

    let root = SyntaxNode::new(SyntaxKind::PLAIN_DOCSTRING, line_cursor.full_range(), root_children);
    Parsed::new(input.to_string(), root)
}