Skip to content

Commit bfcb7d5

Browse files
committed
Remove the message system tree website generation intermediate generation step
1 parent 51031d6 commit bfcb7d5

8 files changed

Lines changed: 220 additions & 172 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,8 @@ jobs:
175175
- name: 📃 Generate code documentation info for website
176176
if: github.event_name == 'push'
177177
run: |
178-
mkdir -p website/generated-new
179-
cargo run -p crate-hierarchy-viz -- website/generated-new/crate_hierarchy.svg
180-
cargo run -p editor-message-tree -- website/generated-new/hierarchical_message_system_tree.txt
178+
cargo run -p crate-hierarchy-viz -- website/generated-new
179+
cargo run -p editor-message-tree -- website/generated-new
181180
182181
- name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
183182
if: github.event_name == 'push'

.github/workflows/website.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,13 @@ jobs:
6464
rustup update stable
6565
echo "🦀 Latest updated version of Rust:"
6666
rustc --version
67-
cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.svg
68-
cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt
67+
cargo run -p crate-hierarchy-viz -- website/generated
68+
cargo run -p editor-message-tree -- website/generated
6969
70-
- name: 🔧 Build auto-generated code docs artifacts into HTML
70+
- name: 🔧 Install website npm dependencies
7171
run: |
7272
cd website
7373
npm ci
74-
npm run generate-editor-structure
7574
7675
- name: 📃 Generate node catalog documentation
7776
run: cargo run -p node-docs -- website/content/learn/node-catalog

tools/crate-hierarchy-viz/src/main.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,11 @@ fn collect_all_dependencies(crate_name: &str, dep_map: &HashMap<String, HashSet<
8585
}
8686

8787
fn main() -> Result<()> {
88-
let output_path = std::env::args_os()
88+
let output_dir = std::env::args_os()
8989
.nth(1)
9090
.map(PathBuf::from)
91-
.ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz <output-file>"))?;
91+
.ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz <output-directory>"))?;
92+
let output_path = output_dir.join("crate-hierarchy.svg");
9293

9394
let workspace_root = std::env::current_dir().unwrap();
9495
let workspace_toml_path = workspace_root.join("Cargo.toml");
@@ -179,9 +180,7 @@ fn main() -> Result<()> {
179180
let dot_content = generate_dot(&crates);
180181
let svg_content = dot_to_svg(&dot_content)?;
181182

182-
if let Some(parent) = output_path.parent() {
183-
fs::create_dir_all(parent).with_context(|| format!("Failed to create directory {:?}", parent))?;
184-
}
183+
fs::create_dir_all(&output_dir).with_context(|| format!("Failed to create directory {:?}", output_dir))?;
185184
fs::write(&output_path, &svg_content).with_context(|| format!("Failed to write to {:?}", output_path))?;
186185

187186
Ok(())

tools/editor-message-tree/src/main.rs

Lines changed: 206 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,45 @@ use std::io::Write;
44
use std::path::PathBuf;
55

66
fn main() -> Result<(), Box<dyn std::error::Error>> {
7-
let output_path = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree <output-file>")?;
7+
let output_dir = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree <output-directory>")?;
8+
std::fs::create_dir_all(&output_dir)?;
89

9-
if let Some(parent) = output_path.parent() {
10-
std::fs::create_dir_all(parent).unwrap();
10+
let tree = Message::message_tree();
11+
12+
// Write the .txt file (plain text tree outline, served as a static download)
13+
let static_dir = output_dir.join("../static/volunteer/guide/codebase-overview");
14+
std::fs::create_dir_all(&static_dir)?;
15+
let mut txt_file = std::fs::File::create(static_dir.join("hierarchical-message-system-tree.txt"))?;
16+
write_tree_txt(&tree, &mut txt_file);
17+
18+
// Write the .html file (structured HTML embedded in the website page)
19+
let mut html = String::new();
20+
write_tree_html(&tree, &mut html);
21+
std::fs::write(output_dir.join("hierarchical-message-system-tree.html"), &html)?;
22+
23+
Ok(())
24+
}
25+
26+
// =================
27+
// PLAIN TEXT OUTPUT
28+
// =================
29+
30+
fn write_tree_txt(tree: &DebugMessageTree, file: &mut std::fs::File) {
31+
if tree.path().is_empty() {
32+
let _ = file.write_all(format!("{}\n", tree.name()).as_bytes());
33+
} else {
34+
let _ = file.write_all(format!("{} `{}#L{}`\n", tree.name(), tree.path(), tree.line_number()).as_bytes());
1135
}
1236

13-
let result = Message::message_tree();
14-
let mut file = std::fs::File::create(&output_path).unwrap();
15-
file.write_all(format!("{} `{}#L{}`\n", result.name(), result.path(), result.line_number()).as_bytes()).unwrap();
16-
if let Some(variants) = result.variants() {
37+
if let Some(variants) = tree.variants() {
1738
for (i, variant) in variants.iter().enumerate() {
1839
let is_last = i == variants.len() - 1;
19-
print_tree_node(variant, "", is_last, &mut file);
40+
write_tree_txt_node(variant, "", is_last, file);
2041
}
2142
}
22-
23-
Ok(())
2443
}
2544

26-
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
27-
// Print the current node
45+
fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
2846
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
2947
("├── ", format!("{prefix}│ "))
3048
} else if is_last {
@@ -34,33 +52,28 @@ fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &
3452
};
3553

3654
if tree.path().is_empty() {
37-
file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes()).unwrap();
55+
let _ = file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes());
3856
} else {
39-
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes())
40-
.unwrap();
57+
let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes());
4158
}
4259

43-
// Print children if any
4460
if let Some(variants) = tree.variants() {
4561
let len = variants.len();
4662
for (i, variant) in variants.iter().enumerate() {
4763
let is_last_child = i == len - 1;
48-
print_tree_node(variant, &child_prefix, is_last_child, file);
64+
write_tree_txt_node(variant, &child_prefix, is_last_child, file);
4965
}
5066
}
5167

52-
// Print message field if any
5368
if let Some(fields) = tree.fields() {
5469
let len = fields.len();
5570
for (i, field) in fields.iter().enumerate() {
5671
let is_last_field = i == len - 1;
5772
let branch = if is_last_field { "└── " } else { "├── " };
58-
59-
file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()).unwrap();
73+
let _ = file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes());
6074
}
6175
}
6276

63-
// Print handler field if any
6477
if let Some(data) = tree.message_handler_fields() {
6578
let len = data.fields().len();
6679
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
@@ -73,32 +86,195 @@ fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &
7386
if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR {
7487
panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name());
7588
} else if tree.name() != FRONTEND_MESSAGE_STR {
76-
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes())
77-
.unwrap();
89+
let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes());
7890

7991
for (i, field) in data.fields().iter().enumerate() {
8092
let is_last_field = i == len - 1;
8193
let branch = if is_last_field { "└── " } else { "├── " };
82-
83-
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
94+
let _ = file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes());
8495
}
8596
}
8697
}
8798

88-
// Print data field if any
8999
if let Some(data) = tree.message_handler_data_fields() {
90100
let len = data.fields().len();
91101
if data.path().is_empty() {
92-
file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes()).unwrap();
102+
let _ = file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes());
93103
} else {
94-
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes())
95-
.unwrap();
104+
let _ = file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes());
96105
}
97106
for (i, field) in data.fields().iter().enumerate() {
98107
let is_last_field = i == len - 1;
99108
let branch = if is_last_field { "└── " } else { "├── " };
100109
let field = &field.0;
101-
file.write_all(format!("{prefix} {branch}{field}\n").as_bytes()).unwrap();
110+
let _ = file.write_all(format!("{prefix} {branch}{field}\n").as_bytes());
111+
}
112+
}
113+
}
114+
115+
// ===========
116+
// HTML OUTPUT
117+
// ===========
118+
119+
const GITHUB_BASE: &str = "https://github.com/GraphiteEditor/Graphite/blob/master/";
120+
const NAMING_SUFFIXES: &[&str] = &["Message", "MessageHandler", "MessageContext"];
121+
122+
fn escape_html(s: &str) -> String {
123+
s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
124+
}
125+
126+
fn github_link(path: &str, line: usize) -> String {
127+
let path = path.replace('\\', "/");
128+
let filename = path.rsplit('/').next().unwrap_or(&path);
129+
format!(r#"<a href="{GITHUB_BASE}{path}#L{line}" target="_blank">{filename}:{line}</a>"#)
130+
}
131+
132+
fn naming_convention_warning(name: &str) -> &'static str {
133+
// Strip generic parameters for the check (e.g. `Foo<Bar>` -> `Foo`)
134+
let base_name = name.split('<').next().unwrap_or(name);
135+
if NAMING_SUFFIXES.iter().any(|suffix| base_name.ends_with(suffix)) {
136+
""
137+
} else {
138+
r#"<span class="warn">(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')</span>"#
139+
}
140+
}
141+
142+
fn write_tree_html(tree: &DebugMessageTree, out: &mut String) {
143+
// Root node
144+
let link = if !tree.path().is_empty() { github_link(tree.path(), tree.line_number()) } else { String::new() };
145+
let escaped_name = escape_html(tree.name());
146+
147+
out.push_str("<ul>\n");
148+
out.push_str(&format!(r#"<li><span class="tree-node"><span class="subsystem">{escaped_name}</span>{link}</span>"#));
149+
150+
if let Some(variants) = tree.variants() {
151+
out.push_str(r#"<div class="nested">"#);
152+
write_tree_html_children(variants, out);
153+
out.push_str("</div>");
154+
}
155+
156+
out.push_str("</li>\n</ul>\n");
157+
}
158+
159+
fn write_tree_html_children(variants: &[DebugMessageTree], out: &mut String) {
160+
out.push_str("<ul>\n");
161+
for variant in variants {
162+
write_tree_html_node(variant, out);
163+
}
164+
out.push_str("</ul>\n");
165+
}
166+
167+
fn write_tree_html_node(tree: &DebugMessageTree, out: &mut String) {
168+
let has_link = !tree.path().is_empty();
169+
let link = if has_link { github_link(tree.path(), tree.line_number()) } else { String::new() };
170+
let escaped_name = escape_html(tree.name());
171+
172+
enum HtmlChild<'a> {
173+
Subtree(&'a DebugMessageTree),
174+
Field(String),
175+
HandlerFields(String, String, usize, Vec<String>),
176+
DataFields(String, String, usize, Vec<String>),
177+
}
178+
179+
// Collect all child entries for this node
180+
let mut children: Vec<HtmlChild> = Vec::new();
181+
182+
if let Some(variants) = tree.variants() {
183+
for variant in variants {
184+
children.push(HtmlChild::Subtree(variant));
185+
}
186+
}
187+
188+
if let Some(fields) = tree.fields() {
189+
for field in fields {
190+
children.push(HtmlChild::Field(field.to_string()));
191+
}
192+
}
193+
194+
const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
195+
if let Some(data) = tree.message_handler_fields()
196+
&& (!data.name().is_empty() || tree.name() == FRONTEND_MESSAGE_STR)
197+
&& tree.name() != FRONTEND_MESSAGE_STR
198+
{
199+
children.push(HtmlChild::HandlerFields(
200+
data.name().to_string(),
201+
data.path().to_string(),
202+
data.line_number(),
203+
data.fields().iter().map(|f| f.0.clone()).collect(),
204+
));
205+
}
206+
207+
if let Some(data) = tree.message_handler_data_fields() {
208+
children.push(HtmlChild::DataFields(
209+
data.name().to_string(),
210+
data.path().to_string(),
211+
data.line_number(),
212+
data.fields().iter().map(|f| f.0.clone()).collect(),
213+
));
214+
}
215+
216+
let has_children = !children.is_empty();
217+
let has_deeper_children = children.iter().any(|child| matches!(child, HtmlChild::Subtree(t) if t.variants().is_some() || t.fields().is_some()));
218+
219+
// Determine role
220+
let role = if has_link {
221+
"subsystem"
222+
} else if has_deeper_children {
223+
"submessage"
224+
} else {
225+
"message"
226+
};
227+
228+
// Naming convention warning (only for linked/subsystem nodes)
229+
let warning = if has_link { naming_convention_warning(tree.name()) } else { "" };
230+
231+
if has_children {
232+
out.push_str(&format!(r#"<li><span class="tree-node"><span class="{role}">{escaped_name}</span>{link}{warning}</span>"#));
233+
out.push_str(r#"<div class="nested"><ul>"#);
234+
out.push('\n');
235+
236+
for child in &children {
237+
match child {
238+
HtmlChild::Subtree(subtree) => write_tree_html_node(subtree, out),
239+
HtmlChild::Field(field) => write_field_html(field, out),
240+
HtmlChild::HandlerFields(name, path, line, fields) => write_handler_or_data_html(name, path, *line, fields, out),
241+
HtmlChild::DataFields(name, path, line, fields) => write_handler_or_data_html(name, path, *line, fields, out),
242+
}
243+
}
244+
245+
out.push_str("</ul>\n</div></li>\n");
246+
} else {
247+
out.push_str(&format!(r#"<li><span class="tree-leaf {role}">{escaped_name}</span>{link}{warning}</li>"#));
248+
out.push('\n');
249+
}
250+
}
251+
252+
fn write_field_html(field: &str, out: &mut String) {
253+
if let Some((name, ty)) = field.split_once(':') {
254+
let name = escape_html(name.trim());
255+
let ty = escape_html(ty.trim());
256+
out.push_str(&format!(r#"<li><span class="tree-leaf field">{name}</span>: <span>{ty}</span></li>"#));
257+
} else {
258+
let escaped = escape_html(field);
259+
out.push_str(&format!(r#"<li><span class="tree-leaf message">{escaped}</span></li>"#));
260+
}
261+
out.push('\n');
262+
}
263+
264+
fn write_handler_or_data_html(name: &str, path: &str, line: usize, fields: &[String], out: &mut String) {
265+
let escaped_name = escape_html(name);
266+
let link = if !path.is_empty() { github_link(path, line) } else { String::new() };
267+
let warning = if !path.is_empty() { naming_convention_warning(name) } else { "" };
268+
269+
if fields.is_empty() {
270+
out.push_str(&format!(r#"<li><span class="tree-leaf subsystem">{escaped_name}</span>{link}{warning}</li>"#));
271+
} else {
272+
out.push_str(&format!(r#"<li><span class="tree-node"><span class="subsystem">{escaped_name}</span>{link}{warning}</span>"#));
273+
out.push_str(r#"<div class="nested"><ul>"#);
274+
out.push('\n');
275+
for field in fields {
276+
write_field_html(field, out);
102277
}
278+
out.push_str("</ul>\n</div></li>\n");
103279
}
104280
}

0 commit comments

Comments
 (0)