iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Modern Web

整合 Google 服務的燃料——透過 Google Apps Script (GAS) 加速你的工作速度系列 第 19

D19 - 如何用 Apps Script 自動化地創造與客製 Google Docs?(六)更改特定內容格式的 Attribute 操作技巧

今天的目標

要怎麼抓出文件中的特定文字或段落,直接改字體的大小、顏色、背景、粗細與字型?先來看今天的結果之一,把文章中的「人」字都改成紅字、粗體與放大——

今天,我們會教用 GAS 搭配 Goolge Doc 來設定,這個要動用到的元素叫做 Attribute。那因為在 Google Slide 中的 Element、Attribute 也很多有重疊,所以這邊就會講細一點,之後就可以一起服用。換句話說,今天會教說怎麼透過 GAS 調整 Google Doc 和 Google Slide 裡面元素的「格式」。我們先複習一下前幾天講過的議題——

那今天,我們會針對上述議題中的「元素」進行。基本上前幾天講了上述元素的操作,今天終於可以進入到「格式調整」了,今天會專注在以下內容——

  1. 如何更改 Google Document 中元素的樣式?

就讓我們開始吧!

前情提要一下

考慮到有些夥伴不一定會都有細讀的文章,就撈叨一點把基本步驟再次附上,如果會的夥伴可以直接跳到 Q5。

我們已經知道大致上,每一個 Google 文件都會有 Element (元件),且每一個 Element 都會有 Attribute (屬性)。今天我們主要會介紹下圖綠色的 Attribute 的部分。

下圖主要是列舉幾個常見用於操作 Element 的 Attribute。

這邊就節錄一本書中的「段落、照片、表格與清單」,來作為今天我們的範例。

好,大致理解基本概念後,就讓我們開始吧。

Step 1 從 Document 中進入 GAS

那這次我們不會用 Google Sheet,而是直接用 Google Doc 進入,借一下 D16 的影片。

一樣第一次會有存取驗證需要大家按一下。這邊仍是借用一下 D2 的影片。

Step 2 設定好 getBody()

我們先用 getActiveDocument() 抓出正在綁定的文件;那假設我們都是針對主要內文(Body)的部分,所以我們先設定好 getbody()

let doc_body = DocumentApp.getActiveDocument().getBody();

因為更新有比較複雜的細節,我們就先來講講刪除。

Q5. 如何用 GAS 設定 Google Doc 的樣式?

基本上之後的步驟會分成:

  1. 讀取/新增目標元素(Element)
    1. 全部讀取
    2. 新增元素
    3. 部分讀取(Specific Range)
  2. 讀取現有屬性(getAttributes()
  3. 更改現有屬性(setAttributes()

Step 3 讀取/新增目標元素(Element)

3-1 和 3-2 主要就是 D16 Element 的讀取與創造 的涵蓋內容,這邊就一樣快速帶給大家看。

Step 3-1 全部段落讀取

這邊就先上一段「讀取」的程式碼——

function readParagraph(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let paragraphs = doc_body.getParagraphs();
  Logger.log(paragraphs)
}

再來看讀取的結果影片——

那如果要改成讀取其他的 Element,可以參考之前的簡易表單來做更換。

Step 3-2 新增元素

那如果我們今天要新增一段表格,可以怎麼做?我們可以運用 appendTable 的功能來執行。

function addTable(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let cells = [[1,2,3],[4,5,6]];
  doc_body.appendTable(cells)
}

跑起來長這樣——

那如果要改成新增其他的 Element,可以參考之前的簡易表單來做操作。

那以上這邊如果覺得太快,或想知道更細部,記得可以回去看 D16 Element 的讀取與創造 ,如果覺得時間OK,那我們就進入到新的部分,也就是「部分讀取」。

Step 3-3 部分讀取元素

比起整段做讀取、更改,我們更常遇到的情況是只要更動部分。像是我想要把文件中的關鍵字抓出來標記顏色等、或是想要動從第三頁到第八頁的表格,詳細要怎麼執行?

基本上第一部,我們都要先跟 GAS 說「我想抓出元素的___部分」。而這邊就要用 findText() 來執行。

findText() 的基本使用架構如下,我們用來示範如何抓到段落中的「人」字。

function testFindText(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let target = '人';
  let searchResult = doc_body.findText(target);

  while (searchResult !== null) {
    Logger.log(searchResult.getElement().asText().getText());
    searchResult = doc_body.findText(target, searchResult);
  }
}

用法上, findText(searchPattern, from) 前面的 searchPattern 用的是 regex(嚴格說起來是 google 的 re2),我們就簡單輸入要找的文字;後面的 from 指的是當找到多個元素後,從哪一個開始?沒輸入的話預設會回傳第一個,但在第一個之後如果要繼續找,就要搭配 while 回圈並將 from 輸入為前一次的搜尋結果。換句話說,就是把下圖手刻的方式轉成 while 迴圈。

跑起來結果長這樣——

那,從清單的只有回傳兩點發現,它確實有抓到「人」字,但是是回傳了「人」字所在的整個元件(Element),如果是在段落(Paragraph)內,就回傳段落,如果是在清單項目(ListItem)內就回傳整個項目。那我們要怎麼抓出特定範圍?這時就要提到 range 元素了。

range 元素與 offset 的概念

Range 是什麼?簡單來說,當我們用滑鼠選取了一段內容後,都會伴隨出range (當然省略掉 select 的部分,不過在這邊我們先注重在 range)。

且每個 range 都有被伴隨的 offset(位移),簡單來說可以當成:是在段落中的哪個位置。

以上面的影片來說,「人」這個字即是位在第四個位置(Offset 為 3);更嚴格地說,「人」這個 range 的在段落中的開始位置(StartOffset) 和結束位置(EndOffset)都是為在 Offset 為 3的部分。

注意因為是程式語言,第一個位置我們要從 Offset 為 0 開始算。位移的概念可以參考如下

那為什麼這個重要?因為當我們等下要改內容時,這就會派上用場。實際上,對 GAS 來說,「部分選取」就是先選整段,再跟我說段落中的哪個位置(Offset)要改。我們快速示範如果要將 Offset 的文字都改成紅色要怎麼做。

我們先來看看沒上 Offset 的結果,會發現就等於整段改成紅色。

同時看看上了 Offset 的結果,會發現只有我們要的「特定範圍」變色。

實際上,我是透過 range 物件中的 getStartOffset()getEndOffsetInclusive() 達到的。先附上程式碼,可以先專注看「Offset」的部分,我們馬上就會來細講改顏色的部分。

function highlightText() {
  let target = '人';
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let searchResult = doc_body.findText(target);
  let textStyle={};
  textStyle[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'

  while (searchResult !== null) {
    let search_text = searchResult.getElement();
    Logger.log(searchResult.getStartOffset() +' '+ searchResult.getEndOffsetInclusive())
    search_text.setAttributes(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),textStyle)
    searchResult = doc_body.findText(target, searchResult);
}

好,那我們能抓到範圍了,接下來就是要改屬性了。但在改屬性前,因為物件的屬性算有點複雜,我個人是會建議先檢查「有沒有這個屬性」。

Step 4 用 getAttributes() 來讀取現有屬性

通常我會先用 getAttributes() 來看我的目標物件有哪些可以調整的屬性。這也可以幫助我們理解怎麼設定屬性。這邊寫一段簡單的程式,會印出目標「段落」、「清單項目」和「文件」的含有屬性。

function testFindText(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let target = '人';
  let searchResult = doc_body.findText(target);

  while (searchResult !== null) {
    Logger.log(searchResult.getElement().asText().getText());
    Logger.log(searchResult.getElement().getAttributes());
    searchResult = doc_body.findText(target, searchResult);
  }

  Logger.log(doc_body.getAttributes())
}

跑起來長這樣——

我這邊把「段落」、「清單項目」與「文件」分開來看。

  1. 「段落」部分:
{STRIKETHROUGH=null, LINK_URL=null, FONT_FAMILY=Source Sans Pro, FONT_SIZE=null, UNDERLINE=null, BACKGROUND_COLOR=null, ITALIC=null, FOREGROUND_COLOR=null, BOLD=null}
  1. 「清單項目」部分
{LINK_URL=null, FOREGROUND_COLOR=null, BOLD=null, BACKGROUND_COLOR=null, UNDERLINE=null, STRIKETHROUGH=null, FONT_FAMILY=Source Sans Pro, FONT_SIZE=null, ITALIC=null}
  1. 「文件」部分
{MARGIN_TOP=21.25984251968504, LINK_URL=null, FONT_SIZE=null, UNDERLINE=null, STRIKETHROUGH=null, FOREGROUND_COLOR=null, FONT_FAMILY=null, BOLD=null, PAGE_HEIGHT=841.68, MARGIN_BOTTOM=72.0, MARGIN_LEFT=72.0, BACKGROUND_COLOR=null, PAGE_WIDTH=595.4399999999999, ITALIC=null, MARGIN_RIGHT=72.0}

這邊要特別提一下我們抓出的資料結構,是一種叫做 dictionary 的結構。這邊的重點會放在,所以如果我們要寫入「屬性」,我們也要套用這結構。

而其中的屬性有三種特性——

  1. 越是上層(Parent)的物件,擁有的屬性越多。子元素有的屬性上層基本上都會有。
  2. 屬性中的順序不一定,但都是種 Key-value Pair(如果想知道原因,主要是因為資料結構是 Dictionary,其內部順序不是重點)
  3. 跟 CSS 階層一樣,下層寫死的屬性會蓋過上層的。這點應該算好理解,因為你設定全文為黑色時,仍是可以設定部分文字為紅色。

特別把第一點拉出來說明,因為待會會用到這概念。

Step 5 用 setAttributes() 更改現有屬性

好,那到底要怎麼改屬性呢?我們先來一段程式碼。

function setElementAttribute(){
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let style = {};
  style[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'
  let paragraphs = doc_body.getParagraphs();
  for(para of paragraphs){
    para.setAttributes(style);
  }
}

跑起來長這樣——

這邊先設定一個 dictionary 叫做 stylelet style = {}),並用 style[key]=value 設定期中的數值(精確來說是 Key-value pair),其中 key 的部分就是我們從 Step 4 中抓出來的屬性們,只是前面多加了 DocumentApp.Attribute ;數值就是 Step 4 的數值們。

可以發現,就是將所有「段落」都改成紅色。至於為什麼「清單」也會變色,複習是因為清單內的文字,其實也有包含一個段落的元素。好,那這是整段的更改,基本上就是要全部元素取得後,針對一個個元素抓出來設定。

那如果我們想做部分文字的更改呢?這邊就可以用我們在 Step 3 最後面的段落,先貼上方便大家比較。

function highlightText() {
  let target = '人';
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let searchResult = doc_body.findText(target);
  let textStyle={};
  textStyle[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'

  while (searchResult !== null) {
    let search_text = searchResult.getElement();
    Logger.log(searchResult.getStartOffset() +' '+ searchResult.getEndOffsetInclusive())
    search_text.setAttributes(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),textStyle)
    searchResult = doc_body.findText(target, searchResult);
}

基本上就是要在 setAttributes() 前加上「起始」與「結束」的 Offset,跑起來長這樣。

好,那我們介紹了「單純改字體顏色」,如果我們想調整的很多,要怎麼辦?沒問題的,這邊直接上程式碼,用「部分元素」做示範。

function highlightText() {
  // target,background
  let target = '人';
  let doc_body = DocumentApp.getActiveDocument().getBody();
  let searchResult = doc_body.findText(target);
  let textStyle={};
  textStyle[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FF0000'
  textStyle[DocumentApp.Attribute.FONT_FAMILY] = 'Calibri';
  textStyle[DocumentApp.Attribute.FONT_SIZE] = 18;
  textStyle[DocumentApp.Attribute.BOLD] = true;

  while (searchResult !== null) {
    let search_text = searchResult.getElement();
    search_text.setAttributes(searchResult.getStartOffset(), searchResult.getEndOffsetInclusive(),textStyle)
    searchResult = doc_body.findText(target, searchResult);
  }
}

跑起來長這樣,確定我們有改到「人」字的顏色、字體與粗體——

但如果想設定其他的參數呢?這邊直接幫大家整理了表格。首先,如果是要改「部分文字」,可以用以下的功能們。

如果是要改「整個段落」,可以用以下的功能們。

如果是要改「文字」以外的部分,可以參考官方文件

好,那這邊就是我們今天的內容。我有參考這篇:Can I color certain words in Google Document using Google Apps Script?,如果有夥伴想將上述功能改成有 UI 的 Add-On,裡面有完整的程式碼。另外,官方也有文件說明 Editing and styling text,也可以搭配參考。


好,那今天就到這邊。今天我們主要交代了 Attribute 的「如何更新」,總算把 Document 的部分告一段落,明天會進入 Slide 的部分,最後會講 Sheet。如果還有問題,透過留言之外,也可以到 Facebook Group,想開很久這次鐵人賽才真的開起來,歡迎來當 Founding Member。如果不想錯過可以訂閱按讚小鈴鐺(?),也歡迎留言跟我說你還想知道什麼做法/主題。我們明天見。


上一篇
D18 - 如何用 Apps Script 自動化地創造與客製 Google Docs?(五)Element 的更新
下一篇
D20 - 如何用 Apps Script 自動化地創造與客製 Google Slides?(一)架構拆解與更改文字
系列文
整合 Google 服務的燃料——透過 Google Apps Script (GAS) 加速你的工作速度30

尚未有邦友留言

立即登入留言