Skip to main content

"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

  1. Open a Google Doc and open the script editor from "Extensions" > "Apps Script".
  2. Paste the following script into the editor.
  3. 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?`;
}
}