freeeの開発情報ポータルサイト

Docs の独自コマンドはサーバなしで動く。

毎週の定例ミーティングで Google Docs にテンプレートを追加するという作業をやったことがあるだろうか?
あるいは毎回変わるファシリテータをその度に手動で書いたことがあるだろうか?
そういう作業は Google Apps Script (GAS) を用いて自動化することが出来る。

GASを動かすのに面倒なサーバ構築はいらない。Googleが用意したエディタでGoogleが用意した関数を使ってJavaScriptを書けば動いてくれる。
毎週テンプレートを追加するような定期的な作業はこれを使うと便利だ。

さあドキュメントを開いて Extensions -> Apps Script からプログラムを書こう。

メニューを追加しよう

独自のメニュー

Docsはメニューをカスタマイズして独自のコマンドを紐づけることができる。Docsを開いたときに呼ばれる onOpen でメニューを追加してやるのが良い。
addItem にはメニュー名と関数名を指定する。

function onOpen() {
  const ui = DocumentApp.getUi();

  ui.createMenu('今日の晩御飯')
    .addItem('何も食べない', 'alertStarvation')
    .addSeparator()
    .addItem('カレー', 'addCurry')
    .addItem('サバ', 'addFish')
    .addItem('タコス', 'addTaco')
    .addToUi();
}

ところで ui.alert を使うとアラートダイアログを出すことができる。開発中に値を確認するためにこれが使えるんだが、ちょっと呼び方が面倒なので簡略化した関数を作っておくと便利。

function alert(text) {
  DocumentApp.getUi().alert(text);
}

特定の場所にテンプレートを挿入しよう

テンプレート付きのドキュメント

定例ミーティングで同じドキュメントを使い回していると、毎回テンプレートを追加するということが起きるだろう。テンプレートの Paragraph を取得して insertParagraph でそれを追加してやると出来る。

function addTemplate() {
  const body = DocumentApp.getActiveDocument().getBody();
 
  var inTemplate = false;
  const templateParagraphs = [];
 
  // テンプレートパラグラフの取得
  body.getParagraphs().forEach((paragraph) => {
    if (paragraph.getText() === 'テンプレート') {
      inTemplate = true;
    }
 
    if (inTemplate) {
      templateParagraphs.push(paragraph.copy());
    }
  })
 
  // テンプレートパラグラフの挿入
  templateParagraphs.forEach((paragraph, i) => {
    if (paragraph.getType() === DocumentApp.ElementType.LIST_ITEM) {
      body.insertListItem(i, paragraph);
    } else {
      body.insertParagraph(i, paragraph);
    }
  });
}

注意! Paragraph を取得したら copy() するのを忘れないで! getParagraphs() で得られるのはすでにbodyにあるパラグラフへの参照で、このままだと新たに追加できないので deep copy しておく必要がある。

注意! getParagraphs() で得られるのは Paragraph の他に ListItem がある。ListItem は Paragraph の一種なんだが、なぜか insertParagraph に渡せない。getType() を見て ListItem なら insertListItem を使う必要がある。

テンプレートを追加するときに日付をつけてやると毎日使えるようになる。

  const date = new Date();
  const dateString = Utilities.formatDate(date, "Asia/Tokyo", "yyyy-MM-dd");
  templateParagraphs[0].setText(dateString);

スナップショットを取ろう

テンプレートを追加するんじゃなくて、まるっとテンプレートに置き換えるやり方もある。
自分のチームでは1スプリント内で共有のドキュメントを使って、スプリントが終わるとそのスナップショットをとってからテンプレートに戻すやり方をしていた。このやり方だとドキュメントのリンクが変わらないのでブックマークしておくのに都合がいいのだ。

スナップショットを取るには DriveApp の makeCopy を使う。

function makePreviousDoc() {
  const doc = DriveApp.getFileById(
    DocumentApp.getActiveDocument().getId()
  );

  doc.makeCopy('前のドキュメント');
}

スナップショットをとったら clear() すると白紙に出来る。

  DocumentApp.getActiveDocument().clear();

次のファシリテータを記入しよう

ファシリテータの書いてある前日のドキュメント ファシリテータの書いてある当日のドキュメント

ミーティングのファシリテータを毎回変えることがあると思う。そういうときにはドキュメントに自動で記入されるようにしよう。
前のミーティングのドキュメントのリンクを1行目に入れておけば、 getParagraphs()[0].getLinkUrl() でそれを使うことが出来る

function addFacilitator() {
  getFacilitatorParagraph(DocumentApp.getActiveDocument())
    .appendText(getFacilitator());
}
 
function getFacilitator() {
  const previousDocUrl = DocumentApp.getActiveDocument().getBody().getParagraphs()[0].getLinkUrl();
  const previousDoc = DocumentApp.openByUrl(previousDocUrl);
  const facilitatorText = getFacilitatorParagraph(previousDoc).getText();
 
  const previousFacilitator = facilitatorText.slice(facilitatorText.indexOf(':') + 1);
  const facilitators = facilitatorText.match(/(.*)/g)[0].slice(1, -1).split('→');
 
  facilitators.push(facilitators[0]);
  return facilitators[facilitators.indexOf(previousFacilitator) + 1];
}
 
function getFacilitatorParagraph(doc) {
  return doc.getBody().getParagraphs().find((paragraph) => paragraph.getText().includes('ファシリテータ'));
}

注意! DocumentApp.openByUrl は引数のurlの形式に厳格で、最後のスラッシュを省略しているとエラーになる。

References

文責

Tomohito Yamanaka
freee会計でWebアプリケーション開発しています。

Twitter: https://twitter.com/intomyam
GitHub: https://github.com/intomyam