Maybe you can stripe candy canes too, given it’s the season. However, this blog post is talking about striping tables! We had another feature request for GoFiler last week that we thought would make a good blog post. This request concerned developing a way for GoFiler to start striping the table after the first dollar sign in the table instead of at the normal body position that GoFiler detects. There are two ways we could do this: we could spend a lot of time modifying a core function of GoFiler, the Polish Table function, or we could make a Legato script that fires on post-process and re-does the striping of the table. The latter is preferable because it doesn’t affect other uses who perhaps don’t want or need this feature. Anyone who does can simply add this script to the extensions folder and that user can get the benefits of a new features.
Friday, December 22. 2017
LDC #64: Stripe tables, not candy canes!
Here is our sample script:
#define CURRENCY_SYMBOLS "$\r€\r£\r¥" boolean ppp_stripe_nq; boolean do_ppp_stripe; void run_ppp_stripe (int f_id, string mode); void clear_table_row_colors (handle sgml, handle edit_object, int sx, int sy); string sgml_to_stripe_pos (handle sgml); /****************************************/ void setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* path to our script file */ /* */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook("TABLE_CLEANUP_POLISH",fnScript,"run_ppp_stripe"); /* Set the Test Hook */ } /* end setup */ /****************************************/ void main(){ /* main */ /****************************************/ setup(); /* run setup */ } /* */ /****************************************/ void run_ppp_stripe(int f_id, string mode){ /* run_ppp_stripe */ /****************************************/ int c_x, c_y; /* cursor x, y position */ int t_ex,t_ey; /* endpoints of table tag */ int rc; /* return code */ dword remember; /* remember checkbox result */ int token; /* token */ string msg; /* message to user */ string stripe_pos; /* stiper position */ string element; /* current sgml element */ handle sgml; /* sgml parser */ handle edit_window; /* handle to edit window */ handle edit_object; /* handle to edit object */ /* */ if(mode!="postprocess"){ /* if not running in post proces */ return; /* quit the script */ } /* */ if (ppp_stripe_nq != true){ /* if we're not in noquery mode */ msg = "Begin table striping on row with first currency symbol?"; /* set user msg */ rc = YesNoRememberBox(msg); /* query user */ remember = GetLastError(); /* get checkbox result from user */ if (rc == IDYES){ /* if user pressed yes */ do_ppp_stripe = true; /* set ppp stripe to true */ } /* */ else { /* if user didn't press yes */ do_ppp_stripe = false; /* set ppp stripe to false */ } /* */ if (remember != ERROR_NONE){ /* if the user checked remember */ ppp_stripe_nq = true; /* do not query next time run */ } /* */ } /* */ if (do_ppp_stripe != true){ /* if user doesn't want this function */ return; /* quit */ } /* */ edit_window = GetActiveEditWindow(); /* get the active edit window */ edit_object = GetEditObject(edit_window); /* get the active edit object */ c_x = GetCaretXPosition(edit_object); /* get caret x position */ c_y = GetCaretYPosition(edit_object); /* get caret y position */ sgml = SGMLCreate(edit_object); /* create SGML object */ element = SGMLPreviousElement(sgml, c_x, c_y); /* get the previous element */ while (element!="" && token != HT_TABLE){ /* while not at the table tag */ element = SGMLPreviousElement(sgml); /* get the previous element */ token = SGMLGetElementToken(sgml); /* get the token */ } /* */ t_ex = SGMLGetItemPosEX(sgml); /* end pos of table */ t_ey = SGMLGetItemPosEY(sgml); /* end pos of table */ stripe_pos = sgml_to_stripe_pos(sgml); /* get stripe start pos */ if (stripe_pos!=""){ /* get SGML parser striping pos */ clear_table_row_colors(sgml,edit_object,t_ex,t_ey); /* clear all background colors of rows */ c_x = GetParameter(stripe_pos,"sx"); /* get caret x position */ c_y = GetParameter(stripe_pos,"sy"); /* get caret y position */ CloseHandle(sgml); /* close the sgml parser */ CloseHandle(edit_object); /* close the handle */ SetCaretPosition(edit_window, c_x, c_y); /* set caret position */ CloseHandle(edit_window); /* close the handle */ RunMenuFunction("TABLE_STRIPING","NoQuery:true"); /* run the table stripe function */ } /* */ } /* */ /****************************************/ string sgml_to_stripe_pos(handle sgml){ /* move SGML to row to stripe on */ /****************************************/ string symbols[]; /* symbols to stop on */ string stripe_pos; /* striper position */ string item; /* an item from the parser */ dword token; /* the token of the curent parse pos */ /* */ symbols = ExplodeString(CURRENCY_SYMBOLS); /* explode symbols to array */ item = SGMLNextItem(sgml); /* get next item in parser */ while(item!="" && token!=HT__TABLE){ /* while not at end of table */ token = SGMLGetElementToken(sgml); /* get token of item */ if (SGMLGetItemType(sgml) == SPI_TYPE_TEXT || /* if item is text or */ SGMLGetItemType(sgml) == SPI_TYPE_CHAR){ /* if item is char entity */ if (FindInList(symbols, item)>(-1)){ /* if it's in the symbol list */ stripe_pos = FormatString("sx:%d;sy:%d", /* get position for striper */ SGMLGetItemPosEX(sgml), SGMLGetItemPosEY(sgml)); /* get position for striper */ return stripe_pos; /* end here */ } /* */ } /* */ item = SGMLNextItem(sgml); /* next */ } /* */ return ""; /* return false */ } /****************************************/ void clear_table_row_colors(handle sgml, handle edit_object, /* clear all table row backgrounds */ int s_sx, int s_sy){ /****************************************/ dword token; /* token of table item */ int sx, sy, ex, ey; /* points of table */ string element; /* element text */ string style[]; /* style of element */ /* */ element = SGMLNextElement(sgml,s_sx,s_sy); /* get next element */ while (element!="" && token!=HT__TABLE){ /* while we have another element */ token = SGMLGetElementToken(sgml); /* get token */ if (token==HT_TR){ /* if we're on a table row */ sx = SGMLGetItemPosSX(sgml); /* get positions of tag */ ex = SGMLGetItemPosEX(sgml); /* get positions of tag */ sy = SGMLGetItemPosSY(sgml); /* get positions of tag */ ey = SGMLGetItemPosEY(sgml); /* get positions of tag */ style = CSSGetProperties(sgml); /* get CSS properties */ style["background-color"] = ""; /* set background to blank */ CSSSetProperties(sgml,style); /* set style of tag */ element = SGMLToString(sgml); /* get text of current tag */ WriteSegment(edit_object,element,sx,sy,ex,ey); /* write out new row tag */ SGMLSetPosition(sgml,ex,ey); /* set parser position */ } /* */ element = SGMLNextElement(sgml); /* get next element */ } /* */ SGMLSetPosition(sgml,s_sx,s_sy); /* put it back where we found it */ } /* */
Let’s start with our defines and global variables. CURRENCY_SYMBOLS is a string that contains all of our recognized symbols for currency. The contents can be changed to put any other currency types you want in it or remove those you don’t. We separated the symbols with the \r new line character, but you can use any delimiter you wish. The ExplodeString function is going to take it and turn it into an array so we can use the FindInList function to search through it. The global variables ppp_stripe_nq and do_ppp_stripe are used to keep track of our user input. The function will ask the user if he or she wants to stripe at the currency symbol and if the response should be remembered. ppp_stripe_nq (named for Polish Post Process No Query; often descriptive variable names help visualize what’s going on) remembers if the user checked the remember box and do_ppp_stripe is set to true if the user clicks “yes” on that dialog. The setup function is barebones this time; we only need to set the hook to the TABLE_CLEANUP_POLISH function. It also runs our run_ppp_stripe function afterwards. The main function simply runs setup so we can hook it manually from the IDE if we want.
#define CURRENCY_SYMBOLS "$\r€\r£\r¥" boolean ppp_stripe_nq; boolean do_ppp_stripe; void run_ppp_stripe (int f_id, string mode); void clear_table_row_colors (handle sgml, handle edit_object, int sx, int sy); string sgml_to_stripe_pos (handle sgml); /****************************************/ void setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* path to our script file */ /* */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook("TABLE_CLEANUP_POLISH",fnScript,"run_ppp_stripe"); /* Set the Test Hook */ } /* end setup */ /****************************************/ void main(){ /* main */ /****************************************/ setup(); /* run setup */ } /* */
The primary function is run_ppp_stripe. This function is run when our hooked function, polish table, is run. We only want it to run post process, though, to clean up our striping, so we need to check if the mode is anything other than postprocess first and return if it is. Next, we check if the user has enabled no query mode by checking the variable ppp_stripe_nq. If not, we can ask the user for input. The YesNoRemember box sets last error dword to whatever the response is from the user, so we can check that using the GetLastError function. If the response is IDYES, we set do_ppp_stripe to true. Otherwise, it’s false. If the YesNoRemember box is anything other than ERROR_NONE, it means they checked the box, so we should set no query mode to true.
/****************************************/ void run_ppp_stripe(int f_id, string mode){ /* run_ppp_stripe */ /****************************************/ int c_x, c_y; /* cursor x, y position */ int t_ex,t_ey; /* endpoints of table tag */ int rc; /* return code */ dword remember; /* remember checkbox result */ int token; /* token */ string msg; /* message to user */ string stripe_pos; /* stiper position */ string element; /* current sgml element */ handle sgml; /* sgml parser */ handle edit_window; /* handle to edit window */ handle edit_object; /* handle to edit object */ /* */ if(mode!="postprocess"){ /* if not running in post proces */ return; /* quit the script */ } /* */ if (ppp_stripe_nq != true){ /* if we're not in noquery mode */ msg = "Begin table striping on row with first currency symbol?"; /* set user msg */ rc = YesNoRememberBox(msg); /* query user */ remember = GetLastError(); /* get checkbox result from user */ if (rc == IDYES){ /* if user pressed yes */ do_ppp_stripe = true; /* set ppp stripe to true */ } /* */ else { /* if user didn't press yes */ do_ppp_stripe = false; /* set ppp stripe to false */ } /* */ if (remember != ERROR_NONE){ /* if the user checked remember */ ppp_stripe_nq = true; /* do not query next time run */ } /* */ } /* */
If the user picked “yes” the last time they were queried, we can continue on and retrieve handles to the edit window and edit objects. Using the edit object, we can get the current caret position to find where the cursor is, create an SGML object, and parse backwards until we hit a table tag or run out of elements to parse. Once we find a table, we need to get to its start position. Once there, we can store the end positions of our table tag for future reference as t_ex and t_ey. Next we can run our sgml_to_stripe_pos function to get the location in the table where our striping should begin. If it’s not blank, we found a currency symbol, so we can redo our table striping.
First we need to run clear_table_row_colors to set all table striping back to default values. Then we can get the X and Y positions that sgml_to_stripe_pos defined for us with the GetParameter function. Importantly, we must use the CloseHandle function on the SGML parser and on the edit object. If those handles are open, we cannot use the RunMenuFunction function because until Legato releases those handles there’s an open transaction still happening and a new one cannot be started. Once they are closed, we can use the SetCaretPosition function to set the location of the caret to the end of the currency symbol. The Table Stripe function can then pick this position up, and we use the RunMenuFunction function to actually call the table stripe function. It’s being used in NoQuery mode, so it will simply stripe starting from our cursor position with default options.
if (do_ppp_stripe != true){ /* if user doesn't want this function */ return; /* quit */ } /* */ edit_window = GetActiveEditWindow(); /* get the active edit window */ edit_object = GetEditObject(edit_window); /* get the active edit object */ c_x = GetCaretXPosition(edit_object); /* get caret x position */ c_y = GetCaretYPosition(edit_object); /* get caret y position */ sgml = SGMLCreate(edit_object); /* create SGML object */ element = SGMLPreviousElement(sgml, c_x, c_y); /* get the previous element */ while (element!="" && token != HT_TABLE){ /* while not at the table tag */ element = SGMLPreviousElement(sgml); /* get the previous element */ token = SGMLGetElementToken(sgml); /* get the token */ } /* */ t_ex = SGMLGetItemPosEX(sgml); /* end pos of table */ t_ey = SGMLGetItemPosEY(sgml); /* end pos of table */ stripe_pos = sgml_to_stripe_pos(sgml); /* get stripe start pos */ if (stripe_pos!=""){ /* get SGML parser striping pos */ clear_table_row_colors(sgml,edit_object,t_ex,t_ey); /* clear all background colors of rows */ c_x = GetParameter(stripe_pos,"sx"); /* get caret x position */ c_y = GetParameter(stripe_pos,"sy"); /* get caret y position */ CloseHandle(sgml); /* close the sgml parser */ CloseHandle(edit_object); /* close the handle */ SetCaretPosition(edit_window, c_x, c_y); /* set caret position */ CloseHandle(edit_window); /* close the handle */ RunMenuFunction("TABLE_STRIPING","NoQuery:true"); /* run the table stripe function */ } /* */ } /* */
The sgml_to_stripe_pos function is pretty simple. It returns a parameter string of the coordinates of the first currency symbol that it finds in the table that matches one of our defined symbols. If it can’t find any of the symbols, the function returns an empty string. The first thing this function does is use the ExplodeString function on our defined currency symbols to get an array of symbols against which we can check. Then our function gets the next item in the SGML parser. We use the SGMLNextItem function instead of the SGMLNextElement function as we did last week. This is because we’re looking for characters, not SGML tags. While our item isn’t blank and we haven’t hit the end of our table, we can get the token of our item and check if the type is a textual word or a character entity. If it’s either, we can use the FindInList function to see if the item we found is indeed a currency symbol. If so, we can return our parameter string containing the end positions of our currency symbol, which will become the start positions for the table striper to run on. Otherwise, we can keep on parsing until we hit the end of our table and return nothing.
/****************************************/ string sgml_to_stripe_pos(handle sgml){ /* move SGML to row to stripe on */ /****************************************/ string symbols[]; /* symbols to stop on */ string stripe_pos; /* striper position */ string item; /* an item from the parser */ dword token; /* the token of the curent parse pos */ /* */ symbols = ExplodeString(CURRENCY_SYMBOLS); /* explode symbols to array */ item = SGMLNextItem(sgml); /* get next item in parser */ while(item!="" && token!=HT__TABLE){ /* while not at end of table */ token = SGMLGetElementToken(sgml); /* get token of item */ if (SGMLGetItemType(sgml) == SPI_TYPE_TEXT || /* if item is text or */ SGMLGetItemType(sgml) == SPI_TYPE_CHAR){ /* if item is char entity */ if (FindInList(symbols, item)>(-1)){ /* if it's in the symbol list */ stripe_pos = FormatString("sx:%d;sy:%d", /* get position for striper */ SGMLGetItemPosEX(sgml), SGMLGetItemPosEY(sgml)); /* get position for striper */ return stripe_pos; /* end here */ } /* */ } /* */ item = SGMLNextItem(sgml); /* next */ } /* */ return ""; /* return false */ }
The clear_table_row_colors function does exactly what it sounds like: it iterates over every row of a table from the coordinates given to it and sets the background color to nothing. This function then can use the SGMLNextElement function to parse through instead of the SGMLNextItem function, because we really only care about table row tags. While we’re not at the end of our table then, we can use the SGMLGetElementToken function to check if we’re on a table row. If so, we can get the tag positions, use our CSS functions to get and modify the style. The WriteSegment function can write our changes back out. Finally we can use the SGMLSetPosition function to move the parser back to the start point.
/****************************************/ void clear_table_row_colors(handle sgml, handle edit_object, /* clear all table row backgrounds */ int s_sx, int s_sy){ /****************************************/ dword token; /* token of table item */ int sx, sy, ex, ey; /* points of table */ string element; /* element text */ string style[]; /* style of element */ /* */ element = SGMLNextElement(sgml,s_sx,s_sy); /* get next element */ while (element!="" && token!=HT__TABLE){ /* while we have another element */ token = SGMLGetElementToken(sgml); /* get token */ if (token==HT_TR){ /* if we're on a table row */ sx = SGMLGetItemPosSX(sgml); /* get positions of tag */ ex = SGMLGetItemPosEX(sgml); /* get positions of tag */ sy = SGMLGetItemPosSY(sgml); /* get positions of tag */ ey = SGMLGetItemPosEY(sgml); /* get positions of tag */ style = CSSGetProperties(sgml); /* get CSS properties */ style["background-color"] = ""; /* set background to blank */ CSSSetProperties(sgml,style); /* set style of tag */ element = SGMLToString(sgml); /* get text of current tag */ WriteSegment(edit_object,element,sx,sy,ex,ey); /* write out new row tag */ SGMLSetPosition(sgml,ex,ey); /* set parser position */ } /* */ element = SGMLNextElement(sgml); /* get next element */ } /* */ SGMLSetPosition(sgml,s_sx,s_sy); /* put it back where we found it */ } /* */
Legato is very useful for creating entirely new functions, but this script shows how it can also be useful to modify existing ones to do new things. Instead of reinventing the wheel, we can simply hook a post process script onto our existing polish table function and give it a brand new level of functionality. We could go further with this, of course, by adding a pre-process options menu that overrides the existing options and only presents the user with choices we want to give them. Then we could pass those settings to the actual polish function and have a completely customized polish table. This would be a significant amount of work though, because the UI for the table polish tool is pretty complex, but it definitely could be done if needed. This is just another example of how you can customize your GoFiler with Legato to get the best results possible.
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