During my time at Royal Publishing, I have developed a stronger command of Photoshop, InDesign, Excel, and other software while expanding my scripting and automation skills. I made it a personal mission to make my job as efficient as possible by automating repetitive tasks and streamlining established workflows.
This led me to develop numerous JavaScript scripts for InDesign that automate tasks such as formatting rosters, adjusting column layouts, standardizing cutlines and titles, converting four-color logos into two-color versions for easier printing, and clearing book contents for reuse in a new season. Tasks such as formatting 5–30 rosters to a consistent standard in under 30 minutes would be considerably more time-consuming without these tools. I also developed Excel macros to streamline data processing, a significant component of my work.
Many of these scripts and macros have fundamentally changed how I approach my workload, reducing manual effort, improving consistency, and allowing me to complete tasks far more efficiently. Through these projects, I have combined my knowledge of publishing software with practical programming skills to solve real production challenges and improve day-to-day workflows.
This script formatted rosters in one click. We wanted the text to be as big as possible whole not exceeding 9pt font.
Here is the order of operations as to what this script does:
   Applies a paragraph style to the entire selection.
   Applies an underline paragraph style to the first line of the selection.
   Applies a leader line to everything that includes the word "coach."
   Increases the font size while not exceeding 9pt font.
   Justifies text.
#target "InDesign"
(function () {
    if (app.documents.length === 0) {
        alert("No document open.");
        return;
    }
    var doc = app.activeDocument;
    // ==================================================
    // CONFIG
    // ==================================================
    var BODY_STYLE   = "BODY";
    var TOP_STYLE    = "TOP UNDERLINE";
    var BOTTOM_STYLE = "BOTTOM COACHES PERIODS";
    var TWO_BODY  = "2 collumn body";
    var TWO_FIRST = "2 collumn header";
    var KEYWORD = "coach";
    var MAX_SIZE = 9, MIN_SIZE = 7, SIZE_STEP = 0.25;

    // ==================================================
    // GET TARGET
    // ==================================================
    if (app.selection.length === 0) {
        alert("Select text or a text frame.");
        return;
    }
    var target, story, tf;
    if (app.selection[0].hasOwnProperty("baseline")) {
        target = app.selection[0];
        story = target.parentStory;
        tf = story.textContainers[0];
    }
    else if (app.selection[0] instanceof TextFrame) {
        tf = app.selection[0];
        story = tf.parentStory;
        target = story;
    }
    else {
        alert("Select text or frame.");
        return;
    }

    // ==================================================
    // MAIN FLOW
    // ==================================================
    applyStyles(target, false);
    justify(tf);
    // 1️⃣ ONE COLUMN → shrink only
    shrinkToFit(target, tf);
    // 2️⃣ FALLBACK → 2 columns ONLY (no scaling/shrinking)
    if (tf.overflows) {
        tf.textFramePreferences.textColumnCount = 2;
        var prefs = tf.textFramePreferences;
        if ("balanceColumns" in prefs)
            prefs.balanceColumns = true;
        else if ("verticalBalanceColumns" in prefs)
            prefs.verticalBalanceColumns = true;
        story.recompose();
        applyStyles(target, true);
        story.recompose();
    }
    alert("Done ✔");

    // ==================================================
    // FUNCTIONS
    // ==================================================
    function justify(frame) {
        frame.textFramePreferences.verticalJustification =
            VerticalJustification.JUSTIFY_ALIGN;
    }

    // ---------- FONT SHRINK (ONLY 1-column phase)
    function shrinkToFit(range, frame) {
        var start = Math.min(range.characters[0].pointSize, MAX_SIZE);
        var finalSize = start;
        for (var s = start; s >= MIN_SIZE; s -= SIZE_STEP) {
            range.characters.everyItem().pointSize = s;
            range.characters.everyItem().leading   = s;
            story.recompose();
            if (!frame.overflows) {
                finalSize = s;
                break;
            }
        }
        normalizeText(range, finalSize);
    }

    // ---------- normalize size/leading only
    function normalizeText(range, size) {
        var chars = range.characters.everyItem();
        chars.pointSize = size;
        chars.leading   = size;
        story.recompose();
    }

    // ---------- APPLY STYLES
    function applyStyles(range, twoCol) {
        var body   = findStyle(twoCol ? TWO_BODY : BODY_STYLE);
        var first  = findStyle(twoCol ? TWO_FIRST : TOP_STYLE);
        var bottom = findStyle(BOTTOM_STYLE);
        var paras = range.paragraphs;
        for (var i = 0; i < paras.length; i++)
            paras[i].appliedParagraphStyle = body;
        if (paras.length > 0)
            paras[0].appliedParagraphStyle = first;
        if (!twoCol && bottom) {
            for (var p = 0; p < paras.length; p++) {
                if (paras[p].contents.toLowerCase().indexOf(KEYWORD) !== -1)
                    paras[p].appliedParagraphStyle = bottom;
            }
        }
    }

    // ---------- style finder
    function findStyle(name, container) {
        container = container || doc;
        var styles = container.paragraphStyles;
        for (var i = 0; i < styles.length; i++)
            if (styles[i].name === name) return styles[i];
        var groups = container.paragraphStyleGroups;
        for (var g = 0; g < groups.length; g++) {
            var found = findStyle(name, groups[g]);
            if (found) return found;
        }
        return null;
    }
})();
This script justifies the second column in the selected text so that it doesn't push the characters to the second line.
#target "InDesign"
(function () {
    if (!app.documents.length) {
        alert("No document open.");
        return;
    }
    if (!app.selection.length || !(app.selection[0] instanceof Text)) {
        alert("Select the schedule text first.");
        return;
    }
    var paras = app.selection[0].paragraphs.everyItem().getElements();
    var MAX_SCALE = 85;
    var MIN_SCALE = 55;
    var EXTRA_REDUCTION = 10;
    var PRECISION = 0.5;
    for (var p = 0; p < paras.length; p++) {
        var para = paras[p];
        if (para.contents.replace(/\s/g, "") === "") continue;
        var chars = para.characters;
        var tabIndexes = [];
        for (var c = 0; c < chars.length; c++) {
            if (chars[c].contents === "\t") {
                tabIndexes.push(c);
            }
        }
        if (tabIndexes.length < 2) continue;
        var startIndex = tabIndexes[0] + 1;
        var endIndex = tabIndexes[1] - 1;
        if (endIndex < startIndex) continue;
        var opponentRange = chars.itemByRange(startIndex, endIndex);
        // Reset opponent column to max
        opponentRange.horizontalScale = MAX_SCALE;
        // If it already fits, skip completely
        if (para.lines.length <= 1) {
            continue;
        }
        // ---- It wraps → perform binary shrink ----
        var low = MIN_SCALE;
        var high = MAX_SCALE;
        var mid;
        while ((high - low) > PRECISION) {
            mid = (low + high) / 2;
            opponentRange.horizontalScale = mid;
            para = paras[p];
            if (para.lines.length > 1) {
                high = mid;
            } else {
                low = mid;
            }
        }
        // Apply best fit
        opponentRange.horizontalScale = low;
        // Apply -10 ONLY because this row required shrinking
        var finalScale = low - EXTRA_REDUCTION;
        if (finalScale < MIN_SCALE) {
            finalScale = MIN_SCALE;
        }
        opponentRange.horizontalScale = finalScale;
    }
})();
This script soft returns after every "," "." or ":" in order to have people's names not cut off. Works exactly like the first one where you select the text.
#target "InDesign"
(function () {
    if (!app.documents.length) {
        alert("No document open.");
        return;
    }
    if (!app.selection.length || !app.selection[0].hasOwnProperty("lines")) {
        alert("Select a text frame or some text.");
        return;
    }
    var sel = app.selection[0];
    var lines = sel.lines;
    var fullText = sel.contents;
    // Find the index of the very last period in the selection
    var lastPeriodOverall = fullText.lastIndexOf(".");
    // Step 1: Insert soft returns
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var contents = line.contents;
        var lastComma = contents.lastIndexOf(",");
        var lastPeriod = contents.lastIndexOf(".");
        // Skip the very last period
        if (lastPeriod === lastPeriodOverall - line.insertionPoints[0].index) {
            lastPeriod = -1;
        }
        var insertIndex = Math.max(lastComma, lastPeriod);
        if (insertIndex !== -1) {
            try {
                line.insertionPoints[insertIndex + 1].contents = SpecialCharacters.FORCED_LINE_BREAK;
            } catch (e) {}
        }
    }
    // Refresh selection lines after insertion
    lines = sel.lines;
    // Step 2: Remove leading spaces
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var chars = line.characters;
        var removeCount = 0;
        // Count leading spaces
        for (var j = 0; j < chars.length; j++) {
            if (chars[j].contents === " ") {
                removeCount++;
            } else {
                break;
            }
        }
        // Remove them if any
        if (removeCount > 0) {
            line.characters.itemByRange(0, removeCount - 1).remove();
        }
    }
})();
↑Back to Top