1use anyhow::Result;
13use log::debug;
14use pulldown_cmark::{Event, Parser as MarkdownParser, Tag};
15use std::collections::BTreeSet;
16use std::path::Path;
17use std::sync::Arc;
18
19use crate::config::BuildConfig;
20use crate::doctree::{kinds, Doctree, Node, Span};
21use crate::document::{
22 Document, DocumentContent, LabelRecord, MarkdownContent, MarkdownNode, RstContent, TocEntry,
23};
24use crate::rst;
25use crate::utils;
26
27pub struct Parser {
28 exclude_patterns: Vec<String>,
32 py: crate::py::PySigConfig,
36 srcdir: Option<std::path::PathBuf>,
41 source_encoding: String,
43}
44
45pub struct ParsedFile {
50 pub document: Document,
51 pub doctree: Doctree,
52}
53
54impl Parser {
55 pub fn new(config: &BuildConfig) -> Result<Self> {
56 Ok(Self {
57 exclude_patterns: config.exclude_patterns.clone(),
58 py: crate::py::PySigConfig::from(config),
59 srcdir: None,
60 source_encoding: config.source_encoding.clone(),
61 })
62 }
63
64 pub fn with_srcdir(mut self, srcdir: std::path::PathBuf) -> Self {
68 self.srcdir = Some(srcdir);
69 self
70 }
71
72 pub fn parse(&self, file_path: &Path, content: &str, docname: &str) -> Result<Document> {
73 Ok(self.parse_full(file_path, content, docname, None)?.document)
74 }
75
76 pub fn parse_full(
83 &self,
84 file_path: &Path,
85 content: &str,
86 docname: &str,
87 found_docs: Option<Arc<BTreeSet<String>>>,
88 ) -> Result<ParsedFile> {
89 let output_path = self.get_output_path(file_path)?;
90 let mut document = Document::new(file_path.to_path_buf(), output_path);
91
92 document.source_mtime = utils::get_file_mtime(file_path)?;
94
95 let extension = file_path
96 .extension()
97 .and_then(|ext| ext.to_str())
98 .unwrap_or("");
99
100 let doctree = match extension {
101 "rst" => self.parse_rst_into(content, file_path, docname, found_docs, &mut document),
102 "md" => {
103 document.content = self.parse_markdown(content)?;
104 document.title = "Untitled".to_string();
105 empty_doctree()
106 }
107 _ => {
108 document.content = DocumentContent::PlainText(content.to_string());
109 document.title = "Untitled".to_string();
110 empty_doctree()
111 }
112 };
113
114 debug!(
115 "Parsed document: {} ({} chars)",
116 file_path.display(),
117 content.len()
118 );
119
120 Ok(ParsedFile { document, doctree })
121 }
122
123 fn parse_rst_into(
127 &self,
128 content: &str,
129 file_path: &Path,
130 docname: &str,
131 found_docs: Option<Arc<BTreeSet<String>>>,
132 document: &mut Document,
133 ) -> Doctree {
134 let output = rst::parse_rst_full(
135 content,
136 &rst::ParseOptions {
137 source_path: file_path.display().to_string(),
138 sphinx: true,
139 docname: docname.to_string(),
140 found_docs,
141 exclude_patterns: self.exclude_patterns.clone(),
142 py: self.py.clone(),
143 srcdir: self.srcdir.clone(),
144 source_encoding: self.source_encoding.clone(),
145 },
146 );
147 {
148 let root = &output.doctree.root;
149
150 document.title = first_section_title(root).unwrap_or_else(|| "Untitled".to_string());
153
154 let mut toc = Vec::new();
159 collect_toc(root, 1, &mut toc);
160 document.toc = toc;
161
162 let mut labels = Vec::new();
164 collect_labels(root, &mut labels);
165 document.labels = labels;
166 }
167
168 document.toctrees = output.toctrees;
169 document.directive_records = output.directive_records;
170 document.role_records = output.role_records;
171 document.registry = output.registry;
172
173 document.content = DocumentContent::RestructuredText(RstContent {
174 raw: content.to_string(),
175 ast: Vec::new(),
176 directives: Vec::new(),
177 });
178
179 output.doctree
180 }
181
182 fn parse_markdown(&self, content: &str) -> Result<DocumentContent> {
183 let mut nodes = Vec::new();
184 let parser = MarkdownParser::new(content);
185 let current_line = 1;
186
187 for event in parser {
188 match event {
189 Event::Start(Tag::Heading { .. }) => {
190 }
192 Event::End(_) => {
193 }
195 Event::Start(Tag::Paragraph) => {
196 }
198 Event::Start(Tag::CodeBlock(_)) => {
199 }
201 Event::Text(text) => {
202 nodes.push(MarkdownNode::Paragraph {
204 content: text.to_string(),
205 line: current_line,
206 });
207 }
208 Event::Code(_code) => {
209 }
211 _ => {
212 }
214 }
215 }
216
217 Ok(DocumentContent::Markdown(MarkdownContent {
218 raw: content.to_string(),
219 ast: nodes,
220 front_matter: None, }))
222 }
223
224 fn get_output_path(&self, source_path: &Path) -> Result<std::path::PathBuf> {
225 let mut output_path = source_path.to_path_buf();
226 output_path.set_extension("html");
227 Ok(output_path)
228 }
229}
230
231fn empty_doctree() -> Doctree {
237 Doctree {
238 root: Node::elem(kinds::DOCUMENT, Span::ZERO),
239 sources: vec!["<document>".to_string()],
240 }
241}
242
243fn first_section_title(root: &Node) -> Option<String> {
244 for child in &root.children {
245 if child.kind == kinds::SECTION {
246 for c in &child.children {
247 if c.kind == kinds::TITLE {
248 return Some(c.astext());
249 }
250 }
251 }
252 }
253 None
254}
255
256fn collect_toc(node: &Node, level: usize, out: &mut Vec<TocEntry>) {
257 for child in &node.children {
258 if child.kind != kinds::SECTION {
259 continue;
260 }
261 if let Some(title) = child.children.iter().find(|c| c.kind == kinds::TITLE) {
262 out.push(TocEntry {
263 title: title.astext(),
264 level,
265 anchor: child.attrs.ids.first().cloned().unwrap_or_default(),
266 line_number: title.span.line as usize,
267 children: Vec::new(),
268 });
269 }
270 collect_toc(child, level + 1, out);
271 }
272}
273
274fn collect_labels(node: &Node, out: &mut Vec<LabelRecord>) {
275 for child in &node.children {
276 if child.kind == kinds::TARGET && !child.attrs.names.is_empty() {
277 for name in &child.attrs.names {
278 out.push(LabelRecord {
279 name: name.clone(),
280 line: child.span.line as usize,
281 });
282 }
283 }
284 collect_labels(child, out);
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::config::BuildConfig;
292 use crate::doctree::AttrValue;
293
294 fn parse_doc(content: &str) -> Document {
295 let parser = Parser::new(&BuildConfig::default()).unwrap();
296 let mut document = Document::new("test.rst".into(), "test.html".into());
298 parser.parse_rst_into(content, Path::new("test.rst"), "index", None, &mut document);
299 document
300 }
301
302 fn find_by_kind<'a>(node: &'a Node, kind: &str) -> Option<&'a Node> {
304 if node.kind == kind {
305 return Some(node);
306 }
307 node.children.iter().find_map(|c| find_by_kind(c, kind))
308 }
309
310 #[test]
311 fn hyphenated_directive_is_recorded() {
312 let doc = parse_doc(".. code-block:: python\n\n x = 1\n");
313 assert_eq!(doc.directive_records.len(), 1);
314 assert_eq!(doc.directive_records[0].name, "code-block");
315 assert_eq!(doc.directive_records[0].arguments, vec!["python"]);
316 assert_eq!(doc.directive_records[0].content, "x = 1");
317 assert_eq!(doc.directive_records[0].line, 1);
318 }
319
320 #[test]
321 fn domain_directive_is_recorded() {
322 let doc = parse_doc(".. py:function:: foo(x)\n\n Does foo.\n");
323 assert_eq!(doc.directive_records.len(), 1);
324 assert_eq!(doc.directive_records[0].name, "py:function");
325 }
326
327 #[test]
328 fn tab_indented_directive_content_does_not_panic() {
329 let doc = parse_doc(".. note::\n\n\tshort\n");
330 assert_eq!(doc.directive_records.len(), 1);
331 assert_eq!(doc.directive_records[0].content, "short");
332 }
333
334 #[test]
335 fn title_and_toc_from_sections() {
336 let doc = parse_doc("Title\n=====\n\nBody.\n\nSub\n---\n\nMore.\n");
337 assert_eq!(doc.title, "Title");
338 assert_eq!(doc.toc.len(), 2);
339 assert_eq!(doc.toc[0].level, 1);
340 assert_eq!(doc.toc[0].anchor, "title");
341 assert_eq!(doc.toc[1].level, 2);
342 assert_eq!(doc.toc[1].title, "Sub");
343 assert_eq!(doc.toc[1].line_number, 6);
344 }
345
346 #[test]
347 fn toctree_entries_recorded_with_lines() {
348 let doc = parse_doc(
349 "Title\n=====\n\n.. toctree::\n :maxdepth: 2\n :glob:\n\n installation\n Linked <other>\n",
350 );
351 assert_eq!(doc.toctrees.len(), 1);
352 let t = &doc.toctrees[0];
353 assert!(t.glob);
354 assert_eq!(t.entries.len(), 2);
355 assert_eq!(t.entries[0].target, "installation");
356 assert_eq!(t.entries[0].line, 8);
357 assert_eq!(t.entries[1].title.as_deref(), Some("Linked"));
358 assert_eq!(t.entries[1].target, "other");
359 }
360
361 #[test]
362 fn labels_and_roles_recorded() {
363 let doc =
364 parse_doc(".. _setup-label:\n\nSee :ref:`setup-label` and :doc:`installation`.\n");
365 assert_eq!(doc.labels.len(), 1);
366 assert_eq!(doc.labels[0].name, "setup-label");
367 assert_eq!(doc.labels[0].line, 1);
368 let names: Vec<&str> = doc.role_records.iter().map(|r| r.name.as_str()).collect();
369 assert_eq!(names, vec!["ref", "doc"]);
370 assert_eq!(doc.role_records[0].target, "setup-label");
371 assert_eq!(doc.role_records[0].line, 3);
372 }
373
374 #[test]
378 fn real_docname_threads_into_refdoc_and_toctree_parent() {
379 let parser = Parser::new(&BuildConfig::default()).unwrap();
380 let mut document = Document::new("guide/install.rst".into(), "guide/install.html".into());
381 let doctree = parser.parse_rst_into(
382 ".. toctree::\n\n other\n\nSee :doc:`other`.\n",
383 Path::new("guide/install.rst"),
384 "guide/install",
385 None,
386 &mut document,
387 );
388
389 let toctree = find_by_kind(&doctree.root, "toctree").expect("toctree node");
390 assert_eq!(
391 toctree.get("parent"),
392 Some(&AttrValue::Str("guide/install".to_string()))
393 );
394
395 let xref = find_by_kind(&doctree.root, "pending_xref").expect("pending_xref node");
396 assert_eq!(
397 xref.get("refdoc"),
398 Some(&AttrValue::Str("guide/install".to_string()))
399 );
400 }
401
402 #[test]
406 fn registry_export_carries_nameids_with_explicitness() {
407 let output = rst::parse_rst_full(
408 "Section\n=======\n\n.. _tgt:\n\nBody.\n",
409 &rst::ParseOptions {
410 source_path: "<snippet>".to_string(),
411 sphinx: true,
412 docname: "index".to_string(),
413 exclude_patterns: Vec::new(),
414 py: Default::default(),
415 srcdir: None,
416 found_docs: None,
417 ..Default::default()
418 },
419 );
420
421 let tgt = output
422 .registry
423 .nameids
424 .iter()
425 .find(|(name, _, _)| name == "tgt")
426 .expect("tgt registered");
427 assert_eq!(tgt, &("tgt".to_string(), Some("tgt".to_string()), true));
428
429 let section = output
430 .registry
431 .nameids
432 .iter()
433 .find(|(name, _, _)| name == "section")
434 .expect("section registered");
435 assert_eq!(
436 section,
437 &("section".to_string(), Some("section".to_string()), false)
438 );
439 }
440}