Google ドキュメントを Markdown 化した

GAS の制約に首を締められながら、 Google ドキュメントを Markdown 化するスクリプトを書きました。こんなの:

![demo.png](https://qiita-image-store.s3.amazonaws.com/0/176893/4feea767-c156-0087-ee73-b8b8fa191b71.png)

## やったこと

アドオンメニューに項目を追加:

``` js
function addAddonMenu(caption, functionName) {
  DocumentApp.getUi().createAddonMenu().addItem(caption, functionName).addToUi();
}

function onOpen() {
  addAddonMenu("Markdownify", "showAsMarkdown");
}
```

（GAS では `onOpen` という名前の関数はドキュメントが開かれたときに実行される。）

`showAsMarkdown` は、アクティブドキュメントを読み込み、 Markdown 文字列を表示する関数です。今回はこのように実装しました。:

``` js
function showAsMarkdown() {
  const doc = DocumentApp.getActiveDocument();
  const ui = DocumentApp.getUi();
  const markdownify = compose(parse, serialize);
  ui.alert(markdownify(doc));
```

`compose` は関数合成関数です。 `parse` と `serialize` はこんな感じに書きました。:

``` js
function parse(doc) {
  const builder = new DocumentParseTreeBuilder();
  const tree = builder.fromGoogleDocumentElement(doc);
  return tree;
}

function serialize(tree) {
  return tree.asMarkdown();
}
```

### `ParseTree` の実装

パーズ木はこんな感じです。:

``` js
function ParseTree(value, children) {
}

ParseTree.prototype.belongTo = function (types) {
  const that = this;
  return types.some(function (type) {
    return (that instanceof type);
  });
};

ParseTree.prototype.asMarkdown = function () {
  return this.childrenAsMarkdown();
};

ParseTree.prototype.childrenAsMarkdown = function () {
  const childrenAsMarkdown = this.children.map(function (child) {
    return child.asMarkdown();
  });
  return childrenAsMarkdown.join("");
};
```

`belongTo()` メソッドは、 `types` にそのオブジェクトのコンストラクターが含まれているかどうかを検査します。 `asMarkdown()` メソッドは、そのオブジェクトを Markdown 化した文字列を返します。 `childrenAsMarkdown()` メソッドは、そのオブジェクトの子に対して、それぞれ `asMarkdown()` を適用し、それを連結したものを返します。

``` js
function DocumentParseTree(value, children) {
  const childTypes = [BodyParseTree];
  this.children = children;

  if (!this.children.every(function (child) {
    return child.belongTo(childTypes);
  })) {
    throw new ParseException("DocumentParseTree's child should be " + conjoin(this.childTypes, "or"));
  }
}
DocumentParseTree.prototype = Object.create(ParseTree.prototype);
DocumentParseTree.prototype.constructor = DocumentParseTree;
```

`DocumentParseTree` コンストラクターで記述されている `if` 文は、葉以外の全てのクラスのコンストラクターに記述しますが、この記事では省略します。

``` js
function BodyParseTree(value, children) {
  const childTypes = [
    ParagraphParseTree,
    TitleParseTree,
    SubtitleParseTree,
    Heading1ParseTree,
    Heading2ParseTree,
    Heading3ParseTree,
    Heading4ParseTree,
    Heading5ParseTree,
    Heading6ParseTree,
    UnorderedListItemParseTree,
    OrderedListItemParseTree,
  ];
  this.value = value;
  this.children = children;
}
BodyParseTree.prototype = Object.create(ParseTree.prototype);
BodyParseTree.prototype.constructor = BodyParseTree;
```

``` js
function ParagraphParseTree(value, children) {
  const childTypes = [Text];
  this.value = value;
  this.children = children;
}
ParagraphParseTree.prototype = Object.create(ParseTree.prototype);
ParagraphParseTree.prototype.constructor = ParagraphParseTree;
```

`TitleParseTree`, `SubtitleParseTree`, `Heading1ParseTree`, .. も同様に実装します。

``` js
function UnorderedListItemParseTree(value, children) {
  const childTypes = [Text];
  this.value = value;
  this.children = children;
}
UnorderedListItemParseTree.prototype = Object.create(ParseTree.prototype);
UnorderedListItemParseTree.prototype.constructor = UnorderedListItemParseTree;
```

`OrderedlistItemParseTree` も同様に実装します。

最後に、 `Text` クラスを実装します。今回のパーズ木の唯一の葉クラスで、 `value` には文字列を格納します。（ GSuite Text でもよかったんだけど、取得が面倒だった。）:

``` js
function Text(value, children) {
  const childTypes = [];
  this.value = value;
  this.children = children;
}
Text.prototype = Object.create(ParseTree.prototype);
Text.prototype.constructor = Text;
```

### `ParseTreeBuilder` の実装

次に、適当なドキュメントを

``` js
{
  constructor: DocumentParseTree,
  value: [Object Document],
  children: [{
    constructor: BodyParseTree,
    value: [Object BodySection],
    children: [{
      constructor: Heading1ParseTree,
      value: [Object Heading1],
      children: [{
        constructor: Text,
        value: "Heading String",
        children: []
      }]
    }, {
      constructor: ParagraphParseTree,
      value: [Object Paragraph],
      children: [{
        constructor: Text,
        value: "The quick brown fox jumps over the lazy dog.",
        children: []
      }]
    }]
  }]
}
```

のようなオブジェクトにパーズするためのクラス `ParseTreeBuilder` を作りました。

``` js
function ParseTreeBuilder() {
}

ParseTreeBuilder.prototype.fromGoogleDocumentElement = function (lmnt) {
  return new this.constructor.Product(lmnt, this.getChildren(lmnt))
};
```

`fromGoogleDocumentElement()` メソッドは Google ドキュメントの `Element` からパーズ木を生成するやつです。これしか作っていませんが。

``` js
function DocumentParseTreeBuilder() {
}
DocumentParseTreeBuilder.Product = DocumentParseTree;

DocumentParseTreeBuilder.prototype.getChildren = function (doc) {
  const body = doc.getBody();
  const bodyBuilder = new BodyParseTreeBuilder();

  return [bodyBuilder.fromGoogleDocumentElement(body)];
};
```

`getChildren()` は子要素の `ParseTree` の配列を返します。

``` js
function BodyParseTreeBuilder() {
}
BodyParseTreeBuilder.Product = BodyParseTree;

BodyParseTreeBuilder.prototype.getChildren = function (body) {
  const children = childrenOf(body);
  return children.map(function (child) {
    var childBuilder = null;
    with (DocumentApp.ElementType) switch (child.getType()) {
      case PARAGRAPH:
        childBuilder = new HeadingOrParagraphParseTreeBuilder().getConcreteBuilder(child);
        break;
    }
    return childBuilder.fromGoogleDocumentElement(child);
  });
};

function HeadingOrParagraphParseTreeBuilder() {
}
// HeadingOrParagraphParseTreeBuilder.Product = ParagraphParseTree;

HeadingOrParagraphParseTreeBuilder.prototype.getConcreteBuilder = function (p) {
  with (DocumentApp.ParagraphHeading) switch (p.getHeading()) {
    case NORMAL:
      return new ParagraphParseTreeBuilder();
      break;
    case HEADING1:
      return new Heading1ParseTreeBuilder();
      break;
  }
};
```

`getConcreteBuilder()` は `DocumentApp.ParagraphHeading` や `DocumentApp.GlyphType` に応じて適切なビルダーを返すメソッドです。

``` js
function ParagraphParseTreeBuilder() {
}
ParagraphParseTreeBuilder.Product = ParagraphParseTree;

ParagraphParseTreeBuilder.prototype = Object.create(HeadingOrParagraphParseTreeBuilder.prototype);
ParagraphParseTreeBuilder.prototype.constructor = ParagraphParseTreeBuilder;

function TitleParseTreeBuilder() {
}
TitleParseTreeBuilder.Product = TitleParseTree;

TitleParseTreeBuilder.prototype = Object.create(HeadingOrParagraphParseTreeBuilder.prototype);
TitleParseTreeBuilder.prototype.constructor = TitleParseTreeBuilder;
```

他の `ParseTree` に対応するものも適当に作ります。
 `TextBuilder` の `getChildren()` メソッドは空の配列を返すようにします。

### `asMarkdown()` の実装

最後に、それぞれのパーズ木に、木を Markdown 化するメソッドを追加します。凡そこんな感じです。:

``` js
ParagraphParseTree.prototype.asMarkdown = function () {
  return EOL + EOL + this.childrenAsMarkdown();
}

Heading1ParseTree.prototype.asMarkdown = function () {
  return EOL + EOL + "# " + this.childrenAsMarkdown() + EOL;
};

UnorderedListItemParseTree.prototype.asMarkdown = function () {
  var ret = "";
  if (this.isBeginning()) {
    ret += EOL;
  }
  ret += EOL + "* " + this.childrenAsMarkdown();
  return ret;
};
```

## 課題

- `ListParseTree` が無い
- テーブルが使えない
- パーズエラーを無視しない