use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationHighlight {
pub page: i64,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
#[serde(default = "default_highlight_color")]
pub color: String,
#[serde(default)]
pub citation_id: String,
#[serde(default)]
pub tooltip: String,
}
fn default_highlight_color() -> String {
"rgba(255, 255, 0, 0.3)".to_string()
}
pub fn generate_pdfjs_citation_config(
document_urn: &str,
highlights: &[CitationHighlight],
viewer_options: Option<Value>,
) -> Value {
let default_viewer_options = json!({
"sidebarView": "thumbs",
"scrollMode": "vertical",
"spreadMode": "none",
"enableAnnotations": true,
"enableHighlights": true,
});
let highlights_json: Vec<Value> = highlights
.iter()
.map(|h| {
json!({
"page": h.page,
"rect": {
"x": h.x,
"y": h.y,
"width": h.width,
"height": h.height,
},
"style": {
"backgroundColor": h.color,
"borderRadius": "2px",
"cursor": "pointer",
},
"citationId": h.citation_id,
"tooltip": h.tooltip,
})
})
.collect();
json!({
"documentUrn": document_urn,
"viewerOptions": viewer_options.unwrap_or(default_viewer_options),
"highlights": highlights_json,
"onHighlightClick": "handleCitationClick",
"citationPanelEnabled": true,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YjsCRDTBinding {
pub component_id: String,
pub crdt_type: String,
#[serde(default = "default_sync_provider")]
pub sync_provider: String,
#[serde(default = "default_true")]
pub awareness_enabled: bool,
#[serde(default)]
pub initial_value: Value,
}
fn default_sync_provider() -> String {
"websocket".to_string()
}
fn default_true() -> bool {
true
}
pub fn scaffold_yjs_crdt_bindings(
canvas_id: &str,
bindings: &[YjsCRDTBinding],
websocket_url: Option<&str>,
) -> Value {
let ws_url = websocket_url.unwrap_or("ws://localhost:1234");
let bindings_json: Vec<Value> = bindings
.iter()
.map(|b| {
json!({
"componentId": b.component_id,
"crdtType": b.crdt_type,
"path": format!("canvas.{}.{}", canvas_id, b.component_id),
"syncProvider": b.sync_provider,
"awarenessEnabled": b.awareness_enabled,
"initialValue": b.initial_value,
})
})
.collect();
json!({
"canvasId": canvas_id,
"provider": {
"type": "WebsocketProvider",
"url": ws_url,
"roomName": format!("canvas-{}", canvas_id),
"awareness": true,
},
"bindings": bindings_json,
"undoManager": {
"enabled": true,
"trackedOrigins": [format!("canvas-{}", canvas_id)],
},
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_citation_highlight_defaults() {
let h = CitationHighlight {
page: 1,
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
color: default_highlight_color(),
citation_id: String::new(),
tooltip: String::new(),
};
assert_eq!(h.page, 1);
assert_eq!(h.x, 10.0);
assert_eq!(h.color, "rgba(255, 255, 0, 0.3)");
}
#[test]
fn test_generate_pdfjs_citation_config() {
let highlights = vec![CitationHighlight {
page: 1,
x: 10.0,
y: 20.0,
width: 50.0,
height: 15.0,
color: "rgba(255, 255, 0, 0.3)".to_string(),
citation_id: "c1".to_string(),
tooltip: "t1".to_string(),
}];
let config = generate_pdfjs_citation_config("urn:document:123", &highlights, None);
assert_eq!(config["documentUrn"], "urn:document:123");
assert_eq!(config["highlights"][0]["page"], 1);
assert_eq!(config["highlights"][0]["citationId"], "c1");
assert_eq!(config["highlights"][0]["tooltip"], "t1");
assert_eq!(config["highlights"][0]["rect"]["x"], 10.0);
assert_eq!(config["onHighlightClick"], "handleCitationClick");
assert_eq!(config["citationPanelEnabled"], true);
assert_eq!(config["viewerOptions"]["sidebarView"], "thumbs");
assert_eq!(config["viewerOptions"]["enableAnnotations"], true);
let custom = json!({"enableAnnotations": false, "customMode": "special"});
let config2 = generate_pdfjs_citation_config("urn:doc:2", &highlights, Some(custom.clone()));
assert_eq!(config2["viewerOptions"], custom);
}
#[test]
fn test_yjs_crdt_binding_defaults() {
let b = YjsCRDTBinding {
component_id: "cmp-1".to_string(),
crdt_type: "Y.Map".to_string(),
sync_provider: default_sync_provider(),
awareness_enabled: true,
initial_value: Value::Null,
};
assert_eq!(b.component_id, "cmp-1");
assert_eq!(b.crdt_type, "Y.Map");
assert_eq!(b.sync_provider, "websocket");
assert!(b.awareness_enabled);
}
#[test]
fn test_scaffold_yjs_crdt_bindings() {
let bindings = vec![YjsCRDTBinding {
component_id: "editor-1".to_string(),
crdt_type: "Y.Text".to_string(),
sync_provider: "websocket".to_string(),
awareness_enabled: true,
initial_value: json!("Init text"),
}];
let config = scaffold_yjs_crdt_bindings("canvas-99", &bindings, None);
assert_eq!(config["canvasId"], "canvas-99");
assert_eq!(config["provider"]["type"], "WebsocketProvider");
assert_eq!(config["provider"]["url"], "ws://localhost:1234");
assert_eq!(config["provider"]["roomName"], "canvas-canvas-99");
assert_eq!(config["bindings"][0]["componentId"], "editor-1");
assert_eq!(config["bindings"][0]["crdtType"], "Y.Text");
assert_eq!(config["bindings"][0]["path"], "canvas.canvas-99.editor-1");
assert_eq!(config["bindings"][0]["initialValue"], "Init text");
assert_eq!(config["undoManager"]["enabled"], true);
assert_eq!(config["undoManager"]["trackedOrigins"][0], "canvas-canvas-99");
let config2 = scaffold_yjs_crdt_bindings("canvas-99", &bindings, Some("ws://example.com:4321"));
assert_eq!(config2["provider"]["url"], "ws://example.com:4321");
}
}