Blockly Python Code Generator
前回(Blockly Menu作成(3))に続けて、googleのガイドを参照して作成していきます。
Google for Education > Blockly > Guides > Get Started
Code Generator
Pythonコードを作成することを目標とします。
まずは、googleのガイドを参照します。
Code Generators
それぞれの言語ごとにスクリプトが準備されています。
Pythonコードを作成するスクリプトは以下のファイルになります。
python_compressed.js
コードを作成する関数はworkspaceToCode()となります。
var code = Blockly.Python.workspaceToCode(workspace);
Tab表示
Blockの編集画面とPythonコードをTab切り替えで表示するため、少しHTMLとCSSは変更します。
まとめ
前回のファイルをベースに変更します。
styles.cssとtoolbox.xmlは前回(Blockly Menu作成(3))のものをそのまま流用します。
HTML
タブ表示に変更します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Blockly Sample</title>
<link rel="stylesheet" href="./styles.css">
<link rel="stylesheet" href="./code.css">
<script src="../blockly/blockly_compressed.js"></script>
<script src="../blockly/blocks_compressed.js"></script>
<script src="../blockly/msg/js/en.js"></script>
<script src="../blockly/python_compressed.js"></script>
<script src="./code.js"></script>
</head>
<body>
<table>
<tr>
<td>
<h1>
<p>Blockly Code Generator</p>
</h1>
</td>
</tr>
<tr>
<td>
<table>
<tr id="tabRow" height="1em">
<td id="tab_blocks" class="tabon">Blocks</td>
<td class="tabmin tab_collapse"> </td>
<td id="tab_python" class="taboff">Python</td>
<td class="tabmin tab_collapse"> </td>
<td id="tab_xml" class="taboff">XML</td>
<td class="tabmax">
<button id="trashButton" class="notext" title="...">
<img src='../blockly/media/1x1.gif' class="trash icon21">
</button>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="99%" colspan=2 id="content_area">
</td>
</tr>
</table>
<div id="blocklyDiv"></div>
<div id="content_blocks" class="content"></div>
<pre id="content_python" class="content prettyprint lang-py"></pre>
<textarea id="content_xml" class="content" wrap="off"></textarea>
</body>
</html>
スタイルシート
タブ表示用のスタイルシートを追加します。
/* Buttons */
button {
margin: 5px;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
font-size: large;
background-color: #eee;
color: #000;
}
button.primary {
border: 1px solid #dd4b39;
background-color: #dd4b39;
color: #fff;
}
button.primary>img {
opacity: 1;
}
button>img {
opacity: 0.6;
vertical-align: text-bottom;
}
button.disabled {
display: none;
}
button.notext {
font-size: 10%;
}
/* Tabs */
#tabRow>td {
border: 1px solid #ccc;
border-bottom: none;
}
td.tabon {
border-bottom-color: #ddd !important;
background-color: #ddd;
padding: 5px 19px;
white-space: nowrap;
}
td.taboff {
cursor: pointer;
padding: 5px 19px;
}
td.taboff:hover {
background-color: #eee;
}
td.tabmin {
border-top-style: none !important;
border-left-style: none !important;
border-right-style: none !important;
}
td.tabmax {
border-top-style: none !important;
border-left-style: none !important;
border-right-style: none !important;
width: 99%;
padding-left: 10px;
padding-right: 10px;
text-align: right;
}
table {
border-collapse: collapse;
margin: 0;
padding: 0;
border: none;
}
td {
padding: 0;
vertical-align: top;
}
.content {
visibility: hidden;
margin: 0;
padding: 1ex;
position: absolute;
direction: ltr;
}
pre.content {
border: 1px solid #ccc;
overflow: scroll;
}
#content_blocks {
padding: 0;
}
.blocklySvg {
border-top: none !important;
}
#content_xml {
resize: none;
outline: none;
border: 1px solid #ccc;
font-family: monospace;
overflow: scroll;
}
/* Buttons */
button {
padding: 1px 10px;
margin: 1px 5px;
}
/* Sprited icons. */
.icon21 {
height: 21px;
width: 21px;
background-image: url(../blockly/demos/code/icons.png);
}
.trash {
background-position: 0px 0px;
}
@media (max-width: 710px) {
.tab_collapse {
display: none;
}
}
JavaScript
タブ動作の機能とコード生成の機能を追加したJavaScriptファイルを作成します。
var Code = {};
/**
* Blockly's main workspace.
* @type {Blockly.WorkspaceSvg}
*/
Code.workspace = null;
Code.loadBlocks = function (defaultXml) {
try {
var loadOnce = window.sessionStorage.loadOnceBlocks;
} catch (e) {
// Firefox sometimes throws a SecurityError when accessing sessionStorage.
// Restarting Firefox fixes this, so it looks like a bug.
var loadOnce = null;
}
if ('BlocklyStorage' in window && window.location.hash.length > 1) {
// An href with #key trigers an AJAX call to retrieve saved blocks.
BlocklyStorage.retrieveXml(window.location.hash.substring(1));
} else if (loadOnce) {
// Language switching stores the blocks during the reload.
delete window.sessionStorage.loadOnceBlocks;
var xml = Blockly.Xml.textToDom(loadOnce);
Blockly.Xml.domToWorkspace(xml, Code.workspace);
} else if (defaultXml) {
// Load the editor with default starting blocks.
var xml = Blockly.Xml.textToDom(defaultXml);
Blockly.Xml.domToWorkspace(xml, Code.workspace);
} else if ('BlocklyStorage' in window) {
// Restore saved blocks in a separate thread so that subsequent
// initialization is not affected from a failed load.
window.setTimeout(BlocklyStorage.restoreBlocks, 0);
}
};
Code.bindClick = function (el, func) {
if (typeof el == 'string') {
el = document.getElementById(el);
}
el.addEventListener('click', func, true);
el.addEventListener('touchend', func, true);
};
Code.getBBox_ = function (element) {
var height = element.offsetHeight;
var width = element.offsetWidth;
var x = 0;
var y = 0;
do {
x += element.offsetLeft;
y += element.offsetTop;
element = element.offsetParent;
} while (element);
return {
height: height,
width: width,
x: x,
y: y
};
};
Code.TABS_ = ['blocks', 'python', 'xml'];
Code.TABS_DISPLAY_ = [
'Blocks', 'Python', 'XML',
];
Code.selected = 'blocks';
Code.tabClick = function (clickedName) {
// If the XML tab was open, save and render the content.
if (document.getElementById('tab_xml').classList.contains('tabon')) {
var xmlTextarea = document.getElementById('content_xml');
var xmlText = xmlTextarea.value;
var xmlDom = null;
try {
xmlDom = Blockly.Xml.textToDom(xmlText);
} catch (e) {
var badXml = "XML のエラーです:\n%1\n\nXML の変更をやめるには「OK」、編集を続けるには「キャンセル」を選んでください。"
var q =
window.confirm(badXml.replace('%1', e));
if (!q) {
// Leave the user on the XML tab.
return;
}
}
if (xmlDom) {
Code.workspace.clear();
Blockly.Xml.domToWorkspace(xmlDom, Code.workspace);
}
}
// Deselect all tabs and hide all panes.
for (var i = 0; i < Code.TABS_.length; i++) {
var name = Code.TABS_[i];
var tab = document.getElementById('tab_' + name);
tab.classList.add('taboff');
tab.classList.remove('tabon');
document.getElementById('content_' + name).style.visibility = 'hidden';
}
// Select the active tab.
Code.selected = clickedName;
var selectedTab = document.getElementById('tab_' + clickedName);
selectedTab.classList.remove('taboff');
selectedTab.classList.add('tabon');
// Show the selected pane.
document.getElementById('content_' + clickedName).style.visibility =
'visible';
Code.renderContent();
Blockly.svgResize(Code.workspace);
};
Code.renderContent = function () {
var content = document.getElementById('content_' + Code.selected);
// Initialize the pane.
if (content.id == 'content_xml') {
var xmlTextarea = document.getElementById('content_xml');
var xmlDom = Blockly.Xml.workspaceToDom(Code.workspace);
var xmlText = Blockly.Xml.domToPrettyText(xmlDom);
xmlTextarea.value = xmlText;
xmlTextarea.focus();
} else if (content.id == 'content_python') {
Code.attemptCodeGeneration(Blockly.Python);
}
};
/**
* Attempt to generate the code and display it in the UI, pretty printed.
* @param generator {!Blockly.Generator} The generator to use.
*/
Code.attemptCodeGeneration = function (generator) {
var content = document.getElementById('content_' + Code.selected);
content.textContent = '';
if (Code.checkAllGeneratorFunctionsDefined(generator)) {
var code = generator.workspaceToCode(Code.workspace);
content.textContent = code;
}
};
/**
* Check whether all blocks in use have generator functions.
* @param generator {!Blockly.Generator} The generator to use.
*/
Code.checkAllGeneratorFunctionsDefined = function (generator) {
var blocks = Code.workspace.getAllBlocks(false);
var missingBlockGenerators = [];
for (var i = 0; i < blocks.length; i++) {
var blockType = blocks[i].type;
if (!generator[blockType]) {
if (missingBlockGenerators.indexOf(blockType) == -1) {
missingBlockGenerators.push(blockType);
}
}
}
var valid = missingBlockGenerators.length == 0;
if (!valid) {
var msg = 'The generator code for the following blocks not specified for ' +
generator.name_ + ':\n - ' + missingBlockGenerators.join('\n - ');
Blockly.alert(msg); // Assuming synchronous. No callback.
}
return valid;
};
Code.init = function () {
var container = document.getElementById('content_area');
var onresize = function (e) {
var bBox = Code.getBBox_(container);
for (var i = 0; i < Code.TABS_.length; i++) {
var el = document.getElementById('content_' + Code.TABS_[i]);
el.style.top = bBox.y + 'px';
el.style.left = bBox.x + 'px';
// Height and width need to be set, read back, then set again to
// compensate for scrollbars.
el.style.height = bBox.height + 'px';
el.style.height = (2 * bBox.height - el.offsetHeight) + 'px';
el.style.width = bBox.width + 'px';
el.style.width = (2 * bBox.width - el.offsetWidth) + 'px';
}
// Make the 'Blocks' tab line up with the toolbox.
if (Code.workspace && Code.workspace.getToolbox().width) {
document.getElementById('tab_blocks').style.minWidth =
(Code.workspace.getToolbox().width - 38) + 'px';
// Account for the 19 pixel margin and on each side.
}
};
window.addEventListener('resize', onresize, false);
var toolbox = document.getElementById("toolbox");
var options = {
toolbox: toolbox,
// scrollbars: true,
grid:
{
spacing: 25,
length: 3,
colour: '#ccc',
snap: true
},
media: '../Blockly/media/',
zoom:
{
controls: true,
wheel: true
}
};
Code.workspace = Blockly.inject('content_blocks', options);
Code.loadBlocks('');
Code.tabClick(Code.selected);
Code.bindClick('trashButton',
function () { Code.discard(); Code.renderContent(); });
for (var i = 0; i < Code.TABS_.length; i++) {
var name = Code.TABS_[i];
Code.bindClick('tab_' + name,
function (name_) { return function () { Code.tabClick(name_); }; }(name));
}
onresize();
Blockly.svgResize(Code.workspace);
}
/**
* Discard all blocks from the workspace.
*/
Code.discard = function () {
var count = Code.workspace.getAllBlocks(false).length;
if (count < 2 ||
window.confirm(Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', count))) {
Code.workspace.clear();
if (window.location.hash) {
window.location.hash = '';
}
}
};
Promise.all(
["toolbox.xml"].map(async file => {
return fetch(file).then(
(res) => {
return res.text();
}
);
})
).then((xmls) => {
xmls.forEach((xml) => {
var parser = new DOMParser();
var doc = parser.parseFromString(xml, "application/xml");
document.body.appendChild(doc.documentElement);
});
}).then(() => {
Code.init();
});
googleのサンプルコードからPythonコードを作成する分だけ抜き出してできる限りコードを減らしました。
もう少し減らせそうな気はしますが、目的は達成できたので、今回はこれで良しとします。