Skip to main content

merman_analysis/
result.rs

1use crate::editor::FenceExpectedSyntax;
2use crate::{
3    AnalysisDiagnostic, AnalysisPayload, DocumentDiagram, DocumentDiagramKind, FenceDelimiter,
4    FenceLineItem, FenceMarker, FenceReferenceGroup, FenceSemanticItem, FenceTextIndex,
5    FenceTextIndexSource, SharedTextSlice, SourceDescriptor, SourceMap, Summary,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::BTreeMap;
10use std::fmt;
11
12#[derive(Debug, Clone)]
13pub struct AnalysisResult {
14    payload: AnalysisPayload,
15    source_map: SourceMap,
16    diagrams: Vec<AnalyzedDiagram>,
17}
18
19impl AnalysisResult {
20    pub fn new(
21        source: SourceDescriptor,
22        source_map: SourceMap,
23        diagnostics: Vec<AnalysisDiagnostic>,
24        diagrams: Vec<AnalyzedDiagram>,
25    ) -> Self {
26        Self {
27            payload: AnalysisPayload::new(source, diagnostics),
28            source_map,
29            diagrams,
30        }
31    }
32
33    pub fn payload(&self) -> &AnalysisPayload {
34        &self.payload
35    }
36
37    pub fn into_payload(self) -> AnalysisPayload {
38        self.payload
39    }
40
41    pub fn source_map(&self) -> &SourceMap {
42        &self.source_map
43    }
44
45    pub fn diagrams(&self) -> &[AnalyzedDiagram] {
46        &self.diagrams
47    }
48
49    pub fn diagnostics(&self) -> &[AnalysisDiagnostic] {
50        &self.payload.diagnostics
51    }
52
53    pub fn to_facts_payload(&self) -> AnalysisFactsPayload {
54        AnalysisFactsPayload::from_result(self)
55    }
56}
57
58#[derive(Debug, Clone)]
59pub struct AnalyzedDiagram {
60    pub source_id: String,
61    pub index: usize,
62    pub kind: DocumentDiagramKind,
63    pub source: SourceDescriptor,
64    pub start: usize,
65    pub body_start: usize,
66    pub body_end: usize,
67    pub end: usize,
68    pub text: SharedTextSlice,
69    pub fence_delimiter: Option<FenceDelimiter>,
70    pub diagnostics: Vec<AnalysisDiagnostic>,
71    pub syntax: AnalysisSyntaxFacts,
72}
73
74impl AnalyzedDiagram {
75    pub fn from_document_diagram(
76        diagram: &DocumentDiagram,
77        diagnostics: Vec<AnalysisDiagnostic>,
78        syntax: AnalysisSyntaxFacts,
79    ) -> Self {
80        Self {
81            source_id: diagram.id.clone(),
82            index: diagram.index,
83            kind: diagram.kind,
84            source: diagram.source.clone(),
85            start: diagram.start,
86            body_start: diagram.body_start,
87            body_end: diagram.body_end,
88            end: diagram.end,
89            text: diagram.text.clone(),
90            fence_delimiter: diagram.fence_delimiter,
91            diagnostics,
92            syntax,
93        }
94    }
95}
96
97#[derive(Debug, Clone)]
98pub struct AnalysisSyntaxFacts {
99    pub diagram_type: Option<String>,
100    pub text_index: FenceTextIndex,
101    pub flowchart: Option<AnalysisFlowchartFacts>,
102}
103
104impl AnalysisSyntaxFacts {
105    pub fn new(diagram_type: Option<String>, text_index: FenceTextIndex) -> Self {
106        Self {
107            diagram_type,
108            text_index,
109            flowchart: None,
110        }
111    }
112
113    pub fn text_scan(text: &str, diagram_type: Option<String>) -> Self {
114        Self {
115            text_index: FenceTextIndex::from_text(text, diagram_type.as_deref()),
116            diagram_type,
117            flowchart: None,
118        }
119    }
120
121    pub fn source(&self) -> FenceTextIndexSource {
122        self.text_index.source()
123    }
124
125    pub fn with_flowchart(mut self, flowchart: Option<AnalysisFlowchartFacts>) -> Self {
126        self.flowchart = flowchart;
127        self
128    }
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct AnalysisFactsPayload {
133    pub version: u32,
134    pub valid: bool,
135    pub summary: Summary,
136    pub source: SourceDescriptor,
137    pub diagnostics: Vec<AnalysisDiagnostic>,
138    pub diagrams: Vec<AnalysisDiagramFacts>,
139}
140
141impl AnalysisFactsPayload {
142    pub fn from_result(result: &AnalysisResult) -> Self {
143        Self {
144            version: result.payload.version,
145            valid: result.payload.valid,
146            summary: result.payload.summary,
147            source: result.payload.source.clone(),
148            diagnostics: result.payload.diagnostics.clone(),
149            diagrams: result
150                .diagrams
151                .iter()
152                .map(|diagram| AnalysisDiagramFacts::from_diagram(diagram, &result.source_map))
153                .collect(),
154        }
155    }
156
157    pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
158        serde_json::to_vec(self)
159    }
160
161    pub fn to_pretty_json_string(&self) -> Result<String, serde_json::Error> {
162        serde_json::to_string_pretty(self)
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct AnalysisDiagramFacts {
168    pub source_id: String,
169    pub index: usize,
170    pub kind: String,
171    pub source: SourceDescriptor,
172    pub span: Option<crate::DiagnosticSpan>,
173    pub body_span: Option<crate::DiagnosticSpan>,
174    pub text_len: usize,
175    pub fence_delimiter: Option<AnalysisFenceDelimiterFacts>,
176    pub syntax: AnalysisDiagramSyntaxFacts,
177}
178
179impl AnalysisDiagramFacts {
180    fn from_diagram(diagram: &AnalyzedDiagram, source_map: &SourceMap) -> Self {
181        Self {
182            source_id: diagram.source_id.clone(),
183            index: diagram.index,
184            kind: diagram_kind_name(diagram.kind).to_string(),
185            source: diagram.source.clone(),
186            span: source_map.span(diagram.start, diagram.end).ok(),
187            body_span: source_map.span(diagram.body_start, diagram.body_end).ok(),
188            text_len: diagram.text.len(),
189            fence_delimiter: diagram
190                .fence_delimiter
191                .map(AnalysisFenceDelimiterFacts::from),
192            syntax: AnalysisDiagramSyntaxFacts::from_syntax(
193                &diagram.syntax,
194                source_map,
195                diagram.body_start,
196            ),
197        }
198    }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct AnalysisFenceDelimiterFacts {
203    pub marker: String,
204    pub len: usize,
205}
206
207impl From<FenceDelimiter> for AnalysisFenceDelimiterFacts {
208    fn from(value: FenceDelimiter) -> Self {
209        Self {
210            marker: fence_marker_name(value.marker()).to_string(),
211            len: value.len(),
212        }
213    }
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct AnalysisDiagramSyntaxFacts {
218    pub diagram_type: Option<String>,
219    pub fact_source: FenceTextIndexSource,
220    pub parser_backed: bool,
221    pub recovered: bool,
222    pub source_mapped_spans: bool,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub flowchart: Option<AnalysisFlowchartFacts>,
225    pub node_ids: Vec<String>,
226    pub class_names: Vec<String>,
227    pub directive_prefixes: Vec<String>,
228    pub references: Vec<AnalysisReferenceFacts>,
229    pub outline_items: Vec<AnalysisLineItemFacts>,
230    pub semantic_items: Vec<AnalysisSemanticItemFacts>,
231    pub expected_syntax: Vec<AnalysisExpectedSyntaxFacts>,
232}
233
234impl AnalysisDiagramSyntaxFacts {
235    fn from_syntax(
236        syntax: &AnalysisSyntaxFacts,
237        source_map: &SourceMap,
238        body_start: usize,
239    ) -> Self {
240        let text_index = &syntax.text_index;
241        let fact_source = text_index.source();
242
243        Self {
244            diagram_type: syntax.diagram_type.clone(),
245            fact_source,
246            parser_backed: fact_source.is_parser_backed(),
247            recovered: fact_source.is_recovered(),
248            source_mapped_spans: fact_source.has_source_mapped_spans(),
249            flowchart: syntax.flowchart.clone(),
250            node_ids: text_index.node_ids().cloned().collect(),
251            class_names: text_index.class_names().cloned().collect(),
252            directive_prefixes: text_index.directive_prefixes().cloned().collect(),
253            references: text_index
254                .references()
255                .map(|(group, spans)| {
256                    AnalysisReferenceFacts::from_reference(group, spans, source_map, body_start)
257                })
258                .collect(),
259            outline_items: text_index
260                .outline_items()
261                .iter()
262                .map(|item| AnalysisLineItemFacts::from_item(item, source_map, body_start))
263                .collect(),
264            semantic_items: text_index
265                .semantic_items()
266                .iter()
267                .map(|item| AnalysisSemanticItemFacts::from_item(item, source_map, body_start))
268                .collect(),
269            expected_syntax: text_index
270                .expected_syntax()
271                .iter()
272                .map(|expected| {
273                    AnalysisExpectedSyntaxFacts::from_expected(expected, source_map, body_start)
274                })
275                .collect(),
276        }
277    }
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
281pub struct AnalysisFlowchartFacts {
282    #[serde(default)]
283    pub direction: Option<String>,
284    #[serde(default, rename = "classDefs")]
285    pub class_defs: BTreeMap<String, Vec<String>>,
286    #[serde(default, rename = "edgeDefaults")]
287    pub edge_defaults: Option<AnalysisFlowchartEdgeDefaults>,
288    #[serde(default, rename = "vertexCalls")]
289    pub vertex_calls: Vec<String>,
290    #[serde(default)]
291    pub nodes: Vec<AnalysisFlowchartNodeFacts>,
292    #[serde(default)]
293    pub edges: Vec<AnalysisFlowchartEdgeFacts>,
294    #[serde(default)]
295    pub subgraphs: Vec<AnalysisFlowchartSubgraphFacts>,
296    #[serde(default)]
297    pub tooltips: BTreeMap<String, String>,
298}
299
300impl AnalysisFlowchartFacts {
301    pub(crate) fn try_from_model(
302        model: &Value,
303    ) -> Result<Option<Self>, AnalysisFlowchartFactsProjectionError> {
304        let diagram_type = model.get("type").and_then(Value::as_str);
305        if !matches!(
306            diagram_type,
307            Some("flowchart" | "flowchart-v2" | "flowchart-elk")
308        ) {
309            return Ok(None);
310        }
311
312        serde_json::from_value(model.clone())
313            .map(Some)
314            .map_err(AnalysisFlowchartFactsProjectionError::from)
315    }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub(crate) struct AnalysisFlowchartFactsProjectionError {
320    message: String,
321}
322
323impl fmt::Display for AnalysisFlowchartFactsProjectionError {
324    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
325        formatter.write_str(&self.message)
326    }
327}
328
329impl std::error::Error for AnalysisFlowchartFactsProjectionError {}
330
331impl From<serde_json::Error> for AnalysisFlowchartFactsProjectionError {
332    fn from(error: serde_json::Error) -> Self {
333        Self {
334            message: error.to_string(),
335        }
336    }
337}
338
339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
340pub struct AnalysisFlowchartEdgeDefaults {
341    #[serde(default)]
342    pub interpolate: Option<String>,
343    #[serde(default)]
344    pub style: Vec<String>,
345}
346
347#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
348pub struct AnalysisFlowchartNodeFacts {
349    pub id: String,
350    #[serde(default)]
351    pub label: Option<String>,
352    #[serde(default, rename = "labelType")]
353    pub label_type: Option<String>,
354    #[serde(default, rename = "layoutShape")]
355    pub layout_shape: Option<String>,
356    #[serde(default)]
357    pub icon: Option<String>,
358    #[serde(default)]
359    pub form: Option<String>,
360    #[serde(default)]
361    pub pos: Option<String>,
362    #[serde(default)]
363    pub img: Option<String>,
364    #[serde(default)]
365    pub constraint: Option<String>,
366    #[serde(default, rename = "assetWidth")]
367    pub asset_width: Option<f64>,
368    #[serde(default, rename = "assetHeight")]
369    pub asset_height: Option<f64>,
370    #[serde(default)]
371    pub classes: Vec<String>,
372    #[serde(default)]
373    pub styles: Vec<String>,
374    #[serde(default)]
375    pub link: Option<String>,
376    #[serde(default, rename = "linkTarget")]
377    pub link_target: Option<String>,
378    #[serde(default, rename = "haveCallback")]
379    pub have_callback: bool,
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383pub struct AnalysisFlowchartEdgeFacts {
384    pub id: String,
385    pub from: String,
386    pub to: String,
387    #[serde(default)]
388    pub label: Option<String>,
389    #[serde(default, rename = "labelType")]
390    pub label_type: Option<String>,
391    #[serde(default, rename = "type")]
392    pub edge_type: Option<String>,
393    #[serde(default)]
394    pub stroke: Option<String>,
395    #[serde(default)]
396    pub interpolate: Option<String>,
397    #[serde(default)]
398    pub classes: Vec<String>,
399    #[serde(default)]
400    pub style: Vec<String>,
401    #[serde(default)]
402    pub animate: Option<bool>,
403    #[serde(default)]
404    pub animation: Option<String>,
405    #[serde(default)]
406    pub length: usize,
407}
408
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
410pub struct AnalysisFlowchartSubgraphFacts {
411    pub id: String,
412    pub title: String,
413    #[serde(default)]
414    pub dir: Option<String>,
415    #[serde(default, rename = "labelType")]
416    pub label_type: Option<String>,
417    #[serde(default)]
418    pub classes: Vec<String>,
419    #[serde(default)]
420    pub styles: Vec<String>,
421    #[serde(default)]
422    pub nodes: Vec<String>,
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
426pub struct AnalysisReferenceFacts {
427    pub name: String,
428    pub kind: crate::EditorSymbolKind,
429    pub spans: Vec<AnalysisFactSpan>,
430}
431
432impl AnalysisReferenceFacts {
433    fn from_reference(
434        group: &FenceReferenceGroup,
435        spans: &[crate::ByteSpan],
436        source_map: &SourceMap,
437        body_start: usize,
438    ) -> Self {
439        Self {
440            name: group.name.clone(),
441            kind: group.kind,
442            spans: spans
443                .iter()
444                .copied()
445                .map(|span| AnalysisFactSpan::from_local(span, source_map, body_start))
446                .collect(),
447        }
448    }
449}
450
451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
452pub struct AnalysisLineItemFacts {
453    pub name: String,
454    pub detail: Option<String>,
455    pub kind: crate::EditorSymbolKind,
456    pub span: AnalysisFactSpan,
457    pub selection: AnalysisFactSpan,
458}
459
460impl AnalysisLineItemFacts {
461    fn from_item(item: &FenceLineItem, source_map: &SourceMap, body_start: usize) -> Self {
462        Self {
463            name: item.name.clone(),
464            detail: item.detail.clone(),
465            kind: item.kind,
466            span: AnalysisFactSpan::from_local(item.span, source_map, body_start),
467            selection: AnalysisFactSpan::from_local(item.selection, source_map, body_start),
468        }
469    }
470}
471
472#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
473pub struct AnalysisSemanticItemFacts {
474    pub name: String,
475    pub detail: Option<String>,
476    pub kind: crate::EditorSymbolKind,
477    pub role: crate::FenceSemanticRole,
478    pub span: AnalysisFactSpan,
479    pub selection: AnalysisFactSpan,
480}
481
482impl AnalysisSemanticItemFacts {
483    fn from_item(item: &FenceSemanticItem, source_map: &SourceMap, body_start: usize) -> Self {
484        Self {
485            name: item.name.clone(),
486            detail: item.detail.clone(),
487            kind: item.kind,
488            role: item.role,
489            span: AnalysisFactSpan::from_local(item.span, source_map, body_start),
490            selection: AnalysisFactSpan::from_local(item.selection, source_map, body_start),
491        }
492    }
493}
494
495#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
496pub struct AnalysisExpectedSyntaxFacts {
497    pub kind: crate::FenceExpectedSyntaxKind,
498    pub span: AnalysisFactSpan,
499}
500
501impl AnalysisExpectedSyntaxFacts {
502    fn from_expected(
503        expected: &FenceExpectedSyntax,
504        source_map: &SourceMap,
505        body_start: usize,
506    ) -> Self {
507        Self {
508            kind: expected.kind,
509            span: AnalysisFactSpan::from_local(expected.span, source_map, body_start),
510        }
511    }
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515pub struct AnalysisFactSpan {
516    pub local: crate::ByteSpan,
517    pub document: Option<crate::DiagnosticSpan>,
518}
519
520impl AnalysisFactSpan {
521    fn from_local(local: crate::ByteSpan, source_map: &SourceMap, body_start: usize) -> Self {
522        let document_start = body_start.saturating_add(local.start);
523        let document_end = body_start.saturating_add(local.end);
524        Self {
525            local,
526            document: source_map.span(document_start, document_end).ok(),
527        }
528    }
529}
530
531fn diagram_kind_name(kind: DocumentDiagramKind) -> &'static str {
532    match kind {
533        DocumentDiagramKind::WholeDocument => "whole_document",
534        DocumentDiagramKind::MermaidFence => "mermaid_fence",
535    }
536}
537
538fn fence_marker_name(marker: FenceMarker) -> &'static str {
539    match marker {
540        FenceMarker::Backtick => "backtick",
541        FenceMarker::Tilde => "tilde",
542        FenceMarker::Colon => "colon",
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549    use serde_json::json;
550
551    #[test]
552    fn flowchart_facts_accept_legacy_flowchart_models() {
553        let model = json!({
554            "type": "flowchart",
555            "direction": "LR",
556            "nodes": [
557                {
558                    "id": "A",
559                    "label": "Alpha"
560                }
561            ],
562            "edges": [
563                {
564                    "id": "L_A_B_0",
565                    "from": "A",
566                    "to": "B",
567                    "length": 1
568                }
569            ]
570        });
571
572        let facts = AnalysisFlowchartFacts::try_from_model(&model)
573            .expect("legacy flowchart model should deserialize")
574            .expect("legacy flowchart model should produce facts");
575
576        assert_eq!(facts.direction.as_deref(), Some("LR"));
577        assert!(
578            facts
579                .nodes
580                .iter()
581                .any(|node| node.id == "A" && node.label.as_deref() == Some("Alpha"))
582        );
583        assert!(
584            facts
585                .edges
586                .iter()
587                .any(|edge| edge.from == "A" && edge.to == "B")
588        );
589    }
590}