1pub mod academic;
28mod collaboration;
29mod forms;
30pub mod legal;
31pub mod phantom;
32mod semantic;
33
34pub use collaboration::{
35 ChangeStatus, ChangeTracking, ChangeType, CollaborationSession, Collaborator, Comment,
36 CommentThread, CommentType, CrdtFormat, CrdtMetadata, CursorPosition, HighlightColor,
37 MaterializationEvent, MaterializationReason, Participant, Peer, Priority, Revision,
38 RevisionHistory, Selection, SessionStatus, SuggestionStatus, SyncState, TextCrdtMetadata,
39 TextCrdtPosition, TextRange, TrackedChange,
40};
41pub use forms::{
42 CheckboxField, Condition, ConditionOperator, ConditionalAction, ConditionalValidation,
43 DatePickerField, DropdownField, DropdownOption, FormData, FormField, FormValidation,
44 RadioGroupField, RadioOption, SignatureField, TextAreaField, TextInputField, ValidationRule,
45};
46pub use phantom::{
47 ConnectionStyle, Phantom, PhantomCluster, PhantomClusters, PhantomConnection, PhantomContent,
48 PhantomPosition, PhantomScope, PhantomSize,
49};
50pub use semantic::{
51 Author, Bibliography, BibliographyEntry, Citation, CitationStyle, EntityLink, EntityType,
52 EntryType, Footnote, Glossary, GlossaryRef, GlossaryTerm, JsonLdMetadata, KnowledgeBase,
53 LocatorType, PartialDate,
54};
55
56use serde::{Deserialize, Serialize};
57use serde_json::Value;
58
59use crate::content::Block;
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct ExtensionBlock {
71 pub namespace: String,
73
74 pub block_type: String,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub id: Option<String>,
80
81 #[serde(default, skip_serializing_if = "Value::is_null")]
85 pub attributes: Value,
86
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub children: Vec<Block>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub fallback: Option<Box<Block>>,
94}
95
96impl ExtensionBlock {
97 #[must_use]
99 pub fn new(namespace: impl Into<String>, block_type: impl Into<String>) -> Self {
100 Self {
101 namespace: namespace.into(),
102 block_type: block_type.into(),
103 id: None,
104 attributes: Value::Null,
105 children: Vec::new(),
106 fallback: None,
107 }
108 }
109
110 #[must_use]
114 pub fn parse_type(type_str: &str) -> Option<(&str, &str)> {
115 type_str.split_once(':')
116 }
117
118 #[must_use]
120 pub fn full_type(&self) -> String {
121 format!("{}:{}", self.namespace, self.block_type)
122 }
123
124 #[must_use]
126 pub fn is_namespace(&self, namespace: &str) -> bool {
127 self.namespace == namespace
128 }
129
130 #[must_use]
132 pub fn is_type(&self, namespace: &str, block_type: &str) -> bool {
133 self.namespace == namespace && self.block_type == block_type
134 }
135
136 #[must_use]
138 pub fn with_id(mut self, id: impl Into<String>) -> Self {
139 self.id = Some(id.into());
140 self
141 }
142
143 #[must_use]
145 pub fn with_attributes(mut self, attributes: Value) -> Self {
146 self.attributes = attributes;
147 self
148 }
149
150 #[must_use]
152 pub fn with_children(mut self, children: Vec<Block>) -> Self {
153 self.children = children;
154 self
155 }
156
157 #[must_use]
159 pub fn with_fallback(mut self, fallback: Block) -> Self {
160 self.fallback = Some(Box::new(fallback));
161 self
162 }
163
164 #[must_use]
166 pub fn fallback_content(&self) -> Option<&Block> {
167 self.fallback.as_deref()
168 }
169
170 #[must_use]
172 pub fn get_attribute(&self, key: &str) -> Option<&Value> {
173 self.attributes.get(key)
174 }
175
176 #[must_use]
178 pub fn get_string_attribute(&self, key: &str) -> Option<&str> {
179 self.attributes.get(key).and_then(Value::as_str)
180 }
181
182 #[must_use]
184 pub fn get_string_array_attribute(&self, key: &str) -> Option<Vec<&str>> {
185 self.attributes.get(key).and_then(|v| {
186 v.as_array()
187 .map(|arr| arr.iter().filter_map(serde_json::Value::as_str).collect())
188 })
189 }
190
191 #[must_use]
193 pub fn get_bool_attribute(&self, key: &str) -> Option<bool> {
194 self.attributes.get(key).and_then(Value::as_bool)
195 }
196
197 #[must_use]
199 pub fn get_i64_attribute(&self, key: &str) -> Option<i64> {
200 self.attributes.get(key).and_then(Value::as_i64)
201 }
202
203 #[must_use]
207 pub fn as_form_field(&self) -> Option<FormField> {
208 if self.namespace != "forms" {
209 return None;
210 }
211 FormField::from_extension(self)
212 }
213}
214
215pub mod namespaces {
217 pub const FORMS: &str = "forms";
219 pub const SEMANTIC: &str = "semantic";
221 pub const COLLABORATION: &str = "collaboration";
223 pub const ACADEMIC: &str = "academic";
225 pub const LEGAL: &str = "legal";
227 pub const PRESENTATION: &str = "presentation";
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use serde_json::json;
235
236 #[test]
237 fn test_extension_block_new() {
238 let ext = ExtensionBlock::new("forms", "textInput");
239 assert_eq!(ext.namespace, "forms");
240 assert_eq!(ext.block_type, "textInput");
241 assert_eq!(ext.full_type(), "forms:textInput");
242 }
243
244 #[test]
245 fn test_parse_type() {
246 assert_eq!(
247 ExtensionBlock::parse_type("forms:textInput"),
248 Some(("forms", "textInput"))
249 );
250 assert_eq!(
251 ExtensionBlock::parse_type("semantic:citation"),
252 Some(("semantic", "citation"))
253 );
254 assert_eq!(ExtensionBlock::parse_type("paragraph"), None);
255 }
256
257 #[test]
258 fn test_extension_block_builder() {
259 let ext = ExtensionBlock::new("forms", "checkbox")
260 .with_id("accept-terms")
261 .with_attributes(json!({
262 "label": "I accept the terms",
263 "required": true
264 }));
265
266 assert_eq!(ext.id, Some("accept-terms".to_string()));
267 assert_eq!(
268 ext.get_string_attribute("label"),
269 Some("I accept the terms")
270 );
271 assert_eq!(ext.get_bool_attribute("required"), Some(true));
272 }
273
274 #[test]
275 fn test_extension_namespace_check() {
276 let ext = ExtensionBlock::new("forms", "textInput");
277 assert!(ext.is_namespace("forms"));
278 assert!(!ext.is_namespace("semantic"));
279 assert!(ext.is_type("forms", "textInput"));
280 assert!(!ext.is_type("forms", "checkbox"));
281 }
282
283 #[test]
284 fn test_extension_with_fallback() {
285 use crate::content::Text;
286
287 let fallback = Block::paragraph(vec![Text::plain("[Form field: Name]")]);
288 let ext = ExtensionBlock::new("forms", "textInput").with_fallback(fallback.clone());
289
290 assert!(ext.fallback_content().is_some());
291 if let Block::Paragraph { children, .. } = ext.fallback_content().unwrap() {
292 assert_eq!(children[0].value, "[Form field: Name]");
293 }
294 }
295
296 #[test]
297 fn test_extension_serialization() {
298 let ext = ExtensionBlock::new("forms", "textInput")
299 .with_id("name")
300 .with_attributes(json!({"label": "Name", "required": true}));
301
302 let json = serde_json::to_string_pretty(&ext).unwrap();
303 assert!(json.contains("\"namespace\": \"forms\""));
304 assert!(json.contains("\"blockType\": \"textInput\""));
305 assert!(json.contains("\"label\": \"Name\""));
306 }
307
308 #[test]
309 fn test_extension_deserialization() {
310 let json = r#"{
311 "namespace": "semantic",
312 "blockType": "citation",
313 "id": "cite-1",
314 "attributes": {
315 "refs": ["smith2023"],
316 "page": 42
317 }
318 }"#;
319
320 let ext: ExtensionBlock = serde_json::from_str(json).unwrap();
321 assert_eq!(ext.namespace, "semantic");
322 assert_eq!(ext.block_type, "citation");
323 assert_eq!(ext.id, Some("cite-1".to_string()));
324 assert_eq!(
325 ext.get_string_array_attribute("refs"),
326 Some(vec!["smith2023"])
327 );
328 assert_eq!(ext.get_i64_attribute("page"), Some(42));
329 }
330}