Scratch みたいなビジュアルプログラミング言語を無料で作って公開する


ブロックをつなぎ合わせるだけでプログラミングができるビジュアルプログラミング言語。Google製のライブラリー [Blockly](https://developers.google.com/blockly/) を使えば、オリジナルのビジュアルプログラミング言語のエディターを作ることができます。

今回は、無料で Web アプリの画面 (フロントエンド) を作れるサービス [CodePen](https://codepen.io/) と、JavaScript のライブラリーを配信している CDN (コンテンツデリバリーネットワーク) の [JsDelivr](https://www.jsdelivr.com/) を使って、Blockly を動かすところまでをやってみます。

CodePen で作ったものは Qiita の記事に埋め込めるので便利です。

- Qiitaで記事にCodePenが埋め込めるようになりました https://qiita.com/Qiita/items/edae7417214c8e957f54

作った Web アプリはこれです。
https://codepen.io/takatama/pen/dqjBQx

<p data-height="600" data-theme-id="0" data-slug-hash="dqjBQx" data-default-tab="result" data-user="takatama" data-pen-title="Blockly Template Tiny" class="codepen">See the Pen <a href="https://codepen.io/takatama/pen/dqjBQx/">Blockly Template Tiny</a> by Hirokazu Takatama (<a href="https://codepen.io/takatama">@takatama</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

なお CodePen 上で Fork すれば、ソースコード一式をコピーして自分のアプリとして編集できるようになります。どうぞお使いください。

https://developers.google.com/blockly/guides/create-custom-blocks/overview を参考にしながら、
オリジナルのビジュアルプログラミング言語作りをお楽しみください！

# CodePen で Blockly を動かす手順

必要なライブラリーを CDN から呼び出し、HTML と XML で Blockly の操作画面 (ワークスペース) を定義し、JavaScript でワークスペースを動作させます。

## ライブラリーを CDN から呼び出す

Blockly は https://www.jsdelivr.com/package/npm/blockly で配布しています。
その中から必要となる以下の JavaScript を CodePen の Settings > JavaScrpit に追加していきます。

ライブラリーは呼び出す順序が必要です。一番上から呼び出されるので、blockley_*.js が一番上になるようにしてください。

### 必須

英語で表示にするため、en.min.js を選んでいます。日本語表示にする場合は、ja.min.js を選んでください。

- https://cdn.jsdelivr.net/npm/blockly@1.0.0/blockly_compressed.min.js
- https://cdn.jsdelivr.net/npm/blockly@1.0.0/blocks_compressed.min.js
- https://cdn.jsdelivr.net/npm/blockly@1.0.0/msg/js/en.min.js

### 作ったブロックからソースコードを出力するために必要なもの

今回は JavaScript を出力します。他に Python, PHP, Lua, Dart もあります。

- https://cdn.jsdelivr.net/npm/blockly@1.0.0/javascript_compressed.min.js

## HTML でワークスペースとソースコード出力画面を作る
公式ドキュメント https://developers.google.com/blockly/guides/configure/web/fixed-size を参考にして、画面を作ります。

Blockly を表示する #blocklyDiv に加えて、ソースコードを表示する #code も追加します。flex レイアウトを使って、2列に表示します。

```html
<div id="content" style="display:flex;">
  <div>
    <div id="code" style="width: 400px; padding: 1px;"><pre/></div>
    <button onclick="runCode();">Run</button>
  </div>
  <div id="blocklyDiv" style="height: 480px; width: 800px;"></div>
</div>
```

### XML でワークスペースを定義する

公式ドキュメント https://developers.google.com/blockly/guides/configure/web/fixed-size を参考にして、表示するブロックを定義します。

``` xml
<xml id="toolbox" style="display: none">
  <block type="controls_if"></block>
  <block type="controls_repeat_ext"></block>
  <block type="logic_compare"></block>
  <block type="math_number"></block>
  <block type="math_arithmetic"></block>
  <block type="text"></block>
  <block type="text_print"></block>
</xml>
```

さらに、ガイダンスとしてブロックひとつだけ最初から表示しておき、何ができるのかを伝えます。#startBlocks で表示するブロックを定義します。

``` xml
<xml id="startBlocks" style="display: none">
  <block type="text_print" id="0@)u5QVs/@*zX^XXw%Lp" x="35" y="21">
    <value name="TEXT">
      <shadow type="text" id="=[)BkyD!BK*AF%PcBi9;">
        <field name="TEXT">Hello, World</field>
      </shadow>
    </value>
  </block>
</xml>
```

## JavaScript でワークスペースを動作させる

徐々に機能を追加していきます。最後に作った JavaScript の全てをまとめて掲載します。

### ワークスペースを初期化する

公式ドキュメント https://developers.google.com/blockly/guides/configure/web/fixed-size を参考にして、ワークスペースを初期化します。

https://developers.google.com/blockly/guides/get-started/web#configuration を参考にして、ゴミ箱を表示するようにしました。

```javascript
var workspace = Blockly.inject('blocklyDiv', {
  toolbox: document.getElementById('toolbox'),
  trashcan: true
});
```

さらに、ガイダンス用のブロック #startBlocks を追加します。

```javascript
var workspace = Blockly.inject('blocklyDiv', {
  toolbox: document.getElementById('toolbox'),
  trashcan: true
});

// 追加
window.setTimeout(function () {
  Blockly.Xml.domToWorkspace(document.querySelector('#startBlocks'), workspace);
}, 0);
```

### ソースコードを出力する

公式ドキュメント https://developers.google.com/blockly/guides/configure/web/code-generators を参考にして、ブロックが変化すると #code にソースコードを表示させます。

```javascript
function myUpdateFunction(event) {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  document.getElementById('code').innerHTML = '<pre>' + code + '</pre>';
}
workspace.addChangeListener(myUpdateFunction);
```

### 出力したソースコードを整形する

code-prettify https://github.com/google/code-prettify を使って出力したソースコードを整形します。

まず、CodePen の Settings > JavaScript で以下のライブラリーを呼び出します。

- https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js

次に、JavaScript を修正し、ソースコードの pre タグに class として prettyprint と lang-js を追加します。さらに PR.prettyPrint() で整形処理を実行します。

```javascript
function myUpdateFunction(event) {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  
  // 修正
  document.getElementById('code').innerHTML = '<pre class="prettyprint lang-js" style="margin: 0px"><span style="font-size:1.1em">' + code + '</span></pre>';

  //追加
  PR.prettyPrint();
}
```

###  Blockly を LocalStorage に保存する

作ったブロックを Web ブラウザーの LocalStorage に保存できるようにします。

残念ながら CodePen では公式ドキュメント https://developers.google.com/blockly/guides/configure/web/cloud-storage の方法が使えませんでした。そこで、https://cdn.jsdelivr.net/npm/blockly@1.0.0/appengine/storage.js を参考にしながら、以下のコードで実現しました。

```javascript
//追加
var KEY = 'BlocklyStorage';

function backupBlocks() {
  if (!'localStorage' in window) return;
  var xml = Blockly.Xml.workspaceToDom(workspace);
  var text = Blockly.Xml.domToText(xml);
  //console.log(text);
  window.localStorage.setItem(KEY, text);
}

function restoreBlocks() {
  var xml = Blockly.Xml.textToDom(window.localStorage[KEY]);
  Blockly.Xml.domToWorkspace(xml, workspace);
}
```

ブロックが変更するたびに保存させます。先ほど作った myUpdateFunction() の中で backupBlocks() を呼び出します。

```javascript
function myUpdateFunction(event) {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  document.getElementById('code').innerHTML = '<pre class="prettyprint lang-js" style="margin: 0px"><span style="font-size:1.1em">' + code + '</span></pre>';
  PR.prettyPrint();
  // 追加
  backupBlocks();
}
workspace.addChangeListener(myUpdateFunction);
```

Web ページを呼び出した後、workspace を作った後に、restoreBlocks() を呼び出します。

```javascript
var workspace = Blockly.inject('blocklyDiv', {
  toolbox: document.getElementById('toolbox'),
  trashcan: true
});

window.setTimeout(function () {
  //修正
  if ('localStorage' in window && window.localStorage[KEY]) {
    restoreBlocks();
  } else {
    Blockly.Xml.domToWorkspace(document.querySelector('#startBlocks'), workspace);
  }
}, 0);
```

### 出力したソースコードを実行する

https://developers.google.com/blockly/guides/app-integration/running-javascript を参考にして、無限ループにならないように工夫します。

```javascript
function runCode() {
  window.LoopTrap = 1000;
  Blockly.JavaScript.INFINITE_LOOP_TRAP = 'if(--window.LoopTrap == 0) throw "Infinite loop.";\n';
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  Blockly.JavaScript.INFINITE_LOOP_TRAP = null;
  try {
    eval(code);
  } catch (e) {
    alert('Bad code: ' + e);
  }
}
```

### 出来上がり

最終的にできた JavaScript のコードです。

```javascript
var KEY = 'BlocklyStorage';

var workspace = Blockly.inject('blocklyDiv', {
  toolbox: document.getElementById('toolbox'),
  trashcan: true
});

window.setTimeout(function () {
  if ('localStorage' in window && window.localStorage[KEY]) {
    restoreBlocks();
  } else {
    Blockly.Xml.domToWorkspace(document.querySelector('#startBlocks'), workspace);
  }
}, 0);

function myUpdateFunction(event) {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  document.getElementById('code').innerHTML = '<pre class="prettyprint lang-js" style="margin: 0px"><span style="font-size:1.1em">' + code + '</span></pre>';
  PR.prettyPrint();
  backupBlocks();
}
workspace.addChangeListener(myUpdateFunction);

function backupBlocks() {
  if (!'localStorage' in window) return;
  var xml = Blockly.Xml.workspaceToDom(workspace);
  var text = Blockly.Xml.domToText(xml);
  //console.log(text);
  window.localStorage.setItem(KEY, text);
}

function restoreBlocks() {
  var xml = Blockly.Xml.textToDom(window.localStorage[KEY]);
    Blockly.Xml.domToWorkspace(xml, workspace);
}

function runCode() {
  window.LoopTrap = 1000;
  Blockly.JavaScript.INFINITE_LOOP_TRAP = 'if(--window.LoopTrap == 0) throw "Infinite loop.";\n';
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  Blockly.JavaScript.INFINITE_LOOP_TRAP = null;
  try {
    eval(code);
  } catch (e) {
    alert('Bad code: ' + e);
  }
}
```