This is a script I’ve wanted to write for a while, because it makes it significantly easier to use custom page breaks in GoFiler. For a long time, GoFiler has supported page break style templates, which let you save and customize a list of page headers and footers. Using this script, you’ll be able to add, remove, and edit entries in that template file without having to edit the HTML.
Friday, August 24. 2018
LDC #99: Page Break Template Manager
This is going to be a fairly lengthy script with a lot of different functions. To make it simpler to break down and understand, we can divide it into two parts. This week’s part will focus on creating the template file if it doesn’t already exist and saving new templates to it. My next blog will be a manager script that lets you edit, rename, and delete templates from the template file.
This week’s script adds the Save Template functionality, which lets the user save a table to the template file by clicking anywhere in the table and running the function from the Document Ribbon. Before the script does anything, it will need to start by ensuring you first have a template file in your application data area or make the template folder and file if needed. Once it confirms a template file exists, it will work back from the caret position and ensure that the caret is in a table. It will then read the table’s code and save it to the template file.
Let’s take a look at how the script does this, starting with the run_save function. The main and setup functions are nearly identical to other functions we’ve done in the past: they just run the script on an open HTML file and set up the hooks respectively, so we don’t need to discuss them in depth.
/****************************************/ int run_save(int f_id, string mode, handle window){ /* run the save function */ /****************************************/ ... variable declarations omitted ... /* */ if (mode != "preprocess"){ /* if not preprocess */ SetLastError(ERROR_NONE); /* set last error to nothing */ return ERROR_EXIT; /* bail out */ } /* */ if (IsWindowHandleValid(window)==false){ /* if we have no valid handle */ window = GetActiveEditWindow(); /* get the active edit window */ window_type = GetEditWindowType(window); /* get the edit window type */ window_type = window_type & EDX_TYPE_ID_MASK; /* mask file type */ if (window_type != EDX_TYPE_PSG_PAGE_VIEW){ /* if we're not in page view */ MessageBox('x',"This function must be used in page view."); /* display error */ return ERROR_EXIT; /* quit running */ } /* */ edit_object = GetEditObject(window); /* get edit object from window */ caret[0] = GetCaretXPosition(window); /* get the caret position in window */ caret[1] = GetCaretYPosition(window); /* get the caret position in window */ } /* */
First, we need to check the mode to see if it’s preprocess. If it’s not, we can set the last error to ERROR_NONE, then return ERROR_EXIT to halt the execution of any further processes caused by running this script. If we’ve been given a window handle by whatever script called this function, we can skip getting the window. If not, we need to get the active window, check to make sure it’s a Page View window, then get the edit object from it to get the caret x and y positions from that.
else{ /* if we were passed a window */ edit_object = GetEditObject(window); /* get edit object from window */ caret[0] = TEST_X; /* set test position x */ caret[1] = TEST_Y; /* set test position y */ } /* */ sgml = SGMLCreate(edit_object); /* get handle to SGML object */ SGMLSetPosition(sgml,caret[0],caret[1]); /* set position in SGML parser */ element = "init"; /* initialize element with a value */ while (element!="TABLE" && element!=""){ /* while element is not table and exists*/ tag = SGMLPreviousElement(sgml); /* get the previous SGML tag */ element = SGMLGetElementString(sgml); /* get the string value of the element */ } /* */ if (element == ""){ /* if the element is empty */ MessageBox('x',"This function must be run inside a table."); /* display message */ return ERROR_EXIT; /* quit running */ } /* */ table_code = SGMLFindClosingElement(sgml, /* get the code of the table */ SP_FCE_CODE_AS_IS | SP_FCE_INCLUDE_WRAPPER); /* get the code of the table */ rc = DialogBox(PAGEBREAK_TEMPLATES, "save_"); /* enter the save dialog */ SetLastError(rc); /* set last error */ return ERROR_EXIT; /* exit */ } /* */
If we have been given a window by another script, we can get the edit object, and set x and y positions in the script to run on. I used defines TEST_X and TEST_Y in this case, because it made it easier to debug, as my sample file had a table at the start, and my defined values put my caret within the table. If I was going to expand this script to be runnable by other scripts more easily, I’d make these variables that are passed to the function instead of defined values so they work on more than just my debug test file. Next, we can create the SGML parser, and parse backwards from our caret position until we encounter an open table tag. If we didn’t encounter an open table tag, we can display an error and return an error code. Once we have our table, we can get the code of the table with the SGMLFindClosingElement tag, using the appropriate flags to get the full SGML code for the table as-is, so we can later store it in our template file. Finally, we want to ask the user for input on naming the template, so we open our dialog. Once we return from our dialog, we can set the last error to whatever the dialog returned, then return an error exit code to prevent further processes from running.
Now let’s take a look at how our dialog function works. We have two functions here, save_validate, and save_ok. When the user presses “ok” on the dialog, both of these are called, first save_validate, then save_ok. If the validate returns anything other than ERROR_NONE, the save_ok function never runs. Otherwise, the save_ok function runs, then the dialog ends and returns the result of the save_ok function.
/****************************************/ int save_validate(){ /* validate the save name dialog */ /****************************************/ string pb_name; /* the name of the page break save */ /* */ pb_name = EditGetText(TEMPLATE_NAME); /* get the template name */ if (pb_name == ""){ /* if page break name is blank */ MessageBox('x',"Template must have a name."); /* display error */ return ERROR_EXIT; /* return error */ } /* */ return ERROR_NONE; /* return no error */ } /* */
Our validation here is really simple. We just check to see if the TEMPLATE_NAME field is empty. If not, then we can keep going, because we have a name for our template. If it is empty, we can display an error then return an error code, so our user can try again to enter a value in the dialog control. We’re going to expand upon this next week, to have an additional check to make sure the name we want to use isn’t already used in a different page break template.
/****************************************/ int save_ok(){ /* after validating the save dialog */ /****************************************/ ... variable declarations omitted... /* */ pb_name = EditGetText(TEMPLATE_NAME); /* get the page break template name */ template_folder = GetApplicationDataFolder(); /* Get the appdata directory */ template_folder = AddPaths(template_folder,"Templates"); /* build path to templates folder */ if (IsFolder(template_folder)==false){ /* if template folder doesn't exist */ CreateFolder(template_folder); /* create the template folder */ } /* */ template_file = AddPaths(template_folder,TEMPLATE_FILENAME); /* set path to template file */ if (IsFile(template_file)==false){ /* if template file doesn't exist */ rc = HTTPGetFile(TEMPLATE_HTTP_LOC,template_file); /* get the template file */ if (IsError(rc)){ /* if we couldn't get the template */ MessageBox('x',"Cannot get template file, error %0x",rc); /* display error */ return ERROR_EXIT; /* return with error */ } /* */ } /* */
First, we need to make sure our template file exists. We can use the GetApplicationDataFolder and AddPaths functions to build a path to where our template folder should be. If it doesn’t exist, we can create it. We can then test to see if the template file is inside it. If not, we can use HTTPGetFile to pull the default file down from our website onto the computer. It’s important to check for an error here, because if there’s no internet connection or our website is down for some reason you need to display that to the user so the script stops executing.
file = OpenMappedTextFile(template_file); /* open the template file */ rc = GetLastError(); /* get the last error */ if (IsError(rc)){ /* if we couldn't open the file */ MessageBox('x',"Cannot open template file."); /* display error message */ return ERROR_EXIT; /* return error */ } /* */ size = GetLineCount(file); /* get number of lines in mapped text */ while (line != TEMPLATE_BODY_LINE && ix<size){ /* while we haven't found the body */ line = ReadLine(file,ix); /* get the line */ ix++; /* increment line counter */ } /* */ if (line!=TEMPLATE_BODY_LINE){ /* if we couldn't find the body */ MessageBox('x',"Template file is not valid."); /* display an error message */ return ERROR_EXIT; /* return error */ } /* */ line = FormatString(P_TEMPLATE,pb_name); /* build line to insert */ line+= "\r\n\r\n"+table_code+"\r\n"; /* build line to insert */ InsertLine(file,ix+1,line); /* insert line */ MappedTextSave(file); /* save our modified file */ rc = GetLastError(); /* get the last error */ if (IsError(rc)){ /* if we can't save */ MessageBox('x',"Cannot save template file."); /* display an error */ return ERROR_EXIT; /* return an error */ } /* */ return ERROR_NONE; /* return no error */ } /* */
Once we have our template file, we can open it using OpenMappedTextFile, so we can read and write to the file. If we can’t open the file, we need to display and return an error to stop executing the script here. Otherwise, we can get the line count, and iterate over the lines until we find the <BODY> HTML tag. We need to insert our new line right after that, so we can build our line using our P_TEMPLATE define, the template name the user entered, and the table code we read out of the HTML file earlier. We can then use the InsertLine and MappedTextSave functions to write our file back out, to complete adding our new template to the template file.
Here’s the full script:
/* PageBreakTemplateManager.ms * * Author: Steven Horowitz * * Notes: Allows a user to save and edit page break templates. */ /************************************************/ /* Defined Values */ /************************************************/ #define TEMPLATE_FILENAME "HeaderFooterList.htm" #define P_TEMPLATE "<P STYLE=\"margin: 0\">{%s}</P>" #define TEMPLATE_HTTP_LOC "http://www.novaworkssoftware.com/files/HeaderFooterList.htm" #define TEMPLATE_BODY_LINE "<BODY STYLE=\"font: 11pt Times New Roman, Times, Serif\">" #define TEST_X 0 #define TEST_Y 10 /************************************************/ /* Function Signatures */ /************************************************/ void setup(); /* set up the hooks */ int run_save(int f_id, string mode, handle window); /* run the save template script */ /************************************************/ /* global values */ /************************************************/ string table_code; /* code for a table being saved */ /****************************************/ void setup() { /* Called from Application Startup */ /****************************************/ string fn; /* file name */ string item[10]; /* menu item array */ string settings; /* the settings file location */ /* */ /* */ item["Code"] = "SAVE_PAGE_BREAK"; /* set hook code */ item["Description"] ="Save the current table as page break preset.";/* set hook description */ item["MenuText"] = "Save Page Break Template"; /* set menu text */ item["Class"] = "DocumentExtension"; /* set class as document */ MenuAddFunction(item); /* add menu item to menu */ fn = GetScriptFilename(); /* get the filename of the script */ MenuSetHook("SAVE_PAGE_BREAK", fn, "run_save"); /* set the hook */ } /* */ /****************************************/ void main(){ /* main function */ /****************************************/ int ix; /* counter */ int size; /* number of open windows */ string filename; /* filename of a window */ string ext; /* extension of current filename */ string windows[][]; /* array of all available windows */ /* */ if(GetScriptParent()=="LegatoIDE"){ /* if we're in IDE mode */ windows = EnumerateEditWindows(); /* get all active edit windows */ size = ArrayGetAxisDepth(windows); /* get the number of open windows */ for(ix=0;ix<size;ix++){ /* for each open edit window */ filename = windows[ix]["Filename"]; /* get the filename of the window open */ ext = GetExtension(filename); /* get the extension to the filename */ if (ext == ".htm"){ /* if the extension is HTML */ MessageBox("running save on file %s",filename); /* display running save */ run_save(0,"preprocess", /* run the save function */ MakeHandle(windows[ix]["ClientHandle"])); /* run the save function on the window */ } /* */ } /* */ } /* */ setup(); /* run setup */ } /* */ /****************************************/ int run_save(int f_id, string mode, handle window){ /* run the save function */ /****************************************/ int rc; /* return code */ dword window_type; /* get the edit window type */ handle edit_object; /* handle to the edit object */ int caret[]; /* caret positions */ string tag; /* full text of element tag */ string element; /* the current element */ handle sgml; /* handle to the SGML parser */ /* */ if (mode != "preprocess"){ /* if not preprocess */ SetLastError(ERROR_NONE); /* set last error to nothing */ return ERROR_EXIT; /* bail out */ } /* */ if (IsWindowHandleValid(window)==false){ /* if we have no valid handle */ window = GetActiveEditWindow(); /* get the active edit window */ window_type = GetEditWindowType(window); /* get the edit window type */ window_type = window_type & EDX_TYPE_ID_MASK; /* mask file type */ if (window_type != EDX_TYPE_PSG_PAGE_VIEW){ /* if we're not in page view */ MessageBox('x',"This function must be used in page view."); /* display error */ return ERROR_EXIT; /* quit running */ } /* */ edit_object = GetEditObject(window); /* get edit object from window */ caret[0] = GetCaretXPosition(window); /* get the caret position in window */ caret[1] = GetCaretYPosition(window); /* get the caret position in window */ } /* */ else{ /* if we were passed a window */ edit_object = GetEditObject(window); /* get edit object from window */ caret[0] = TEST_X; /* set test position x */ caret[1] = TEST_Y; /* set test position y */ } /* */ sgml = SGMLCreate(edit_object); /* get handle to SGML object */ SGMLSetPosition(sgml,caret[0],caret[1]); /* set position in SGML parser */ element = "init"; /* initialize element with a value */ while (element!="TABLE" && element!=""){ /* while element is not table and exists*/ tag = SGMLPreviousElement(sgml); /* get the previous SGML tag */ element = SGMLGetElementString(sgml); /* get the string value of the element */ } /* */ if (element == ""){ /* if the element is empty */ MessageBox('x',"This function must be run inside a table."); /* display message */ return ERROR_EXIT; /* quit running */ } /* */ table_code = SGMLFindClosingElement(sgml, /* get the code of the table */ SP_FCE_CODE_AS_IS | SP_FCE_INCLUDE_WRAPPER); /* get the code of the table */ rc = DialogBox(PAGEBREAK_TEMPLATES, "save_"); /* enter the save dialog */ SetLastError(rc); /* set last error */ return ERROR_EXIT; /* exit */ } /* */ /****************************************/ int save_validate(){ /* validate the save name dialog */ /****************************************/ string pb_name; /* the name of the page break save */ /* */ pb_name = EditGetText(TEMPLATE_NAME); /* get the template name */ if (pb_name == ""){ /* if page break name is blank */ MessageBox('x',"Template must have a name."); /* display error */ return ERROR_EXIT; /* return error */ } /* */ return ERROR_NONE; /* return no error */ } /* */ /****************************************/ int save_ok(){ /* after validating the save dialog */ /****************************************/ int rc; /* return code */ int ix; /* iterator */ int size; /* size of the mapped text file */ string pb_name; /* page break name */ string line; /* content of line of mapped text file */ handle file; /* handle to the template file */ string template_file; /* the template file */ string template_folder; /* templates folder */ /* */ pb_name = EditGetText(TEMPLATE_NAME); /* get the page break template name */ template_folder = GetApplicationDataFolder(); /* Get the appdata directory */ template_folder = AddPaths(template_folder,"Templates"); /* build path to templates folder */ if (IsFolder(template_folder)==false){ /* if template folder doesn't exist */ CreateFolder(template_folder); /* create the template folder */ } /* */ template_file = AddPaths(template_folder,TEMPLATE_FILENAME); /* set path to template file */ if (IsFile(template_file)==false){ /* if template file doesn't exist */ rc = HTTPGetFile(TEMPLATE_HTTP_LOC,template_file); /* get the template file */ if (IsError(rc)){ /* if we couldn't get the template */ MessageBox('x',"Cannot get template file, error %0x",rc); /* display error */ return ERROR_EXIT; /* return with error */ } /* */ } /* */ file = OpenMappedTextFile(template_file); /* open the template file */ rc = GetLastError(); /* get the last error */ if (IsError(rc)){ /* if we couldn't open the file */ MessageBox('x',"Cannot open template file."); /* display error message */ return ERROR_EXIT; /* return error */ } /* */ size = GetLineCount(file); /* get number of lines in mapped text */ while (line != TEMPLATE_BODY_LINE && ix<size){ /* while we haven't found the body */ line = ReadLine(file,ix); /* get the line */ ix++; /* increment line counter */ } /* */ if (line!=TEMPLATE_BODY_LINE){ /* if we couldn't find the body */ MessageBox('x',"Template file is not valid."); /* display an error message */ return ERROR_EXIT; /* return error */ } /* */ line = FormatString(P_TEMPLATE,pb_name); /* build line to insert */ line+= "\r\n\r\n"+table_code+"\r\n"; /* build line to insert */ InsertLine(file,ix+1,line); /* insert line */ MappedTextSave(file); /* save our modified file */ rc = GetLastError(); /* get the last error */ if (IsError(rc)){ /* if we can't save */ MessageBox('x',"Cannot save template file."); /* display an error */ return ERROR_EXIT; /* return an error */ } /* */ return ERROR_NONE; /* return no error */ } /* */ /************************************************/ /* dialog controls */ /************************************************/ #beginresource /****************************************/ /* save template dialog */ /****************************************/ #define PAGEBREAK_TEMPLATES 101 #define TEMPLATE_PROPERTIES 102 #define TEMPLATE_NAME 103 PAGEBREAK_TEMPLATES DIALOGEX 0, 0, 240, 66 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Header / Footer Properties" FONT 8, "MS Sans Serif" { CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 132, 50, 50, 14 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 187, 50, 50, 14 CONTROL "Name:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 14, 25, 40, 13, 0 CONTROL "Template Properties:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 10, 8, 86, 13, 0 CONTROL "Frame1", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 84, 12, 146, 1, 0 CONTROL "", TEMPLATE_NAME, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 53, 25, 172, 12, 0 } #endresource
While this script doesn’t break any new ground in showing new features or functions of Legato, I think it’s another good example of how to improve existing functionality in GoFiler with a fairly simple Legato script. A couple hundred lines of code here, maybe a few hours work, and it’s now significantly easier to add templates to the page break template file. Next week, we’re going to expand on this, adding even more functionality, making GoFiler even easier to use.
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
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato