draft-jsというFacebook製のWYSIWYG構築ツールのBlockオブジェクトに変換


# はじめに

PloneのSPA CMSツールである、Voltoでは、Block編集を使っています。
そのBlock編集をになうのが、Facebook製のWYSIWYG構築ツールの `draft-js` です。

draft-jsを理解することが、VoltoのBlockを理解することになります。

# Block・HTML・JSONの関係

## Block

draft-jsでは、JSON形式のデータを元に、Block構造を内部を表します。
ここでいうBlock構造は、複数のBlockからなる複数の要素ではなく、一つの箱であるBlockの内部要素をしめしています。

Voltoでは、複数のBlockを組み合わせる事で、コンテンツのページを作っていきますが、今回の話題は、一つのBlockの内部構造についてフォーカスします。

## HTML

レンダリング結果はHTMLとして表現されます。
しかし、draft-jsが持つ内部構造は特殊なオブジェクトです。その中間地点として、JSONの構造があります。
このJSON構造を用いることで、データベースへの保存やデータベースからの読み込みを実現しています。

## JSON

JSON構造の永続化したデータをdraft-jsの内部構造に変換したり、HTMLをdraft-jsの内部構造に変換したりすることをサポートしています。

# 各種変換

公式ドキュメント
https://draftjs.org/docs/api-reference-data-conversion/

## 内部構造 -> JSON

`convertFromRaw()`

以下の様な `RawDraftContentState` を `ContentState` というJSONに変換できる

```js
{
  contentBlocks: [ ContentBlock { _map: [Map] } ],
  entityMap: {
    getLastCreatedEntityKey: [Function: getLastCreatedEntityKey],
    create: [Function: create],
    add: [Function: add],
    get: [Function: get],
    mergeData: [Function: mergeData],
    replaceData: [Function: replaceData],
    __getLastCreatedEntityKey: [Function: __getLastCreatedEntityKey],
    __create: [Function: __create],
    __add: [Function: __add],
    __get: [Function: __get],
    __mergeData: [Function: __mergeData],
    __replaceData: [Function: __replaceData]
  }
}
```

変換後の `ContentState`

```JSON
{
  blocks: [
    {
      key: 'csete',
      text: 'test',
      type: 'unstyled',
      depth: 0,
      inlineStyleRanges: [],
      entityRanges: [],
      data: {}
    }
  ],
  entityMap: {}
}
```


## JSON -> 内部構造

`convertToRaw()`

convertFromRawの反対に、`ContentState` を `RawDraftContentState` 内部構造に変換できる


## HTML -> 内部構造

`convertFromHTML()`

HTMLを Blockのオブジェクトに変換できる

```HTML
<div>test</div>
```


# 移行用にHTMLからJSONを作る

## 関連パッケージのインストール

```bash
$ nvm use v12.14.1
$ npm init
```

```bash
$ npm install express --save
$ npm install body-parser --save
$ npm install draft-js --save
$ npm install react --save
$ npm install react-dom --save
$ npm install jsdom --save
```


## コードサンプル

実際に変換に使ったコード

```app.js
var { ContentState, convertToRaw, convertFromHTML } = require("draft-js");
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM();
document = dom.window.document;
HTMLElement = require("jsdom/lib/jsdom/living/nodes/HTMLBodyElement-impl")
  .implementation;
HTMLAnchorElement = require("jsdom/lib/jsdom/living/nodes/HTMLAnchorElement-impl")
  .implementation;
Node = require("jsdom/lib/jsdom/living/nodes/Node-impl").implementation;  // これが無いと不十分なBlockができた。

var blocksFromHTML = convertFromHTML("<div>123</div>");

var data2 = ContentState.createFromBlockArray(
  blocksFromHTML.contentBlocks,
  blocksFromHTML.entityMap
);
var r = convertToRaw(data2);
console.log(r);
```

```
$ node app.js
```

以下を参考にしました。(中国語で説明は読めませんでしたが、コードを参考にしました。)
https://blog.csdn.net/ISaiSai/article/details/73250473

今回は、手元のローカルマシンでnodeを使って動かしました。
そのため、jsdomというライブラリで、DOMを作ってあげる必要がありました。
jsdomのバージョンが最近上がったようで、以前のサンプルが動かなくなっていて、苦労しました。


# APIサーバで変換後のJSONを受け取れるように

今回の要件だと、変換時やインポート時にPythonを用いています。(既存の試算を生かすためにも)
その為、この変換だけ外出しして、REST APIで変換後のデータを受け取れたら便利だと思っています。

ExpressライブラリでNodeで動くAPIサーバを書きました。

```server.js
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const port = 3000;

const { ContentState, convertToRaw, convertFromHTML } = require("draft-js");
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

app.post("/", (req, res, next) => {
  const data = req.body.html;

  const dom = new JSDOM();
  document = dom.window.document;
  HTMLElement = require("jsdom/lib/jsdom/living/nodes/HTMLBodyElement-impl")
    .implementation;
  HTMLAnchorElement = require("jsdom/lib/jsdom/living/nodes/HTMLAnchorElement-impl")
    .implementation;
  Node = require("jsdom/lib/jsdom/living/nodes/Node-impl").implementation;  // これが無いと不十分なBlockができた。
  const blockRawData = convertFromHTML(data);
  console.log(blockRawData);
  const blockRawData2 = ContentState.createFromBlockArray(
    blockRawData.contentBlocks,
    blockRawData.entityMap
  );
  const blockData = convertToRaw(blockRawData2);
  console.log(blockData);
  res.send(blockData);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));
```

```bash
$ curl -H "Content-Type: application/json" -X POST http://localhost:3000/ -d '{"html": "<div>test</div>"}'
{"blocks":[{"key":"s992","text":"test","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}
```

```JSON
{
  blocks: [
    {
      key: 's992',
      text: 'test',
      type: 'unstyled',
      depth: 0,
      inlineStyleRanges: [],
      entityRanges: [],
      data: {}
    }
  ],
  entityMap: {}
}
```