konoma 0.28.5

Terminal file browser built for AI pair-programming — full-screen previews (Markdown, images, PDF, CSV), a git suite (jj/Jujutsu in preview), and an agent-watch mode that follows your AI's edits (macOS and Linux)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! The intermediate representation the flowchart parser produces.
//!
//! This is the whole output of stage 1b: structure, no geometry. Nothing here knows about
//! pixels, fonts or SVG — those arrive in later stages and read this model. Keeping the two
//! apart is what makes the parser testable on its own: every rule in
//! `docs/FEATURE-MERMAID-RENDERER.md` §2 can be checked by asserting on these values, without
//! a font database or a layout run.
//!
//! The shape of the model deliberately mirrors mermaid's own `FlowDB`, because that is the
//! only description of the language that is not a guess: node list in declaration order, edge
//! list in declaration order, and a flat list of subgraphs whose membership lists name their
//! *direct* children (nested subgraphs appear in their parent by id).

use std::collections::HashMap;
use std::fmt;

/// Layout direction of a chart or of one subgraph.
///
/// mermaid accepts five spellings; `TD` ("top down") is an alias of `TB` and is folded into it
/// on the way in, exactly as `FlowDB::setDirection` does, so downstream code only ever sees
/// four values.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Direction {
    /// `TB` / `TD` — the default when the header names no direction.
    #[default]
    TopToBottom,
    /// `BT`
    BottomToTop,
    /// `LR`
    LeftToRight,
    /// `RL`
    RightToLeft,
}

impl Direction {
    /// The canonical mermaid spelling (`TD` normalises to `TB`).
    pub fn as_str(self) -> &'static str {
        match self {
            Direction::TopToBottom => "TB",
            Direction::BottomToTop => "BT",
            Direction::LeftToRight => "LR",
            Direction::RightToLeft => "RL",
        }
    }

    /// Parses one of `TB` `TD` `BT` `RL` `LR`. Anything else is not a direction.
    pub fn parse(s: &str) -> Option<Direction> {
        match s {
            "TB" | "TD" => Some(Direction::TopToBottom),
            "BT" => Some(Direction::BottomToTop),
            "LR" => Some(Direction::LeftToRight),
            "RL" => Some(Direction::RightToLeft),
            _ => None,
        }
    }
}

/// The outline a node is drawn with.
///
/// mermaid has 53 named shapes as of v11.17; konoma draws a small family and folds the rest
/// onto the nearest member (`docs/FEATURE-MERMAID-RENDERER.md` §2-4: "53 形状すべての描き分けは
/// 不要"). The *names* are still all recognised — an unknown name is an error, because that is
/// the difference between a diagram konoma understood and one it guessed at.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Shape {
    /// `A[text]`, and every boxy member of the 53. Also the shape of a node that was only ever
    /// mentioned, which is why it is the default.
    #[default]
    Rect,
    /// `A(text)`
    RoundedRect,
    /// `A([text])`
    Stadium,
    /// `A[[text]]`
    Subroutine,
    /// `A[(text)]`
    Cylinder,
    /// `A((text))`
    Circle,
    /// `A(((text)))`
    DoubleCircle,
    /// `A{text}`
    Diamond,
    /// `A{{text}}`
    Hexagon,
    /// `A>text]`
    Odd,
    /// `A[/text\]` — base at the bottom.
    Trapezoid,
    /// `A[\text/]` — base at the top.
    InvTrapezoid,
    /// `A[/text/]` — parallelogram leaning right.
    LeanRight,
    /// `A[\text\]` — parallelogram leaning left.
    LeanLeft,
    /// `A@{ shape: text }` — a label with no outline at all.
    Text,
}

/// What is drawn at the end(s) of an edge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Arrow {
    /// `---` — a plain line.
    None,
    /// `-->`
    Point,
    /// `--x`
    Cross,
    /// `--o`
    Circle,
    /// `<-->`
    DoublePoint,
    /// `x--x`
    DoubleCross,
    /// `o--o`
    DoubleCircle,
    /// The two halves of a `-- text -->` link disagreed (`x-- text -->`), which mermaid reports
    /// as `INVALID`. The edge still exists; it simply has no defensible arrow.
    Invalid,
}

/// How an edge's line is drawn.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Stroke {
    /// `---`
    Normal,
    /// `===`
    Thick,
    /// `-.-`
    Dotted,
    /// `~~~`
    Invisible,
    /// Start and end of a `-- text -->` link named different strokes.
    Invalid,
}

/// One node of the chart.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
    /// The identifier written in the source. Unique within a chart.
    pub id: String,
    /// The text drawn inside the shape, already decoded: `<br>` variants and literal `\n` have
    /// become real newlines and `#nn;` entities have become their characters. When the source
    /// never gave the node a label this is the id, which is what mermaid draws.
    pub label: String,
    /// The outline. Defaults to [`Shape::Rect`] for a node that was only ever mentioned.
    pub shape: Shape,
    /// Class names attached by `:::name` or by a `class` statement, in the order they were
    /// applied. konoma does not colour nodes yet; the names are carried so that it can.
    pub classes: Vec<String>,
    /// Declarations from a `style` statement naming this node, e.g. `fill:#f9f`.
    pub styles: Vec<String>,
}

/// One edge of the chart.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edge {
    /// `e1` for `A e1@--> B`, otherwise mermaid's generated `L_<from>_<to>_<n>`.
    pub id: String,
    /// True when the id came from the source rather than from the generator.
    pub user_defined_id: bool,
    /// Id of the source node (or of a subgraph used as an endpoint).
    pub from: String,
    /// Id of the target node (or of a subgraph used as an endpoint).
    pub to: String,
    /// Arrow heads.
    pub arrow: Arrow,
    /// Line style.
    pub stroke: Stroke,
    /// mermaid's "link length": the rank distance the layout should try to span. Derived from
    /// the number of dashes (`line.length - 1`), except for dotted links where it is the number
    /// of dots. Clamped to 10, as `FlowDB::addSingleLink` does.
    pub length: usize,
    /// The text on the edge, from either `-->|text|` or `-- text -->`, decoded like a node
    /// label. `None` when the link carried no text (which is not the same as an empty one).
    pub label: Option<String>,
    /// Class names attached by a `class` statement naming this edge's id.
    pub classes: Vec<String>,
}

/// One `subgraph` block.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subgraph {
    /// The identifier. Either written in the source (`subgraph one [Title]`, or `subgraph one`)
    /// or generated as `subGraph<n>`; see [`Subgraph::auto_id`].
    pub id: String,
    /// The text drawn on the block's frame. Empty for `subgraph` with no title at all.
    pub title: String,
    /// Ids of the *direct* members, in declaration order. A nested subgraph appears here by its
    /// own id, so the tree is read by following ids. A node that already belonged to an earlier
    /// subgraph is not repeated here (mermaid's `makeUniq`), which is what keeps a nested block's
    /// nodes out of its parent's list.
    pub members: Vec<String>,
    /// A `direction` statement inside the block, which overrides the chart direction for it.
    pub direction: Option<Direction>,
    /// Class names attached by a `class` statement naming this subgraph.
    pub classes: Vec<String>,
    /// True when [`Subgraph::id`] was generated because the source gave only a title.
    pub auto_id: bool,
}

/// One `classDef` statement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClassDef {
    /// The class name; `default` is a legal name and means "every node".
    pub name: String,
    /// The declarations, split on `,`, e.g. `["fill:#f9f", "stroke:#333"]`.
    pub styles: Vec<String>,
}

/// Which links a `linkStyle` statement applies to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkStyleTarget {
    /// `linkStyle default ...`
    Default,
    /// `linkStyle 0,2 ...` — indices into [`Flowchart::edges`].
    Indices(Vec<usize>),
}

/// One `linkStyle` statement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkStyle {
    /// Which links it applies to.
    pub target: LinkStyleTarget,
    /// The declarations, split on `,`.
    pub styles: Vec<String>,
    /// The `interpolate <curve>` argument, if the statement carried one — the strongest link in
    /// the curve priority chain `render::spec_of`'s own doc states (`linkStyle <n> interpolate` >
    /// `linkStyle default interpolate` > `%%{init}%%`'s `flowchart.curve` > `[ui] mermaid_curve`).
    pub interpolate: Option<String>,
}

/// A parsed `flowchart` / `graph` diagram.
#[derive(Debug, Clone, Default)]
pub struct Flowchart {
    /// Chart direction; `TB` when the header named none.
    pub direction: Direction,
    /// Nodes in declaration order.
    pub nodes: Vec<Node>,
    /// Edges in declaration order. `linkStyle` indices refer to positions in this list.
    pub edges: Vec<Edge>,
    /// Subgraphs in *completion* order — a nested block is registered before the block that
    /// contains it, because that is when its `end` is reached. mermaid numbers its generated
    /// `subGraph<n>` ids off the same counter, so the order is part of the observable output.
    pub subgraphs: Vec<Subgraph>,
    /// `classDef` statements, in source order.
    pub class_defs: Vec<ClassDef>,
    /// `linkStyle` statements, in source order.
    pub link_styles: Vec<LinkStyle>,
    /// `title:` from a YAML front matter block, if there was one.
    pub title: Option<String>,
    /// `flowchart.curve` from an `%%{init}%%` directive, if the source had one and it named that
    /// key (`preprocess::init_flowchart_curve`). The chart-wide default `[ui] mermaid_curve`
    /// falls back to when this is `None`; a per-edge `linkStyle ... interpolate` still wins over
    /// both (`render::spec_of`'s own doc has the full priority order).
    pub curve: Option<String>,
    /// `accTitle:` — an accessibility title. Not drawn.
    pub acc_title: Option<String>,
    /// `accDescr:` or `accDescr { ... }` — an accessibility description. Not drawn.
    pub acc_descr: Option<String>,
    /// id -> index into [`Flowchart::nodes`].
    index: HashMap<String, usize>,
}

impl Flowchart {
    /// An empty chart with a direction and (optionally) a front matter title and an `%%{init}%%`
    /// `flowchart.curve`. Used by the parser; the id index is private, so a struct literal cannot
    /// be written from outside.
    pub(super) fn new(
        direction: Direction,
        title: Option<String>,
        curve: Option<String>,
    ) -> Flowchart {
        Flowchart {
            direction,
            title,
            curve,
            ..Flowchart::default()
        }
    }

    /// Looks a node up by id.
    pub fn node(&self, id: &str) -> Option<&Node> {
        self.index.get(id).and_then(|i| self.nodes.get(*i))
    }

    /// Looks an edge up by id (generated or user-defined).
    pub fn edge(&self, id: &str) -> Option<&Edge> {
        self.edges.iter().find(|e| e.id == id)
    }

    /// Looks a subgraph up by id.
    pub fn subgraph(&self, id: &str) -> Option<&Subgraph> {
        self.subgraphs.iter().find(|s| s.id == id)
    }

    /// The ids of every node, in declaration order. Handy in tests and for the layout stage,
    /// which needs a stable iteration order to keep its output reproducible.
    pub fn node_ids(&self) -> Vec<&str> {
        self.nodes.iter().map(|n| n.id.as_str()).collect()
    }

    /// Registers a node if it is new and returns its index. Used by the parser only.
    pub(super) fn intern_node(&mut self, id: &str) -> usize {
        if let Some(i) = self.index.get(id) {
            return *i;
        }
        let i = self.nodes.len();
        self.nodes.push(Node {
            id: id.to_string(),
            label: id.to_string(),
            shape: Shape::Rect,
            classes: Vec::new(),
            styles: Vec::new(),
        });
        self.index.insert(id.to_string(), i);
        i
    }

    /// Rebuilds the id index after nodes have been removed. Used by the parser only.
    pub(super) fn reindex(&mut self) {
        self.index.clear();
        for (i, n) in self.nodes.iter().enumerate() {
            self.index.insert(n.id.clone(), i);
        }
    }

    pub(super) fn node_mut(&mut self, id: &str) -> Option<&mut Node> {
        let i = *self.index.get(id)?;
        self.nodes.get_mut(i)
    }

    pub(super) fn edge_mut(&mut self, id: &str) -> Option<&mut Edge> {
        self.edges.iter_mut().find(|e| e.id == id)
    }

    pub(super) fn subgraph_mut(&mut self, id: &str) -> Option<&mut Subgraph> {
        self.subgraphs.iter_mut().find(|s| s.id == id)
    }
}

/// Why a source could not be read as a flowchart.
///
/// The renderer's contract (`docs/FEATURE-MERMAID-RENDERER.md` §1) is that a failure means "do
/// not draw" — konoma falls back to the text diagram. So every variant here has to be a case
/// where drawing *something* would be worse than drawing nothing: a diagram of another kind, a
/// diagram with no nodes, an unclosed construct whose extent we would have to guess.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
    /// The source held no statements at all.
    Empty,
    /// The first keyword was not `flowchart` or `graph`. The renderer must not draw this: the
    /// current crate turns `venn-beta` into three boxes labelled "venn", "beta" and "sets".
    NotAFlowchart {
        /// The keyword that was found, for the message.
        header: String,
    },
    /// The header was a flowchart but the body declared no node. The current crate answers a
    /// bare `graph TD` with a 16x16 transparent SVG that gets blown up to full width.
    NoNodes,
    /// A shape bracket was opened and never closed, so the label's extent is a guess.
    UnclosedShape {
        /// 1-based line in the original source.
        line: usize,
    },
    /// A `"` was opened and never closed. In mermaid this silently swallows the rest of the
    /// diagram, which is exactly the kind of quiet wrong answer that must not be drawn.
    UnclosedString {
        /// 1-based line in the original source.
        line: usize,
    },
    /// `A@{ ... }` named a shape mermaid does not have, or spelled a real one with capitals or
    /// an underscore, both of which mermaid rejects outright.
    UnknownShape {
        /// The name as written.
        name: String,
        /// 1-based line in the original source.
        line: usize,
    },
    /// `A@{ ... }` was opened and never closed.
    UnclosedShapeData {
        /// 1-based line in the original source.
        line: usize,
    },
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ParseError::Empty => write!(f, "empty diagram"),
            ParseError::NotAFlowchart { header } => {
                write!(f, "not a flowchart: diagram starts with `{header}`")
            }
            ParseError::NoNodes => write!(f, "flowchart declares no nodes"),
            ParseError::UnclosedShape { line } => {
                write!(f, "unclosed node shape at line {line}")
            }
            ParseError::UnclosedString { line } => {
                write!(f, "unclosed `\"` at line {line}")
            }
            ParseError::UnknownShape { name, line } => write!(
                f,
                "no such shape: `{name}` at line {line} (shape names are lowercase and use `-`)"
            ),
            ParseError::UnclosedShapeData { line } => {
                write!(f, "unclosed `@{{` at line {line}")
            }
        }
    }
}

impl std::error::Error for ParseError {}