I've tried searching the issue and came up with nothing, so I'm looking for help.
What I'm trying to do is add incrementing IDs to individual citations by:
Searching a Google Doc for a specific combination of characters that signal the end of the citation (checking to see if there are any in the document at all).
If the characters are there, I'd like to find the first one, then place a '1' in between the two characters. Then the second one should have a '2' between it, the third should have a '3', and so on and so forth ensuring that all of the sets of characters have been replaced with unique number IDs
What is going wrong currently:
Due to the elemental structure of the Google Doc, my script is replacing ALL instances within a paragraph with the same ID number, which can be 1 instance or it can be 20. When it moves to the next paragraph, every found instance of the character combination is getting the incremented ID.
I need a genius' help.
Here's the code base I've found that I've been struggling to modify:
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var foundElement = body.findText("]]");
var i = 1;
while (foundElement != null) {
// Get the text object from the element
var foundText = foundElement.getElement().asText();
// Where in the Element is the found text?
var start = foundElement.getStartOffset();
var end = foundElement.getEndOffsetInclusive();
// Change the text
foundText.replaceText("]]","]"+i+"]");
// Find the next match
foundElement = body.findText("]]", foundElement);
i++
}
Issue
replaceText() will replace all the occurence in the element.
Solution
To avoid this, you should use the start and end to delete and insert text.
Please see my code below. This worked on my end.
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var foundElement = body.findText("]]");
var i = 1;
while (foundElement != null) {
// Get the text object from the element
var foundText = foundElement.getElement().asText();
// Where in the Element is the found text?
var start = foundElement.getStartOffset();
var end = foundElement.getEndOffsetInclusive();
// Change the text
foundText.deleteText(start, end)
foundText.insertText(start, "]"+i+"]")
// Find the next match
foundElement = body.findText("]]", foundElement);
i++
}
As you can see, I just modified the line where you replaceText() and transform it to a combination of deleteText() and insertText() methods.
Try this:
function myfunction() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var foundElement = body.findText("]]");
var i = 1;
while (foundElement != null) {
var foundText = foundElement.getElement().asText();
var start = foundElement.getStartOffset();
var end = foundElement.getEndOffsetInclusive();
foundText.replaceText("]]", "]" + i++ + "]");
foundElement = body.findText("]]", end);//Modifed this and the end point of the last search. And the i++ will use the i as it currently is and then increment it after the current operation.
}
}
Each next search needs to start searching after the location of the preceeding element. range element getEndOffsetInclusive()
Related
I want to set the font size in a google doc header, but I get the error 'setAttributes is not a function'. I can't really understand the structure of a header (it seems), as to where to set the attributes.
var style = {};
style[DocumentApp.Attribute.FONT_SIZE] = 16;
style[DocumentApp.Attribute.BOLD] = true;
var range = this.Doc.getHeader(
range.setText(text)
var m = range.getNumChildren();
for (var i =0; i<m;i++){
var cld = range.getChild(i);
var ct = cld.getText();
var cat = cld.getAttributes();
cld.setAttibutes(style);
}
In the code above I can set the text in the header, and I can see the text in the 1st child element "ct", but I can't set the attributes. cld.getAttributes() returns nulls, so I'm thinking the attributes are set on a higher element. I just don't know which.
Issue:
getChild returns element, not Text, thus you need to change cld to Text via asText and then use getText().
As for the setAttributes function, I didn't encounter the issue you had, it just worked so I have no clue on why it errors on yours.
Can you try this one?
Code:
function updateHeader() {
var doc = DocumentApp.getActiveDocument();
var range = doc.getHeader();
var text = "this is the new header";
var style = {};
style[DocumentApp.Attribute.FONT_SIZE] = 16;
style[DocumentApp.Attribute.BOLD] = true;
var m = range.getNumChildren();
for (var i = 0; i < m; i++){
var cld = range.getChild(i);
var ct = cld.asText().getText(); // get text value
var cat = cld.getAttributes(); // get attributes
Logger.log(ct); // print old text
Logger.log(cat); // print old attributes
cld.asText().setText(text); // set text as header value
cld.setAttributes(style); // set attributes
Logger.log(cld.asText().getText()); // print new text
Logger.log(cld.getAttributes()); // print new attributes
}
}
Sample:
Output:
Logs:
Note:
I placed setText inside the loop to show you the difference of before and after the update of the value of the text and attribute.
I need to replace the word without affecting hyperlink (hyperlink must be preserved for the replaced word) and those with non hyperlinks the replace must happen in a regular way.
Here is the link of the coded Docs
I have tried with
function run() {
var findtext = "Search";
var replacetext = "Replacer";
var body = DocumentApp.getActiveDocument().getBody();
var foundElement = body.findText(findtext);
while (foundElement != null) {
var foundText = foundElement.getElement().asText();
var startOffset = foundElement.getStartOffset();
var endOffsetInclusive = foundElement.getEndOffsetInclusive();
var hyperlink = foundText.getLinkUrl(0);
foundText.insertText(0, findtext);
foundText.setLinkUrl(startOffset + findtext.length, endOffsetInclusive + findtext.length, hyperlink);
foundText.deleteText(startOffset + findtext.length, endOffsetInclusive + findtext.length)
foundElement = body.findText(findtext, foundElement);
}
}
The main issue is treating the result from findText as a word.
It is tricky because you can't get a "word" element. You have to:
Take the whole paragraph element that findText returns. This contains the search result.
Get the index values of the start and end of the found word.
Get the hyperlink at that index
Delete the text between those indices
Insert the new text and then assign the hyperlink with the new indices.
For example:
foundText.insertText(0, findtext)
Inserts the text you are looking for, i.e. "Search", at the start of the element which the result is in.
This:
var hyperlink = foundText.getLinkUrl(0)
This will only get the hyperlink found at the start of the paragraph, for example, which means that if the first word of the paragraph has a hyperlink, this is what it will return. In getLinkUrl() you should use the start index of the search result.
Solution
This code will replace text and will keep the hyperlink, if it has one.
function replaceTextKeepHyperlink(textToReplace, ReplacementText) {
var body = DocumentApp.getActiveDocument().getBody();
var searchResult = body.findText(textToReplace);
while (searchResult != null) {
// Getting info about result
var foundText = searchResult.getElement().asText();
var start = searchResult.getStartOffset();
var end = searchResult.getEndOffsetInclusive();
var hyperlink = searchResult.getElement().getLinkUrl(start);
// Modifying text
foundText.deleteText(start, end)
foundText.insertText(start, ReplacementText)
foundText.setLinkUrl(start, start + ReplacementText.length - 1, hyperlink)
// Moving to next search result
searchResult = body.findText(textToReplace, searchResult);
}
}
It will not keep any other formatting though, so for that you would have add in some lines to the "Getting info" and "Modifying" parts of the code.
Reference
text methods
Update
mshcruz found that if you called the function with parameters like this:
replaceTextKeepHyperlink("Search", "PrefixedSearch")
The function gets caught in an infinite loop, because it finds the text its looking for in the text its just replaced, replaces that part, and on and on.
He provided the fix which is incorporated below with a try block to avoid the error that it produces if a textToReplace is found at the end of the document:
function replaceTextKeepHyperlink(textToReplace, ReplacementText) {
var body = DocumentApp.getActiveDocument().getBody();
var searchResult = body.findText(textToReplace);
while (searchResult != null) {
var foundText = searchResult.getElement().asText();
var start = searchResult.getStartOffset();
var end = searchResult.getEndOffsetInclusive();
var hyperlink = searchResult.getElement().getLinkUrl(start);
foundText.deleteText(start, end)
foundText.insertText(start, ReplacementText)
foundText.setLinkUrl(start, start + ReplacementText.length - 1, hyperlink)
try {
let rangeBuilder = DocumentApp.getActiveDocument().newRange();
rangeBuilder.addElement(searchResult.getElement(), start, end+ReplacementText.length - 1);
searchResult = rangeBuilder.getRangeElements()[0];
} catch (e){
Logger.log("End of Document")
return null
}
searchResult = body.findText(textToReplace, searchResult);
}
}
Trying to work out how to remove multiple line breaks from Google Documents (not spreadsheets).
I've tried this and many variations thereof:
function searchAndReplace() {
var bodyElement = DocumentApp.getActiveDocument().getBody();
bodyElement.replaceText("\r\r", '\r');
}
Any idea please?
Noob to all of this...
Purpose is to replicate the search and replace in MS Word for ^p
Here is a rather "radical" method if your document has only paragraphs with text (images or other elements will be lost). See doc about element types here
(comments in code)
function removeNewLines(){
var doc = DocumentApp.getActiveDocument();
var text = doc.getBody().getText();// get a string
var textMod=text.replace(/\n/g,'');// replace all \n with ''
Logger.log(textMod);//optional check in logger
doc.getBody().clear().appendParagraph(textMod);// empty the doc and apend new texr
doc.saveAndClose();// save the result
}
I wanted to do the same thing (replace two new lines with a single new line). Ended up with the following as replaceText() doesn't accept \n for some reason.
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
var text = body.editAsText();
var text_content = text.getText();
for(var i = 0, offset_i = 0; i < (text_content.length); i++){
if((text_content.charCodeAt(i)==10) && (text_content.charCodeAt(i-1)==10)){
text.deleteText(i-1-offset_i, i-1-offset_i)
offset_i++;
}
}
}
This code helped me to remove doubled new lines in document:
function removeDoubleNewLines(){
var doc = DocumentApp.getActiveDocument();
var paragraphs = doc.getBody().getParagraphs();
var paragraph;
for (var i = 0; i < paragraphs.length-1; i++) {
paragraph = paragraphs[i];
if(paragraph.getText() === '' &&
paragraph.getNumChildren() === 0) {
paragraph.removeFromParent();
}
}
}
Is it possible to append a table after a selection in Google Document using Apps Script?
Only example I can find is this:
var body = DocumentApp.getActiveDocument().getBody();
var table = body.appendTable();
...which means to the end of document.
Thanks
EDIT:
How I make selection is:
var selection = doc.getSelection();
which is basicaly what is selected by mouse drag selection (blued out) on document editor.
From there I start to iterrate:
var elements = selection.getSelectedElements();
var element = elements[0].getElement();
var startOffset = elements[0].getStartOffset(); // -1 if whole element
var endOffset = elements[0].getEndOffsetInclusive(); // -1 if whole element
This might be a part of PARAGRAPH.
Here is code that inserts a table at the current selection. It may need to be modified a little, but the point is; that it finds the child index of the body at the point of the selection.
function insertTableAtSelection() {
// insert table as selection
var theDoc = DocumentApp.getActiveDocument();
var selection = theDoc.getSelection();
Logger.log('selection: ' + selection);
if (selection) {
var elements = selection.getRangeElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
Logger.log('element: ' + element.getElement());
};
var theElmt = element;
var selectedElmt = theElmt.getElement();
Logger.log('selectedElmt: ' + selectedElmt);
var parent = selectedElmt.getParent();
var insertPoint = parent.getChildIndex(selectedElmt);
Logger.log('insertPoint: ' + insertPoint);
var body = theDoc.getBody();
var table = body.insertTable(insertPoint + 1, [['one','two','three'],['yellow', 'green', 'red']]);
};
};
The insertPoint is increased by one:
insertPoint + 1
That gets the table just beyond the selection.
For anyone reading this post who may want to adapt this code, keep in mind that this code only completes if there is a selection; the user needs to have highlighted some amount of content for there to be a selection.
You can append a table anywhere in the document, you only need to get the container element of your search result and append the table to it.
Have a look at this post for example to see how documents are build (but there are more... search on this forum with Google-Apps-Script tag.
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.