Skip to main content

mq_db/
block.rs

1use rustc_hash::FxHashMap;
2
3/// Unique identifier for a block within a document.
4pub type BlockId = u32;
5
6/// Unique identifier for a document within the store.
7pub type DocumentId = u32;
8
9/// Line/column span in the source document (1-based, matching mq-markdown).
10#[derive(Debug, Clone, PartialEq)]
11pub struct Span {
12    pub start_line: usize,
13    pub start_col: usize,
14    pub end_line: usize,
15    pub end_col: usize,
16}
17
18/// Strongly-typed block categories mapping directly to Markdown constructs.
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum BlockType {
21    /// Heading (H1–H6). The `depth` property carries the level (1–6).
22    Heading,
23    /// Regular paragraph / inline text content (Text, Emphasis, Strong, etc.).
24    Paragraph,
25    /// Fenced or indented code block. The `lang` property carries the language.
26    Code,
27    /// List item (ordered or unordered).
28    List,
29    /// Table cell.
30    TableCell,
31    /// Table row.
32    TableRow,
33    /// Table alignment row.
34    TableAlign,
35    /// Block quote.
36    Blockquote,
37    /// Horizontal rule (`---`).
38    HorizontalRule,
39    /// Raw HTML block.
40    Html,
41    /// YAML front-matter. Properties are populated from parsed YAML keys.
42    Yaml,
43    /// TOML front-matter.
44    Toml,
45    /// Math block (`$$...$$`).
46    Math,
47    /// Link definition (`[id]: url`).
48    Definition,
49    /// Footnote definition (`[^id]: ...`).
50    Footnote,
51}
52
53impl BlockType {
54    pub fn as_str(&self) -> &'static str {
55        match self {
56            BlockType::Heading => "heading",
57            BlockType::Paragraph => "paragraph",
58            BlockType::Code => "code",
59            BlockType::List => "list",
60            BlockType::TableCell => "table_cell",
61            BlockType::TableRow => "table_row",
62            BlockType::TableAlign => "table_align",
63            BlockType::Blockquote => "blockquote",
64            BlockType::HorizontalRule => "horizontal_rule",
65            BlockType::Html => "html",
66            BlockType::Yaml => "yaml",
67            BlockType::Toml => "toml",
68            BlockType::Math => "math",
69            BlockType::Definition => "definition",
70            BlockType::Footnote => "footnote",
71        }
72    }
73}
74
75impl std::fmt::Display for BlockType {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.as_str())
78    }
79}
80
81/// A row-polymorphic property value – any attribute that varies by block type.
82///
83/// Common keys per block type:
84/// - `Heading`  → `"depth": Int`, `"slug": String`
85/// - `Code`     → `"lang": String`, `"meta": String`, `"fence": Bool`
86/// - `List`     → `"ordered": Bool`, `"level": Int`, `"checked": Bool`
87/// - `TableCell`→ `"row": Int`, `"column": Int`
88/// - `Yaml`     → parsed frontmatter keys (e.g. `"title"`, `"tags"`)
89#[derive(Debug, Clone, PartialEq)]
90pub enum PropertyValue {
91    String(String),
92    Int(i64),
93    Float(f64),
94    Bool(bool),
95    Array(Vec<PropertyValue>),
96    Null,
97}
98
99impl PropertyValue {
100    pub fn as_str(&self) -> Option<&str> {
101        if let Self::String(s) = self {
102            Some(s)
103        } else {
104            None
105        }
106    }
107
108    pub fn as_int(&self) -> Option<i64> {
109        if let Self::Int(i) = self {
110            Some(*i)
111        } else {
112            None
113        }
114    }
115
116    pub fn as_bool(&self) -> Option<bool> {
117        if let Self::Bool(b) = self {
118            Some(*b)
119        } else {
120            None
121        }
122    }
123
124    pub fn as_array(&self) -> Option<&[PropertyValue]> {
125        if let Self::Array(a) = self {
126            Some(a)
127        } else {
128            None
129        }
130    }
131}
132
133impl From<String> for PropertyValue {
134    fn from(s: String) -> Self {
135        Self::String(s)
136    }
137}
138
139impl From<&str> for PropertyValue {
140    fn from(s: &str) -> Self {
141        Self::String(s.to_string())
142    }
143}
144
145impl From<i64> for PropertyValue {
146    fn from(i: i64) -> Self {
147        Self::Int(i)
148    }
149}
150
151impl From<u8> for PropertyValue {
152    fn from(i: u8) -> Self {
153        Self::Int(i as i64)
154    }
155}
156
157impl From<u32> for PropertyValue {
158    fn from(i: u32) -> Self {
159        Self::Int(i as i64)
160    }
161}
162
163impl From<usize> for PropertyValue {
164    fn from(i: usize) -> Self {
165        Self::Int(i as i64)
166    }
167}
168
169impl From<bool> for PropertyValue {
170    fn from(b: bool) -> Self {
171        Self::Bool(b)
172    }
173}
174
175/// Open-record property bag – acts like extra virtual columns in a
176/// row-polymorphic schema. Keyed by string name, typed by [`PropertyValue`].
177#[derive(Debug, Clone, Default, PartialEq)]
178pub struct Properties(FxHashMap<String, PropertyValue>);
179
180impl Properties {
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    pub fn get(&self, key: &str) -> Option<&PropertyValue> {
186        self.0.get(key)
187    }
188
189    pub fn set(&mut self, key: impl Into<String>, value: impl Into<PropertyValue>) {
190        self.0.insert(key.into(), value.into());
191    }
192
193    pub fn contains_key(&self, key: &str) -> bool {
194        self.0.contains_key(key)
195    }
196
197    pub fn iter(&self) -> impl Iterator<Item = (&String, &PropertyValue)> {
198        self.0.iter()
199    }
200}
201
202/// A single logical block extracted from a Markdown document.
203///
204/// # Interval Index
205///
206/// The `pre` and `post` fields implement the **Nested Set / Pre-Post Order**
207/// interval index for the *section hierarchy* (defined by headings).
208///
209/// A block `A` is a descendant of block `B` iff:
210/// ```text
211/// B.pre < A.pre  &&  A.post < B.post
212/// ```
213/// This reduces ancestor/descendant checks to O(1) integer comparisons,
214/// eliminating recursive tree traversal entirely.
215#[derive(Debug, Clone, PartialEq)]
216pub struct Block {
217    pub id: BlockId,
218    pub document_id: DocumentId,
219    pub block_type: BlockType,
220    /// Plain-text content rendered from the block (without markup).
221    pub content: String,
222    /// Source location (line/column, 1-based). `None` for synthetic blocks.
223    pub span: Option<Span>,
224    /// Pre-order DFS number – left boundary of the section interval.
225    pub pre: u32,
226    /// Post-order DFS number – right boundary of the section interval.
227    pub post: u32,
228    /// Row-polymorphic extra attributes; keyed by property name.
229    pub properties: Properties,
230}
231
232impl Block {
233    /// Returns `true` if `other` is a descendant of `self` in the section
234    /// hierarchy (i.e. `other` is "under" `self`).
235    ///
236    /// Uses the interval index for O(1) comparison.
237    pub fn contains(&self, other: &Block) -> bool {
238        self.pre < other.pre && other.post < self.post
239    }
240
241    /// Returns `true` if `self` is a descendant of `ancestor`.
242    pub fn is_under(&self, ancestor: &Block) -> bool {
243        ancestor.pre < self.pre && self.post < ancestor.post
244    }
245
246    /// Returns `true` if `self` falls within the given ancestor interval
247    /// `(anc_pre, anc_post)`.
248    pub fn is_under_interval(&self, anc_pre: u32, anc_post: u32) -> bool {
249        anc_pre < self.pre && self.post < anc_post
250    }
251
252    /// Heading depth (1–6), or `None` for non-heading blocks.
253    pub fn heading_depth(&self) -> Option<u8> {
254        self.properties.get("depth")?.as_int().map(|d| d as u8)
255    }
256
257    /// Code language tag (e.g. `"rust"`), or `None` for non-code blocks.
258    pub fn code_lang(&self) -> Option<&str> {
259        self.properties.get("lang")?.as_str()
260    }
261}