finding Text with specific format and delete it - google-apps-script

I have a big google doc file with over 100 pages(with tables etc) and there is some reference text in that document in multiple locations reference texts are highlighted with the color "grey", I want to have a function that can find those colors/style in the table or paragraph and delete it. So Step 1 is finding it, and then deleting(removing those texts from the document) it in one go.
How we did it in MS Word is, we created custom styles and assign those styles to those "Remarks Text"(in grey) and in VBA we look for text matching the style name, and if it returns true than we delete those texts. As much i know about doc, there is no option to create custom styles.
Here is the code I am trying:-
function removeText()
{
var doc = DocumentApp.getActiveDocument()
var body = doc.getBody()
body.getParagraphs().map(r=> {
if(r.getAttributes().BACKGROUND_COLOR === "#cccccc")
{
//Don't know what to do next, body.removeChild(r.getChild()) not working
}
})
}
Can you guide me on how I can achieve this effectively please.
Thanks

Try this
body.getParagraphs().forEach( r => {
if( r.getAttributes().BACKGROUND_COLOR === "#cccccc" ) {
r.removeFromParent();
}
}
Reference
Paragraph.removeFromParent()

Google Apps Script hasn't a method to find text based on their style attributes, instead we need to get each part and in order to be able to get their attributes. The following example, if the format is applied to the whole paragraph, it is deleted, if not, it uses the regular expression for finding any single character ..
function removeHighlightedText() {
// In case that we want to remove the hightlighting instead of deleting the content
const style = {};
style[DocumentApp.Attribute.BACKGROUND_COLOR] = null;
const backgroundColor = '#cccccc';
const doc = DocumentApp.getActiveDocument();
const searchPattern = '.';
let rangeElement = null;
const rangeElements = [];
doc.getParagraphs().forEach(paragraph => {
if (paragraph.getAttributes().BACKGROUND_COLOR === backgroundColor) {
paragraph.removeFromParent();
// Remove highlighting
// paragraph.setAttributes(style);
} else {
// Collect the rangeElements to be processed
while (rangeElement = paragraph.findText(searchPattern, rangeElement)) {
if (rangeElement != null && rangeElement.getStartOffset() != -1) {
const element = rangeElement.getElement();
if (element.getAttributes(rangeElement.getStartOffset()).BACKGROUND_COLOR === backgroundColor) {
rangeElements.push(rangeElement)
}
}
}
}
});
// Process the collected rangeElements in reverse order (makes things easier when deleting content)
rangeElements.reverse().forEach(r => {
if (r != null && r.getStartOffset() != -1) {
const element = r.getElement();
// Remove text
element.asText().deleteText(r.getStartOffset(), r.getEndOffsetInclusive())
// Remove highlighting
// element.setAttributes(textLocation.getStartOffset(), textLocation.getEndOffsetInclusive(), style);
}
});
}

Related

How to sort sections of a GDocs document alphabetically?

Starting point: I have a doc with sections separated by page breaks.
Goal: I want a Google Apps Script that scans through a doc and sorts the sections alphabetically (aka everything between a heading1 and a page break). The manual equivalent would be cut-pasting sections around until sorted.
To illustrate, here's an example document: initial state and desired end state.
So far, this is what I have:
A script that searches through a doc and identifies the headings with a given format (heading1). I could sort the headings alphabetically, but I don't know how to move whole sections around in the document.
function findHeading1s() {
let doc = DocumentApp.getActiveDocument();//.openById('<doc-id>');
var body = doc.getBody();
// Define the search parameters.
let searchType = DocumentApp.ElementType.PARAGRAPH;
let searchHeading = DocumentApp.ParagraphHeading.HEADING1;
let searchResult = null;
let headers = [];
// Search until the paragraph is found.
while (searchResult = body.findElement(searchType, searchResult)) {
let par = searchResult.getElement().asParagraph();
let parText = par.getText();
if (par.getHeading() == searchHeading && parText != "") {
headers.push(parText);
Logger.log(headers); //Headers in current order: [Zone, Template, Example]
}
}
Logger.log(headers.sort()) //[Example, Template, Zone]
}
Any ideas how to move everything between a heading and the following pagebreak? I don't mind if the sorted end result is in a new document.
Is this doable with existing GAS capabilities? Tried to debug the body/paragraph elements to get some idea on how to solve this, but I only get the functions of the object, not the actual content. Not super intuitive.
Here are the steps I think are needed:
Identify heading1 headings ✅
Sort headings alphabetically ✅
Find each heading in the doc
Cut-paste section in the appropriate position of the doc (or append to a new doc in the correct order)
Thanks!
Issue:
You want to sort a series of document sections according to the section's heading text.
Solution:
Assuming that each section starts with a heading, you can do the following:
Iterate through all the body elements and get info on which indexes does each section start and end (based on the location of the headings). Methods getNumChildren and getChild would be useful here. You should end up with an array of section objects, each containing the start and end indexes as well as the heading text.
Sort the section objects according to the heading text.
Iterate through the sections array, and append the children corresponding to each section indexes to the end of the document. Useful methods here are copy and the different append methods (e.g. appendParagraph). Here's a related question.
Please notice that only PARAGRAPH, TABLE and LIST_ITEM are appended on the sample below; if there are other element types present in your original content, you should add the corresponding condition to the code.
When appending the elements, it's important to use the original elements Attributes; otherwise, things like list bullets might not get appended (see this related question).
After the copied elements are appended, remove the original ones (here you can use removeFromParent).
Code sample:
function findHeading1s() {
let doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
const parType = DocumentApp.ElementType.PARAGRAPH;
const headingType = DocumentApp.ParagraphHeading.HEADING1;
const numChildren = body.getNumChildren();
let sections = [];
for (let i = 0; i < numChildren; i++) {
const child = body.getChild(i);
let section;
if (child.getType() === parType) {
const par = child.asParagraph();
const parText = par.getText();
if (par.getHeading() === headingType && parText != "") {
section = {
startIndex: i,
headingText: parText
}
if (sections.length) sections[sections.length - 1]["endIndex"] = i - 1;
sections.push(section);
}
}
}
const lastSection = sections.find(section => !section["endIndex"]);
if (lastSection) lastSection["endIndex"] = numChildren - 1;
sections.sort((a,b) => a.headingText.localeCompare(b.headingText));
sections.forEach(section => {
const { startIndex, endIndex } = section;
for (let i = startIndex; i <= endIndex; i++) {
const element = body.getChild(i);
const copy = element.copy();
const type = element.getType();
const attributes = element.getAttributes();
if (type == DocumentApp.ElementType.PARAGRAPH) {
body.appendParagraph(copy).setAttributes(attributes);
} else if (type == DocumentApp.ElementType.TABLE) {
body.appendTable(copy).setAttributes(attributes);
} else if (type == DocumentApp.ElementType.LIST_ITEM) {
body.appendListItem(copy).setAttributes(attributes);
}
}
});
for (let i = numChildren - 1; i >= 0; i--) {
body.getChild(i).removeFromParent();
}
}

xpath in apps script?

I made a formula to extract some Wikipedia data in Google Seets which works fine. Here is the formula:
=regexreplace(join("",flatten(IMPORTXML(D2,".//p[preceding-sibling::h2[1][contains(., 'Geography')]]"))),"\[[^\]]+\]","")&char(10)&char(10)&iferror(regexreplace(join("",flatten(IMPORTXML(D2,".//p[preceding-sibling::h2[1][contains(., 'Education')]]"))),"\[[^\]]+\]",""))
Where D2 is a URL like https://en.wikipedia.org/wiki/Abbeville,_Alabama
This extracts some Geography and Education data from the Wikipedia page. Trouble is that importxml only runs a few times before it dies due to quota.
So I thought maybe better to use Apps Script where there are much higher limits on fetching and parsing. I could not see a good way however of using Xpath in Apps Script. Older posts on the web discuss using a deprecated service called Xml but it seems to no longer work. There is a Service called XmlService which looks like it may do the job but you can't just plug in an Xpath. It looks like a lot of sweating to get to the result. Any solutions out there where you can just plug in Xpath?
Here is an alternative solution I actually do in a case like this.
I have used XmlService but only for parsing the content, not for using Xpath. This makes use of the element tags and so far pretty consistent on my tests. Although, it might need tweaks when certain tags are in the result and you might have to include them into the exclusion condition.
Tested the code below in both links:
https://en.wikipedia.org/wiki/Abbeville,_Alabama#Geography
https://en.wikipedia.org/wiki/Montgomery,_Alabama#Education
My test shows that the formula above used did not return the proper output from the 2nd link while the code does. (Maybe because it was too long)
Code:
function getGeoAndEdu(path) {
var data = UrlFetchApp.fetch(path).getContentText();
// wikipedia is divided into sections, if output is cut, increase the number
var regex = /.{1,100000}/g;
var results = [];
// flag to determine if matches should be added
var foundFlag = false;
do {
m = regex.exec(data);
if (foundFlag) {
// if another header is found during generation of data, stop appending the matches
if (matchTag(m[0], "<h2>"))
foundFlag = false;
// exclude tables, sub-headers and divs containing image description
else if(matchTag(m[0], "<div") || matchTag(m[0], "<h3") ||
matchTag(m[0], "<td") || matchTag(m[0], "<th"))
continue;
else
results.push(m[0]);
}
// start capturing if either IDs are found
if (m != null && (matchTag(m[0], "id=\"Geography\"") ||
matchTag(m[0], "id=\"Education\""))) {
foundFlag = true;
}
} while (m);
var output = results.map(function (str) {
// clean tags for XmlService
str = str.replace(/<[^>]*>/g, '').trim();
decode = XmlService.parse('<d>' + str + '</d>')
// convert html entity codes (e.g.  ) to text
return decode.getRootElement().getText();
// filter blank results due to cleaning and empty sections
// separate data and remove citations before returning output
}).filter(result => result.trim().length > 1).join("\n").replace(/\[\d+\]/g, '');
return output;
}
// check if tag is found in string
function matchTag(string, tag) {
var regex = RegExp(tag);
return string.match(regex) && string.match(regex)[0] == tag;
}
Output:
Difference:
Formula ending output
Script ending output
Education ending in wikipedia
Note:
You still have quota when using UrlFetchApp but should be better than IMPORTXML's limit depending on the type of your account.
Reference:
Apps Script Quotas
Sorry I got very busy this week so I didn't reply. I took a look at your answer which seems to work fine, but it was quite code heavy. I wanted something I would understand so I coded my own solution. not that mine is any simpler. It's just my own code so it's easier for me to follow:
function getTextBetweenTags(html, paramatersInFirstTag, paramatersInLastTag) { //finds text values between 2 tags and removes internal tags to leave plain text.
//eg getTextBetweenTags(html,[['class="mw-headline"'],['id="Geography"']],[['class="wikitable mw-collapsible mw-made-collapsible"']])
// **Note: you may want to replace &#number; with ascII number
var openingTagPos = null;
var closingTagPos = null;
var previousChar = '';
var readingTag = false;
var newTag = '';
var tagEnd = false;
var regexFirstTagParams = [];
var regexLastTagParams = [];
//prepare regexes to test for parameters in opening and closing tags. put regexes in arrays so each condition can be tested separately
for (var i in paramatersInFirstTag) {
regexFirstTagParams.push(new RegExp(escapeRegex(paramatersInFirstTag[i][0])))
}
for (var i in paramatersInLastTag) {
regexLastTagParams.push(new RegExp(escapeRegex(paramatersInLastTag[i][0])))
}
var startTagIndex = null;
var endTagIndex = null;
var matches = 0;
for (var i = 0; i < html.length - 1; i++) {
var nextChar = html.substr(i, 1);
if (nextChar == '<' && previousChar != '\\') {
readingTag = true;
}
if (nextChar == '>' && previousChar != '\\') { //if end of tag found, check tag matches start or end tag
readingTag = false;
newTag += nextChar;
//test for firstTag
if (startTagIndex == null) {
var alltestsPass = true;
for (var j in regexFirstTagParams) {
if (!regexFirstTagParams[j].test(newTag)) alltestsPass = false;
}
if (alltestsPass) {
startTagIndex = i + 1;
//console.log('Start Tag',startTagIndex)
matches++;
}
}
//test for lastTag
else if (startTagIndex != null) {
var alltestsPass = true;
for (var j in regexLastTagParams) {
if (!regexLastTagParams[j].test(newTag)) alltestsPass = false;
}
if (alltestsPass) {
endTagIndex = i + 1;
matches++;
}
}
if(startTagIndex && endTagIndex) break;
newTag = '';
}
if (readingTag) newTag += nextChar;
previousChar = nextChar;
}
if (matches < 2) return 'No matches';
else return html.substring(startTagIndex, endTagIndex).replace(/<[^>]+>/g, '');
}
function escapeRegex(string) {
if (string == null) return string;
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
My function requires an array of attributes for the start tag and an array of attributes for the end tag. It gets any text in between and removes any tags found inbetween. One issue I also noticed was there were often special characters (eg  ) so they need to be replaced. I did that outside the scope of the function above.
The function could be easily improved to check the tag type (eg h2), but it wasn't necessary for the wikipedia case.
Here is a function where I called the above function. the html variable is just the result of UrlFetchApp.fetch('some wikipedia city url').getContextText();
function getWikiTexts(html) {
var geography = getTextBetweenTags(html, [['class="mw-headline"'], ['id="Geography']], [['class="mw-headline"']]);
var economy = getTextBetweenTags(html, 'span', [['class="mw-headline"'], ['id="Economy']], 'span', [['class="mw-headline"']])
var education = getTextBetweenTags(html, 'span', [['class="mw-headline"'], ['id="Education']], 'span', [['class="mw-headline"']])
var returnString = '';
if (geography != 'No matches' && !/Wikipedia/.test(geography)) returnString += geography + '\n';
if (economy != 'No matches' && !/Wikipedia/.test(economy)) returnString += economy + '\n';
if (education != 'No matches' && !/Wikipedia/.test(education)) returnString += education + '\n';
return returnString
}
Thanks for posting your answer.

How to select all underlined text in a paragraph

I'm trying to create a google apps script that will format certain parts of a paragraph. For example, text that is underlined will become bolded/italicized as well.
One docs add-on I have tried has a similar feature: https://imgur.com/a/5Cw6Irn (this is exactly what I'm trying to achieve)
How can I write a function that will select a certain type of text and format it?
**I managed to write a script that iterates through every single letter in a paragraph and checks if it's underlined, but it becomes extremely slow as the paragraph gets longer, so I'm looking for a faster solution.
function textUnderline() {
var selectedText = DocumentApp.getActiveDocument().getSelection();
if(selectedText) {
var elements = selectedText.getRangeElements();
for (var index = 0; index < elements.length; index++) {
var element = elements[index];
if(element.getElement().editAsText) {
var text = element.getElement().editAsText();
var textLength = text.getText().length;
//For every single character, check if it's underlined and then format it
for (var i = 0; i < textLength; i++) {
if(text.isUnderline(i)) {
text.setBold(i, i, true);
text.setBackgroundColor(i,i,'#ffff00');
} else {
text.setFontSize(i, i, 8);
}
}
}
}
}
}
Use getTextAttributeIndices:
There is no need to check each character in the selection. You can use getTextAttributeIndices() to get the indices in which the text formatting changes. This method:
Retrieves the set of text indices that correspond to the start of distinct text formatting runs.
You just need to iterate through these indices (that is, check the indices in which text formatting changes), which are a small fraction of all character indices. This will greatly increase efficiency.
Code sample:
function textUnderline() {
var selectedText = DocumentApp.getActiveDocument().getSelection();
if(selectedText) {
var elements = selectedText.getRangeElements();
for (var index = 0; index < elements.length; index++) {
var element = elements[index];
if(element.getElement().editAsText) {
var text = element.getElement().editAsText();
var textRunIndices = text.getTextAttributeIndices();
var textLength = text.getText().length;
for (let i = 0; i < textRunIndices.length; i++) {
const startOffset = textRunIndices[i];
const endOffset = i + 1 < textRunIndices.length ? textRunIndices[i + 1] - 1 : textLength - 1;
if (text.isUnderline(textRunIndices[i])) {
text.setBold(startOffset, endOffset, true);
text.setBackgroundColor(startOffset, endOffset,'#ffff00');
} else {
text.setFontSize(startOffset, endOffset, 8);
}
}
}
}
}
}
Reference:
getTextAttributeIndices()
Based on the example shown in the animated gif, it seems your procedure needs to
handle a selection
set properties if the selected region is of some format (e.g. underlined)
set properties if the selected region is NOT of some format (e.g. not underlined)
finish as fast as possible
and your example code achieves all these goals expect the last one.
The problem is that you are calling the text.set...() functions at each index position. Each call is synchronous and blocks the code until the document is updated, thus your run time grows linearly with each character in the selection.
My suggestion is to build up a collection of subranges from the selection range and then for each subrange use text.set...(subrange.start, subrange.end) to apply the formatting. Now the run time will be dependent on chunks of characters, rather than single characters. i.e., you will only update when the formatting switches back and forth from, in your example, underlined to not underlined.
Here is some example code that implements this subrange idea. I separated the specific predicate function (text.isUnderline) and specific formatting effects into their own functions so as to separate the general idea from the specific implementation.
// run this function with selection
function transformUnderlinedToBoldAndYellow() {
transformSelection("isUnderline", boldYellowOrSmall);
}
function transformSelection(stylePredicateKey, stylingFunction) {
const selectedText = DocumentApp.getActiveDocument().getSelection();
if (!selectedText) return;
const getStyledSubRanges = makeStyledSubRangeReducer(stylePredicateKey);
selectedText.getRangeElements()
.reduce(getStyledSubRanges, [])
.forEach(stylingFunction);
}
function makeStyledSubRangeReducer(stylePredicateKey) {
return function(ranges, rangeElement) {
const {text, start, end} = unwrapRangeElement(rangeElement);
if (start >= end) return ranges; // filter out empty selections
const range = {
text, start, end,
styled: [], notStyled: [] // we will extend our range with subranges
};
const getKey = (isStyled) => isStyled ? "styled" : "notStyled";
let currentKey = getKey(text[stylePredicateKey](start));
range[currentKey].unshift({start: start});
for (let index = start + 1; index <= end; ++index) {
const isStyled = text[stylePredicateKey](index);
if (getKey(isStyled) !== currentKey) { // we are switching styles
range[currentKey][0].end = index - 1; // note end of this style
currentKey = getKey(isStyled);
range[currentKey].unshift({start: index}); // start new style range
}
}
ranges.push(range);
return ranges;
}
}
// a helper function to unwrap a range selection, deals with isPartial,
// maps RangeElement => {text, start, end}
function unwrapRangeElement(rangeElement) {
const isPartial = rangeElement.isPartial();
const text = rangeElement.getElement().asText();
return {
text: text,
start: isPartial
? rangeElement.getStartOffset()
: 0,
end: isPartial
? rangeElement.getEndOffsetInclusive()
: text.getText().length - 1
};
}
// apply specific formatting to satisfy the example
function boldYellowOrSmall(range) {
const {text, start, end, styled, notStyled} = range;
styled.forEach(function setTextBoldAndYellow(range) {
text.setBold(range.start, range.end || end, true);
text.setBackgroundColor(range.start, range.end || end, '#ffff00');
});
notStyled.forEach(function setTextSmall(range) {
text.setFontSize(range.start, range.end || end, 8);
});
}

Script that would find and mark the same words in the paragraph

I'm a fiction writer and I used to do my writing in MS Word. I've written some macros to help me edit the fiction text and one of them check the paragraph and marks (red) the duplicate (or triplicate words, etc). Example:
"I came **home**. And while at **home** I did this and that."
Word "home" is used twice and worth checking if I really can't change the sentence.
Now I mostly use google documents for writing, but I still have to do my editing in MS Word, mostly just because of this macro - I am not able to program it in the google script.
function PobarvajBesede() {
var doc = DocumentApp.getActiveDocument();
var cursor = DocumentApp.getActiveDocument().getCursor();
var surroundingText = cursor.getSurroundingText().getText();
var WordsString = WORDS(surroundingText);
Logger.log(WordsString);
//so far, so good. But this doesn't work:
var SortedWordsString = SORT(WordsString[1],1,False);
// and I'm lost.
}
function WORDS(input) {
var input = input.toString();
var inputSplit = input.split(" ");
// Logger.log(inputSplit);
inputSplit = inputSplit.toString();
var punctuationless = inputSplit.replace(/[.,\/#!$%\?^&\*;:{}=\-_`~()]/g," ");
var finalString = punctuationless.replace(/\s{2,}/g," ");
finalString = finalString.toLowerCase();
return finalString.split(" ") ;
}
If I could only get a list of words (in uppercase, longer than 3 characters), sorted by the number of their appearances in the logger, it would help me a lot:
HOME (2)
AND (1)
...
Thank you.
Flow:
Transform the string to upper case and sanitize the string of all non ascii characters
After splitting the string to word array, reduce the array to a object of word:count
Map the reduced object to a 2D array [[word,count of this word],[..],...] and sort the array by the inner array's count.
Snippet:
function wordCount(str) {
str = str || 'I came **home**. And while at **home** I did this and that.';
var countObj = str
.toUpperCase() //'I CAME **HOME**...'
.replace(/[^A-Z ]/g, '') //'I CAME HOME...'
.split(' ') //['I', 'CAME',..]
.reduce(function(obj, word) {
if (word.length >= 3) {
obj[word] = obj[word] ? ++obj[word] : 1;
}
return obj;
}, {}); //{HOME:2,DID:1}
return Object.keys(countObj)
.map(function(word) {
return [word, countObj[word]];
}) //[['HOME',2],['CAME',1],...]
.sort(function(a, b) {
return b[1] - a[1];
});
}
console.info(wordCount());
To read and practice:
Object
Array methods
This is a combination of TheMaster answer and some of my work. I need to learn more about the way he did it so I spent some learning time today. This function eliminates some problems I was having the carriage returns and it also removes items that only appear once. You should probably pick TheMasters solution as I couldn't have done it without his work.
function getDuplicateWords() {
var str=DocumentApp.getActiveDocument().getBody().getText();
var countObj = str
.toUpperCase()
.replace(/\n/g,' ')
.replace(/[^A-Z ]/g, '')
.split(' ')
.reduce(function(obj, word) {
if (word.length >= 2) {
obj[word] = obj[word] ? ++obj[word] : 1;
}
return obj;
}, {});
var oA=Object.keys(countObj).map(function(word){return [word, countObj[word]];}).filter(function(elem){return elem[1]>1;}).sort(function(a,b){return b[1]-a[1]});
var userInterface=HtmlService.createHtmlOutput(oA.join("<br />"));
DocumentApp.getUi().showSidebar(userInterface);
}
function onOpen() {
DocumentApp.getUi().createMenu('MyMenu')
.addItem('Get Duplicates','getDuplicateWords' )
.addToUi();
}
And yes I was having problems with get the results to change in my last solution.

How to search and replace a horizontal rule and linebreaks

I need to automatically delete all horizontal rules which are surrounded by 6 linebreaks (3 before and 3 after) on a google doc.
This piece of code seems to put in the logs the correct linebreaks I want to delete (that's a first step) :
function myFunction() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody().getText();
var pattern = /\s\s\s\s/g;
while (m=pattern.exec(body)) { Logger.log(m[0]); }
}
I have two questions :
What tool can I use to delete these linebreaks (I don't yet understand the subtilies of using replace or replaceText, all my attemps with these have failed) ?
How can I add to my var pattern (the pattern to be deleted) a horizontal rule ? I tried /\s\s\s\sHorizontalRule\s\s\s\s/g but of course it did not work.
Horizontal rule is an element inside a paragraph (or sometimes inside a list item). Since it is not text, it can not be found or replaced by means of a regular expression. We should search for objects which are specially arranged in the document body, and delete them if found.
Consider the following code example:
function deleteHR() {
var body = DocumentApp.getActiveDocument().getBody();
var hr = null, hrArray = [], countDeleted = 0;
// Collect all horizontal rules in the Document
while (true) {
hr = body.findElement(DocumentApp.ElementType.HORIZONTAL_RULE, hr);
if (hr == null) break;
hrArray.push(hr);
}
hrArray.forEach(function(hr) {
var p = hr.getElement().getParent();
// Get empty paragraphs as siblings (if any)
var prevSiblings = getSiblings(p, 3, true),
nextSiblings = getSiblings(p, 3, false);
// Define a short function for batch deleting items
function remove(e) {
e.removeFromParent();
}
// If empty paragraphs exist (3 before and 3 after)
if (prevSiblings.length == 3 && nextSiblings.length == 3) {
// then delete them as well as the rule itself
hr.getElement().removeFromParent();
prevSiblings.forEach(remove);
nextSiblings.forEach(remove);
countDeleted++;
}
});
// Optional report
Logger.log(countDeleted + ' rules deleted');
}
// Recursive search for empty paragraphs as siblings
function getSiblings(p, n, isPrevious) {
if (n == 0) return [];
if (isPrevious) {
p = p.getPreviousSibling();
} else {
p = p.getNextSibling();
}
if (p == null) return [];
if (p.getType() != DocumentApp.ElementType.PARAGRAPH) return [];
if (p.asParagraph().getText().length > 0) return [];
var siblings = getSiblings(p, n - 1, isPrevious);
siblings.push(p);
return siblings;
}
Main function deleteHR() does all the work. However it appears helpful to use another separate function getSiblings() for recursive search for empty paragraphs. May be, this way is not the only, but it works.