Skip to main content
dublove
Legend
June 3, 2026
解決済み

Is there a way to move all paragraph styles starting with @m2 to the left side of the TocStyle window?

Hello everyone~

The paragraph styles starting with @m2 are the ones I want to use for generating the “table of contents”.


However, the Toc Style window is too small to work with comfortably, and I’m worried about missing something when paragraph styles are changed.

If I could move all @m2 paragraph styles to the left side of the TOC Style window at once, it would save me a lot of work.

The number 2 in @m2 indicates the second level.

 

    解決に役立った回答 Eugene Tyson

    @dublove 

     

    /*
    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, "");
    }
    }());

     

    返信数 3

    dublove
    dublove作成者
    Legend
    June 3, 2026

    Wow, you actually made a dialog box.
    What a crazy idea—it’s absolutely brilliant.

    Community Expert
    June 3, 2026

    InDesign exposes tocStyle.tocStyleEntries.add(styleName, { level: ... }), and TOCStyleEntry.level controls the TOC indent level. 

     

    Going by your other requests lately, simply pushing the styles over isn’t going to work for you so I’ll see if it’s possible to give you a dialog box to pick other styles starting the style name structure. 

     

    And you can pick the level it goes in at - even though I think you want m2 to be always level 2 and m3 to always be level 3 - this still gives you a bit more control for little interaction.

     

    /*
    Add @m2 Styles to TOC.jsx

    Adds every paragraph style whose name starts with "@m2" to a selected
    Table of Contents Style, using TOC level 2 by default.

    The script previews the exact styles first, then runs as one undo step.
    */

    (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;
    }

    var tocStyleNames = [];
    for (var i = 0; i < doc.tocStyles.length; i++) {
    tocStyleNames.push(doc.tocStyles[i].name);
    }

    var dialog = new Window("dialog", "Add Paragraph Styles to TOC");
    dialog.orientation = "column";
    dialog.alignChildren = ["fill", "top"];
    dialog.margins = 16;

    var tocGroup = dialog.add("group");
    tocGroup.orientation = "column";
    tocGroup.alignChildren = ["fill", "top"];
    tocGroup.add("statictext", undefined, "Add styles to this Table of Contents Style:");

    var tocDropdown = tocGroup.add("dropdownlist", undefined, tocStyleNames);
    tocDropdown.selection = 0;

    var prefixGroup = dialog.add("group");
    prefixGroup.orientation = "column";
    prefixGroup.alignChildren = ["fill", "top"];
    prefixGroup.add("statictext", undefined, "Paragraph style name starts with:");

    var prefixField = prefixGroup.add("edittext", undefined, "@m2");
    prefixField.characters = 28;

    var levelGroup = dialog.add("group");
    levelGroup.orientation = "column";
    levelGroup.alignChildren = ["fill", "top"];
    levelGroup.add("statictext", undefined, "TOC level:");

    var levelField = levelGroup.add("edittext", undefined, "2");
    levelField.characters = 8;

    var updateExistingCheckbox = dialog.add(
    "checkbox",
    undefined,
    "Update matching styles already included in the TOC Style"
    );
    updateExistingCheckbox.value = false;

    var buttonGroup = dialog.add("group");
    buttonGroup.alignment = ["right", "top"];
    buttonGroup.add("button", undefined, "Cancel", { name: "cancel" });
    buttonGroup.add("button", undefined, "Preview", { name: "ok" });

    if (dialog.show() !== 1) {
    return;
    }

    var prefix = trim(prefixField.text);
    var level = parseInt(levelField.text, 10);

    if (prefix === "") {
    alert("Enter a paragraph style prefix.");
    return;
    }

    if (isNaN(level) || level < 1) {
    alert("Enter a TOC level of 1 or higher.");
    return;
    }

    var tocStyle = doc.tocStyles.itemByName(tocDropdown.selection.text);
    if (!tocStyle.isValid) {
    alert("No matching TOC Style was found.");
    return;
    }

    var matchingStyles = getParagraphStylesStartingWith(doc, prefix);

    if (matchingStyles.length === 0) {
    alert("No paragraph styles start with \"" + prefix + "\".");
    return;
    }

    var existingEntries = getExistingEntriesByParagraphStyleName(tocStyle);
    var toAdd = [];
    var toUpdate = [];
    var alreadyIncluded = [];

    for (var j = 0; j < matchingStyles.length; j++) {
    var candidateStyle = matchingStyles[j];
    var candidateEntry = existingEntries[candidateStyle.name];

    if (candidateEntry && candidateEntry.isValid) {
    if (updateExistingCheckbox.value && candidateEntry.level !== level) {
    toUpdate.push(candidateStyle);
    } else {
    alreadyIncluded.push(candidateStyle);
    }
    } else {
    toAdd.push(candidateStyle);
    }
    }

    if (toAdd.length === 0 && toUpdate.length === 0) {
    alert("All matching styles are already included in \"" + tocStyle.name + "\".");
    return;
    }

    if (!confirm(buildPreview(tocStyle.name, prefix, level, toAdd, toUpdate, alreadyIncluded))) {
    return;
    }

    var beforeCount = tocStyle.tocStyleEntries.length;
    var added = 0;
    var updated = 0;
    var skipped = alreadyIncluded.length;
    var unexpected = [];

    for (var k = 0; k < toUpdate.length; k++) {
    var entryToUpdate = existingEntries[toUpdate[k].name];
    entryToUpdate.level = level;
    updated++;
    }

    for (var m = 0; m < toAdd.length; m++) {
    var paragraphStyle = toAdd[m];
    var newEntry = tocStyle.tocStyleEntries.add(paragraphStyle.name, { level: level });
    added++;

    if (newEntry.name.indexOf(prefix) !== 0) {
    unexpected.push(newEntry.name);
    newEntry.remove();
    added--;
    }
    }

    if (tocStyle.tocStyleEntries.length < beforeCount) {
    throw new Error("The TOC entry count went down. Use Undo once to restore the previous TOC Style state.");
    }

    if (unexpected.length > 0) {
    throw new Error(
    "InDesign added entries that do not start with \"" + prefix + "\":\r" +
    unexpected.join("\r") +
    "\r\rThose suspicious entries were removed. Check for duplicate paragraph style names in style groups."
    );
    }

    alert(
    "Finished updating \"" + tocStyle.name + "\".\r\r" +
    "Added: " + added + "\r" +
    "Updated: " + updated + "\r" +
    "Already included: " + skipped
    );
    }

    function getParagraphStylesStartingWith(documentRef, stylePrefix) {
    var matches = [];
    var styles = documentRef.allParagraphStyles;

    for (var k = 0; k < styles.length; k++) {
    var style = styles[k];

    if (!style.isValid || style.name === "[No Paragraph Style]") {
    continue;
    }

    if (style.name.indexOf(stylePrefix) === 0) {
    matches.push(style);
    }
    }

    return matches;
    }

    function getExistingEntriesByParagraphStyleName(tocStyleRef) {
    var entries = {};

    for (var k = 0; k < tocStyleRef.tocStyleEntries.length; k++) {
    var entry = tocStyleRef.tocStyleEntries[k];
    entries[entry.name] = entry;

    try {
    if (entry.formatStyle && entry.formatStyle.isValid) {
    entries[entry.formatStyle.name] = entry;
    }
    } catch (_) {
    }
    }

    return entries;
    }

    function buildPreview(tocStyleName, prefix, level, toAdd, toUpdate, alreadyIncluded) {
    var previewLimit = 30;
    var message = "TOC Style: " + tocStyleName + "\r" +
    "Prefix: " + prefix + "\r" +
    "TOC level: " + level + "\r\r" +
    "Will add: " + toAdd.length + "\r" +
    previewList(toAdd, previewLimit) +
    "\rWill update: " + toUpdate.length + "\r" +
    previewList(toUpdate, previewLimit) +
    "\rAlready included, left alone: " + alreadyIncluded.length + "\r\r" +
    "Continue?";

    return message;
    }

    function previewList(styles, limit) {
    var lines = "";
    var count = Math.min(styles.length, limit);

    for (var k = 0; k < count; k++) {
    lines += " " + styles[k].name + "\r";
    }

    if (styles.length > limit) {
    lines += " ...and " + (styles.length - limit) + " more\r";
    }

    return lines;
    }

    function trim(value) {
    return value.replace(/^\s+|\s+$/g, "");
    }
    }());

     

     

    dublove
    dublove作成者
    Legend
    June 3, 2026

    Hi ​@Eugene Tyson 

    Can the regular expression be modified?

    ^@m\d+
    It doesn't seem to have worked.
    I'll provide another sample.
     

    Community Expert
    June 3, 2026

    Give me a few hours to update, I think I have a script that already allows to input Regex directly - let me see if I can plug it in here so you can use your own regex.

    Community Expert
    June 3, 2026

    Try the following. I have not tested it but worth giving a shot

    var doc = app.documents[0]
    var tocStyle = doc.tocStyles.itemByName("aa")
    for(var i = 0; i < doc.paragraphStyles.length; i++) {
    var nm = doc.paragraphStyles[i].name
    if(nm.match(/^@m2/))
    tocStyle.tocStyleEntries.add(nm)
    }

    Edit:- ​@dublove I have edited the code. The previous version has some typos. Give it a try now

    -Manan