M0: compilable skeleton — Kigi 0.1.0 fork surgery

Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,373 @@
use std::collections::{HashMap, HashSet};
use dagre_rust::{GraphConfig, GraphEdge, GraphNode};
use graphlib_rust::{Edge, Graph};
pub type DagreGraph = Graph<GraphConfig, GraphNode, GraphEdge>;
pub struct ExtractedCluster {
pub graph: DagreGraph,
pub children: HashMap<String, ExtractedCluster>,
}
#[derive(Debug, Clone)]
struct ClusterDbEntry {
anchor_id: String,
external_connections: bool,
}
#[derive(Debug, Default)]
struct AdjustState {
cluster_db: HashMap<String, ClusterDbEntry>,
descendants: HashMap<String, HashSet<String>>,
}
pub fn adjust_clusters_and_edges(graph: &mut DagreGraph) -> HashMap<String, ExtractedCluster> {
let mut state = AdjustState::default();
let mut parents: HashMap<String, String> = HashMap::new();
let nodes = graph.nodes();
for id in &nodes {
if graph.children(id).is_empty() {
continue;
}
let descendants = extract_descendants(id, graph, &mut parents);
state.descendants.insert(id.clone(), descendants);
let anchor_id = find_non_cluster_child(id, graph, id).unwrap_or_else(|| id.clone());
state.cluster_db.insert(
id.clone(),
ClusterDbEntry {
anchor_id,
external_connections: false,
},
);
}
let edges = graph.edges();
for id in &nodes {
if graph.children(id).is_empty() {
continue;
}
for edge in &edges {
let d1 = is_descendant(&edge.v, id, &state);
let d2 = is_descendant(&edge.w, id, &state);
if d1 != d2 {
if let Some(entry) = state.cluster_db.get_mut(id) {
entry.external_connections = true;
}
break;
}
}
}
let cluster_ids: Vec<String> = state.cluster_db.keys().cloned().collect();
for id in cluster_ids {
let Some(non_cluster_child) = state
.cluster_db
.get(&id)
.map(|entry| entry.anchor_id.clone())
else {
continue;
};
let Some(parent) = graph.parent(&non_cluster_child) else {
continue;
};
if parent == &id {
continue;
}
let Some(parent_entry) = state.cluster_db.get(parent) else {
continue;
};
if !parent_entry.external_connections {
if let Some(entry) = state.cluster_db.get_mut(&id) {
entry.anchor_id = parent.clone();
}
}
}
let edge_objs = graph.edges();
for edge_obj in edge_objs {
if !state.cluster_db.contains_key(&edge_obj.v)
&& !state.cluster_db.contains_key(&edge_obj.w)
{
continue;
}
let Some(edge_label) = graph.edge_with_obj(&edge_obj).cloned() else {
continue;
};
let v = get_anchor_id(&edge_obj.v, &state);
let w = get_anchor_id(&edge_obj.w, &state);
graph.remove_edge_with_obj(&edge_obj);
if v != edge_obj.v {
if let Some(parent) = graph.parent(&v) {
if let Some(entry) = state.cluster_db.get_mut(parent) {
entry.external_connections = true;
}
}
}
if w != edge_obj.w {
if let Some(parent) = graph.parent(&w) {
if let Some(entry) = state.cluster_db.get_mut(parent) {
entry.external_connections = true;
}
}
}
let _ = graph.set_edge(&v, &w, Some(edge_label), edge_obj.name.clone());
}
extractor(graph, &state, 0)
}
fn extract_descendants(
id: &String,
graph: &DagreGraph,
parents: &mut HashMap<String, String>,
) -> HashSet<String> {
let children = graph.children(id);
let mut res: HashSet<String> = children.iter().cloned().collect();
for child in children {
parents.insert(child.clone(), id.clone());
res.extend(extract_descendants(&child, graph, parents));
}
res
}
fn is_descendant(id: &String, ancestor_id: &String, state: &AdjustState) -> bool {
state
.descendants
.get(ancestor_id)
.is_some_and(|desc| desc.contains(id))
}
fn edge_in_cluster(edge: &Edge, cluster_id: &String, state: &AdjustState) -> bool {
if &edge.v == cluster_id || &edge.w == cluster_id {
return false;
}
let Some(cluster_descendants) = state.descendants.get(cluster_id) else {
return false;
};
cluster_descendants.contains(&edge.v) || cluster_descendants.contains(&edge.w)
}
fn find_common_edges(graph: &DagreGraph, id1: &String, id2: &String) -> Vec<(String, String)> {
let edges = graph.edges();
let edges1: Vec<&Edge> = edges
.iter()
.filter(|edge| &edge.v == id1 || &edge.w == id1)
.collect();
let edges2: Vec<&Edge> = edges
.iter()
.filter(|edge| &edge.v == id2 || &edge.w == id2)
.collect();
let edges1_prim: Vec<(String, String)> = edges1
.into_iter()
.map(|edge| {
let v = if &edge.v == id1 {
id2.clone()
} else {
edge.v.clone()
};
let w = if &edge.w == id1 {
id1.clone()
} else {
edge.w.clone()
};
(v, w)
})
.collect();
let edges2_prim: Vec<(String, String)> = edges2
.into_iter()
.map(|edge| (edge.v.clone(), edge.w.clone()))
.collect();
edges1_prim
.into_iter()
.filter(|(v, w)| edges2_prim.iter().any(|(v2, w2)| v == v2 && w == w2))
.collect()
}
fn find_non_cluster_child(id: &String, graph: &DagreGraph, cluster_id: &String) -> Option<String> {
let children = graph.children(id);
if children.is_empty() {
return Some(id.clone());
}
let mut reserve: Option<String> = None;
for child in children {
let Some(candidate) = find_non_cluster_child(&child, graph, cluster_id) else {
continue;
};
let candidate_id = candidate.clone();
let common_edges = find_common_edges(graph, cluster_id, &candidate_id);
if !common_edges.is_empty() {
reserve = Some(candidate);
} else {
return Some(candidate);
}
}
reserve
}
fn get_anchor_id(id: &String, state: &AdjustState) -> String {
let Some(entry) = state.cluster_db.get(id) else {
return id.clone();
};
if !entry.external_connections {
return id.clone();
}
entry.anchor_id.clone()
}
fn new_cluster_graph(rankdir: &str) -> DagreGraph {
let dir = if rankdir == "tb" { "lr" } else { "tb" };
let mut g: DagreGraph = Graph::new(Some(graphlib_rust::GraphOption {
directed: Some(true),
multigraph: Some(true),
compound: Some(true),
}));
g.set_graph(GraphConfig {
rankdir: Some(dir.to_string()),
nodesep: Some(50.0),
ranksep: Some(50.0),
marginx: Some(8.0),
marginy: Some(8.0),
..Default::default()
});
g
}
fn copy(
cluster_id: &String,
graph: &mut DagreGraph,
new_graph: &mut DagreGraph,
root_id: &String,
state: &AdjustState,
) {
let mut nodes = graph.children(cluster_id);
if cluster_id != root_id {
nodes.push(cluster_id.clone());
}
for node in nodes {
if !graph.children(&node).is_empty() {
copy(&node, graph, new_graph, root_id, state);
} else {
let Some(data) = graph.node(&node).cloned() else {
continue;
};
new_graph.set_node(node.clone(), Some(data));
if let Some(parent) = graph.parent(&node) {
if root_id != parent {
let _ = new_graph.set_parent(&node, Some(parent.clone()));
}
}
if cluster_id != root_id && node != *cluster_id {
let _ = new_graph.set_parent(&node, Some(cluster_id.clone()));
}
let edge_objs: Vec<Edge> = graph
.edges()
.into_iter()
.filter(|e| e.v == node || e.w == node)
.collect();
for edge_obj in edge_objs {
let Some(edge_label) = graph.edge_with_obj(&edge_obj).cloned() else {
continue;
};
if edge_in_cluster(&edge_obj, root_id, state) {
let _ = new_graph.set_edge(
&edge_obj.v,
&edge_obj.w,
Some(edge_label),
edge_obj.name.clone(),
);
}
}
}
graph.remove_node(&node);
}
}
fn extractor(
graph: &mut DagreGraph,
state: &AdjustState,
depth: usize,
) -> HashMap<String, ExtractedCluster> {
if depth > 10 {
return HashMap::new();
}
let nodes = graph.nodes();
if !nodes.iter().any(|node| !graph.children(node).is_empty()) {
return HashMap::new();
}
let mut extracted: HashMap<String, ExtractedCluster> = HashMap::new();
let rankdir = graph
.graph()
.rankdir
.clone()
.unwrap_or_else(|| "tb".to_string());
for node in nodes {
if graph.node(&node).is_none() {
continue;
}
if graph.children(&node).is_empty() {
continue;
}
let Some(entry) = state.cluster_db.get(&node) else {
continue;
};
if entry.external_connections {
continue;
}
let mut cluster_graph = new_cluster_graph(&rankdir);
copy(&node, graph, &mut cluster_graph, &node, state);
let children = extractor(&mut cluster_graph, state, depth + 1);
extracted.insert(
node.clone(),
ExtractedCluster {
graph: cluster_graph,
children,
},
);
}
extracted
}
@@ -0,0 +1,569 @@
use std::collections::HashMap;
use crate::ast::{EdgeStyle, FlowchartGraph, GraphDirection, NodeShape};
use crate::config::RenderConfig;
use crate::layout::{LayoutEdge, LayoutNode, LayoutResult, LayoutSubgraph};
use crate::text_wrap::{
measure_wrapped_lines_with_font_size, scale_char_width, wrap_text_lines, DEFAULT_CHAR_WIDTH,
DEFAULT_FONT_SIZE, DEFAULT_WRAP_WIDTH,
};
use dagre_rust::layout::layout as dagre_layout;
use dagre_rust::{GraphConfig, GraphEdge, GraphNode};
use graphlib_rust::Graph;
use super::cluster_adjust::{adjust_clusters_and_edges, ExtractedCluster};
use super::{flow_data, flow_db};
const FLOWCHART_PADDING: f64 = 15.0;
const EDGE_LABEL_PADDING: f64 = 2.0;
const SUBGRAPH_PADDING: f64 = 8.0;
#[derive(Debug, Clone)]
struct NodeMeta {
label: String,
shape: NodeShape,
width: f64,
height: f64,
fill_color: Option<String>,
stroke_color: Option<String>,
is_group: bool,
title: Option<String>,
}
#[derive(Debug, Clone)]
struct EdgeMeta {
label: Option<String>,
style: EdgeStyle,
}
#[derive(Debug, Default)]
struct LocalLayout {
nodes: HashMap<String, LayoutNode>,
edges: Vec<LayoutEdge>,
subgraphs: Vec<LayoutSubgraph>,
width: f64,
height: f64,
}
#[derive(Debug, Clone, Copy)]
struct PortLayoutOptions {
node_spacing: f64,
rank_spacing: f64,
padding: f64,
wrapping_width: f64,
font_size: f64,
}
impl Default for PortLayoutOptions {
fn default() -> Self {
Self {
node_spacing: 50.0,
rank_spacing: 50.0,
padding: FLOWCHART_PADDING,
wrapping_width: DEFAULT_WRAP_WIDTH,
font_size: DEFAULT_FONT_SIZE,
}
}
}
impl PortLayoutOptions {
fn from_render_config(config: &RenderConfig) -> Self {
let default = Self::default();
Self {
node_spacing: config
.flowchart
.node_spacing
.map(f64::from)
.unwrap_or(default.node_spacing),
rank_spacing: config
.flowchart
.rank_spacing
.map(f64::from)
.unwrap_or(default.rank_spacing),
padding: config
.flowchart
.padding
.map(f64::from)
.unwrap_or(default.padding),
wrapping_width: config
.flowchart
.wrapping_width
.map(f64::from)
.unwrap_or(default.wrapping_width),
font_size: config.font_size_px().unwrap_or(default.font_size),
}
}
}
pub fn compute_layout_ported(flowchart: &FlowchartGraph) -> LayoutResult {
compute_layout_ported_with_config(flowchart, &RenderConfig::default())
}
pub fn compute_layout_ported_with_config(
flowchart: &FlowchartGraph,
config: &RenderConfig,
) -> LayoutResult {
let options = PortLayoutOptions::from_render_config(config);
let db = flow_db::from_flowchart_graph(flowchart);
let data = flow_data::get_data(&db);
let rankdir = match db.direction {
GraphDirection::TopToBottom => "tb",
GraphDirection::BottomToTop => "bt",
GraphDirection::LeftToRight => "lr",
GraphDirection::RightToLeft => "rl",
};
let mut node_meta: HashMap<String, NodeMeta> = HashMap::new();
for node in &data.nodes {
let (fill_color, stroke_color) =
node.styles.iter().fold((None, None), |mut acc, (k, v)| {
if k == "fill" {
acc.0 = Some(v.clone());
}
if k == "stroke" {
acc.1 = Some(v.clone());
}
acc
});
let (width, height) = if node.is_group {
(0.0, 0.0)
} else {
measure_node(&node.label, node.shape, &options)
};
node_meta.insert(
node.id.clone(),
NodeMeta {
label: node.label.clone(),
shape: node.shape,
width,
height,
fill_color,
stroke_color,
is_group: node.is_group,
title: if node.is_group {
Some(node.label.clone())
} else {
None
},
},
);
}
let mut edge_meta: HashMap<(String, String), EdgeMeta> = HashMap::new();
let mut g: Graph<GraphConfig, GraphNode, GraphEdge> =
Graph::new(Some(graphlib_rust::GraphOption {
directed: Some(true),
multigraph: Some(true),
compound: Some(true),
}));
g.set_graph(GraphConfig {
rankdir: Some(rankdir.to_string()),
nodesep: Some(options.node_spacing as f32),
ranksep: Some(options.rank_spacing as f32),
marginx: Some(8.0),
marginy: Some(8.0),
..Default::default()
});
for node in &data.nodes {
if node.is_group {
g.set_node(
node.id.clone(),
Some(GraphNode {
width: 0.0,
height: 0.0,
padding: Some(SUBGRAPH_PADDING as f32),
..Default::default()
}),
);
} else {
let Some(meta) = node_meta.get(&node.id) else {
continue;
};
g.set_node(
node.id.clone(),
Some(GraphNode {
width: meta.width as f32,
height: meta.height as f32,
..Default::default()
}),
);
}
if let Some(parent_id) = &node.parent_id {
let _ = g.set_parent(&node.id, Some(parent_id.clone()));
}
}
for edge in &data.edges {
let mut edge_label = GraphEdge {
labelpos: Some("c".to_string()),
..Default::default()
};
if let Some(label) = &edge.label {
if let Some((width, height)) = edge_label_dimensions(label, &options) {
edge_label.width = Some(width as f32);
edge_label.height = Some(height as f32);
}
}
edge_meta.insert(
(edge.start.clone(), edge.end.clone()),
EdgeMeta {
label: edge.label.clone(),
style: edge.style,
},
);
let _ = g.set_edge(&edge.start, &edge.end, Some(edge_label), None);
}
let mut extracted = adjust_clusters_and_edges(&mut g);
apply_options_to_extracted(&mut extracted, &options);
let layout = layout_recursive(&mut g, &mut extracted, &node_meta, &edge_meta);
LayoutResult {
nodes: layout.nodes,
edges: layout.edges,
subgraphs: layout.subgraphs,
width: layout.width,
height: layout.height,
}
}
fn apply_options_to_extracted(
extracted: &mut HashMap<String, ExtractedCluster>,
options: &PortLayoutOptions,
) {
for cluster in extracted.values_mut() {
let graph_config = cluster.graph.graph_mut();
graph_config.nodesep = Some(options.node_spacing as f32);
graph_config.ranksep = Some(options.rank_spacing as f32);
apply_options_to_extracted(&mut cluster.children, options);
}
}
fn layout_recursive(
graph: &mut Graph<GraphConfig, GraphNode, GraphEdge>,
extracted: &mut HashMap<String, ExtractedCluster>,
node_meta: &HashMap<String, NodeMeta>,
edge_meta: &HashMap<(String, String), EdgeMeta>,
) -> LocalLayout {
let mut child_layouts: HashMap<String, LocalLayout> = HashMap::new();
for (cluster_id, cluster) in extracted.iter_mut() {
let layout = layout_recursive(
&mut cluster.graph,
&mut cluster.children,
node_meta,
edge_meta,
);
if let Some(node) = graph.node_mut(cluster_id) {
node.width = layout.width as f32;
node.height = layout.height as f32;
}
child_layouts.insert(cluster_id.clone(), layout);
}
dagre_layout(graph);
let mut layout = extract_local_layout(graph, node_meta, edge_meta);
for (cluster_id, mut child_layout) in child_layouts {
let Some(cluster_node) = graph.node(&cluster_id) else {
continue;
};
let dx = cluster_node.x as f64 - cluster_node.width as f64 / 2.0;
let dy = cluster_node.y as f64 - cluster_node.height as f64 / 2.0;
shift_layout(&mut child_layout, dx, dy);
for (id, node) in child_layout.nodes {
layout.nodes.insert(id, node);
}
layout.edges.extend(child_layout.edges);
layout.subgraphs.extend(child_layout.subgraphs);
}
layout
}
fn extract_local_layout(
graph: &Graph<GraphConfig, GraphNode, GraphEdge>,
node_meta: &HashMap<String, NodeMeta>,
edge_meta: &HashMap<(String, String), EdgeMeta>,
) -> LocalLayout {
let mut nodes: HashMap<String, LayoutNode> = HashMap::new();
let mut subgraphs: Vec<LayoutSubgraph> = Vec::new();
for node_id in graph.nodes() {
let Some(meta) = node_meta.get(&node_id) else {
continue;
};
let Some(node) = graph.node(&node_id) else {
continue;
};
if meta.is_group {
let width = node.width as f64;
let height = node.height as f64;
if width <= 0.0 || height <= 0.0 {
continue;
}
subgraphs.push(LayoutSubgraph {
id: node_id.clone(),
title: meta.title.clone(),
x: node.x as f64 - width / 2.0,
y: node.y as f64 - height / 2.0,
width,
height,
});
} else {
nodes.insert(
node_id.clone(),
LayoutNode {
id: node_id.clone(),
x: node.x as f64,
y: node.y as f64,
width: meta.width,
height: meta.height,
shape: meta.shape,
label: meta.label.clone(),
fill_color: meta.fill_color.clone(),
stroke_color: meta.stroke_color.clone(),
},
);
}
}
let mut edges: Vec<LayoutEdge> = Vec::new();
for edge_obj in graph.edges() {
let Some(edge_label) = graph.edge_with_obj(&edge_obj) else {
continue;
};
let Some(meta) = edge_meta.get(&(edge_obj.v.clone(), edge_obj.w.clone())) else {
continue;
};
let points: Vec<(f64, f64)> = edge_label
.points
.as_ref()
.map(|pts| pts.iter().map(|p| (p.x as f64, p.y as f64)).collect())
.unwrap_or_default();
let label_pos = meta.label.as_ref().and_then(|label| {
if label.trim().is_empty() {
None
} else if edge_label.width.unwrap_or(0.0) > 0.0
|| edge_label.height.unwrap_or(0.0) > 0.0
{
Some((edge_label.x as f64, edge_label.y as f64))
} else {
None
}
});
edges.push(LayoutEdge {
from: edge_obj.v,
to: edge_obj.w,
label: meta.label.clone(),
style: meta.style,
points,
label_pos,
});
}
let graph_width = graph.graph().width as f64;
let graph_height = graph.graph().height as f64;
LocalLayout {
nodes,
edges,
subgraphs,
width: graph_width,
height: graph_height,
}
}
fn shift_layout(layout: &mut LocalLayout, dx: f64, dy: f64) {
for node in layout.nodes.values_mut() {
node.x += dx;
node.y += dy;
}
for edge in &mut layout.edges {
for point in &mut edge.points {
point.0 += dx;
point.1 += dy;
}
if let Some((x, y)) = edge.label_pos.as_mut() {
*x += dx;
*y += dy;
}
}
for sg in &mut layout.subgraphs {
sg.x += dx;
sg.y += dy;
}
}
fn measure_node(label: &str, shape: NodeShape, options: &PortLayoutOptions) -> (f64, f64) {
let char_width = scale_char_width(DEFAULT_CHAR_WIDTH, options.font_size);
let lines = wrap_text_lines(label, options.wrapping_width, char_width);
let (text_width, text_height) =
measure_wrapped_lines_with_font_size(&lines, char_width, options.font_size);
let padding = options.padding;
match shape {
NodeShape::Rectangle => (text_width + padding * 4.0, text_height + padding * 2.0),
NodeShape::RoundedRectangle => (text_width + padding * 2.0, text_height + padding * 2.0),
NodeShape::Subroutine => {
let w = text_width + padding;
let h = text_height + padding;
(w + 16.0, h)
}
NodeShape::Asymmetric => {
let w = text_width + padding;
let h = text_height + padding;
(w + h / 4.0, h)
}
NodeShape::Hexagon => {
let h = text_height + padding;
let w = text_width + padding * 2.5;
(w * 7.0 / 6.0, h)
}
NodeShape::Diamond => {
let w = text_width + padding;
let h = text_height + padding;
let s = w + h;
(s, s)
}
NodeShape::Circle => {
let diameter = text_width + padding;
(diameter, diameter)
}
NodeShape::StartState => (14.0, 14.0),
NodeShape::EndState => (20.0, 20.0),
NodeShape::ForkJoin => (70.0, 10.0),
NodeShape::Stadium => {
let h = text_height + padding;
let w = text_width + h / 4.0 + padding;
(w, h)
}
NodeShape::Cylinder => {
let w = text_width + padding;
let rx = w / 2.0;
let ry = rx / (2.5 + w / 50.0);
let h = text_height + ry + padding;
(w, h + 2.0 * ry)
}
}
}
fn edge_label_dimensions(label: &str, options: &PortLayoutOptions) -> Option<(f64, f64)> {
if label.trim().is_empty() {
return None;
}
let char_width = scale_char_width(DEFAULT_CHAR_WIDTH, options.font_size);
let lines = wrap_text_lines(label, options.wrapping_width, char_width);
if lines.is_empty() {
return None;
}
let (text_width, text_height) =
measure_wrapped_lines_with_font_size(&lines, char_width, options.font_size);
let width = text_width + EDGE_LABEL_PADDING * 2.0;
let height = text_height + EDGE_LABEL_PADDING * 2.0;
Some((width, height))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Edge, FlowchartGraph, GraphDirection, Statement};
use crate::config::FlowchartConfig;
#[test]
fn ported_layout_uses_spacing_config() {
let graph = FlowchartGraph {
direction: GraphDirection::TopToBottom,
statements: vec![Statement::Edge(Edge {
from: "A".to_string(),
to: "B".to_string(),
label: None,
style: EdgeStyle::Arrow,
})],
};
let default_layout = compute_layout_ported(&graph);
let config = RenderConfig {
flowchart: FlowchartConfig {
rank_spacing: Some(140),
..Default::default()
},
..Default::default()
};
let configured_layout = compute_layout_ported_with_config(&graph, &config);
let default_delta = default_layout.nodes["B"].y - default_layout.nodes["A"].y;
let configured_delta = configured_layout.nodes["B"].y - configured_layout.nodes["A"].y;
assert!(configured_delta > default_delta);
}
#[test]
fn ported_layout_uses_padding_and_wrapping_config() {
let default = PortLayoutOptions::default();
let config = RenderConfig {
flowchart: FlowchartConfig {
padding: Some(4),
wrapping_width: Some(70),
..Default::default()
},
..Default::default()
};
let configured = PortLayoutOptions::from_render_config(&config);
let default_size = measure_node(
"Long label that wraps across several rendered lines",
NodeShape::Rectangle,
&default,
);
let configured_size = measure_node(
"Long label that wraps across several rendered lines",
NodeShape::Rectangle,
&configured,
);
assert!(configured_size.1 > default_size.1);
assert!(configured_size.0 < default_size.0);
}
#[test]
fn ported_layout_uses_font_size_config() {
let default = PortLayoutOptions::default();
let config = RenderConfig {
font_size: Some("32px".to_string()),
..Default::default()
};
let configured = PortLayoutOptions::from_render_config(&config);
let default_size = measure_node("Font", NodeShape::Rectangle, &default);
let configured_size = measure_node("Font", NodeShape::Rectangle, &configured);
let (default_label_width, default_label_height) =
edge_label_dimensions("Edge", &default).expect("label should have dimensions");
let (configured_label_width, configured_label_height) =
edge_label_dimensions("Edge", &configured).expect("label should have dimensions");
assert!(configured_size.0 > default_size.0);
assert!(configured_size.1 > default_size.1);
assert!(configured_label_width > default_label_width);
assert!(configured_label_height > default_label_height);
}
}
@@ -0,0 +1,75 @@
use crate::ast::{EdgeStyle, NodeShape};
use super::flow_db::{FlowDb, FlowEdge, FlowVertex};
#[derive(Debug, Clone)]
pub struct FlowData {
pub nodes: Vec<FlowDataNode>,
pub edges: Vec<FlowDataEdge>,
}
#[derive(Debug, Clone)]
pub struct FlowDataNode {
pub id: String,
pub label: String,
pub shape: NodeShape,
pub parent_id: Option<String>,
pub styles: Vec<(String, String)>,
pub is_group: bool,
}
#[derive(Debug, Clone)]
pub struct FlowDataEdge {
pub start: String,
pub end: String,
pub label: Option<String>,
pub style: EdgeStyle,
}
pub fn get_data(db: &FlowDb) -> FlowData {
let mut nodes: Vec<FlowDataNode> = Vec::new();
for sg in db.subgraphs.iter().rev() {
nodes.push(FlowDataNode {
id: sg.id.clone(),
label: sg.title.clone().unwrap_or_else(|| sg.id.clone()),
shape: NodeShape::Rectangle,
parent_id: sg.parent_id.clone(),
styles: Vec::new(),
is_group: true,
});
}
for id in &db.vertex_order {
if let Some(v) = db.vertices.get(id) {
nodes.push(make_node_data(db, v));
}
}
let edges: Vec<FlowDataEdge> = db.edges.iter().map(make_edge_data).collect();
FlowData { nodes, edges }
}
fn make_node_data(db: &FlowDb, v: &FlowVertex) -> FlowDataNode {
let parent_id = db.node_to_subgraph.get(&v.id).cloned();
let styles = db.node_styles.get(&v.id).cloned().unwrap_or_default();
FlowDataNode {
id: v.id.clone(),
label: v.label.clone(),
shape: v.shape,
parent_id,
styles,
is_group: false,
}
}
fn make_edge_data(e: &FlowEdge) -> FlowDataEdge {
FlowDataEdge {
start: e.start.clone(),
end: e.end.clone(),
label: e.label.clone(),
style: e.style,
}
}
+131
View File
@@ -0,0 +1,131 @@
use std::collections::HashMap;
use crate::ast::{EdgeStyle, FlowchartGraph, GraphDirection, NodeShape, Statement};
#[derive(Debug, Clone)]
pub struct FlowDb {
pub direction: GraphDirection,
pub vertices: HashMap<String, FlowVertex>,
pub vertex_order: Vec<String>,
pub edges: Vec<FlowEdge>,
pub subgraphs: Vec<FlowSubgraph>,
pub node_to_subgraph: HashMap<String, String>,
pub node_styles: HashMap<String, Vec<(String, String)>>,
}
#[derive(Debug, Clone)]
pub struct FlowVertex {
pub id: String,
pub label: String,
pub shape: NodeShape,
}
#[derive(Debug, Clone)]
pub struct FlowEdge {
pub start: String,
pub end: String,
pub label: Option<String>,
pub style: EdgeStyle,
}
#[derive(Debug, Clone)]
pub struct FlowSubgraph {
pub id: String,
pub title: Option<String>,
pub parent_id: Option<String>,
}
pub fn from_flowchart_graph(graph: &FlowchartGraph) -> FlowDb {
let mut db = FlowDb {
direction: graph.direction,
vertices: HashMap::new(),
vertex_order: Vec::new(),
edges: Vec::new(),
subgraphs: Vec::new(),
node_to_subgraph: HashMap::new(),
node_styles: HashMap::new(),
};
collect_statements(&mut db, &graph.statements, None);
db
}
fn collect_statements(db: &mut FlowDb, statements: &[Statement], current_subgraph: Option<&str>) {
for stmt in statements {
match stmt {
Statement::Node(node) => {
ensure_vertex(db, &node.id, node.label.as_deref(), node.shape);
maybe_assign_to_subgraph(db, &node.id, current_subgraph);
}
Statement::Edge(edge) => {
ensure_vertex(db, &edge.from, None, NodeShape::Rectangle);
ensure_vertex(db, &edge.to, None, NodeShape::Rectangle);
maybe_assign_to_subgraph(db, &edge.from, current_subgraph);
maybe_assign_to_subgraph(db, &edge.to, current_subgraph);
db.edges.push(FlowEdge {
start: edge.from.clone(),
end: edge.to.clone(),
label: edge.label.clone(),
style: edge.style,
});
}
Statement::Subgraph(subgraph) => {
collect_statements(db, &subgraph.statements, Some(&subgraph.id));
db.subgraphs.push(FlowSubgraph {
id: subgraph.id.clone(),
title: subgraph.title.clone().or_else(|| Some(subgraph.id.clone())),
parent_id: current_subgraph.map(|s| s.to_string()),
});
}
Statement::Style(style) => {
ensure_vertex(db, &style.node_id, None, NodeShape::Rectangle);
maybe_assign_to_subgraph(db, &style.node_id, current_subgraph);
db.node_styles
.entry(style.node_id.clone())
.or_default()
.extend(style.properties.iter().cloned());
}
}
}
}
fn ensure_vertex(db: &mut FlowDb, id: &str, label: Option<&str>, shape: NodeShape) {
if is_subgraph_id(db, id) {
return;
}
let id = id.to_string();
match db.vertices.get_mut(&id) {
Some(v) => {
if let Some(label) = label {
v.label = label.to_string();
v.shape = shape;
}
}
None => {
let label = label.unwrap_or(id.as_str()).to_string();
db.vertex_order.push(id.clone());
db.vertices
.insert(id.clone(), FlowVertex { id, label, shape });
}
}
}
fn maybe_assign_to_subgraph(db: &mut FlowDb, node_id: &str, current_subgraph: Option<&str>) {
if is_subgraph_id(db, node_id) {
return;
}
let Some(subgraph_id) = current_subgraph else {
return;
};
db.node_to_subgraph
.entry(node_id.to_string())
.or_insert_with(|| subgraph_id.to_string());
}
fn is_subgraph_id(db: &FlowDb, id: &str) -> bool {
db.subgraphs.iter().any(|subgraph| subgraph.id == id)
}
@@ -0,0 +1,6 @@
use crate::ast::FlowchartGraph;
use crate::error::MermaidError;
pub fn parse_flowchart(mermaid_source: &str) -> Result<FlowchartGraph, MermaidError> {
crate::parser::parse_mermaid(mermaid_source)
}
+40
View File
@@ -0,0 +1,40 @@
mod cluster_adjust;
mod dagre_layout_port;
mod flow_data;
mod flow_db;
mod flow_parser;
use crate::error::MermaidError;
use crate::theme::MermaidTheme;
use crate::RenderConfig;
pub fn render_mermaid_to_svg_ported(
mermaid_source: &str,
theme: &MermaidTheme,
config: &RenderConfig,
) -> Result<String, MermaidError> {
let graph = flow_parser::parse_flowchart(mermaid_source)?;
let layout_result = dagre_layout_port::compute_layout_ported_with_config(&graph, config);
Ok(crate::svg_renderer::render_with_config(
&layout_result,
theme,
config,
))
}
// HERMETIC VENDORING PATCH: the experimental dagre flowchart "port" is disabled
// unconditionally. Upstream gated it on the `MERMAID_TO_SVG_USE_PORT` env var;
// reading the environment makes rendering non-deterministic over untrusted
// input, and the port mis-routes back-edges on cyclic flowcharts (detached
// arrowheads) — the exact defect this engine was adopted to fix. The default
// `layout::compute_layout` path routes cycles correctly.
pub fn is_enabled() -> bool {
false
}
#[allow(dead_code)]
pub(crate) fn compute_layout_ported(
flowchart: &crate::ast::FlowchartGraph,
) -> crate::layout::LayoutResult {
dagre_layout_port::compute_layout_ported(flowchart)
}