stanhope/
main.rs

1#![doc(html_logo_url = "https://images.squarespace-cdn.com/content/v1/60416644b68a5453a868e856/1628206791960-M61IU1QP850NRLI9CE07/Stanhope+Etching.jpg?format=2500w")]
2//! **Stanhope**, named after the Stanhope Printing Press that improved upon Gutenberg's design by using sturdy iron and steel parts and hand levers to increase printing speed and quality.
3//! 
4//! This application takes an input argument file in a specific markup and produces a static, printable HTML page based on the markup file's contents.
5//! 
6//! The real power in this application is the markup language, which has these features:
7//! * Single-purpose: just for writing processes that can be printed as documents
8//! * General: suitable for writing any process
9//! * Consistent: output branding and style is centrally controlled for 100% consistent processes in an organization's library
10//! * *Visually* flexible: custom templates can mold the look/feel of the HTML produced
11//! * *Format* rigid: we want process authors to think carefully to help operators in the field, and Stanhope forces authors to conform to pre-thought out process language and structure that solves this
12
13mod read_csv;
14mod read_ebml;
15mod write_ebml;
16mod write_html;
17mod write_script;
18mod write_webmenu;
19
20//use std::process::Command; // needed for current implementation of PDF generation
21use std::{
22    process::Command,
23    //fs::{self, File},
24    //io::{self, BufRead, BufReader},
25    fs::{self},
26    io::{self, Error, ErrorKind, Result},
27    path::Path,
28};
29use chrono::prelude::*;
30use read_ebml::Process;
31
32use clap::Parser; // used for command line parameters and --help generation
33use read_csv::read_csv; // 
34use write_ebml::get_process_folder_name;
35use write_ebml::write_ebml;
36use read_ebml::read_ebml;
37use write_html::generate_complete_html;
38use write_webmenu::generate_complete_webmenu;
39use glob::glob;
40
41
42/// Command Line Interface (CLI) argument structure
43#[derive(Parser, Debug)]
44#[command(version, about =
45"\n\n\x1b[1;30;47mStanope\x1b[0m\x1b[30;47m, the Easy Button process generation engine.\x1b[0m
46
47Arguments passed into the options should be surrounded by single or double quotes, e.g.
48% ./stanhope -p \"EB-WI-0010\"    \x1b[36m<< double quotes are accepted\x1b[0m
49% ./stanhope -a 'EB-WI-*'       \x1b[36m<< single quotes are accepted\x1b[0m",
50author = "Strativus Group <contact@strativusgroup.com>",
51long_about = None,
52after_help = 
53"
54")]
55struct StanhopeArgs {
56    /// Read a single CSV file to create many new first-draft processes from scratch
57    /// 
58    /// - CSV-FILE
59    ///   - Comma-separated text file that strictly conforms to the template below
60    ///   - Exception: _TFlag_ columns (7th column and beyond) are user-defined
61    ///   - Free Excel file to modify and export as CSV: <https://stanhope.strativusgroup.com/latest/DocumentList.xlsx>
62    /// 
63    /// | "Document Number" | "Title" | "Subject" | "Product" | "Author" | "Reviewer" | _TFlag_ | _TFlag_ | ... |
64    /// | ----------------- | ------- | --------- | --------- | -------- | ---------- | ------- | ------- | --- |
65    /// | ...               | ...     | ...       | ...       | ...      | ...        |    x    |         | ... |
66    /// | ...               | ...     | ...       | ...       | ...      | ...        |         |    x    | ... |
67    /// | ...               | ...     | ...       | ...       | ...      | ...        |         |         | ... |
68    /// | ...               | ...     | ...       | ...       | ...      | ...        |    x    |    x    | ... |
69    #[arg(short, long, value_name = "CSV-FILE", default_value_t = String::from(""), verbatim_doc_comment)]
70    listgen: String,
71
72    /// Generate (or overwrite) a menu that indexes useful information for every process in a library
73    /// 
74    /// WebMenu.html is generated in the Process Library root folder
75    #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
76    webmenu: bool,
77
78    /// Read a single EBML and produce an HTML file as well as a PDF of that process
79    /// 
80    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
81    ///   - Wildcards are accepted, e.g. *, ?, [0-9]
82    ///   - Wildcards follow "glob" formatting: <https://docs.rs/glob/0.3.3/glob/struct.Pattern.html>
83    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
84    process_ebml: String,
85
86    /// Export every "Command" line from a single process into a script with a defined language/format
87    /// 
88    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
89    /// - SCRIPT-FORMAT currently accepts
90    ///     ActionScript, AppleScript, bash, CoffeeScript, Dart,
91    ///     Elixir, JavaScript, Julia, Lua, MATLAB, Perl, PHP,
92    ///     PowerShell, Python, R, Ruby, TypeScript, VB.NET
93    ///         Full list and backlog:  https://stanhope.strativusgroup.com/doc/stanhope/write_script/fn.learn.html
94    /// - PAUSE-AFTER-EACH is true or false, true meaning "pause after each command"
95    #[arg(short, long, use_value_delimiter = true, value_delimiter = ' ', num_args = 3, value_names = ["DOCUMENT-NUMBER","SCRIPT-FORMAT","PAUSE-AFTER-EACH"], verbatim_doc_comment)] 
96    scriptify_process: Vec<String>,
97
98    /// Only perform the archiving process, i.e. create a new archive in "previous"
99    /// 
100    /// Note: this operation is performed at the end of every "Process EBML" action
101    /// This option _only_ performs the archiving, not the processing.
102    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
103    ///   - Wildcards are accepted, e.g. *, ?, [0-9]
104    ///   - Wildcards follow "glob" formatting: <https://docs.rs/glob/0.3.3/glob/struct.Pattern.html>
105    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
106    archive_process: String,
107
108    /// Inspect a document by reading its EBML and returning information (no file generation)
109    /// 
110    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
111    ///   - Wildcards are accepted, e.g. *, ?, [0-9]
112    ///   - Wildcards follow "glob" formatting: <https://docs.rs/glob/0.3.3/glob/struct.Pattern.html>
113    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
114    inspect_process: String,
115
116    /// Delete all but one "previous" version for each Revision for a given process
117    /// 
118    ///   For example, for a document that has many "previous" copies across four
119    ///   Revisions (-,A,B,C), then this option deletes all subdirectories except
120    ///   four: the one with the latest timestamp for each Revision variant
121    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
122    cleanup_previous: String,
123
124    /// Output more information to stdout as Stanhope executes (flag)
125    #[arg(short, long, default_value_t = false,)]
126    verbose: bool,
127
128    /// Display syntax help for Easy Button Markup Language (EBML)
129    #[arg(short, long, default_value_t = false,)]
130    ebml_help: bool,
131}
132
133/// Recursive copy of all contents in one folder to another
134fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
135    fs::create_dir_all(&dst)?;
136    for entry in fs::read_dir(src)? {
137        let entry = entry?;
138        let ty = entry.file_type()?;
139        if ty.is_dir() {
140            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
141        } else {
142            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
143        }
144    }
145    Ok(())
146}
147
148/// Copy everything that ISN'T a directory... just the files
149fn copy_files_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
150    fs::create_dir_all(&dst)?;
151    for entry in fs::read_dir(src)? {
152        let entry = entry?;
153        let ty = entry.file_type()?;
154        if ty.is_dir() {
155            ();
156        } else {
157            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
158        }
159    }
160    Ok(())
161}
162
163/// Perform OS functions to stash a copy of all working material for a process, with specifically-named archive folder in "previous"
164fn archive_single_process(new_proc: Process, verbose: &bool) {
165    // if it doesn't already exist, make a new folder called "previous"
166    //let previous_folder = new_proc.get_number().to_string()+"/previous"; // Old convention: process number is the process folder name. Now it's ( NUMBER - TITLE )
167    let previous_folder = get_process_folder_name(&new_proc).to_owned() + "/previous";
168    match Path::new(&previous_folder).exists() {
169        true => (),
170        false => { let _ = fs::create_dir(&previous_folder); },
171    };
172    let root_assets_folder = "./assets";
173    let previous_assets_folder = previous_folder.clone() + "/assets";
174    match Path::new(&previous_assets_folder).exists() {
175        true => (),
176        false => { let _ = fs::create_dir(&previous_assets_folder); },
177    };
178
179    // make a new folder to archive everything we just did in "previous"
180    let rev_string = new_proc.get_revision();
181    let right_now_string = Utc::now().format("UTC-%Y-%m-%d-T%H-%M-%S").to_string();
182    let timestamp_folder = previous_folder.clone() + "/Rev_" + new_proc.get_revision() + "_" + &right_now_string;
183    if *verbose { 
184        println!("Revision: {}",&rev_string);
185        println!("SystemTime::now() ...ish: {}",right_now_string);
186        println!("Trying to create this folder: {:?}",timestamp_folder);
187    }
188    let _ = fs::create_dir(&timestamp_folder);
189    // copy assets
190    let _ = copy_dir_all(root_assets_folder,previous_assets_folder);
191    // copy EBML, HTML, and PDF plus any other non-folder files in the process directory
192    let _ = copy_files_all(get_process_folder_name(&new_proc),timestamp_folder.clone());
193}
194
195/// File management: return a vector of directories eligible to delete as a "cleanup" operation
196/// 
197/// Eligibility criterion: any directory that *isn't* the most recent for its revision.
198/// 
199/// Example: If the contents of "previous" is the following list of directories:
200/// 1. Rev_-_UTC-2025-01-01-T00-00-00
201/// 1. Rev_-_UTC-2025-01-01-T00-00-01
202/// 1. Rev_A_UTC-2025-01-01-T00-05-00
203/// 
204/// The only eligible directory for deletion would be #1, because #2 is the latest Rev -, and #3 is the latest (only) Rev A.
205fn list_directories_to_cleanup(prev:String) -> Vec<String> {
206
207    let mut all_revs:Vec<String> = vec![];
208    let mut directories_to_cleanup:Vec<String> = vec![];
209    let all_archives = glob(&(prev.clone() + "/Rev_*_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
210
211    for entry in all_archives {
212        match entry {
213            Ok(path) => {
214                let archive = path.file_name().expect("huh?").to_str().expect("huh?");
215                let pos = archive.find("_UTC-").expect("rust is hard");
216                let rev = String::from(&archive[4..pos]); 
217                all_revs.push(rev);
218            },
219            Err(e) => println!("{:?}",e),
220        }
221    }
222
223    all_revs.dedup();
224    println!("For this process, these are all the revisions: {:?}",&all_revs);
225
226    for rev in all_revs {
227        println!("\nHere are all of the archive folders for Rev {}:",rev);
228        let these = glob(&(prev.clone() + "/Rev_" + &rev + "_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
229        let mut vec:Vec<String> = vec![];
230        for this in these {
231            match this {
232                Ok(path) => vec.push(path.file_name().expect("huh?").to_str().expect("huh?").to_string()),
233                Err(e) => println!("{:?}",e),    
234            }
235        }
236        vec.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
237        let keeper = vec.pop().expect("pop?");
238        for d in &vec {
239            println!("      {} \x1b[93mDELETE\x1b[0m",&d);
240        }
241        println!("   💾 {} 💾  << only one to be saved...",&keeper);
242        directories_to_cleanup.append(&mut vec);
243    }
244    directories_to_cleanup
245}
246
247/// Accept wildcard statements describing a subset of process folders, and return all matches
248fn globify_document_number(input:&String) -> Vec<String> {
249    let all_results = match input.chars().last().unwrap() {
250        '*' => glob(&(input.to_owned())).expect("Failed to read glob pattern"),
251        _ => glob(&(input.to_owned()+"*")).expect("Failed to read glob pattern"),
252    };
253    let mut matching_process_folders:Vec<String> = vec![];
254
255    for entry in all_results {
256        match entry {
257            Ok(path) => {
258                let process_folder = path.file_name().expect("huh?").to_str().expect("huh?");
259                matching_process_folders.push(String::from(process_folder));
260            },
261            Err(e) => println!("{:?}",e),
262        }
263    }
264    matching_process_folders
265}
266
267/// For a set of ordered commands to attempt, gracefully cycle through them until one of them works
268fn attempt_fallbacks(fallbacks: Vec<Box<dyn Fn() -> Result<i32>>>) -> Result<i32> {
269    let mut last_error = None;
270
271    for fallback in fallbacks {
272        match fallback() {
273            Ok(value) => return Ok(value),
274            Err(e) => {
275                println!("Call failed, trying next... (Error: {})", e);
276                last_error = Some(e);
277            }
278        }
279    }
280
281    Err(last_error.unwrap_or_else(|| Error::new(ErrorKind::Other, "No fallbacks provided")))
282}
283
284/// Run Stanhope in one of (or more!) of its modes, each with specific input and expected file generation output.
285/// 
286/// _Examples:_
287/// >    stanhope -vl "DocumentList.csv"
288/// >    stanhope --verbose --listgen "DocumentList.csv"
289/// >    stanhope -w
290/// >    stanhope --webmenu
291/// >    stanhope -vp "EB-WI-00*"
292/// >    stanhope --verbose --process-ebml "EB-WI-00*"
293/// >    stanhope -vs "EB-WI-0?00"
294/// >    stanhope --verbose --scriptify-process "EB-WI-0?00"
295fn main() {
296
297    let args = StanhopeArgs::parse();
298
299    // /////////////////////////////////////////////////////////////////////////////////////////////
300    // If user specified a CSV file to import, then process it appropriately 
301    if !(&args.listgen=="") {
302
303        // Read each line of the CSV file
304        let needed_processes_list = read_csv(&args.listgen,args.verbose.clone());
305        if args.verbose { println!("Stanhope found {} EBML files to create in the CSV file.",needed_processes_list.len()); }
306        // Write to new files
307        for new_proc_to_create in needed_processes_list {
308            if args.verbose { new_proc_to_create.display_process_to_stdout() };
309            write_ebml(&new_proc_to_create,args.verbose.clone());
310        }
311
312    }
313
314    // /////////////////////////////////////////////////////////////////////////////////////////////
315    // If user specified a single process to "scriptify" along with a scripting format
316    if !(args.scriptify_process.len()==0) {
317        // First, we'll allow wildcards and look into every matching Document Number
318        let matching_processes = globify_document_number(&args.scriptify_process[0]);
319        if args.verbose { println!("{:?}",&matching_processes) };
320
321        if matching_processes.len() > 0 {
322
323            for process in matching_processes {
324
325
326                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
327
328                if args.verbose {
329                    println!("");
330                    println!("=== Stanhope Scriptification =========================================================");
331                    println!("===");
332                    println!("=== >> Process:          {}",&process);
333                    println!("=== >> Script Format:    {}",&args.scriptify_process[1]);
334                    println!("=== >> Pause after each? {}",&args.scriptify_process[2]);
335                    println!("===");
336                    println!("======================================================================================");
337                    println!("");
338                }
339                
340                if args.verbose { print!("[A] Attempting to read EBML -> {} ...",&file_to_read); }
341                let new_proc = read_ebml(&file_to_read);
342                if args.verbose { print!("success!\n"); }
343                
344                let all_commands = new_proc.get_all_command_lines();
345                if args.verbose { print!("[B] Stanhope found {} lines of type \"Command\" in the file.\n",all_commands.len()); }
346
347                if args.verbose { print!("[C] Processing script format \"{}\"\n",&args.scriptify_process[1]); }
348                
349
350                let wait:bool = match args.scriptify_process[2].to_uppercase().as_str() {
351                    "F" | "FALSE" | "N" | "NO" => false,
352                    _ => true,
353                };
354                write_script::script_file_from_process(new_proc,write_script::learn(&args.scriptify_process[1]),wait)
355
356
357
358                /*
359                match args.scriptify_process[1].as_str() {
360                    "bash" => {
361                        let suffix = ".sh".to_string();
362                        let all_script_lines = write_script::bash_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
363                        write_script::write_script(all_script_lines,&process,&suffix);
364                    },
365                    "powershell" => {
366                        let suffix = ".ps1".to_string();
367                        let all_script_lines = write_script::powershell_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
368                        write_script::write_script(all_script_lines,&process,&suffix);
369                    },
370                    "python" => {
371                        let suffix = ".py".to_string();
372                        let all_script_lines = write_script::python_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
373                        write_script::write_script(all_script_lines,&process,&suffix);
374                    }
375                    "applescript" => {
376                        let suffix = ".command".to_string();
377                        let all_script_lines = write_script::bash_file_from_commands(all_commands,&process,&args.scriptify_process[2],&suffix);
378                        write_script::write_script(all_script_lines,&process,&suffix);                
379                    }
380                    _ => {
381                        println!("That type of <SCRIPT FORMAT> is not yet implemented in stanhope.");
382                        println!("Case-sensitive options are: \x1b[93mbash\x1b[0m, \x1b[93mpowershell\x1b[0m, \x1b[93mpython\x1b[0m, \x1b[93mapplescript\x1b[0m");
383                    }
384                } // End Match of type
385                */
386
387
388
389
390
391
392            } // End For loop through matching_processes
393        }; // End conditional to check that more than zero processes are requested
394    } // End Scriptify Process(es)
395
396    // /////////////////////////////////////////////////////////////////////////////////////////////
397    // If user specified an EBML file, then convert it to HTML and subsequently PDF 
398    if !(&args.process_ebml=="") {
399        // First, we'll allow wildcards and look into every matching Document Number
400        let matching_processes = globify_document_number(&args.process_ebml);
401        if args.verbose { println!("{:?}",&matching_processes) };
402
403        if matching_processes.len() > 0 {
404
405            for process in matching_processes {
406
407                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
408                if args.verbose { println!("Attempting to process this file: {}",&file_to_read) };
409
410                let new_proc = read_ebml(&file_to_read);
411                if args.verbose { new_proc.display_process_to_stdout() }; 
412
413                // first, delete old html file, then generate a new one
414                if args.verbose { println!("Attempting to delete old HTML file: {:?}",fs::remove_file(&file_to_read.replace(".ebml",".html"))); } else { let _ = fs::remove_file(&file_to_read.replace(".ebml",".html")); }
415                
416                generate_complete_html(&String::from(&file_to_read.replace(".ebml",".html")),&new_proc);
417
418                //windows chrome
419                let win_chrome_path = "& 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' --headless --no-pdf-header-footer --print-to-pdf=\"";
420                let win_process_library = "$PWD\\";
421                let win_chrome_string_to_throw = &(win_chrome_path.to_string() + win_process_library + &file_to_read.replace(".ebml",".pdf") + "\" \"" + win_process_library + &file_to_read.clone().replace(".ebml",".html\""));
422                //posix chrome
423                let posix_chrome = "chrome --headless --print-to-pdf=\"".to_string() + &file_to_read.clone().replace(".ebml",".pdf") + "\" \"" + &file_to_read.clone().replace(".ebml",".html") + "\" --no-pdf-header-footer";
424                //posix chromium
425                let posix_chromium = "chromium --headless --print-to-pdf=\"".to_string() + &file_to_read.clone().replace(".ebml",".pdf") + "\" \"" + &file_to_read.clone().replace(".ebml",".html") + "\" --no-pdf-header-footer";
426
427                let _output = if cfg!(target_os = "windows") {
428
429                    let mut fallbacks: Vec<Box<dyn Fn() -> Result<i32>>> = Vec::new();
430                    fallbacks.push(Box::new(move || {
431                        
432                        if args.verbose { println!("Here's my command string to chrome:\n\n{}\n\n",win_chrome_string_to_throw); }
433                        Command::new("powershell.exe")
434                            .args(["-Command", win_chrome_string_to_throw])
435                            .output()
436                            .expect("failed to execute process");
437                        
438                        Err(Error::new(ErrorKind::Other, "Chrome call to OS attempted."))
439                    }));
440
441                } else {
442
443                    // Create a sequence of commands to try
444                    let mut fallbacks: Vec<Box<dyn Fn() -> Result<i32>>> = Vec::new();
445                    fallbacks.push(Box::new(move || {
446            
447                        Command::new("sh")
448                            .arg("-c")
449                            .arg(&(posix_chrome))
450                            .output()
451                            .expect("failed to execute process");
452
453                        Err(Error::new(ErrorKind::Other, "Chrome call to OS attempted."))
454                    }));
455                    fallbacks.push(Box::new(move || {
456                        
457                        Command::new("sh")
458                            .arg("-c")
459                            .arg(&(posix_chromium))
460                            .output()
461                            .expect("failed to execute process");
462                        
463                        Err(Error::new(ErrorKind::Other, "Chromium call to OS attempted."))
464                    }));
465                    let _result = attempt_fallbacks(fallbacks);
466                    
467                };
468
469                archive_single_process(new_proc,&args.verbose);
470
471            } // End For loop through matching_processes
472        } // End If condition for length of matching_processes
473    } // End PROCESS EBML
474
475    // /////////////////////////////////////////////////////////////////////////////////////////////
476    // If user specified a single process to archive, just do the archiving
477    if !(&args.archive_process=="") {
478        // First, we'll allow wildcards and look into every matching Document Number
479        let matching_processes = globify_document_number(&args.archive_process);
480        if args.verbose { println!("{:?}",&matching_processes) };
481
482        if matching_processes.len() > 0 {
483
484            for process in matching_processes {
485
486                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
487
488                let new_proc = read_ebml(&file_to_read);
489                archive_single_process(new_proc,&args.verbose);
490            }
491        }
492    }
493
494    // /////////////////////////////////////////////////////////////////////////////////////////////
495    // If user specified to clean up a single process
496    if !(&args.cleanup_previous=="") {
497        // First, we'll allow wildcards and look into every matching Document Number
498        let matching_processes = globify_document_number(&args.cleanup_previous);
499        if args.verbose { println!("{:?}",&matching_processes) };
500
501        if matching_processes.len() > 0 {
502
503            for process in matching_processes {
504
505                let prevs = "./".to_owned() + &process + "/previous";
506
507                let delete_these = list_directories_to_cleanup(prevs);
508
509                for dir in delete_these {
510                    println!("Attempting to delete {} ... ",&dir);
511                    let delete_this = "./".to_owned() + &process + "/previous/" + &dir;
512                    let _ = fs::remove_dir_all(delete_this);
513                }
514            }
515        }
516    }
517
518    // /////////////////////////////////////////////////////////////////////////////////////////////
519    // If user specified a single process to inspect
520    if !(&args.inspect_process=="") {
521        // First, we'll allow wildcards and look into every matching Document Number
522        let matching_processes = globify_document_number(&args.inspect_process);
523        if args.verbose { println!("{:?}",&matching_processes) };
524
525        if matching_processes.len() > 0 {
526
527            for process in matching_processes {
528
529                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
530
531                let new_proc = read_ebml(&file_to_read);
532                new_proc.display_process_to_stdout();
533            }
534        }
535    }
536
537    // /////////////////////////////////////////////////////////////////////////////////////////////
538    // If user requested WebMenu generation
539    if args.webmenu {
540        generate_complete_webmenu(&args.verbose);
541    }
542
543    // /////////////////////////////////////////////////////////////////////////////////////////////
544    // If user requested EBML help, show some text to stdout
545    if args.ebml_help {
546        println!("
547
548Note: Process files passed into --process-ebml are specifically-formatted EBML files (Easy Button Markup Language).
549
550==================================================
551==   Easy Button Markup Language Cheat Sheet   ===
552==================================================
553
554// <COMMENT>
555Template | <CSS FILENAME FOUND IN ASSETS FOLDER>
556
557Title        | <PROCESS TITLE>
558Number       | <PROCESS NUMBER>
559Author       | <PROCESS AUTHOR>
560Reviewer     | <PROCESS REVIEWER>
561Process Type | <CATEGORY>, e.g. Test Procedure
562
563Subject       | <THING TO WHICH THE PROCESS APPLIES> 
564Subject Image | <FILENAME OF IMAGE OF SUBJECT>
565Product       | <THING PRODUCED BY THIS PROCESS> - optional
566Product Image | <FILENAME OF IMAGE OF PRODUCT> - optional
567
568Revision | <LATEST REV NUM>       | <DESCRIPTION OF CHANGE>
569Revision | <INTERMEDIATE REV NUM> | <DESCRIPTION OF CHANGE>
570Revision | <FIRST REV NUM>        | <INITIAL DESCTIPTION>
571
572Section  | <TITLE OF SECTION>
573    Step | <TITLE OF STEP>
574        
575        // These all must occur within a STEP
576        
577        Context      | <PLAIN TEXT STATEMENTS FOR CONTEXT>
578        Command      | <VERBATIM COMPUTER CODE>
579        Warning      | <BIG BOLD TEXT TO CAPTURE ATTENTION>
580        Image        | <FILENAME> | <CAPTION TEXT>
581        Resource     | <THING YOU NEED TO COMPLETE THIS STEP> | <CALIBRATED?>
582        Objective    | <ACHIEVED PURPOSE OF THIS PROCESS>
583        Out of Scope | <NOTE ABOUT WHAT NOT TO DO HERE>
584
585        // Verification methods can be single letter: A, I, D, T, S
586        // They can also be full words: Analysis, Inspection, Demonstration, Test, Sampling
587        Verification | <REQUIREMENT ID> | <FULL TEXT OF REQUIREMENT> | <VERIFICATION METHOD>
588
589        // Action lines can be grouped together to form consecutive table rows
590        // To optionally include Two-Party Verification, <TPV> can be \"TPV\" 
591        // To NOT make an action Two-Party Verified, stop at the <EXPECTED RESULT>
592        Action   | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
593        Action   | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
594
595For rendered examples, see https://stanhope.strativusgroup.com/latest/stylesheetviewer.html");
596    }
597
598}