@@ -4,27 +4,45 @@ use std::io::Write;
44use std:: path:: PathBuf ;
55
66fn 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 ( '&' , "&" ) . replace ( '<' , "<" ) . replace ( '>' , ">" )
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