[GAS] Googleドキュメントで全消去の後appendParagraphしたら先頭に空行ができる

## はじめに
久しぶりにGoogleドキュメントへＧＡＳ構築する機会がありましたので、小ネタ扱いの備忘録です。Googleドキュメント関連ではあまりピンポイントな情報にたどり着けないことが多いので、できる限り具体的な内容のタイトルにしてみました。

## 目的
タイトルの通り、スクリプトを実行したところ、先頭に空行ができてしまいました[^1]。これを削除したい、もしくは空行を出さずにスマートに先頭から追加できないか、についての検証です。

[^1]: 正確には元々存在する空段落以降にダミー文字列が順次追加された状態なので、空行が**できる**ということではないです。

## ロジック
以下、検証用に既存のドキュメントデータを全消去した後、順次、ダミー文字列を追加していくという単純なスクリプトを書いてみました。

```javascript:sample01.gs
function main(){
  const Doc  = DocumentApp.getActiveDocument()
  const Body = Doc.getBody()

  Body.clear() // 全消去

  // ダミー文字列を用意し順次展開でパラグラフセット
  const arr = Array(10).fill('王様の耳はパンの耳')
  arr.forEach(para => Body.appendParagraph(para))
}
// 先頭に空行
```

結果は先頭行が空行になります。

元々新規作成時や全消去（初期化）した場合、細かいことは置いておいて、`Document`の`Body`には１つの`Paragraph`があり、空の`Text`がある状態です[^2]。つまり初期状態では空段落が最初から存在していて、そこに`appendParagraph`していくわけなので、当然の結果です。

同様のことは`insertParagraph`でも起きます。

```javascript:sample02.gs
function main(){
  /* 省略 */
  arr.forEach((para, idx) => Body.insertParagraph(idx, para))
}
// 末尾に空行
```

この場合は先頭行ではなく、末尾が空行になります。元々あった空行が`insertParagraph`によって押し出されるような感じになるので、これも当然の結果です。

[^2]: ドキュメントの詳細な構造については[公式ガイド](https://developers.google.com/apps-script/guides/docs#structure_of_a_document)にて確認できます。

### 解決策について
お恥ずかしいことですが、久しぶりかつドキュメントの振る舞いを理解していなかったためこの問題に嵌ってしまいました。現象の原因に納得ができたため、その解決策として以下の２パターンについて検証してみました。

1. 順次処理の後で`Body.removeChild`する
1. 先頭（もしくは末尾）のみ元の空行と差し替える

### 1-順次処理の後で`Body.removeChild`する
`Body`直下の子要素を削除する`removeChild`を使用します。引数に`Child`を取りますので、`Body.getChild(インデックス値)`で`Child`として渡します。

#### 1-1 `appendParagraph`の後、先頭行を削除する

```javascript:sample03.gs
function main(){
  /* 省略 */
  arr.forEach(para => Body.appendParagraph(para))
  Body.removeChild(Body.getChild(0))
}
```

#### 1-2 `insertParagraph`の後、末尾行を削除する

```javascript:sample04.gs
function main(){
  /* 省略 */
  arr.forEach((para, idx) => Body.insertParagraph(idx, para))
  Body.removeChild(Body.getChild(Body.getParagraphs().length-1))
}
//Exception: Can't remove the last paragraph in a document section.
```

最終段落は削除できない仕様のようで、実行するとエラーになります。`Exception: Can't remove the last paragraph in a document section.`

### 2-先頭（もしくは末尾）のみ元の空行と差し替える
```javascript:sample05.gs
function main(){
  const Doc  = DocumentApp.getActiveDocument()
  const Body = Doc.getBody()
  const firstPara = Body.clear().getParagraphs()[0] // 先頭行を取得

  const sentences     = Array(10).fill('王様の耳はパンの耳')
  const firstSentence = sentences.shift()
  firstPara.setText(firstSentence) // 先頭行を差し替え

  /* 省略 sample01参照 */
}
```

結果は想定通りでした。しかし、`removeChild`したくないことにこだわったことで、かえって面倒くさいことをしている印象です。この方法は個別案件における何らかの優位性がない限りは使うことはないでしょう。
※末尾を元の空行と差し替える処理につきましては、上記理由より割愛します。

## 最後に
執筆時点では`sample03`がシンプルでスマートかと思います。ドキュメント操作では基本的に`Paragraph`単位の順次処理になると思いますので、より効率的で高速なロジックを意識しないといけないですね。ただ、スプレッドシートの`setValues`のような一括挿入、差し替えができるようなメソッドがあればそれで済む話なのにとも思います。（知らないだけかもしれませんが）

余談として、`appendParagraph`と`insertParagpaph`ではどちらが高速か、1000文[^3] の新規追加で検証しました。各々5回試行の平均値となります。ご参考まで。

|method|time|
|:--|:-:|
|appendParagraph|約８秒|
|insertParagpaph|約３秒|

[^3]: 10000文で検証しようとしたところ、右記のように怒られました。`Too many changes applied before saving document. Please save changes in smaller batches using Document.saveAndClose(), then reopen the document with Document.openById().`　妥協して5000文にしたところ、`Service Documents failed while accessing document with id xxxxxxxxxxxxxxxxxxxx`とまた怒られましたのでさらに妥協しました。

### [参考]
+ [Extending Google Docs  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/guides/docs#structure_of_a_document)