1

I'm trying to replace some text within a Word Online document but cannot get it to work.

'{{test}} , [[test]] , {test}' results in '13 , 2 , 3' and not '1 , 2 , 3'.

The first text seems to be processed twice.

Any help much appreciated!

Office.initialize = function(reason) {

    function ready() {

        var myTags = [
            { "value": "1", "text": "{{test}}" },
            { "value": "2", "text": "[[test]]" },
            { "value": "3", "text": "{test}" }
        ];

        async function FillTag(tag) {

            await Word.run(async function(context) {

                    var options = Word.SearchOptions.newObject(context);
                    options.matchWildCards = false;

                    var searchResults = context.document.body.search(tag.text, options);
                    context.load(searchResults, 'text');

                    await context.sync();

                    searchResults.items.forEach(function(item) {
                        item.insertText(tag.value, Word.InsertLocation.replace);
                    });
                    await context.sync();
                })
                .catch(function(error) {
                    console.log('Error: ' + JSON.stringify(error));
                    if (error instanceof OfficeExtension.Error) {
                        console.log('Debug info: ' + JSON.stringify(error.debugInfo));
                    }
                });
        }

        async function ProcessArray(myTags) {
            myTags.forEach(async function(tag) {
                await FillTag(tag);
            });
        }

        ProcessArray(myTags);
    }

    if (document.readyState !== 'loading') {
        ready();
    }
    else {
        document.addEventListener('DOMContentLoaded', ready);
    }
};
5
  • Please provide an small example of the document content that you are searching. Commented Jan 12, 2018 at 16:49
  • Hi Rick, Just paste this sentence {{test}} , [[test]] , {test} into a blank document. I hope I understand you correctly. The tags I'm searching for are stored in the myTags object in my example. Commented Jan 12, 2018 at 17:19
  • @DutchDan -- I've been repro'd your issue and am troubleshooting now. Will hopefully have more info for you soon. Commented Jan 12, 2018 at 19:10
  • Hi Kim, many thanks for looking. I've been trying to search the body for the tag and then selecting and deleting it. If you do that then the text is gone but one can still see the selection in the background. Something stays behind is my feeling. Commented Jan 12, 2018 at 19:20
  • @DutchDan -- seems like I figured this out...please see my answer below. Commented Jan 12, 2018 at 19:56

2 Answers 2

2

In your ProcessArray() function, try replacing the forEach statement with a for...of statement, as shown here:

async function ProcessArray(myTags) {
    for (var tag of myTags) {
        await FillTag(tag);
    }
}

Seems that the forEach statement fires off multiple asynchronous calls, without actually awaiting the completion of FillTag each time. If you replace the forEach statement with for...of as shown above, you should get the expected result.


UPDATE (additional info re code structure):

@DutchDan -- now that your initial issue has been resolved, here's a more optimal way to structure your code.

Office.initialize = function () {
    $(document).ready(function () {        
        FindAndReplace();
    });
};

async function FindAndReplace() {

    var myTags = [
        { "value": "1", "text": "{{test}}" },
        { "value": "2", "text": "[[test]]" },
        { "value": "3", "text": "{test}" }
    ];

    await Word.run(async (context) => {

        for (var tag of myTags) {
            var options = Word.SearchOptions.newObject(context);
            options.matchWildCards = false;

            var searchResults = context.document.body.search(tag.text, options);

            context.load(searchResults, 'text');

            await context.sync();

            searchResults.items.forEach(function (item) {
                item.insertText(tag.value, Word.InsertLocation.replace);
            });

            await context.sync();
        }
    }).catch(errorHandler);
}

Note: You can quickly and easily try this snippet yourself by using Script Lab (https://aka.ms/getscriptlab). Simply install the Script Lab add-in (free), then choose "Import" in the navigation menu, and use the following Gist URL: https://gist.github.com/kbrandl/b0c9d9ce0dd1ef16d61372cb84636898.

Sign up to request clarification or add additional context in comments.

3 Comments

Kim, you're the best!! It works just as expected. Never thought there would be my error. Again many thanks!
@DutchDan, happy I could help. I've also updated my answer to include a code sample that shows a more optimal way to structure your code.
Great answer. See the 12/15/18 edit to my answer for an alternative that might be better in cases where there are a much larger number of search tags.
1

This is more a debugging suggestion than an answer, but it can be edited later. Please install the Script Lab tool from AppSource into Word. One of the sample snippets that you'll find in it is called Search. One of the functions in the snippet is basicSearch. I replaced the search text "Online" with "{{test}}" and I replaced the line that highlights found text in yellow with the following line:

results.items[i].insertText("1", Word.InsertLocation.replace);

This worked fine, so in simple enough scenarios, it can find and replace "{{test}}" accurately.

Could you please try this yourself and then gradually change the method to more closely resemble yours and see at what point it begins to break?


Edit 1/15/18:

@Kim Brandl's answer is probably the best for you, assuming that you really have just 3 search strings. It does, however, have a context.sync inside a loop. Since each sync is a roundtrip to the Office host, that can be a peformance problem when the number of inputs is large and/or the add-in is running in Office Online (which means the Office host is across the internet instread of the same machine).

For anyone reading this who has a large number of input strings, here's a solution that guarantees no more than 3 syncs are needed in the entire Word.run. It also directly attacks the source of the problem that you are trying to solve, which is the relative locations of some found ranges to others (specifically, some are inside others).

The strategy, which I also used in Word-Add-in-Angular2-StyleChecker, is to first load all the ranges and then use the Range.compareLocationWith method and the LocationRelation enum to find the relative location information you need. Finally, use each range's relative location to other ranges to determine whether/how to process it.

Here's the function. Following Kim's example I put the whole snippet in this gist, which you can import to Script Lab tool from AppSource . (See instructions in Kim Brandl's answer.)

async function FindAndReplace() {

    let myTags = [
        { "value": "1", "text": "{{test}}" },
        { "value": "2", "text": "[[test]]" },
        { "value": "3", "text": "{test}" },
        { "value": "4", "text": "bob" },
        { "value": "5", "text": "bobb" },
        { "value": "6", "text": "ssally" },
        { "value": "7", "text": "sally" }
    ];

    let allSearchResults = [];

    await Word.run(async (context) => {    
        for (let tag of myTags) {    
            let options = Word.SearchOptions.newObject(context);
            options.matchWildCards = false;
            let searchResults = context.document.body.search(tag.text, options);
            searchResults.load('text');

            // Store each set of found ranges and the text that should replace 
            // them together, so we don't have to reconstruct the correlation 
            // after the context.sync.
            let correlatedSearchResult = {
                searchHits: searchResults, 
                replacementString: tag.value
            }           
            allSearchResults.push(correlatedSearchResult);       
        }

        await context.sync();

        // Now that we've loaded the found ranges we correlate each to
        // its replacement string, and then find each range's location relation
        // to every other. For example, 'bob' would be Inside 'xbobx'. 
        let correlatedFoundRanges = [];
        allSearchResults.forEach(function (correlatedSearchResult) {
            correlatedSearchResult.searchHits.items.forEach(function (foundRange) {
                let correlatedFoundRange = {
                    range: foundRange,
                    replacementText: correlatedSearchResult.replacementString,
                    locationRelations: []
                }
                correlatedFoundRanges.push(correlatedFoundRange);                
            });
        });

        // Two-dimensional loop over the found ranges to find each one's 
        // location relation with every other range.
        for (let i = 0; i < correlatedFoundRanges.length; i++) {
            for (let j = 0; j < correlatedFoundRanges.length; j++) {
                if (i !== j) // Don't need the range's location relation with itself.
                {
                    let locationRelation = correlatedFoundRanges[i].range.compareLocationWith(correlatedFoundRanges[j].range);
                    correlatedFoundRanges[i].locationRelations.push(locationRelation);
                }
            }
        }

        // It is not necesary to *explicitly* call load() for the 
        // LocationRelation objects, but a sync is required to load them.
        await context.sync();    

        let nonReplaceableRanges = [];
        correlatedFoundRanges.forEach(function (correlatedFoundRange) {
            correlatedFoundRange.locationRelations.forEach(function (locationRelation) {
                switch (locationRelation.value) {
                    case "Inside":
                    case "InsideStart":
                    case "InsideEnd":

                        // If the range is contained inside another range,
                        // blacklist it.
                        nonReplaceableRanges.push(correlatedFoundRange);
                        break;
                    default:
                        // Leave it off the blacklist, so it will get its 
                        // replacement string.
                        break;
                }
            });
        });

        // Do the replacement, but skip the blacklisted ranges.
        correlatedFoundRanges.forEach(function (correlatedFoundRange) {
            if (nonReplaceableRanges.indexOf(correlatedFoundRange) === -1) {
                correlatedFoundRange.range.insertText(correlatedFoundRange.replacementText, Word.InsertLocation.replace);
            }
        })

        await context.sync();
    });
}

3 Comments

Hi Rick, sorry for being so vague! The problem is not that the text isn't being replaced but '{{test}}' is being replaced twice. First by it's value 1 (see myTag object) and later the number 3 is added also. So with iteration three {test} is being found within {{test}} which should have been replaced by 1. I make it sound complicated but {{test}} is replaced by 1 but some ghost text remains so {test} is also found. So the replacement should look like 1 , 2 , 3 but it ends up as 13 , 2 , 3
Actually, your question was clear. I just gave a hurried answer. To make up for it, I've edited it with an alternative solution that might be better in some scenarios than @Kim Brandl's.
Hi Rick, sorry for reacting sooo late but just wanted to thank you for that amazing piece of code above! It helped me enormously to better understand what's going on under the hood and it served as a blueprint for a new version of my add-in. Thanks again.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.