One common task when working on an HTML document in GoFiler is to save different versions of the file. Every time a copy is sent out for review, users can do a save as operation, and save a new copy of the file with a different version number. GoFiler by default allows you to create backup copies of the file (.bak files) whenever you press the save button, but that’s often too many backups, and they don’t represent true versions. The example script in this blog post then attempts to automate the process of creating different versions of files. This script will trigger when the user presses the “To Browser” button on an HTML file, and ask if the user wants to create a previous version folder of this file. If the user presses “Yes”, the script makes a new folder called “Revisions”, puts a folder stamped with the version number, date, and time into it, and copies all the HTML and images out of your folder into this previous version folder.
Our full sample script is below:
/* AutomateProofVersions.ms
*
* Author: Steven Horowitz
*
* Rev 01 SCH 3/06/18
*
*/
#define REV_FOLDER_NM "Revisions"
#define REV_INSTANCE_FOLDER_NM "v%03d (%s)"
#define NEW_REV_QUERY "Would you like to create a previous version folder for this file?"
#define CANNOT_CREATE_FOLDER_MSG "Cannot create target folder."
#define FOLDER_CREATE_MSG "New version folder created at %s."
#define TIME_TEMPLATE "m-d-y Gi"
void setup();
void run(int f_id, string mode, handle window);
int get_version_num(string foldername);
void setup(){
string fn;
fn = GetScriptFilename();
MenuSetHook("FILE_LAUNCH", fn, "run");
}
void main(){
int ix;
int size;
string windows[][];
handle window;
if (GetScriptParent() == "LegatoIDE"){
windows = EnumerateEditWindows();
size = ArrayGetAxisDepth(windows);
for (ix = 0 ; ix < size; ix++){
if (windows[ix]["FileTypeToken"] == "FT_HTML"){
run (0,"preprocess", MakeHandle(windows[ix]["ClientHandle"]));
}
}
}
setup();
}
void run(int f_id, string mode, handle window){
dword wType;
int rc;
string folder;
string datestamp;
string file_content;
string filename;
string rev_folder;
string other_files[];
string new_rev_folder;
string new_foldername;
string rev_folders[];
string ext;
int num_files;
int num_folders;
int next_version;
int version;
int ix;
if (mode != "preprocess"){
return;
}
if (IsWindowHandleValid(window)==false){
window = GetActiveEditWindow();
wType = GetEditWindowType(window) & EDX_TYPE_ID_MASK;
if (wType != EDX_TYPE_PSG_PAGE_VIEW){
return;
}
}
rc = YesNoBox('q',NEW_REV_QUERY);
if (rc != IDYES){
return;
}
filename = GetEditWindowFilename(window);
folder = GetFilePath(filename);
rev_folder = AddPaths(folder,REV_FOLDER_NM);
if (IsFolder(rev_folder)==false){
rc = CreateFolder(rev_folder);
if (IsError(rc)){
MessageBox('x',CANNOT_CREATE_FOLDER_MSG);
return;
}
}
rev_folders = EnumerateFolders(AddPaths(rev_folder,"*.*"));
num_folders = ArrayGetAxisDepth(rev_folders);
next_version = 0;
for(ix=0; ix<num_folders; ix++){
version = get_version_num(rev_folders[ix]);
if (version >= next_version && version>0){
next_version = version;
}
}
next_version++;
datestamp = FormatDate(GetLocalTime(),TIME_TEMPLATE);
new_foldername = FormatString(REV_INSTANCE_FOLDER_NM,next_version,datestamp);
new_foldername = AddPaths(rev_folder,new_foldername);
rc = CreateFolder(new_foldername);
MessageBox('i',FOLDER_CREATE_MSG,new_foldername);
if (IsError(rc)){
MessageBox('x',CANNOT_CREATE_FOLDER_MSG);
return;
}
other_files = EnumerateFiles(AddPaths(folder,"*.html;*.htm;*.jpg;*.gif"));
num_files = ArrayGetAxisDepth(other_files);
for(ix=0; ix<num_files; ix++){
filename = GetFilename(other_files[ix]);
CopyFile(AddPaths(folder,filename),AddPaths(new_foldername,filename));
}
}
int get_version_num(string foldername){
string version;
int rc;
int val;
int vpos;
vpos = FindInString(foldername, "v");
version = GetStringSegment(foldername, vpos+1, 3);
val = DecimalToInteger(version);
rc = GetLastError();
if (IsError(rc)){
return (-1);
}
return val;
}
First, let’s take a look at the defined values. I tried to make this as customizable as possible, so using defines for things like folder name templates makes it easy to change or modify them in the future. REV_FOLDER_NM is the name of the master revisions folder everything goes into. REV_INSTANCE_FOLDER_NM is the template used for the name of a specific revision. The “v%03d” is important to keep together so you have a 3 digit revision number like “v004”, and the second %s is very important to keep there because that’s where the date time stamp goes. Other than that, things can be added or removed from this template. NEW_REV_QUERY is just the question that pops up when the user is prompted for making a new revision folder. The CANNOT_CREATE_FOLDER_MSG and FOLDER_CREATE_MSG defines are pretty self explanatory, they are the messages that show up when a new folder cannot be created, or was created respectively. Finally, TIME_TEMPLATE is the template used when making the individual revision folders. It’s the date format string inserted into the string parameter of the REV_INSTANCE_FOLDER_NM define.
#define REV_FOLDER_NM "Revisions"
#define REV_INSTANCE_FOLDER_NM "v%03d (%s)"
#define NEW_REV_QUERY "Would you like to create a previous version folder for this file?"
#define CANNOT_CREATE_FOLDER_MSG "Cannot create target folder."
#define FOLDER_CREATE_MSG "New version folder created at %s."
#define TIME_TEMPLATE "m-d-y Gi"
The main function is very simple, we just want something to cycle through all open windows, and if it finds an HTML window, run our script on it. This is purely for testing purposes, and exactly how this works has been covered in previous blog posts.
void main(){
int ix;
int size;
string windows[][];
handle window;
if (GetScriptParent() == "LegatoIDE"){
windows = EnumerateEditWindows();
size = ArrayGetAxisDepth(windows);
for (ix = 0 ; ix < size; ix++){
if (windows[ix]["FileTypeToken"] == "FT_HTML"){
run (0,"preprocess", MakeHandle(windows[ix]["ClientHandle"]));
}
}
}
setup();
}
I’m skipping the setup function, because all that does is hook our script to the proof to browser function and is pretty obvious in how it works. The run function is where all the fun stuff happens. First, we need to check to ensure we’re running in preprocess mode. Then, we can check to see if we were passed a valid handle. This would only happen if running from the IDE for debugging. If we’re not debugging, we need to grab the active edit window, and check if it’s a page view. If so, we can go ahead and use it. Otherwise, we can return here. After we have our window, we can ask the user with a YesNoBox function if they want to create a new version. If they click anything but yes, we can exit here. Otherwise, we need to get the filename and folder of the HTML file we’re in, and build the path to our revisions folder. If that folder doesn’t exist, we need to try to create it using the CreateFolder function. Whenever we create a folder, we always need to use IsError on the resulting return code to ensure that it actually created correctly. There are many reasons why a folder might not be able to be created, so it’s best to test to make sure it actually worked.
void run(int f_id, string mode, handle window){
....omitted declarations...
if (mode != "preprocess"){
return;
}
if (IsWindowHandleValid(window)==false){
window = GetActiveEditWindow();
wType = GetEditWindowType(window) & EDX_TYPE_ID_MASK;
if (wType != EDX_TYPE_PSG_PAGE_VIEW){
return;
}
}
rc = YesNoBox('q',NEW_REV_QUERY);
if (rc != IDYES){
return;
}
filename = GetEditWindowFilename(window);
folder = GetFilePath(filename);
rev_folder = AddPaths(folder,REV_FOLDER_NM);
if (IsFolder(rev_folder)==false){
rc = CreateFolder(rev_folder);
if (IsError(rc)){
MessageBox('x',CANNOT_CREATE_FOLDER_MSG);
return;
}
}
Now that we have our folder, we can use EnumerateFolders on our revisions folder to get all the different version folder names. We can use ArrayGetAxisDepth to get the number of those folders, then iterate over each of them. We’re just looking for the next version number, so for each folder we need to run our get_version_num function to get the version, and if it’s greater than or equal to the next_version variable, we can reset next_version to it’s value and keep going on. This should ensure that next_version will equal the highest version in the folder. We can then add one because we’re making a new version, get a datestamp by using the FormatDate and GetLocalTime functions, and build our new folder name. Once we have our new foldername, we can use AddPath to make it a complete path, and run the CreateFolder function again, being sure to let the user know we created a new folder, and also checking to make sure we actually created the folder.
rev_folders = EnumerateFolders(AddPaths(rev_folder,"*.*"));
num_folders = ArrayGetAxisDepth(rev_folders);
next_version = 0;
for(ix=0; ix<num_folders; ix++){
version = get_version_num(rev_folders[ix]);
if (version >= next_version && version>0){
next_version = version;
}
}
next_version++;
datestamp = FormatDate(GetLocalTime(),TIME_TEMPLATE);
new_foldername = FormatString(REV_INSTANCE_FOLDER_NM,next_version,datestamp);
new_foldername = AddPaths(rev_folder,new_foldername);
rc = CreateFolder(new_foldername);
MessageBox('i',FOLDER_CREATE_MSG,new_foldername);
if (IsError(rc)){
MessageBox('x',CANNOT_CREATE_FOLDER_MSG);
return;
}
All that’s left to do now is to copy the contents of all files from our current folder into our revisions folder. We need to use EnumerateFiles to get all html, htm, jpg, and gif files first. Then we can iterate over each, and use CopyFile to copy it into it’s new location.
other_files = EnumerateFiles(AddPaths(folder,"*.html;*.htm;*.jpg;*.gif"));
num_files = ArrayGetAxisDepth(other_files);
for(ix=0; ix<num_files; ix++){
filename = GetFilename(other_files[ix]);
CopyFile(AddPaths(folder,filename),AddPaths(new_foldername,filename));
}
}
The get_version_num function’s job is to take a folder name, and return the version number from it. It’s pretty simple, it needs to get the position of the “v” character in the folder name, then get the three numbers after it, and convert them to an integer. If the last error was an error, then it means the folder didn’t have a valid version number or we couldn’t get the version number, so we can return -1, which our run function will handle accordingly.
int get_version_num(string foldername){
string version;
int rc;
int val;
int vpos;
vpos = FindInString(foldername, "v");
version = GetStringSegment(foldername, vpos+1, 3);
val = DecimalToInteger(version);
rc = GetLastError();
if (IsError(rc)){
return (-1);
}
return val;
}
Automation tasks like this are where Legato truly shines. If you have some very basic task that you want to insure is always done, taking a few hours to write a script for it will save a lot of time (and headache, if someone forgets to do it) over manually creating revision folders like this. Really any simple, repetitive task is a great application for Legato.
Steven Horowitz has been working for Novaworks for over five years as a technical expert with a focus on EDGAR HTML and XBRL. Since the creation of the Legato language in 2015, Steven has been developing scripts to improve the GoFiler user experience. He is currently working toward a Bachelor of Sciences in Software Engineering at RIT and MCC. |
Additional Resources
Novaworks’ Legato Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato