Skip to main content

cdx_core/extensions/semantic/
glossary.rs

1//! Glossary and term definitions for documents.
2
3use serde::{Deserialize, Serialize};
4
5/// A glossary containing term definitions.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Glossary {
9    /// Glossary terms.
10    pub terms: Vec<GlossaryTerm>,
11}
12
13impl Glossary {
14    /// Create a new empty glossary.
15    #[must_use]
16    pub fn new() -> Self {
17        Self { terms: Vec::new() }
18    }
19
20    /// Add a term.
21    pub fn add_term(&mut self, term: GlossaryTerm) {
22        self.terms.push(term);
23    }
24
25    /// Find a term by its ID.
26    #[must_use]
27    pub fn get(&self, id: &str) -> Option<&GlossaryTerm> {
28        self.terms.iter().find(|t| t.id == id)
29    }
30
31    /// Find terms by text (case-insensitive).
32    #[must_use]
33    pub fn find_by_text(&self, text: &str) -> Option<&GlossaryTerm> {
34        let lower = text.to_lowercase();
35        self.terms.iter().find(|t| {
36            t.term.to_lowercase() == lower || t.aliases.iter().any(|a| a.to_lowercase() == lower)
37        })
38    }
39
40    /// Get the number of terms.
41    #[must_use]
42    pub fn len(&self) -> usize {
43        self.terms.len()
44    }
45
46    /// Check if the glossary is empty.
47    #[must_use]
48    pub fn is_empty(&self) -> bool {
49        self.terms.is_empty()
50    }
51}
52
53impl Default for Glossary {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59/// A glossary term definition.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct GlossaryTerm {
63    /// Unique identifier.
64    pub id: String,
65
66    /// The term being defined.
67    pub term: String,
68
69    /// Definition text.
70    pub definition: String,
71
72    /// Alternative forms or spellings.
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub aliases: Vec<String>,
75
76    /// Related terms (by ID).
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub see_also: Vec<String>,
79
80    /// Category or subject area.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub category: Option<String>,
83
84    /// Pronunciation guide.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub pronunciation: Option<String>,
87
88    /// Etymology or origin.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub etymology: Option<String>,
91
92    /// Usage notes.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub usage: Option<String>,
95}
96
97impl GlossaryTerm {
98    /// Create a new glossary term.
99    #[must_use]
100    pub fn new(
101        id: impl Into<String>,
102        term: impl Into<String>,
103        definition: impl Into<String>,
104    ) -> Self {
105        Self {
106            id: id.into(),
107            term: term.into(),
108            definition: definition.into(),
109            aliases: Vec::new(),
110            see_also: Vec::new(),
111            category: None,
112            pronunciation: None,
113            etymology: None,
114            usage: None,
115        }
116    }
117
118    /// Add an alias.
119    #[must_use]
120    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
121        self.aliases.push(alias.into());
122        self
123    }
124
125    /// Add a "see also" reference.
126    #[must_use]
127    pub fn with_see_also(mut self, term_id: impl Into<String>) -> Self {
128        self.see_also.push(term_id.into());
129        self
130    }
131
132    /// Set category.
133    #[must_use]
134    pub fn with_category(mut self, category: impl Into<String>) -> Self {
135        self.category = Some(category.into());
136        self
137    }
138
139    /// Set pronunciation.
140    #[must_use]
141    pub fn with_pronunciation(mut self, pronunciation: impl Into<String>) -> Self {
142        self.pronunciation = Some(pronunciation.into());
143        self
144    }
145}
146
147/// A reference to a glossary term in the document.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct GlossaryRef {
150    /// ID of the glossary term.
151    #[serde(rename = "ref", alias = "termId")]
152    pub term_id: String,
153
154    /// Display text (if different from term).
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub display: Option<String>,
157}
158
159impl GlossaryRef {
160    /// Create a new glossary reference.
161    #[must_use]
162    pub fn new(term_id: impl Into<String>) -> Self {
163        Self {
164            term_id: term_id.into(),
165            display: None,
166        }
167    }
168
169    /// Set custom display text.
170    #[must_use]
171    pub fn with_display(mut self, display: impl Into<String>) -> Self {
172        self.display = Some(display.into());
173        self
174    }
175}