Last week, we explored editing SGML properties with Legato. By using some of the Legato SDK functions, we can read the properties of an SGML tag and change those properties to new values. This is pretty simple and straightforward. However, what if we want to do something based on a particular setting? For example, what if we want to employ an “Indent Body of Paragraph” function, where the first line of the paragraph doesn’t move but the subsequent lines are indented? We can’t simply read the indent value and add to it because we cannot guarantee that the same unit of measurement is being used. We need to read the unit, normalize it into a standard unit, add to it, convert it back to the original measurement style, and write it back out. It’s a bit more involved than simply setting properties, but it lets us perform some pretty interesting operations.
Friday, October 27. 2017
LDC #57: More CSS Property Editing With Legato
When we read our value from the SGML parser, it is going to come in as a PVALUE, type of a dword, which is a bitwise arrangement of a parameter type and parameter data. This allows us to store the the data type, as well as the data value, as a single dword instead of multiple variables. Legato also has the ability to translate these PVALUE variables into twips, which are a standardized unit of measurement. A twip is 1/20 of a point (pt) or 1/1440th of an inch (in). So in our case, we can get the measurement of an attribute as a PVALUE, convert it to a twip, increment it, convert it back, and set it into our document.
Let’s take a look at our example script, which indents the body of a paragraph using this method:
// // GoFiler Legato Script - Indent Second Line Text // -------------------------------------------------- // // Rev 10/16/2017 // // (c) 2017 Novaworks, LLC -- All rights reserved. // // Will examine active HTML window to indent the second line of it 20pt (400 twips) // // Notes: // // - Requires GoFiler 4.20a or later. #define INDENT_TWIPS 400 void run_indent (int f_id, string mode); void run_outdent (int f_id, string mode); void indent_para (int f_id, string mode, int weight); string do_indent (handle sgml, int weight); /****************************************/ 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_INDENT_BODY"; /* Function Code */ item["MenuText"] = "&Indent Paragraph Body"; /* Menu Text */ item["Description"] = "<B>Indent Paragraph Body</B>\r\rIndent the"; /* Description (long) */ item["Description"]+= " paragraph body."; /* * description */ item["Class"] = "DocumentExtension"; /* add to document toolbar */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuAddFunction(item); /* add menu function */ MenuSetHook(item["Code"], fnScript, "run_indent"); /* Set the Test Hook */ /* * Outdent */ item["Code"] = "EXTENSION_OUTDENT_BODY"; /* Function Code */ item["MenuText"] = "&Outdent Paragraph Body"; /* Menu Text */ item["Description"] = "<B>Outdent Paragraph Body</B>\r\rOutdent"; /* Description (long) */ item["Description"]+= " the paragraph body."; /* * description */ item["Class"] = "DocumentExtension"; /* add to document toolbar */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuAddFunction(item); /* add menu function */ MenuSetHook(item["Code"], fnScript, "run_outdent"); /* Set the Test Hook */ QuickKeyRegister("Page View","3_KEY_CONTROL", /* set hotkey to ctrl 2 */ "EXTENSION_INDENT_BODY"); /* set hotkey to ctrl 2 */ QuickKeyRegister("Page View","2_KEY_CONTROL", /* set hotkey to ctrl 2 */ "EXTENSION_OUTDENT_BODY"); /* set hotkey to ctrl 2 */ return ERROR_NONE; /* Return value (does not matter) */ } /* 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_outdent(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ if (mode!="preprocess"){ return; } indent_para(f_id, mode, (-1)); /* outdent the paragraph */ } /* */ /****************************************/ void run_indent(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ if (mode!="preprocess"){ return; } indent_para(f_id, mode, 1); /* indent the paragraph */ } /* */ /****************************************/ void indent_para(int f_id, string mode, int weight){ /* indent the paragraph */ /****************************************/ int ex,ey,sx,sy; /* positional variables */ string token; /* token of current element */ 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 */ token = SGMLGetElementString(sgml); /* get token of current element */ if (token=="P"){ /* check if the tag is Paragraph */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ WriteSegment(edit_object,do_indent(sgml,weight),sx,sy,ex,ey); /* write new text */ break; /* end the loop */ } /* */ element = SGMLPreviousElement(sgml); /* get the next sgml element */ } /* */ } /* end setup */ /****************************************/ string do_indent(handle sgml, int weight){ /* removes all spacing from the item */ /****************************************/ dword first_line; /* first line indent */ dword margin_left; /* left padding */ int first_line_t; /* first line indent in twips */ int margin_left_t; /* second line indent in twips */ string properties[]; /* properties to put into string */ /* */ first_line = SGMLGetParameterValue(sgml,"text-indent"); /* get first line property */ if (IsError(GetLastError()) || first_line==0){ /* if we have an error */ first_line = PT_PT; /* set to zero point */ } /* */ margin_left = SGMLGetParameterValue(sgml, "margin-left"); /* get margin left */ if (IsError(GetLastError()) || margin_left==0){ /* if we have an error */ margin_left = PT_PT; /* set to zero point */ } /* */ first_line_t = SGMLValueToTWIPS(first_line); /* get first line as twips */ first_line_t = first_line_t - (weight * INDENT_TWIPS); /* indent by 20 pt */ margin_left_t = SGMLValueToTWIPS(margin_left); /* */ margin_left_t = margin_left_t + (weight * INDENT_TWIPS); /* indent by 20 pt */ /* */ properties = SGMLGetParameters(sgml); /* get props of current item */ margin_left = SGMLTWIPSToValue(margin_left_t,margin_left,true); /* set margin left */ if (IsError(GetLastError())){ /* if we have an error */ margin_left = PT_PT; /* set to zero point */ } /* */ first_line = SGMLTWIPSToValue(first_line_t,first_line,true); /* set first line indent */ if (IsError(GetLastError())){ /* if we have an error */ first_line = PT_PT; /* set to zero point */ } /* */ properties["margin-left"] = SGMLValueToString(margin_left,sgml); /* set value in props array */ properties["text-indent"] = SGMLValueToString(first_line,sgml); /* set value in props array */ /* */ CSSSetProperties(sgml,properties); /* set properties in SGML parser */ return SGMLToString(sgml); /* return modified element tag */ } /* */
Our setup function is pretty basic. We’re adding two menu functions here: “Indent Paragraph Body” and “Outdent Paragraph Body”. The indent routine increments the body of the paragraph by 400 twips (or 20 pt). The outdent function moves the paragraph back by the same amount. These functions are also added to the Quick Keys in GoFiler by using the QuickKeyRegister function as hotkeys CTRL+2 and CTRL+3, respectively.
/****************************************/ 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_INDENT_BODY"; /* Function Code */ item["MenuText"] = "&Indent Paragraph Body"; /* Menu Text */ item["Description"] = "<B>Indent Paragraph Body</B>\r\rIndent the"; /* Description (long) */ item["Description"]+= " paragraph body."; /* * description */ item["Class"] = "DocumentExtension"; /* add to document toolbar */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuAddFunction(item); /* add menu function */ MenuSetHook(item["Code"], fnScript, "run_indent"); /* Set the Test Hook */ /* * Outdent */ item["Code"] = "EXTENSION_OUTDENT_BODY"; /* Function Code */ item["MenuText"] = "&Outdent Paragraph Body"; /* Menu Text */ item["Description"] = "<B>Outdent Paragraph Body</B>\r\rOutdent"; /* Description (long) */ item["Description"]+= " the paragraph body."; /* * description */ item["Class"] = "DocumentExtension"; /* add to document toolbar */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuAddFunction(item); /* add menu function */ MenuSetHook(item["Code"], fnScript, "run_outdent"); /* Set the Test Hook */ QuickKeyRegister("Page View","3_KEY_CONTROL", /* set hotkey to ctrl 2 */ "EXTENSION_INDENT_BODY"); /* set hotkey to ctrl 2 */ QuickKeyRegister("Page View","2_KEY_CONTROL", /* set hotkey to ctrl 2 */ "EXTENSION_OUTDENT_BODY"); /* set hotkey to ctrl 2 */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */
The run_outdent and run_indent functions are hooked into the menu by the setup function. They both trigger the indent_para function, setting the weight parameter to 1 or -1. This specifies the direction the indent is going. If it’s a -1, the indent_para function outdents it. If it’s 1, the function indents instead.
/****************************************/ void run_outdent(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ if (mode!="preprocess"){ return; } indent_para(f_id, mode, (-1)); /* outdent the paragraph */ } /* */ /****************************************/ void run_indent(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ if (mode!="preprocess"){ return; } indent_para(f_id, mode, 1); /* indent the paragraph */ } /* */
The indent_para function is where we actually parse the document. This function is very similar to the run function from last week. It gets the active edit window and checks to make sure it’s actually an edit window. Then it gets the Edit Object, creates an SGML Object, and acquires the X and Y position of the cursor. Using the SGMLGetPreviousElement function, it obtains the element before the cursor, and then while we have a previous element we examine the tag to see if we need to do anything. Unlike last week, this time we’ll use the SGMLGetEelementString function to get a string value to represent the type of tag we’re looking at. If it returns a “P”, then that means we have a paragraph and we can proceed to get our start and end coordinates. After we use the WriteSegment function to write out our new tag, we break out of the loop.
/****************************************/ void indent_para(int f_id, string mode, int weight){ /* indent the paragraph */ /****************************************/ int ex,ey,sx,sy; /* positional variables */ string token; /* token of current element */ 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 */ token = SGMLGetElementString(sgml); /* get token of current element */ if (token=="P"){ /* check if the tag is Paragraph */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ WriteSegment(edit_object,do_indent(sgml,weight),sx,sy,ex,ey); /* write new text */ break; /* end the loop */ } /* */ element = SGMLPreviousElement(sgml); /* get the next sgml element */ } /* */ } /* end setup */
The do_indent function is what handles reading the tag and changing the indent based on the given weight variable. First, we need to get the PVALUEs for our first_line and margin_left variables. If the SGMLGetParameterValue function returns an error or it retrieves a zero, we can set it to PT_PT, which is a PVALUE that equals 0 pt. Then the SGMLValueToTWIPS function converts the PVALUE to a twip value. The math is pretty simple for this: the first line needs to have the weight * INDENT_TWIPS (which is 400, or 20 pt) subtracted from it, while the margin left needs to have the weight * INDENT_TWIPS added to it. This makes it look like the first line isn’t changing, while the body of the paragraph is indenting or outdenting depending on the weight given. After we’ve done our math to figure out the twip value for our parameters, we need to convert it back to a PVALUE with the SGMLTWIPSToValue function. If that returns an error, we can again simply set it to PT_PT, or 0 pt. Then we can convert those values back to string values with the SGMLValueToString function, set them into our array of properties, and use the CSSSetProperties function to put it back into our element. Finally, we use the SGMLToString function to return our new element as a string so the WriteSegment function in the indent_para function can write it back out to our document.
/****************************************/ string do_indent(handle sgml, int weight){ /* removes all spacing from the item */ /****************************************/ dword first_line; /* first line indent */ dword margin_left; /* left padding */ int first_line_t; /* first line indent in twips */ int margin_left_t; /* second line indent in twips */ string properties[]; /* properties to put into string */ /* */ first_line = SGMLGetParameterValue(sgml,"text-indent"); /* get first line property */ if (IsError(GetLastError()) || first_line==0){ /* if we have an error */ first_line = PT_PT; /* set to zero point */ } /* */ margin_left = SGMLGetParameterValue(sgml, "margin-left"); /* get margin left */ if (IsError(GetLastError()) || margin_left==0){ /* if we have an error */ margin_left = PT_PT; /* set to zero point */ } /* */ first_line_t = SGMLValueToTWIPS(first_line); /* get first line as twips */ first_line_t = first_line_t - (weight * INDENT_TWIPS); /* indent by 20 pt */ margin_left_t = SGMLValueToTWIPS(margin_left); /* */ margin_left_t = margin_left_t + (weight * INDENT_TWIPS); /* indent by 20 pt */ /* */ properties = SGMLGetParameters(sgml); /* get props of current item */ margin_left = SGMLTWIPSToValue(margin_left_t,margin_left,true); /* set margin left */ if (IsError(GetLastError())){ /* if we have an error */ margin_left = PT_PT; /* set to zero point */ } /* */ first_line = SGMLTWIPSToValue(first_line_t,first_line,true); /* set first line indent */ if (IsError(GetLastError())){ /* if we have an error */ first_line = PT_PT; /* set to zero point */ } /* */ properties["margin-left"] = SGMLValueToString(margin_left,sgml); /* set value in props array */ properties["text-indent"] = SGMLValueToString(first_line,sgml); /* set value in props array */ /* */ CSSSetProperties(sgml,properties); /* set properties in SGML parser */ return SGMLToString(sgml); /* return modified element tag */ } /* */
This is just another example of how Legato can be used to modify HTML tags. This one is a bit more complicated but still fairly simple overall. Using Legato to modify CSS properties, you can automate a lot of annoying manual work inside a document that could otherwise take a lot of time. Since Legato can directly interpret the CSS it is easy to deal with the multitude of units and coding standards CSS allows.
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