Skip to main content

lex_core/lex/ast/elements/
annotation.rs

1//! Annotation
2//!
3//! Annotations are a core element in lex, but not the document's content , rather it's metadata one.
4//! They provide not only a way for authors and collaborators to register non content related
5//! information, but the right hooks for tooling to build on top of lex (e.g., comments, review
6//! metadata, publishing hints).
7//!
8//! As such they provide : -
9//! - labels: a way to identify the annotation
10//! - parameters (optional): a way to provide structured data
11//! - Optional content, like all other elements:
12//!     - Nestable containter that can host any element but sessions
13//!     - Shorthand for for single or no content annotations.
14//!
15//!
16//! Syntax:
17//!   Short Hand Form:
18//!     <lex-marker> <label> <parameters>? <lex-marker>
19//!   Long Hand Form:
20//!     <lex-marker> <label> <parameters>? <lex-marker>
21//!     <indent> <content> ... any number of content elements
22//!     <dedent> <lex-marker>
23//!
24//! Parsing Structure:
25//!
26//! | Element    | Prec. Blank | Head                | Blank | Content | Tail          |
27//! |------------|-------------|---------------------|-------|---------|---------------|
28//! | Annotation | Optional    | DataMarkerLine | Yes   | Yes     | dedent        |
29//!
30//! Special Case: Short form annotations are one-liners without content or dedent.
31//!
32//!  Examples:
33//!      Label only:
34//!         :: image ::  
35//!      Label and parameters:
36//!         :: note severity=high :: Check this carefully
37//!      Marker form (no content):
38//!         :: debug ::
39//!      Parameters augmenting the label:
40//!         :: meta type=python :: (parameters need an accompanying label)
41//!      Long Form:
42//!         :: label ::
43//!             John has reviewed this paragraph. Hence we're only lacking:
44//!             - Janest's approval
45//!             - OK from legal
46//! Learn More:
47//! - The annotation spec: specs/v1/elements/annotation.lex
48//! - The annotation sample: specs/v1/samples/element-based/annotations/annotations.simple.lex
49//! - Labels: specs/v1/elements/label.lex
50//! - Parameters: specs/v1/elements/parameter.lex
51
52use super::super::range::{Position, Range};
53use super::super::traits::{AstNode, Container, Visitor, VisualStructure};
54use super::container::GeneralContainer;
55use super::content_item::ContentItem;
56use super::data::Data;
57use super::label::Label;
58use super::parameter::Parameter;
59use super::typed_content::ContentElement;
60use std::fmt;
61
62/// An annotation represents some metadata about an AST element.
63///
64/// # Reserved label namespace
65///
66/// Labels starting with `lex.` (the `lex.*` namespace, [`Annotation::RESERVED_NAMESPACE_PREFIX`])
67/// are reserved for core-defined semantics. Third-party tooling must not author labels in
68/// this namespace; the core may add new `lex.*` labels without a coordinating versioning
69/// concern. Non-reserved labels remain freely available for extensions
70/// (`mycompany.include`, `docs.embed`, etc.).
71///
72/// The current set of reserved labels:
73/// - [`Annotation::INCLUDE_LABEL`] (`"lex.include"`) — see `comms/specs/proposals/includes.lex`.
74#[derive(Debug, Clone, PartialEq)]
75pub struct Annotation {
76    pub data: Data,
77    pub children: GeneralContainer,
78    pub location: Range,
79}
80
81impl Annotation {
82    /// Reserved label prefix for core-defined annotation semantics.
83    ///
84    /// Any annotation whose label starts with this prefix is owned by the Lex
85    /// core and may carry behavior in the resolver / analysis layers. External
86    /// authors should pick a different namespace.
87    pub const RESERVED_NAMESPACE_PREFIX: &'static str = "lex.";
88
89    /// Reserved label for the include directive.
90    ///
91    /// An annotation with this label and a `src=` parameter is interpreted by
92    /// `lex_core::includes` as a request to splice another Lex file's content
93    /// into the parent container at the annotation's position. See
94    /// `comms/specs/proposals/includes.lex` for the full design.
95    pub const INCLUDE_LABEL: &'static str = "lex.include";
96
97    fn default_location() -> Range {
98        Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
99    }
100    pub fn new(label: Label, parameters: Vec<Parameter>, children: Vec<ContentElement>) -> Self {
101        let data = Data::new(label, parameters);
102        Self::from_data(data, children)
103    }
104    pub fn marker(label: Label) -> Self {
105        Self::from_data(Data::new(label, Vec::new()), Vec::new())
106    }
107    pub fn with_parameters(label: Label, parameters: Vec<Parameter>) -> Self {
108        Self::from_data(Data::new(label, parameters), Vec::new())
109    }
110    pub fn from_data(data: Data, children: Vec<ContentElement>) -> Self {
111        Self {
112            data,
113            children: GeneralContainer::from_typed(children),
114            location: Self::default_location(),
115        }
116    }
117
118    /// Preferred builder
119    pub fn at(mut self, location: Range) -> Self {
120        self.location = location;
121        self
122    }
123
124    /// Range covering only the annotation header (label + parameters).
125    pub fn header_location(&self) -> &Range {
126        &self.data.location
127    }
128
129    /// Bounding range covering only the annotation's children.
130    pub fn body_location(&self) -> Option<Range> {
131        Range::bounding_box(self.children.iter().map(|item| item.range()))
132    }
133
134    /// Whether this annotation's label is in the reserved `lex.*` namespace.
135    pub fn is_reserved(&self) -> bool {
136        self.data
137            .label
138            .value
139            .starts_with(Self::RESERVED_NAMESPACE_PREFIX)
140    }
141
142    /// Whether this annotation is the include directive (label `lex.include`).
143    ///
144    /// Hides the string-match on the reserved label so callers don't sprinkle
145    /// `annotation.label == "lex.include"` throughout the codebase. Also serves
146    /// as the migration boundary if a future version models includes as a
147    /// distinct AST node type.
148    pub fn is_include(&self) -> bool {
149        self.data.label.value == Self::INCLUDE_LABEL
150    }
151
152    /// The `src=` parameter value with surrounding quotes stripped and
153    /// escape sequences resolved, if present.
154    ///
155    /// Useful on its own for any annotation that uses a `src` parameter
156    /// (verbatim-via-annotation, future `lex.*` directives, etc.). For
157    /// the include-specific case, callers typically pair this with
158    /// [`Annotation::is_include`].
159    ///
160    /// Returns an owned `String` rather than `&str` because escape
161    /// resolution may need to allocate. For quote-free values (the common
162    /// case) the returned string is the same as `parameter.value` minus
163    /// the leading/trailing `"`.
164    pub fn include_src(&self) -> Option<String> {
165        self.data
166            .parameters
167            .iter()
168            .find(|p| p.key == "src")
169            .map(|p| p.unquoted_value())
170    }
171}
172
173impl AstNode for Annotation {
174    fn node_type(&self) -> &'static str {
175        "Annotation"
176    }
177    fn display_label(&self) -> String {
178        if self.data.parameters.is_empty() {
179            self.data.label.value.clone()
180        } else {
181            format!(
182                "{} ({} params)",
183                self.data.label.value,
184                self.data.parameters.len()
185            )
186        }
187    }
188    fn range(&self) -> &Range {
189        &self.location
190    }
191
192    fn accept(&self, visitor: &mut dyn Visitor) {
193        visitor.visit_annotation(self);
194        super::super::traits::visit_children(visitor, &self.children);
195        visitor.leave_annotation(self);
196    }
197}
198
199impl VisualStructure for Annotation {
200    fn is_source_line_node(&self) -> bool {
201        true
202    }
203
204    fn has_visual_header(&self) -> bool {
205        true
206    }
207}
208
209impl Container for Annotation {
210    fn label(&self) -> &str {
211        &self.data.label.value
212    }
213    fn children(&self) -> &[ContentItem] {
214        &self.children
215    }
216    fn children_mut(&mut self) -> &mut Vec<ContentItem> {
217        self.children.as_mut_vec()
218    }
219}
220
221impl fmt::Display for Annotation {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(
224            f,
225            "Annotation('{}', {} params, {} items)",
226            self.data.label.value,
227            self.data.parameters.len(),
228            self.children.len()
229        )
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::lex::ast::elements::paragraph::Paragraph;
237    use crate::lex::ast::elements::typed_content::ContentElement;
238
239    #[test]
240    fn test_annotation_header_and_body_locations() {
241        let header_range = Range::new(0..4, Position::new(0, 0), Position::new(0, 4));
242        let child_range = Range::new(10..20, Position::new(1, 0), Position::new(2, 0));
243        let label = Label::new("note".to_string()).at(header_range.clone());
244        let data = Data::new(label, Vec::new()).at(header_range.clone());
245        let child = ContentElement::Paragraph(
246            Paragraph::from_line("body".to_string()).at(child_range.clone()),
247        );
248
249        let annotation = Annotation::from_data(data, vec![child]).at(Range::new(
250            0..25,
251            Position::new(0, 0),
252            Position::new(2, 0),
253        ));
254
255        assert_eq!(annotation.header_location().span, header_range.span);
256        assert_eq!(annotation.body_location().unwrap().span, child_range.span);
257    }
258
259    fn ann(label: &str, params: Vec<(&str, &str)>) -> Annotation {
260        let parameters = params
261            .into_iter()
262            .map(|(k, v)| Parameter::new(k.to_string(), v.to_string()))
263            .collect();
264        Annotation::with_parameters(Label::new(label.to_string()), parameters)
265    }
266
267    #[test]
268    fn test_is_reserved() {
269        assert!(ann("lex.include", vec![]).is_reserved());
270        assert!(ann("lex.foo.bar", vec![]).is_reserved());
271        // Boundary: a label that starts with "lex" but not "lex." is NOT reserved.
272        assert!(!ann("lexicon", vec![]).is_reserved());
273        assert!(!ann("review", vec![]).is_reserved());
274        assert!(!ann("mycompany.include", vec![]).is_reserved());
275    }
276
277    #[test]
278    fn test_is_include() {
279        assert!(ann("lex.include", vec![("src", "x.lex")]).is_include());
280        // Other lex.* labels are reserved but not includes.
281        assert!(!ann("lex.something_else", vec![]).is_include());
282        // Same trailing label without the lex. prefix is not an include.
283        assert!(!ann("include", vec![("src", "x.lex")]).is_include());
284    }
285
286    #[test]
287    fn test_include_src() {
288        let with_src = ann("lex.include", vec![("src", "chapters/01.lex")]);
289        assert_eq!(with_src.include_src().as_deref(), Some("chapters/01.lex"));
290
291        // The accessor is independent of the label — works for any annotation
292        // that happens to carry a `src` parameter (verbatim-via-annotation, etc.)
293        let other_with_src = ann("image", vec![("src", "diagram.png")]);
294        assert_eq!(other_with_src.include_src().as_deref(), Some("diagram.png"));
295
296        // No src parameter → None.
297        assert_eq!(ann("lex.include", vec![]).include_src(), None);
298        assert_eq!(
299            ann("lex.include", vec![("title", "Chapter 1")]).include_src(),
300            None
301        );
302    }
303
304    #[test]
305    fn test_include_src_strips_quotes_from_parsed_value() {
306        // When the parser stores a quoted string, include_src should return
307        // the unquoted form so callers (the resolver) can use it as a path.
308        let with_quoted = ann("lex.include", vec![("src", "\"chapters/01.lex\"")]);
309        assert_eq!(
310            with_quoted.include_src().as_deref(),
311            Some("chapters/01.lex")
312        );
313    }
314
315    #[test]
316    fn test_constants_match_documented_values() {
317        assert_eq!(Annotation::RESERVED_NAMESPACE_PREFIX, "lex.");
318        assert_eq!(Annotation::INCLUDE_LABEL, "lex.include");
319        // Sanity: the include label is itself reserved.
320        assert!(Annotation::INCLUDE_LABEL.starts_with(Annotation::RESERVED_NAMESPACE_PREFIX));
321    }
322}