I was discussing inline XBRL with a client the other day when a thought occurred to me. GoFiler uses XDX to mark information in a document to convert it to inline XBRL at a later time, which is generally great, but it means if you have a line item on a table that doesn’t actually have a label (like a total line, for example), then GoFiler is just going to pick the standard label for that element. That’s not a bad thing, in and of itself, but if you want to customize the label, there needs to be some text on the line item for XDX to pick up. Adding hidden text to the document is a great way to add “hints” for XDX, without actually changing the look of the exported document.
When making text hidden, we need to take special care that we don’t actually change what the output of our document looks like. To that effect, we cannot just make a single table cell or paragraph hidden, because that would alter the spacing of our document. Making a table cell hidden, for example, will just cause all the other cells to shift over to the left to make up for the gap of the hidden cell. Making a paragraph hidden will alter the spacing between other HTML blocks in our file. We can get around this by wrapping the contents of our HTML block with font tags, that just have the display CSS property set to none. This will allow our block to still take up the space it needs to take up, without showing the text inside of it. This means you could add a label to a table cell that otherwise has no label, and then use the script to hide the text. The resulting inline XBRL would look exactly like the original HTML document but the label used for the element would be your custom label rather than the standard label.
Our script this week is going to use the SGML parser in GoFiler to automatically add these tags to the document for us to make it a little easier.
#define HIDE_TAG "<FONT STYLE=\"display:none\">"
#define CLOSE_HIDE "</FONT>"
First, let’s take a look at our defined values. I’m using HIDE_TAG and CLOSE_HIDE as the names of my opening and closing tags for the selection of the document that will be hidden. These are FONT tags, with the display property set to none. FONT is a generally inoffensive and inert tag, that can be stacked, so having extra tags in the document shouldn’t cause any issues. FONT tags are not allowed by the EDGAR system within an inline XBRL document, but GoFiler compensates for that when converting HTML to inline XBRL, so there’s no issue there.
This script has the standard setup and main functions that most other scripts have, so we’re going to skip over them, and jump right into our run function, where the work actually happens.
int run(int f_id, string mode){
... variable declarations omitted ...
if(mode!="preprocess"){
return ERROR_NONE;
}
window = GetActiveEditWindow();
wType = GetEditWindowType(window) & EDX_TYPE_ID_MASK;
if (wType != EDX_TYPE_PSG_PAGE_VIEW){
MessageBox('x',"This function must be used in page view, current view is %0x",wType);
return ERROR_NONE;
}
edit_obj = GetEditObject(window);
Our run function begins like most other run functions.... we need to first check the run mode to ensure we’re not running in anything other than preprocess mode so our script only runs once. After we’ve checked that, we can get the active edit window with GetActiveEditWindow, and get the type of window with GetEditWindowType. We do this because we need to know the script is running in Page View. If it’s not, the function will return an error message and then return. Once we know we’re in a Page View window, we can get the edit object for the view, and continue.
sgml = SGMLCreate(edit_obj);
sx = GetCaretXPosition(edit_obj);
sy = GetCaretYPosition(edit_obj);
rc = SGMLSetPosition(sgml,sx,sy);
if (IsError(rc)){
MessageBox('x',"Cannot set SMGL parser to caret position.");
return ERROR_EXIT;
}
Next, we need to get a handle to our SGML parser object with SGMLCreate. There are other ways to parse through a file, but I find the SGML parser is probably the easiest way to do it. Once we have our parser, we can use the GetCaretXPosition and GetCaretYPosition functions to get our caret’s position in the document, and set our SGML parser to that position. It’s important to check that this function worked correctly, because, if for some reason our caret position is out of bounds (this shouldn’t happen), we want to know if our SGML parser failed to set its location correctly. If the SGML parser position is wrong, it could lead to the wrong text being highlighted, so erring on the side of caution here and checking the return value is a good idea.
tag = SGMLPreviousElement(sgml);
element = SGMLGetElementString(sgml);
while ((HTMLIsBlockElement(element)==false) && (tag!="")){
tag = SGMLPreviousElement(sgml);
element = SGMLGetElementString(sgml);
}
So far so good, we now have our document, all the relevant handles, and our SGML parser is created and in the right place. We can then use the SGMLPreviousElement function to grab the last element before our caret, so we can test to see if it’s a block element or not. The SGMLPreviousElement function just returns the full element opening tag, so we can then use SGMLGetElementString to get the element type as a string. This will return “P” for a paragraph tag, “TD” for a table cell, etc. After we have our element as a string, we can enter a while loop to iterate backwards until we hit a block element. This is an important step, because if we don’t do this, our we might end up breaking on an inline tag like a bold tag or something, and only hiding part of our text.
open = tag;
sx = SGMLGetItemPosSX(sgml);
sy = SGMLGetItemPosSY(sgml);
tag = SGMLFindClosingElement(sgml,SP_FCE_CODE_AS_IS);
ex = SGMLGetItemPosEX(sgml);
ey = SGMLGetItemPosEY(sgml);
close = FormatString("</%s>",element);
tag = open+HIDE_TAG+tag+CLOSE_HIDE+close;
WriteSegment(edit_obj,tag,sx,sy,ex,ey);
CloseHandle(sgml);
CloseHandle(window);
CloseHandle(edit_obj);
return ERROR_NONE;
}
After we’ve successfully moved our parser to the start of the block we want to hide, we can make a copy of that tag, get our start and end coordinates, build our new tag value, and write it out with WriteSegment. Finally, we can close our handles, and return.
This is a fairly simple bit of code, but it automates a task that would take 6-7 separate actions in GoFiler’s UI to do. The script took maybe an hour to write, so if it saves a minute each time it’s used, it will easily pay for itself in time savings. It’s always important to keep that in mind: how much time will a script save vs how much time will it take to write. There’s very little point in automating a task that’s done rarely, if the automation takes days (or longer) to write.
Here's the full script without commentary.
#define HIDE_TAG "<FONT STYLE=\"display:none\">"
#define CLOSE_HIDE "</FONT>"
void setup();
int run(int f_id, string mode, string file);
void main(){
setup();
}
void setup(){
string menu[];
string fn;
menu["Code"] = "HIDE_BLOCK_CONTENTS";
menu["MenuText"] = "Hide Block Contents";
menu["Description"] = "Hides the contents of any block tag.";
MenuAddFunction(menu);
fn = GetScriptFilename();
MenuSetHook(menu["Code"],fn,"run");
}
int run(int f_id, string mode){
int sx,sy,ex,ey;
int rc;
handle window;
handle edit_obj;
handle sgml;
dword wType;
string tag;
string element;
string open;
string close;
if(mode!="preprocess"){
return ERROR_NONE;
}
window = GetActiveEditWindow();
wType = GetEditWindowType(window) & EDX_TYPE_ID_MASK;
if (wType != EDX_TYPE_PSG_PAGE_VIEW){
MessageBox('x',"This function must be used in page view, current view is %0x",wType);
return ERROR_NONE;
}
edit_obj = GetEditObject(window);
sgml = SGMLCreate(edit_obj);
sx = GetCaretXPosition(edit_obj);
sy = GetCaretYPosition(edit_obj);
rc = SGMLSetPosition(sgml,sx,sy);
if (IsError(rc)){
MessageBox('x',"Cannot set SMGL parser to caret position.");
return ERROR_EXIT;
}
tag = SGMLPreviousElement(sgml);
element = SGMLGetElementString(sgml);
while ((HTMLIsBlockElement(element)==false) && (tag!="")){
tag = SGMLPreviousElement(sgml);
element = SGMLGetElementString(sgml);
}
open = tag;
sx = SGMLGetItemPosSX(sgml);
sy = SGMLGetItemPosSY(sgml);
tag = SGMLFindClosingElement(sgml,SP_FCE_CODE_AS_IS);
ex = SGMLGetItemPosEX(sgml);
ey = SGMLGetItemPosEY(sgml);
close = FormatString("</%s>",element);
tag = open+HIDE_TAG+tag+CLOSE_HIDE+close;
WriteSegment(edit_obj,tag,sx,sy,ex,ey);
CloseHandle(sgml);
CloseHandle(window);
CloseHandle(edit_obj);
return ERROR_NONE;
}
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
Novaworks’ Legato Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato