Below are a few Apps Scripts you can use with Google Docs in a browser (NOT a mobile app).
/**
* Add a logo image into the header of all Tabs and Subtabs of every Docs file in a given folder.
* Author: Mr Shane.
* Version: 2024-11-11
*/
function addLogoToAllTabs() {
const folderId = "1yYtvYZ7G4S1yqG947yavqKoJhQE_JMl8"; // Configure the Folder ID where the Docs files are stored.
const imageId = "1nPWkU8vW-oylyMaLqxQRxVmxj4u53eLo"; // Configure the File ID of the PNG logo image.
const folder = DriveApp.getFolderById(folderId); // Get the folder.
const imageFile = DriveApp.getFileById(imageId); // Get the logo image.
const imageBlob = imageFile.getBlob().setContentType("image/png"); // Ensure the image is a PNG.
const files = folder.getFilesByType(MimeType.GOOGLE_DOCS); // Get all Google Docs files in the folder.
while (files.hasNext()) {
const file = files.next();
const doc = DocumentApp.openById(file.getId());
let tabs = doc.getTabs(); // Get all first-level tabs in the document.
const stack = []; // Initialize an array to stack tabs and subtabs during iteration.
for (let i = 0; i < tabs.length; i++) { // Add all tabs to the stack.
stack.push(tabs[i]);
}
while (stack.length > 0) {
const currentTab = stack.pop(); // Get the last Tab from the stack.
const documentTab = currentTab.asDocumentTab(); // Retrieve the Tab contents as a DocumentTab.
let header = documentTab.getHeader(); // Get or create the header for this tab.
if (!header) { // IF there is no header...
header = documentTab.addHeader(); // Create a header.
}
header.clear(); // Clear existing content in the header.
const table = header.appendTable([["", ""]]); // Insert a table in the header with two columns.
table.setBorderWidth(0); // Remove table borders for a cleaner look.
const rightCell = table.getCell(0, 1); // Set the right cell for inserting the image.
rightCell.insertImage(0, imageBlob); // Insert the image into the right cell.
const childTabs = currentTab.getChildTabs(); // Get child tabs (subtabs) and push them to the stack.
for (let j = 0; j < childTabs.length; j++) {
stack.push(childTabs[j]);
}
}
doc.saveAndClose(); // Save and close the document after modifications.
}
}
/**
* Bionic Reading WITHOUT an add-on
* Author: Mr Shane
* Version: 2025-05-13
*
* If you want an add-on, then try this one:
* @see https://chromewebstore.google.com/detail/bionic-reading/kdfkejelgkdjgfoolngegkhkiecmlflj
*
* Documentation:
* @see https://developers.google.com/apps-script/reference/document/element-type
* @see https://developers.google.com/apps-script/reference/document/paragraph-heading
*/
function onOpen() {
const ui = DocumentApp.getUi(); // Get the UI of the active document.
ui.createMenu("⚠️Admin Menu⚠️") // Create a custom menu.
.addItem(`Bionic "Normal text" outside tables`, "bionicNormalTextOutsideTable") // Add menu item.
.addItem(`Bionic All "Normal text"`, "bionicNonHeaders") // Add menu item.
.addItem(`Bionic EVERY Word`, "bionicEveryWord") // Add menu item.
.addItem(`Bold all text outside tables`, "boldTextOutsideTable") // Add menu item.
.addItem(`Bold first 2 characters of EVERY word`, "boldFirst2CharactersOfEveryWord") // Add menu item.
.addItem(`Bold all "Normal text"`, "boldAllNormalText") // Add menu item.
.addToUi(); // Add the menu to the UI.
}
function bionicNormalTextOutsideTable() {
const body = DocumentApp.getActiveDocument().getBody(); // Get body of document.
const tables = body.getTables(); // Get all tables.
const paragraphs = []; // Prepare container for target paragraphs.
for (let i = 0; i < body.getNumChildren(); i++) { // Loop through children of body.
const child = body.getChild(i); // Get child element.
if (child.getType() === DocumentApp.ElementType.PARAGRAPH) { // If element is a paragraph...
const para = child.asParagraph(); // Cast to paragraph.
if (!isInsideTable_(para, tables) && para.getHeading() === DocumentApp.ParagraphHeading.NORMAL) // If outside table and normal...
paragraphs.push(para); // Add to list.
}
}
applyBionicStyleToParagraphs(paragraphs, word => Math.floor((word.length - 1) / 2)); // Apply style.
}
function bionicNonHeaders() {
const body = DocumentApp.getActiveDocument().getBody(); // Get body.
const allParagraphs = body.getParagraphs(); // Get all paragraphs.
const normalParagraphs = allParagraphs.filter(p => p.getHeading() === DocumentApp.ParagraphHeading.NORMAL); // Filter normal text.
applyBionicStyleToParagraphs(normalParagraphs, word => Math.floor(word.length / 2)); // Apply style.
}
function bionicEveryWord() {
const body = DocumentApp.getActiveDocument().getBody(); // Get body.
const text = body.getText(); // Get all text.
const words = text.split(/\s/); // Split into words.
let currentPosition = 0; // Position in text.
for (let j = 0; j < words.length; j++) { // Loop through words.
const word = words[j];
if (word.trim() !== "") { // If not empty...
const start = currentPosition + text.substring(currentPosition).indexOf(word); // Word start.
const boldLength = Math.ceil(word.length / 2); // Characters to bold.
const end = start + boldLength; // End of bold.
if (boldLength > 0) { // Only bold if range valid.
body.editAsText().setBold(start, end - 1, true); // Bold part of word.
}
currentPosition = start + word.length; // Move to next word.
}
}
}
function boldFirst2CharactersOfEveryWord() {
const body = DocumentApp.getActiveDocument().getBody(); // Get body.
const text = body.getText(); // Get all text.
const words = text.split(/\s/); // Split into words.
let currentPosition = 0; // Track position.
for (let i = 0; i < words.length; i++) { // Iterate words.
const word = words[i];
if (word.trim() !== "") { // If not empty...
const start = currentPosition + text.substring(currentPosition).indexOf(word); // Word start.
const end = start + Math.min(2, word.length); // End = 2 characters max.
body.editAsText().setBold(start, end - 1, true); // Bold first 2.
currentPosition = start + word.length; // Move forward.
}
}
}
function boldAllNormalText() {
const body = DocumentApp.getActiveDocument().getBody(); // Get body.
const paragraphs = body.getParagraphs(); // Get all paragraphs.
for (let i = 0; i < paragraphs.length; i++) { // Iterate paragraphs.
const para = paragraphs[i];
if (para.getHeading() === DocumentApp.ParagraphHeading.NORMAL) // If normal text...
para.setBold(true); // Bold all text in paragraph.
}
}
function boldTextOutsideTable() {
const body = DocumentApp.getActiveDocument().getBody(); // Get document body.
const tables = body.getTables(); // Get tables.
for (let i = 0; i < body.getNumChildren(); i++) { // Iterate children.
const child = body.getChild(i);
if (child.getType() === DocumentApp.ElementType.PARAGRAPH) { // If paragraph...
const para = child.asParagraph(); // Cast.
if (!isInsideTable_(para, tables)) // If not inside table...
para.setBold(true); // Bold all.
}
}
}
/*************************************** HELPER FUNCTIONS ***************************************/
// Apply bionic-style bolding to a list of paragraphs
function applyBionicStyleToParagraphs(paragraphs) {
for (let i = 0; i < paragraphs.length; i++) { // Iterate over each paragraph.
const paragraph = paragraphs[i];
const text = paragraph.getText(); // Get paragraph text.
const words = text.split(/\s+/); // Split into words.
let currentPosition = 0; // Track character index in paragraph.
for (let j = 0; j < words.length; j++) { // Iterate over words.
const word = words[j];
if (word.trim() === "") continue; // Skip empty words.
const indexInText = text.indexOf(word, currentPosition); // Find word from current position.
if (indexInText === -1) continue; // Skip if not found.
const start = indexInText; // Start index of word.
const charsToBold = Math.ceil(word.length / 2); // Bold >50% for odd lengths.
const end = start + charsToBold; // End index.
if (end > start) { // Only apply bold if range is valid.
paragraph.editAsText().setBold(start, end - 1, true); // Apply bold.
}
currentPosition = start + word.length; // Move to next word.
}
}
}
// Check if a paragraph is inside any table
function isInsideTable_(paragraph, tables) {
for (let i = 0; i < tables.length; i++) { // Loop through tables.
if (isElementInside_(paragraph, tables[i])) return true; // Found inside.
}
return false; // Not inside any.
}
// Check if an element is nested inside another
function isElementInside_(element, container) {
let parent = element.getParent(); // Start from immediate parent.
while (parent != null) { // Traverse up.
if (parent.getType() === container.getType() && parent.getId() === container.getId()) // Match by ID and type.
return true; // Found match.
parent = parent.getParent(); // Go up.
}
return false; // No match found.
}
/**
* Count words for ALL tabs in a Docs file.
* At the time of writing, the "Word count" of Google Docs
* can't count words from multiple Tabs or Subtabs.
* This script solves that problem.
* Author: Mr Shane
* Version: 2025-04-28
* @see https://developers.google.com/apps-script/guides/docs/tabs
* @OnlyCurrentDoc
*/
function onOpen() {
const ui = DocumentApp.getUi(); // Get the user interface for the document
ui.createMenu("⚠️Admin Menu⚠️") // Create a custom menu.
.addItem("Count words of ALL Tabs and Subtabs", "countWordsInAllTabs") // Add the menu item.
.addToUi(); // Add the menu to the UI.
}
function countWordsInAllTabs() {
const doc = DocumentApp.getActiveDocument(); // Get the active document.
const ui = DocumentApp.getUi(); // Get the user interface for the document
const tabs = doc.getTabs(); // Get all top-level Tabs in the document.
let totalWords = 0; // Initialize total word count.
const stack = [...tabs]; // Initialize a stack with the top-level Tabs.
while (stack.length > 0) {
const currentTab = stack.pop(); // Process the last tab in the stack.
const documentTab = currentTab.asDocumentTab(); // Convert to a DocumentTab.
const bodyText = documentTab.getBody().getText(); // Get the body text.
totalWords += bodyText.trim().split(/\s+/).filter(Boolean).length; // Count words.
const childTabs = currentTab.getChildTabs(); // Get Subtabs.
stack.push(...childTabs); // Add Subtabs to the stack for further processing.
}
// Display the result to the user.
ui.alert(
`Word Count Result`,
`Total word count across all tabs, subtabs, and sub-subtabs: ${totalWords}`,
ui.ButtonSet.OK
);
}
/**
* Word count for EACH and ALL tabs in a Docs file.
* At the time of writing, the "Word count" of Google Docs
* can't count words from multiple Tabs or Subtabs.
* This script solves that problem.
* Author: Mr Shane
* Version: 2024-04-28
* @see https://developers.google.com/apps-script/guides/docs/tabs
* @OnlyCurrentDoc
*/
function onOpen() {
DocumentApp.getUi() // Get the user interface for the document
.createMenu("⚠️Admin Menu⚠️") // Create a custom menu named "Admin Menu"
.addItem("Count words in all Tabs and Subtabs", "countWordsInAllTabs") // Add a menu item to trigger word count
.addToUi(); // Add the menu to the UI
}
function countWordsInAllTabs() {
const doc = DocumentApp.getActiveDocument(); // Get the active document
const ui = DocumentApp.getUi(); // Get the user interface for the document
const allTabs = getAllTabs(doc); // Get a flat list of all tabs and subtabs
let results = []; // Initialize an array to hold tab titles and word counts
let totalWords = 0; // Initialize a variable to keep the total word count
for (const tabInfo of allTabs) { // Loop through each tab
const { id, title } = tabInfo; // Destructure id and title from tab info
const documentTab = doc.getTab(id).asDocumentTab(); // Open the tab by ID and get as a DocumentTab
const bodyText = documentTab.getBody().getText(); // Get the body text of the tab
const wordCount = bodyText.trim().split(/\s+/).filter(Boolean).length; // Count the words by splitting and filtering empty strings
totalWords += wordCount; // Add this tab's word count to the total
results.push(`${title}: ${wordCount} words`); // Add the title and word count to results
}
results.push(""); // Add an empty line before total
results.push(`Total Word Count: ${totalWords} words`); // Add the total word count to the results
ui.alert( // Show an alert with the word counts
"Word Count by Tab/Subtab", // Alert title
results.join("\n"), // Join all tab results with newlines
ui.ButtonSet.OK // Set alert button
);
}
/**
* Returns a flat list of all tabs (ID and Title) in the document, in the order they appear.
*/
function getAllTabs(doc) {
const allTabs = []; // Initialize an array to collect all tabs
for (const tab of doc.getTabs()) { // Loop through all top-level tabs
addCurrentAndChildTabs(tab, allTabs); // Recursively add tab and its children
}
return allTabs; // Return the collected tabs
}
/**
* Recursively adds the current tab and its child tabs.
*/
function addCurrentAndChildTabs(tab, allTabs) {
allTabs.push({ id: tab.getId(), title: tab.getTitle() }); // Add the current tab's ID and title
for (const childTab of tab.getChildTabs()) { // Loop through child tabs
addCurrentAndChildTabs(childTab, allTabs); // Recursively add each child tab
}
}
/**
* ATTENTION: Currently, there is no ability to create a document tab.
*
* If you want the ability to use Apps Script to create/move tabs in Google Docs,
* please upvote these IssueTrackers so that Google will see there is good demand for this feature to be developed:
* @see https://issuetracker.google.com/issues/375867285
* @see https://issuetracker.google.com/issues/391499599
*
* Potential prerequisites: Enable 'Google Docs API'.
* Documentation
* @see https://developers.google.com/apps-script/advanced/docs
* @see https://developers.google.com/workspace/docs/api/concepts/document
* @see https://developers.google.com/workspace/docs/api/how-tos/tabs#documentscreate
*/
/**
* ATTENTION: Currently, there is no ability to duplicate a document tab.
*
* If you want the ability to use Apps Script to create/move tabs in Google Docs,
* please upvote these IssueTrackers so that Google will see there is good demand for this feature to be developed:
* @see https://issuetracker.google.com/issues/375867285
* @see https://issuetracker.google.com/issues/391499599
*
* Potential prerequisites: Enable 'Google Docs API'.
* Documentation
* @see https://developers.google.com/apps-script/advanced/docs
* @see https://developers.google.com/workspace/docs/api/concepts/document
* @see https://developers.google.com/workspace/docs/api/how-tos/tabs#documentscreate
*/
/** START ONOPEN */
function onOpen(){
customMenu();
}
/** END ONOPEN */
/** CUSTOM MENU */
function customMenu(){
const ui = DocumentApp.getUi();
ui.createMenu("⚠️Admin Menu⚠️")
.addItem("Convert Doc to PDF in Current Folder v1", "docToPdfCurrentFolder_v1")
.addItem("Convert Doc to PDF in Current Folder v2", "docToPdfCurrentFolder_v2")
.addItem("Convert Doc to PDF in Nominated Folder v1", "docToPdfNominatedFolder_v1")
.addItem("Convert Doc to PDF in Nominated Folder v2", "docToPdfNominatedFolder_v2")
.addItem("Convert Doc to PDF in Root Directory", "docToPdfRootDir")
.addToUi()
}
/** END CUSTOM MENU */
/**
* Export the active Docs file (including all Tabs and Subtabs) to PDF in the current folder.
* This script maintains the page orientation.
*/
function docToPdfCurrentFolder_v1() {
const doc = DocumentApp.getActiveDocument(); // Get the active Docs file.
const docID = doc.getId(); // Get ID of the active Docs file.
const folderID = DriveApp.getFileById(docID).getParents().next().getId(); // Get ID of the folder the Doc is stored in.
const folder = DriveApp.getFolderById(folderID); // Get the folder by ID.
const pdfBlob = doc.getBlob(); // Get the file as a Blob.
folder.createFile(pdfBlob); // Create the PDF file in Google Drive folder.
}
/**
* Export the active Docs file (including all Tabs and Subtabs) to PDF in the current folder.
* This script maintains the page orientation.
*/
function docToPdfCurrentFolder_v2() {
const doc = DocumentApp.getActiveDocument(); // Get the active Docs file.
const docID = doc.getId(); // Get ID of the active Docs file.
const folderID = DriveApp.getFileById(docID).getParents().next().getId(); // Get ID of the folder the Doc is stored in.
const folder = DriveApp.getFolderById(folderID); // Get the Drive folder by its ID.
const pdfBlob = doc.getAs('application/pdf'); // Get the file as a PDF.
folder.createFile(pdfBlob); // Create the PDF file in Google Drive folder.
}
/**
* Export the active Docs file (including all Tabs and Subtabs) to PDF in a specified folder.
* This script maintains the page orientation.
*/
function docToPdfNominatedFolder_v1() {
const doc = DocumentApp.getActiveDocument(); // Get the active Docs file.
const folderID = '1RvHowhvddb3JrssUZrytMH6k1zKNN_E8'; // Configure ID of target folder.
const folder = DriveApp.getFolderById(folderID); // Get the Drive folder by its ID.
const pdfBlob = doc.getBlob(); // Get the file as a Blob.
folder.createFile(pdfBlob); // create new PDF file in Google Drive folder
}
/**
* Export the active Docs file (including all Tabs and Subtabs) to PDF in the specified folder.
* This script maintains the page orientation.
*/
function docToPdfNominatedFolder_v2() {
const doc = DocumentApp.getActiveDocument(); // Get the active Docs file.
const folderID = '1RvHowhvddb3JrssUZrytMH6k1zKNN_E8'; // Configure ID of target folder.
const folder = DriveApp.getFolderById(folderID); // Get the Drive folder by its ID.
const pdfBlob = doc.getAs('application/pdf'); // Get the file as a PDF.
folder.createFile(pdfBlob); // create new PDF file in Google Drive folder
}
/**
* Export the active Docs file (including all Tabs and Subtabs) to PDF in the root directory.
* This script maintains the page orientation.
*/
function docToPdfRootDir() {
const doc = DocumentApp.getActiveDocument();
const ui = DocumentApp.getUi();
const result = ui.alert(
'Save As PDF?',
'Save current document (Name: ' + doc.getName() + '.pdf) as PDF',
ui.ButtonSet.YES_NO);
if (result === ui.Button.YES) {
let docBlob = doc.getAs('application/pdf');
docBlob.setName(doc.getName() + ".pdf");
const file = DriveApp.createFile(docBlob);
ui.alert('Your PDF file is available at ' + file.getUrl());
} else {
ui.alert('Request has been cancelled.');
}
}
/**
* Export the active tab as PDF (without the extra tab title page).
* @see https://developers.google.com/apps-script/guides/docs/tabs
*
* Author: Mr Shane
* Version: 2025-09-12
*/
function createPDFActiveTab() {
const doc = DocumentApp.getActiveDocument();
const tab = doc.getActiveTab();
const url = `https://docs.google.com/document/d/${doc.getId()}/export?format=pdf&tab=${tab.getId()}`;
const params = {headers: {"Authorization": 'Bearer ' + ScriptApp.getOAuthToken()}};
const response = UrlFetchApp.fetch(url, params);
const blob = response.getBlob();
DriveApp.createFile(blob.setName(tab.getTitle()));
}
/**
* replaceText() Find
* "\\s*" = any spaces
* "\\t" = tabs
* "\\v" = soft returns
* "^\\s+" = leading spaces (at beginning of paragraphs)
* "\\s+$" = trailing spaces (at end of paragraphs)
*
* replaceText() Replace
* "\t" = tabs
* "\n" = new-lines
* "^\\s+" = leading spaces (at beginning of paragraphs)
* "\\s+$" = trailing spaces (at end of paragraphs)
*
* getText().replace() Find (WARNING: ALL formatting will be removed when using the getText().replace() & setText() methods!)
* /\t/g = tabs
* /\n/g = new-lines
* /\n\n/g = double new-lines
*
* getText().replace() Replace (WARNING: ALL formatting will be removed when using the getText().replace() & setText() methods!)
* "\t" = tabs
* "\n" = new-lines
*/
/**
* Find & replace in the selected text.
* Pros: Maintains the text formatting
* Cons: Is case sensitive.
*/
function multipleFindAndReplace() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const s = doc.getSelection(); // Get the current selection.
if (s) {
s.getRangeElements().forEach(e => {
e.getElement().replaceText("find", "replace");
e.getElement().replaceText("Find", "Replace");
})
} else {
DocumentApp.getUi().alert('No text selected. Please select some text and try again.');
}
}
/**** Docs Custom Menu **************************************************************************/
function onOpen() {
DocumentApp.getUi().createMenu("⚠️Admin Menu⚠️") // Create a custom menu
.addItem('Replace Space with Tabs - ALL', 'replaceSpacesWithTabsAll')
.addItem('Replace Space with Tabs - Beginning of Paragraphs Only', 'replaceSpacesWithTabsParagraphStart')
.addItem('Replace Space with Tabs - Selected Text Only', 'replaceSpacesWithTabsSelectedText')
.addItem('Replace Comma-Space with Newline - Selected Text Only', 'replaceCommaSpacesWithNewlineSelectedText')
.addItem('Replace Space-Hyphen-Space with Newline - Selected Text Only', 'rreplaceSpaceHyphenSpaceWithNewlineSelectedText')
.addItem('Remove Leading & Trailing Spaces - At Beginning & End of Paragraphs', 'removeSpacesParagraphStartEnd')
.addSeparator()
.addItem('Replace Tabs with Newline - Within Text Only', 'replaceTabsWithNewlineInText')
.addItem('Replace Tabs with Newline - ALL (ALL formatting will be removed)', 'replaceTabsWithNewLineAll')
.addSeparator()
.addItem('Reset Leading "First line indent" tabs to 0', 'setFirstLineIndentToZero')
.addSeparator()
.addItem('Add a New Line After Each Paragraph', 'addNewLineAfterParagraphs')
.addSeparator()
.addItem('Replace Double Newline with Single Newline - ALL (ALL formatting will be removed)', 'setTextdoubleNewlineToSingleNewline')
.addItem('Replace Double Newline with Single Newline - ALL', 'doubleNewlineToSingleNewline')
.addItem('Replace Double Newline with Double Newline + Opening Quotes - ALL (ALL formatting will be removed)', 'doubleNewlineToDoubleNewlineDoubleQuotes')
.addItem('Replace Double Newline with Double Newline + Opening Quotes - ALL', 'afterDoubleNewlineAddDoubleQuoteInFront')
.addItem('Replace Double Newline with Double Newline + Opening & Closing Quotes - ALL', 'afterDoubleNewlineWrapWithDoubleQuotes')
.addItem('Replace Double Newline with Double Newline + Opening & Closing Quotes - Selected Text Only', 'wrapDoubleQuotesToSelectedParagraphs')
.addItem('Add a New Line After Each Paragraph.', 'addNewLineAfterParagraphs')
.addSeparator()
.addItem('Replace Hard Returns with Soft Returns - ALL', 'replaceHardWithSoft')
.addToUi();
}
/**** REPLACE SPACES ****************************************************************************/
/**
* Replace Space with Tabs - ALL
*/
function replaceSpacesWithTabsAll() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
body.editAsText().replaceText("\\s*", "\t"); // Replace ALL spaces with tabs.
}
/**
* Replace Space with Tabs - At Beginning of Paragraphs Only
*/
function replaceSpacesWithTabsParagraphStart() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const paragraphs = body.getParagraphs(); // Get the paragraphs in the body.
for ( i=0 ; i < paragraphs.length; i++) {
paragraphs[i].replaceText("^\\s+", "\t"); // Replace leading spaces with tabs.
}
}
/**
* Replace Space with Tabs - Range Element of Selected Text Only
*/
function replaceSpacesWithTabsSelectedText() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const s = doc.getSelection(); // Get the current selection.
if (s) {
s.getRangeElements().forEach(e => {
e.getElement().replaceText("\\s*", "\t");
})
} else {
DocumentApp.getUi()
.alert('No text selected. Please select some text and try again.');
}
}
/**
* Replace Comma-Space with Newline - Range Element of Selected Text Only
*/
function replaceCommaSpacesWithNewlineSelectedText() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const s = doc.getSelection(); // Get the current selection.
if (s) {
s.getRangeElements().forEach(e => {
e.getElement().replaceText(",\\s*", "\n");
})
} else {
DocumentApp.getUi().alert('No text selected. Please select some text and try again.');
}
}
/**
* Replace Space-Hyphen-Space with Newline - Range Element of Selected Text Only
*/
function replaceSpaceHyphenSpaceWithNewlineSelectedText() {
const doc = DocumentApp.getActiveDocument(); // Get the active document.
const s = doc.getSelection(); // Get the current selection.
if (s) {
s.getRangeElements().forEach(e => {
e.getElement().replaceText(" - ", "\n");
});
} else {
DocumentApp.getUi().alert('No text selected. Please select some text and try again.');
}
}
/**** REMOVE SPACES *****************************************************************************/
/**
* Remove Leading & Trailing Spaces - At Beginning & End of Paragraphs
*/
function removeSpacesParagraphStartEnd() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const p = body.getParagraphs(); // Get the paragraphs in the body.
for ( i=0 ; i < p.length; i++) {
p[i].replaceText("^\\s+", ""); // Remove leading spaces.
p[i].replaceText("\\s+$", ""); // Remove trailing spaces.
}
}
/**** REPLACE TABS ******************************************************************************/
/**
* Replace Tabs with Newline - Within Text Only
* NOTE: This will NOT replace "First line indent" at the beginning of paragraphs.
*/
function replaceTabsWithNewlineInText() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
body.editAsText().replaceText("\\t", "\n");
}
/**
* Replace Tabs with Newline - ALL
* WARNING: ALL formatting will be removed when using the replace() & setText() methods!
*/
function replaceTabsWithNewLineAll() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const bodyText = body.getText(); // Get the text from the body.
const textReplacement = bodyText.replace(/\t/g, "\n");
body.setText( textReplacement );
}
/**** RESET FIRST LINE INDENT *******************************************************************/
/**
* Reset Leading "First line indent" tabs to 0
*/
function setFirstLineIndentToZero() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const paragraphs = body.getParagraphs(); // Get the paragraphs in the body.
for (let i = 0; i < paragraphs.length; i++) {
const paragraph = paragraphs[i];
paragraph.setAttributes({ INDENT_FIRST_LINE: 0 });
}
}
/**** ADD NEW-LINE ******************************************************************************/
/**
* Add a New Line After Each Paragraph.
* NOTE: A new line is effectively the beginning of a new paragraph.
*/
function addNewLineAfterParagraphs() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const paragraphs = body.getParagraphs(); // Get the paragraphs in the body.
paragraphs.forEach(paragraph => {
paragraph.appendText("\n");
});
}
/**** REPLACE DOUBLE NEW-LINE *******************************************************************/
/**
* Replace Double Newline with Single Newline - ALL
* WARNING: ALL formatting will be removed when using the replace() & setText() methods!
*/
function setTextdoubleNewlineToSingleNewline() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const bodyText = body.getText(); // Get the text from the body.
const textReplacement = bodyText.replace( /\n\n/g, "\n" );
body.setText( textReplacement );
}
/**
* Replace Double Newline with Single Newline - ALL.
* NOTE: A new line is effectively the beginning of a new paragraph.
*/
function doubleNewlineToSingleNewline() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const paragraphs = body.getParagraphs(); // Get the paragraphs in the body.
for (let i = 1; i < paragraphs.length; i++) { // Loop through paragraphs starting from the second one
const previousParagraph = paragraphs[i - 1]; // Get the previous paragraph
const currentParagraph = paragraphs[i]; // Get the current paragraph
if (previousParagraph.getText().trim() === "" && currentParagraph.getText().trim() !== "") { // Check if the previous paragraph is empty and the current paragraph is not
previousParagraph.removeFromParent(); // Remove the previous paragraph from its parent (the body)
}
}
}
/**
* Replace Double Newline with Double Newline + Opening Quotes - ALL
* WARNING: ALL formatting will be removed when using the replace() & setText() methods!
*/
function doubleNewlineToDoubleNewlineDoubleQuotes() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const bodyText = body.getText(); // Get the text from the body.
const textReplacement = bodyText.replace( /\n\n/g, '\n\n"' );
body.setText( textReplacement );
}
/**
* Replace Double Newline with Double Newline + Opening Quotes - ALL.
* NOTE: A new line is effectively the beginning of a new paragraph.
*/
function afterDoubleNewlineAddDoubleQuoteInFront() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const paragraphs = body.getParagraphs(); // Get the paragraphs in the body.
for (let i = 1; i < paragraphs.length; i++) {
const previousParagraph = paragraphs[i - 1];
const currentParagraph = paragraphs[i];
if ( previousParagraph.getText().trim() === "" && currentParagraph.getText().trim() !== "" ) {
currentParagraph.insertText(0, '"');
}
}
}
/**
* Replace Double Newline with Double Newline + Opening & Closing Quotes - ALL.
* NOTE: A new line is effectively the beginning of a new paragraph.
*/
function afterDoubleNewlineWrapWithDoubleQuotes() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const body = doc.getBody(); // Get the document body.
const paragraphs = body.getParagraphs(); // Get the paragraphs in the body.
for (let i = 1; i < paragraphs.length; i++) {
const previousParagraph = paragraphs[i - 1];
const currentParagraph = paragraphs[i];
if ( previousParagraph.getText().trim() === "" && currentParagraph.getText().trim() !== "" ) {
currentParagraph.insertText(0, '"');
currentParagraph.appendText('"');
}
}
}
/**
* Replace Double Newline with Double Newline + Opening & Closing Quotes - Range Element of Selected Text Only.
* NOTE: A new line is effectively the beginning of a new paragraph.
* ATTENTION: This script requires you select a new line AFTER the last paragraph to be effected.
*/
function wrapDoubleQuotesToSelectedParagraphs() {
const doc = DocumentApp.getActiveDocument(); // Get the activeDocument.
const s = doc.getSelection(); // Get the current selection.
if (s) {
const paragraphs = s.getRangeElements()
.map(element => element.getElement())
.filter(element => element.getType() === DocumentApp.ElementType.PARAGRAPH);
for (let i = 1; i < paragraphs.length; i++) {
const previousParagraph = paragraphs[i - 1];
const currentParagraph = paragraphs[i];
if ( previousParagraph.getText().trim() === "" && currentParagraph.getText().trim() !== "" ) {
currentParagraph.insertText(0, '"');
currentParagraph.appendText('"');
}
}
} else {
DocumentApp.getUi().alert('No text selected. Please select some text and try again.');
}
}
/**** REPLACE HARD RETURNS ***********************************************************************/
/**
* Replace ALL hard returns with soft returns
* Prerequisites: Google Docs API is enabled.
* Author: Mr Shane
* Version: 2025-04-24
* @see https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/batchUpdate
* @see https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request#replacealltextrequest
*/
function replaceHardWithSoft() {
const doc = DocumentApp.getActiveDocument();
const requests = [
{
replaceAllText: {
containsText: { text: '\n' }, // find hard return new lines
replaceText: '\v' // replace with soft return new lines
}
}
];
Docs.Documents.batchUpdate({ requests }, doc.getId());
}
/**
* Format rows of a Docs table with alternating colours
* Author: Mr Shane
* Version: 2024-06-14
* Prerequisites: Must have double clicked a cell in a table so that it's highlighted.
* @OnlyCurrentDoc
*/
function onOpen(e) {
// Functions to run onOpen (eg: a custom menu)
customMenu();
}
function customMenu(){
const ui = DocumentApp.getUi();
ui.createMenu("⚠️Admin Menu⚠️")
.addItem("🚧 Banded rows", "changeTableRowColors_")
.addToUi()
}
function changeTableRowColors_() {
const doc = DocumentApp.getActiveDocument();
const selection = doc.getSelection();
const ui = DocumentApp.getUi();
if (!selection) {
ui.alert('Double click a cell in a table, until it is highlighted, then run the script again.');
return;
}
const elements = selection.getRangeElements();
for (let i = 0; i < elements.length; i++) {
let element = elements[i].getElement();
while (element) {
if (element.getType() === DocumentApp.ElementType.TABLE_CELL) {
const table = getTableFromCell_(element);
if (table) {
changeRowColors_(table);
ui.alert('Background color of every second row has been changed to light grey.');
}
return;
}
element = element.getParent();
}
}
ui.alert('You have selected text that is not in a table.\n\nDouble click a cell in a table, until it is highlighted, then run the script again.');
}
function getTableFromCell_(cell) {
let row = cell.getParent();
while (row && row.getType() !== DocumentApp.ElementType.TABLE_ROW) {
row = row.getParent();
}
if (!row) { return null; }
let table = row.getParent();
while (table && table.getType() !== DocumentApp.ElementType.TABLE) {
table = table.getParent();
}
return table;
}
function changeRowColors_(table) {
const numRows = table.getNumRows();
for (let i = 1; i < numRows; i += 2) {
const row = table.getRow(i);
const numCells = row.getNumCells();
for (let j = 0; j < numCells; j++) {
const cell = row.getCell(j);
cell.setBackgroundColor('#D3D3D3'); // Light grey color
}
}
}
/**
* Case sensitive formatting all instances of selected text.
* Author: Mr Shane
* Version: 2025-05-13
*/
// Adds a custom menu on open
function onOpen() {
const ui = DocumentApp.getUi(); // Get UI object
ui.createMenu("⚠️Admin Menu⚠️") // Create a custom menu
.addItem("Bold ALL instances of selected text (Case Sensitive)", "boldAllInstancesOfSelected") // Add bold option
.addItem("Italic ALL instances of selected text (Case Sensitive)", "italicAllInstancesOfSelected") // Add italic option
.addItem("Colour ALL instances of selected text (Case Sensitive)", "colourAllInstancesOfSelected") // Add colour option
.addToUi(); // Add menu to UI
}
// Bolds all instances of selected text
function boldAllInstancesOfSelected() {
handleTextFormatting((text, start, end) => text.setBold(start, end, true), "bolded"); // Apply bold formatting
}
// Italicizes all instances of selected text
function italicAllInstancesOfSelected() {
handleTextFormatting((text, start, end) => text.setItalic(start, end, true), "italicized"); // Apply italic formatting
}
// Colours all instances of selected text
function colourAllInstancesOfSelected() {
const selectedText = getSelectedText(); // Extract selected text
if (!selectedText) return; // Stop if no valid selection
const ui = DocumentApp.getUi(); // Get UI
const response = ui.prompt(`Enter a hex color for "${selectedText}" (e.g., FF5733 or #FF5733):`, ui.ButtonSet.OK_CANCEL); // Ask for color
if (response.getSelectedButton() !== ui.Button.OK) return; // Exit if cancelled
let color = response.getResponseText().trim().toUpperCase(); // Format color input
if (!/^#?[0-9A-F]{6}$/i.test(color)) { // Validate hex format
ui.alert('Invalid hex code! Please enter a valid 6-digit hex color.'); // Alert on error
return;
}
if (!color.startsWith('#')) color = `#${color}`; // Add '#' if missing
handleTextFormatting((text, start, end) => text.setForegroundColor(start, end, color), `recoloured to "${color}"`, selectedText); // Apply color
}
/******************************************* HELPER FUNCTIONS *******************************************/
// Gets selected text from user selection
function getSelectedText() {
const selection = DocumentApp.getActiveDocument().getSelection(); // Get selected content
if (!selection) {
DocumentApp.getUi().alert('Please select the text.'); // Alert if nothing selected
return null;
}
const selectedText = selection.getRangeElements() // Get selected range elements
.filter(el => el.getElement().editAsText()) // Keep only text elements
.map(el => el.getElement().asText().getText().substring(el.getStartOffset(), el.getEndOffsetInclusive() + 1)) // Extract text
.join('');
if (!selectedText.trim()) {
DocumentApp.getUi().alert('No valid text selected.'); // Alert on empty or whitespace
return null;
}
return selectedText; // Return trimmed text
}
// Handles formatting logic for bold, italic, colour
function handleTextFormatting(applyStyleFn, actionLabel, customText) {
const doc = DocumentApp.getActiveDocument(); // Get document
const text = doc.getBody().editAsText(); // Get editable text
const targetText = customText || getSelectedText(); // Use provided or selected text
if (!targetText) return; // Exit if no text
let index = -1, occurrences = 0; // Initialize
while ((index = text.getText().indexOf(targetText, index + 1)) !== -1) { // Loop through all instances
applyStyleFn(text, index, index + targetText.length - 1); // Apply formatting
occurrences++; // Count
}
DocumentApp.getUi().alert(`All ${occurrences} instances of "${targetText}" have been ${actionLabel}.`); // Show result
}
/**
* Hyphenate each paragraph of the selected text.
* Author: Mr Shane
* Version: 2024-04-19
*/
function onOpen() {
const ui = DocumentApp.getUi();
ui.createMenu('⚠️ Admin Menu ⚠️')
.addItem('Hyphenate the selected text', 'addHyphensToSelectedParagraphs')
.addToUi();
}
function addHyphensToSelectedParagraphs() {
const doc = DocumentApp.getActiveDocument(); // Get the active Google Docs document
const selection = doc.getSelection(); // Get the current selection in the document
if (!selection) { // Check if there is no selection
Logger.log("No text selected."); // Log message if no selection is made
return; // Exit the function if no selection is found
}
let isFirst = true; // Flag if the first element in the selection should be skipped. (true = skip, false = include)
selection.getRangeElements().forEach((rangeEl) => { // Loop through each element in the selection
const el = rangeEl.getElement(); // Get the element of the range (text or paragraph)
if ([DocumentApp.ElementType.TEXT, DocumentApp.ElementType.PARAGRAPH].includes(el.getType())) { // IF ElementType is TEXT or PARAGRAPH...
const textContent = el.getType() === DocumentApp.ElementType.TEXT ? el.asText().getText() : el.asParagraph().getText(); // Get the text content of the element
if (!isFirst && textContent.trim()) { // If it's not the first element and text is not empty
const newText = `- ${textContent}`; // Add hyphen space to the text
if (el.getType() === DocumentApp.ElementType.TEXT) { // IF ElementType is TEXT...
el.asText().editAsText().replaceText(textContent, newText); // Modify text element
} else if (el.getType() === DocumentApp.ElementType.PARAGRAPH) { // IF ElementType is PARAGRAPH...
el.asParagraph().editAsText().replaceText(textContent, newText); // Modify paragraph element
}
}
isFirst = false; // Mark that the element has been processed
}
});
}
/**
* Identify each tab that contains a search string
* Author: Mr Shane
* Version: 2025-05-25
* @see https://developers.google.com/apps-script/guides/docs/tabs
* @OnlyCurrentDoc
*/
function onOpen() {
DocumentApp.getUi() // Get the user interface for the document
.createMenu("⚠️Admin Menu⚠️") // Create a custom menu named "Admin Menu"
.addItem("Search Tabs for a String", "searchTabsForString") // Add a menu item to search for the text string
.addToUi(); // Add the menu to the UI
}
function searchTabsForString() {
const ui = DocumentApp.getUi(); // Get the UI object for user interaction
const prompt = ui.prompt("Search Tabs", "Enter a string to search for:", ui.ButtonSet.OK_CANCEL); // Show prompt to ask for search string
const button = prompt.getSelectedButton(); // Get which button the user clicked (OK or Cancel)
const searchText = prompt.getResponseText().trim(); // Get the user’s input text and trim whitespace
if (button !== ui.Button.OK || !searchText) return; // Exit if user didn't press OK or entered empty text
const doc = DocumentApp.getActiveDocument(); // Get the active document
const allTabs = getAllTabs(doc); // Retrieve a flat list of all tabs and subtabs in the document
const matches = []; // Initialize array to store titles of tabs containing the search string
for (const { id, title } of allTabs) { // Loop over each tab's id and title
const tab = doc.getTab(id).asDocumentTab(); // Get the tab as a DocumentTab object
const text = tab.getBody().getText(); // Get the full text content of the tab
if (text.includes(searchText)) { // Check if the tab's text contains the search string
matches.push(title); // Add the tab title to matches if found
}
}
const message = matches.length // Prepare the message for the alert dialog
? `Tabs containing "${searchText}":\n\n` + matches.join("\n") // List tabs if any matches
: `No tabs contain the string "${searchText}".`; // Otherwise, say no tabs found
ui.alert("Search Results", message, ui.ButtonSet.OK); // Show alert with search results
}
/**
* Returns a flat list of all tabs (ID and Title) in the document, in the order they appear.
*/
function getAllTabs(doc) {
const allTabs = []; // Initialize an array to collect all tabs
for (const tab of doc.getTabs()) { // Loop through all top-level tabs
addCurrentAndChildTabs(tab, allTabs); // Recursively add tab and its children
}
return allTabs; // Return the collected tabs
}
/**
* Recursively adds the current tab and its child tabs.
*/
function addCurrentAndChildTabs(tab, allTabs) {
allTabs.push({ id: tab.getId(), title: tab.getTitle() }); // Add the current tab's ID and title
for (const childTab of tab.getChildTabs()) { // Loop through child tabs
addCurrentAndChildTabs(childTab, allTabs); // Recursively add each child tab
}
}
/**
* Inserts a styled date, time, and user email at the end of the paragraph (or element) where the cursor is currently positioned.
* Author: Mr Shane
* Version: 2025-08-21
*/
function onOpen() {
DocumentApp.getUi().createMenu("Timestamp") // Create a new menu item.
.addItem("Add TimeStamp", "insertTimestamp") // Add a button that calls the insertTimestamp function.
.addToUi(); // Add the menu to the document's UI.
}
function insertTimestamp() {
const doc = DocumentApp.getActiveDocument();
const cursor = doc.getCursor(); // Get the user's cursor position.
const element = cursor.getElement().asText(); // Get the text element at the cursor.
const originalTextLength = element.getText().length; // Get the length of the text before adding anything.
const date = new Date();
const email = Session.getActiveUser().getEmail();
const timestampText = ` -Complete: ${date.toDateString()} ${date.toLocaleTimeString()} | ${email} `; // Construct the timestamp string.
const timestampStyle = { // Define the visual style for the timestamp.
[DocumentApp.Attribute.FONT_FAMILY]: "Merriweather",
[DocumentApp.Attribute.ITALIC]: true,
[DocumentApp.Attribute.BOLD]: true,
[DocumentApp.Attribute.BACKGROUND_COLOR]: "#eeeeee",
[DocumentApp.Attribute.FOREGROUND_COLOR]: "#34a853",
};
// The end index for styling is the original length plus the new text length, minus 2 to avoid styling the final space.
const stylingEndIndex = originalTextLength + timestampText.length - 2;
element.appendText(timestampText) // Add the timestamp text to the element.
.setAttributes(originalTextLength, stylingEndIndex, timestampStyle); // Apply the style to the newly added text.
}
/** Jump to end of Tab or Document.
* Author: Mr Shane.
* Version: 2024-11-24
* @see https://developers.google.com/apps-script/reference/document/document
* @see https://developers.google.com/apps-script/reference/document/document-tab
* @see https://developers.google.com/apps-script/reference/document/tab
*/
function onOpen() {
const ui = DocumentApp.getUi(); // Get the UI of the active document.
ui.createMenu("⚠️Admin Menu⚠️") // The name of the custom menu.
.addItem("Jump to End of Current Tab", "jumpToEndCurrentTab")
.addItem("Jump to End of Document", "jumpToEndOfLastTab")
.addToUi(); // Add the menu to the UI.
}
/**
* Jump to the end of the current tab.
*/
function jumpToEndCurrentTab(doc){
const doc = DocumentApp.getActiveDocument(); // Get the active document.
const body = doc.getBody(); // Get the body of the current tab.
const numChildren = body.getNumChildren();// Get the number of elements in the body.
const lastElement = body.getChild(numChildren - 1); // Get the last element in the body (the last paragraph or content).
const position = doc.newPosition(lastElement,0); // Get the position at the end of the last element.
doc.setCursor(position); // Set the cursor in the document body at the end of the last element.
}
/**
* Jump to the end of the last tab.
*/
function jumpToEndOfLastTab(doc) {
const doc = DocumentApp.getActiveDocument(); // Get the active document.
const tabs = doc.getTabs(); // Get all first-level tabs.
let lastTab = tabs[tabs.length - 1]; // Start with the last top-level tab.
while (lastTab.getChildTabs().length > 0) {
lastTab = lastTab.getChildTabs()[lastTab.getChildTabs().length - 1]; // Go to the last child tab.
}
doc.setActiveTab(lastTab.getId()); // Activate the last tab.
const documentTab = lastTab.asDocumentTab(); // Get the last tab as a DocumentTab.
const body = documentTab.getBody(); // Get the body of the last tab.
const numChildren = body.getNumChildren();// Get the number of elements in the body.
const lastElement = body.getChild(numChildren - 1); // Get the last element in the body (the last paragraph or content).
const position = doc.newPosition(lastElement,0); // Get the position at the end of the last element.
doc.setCursor(position); // Set the cursor in the document body at the end of the last element.
}
/** Log comments and quotedFileContent text values.
* Note: The results of this script don't include replies to comments.
* Author: Mr Shane
* Version: 2024-10-16
* Prerequisites: Drive API v3
* @see https://developers.google.com/drive/api/reference/rest/v3/comments
* @see https://developers.google.com/drive/api/reference/rest/v3/comments/list
*/
function getAllCommentsFromGoogleDoc() {
const documentId = DocumentApp.getActiveDocument().getId(); // Get the fileId of the active Doc.
try {
const response = Drive.Comments.list(documentId, { // Retrieve the list of comments for the specified Google Docs file.
fields: "comments(content, resolved, quotedFileContent)" // The fields parameter specifies which fields to return from the comments.
});
const commentsList = response.comments || []; // Extract the comments array from the response; if there are no comments, default to an empty array.
Logger.log(`Total comments retrieved: ${commentsList.length}`); // Log the total number of comments retrieved.
if (commentsList.length > 0) { // IF there are any comments to process, then...
commentsList.forEach(comment => { // Loop through each comment in the comments list, and...
const resolvedStatus = comment.resolved !== undefined ? comment.resolved : 'undefined'; // Determine if the comment is resolved and handle undefined status.
Logger.log(`Comment content: ${comment.content}, Resolved: ${resolvedStatus}`); // Log the comment content and its resolved status.
const quotedFileContent = comment.quotedFileContent; // Access the quotedFileContent.
let quotedContentOutput; // Prepare a structured output for the quotedFileContent.
if (quotedFileContent && quotedFileContent.mimeType === "text/html") { // Check the mimeType and provide appropriate messages
// If the mimeType is "text/html", log the actual quoted content
quotedContentOutput = {
mimeType: quotedFileContent.mimeType,
value: quotedFileContent.value
};
} else {
// If the mimeType is NOT "text/html", provide a different message
quotedContentOutput = {
mimeType: quotedFileContent ? quotedFileContent.mimeType : "N/A",
value: "Quoted file content is not in text format."
};
}
Logger.log(`Full comment object: ${JSON.stringify({ quotedFileContent: quotedContentOutput })}`); // Log the structured comment object.
});
} else {
Logger.log("No comments found."); // Log a message if no comments are found.
}
} catch (error) {
Logger.log("Error retrieving comments: " + error.message); // Log an error message if the comment retrieval process fails.
}
}
/**
* Logs the activity history of the active Google Doc.
* Author: Mr Shane
* Version 2025-09-03
* Prerequisites: Drive Activity API v2
* @see https://developers.google.com/workspace/drive/activity/v2/reference/rest/v2/activity/action
*/
function logDocumentActivity() {
const doc = DocumentApp.getActiveDocument();
const fileId = doc.getId();
try {
const response = DriveActivity.Activity.query({
'itemName': 'items/' + fileId,
'pageSize': 20, // Adjust as needed
});
const activities = response.activities;
if (activities && activities.length > 0) {
Logger.log('Timestamp, Action, Actors');
activities.forEach(function(activity) {
const primaryAction = activity.primaryActionDetail;
const actionType = primaryAction ? Object.keys(primaryAction)[0] : 'Unknown';
const formattedAction = actionType.charAt(0).toUpperCase() + actionType.slice(1);
const actors = activity.actors.map(actor => {
if (actor.user && actor.user.knownUser) {
return actor.user.knownUser.personName;
}
return 'Unknown user';
}).join(', ');
const date = new Date(activity.timestamp);
const formattedTimestamp = Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
Logger.log(`${formattedTimestamp}, ${formattedAction}, ${actors}`);
});
} else {
Logger.log('No activities found for this document.');
}
} catch (e) {
Logger.log('An error occurred: ' + e.toString());
}
}
/**
* Move selected text under a nominated title/heading
* Author: Mr Shane
* Version: 2024-01-11
* @OnlyCurrentDoc
*/
/** START ONOPEN */
function onOpen() {
customMenu();
}
/** END ONOPEN */
/** CUSTOM MENU */
function customMenu(){
const ui = DocumentApp.getUi();
ui.createMenu("⚠️Admin Menu⚠️")
.addItem("Move to Title1", "moveTextToTitle1") // Modify "Move to Xxxxxxxxxxxxx" to be what you want it to read in the menu.
.addItem("Move to Title2", "moveTextToTitle2") // Do the same for each row of addItem, and remove or add more if required.
.addItem("Move to Title3", "moveTextToTitle3")
.addItem("Move to Title4", "moveTextToTitle4")
.addToUi()
}
/** END CUSTOM MENU */
/** USER CONFIGURED VARIABLES - Configure your titles here by changing the red text. */
title1 = "This is title 1";
title2 = "This is title 2";
title3 = "This is title 3";
title4 = "This is title 4";
/** END USER CONFIGURED VARIABLES e*/
/** ONLY 2 functions have been provided below, create more as required for each heading/title. */
function moveTextToTitle1() {
const doc = DocumentApp.getActiveDocument(); // Get the current document.
const selection = doc.getSelection(); // Get the selected text range.
if (selection) { // Check if there is a selection.
const body = doc.getBody(); // Get the body of the document.
let titleParagraph = null; // Create an empty titleParagraph variable/array.
let paragraphs = body.getParagraphs();
for (let i = 0; i < paragraphs.length; i++) { // Iterate through paragraphs...
if (paragraphs[i].getText() === title1 && paragraphs[i].getHeading() !== DocumentApp.ParagraphHeading.NORMAL) { // Find paragrapgs that are NOT normal heading.
titleParagraph = paragraphs[i];
break;
}
}
if (titleParagraph) { // If a valid title paragraph is found...
const newParagraph = body.insertParagraph(body.getChildIndex(titleParagraph.getNextSibling()), ''); // Create a new paragraph.
const elements = selection.getRangeElements(); // Move the selected text to the new paragraph.
for (let i = 0; i < elements.length; i++) {
const element = elements[i].getElement();
newParagraph.appendText(element.getText());
element.clear(); // Clear the text from the original location
}
Logger.log("Selected text moved to a new paragraph under '"+title1+"'"); // Log a message to confirm the operation.
} else {
Logger.log("No valid title '"+title1+"' found in the document"); // Log a message if a valid title is not found.
}
} else {
Logger.log("No text selected"); // Log a message if there is no selection.
}
}
function moveTextToTitle2() {
const doc = DocumentApp.getActiveDocument(); // Get the current document.
const selection = doc.getSelection(); // Get the selected text range.
if (selection) { // Check if there is a selection.
const body = doc.getBody(); // Get the body of the document.
let titleParagraph = null; // Create an empty titleParagraph variable/array.
let paragraphs = body.getParagraphs();
for (let i = 0; i < paragraphs.length; i++) { // Iterate through paragraphs...
if (paragraphs[i].getText() === title2 && paragraphs[i].getHeading() !== DocumentApp.ParagraphHeading.NORMAL) { // Find paragrapgs that are NOT normal heading.
titleParagraph = paragraphs[i];
break;
}
}
if (titleParagraph) { // If a valid title paragraph is found...
const newParagraph = body.insertParagraph(body.getChildIndex(titleParagraph.getNextSibling()), ''); // Create a new paragraph.
const elements = selection.getRangeElements(); // Move the selected text to the new paragraph.
for (let i = 0; i < elements.length; i++) {
const element = elements[i].getElement();
newParagraph.appendText(element.getText());
element.clear(); // Clear the text from the original location
}
Logger.log("Selected text moved to a new paragraph under '"+title2+"'"); // Log a message to confirm the operation.
} else {
Logger.log("No valid title '"+title2+"' found in the document"); // Log a message if a valid title is not found.
}
} else {
Logger.log("No text selected"); // Log a message if there is no selection.
}
}
/**
* NEW version written in response to the new Tabs feature.
* Documentation date: 2024-08-29
* @see https://developers.google.com/apps-script/guides/docs/tabs#changes-to-document-class-structure
*/
function onOpen() {
customMenu();
}
/** END ONOPEN */
/** CUSTOM MENU */
function customMenu(){
const ui = DocumentApp.getUi();
ui.createMenu("⚠️Admin Menu⚠️")
.addItem("Move to Title1", "moveTextToTitle1")
.addItem("Move to Title2", "moveTextToTitle2")
.addItem("Move to Title3", "moveTextToTitle3")
.addItem("Move to Title4", "moveTextToTitle4")
.addToUi();
}
/** END CUSTOM MENU */
/** USER CONFIGURED VARIABLES - Configure your titles here */
const title1 = "This is title 1";
const title2 = "This is title 2";
const title3 = "This is title 3";
const title4 = "This is title 4";
/** END USER CONFIGURED VARIABLES */
/** ONLY 2 functions have been provided below, create more as required for each heading/title. */
function moveTextToTitle1() {
const doc = DocumentApp.getActiveDocument(); // Get the current document.
const documentTab = doc.getActiveTab().asDocumentTab(); // Access the active tab as DocumentTab
const selection = doc.getSelection(); // Get the selected text range.
if (selection) { // Check if there is a selection.
const body = documentTab.getBody(); // Get the body of the active tab.
let titleParagraph = null; // Create an empty titleParagraph variable/array.
let paragraphs = body.getParagraphs();
for (let i = 0; i < paragraphs.length; i++) { // Iterate through paragraphs...
if (paragraphs[i].getText() === title1 && paragraphs[i].getHeading() !== DocumentApp.ParagraphHeading.NORMAL) {
titleParagraph = paragraphs[i]; // Find paragraphs that are NOT normal heading.
break;
}
}
if (titleParagraph) { // If a valid title paragraph is found...
const newParagraph = body.insertParagraph(body.getChildIndex(titleParagraph.getNextSibling()), ''); // Create a new paragraph.
const elements = selection.getRangeElements(); // Move the selected text to the new paragraph.
for (let i = 0; i < elements.length; i++) {
const element = elements[i].getElement();
newParagraph.appendText(element.getText());
element.clear(); // Clear the text from the original location.
}
Logger.log("Selected text moved to a new paragraph under '"+title1+"'"); // Log a message to confirm the operation.
} else {
Logger.log("No valid title '"+title1+"' found in the document"); // Log a message if a valid title is not found.
}
} else {
Logger.log("No text selected"); // Log a message if there is no selection.
}
}
function moveTextToTitle2() {
const doc = DocumentApp.getActiveDocument();
const documentTab = doc.getActiveTab().asDocumentTab(); // Access the active tab
const selection = doc.getSelection();
if (selection) {
const body = documentTab.getBody(); // Use the active tab body
let titleParagraph = null;
let paragraphs = body.getParagraphs();
for (let i = 0; i < paragraphs.length; i++) {
if (paragraphs[i].getText() === title2 && paragraphs[i].getHeading() !== DocumentApp.ParagraphHeading.NORMAL) {
titleParagraph = paragraphs[i];
break;
}
}
if (titleParagraph) {
const newParagraph = body.insertParagraph(body.getChildIndex(titleParagraph.getNextSibling()), '');
const elements = selection.getRangeElements();
for (let i = 0; i < elements.length; i++) {
const element = elements[i].getElement();
newParagraph.appendText(element.getText());
element.clear();
}
Logger.log("Selected text moved to a new paragraph under '"+title2+"'");
} else {
Logger.log("No valid title '"+title2+"' found in the document");
}
} else {
Logger.log("No text selected");
}
}
/**
* Recolour all instances of the selected text.
* Author: Mr Shane
* Version: 2025-02-24
*/
function onOpen(){
DocumentApp.getUi().createMenu("⚠️Admin Menu⚠️")
.addItem("Find & Replace: Recolour to red", "findAndReplaceSetColorToRed")
.addToUi();
}
function findAndReplaceSetColorToRed() {
const colour = "#FF0000"; // Configure the new colour for the selected text.
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
const selection = doc.getSelection();
if (!selection) return Logger.log("No text selected.");
const selectedText = selection.getRangeElements()
.map(e => e.getElement().asText()?.getText().substring(e.getStartOffset(), e.getEndOffsetInclusive() + 1))
.join("")
.trim();
if (!selectedText) return Logger.log("Empty or non-text selection.");
let searchResult = body.findText(selectedText);
while (searchResult) {
searchResult.getElement().asText().setForegroundColor(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(), colour);
searchResult = body.findText(selectedText, searchResult);
}
}
/**
* Remove all linked images.
* Author: Mr Shane
* Version: 2025-03-30
*/
function removeLinkedImages() {
const body = DocumentApp.getActiveDocument().getBody();
(function removeImages(element) {
for (let i = element.getNumChildren() - 1; i >= 0; i--) {
const child = element.getChild(i);
if (child.getType() === DocumentApp.ElementType.INLINE_IMAGE && child.getAttributes()[DocumentApp.Attribute.LINK_URL]) {
element.removeChild(child);
} else if (child.getType() === DocumentApp.ElementType.PARAGRAPH || child.getType() === DocumentApp.ElementType.TABLE_CELL) {
removeImages(child);
}
}
})(body);
}
/**
* Replace placeholder text with sequential numbers across different scopes in a Google Doc.
* Author: Mr Shane
* Version: 2025-05-12
*/
const PLACEHOLDER = '\\[\\[n\\]\\]'; // Configure the placeholder as a regex string. In this case it's [[n]]
/**
* Adds a custom menu to the Google Docs UI on open.
*/
function onOpen() {
const ui = DocumentApp.getUi(); // Get the UI of the document
ui.createMenu("Replace the placeholder") // Create a new menu
.addItem("Active Tab Only", "replacePlaceholdersInActiveTabOnly") // Add menu item for active tab only
.addItem("Active Tab and Children", "replacePlaceholdersInActiveTabAndChildren") // Add menu item for active + children
.addItem("All Tabs in the file", "replacePlaceholdersInAllTabsInTheFile") // Add menu item for all tabs
.addItem("Each Tab and Child tab", "replacePlaceholdersInEachTabAndChildtab") // Add menu item for per-tab reset
.addItem("Each Tab and Child tab Group", "replacePlaceholdersInEachTabAndChildtabGroup") // Add menu item for group reset
.addToUi(); // Show menu
}
/********************************* REPLACEMENT FUNCTIONS ************************************/
/**
* Replaces the placeholder in the currently active tab only.
*/
function replacePlaceholdersInActiveTabOnly() {
const body = DocumentApp.getActiveDocument().getBody(); // Get the body of the active document
replacePlaceholdersInBody(body, 1); // Start replacing from 1
}
/**
* Replaces the placeholder in the active tab (or subtab) and its child tabs using a shared counter.
*/
function replacePlaceholdersInActiveTabAndChildren() {
const doc = DocumentApp.getActiveDocument(); // Get active document
const activeTab = doc.getActiveTab(); // Get active tab
const tabs = []; // Array to collect tabs
addCurrentAndChildTabs(activeTab, tabs); // Populate with active and child tabs
replacePlaceholdersInTabs(doc, tabs); // Replace across all collected tabs
}
/**
* Replaces the placeholder in all tabs (top-level and child tabs) using a shared counter.
*/
function replacePlaceholdersInAllTabsInTheFile() {
const doc = DocumentApp.getActiveDocument(); // Get active document
const tabs = getAllTabs(doc); // Get all tabs
replacePlaceholdersInTabs(doc, tabs); // Replace across all
}
/**
* Replaces the placeholder in each tab and child tab individually, resetting count for each tab or subtab.
*/
function replacePlaceholdersInEachTabAndChildtab() {
const doc = DocumentApp.getActiveDocument(); // Get active document
const allTabs = getAllTabs(doc); // Get all tabs
let lastParent = null, count = 1; // Track parent tab and counter
for (const tab of allTabs) {
if (tab.title !== lastParent) { count = 1; lastParent = tab.title; } // Reset if new parent tab
const body = doc.getTab(tab.id).asDocumentTab().getBody(); // Get tab body
count = replacePlaceholdersInBody(body, count); // Replace and update counter
}
}
/**
* Replaces the placeholder per group (parent tab + child tabs), resetting count for each group.
*/
function replacePlaceholdersInEachTabAndChildtabGroup() {
const doc = DocumentApp.getActiveDocument(); // Get active document
for (const parent of doc.getTabs()) {
let count = 1; // Reset counter for group
const group = []; // Array to hold parent and children
addCurrentAndChildTabs(parent, group); // Get all in group
for (const tab of group) {
const body = doc.getTab(tab.id).asDocumentTab().getBody(); // Get tab body
count = replacePlaceholdersInBody(body, count); // Replace and update counter
}
}
}
/************************************ HELPER FUNCTIONS **************************************/
/**
* Replaces the placeholder in a given Document Body, starting from a specific number.
* @param {Body} body - The body of the document tab
* @param {number} start - The number to start from
* @returns {number} - The next number after the last replacement
*/
function replacePlaceholdersInBody(body, start) {
let found, count = start; // Initialize variables
while ((found = body.findText(PLACEHOLDER)) !== null) { // Search for placeholder
found.getElement().asText().replaceText(PLACEHOLDER, String(count++)); // Replace and increment
}
return count; // Return updated counter
}
/**
* Replaces the placeholder in a set of tabs with a shared counter.
* @param {Document} doc - The active Google Document
* @param {Array} tabs - Array of tab info objects with id and title
*/
function replacePlaceholdersInTabs(doc, tabs) {
let count = 1; // Start from 1
for (const tab of tabs) {
const body = doc.getTab(tab.id).asDocumentTab().getBody(); // Get body of each tab
count = replacePlaceholdersInBody(body, count); // Replace placeholders
}
}
/**
* Recursively collects all tabs and their child tabs from the document.
* @param {Document} doc - The active Google Document
* @returns {Array} Array of tab info objects
*/
function getAllTabs(doc) {
const tabs = []; // Store all tabs
for (const tab of doc.getTabs()) addCurrentAndChildTabs(tab, tabs); // Add top and child tabs
return tabs; // Return collected
}
/**
* Recursively adds a tab and its children to the provided array.
* @param {Tab} tab - The current tab to process
* @param {Array} collected - Array to hold tab info
*/
function addCurrentAndChildTabs(tab, collected) {
collected.push({ id: tab.getId(), title: tab.getTitle() }); // Add this tab
for (const child of tab.getChildTabs()) addCurrentAndChildTabs(child, collected); // Add children recursively
}
/**
* Select all normal text in the document, at the same time.
* Author: Mr Shane
* Version: 2024-01-13
*/
function selectAllNormalText() {
const doc = DocumentApp.getActiveDocument(); // Get the active Google Docs document.
const body = doc.getBody(); // Get the document body.
const rangeBuilder = doc.newRange(); // Create a range builder.
for (let i = 0; i < body.getNumChildren(); i++) { // Iterate through each paragraph
const child = body.getChild(i);
// IF the element is a paragraph and its heading style is normal...
if (child.getType() === DocumentApp.ElementType.PARAGRAPH &&
child.asParagraph().getHeading() === DocumentApp.ParagraphHeading.NORMAL) {
rangeBuilder.addElement(child); // Add the paragraph to the range builder.
}
}
const finalRange = rangeBuilder.build(); // Build the final range and set the selection.
doc.setSelection(finalRange); // Select the Normal text.
}
/**
* Select all normal text in the document, at the same time.
* Author: Mr Shane
* Version: 2024-08-29
* NEW version written in response to the new Tabs feature.
* Documentation date: 2024-08-29
* @see https://developers.google.com/apps-script/guides/docs/tabs#changes-to-document-class-structure
*/
function selectAllNormalText() {
const doc = DocumentApp.getActiveDocument(); // Get the active Google Docs document.
const documentTab = doc.getActiveTab().asDocumentTab(); // Access the active tab as DocumentTab
const body = documentTab.getBody(); // Get the document body for the active tab.
const rangeBuilder = documentTab.newRange(); // Create a range builder for the active tab.
// Iterate through each paragraph in the document's body
for (let i = 0; i < body.getNumChildren(); i++) {
const child = body.getChild(i);
// IF the element is a paragraph and its heading style is normal...
if (child.getType() === DocumentApp.ElementType.PARAGRAPH &&
child.asParagraph().getHeading() === DocumentApp.ParagraphHeading.NORMAL) {
rangeBuilder.addElement(child); // Add the paragraph to the range builder.
}
}
const finalRange = rangeBuilder.build(); // Build the final range.
documentTab.setSelection(finalRange); // Set the selection for the active tab.
}
/**
* Select all text (in the current Tab) with the given format.
* Author: Mr Shane
* Version: 2024-12-02
*/
function onOpen() {
DocumentApp.getUi() // Access the document's user interface.
.createMenu("⚠️Admin Menu⚠️") // Create a custom menu named "Strikethrough Selector".
.addItem("Select All Strikethrough Text", "selectAllStrikethroughText") // Add a menu item linked to the main function.
.addToUi(); // Add the menu to the document's UI.
}
function selectAllStrikethroughText() {
const doc = DocumentApp.getActiveDocument(); // Get the active document.
const range = doc.newRange(); // Initialize a new range for selection.
doc.getBody().getParagraphs().forEach(p => { // Loop through all paragraphs in the document.
const text = p.editAsText(); // Get the paragraph as editable text.
let start = null; // Track the start of strikethrough ranges.
[...text.getText(), null].forEach((_, i) => { // Loop through characters, with `null` as a virtual end.
if (i < text.getText().length && text.isStrikethrough(i)) { // Check for strikethrough and valid index.
if (start === null) start = i; // Mark the start of a strikethrough range if not already set.
} else if (start !== null) { // If strikethrough ends and a range is active.
range.addElementsBetween(text, start, text, i - 1); // Add the range to the selection.
start = null; // Reset the start to indicate no active range.
}
});
});
doc.setSelection(range.build()); // Set the final selection in the document.
}
/**
* Jump to the end of the document when the file is opened.
* Author: Mr Shane
* Version: 2024-11-24
* @see https://developers.google.com/apps-script/reference/document/document
* @see https://developers.google.com/apps-script/reference/document/document-tab
* @see https://developers.google.com/apps-script/reference/document/tab
*/
function onOpen(e){
const doc = e.source; // Get the active document.
const tabs = doc.getTabs(); // Get all first-level tabs.
let lastTab = tabs[tabs.length - 1]; // Start with the last top-level tab.
while (lastTab.getChildTabs().length > 0) {
lastTab = lastTab.getChildTabs()[lastTab.getChildTabs().length - 1]; // Go to the last child tab.
}
doc.setActiveTab(lastTab.getId()); // Activate the last tab.
const documentTab = lastTab.asDocumentTab(); // Get the last tab as a DocumentTab.
const body = documentTab.getBody(); // Get the body of the last tab.
const numChildren = body.getNumChildren();// Get the number of elements in the body.
const lastElement = body.getChild(numChildren - 1); // Get the last element in the body (the last paragraph or content).
const position = doc.newPosition(lastElement,0); // Get the position at the end of the last element.
doc.setCursor(position); // Set the cursor in the document body at the end of the last element.
}
/**
* onOpen(e) Update the header with the filename.
* Prerequisites: Header has been added via 'Insert > Headers and footers > Header'.
*/
function onOpen(e) {
const doc = e.source;
const fileName = doc.getName();
const oldheader = doc.getHeader();
oldheader.clear();
const newheader = oldheader.appendParagraph(fileName);
const style = {};
style[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.CENTER;
style[DocumentApp.Attribute.FONT_FAMILY] = 'Calibri';
style[DocumentApp.Attribute.FONT_SIZE] = 18;
style[DocumentApp.Attribute.BOLD] = true;
newheader.setAttributes(style);
const oldheadertext = oldheader.editAsText(); // Removed the newline that remains after appending the newheader to the cleared oldheader
oldheadertext.setText(oldheadertext.getText().replace(/\n/g, ''));
}