This week we are going to take a look at another client request to be able to remove multiple hyperlinks at the same time. In GoFiler there is a Remove Hyperlink function that will find where the caret currently is and if it is within a hyperlink it will remove the link. If the caret is located before or within an anchor/bookmark tag, the function will remove the anchor instead. However, if you have any text selected the tool gives the user a popup box and does not remove anything. So today we’re going to look at a simple script to hook into the Remove Hyperlink function, check if the user has text highlighted, and if so, remove all hyperlinks and bookmarks that we find in the highlighted section.
Friday, February 02. 2018
LDC #70: Removing All Selected Hyperlinks
The largest difference between this and previous scripts that we’ve written in this blog is that this script extends functionality that already exists in GoFiler. As such, we’re hooking our script into a menu function that already exists, rather than adding a new menu function in a Tools dropdown. We’ll be using Legato’s SGML parsing capabilities to extend our search area to make sure that we get link tags that aren’t fully selected and then again to parse through and remove the tags.
Without further ado, let’s hop into the code.
int run_delete (int f_id, string mode); /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ MenuSetHook("DOCUMENT_HYPERLINK_REMOVE", /* Set the Test Hook */ GetScriptFilename(), "run_delete"); /* * con't */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ void main(){ /* main */ /****************************************/ setup(); /* run setup */ } /* */ /****************************************/ int run_delete(int f_id, string mode){ /* promote underlines to table cells */ /****************************************/ int ex,ey,sx,sy; /* positional variables */ int s_mode; /* selection mode */ int x, y; /* current position */ string startpos, endpos; /* position to start parsing from */ boolean searching; /* currently searching */ dword token; /* token of current element */ string element; /* sgml element */ handle sgml; /* sgml object */ handle edit_object; /* edit object */ handle edit_window; /* edit window */ /* */ if (mode!="preprocess"){ /* if not running preprocess */ return ERROR_NONE; /* quit */ } /* */ 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_NONE; /* return */ } /* */ edit_object = GetEditObject(edit_window); /* create edit object */ s_mode = GetSelectMode(edit_object); /* get selection mode of object */ if (s_mode == EDO_LINEAR_SELECT){ /* if we have a selection */ sx = GetSelectStartXPosition(edit_window); /* get the selection start */ sy = GetSelectStartYPosition(edit_window); /* get the selection start */ ex = GetSelectEndXPosition(edit_window); /* get the selection end */ ey = GetSelectEndYPosition(edit_window); /* get the selection end */ } /* */ else{ /* otherwise */ return ERROR_NONE; /* continue onward */ } /* */ if (sx == -1 || sy == -1){ /* if sx or sy is an error */ MessageBox('x',"Cannot get selection position."); /* display error */ return ERROR_NONE; /* continue onward */ } /* */ sgml = SGMLCreate(edit_object); /* create sgml object */ SGMLSetPosition(sgml, ex, ey); /* go to the end */ searching = true; /* searching */ while (searching) { /* while still searching */ element = SGMLNextElement(sgml); /* go forwards */ token = SGMLGetElementToken(sgml); /* get token of current element */ if (HTMLIsBlockElement(sgml) || token == HT_A) { /* if end of block or start of link */ element = SGMLPreviousElement(sgml); /* go backwards */ searching = false; /* reached the end */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ } /* done */ if (token == HT__A) { /* if end of link */ searching = false; /* done searching */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ } /* */ if (element == "") { /* if EOD */ searching = false; /* done searching and don't set new pos */ } /* */ } /* */ SGMLSetPosition(sgml, sx, sy); /* Go to the beginning */ element = SGMLNextItem(sgml); /* Prime the pump */ searching = true; /* searching */ while (searching) { /* while still searching */ element = SGMLPreviousElement(sgml); /* go backwards */ token = SGMLGetElementToken(sgml); /* get token of current element */ if (HTMLIsBlockElement(sgml) || token == HT__A) { /* if end of block or end of link */ element = SGMLNextElement(sgml); /* go forwards */ searching = false; /* done searching */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ } /* */ if (token == HT_A) { /* if start of link */ searching = false; /* done searching */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ } /* */ if (element == "") { /* if end of file */ searching = false; /* done searching */ } /* */ } /* */ SGMLSetDataRange(sgml, sx, sy, ex, ey); /* set the boundaries */ element = SGMLNextElement(sgml); /* get the first sgml element */ while(element != ""){ /* while element isn't empty */ token = SGMLGetElementToken(sgml); /* get token of current element */ if (token == HT_A || token == HT__A){ /* if we've got an A tag */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ WriteSegment(edit_object,"",sx,sy,ex,ey); /* remove U tag */ SGMLSetPosition(sgml,sx,sy); /* reset parser to where tag used to be */ } /* */ element = SGMLNextElement(sgml); /* get the next sgml element */ } /* */ return ERROR_EXIT; /* upon finishing stop menu from firing */ } /* */
This script can be split up into three distinct sections: setting up, figuring out our search area, and search to destroy.
int run_delete (int f_id, string mode); /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ MenuSetHook("DOCUMENT_HYPERLINK_REMOVE", /* Set the Test Hook */ GetScriptFilename(), "run_delete"); /* * con't */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ void main(){ /* main */ /****************************************/ setup(); /* run setup */ } /* */
Our first half of the setup code looks very familiar to those who have been reading the blog for a some time. We have a setup function that sets our hook into the menu. We are hooking to the DOCUMENT_HYPERLINK_REMOVE function, which is the button on the bottom of the Hyperlinks section on the Document ribbon, and we are telling the script to run the run_delete function that we have defined above. The main function, which is called when we run the script from the IDE, just calls the setup function. The second half of the setup code is the beginning of our run_delete function
/****************************************/ int run_delete(int f_id, string mode){ /* promote underlines to table cells */ /****************************************/ int ex,ey,sx,sy; /* positional variables */ int s_mode; /* selection mode */ boolean searching; /* currently searching */ dword token; /* token of current element */ string element; /* sgml element */ handle sgml; /* sgml object */ handle edit_object; /* edit object */ handle edit_window; /* edit window */ /* */ if (mode!="preprocess"){ /* if not running preprocess */ return ERROR_NONE; /* quit */ } /* */ 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_NONE; /* return */ } /* */ edit_object = GetEditObject(edit_window); /* create edit object */ s_mode = GetSelectMode(edit_object); /* get selection mode of object */ if (s_mode == EDO_LINEAR_SELECT){ /* if we have a selection */ sx = GetSelectStartXPosition(edit_window); /* get the selection start */ sy = GetSelectStartYPosition(edit_window); /* get the selection start */ ex = GetSelectEndXPosition(edit_window); /* get the selection end */ ey = GetSelectEndYPosition(edit_window); /* get the selection end */ } /* */ else{ /* otherwise */ return ERROR_NONE; /* continue onward */ } /* */ if (sx == -1 || sy == -1){ /* if sx or sy is an error */ MessageBox('x',"Cannot get selection position."); /* display error */ return ERROR_NONE; /* continue onward */ } /* */ sgml = SGMLCreate(edit_object); /* create sgml object */
There are a lot of variables in this one function, as this is the only function we use in this script. We keep track of the beginning and ending positions for our parse, whether we are currently searching forward or backward, what our current SGML token and element is, and some handles for the SGML parser, edit object, and edit window that are being used. The first thing we check is whether we are running as a preprocess. If we are not, we want to return back to GoFiler with an ERROR_NONE. You will notice that we are going to use ERROR_NONE to return to the program when we want GoFiler to continue running the function afterwards, but will use ERROR_EXIT if our function finishes properly. While this may seem counter-intuitive, the reasoning here is that GoFiler is watching our return code to see whether or not it should run the menu function that is actually associated with the button, DOCUMENT_HYPERLINK_REMOVE. If we say there is no error, GoFiler will run the menu function. If we tell GoFiler to exit, the menu function will not be run and we will have successfully overwritten GoFiler’s functionality.
After we check if we are running at the right time we check to see if the function is being run in an Edit Window object. This should never not be the case because of the way GoFiler disables functions, but I’ve put the check in just to be absolutely sure. Next, we get the current edit object that is in the current edit window, and then we get the select mode of the edit object. We want to make sure that the user currently has just text selected. If there is no selection or a different select mode is being used we will pass back to GoFiler to handle the function. No need to do extra work when GoFiler will do the work for us already.
Finally to end the setup, we get the starting and ending positions of the text that is selected and make sure that the selection is valid and that nothing went wrong during setup. We get the start and end X and Y positions from the current edit window and store them in the variables we made to keep track of the outer boundary of our parse area. We check to make sure that there’s not an error in getting the boundaries by making sure that the starting values aren’t -1. Finally we create an SGML parser using our edit object.
Now we are ready to start the real work. The next step is to figure out the boundaries of our search area. We want to trust our user to have all of each hyperlink they want deleted in the selection, but we can’t. Instead, we have to increase our search area to make sure that we are getting rid of both ends of a hyperlink inside which our selection could potentially start or end. We’re going to go forward from the end until we reach a boundary or a link tag, and then modify our end to that point. We can then repeat that process in reverse to find where to begin. Let’s take a look at the code to go forwards from the end:
SGMLSetPosition(sgml, ex, ey); /* go to the end */ searching = true; /* searching */ while (searching) { /* while still searching */ element = SGMLNextElement(sgml); /* go forwards */ token = SGMLGetElementToken(sgml); /* get token of current element */ if (HTMLIsBlockElement(sgml) || token == HT_A) { /* if end of block or start of link */ element = SGMLPreviousElement(sgml); /* go backwards */ searching = false; /* reached the end */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ } /* done */ if (token == HT__A) { /* if end of link */ searching = false; /* done searching */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ } /* */ if (element == "") { /* if EOD */ searching = false; /* done searching and don't set new pos */ } /* */ } /* */
The first thing we do is set our parser position to the end of the user’s selected area. We then set our searching boolean to true. Then a while loop runs until searching is no longer true. Each time we go through the loop we get the next element from the parser, and then get the token for that element. If the current element is a block element (ADDRESS, BLOCKQUOTE, CENTER, DIV, DD, DT, H1, H2, H3, H4, H5, H6, LI, P, PRE, CAPTION, TD, TH) or an open link tag we want to not include that in our search area, so we back up an element and set the ending X and Y to the previous element’s end. If the current element is a closing link tag we want to include that because that means that the opening tag is included in our search area (either within the selected area or we’ll find it upon working backwards from the beginning of the selection), so we set the end position to the end of the link tag. Finally, if we hit the end of a document without either of the other conditions being true we don’t change the end of the search area. This condition should be rarely met, but should be there to prevent an infinite loop just in case. We then move to the front and find the starting point.
SGMLSetPosition(sgml, sx, sy); /* Go to the beginning */ element = SGMLNextItem(sgml); /* Prime the pump */ searching = true; /* searching */ while (searching) { /* while still searching */ element = SGMLPreviousElement(sgml); /* go backwards */ token = SGMLGetElementToken(sgml); /* get token of current element */ if (HTMLIsBlockElement(sgml) || token == HT__A) { /* if end of block or end of link */ element = SGMLNextElement(sgml); /* go forwards */ searching = false; /* done searching */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ } /* */ if (token == HT_A) { /* if start of link */ searching = false; /* done searching */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ } /* */ if (element == "") { /* if end of file */ searching = false; /* done searching */ } /* */ } /* */
You’ll notice this code looks remarkably similar to the code at the end, but everything is reversed. We don’t want to include end tags or block elements at the beginning, but do want to include open link tags.
Now we move to the final third of our script: the search and destroy portion. Here’s the code:
SGMLSetDataRange(sgml, sx, sy, ex, ey); /* set the boundaries */ element = SGMLNextElement(sgml); /* get the first sgml element */ while(element != ""){ /* while element isn't empty */ token = SGMLGetElementToken(sgml); /* get token of current element */ if (token == HT_A || token == HT__A){ /* if we've got an A tag */ sx = SGMLGetItemPosSX(sgml); /* get sx */ sy = SGMLGetItemPosSY(sgml); /* get sy */ ex = SGMLGetItemPosEX(sgml); /* get ex */ ey = SGMLGetItemPosEY(sgml); /* get ey */ WriteSegment(edit_object,"",sx,sy,ex,ey); /* remove U tag */ SGMLSetPosition(sgml,sx,sy); /* reset parser to where tag used to be */ } /* */ element = SGMLNextElement(sgml); /* get the next sgml element */ } /* */ return ERROR_EXIT; /* upon finishing stop menu from firing */ } /* */
We restrict our search area to the coordinates that we just discovered in the code above using the SGMLSetDataRange function. This function moves the parser to the beginning of the range, and as soon as it moves out of that range it will return a blank string, which allows us to easily exit our search loop. We then enter our loop, getting the token from our SGML parser. If the token is an <A> or </A> tag, we want to get the start and end positions of the tag itself within the file, and then replace the area between the start and end with nothing. We then reset the position of the SGML parser to the beginning of the tag, and then continue onward. Our loop continues getting the next element until we no longer have an element to get, at which point we exit the loop and exit the function. Remember that here we use ERROR_EXIT as our return code in order to stop GoFiler from running the menu function after our function.
This script shows the power of using Legato to extend functionality that already exists within GoFiler. You can hook a script to any menu function, causing your script to take precedence over the existing functionality. Want to validate a document every time that you refresh the display? You can do that. Stop a user from filing until they validate the project first? You can do that too. The only limit is your imagination.
Joshua Kwiatkowski is a developer at Novaworks, primarily working on Novaworks’ cloud-based solution, GoFiler Online. He is a graduate of the Rochester Institute of Technology with a Bachelor of Science degree in Game Design and Development. He has been with the company since 2013. |
Additional Resources