1

I hope everyone is in good health. This post is my continue of my previous post

My main goal

So main goal was to get the hyperlink and change it the text linked with it. I initially used code from this post and modified it to change the text of first hyperlink. Here is my modified code to change the text of first hyperlink.

function onOpen() {
  const ui = DocumentApp.getUi();
  ui.createMenu('What to do?')
    .addItem('HyperLink Modifier', 'findAndReplacetext')
    .addToUi();
}


/**
 * Get an array of all LinkUrls in the document. The function is
 * recursive, and if no element is provided, it will default to
 * the active document's Body element.
 *
 * @param    element The document element to operate on. 
 * .
 * @returns {Array}         Array of objects, vis
 *                              {element,
 *                               startOffset,
 *                               endOffsetInclusive, 
 *                               url}
 */
function getAllLinks(element) {
  var links = [];
  element = element || DocumentApp.getActiveDocument().getBody();
  
  if (element.getType() === DocumentApp.ElementType.TEXT) {
    var textObj = element.editAsText();
    var text = element.getText();
    var inUrl = false;
    for (var ch=0; ch < text.length; ch++) {
      var url = textObj.getLinkUrl(ch);
      if (url != null) {
        if (!inUrl) {
          // We are now!
          inUrl = true;
          var curUrl = {};
          curUrl.element = element;
          curUrl.url = String( url ); // grab a copy
          curUrl.startOffset = ch;
        }
        else {
          curUrl.endOffsetInclusive = ch;
        }          
      }
      else {
        if (inUrl) {
          // Not any more, we're not.
          inUrl = false;
          links.push(curUrl);  // add to links
          curUrl = {};
        }
      }
    }
    if (inUrl) {
      // in case the link ends on the same char that the element does
      links.push(curUrl); 
    }
  }
  else {
    var numChildren = element.getNumChildren();
    for (var i=0; i<numChildren; i++) {
      links = links.concat(getAllLinks(element.getChild(i)));
    }
  }
  return links;
}

/**
 * Replace all or part of UrlLinks in the document.
 *
 * @param {String} searchPattern    the regex pattern to search for 
 * @param {String} replacement      the text to use as replacement
 *
 * @returns {Number}                number of Urls changed 
 */

function findAndReplacetext() {
    var links = getAllLinks();
    while(links.length > 0){
      var link = links[0];
      var paragraph = link.element.getText();
      var linkText = paragraph.substring(link.startOffset, link.endOffsetInclusive+1);
      var newlinkText = `(${linkText})[${link.url}]`
      link.element.deleteText(link.startOffset, link.endOffsetInclusive);
      link.element.insertText(link.startOffset, newlinkText);
      links = getAllLinks();
    }
}

String.prototype.betterReplace = function(search, replace, position) {
  if (this.length > position) {
    return this.slice(0, position) + this.slice(position).replace(search, replace);
  }
  return this;
}

Note: I used insertText and deleteText functions to update the text value of hyperlink.

My problem with above code

Now the problem was that this code was running too slow. I thought may be it was because I was running the script every-time I needed to search for next hyperlink, So maybe I can break the loop and only get the first hyperlink each time.
Then from my previous post the guy gave me a solution to break loop and only get the first hyperlink but when I tried the new code unfortunately it was still slow. In that post he also proposed me a new method by using Google Docs API, I tried using that it was was super fast. Here is the code using Google Docs API

function myFunction() {
    const doc = DocumentApp.getActiveDocument();
    const res = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
      if (paragraph && paragraph.elements) {
        paragraph.elements.forEach(({textRun}) => {
          if (textRun && textRun.textStyle && textRun.textStyle.link) {
            ar.push({text: textRun.content, url: textRun.textStyle.link.url});
          }
        });
      }
      return ar;
    }, []);
    console.log(res)  // You can retrieve 1st link and test by console.log(res[0]).
  }

My new problem

I liked the new code but I am stuck again at this point as I am unable to find how can I change the text associated with the hyperlink. I tried using the functions setContent and setUrl but they don't seem to work. Also I am unable to find the documentation for these functions on main documentation of this API. I did find I reference for previously mentioned functions here but they are not available for appscript.
Here is the sample document I am working on
https://docs.google.com/document/d/1eRvnR2NCdsO94C5nqly4nRXCttNziGhwgR99jElcJ_I/edit?usp=sharing

End note:

I hope I was able to completly convey my message and all the details assosiated with it. If not kindly don't be mad at me, I am still in learning process and my English skills are pretty weak. Anyway if you want any other data let me know in the comments and Thanks for giving your time I really appreciate that.

abdulsamad
  • 158
  • 1
  • 10
  • "they are not available for appscript." No. They are available. See https://developers.google.com/apps-script/guides/services/advanced#how_method_signatures_are_determined and https://developers.google.com/apps-script/advanced/docs#insert_and_style_text – TheMaster Jan 07 '21 at 10:23
  • About `how can I change the text associated with the hyperlink`, can I ask you about the output result you expect? I think that your shared Google Document is the sample input situation. So can I ask you about the result by the script you expected? And also, can you provide the detail of `I tried using the functions setContent and setUrl but they don't seem to work.`? – Tanaike Jan 07 '21 at 12:41
  • I mentioned in main goal heading that my goal is to change the text assosicated with hyperlink. That is working fine with the recursive function but it is slow, The Google API function work fast but I am still trying to understand how to use it to update hyperlink text. – abdulsamad Jan 07 '21 at 16:23
  • Do you want to change the displayed text or the linked URL? Or both? – Iamblichus Jan 11 '21 at 14:45
  • @lambllichus I ran the old code and it appears to remove the link and change to this sort of thing "Google uses structured data that it finds on the (web)[https://en.wikipedia.org/wiki/Web] to understand ..." with the link visible but not clickable. This comment made it clickable but it is not in the doc. – aNewb Jan 11 '21 at 21:17
  • Yes I want exactly that I want all hyperlinks to get in this format but it does not apply to all hyperlinks – abdulsamad Jan 13 '21 at 18:28
  • Not sure I understand. What format they are in, and what format they should be? – Iamblichus Jan 14 '21 at 12:23
  • I only what to change the displayed text. and format would be (displayed text)[hyperlink text] but for that I first need to get both and then delete the hyperlink but my code is not deleting all hyperlinks, you can check it by running it on a document. – abdulsamad Jan 14 '21 at 16:14
  • So we have step 1 which is deleting the hyperlinks from your code. What would be step 2? You want to change the hyperlinks to simple text in this format? `(displayed text)[hyperlink text]`? What's the reason for this? – Iamblichus Jan 15 '21 at 14:04
  • That was an old customer requirement, although I solved it but I recently discovered Google Doc API which is much faster. Now I was trying to delete hyperlinks using this API but does not deletes all hyperlinks. – abdulsamad Jan 15 '21 at 16:17

1 Answers1

1

In order to remove all the hyperlink from your document, you can do the following:

  • First, retrieve the start and end indexes of these hyperlinks. This can be done by calling documents.get, iterate through all elements in the body content, checking which ones are paragraphs, iterating through the corresponding TextRun, and checking which TextRuns contain a TextStyle with a link property. All this is already done in the code you provided in your question.
  • Next, for all TextRuns that include a link, retrieve their startIndex and endIndex.
  • Using these retrieved indexes, call batchUpdate to make an UpdateTextStyleRequest. You want to remove the link property between each pair of indexes, and for that you would just need to set fields to link (in order to specify which properties you want to update) and don't set a link property in the textStyle property you provide in the request since, as the docs for TextStyle say:

link: If unset, there is no link.

Code sample:

function removeHyperlinks() {
  const doc = DocumentApp.getActiveDocument();
  const hyperlinkIndexes = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
    if (paragraph && paragraph.elements) {
      paragraph.elements.forEach(element => {
        const textRun = element.textRun;
        if (textRun && textRun.textStyle && textRun.textStyle.link) {
          ar.push({startIndex: element.startIndex, endIndex: element.endIndex });
        }
      });
    }
    return ar;
  }, []);
  hyperlinkIndexes.forEach(hyperlinkIndex => {
    const resourceUpdateStyle = {
      requests: [
        {
          updateTextStyle: {
            textStyle: {},
            fields: "link",
            range: {
              startIndex: hyperlinkIndex.startIndex,
              endIndex: hyperlinkIndex.endIndex
            }
          }
        }
      ]    
    }
    Docs.Documents.batchUpdate(resourceUpdateStyle, doc.getId());
  });
}
Iamblichus
  • 18,540
  • 2
  • 11
  • 27
  • So the thing is I can't delete all hyperlink's displayed text using Google Doc API as it uses textruns, and after reading [textruns in detail](https://developers.google.com/docs/api/concepts/structure), I came to know that there can be multiple textruns inside single hyperlink's displayed text, if it have different styles. **For Example** ; [**If I have** a link like this](http://test.com) in which half of the text is bold. So when I use textrun it will detect "_If I have_" as one textrun and "_a link like this_" as second textrun, even thought they bought have same hyperlink. – abdulsamad Jan 18 '21 at 18:10
  • Thanks for the code. It works perfectly fine when we need to remove all hyperlinks but it will not work for my purpose. I also needed to delete hyperlink's displayed test. So I have decided not to use Google Doc API. But anyway again thank you for clarifying. Always stay happy :) – abdulsamad Jan 18 '21 at 18:11
  • @abdulsamad Removing the corresponding text could be accomplished via [ReplaceAllTextRequest](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceAllTextRequest), after retrieving the substring for each textRun with hyperlink. I'd suggest you to post another question if you need help with that. – Iamblichus Jan 19 '21 at 08:05
  • Good idea but as I mentioned earlier having any styles inside hyperlink divides a hyperlink in multiple **textruns** . **For example :** A link like this [_New_ link](https://www.google.com) in which half text is Italic and other is non-Italic, it would break the above link into two textruns, First textrun would contain "New" word and second will have "link" word but I need complete word "New link" as single textrun. Because of which I can't use Google API to solve my problem as it works mainly on [textrun](https://developers.google.com/docs/api/concepts/structure#text_runs) concept – abdulsamad Jan 23 '21 at 13:36