Google Apps Script - How to get Text from Paragraph? - google-apps-script

Trying to write a function that pulls text between <> brackets in a document, writes to html and allows the user to replace the bracketed text with a user input field (via the find and replace function). Having trouble getting the actual bracketed text from the google doc. The closest I have gotten is returning the paragraph the bracketed text is in, but that does not work because then the entire paragraph gets replaced instead of only the bracketed text.
This is the most recent error:
TypeError: Cannot find function getStartOffset in object Text. (line
11, file "Code", project "Find and Replace Script")
function doGet() {
var docURL = DocumentApp.openByUrl('XXXX')
var body = docURL.getBody();
var fel0 = body.findText('<*>')
var el0 = fel0.getElement();
var startOffset = el0.getStartOffset();
var endOffset = el0.getEndOffsetInclusive();
var text = el0.asText().getText()
if (elements[0].isPartial())
text = el0.substring(startOffset,endOffset+1);
var template = HtmlService.createTemplateFromFile('urlinput.html');
template.el0 = el0;
return template.evaluate();
}
function findreplace(form){
var docURL = DocumentApp.openByUrl('XXXX')
var body = docURL.getBody();
body.replaceText(body.findText('<*>',fel0).getElement().asText().getText())
}
How do I get the actual found text from that body.findText('<*>') object? A big part that makes this difficult is the * wildcard between the <> brackets.

Try this:
This is just a quick little example to help you to get past your current problem.
function findAndReplace(){
var doc=DocumentApp.getActiveDocument();
var body=doc.getBody();
var rel1=body.findText('<.*>');
var el1=rel1.getElement();
var t=el1.asText().getText();
var x=rel1.getStartOffset();
var y=rel1.getEndOffsetInclusive();
var p=rel1.isPartial();
el1.asText().replaceText('<.*>', 'You\'ve been replaced.');
//Logger.log('\nt:%s\nx:%s\ny:%s\np:%s',t,x,y,p?'true':'false');
//var end="is near";
}
This also works:
function findAndReplace(){
DocumentApp.getActiveDocument().getBody().replaceText('<.*>', 'You\'ve been replaced.');
}
This is what you actually asked for:
As usual, I read some of the problem and then went off an did what I wanted to do. I guess you wanted to get the text. So here's another short example.
function findMyText(){
var body=DocumentApp.getActiveDocument().getBody();
var rel=body.findText('<.*>');
var el=rel.getElement();
var eltxt=el.asText().getText();
var txt=eltxt.slice(rel.getStartOffset()+1,rel.getEndOffsetInclusive())
DocumentApp.getUi().alert(txt);
}
I think your only problem was that you needed the .* which means zero or more of any character. The search pattern is a regular expression enclosed in quotes. I hope this helps.

Related

Replace text in a run using RichTextValueBuilder in a Google Spreadsheet using Google Apps Script

I am trying to manipulate the rich text value of a field.
I can get a cell's rich text value, I can get the runs, and I can update links but I can't figure out how to remove links or manipulate part of a run. Using this cell as an example:
The word emergency, including some spaces before and after, is a link. I want to keep the link and spaces but I want to remove the link from the spaces.
var richTextValue = range.getRichTextValue();
var newRichTextValue = richTextValue.copy();
// get all the runs
richTextValue.getRuns().forEach(run => {
// we only need runs that are links
if (run.getLinkUrl()) {
// get the run text
var text = run.getText();
// if the text of this run starts with spaces
if (hasLeadingSpaces = text.match(/^\s+/)) {
var numSpaces = hasLeadingSpaces[0].length;
// now what?
}
}
});
I know where the spaces start and end but there doesn't seem to be a way to set text of a run or just part of a RichTextValue.
RichTextValueBuilder.setText only lets you set all text, not partial.
I can make a copy of the run, so I get a RichTextValueBuilder and then set the entire text, and re-add the link to the relevant part, but how can I save that RichTextValueBuilder back to the whole?
For this particular case I can propose this code:
function myFunction() {
var range = SpreadsheetApp.getActiveSheet().getRange('a1');
var richTextValue = range.getRichTextValue();
var all_text = richTextValue.getText();
var runs = richTextValue.getRuns();
var link = runs.filter(r => r.getLinkUrl())[0]; // a first link from the cell
var link_url = link.getLinkUrl();
var link_text = link.getText().trim();
var link_style = link.getTextStyle();
all_text = all_text.replace(/\s+/g, ' '); // hope the extra spaces are only around link
var start = all_text.indexOf(link_text); // hope the text is unique
var end = start + link_text.length;
const newRichTextValue = SpreadsheetApp.newRichTextValue()
.setText(all_text)
.setLinkUrl(start, end, link_url)
.setTextStyle(start, end, link_style)
.build();
range.clear().setRichTextValue(newRichTextValue);
}
But it's probably is not a universal solution. For example it doesn't keep the text styles of the text around the link. Etc.

App Script Google Docs Replace Text in Body

I'm having trouble, replacing text in a Google Doc, using App Script.
Inside the document I have certain tags/tokens like ${TOKEN.SUBTOKEN}. I get the document as text and extract all the TOKEN's as a list with a Regex without issue, but when I want to use the replaceText function, I have issues. When I execute the replaceText line, it doesn't change the value in the document, but returns an element from the document. I can't find a way to replace the text I'm targeting.
var doc = DocumentApp.openById(docId);
var docBody = doc.getBody();
// tokenValues is the Object that contains the values to replace.
var fields = docBody.getText().match(/\$\{[a-z0-9\.\_]+\}/gi);
for (var i = 0; i < fields.length; i++ ) {
fields[i] = fields[i].substring(2, fields[i].length-1);
}
for (var i; i < fields.length; i++) {
Logger.log(docBody.replaceText(new RegExp('\\${ *' + fields[i].replace('/\./g', '\.') + '\ *}', tokenValues[i]));
}
How should I approach this, I'm having trouble with It because the documentation is not that explicit, (or maybe I don't understand it)
I did something similar to your question.
Here's the text:
{{ClientName}} would like to have a {{Product}} {{done/created}}. The purpose of this {{Product}} is to {{ProductPurpose}}. We have experience with such testing and development, and will develop and test the {{Product}} for {{ClientName}}.
Here's the code:
function searchReplace(){
var regex1=new RegExp('{{([a-zA-Z/]+)}}','g');
var tA=[];
var srchA=[];
var fldA=[];
var s=DocumentApp.getActiveDocument().getBody().getText();
while((tA=regex1.exec(s))!==null){//get all fields
fldA.push(tA[1]);
}
for(var i=0;i<fldA.length;i++){//Get unique fields
if(srchA.indexOf(fldA[i])==-1){
srchA.push(fldA[i]);
}
}
var doc=DocumentApp.getActiveDocument();
var body=doc.getBody();
for(var i=0;i<srchA.length;i++){
var searchPattern=Utilities.formatString('\\{\\{(%s)\\}\\}', srchA[i]);//need double backslashes here.
var prompt=Utilities.formatString('Enter Replacement for %s',srchA[i]);
var resp=DocumentApp.getUi().prompt('Replacement Text',prompt , DocumentApp.getUi().ButtonSet.OK_CANCEL)
if(resp.getSelectedButton()==DocumentApp.getUi().Button.OK){
body.replaceText(searchPattern, resp.getResponseText());//replaces all instances of the field
}
}
}

Google Apps Script: find all occurrences and set style

I'm having trouble figuring out how find/next works in Google Apps Script.
In the example here I'm trying to superscript the numbers in a list of authors. But my loop doesn't exit, presumably because it just keeps selecting the first item over and over again.
EDIT: Updated with 'from' parameter: Loop still does not exit? I added some log comments and it is still getting stuck on the first item (start offset and end offset are both = 10)
var doc = DocumentApp.create('test1');
var authors = "Josh Smith1, Zach Johnson2";
var paragraph = doc.getBody().appendParagraph(authors);
var text = paragraph.editAsText();
var range = text.findText('[0-9]+');
while(range)
{
Logger.log("START OFFSET: " + range.getStartOffset());
Logger.log("END OFFSET: " + range.getEndOffsetInclusive());
Logger.log("PARAGRAPH ELEMENT: "+ paragraph.editAsText().getText());
text.setTextAlignment(range.getStartOffset(),range.getEndOffsetInclusive(),DocumentApp.TextAlignment.SUPERSCRIPT);
range = text.findText('[0-9]+', range);
}
ORIGINAL
var authors = "Josh Smith1, Zach Johnson2";
var paragraph = doc.getBody().appendParagraph(authors);
var text = paragraph.editAsText();
var range = text.findText('[0-9]+');
while(range)
{
text.setTextAlignment(range.getStartOffset(),range.getEndOffsetInclusive(),DocumentApp.TextAlignment.SUPERSCRIPT);
range = text.findText('[0-9]+');
}
INFO: The solution provided here is correct, but as for now there is a bug in Google Apps Script, which would make this code to loop. There is no fix to the date.
You are correct about that it's stuck on the first item. However, it's really easy to fix. findText() function can take one or two parameters. The first parameter is always search pattern and the second optional parameter is a place from where it should start searching. So the fixed code is:
var authors = "Josh Smith1, Zach Johnson2";
var paragraph = doc.getBody().appendParagraph(authors);
var text = paragraph.editAsText();
var range = text.findText('[0-9]+');
while(range)
{
text.setTextAlignment(range.getStartOffset(),range.getEndOffsetInclusive(),DocumentApp.TextAlignment.SUPERSCRIPT);
range = text.findText('[0-9]+', range);
}
That should fix it for you.

Is there a way to make a text box or other widget be formatted to be currency? Or format to have two decimals?

I am looking to format my data before it goes into a google sheet. I would like to format the text entry into packageABox to be currency before/as it goes into google sheets. Is there a way to format currency or at least format to always have two decimal places when it goes from the text box to it's destination? Thanks!
function doGet(e) {
var app = UiApp.createApplication()
var panel = app.createVerticalPanel();
var grid = app.createGrid(70, 1).setId('grid');
var packageALbl = app.createLabel('Bid Pricing: Enter your pricing for Package A *').setId('packageALbl').setVisible(false);
var packageABox = app.createTextBox().setName('packageABox').setId('packageABox').setVisible(false);
grid.setWidget(35, 0, packageALbl);
grid.setWidget(36, 0, packageABox);
app.getElementById('packageABox').setValue('');
var button = app.createButton('Submit').setId('Submit').setVisible(false);
var submitHandler = app.createServerClickHandler('submit');
submitHandler.addCallbackElement(grid);
button.addClickHandler(submitHandler);
panel.add(grid)
app.add(panel)
return app;
}
function submit(e) {
//Write the data in the text boxes back to the Spreadsheet
var app = UiApp.getActiveApplication();
var doc = SpreadsheetApp.openById('0AhvH2AsQokNfdDFQUVUxM3hqLVhSdnVmUXdrRUlmYXc');
var packageA = e.parameter.packageABox
var range = s.getRange(lastRow+1,2,1,1).setValues([[packageA]]);
return app;
}
Format it yourself in submit(). No uiapp element will do it automatically.
You can use javascript functions to achieve that result, there is also a useful formatString() method in Google Apps Script.(see also other doc here)
an example among others :
function test(){
Logger.log(toCurrency(345678.1));
}
function toCurrency(val){
var outString= Utilities.formatString('%2.2f', Number(val));
return '$'+outString;
}
Note that if you are getting this value in a Handler function using e.parameter.xxx, the value will be a string, not a number....
To handle that, simply change like this :
toCurrency(Number('345678.1'));// convert argument to number

replacing strings in a document and undo

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);
}
}
}
}
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.