Skip to main content

quarto_source_map/
file_origin.rs

1//! Structured provenance for virtual files
2//!
3//! Some files registered in a [`SourceContext`](crate::SourceContext) are
4//! *virtual*: their content was extracted from another file — a notebook
5//! cell from a `.ipynb`, a region of a larger document. [`FileOrigin`]
6//! records that relationship on the file's
7//! [`FileMetadata`](crate::FileMetadata) so consumers can address the
8//! position the author knows (the owning cell), hyperlink the real file
9//! on disk, and emit structured location data — instead of encoding all
10//! of that into a synthetic file path.
11
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15/// Where a virtual file's content really comes from.
16///
17/// Attached to a file via `FileMetadata::origin`. The canonical display
18/// label (via [`fmt::Display`]) is what diagnostics show the user; the
19/// structured fields are what JSON consumers get.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23pub enum FileOrigin {
24    /// Content extracted from one cell of a computational notebook.
25    NotebookCell {
26        /// Path of the notebook file on disk — the hyperlink target and
27        /// the `file` reported in structured output.
28        notebook_path: String,
29        /// 1-based index of the cell within the notebook, counted over
30        /// all cells regardless of type.
31        cell_index: usize,
32        /// The cell's `id` field (nbformat ≥ 4.5), when present.
33        /// Displayed in structured output only, never in text labels.
34        cell_id: Option<String>,
35        /// The nbformat cell type (`"code"`, `"markdown"`, `"raw"`).
36        cell_type: String,
37    },
38}
39
40impl fmt::Display for FileOrigin {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            FileOrigin::NotebookCell {
44                notebook_path,
45                cell_index,
46                cell_type,
47                ..
48            } => write!(f, "{notebook_path}[cell {cell_index}, {cell_type}]"),
49        }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn origin() -> FileOrigin {
58        FileOrigin::NotebookCell {
59            notebook_path: "notebook.ipynb".into(),
60            cell_index: 2,
61            cell_id: Some("abc123".into()),
62            cell_type: "markdown".into(),
63        }
64    }
65
66    #[test]
67    fn display_is_the_cell_qualified_label() {
68        assert_eq!(origin().to_string(), "notebook.ipynb[cell 2, markdown]");
69    }
70
71    #[test]
72    fn display_omits_cell_id() {
73        // cell.id is structured-output-only; the text label must not
74        // change when a notebook gains or loses nbformat 4.5 ids.
75        let no_id = FileOrigin::NotebookCell {
76            notebook_path: "notebook.ipynb".into(),
77            cell_index: 2,
78            cell_id: None,
79            cell_type: "markdown".into(),
80        };
81        assert_eq!(no_id.to_string(), origin().to_string());
82    }
83
84    #[test]
85    fn serde_round_trip_keeps_all_fields() {
86        let json = serde_json::to_string(&origin()).unwrap();
87        assert_eq!(
88            json,
89            r#"{"kind":"notebook_cell","notebook_path":"notebook.ipynb","cell_index":2,"cell_id":"abc123","cell_type":"markdown"}"#
90        );
91        let back: FileOrigin = serde_json::from_str(&json).unwrap();
92        assert_eq!(back, origin());
93    }
94
95    #[test]
96    fn serde_round_trip_without_cell_id() {
97        let o = FileOrigin::NotebookCell {
98            notebook_path: "n.ipynb".into(),
99            cell_index: 7,
100            cell_id: None,
101            cell_type: "code".into(),
102        };
103        let back: FileOrigin = serde_json::from_str(&serde_json::to_string(&o).unwrap()).unwrap();
104        assert_eq!(back, o);
105    }
106}