Skip to main content
dublove
Legend
July 25, 2025
Answered

Can it automatically establish footnote relationships between static footnote text and the main text

  • July 25, 2025
  • 1 reply
  • 188 views

I have a piece of text. It has footnotes, but the footnotes are not properly linked to the main text.


In other words, the footnotes and the main text are separate.
They may be in the same text box, though.

Perhaps I need to select a uniform footnote format.

 

Now I need to link the footnotes to the main text.
I can only enter footnotes manually. Can this be done automatically with a script?

Correct answer m1b

Hi @dublove try this script. - Mark

/**
 * @file Link Fake Footnotes with Fake Sources.js
 *
 * Create real footnotes (source reference and actual footnote)
 * from fake footnote sources and fake footnotes.
 *
 * Usage:
 *  1. place cursor into story
 *  2. run script.
 *
 * Requirements:
 *   - must have fake sources styled with a character style (see settings).
 *   - must have fake footnotes styled with a paragraph style (see settings).
 *
 * @author m1b
 * @version 2025-07-25
 * @discussion https://community.adobe.com/t5/indesign-discussions/can-it-automatically-establish-footnote-relationships-between-static-footnote-text-and-the-main-text/m-p/15429016
 */
function main() {

    var settings = {
        fakeSourceCharacterStyleName: 'Source',
        fakeSourceParagraphStyleName: 'myfooter',
        matchFakeSourceNumberGrep: '\\[\\d+\\]',
        matchFakeFootnoteNumberRegexp: /^\[\d+\]\s*/,
    };

    var doc = app.activeDocument;

    if (
        0 === doc.selection.length
        || 'InsertionPoint' !== doc.selection[0].constructor.name
    )
        return alert('Please place your insertion point into a story and try again.');

    // get the styles
    var sourceStyle = getThing(doc.allCharacterStyles, 'name', settings.fakeSourceCharacterStyleName);

    if (!sourceStyle)
        return alert('Did not find any character style "' + settings.fakeSourceCharacterStyleName + '".');

    var footerStyle = getThing(doc.allParagraphStyles, 'name', settings.fakeSourceParagraphStyleName);

    if (!footerStyle)
        return alert('Did not find any paragraph style "' + settings.fakeSourceParagraphStyleName + '".');

    // document footnote options (see https://www.indesignjs.de/extendscriptAPI/indesign-latest/index.html#FootnoteOption.html)
    doc.footnoteOptions.properties = {
        footnoteTextStyle: footerStyle,
        markerPositioning: FootnoteMarkerPositioning.NORMAL_MARKER,
        prefix: '[',
        separatorText: '\t',
        showPrefixSuffix: FootnotePrefixSuffix.PREFIX_SUFFIX_BOTH, // or FootnotePrefixSuffix.PREFIX_SUFFIX_REFERENCE,
        suffix: ']',
    };

    // collect the fake footnote sources
    var fakeSources = getTexts({
        items: doc.selection[0],
        findGrepPreferences: { findWhat: settings.matchFakeSourceNumberGrep, appliedCharacterStyle: sourceStyle },
    });

    if (0 === fakeSources.length)
        return alert('Did not find any fake footnote sources styled with "' + settings.fakeSourceCharacterStyleName + '".');

    // collect the fake footnotes
    var fakeFootnotes = getTexts({
        items: doc.selection[0],
        findGrepPreferences: { findWhat: '^.*(?:\\r?|$)', appliedParagraphStyle: footerStyle },
    });

    if (0 === fakeFootnotes.length)
        return alert('Did not find any fake footnotes styled with "' + settings.fakeSourceParagraphStyleName + '".');

    var len = Math.min(fakeSources.length, fakeFootnotes.length);
    var story = fakeSources[0].parentStory;

    // create a temporary text frame for the fake footnotes
    var tempFrame = story.textContainers[0].parentPage.textFrames.add({ contents: '\r' });
    var tempStory = tempFrame.texts[0].parentStory;

    // move the fake footnotes to the temporary frame
    for (var i = len - 1; i >= 0; i--)
        fakeFootnotes[i].move(LocationOptions.AT_BEGINNING, tempStory);

    for (var i = len - 1, footnote, fakeSource, fakeFootnote, index, start, end; i >= 0; i--) {

        fakeSource = fakeSources[i];
        fakeFootnote = tempStory.paragraphs[i];

        index = fakeSource.index;
        fakeSource.contents = '';

        // make a new footnote
        footnote = story.footnotes.add(
            LocationOptions.AFTER,
            fakeSource.parentStory.insertionPoints[index]
        );

        // identify the start and end of the footnote's actual text, after the number and before carriage return
        start = (fakeFootnote.contents.match(settings.matchFakeFootnoteNumberRegexp) || [[]])[0].length;
        end = ('\r' === fakeFootnote.characters.lastItem().contents) ? fakeFootnote.contents.length - 2 : fakeFootnote.contents.length - 1;

        // add the text from the fake footnote to the real footnote
        fakeFootnote.characters.itemByRange(start, end).move(LocationOptions.AT_END, footnote.texts[0]);

    }

    // clean up
    tempFrame.remove();

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Link Fake Footnotes');

/**
 * Returns texts found in `items`.
 *
 * Note: this is intended to use with
 * findGrep - it won't work without
 * a `findGrepPreferences` object.
 * ----------------------------------------------
 * Example - styled paragraphs of document:
 *
 *   var texts = getTexts({
 *     items: doc,
 *     findGrepPreferences: {
 *         appliedParagraphStyle: 'My Style',
 *     },
 *   });
 * ----------------------------------------------
 * @author m1b
 * @version 2024-04-24
 * @param {Object} options
 * @param {*} options.items - the DOM objects to extract text from, eg. Document or Document.selection.
 * @param {Object} options.findGrepPreferences - properties to configure findGrep.
 * @param {Function} [options.filter] - function that, given the object, returns true to collect its text (default: no filter).
 * @returns {Array<Text>}
 */
function getTexts(options) {

    options = options || {};

    if (undefined == options.items)
        throw Error('getTexts: expected `options.items` parameter.');

    if ('undefined' == options.findGrepPreferences)
        throw Error('getTexts: expected `options.findGrepPreferences` parameter.');

    var items = options.items,
        texts = [];

    if ('Document' === items.constructor.name)
        items = items.stories;

    if (!items.hasOwnProperty(0))
        items = [items];

    // set up find grep
    app.findGrepPreferences = NothingEnum.NOTHING;
    app.changeGrepPreferences = NothingEnum.NOTHING;

    for (var key in options.findGrepPreferences)
        if (options.findGrepPreferences.hasOwnProperty(key))
            app.findGrepPreferences[key] = options.findGrepPreferences[key];

    for (var i = 0; i < items.length; i++) {

        if (
            undefined != options.filter
            && true !== options.filter(items[i])
        )
            continue;

        if ('InsertionPoint' === items[i].constructor.name)
            items[i] = items[i].parentStory.texts[0];

        if (
            undefined != items[i].contents
            && 0 === items[i].contents.length
        )
            continue;

        if ('function' !== typeof items[i].findGrep)
            continue;

        texts = texts.concat(items[i].findGrep());

    }

    return texts;

};

/**
 * Returns a thing with matching property.
 * If `key` is undefined, evaluate the object itself.
 * @author m1b
 * @version 2024-04-21
 * @param {Array|Collection} things - the things to look through.
 * @param {String} [key] - the property name (default: undefined).
 * @param {*} value - the value to match.
 * @returns {*?} - the thing, if found.
 */
function getThing(things, key, value) {

    for (var i = 0; i < things.length; i++)
        if ((undefined == key ? things[i] : things[i][key]) == value)
            return things[i];

};

1 reply

m1b
Community Expert
m1bCommunity ExpertCorrect answer
Community Expert
July 25, 2025

Hi @dublove try this script. - Mark

/**
 * @file Link Fake Footnotes with Fake Sources.js
 *
 * Create real footnotes (source reference and actual footnote)
 * from fake footnote sources and fake footnotes.
 *
 * Usage:
 *  1. place cursor into story
 *  2. run script.
 *
 * Requirements:
 *   - must have fake sources styled with a character style (see settings).
 *   - must have fake footnotes styled with a paragraph style (see settings).
 *
 * @author m1b
 * @version 2025-07-25
 * @discussion https://community.adobe.com/t5/indesign-discussions/can-it-automatically-establish-footnote-relationships-between-static-footnote-text-and-the-main-text/m-p/15429016
 */
function main() {

    var settings = {
        fakeSourceCharacterStyleName: 'Source',
        fakeSourceParagraphStyleName: 'myfooter',
        matchFakeSourceNumberGrep: '\\[\\d+\\]',
        matchFakeFootnoteNumberRegexp: /^\[\d+\]\s*/,
    };

    var doc = app.activeDocument;

    if (
        0 === doc.selection.length
        || 'InsertionPoint' !== doc.selection[0].constructor.name
    )
        return alert('Please place your insertion point into a story and try again.');

    // get the styles
    var sourceStyle = getThing(doc.allCharacterStyles, 'name', settings.fakeSourceCharacterStyleName);

    if (!sourceStyle)
        return alert('Did not find any character style "' + settings.fakeSourceCharacterStyleName + '".');

    var footerStyle = getThing(doc.allParagraphStyles, 'name', settings.fakeSourceParagraphStyleName);

    if (!footerStyle)
        return alert('Did not find any paragraph style "' + settings.fakeSourceParagraphStyleName + '".');

    // document footnote options (see https://www.indesignjs.de/extendscriptAPI/indesign-latest/index.html#FootnoteOption.html)
    doc.footnoteOptions.properties = {
        footnoteTextStyle: footerStyle,
        markerPositioning: FootnoteMarkerPositioning.NORMAL_MARKER,
        prefix: '[',
        separatorText: '\t',
        showPrefixSuffix: FootnotePrefixSuffix.PREFIX_SUFFIX_BOTH, // or FootnotePrefixSuffix.PREFIX_SUFFIX_REFERENCE,
        suffix: ']',
    };

    // collect the fake footnote sources
    var fakeSources = getTexts({
        items: doc.selection[0],
        findGrepPreferences: { findWhat: settings.matchFakeSourceNumberGrep, appliedCharacterStyle: sourceStyle },
    });

    if (0 === fakeSources.length)
        return alert('Did not find any fake footnote sources styled with "' + settings.fakeSourceCharacterStyleName + '".');

    // collect the fake footnotes
    var fakeFootnotes = getTexts({
        items: doc.selection[0],
        findGrepPreferences: { findWhat: '^.*(?:\\r?|$)', appliedParagraphStyle: footerStyle },
    });

    if (0 === fakeFootnotes.length)
        return alert('Did not find any fake footnotes styled with "' + settings.fakeSourceParagraphStyleName + '".');

    var len = Math.min(fakeSources.length, fakeFootnotes.length);
    var story = fakeSources[0].parentStory;

    // create a temporary text frame for the fake footnotes
    var tempFrame = story.textContainers[0].parentPage.textFrames.add({ contents: '\r' });
    var tempStory = tempFrame.texts[0].parentStory;

    // move the fake footnotes to the temporary frame
    for (var i = len - 1; i >= 0; i--)
        fakeFootnotes[i].move(LocationOptions.AT_BEGINNING, tempStory);

    for (var i = len - 1, footnote, fakeSource, fakeFootnote, index, start, end; i >= 0; i--) {

        fakeSource = fakeSources[i];
        fakeFootnote = tempStory.paragraphs[i];

        index = fakeSource.index;
        fakeSource.contents = '';

        // make a new footnote
        footnote = story.footnotes.add(
            LocationOptions.AFTER,
            fakeSource.parentStory.insertionPoints[index]
        );

        // identify the start and end of the footnote's actual text, after the number and before carriage return
        start = (fakeFootnote.contents.match(settings.matchFakeFootnoteNumberRegexp) || [[]])[0].length;
        end = ('\r' === fakeFootnote.characters.lastItem().contents) ? fakeFootnote.contents.length - 2 : fakeFootnote.contents.length - 1;

        // add the text from the fake footnote to the real footnote
        fakeFootnote.characters.itemByRange(start, end).move(LocationOptions.AT_END, footnote.texts[0]);

    }

    // clean up
    tempFrame.remove();

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Link Fake Footnotes');

/**
 * Returns texts found in `items`.
 *
 * Note: this is intended to use with
 * findGrep - it won't work without
 * a `findGrepPreferences` object.
 * ----------------------------------------------
 * Example - styled paragraphs of document:
 *
 *   var texts = getTexts({
 *     items: doc,
 *     findGrepPreferences: {
 *         appliedParagraphStyle: 'My Style',
 *     },
 *   });
 * ----------------------------------------------
 * @author m1b
 * @version 2024-04-24
 * @param {Object} options
 * @param {*} options.items - the DOM objects to extract text from, eg. Document or Document.selection.
 * @param {Object} options.findGrepPreferences - properties to configure findGrep.
 * @param {Function} [options.filter] - function that, given the object, returns true to collect its text (default: no filter).
 * @returns {Array<Text>}
 */
function getTexts(options) {

    options = options || {};

    if (undefined == options.items)
        throw Error('getTexts: expected `options.items` parameter.');

    if ('undefined' == options.findGrepPreferences)
        throw Error('getTexts: expected `options.findGrepPreferences` parameter.');

    var items = options.items,
        texts = [];

    if ('Document' === items.constructor.name)
        items = items.stories;

    if (!items.hasOwnProperty(0))
        items = [items];

    // set up find grep
    app.findGrepPreferences = NothingEnum.NOTHING;
    app.changeGrepPreferences = NothingEnum.NOTHING;

    for (var key in options.findGrepPreferences)
        if (options.findGrepPreferences.hasOwnProperty(key))
            app.findGrepPreferences[key] = options.findGrepPreferences[key];

    for (var i = 0; i < items.length; i++) {

        if (
            undefined != options.filter
            && true !== options.filter(items[i])
        )
            continue;

        if ('InsertionPoint' === items[i].constructor.name)
            items[i] = items[i].parentStory.texts[0];

        if (
            undefined != items[i].contents
            && 0 === items[i].contents.length
        )
            continue;

        if ('function' !== typeof items[i].findGrep)
            continue;

        texts = texts.concat(items[i].findGrep());

    }

    return texts;

};

/**
 * Returns a thing with matching property.
 * If `key` is undefined, evaluate the object itself.
 * @author m1b
 * @version 2024-04-21
 * @param {Array|Collection} things - the things to look through.
 * @param {String} [key] - the property name (default: undefined).
 * @param {*} value - the value to match.
 * @returns {*?} - the thing, if found.
 */
function getThing(things, key, value) {

    for (var i = 0; i < things.length; i++)
        if ((undefined == key ? things[i] : things[i][key]) == value)
            return things[i];

};
dublove
dubloveAuthor
Legend
July 25, 2025

Hi m1b.

So fast?
What an amazing speed, it's unbelievable.

Thank you very much.