rust_lstar/automata/
dot_parser.rs1use super::state::State;
2use super::transition::Transition;
3use super::Automata;
4use crate::letter::Letter;
5use uuid::Uuid;
6
7pub fn parse_dot(input: &str) -> Result<Automata, String> {
9 let input = input.trim();
10 if input.is_empty() {
11 return Err("Input DOT is empty".to_string());
12 }
13
14 if !input.starts_with("digraph ") {
15 return Err("DOT should start with 'digraph'".to_string());
16 }
17
18 let i_start_graph_def = input
19 .find('{')
20 .ok_or("Missing opening '{' for graph definition")?;
21
22 let automata_name = input[8..i_start_graph_def].trim().trim_matches('"');
23
24 if automata_name.is_empty() {
25 return Err("Automata name is empty".to_string());
26 }
27
28 let graph_def = input[i_start_graph_def + 1..].trim();
29 let graph_entries: Vec<&str> = graph_def.split(';').collect();
30
31 let mut states: Vec<State> = Vec::new();
32 for graph_entry in graph_entries {
33 if let Err(e) = parse_graph_entry(graph_entry, &mut states) {
34 return Err(format!(
35 "Error parsing graph entry '{}': {}",
36 graph_entry, e
37 ));
38 }
39 }
40 Ok(Automata::new(
41 State::new("S0".to_string()),
42 automata_name.to_string(),
43 ))
44}
45
46pub fn parse_graph_entry(graph_entry: &str, states: &mut Vec<State>) -> Result<(), String> {
47 let graph_entry = graph_entry.trim();
48 if graph_entry.is_empty() {
49 return Ok(());
50 }
51
52 if graph_entry.contains("[shape=") {
53 if let Some(start) = graph_entry.find('"') {
55 if let Some(end) = graph_entry[start + 1..].find('"') {
56 let state_name = &graph_entry[start + 1..start + 1 + end];
57 states.push(State::new(state_name.to_string()));
58 }
59 }
60 } else if graph_entry.contains("->") && graph_entry.contains("label=") {
61 let parts: Vec<&str> = graph_entry.split("->").collect();
63 if parts.len() >= 2 {
64 let src = extract_quoted_value(parts[0])?;
65 let dest = extract_quoted_value(parts[1])?;
66
67 let label = extract_label_value(graph_entry)?;
68 let label_parts: Vec<&str> = label.split('/').map(|s| s.trim()).collect();
69 let (input, output) = if label_parts.len() >= 2 {
70 (label_parts[0].to_string(), label_parts[1].to_string())
71 } else {
72 (label, "".to_string())
73 };
74
75 let t_name =
76 extract_url_value(graph_entry).unwrap_or_else(|| Uuid::new_v4().to_string());
77
78 if let Some(state) = states.iter_mut().find(|s| s.name == src) {
80 state.add_transition(Transition::new_with_source(
81 t_name,
82 src.clone(),
83 State::new(dest),
84 Letter::new(input),
85 Letter::new(output),
86 ));
87 } else {
88 return Err(format!("Source state '{}' not found for transition", src));
89 }
90 }
91 }
92
93 Ok(())
94}
95
96fn extract_quoted_value(s: &str) -> Result<String, String> {
97 if let Some(start) = s.find('"') {
98 if let Some(end) = s[start + 1..].find('"') {
99 return Ok(s[start + 1..start + 1 + end].to_string());
100 }
101 }
102 Err("Quoted value not found".to_string())
103}
104
105fn extract_label_value(s: &str) -> Result<String, String> {
106 if let Some(label_pos) = s.find("label=\"") {
107 let label_start = label_pos + 7; if let Some(label_end_rel) = s[label_start..].find('"') {
109 return Ok(s[label_start..label_start + label_end_rel].to_string());
110 }
111 }
112 Err("Label value not found".to_string())
113}
114
115fn extract_url_value(s: &str) -> Option<String> {
116 if let Some(url_pos) = s.find("URL=") {
117 let url_start = url_pos + 4; if let Some(open_q) = s[url_start..].find('"') {
119 let real_start = url_start + open_q + 1;
120 if let Some(end_q) = s[real_start..].find('"') {
121 return Some(s[real_start..real_start + end_q].to_string());
122 }
123 }
124 }
125 None
126}
127
128pub fn build_dot_code(automata: &Automata) -> String {
130 let mut lines: Vec<String> = Vec::new();
131
132 lines.push(format!("digraph \"{}\" {{", automata.name));
133
134 let can_use_flat_transitions = !automata.transitions.is_empty()
135 && automata
136 .transitions
137 .iter()
138 .all(|transition| !transition.source_state.is_empty());
139
140 if can_use_flat_transitions {
141 let mut state_names: Vec<String> = vec![automata.initial_state.name.clone()];
142 for transition in &automata.transitions {
143 if !state_names.contains(&transition.source_state) {
144 state_names.push(transition.source_state.clone());
145 }
146 if !state_names.contains(&transition.output_state.name) {
147 state_names.push(transition.output_state.name.clone());
148 }
149 }
150
151 for state_name in &state_names {
152 let shape = if state_name == &automata.initial_state.name {
153 "doubleoctagon"
154 } else {
155 "ellipse"
156 };
157 lines.push(format!(
158 " \"{}\" [shape={}, style=filled, fillcolor=white, URL=\"{}\"];",
159 state_name, shape, state_name
160 ));
161 }
162
163 for transition in &automata.transitions {
164 let label = transition.label();
165 lines.push(format!(
166 " \"{}\" -> \"{}\" [fontsize=5, label=\"{}\", URL=\"{}\"];",
167 transition.source_state, transition.output_state.name, label, transition.name
168 ));
169 }
170 } else {
171 let states = automata.get_states();
173 for state in &states {
174 let shape = if state.name == automata.initial_state.name {
175 "doubleoctagon"
176 } else {
177 "ellipse"
178 };
179 lines.push(format!(
180 " \"{}\" [shape={}, style=filled, fillcolor=white, URL=\"{}\"];",
181 state.name, shape, state.name
182 ));
183 }
184
185 for current_state in &states {
186 for transition in ¤t_state.transitions {
187 let output_state = &transition.output_state;
188 let input = current_state.name.clone();
189 let output = output_state.name.clone();
190 let label = &transition.label();
191 lines.push(format!(
192 " \"{}\" -> \"{}\" [fontsize=5, label=\"{}\", URL=\"{}\"];",
193 input, output, label, transition.name
194 ));
195 }
196 }
197 }
198
199 lines.push("}".to_string());
200 lines.join("\n")
201}