Unlocking Power: Leverage the Google Docs API Beyond Apps Script's Document Service

Gists

Abstract

Google Apps Script offers Document service for basic document tasks and Google Docs API for advanced control, requiring more technical expertise. This report bridges the gap with sample scripts to unlock the API’s potential.

Introduction

Google Apps Script provides two powerful tools for managing Google Documents: the Document service (DocumentApp) and the Google Docs API. Ref, Ref While the Document service offers a user-friendly interface for common document manipulation tasks within Apps Script, it has limitations. The Google Docs API, on the other hand, grants finer-grained control over document elements and functionalities not readily available through the Document service.

Despite its power, the Google Docs API requires more technical knowledge compared to the Document service. This report aims to bridge that gap by providing several sample scripts demonstrating how to use the Google Docs API for tasks beyond the capabilities of the Document service. These scripts will be making it easier for users to leverage the full potential of the Google Docs API for their document management needs.

Usage

Preparation

When you test the following sample scripts, please create a new Google Document, and please set the document for each test. You can see the sample situation as the image in each sample.

Enable Google Docs API

Before you use the following scripts, please enable Google Docs API at Advanced Google services. Ref

Samples

1. Change orientation of page Portrait to Landscape and vice versa

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  Docs.Documents.batchUpdate(
    {
      requests: [
        {
          updateDocumentStyle: {
            documentStyle: { flipPageOrientation: true },
            fields: "flipPageOrientation",
          },
        },
      ],
    },
    documentId
  );
}

When this script is run, the situation of the above figure is obtained.

2. Merge cells in a table

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const obj = Docs.Documents.get(documentId).body.content;
  const table = obj.find((e) => e.table);
  if (!table) return;
  const requests = [];
  for (let r = 1; r < table.table.rows; r++) {
    requests.push({
      mergeTableCells: {
        tableRange: {
          columnSpan: 2,
          rowSpan: 1,
          tableCellLocation: {
            tableStartLocation: { index: table.startIndex },
            rowIndex: r,
            columnIndex: 1,
          },
        },
      },
    });
  }
  if (requests.length == 0) return;
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

The cells B2:C2 and B3:C3 are merged, respectively. When you want to change the merge area, please modify the request body of Google Docs API.

3. Create footnote

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  // This is a sample placeholer and replacement data.
  const data = {
    "{{footnote1}}": { text: "sample footnote 1", fontColor: "#ff0000" },
    "{{footnote2}}": { text: "sample footnote 2", fontColor: "#ff9900" },
    "{{footnote3}}": { text: "sample footnote 3", fontColor: "#38761d" },
    "{{footnote4}}": { text: "sample footnote 4", fontColor: "#3c78d8" },
    "{{footnote5}}": { text: "sample footnote 5", fontColor: "#674ea7" },
    "{{footnote6}}": { text: "sample footnote 6", fontColor: "#741b47" },
  };

  // Create request body.
  const para = (o, e) => {
    e.paragraph.elements.forEach((f) => {
      if (f.textRun) {
        const m = [...f.textRun.content.matchAll(r)];
        if (m.length > 0) {
          m.forEach((c) => {
            o.texts.push(c[0]);
            o.requests1.push(
              {
                createFootnote: { location: { index: f.startIndex + c.index } },
              },
              {
                deleteContentRange: {
                  range: {
                    startIndex: f.startIndex + c.index,
                    endIndex: f.startIndex + c.index + c[0].length,
                  },
                },
              }
            );
          });
        }
      }
    });
  };

  // Retrieve the initial object from Google Document.
  const obj1 = Docs.Documents.get(documentId).body.content;

  // Retrieving texts and creating footnotes.
  const dataObj = Object.fromEntries(
    Object.entries(data).map(([k, v]) => {
      const c = SpreadsheetApp.newColor().setRgbColor(v.fontColor).asRgbColor();
      return [
        k,
        {
          text: v.text,
          fontColor: {
            red: c.getRed() / 255,
            green: c.getGreen() / 255,
            blue: c.getBlue() / 255,
          },
        },
      ];
    })
  );
  const keys = Object.keys(dataObj);
  const r = new RegExp(keys.join("|"), "g");
  let { requests1, texts } = obj1.reduce(
    (o, e) => {
      if (e.paragraph) {
        para(o, e);
      } else if (e.table) {
        e.table.tableRows.forEach((row) => {
          if (row.tableCells) {
            row.tableCells.forEach((col) => {
              if (col.content) {
                col.content.forEach((co) => {
                  if (co.paragraph) {
                    para(o, co);
                  }
                });
              }
            });
          }
        });
      }
      return o;
    },
    { requests1: [], texts: [] }
  );
  const putTexts = texts.map((t) => dataObj[t].text);

  const { replies } = Docs.Documents.batchUpdate(
    { requests: requests1.reverse() },
    documentId
  );
  const footnoteIds = replies
    .reduce((ar, e) => {
      if (e && e.createFootnote && e.createFootnote.footnoteId) {
        ar.push(e.createFootnote.footnoteId);
      }
      return ar;
    }, [])
    .reverse();

  // Retrieve the object after footnotes were created.
  const { footnotes } = Docs.Documents.get(documentId);

  // Inserting texts to footnotes.
  const requests2 = footnoteIds.reduce((ar, id, i) => {
    const o = footnotes[id];
    if (o) {
      ar.push(
        {
          insertText: {
            location: { segmentId: id, index: o.content[0].endIndex - 1 },
            text: putTexts[i],
          },
        },
        {
          updateTextStyle: {
            range: {
              segmentId: id,
              startIndex: o.content[0].endIndex - 1,
              endIndex: putTexts[i].length + 1,
            },
            textStyle: {
              foregroundColor: {
                color: { rgbColor: dataObj[texts[i]].fontColor },
              },
            },
            fields: "foregroundColor",
          },
        }
      );
    }
    return ar;
  }, []);
  Docs.Documents.batchUpdate({ requests: requests2 }, documentId);
}

When this script is run, the situation of the above figure is obtained.

You can see more information about managing footnotes at this post.

4. Highlight superscript numbers of footnotes

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const obj = Docs.Documents.get(documentId);
  const requests = obj.body.content.reduce((ar, e) => {
    if (e.paragraph) {
      e.paragraph.elements.forEach((f) => {
        if (f.footnoteReference) {
          ar.push({
            updateTextStyle: {
              textStyle: {
                backgroundColor: {
                  color: { rgbColor: { red: 0, green: 1, blue: 0 } },
                },
              },
              range: { startIndex: f.startIndex, endIndex: f.endIndex },
              fields: "backgroundColor",
            },
          });
        }
      });
    }
    return ar;
  }, []);
  if (requests.length == 0) return;
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

You can see that the style of the superscript numbers of the footnotes is highlighted.

5. Reset borders and shading

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const obj = Docs.Documents.get(documentId).body.content;
  Docs.Documents.batchUpdate(
    {
      requests: [
        {
          updateParagraphStyle: {
            range: { startIndex: 1, endIndex: obj.pop().endIndex },
            fields:
              "borderBetween,borderBottom,borderTop,borderLeft,borderRight,shading",
            paragraphStyle: {},
          },
        },
      ],
    },
    documentId
  );
}

When this script is run, the situation of the above figure is obtained.

You can see that the parameters of the borders and shading were reset.

6. Create checkbox bullets

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  // Append paragraphs.
  const sampleList = ["sample1", "sample2", "sample3", "sample4", "sample5"];
  const doc = DocumentApp.openById(documentId);
  const body = doc.getBody();
  const idxs = sampleList.map((t) =>
    body.getChildIndex(body.appendParagraph(t))
  );
  doc.saveAndClose();

  // Create checkbox bullets.
  const content = Docs.Documents.get(documentId).body.content;
  const requests = idxs.map((id) => ({
    createParagraphBullets: {
      bulletPreset: "BULLET_CHECKBOX",
      range: {
        startIndex: content[id + 1].startIndex,
        endIndex: content[id + 1].endIndex,
      },
    },
  }));
  if (requests.length == 0) return;
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

7. Change mergin of section

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const sectionNumber = 1; // 1 is the first section.

  const obj = Docs.Documents.get(documentId, {
    fields: "body(content(sectionBreak,startIndex,endIndex))",
  }).body.content.filter((e) => e.sectionBreak);
  const section = obj[sectionNumber - 1];
  if (!section) {
    throw new Error(`No section of ${sectionNumber} is not found.`);
  }
  const { startIndex, endIndex } = section;
  const requests = [
    {
      updateSectionStyle: {
        range: { startIndex: startIndex || 0, endIndex },
        sectionStyle: {
          marginLeft: { unit: "PT", magnitude: 0 },
          marginRight: { unit: "PT", magnitude: 0 },
          marginTop: { unit: "PT", magnitude: 0 },
          marginBottom: { unit: "PT", magnitude: 0 },
        },
        fields: "marginLeft,marginRight,marginTop,marginBottom",
      },
    },
  ];
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

8. Uncheck Allow row to overflow across pages of table property

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const tableNumber = 1; // First table is used.

  const tables = Docs.Documents.get(documentId).body.content.filter(
    (e) => e.table
  );
  if (tables.length == 0) return;
  const table = tables[tableNumber - 1];
  const requests = [
    {
      updateTableRowStyle: {
        tableRowStyle: { preventOverflow: true },
        tableStartLocation: { index: table.startIndex },
        fields: "preventOverflow",
      },
    },
  ];
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

9. Set custom line & paragraph spacing

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const obj = Docs.Documents.get(documentId).body.content.filter(
    (e) => e.paragraph
  );
  const o1 = obj[0];
  const o2 = obj.pop();
  const requests = [
    {
      updateParagraphStyle: {
        paragraphStyle: { lineSpacing: 50 },
        range: { startIndex: o1.startIndex, endIndex: o2.endIndex },
        fields: "lineSpacing",
      },
    },
  ];
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

10. Replace placeholders including multiple paragraphs

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const replaceText = "deleted";

  const doc = DocumentApp.openById(documentId);
  const matches = doc
    .getBody()
    .getText()
    .match(/\{\{[\s\S\w]+?\}\}/g);
  if (!matches || matches.length == 0) return;
  const requests = matches.map((text) => ({
    replaceAllText: { containsText: { matchCase: false, text }, replaceText },
  }));
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

In this sample, the paragraphs enclosed by {{ and }} are replaced with text deleted.

11. Change soft line breaks for hard line breaks and vice versa

This script is from this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const replaceAllText = {
    replaceText: "\n",
    containsText: { text: "\u000b" },
  }; // soft line break to hard line break
  // const replaceAllText = { replaceText: "\u000b", containsText: { text: "\n" } }; // hard line break to soft line break

  Docs.Documents.batchUpdate({ requests: [{ replaceAllText }] }, documentId);
}

When this script is run, the situation of the above figure is obtained.

  • When const replaceAllText = { replaceText: "\n", containsText: { text: "\u000b" } }; is used, the soft line-breaks are changed to the hard line-breaks.
  • When const replaceAllText = { replaceText: "\u000b", containsText: { text: "\n" } }; is used, the hard line breaks are changed to the soft line breaks.

12. Manage table borders

This script is from this answer and this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const tableNumber = 1; // First table is used.

  const tables = Docs.Documents.get(documentId).body.content.filter(
    (e) => e.table
  );
  if (tables.length == 0) return;
  const table = tables[tableNumber - 1];
  const tableStart = table.startIndex;
  const borders = ["borderTop", "borderBottom", "borderLeft", "borderRight"];
  const resource = {
    requests: [
      {
        updateTableCellStyle: {
          tableStartLocation: { index: tableStart },
          tableCellStyle: borders.reduce(
            (o, e) => (
              (o[e] = {
                width: { magnitude: 0, unit: "PT" },
                dashStyle: "SOLID",
                color: { color: { rgbColor: { blue: 0 } } },
              }),
              o
            ),
            {}
          ),
          fields: borders.join(","),
        },
      },
      {
        updateTableCellStyle: {
          tableRange: {
            tableCellLocation: {
              tableStartLocation: { index: tableStart },
              rowIndex: 0,
              columnIndex: 0,
            },
            rowSpan: table.table.rows,
            columnSpan: 1,
          },
          tableCellStyle: {
            borderLeft: {
              dashStyle: "SOLID",
              width: { magnitude: 2.25, unit: "PT" },
              color: { color: { rgbColor: { red: 1 } } },
            },
          },
          fields: "borderLeft",
        },
      },
    ],
  };
  Docs.Documents.batchUpdate(resource, documentId);
}

When this script is run, the situation of the above figure is obtained.

13. Manage nested lists

This script is from this answer and this answer on Stackoverflow.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const sampleList = [
    "sample1",
    " sample1-1",
    " sample1-2",
    "  sample1-2-1",
    "  sample1-2-2",
    " sample1-3",
    "sample2",
    " sample2-1",
    " sample2-2",
    " sample2-3",
  ];

  const startIndex = 1;
  const text = sampleList.reduce(
    (t, e) =>
      (t += e.startsWith(" ") ? `${e.replace(/ /g, "\t")}\n` : `${e}\n`),
    "\n"
  );
  if (text.length == 1) return;
  const requests = [
    { insertText: { text, location: { index: startIndex } } },
    {
      createParagraphBullets: {
        range: {
          startIndex: startIndex + 1,
          endIndex: startIndex + text.length,
        },
        bulletPreset: "NUMBERED_DECIMAL_NESTED",
      },
    },
  ];
  Docs.Documents.batchUpdate({ requests }, documentId);
}

When this script is run, the situation of the above figure is obtained.

14. Retrieve all URLs from document

This script is from my blog.

function myFunction() {
  const documentId = "###"; // Please set document ID.

  const content = Docs.Documents.get(documentId).body.content;
  const urls = [];
  JSON.parse(JSON.stringify(content), (k, v) => {
    if (k == "url") urls.push(v);
  });
  console.log(urls);
}

When this script is run, all URLs in the document are retrieved as an array.

 Share!