Skip to content

Instantly share code, notes, and snippets.

@mengwong
Last active December 31, 2023 15:52
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mengwong/f597d064366f555b5f4e to your computer and use it in GitHub Desktop.
Save mengwong/f597d064366f555b5f4e to your computer and use it in GitHub Desktop.
number headers in Google Docs
// PROBLEM: in Google Documents, sometimes you want numbered headings, but the UI makes you a new list each time.
// INSIGHT: it's possible, but you have to script it.
// SOLUTION: this script adds OL+LI to Hs, all using the same listID
// DOWNLOAD: get the latest version of this gist from https://gist.github.com/mengwong/f597d064366f555b5f4e
// INSTALL: Go to Tools / Editor. Paste this in. Hit save. You can run it by pressing the "play" button, or from the Add-Ons menu.
//
// USAGE:
// REQUIRED: Define all your headings as actual headings and not just big bold Normal. Come on, people, learn to use paragraph styles already.
// OPTIONAL: Manually make the first H1 in the document a numbered list. We'll use that as a reference heading for all the others.
// OPTIONAL: Manually set the list style to "Decimal Outline" (the style that has 1.1.2 type numbering) ... I like this best.
// OPTIONAL: Arrange your reference heading's indentation to taste. I usually hang the number into the left margin and keep the heading text flush left with Normal.
// ACTION! Run this function from within the tools editor. You can rerun this function again later when you need to.
// Or just run it from the Add-Ons menu.
//
// AUTHOR: mengwong
// LICENSE: CC0 1.0 Universal
function numberHeaders() {
var body = DocumentApp.getActiveDocument().getBody();
var changesCount = 0;
var referenceListItem = body.getListItems().filter(function(l){return l.getHeading() == DocumentApp.ParagraphHeading.HEADING1})[0]
|| makeReferenceListItem_(body);
// TODO: maybe the user has H2 and H3 but no H1. Respect that decision by taking biggestHeader=1/2/3.
for (var pi = body.getNumChildren() -1; pi >= 0; pi--) {
var para = body.getChild(pi);
if (headingLevel_(para) < 1) { continue }
li_fix_(h2li_(body,para,pi),referenceListItem); changesCount++;
}
if (changesCount == 0) { DocumentApp.getUi().alert(
'Nothing Happened.',
'Why? Because your document doesn\'t use actual headers.\n'+
'Look under the Format / Paragraph Styles menu.\n'+
'Do you see Heading 1, 2, 3, 4, 5, 6?\n'+
'Please, please, please use proper header styles.\n'+
'Because hand-drawn ghetto-headers make baby Jesus cry.',
DocumentApp.getUi().ButtonSet.OK);
}
}
function makeReferenceListItem_(body) {
// didn't set up your first H1 as a list? fine, i will do it for you.
for (var pi = 0; pi < body.getNumChildren(); pi++) {
var para = body.getChild(pi);
if (headingLevel_(para) != 1) { continue }
var referenceListItem = h2li_(body, para, pi)
.setIndentFirstLine(para.getIndentStart()-12)
.setIndentStart (para.getIndentStart());
break;
}
return referenceListItem;
}
function h2li_(body, para, pi) {
var newPara = body.insertListItem(pi, para.getText())
.setHeading(para.getHeading())
// .setGlyphType(DocumentApp.GlyphType.NUMBER)
// https://code.google.com/p/google-apps-script-issues/issues/detail?id=4642
.setNestingLevel(headingLevel_(para)-1)
.setSpacingBefore(para.getSpacingBefore())
.setSpacingAfter (para.getSpacingAfter())
;
para.removeFromParent();
return newPara;
}
function li_fix_(para, referenceListItem) {
para.setListId(referenceListItem)
.setIndentFirstLine(referenceListItem.getIndentFirstLine())
.setIndentStart( referenceListItem.getIndentStart())
;
return para;
}
function headingLevel_(para) {
with (DocumentApp.ParagraphHeading) {
switch(para.getHeading()) {
case HEADING1: return 1;
case HEADING2: return 2;
case HEADING3: return 3;
case HEADING4: return 4;
case HEADING5: return 5;
case HEADING6: return 6;
case TITLE: return 0;
case SUBTITLE: return 0.5;
case NORMAL: return -1;
default: return;
}
}
}
/**
* Creates a menu entry in the Google Docs UI when the document is opened.
*
* @param {object} e The event parameter for a simple onOpen trigger. To
* determine which authorization mode (ScriptApp.AuthMode) the trigger is
* running in, inspect e.authMode.
*/
function onOpen(e) {
DocumentApp.getUi().createAddonMenu()
.addItem('Add Numbers to Headings', 'numberHeaders')
.addToUi();
}
/**
* Runs when the add-on is installed.
*
* @param {object} e The event parameter for a simple onInstall trigger. To
* determine which authorization mode (ScriptApp.AuthMode) the trigger is
* running in, inspect e.authMode. (In practice, onInstall triggers always
* run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or
* AuthMode.NONE.)
*/
function onInstall(e) {
onOpen(e);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment