I'm working on an automated slide setup and depending on some opt-out variables I need to remove some of the slides if they are not desired in the final output. To solve this I have created a script that adds a simple text string {{remove-this-slide}} to the slides that need to be deleted.
However, when trying to get a script to delete the slides containing that string it keeps deleting my entire presentation...
This is what I have:
function deleteFunction() {
var currentPresentationSlide = SlidesApp.getActivePresentation().getSlides();
for (i = 0; i < currentPresentationSlide.length; i++) {
if (currentPresentationSlide[i].getPageElements().indexOf('{{remove-this-slide}}') > -1); {
currentPresentationSlide[i].remove();
}
}
}
Can anyone figure out what's going wrong here?
How about this modification?
Modification points :
The reason that the entire slides are deleted is ; after if (currentPresentationSlide[i].getPageElements().indexOf('{{remove-this-slide}}') > -1);. By this ;, if doesn't work and currentPresentationSlide[i].remove(); is always run.
The text data cannot be retrieved from currentPresentationSlide[i].getPageElements(). When you want to search the text from the text box, please use currentPresentationSlide[i].getShapes().
From your question, I was not sure where you want to search the text from. So I supposed that you want to search the text from shapes. The shape includes the text box.
Modified script :
function deleteFunction() {
var currentPresentationSlide = SlidesApp.getActivePresentation().getSlides();
for (i = 0; i < currentPresentationSlide.length; i++) {
var shapes = currentPresentationSlide[i].getShapes();
for (j = 0; j < shapes.length; j++) {
if (shapes[j].getText().asString().indexOf('{{remove-this-slide}}') > -1) {
currentPresentationSlide[i].remove();
}
}
}
}
Reference :
getShapes()
If I misunderstand your question, I'm sorry.
There is a small bug in the code from #Tanaike. Because there might be more shapes in the same slide, you have to break the loop after deleting the slide.
Otherwise the code tries to traverse the shapes of a deleted slide, producing an error.
So the correct snippet looks like:
function deleteFunction() {
var currentPresentationSlide = SlidesApp.getActivePresentation().getSlides();
for (i = 0; i < currentPresentationSlide.length; i++) {
var shapes = currentPresentationSlide[i].getShapes();
for (j = 0; j < shapes.length; j++) {
if (shapes[j].getText().asString().indexOf('{{remove-this-slide}}') > -1) {
currentPresentationSlide[i].remove();
break;
}
}
}
}
Related
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);
});
}
I'm trying to replace text inside a textbox of a slide while retaining the text formatting of the previous text. Is there a way for this to work using google apps scripts methods?
I tried converting the Google Slide to a PDF then to DOC to get the HTML value so that I can somehow retain the text formatting. While the conversions work, I'm stuck at the part where I have to replace the text within the TextBox while retaining the original text formatting.
So far this is what I have:
function replacePresentationContent(presentationCopyId, slideId, shapeId, content) {
var presentationCopy = SlidesApp.openById(presentationCopyId);
var slidesCopy = presentationCopy.getSlides();
for (var i = 0; i < this.getSlidesCount(presentationCopy); i++) {
var slideCopy = slidesCopy[i];
var slidesCopyId = slideCopy.getObjectId();
var shapesCopy = slideCopy.getShapes();
if (slidesCopyId === slideId) {
for (var j = 0; j < shapesCopy.length; j++) {
if (shapesCopy[j].getObjectId() === shapeId) {
var textRange = shapesCopy[j].getText();
textRange.setText(content);
}
}
}
}
}
You can do this by manipulating the text of the shape in the following way:
var shape = slide.getShapes()[0]; //Change this to get the Shape you want
shape.getText().setText("The new text you want here, but with the same formatting!");
Hope this helps!
so I found that you can use the function getRuns() to isolate the formatted text. Hope this helps.
I am just starting with Google Apps Script and following the Add-on quickstart
https://developers.google.com/apps-script/quickstart/docs
In the quickstart you can create a simple add-on to get a selection from a document and translate it with the LanguageApp service. The example gets the underlying text using this:
function getSelectedText() {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var text = [];
var elements = selection.getSelectedElements();
for (var i = 0; i < elements.length; i++) {
if (elements[i].isPartial()) {
var element = elements[i].getElement().asText();
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
text.push(element.getText().substring(startIndex, endIndex + 1));
} else {
var element = elements[i].getElement();
// Only translate elements that can be edited as text; skip images and
// other non-text elements.
if (element.editAsText) {
var elementText = element.asText().getText();
// This check is necessary to exclude images, which return a blank
// text element.
if (elementText != '') {
text.push(elementText);
}
}
}
}
if (text.length == 0) {
throw 'Please select some text.';
}
return text;
} else {
throw 'Please select some text.';
}
}
It gets the text only: element.getText(), without any formatting.
I know the underlying object is not html, but is there a way to get the selection converted into a HTML string? For example, if the selection has a mix of formatting, like bold:
this is a sample with bold text
Then is there any method, extension, library, etc, -- like element.getHTML() -- that could return this?
this is a sample with <b>bold</b> text
instead of this?
this is a sample with bold text
There is a script GoogleDoc2HTML by Omar AL Zabir. Its purpose is to convert the entire document into HTML. Since you only want to convert rich text within the selected element, the function relevant to your task is processText from the script, shown below.
The method getTextAttributeIndices gives the starting offsets for each change of text attribute, like from normal to bold or back. If there is only one change, that's the attribute for the entire element (typically paragraph), and this is dealt with in the first part of if-statement.
The second part deals with the general case, looping over the indices and inserting HTML markup corresponding to the attributes.
The script isn't maintained, so consider it as a starting point for your own code, rather than a ready-to-use library. There are some unmerged PRs that improve the conversion process, in particular for inline links.
function processText(item, output) {
var text = item.getText();
var indices = item.getTextAttributeIndices();
if (indices.length <= 1) {
// Assuming that a whole para fully italic is a quote
if(item.isBold()) {
output.push('<b>' + text + '</b>');
}
else if(item.isItalic()) {
output.push('<blockquote>' + text + '</blockquote>');
}
else if (text.trim().indexOf('http://') == 0) {
output.push('' + text + '');
}
else {
output.push(text);
}
}
else {
for (var i=0; i < indices.length; i ++) {
var partAtts = item.getAttributes(indices[i]);
var startPos = indices[i];
var endPos = i+1 < indices.length ? indices[i+1]: text.length;
var partText = text.substring(startPos, endPos);
Logger.log(partText);
if (partAtts.ITALIC) {
output.push('<i>');
}
if (partAtts.BOLD) {
output.push('<b>');
}
if (partAtts.UNDERLINE) {
output.push('<u>');
}
// If someone has written [xxx] and made this whole text some special font, like superscript
// then treat it as a reference and make it superscript.
// Unfortunately in Google Docs, there's no way to detect superscript
if (partText.indexOf('[')==0 && partText[partText.length-1] == ']') {
output.push('<sup>' + partText + '</sup>');
}
else if (partText.trim().indexOf('http://') == 0) {
output.push('' + partText + '');
}
else {
output.push(partText);
}
if (partAtts.ITALIC) {
output.push('</i>');
}
if (partAtts.BOLD) {
output.push('</b>');
}
if (partAtts.UNDERLINE) {
output.push('</u>');
}
}
}
}
Ended up making a script to support my use-case of bold+links+italics:
function getHtmlOfElement(element) {
var text = element.editAsText();
var string = text.getText();
var indices = text.getTextAttributeIndices();
var output = [];
for (var i = 0; i < indices.length; i++) {
var offset = indices[i];
var startPos = offset;
var endPos = i+1 < indices.length ? indices[i+1]: string.length;
var partText = string.substring(startPos, endPos);
var isBold = text.isBold(offset);
var isItalic = text.isItalic(offset);
var linkUrl = text.getLinkUrl(offset);
if (isBold) {
output.push('<b>');
}
if (isItalic) {
output.push('<i>');
}
if (linkUrl) {
output.push('<a href="' + linkUrl + '">');
}
output.push(partText);
if (isBold) {
output.push('</b>');
}
if (isItalic) {
output.push('</i>');
}
if (linkUrl) {
output.push('</a>');
}
}
return output.join("");
}
You can simply call it using something like:
getHtmlOfElement(myTableCell); // returns something like "<b>Bold</b> test."
This is obviously a workaround, but you can copy/paste a Google Doc into a draft in Gmail and then that draft can be turned into HTML using
GmailApp.getDraft(draftId).getMessage().getBody().toString();
I found this thread trying to skip that step by going straight from a Doc to HTML, but I thought I'd share.
Sorry, i'm not a programmer,
I'm using this gmail app script (CLEANINBOX) and it works great in roder to keep my inbox clean.
As I need to use INBOX app (mobile) and GMAIL app (desktop) at the same time I need to implement this script in order to avoid that PINNED messages in INBOX get archieved.
I inserted a condition in the IF sequence and it does not work
After struggling a bit I realized (correct me if I'm wrong) that the following code is not working becouse !thread.getLabels()=='PINNED') IS NOT BOOLEAN
Can anybody point me to the correct code?
function cleanInbox() {
var threads = GmailApp.getInboxThreads();
for (var i = 0; i < threads.length; i++) {
var thread=threads[i];
if (!thread.hasStarredMessages() && !thread.isUnread() && !thread.getLabels()=='PINNED') {
GmailApp.moveThreadToArchive(threads[i]);
}
}
}
Ok it was much easyer then expected...
I just needed to narrow down the number of threads to work with, and i did it just excluding those with "pinned" label
var threads = GmailApp.search('in:inbox -label:pinned')
solved
thanks for input
The statement !thread.getLabels()=='PINNED' is boolean (as the operand == outputs true or false). I think your problem is that thread.getLabels() does not return a string and therefore it will never be equal to 'PINNED'
EDIT:
The getLabels() method return value is GmailLabel[]. You can iterate over the labels and check your condition by adding this code:
for (var i = 0; i < threads.length; i++) {
var thread=threads[i];
if (!thread.hasStarredMessages() && !thread.isUnread()) {
var labels = thread.getLabels();
var isPinned = false;
for (var j = 0; i < labels.length; i++) {
if (labels[j].getName() == "PINNED"){
isPinned = true;
break;
}
if (!isPinned){
GmailApp.moveThreadToArchive(threads[i]);
}
}
I'm creating a document dynamically with some heading structure
doc = DocumentApp.create("My Document");
doc.appendParagraph("Main").setHeading(DocumentApp.ParagraphHeading.HEADING1);
var section = doc.appendParagraph("Section 1");
section.setHeading(DocumentApp.ParagraphHeading.HEADING2);
I can open it online, insert Table of contents and can access directly to "Section 1" by url like:
https://docs.google.com/document/d/1aA...FQ/edit#heading=h.41bpnx2ug57j
The question is: How I can get similar url/id to the "Section 1" in the code at run time and use it later as a link?
If I can't - is there any way to set something like anchor/bookmark and get it's url?
Thanks!
Starting to test Google Apps in depth, I had issues with the limited features related to the management of table of contents. I bumped into the code you proposed and used it as a starting point to write my own function to format a table of content:
- applying proper headings styles,
- numeroting the different parts.
I hope this would help some of you improving Google Docs templates:
/**
* Used to properly format the Table of Content object
*/
function formatToc() {
//Define variables
var level1 = 0;
var level2 = 0;
// Define custom paragraph styles.
var style1 = {};
style1[DocumentApp.Attribute.FONT_FAMILY] = DocumentApp.FontFamily.ARIAL;
style1[DocumentApp.Attribute.FONT_SIZE] = 18;
style1[DocumentApp.Attribute.BOLD] = true;
style1[DocumentApp.Attribute.FOREGROUND_COLOR] = '#ff0000';
var style2 = {};
style2[DocumentApp.Attribute.FONT_FAMILY] = DocumentApp.FontFamily.ARIAL;
style2[DocumentApp.Attribute.FONT_SIZE] = 14;
style2[DocumentApp.Attribute.BOLD] = true;
style2[DocumentApp.Attribute.FOREGROUND_COLOR] = '#007cb0';
// Search document's body for the table of contents (assuming there is one and only one).
var toc = doc.getBody().findElement(DocumentApp.ElementType.TABLE_OF_CONTENTS).getElement().asTableOfContents();
//Loop all the table of contents to apply new formating
for (var i = 0; i < toc.getNumChildren(); i++) {
//Search document's body for corresponding paragraph & retrieve heading
var searchText = toc.getChild(i).getText();
for (var j=0; j<doc.getBody().getNumChildren(); j++) {
var par = doc.getBody().getChild(j);
if (par.getType() == DocumentApp.ElementType.LIST_ITEM) {
var searchcomp = par.getText();
if (par.getText() == searchText) {
// Found corresponding paragrapg and update headingtype.
var heading = par.getHeading();
var level = par.getNestingLevel();
}
}
}
//Insert Paragraph number before text
if (level==0) {
level1++;
level2=0;
toc.getChild(i).editAsText().insertText(0,level1+". ");
}
if (level==1) {
level2++;
toc.getChild(i).editAsText().insertText(0,level1+"."+level2+". ");
}
//Apply style corresponding to heading
if (heading == DocumentApp.ParagraphHeading.HEADING1) {
toc.getChild(i).setAttributes(style1);
}
if (heading == DocumentApp.ParagraphHeading.HEADING2) {
toc.getChild(i).setAttributes(style2);
}
}
}
Now it is impossible to get a document part (section, paragraph, etc) link without having a TOC. Also there is no way to manage bookmarks from a GAS. There is an issue on the issue tracker. You can star the issue to promote it.
There is a workaround by using a TOC. The following code shows how to get URL from a TOC. It works only if the TOC exists, if to delete it, the links do not work anymore.
function testTOC() {
var doc = DocumentApp.openById('here is doc id');
for (var i = 0; i < doc.getNumChildren(); i++) {
var p = doc.getChild(i);
if (p.getType() == DocumentApp.ElementType.TABLE_OF_CONTENTS) {
var toc = p.asTableOfContents();
for (var ti = 0; ti < toc.getNumChildren(); ti++) {
var itemToc = toc.getChild(ti).asParagraph().getChild(0).asText();
var itemText = itemToc.getText();
var itemUrl = itemToc.getLinkUrl();
}
break;
}
}
}
The function iterates all document parts, finds the 1st TOC, iterates it and the variables itemText and itemUrl contain a TOC item text and URL. The URLs have #heading=h.uuj3ymgjhlie format.
Since the time the accepted answer was written, the ability to manage bookmarks inside Google Apps Script code was introduced. So it is possible to get a similar URL, though not the same exact URL as in example. You can manually insert a bookmark at the section heading, and use that bookmark to link to the section heading. It seems that for the purposes of the question, it will suffice. Here is some sample code (including slight modifications of code from question):
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
body.appendParagraph("Main").setHeading(DocumentApp.ParagraphHeading.HEADING1);
var section = body.appendParagraph("Section 1");
section.setHeading(DocumentApp.ParagraphHeading.HEADING2);
// create and position bookmark
var sectionPos = doc.newPosition(section, 0);
var sectionBookmark = doc.addBookmark(sectionPos);
// add a link to the section heading
var paragraph = body.appendParagraph("");
paragraph.appendText("Now we add a ");
paragraph.appendText("link to the section heading").setLinkUrl('#bookmark=' + sectionBookmark.getId());
paragraph.appendText(".");
Is it imperative that the document is a native Google docs type (ie. application/vnd.google-apps.document)?
If you stored the document as text/html you would have much greater control over how you assemble the document and how you expose it, eg with anchors.