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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user