pub mod academic;
mod collaboration;
mod forms;
pub mod legal;
pub mod phantom;
mod semantic;
pub use collaboration::{
ChangeStatus, ChangeTracking, ChangeType, CollaborationSession, Collaborator, Comment,
CommentThread, CommentType, CrdtFormat, CrdtMetadata, CursorPosition, HighlightColor,
MaterializationEvent, MaterializationReason, Participant, Peer, Priority, Revision,
RevisionHistory, Selection, SessionStatus, SuggestionStatus, SyncState, TextCrdtMetadata,
TextCrdtPosition, TextRange, TrackedChange,
};
pub use forms::{
CheckboxField, Condition, ConditionOperator, ConditionalAction, ConditionalValidation,
DatePickerField, DropdownField, DropdownOption, FormData, FormField, FormValidation,
RadioGroupField, RadioOption, SignatureField, TextAreaField, TextInputField, ValidationRule,
};
pub use phantom::{
ConnectionStyle, Phantom, PhantomCluster, PhantomClusters, PhantomConnection, PhantomContent,
PhantomPosition, PhantomScope, PhantomSize,
};
pub use semantic::{
Author, Bibliography, BibliographyEntry, Citation, CitationStyle, EntityLink, EntityType,
EntryType, Footnote, Glossary, GlossaryRef, GlossaryTerm, JsonLdMetadata, KnowledgeBase,
LocatorType, PartialDate,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::content::Block;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExtensionBlock {
pub namespace: String,
pub block_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub attributes: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<Block>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<Box<Block>>,
}
impl ExtensionBlock {
#[must_use]
pub fn new(namespace: impl Into<String>, block_type: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
block_type: block_type.into(),
id: None,
attributes: Value::Null,
children: Vec::new(),
fallback: None,
}
}
#[must_use]
pub fn parse_type(type_str: &str) -> Option<(&str, &str)> {
type_str.split_once(':')
}
#[must_use]
pub fn full_type(&self) -> String {
format!("{}:{}", self.namespace, self.block_type)
}
#[must_use]
pub fn is_namespace(&self, namespace: &str) -> bool {
self.namespace == namespace
}
#[must_use]
pub fn is_type(&self, namespace: &str, block_type: &str) -> bool {
self.namespace == namespace && self.block_type == block_type
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_attributes(mut self, attributes: Value) -> Self {
self.attributes = attributes;
self
}
#[must_use]
pub fn with_children(mut self, children: Vec<Block>) -> Self {
self.children = children;
self
}
#[must_use]
pub fn with_fallback(mut self, fallback: Block) -> Self {
self.fallback = Some(Box::new(fallback));
self
}
#[must_use]
pub fn fallback_content(&self) -> Option<&Block> {
self.fallback.as_deref()
}
#[must_use]
pub fn get_attribute(&self, key: &str) -> Option<&Value> {
self.attributes.get(key)
}
#[must_use]
pub fn get_string_attribute(&self, key: &str) -> Option<&str> {
self.attributes.get(key).and_then(Value::as_str)
}
#[must_use]
pub fn get_string_array_attribute(&self, key: &str) -> Option<Vec<&str>> {
self.attributes.get(key).and_then(|v| {
v.as_array()
.map(|arr| arr.iter().filter_map(serde_json::Value::as_str).collect())
})
}
#[must_use]
pub fn get_bool_attribute(&self, key: &str) -> Option<bool> {
self.attributes.get(key).and_then(Value::as_bool)
}
#[must_use]
pub fn get_i64_attribute(&self, key: &str) -> Option<i64> {
self.attributes.get(key).and_then(Value::as_i64)
}
#[must_use]
pub fn as_form_field(&self) -> Option<FormField> {
if self.namespace != "forms" {
return None;
}
FormField::from_extension(self)
}
}
pub mod namespaces {
pub const FORMS: &str = "forms";
pub const SEMANTIC: &str = "semantic";
pub const COLLABORATION: &str = "collaboration";
pub const ACADEMIC: &str = "academic";
pub const LEGAL: &str = "legal";
pub const PRESENTATION: &str = "presentation";
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_extension_block_new() {
let ext = ExtensionBlock::new("forms", "textInput");
assert_eq!(ext.namespace, "forms");
assert_eq!(ext.block_type, "textInput");
assert_eq!(ext.full_type(), "forms:textInput");
}
#[test]
fn test_parse_type() {
assert_eq!(
ExtensionBlock::parse_type("forms:textInput"),
Some(("forms", "textInput"))
);
assert_eq!(
ExtensionBlock::parse_type("semantic:citation"),
Some(("semantic", "citation"))
);
assert_eq!(ExtensionBlock::parse_type("paragraph"), None);
}
#[test]
fn test_extension_block_builder() {
let ext = ExtensionBlock::new("forms", "checkbox")
.with_id("accept-terms")
.with_attributes(json!({
"label": "I accept the terms",
"required": true
}));
assert_eq!(ext.id, Some("accept-terms".to_string()));
assert_eq!(
ext.get_string_attribute("label"),
Some("I accept the terms")
);
assert_eq!(ext.get_bool_attribute("required"), Some(true));
}
#[test]
fn test_extension_namespace_check() {
let ext = ExtensionBlock::new("forms", "textInput");
assert!(ext.is_namespace("forms"));
assert!(!ext.is_namespace("semantic"));
assert!(ext.is_type("forms", "textInput"));
assert!(!ext.is_type("forms", "checkbox"));
}
#[test]
fn test_extension_with_fallback() {
use crate::content::Text;
let fallback = Block::paragraph(vec![Text::plain("[Form field: Name]")]);
let ext = ExtensionBlock::new("forms", "textInput").with_fallback(fallback.clone());
assert!(ext.fallback_content().is_some());
if let Block::Paragraph { children, .. } = ext.fallback_content().unwrap() {
assert_eq!(children[0].value, "[Form field: Name]");
}
}
#[test]
fn test_extension_serialization() {
let ext = ExtensionBlock::new("forms", "textInput")
.with_id("name")
.with_attributes(json!({"label": "Name", "required": true}));
let json = serde_json::to_string_pretty(&ext).unwrap();
assert!(json.contains("\"namespace\": \"forms\""));
assert!(json.contains("\"blockType\": \"textInput\""));
assert!(json.contains("\"label\": \"Name\""));
}
#[test]
fn test_extension_deserialization() {
let json = r#"{
"namespace": "semantic",
"blockType": "citation",
"id": "cite-1",
"attributes": {
"refs": ["smith2023"],
"page": 42
}
}"#;
let ext: ExtensionBlock = serde_json::from_str(json).unwrap();
assert_eq!(ext.namespace, "semantic");
assert_eq!(ext.block_type, "citation");
assert_eq!(ext.id, Some("cite-1".to_string()));
assert_eq!(
ext.get_string_array_attribute("refs"),
Some(vec!["smith2023"])
);
assert_eq!(ext.get_i64_attribute("page"), Some(42));
}
}