This week we examine a script that replaces Wingdings checkboxes with Unicode checkboxes. This is a very useful thing to do for cross-browser compatibility, as some web browsers do not display Wingdings font sets correctly. Our script iterates over the document and examines every HTML tag to determine if it’s a font tag. If it is, the script checks if the tag has Wingdings set as its font. If so, the script then checks if the font tag is bold or not and stores this information. If the font tag has bold tags inside of it, the script notes that before removing them. The script also removes any space characters and checks the contents of the font tag. If all that remains is an ‘x’ character, it replaces the ‘x’ with a Unicode checkbox. If all that’s left is a ¨ char or an ‘o’ character, it replaces it with a Unicode open box. The script then adds tags as appropriate to embolden the checkbox.
Friday, October 14. 2016
Legato Developers Corner #5: Converting Wingding Checkboxes to Unicode
Like previous weeks, this script is a .ms file so it runs at startup. It has the same three user functions as well: setup, main, and run. It also hooks our conversion script into a menu function. This time we’ll cover the following topics:
- Defines
- Object handles
- SGML parsing
- The WriteSegment SDK function
Note as well the different comment style used in this script. This style is more verbose, and more work, but lets you narrarate everything done in the script for clarity.
Our Sample Script
// GoFiler Legato Script - Replace Wingdings Checkboxes // ------------------------------------------ // // Rev 10/13/2016 // // // (c) 2016 Novaworks, LLC -- All rights reserved. // // Will examine active HTML window to replace any wingdings checkboxes with Unicode checkboxes // // Notes: // // - Requires GoFiler 4.15a or later. // #define CHECKEDBOX "☒" /* character to use for checked checkboxes */ #define UNCHECKEDBOX "☐" /* character to use for unchecked checkboxes */ /********************************************************/ /* Global Items */ /* ------------ */ /********************************************************/ string entries[]; /* wingdings character entries */ int run (int f_id, string mode);/* Call from Hook Processor */ /********************************************************/ /* Menu Setup and Patch */ /* -------------------- */ /********************************************************/ /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Define Function */ item["Code"] = "EXTENSION_REMOVE_WINGDINGS"; /* Function Code */ item["MenuText"] = "&Replace Wingdings Checkboxes"; /* Menu Text */ item["Description"] = "<B>Replace Wingdings Checkboxes</B> "; /* Description (long) */ item["Description"]+= "\r\rReplace wingdings with unicode chars."; /* * description */ /* * Check for Existing */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* * Registration */ rc = MenuAddFunction(item); /* Add the item */ if (IsError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook(item["Code"], fnScript, "run"); /* Set the Test Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /************************************************/ /* First Run from Menu Hook */ /* ------------------------ */ /************************************************/ /****************************************/ int main() { /* Initialize from Hook Processor */ /****************************************/ string s1; /* General */ /* */ s1 = GetScriptParent(); /* Get the parent */ if (s1 == "LegatoIDE") { /* Is run from the IDE (debug) */ setup(); /* Add to the menu */ run(0,"preprocess"); } /* end IDE run */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int run(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ int counter; /* increment counter */ int ex,ey,sx,sy; /* positional variables */ boolean bold; /* check if the checkbox is bold */ string opentag; /* opening tag to write out */ string closetag; /* closing tag to write out */ string element; /* sgml element */ handle sgml; /* sgml object */ handle edit_object; /* edit object */ handle edit_window; /* edit window handle */ string text; /* closing element of sgml object */ if (mode!="preprocess"){ /* if mode is not preprocess */ return ERROR_NONE; /* return no error */ } /* */ edit_window = GetActiveEditWindow(); /* get handle to edit window */ if(IsError(edit_window)){ /* get active edit window */ MessageBox('x',"This is not an edit window."); /* display error */ return ERROR_EXIT; /* return */ } /* */ edit_object = GetEditObject(edit_window); /* create edit object */ sgml = SGMLCreate(edit_object); /* create sgml object */ element = SGMLNextElement(sgml); /* get the first sgml element */ while(element != ""){ /* while element isn't empty */ if (IsError(element)){ /* if it couldn't read the element */ MessageBox('x',"Could not read HTML element, aborting."); /* print error */ return ERROR_EXIT; /* return error */ } /* check if not last tag */ if (FindInString(element,"<font",0,false)>(-1)){ /* Is it a font tag? */ if (FindInString(element,"bold",0,false)>(-1)){ /* Check if the font tag specifies bold */ bold = true; /* remember it was bold */ } /* */ else{ /* if it wasn't bold */ bold = false; /* remember it wasn't bold */ } /* */ if (FindInString(element,"wingdings")>(-1)){ /* Does it have wingdings? */ sx = SGMLGetItemPosSX(sgml); /* get start x pos */ sy = SGMLGetItemPosSY(sgml); /* get start y pos */ text = SGMLFindClosingElement(sgml); /* get the text in the tag */ if (text!=""){ /* if we can find the closing element */ if(FindInString(text,"<b>")>(-1)){ /* if the text inside is bold */ bold = true; /* remember we have bold for later */ text = ReplaceInString(text,"<b>","",true); /* remove open b tags */ text = ReplaceInString(text,"</b>","",true); /* remove closing b tags */ } /* */ ex = SGMLGetItemPosEX(sgml); /* get end x pos */ ey = SGMLGetItemPosEY(sgml); /* get end y pos */ text = ReplaceInString(text," ","",true); /* remove extra space characters */ if (bold==true){ /* if the text was bold */ opentag = "<b>"; /* opening tag */ closetag = "</b>"; /* closing tag */ } /* */ else{ /* if it wasn't bold, write nothing */ opentag = ""; /* opening tag */ closetag = ""; /* closing tag */ } /* */ if (TrimString(text)=="x"){ /* if it's a checked box */ WriteSegment(edit_object,opentag+CHECKEDBOX+closetag, /* write out checked box char */ sx,sy,ex,ey); /* write out checked box char */ } /* */ else{ /* if it's not a checked box */ WriteSegment(edit_object,opentag+UNCHECKEDBOX+closetag, /* write out unchecked box char */ sx,sy,ex,ey); /* write out unchecked box char */ } /* */ SGMLSetPosition(sgml,ex,ey); /* set position of writer to end */ counter++; /* increment counter */ } /* check if it's last tag */ } } /* */ element = SGMLNextElement(sgml); /* get the next sgml element */ } /* */ CloseHandle(edit_object); /* close edit object */ MessageBox('i',"Found and removed %d checkboxes.",counter); /* messagebox */ return ERROR_NONE; /* Exit Done */ } /* end setup */
Script Walkthrough
The first thing to note is the use of the DEFINE keyword. The DEFINE keyword instructs the script interpreter to replace the identifier that immediately follows the keyword with the value specified after that. So,
for example, we define two identifiers, CHECKEDBOX and UNCHECKEDBOX, to be our two Unicode characters we’d like to insert. Using the DEFINE keyword allows you both to make your code more understandable by using semantically meaningful terms in place of less meaningful values and to facilitate easy changing of certain values (like constants, such as our characters in this script).
#define CHECKEDBOX "☒" /* character to use for checked checkboxes */ #define UNCHECKEDBOX "☐" /* character to use for unchecked checkboxes */
The contents of the setup and main functions won’t be covered in detail. For more information on hooking a user function into a menu function, see previous blog posts. For now, let’s examine the run function where we parse the SGML file and replace the Wingdings characters with Unicode characters. As in previous scripts, we check the mode of the calling function and exit if it is not in a pre-process state. Now we use some SDK functions to get handles to the edit window and its contents.
edit_window = GetActiveEditWindow(); /* get handle to edit window */ if(IsError(edit_window)){ /* get active edit window */ MessageBox('x',"This is not an edit window."); /* display error */ return ERROR_EXIT; /* return */ } /* */ edit_object = GetEditObject(edit_window); /* create edit object */ sgml = SGMLCreate(edit_object); /* create sgml object */
The GetActiveEditWindow SDK function returns a handle to the edit window. Whenever you’re retrieving a handle, it’s good practice to check to ensure the handle is valid before using it, and we do that with the IsError SDK function and exiting if the handle is invalid. After that, we use the GetEditObject SDK function to retrieve a handle to the contents of the edit window. Using that handle, we create an SGML object using the SGMLCreate SDK function. At this point, we now have a handle to an SGML object we can parse, which we do with a while loop. It should be noted that Legato handles are managed by Legato rather than the operating system.
We then iterate through the SGML tags present in the SGML object and test them for our conditions of interest, i.e., if it’s a font tag, if it’s bold, and if it contains Wingdings characters. First, we check to see if the tag is a font tag:
if (FindInString(element,"<font",0,false)>(-1)){ /* Is it a font tag? */ ... }
This conditional expression makes use of the FindInString SDK function to determine if the “<font” string occurs within the tag. A position greater than -1 indicates the string in present inside the element. If it is a font tag, we process further.
if (FindInString(element,"bold",0,false)>(-1)){ /* Check if the font tag specifies bold */ bold = true; /* remember it was bold */ } /* */ else{ /* if it wasn't bold */ bold = false; /* remember it wasn't bold */ } /* */
Again we use the FindInString SDK function to determine if the tag is bold. If so, we set a boolean flag to true. Now we look to determine if the font tag includes Wingdings, and, if so, we get the position of the tag. Note that the SGMLGetItemPosSX and SGMLGetItemPosSY SDK functions return the starting position of the last SGML item parsed within the SGML object. Thus, if this tag contains Wingdings, we retrieve it’s starting position. The SGMLFindClosingElement SDK function advances the internal position of the SGML parser to the closing tag, so we can retrieve the position of that later. It also returns any HTML or text between the opening and closing tags. We also check for the presence of bold tags (“<b>”), and flag their presence while removing them.
if (FindInString(element,"wingdings")>(-1)){ /* Does it have wingdings? */ sx = SGMLGetItemPosSX(sgml); /* get start x pos */ sy = SGMLGetItemPosSY(sgml); /* get start y pos */ text = SGMLFindClosingElement(sgml); /* get the text in the tag */ if (text!=""){ /* if we can find the closing element */ if(FindInString(text,"<b>")>(-1)){ /* if the text inside is bold */ bold = true; /* remember we have bold for later */ text = ReplaceInString(text,"<b>","",true); /* remove open b tags */ text = ReplaceInString(text,"</b>","",true); /* remove closing b tags */ } /* */ ex = SGMLGetItemPosEX(sgml); /* get end x pos */ ey = SGMLGetItemPosEY(sgml); /* get end y pos */ text = ReplaceInString(text," ","",true); /* remove extra space characters */ if (bold==true){ /* if the text was bold */ opentag = "<b>"; /* opening tag */ closetag = "</b>"; /* closing tag */ } /* */
We then use the SDK functions SGMLGetItemPosEX and SGMLGetItemPosEY to store the ending position of the current SGML element, which should be the closing font tag. We then remove extraneous white space. Depending on our previously set bold flag and if an ‘x’ character lies in the text of the SGML element, we then create our string with or without bold tags and with either our CHECKEDBOX or UNCHECKEDBOX identifier. To put our modified tag into the SGML object, we use the SDK function WriteSegment, which takes a handle to the edit object, the newly crafted string, and the start and end positions of the element as parameters.
if (TrimString(text)=="x"){ /* if it's a checked box */ WriteSegment(edit_object,opentag+CHECKEDBOX+closetag, /* write out checked box char */ sx,sy,ex,ey); /* write out checked box char */ } /* */ else{ /* if it's not a checked box */ WriteSegment(edit_object,opentag+UNCHECKEDBOX+closetag, /* write out unchecked box char */ sx,sy,ex,ey); /* write out unchecked box char */ } /* */ SGMLSetPosition(sgml,ex,ey); /* set position of writer to end */ counter++;
The WriteSegment SDK function is extremely powerful in that it can write any string to a specified location inside an edit object. However, you should exercise caution in using this function as it can cause internal errors or unexpected behavior given we are parsing the SGML object (which was made from the edit object) based on a particular internal position. To counteract that, we employ the SGMLSetPosition SDK function to reset our internal pointer within the object to the end of the current element. Thus we can iterate to the next element in our while loop after incrementing our counter, which is keeping track of the number of items changed.
Finally, we close our edit object handle and alert the user of the number of Wingdings checkboxes the script changed.
CloseHandle(edit_object); /* close edit object */ MessageBox('i',"Found and removed %d checkboxes.",counter); /* messagebox */ return ERROR_NONE; /* Exit Done */
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