"Insert Page Break by Heading Level" 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.
- Insert a page break before a specified heading level.
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 TARGET_HEADING_LEVEL
at the beginning of the code to the desired heading level (an integer from 1 to 6) where you want to insert page breaks.
Detailed instructions are provided in the comments within the code.
Example: const TARGET_HEADING_LEVEL = 3;
// To insert page breaks before "Heading 3"
insert-page-break-before-heading.js
// --- Configuration Section ---
/**
* @fileoverview A Google Apps Script to insert a page break before a specified heading level.
*
* ## Prerequisites for Running
*
* ・Important: Select the function to run > choose [ insertPageBreakBeforeHeading ].
*
* ## How to Configure
*
* 1. Change the value of **TARGET_HEADING_LEVEL** to the desired heading level (an integer from 1 to 6).
* - 1: "Heading 1" style in Google Docs
* - 2: "Heading 2" style in Google Docs
* - 3: "Heading 3" style in Google Docs
* - 4: "Heading 4" style in Google Docs
* - 5: "Heading 5" style in Google Docs
* - 6: "Heading 6" style in Google Docs
*
* 2. **Note:**
* - This script recognizes Google Docs' **standard heading styles**.
* - For the heading text where you want to insert a page break, **you must apply** the corresponding
* **"Heading X" style** from the menu [Format] > [Paragraph styles].
* - (Simply changing the font size or weight will not be recognized as a heading.)
*
* 3. After making changes, **be sure to save the script file** (click the floppy disk icon).
*/
const TARGET_HEADING_LEVEL = 3; // Example: To insert a page break before the "Heading 3" style
// --- End of Configuration Section ---
// --- Script Code Below (No changes needed) ---
/** Global constant: Script name */
const SCRIPT_NAME = 'Custom Script';
/** Global constant: Menu name for configuration errors */
const SCRIPT_ERROR_MENU_NAME = `${SCRIPT_NAME} (Configuration Error)`;
/**
* Adds a custom menu when the document is opened.
*/
function onOpen() {
try {
if (!isValidHeadingLevel(TARGET_HEADING_LEVEL)) {
Logger.log(`onOpen: TARGET_HEADING_LEVEL (${TARGET_HEADING_LEVEL}) is invalid. Displaying error menu.`);
showConfigurationErrorMenu("Invalid setting (integer 1-6)");
return;
}
DocumentApp.getUi()
.createMenu(SCRIPT_NAME)
.addItem(`Insert Page Break Before "Heading ${TARGET_HEADING_LEVEL}"`, 'insertPageBreakBeforeHeading')
.addToUi();
Logger.log(`onOpen: Menu added successfully (Target: Heading ${TARGET_HEADING_LEVEL}).`);
} catch (e) {
Logger.log(`onOpen: Error adding menu: ${e}\n${e.stack}`);
showConfigurationErrorMenu("Error adding menu");
}
}
/**
* Adds an error menu item if there is a configuration error.
* @param {string} reason The reason for the error.
*/
function showConfigurationErrorMenu(reason) {
try {
DocumentApp.getUi()
.createMenu(SCRIPT_ERROR_MENU_NAME)
.addItem(`Check settings (${reason})`, 'showConfigurationError')
.addToUi();
} catch (e) {
Logger.log(`showConfigurationErrorMenu: Further error while displaying error menu: ${e}`);
// Only log, as the UI might be unavailable.
}
}
/**
* Alert function to display when there is a configuration error.
*/
function showConfigurationError() {
let currentSetting = "Undefined or inaccessible";
try {
currentSetting = (typeof TARGET_HEADING_LEVEL !== 'undefined') ? String(TARGET_HEADING_LEVEL) : "Undefined";
} catch(e) { /* Ignore errors if inaccessible */ }
const message = `There is a problem with the script's configuration.\n\n`
+ `Current setting value: ${currentSetting}\n\n`
+ `Open the script editor, check and correct the "TARGET_HEADING_LEVEL" value (an integer from 1 to 6) at the top of the file, and **save the file**.\n\n`
+ `(e.g., const TARGET_HEADING_LEVEL = 3;)`
DocumentApp.getUi().alert("Script Configuration Error", message, DocumentApp.getUi().ButtonSet.OK);
}
/**
* Checks if the specified heading level is valid (an integer from 1 to 6).
* @param {any} level The value to check.
* @return {boolean} True if valid.
*/
function isValidHeadingLevel(level) {
return typeof level === 'number' && Number.isInteger(level) && level >= 1 && level <= 6;
}
/**
* Returns the corresponding ParagraphHeading constant for a given numeric heading level.
* @param {number} level The heading level (integer 1-6).
* @return {DocumentApp.ParagraphHeading | null} The corresponding constant, or null if invalid.
*/
function getHeadingConstant(level) {
// Assumes it's already checked by isValidHeadingLevel, but as a safeguard
if (!isValidHeadingLevel(level)) {
Logger.log(`getHeadingConstant: Invalid level: ${level}`);
return null;
}
// Use an object lookup for brevity
const headingMap = {
1: DocumentApp.ParagraphHeading.HEADING1,
2: DocumentApp.ParagraphHeading.HEADING2,
3: DocumentApp.ParagraphHeading.HEADING3,
4: DocumentApp.ParagraphHeading.HEADING4,
5: DocumentApp.ParagraphHeading.HEADING5,
6: DocumentApp.ParagraphHeading.HEADING6,
};
return headingMap[level] || null; // Return null if level has no corresponding value
}
/**
* Gets the name of a ParagraphHeading Enum from its value.
* @param {DocumentApp.ParagraphHeading | any} enumValue The ParagraphHeading Enum value.
* @return {string} The name of the Enum (e.g., 'HEADING3'), or 'UNKNOWN' if not found.
*/
function getHeadingEnumName(enumValue) {
if (enumValue === null || typeof enumValue === 'undefined') {
return 'NULL_OR_UNDEFINED';
}
// Uses a cache for efficiency (searches all values only on the first run)
if (!this.headingEnumNameCache) {
this.headingEnumNameCache = {};
for (const key in DocumentApp.ParagraphHeading) {
// Avoid properties from the prototype chain
if (Object.prototype.hasOwnProperty.call(DocumentApp.ParagraphHeading, key)) {
this.headingEnumNameCache[DocumentApp.ParagraphHeading[key]] = key;
}
}
}
return this.headingEnumNameCache[enumValue] || `UNKNOWN_STYLE (${String(enumValue)})`;
}
/**
* Inserts a page break immediately before elements of a specified heading level in the document.
*/
function insertPageBreakBeforeHeading() {
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(); // Doubles as a check for access permissions
} catch (e) {
handleExecutionError("Document Access Error", e);
return;
}
if (!isValidHeadingLevel(TARGET_HEADING_LEVEL)) {
Logger.log(`insertPageBreakBeforeHeading: Invalid setting value (${TARGET_HEADING_LEVEL}).`);
showConfigurationError(); // Show detailed configuration error
return;
}
const targetHeadingConstant = getHeadingConstant(TARGET_HEADING_LEVEL);
if (!targetHeadingConstant) {
// This error should not normally occur (due to isValidHeadingLevel check)
Logger.log(`insertPageBreakBeforeHeading: Failed to get heading constant (level: ${TARGET_HEADING_LEVEL}).`);
DocumentApp.getUi().alert(`Script internal error: Could not get heading style constant for setting (${TARGET_HEADING_LEVEL}).`);
return;
}
const targetHeadingName = getHeadingEnumName(targetHeadingConstant);
Logger.log(`--- Page Break Insertion Process Start ---`);
Logger.log(`Document: ${doc.getName()}, Target Heading: ${TARGET_HEADING_LEVEL} (${targetHeadingName})`);
// --- 2. Element Traversal and Page Break Insertion ---
const body = doc.getBody();
const numChildren = body.getNumChildren();
let pageBreakInsertedCount = 0;
let foundTargetHeadings = 0;
Logger.log(`Number of elements: ${numChildren}. Traversing in reverse...`);
for (let i = numChildren - 1; i >= 0; i--) {
const element = body.getChild(i);
const elementType = element.getType();
let headingStyle = null;
try {
// Only check element types that can have a heading style
if (elementType === DocumentApp.ElementType.PARAGRAPH) {
headingStyle = element.asParagraph().getHeading();
} else if (elementType === DocumentApp.ElementType.LIST_ITEM) {
headingStyle = element.asListItem().getHeading();
} else {
continue; // Skip irrelevant elements
}
// Check if it's the target heading style
if (headingStyle === targetHeadingConstant) {
foundTargetHeadings++;
// Logger.log(` Found: Index ${i}, Type: ${elementType}`); // Uncomment if needed
// Insert if it's not the start of the document and the preceding element is not a page break
if (i > 0 && body.getChild(i - 1).getType() !== DocumentApp.ElementType.PAGE_BREAK) {
try {
body.insertPageBreak(i);
pageBreakInsertedCount++;
// Logger.log(` -> Page break inserted (Index ${i})`); // Uncomment if needed
} catch (insertError) {
Logger.log(`! Error while inserting page break at Index ${i}: ${insertError}`);
// Do not notify user, just log, and continue processing
}
} else if (i > 0) {
// Logger.log(` -> Skipped (preceded by a page break)`); // Uncomment if needed
} else {
// Logger.log(` -> Skipped (start of document)`); // Uncomment if needed
}
}
} catch (elementError) {
// Log unexpected errors during element processing and continue
Logger.log(`! Error processing Index ${i} (Type: ${elementType}): ${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 target headings (${targetHeadingName}): ${foundTargetHeadings} `);
Logger.log(`Inserted page breaks: ${pageBreakInsertedCount}`);
const resultMessage = buildResultMessage(pageBreakInsertedCount, foundTargetHeadings, targetHeadingName, duration);
DocumentApp.getUi().alert("Processing Complete", resultMessage, DocumentApp.getUi().ButtonSet.OK);
Logger.log(`--- Page Break Insertion Process End ---`);
}
/**
* Handles runtime errors and notifies the user.
* @param {string} context The context in which the error occurred.
* @param {Error} error The error object that was thrown.
*/
function handleExecutionError(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.
* @param {number} insertedCount The number of page breaks inserted.
* @param {number} foundCount The number of target headings found.
* @param {string} headingName The name of the target heading style.
* @param {number} duration The processing time in seconds.
* @returns {string} The message to be displayed in an alert.
*/
function buildResultMessage(insertedCount, foundCount, headingName, duration) {
const headingLevelName = `"${headingName}" style (Heading ${TARGET_HEADING_LEVEL})`;
if (insertedCount > 0) {
return `Inserted page breaks before ${insertedCount} instance(s) of ${headingLevelName}.\n\nProcessing time: ${duration.toFixed(2)} seconds`;
} else if (foundCount > 0) {
return `${foundCount} instance(s) of ${headingLevelName} were found, but no page breaks were inserted.\n(Because the heading is at the start of the document or a page break already exists.)\n\nProcessing time: ${duration.toFixed(2)} seconds`;
} else {
return `No elements with the ${headingLevelName} style were found.\n\nPlease check the following:\n`
+ `1. Is the setting value (${TARGET_HEADING_LEVEL}) correct?\n`
+ `2. Has the correct heading style been applied to the target text from [Format] > [Paragraph styles]?\n`
+ `3. Have the script permissions been authorized?`;
}
}