"Page Break (by String Detection)" Apps Script for Google Docs
Background
- While editing a very long document, I was repeatedly using
⌘+Enter
to insert page breaks, which was taking too much time. - Since I couldn't find a service like a Chrome extension that offered this functionality, I decided to create it myself.
Use Cases
- Streamline the editing of very long Google Docs, such as books.
- Inserts a page break before any paragraph that starts with one of several specified strings (prefix matching).
How to Use
- Open a Google Doc and open the script editor from "Extensions" > "Apps Script".
- Paste the following script into the editor.
- Edit the configuration section within the script, then save and run it.
How to Configure the Script
After pasting the script, change the value of PAGEBREAK_PREFIXES
at the top of the code to an array of strings you want to use as page break markers.
- This setting works with prefix matching.
- The array must contain one or more marker strings (e.g.,
["Chapter ", "Figure-"]
). - It is case-sensitive.
- More detailed instructions are provided in the comments within the code.
Example: const PAGEBREAK_PREFIXES = ["Problem Statement (Japan", "Chapter"];
insert-page-break-by-string.js
// --- Configuration Section ---
/**
* @fileoverview A script that inserts a page break before any paragraph that starts with one of several specified strings.
*
* ## Prerequisites for Running
*
* ・Important: Select the function to run > choose [ insertPageBreakAtPrefixMarkers ].
*
*
* ## How to Configure the Marker Strings
*
* 1. Change the value of **PAGEBREAK_PREFIXES** to an **array** of **prefixes** (the beginning part) of the strings you want to use as page break markers.
* - This system searches for text that starts with a matching prefix.
* - The array should be enclosed in square brackets `[]`, and each prefix string should be enclosed in double `"` or single `'` quotes, separated by commas `,`.
* - **Number of elements:** The array must contain **at least one** valid prefix string. You can set as many as you like.
* - **Valid Prefixes:**
* - Empty strings (`""`) or strings containing only whitespace (`" "`) are **ignored** and will not be used as markers.
* - It is case-sensitive.
*
* 2. **Configuration Examples:**
* - **For a single prefix:**
* Example: `const PAGEBREAK_PREFIXES = ["PAGEBREAK"];`
* This will insert a page break before any paragraph that starts with "PAGEBREAK" (e.g., "PAGEBREAK" or "PAGEBREAK Section Title").
*
* - **For two prefixes:**
* Example: `const PAGEBREAK_PREFIXES = ["Chapter ", "Appendix "];`
* This will insert a page break before paragraphs starting with "Chapter " (with a space) or "Appendix " (with a space), such as "Chapter 1 Introduction" or "Appendix A References".
*
* - **For three or more prefixes:**
* Example: `const PAGEBREAK_PREFIXES = ["Figure-", "Table-", "List "];`
* This will insert a page break before paragraphs starting with "Figure-", "Table-", or "List " (with a space), such as "Figure-1 System Architecture", "Table-2 Parameter List", or "List 1 Procedures".
*
* - **Key Points:**
* - Whether you include a space in the prefix affects what it matches.
* Example: `"Chapter"` matches "ChapterEnd", but `"Chapter "` does not.
* - You can add as many strings as you need, separated by commas.
*
* 3. **How it works:**
* - A page break will be inserted **immediately before** any paragraph whose text **starts with** one of the **valid prefixes** in this array.
* - It only checks the text of Paragraph elements.
*
* 4. After making changes, **be sure to save the script file** (click the floppy disk icon).
*/
const PAGEBREAK_PREFIXES = ["Problem Statement (Japan", "Chapter"]; // Example: ["Figure-", "Table-"] or ["MyMarker"]. See comments above for how to configure.
// --- End of Configuration Section ---
// --- Global Constants ---
const SCRIPT_NAME_MULTI_PREFIX = 'Custom Page Break (Multi-Prefix)';
const SCRIPT_ERROR_MENU_NAME_MULTI_PREFIX = `${SCRIPT_NAME_MULTI_PREFIX} (Config Error)`;
/**
* Extracts valid (non-empty and not just whitespace) strings from the configured prefixes array.
* @param {any} prefixes The configured PAGEBREAK_PREFIXES value.
* @returns {string[]} An array of valid prefix strings.
*/
function getValidPrefixes(prefixes) {
if (!Array.isArray(prefixes)) {
return []; // Return an empty array if it's not an array
}
// Use filter to extract strings that are not empty after trimming
return prefixes.filter(prefix => typeof prefix === 'string' && prefix.trim().length > 0);
}
/**
* Adds a custom menu when the document is opened.
*/
function onOpen_MultiPrefix() {
try {
const validPrefixes = getValidPrefixes(PAGEBREAK_PREFIXES);
// If there are no valid prefixes, show an error menu
if (validPrefixes.length === 0) {
Logger.log(`onOpen_MultiPrefix: No valid page break prefixes configured (${JSON.stringify(PAGEBREAK_PREFIXES)}). Showing error menu.`);
showConfigurationErrorMenu_MultiPrefix("No valid page break prefixes configured");
return;
}
// Generate the menu item text (adjusting the displayed prefixes)
let menuItemText = `Break page at prefixes (${validPrefixes.slice(0, 2).map(p => `"${p}"`).join(', ')}...)`;
if (validPrefixes.length === 1) {
menuItemText = `Break page at prefix "${validPrefixes[0]}"`;
} else if (validPrefixes.length === 2) {
menuItemText = `Break page at prefixes (${validPrefixes.map(p => `"${p}"`).join(', ')})`;
}
DocumentApp.getUi()
.createMenu(SCRIPT_NAME_MULTI_PREFIX)
.addItem(menuItemText, 'insertPageBreakAtPrefixMarkers')
.addToUi();
Logger.log(`onOpen_MultiPrefix: Menu added successfully (Valid prefixes: ${JSON.stringify(validPrefixes)}).`);
} catch (e) {
Logger.log(`onOpen_MultiPrefix: Error adding menu: ${e}\n${e.stack}`);
showConfigurationErrorMenu_MultiPrefix("Error adding menu");
}
}
/**
* Adds an error menu item if there is a configuration error.
* @param {string} reason The reason for the error.
*/
function showConfigurationErrorMenu_MultiPrefix(reason) {
try {
DocumentApp.getUi()
.createMenu(SCRIPT_ERROR_MENU_NAME_MULTI_PREFIX)
.addItem(`Check settings (${reason})`, 'showConfigurationError_MultiPrefix')
.addToUi();
} catch (e) {
Logger.log(`showConfigurationErrorMenu_MultiPrefix: Further error while displaying error menu: ${e}`);
}
}
/**
* Alert function to display when there is a configuration error.
*/
function showConfigurationError_MultiPrefix() {
let currentSetting = "Undefined or inaccessible";
try {
// Check if the setting is an array before trying to stringify it
currentSetting = typeof PAGEBREAK_PREFIXES !== 'undefined'
? (Array.isArray(PAGEBREAK_PREFIXES) ? JSON.stringify(PAGEBREAK_PREFIXES) : `Invalid setting value (${PAGEBREAK_PREFIXES})`)
: "Undefined";
} catch(e) {
currentSetting = "Error displaying the setting value";
}
const message = `There is a problem with the script's configuration.\n\n`
+ `Current setting value (PAGEBREAK_PREFIXES): ${currentSetting}\n\n`
+ `Open the script editor, check and correct the "PAGEBREAK_PREFIXES" value at the top of the file, and **save the file**.\n\n`
+ `This value must be an **array** containing **at least one** non-empty prefix string (e.g., ["PREFIX1", "PREFIX2"]).\n`
+ `Empty strings "" or whitespace-only strings " " are ignored.\n\n`
+ `(Example: const PAGEBREAK_PREFIXES = ["Chapter ", "Section ", "Figure-"];)` // More specific example
DocumentApp.getUi().alert("Script Configuration Error", message, DocumentApp.getUi().ButtonSet.OK);
}
/**
* Inserts page breaks in the document before paragraphs that start with any of the valid prefixes.
*/
function insertPageBreakAtPrefixMarkers() {
const startTime = new Date();
let doc;
// --- 1. Initialization and Validation ---
try {
doc = DocumentApp.getActiveDocument();
if (!doc) throw new Error("Could not get the active document.");
doc.getName(); // Check access permissions
} catch (e) {
handleExecutionError_MultiPrefix("Document Access Error", e);
return;
}
// Get only valid prefixes from the settings
const validPrefixes = getValidPrefixes(PAGEBREAK_PREFIXES);
// If there are no valid prefixes, stop processing and show an error message
if (validPrefixes.length === 0) {
Logger.log(`insertPageBreakAtPrefixMarkers: Check before processing found no valid page break prefixes.`);
showConfigurationError_MultiPrefix("No valid page break prefixes configured");
return;
}
Logger.log(`--- Page Break Insertion by Multiple Prefixes Start ---`);
Logger.log(`Document: ${doc.getName()}`);
Logger.log(`Valid target prefixes (used for search): ${JSON.stringify(validPrefixes)}`); // Log the list actually being used
// --- 2. Element Traversal and Page Break Insertion ---
const body = doc.getBody();
const numChildren = body.getNumChildren();
let pageBreakInsertedCount = 0;
let foundMarkersCount = 0;
Logger.log(`Number of elements: ${numChildren}. Traversing in reverse...`);
for (let i = numChildren - 1; i >= 0; i--) {
const element = body.getChild(i);
if (element.getType() === DocumentApp.ElementType.PARAGRAPH) {
const paragraph = element.asParagraph();
let paragraphText;
try {
paragraphText = paragraph.getText();
// Check if the paragraph starts with any of the valid prefixes
if (validPrefixes.some(prefix => paragraphText.startsWith(prefix))) {
foundMarkersCount++;
// Logger.log(` Found: Index ${i}, Text: "${paragraphText}", Prefix: ${validPrefixes.find(p => paragraphText.startsWith(p))}`);
if (i > 0 && body.getChild(i - 1).getType() !== DocumentApp.ElementType.PAGE_BREAK) {
try {
body.insertPageBreak(i);
pageBreakInsertedCount++;
// Logger.log(` -> Page break inserted (Index ${i})`);
// Optional: To remove the marker paragraph itself
// try {
// paragraph.removeFromParent();
// Logger.log(` -> Removed the matching paragraph (original Index ${i+1}).`);
// } catch (removeError) {
// Logger.log(`! Error removing marker paragraph at Index ${i+1}: ${removeError}`);
// }
} catch (insertError) {
Logger.log(`! Error inserting page break at Index ${i}: ${insertError}`);
}
} // else: skip (already a page break or at the beginning of the doc)
}
} catch (elementError) {
Logger.log(`! Error processing Index ${i} (Type: PARAGRAPH): ${elementError}. Skipping.`);
}
}
}
// --- 3. Result Reporting ---
const endTime = new Date();
const duration = (endTime.getTime() - startTime.getTime()) / 1000;
Logger.log(`--- Traversal End ---`);
Logger.log(`Processing time: ${duration.toFixed(2)} seconds`);
Logger.log(`Detected paragraphs starting with specified prefixes (${JSON.stringify(validPrefixes)}): ${foundMarkersCount}`);
Logger.log(`Inserted page breaks: ${pageBreakInsertedCount}`);
const resultMessage = buildResultMessage_MultiPrefix(pageBreakInsertedCount, foundMarkersCount, validPrefixes, duration);
DocumentApp.getUi().alert("Processing Complete", resultMessage, DocumentApp.getUi().ButtonSet.OK);
Logger.log(`--- Page Break Insertion by Multiple Prefixes End ---`);
}
/**
* Handles runtime errors and notifies the user.
* (handleExecutionError_MultiPrefix function is unchanged)
* @param {string} context The context in which the error occurred.
* @param {Error} error The error object that was thrown.
*/
function handleExecutionError_MultiPrefix(context, error) {
Logger.log(`Runtime Error (${context}): ${error}\n${error.stack}`);
let userMessage = `An error occurred while running the script.\n\nContext: ${context}`;
if (error.message) {
if (error.message.includes("Authorization required")) {
userMessage = "The script does not have the necessary permissions to run.\n\nPlease reload the document or run the script again and grant permission in the authorization prompt.";
} else {
userMessage += `\nDetails: ${error.message}`;
}
}
DocumentApp.getUi().alert("Script Error", userMessage, DocumentApp.getUi().ButtonSet.OK);
}
/**
* Builds a message for the user based on the processing results.
* (buildResultMessage_MultiPrefix function is unchanged, displays only valid prefixes)
* @param {number} insertedCount The number of page breaks inserted.
* @param {number} foundCount The number of matching paragraphs found.
* @param {string[]} validPrefixes The array of valid prefixes.
* @param {number} duration The processing time in seconds.
* @returns {string} The message to be displayed in an alert.
*/
function buildResultMessage_MultiPrefix(insertedCount, foundCount, validPrefixes, duration) {
// Limit the displayed prefixes to a maximum of 3 and wrap them in quotes
const displayPrefixes = validPrefixes.slice(0, 3).map(p => `"${p}"`);
const prefixDescription = `paragraphs starting with the specified prefixes (${displayPrefixes.join(', ')}${validPrefixes.length > 3 ? '...' : ''})`;
if (insertedCount > 0) {
return `Inserted page breaks before ${insertedCount} ${prefixDescription}.\n\nProcessing time: ${duration.toFixed(2)} seconds`;
} else if (foundCount > 0) {
return `${foundCount} ${prefixDescription} were found, but no page breaks were inserted.\n(Because the paragraph is at the start of the document or a page break already exists.)\n\nProcessing time: ${duration.toFixed(2)} seconds`;
} else {
return `No ${prefixDescription} were found.\n\nPlease check the following:\n`
+ `1. Are valid prefixes configured in "PAGEBREAK_PREFIXES" at the top of the script? (Current valid settings: ${JSON.stringify(validPrefixes)})\n`
+ `2. Does the document contain paragraphs that start with one of these prefixes? (Case-sensitive)\n`
+ `3. Have the script permissions been authorized?`;
}
}