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_xlsx;
15mod read_ebml;
16mod write_ebml;
17mod write_html;
18mod write_script;
19mod write_webmenu;
20
21//use std::process::Command; // needed for current implementation of PDF generation
22use std::{
23    process::Command,
24    //fs::{self, File},
25    //io::{self, BufRead, BufReader},
26    fs::{self},
27    io::{self, Error, ErrorKind, Result},
28    path::Path,
29};
30use chrono::prelude::*;
31use read_ebml::Process;
32
33use clap::Parser; // used for command line parameters and --help generation
34use read_csv::read_csv; // 
35use read_xlsx::read_xlsx;
36use write_ebml::get_process_folder_name;
37use write_ebml::write_ebml;
38use read_ebml::read_ebml;
39use crate::read_ebml::SubStep;
40use write_html::generate_complete_html;
41use write_webmenu::generate_complete_webmenu;
42use glob::glob;
43
44/// Command Line Interface (CLI) argument structure
45#[derive(Parser, Debug)]
46#[command(version, about =
47"\n\n\x1b[1;30;47mStanope\x1b[0m\x1b[30;47m, the Easy Button process generation engine.\x1b[0m
48
49Arguments passed into the options should be surrounded by single or double quotes, e.g.
50% ./stanhope -p \"EB-WI-0010\"    \x1b[36m<< double quotes are accepted\x1b[0m
51% ./stanhope -a 'EB-WI-*'       \x1b[36m<< single quotes are accepted\x1b[0m",
52author = "Strativus Group <contact@strativusgroup.com>",
53long_about = None,
54after_help = 
55"
56")]
57struct StanhopeArgs {
58    /// Read a spreadsheet (Excel or CSV) to pre-populate a process library
59    /// 
60    /// - SPREADSHEET-FILE
61    ///   - Excel (.xlsx) or Comma-separated text file that strictly conforms to the template below
62    ///   - Exception: _TFlag_ columns (7th column and beyond) are user-defined
63    ///   - Free Excel file to use as a template: <https://stanhope.strativusgroup.com/latest/DocumentList.xlsx>
64    /// 
65    /// | "Document Number" | "Title" | "Subject" | "Product" | "Author" | "Reviewer" | _TFlag_ | _TFlag_ | ... |
66    /// | ----------------- | ------- | --------- | --------- | -------- | ---------- | ------- | ------- | --- |
67    /// | ...               | ...     | ...       | ...       | ...      | ...        |    x    |         | ... |
68    /// | ...               | ...     | ...       | ...       | ...      | ...        |         |    x    | ... |
69    /// | ...               | ...     | ...       | ...       | ...      | ...        |         |         | ... |
70    /// | ...               | ...     | ...       | ...       | ...      | ...        |    x    |    x    | ... |
71    #[arg(short, long, value_name = "SPREADSHEET-FILE", default_value_t = String::from(""), verbatim_doc_comment)]
72    listgen: String,
73
74    /// Generate (or overwrite) a menu that indexes useful information for every process in a library
75    /// 
76    /// WebMenu.html is generated in the Process Library root folder
77    #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
78    webmenu: bool,
79
80    /// Report missing graphics in the library, or (verbose) report all graphics, indicating missing ones
81    /// 
82    /// The report is printed to stdout
83    #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
84    graphics_audit: bool,
85
86    /// Report naming discrepancies in the library among {folder, EBML file, Document Number + Title}
87    /// 
88    /// The report is printed to stdout
89    #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
90    filename_audit: bool,
91
92    /// Report how well the entire library is portion-marked.
93    /// 
94    /// Portion-marking checks assume EBML lines start with a parenthetical code, e.g. (XYZ)
95    /// Everything is checked: the file name, number, title, sections, steps, substeps...
96    /// 
97    /// The report is printed to stdout
98    #[arg(short, long, default_value_t = false, verbatim_doc_comment)]
99    marking_audit: bool,
100
101    /// Read a single EBML and produce an HTML file as well as a PDF of that process
102    /// 
103    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
104    ///   - Wildcards are accepted, e.g. *, ?, [0-9]
105    ///   - Wildcards follow "glob" formatting: <https://docs.rs/glob/0.3.3/glob/struct.Pattern.html>
106    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
107    process_ebml: String,
108
109    /// Export every "Command" line from a single process into a script with a defined language/format
110    /// 
111    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
112    /// - SCRIPT-FORMAT currently accepts
113    ///     ActionScript, AppleScript, bash, CoffeeScript, Dart,
114    ///     Elixir, JavaScript, Julia, Lua, MATLAB, Perl, PHP,
115    ///     PowerShell, Python, R, Ruby, TypeScript, VB.NET
116    ///         Full list and backlog:  <https://stanhope.strativusgroup.com/doc/stanhope/write_script/fn.learn.html>
117    /// - PAUSE-AFTER-EACH is true or false, true meaning "pause after each command"
118    #[arg(short, long, use_value_delimiter = true, value_delimiter = ' ', num_args = 3, value_names = ["DOCUMENT-NUMBER","SCRIPT-FORMAT","PAUSE-AFTER-EACH"], verbatim_doc_comment)] 
119    scriptify_process: Vec<String>,
120
121    /// Only perform the archiving process, i.e. create a new archive in "previous"
122    /// 
123    /// Note: this operation is performed at the end of every "Process EBML" action
124    /// This option _only_ performs the archiving, not the processing.
125    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
126    ///   - Wildcards are accepted, e.g. *, ?, [0-9]
127    ///   - Wildcards follow "glob" formatting: <https://docs.rs/glob/0.3.3/glob/struct.Pattern.html>
128    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
129    archive_process: String,
130
131    /// Inspect a document by reading its EBML and returning information (no file generation)
132    /// 
133    /// - DOCUMENT-NUMBER should be surrounded by quotes, particularly if wildcards are used
134    ///   - Wildcards are accepted, e.g. *, ?, [0-9]
135    ///   - Wildcards follow "glob" formatting: <https://docs.rs/glob/0.3.3/glob/struct.Pattern.html>
136    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
137    inspect_process: String,
138
139    /// Delete all but one "previous" version for each Revision for a given process
140    /// 
141    ///   For example, for a document that has many "previous" copies across four
142    ///   Revisions (-,A,B,C), then this option deletes all subdirectories except
143    ///   four: the one with the latest timestamp for each Revision variant
144    #[arg(short, long, value_name = "DOCUMENT-NUMBER", default_value_t = String::from(""), verbatim_doc_comment)]
145    cleanup_previous: String,
146
147    /// Output more information to stdout as Stanhope executes (flag)
148    #[arg(short, long, default_value_t = false,)]
149    verbose: bool,
150
151    /// Display syntax help for Easy Button Markup Language (EBML)
152    #[arg(short, long, default_value_t = false,)]
153    ebml_help: bool,
154}
155
156/// Recursive copy of all contents in one folder to another
157fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
158    fs::create_dir_all(&dst)?;
159    for entry in fs::read_dir(src)? {
160        let entry = entry?;
161        let ty = entry.file_type()?;
162        if ty.is_dir() {
163            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
164        } else {
165            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
166        }
167    }
168    Ok(())
169}
170
171/// Copy everything that ISN'T a directory... just the files
172fn copy_files_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
173    fs::create_dir_all(&dst)?;
174    for entry in fs::read_dir(src)? {
175        let entry = entry?;
176        let ty = entry.file_type()?;
177        if ty.is_dir() {
178            ();
179        } else {
180            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
181        }
182    }
183    Ok(())
184}
185
186/// Perform OS functions to stash a copy of all working material for a process
187fn archive_single_process(new_proc: Process, verbose: &bool) {
188    // if it doesn't already exist, make a new folder called "previous"
189    //let previous_folder = new_proc.get_number().to_string()+"/previous"; // Old convention: process number is the process folder name. Now it's ( NUMBER - TITLE )
190    let previous_folder = get_process_folder_name(&new_proc).to_owned() + "/previous";
191    match Path::new(&previous_folder).exists() {
192        true => (),
193        false => { let _ = fs::create_dir(&previous_folder); },
194    };
195    let root_assets_folder = "./assets";
196    let previous_assets_folder = previous_folder.clone() + "/assets";
197    match Path::new(&previous_assets_folder).exists() {
198        true => (),
199        false => { let _ = fs::create_dir(&previous_assets_folder); },
200    };
201
202    // make a new folder to archive everything we just did in "previous"
203    let rev_string = new_proc.get_revision();
204    let right_now_string = Utc::now().format("UTC-%Y-%m-%d-T%H-%M-%S").to_string();
205    let timestamp_folder = previous_folder.clone() + "/Rev_" + new_proc.get_revision() + "_" + &right_now_string;
206    if *verbose { 
207        println!("Revision: {}",&rev_string);
208        println!("SystemTime::now() ...ish: {}",right_now_string);
209        println!("Trying to create this folder: {:?}",timestamp_folder);
210    }
211    let _ = fs::create_dir(&timestamp_folder);
212    // copy assets
213    let _ = copy_dir_all(root_assets_folder,previous_assets_folder);
214    // copy EBML, HTML, and PDF plus any other non-folder files in the process directory
215    let _ = copy_files_all(get_process_folder_name(&new_proc),timestamp_folder.clone());
216}
217
218/// File management: return a vector of directories eligible to delete as a "cleanup" operation
219/// 
220/// Eligibility criterion: any directory that *isn't* the most recent for its revision.
221/// 
222/// Example: If the contents of "previous" is the following list of directories:
223/// 1. Rev_-_UTC-2025-01-01-T00-00-00
224/// 1. Rev_-_UTC-2025-01-01-T00-00-01
225/// 1. Rev_A_UTC-2025-01-01-T00-05-00
226/// 
227/// The only eligible directory for deletion would be #1, because #2 is the latest Rev -, and #3 is the latest (only) Rev A.
228fn list_directories_to_cleanup(prev:String) -> Vec<String> {
229
230    let mut all_revs:Vec<String> = vec![];
231    let mut directories_to_cleanup:Vec<String> = vec![];
232    let all_archives = glob(&(prev.clone() + "/Rev_*_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
233
234    for entry in all_archives {
235        match entry {
236            Ok(path) => {
237                let archive = path.file_name().expect("huh?").to_str().expect("huh?");
238                let pos = archive.find("_UTC-").expect("rust is hard");
239                let rev = String::from(&archive[4..pos]); 
240                all_revs.push(rev);
241            },
242            Err(e) => println!("{:?}",e),
243        }
244    }
245
246    all_revs.dedup();
247    println!("For this process, these are all the revisions: {:?}",&all_revs);
248
249    for rev in all_revs {
250        println!("\nHere are all of the archive folders for Rev {}:",rev);
251        let these = glob(&(prev.clone() + "/Rev_" + &rev + "_UTC-*-*-*-T*-*-*")).expect("Failed to read glob pattern");
252        let mut vec:Vec<String> = vec![];
253        for this in these {
254            match this {
255                Ok(path) => vec.push(path.file_name().expect("huh?").to_str().expect("huh?").to_string()),
256                Err(e) => println!("{:?}",e),    
257            }
258        }
259        vec.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
260        let keeper = vec.pop().expect("pop?");
261        for d in &vec {
262            println!("      {} \x1b[93mDELETE\x1b[0m",&d);
263        }
264        println!("   💾 {} 💾  << only one to be saved...",&keeper);
265        directories_to_cleanup.append(&mut vec);
266    }
267    directories_to_cleanup
268}
269
270/// Accept wildcard statements describing a subset of process folders, and return all matches
271pub fn globify_document_number(input:&String) -> Vec<String> {
272    let all_results = match input.chars().last().unwrap() {
273        '*' => glob(&(input.to_owned())).expect("Failed to read glob pattern"),
274        _ => glob(&(input.to_owned()+"*")).expect("Failed to read glob pattern"),
275    };
276    let mut matching_process_folders:Vec<String> = vec![];
277
278    for entry in all_results {
279        match entry {
280            Ok(path) => {
281                let process_folder = path.file_name().expect("huh?").to_str().expect("huh?");
282                matching_process_folders.push(String::from(process_folder));
283            },
284            Err(e) => println!("{:?}",e),
285        }
286    }
287    matching_process_folders
288}
289
290/// Gracefully cycle through ordered OS commands until one of them works
291fn attempt_fallbacks(fallbacks: Vec<Box<dyn Fn() -> Result<i32>>>) -> Result<i32> {
292    let mut last_error = None;
293
294    for fallback in fallbacks {
295        match fallback() {
296            Ok(value) => return Ok(value),
297            Err(e) => {
298                println!("Call failed, trying next... (Error: {})", e);
299                last_error = Some(e);
300            }
301        }
302    }
303
304    Err(last_error.unwrap_or_else(|| Error::new(ErrorKind::Other, "No fallbacks provided")))
305}
306
307/// Look into each process folder, read the EBML, find the images, and check to see if they exist, print findings to stdout
308/// 
309/// This function finds all images that the EBML will use in its HTML output when processed:
310/// - "Image" lines in the EBML
311/// - "Image" lines in followed "Section Reference" links to other processes
312/// - "Subject Image" line
313/// - "Product Image" line
314/// 
315/// Verbose version lists all images, not just the missing ones
316fn print_graphics_audit_to_stdout(v:&bool) {
317    let list_of_process_folders = write_webmenu::find_all_process_folders(".");
318    //println!("{:?}",list_of_process_folders);
319    print_title_block_to_stdout("Library Graphics Audit");
320    for folder in list_of_process_folders {
321        let folder_string = folder.file_name().expect("No bueno?").to_str().expect("Really bueno?").to_string();
322        let new_ebml_str = folder_string.clone() + "/" + &folder_string + ".ebml";
323        let new_process = read_ebml(&("./".to_owned()+&new_ebml_str));
324
325        if new_process.get_missing_image_count() == 0 {
326            println!(" [ No missing images! ]    {}",&folder_string);
327        } else {
328            println!(" ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒    {}",&folder_string);
329        }
330        if new_process.get_missing_image_count() > 0 {
331            println!("            ▒▒▒▒▒▒▒▒▒▒▒      -> Missing Images");
332            for image in new_process.get_missing_images() {
333            println!("               ▒▒▒▒▒▒▒▒        {}",image);
334            }
335        }
336        if *v {
337            if new_process.get_all_images().len() > 0 {
338                println!("                             -> All Images");
339                for image in new_process.get_all_images() {
340                    println!("                               {}",image.0);
341                } 
342            } else { println!("                             -> No images at all, in fact..."); }
343        }
344    }
345}
346
347/// Look into each process folder, read the EBML, find the Number and Title, check consistency, print findings to stdout
348/// 
349/// The fundamental assumption is that stanhope will find any given EBML file in this location:
350/// ./<DOCUMENT NUMBER> - <DOCUMENT TITLE>/<DOCUMENT NUMBER> - <DOCUMENT TITLE>.ebml
351/// 
352/// That is,
353/// - a process's folder is named "<DOCUMENT NUMBER> - <DOCUMENT TITLE>"
354/// - the EBML file is named "<DOCUMENT NUMBER> - <DOCUMENT TITLE>.ebml"
355/// 
356/// This function attempts to traverse the library, checking these assumptions as it goes, and printing to stdout
357/// 
358/// Verbose version lists all diagnostic information about every process, not just the issues it finds
359fn print_filename_audit_to_stdout(v:&bool) {
360    let list_of_process_folders = write_webmenu::find_all_process_folders(".");
361    print_title_block_to_stdout("Library Filename Audit");
362    for folder in list_of_process_folders {
363        let expected_folder_string = folder.file_name().expect("No bueno?").to_str().expect("Really bueno?").to_string();
364        let expected_ebml_str = expected_folder_string.clone() + "/" + &expected_folder_string + ".ebml";
365        println!("{}→ {}",if *v {"\n"}else{""},expected_folder_string);
366        match Path::new(&expected_ebml_str).exists() {
367            true => {
368                if *v { println!(" ↳ {}",expected_ebml_str); }
369                let new_proc = read_ebml(&("./".to_owned()+&expected_ebml_str));
370                if *v { println!(" ↳ EBML file exists as named above."); }
371                if *v { println!("   ↳ EBML Contents 'Number' : {}",new_proc.get_number()); }
372                if *v { println!("   ↳ EBML Contents 'Title'  : {}",new_proc.get_title()); }
373                if expected_folder_string == (new_proc.get_number().to_owned() + " - " + new_proc.get_title()).to_string() {
374                    if *v { println!(" ↳ CLEAN AUDIT: EBML CONTENTS MATCH FOLDER AND FILE NAMES"); }
375                } else {
376                    println!(" ↳ \x1b[31mWARNING: SOMETHING IS MISMATCHED BETWEEN FOLDER NAME, FILE NAME, AND EBML CONTENTS\x1b[0m");
377                    println!(" ↳ \x1b[33mSpecial characters in EBML title converted to '_' in folder/file names might be OK\x1b[0m");
378                }
379            },
380            false => {
381                println!(" ↳ \x1b[31m{}\x1b[0m",expected_ebml_str);
382                println!(" ↳ \x1b[31mWARNING: LIKELY FOLDER NAME MISMATCH WITH EBML NAME\x1b[0m");
383            },
384        };
385    }
386}
387
388/// Look into each process folder, read the EBML as well as the HTML!
389/// 
390/// The function tries to understand how a process is portion-marked, and report results
391/// 
392/// Verbose version lists all diagnostic information about every process, not just the issues it finds
393fn print_marking_audit_to_stdout(v:&bool) {
394    let list_of_process_folders = write_webmenu::find_all_process_folders(".");
395    print_title_block_to_stdout("(Portion) Marking Audit");
396
397    println!("{} folders...",list_of_process_folders.len());
398    
399    for folder in list_of_process_folders {
400        if *v {print!("\n================================================================================");}
401        let mut numer:u16 = 0;
402        let mut denom:u16 = 0;
403
404        let folder_string = folder.file_name().expect("No bueno?").to_str().expect("Really bueno?").to_string();
405
406        let new_ebml_str = folder_string.clone() + "/" + &folder_string + ".ebml";
407        let new_process = read_ebml(&("./".to_owned()+&new_ebml_str));
408
409        let classification_header = get_classification_header(new_process.get_all_templates());
410
411        print!("\n");
412        println!("{}",&folder_string);
413        println!("\x1b[1;37;44m{}\x1b[0m",classification_header);
414        
415        (numer,denom) = tally_portion_marking_check(&folder_string,numer,denom,&true);
416
417        //(numer,denom) = tally_portion_marking_check(new_process.get_process_file(),numer,denom,v);
418        (numer,denom) = tally_portion_marking_check(new_process.get_title(),numer,denom,v);
419        (numer,denom) = tally_portion_marking_check(new_process.get_subject(),numer,denom,v);
420        (numer,denom) = tally_portion_marking_check(new_process.get_product(),numer,denom,v);
421        for rev in new_process.get_all_revisions() {
422            (numer,denom) = tally_portion_marking_check(&rev.1,numer,denom,v);
423        }
424        for sec in new_process.get_all_sections() {
425            (numer,denom) = tally_portion_marking_check(&sec.get_title(),numer,denom,v);
426            for stp in sec.get_all_steps() {
427                (numer,denom) = tally_portion_marking_check(&stp.get_text(),numer,denom,v);
428                for sub in stp.get_all_sub_steps() {
429                    match sub {
430                        SubStep::Objective(s)      => (numer,denom) = tally_portion_marking_check(&s,numer,denom,v),
431                        SubStep::OutOfScope(s)     => (numer,denom) = tally_portion_marking_check(&s,numer,denom,v),
432                        SubStep::ActionSequence(vec) => for a in vec {
433                            (numer,denom) = tally_portion_marking_check(a.get_perform(),numer,denom,v);
434                            (numer,denom) = tally_portion_marking_check(a.get_expect(),numer,denom,v);
435                        },
436                        SubStep::Command(_)        => (),
437                        SubStep::Image(_,s)        => (numer,denom) = tally_portion_marking_check(&s,numer,denom,v),
438                        SubStep::Warning(s)        => (numer,denom) = tally_portion_marking_check(&s,numer,denom,v),
439                        SubStep::Verification(r)   => (numer,denom) = tally_portion_marking_check(r.get_text(),numer,denom,v),
440                        SubStep::Resource(r)       => (numer,denom) = tally_portion_marking_check(r.get_name(),numer,denom,v),
441                        SubStep::Context(s)        => (numer,denom) = tally_portion_marking_check(&s,numer,denom,v),
442                        SubStep::Table(t)          => (numer,denom) = tally_portion_marking_check(t.get_caption(),numer,denom,v),
443                    };
444                }
445            }
446        }
447        println!("📗 Properly Portion-Marked: {:4}",numer);
448        println!("📕 Total lines checked:     {:4}",denom);
449        println!("📚 Success percentage:      {:4.0}%",(f32::from(numer)/f32::from(denom)*100.0));
450    }
451
452    /// For a [Process]'s list of applied templates, this function predicts what will be displayed in the classification header
453    /// 
454    /// Assumptions:
455    /// - the base CSS file is "assets/stanhope.css"
456    /// - the input Vec<String> is a return value from Process::get_all_templates()
457    /// - template strings correspond to files in "assets/TEMPLATE.css"
458    fn get_classification_header(v:&Vec<String>) -> String {
459        let mut classy:String; // temporary value of whatever the subfunction finds
460        let mut classification:String = String::from("UNKNOWN, OR NOT FOUND ANYWHERE..."); // The actual thing we'll return
461        let mut success:bool;
462
463        // First thing: check assets/stanhope.css (the base CSS file)
464        (classy,success) = get_header_from_css_file("assets/stanhope.css");
465        // When we call this funciton, we'll commit the temp value only if it was actually found (success) vs. default return
466        if success { classification = classy; }
467
468        // Now we'll read the Process struct itself to get all of the different templates applied (each is an assets/*.css file)
469        for template in v {
470            (classy,success) = get_header_from_css_file(&("assets/".to_owned()+template));
471            if success { classification = classy; }
472        }
473        return classification;
474        // Note: we know that stanhope.css applies "UNCLASSIFIED" so classification should never be returned with the default above...
475    }
476
477    
478    fn get_header_from_css_file(f:&str) -> (String,bool) {
479        use std::fs::File;
480        use std::io::{BufRead, BufReader};
481        
482        let mut header:String = String::from("UNKNOWN, OR NOT FOUND ANYWHERE...");
483        let mut success:bool = false;
484
485        //println!("Trying '{}'...",f);
486        //print!("Trying '{}' ... ",f);
487        match Path::new(f).exists() {
488            true => {
489                //print!("found file ... ");
490                let file = File::open(f).unwrap();
491                let reader = BufReader::new(file);
492                let mut flag:bool = false;
493
494                for line in reader.lines() {
495                    let line = line.unwrap(); // Unwrap the Result for each line
496
497                    match flag {
498                        true => {
499                            if line.contains("content:") {
500                                //print!("found the right text!");
501                                if line.contains("'") {
502                                    header = line.split("'").nth(1).expect("?").to_string();
503                                    success = true;
504                                } else if line.contains('\"') {
505                                    header = line.split('\"').nth(1).expect("?").to_string();
506                                    success = true;
507                                }
508                            }
509                            flag = false;
510                        },
511                        false => {
512                            if line.contains(".classification p::before") {
513                                flag = true;
514                                if line.contains("content:") {
515                                    //print!("found the right text!");
516                                    if line.contains("'") {
517                                        header = line.split("'").nth(1).expect("?").to_string();
518                                        success = true;
519                                    } else if line.contains('\"') {
520                                        header = line.split('\"').nth(1).expect("?").to_string();
521                                        success = true;
522                                    }
523                                    flag = false;
524                                }
525                            }
526                        },
527                    }
528                }
529
530                if !success {
531                    //print!("text pattern not found in this file.");
532                }
533            },
534            false => (),//print!("file not found!"),
535        };
536        //print!("\n");
537        return (header,success)
538    }
539
540    fn tally_portion_marking_check(s:&str,mut n:u16,mut d:u16,v:&bool) -> (u16,u16) {
541        d = d + 1;
542        if s.len() == 0 || probably_portion_marked(s) {
543            n = n + 1;
544            if *v {
545                println!("✅ {}",s.trim());
546            }
547        } else {
548            if *v {
549                println!("❌ {}",s.trim());
550            }
551        }
552        return (n,d);
553    }
554
555    fn probably_portion_marked(s:&str) -> bool {
556        s.trim().starts_with('(') && s.contains(')')
557    }
558
559    // Summary statistics
560}
561
562/// Utility function to print a box outline around text in stdout, perhaps for a title
563/// 
564/// Example for "Beep Boop"
565/// ╭───────────╮
566/// │ Beep Boop │
567/// ╰───────────╯
568fn print_title_block_to_stdout(s:&str) {
569    let to_print = s.trim();
570    print!("╭─"); print!("{:─<1$}", "", to_print.len()); print!("─╮\n");
571    print!("│ "); print!("{}",&to_print); print!(" │\n");
572    print!("╰─"); print!("{:─<1$}", "", to_print.len());print!("─╯\n");
573}
574
575/// Run Stanhope in one of (or more!) of its modes
576/// 
577/// _Examples:_
578/// >    stanhope -vl "DocumentList.csv"
579/// >    stanhope --verbose --listgen "DocumentList.csv"
580/// >    stanhope -w
581/// >    stanhope --webmenu
582/// >    stanhope -vp "EB-WI-00*"
583/// >    stanhope --verbose --process-ebml "EB-WI-00*"
584/// >    stanhope -vs "EB-WI-0?00"
585/// >    stanhope --verbose --scriptify-process "EB-WI-0?00"
586fn main() {
587
588    let args = StanhopeArgs::parse();
589
590    // /////////////////////////////////////////////////////////////////////////////////////////////
591    // If user specified a Spreadsheet file to import, then process it appropriately 
592    if !(&args.listgen=="") {
593
594        let Some((_, last_part)) = &args.listgen.rsplit_once('.') else { todo!() };
595        let needed_processes_list = match *last_part {
596            "csv" => {
597                // Read each line of the CSV file
598                read_csv(&args.listgen,args.verbose.clone())
599                
600            },
601            "xlsx" => {
602                // Read each line of the Excel file
603                read_xlsx(&args.listgen,args.verbose.clone())
604            },
605            _ => {
606                vec!(Process::new())
607            },
608        };
609
610        if args.verbose { println!("Stanhope found {} EBML files to create in the spreadsheet.",needed_processes_list.len()); }
611        
612        // Write to new files
613        for new_proc_to_create in needed_processes_list {
614            if args.verbose { new_proc_to_create.display_process_to_stdout() };
615            write_ebml(&new_proc_to_create,args.verbose.clone());
616        }
617
618    }
619
620    // /////////////////////////////////////////////////////////////////////////////////////////////
621    // If user specified a single process to "scriptify" along with a scripting format
622    if !(args.scriptify_process.len()==0) {
623        // First, we'll allow wildcards and look into every matching Document Number
624        let matching_processes = globify_document_number(&args.scriptify_process[0]);
625        if args.verbose { println!("{:?}",&matching_processes) };
626
627        if matching_processes.len() > 0 {
628
629            for process in matching_processes {
630
631
632                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
633
634                if args.verbose {
635                    println!("");
636                    println!("=== Stanhope Scriptification =========================================================");
637                    println!("===");
638                    println!("=== >> Process:          {}",&process);
639                    println!("=== >> Script Format:    {}",&args.scriptify_process[1]);
640                    println!("=== >> Pause after each? {}",&args.scriptify_process[2]);
641                    println!("===");
642                    println!("======================================================================================");
643                    println!("");
644                }
645                
646                if args.verbose { print!("[A] Attempting to read EBML -> {} ...",&file_to_read); }
647                let new_proc = read_ebml(&file_to_read);
648                if args.verbose { print!("success!\n"); }
649                
650                let all_commands = new_proc.get_all_command_lines();
651                if args.verbose { print!("[B] Stanhope found {} lines of type \"Command\" in the file.\n",all_commands.len()); }
652
653                if args.verbose { print!("[C] Processing script format \"{}\"\n",&args.scriptify_process[1]); }
654                
655
656                let wait:bool = match args.scriptify_process[2].to_uppercase().as_str() {
657                    "F" | "FALSE" | "N" | "NO" => false,
658                    _ => true,
659                };
660                write_script::script_file_from_process(new_proc,write_script::learn(&args.scriptify_process[1]),wait)
661
662            } // End For loop through matching_processes
663        }; // End conditional to check that more than zero processes are requested
664    } // End Scriptify Process(es)
665
666    // /////////////////////////////////////////////////////////////////////////////////////////////
667    // If user specified an EBML file, then convert it to HTML and subsequently PDF 
668    if !(&args.process_ebml=="") {
669        // First, we'll allow wildcards and look into every matching Document Number
670        let matching_processes = globify_document_number(&args.process_ebml);
671        if args.verbose { println!("{:?}",&matching_processes) };
672
673        if matching_processes.len() > 0 {
674
675            for process in matching_processes {
676
677                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
678                if args.verbose { println!("Attempting to process this file: {}",&file_to_read) };
679
680                let new_proc = read_ebml(&file_to_read);
681                if args.verbose { new_proc.display_process_to_stdout() }; 
682
683                // first, delete old html file, then generate a new one
684                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")); }
685                
686                generate_complete_html(&String::from(&file_to_read.replace(".ebml",".html")),&new_proc);
687
688                //windows chrome
689                let win_chrome_path = "& 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' --headless --no-pdf-header-footer --print-to-pdf=\"";
690                let win_process_library = "$PWD\\";
691                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\""));
692                //posix chrome
693                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";
694                //posix chromium
695                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";
696
697                let _output = if cfg!(target_os = "windows") {
698
699                    let mut fallbacks: Vec<Box<dyn Fn() -> Result<i32>>> = Vec::new();
700                    fallbacks.push(Box::new(move || {
701                        
702                        if args.verbose { println!("Here's my command string to chrome:\n\n{}\n\n",win_chrome_string_to_throw); }
703                        Command::new("powershell.exe")
704                            .args(["-Command", win_chrome_string_to_throw])
705                            .output()
706                            .expect("failed to execute process");
707                        
708                        Err(Error::new(ErrorKind::Other, "Chrome call to OS attempted."))
709                    }));
710
711                } else {
712
713                    // Create a sequence of commands to try
714                    let mut fallbacks: Vec<Box<dyn Fn() -> Result<i32>>> = Vec::new();
715                    fallbacks.push(Box::new(move || {
716            
717                        Command::new("sh")
718                            .arg("-c")
719                            .arg(&(posix_chrome))
720                            .output()
721                            .expect("failed to execute process");
722
723                        Err(Error::new(ErrorKind::Other, "Chrome call to OS attempted."))
724                    }));
725                    fallbacks.push(Box::new(move || {
726                        
727                        Command::new("sh")
728                            .arg("-c")
729                            .arg(&(posix_chromium))
730                            .output()
731                            .expect("failed to execute process");
732                        
733                        Err(Error::new(ErrorKind::Other, "Chromium call to OS attempted."))
734                    }));
735                    let _result = attempt_fallbacks(fallbacks);
736                    
737                };
738
739                archive_single_process(new_proc,&args.verbose);
740
741            } // End For loop through matching_processes
742        } // End If condition for length of matching_processes
743    } // End PROCESS EBML
744
745    // /////////////////////////////////////////////////////////////////////////////////////////////
746    // If user specified a single process to archive, just do the archiving
747    if !(&args.archive_process=="") {
748        // First, we'll allow wildcards and look into every matching Document Number
749        let matching_processes = globify_document_number(&args.archive_process);
750        if args.verbose { println!("{:?}",&matching_processes) };
751
752        if matching_processes.len() > 0 {
753
754            for process in matching_processes {
755
756                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
757
758                let new_proc = read_ebml(&file_to_read);
759                archive_single_process(new_proc,&args.verbose);
760            }
761        }
762    }
763
764    // /////////////////////////////////////////////////////////////////////////////////////////////
765    // If user specified to clean up a single process
766    if !(&args.cleanup_previous=="") {
767        // First, we'll allow wildcards and look into every matching Document Number
768        let matching_processes = globify_document_number(&args.cleanup_previous);
769        if args.verbose { println!("{:?}",&matching_processes) };
770
771        if matching_processes.len() > 0 {
772
773            for process in matching_processes {
774
775                let prevs = "./".to_owned() + &process + "/previous";
776
777                let delete_these = list_directories_to_cleanup(prevs);
778
779                for dir in delete_these {
780                    println!("Attempting to delete {} ... ",&dir);
781                    let delete_this = "./".to_owned() + &process + "/previous/" + &dir;
782                    let _ = fs::remove_dir_all(delete_this);
783                }
784            }
785        }
786    }
787
788    // /////////////////////////////////////////////////////////////////////////////////////////////
789    // If user specified a single process to inspect
790    if !(&args.inspect_process=="") {
791        // First, we'll allow wildcards and look into every matching Document Number
792        let matching_processes = globify_document_number(&args.inspect_process);
793        if args.verbose { println!("{:?}",&matching_processes) };
794
795        if matching_processes.len() > 0 {
796
797            for process in matching_processes {
798
799                let file_to_read = "./".to_owned() + &process + "/" + &process + ".ebml";
800
801                let new_proc = read_ebml(&file_to_read);
802                new_proc.display_process_to_stdout();
803            }
804        }
805    }
806
807    // /////////////////////////////////////////////////////////////////////////////////////////////
808    // If user requested WebMenu generation
809    if args.webmenu {
810        generate_complete_webmenu(&args.verbose);
811    }
812
813    // /////////////////////////////////////////////////////////////////////////////////////////////
814    // If user runs a graphics audit
815    if args.graphics_audit {
816        print_graphics_audit_to_stdout(&args.verbose);
817    }
818
819    // /////////////////////////////////////////////////////////////////////////////////////////////
820    // If user requested Filename Audit generation
821    if args.filename_audit {
822        print_filename_audit_to_stdout(&args.verbose);
823    }
824
825    // /////////////////////////////////////////////////////////////////////////////////////////////
826    // If user requested (Portion) Marking Audit generation
827    if args.marking_audit {
828        print_marking_audit_to_stdout(&args.verbose);
829    }
830
831    // /////////////////////////////////////////////////////////////////////////////////////////////
832    // If user requested EBML help, show some text to stdout
833    if args.ebml_help {
834        println!("{}",format!("
835
836Note: Process files passed into --process-ebml are specifically-formatted EBML files (Easy Button Markup Language).
837
838\x1b[36m
839=================================================
840==   Easy Button Markup Language Cheat Sheet   ==
841==                                             ==
842==  Everything below this block is valid EBML  ==
843=================================================
844\x1b[0m
845
846// <COMMENT>
847Template | <CSS FILENAME FOUND IN ASSETS FOLDER>
848
849Title        | <PROCESS TITLE>
850Number       | <PROCESS NUMBER>
851Author       | <PROCESS AUTHOR>
852Reviewer     | <PROCESS REVIEWER>
853Process Type | <CATEGORY>, e.g. Test Procedure
854
855Subject       | <THING TO WHICH THE PROCESS APPLIES> 
856Subject Image | <FILENAME OF IMAGE OF SUBJECT>
857Product       | <THING PRODUCED BY THIS PROCESS> - optional
858Product Image | <FILENAME OF IMAGE OF PRODUCT> - optional
859
860Revision | <LATEST REV NUM>       | <DESCRIPTION OF CHANGE>
861Revision | <INTERMEDIATE REV NUM> | <DESCRIPTION OF CHANGE>
862Revision | <FIRST REV NUM>        | <INITIAL DESCTIPTION>
863
864Section  | <TITLE OF SECTION>
865    Step | <TITLE OF STEP>
866        
867        // These all must occur within a STEP
868        
869        Context      | <PLAIN TEXT STATEMENTS FOR CONTEXT>
870        Command      | <VERBATIM COMPUTER CODE>
871        Warning      | <BIG BOLD TEXT TO CAPTURE ATTENTION>
872        Image        | <FILENAME> | <CAPTION TEXT>
873        Resource     | <THING YOU NEED TO COMPLETE THIS STEP> | <CALIBRATED?>
874        Objective    | <ACHIEVED PURPOSE OF THIS PROCESS>
875        Out of Scope | <NOTE ABOUT WHAT NOT TO DO HERE>
876
877        // Verification methods can be single letter: A, I, D, T, S
878        // They can also be full words: Analysis, Inspection, Demonstration, Test, Sampling
879        Verification | <REQUIREMENT ID> | <FULL TEXT OF REQUIREMENT> | <VERIFICATION METHOD>
880
881        // Action lines can be grouped together to form consecutive table rows
882        // To optionally include Two-Party Verification, <TPV> can be \"TPV\" 
883        // To NOT make an action Two-Party Verified, stop at the <EXPECTED RESULT>
884        Action   | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
885        Action   | <THING TO DO> | <EXPECTED RESULT> | <TPV?> - optional
886
887// You can reference a different process's section and stanhope will fetch it for you
888Section Reference | <OTHER PROCESS> | <TITLE OF SECTION>
889
890
891// Alternate Aliases for EBML lines are below
892// Everything here continues to be valid EBML
893// Note: the EBML line designators (left of the first pipe) are NOT case sensitive
894// 'section' is the same as 'SECTION'
895// 'rev' is the same as 'REV'
896
897REV | C | 'Revision' has a new alias!
898rev | B | EBML line indicators are NOT case sensitive!
899ReV | A | I mean, you *could* do that...
900Rev | - | This is revision 'dash'
901
902SEC | Section Alias 'SEC' (or 'sec' etc.) works!
903    STP | Step Alias 'STP' (or 'stp' etc.) works!
904
905        // 'Context' has several new aliases
906        Comment | Context alias
907        TXT | Context alias
908        CMT | Context alias, perhaps short for 'comment'
909
910        // 'Command' has a new alias
911        CMD | Command alias
912        > | Command alias
913        % | Command alias
914        $ | Command alias
915        # | Command alias
916
917        // 'Image' has a new alias
918        IMG | image.ext | This caption appears under the image
919        PICTURE | image.ext | This caption appears under the image
920        PIC | image.ext | This caption appears under the image
921        Figure | image.ext | This caption appears under the image
922        FIG | image.ext | This caption appears under the image
923
924        // 'Action' has a new alias
925        // The Two-Party Verification flag has some aliases as well
926        DO | Do this action | Expect this result
927        DO | Do this action | Expect this result | TPV
928        DO | Do this action | Expect this result | T
929        DO | Do this action | Expect this result | TRUE
930        DO | Do this action | Expect this result | Y
931        DO | Do this action | Expect this result | YES
932        DO | Do this action | Expect this result | Two Party Verification
933        DO | Do this action | Expect this result | Two-Party Verification
934        // That makes an eight-row Action table, with TPVs for the last seven actions
935
936        // 'Warning' has new aliases
937        WARN | Warning text
938        WRN | Warning text
939        WAR | Warning text
940        Alert | Warning text
941        ! | Warning text
942
943        // 'Verification' has new aliases
944        VER | ReqID | Verification Text | Method
945        REQUIREMENT | ReqID | Verification Text | Method
946        REQ | ReqID | Verification Text | Method
947        // The methods of verification have a few aliases as well
948        REQ | ReqID | Verification Text | Demonstration
949        REQ | ReqID | Verification Text | Demo
950        REQ | ReqID | Verification Text | D
951        REQ | ReqID | Verification Text | Inspection
952        REQ | ReqID | Verification Text | I
953        REQ | ReqID | Verification Text | Analysis
954        REQ | ReqID | Verification Text | A
955        REQ | ReqID | Verification Text | Sampling
956        REQ | ReqID | Verification Text | Sample
957        REQ | ReqID | Verification Text | S
958        REQ | ReqID | Verification Text | Test
959        REQ | ReqID | Verification Text | T
960
961        // 'Resource' has a new alias
962        // There are several ways to indicate if a resource needs calibration, too!
963        RES | Name of resource
964        RES | Name of calibrated resource | C
965        RES | Name of calibrated resource | CAL
966        RES | Name of calibrated resource | CALIB
967        RES | Name of calibrated resource | CALIBRATE
968        RES | Name of calibrated resource | CALIBRATED
969        RES | Name of calibrated resource | CALIBRATION
970        RES | Name of calibrated resource | Y
971        RES | Name of calibrated resource | YES
972        RES | Name of calibrated resource | T
973        RES | Name of calibrated resource | TRUE
974        
975        // 'Objective' has a new alias
976        OBJ | Objective achieved!
977
978        // 'Out of Scope' has a new alias
979        OOS | Description of what is not in this process scope
980
981// 'Section Reference' has a new alias
982SECREF | Process-Number | Title of Section to Fetch
983
984\x1b[36m
985=================================================
986==  Everything above this block is valid EBML  ==
987==                                             ==
988==   Easy Button Markup Language Cheat Sheet   ==
989=================================================
990\x1b[0m
991
992For extensive examples of EBML in action, see https://stanhope.strativusgroup.com/latest/stylesheetviewer.html"));
993    }
994
995}