replacing strings in a document and undo

2019-05-07 17:45发布

问题:

In a mailMerge script I'm working on I use .replaceText() to replace fields with their corresponding values in a database.

The interface allows to test in the document to see if the result is looking as expected and I need to have a 'UNDO' function to get my fields in their original position so that I can use it with other values.(this script is bounded to a document in a side bar, see this post for illustration)

The script below does that pretty well by keeping in memory the field names an their replacement values. The only detail that bothers me is that I had to define a special "empty" label for fields that have no values in the current test data to prevent losing their track in the document. (I used a numbered identifier like °vide12°).

This is working perfectly but it's not ideal since the document in test mode is not exactly a representation of the final document because of these °videXX° that I use...

The question is : does anyone have a better idea or another approach to "localize" the replacement data when there is no data in a less visible way ? (I know this sound weird... that's why I explain the whole situation :-)

Considering the way Google Docs are build I thought that I could get the complete element structure and rebuild the doc from that info but I'm afraid it won't be possible since the smallest element is a paragraph and fields are mainly just single words...

Here is the relevant part of the code I use, I added a few comments to make it (hopefully) clear.

function valuesInDoc(e){ // this function replaces the fields with database values
  var app = UiApp.getActiveApplication(); 
  var listVal = UserProperties.getProperty('listSel').split(',');
  var replacements = [];
  var doc = DocumentApp.getActiveDocument();
  var body = doc.getBody();
  var find = body.findText('#ch');
  if(find == null){var ui = DocumentApp.getUi() ; ui.alert("Aucun champ (#chX#) trouvé dans le document... Veuillez insérer des identifiants aux endroits souhaités");return app};
  var curData =   UserProperties.getProperty('selItem').split('|');
  var Headers = [];
  var OriHeaders = UserProperties.getProperty('Headers').split('|');
  for(n=0;n<OriHeaders.length;++n){
    Headers.push('#'+OriHeaders[n]+'#');
  }
  var fctSpe = 0 ;
  for(var i in Headers){if(Headers[i].indexOf('SS')>-1){fctSpe = i}}
  for(var n=0;n<listVal.length;++n){
    var realIdx = Number(listVal[n]);
    var newField = ChampSpecial(curData,realIdx,fctSpe);
    if(newField!=''){replacements.push(newField+'∏'+'#ch'+(n+1)+'#')};
    //Logger.log('value in '+n+'='+realIdx+'  >>  '+Headers[realIdx]+'  =  '+ChampSpecial(curData,realIdx,fctSpe))
    app.getElementById('textField'+(n+1)).setHTML(ChampSpecial(curData,realIdx,fctSpe));
    if(e.parameter.source!='dataSelection'){
    body.replaceText('#ch'+(n+1)+'#',newField);
    }
  }
  UserProperties.setProperty('replacements',replacements.join('|'));// memorize the replacement pattern
  cloakOn();// hide hidden fields
  return app;
}



function fieldsInDoc(e){ // this function does the reverse process and restores the field identifiers
  cloakOff();// show hidden fields
  var replacements = UserProperties.getProperty('replacements').split('|');
  var doc = DocumentApp.getActiveDocument();
  var body = doc.getBody();
  for(var n=0;n<replacements.length;++n){
    var field = replacements[n].split('∏')[1];
    var testVal = replacements[n].split('∏')[0];    
    body.replaceText(testVal,field);
    }
}

function ChampSpecial(curData,idx,ref){ // this function handles a special case for a specific field, the relevant part is right below, see comment
  if(idx==-1){return''};
  if(curData[idx-1]==''){return'°vide'+idx+'°'};// this is the "empty" identifier
  if(idx<ref){return curData[idx]};
  if(idx>ref){return curData[idx-1]}
  var firstSpace = curData[idx-1].indexOf(' ');
  var apos = curData[idx-1].indexOf("'");
//Logger.log('firstSpace='+firstSpace+'  apos='+apos)
  if(firstSpace<4&&firstSpace>-1){return curData[idx-1].substring(firstSpace+1)};
  if(apos<3&&apos>-1){return curData[idx-1].substring(apos+1)};
  return curData[idx-1];
}

EDIT : thanks to Mogsdad's brilliant answer I wrote these 2 functions to hide/show the unused fields. Sinc in my case I use °XX° (XX=2 digit number) to keep track of the unused fields I had to modify his code to look for this particular string and used 2 loops to get all the fields.

I call these function from the menu AND from the two other functions that handle the replacement (I updated the code above as well)

It might appear a waste of time since I iterate more that 100 times but the result is instantaneous... so why bother ? here is the code in case it gives someone an idea.

function cloakOn() {
  var doc = DocumentApp.getActiveDocument();
  var body = doc.getBody();
  var found = [];
  for(var n=1;n<23;++n){
    for(var f=0;f<5;++f){
      if(f==0){found[f] = body.findText('°'+Utilities.formatString("%02d",n)+'°')}else{found[f] = body.findText('°'+Utilities.formatString("%02d",n)+'°',found[f-1])}
      if(found[f]!=null){
        var elemTxt = found[f].getElement().asText();
        elemTxt.setFontSize(found[f].getStartOffset(), found[f].getEndOffsetInclusive(),0)
        var background = elemTxt.getBackgroundColor(found[f].getStartOffset()) || "#ffffff";
        elemTxt.setForegroundColor(found[f].getStartOffset(), found[f].getEndOffsetInclusive(), background);
      }
    }
  }
}

function cloakOff() {
  var doc = DocumentApp.getActiveDocument();
  var body = doc.getBody();
  var found = [];
  for(var n=1;n<23;++n){
    for(var f=0;f<5;++f){
      if(f==0){found[f] = body.findText('°'+Utilities.formatString("%02d",n)+'°')}else{found[f] = body.findText('°'+Utilities.formatString("%02d",n)+'°',found[f-1])}
      if(found[f]!=null){
        var elemTxt = found[f].getElement().asText();
        var size = elemTxt.getParent().getFontSize();
        elemTxt.setFontSize(found[f].getStartOffset(), found[f].getEndOffsetInclusive(),size)
        var background = elemTxt.getBackgroundColor(found[f].getStartOffset()) || "#000000";
        elemTxt.setForegroundColor(found[f].getStartOffset(), found[f].getEndOffsetInclusive(), background);
      }
    }
  }
}

回答1:

Serge, I've been working on the very same problem! I've got a partial workaround to share, and some ideas to take it further.

There is no way to embed hidden text in Google Docs, as eloquently stated by Gill on the old forum. If there was, your mailmerge would be trivial!

How about making your tags or "cookies" (almost) invisible, though? Below is a scriplet that adds a "cloaking" function to a document. It has extras as well; it queries the user for text to cloak, then searches for all instances of that text and cloaks them. The idea I settled on was to make the text as small as possible (fontsize 0) and to match the foreground color to the background color.

// in menu:       .addItem('Text Cloaking', 'cloakOn')

/**
 * Find all matches of target text in current document, and cloak them.
 * At this time, that consists of making the text tiny, but still visible.
 * This is an experiment - my hope was to find a way to implement something
 * like document variables, placeholders that would not be forgotten, so
 * that values could be changed, or even dynamic.
 *
 * @param {String} target     (Optional) The text or regex to search for. 
 *                            See Body.findText() for details.
 * @param {String} background (Optional) The desired highlight color.
 *                            A default orange is provided.
 */
function cloakOn(target) {
  // If no search parameter was provided, ask for one
  if (arguments.length == 0) {
    var ui = DocumentApp.getUi();
    var result = ui.prompt('Text Cloaking',
      'Enter text to cloak:', ui.ButtonSet.OK_CANCEL);
    // Exit if user hit Cancel.
    if (result.getSelectedButton() !== ui.Button.OK) return;
    // else
    target = result.getResponseText();
  }
  var doc = DocumentApp.getActiveDocument();
  var bodyElement = doc.getBody();
  var searchResult = bodyElement.findText(target);

  while (searchResult !== null) {
    var thisElement = searchResult.getElement();
    var thisElementText = thisElement.asText();

    //Logger.log(url);
    thisElementText.setFontSize(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),0);
    var background = thisElementText.getBackgroundColor(searchResult.getStartOffset()) || "#ffffff";
    thisElementText.setForegroundColor(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),
                                       background);

    // search for next match
    searchResult = bodyElement.findText(target, searchResult);
  }
}

To make use of this in the text-replacement operation, the replacement text would carry a cloaked tag (as you're doing). I think you'd want to make your tags as short as possible, so that the white space they occupy in the final document is very small - I was playing with using a series of unicode characters as digits, to give a large range of 2-digit 'numbers' that would be unlikely to show up in any other context.