pub(crate) mod block;
pub(crate) mod digits;
pub mod inline;
pub mod lines;
mod punctuation;
use crate::doctree::Doctree;
#[derive(Debug, Clone)]
pub struct ParseOptions {
pub source_path: String,
pub sphinx: bool,
pub docname: String,
pub found_docs: Option<std::sync::Arc<std::collections::BTreeSet<String>>>,
pub exclude_patterns: Vec<String>,
pub py: crate::py::PySigConfig,
pub srcdir: Option<std::path::PathBuf>,
pub source_encoding: String,
}
pub const DEFAULT_SOURCE_ENCODING: &str = "utf-8-sig";
impl Default for ParseOptions {
fn default() -> Self {
ParseOptions {
source_path: "<string>".to_string(),
sphinx: false,
docname: "index".to_string(),
found_docs: None,
exclude_patterns: Vec::new(),
py: crate::py::PySigConfig::default(),
srcdir: None,
source_encoding: DEFAULT_SOURCE_ENCODING.to_string(),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DirectiveRecord {
pub source: u16,
pub name: String,
pub arguments: Vec<String>,
pub options: Vec<(String, String)>,
pub content: String,
pub line: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RoleRecord {
pub source: u16,
pub name: String,
pub full_name: String,
pub target: String,
pub display: Option<String>,
pub line: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ToctreeRecord {
pub glob: bool,
pub entries: Vec<ToctreeEntryRecord>,
pub source: u16,
pub line: u32,
pub warnings: Vec<crate::env::toctree::ToctreeWarning>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ToctreeEntryRecord {
pub title: Option<String>,
pub target: String,
pub line: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProgramOptionRecord {
pub source: u16,
pub program: Option<String>,
pub name: String,
pub node_id: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PyObjectRecord {
pub fullname: String,
pub objtype: String,
pub node_id: String,
pub aliased: bool,
pub source: u16,
pub lineno: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PyModuleRecord {
pub name: String,
pub node_id: String,
pub synopsis: String,
pub platform: String,
pub deprecated: bool,
pub source: u16,
pub lineno: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ObjectRegistration {
pub source: u16,
pub objtype: String,
pub name: String,
pub node_id: String,
pub line: u32,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RegistryExport {
pub nameids: Vec<(String, Option<String>, bool)>,
pub index_serial: u32,
pub program_options: Vec<ProgramOptionRecord>,
pub std_objects: Vec<ObjectRegistration>,
pub py_objects: Vec<PyObjectRecord>,
pub py_modules: Vec<PyModuleRecord>,
pub log_warnings: Vec<ParseLogWarning>,
pub dependencies: Vec<String>,
pub included: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ParseLogWarning {
pub source: u16,
pub message: String,
pub line: u32,
pub doc2path_location: bool,
}
impl ParseLogWarning {
pub fn rendered_path(&self, source_path: &str) -> String {
if self.doc2path_location {
format!("{source_path}.rst")
} else {
source_path.to_string()
}
}
}
pub struct ParseOutput {
pub doctree: Doctree,
pub directive_records: Vec<DirectiveRecord>,
pub role_records: Vec<RoleRecord>,
pub toctrees: Vec<ToctreeRecord>,
pub registry: RegistryExport,
}
pub fn parse_rst(source: &str, opts: &ParseOptions) -> Doctree {
parse_rst_full(source, opts).doctree
}
pub fn parse_rst_full(source: &str, opts: &ParseOptions) -> ParseOutput {
let mut parser = block::BlockParser::new(source, &opts.source_path);
parser.sphinx = opts.sphinx;
parser.docname = opts.docname.clone();
parser.found_docs = opts.found_docs.clone();
parser.exclude_patterns = opts.exclude_patterns.clone();
parser.py = opts.py.clone();
parser.srcdir = opts.srcdir.clone();
parser.source_encoding = opts.source_encoding.clone();
parser.parse_document_full()
}
#[cfg(test)]
mod tests {
use super::*;
const COMPLETE_REGISTRY: &str = r#"{"nameids":[],"index_serial":0,
"program_options":[{"source":0,"program":null,"name":"-f","node_id":"a"}],
"std_objects":[{"source":0,"objtype":"envvar","name":"P","node_id":"b","line":1}],
"py_objects":[{"fullname":"m.f","objtype":"function","node_id":"m.f",
"aliased":false,"source":0,"lineno":1}],
"py_modules":[{"name":"m","node_id":"module-m","synopsis":"","platform":"",
"deprecated":false,"source":0,"lineno":1}],
"log_warnings":[{"source":0,"message":"m","line":2,"doc2path_location":false}],
"dependencies":["part.rst"],"included":["part"]}"#;
fn must_miss(path: &[&str], name: &str) {
let mut value: serde_json::Value = serde_json::from_str(COMPLETE_REGISTRY).unwrap();
serde_json::from_value::<RegistryExport>(value.clone())
.expect("the complete current shape decodes");
let mut slot = &mut value;
for part in path {
slot = match part.parse::<usize>() {
Ok(index) => &mut slot[index],
Err(_) => &mut slot[*part],
};
}
slot.as_object_mut()
.unwrap()
.remove(name)
.unwrap_or_else(|| panic!("{path:?}/{name} is not in the complete shape"));
let error = serde_json::from_value::<RegistryExport>(value)
.err()
.unwrap_or_else(|| panic!("a registry missing {path:?}/{name} decoded"))
.to_string();
assert!(
error.contains(&format!("missing field `{name}`")),
"the decode must fail on the missing {path:?}/{name}, not elsewhere: {error}"
);
}
#[test]
fn a_registry_written_before_the_std_records_existed_fails_to_decode() {
for field in [
"program_options",
"std_objects",
"py_objects",
"py_modules",
"log_warnings",
"dependencies",
"included",
] {
must_miss(&[], field);
}
}
#[test]
fn records_written_before_the_source_field_existed_fail_to_decode() {
must_miss(&["program_options", "0"], "source");
must_miss(&["std_objects", "0"], "source");
must_miss(&["py_objects", "0"], "source");
must_miss(&["py_modules", "0"], "source");
must_miss(&["log_warnings", "0"], "source");
must_miss(&["log_warnings", "0"], "doc2path_location");
}
#[test]
fn document_records_written_before_their_source_field_existed_fail_to_decode() {
fn must_miss<T: serde::de::DeserializeOwned>(complete: &str, stale: &str) {
serde_json::from_str::<T>(complete).expect("the current shape decodes");
let error = serde_json::from_str::<T>(stale)
.err()
.unwrap_or_else(|| panic!("a stale record decoded: {stale}"))
.to_string();
assert!(
error.contains("missing field `source`"),
"the decode must fail on the missing source field, not elsewhere: \
{error} ({stale})"
);
}
must_miss::<DirectiveRecord>(
r#"{"source":1,"name":"note","arguments":[],"options":[],"content":"x","line":3}"#,
r#"{"name":"note","arguments":[],"options":[],"content":"x","line":3}"#,
);
must_miss::<RoleRecord>(
r#"{"source":1,"name":"ref","full_name":"ref","target":"t","display":null,
"line":3}"#,
r#"{"name":"ref","full_name":"ref","target":"t","display":null,"line":3}"#,
);
must_miss::<crate::env::toctree::ToctreeWarning>(
r#"{"source":1,"line":3,"message":"m","category":null,"kind":"MissingDocument"}"#,
r#"{"line":3,"message":"m","category":null,"kind":"MissingDocument"}"#,
);
must_miss::<ToctreeRecord>(
r#"{"glob":false,"entries":[],"source":1,"line":3,"warnings":[]}"#,
r#"{"glob":false,"entries":[],"line":3,"warnings":[]}"#,
);
}
}