Extracting bold text in a Google Slide, via Apps Script - google-apps-script

Using Apps Script, how to get the list (to be copied and paste in another place) of all the words in bold type contained in the shapes of a Google Slide?

It takes a bit of drilling down but here is an example of finding bold text in a Slide Presentation.
I tested it with a test presentation with normal and bold text scattered throughout.
function test() {
try {
let boldText = [];
let presentation = SlidesApp.getActivePresentation();
let slides = presentation.getSlides();
slides.forEach( slide => {
let shapes = slide.getShapes();
shapes.forEach( shape => {
let text = shape.getText();
let paragraphs = text.getParagraphs();
paragraphs.forEach( paragraph => {
let runs = paragraph.getRange().getRuns();
runs.forEach( run => {
if( run.getTextStyle().isBold() ) {
boldText.push(run.asString());
}
}
);
}
);
}
);
}
);
}
catch(err) {
console.log(err)
}
}
Reference
SlidesApp.getActivePresentation()
Presentation.getSlides()
Slide.getShapes()
Shape.getText()
TextRange.getParagraphs()
Paragraph.getRange()
TextRange.getRuns()
TextRange.getTextStyle()
TextStyle.isBold()
Array.push()

Related

How to Bookmark selection

I want to program the same functionality from Insert>Bookmark on Google Docs. I already used the addBookmark(cursor), now I want to do it on a selected text/image. See screenshot for reference ("congue" is the selected word).
You can add a bookmark at the selection using this as an example.
function addBookmark() {
try {
let doc = DocumentApp.getActiveDocument();
let selection = doc.getSelection();
if( selection ) {
let elements = selection.getRangeElements();
let element = elements[0];
let position = element.getStartOffset();
position = doc.newPosition(element.getElement(),position);
doc.addBookmark(position);
}
}
catch(err) {
console.log(err);
}
}
Reference
Document.getSelection()
Selection.getRangeElements()
RangeElement.getElement()
RangeElement.getStartOffset()
Document.newPosition()

Is it possible to view a list of all objects used in Google Slides?

When some objects in Google Slides get hidden behind another object it may be later hard to find them on the slide.
Is it possible, for example, to see a panel with a list of all objects which are present on a given slide? And possibly edit them even if they are in the bottom layer (completely hidden behind another object)? This might be useful for animations when an object is displayed later and fully covers a previously displayed object.
Your goal I believe is as follows.
Your Google Slides has several text boxes of the same size and the same position.
You want to retrieve the list of texts from the text boxes and want to change the texts using a simpler method.
In this case, I thought that when the sidebar created by Google Apps Script is used for changing the texts, your goal might be able to be simply achieved.
The sample script is as follows.
Usage:
1. Prepare script:
Please copy and paste the following script to the script editor of Google Slides and save the script. And then, please reopen the Google Slides. By this, the custom menu "sample" is created for the Google Slides. When "RUN" in the custom menu "sample" is opened, the script is run.
Code.gs
Please copy and paste this script as Code.gs.
function onOpen() {
SlidesApp.getUi().createMenu("sample").addItem("RUN", "openSidebar").addToUi();
}
function openSidebar() {
const html = HtmlService.createHtmlOutputFromFile("index").setTitle("sample");
SlidesApp.getUi().showSidebar(html);
}
function getSelectedShapes() {
const select = SlidesApp.getActivePresentation().getSelection();
const pageElementRange = select.getPageElementRange();
if (pageElementRange) {
const obj = pageElementRange.getPageElements().reduce((ar, e) => {
if (e.getPageElementType() == SlidesApp.PageElementType.SHAPE) {
const shape = e.asShape();
ar.push({objectId: shape.getObjectId(), text: shape.getText().asString().trim()});
}
return ar;
}, []).reverse();
return obj;
}
return [];
}
function updatedTexts(o) {
const select = SlidesApp.getActivePresentation().getSelection();
const slide = select.getCurrentPage();
const obj = slide.getShapes().reduce((o, e) => Object.assign(o, {[e.getObjectId()]: {shape: e, text: e.getText().asString().trim()}}), {});
o.forEach(({objectId, text}) => {
if (obj[objectId] && obj[objectId].text != text) {
obj[objectId].shape.getText().setText(text);
}
});
return "Done";
}
index.html
Please copy and paste this script as index.html.
<input type="button" id="main" value="Get selected shapes" onClick="main()">
<div id="shapes"></div>
<input type="button" id="update" value="Updated texts" onClick="updatedTexts()" style="display:none">
<script>
function main() {
document.getElementById("main").disabled = true;
document.getElementById("shapes").innerHTML = "";
google.script.run.withSuccessHandler(o => {
if (o.length == 0) {
document.getElementById("update").style.display = "none";
return;
}
const div = document.getElementById("shapes");
o.forEach(({objectId, text}) => {
const input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("id", objectId);
input.setAttribute("value", text);
div.appendChild(input);
});
document.getElementById("update").style.display = "";
document.getElementById("main").disabled = false;
}).getSelectedShapes();
}
function updatedTexts() {
const inputs = document.getElementById("shapes").getElementsByTagName('input');
const obj = [...inputs].map(e => ({objectId: e.id, text: e.value}));
console.log(obj)
google.script.run.withSuccessHandler(e => console.log(e)).updatedTexts(obj);
}
</script>
2. Testing:
Please reopen Google Slides. By this, the custom menu is created. Please open "sample" -> "RUN". By this, the sidebar is opened.
Please select the text boxes on Google Slides.
Click "Get selected shapes" button.
By this, the selected text boxes are retrieved and you can see the texts of text boxes.
Modify the texts.
Click "Updated texts" button.
By this, the modified texts are reflected in the text boxes.
Also, you can see it with the following demonstration movie.
Note:
This is a simple sample script. So please modify the above script and HTML style for your actual situation.
References:
Custom sidebars

Google Docs API - complete documentation (hyperlink issue)

I hope everyone is in good health. This post is my continue of my previous post
My main goal
So main goal was to get the hyperlink and change it the text linked with it. I initially used code from this post and modified it to change the text of first hyperlink. Here is my modified code to change the text of first hyperlink.
function onOpen() {
const ui = DocumentApp.getUi();
ui.createMenu('What to do?')
.addItem('HyperLink Modifier', 'findAndReplacetext')
.addToUi();
}
/**
* Get an array of all LinkUrls in the document. The function is
* recursive, and if no element is provided, it will default to
* the active document's Body element.
*
* #param element The document element to operate on.
* .
* #returns {Array} Array of objects, vis
* {element,
* startOffset,
* endOffsetInclusive,
* url}
*/
function getAllLinks(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();
if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
var inUrl = false;
for (var ch=0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null) {
if (!inUrl) {
// We are now!
inUrl = true;
var curUrl = {};
curUrl.element = element;
curUrl.url = String( url ); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
if (inUrl) {
// in case the link ends on the same char that the element does
links.push(curUrl);
}
}
else {
var numChildren = element.getNumChildren();
for (var i=0; i<numChildren; i++) {
links = links.concat(getAllLinks(element.getChild(i)));
}
}
return links;
}
/**
* Replace all or part of UrlLinks in the document.
*
* #param {String} searchPattern the regex pattern to search for
* #param {String} replacement the text to use as replacement
*
* #returns {Number} number of Urls changed
*/
function findAndReplacetext() {
var links = getAllLinks();
while(links.length > 0){
var link = links[0];
var paragraph = link.element.getText();
var linkText = paragraph.substring(link.startOffset, link.endOffsetInclusive+1);
var newlinkText = `(${linkText})[${link.url}]`
link.element.deleteText(link.startOffset, link.endOffsetInclusive);
link.element.insertText(link.startOffset, newlinkText);
links = getAllLinks();
}
}
String.prototype.betterReplace = function(search, replace, position) {
if (this.length > position) {
return this.slice(0, position) + this.slice(position).replace(search, replace);
}
return this;
}
Note: I used insertText and deleteText functions to update the text value of hyperlink.
My problem with above code
Now the problem was that this code was running too slow. I thought may be it was because I was running the script every-time I needed to search for next hyperlink, So maybe I can break the loop and only get the first hyperlink each time. Then from my previous post the guy gave me a solution to break loop and only get the first hyperlink but when I tried the new code unfortunately it was still slow. In that post he also proposed me a new method by using Google Docs API, I tried using that it was was super fast. Here is the code using Google Docs API
function myFunction() {
const doc = DocumentApp.getActiveDocument();
const res = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(({textRun}) => {
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({text: textRun.content, url: textRun.textStyle.link.url});
}
});
}
return ar;
}, []);
console.log(res) // You can retrieve 1st link and test by console.log(res[0]).
}
My new problem
I liked the new code but I am stuck again at this point as I am unable to find how can I change the text associated with the hyperlink. I tried using the functions setContent and setUrl but they don't seem to work. Also I am unable to find the documentation for these functions on main documentation of this API. I did find I reference for previously mentioned functions here but they are not available for appscript. Here is the sample document I am working on
https://docs.google.com/document/d/1eRvnR2NCdsO94C5nqly4nRXCttNziGhwgR99jElcJ_I/edit?usp=sharing
End note:
I hope I was able to completly convey my message and all the details assosiated with it. If not kindly don't be mad at me, I am still in learning process and my English skills are pretty weak. Anyway if you want any other data let me know in the comments and Thanks for giving your time I really appreciate that.
In order to remove all the hyperlink from your document, you can do the following:
First, retrieve the start and end indexes of these hyperlinks. This can be done by calling documents.get, iterate through all elements in the body content, checking which ones are paragraphs, iterating through the corresponding TextRun, and checking which TextRuns contain a TextStyle with a link property. All this is already done in the code you provided in your question.
Next, for all TextRuns that include a link, retrieve their startIndex and endIndex.
Using these retrieved indexes, call batchUpdate to make an UpdateTextStyleRequest. You want to remove the link property between each pair of indexes, and for that you would just need to set fields to link (in order to specify which properties you want to update) and don't set a link property in the textStyle property you provide in the request since, as the docs for TextStyle say:
link: If unset, there is no link.
Code sample:
function removeHyperlinks() {
const doc = DocumentApp.getActiveDocument();
const hyperlinkIndexes = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(element => {
const textRun = element.textRun;
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({startIndex: element.startIndex, endIndex: element.endIndex });
}
});
}
return ar;
}, []);
hyperlinkIndexes.forEach(hyperlinkIndex => {
const resourceUpdateStyle = {
requests: [
{
updateTextStyle: {
textStyle: {},
fields: "link",
range: {
startIndex: hyperlinkIndex.startIndex,
endIndex: hyperlinkIndex.endIndex
}
}
}
]
}
Docs.Documents.batchUpdate(resourceUpdateStyle, doc.getId());
});
}

Any time a section is mentioned in the document, I want that mention to become a link to the corresponding bookmark

Goal: I have a very long document with many unique sections that each have bookmarks. Any time a section is mentioned in the document, I want that mention to become a link to the corresponding bookmark. It doesn't have to be event-driven, I intend to do it from a menu.
I have the below code written to get a list of the names of each bookmarked line so I can match it to the words in the doc. I'm trying to figure out what line of code to use to link specific text to that bookmark. I've tried to use the setLinkUrl("beginningofurl" + id[i]) code, but the ID of the bookmarks doesn't tell me if it's a header or regular text, and sometimes it is just regular text. I'm wondering if there's a better way of doing this?
var DOC = DocumentApp.getActiveDocument();
function Setlink() {
var bookmarks = DOC.getBookmarks();
var names = [];
for (var i = 0; i < bookmarks.length; i++){
names.push(bookmarks[i].getPosition().getSurroundingText().getText());
}
Logger.log(names);
}
Headings are a property of Paragraph elements. To check a Bookmark to see if it is in a paragraph of a certain Paragraph Heading, we need to get the Position, then the Element, and then check if the Element is indeed a Paragraph before we can check the Paragraph Heading.
We can put our test for if an Element is a heading in a predicate function named isElementInHeading that will return true or false when given an Element.
function isElementInHeading(element) {
if (element.getType() !== DocumentApp.ElementType.PARAGRAPH) {
return false;
}
const {ParagraphHeading} = DocumentApp;
switch (element.getHeading()) {
case ParagraphHeading.HEADING1:
case ParagraphHeading.HEADING2:
case ParagraphHeading.HEADING3:
case ParagraphHeading.HEADING4:
case ParagraphHeading.HEADING5:
case ParagraphHeading.HEADING6:
return true;
}
return false;
}
This can be used to both filter the bookmarks to include only those that mark headings, and to skip over the same headings when using setLinkUrl.
The strategy in this example is to collect both the bookmark's ID and the desired text in one go using a reducer function, then search through the document for each bit of text, check that we didn't just find the header again, and then apply the link.
I am not quite sure how you are getting the URL, but I found just copying and pasting the URL into the script as const url = "https://docs.google.com/.../edit#bookmark="; worked for me.
// for Array.prototype.reduce
function getHeadingBookmarksInfo(bookmarks, bookmark) {
const element = bookmark.getPosition().getElement();
if (isElementInHeading(element)) {
return [
...bookmarks,
{ id: bookmark.getId(), text: element.getText() }
];
}
return bookmarks;
}
function updateLinks() {
const doc = DocumentApp.getActiveDocument();
const bookmarks = doc.getBookmarks();
const headingBookmarksInfo = bookmarks.reduce(getHeadingBookmarksInfo, []);
const body = doc.getBody();
headingBookmarksInfo.forEach(function(info) {
const {id, text} = info;
let foundRef = body.findText(text);
while (foundRef !== null) {
const element = foundRef.getElement();
if (!isElementInHeading(element.getParent())) {
element.asText()
.setLinkUrl(
foundRef.getStartOffset(),
foundRef.getEndOffsetInclusive(),
url + id // assumes url is hardcoded in global scope
);
}
foundRef = body.findText(text, foundRef);
}
});
}

How to adding item menus with the java script of an loop's sub menus inside like this example ".addItem("Go to This Sheet", "S"+i+"GoToS")"?

Hy, Everyone .. I have a question about creating the menu in GAS (Google Apps Scripts) to implement to a Google Spreadsheet without a three of the scripts which is has been take long long way and take my energy too and many many so many lines of the scripts like this I have created. This is the script. Here's the code :
function Menu1() {
var ui = s.getUi(),
s = SpreadsheetApp,
ss = s.getAcgtiveSpreadsheet(),
sss = ss.getSheets(),
madeMenu = ui.createMenu('Sheet Tools Just For an Example Menus');
for (var i=0; i < sss.length; i++){
madeMenu.addSubMenu(ui.createMenu(sss[i].getName())
.addItem('Go to ...', 'S'+i+'GoToS')
.addItem('Rename ...', 'S'+i+'RenameS')
.addItem('Move ...', 'S'+i+'MoveS'))
madeMenu.addToUi();
}
}
function GoToS(getSheetNumber) {
var sheet = sss[getSheetNumber];
ss.setActiveSheet(sheet);
}
This of the main of my problems cames to !!! Because these structures of the scripts and then so to make me had to create this lines. See at the below :
function S0GoToS() {
GoToS(0)
}
function S1GoToS() {
GoToS(1)
}
function S2GoToS() {
GoToS(2)
}
function S3GoToS() {
GoToS(3)
}
function S4GoToS() {
GoToS(4)
}
function S5GoToS() {
GoToS(5)
}
The question is How to create them without the third-sub of the scripts ??? I thought and I hope there is the another way to create these for sure yes I believe there is but that just the because I don't know how about that way. Please someone chould be can help me to solve this case. Any achieves will be appreciated. Thanks in advance has taken in your time and I appologies for my poor english.
You can, in fact, generate those functions dynamically. The idea is to keep a for-loop outside of any of your functions, in the "global" scope, which will generate all these functions. Afterwards, they can be called by a menu action. Your could would look like the following:
// function "constructors"
function createGoToFunction(sheetIndex) {
return function() {
var sheet = SpreadsheetApp.getActive().getSheets()[sheetIndex];
sheet.activate();
}
}
function createRenameFunction(sheetIndex) {
return function() {
// Your rename logic
}
}
function createMoveFunction(sheetIndex) {
return function() {
// Your move logic
}
}
// functions definition
this['ALL_SHEETS'] = SpreadsheetApp.getActive().getSheets();
for (i=0; i<this['ALL_SHEETS'].length; i++) {
this['S'+i+'GoToS'] = createGoToFunction(i);
this['S'+i+'RenameS'] = createRenameFunction(i);
this['S'+i+'MoveS'] = createMoveFunction(i);
}
delete this['ALL_SHEETS'];
delete this['i'];
function Menu1() {
var ui = SpreadsheetApp.getUi();
var sheets = SpreadsheetApp.getActive().getSheets();
var madeMenu = ui.createMenu('Sheet Tools Just For an Example Menus');
for (var i=0; i < sheets.length; i++){
var subMenu = ui.createMenu(sheets[i].getName())
.addItem('Go to ...', 'S'+i+'GoToS')
.addItem('Rename ...', 'S'+i+'RenameS')
.addItem('Move ...', 'S'+i+'MoveS');
madeMenu.addSubMenu(subMenu);
}
madeMenu.addToUi();
}
function onOpen() {
Menu1();
}
In order to implement your own functionality for the functions, you just have to change the body of them defined on top (see createGoToFunction as an example).