関数型のテンプレートエンジンを作ってみる(0.2.2) - パーサを書く
前回の続きで、前回作ったトークナイザからパーサを書く。パーサの作成には引き続きJParsecのライブラリを使用する。
おさらい&今日のミッション
前回作ったnewTokenizer(Terminals)で生成するトークナイザ(字句解析部)は、Stringを受け取りList
今回作るパーサ(構文解析部)はList
Terminals tags; // タグ用のTerminalsが入っているとする。 /*一般的な形*/ public Parser<X> newParser() { Parser<List<Token>> tokenizer = Tokenizers.newTokenizer(tags); Parser<X> parser = ...//今回ここの処理を書く return parser.from(tokenizer); // 合わせて最終的なパーサ }
テンプレートエンジンのパーサとしては、テンプレート(仮にSimpleTemplate型)を生成するParser
public interface SimpleTemplatePrototype { public SimpleTemplate instantiate(Context<Object, SimpleTemplate> lexicalContext); }
Terminals tags; // タグ用のTerminalsが入っているとする。 /* StringからSimpleTemplatePrototypeを生成するパーサを生成する */ public Parser<SimpleTemplatePrototype> newParser() { Parser<List<Token>> tokenizer = Tokenizers.newTokenizer(tags); Parser<SimpleTemplatePrototype> parser = ...//今回ここの処理を書く return parser.from(tokenizer); // 合わせて最終的なパーサ }
以下、パーサを組み立てるのに必要な要素を実装して行く。要素1〜要素5はTemplateParsersというクラスに書く。
要素1 リテラル文字列をパースする部分
前回のliteralTokenizerが吐き出すトークンを受け取る部分になる。
public static final String LITERAL_NAME = "TEXT"; public static Parser<SimpleTemplatePrototype> makeLiteralParser() { return Terminals.fragment(LITERAL_NAME).map( new Map<String, SimpleTemplatePrototype>() { public SimpleTemplatePrototype map(String source) { return new SimpleLiteralPrototype(source); } } ); }
literalTokenizerはLITERAL_NAMEでタグ付けされたFragmentを戻すようにしているので、その内容を受け取るにはTerminals.fragment(LITERAL_NAME)でParserを作り文字列を受け取る。受け取った文字列を元にSimpleLiteralPrototypeを生成している。
SimpleLiteralPrototypeはSimpleTemplatePrototypeの実装クラスで、instantiate()するとSimpleLiteralというTemplateクラスを生成する(中身は後で実装する。) SimpleLiteralは前々回作ったものと同じで良いだろう。
要素2 タグの属性部分をパースする部分
次にタグをパースするParserを作りたいが、その前にタグ内の属性部分をパースするParserを書く。
まず、「name=label」のパターン。両方がTerminals.Identifierなので、Terminals.Identifier.Parserを使う。
public static final Terminals operators = Terminals.operators("/", "="); ... private static Parser<Attribute> makeAttributeParser() { Parser<Attribute> withSymbol = Parsers.sequence( Terminals.Identifier.PARSER, operators.token("="), Terminals.Identifier.PARSER, new Map3<String, Token, String, Attribute>() { public Attribute map(String name, Token op, String label) { return new Attribute(name, new SymbolValue(label)); } });
「
なお、operatorsはtokenizerを作るのに使ったものと同一である必要がある。
次は「name="expression"」のパターン。ダブルクォートの部分にはトークナイザ側でTerminals.StringLiteral.DOUBLE_QUOTE_TOKENIZERを使ったので、パースするときはTerminals.StringLiteral.PARSERで受ける。
Parser<Attribute> withExpression = Parsers.sequence( Terminals.Identifier.PARSER, operators.token("="), Terminals.StringLiteral.PARSER, new Map3<String, Token, String, Attribute>() { public Attribute map(String name, Token op, String expression) { return new Attribute(name, new Expression(expression)); } });
二つまとめて属性部分のパーサを組み立てる。
return Parsers.or(withExpression, withSymbol);
}
要素3 シングルタグをパースする部分(defineの場合の例)
シングルタグは「{define name=hoge value="fuga"/}」の用に「{」+タグ名で始まり「/」+「}」で終わっている。
defineタグなら以下のようになる。
public static final Terminals startBrace = Terminals.operators("{"); public static final Terminals endBrace = Terminals.operators("}"); public static final Terminals operators = Terminals.operators("/", "="); public static final Terminals tags; // "insert", "define"を含むTerminalsが入っているとする ... Parser<SimpleTemplatePrototype> defineSingleTagParser = // 「{」で始まり startBrace.token("{").next( Parsers.sequence( // defineの後、属性が0個以上 tags.token("define"), makeAttributeParser().many(), new Map2<Token, List<Attribute>, SimpleTemplatePrototype>() { // SimpleTemplatePrototypeを生成する public SimpleTemplatePrototype map(Token tagToken, List<Attribute> attributes) { String tagName = tagToken.toString(); // insertのはず // name属性を探す Attribute nameAttribute = Attribute.findAttribute("name", attributes); // value属性を探す Attribute valueAttribute = Attribute.findAttribute("value", attributes); // value属性の値がSymbolかExpressionかで final SimpleSource valueSource; if (valueAttribute.getValue().isSymbol()) { Symbol symbol = valueAttribute.getValue().getSymbol(); valueSource = new SimpleReference(symbol); } else { final String expression = valueAttribute.getValue().getExpression(); valueSource = new SimpleExpression(expression); } // SimpleDefineProcessorPrototypeを生成して戻す。 return new SimpleDefineProcessorPrototype( nameAttribute.getValue().getSymbol(), valueSource); } } ) // 「/」+「}」で終わる ).followedBy(operators.token("/").next(endBrace.token("}")));
SimpleDefineProcessorPrototypeはSimpleTemplatePrototypeの実装クラスで、instantiate()するとSimpleDefineProcessorというTemplateクラスを生成する(中身は後で実装する。)
startBrace、endBrace、operators、tagsは、やはりtokenizerを作るのに使ったものと同一である必要がある。
SimpleReference、SimpleExpressionはSimpleSourceの実装クラスで、テンプレート評価時に固定値を戻したり、コンテキストから変数を取り出したりする。(実装内容については後日エンジン部分実装時に説明)
public interface SimpleSource { SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context); }
要素4 コンテナタグをパースする部分(defineの場合の例)
コンテナタグは「{define name=hoge}Hogeです{/define}」の用に「{」+タグ名で始まり「}」で終わるタグ開始部と、タグの中身と、「{/」+タグ名で始まり「}」で終わるタグ終了部で構成される。
defineタグなら以下のようになる。
Parser<List<SimpleTemplatePrototype>> contentParser; // タグの中身をパースするParser ... Parser<SimpleTemplatePrototype> defineContainerTagParser = Parsers.sequence( // 開始部: "{" "define" <attribute>* "}" startBrace.token("{").next( Parsers.sequence( tags.token("define"), makeAttributeParser().many(), new Map2<Token, List<Attribute>, List<Attribute>>() { public List<Attribute> map(Token tagToken, List<Attribute> attrs) { return attrs; } } ) ).followedBy(endBrace.token("}")), // タグの中身 contentParser, // タグ終了部 "{" "/" "define" "}" startBrace.token("{").next(operators.token("/")).next( tags.token("define").map(new Map<Token, Void>() { // 終了部はマッチングにだけ使用(戻り値はVoid) public Void map(Token token) { return null; } }) ).followedBy(endBrace.token("}")), new Map3<List<Attribute>, List<SimpleTemplatePrototype>, Void, SimpleTemplatePrototype>() { // タグ開始部から属性のリストを受け取り、contentParserからはタグの中身を受け取る。 public SimpleTemplatePrototype map( List<Attribute> attributes, final List<SimpleTemplatePrototype> contents, Void d) { // name属性を探す Attribute nameAttribute = Attribute.findAttribute("name", attributes); // SimpleDefineProcessorPrototypeを生成して戻す。 return new SimpleDefineProcessorPrototype( nameAttribute.getValue().getSymbol(), new SimpleContainerPrototype(contents)); } });
SimpleContainerPrototypeはSimpleTemplatePrototypeおよびSimpleSourceの実装クラスで、instantiate()するとSimpleContainerというTemplateクラスを生成する(中身は後で実装する。)
contentParserはタグの中身をパースするParserだが、タグの中にタグを書く事も出来るようにする為には、結局テンプレート全体をパースできるParserが必要になる。Jparsecでは、Parser.newReference()で参照を作り、後で中身をセットする事で、再帰構造を実現する。
要素3+4 タグをパースする部分を汎用にする
上記の例はdefineタグ用だったけど、タグ名の部分以外は基本的にどのタグも同じようなコードになる。
そこで、TagHandlerというインタフェースを導入して、後付けでタグを増やせるようにする。
まず、TagHandlerの定義
package sample.hotplate.sample.parser; import java.util.List; import sample.hotplate.sample.prototype.SimpleTemplatePrototype; /** * パース時にタグを取り扱う為のインタフェース */ public interface TagHandler { /** @return このハンドラが扱うタグ名の一覧を戻す。 */ String[] tagNames(); /** * @return シングルタグを受け取る場合true、受け取らない場合はfalseを戻す。 * @param tagName タグ名 */ boolean requireSingleTag(String tagName); /** * @return コンテナタグを受け取る場合true、受け取らない場合はfalseを戻す。 * @param tagName タグ名 */ boolean requireContainerTag(String tagName); /** * @return シングルタグの定義を受け取ってタグ用のSimpleTemplatePrototypeを戻す。 * @param tagName タグ名 * @param attributes 属性のリスト */ SimpleTemplatePrototype handleSingleTag(String tagName, List<Attribute> attributes); /** * @return コンテナタグの定義を受け取ってタグ用のSimpleTemplatePrototypeを戻す。 * @param tagName タグ名 * @param attributes 属性のリスト * @param elements タグの中身を含むSimpleTemplatePrototypeのList */ SimpleTemplatePrototype handleContainerTag( String tagName, List<Attribute> attributes, List<SimpleTemplatePrototype> elements); }
これを使って、シングルタグとコンテナタグのパーサの生成部分を書き直す。
// シングルタグパーサを生成する。 public static Parser<SimpleTemplatePrototype> makeSingleTagParser( Terminals tags, String tagName, final TagHandler handler) { return startBrace.token("{").next( Parsers.sequence( tags.token(tagName), makeAttributeParser().many(), new Map2<Token, List<Attribute>, SimpleTemplatePrototype>() { public SimpleTemplatePrototype map(Token tagToken, List<Attribute> attributes) { String tagName = tagToken.toString(); return handler.handleSingleTag(tagName, attributes); } } ) ).followedBy(operators.token("/").next(endBrace.token("}"))); } // コンテナタグパーサを生成する。タグ開始部 + 中身 + タグ終了部 public static Parser<SimpleTemplatePrototype> makeContainerTagParser( Terminals tags, Parser<List<SimpleTemplatePrototype>> contentParser, final String tagName, final TagHandler handler) { return Parsers.sequence( startTagParser(tags, tagName), contentParser, endTagParser(tags, tagName), new Map3<List<Attribute>, List<SimpleTemplatePrototype>, Void, SimpleTemplatePrototype>() { public SimpleTemplatePrototype map( List<Attribute> attributes, final List<SimpleTemplatePrototype> contents, Void d) { return handler.handleContainerTag(tagName, attributes, contents); } }); } private static Parser<List<Attribute>> startTagParser(Terminals tags, String tagName) { return startBrace.token("{").next( Parsers.sequence( tags.token(tagName), makeAttributeParser().many(), new Map2<Token, List<Attribute>, List<Attribute>>() { public List<Attribute> map(Token tagToken, List<Attribute> attrs) { return attrs; } } ) ).followedBy(endBrace.token("}")); } private static Parser<Void> endTagParser(Terminals tags, String tagName) { return startBrace.token("{").next(operators.token("/")).next( tags.token(tagName).map(new Map<Token, Void>() { public Void map(Token token) { return null; } }) ).followedBy(endBrace.token("}")); }
define用のTagHandlerの実装はは次のようになる。
package sample.hotplate.sample.parser.handler; import java.util.List; import sample.hotplate.sample.parser.Attribute; import sample.hotplate.sample.parser.TagHandler; import sample.hotplate.sample.processor.prototype.SimpleDefineProcessorPrototype; import sample.hotplate.sample.prototype.SimpleContainerPrototype; import sample.hotplate.sample.prototype.SimpleTemplatePrototype; public class DefineTagHandler implements TagHandler { @Override public String[] tagNames() { return new String[] {"define"}; } @Override public boolean requireSingleTag(String tagName) { return true; // シングルタグも可能 } @Override public boolean requireContainerTag(String tagName) { return true; // コンテナタグも可能 } // シングルタグを生成 @Override public SimpleTemplatePrototype handleSingleTag(String tagName, List<Attribute> attributes) { Attribute nameAttribute = Attribute.findAttribute("name", attributes); Attribute valueAttribute = Attribute.findAttribute("value", attributes); return new SimpleDefineProcessorPrototype( nameAttribute.getValue().getSymbol(), TagHandlerUtils.valueSource(valueAttribute)); } // コンテナタグを生成 @Override public SimpleTemplatePrototype handleContainerTag(String tagName, List<Attribute> attributes, List<SimpleTemplatePrototype> elements) { Attribute nameAttribute = Attribute.findAttribute("name", attributes); return new SimpleDefineProcessorPrototype( nameAttribute.getValue().getSymbol(), new SimpleContainerPrototype(elements)); } }
SimpleSourceを作り出す所はUtils化しておく。
package sample.hotplate.sample.parser.handler; import sample.hotplate.core.Symbol; import sample.hotplate.sample.parser.Attribute; import sample.hotplate.sample.source.SimpleExpression; import sample.hotplate.sample.source.SimpleReference; import sample.hotplate.sample.source.SimpleSource; public final class TagHandlerUtils { public static SimpleSource makeSource(Attribute attribute) { final SimpleSource source; if (attribute.getValue().isSymbol()) { Symbol symbol = attribute.getValue().getSymbol(); source = new SimpleReference(symbol); } else { final String expression = attribute.getValue().getExpression(); source = new SimpleExpression(expression); } return source; } }
ついでにinsert用のTagHandlerも実装。SimpleTemplatePrototypeの実装クラスであるSimpleInsertProcessorPrototypeを生成する。
package sample.hotplate.sample.parser.handler; import java.util.Collections; import java.util.List; import sample.hotplate.sample.parser.Attribute; import sample.hotplate.sample.parser.TagHandler; import sample.hotplate.sample.processor.prototype.SimpleInsertProcessorPrototype; import sample.hotplate.sample.prototype.SimpleContainerPrototype; import sample.hotplate.sample.prototype.SimpleTemplatePrototype; public class InsertTagHandler implements TagHandler { @Override public String[] tagNames() { return new String[] {"insert"}; } @Override public boolean requireSingleTag(String tagName) { return true; // シングルタグも可能 } @Override public boolean requireContainerTag(String tagName) { return true; // コンテナタグも可能 } // シングルタグを生成 @Override public SimpleTemplatePrototype handleSingleTag(String tagName, List<Attribute> attributes) { Attribute valueAttribute = Attribute.findAttribute("value", attributes); return new SimpleInsertProcessorPrototype( TagHandlerUtils.makeSource(valueAttribute), new SimpleContainerPrototype(Collections.<SimpleTemplatePrototype>emptyList())); } // コンテナタグを生成 @Override public SimpleTemplatePrototype handleContainerTag(String tagName, List<Attribute> attributes, List<SimpleTemplatePrototype> elements) { Attribute valueAttribute = Attribute.findAttribute("value", attributes); return new SimpleInsertProcessorPrototype( TagHandlerUtils.makeSource(valueAttribute), new SimpleContainerPrototype(elements)); } }
要素5 Parser>からParserを作る
端的にSimpleTemplatePrototypeのリストからSimpleContainerPrototypeを生成する。
/** * @return listParserを元にSimpleTemplatePrototypeを生成するParserを戻す * @param listParser List<SimpleTemplatePrototype>を生成するParser */ public static Parser<SimpleTemplatePrototype> flatten(Parser<List<SimpleTemplatePrototype>> listParser) { return listParser.map(new Map<List<SimpleTemplatePrototype>, SimpleTemplatePrototype>() { public SimpleTemplatePrototype map(List<SimpleTemplatePrototype> elements) { return new SimpleContainerPrototype(elements); } }); }
全体のパーサを書く
全体のパーサを生成するParserFactoryというクラスを作る。
TokenizerとParserの両方で使うフィールドをまとめる。あとTagHandlerを登録できるようにする。
public final class ParserFactory { public static final Terminals startBrace = Terminals.operators("{"); public static final Terminals endBrace = Terminals.operators("}"); public static final Terminals operators = Terminals.operators("/", "="); public static final String LITERAL_NAME = "TEXT"; public static final ParserFactory instance = new ParserFactory(); /** タグ名をキー、TagHandlerを値とするMap */ private Map<String, TagHandler> handlers = new LinkedHashMap<String, TagHandler>(); public ParserFactory() { registerHandlers(); } private void registerHandlers() { registerHandler(new DefineTagHandler()); registerHandler(new InsertTagHandler()); } public synchronized void registerHandler(TagHandler handler) { for (String tagName : handler.tagNames()) { handlers.put(tagName, handler); } }
まず、TagHandlerのリストからタグ用のParserを作る部分。
private List<Parser<SimpleTemplatePrototype>> makeTagParses( Map<String, TagHandler> handlers, Terminals tags, Reference<List<SimpleTemplatePrototype>> listParserRef) { List<Parser<SimpleTemplatePrototype>> tagParsers = new ArrayList<Parser<SimpleTemplatePrototype>>(); for (String tagName: handlers.keySet()) { TagHandler handler = handlers.get(tagName); // コンテナタグありならコンテナタグのパーサを作って追加 if (handler.requireContainerTag(tagName)) { tagParsers.add( TemplateParsers.makeContainerTagParser(tags, listParserRef.lazy(), tagName, handler)); } // シングルタグありならシングルタグのパーサを作って追加 if (handler.requireSingleTag(tagName)) { tagParsers.add( TemplateParsers.makeSingleTagParser(tags, tagName, handler)); } } return tagParsers; }
リテラル用のParserとタグ用のParserから全体用のParserを作る部分。
private Parser<SimpleTemplatePrototype> makeParser(Map<String, TagHandler> handlers, Terminals tags) { // タグの中身を再帰的にパースする為のReference Reference<List<SimpleTemplatePrototype>> contentParserRef = Parser.newReference(); // 全タグのパーサを取得 List<Parser<SimpleTemplatePrototype>> parsers = makeTagParses(handlers, tags, contentParserRef); // リテラル用のパーサを追加 parsers.add(TemplateParsers.makeLiteralParser()); // パーサーの全体を作成 Parser<List<SimpleTemplatePrototype>> listParser = Parsers.or(parsers).many(); // タグの内容のパーサとしてセット contentParserRef.set(listParser); // List<SimpleTemplatePrototype>ではなくSimpleTemplatePrototypeを吐くように調整 return TemplateParsers.flatten(listParser); }
最後にトークナイザと合わせて完成。タグ名のTerminalsはhandlersのキーから生成。
public synchronized Parser<SimpleTemplatePrototype> newParser() { // Tag用のTerminalsを作成 Terminals tags = makeTags(this.handlers); // Tokenizer(字句解析部)を作成 Parser<List<Token>> tokenizer = Tokenizers.newTokenizer(tags); // Parser(構文解析部)を作成 Parser<SimpleTemplatePrototype> parser = makeParser(this.handlers, tags); // 両方を合わせてパーサを作成 return parser.from(tokenizer); } private Terminals makeTags(Map<String, TagHandler> handlers) { String[] processorNames = handlers.keySet().toArray(new String[0]); return Terminals.caseSensitive(new String[0], processorNames); }
これで文字列からSimpleTemplatePrototypeを生成するパーサが出来たので、次回はテンプレートエンジン本体の実装に入る。