stanhope/write_ebml.rs
1//! *Write an EBML file using a prototype, and some basic information about a new process*
2//!
3//! Assumption: you know some basic information about a Process struct, enough to fill the basic fields:
4//! * **Number** - Document Number
5//! * **Title** - Document Title
6//! * **Subject** - To what the process applies
7//! * **Product** - Output of this process
8//! * **Author** - Who is to compose this process
9//! * **Reviewer** - Prior to publishing, who is to review
10//!
11//! *Note: these aren't the only fields in the Process struct; these are the fields that are strictly required in a well-formatted CSV file when **stanhope** is invoked to read in a list of new processes to create.*
12//!
13//! An EBML prototype file is required, and should be in the "assets" folder.
14
15use crate::read_ebml;
16use crate::read_ebml::Process;
17use crate::write_html;
18
19use std::{
20 fs::{self, OpenOptions, File},
21 io::{self, BufRead, BufReader, Write},
22 path::Path,
23};
24
25/// Apply the Library folder naming convention introduced in stanhope 0.56.0
26///
27/// The naming convention for a process folder within a library is:
28/// > Process Library/NUMBER - TITLE/
29///
30/// For reference, this breaks the previous convention (prior to 0.56.0) which was
31/// > Process Library/NUMBER/
32///
33/// The reason for this breaking change is the quality-of-life for users who can't memorize everything by document number alone
34///
35/// Note: the "TITLE" string is sanitized to replace illegal characers with underscores ("_"). Some "illegal" characers might be allowed in file systems, but this function takes a conservative approach to string sanitization. For this purpose, illegal characters include /\:*?"<>|#%&+{}
36pub fn get_process_folder_name(p:&Process) -> String {
37 String::from("./".to_owned() + p.get_number() + " - " + &p.get_title()
38 .replace("/","_")
39 .replace("\\","_")
40 .replace(":","_")
41 .replace("*","_")
42 .replace("?","_")
43 .replace("\"","_")
44 .replace("<","_")
45 .replace(">","_")
46 .replace("|","_")
47 .replace("#","_")
48 .replace("%","_")
49 .replace("&","_")
50 .replace("+","_")
51 .replace("{","_")
52 .replace("}","_")
53 )
54}
55
56/// Create a new folder with the same name as the Document **Number**, and write a new EBML file to it.
57///
58/// If the folder already exists, write the file in that directory.
59///
60/// If the folder *and* the file already exist, don't do anything! This function is really only to create a fresh EBML file as a starting point.
61pub fn write_ebml(p:&Process,verbose:bool) {
62
63 // New convention: PROCESS NUMBER - PROCESS TITLE is the folder name in the library...
64 let path = get_process_folder_name(p);
65
66 // Get loud in that stdout
67 if verbose { println!("\n{} folder already exists: {}", &path, Path::new(&path).exists()); }
68
69 /* CRITICAL BREAKING UPDATE!!
70 // Assumption: every process has its own folder, and that folder is named the PROCESS NUMBER
71 //
72 // That folder also contains the EBML file (that's where we'll put it)
73 // let path = "./".to_string() + &p.get_number();
74 */
75
76 // Assumption: EBML file names are the same as the folder name, e.g. "NUMBER - TITLE.ebml"
77 let path_to_file = path.to_string() + "/" + &path + ".ebml";
78
79 // Scream out to stdout whether or not the file and/or path already exist
80 if verbose {
81 println!("{} file already exists: {}", &path_to_file, Path::new(&path_to_file).exists());
82 println!("Path to the new file will be: {}",path_to_file);
83 }
84
85 // If the EBML file doesn't already exist in the folder
86 if !Path::new(&path_to_file).exists() {
87
88 // Check to see if the folder exists, and create it if it doesn't
89 match Path::new(&path).exists() {
90 true => (),
91 false => { let _ = fs::create_dir(&path); },
92 };
93
94 // Open the file to write
95 let mut file = OpenOptions::new()
96 .read(true)
97 .write(true)
98 .create(true)
99 .open(&path_to_file)
100 .expect("Could not open the file!");
101
102 // Call function that references a template EBML file, and uses Process information to replace variables
103 fill_in_template_ebml_file(&mut file,p);
104 // That function actually does all the writing of the EBML file
105
106 // The HTML file will be in the same folder with the same name, with .html as the extension
107 let path_to_html_file = path.to_string() + "/" + &path + ".html";
108 // We just wrote an EBML file, so we'll use the built-in function to read it back as a Process object
109 let read_it_back_now = read_ebml::read_ebml(&path_to_file);
110 // Use built-in functions to write HTML from the Process we extracted from the EBML we generated
111 write_html::generate_complete_html(&path_to_html_file,&read_it_back_now);
112
113 // Windows users: copy a specific batch script into each process folder (presume that it exists in .\assets folder).
114 if cfg!(target_os = "windows") { let _did_copy_work = fs::copy("./assets/EasyButton.bat",path.to_string() + "/EasyButton.bat"); }
115 // Mac users: copy a specific command script into each process folder (presume that it exists in .\assets folder).
116 if cfg!(target_os = "macos") { let _did_copy_work = fs::copy("./assets/EasyButton.command",path.to_string() + "/EasyButton.command"); }
117 // Linux users: copy a specific shell script into each process folder (presume that it exists in .\assets folder).
118 if cfg!(target_os = "linux") { let _did_copy_work = fs::copy("./assets/EasyButton.sh",path.to_string() + "/EasyButton.sh"); }
119
120 // If the file already exists, this function doesn't do anything
121 } else { if verbose { println!("File already exists!"); print!("\n"); } };
122
123}
124
125/// Write a new EBML file that is line-for-line identical to a reference prototype file, except replace the file's variables with information from the Process struct.
126///
127/// Variables in the prototype follow this pattern: $VARIABLE, where VARIABLE represents the Process.variable field, accessed with Process.get_variable() << "variable" is not an actual field... used as syntactical example...
128fn fill_in_template_ebml_file(f:&mut File,p:&Process) {
129
130 // Assume the template file exists in the Assets folder
131 let template_file = "./assets/EBML-prototype.ebml";
132
133 // Read every line from the template file into a Vec<String>
134 fn lines_from_file(filename: impl AsRef<Path>) -> io::Result<Vec<String>> {
135 BufReader::new(File::open(filename)?).lines().collect()
136 }
137
138 // Call the function above
139 let lines = lines_from_file(template_file).expect("Could not load lines");
140 // Use built-in functions to pull out the information out of the process passed into this function
141 let number = p.get_number();
142 let title = p.get_title();
143 let subject = p.get_subject();
144 let product = p.get_product();
145 let author = p.get_author();
146 let reviewer = p.get_reviewer();
147
148 // Special case: Templates. This is the one piece of information in the Process structure that isn't as simple as find-and-replace.
149 //
150 // However, we will collapse the complexity into a simple find-and-replace by making the "replace" part very convoluted.
151 //
152 // If there aren't any templates, then we'll comment out the Template line (clever!)
153 // If there are templates, then make lines like this: "Template|Template.css"
154 let all_templates = p.get_all_templates();
155 let mut template = if !(all_templates.len()==0) {
156 "Template|".to_string() + &all_templates[0]
157 } else {
158 "//Template|Example.css".to_string()
159 };
160 // That's enough if the number of Templates is 1, but for more than 1 we will add more lines with line breaks prior
161 //
162 // All of this happens in the same string. When it is written out ONCE in the same find/replace as simpler variables, the line breaks will become good EBML formatting
163 if all_templates.len()>1 {
164 for ii in 1..all_templates.len() {
165 template = template + "\nTemplate|" + &all_templates[ii];
166 }
167 }
168
169 // For every line we read from the EBML template, write that **same** line out to the new file
170 // In so doing, pass the line to write through each of the find/replace filters, including the convoluted "Template" one
171 for line in lines {
172 let mut new_line = line;
173 new_line = new_line.replace("$NUMBER",number);
174 new_line = new_line.replace("$TITLE",title);
175 new_line = new_line.replace("$SUBJECT",subject);
176 new_line = new_line.replace("$PRODUCT",product);
177 new_line = new_line.replace("$AUTHOR",author);
178 new_line = new_line.replace("$REVIEWER",reviewer);
179 new_line = new_line.replace("Template|$TEMPLATE",&template);
180 new_line = new_line + "\n";
181 f.write(new_line.as_bytes()).expect("Could not write template line into new file!");
182 }
183
184}