In past blog posts, we’ve used Legato to remove HTML tags or to add new ones. One function we haven’t really explored is looking at a tag and changing its properties. Editing CSS properties with Legato is extremely powerful because it lets us change the document’s appearance in clean, interesting, and intelligent ways. This week’s script is a very basic but very powerful example of this concept.
Friday, October 20. 2017
LDC #56: Editing CSS Properties With Legato
We’ll begin by looking for the nearest block tag around our current cursor position and removing the padding and margins from it, thereby ensuring the text isn’t indented. This is a snap using the functions CSSGetProperties and CSSSetProperties. These functions retrieve an array of properties, and with that we can change what we want and set them back into the element without having to worry about parsing the CSS attributes ourselves. This is a huge time saver, as writing a CSS parser would be an extremely involved affair and take a very long time.
This week’s script example:
// // GoFiler Legato Script - Remove Margins And Padding // -------------------------------------------------- // // Rev 10/16/2017 // // (c) 2017 Novaworks, LLC -- All rights reserved. // // Will examine active HTML window to remove margins and padding // // Notes: // // - Requires GoFiler 4.20a or later. void run (int f_id, string mode);/* Call from Hook Processor */ boolean is_block (string item); /* true if item is a block tag */ string remove_spacing (handle sgml); /* modify the current element */ /****************************************/ void setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ /* ** Add Menu Item */ /* * Define Function */ item["Code"] = "EXTENSION_REMOVE_MARGINS"; /* Function Code */ item["MenuText"] = "&Remove All Margins/Padding"; /* Menu Text */ item["Description"] = "<B>Remove Margins And Padding</B>\r\rRemove";/* Description (long) */ item["Description"]+= " all margins and padding on block items."; /* * description */ item["Class"] = "DocumentExtension"; /* add to document toolbar */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuAddFunction(item); /* add function to menu */ MenuSetHook(item["Code"], fnScript, "run"); /* Set the Test Hook */ QuickKeyRegister("Page View","1_KEY_CONTROL", /* set hotkey to ctrl 1 */ "EXTENSION_REMOVE_MARGINS"); /* set hotkey to ctrl 1 */ } /* end setup */ /****************************************/ void main() { /* Initialize from Hook Processor */ /****************************************/ if (GetScriptParent()=="LegatoIDE"){ /* if not running in IDE */ setup(); /* Add to the menu */ } /* */ } /* end setup */ /****************************************/ void run(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ int ex,ey,sx,sy; /* positional variables */ string element; /* sgml element */ handle sgml; /* sgml object */ handle edit_object; /* edit object */ handle edit_window; /* edit window handle */ /* */ 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; /* return */ } /* */ edit_object = GetEditObject(edit_window); /* create edit object */ sgml = SGMLCreate(edit_object); /* create sgml object */ sx = GetCaretXPosition(edit_object); /* get cursor x pos */ sy = GetCaretYPosition(edit_object); /* get cursor y pos */ element = SGMLPreviousElement(sgml,sx,sy); /* get the first sgml element */ while(element != ""){ /* while element isn't empty */ if (is_block(element)){ /* check if the tag is blcok */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ WriteSegment(edit_object,remove_spacing(sgml),sx,sy,ex,ey); /* write new text */ break; /* end the loop */ } /* */ element = SGMLPreviousElement(sgml); /* get the next sgml element */ } /* */ } /* end setup */ /****************************************/ string remove_spacing(handle sgml){ /* removes all spacing from the itme */ /****************************************/ string properties[]; /* properties to put into string */ /* */ properties = CSSGetProperties(sgml); /* get current properties of element */ properties["margin"] = "0"; /* set margin to zero */ properties["padding"] = "0"; /* set margin to zero */ properties["text-indent"] = ""; /* remove text indent */ CSSSetProperties(sgml,properties); /* set properties in SGML parser */ return SGMLToString(sgml); /* return modified element tag */ } /* */ /****************************************/ boolean is_block(string item){ /* true if item is a block tag */ /****************************************/ if (FindInString(item,"<p")==0){ /* if it's a paragraph */ return true; /* return true */ } /* */ if (FindInString(item,"<div")==0){ /* if it's a division */ return true; /* return true */ } /* */ if (FindInString(item,"<td")==0){ /* if it's a table cell */ return true; /* return true */ } /* */ if (FindInString(item,"<th")==0){ /* if it's a table header */ return true; /* return true */ } /* */ return false; /* return false */ }
Let’s begin with the setup function. This is pretty standard, and we’ve covered almost everything here in depth in other blog posts. The menu item’s class is set to “DocumentExtension”, so it shows up on the document toolbar, which is only available in HTML files. The one new item is the QuickKeyRegister function call. This registers our function to the hotkey CTRL+1. Doing this overrides the existing hotkey, which is a hang table function, with our new function. You can use the QuickKeyRegister function to set custom hotkeys for any function that is defined in the specific view you’re working in. CTRL+1 is a specific hotkey for page view, so we can override it. Conversely, CTRL+O is the generic open file function, which isn’t specific to page view, so it cannot be overridden.
/****************************************/ void setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ /* ** Add Menu Item */ /* * Define Function */ item["Code"] = "EXTENSION_REMOVE_MARGINS"; /* Function Code */ item["MenuText"] = "&Remove All Margins/Padding"; /* Menu Text */ item["Description"] = "<B>Remove Margins And Padding</B>\r\rRemove";/* Description (long) */ item["Description"]+= " all margins and padding on block items."; /* * description */ item["Class"] = "DocumentExtension"; /* add to document toolbar */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuAddFunction(item); /* add function to menu */ MenuSetHook(item["Code"], fnScript, "run"); /* Set the Test Hook */ QuickKeyRegister("Page View","1_KEY_CONTROL", /* set hotkey to ctrl 1 */ "EXTENSION_REMOVE_MARGINS"); /* set hotkey to ctrl 1 */ } /* end setup */
Our main function here is pretty sparse. It simply checks if the script is running in the IDE, and if so, it runs setup.
/****************************************/ void main() { /* Initialize from Hook Processor */ /****************************************/ if (GetScriptParent()=="LegatoIDE"){ /* if not running in IDE */ setup(); /* Add to the menu */ } /* */ } /* end setup */
The run function is called from the menu hook. It uses the GetActiveEditWindow function to get the current window. If it was unable to get the window, it displays a message and exits. Otherwise, it gets the Edit Object associated with that window with the GetEditObject function, creates an SGML parser with the SGMLCreate function, retrieves the cursor positions for the current Edit Object, and uses the SGMLGetPreviousElement function to get the last HTML tag.
/****************************************/ void run(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ int ex,ey,sx,sy; /* positional variables */ 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 */ /* */ 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; /* return */ } /* */ edit_object = GetEditObject(edit_window); /* create edit object */ sgml = SGMLCreate(edit_object); /* create sgml object */ sx = GetCaretXPosition(edit_object); /* get cursor x pos */ sy = GetCaretYPosition(edit_object); /* get cursor y pos */ element = SGMLPreviousElement(sgml,sx,sy); /* get the first sgml element */
While we have an element to analyze, we can check the element to see if it’s a block element with the function is_block . The only tags we’re interested are block tags, like paragraphs, divisions, table cells, and table headers. There are others, but those just listed are the common ones used in EDGAR filings. If our element is not a block tag, we can get the previous tag, and check again. If this one is a block tag, we can obtain the start and end positions of it and use the WriteSegment function to write out a copy of the tag, modified by the function remove_spacing.
while(element != ""){ /* while element isn't empty */ if (is_block(element)){ /* check if the tag is blcok */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ WriteSegment(edit_object,remove_spacing(sgml),sx,sy,ex,ey); /* write new text */ break; /* end the loop */ } /* */ element = SGMLPreviousElement(sgml); /* get the next sgml element */ } /* */ } /* end setup */
The remove_spacing function performs the actual modifications of our CSS properties. We can use the SDK function CSSGetProperties to get an array of the CSS properties applying to this tag. Each item in the array is identified by a key:value pair, with the key being the CSS property name and the value being the value of the property. Our goal here is to set the margin, padding, and text-indent and them to zero, so we can set those properties into our array, and then use CSSSetProperties to change the SGML object’s element. After the element is changed, the SGMLToString function gets a string value for the current element that we just modified and returns it, so our run function can write it out with the WriteSegment function.
/****************************************/ string remove_spacing(handle sgml){ /* removes all spacing from the itme */ /****************************************/ string properties[]; /* properties to put into string */ /* */ properties = CSSGetProperties(sgml); /* get current properties of element */ properties["margin"] = "0"; /* set margin to zero */ properties["padding"] = "0"; /* set margin to zero */ properties["text-indent"] = ""; /* remove text indent */ CSSSetProperties(sgml,properties); /* set properties in SGML parser */ return SGMLToString(sgml); /* return modified element tag */ } /* */
Our is_block function is very simple. Its purpose is to examine the SGML tag it’s given, and if the tag begins with something that looks like a block tag, the function returns true. Otherwise, it returns false. In later versions of GoFiler, this will likely be replaced with a Legato SDK function , so there will be no need for the user to define something like this.
/****************************************/ boolean is_block(string item){ /* true if item is a block tag */ /****************************************/ if (FindInString(item,"<p")==0){ /* if it's a paragraph */ return true; /* return true */ } /* */ if (FindInString(item,"<div")==0){ /* if it's a division */ return true; /* return true */ } /* */ if (FindInString(item,"<td")==0){ /* if it's a table cell */ return true; /* return true */ } /* */ if (FindInString(item,"<th")==0){ /* if it's a table header */ return true; /* return true */ } /* */ return false; /* return false */ }
This simple script is a good example of how to use Legato to edit CSS properties in HTML objects. This could easily be expanded to do a lot more than edit margins. It could be used to store paragraph or cell style presets, for example, that would increase the speed of formatting text. It could also be modified to work on multiple selection types, such as highlighting a table row and removing all the indents or padding on the entire row instead of one cell at a time. It could be used to search through a document and find any tables or paragraphs that are aligned center,and replace them with “margin auto” for inline XBRL compliance. The possibilities are really endless, and this is just scratching the surface.
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