oxirs_shacl/visual_editor/
mod.rs

1//! Visual Shape Editor Support
2//!
3//! Provides export formats and representations suitable for visual shape editing:
4//!
5//! - **GraphViz/DOT** - Export shapes as DOT graphs for visualization
6//! - **Mermaid** - Generate Mermaid diagram syntax
7//! - **JSON Schema** - Export as JSON Schema for web-based editors
8//! - **SVG** - Generate SVG diagrams of shapes
9//! - **Interactive Editor Protocol** - JSON protocol for real-time editors
10//!
11//! ## Example
12//!
13//! ```rust,ignore
14//! use oxirs_shacl::visual_editor::{ShapeVisualizer, ExportFormat};
15//!
16//! let visualizer = ShapeVisualizer::new();
17//! let dot = visualizer.export(&shape, ExportFormat::Dot)?;
18//! let mermaid = visualizer.export(&shape, ExportFormat::Mermaid)?;
19//! ```
20
21use crate::{ConstraintComponentId, Shape, ShapeType, Target};
22use serde::{Deserialize, Serialize};
23use std::collections::{HashMap, HashSet};
24use std::fmt::Write;
25
26/// Visual export format
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ExportFormat {
29    /// GraphViz DOT format
30    Dot,
31    /// Mermaid diagram syntax
32    Mermaid,
33    /// JSON Schema format
34    JsonSchema,
35    /// SVG diagram
36    Svg,
37    /// PlantUML format
38    PlantUml,
39    /// D3.js JSON format
40    D3Json,
41    /// Cytoscape.js JSON format
42    CytoscapeJson,
43}
44
45/// Shape visualizer configuration
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct VisualizerConfig {
48    /// Include constraint details
49    pub show_constraints: bool,
50    /// Include targets
51    pub show_targets: bool,
52    /// Include metadata
53    pub show_metadata: bool,
54    /// Color scheme
55    pub color_scheme: ColorScheme,
56    /// Shape node style
57    pub shape_style: ShapeStyle,
58    /// Property node style
59    pub property_style: PropertyStyle,
60    /// Layout direction
61    pub layout_direction: LayoutDirection,
62    /// Max label length (truncate)
63    pub max_label_length: usize,
64    /// Show prefixes or full IRIs
65    pub use_prefixes: bool,
66    /// Namespace prefixes
67    pub prefixes: HashMap<String, String>,
68}
69
70impl Default for VisualizerConfig {
71    fn default() -> Self {
72        let mut prefixes = HashMap::new();
73        prefixes.insert("sh".to_string(), "http://www.w3.org/ns/shacl#".to_string());
74        prefixes.insert(
75            "xsd".to_string(),
76            "http://www.w3.org/2001/XMLSchema#".to_string(),
77        );
78        prefixes.insert(
79            "rdf".to_string(),
80            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
81        );
82        prefixes.insert(
83            "rdfs".to_string(),
84            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
85        );
86        prefixes.insert("foaf".to_string(), "http://xmlns.com/foaf/0.1/".to_string());
87        prefixes.insert("schema".to_string(), "http://schema.org/".to_string());
88        prefixes.insert("ex".to_string(), "http://example.org/".to_string());
89
90        Self {
91            show_constraints: true,
92            show_targets: true,
93            show_metadata: false,
94            color_scheme: ColorScheme::default(),
95            shape_style: ShapeStyle::default(),
96            property_style: PropertyStyle::default(),
97            layout_direction: LayoutDirection::TopToBottom,
98            max_label_length: 50,
99            use_prefixes: true,
100            prefixes,
101        }
102    }
103}
104
105/// Color scheme for visualization
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ColorScheme {
108    /// Node shape fill color
109    pub shape_fill: String,
110    /// Node shape border color
111    pub shape_border: String,
112    /// Property shape fill color
113    pub property_fill: String,
114    /// Property shape border color
115    pub property_border: String,
116    /// Target fill color
117    pub target_fill: String,
118    /// Constraint fill color
119    pub constraint_fill: String,
120    /// Edge color
121    pub edge_color: String,
122    /// Text color
123    pub text_color: String,
124    /// Required indicator color
125    pub required_color: String,
126}
127
128impl Default for ColorScheme {
129    fn default() -> Self {
130        Self {
131            shape_fill: "#e8f4fc".to_string(),
132            shape_border: "#3498db".to_string(),
133            property_fill: "#f9f9f9".to_string(),
134            property_border: "#7f8c8d".to_string(),
135            target_fill: "#d5f5e3".to_string(),
136            constraint_fill: "#fef9e7".to_string(),
137            edge_color: "#34495e".to_string(),
138            text_color: "#2c3e50".to_string(),
139            required_color: "#e74c3c".to_string(),
140        }
141    }
142}
143
144/// Shape node style
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ShapeStyle {
147    /// Node shape (box, ellipse, etc.)
148    pub shape: String,
149    /// Font size
150    pub font_size: u32,
151    /// Font family
152    pub font_family: String,
153    /// Border width
154    pub border_width: u32,
155}
156
157impl Default for ShapeStyle {
158    fn default() -> Self {
159        Self {
160            shape: "box".to_string(),
161            font_size: 12,
162            font_family: "Arial".to_string(),
163            border_width: 2,
164        }
165    }
166}
167
168/// Property node style
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PropertyStyle {
171    /// Node shape
172    pub shape: String,
173    /// Font size
174    pub font_size: u32,
175    /// Font family
176    pub font_family: String,
177    /// Border width
178    pub border_width: u32,
179}
180
181impl Default for PropertyStyle {
182    fn default() -> Self {
183        Self {
184            shape: "record".to_string(),
185            font_size: 10,
186            font_family: "Arial".to_string(),
187            border_width: 1,
188        }
189    }
190}
191
192/// Layout direction
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194pub enum LayoutDirection {
195    TopToBottom,
196    LeftToRight,
197    BottomToTop,
198    RightToLeft,
199}
200
201impl LayoutDirection {
202    fn as_dot(self) -> &'static str {
203        match self {
204            LayoutDirection::TopToBottom => "TB",
205            LayoutDirection::LeftToRight => "LR",
206            LayoutDirection::BottomToTop => "BT",
207            LayoutDirection::RightToLeft => "RL",
208        }
209    }
210
211    fn as_mermaid(self) -> &'static str {
212        match self {
213            LayoutDirection::TopToBottom => "TD",
214            LayoutDirection::LeftToRight => "LR",
215            LayoutDirection::BottomToTop => "BT",
216            LayoutDirection::RightToLeft => "RL",
217        }
218    }
219}
220
221/// Shape visualizer
222#[derive(Debug)]
223pub struct ShapeVisualizer {
224    config: VisualizerConfig,
225}
226
227impl ShapeVisualizer {
228    /// Create a new visualizer
229    pub fn new() -> Self {
230        Self {
231            config: VisualizerConfig::default(),
232        }
233    }
234
235    /// Create with configuration
236    pub fn with_config(config: VisualizerConfig) -> Self {
237        Self { config }
238    }
239
240    /// Export a single shape
241    pub fn export(&self, shape: &Shape, format: ExportFormat) -> String {
242        self.export_multiple(std::slice::from_ref(shape), format)
243    }
244
245    /// Export multiple shapes
246    pub fn export_multiple(&self, shapes: &[Shape], format: ExportFormat) -> String {
247        match format {
248            ExportFormat::Dot => self.to_dot(shapes),
249            ExportFormat::Mermaid => self.to_mermaid(shapes),
250            ExportFormat::JsonSchema => self.to_json_schema(shapes),
251            ExportFormat::Svg => self.to_svg(shapes),
252            ExportFormat::PlantUml => self.to_plantuml(shapes),
253            ExportFormat::D3Json => self.to_d3_json(shapes),
254            ExportFormat::CytoscapeJson => self.to_cytoscape_json(shapes),
255        }
256    }
257
258    /// Export to GraphViz DOT format
259    pub fn to_dot(&self, shapes: &[Shape]) -> String {
260        let mut dot = String::new();
261
262        writeln!(dot, "digraph SHACL_Shapes {{").ok();
263        writeln!(dot, "  rankdir={};", self.config.layout_direction.as_dot()).ok();
264        writeln!(
265            dot,
266            "  node [fontname=\"{}\"];",
267            self.config.shape_style.font_family
268        )
269        .ok();
270        writeln!(
271            dot,
272            "  edge [fontname=\"{}\"];",
273            self.config.shape_style.font_family
274        )
275        .ok();
276        writeln!(dot).ok();
277
278        // Style definitions
279        writeln!(dot, "  // Node shape styles").ok();
280        writeln!(
281            dot,
282            "  node [shape={}, style=filled, fillcolor=\"{}\", color=\"{}\", fontsize={}];",
283            self.config.shape_style.shape,
284            self.config.color_scheme.shape_fill,
285            self.config.color_scheme.shape_border,
286            self.config.shape_style.font_size
287        )
288        .ok();
289        writeln!(dot).ok();
290
291        for shape in shapes {
292            self.shape_to_dot(&mut dot, shape);
293        }
294
295        // Add relationships between shapes
296        let shape_ids: HashSet<_> = shapes.iter().map(|s| s.id.as_str()).collect();
297        for shape in shapes {
298            for (_, constraint) in &shape.constraints {
299                // Check for sh:node or sh:class references
300                if let crate::constraints::Constraint::Node(node_constraint) = constraint {
301                    let ref_id = node_constraint.shape.as_str();
302                    if shape_ids.contains(ref_id) {
303                        let from_id = self.sanitize_id(&shape.id.0);
304                        let to_id = self.sanitize_id(ref_id);
305                        writeln!(
306                            dot,
307                            "  {} -> {} [label=\"sh:node\", style=dashed];",
308                            from_id, to_id
309                        )
310                        .ok();
311                    }
312                }
313            }
314        }
315
316        writeln!(dot, "}}").ok();
317        dot
318    }
319
320    fn shape_to_dot(&self, dot: &mut String, shape: &Shape) {
321        let shape_id = self.sanitize_id(&shape.id.0);
322        let label = self.compact_iri(&shape.id.0);
323
324        // Create shape node
325        let shape_type_label = match shape.shape_type {
326            ShapeType::NodeShape => "NodeShape",
327            ShapeType::PropertyShape => "PropertyShape",
328        };
329
330        writeln!(dot, "  // Shape: {}", label).ok();
331        writeln!(
332            dot,
333            "  {} [label=\"{} ({})\\n{}\"];",
334            shape_id,
335            label,
336            shape_type_label,
337            shape.label.as_deref().unwrap_or("")
338        )
339        .ok();
340
341        // Add targets
342        if self.config.show_targets && !shape.targets.is_empty() {
343            for (idx, target) in shape.targets.iter().enumerate() {
344                let target_id = format!("{}_target_{}", shape_id, idx);
345                let target_label = self.target_label(target);
346
347                writeln!(
348                    dot,
349                    "  {} [shape=ellipse, style=filled, fillcolor=\"{}\", label=\"{}\"];",
350                    target_id, self.config.color_scheme.target_fill, target_label
351                )
352                .ok();
353                writeln!(dot, "  {} -> {} [label=\"target\"];", shape_id, target_id).ok();
354            }
355        }
356
357        // Add constraints
358        if self.config.show_constraints && !shape.constraints.is_empty() {
359            let constraints_id = format!("{}_constraints", shape_id);
360            let mut constraint_labels = Vec::new();
361
362            for (comp_id, constraint) in &shape.constraints {
363                let constraint_label = self.constraint_label(comp_id, constraint);
364                constraint_labels.push(constraint_label);
365            }
366
367            let constraints_label = constraint_labels.join("\\n");
368            writeln!(
369                dot,
370                "  {} [shape=note, style=filled, fillcolor=\"{}\", label=\"{}\", fontsize={}];",
371                constraints_id,
372                self.config.color_scheme.constraint_fill,
373                constraints_label,
374                self.config.property_style.font_size
375            )
376            .ok();
377            writeln!(
378                dot,
379                "  {} -> {} [style=dotted, label=\"constraints\"];",
380                shape_id, constraints_id
381            )
382            .ok();
383        }
384
385        writeln!(dot).ok();
386    }
387
388    /// Export to Mermaid diagram syntax
389    pub fn to_mermaid(&self, shapes: &[Shape]) -> String {
390        let mut mermaid = String::new();
391
392        writeln!(
393            mermaid,
394            "flowchart {}",
395            self.config.layout_direction.as_mermaid()
396        )
397        .ok();
398
399        for shape in shapes {
400            self.shape_to_mermaid(&mut mermaid, shape);
401        }
402
403        mermaid
404    }
405
406    fn shape_to_mermaid(&self, mermaid: &mut String, shape: &Shape) {
407        let shape_id = self.sanitize_id(&shape.id.0);
408        let label = self.compact_iri(&shape.id.0);
409
410        let shape_type = match shape.shape_type {
411            ShapeType::NodeShape => "NodeShape",
412            ShapeType::PropertyShape => "PropertyShape",
413        };
414
415        // Shape node
416        writeln!(
417            mermaid,
418            "    {}[\"<b>{}</b><br/><small>{}</small>\"]",
419            shape_id, label, shape_type
420        )
421        .ok();
422
423        // Style the node
424        writeln!(
425            mermaid,
426            "    style {} fill:{},stroke:{},stroke-width:{}px",
427            shape_id,
428            self.config.color_scheme.shape_fill,
429            self.config.color_scheme.shape_border,
430            self.config.shape_style.border_width
431        )
432        .ok();
433
434        // Targets
435        if self.config.show_targets {
436            for (idx, target) in shape.targets.iter().enumerate() {
437                let target_id = format!("{}_t{}", shape_id, idx);
438                let target_label = self.target_label(target);
439
440                writeln!(mermaid, "    {}((\"{}\"))", target_id, target_label).ok();
441                writeln!(
442                    mermaid,
443                    "    style {} fill:{},stroke:#27ae60",
444                    target_id, self.config.color_scheme.target_fill
445                )
446                .ok();
447                writeln!(mermaid, "    {} -->|target| {}", shape_id, target_id).ok();
448            }
449        }
450
451        // Constraints
452        if self.config.show_constraints && !shape.constraints.is_empty() {
453            let constraints_id = format!("{}_c", shape_id);
454            let mut labels = Vec::new();
455
456            for (comp_id, constraint) in &shape.constraints {
457                labels.push(self.constraint_label(comp_id, constraint));
458            }
459
460            writeln!(
461                mermaid,
462                "    {}[\"{}\"]",
463                constraints_id,
464                labels.join("<br/>")
465            )
466            .ok();
467            writeln!(
468                mermaid,
469                "    style {} fill:{},stroke:#f39c12",
470                constraints_id, self.config.color_scheme.constraint_fill
471            )
472            .ok();
473            writeln!(
474                mermaid,
475                "    {} -.->|constraints| {}",
476                shape_id, constraints_id
477            )
478            .ok();
479        }
480    }
481
482    /// Export to JSON Schema format (for web editors)
483    pub fn to_json_schema(&self, shapes: &[Shape]) -> String {
484        let mut schema = serde_json::json!({
485            "$schema": "https://json-schema.org/draft/2020-12/schema",
486            "$id": "shacl-shapes-schema",
487            "type": "object",
488            "title": "SHACL Shapes",
489            "definitions": {}
490        });
491
492        let definitions = schema["definitions"].as_object_mut().unwrap();
493
494        for shape in shapes {
495            let shape_schema = self.shape_to_json_schema(shape);
496            let shape_name = self.compact_iri(&shape.id.0).replace(':', "_");
497            definitions.insert(shape_name, shape_schema);
498        }
499
500        serde_json::to_string_pretty(&schema).unwrap_or_default()
501    }
502
503    fn shape_to_json_schema(&self, shape: &Shape) -> serde_json::Value {
504        let mut properties = serde_json::Map::new();
505        let mut required = Vec::new();
506
507        for (comp_id, constraint) in &shape.constraints {
508            if let Some((prop_name, prop_schema, is_required)) =
509                self.constraint_to_json_schema(comp_id, constraint)
510            {
511                properties.insert(prop_name.clone(), prop_schema);
512                if is_required {
513                    required.push(prop_name);
514                }
515            }
516        }
517
518        serde_json::json!({
519            "type": "object",
520            "title": shape.label.as_deref().unwrap_or(&self.compact_iri(&shape.id.0)),
521            "description": shape.description.clone().unwrap_or_default(),
522            "properties": properties,
523            "required": required
524        })
525    }
526
527    fn constraint_to_json_schema(
528        &self,
529        comp_id: &ConstraintComponentId,
530        constraint: &crate::constraints::Constraint,
531    ) -> Option<(String, serde_json::Value, bool)> {
532        // This is a simplified mapping - in production you'd handle all constraint types
533        let comp_name = comp_id.as_str();
534
535        match constraint {
536            crate::constraints::Constraint::Datatype(dt) => {
537                let dt_iri = dt.datatype_iri.as_str();
538                let json_type = if dt_iri.contains("string") || dt_iri.contains("date") {
539                    "string"
540                } else if dt_iri.contains("integer") {
541                    "integer"
542                } else if dt_iri.contains("decimal") || dt_iri.contains("float") {
543                    "number"
544                } else if dt_iri.contains("boolean") {
545                    "boolean"
546                } else {
547                    "string"
548                };
549
550                let mut schema = serde_json::json!({ "type": json_type });
551                if dt_iri.contains("date") {
552                    schema["format"] = serde_json::json!("date");
553                }
554
555                Some((comp_name.to_string(), schema, false))
556            }
557            crate::constraints::Constraint::MinCount(mc) => Some((
558                comp_name.to_string(),
559                serde_json::json!({}),
560                mc.min_count >= 1,
561            )),
562            crate::constraints::Constraint::Pattern(pat) => Some((
563                comp_name.to_string(),
564                serde_json::json!({
565                    "type": "string",
566                    "pattern": pat.pattern
567                }),
568                false,
569            )),
570            crate::constraints::Constraint::MinLength(ml) => Some((
571                comp_name.to_string(),
572                serde_json::json!({
573                    "type": "string",
574                    "minLength": ml.min_length
575                }),
576                false,
577            )),
578            crate::constraints::Constraint::MaxLength(ml) => Some((
579                comp_name.to_string(),
580                serde_json::json!({
581                    "type": "string",
582                    "maxLength": ml.max_length
583                }),
584                false,
585            )),
586            _ => None,
587        }
588    }
589
590    /// Export to SVG diagram
591    pub fn to_svg(&self, shapes: &[Shape]) -> String {
592        let mut svg = String::new();
593
594        // Calculate dimensions
595        let shape_height = 80;
596        let shape_width = 200;
597        let padding = 20;
598        let total_height = shapes.len() * (shape_height + padding) + padding;
599        let total_width = shape_width + padding * 2;
600
601        writeln!(
602            svg,
603            r#"<?xml version="1.0" encoding="UTF-8"?>
604<svg xmlns="http://www.w3.org/2000/svg"
605     viewBox="0 0 {} {}"
606     width="{}" height="{}">"#,
607            total_width, total_height, total_width, total_height
608        )
609        .ok();
610
611        // Add style
612        writeln!(
613            svg,
614            r#"<style>
615    .shape-rect {{ fill: {}; stroke: {}; stroke-width: {}; }}
616    .shape-text {{ font-family: {}; font-size: {}px; fill: {}; }}
617    .constraint-text {{ font-family: {}; font-size: {}px; fill: #666; }}
618</style>"#,
619            self.config.color_scheme.shape_fill,
620            self.config.color_scheme.shape_border,
621            self.config.shape_style.border_width,
622            self.config.shape_style.font_family,
623            self.config.shape_style.font_size,
624            self.config.color_scheme.text_color,
625            self.config.property_style.font_family,
626            self.config.property_style.font_size
627        )
628        .ok();
629
630        // Draw shapes
631        for (idx, shape) in shapes.iter().enumerate() {
632            let y = padding + idx * (shape_height + padding);
633            self.shape_to_svg(&mut svg, shape, padding, y, shape_width, shape_height);
634        }
635
636        writeln!(svg, "</svg>").ok();
637        svg
638    }
639
640    fn shape_to_svg(
641        &self,
642        svg: &mut String,
643        shape: &Shape,
644        x: usize,
645        y: usize,
646        width: usize,
647        height: usize,
648    ) {
649        let label = self.compact_iri(&shape.id.0);
650        let shape_type = match shape.shape_type {
651            ShapeType::NodeShape => "NodeShape",
652            ShapeType::PropertyShape => "PropertyShape",
653        };
654
655        // Rectangle
656        writeln!(
657            svg,
658            r#"  <rect class="shape-rect" x="{}" y="{}" width="{}" height="{}" rx="5" />"#,
659            x, y, width, height
660        )
661        .ok();
662
663        // Title
664        writeln!(
665            svg,
666            r#"  <text class="shape-text" x="{}" y="{}" text-anchor="middle">{}</text>"#,
667            x + width / 2,
668            y + 25,
669            label
670        )
671        .ok();
672
673        // Type
674        writeln!(
675            svg,
676            r#"  <text class="constraint-text" x="{}" y="{}" text-anchor="middle">({})</text>"#,
677            x + width / 2,
678            y + 45,
679            shape_type
680        )
681        .ok();
682
683        // Constraint count
684        if !shape.constraints.is_empty() {
685            writeln!(
686                svg,
687                r#"  <text class="constraint-text" x="{}" y="{}" text-anchor="middle">{} constraints</text>"#,
688                x + width / 2,
689                y + 65,
690                shape.constraints.len()
691            )
692            .ok();
693        }
694    }
695
696    /// Export to PlantUML format
697    pub fn to_plantuml(&self, shapes: &[Shape]) -> String {
698        let mut uml = String::new();
699
700        writeln!(uml, "@startuml").ok();
701        writeln!(uml, "skinparam class {{").ok();
702        writeln!(
703            uml,
704            "  BackgroundColor {}",
705            self.config.color_scheme.shape_fill
706        )
707        .ok();
708        writeln!(
709            uml,
710            "  BorderColor {}",
711            self.config.color_scheme.shape_border
712        )
713        .ok();
714        writeln!(uml, "}}").ok();
715        writeln!(uml).ok();
716
717        for shape in shapes {
718            let label = self.compact_iri(&shape.id.0).replace(':', "_");
719            let shape_type = match shape.shape_type {
720                ShapeType::NodeShape => "<<NodeShape>>",
721                ShapeType::PropertyShape => "<<PropertyShape>>",
722            };
723
724            writeln!(uml, "class \"{}\" {} {{", label, shape_type).ok();
725
726            for (comp_id, constraint) in &shape.constraints {
727                let constraint_label = self.constraint_label(comp_id, constraint);
728                writeln!(uml, "  + {}", constraint_label).ok();
729            }
730
731            writeln!(uml, "}}").ok();
732            writeln!(uml).ok();
733        }
734
735        writeln!(uml, "@enduml").ok();
736        uml
737    }
738
739    /// Export to D3.js JSON format
740    pub fn to_d3_json(&self, shapes: &[Shape]) -> String {
741        let mut nodes = Vec::new();
742        let mut links = Vec::new();
743
744        for shape in shapes {
745            let node = serde_json::json!({
746                "id": shape.id.as_str(),
747                "label": self.compact_iri(&shape.id.0),
748                "type": match shape.shape_type {
749                    ShapeType::NodeShape => "node",
750                    ShapeType::PropertyShape => "property"
751                },
752                "constraints": shape.constraints.len(),
753                "targets": shape.targets.len()
754            });
755            nodes.push(node);
756
757            // Add links for shape references
758            for (_, constraint) in &shape.constraints {
759                if let crate::constraints::Constraint::Node(nc) = constraint {
760                    links.push(serde_json::json!({
761                        "source": shape.id.as_str(),
762                        "target": nc.shape.as_str(),
763                        "type": "sh:node"
764                    }));
765                }
766            }
767        }
768
769        let result = serde_json::json!({
770            "nodes": nodes,
771            "links": links
772        });
773
774        serde_json::to_string_pretty(&result).unwrap_or_default()
775    }
776
777    /// Export to Cytoscape.js JSON format
778    pub fn to_cytoscape_json(&self, shapes: &[Shape]) -> String {
779        let mut elements = Vec::new();
780
781        for shape in shapes {
782            // Add node
783            let node = serde_json::json!({
784                "data": {
785                    "id": shape.id.as_str(),
786                    "label": self.compact_iri(&shape.id.0),
787                    "type": match shape.shape_type {
788                        ShapeType::NodeShape => "nodeShape",
789                        ShapeType::PropertyShape => "propertyShape"
790                    }
791                },
792                "classes": match shape.shape_type {
793                    ShapeType::NodeShape => "nodeShape",
794                    ShapeType::PropertyShape => "propertyShape"
795                }
796            });
797            elements.push(node);
798
799            // Add edges for references
800            for (_, constraint) in &shape.constraints {
801                if let crate::constraints::Constraint::Node(nc) = constraint {
802                    let edge = serde_json::json!({
803                        "data": {
804                            "source": shape.id.as_str(),
805                            "target": nc.shape.as_str(),
806                            "label": "sh:node"
807                        },
808                        "classes": "reference"
809                    });
810                    elements.push(edge);
811                }
812            }
813        }
814
815        let result = serde_json::json!({
816            "elements": elements,
817            "style": [
818                {
819                    "selector": "node.nodeShape",
820                    "style": {
821                        "background-color": self.config.color_scheme.shape_fill,
822                        "border-color": self.config.color_scheme.shape_border,
823                        "label": "data(label)"
824                    }
825                },
826                {
827                    "selector": "node.propertyShape",
828                    "style": {
829                        "background-color": self.config.color_scheme.property_fill,
830                        "border-color": self.config.color_scheme.property_border,
831                        "shape": "rectangle",
832                        "label": "data(label)"
833                    }
834                },
835                {
836                    "selector": "edge",
837                    "style": {
838                        "line-color": self.config.color_scheme.edge_color,
839                        "target-arrow-color": self.config.color_scheme.edge_color,
840                        "target-arrow-shape": "triangle",
841                        "label": "data(label)"
842                    }
843                }
844            ]
845        });
846
847        serde_json::to_string_pretty(&result).unwrap_or_default()
848    }
849
850    /// Get interactive editor protocol JSON
851    pub fn to_editor_protocol(&self, shapes: &[Shape]) -> String {
852        let editor_shapes: Vec<_> = shapes
853            .iter()
854            .map(|s| self.shape_to_editor_json(s))
855            .collect();
856
857        let result = serde_json::json!({
858            "version": "1.0",
859            "protocol": "shacl-editor",
860            "shapes": editor_shapes,
861            "config": {
862                "prefixes": self.config.prefixes,
863                "colorScheme": self.config.color_scheme
864            }
865        });
866
867        serde_json::to_string_pretty(&result).unwrap_or_default()
868    }
869
870    fn shape_to_editor_json(&self, shape: &Shape) -> serde_json::Value {
871        let targets: Vec<_> = shape
872            .targets
873            .iter()
874            .map(|t| self.target_to_editor_json(t))
875            .collect();
876
877        let constraints: Vec<_> = shape
878            .constraints
879            .iter()
880            .map(|(comp_id, c)| self.constraint_to_editor_json(comp_id, c))
881            .collect();
882
883        serde_json::json!({
884            "id": shape.id.as_str(),
885            "label": self.compact_iri(&shape.id.0),
886            "humanLabel": shape.label,
887            "description": shape.description,
888            "type": match shape.shape_type {
889                ShapeType::NodeShape => "NodeShape",
890                ShapeType::PropertyShape => "PropertyShape"
891            },
892            "targets": targets,
893            "constraints": constraints,
894            "severity": format!("{:?}", shape.severity),
895            "deactivated": shape.deactivated,
896            "metadata": {
897                "groups": shape.groups,
898                "order": shape.order,
899                "extends": shape.extends.iter().map(|s| s.as_str()).collect::<Vec<_>>()
900            }
901        })
902    }
903
904    fn target_to_editor_json(&self, target: &Target) -> serde_json::Value {
905        match target {
906            Target::Class(node) => serde_json::json!({
907                "type": "class",
908                "value": node.as_str(),
909                "label": self.compact_iri(node.as_str())
910            }),
911            Target::Node(node) => serde_json::json!({
912                "type": "node",
913                "value": node.to_string(),
914                "label": self.compact_iri(&node.to_string())
915            }),
916            Target::SubjectsOf(node) => serde_json::json!({
917                "type": "subjectsOf",
918                "value": node.as_str(),
919                "label": self.compact_iri(node.as_str())
920            }),
921            Target::ObjectsOf(node) => serde_json::json!({
922                "type": "objectsOf",
923                "value": node.as_str(),
924                "label": self.compact_iri(node.as_str())
925            }),
926            Target::Sparql(sparql_target) => serde_json::json!({
927                "type": "sparql",
928                "value": &sparql_target.query,
929                "label": "SPARQL Target"
930            }),
931            Target::Implicit(node) => serde_json::json!({
932                "type": "implicit",
933                "value": node.as_str(),
934                "label": self.compact_iri(node.as_str())
935            }),
936            Target::Union(union_target) => serde_json::json!({
937                "type": "union",
938                "value": format!("Union of {} targets", union_target.targets.len()),
939                "label": "Union Target"
940            }),
941            Target::Intersection(intersection_target) => serde_json::json!({
942                "type": "intersection",
943                "value": format!("Intersection of {} targets", intersection_target.targets.len()),
944                "label": "Intersection Target"
945            }),
946            Target::Difference(_diff_target) => serde_json::json!({
947                "type": "difference",
948                "value": "Difference Target",
949                "label": format!("Difference (primary - exclusion)"),
950            }),
951            Target::Conditional(_cond_target) => serde_json::json!({
952                "type": "conditional",
953                "value": "Conditional Target",
954                "label": format!("Conditional (with condition)")
955            }),
956            Target::Hierarchical(hier_target) => serde_json::json!({
957                "type": "hierarchical",
958                "value": format!("{:?}", hier_target.relationship),
959                "label": format!("Hierarchical (depth: {:?})", hier_target.max_depth)
960            }),
961            Target::PathBased(path_target) => serde_json::json!({
962                "type": "pathBased",
963                "value": "Path-based Target",
964                "label": format!("Path-based (direction: {:?})", path_target.direction)
965            }),
966        }
967    }
968
969    fn constraint_to_editor_json(
970        &self,
971        comp_id: &ConstraintComponentId,
972        constraint: &crate::constraints::Constraint,
973    ) -> serde_json::Value {
974        serde_json::json!({
975            "componentId": comp_id.as_str(),
976            "label": self.compact_iri(comp_id.as_str()),
977            "description": self.constraint_label(comp_id, constraint)
978        })
979    }
980
981    // Helper methods
982
983    fn sanitize_id(&self, id: &str) -> String {
984        id.chars()
985            .map(|c| if c.is_alphanumeric() { c } else { '_' })
986            .collect()
987    }
988
989    fn compact_iri(&self, iri: &str) -> String {
990        if !self.config.use_prefixes {
991            return iri.to_string();
992        }
993
994        for (prefix, namespace) in &self.config.prefixes {
995            if iri.starts_with(namespace) {
996                let local = &iri[namespace.len()..];
997                return format!("{}:{}", prefix, local);
998            }
999        }
1000
1001        // Try to get just the local name
1002        if let Some(pos) = iri.rfind('#') {
1003            return iri[pos + 1..].to_string();
1004        }
1005        if let Some(pos) = iri.rfind('/') {
1006            return iri[pos + 1..].to_string();
1007        }
1008
1009        iri.to_string()
1010    }
1011
1012    fn target_label(&self, target: &Target) -> String {
1013        match target {
1014            Target::Class(node) => format!("class: {}", self.compact_iri(node.as_str())),
1015            Target::Node(node) => format!("node: {}", self.compact_iri(&node.to_string())),
1016            Target::SubjectsOf(node) => {
1017                format!("subjectsOf: {}", self.compact_iri(node.as_str()))
1018            }
1019            Target::ObjectsOf(node) => {
1020                format!("objectsOf: {}", self.compact_iri(node.as_str()))
1021            }
1022            Target::Sparql(_) => "SPARQL target".to_string(),
1023            Target::Implicit(node) => format!("implicit: {}", self.compact_iri(node.as_str())),
1024            Target::Union(union_target) => format!("union: {} targets", union_target.targets.len()),
1025            Target::Intersection(intersection_target) => {
1026                format!(
1027                    "intersection: {} targets",
1028                    intersection_target.targets.len()
1029                )
1030            }
1031            Target::Difference(_) => "difference target".to_string(),
1032            Target::Conditional(_) => "conditional target".to_string(),
1033            Target::Hierarchical(hier_target) => {
1034                format!("hierarchical: {:?}", hier_target.relationship)
1035            }
1036            Target::PathBased(_) => "path-based target".to_string(),
1037        }
1038    }
1039
1040    fn constraint_label(
1041        &self,
1042        comp_id: &ConstraintComponentId,
1043        constraint: &crate::constraints::Constraint,
1044    ) -> String {
1045        let comp_name = self.compact_iri(comp_id.as_str());
1046
1047        match constraint {
1048            crate::constraints::Constraint::MinCount(mc) => {
1049                format!("{} = {}", comp_name, mc.min_count)
1050            }
1051            crate::constraints::Constraint::MaxCount(mc) => {
1052                format!("{} = {}", comp_name, mc.max_count)
1053            }
1054            crate::constraints::Constraint::Datatype(dt) => {
1055                format!(
1056                    "{} = {}",
1057                    comp_name,
1058                    self.compact_iri(dt.datatype_iri.as_str())
1059                )
1060            }
1061            crate::constraints::Constraint::Pattern(pat) => {
1062                format!("{} = /{}/", comp_name, pat.pattern)
1063            }
1064            crate::constraints::Constraint::MinLength(ml) => {
1065                format!("{} = {}", comp_name, ml.min_length)
1066            }
1067            crate::constraints::Constraint::MaxLength(ml) => {
1068                format!("{} = {}", comp_name, ml.max_length)
1069            }
1070            _ => comp_name,
1071        }
1072    }
1073}
1074
1075impl Default for ShapeVisualizer {
1076    fn default() -> Self {
1077        Self::new()
1078    }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use super::*;
1084    use crate::constraints::cardinality_constraints::MinCountConstraint;
1085    use crate::constraints::value_constraints::DatatypeConstraint;
1086    use crate::ShapeId;
1087    use oxirs_core::NamedNode;
1088
1089    fn create_test_shape() -> Shape {
1090        let mut shape = Shape::new(
1091            ShapeId::new("http://example.org/PersonShape"),
1092            ShapeType::NodeShape,
1093        );
1094        shape.label = Some("Person Shape".to_string());
1095        shape.targets.push(Target::Class(
1096            NamedNode::new("http://xmlns.com/foaf/0.1/Person").unwrap(),
1097        ));
1098        shape.add_constraint(
1099            ConstraintComponentId::new("sh:minCount"),
1100            crate::constraints::Constraint::MinCount(MinCountConstraint { min_count: 1 }),
1101        );
1102        shape.add_constraint(
1103            ConstraintComponentId::new("sh:datatype"),
1104            crate::constraints::Constraint::Datatype(DatatypeConstraint {
1105                datatype_iri: NamedNode::new("http://www.w3.org/2001/XMLSchema#string").unwrap(),
1106            }),
1107        );
1108        shape
1109    }
1110
1111    #[test]
1112    fn test_dot_export() {
1113        let visualizer = ShapeVisualizer::new();
1114        let shape = create_test_shape();
1115
1116        let dot = visualizer.to_dot(&[shape]);
1117        assert!(dot.contains("digraph"));
1118        assert!(dot.contains("PersonShape"));
1119    }
1120
1121    #[test]
1122    fn test_mermaid_export() {
1123        let visualizer = ShapeVisualizer::new();
1124        let shape = create_test_shape();
1125
1126        let mermaid = visualizer.to_mermaid(&[shape]);
1127        assert!(mermaid.contains("flowchart"));
1128        assert!(mermaid.contains("PersonShape"));
1129    }
1130
1131    #[test]
1132    fn test_json_schema_export() {
1133        let visualizer = ShapeVisualizer::new();
1134        let shape = create_test_shape();
1135
1136        let json_schema = visualizer.to_json_schema(&[shape]);
1137        assert!(json_schema.contains("$schema"));
1138        assert!(json_schema.contains("definitions"));
1139    }
1140
1141    #[test]
1142    fn test_svg_export() {
1143        let visualizer = ShapeVisualizer::new();
1144        let shape = create_test_shape();
1145
1146        let svg = visualizer.to_svg(&[shape]);
1147        assert!(svg.contains("<svg"));
1148        assert!(svg.contains("PersonShape"));
1149    }
1150
1151    #[test]
1152    fn test_plantuml_export() {
1153        let visualizer = ShapeVisualizer::new();
1154        let shape = create_test_shape();
1155
1156        let uml = visualizer.to_plantuml(&[shape]);
1157        assert!(uml.contains("@startuml"));
1158        assert!(uml.contains("@enduml"));
1159    }
1160
1161    #[test]
1162    fn test_d3_json_export() {
1163        let visualizer = ShapeVisualizer::new();
1164        let shape = create_test_shape();
1165
1166        let d3 = visualizer.to_d3_json(&[shape]);
1167        assert!(d3.contains("nodes"));
1168        assert!(d3.contains("links"));
1169    }
1170
1171    #[test]
1172    fn test_cytoscape_export() {
1173        let visualizer = ShapeVisualizer::new();
1174        let shape = create_test_shape();
1175
1176        let cytoscape = visualizer.to_cytoscape_json(&[shape]);
1177        assert!(cytoscape.contains("elements"));
1178        assert!(cytoscape.contains("style"));
1179    }
1180
1181    #[test]
1182    fn test_editor_protocol() {
1183        let visualizer = ShapeVisualizer::new();
1184        let shape = create_test_shape();
1185
1186        let protocol = visualizer.to_editor_protocol(&[shape]);
1187        assert!(protocol.contains("shacl-editor"));
1188        assert!(protocol.contains("shapes"));
1189    }
1190
1191    #[test]
1192    fn test_compact_iri() {
1193        let visualizer = ShapeVisualizer::new();
1194
1195        let compacted = visualizer.compact_iri("http://xmlns.com/foaf/0.1/Person");
1196        assert_eq!(compacted, "foaf:Person");
1197
1198        let compacted = visualizer.compact_iri("http://example.org/test");
1199        assert_eq!(compacted, "ex:test");
1200    }
1201}