ここに構文が入ります。上記の EBNF の構文の文字列を EBNFParser に渡し、EBNFParser 自身のパーサーのコードを以下の通り生成しました。 字下げは Aptana Studio 等の IDE で簡単にできるのでジェネレーターの中では行っていません。
ここに結果が入ります。
JsDoc Toolkit で作成したドキュメントはこちら。
今回、一つのクラス EBNFParser を自動生成した部分 parser02.js と 手入力している部分 generator02.js に 分割したのですが、JsDoc Toolkit では EBNFParser クラスが認識されなくなりました。プログラムとしては 動作するのですが、ドキュメントに難ありなので、次回から自動生成部分と手入力部分を一つにまとめるか、手入力している 部分を別のクラスにするか、そのような対策をしたいと思います。
[註] クラス EBNFParser のドキュメントができなかったのは、ファイルを分割したためではなく、@class の指定が されていなかったと判明しました。上記の記述には誤りがあります。
<head> <meta charset="UTF-8"> <title>JavaScriptでプログラミング - Generator 0.2</title> <link type="text/css" rel="stylesheet" href="lib/jasmine-1.3.1/jasmine.css"> <link type="text/css" rel="stylesheet" href="css/spec.css"> <link type="text/css" rel="stylesheet" href="css/shCoreEclipse.css"> <link type="text/css" rel="stylesheet" href="css/shThemeEclipse.css"> <script type="text/javascript" src="js/XRegExp.js"></script> <script type="text/javascript" src="js/shCore.js"></script> <script type="text/javascript" src="js/shBrushJScript.js"></script> <script type="text/javascript" src="js/shBrushXml.js"></script> <script type="text/javascript" src="js/shBrushCss.js"></script> <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script> <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script> <!-- include source files here... --> <script type="text/javascript" src="js/generator02/source01.js"></script> <script type="text/javascript" src="js/generator02/lex03.js"></script> <script type="text/javascript" src="js/generator02/parse02.js"></script> <script type="text/javascript" src="js/generator02/generator02.js"></script> <!-- include spec files here... --> <script type="text/javascript" src="js/generator02/sourcespec01.js"></script> <script type="text/javascript" src="js/generator02/lexspec03.js"></script> <script type="text/javascript" src="js/generator02/generatorspec02.js"></script>後半は Jasmine によるテスト結果を HTML 上で展開するレポーターと呼ばれる部分です。前回から変更ありません。
<script type="text/javascript">
SyntaxHighlighter.all();
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
</script>
クラス EBNFParser のソースコードです。パーサーを生成するためのツールです。
以下の部分では HTML にある EBNF の構文から EBNFParser クラスの一番上位のメソッド syntax() を呼び出してパーサーを生成し HTML に格納しています。今回は空白の読み飛ばしが不要な id(非終端記号) を gapFree という引数で EBNFParser のコンストラクタに渡しています。
window.onload = function() {
// define EBNF syntax
var syntax = document.querySelector('#syntax');
var gapFree = ["idl", "id", "terminal"];
var ebnfSyntax = "syntax = rule, {rule}.\n";
ebnfSyntax += "rule = idl, '=', alternative, '.'.\n";
ebnfSyntax += "alternative = sequence, {'|', sequence}.\n";
ebnfSyntax += "sequence = term, {',', term}.\n";
ebnfSyntax += "term = primary.\n";
ebnfSyntax += "primary = terminal | id | repeat | option | group.\n";
ebnfSyntax += "terminal = sq, character, {character}, sq.\n";
ebnfSyntax += "idl = letter, {letter | digit}.\n";
ebnfSyntax += "id = letter, {letter | digit}.\n";
ebnfSyntax += "option = '[', alternative, ']'.\n";
ebnfSyntax += "repeat = '{', alternative, '}'.\n";
ebnfSyntax += "group = '(', alternative, ')'.\n";
ebnfSyntax += "letter = lower | upper.\n";
ebnfSyntax += "character = symbol | letter | digit.\n";
ebnfSyntax += "symbol = '=' | ',' | '.' | '|' |\n";
ebnfSyntax += " '[' | ']' | '{' | '}' | '(' | ')'.\n";
syntax.innerHTML = ebnfSyntax;
var ebnf = new EBNFParser(ebnfSyntax, gapFree, "EBNF", "0.2");
// get element from HTML and set text (code)
var result = document.querySelector('#result');
result.innerHTML = ebnf.syntax();
};
以下コードはパーサーで解析した結果からコードを生成する部分です。ランナーと命名しました。
この部分を変更することでジェネレーター、インタープリターなどさまざまな出力を行うことができます。
今回は空白を読み飛ばす処理と、大文字と小文字を区別するかしないかを変更する処理を追加しました。
/**
* Parser Runner (Generator) for EBNF Syntax
* @param {String} n number of match
* @param {Object} match array of parsed result
* @param {String} parser caller method name
* @return {String} generated code
* @since 0.2
*/
EBNFParser.prototype.run = function(n, match, parser) {
var code = "";
if (parser == "syntax") {
code += "/**\n";
code += " * @fileOverview " + this.name + "Parser - " + this.name + " Parser Object\n";
code += " * @version " + this.ver + "\n";
code += " * @author EBNFParser written by Nonki Takahashi\n";
code += " */\n";
code += "\n";
code += "/**\n";
code += " * " + this.name + " Parser Generator Object\n";
code += " * @this {" + this.name + "Parser}\n";
code += " * @param {String} syntax syntax object for generate parser\n";
code += " * @param {Object} gapFree array of gap free id\n";
code += " * @param {String} name name of the parser\n";
code += " * @param {String} ver version of the parser\n";
code += " * @param {Boolean} ignoreCase true if ignore case (optional)\n";
code += " * @property {String} buf syntax buffer\n";
code += " * @property {Integer} ptr syntax buffer pointer\n";
code += " * @property {Object} gapFree array of gap free id\n";
code += " * @property {String} name name of the parser\n";
code += " * @property {String} ver version of the parser\n";
code += " * @property {Boolean} ignoreCase true if ignore case\n";
code += " * @since " + this.ver + "\n";
code += " */\n";
code += this.name + "Parser = function(syntax, gapFree, name, ver, ignoreCase) {\n";
code += "this.gapFree = gapFree;\n";
code += "this.name = name;\n";
code += "this.ver = ver;\n";
code += "if (ignoreCase)\n";
code += "this.ignoreCase = true;\n";
code += "else\n";
code += "this.ignoreCase = false;\n";
code += "// inherit the methods of class Lex\n";
code += "Lex.call(this, syntax);\n";
code += "};\n";
code += this.name + "Parser.prototype = new Lex();\n";
code += "\n";
for (var i = 0; i <= n; i++) {
if (match[i] != true && match[i] != null)
code += match[i];
}
} else if (parser == "rule") {
code += "/**\n";
var ptr = this.buf.lastIndexOf(match[0], this.ptr);
code += " * " + this.buf.slice(ptr, this.ptr) + "\n";
code += " * @returns {String} code generated if matched, null if not matched\n";
code += " * @since " + this.ver + "\n";
code += " */\n";
code += this.name + "Parser.prototype." + match[0] + " = function() {\n";
code += "var match = [];\n";
code += "var save = this.ptr;\n";
code += "var n = -1;\n";
var inGapFree = false;
for (var i in this.gapFree) {
if (this.gapFree[i] == match[0]) {
code += "this.sp();\n";
inGapFree = true;
break;
}
}
if (inGapFree)
code += match[2].replace(/this\.sp\(\);\n/g, "");
else
code += match[2];
code += "if (!match[n]) {\n";
code += "this.ptr = save;\n";
code += "return null;\n";
code += "}\n";
code += "return this.run(n, match, \"" + match[0] + "\");\n";
code += "};\n\n";
} else if (parser == "alternative") {
var i = 0;
code += match[i];
for ( i = 2; i <= n; i += 2) {
if (match[i] != true && match[i] != null) {
code += "if (!match[n]) {\n";
code += "n--;\n";
code += match[i];
code += "}\n";
}
}
} else if (parser == "sequence") {
var i = 0;
code += "this.sp();\n";
code += match[i];
for ( i = 2; i <= n; i += 2) {
if (match[i] != true && match[i] != null) {
code += "if (match[n]) {\n";
code += "this.sp();\n";
code += match[i];
code += "}\n";
}
}
} else if (parser == "term" || parser == "primary") {
code += match[0];
} else if (parser == "terminal") {
code += "match[++n] = this.text(";
for (var i = 0; i <= n; i++) {
if (match[i] != true && match[i] != null)
code += match[i];
}
code += ");\n";
} else if (parser == "idl") {
for (var i = 0; i <= n; i++) {
if (match[i] != true && match[i] != null)
code += match[i];
}
} else if (parser == "id") {
code += "match[++n] = this.";
for (var i = 0; i <= n; i++) {
if (match[i] != true && match[i] != null)
code += match[i];
}
code += "();\n";
} else if (parser == "option") {
code += match[1];
code += "match[++n] = true;\n";
} else if (parser == "repeat") {
code += "while(match[n]) {\n";
code += match[1];
code += "}\n";
code += "match[++n] = true;\n";
} else if (parser == "group") {
code += match[1];
} else if (parser == "letter" || parser == "character" || parser == "symbol") {
code += match[0];
}
return code;
};
詳細は、リンク先をご覧下さい。
describe("Source 仕様 0.1", function() {
var exp = "1+2=";
var src;
beforeEach(function() {
src = new Source(exp); // Lexer and Parser Source
});
it("eod() は false を返す", function() {
expect(src.eod()).toBeFalsy();
});
describe("先頭でpush()しptr++したとき、", function() {
beforeEach(function() {
src.rewind();
src.push();
src.ptr++;
});
it("pop(true)後、ptrは 0 を返す", function() {
var p = src.ptr;
src.pop(true);
expect(src.ptr).toEqual(0);
});
it("pop(false)後、ptrは 1 を返す", function() {
var p = src.ptr;
src.pop(false);
expect(src.ptr).toEqual(1);
});
});
});
describe("Lex 仕様 0.3", function() {
var exp = "1+2=";
var la;
beforeEach(function() {
la = new Lex(exp); // Lexical Analiser
});
it("eod() は false を返す", function() {
expect(la.eod()).toBeFalsy();
});
it("digit() は '1' を返す", function() {
expect(la.digit()).toEqual('1');
});
it("upper() は null を返す", function() {
expect(la.upper()).toBe(null);
});
it("lower() は null を返す", function() {
expect(la.lower()).toBe(null);
});
it("sp() は null を返す", function() {
expect(la.sp()).toBeFalsy();
});
it("sq() は null を返す", function() {
expect(la.sq()).toBeFalsy();
});
it("ch('1') は '1' を返す", function() {
expect(la.ch('1')).toEqual('1');
});
it("text('1') は '1' を返す", function() {
expect(la.text('1')).toBe('1');
});
it("text('1+') は '1+' を返す", function() {
expect(la.text('1+')).toBe('1+');
});
it("text('2') は null を返す", function() {
expect(la.text('2')).toBe(null);
});
describe("3文字読んだ時点で、", function() {
beforeEach(function() {
la.rewind();
la.digit();
la.ch('+');
la.digit();
});
it("eod() は false を返す", function() {
expect(la.eod()).toBe(false);
});
it("digit() は null を返す", function() {
expect(la.digit()).toBe(null);
});
it("upper() は null を返す", function() {
expect(la.upper()).toBe(null);
});
it("lower() は null を返す", function() {
expect(la.lower()).toEqual(null);
});
it("ch('=') は '=' を返す", function() {
expect(la.ch('=')).toBe('=');
});
});
describe("4文字読んだ時点で、", function() {
beforeEach(function() {
la.rewind();
la.digit();
la.ch('+');
la.digit();
la.ch('=');
});
it("eod() は true を返す", function() {
expect(la.eod()).toBeTruthy();
});
});
});
describe("Generator 仕様 0.2", function() {
var g = new EBNFParser("syntax = {line, nl}.", [], "Test", "0.2");
var code = "/**\n";
code += " * @fileOverview TestParser - Test Parser Object\n";
code += " * @version 0.2\n";
code += " * @author EBNFParser written by Nonki Takahashi\n";
code += " */\n";
code += "\n";
code += "/**\n";
code += " * Test Parser Generator Object\n";
code += " * @this {TestParser}\n";
code += " * @param {String} syntax syntax object for generate parser\n";
code += " * @param {Object} gapFree array of gap free id\n";
code += " * @param {String} name name of the parser\n";
code += " * @param {String} ver version of the parser\n";
code += " * @param {Boolean} ignoreCase true if ignore case (optional)\n";
code += " * @property {String} buf syntax buffer\n";
code += " * @property {Integer} ptr syntax buffer pointer\n";
code += " * @property {Object} gapFree array of gap free id\n";
code += " * @property {String} name name of the parser\n";
code += " * @property {String} ver version of the parser\n";
code += " * @property {Boolean} ignoreCase true if ignore case\n";
code += " * @since 0.2\n";
code += " */\n";
code += "TestParser = function(syntax, gapFree, name, ver, ignoreCase) {\n";
code += "this.gapFree = gapFree;\n";
code += "this.name = name;\n";
code += "this.ver = ver;\n";
code += "if (ignoreCase)\n";
code += "this.ignoreCase = true;\n";
code += "else\n";
code += "this.ignoreCase = false;\n";
code += "// inherit the methods of class Lex\n";
code += "Lex.call(this, syntax);\n";
code += "};\n";
code += "TestParser.prototype = new Lex();\n";
code += "\n";
code += "/**\n";
code += " * syntax = {line, nl}.\n";
code += " * @returns {String} code generated if matched, null if not matched\n";
code += " * @since 0.2\n";
code += " */\n";
code += "TestParser.prototype.syntax = function() {\n";
code += "var match = [];\n";
code += "var save = this.ptr;\n";
code += "var n = -1;\n";
code += "this.sp();\n";
code += "while(match[n]) {\n";
code += "this.sp();\n";
code += "match[++n] = this.line();\n";
code += "if (match[n]) {\n";
code += "this.sp();\n";
code += "match[++n] = this.nl();\n";
code += "}\n";
code += "}\n";
code += "match[++n] = true;\n";
code += "if (!match[n]) {\n";
code += "this.ptr = save;\n";
code += "return null;\n";
code += "}\n";
code += "return this.run(n, match, \"syntax\");\n";
code += "};\n\n";
it("syntax() は " + code + " を返す", function() {
expect(g.syntax()).toEqual(code);
});
});