Update link to heading in google docs
Asked Answered
P

1

7

In google docs one can easily add headings and link to them from inside of the document. But when the heading text changes, the link text does not change. Is there a way to change that behavior or update the link text automatically?

Porbeagle answered 30/4, 2019 at 14:43 Comment(1)
Hey Sebastian, did you work out how to do this? It's exactly what I want to do.Overfill
C
4

I know it is about 1 1/2 years, but maybe this will help. I have had the exact same problem and wrote a function that will update all the links to the headings in a document. Since I could not find any built-in functions or add-ons, the only way was to script it.

Some things to consider:

  • This needs a current table of contents to work. If you don't have (or do not want) a TOC, you can insert one, run that function and delete it afterwards. Also, I have only tested it with a TOC that contains page numbers.
  • It will update ALL texts of links to headings in the document. However, links to everything else remain untouched.
  • Please use at your own risk (maybe try it out in a copy of your document). I have tested it, but the testing could have been more thorough. Also, this is my first in scripting Docs.

Paste this in the Script editor of your doc and run replaceHeadingLinks. Links that the script could not update (because they link to a heading that does not exist anymore) will be output in the console.

function replaceHeadingLinks() {
  var curDoc = DocumentApp.getActiveDocument();
  var links = getAllLinks_(curDoc.getBody());
  var headings = getAllHeadings_(curDoc.getBody());
  var deprecatedLinks = []; // holds all links to headings that do not exist anymore.
    
  links.forEach(function(link) {
  
    if(link.url.startsWith('#heading')) {
    
      // get the new heading text
      var newHeadingText = headings.get(link.url);
      
      // if the link does not exist anymore, we cannot update it.
      if(typeof newHeadingText !== "undefined") {
        
        var newOffset = link.startOffset + newHeadingText.length - 1;
        
        // delete the old text, insert new one and set link
        link.element.deleteText(link.startOffset, link.endOffsetInclusive);
        link.element.insertText(link.startOffset, newHeadingText);
        link.element.setLinkUrl(link.startOffset, newOffset, link.url);
      
      } else {
        deprecatedLinks.push(link);
      }
      
    }
  
  }
  ) 
  
  // error handling: show deprecated links:
  
  if(deprecatedLinks.length > 0) {
    Logger.log("Links we could not update:");
    
    for(var i = 0; i < deprecatedLinks.length; i++) {
      var link = deprecatedLinks[i];
      var oldText = link.element.getText().substring(link.startOffset, link.endOffsetInclusive);
      Logger.log("heading: " + link.url + " / description: " + oldText);
    }
  } else {
    Logger.log("all links updated");
  }
  
}


/**
 * 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} element The document element to operate on. 
 * .
 * @returns {Array}         Array of objects, vis
 *                              {element,
 *                               startOffset,
 *                               endOffsetInclusive, 
 *                               url}
 *
 * Credits: https://mcmap.net/q/617483/-get-all-links-in-a-document/40730088
 */
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;
    var curUrl = {};
    for (var ch=0; ch < text.length; ch++) {
      var url = textObj.getLinkUrl(ch);
      if (url != null) {
        if (!inUrl) {
          // We are now!
          inUrl = true;
          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 = {};
        }
      }
    }
    // edge case: link is at the end of a paragraph
    // check if object is empty
    if(inUrl && (Object.keys(curUrl).length !== 0 || curUrl.constructor !== Object)) {
      links.push(curUrl);  // add to links
      curUrl = {};
    }
  }
  else {
    // only traverse if the element is traversable
    if(typeof element.getNumChildren !== "undefined") {
        var numChildren = element.getNumChildren();
      
        for (var i=0; i<numChildren; i++) {
  
        // exclude Table of Contents
       
        child = element.getChild(i);
        if(child.getType() !== DocumentApp.ElementType.TABLE_OF_CONTENTS) {
          links = links.concat(getAllLinks_(element.getChild(i)));
        }
      }
    }
  }

  return links;
}


/**
 * returns a map of all headings within an element. The map key
 * is the heading ID, such as h.q1xuchg2smrk
 *
 * THIS REQUIRES A CURRENT TABLE OF CONTENTS IN THE DOCUMENT TO WORK PROPERLY.
 *
 * @param {Element} element The document element to operate on. 
 * .
 * @returns {Map} Map with heading ID as key and the heading element as value.
 */
function getAllHeadings_(element) {
  
  var headingsMap = new Map();
  
  var p = element.findElement(DocumentApp.ElementType.TABLE_OF_CONTENTS).getElement();

  if(p !== null) {
      var toc = p.asTableOfContents();
      for (var ti = 0; ti < toc.getNumChildren(); ti++) {
        
        var itemToc = toc.getChild(ti).asParagraph().getChild(0).asText();
        var itemText = itemToc.getText();
        var itemUrl =  itemToc.getLinkUrl(0);
        var itemDesc = null;
    
        // strip the line numbers if TOC contains line numbers
        var itemText = itemText.match(/(.*)\t/)[1];
        headingsMap.set(itemUrl,itemText);
      }
    }
    return headingsMap;
}
Cutwater answered 2/11, 2020 at 21:27 Comment(1)
Thanks, this worked well for me. It is crazy that Google does not have a better support for handling references in 2023.Herbst

© 2022 - 2024 — McMap. All rights reserved.