Answered
/*
Add @m2 Styles to TOC.jsx
Version: 3.1
Change in v3.1:
Adds comments and changes the default regexes so they work with
@2, @3, @5 as well as @m2, @m3, @m5.
Change in v3.0:
Allows the TOC level to be a fixed number or extracted from each
paragraph style name with a regular expression.
Adds paragraph styles matching a regular expression to a selected
Table of Contents Style. Defaults to styles named like:
@m2 Heading
@m2-Middle Title
@m3 H3
@2 Heading
@3 H3
The default paragraph style regex is ^@m?\d+(?!\d).
The default level regex is ^@m?(\d+).
*/
(function () {
if (app.documents.length === 0) {
alert("Open an InDesign document first.");
return;
}
app.doScript(
main,
ScriptLanguage.JAVASCRIPT,
undefined,
UndoModes.ENTIRE_SCRIPT,
"Add @m2 Styles to TOC"
);
function main() {
var doc = app.activeDocument;
if (doc.tocStyles.length === 0) {
alert("This document has no Table of Contents Styles.");
return;
}
// Build the list of Table of Contents Styles shown in the dropdown.
var tocNames = [];
for (var i = 0; i < doc.tocStyles.length; i++) {
tocNames.push(doc.tocStyles[i].name);
}
// Build a small dialog so the user can choose the TOC Style,
// choose which paragraph styles to include, and choose how levels are set.
var dialog = new Window("dialog", "Add Styles to TOC");
dialog.orientation = "column";
dialog.alignChildren = ["fill", "top"];
dialog.margins = 16;
dialog.add("statictext", undefined, "Table of Contents Style:");
var tocDropdown = dialog.add("dropdownlist", undefined, tocNames);
tocDropdown.selection = getDefaultTocSelection(tocNames);
dialog.add("statictext", undefined, "Paragraph style name regex:");
var regexField = dialog.add("edittext", undefined, "^@m?\\d+(?!\\d)");
regexField.characters = 34;
dialog.add("statictext", undefined, "TOC level number or regex:");
var levelField = dialog.add("edittext", undefined, "^@m?(\\d+)");
levelField.characters = 34;
var updateExisting = dialog.add("checkbox", undefined, "Update matching styles already in the TOC Style");
updateExisting.value = false;
var buttons = dialog.add("group");
buttons.alignment = ["right", "top"];
buttons.add("button", undefined, "Cancel", { name: "cancel" });
buttons.add("button", undefined, "Preview", { name: "ok" });
if (dialog.show() !== 1) {
return;
}
// Read the user's choices from the dialog.
var regexText = trim(regexField.text);
var levelText = trim(levelField.text);
if (regexText === "") {
alert("Enter a regular expression.");
return;
}
if (levelText === "") {
alert("Enter a TOC level number or a regular expression that extracts one.");
return;
}
var matcher;
try {
// This regex decides which paragraph styles should be added.
matcher = new RegExp(regexText);
} catch (error) {
alert("Invalid regular expression:\r" + error);
return;
}
// The level resolver decides what TOC level each matching style should use.
// It can use one fixed level, or extract a number from each style name.
var levelResolver = makeLevelResolver(levelText);
if (!levelResolver.ok) {
alert(levelResolver.message);
return;
}
var tocStyle = doc.tocStyles.itemByName(tocDropdown.selection.text);
if (!tocStyle.isValid) {
alert("No matching TOC Style was found.");
return;
}
var existing = getExistingEntryMap(tocStyle);
var matches = getMatchingParagraphStyles(doc, matcher);
if (matches.length === 0) {
alert("No paragraph styles matched:\r" + regexText);
return;
}
var toAdd = [];
var toUpdate = [];
var alreadyThere = [];
var levelFailures = [];
// Sort every matching paragraph style into one of three groups:
// add it, update it, or leave it alone because it is already included.
for (var j = 0; j < matches.length; j++) {
var style = matches[j];
var entryName = getTocEntryName(style);
var entry = existing[entryName] || existing[style.name];
var levelResult = levelResolver.getLevel(style.name);
if (!levelResult.ok) {
levelFailures.push(style.name + " - " + levelResult.message);
continue;
}
if (entry && entry.isValid) {
if (updateExisting.value && entry.level !== levelResult.level) {
toUpdate.push({ style: style, entry: entry, entryName: entryName, level: levelResult.level });
} else {
alreadyThere.push(style);
}
} else {
toAdd.push({ style: style, entryName: entryName, level: levelResult.level });
}
}
if (levelFailures.length > 0) {
alert(
"These styles matched the paragraph style regex, but no valid TOC level could be extracted:\r\r" +
levelFailures.join("\r")
);
return;
}
if (toAdd.length === 0 && toUpdate.length === 0) {
alert("The matching styles are already included in \"" + tocStyle.name + "\".");
return;
}
if (!confirm(buildPreview(tocStyle.name, regexText, levelText, toAdd, toUpdate, alreadyThere))) {
return;
}
var added = 0;
var updated = 0;
var failed = [];
// Update existing TOC entries only if the user ticked the checkbox.
for (var k = 0; k < toUpdate.length; k++) {
toUpdate[k].entry.level = toUpdate[k].level;
updated++;
}
// Add missing paragraph styles to the selected TOC Style.
for (var m = 0; m < toAdd.length; m++) {
try {
tocStyle.tocStyleEntries.add(toAdd[m].entryName, { level: toAdd[m].level });
added++;
} catch (error) {
failed.push(toAdd[m].entryName + " - " + error);
}
}
var message = "Finished updating \"" + tocStyle.name + "\".\r\r" +
"Added: " + added + "\r" +
"Updated: " + updated + "\r" +
"Already included: " + alreadyThere.length;
if (failed.length > 0) {
message += "\r\rFailed:\r" + failed.join("\r");
}
alert(message);
}
function getMatchingParagraphStyles(doc, matcher) {
var result = [];
var styles = doc.allParagraphStyles;
// doc.allParagraphStyles includes styles inside paragraph style groups.
for (var i = 0; i < styles.length; i++) {
var style = styles[i];
if (!style.isValid || style.name === "[No Paragraph Style]") {
continue;
}
matcher.lastIndex = 0;
if (matcher.test(style.name)) {
result.push(style);
}
}
return result;
}
function getExistingEntryMap(tocStyle) {
var map = {};
// Store current TOC entries so the script does not add duplicates.
for (var i = 0; i < tocStyle.tocStyleEntries.length; i++) {
var entry = tocStyle.tocStyleEntries[i];
map[entry.name] = entry;
}
return map;
}
function makeLevelResolver(levelText) {
var fixedLevel = parseInt(levelText, 10);
// If the user enters 2, 3, 5, etc., use that same level for every style.
if (/^\d+$/.test(levelText) && fixedLevel > 0) {
return {
ok: true,
getLevel: function () {
return { ok: true, level: fixedLevel };
}
};
}
var levelRegex;
try {
// Otherwise, treat the field as a regex that extracts the level.
// Example: ^@m?(\d+) reads 2 from @2 or @m2.
levelRegex = new RegExp(levelText);
} catch (error) {
return {
ok: false,
message: "Invalid TOC level regular expression:\r" + error
};
}
return {
ok: true,
getLevel: function (styleName) {
levelRegex.lastIndex = 0;
var match = levelRegex.exec(styleName);
if (!match) {
return {
ok: false,
message: "No level match from \"" + levelText + "\""
};
}
// If the regex has a capture group, use group 1.
// Otherwise, use the whole matched text.
var value = match.length > 1 ? match[1] : match[0];
var parsedLevel = parseInt(value, 10);
if (isNaN(parsedLevel) || parsedLevel < 1) {
return {
ok: false,
message: "Matched \"" + value + "\", which is not a level of 1 or higher"
};
}
return { ok: true, level: parsedLevel };
}
};
}
function getTocEntryName(style) {
var names = [style.name];
var parent = style.parent;
// Paragraph styles inside groups need a full path for TOC scripting:
// Group Name:Subgroup Name:Style Name
while (parent && parent.constructor && parent.constructor.name === "ParagraphStyleGroup") {
names.unshift(parent.name);
parent = parent.parent;
}
return names.join(":");
}
function buildPreview(tocName, regexText, levelText, toAdd, toUpdate, alreadyThere) {
return "TOC Style: " + tocName + "\r" +
"Regex: " + regexText + "\r" +
"TOC level: " + levelText + "\r\r" +
"Will add: " + toAdd.length + "\r" +
listNames(toAdd) +
"\rWill update: " + toUpdate.length + "\r" +
listNames(toUpdate) +
"\rAlready included, left alone: " + alreadyThere.length + "\r\r" +
"Continue?";
}
function listNames(items) {
var lines = "";
var limit = Math.min(items.length, 25);
for (var i = 0; i < limit; i++) {
lines += " " + (items[i].entryName || items[i].name);
if (items[i].level !== undefined) {
lines += " -> level " + items[i].level;
}
lines += "\r";
}
if (items.length > limit) {
lines += " ...and " + (items.length - limit) + " more\r";
}
return lines;
}
function getDefaultTocSelection(names) {
for (var i = 0; i < names.length; i++) {
if (names[i] !== "[Default]") {
return i;
}
}
return 0;
}
function trim(value) {
return value.replace(/^\s+|\s+$/g, "");
}
}());
Sign up
Already have an account? Login
To post, reply, or follow discussions, please sign in with your Adobe ID.
Sign inSign in to Adobe Community
No account yet? Create an account
To post, reply, or follow discussions, please sign in with your Adobe ID.
Sign inEnter your E-mail address. We'll send you an e-mail with instructions to reset your password.

