javascript編寫一個簡單的編譯器(理解抽象語法樹AST)
編譯器 是一種接收一段程式碼,然後把它轉成一些其他一種機制。
我們現在來做一個在一張紙上畫出一條線,那麼我們畫出一條線需要定義的條件如下:
使用 Paper定義紙的顏色,Pen定義筆的顏色,Line指畫出一條線,100指在顏色引數中代表100%的黑色 或 css中的rgb(0%,0%,0%). 那麼生成的線使用灰色來表示,那麼就是 50了,紙的面積是 100*100, 線條的寬度是1,線段的起點和終點是相對於左下角的x,y座標來定義。
Paper 0 (含義是: 定義紙的顏色是白色)
Pen 100 (含義是: 定義筆的顏色是黑色)
Line 0 50 100 50 (含義是:x軸0到100,說明是橫向從起點到終點,y軸是50到50,說明是一張紙的中點是一條直線)。
那麼編譯器是如何工作的?
編譯器一般會經過如下幾個步驟:
1. 詞法分析
2. 語法分析
3. 轉換
4. 程式碼生成
1-1 詞法分析(也可以叫做標記)
詞法分析將每個關鍵字(也可以叫標記)使用空格分開. 比如:
Paper 0
Pen 100
Line 0 50 100 50
如上,我們可以把 Paper, Pen,Line 的型別統一可以叫 word, 值就是各個單詞了 那麼 後面的數字型別我們可以統一叫 number;
比如我們輸入 "Paper 0", 那麼我們輸出的話就變成如下:
[ {type: "word", value: "Paper"}, {type: "number", value: "100"} ]
程式碼如下:
function lexical (code) { return code.split(/\s+/) .filter(function(t) { return t.length > 0 }).map(function(t) { console.log(t); return isNaN(t) ? {type: 'word', value: t} : {type: 'number', value: t} }); } var res = lexical("Paper 0"); console.log(res); // [{type: "word", value: "Paper"}, {type: "number", value: '100'}]
1-2 語法分析
語法分析是遍歷每個標記,尋找語法資訊,並且構建一個叫做AST(抽象語法樹)的物件。
下面我們對上面詞法分析生成的標記 [{type: "word", value: "Paper"}, {type: "number", value: '100'}] 這樣的資料,使用語法分析
構建一個AST(抽象語法樹)的物件。程式碼如下:
function parser(tokens) { var AST = { type: 'Drawing', body: [] }; // 迴圈依次取出第一個元素,然後刪除第一個元素 while (tokens.length > 0) { var currentItem = tokens.shift(); // 判斷型別,如果是單詞的話,我們就分析它的語法 if (currentItem.type === 'word') { switch(currentItem.value) { case 'Paper' : var expression = { type: 'CallExpression', name: 'Paper', arguments: [] }; // 繼續陣列中欄位的型別 var nextItem = tokens.shift(); if (nextItem.type === 'number') { // 在expression物件內部加入引數資訊 expression.arguments.push({ type: 'NumberLiteral', value: nextItem.value }) // 將expression物件放入我們的AST的body內 AST.body.push(expression); } else { throw 'Paper command must be followed by a number.' } break; case 'Pen' : /* 更多程式碼 */ break; case 'Line': /* 更多程式碼 */ break; } } } return AST; } var data = [ { type: 'word', value: 'Paper'}, { type: 'number', value: 100} ]; var output = parser(data); console.log(output); // 列印資訊如下 /* var output = { 'type': 'Drawing', 'body': [{ "type": "CallExpression", "name": "Paper", "arguments": [{ "type": "NumberLiteral", "value": "100" }] }] } */
1-3 轉換器函式
我們在語法分析上面通過詞法分析生成的物件後,在語法分析建立了一個AST(抽象語法樹)結構,但是上面的AST結構對我們建立SVG檔案沒有什麼用處,
在SVG中,我們可以使用元素(element)來表示一個Paper。那麼轉換器函式將AST轉換成另一種對SVG友好的AST。程式碼如下:
function transformer(ast) { var svg_ast = { tag: 'svg', attr: { width: 100, height: 100, viewBox: '0 0 100 100', xmlns: 'http://www.w3.org/2000/svg', version: '1.1' }, body: [] }; // 迴圈呼叫ast表示式 while (ast.body.length > 0) { // 依次取出陣列的第一個元素,然後在陣列中刪除該元素 var node = ast.body.shift(); switch (node.name) { case 'Paper' : var paper_color = 100 - node.arguments[0].value; // 在svg_ast的body內加入rect元素資訊 svg_ast.body.push({ tag: 'rest', attr: { x: 0, y: 0, width: 100, height: 100, fill: 'rgb(' + paper_color + '%,' + paper_color + '%,' + paper_color + '%)' } }) break; case 'Pen' : var pen_color = 100 - node.arguments[0].value; /* 很多程式碼 */ break; case 'Line' : /* 很多程式碼 */ break; } } return svg_ast; } var inputElem = { 'type': 'Drawing', 'body': [{ "type": "CallExpression", "name": "Paper", "arguments": [{ "type": "NumberLiteral", "value": "100" }] }] }; var output = transformer(inputElem); console.log(output); /* 列印資訊如下: var output = { "tag": "svg", "attr": { "width": 100, "height": 100, "viewBox": "0 0 100 100", "xmlns": "http://www.w3.org/2000/svg", "version": "1.1" }, "body": [{ "tag": "rect", "attr": { "x": 0, "y": 0, "width": 100, "height": 100, "fill": "rgb(0%, 0%, 0%)" } }] } */
1-4 生成器函式
作為編譯器的最後一步,生成器函式基於上一步產生的新AST來生成SVG程式碼。
程式碼如下:
function generator(svg_ast) { /* 從attr 物件中建立屬性字串 {"width": 100, "height": 100} => 'width="100" height="100"' */ function createAttrString(attr) { return Object.keys(attr).map(function(key) { return key + '="'+attr[key]+'"' }).join(' '); } // 為svg標籤建立屬性字串 var svg_attr = createAttrString(svg_ast.attr); // width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1" // console.log(svg_attr); // 為每個 svg_ast body中的元素,生成svg標籤 var elements = svg_ast.body.map(function(node) { return '<' + node.tag + ' ' + createAttrString(node.attr) + '></' + node.tag + '>' }).join('\n\t'); // 使用開和關的svg標籤包裝來完成svg程式碼 return '<svg '+ svg_attr +'>\n' + elements + '\n</svg>' } var svg_ast = { "tag": "svg", "attr": { "width": 100, "height": 100, "viewBox": "0 0 100 100", "xmlns": "http://www.w3.org/2000/svg", "version": "1.1" }, "body": [{ "tag": "rect", "attr": { "x": 0, "y": 0, "width": 100, "height": 100, "fill": "rgb(0%, 0%, 0%)" } }] } var g = generator(svg_ast); console.log(g); /* 列印輸出如下: <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1"> <rect x="0" y="0" width="100" height="100" fill="rgb(0%, 0%, 0%)"></rect> </svg> */
1-5 把上面的 lexical.js , grammar.js, converter.js 和 generator.js 組裝在一起,來作為一個編譯器。
我們把這個編譯器取個名字叫 svgCompile 編譯器吧,我們逐步理解了編譯器的步驟,先是建立 詞法分析器,然後建立 語法分析器,接著是 轉換器,最後就是生成器方法,現在我們需要新增一個 compile(編譯)方法來鏈式呼叫這四個方法。
function svgCompile(code) { return generator(transformer(parser(lexical(code)))); }
下面是compile.html程式碼如下:
<!DOCTYPE html> <html> <head> <title></title> <script src="./js/lexical.js"></script> <script src="./js/parser.js"></script> <script src="./js/transformer.js"></script> <script src="./js/generator.js"></script> <script src="./js/compile.js"></script> </head> <body> <script> // 呼叫svgCompile編譯器 var code = 'Paper 0 Pen 100 Line 0 50 100 50'; var svg = svgCompile(code); console.log(svg); document.body.innerHTML = svg; /* 列印資訊如下: <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1"> <rest x="0" y="0" width="100" height="100" fill="rgb(100%,100%,100%)"></rest> <line x1="0" y1="50" x2="100" y2="50" stroke-linecap="round" stroke="rgb(0%,0%,0%)"></line> </svg> */ </script> </body> </html>
下面就是實現使用svg 畫一條線的demo.
Tips: 通過網上的資料學習的,關鍵是想先來理解如何編寫一個簡單的編譯器,及簡單的理解AST(抽象語法樹)