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_bool_attribute(&self, key: &str) -> Option<bool> {
185 self.attributes.get(key).and_then(Value::as_bool)
186 }
187
188 #[must_use]
190 pub fn get_i64_attribute(&self, key: &str) -> Option<i64> {
191 self.attributes.get(key).and_then(Value::as_i64)
192 }
193
194 #[must_use]
198 pub fn as_form_field(&self) -> Option<FormField> {
199 if self.namespace != "forms" {
200 return None;
201 }
202 FormField::from_extension(self)
203 }
204}
205
206pub mod namespaces {
208 pub const FORMS: &str = "forms";
210 pub const SEMANTIC: &str = "semantic";
212 pub const COLLABORATION: &str = "collaboration";
214 pub const ACADEMIC: &str = "academic";
216 pub const LEGAL: &str = "legal";
218 pub const PRESENTATION: &str = "presentation";
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use serde_json::json;
226
227 #[test]
228 fn test_extension_block_new() {
229 let ext = ExtensionBlock::new("forms", "textInput");
230 assert_eq!(ext.namespace, "forms");
231 assert_eq!(ext.block_type, "textInput");
232 assert_eq!(ext.full_type(), "forms:textInput");
233 }
234
235 #[test]
236 fn test_parse_type() {
237 assert_eq!(
238 ExtensionBlock::parse_type("forms:textInput"),
239 Some(("forms", "textInput"))
240 );
241 assert_eq!(
242 ExtensionBlock::parse_type("semantic:citation"),
243 Some(("semantic", "citation"))
244 );
245 assert_eq!(ExtensionBlock::parse_type("paragraph"), None);
246 }
247
248 #[test]
249 fn test_extension_block_builder() {
250 let ext = ExtensionBlock::new("forms", "checkbox")
251 .with_id("accept-terms")
252 .with_attributes(json!({
253 "label": "I accept the terms",
254 "required": true
255 }));
256
257 assert_eq!(ext.id, Some("accept-terms".to_string()));
258 assert_eq!(
259 ext.get_string_attribute("label"),
260 Some("I accept the terms")
261 );
262 assert_eq!(ext.get_bool_attribute("required"), Some(true));
263 }
264
265 #[test]
266 fn test_extension_namespace_check() {
267 let ext = ExtensionBlock::new("forms", "textInput");
268 assert!(ext.is_namespace("forms"));
269 assert!(!ext.is_namespace("semantic"));
270 assert!(ext.is_type("forms", "textInput"));
271 assert!(!ext.is_type("forms", "checkbox"));
272 }
273
274 #[test]
275 fn test_extension_with_fallback() {
276 use crate::content::Text;
277
278 let fallback = Block::paragraph(vec![Text::plain("[Form field: Name]")]);
279 let ext = ExtensionBlock::new("forms", "textInput").with_fallback(fallback.clone());
280
281 assert!(ext.fallback_content().is_some());
282 if let Block::Paragraph { children, .. } = ext.fallback_content().unwrap() {
283 assert_eq!(children[0].value, "[Form field: Name]");
284 }
285 }
286
287 #[test]
288 fn test_extension_serialization() {
289 let ext = ExtensionBlock::new("forms", "textInput")
290 .with_id("name")
291 .with_attributes(json!({"label": "Name", "required": true}));
292
293 let json = serde_json::to_string_pretty(&ext).unwrap();
294 assert!(json.contains("\"namespace\": \"forms\""));
295 assert!(json.contains("\"blockType\": \"textInput\""));
296 assert!(json.contains("\"label\": \"Name\""));
297 }
298
299 #[test]
300 fn test_extension_deserialization() {
301 let json = r#"{
302 "namespace": "semantic",
303 "blockType": "citation",
304 "id": "cite-1",
305 "attributes": {
306 "ref": "smith2023",
307 "page": 42
308 }
309 }"#;
310
311 let ext: ExtensionBlock = serde_json::from_str(json).unwrap();
312 assert_eq!(ext.namespace, "semantic");
313 assert_eq!(ext.block_type, "citation");
314 assert_eq!(ext.id, Some("cite-1".to_string()));
315 assert_eq!(ext.get_string_attribute("ref"), Some("smith2023"));
316 assert_eq!(ext.get_i64_attribute("page"), Some(42));
317 }
318}