3

I wrote a Chrome extension that counts words in a Google Doc, and compares them to suggested word counts from a data source (CSV, table, or database).

My method was to parse each span.kix-lineview-text-block on the page, which stopped working when Google switched to an SVG canvas display. Here's a screenshot showing all word counts at 0.

The recommended alternative to parsing the page is authentication. That is, using Oauth 2.0 to authorize requests and edit content through the well-documented Google Docs API.

GDocs' API Overview makes sense. But I'm new to authentication, and too much of a noob to make sense of this answer. Google offers a quickstart tutorial, but I haven't been able to get it working as an extension.

Clearly there's a gap in my knowledge, and I'm at a loss for what to search for ("Google Docs chrome extension authentication" lead me here...) Most of Google's examples use Java/PHP/Python, which makes me wonder if I'm barking up the wrong tree.

Could someone smarter than me point out what I'm looking for and/or where to learn it?

TL;DR - I have a mostly-working Chrome extension that needs data from a Google Doc. How do I draw the rest of the owl?

GI-IO5T
  • 31
  • 1

1 Answers1

6

It seems wasteful to fetch data from a remote API when it's all present locally so here's a workaround that extracts the text from the doc's internals. Since content scripts run in an isolated environment, we'll have to put the extractor code into the page context and use DOM messaging to communicate with the content script.

Content script (ManifestV2):

// Adds the extractor into the page context (aka "main world")
const script = document.createElement('script');
const eventId = `${Math.random()}${performance.now()}`;
script.textContent = `(${eventId => {
  window.addEventListener(eventId, () => {
    const doc = document.querySelector('.docs-texteventtarget-iframe').contentDocument;
    const key = Object.keys(doc).find(k => k.startsWith('closure_'));
    const res = dig(key ? doc[key] : doc.defaultView, new Set());
    window.dispatchEvent(new CustomEvent(`${eventId}res`, { detail: res || '' }));
  });
  function dig(src, seen) {
    seen.add(src);
    if (!Array.isArray(src)) src = Object.values(src);
    for (let v, len = src.length, i = 0; i < len; i++) {
      try {
        if (!(v = src[i]) ||
            Object.prototype.toString.call(v) === '[object Window]' ||
            seen.has(v)) {
          continue;
        }
      } catch (e) {}
      seen.add(v)
      if (typeof v === 'string' && v[0] === '\x03' && v.endsWith('\n') ||
          typeof v === 'object' && (v = dig(v, seen))) {
        return v;
      }
    }
  }
}})(${JSON.stringify(eventId)})`;
document.documentElement.appendChild(script);
script.remove();

// Listens for messages from the extension
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg === 'getDocText') {
    sendResponse(getDocText());
  }
});

// Calls the extractor via synchronous DOM messaging
function getDocText() {
  let res;
  window.addEventListener(`${eventId}res`, e => { res = e.detail }, {once: true});
  window.dispatchEvent(new CustomEvent(eventId));
  return res;
}

In ManifestV3 the only difference is that instead of script.textContent you will use script.src + a separate script file exposed in web_accessible_resources as shown in method 1 here. In the future ManifestV3 will allow registering code in the main world directly.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • When I run the code above, `getDocText()` returns an empty string. This happens with the extension, and the browser console. Specifically: `(document.querySelector('.docs-texteventtarget-iframe').contentDocument);` returns an html document, and `Object.keys(doc).find(k => k.startsWith('closure_'));` returns a string that looks like `closure_lm_123456`. `dig` returns an empty string (`''` in the console). **This started after my last browser update. Your code works when I roll back to older versions.** Are you able to `getDocText()` using the latest Chrome-based browsers? – GI-IO5T Oct 15 '21 at 16:58
  • Still works for me. Try debugging in devtools by adding breakpoints or `debugger` statements in code. – wOxxOm Oct 15 '21 at 17:09
  • I wish I had better news to report. I'm still trying to get it to work - no luck on latest Windows 10 with latest Brave or Chrome. I made a new project and copy/pasted directly, and received the same results. If you say it works, that's reassuring. It means I'm missing some simple thing, and once I figure it out everything will be great! Thanks for your help so far :) – GI-IO5T Oct 15 '21 at 22:42
  • @GI-IO5T Have you managed to solve this. I get the same results that you are getting too and I am not sure why. – Michael Feb 19 '22 at 03:53
  • Try [switching the site into DOM mode](https://stackoverflow.com/a/70996235) (in page context as well). – wOxxOm Feb 19 '22 at 04:30
  • I did manage to make the code work. The line needs to be changed to: ` if (typeof v === 'string' && v[0] === '\x03' && v.endsWith('\n') ||` Basically the text does not necessarily end with \x03 but it starts like that. The solution perhaps does not work since you cannot detect the text in page until you switch to DOM mode but it definitely allows for reading text. I wonder how Grammarly does it since, it uses this iframe to read the text but somehow it also detect the position of text in page under Canvas mode. Any ideas? – Michael Feb 19 '22 at 04:43
  • There is a second hidden property that can be set on the site, I forgot its name. It renders Canvas + DOM. – wOxxOm Feb 19 '22 at 05:09
  • @wOxxOm this is great! How did you find out all these different things to do this? Like knowing to search for "closure_" etc? Is there somewhere where all this is documented? – rasen58 Sep 26 '22 at 00:18
  • @rasen58, I've inspected the web page scripts. – wOxxOm Sep 26 '22 at 02:33
  • Is there a way to extract the position of the text on the page or the styling information of the text? – Samarth Agarwal Dec 19 '22 at 18:53