luaでPHPライクにwebに組み込めるテンプレートを作ってみた。


luaを使って webアプリ作れれば面白くね?
と、いうことで、 PHPのノリで lua を使えるテンプレートを作ってみました。

<html>
<body>
<?lua for(i=1,99){ 
	local key=""..i.."";
	if( not out["records"][key]){ break; }
	local value = out["records"][key];
?>
	<div class="img" id="img-1">
		<div><a href="javascript:void(0)" onclick="mediaplay(<?= key ?>)">
			<img width="255" height="255"  src='data:image/jpeg;base64,<?= value["image"] ?>'><br />
			<div class="msg" ><span class="select_mark"><?= key ?></span><?= value["title"] ?></div>
		</a></div>
	</div>
<?lua } ?>
</body>
<html>

こんなふうに書くと、 lua プログラムに変換した文字列にしてくれる関数です。
変換したファイル名と、この構文を格納する関数名を入れれば、
functionname(out) { } という関数が手に入ります。
それを呼べば htmlが出力されるといった感じです。

outは、このhtmlテンプレートが利用する文字列を保持しているテーブルです。
PHPでいう htmlspecialchars などを施してあげるなどすれば、自動タグエスケープなどもできますね。

std::string convertTemplate(const std::string & filename,const std::string functionname)
{
	enum TPL_STATE
	{
		 TPL_STATE_HTML
		,TPL_STATE_CODE
		,TPL_STATE_DOUBLE_QUOTE
		,TPL_STATE_SINGLE_QUOTE
	};
	TPL_STATE state = TPL_STATE_HTML;

	std::ostringstream out;
	out << "function " << functionname << "(out) ";
	out << "print( [[";

	const char * lineCommentStart = NULL;
	bool easy_echo = false;
	std::ifstream TPL( filename );
	std::string line;
	while( std::getline(TPL,line) )
	{
		lineCommentStart = NULL;

		const char * p = line.c_str();
		const char * start = p;
		for( ; *p ; ++p )
		{
			if (state == TPL_STATE_HTML)
			{
				if (*p == '<'  && *(p+1) == '?')
				{
					if (*(p+2) == '=')
					{
						//<?=
						state = TPL_STATE_CODE;
						out << std::string(start , 0 , (int)(p - start) )  << "]] ); print(";
						start = p + 2 + 1;
						p+=2;
						easy_echo = true;
						lineCommentStart = NULL;
					}
					else if (*(p+2) == 'l' && *(p+3) == 'u' && *(p+4) == 'a')
					{
						//<?lua
						state = TPL_STATE_CODE;
						out << std::string(start , 0 , (int)(p - start) )  << "]] ); ";
						start = p + 4 + 1;
						p+=4;
						easy_echo = false;
						lineCommentStart = NULL;
					}
				}
			}
			else if (state == TPL_STATE_CODE)
			{
				if (*p == '\\' && *(p+1) == '"')
				{
					p++;	//skip
				}
				else if (*p == '\\' && *(p+1) == '\'')
				{
					p++;	//skip
				}
				else if ( (*p == '-' && *(p+1) == '-') || (*p == '/' && *(p+1) == '/') )
				{
					if (lineCommentStart == NULL)
					{
						lineCommentStart = p;
					}
				}
				else if (*p == '"')
				{
					state = TPL_STATE_DOUBLE_QUOTE;
				}
				else if (*p == '\'')
				{
					state = TPL_STATE_SINGLE_QUOTE;
				}
				else if (*p == '?' && *(p+1) == '>')
				{
					state = TPL_STATE_HTML;
					const char * codeend = p;

					//今の行にシングルコメントが入っている場合は、そのコメントの前まで。
					if ( lineCommentStart )
					{
						codeend = lineCommentStart;
					}

					if ( easy_echo  )
					{
						out << std::string(start , 0 , (int)(codeend - start) )  << "); print( [[";
					}
					else 
					{
						out << std::string(start , 0 , (int)(codeend - start) )  << "; print( [[";
					}
					start = p + 2 ;
					p ++;
				}
			}
			else if (state == TPL_STATE_DOUBLE_QUOTE)
			{
				if (*p == '\\' && *(p+1) == '"')
				{
					p++;	//skip
				}
				else if (*p == '"')
				{
					state = TPL_STATE_CODE;
				}
			}
			else if (state == TPL_STATE_DOUBLE_QUOTE)
			{
				if (*p == '\\' && *(p+1) == '\'')
				{
					p++;	//skip
				}
				else if (*p == '\'')
				{
					state = TPL_STATE_CODE;
				}
			}
		}
		out << std::string(start , 0 , (int)(p - start) ) << std::endl;
	}
	if(state == TPL_STATE_HTML)
	{
		out << "]] );";
	}
	out << " end";
/*
	//空出力を削除してパフォーマンスを上げる
	std::string sourceCode = out.str();
	sourceCode = XLStringUtil::replace(sourceCode,"print( [[]] );","");
	sourceCode = XLStringUtil::replace(sourceCode,"print( [[\r\n]] );","");
*/
	return sourceCode;
}

これに htmlspecialchars みたいなものを組み込んで動くルーチンにしたのが、l_webload関数です。
https://github.com/rti7743/kaden_voice/blob/master/naichichi2/naichichi2/ScriptRunner.cpp#L637


ライブラリに依存しているので、コード自体はそのままだと動きません。
これは、C++が文字列リプレースみたいなものを標準で持っていないためですね。ダメダメですね。困りますね。
(std::string の replaceは使えない謎仕様だし。)


とりあえず、雰囲気(なぜか変換できる)だけでも味わってください。あんまり綺麗なソースではないですが。

//web専用 テンプレートを読み込む
int ScriptRunner::l_webload(lua_State* L)
{
	ScriptRunner* _this = __this(L);
	_this->PoolMainWindow->SyncInvokeLog(std::string() + "lua function:" + lua_funcdump(L,"webload") ,LOG_LEVEL_DEBUG);

	//テンプレート名
	if (! lua_isstring(L,-2) )
	{
		return luaL_errorHelper(L,lua_funcdump(L,"webload") + "の第1引数が文字列ではありません");
	}
	//table
	if (! lua_istable(L,-1) )
	{
		return luaL_errorHelper(L,lua_funcdump(L,"webload") + "の第2引数がテーブルではありません");
	}
	//tableをすべてエスケープする。
	//動的にテーブルを構築するのはスタックが汚れてしまって無理があるので、
	//一度配列に構築した後、それをテーブルとして複写する.
	//一次元のリストだが、配列の操作を取得時と、再構築時に同じことを行うわけだから、次元は無視してもよい。
	struct _temp
	{
		std::string key;
		std::string value;
		enum type
		{
			 _temp_type_value
			,_temp_type_nest
			,_temp_type_up
		}
		type;
	};
	struct _nest_lamba
	{
		lua_State* L;
		int index;

		_nest_lamba(lua_State* L,int index) : L(L) , index(index) { func(); };
		//再起するのでクラス内クラスで。
		void func()
		{
			_USE_WINDOWS_ENCODING;
			lua_pushnil(L);
			while (lua_next(L, index - 1) != 0) 
			{
				_temp* p = new _temp;
#ifdef _WINDOWS
				p->key = _U2A(lua_tostring(L, -2));
#else
				p->key = lua_tostring(L, -2);
#endif
				if (lua_istable(L, -1))
				{
					p->type = _temp::_temp_type_nest;
					safeArray.push_back(p);

					func();

					_temp* p2 = new _temp;
					p2->type = _temp::_temp_type_up;
					safeArray.push_back(p2);
				}
				else
				{
					p->type = _temp::_temp_type_value;
					if (p->key.size() >= 2 && p->key[0] == '_' && p->key[1] == '_')
					{//キーが __hogehoge のように、 __ で始まる場合のみ自動エスケープをしない.
						p->value = lua_dump(L,-1 , false,0);
					}
					else
					{
						p->value = XLStringUtil::htmlspecialchars(lua_dump(L,-1 ,false, 0));
					}
					safeArray.push_back(p);
				}

				lua_pop(L, 1);
			}
		}
		std::list< _temp* > safeArray;
	} 
	//テーブルの読み込み処理
	nest(L,-1);

	//テンプレートの読み込み
	std::string functionanme = "webtemplate";
	std::string filename = lua_tostringHelper(L,-2);
	filename = _this->PoolMainWindow->Httpd.WebPathToRealPath(filename);
	std::string tplcode = convertTemplate(filename,functionanme);

	//ここからスタックを破壊するので、関数名を避難させる。
	std::string func = lua_funcdump(L,"webload");

	//テンプレートコードの読み込み
	if ( luaL_loadstring(L, tplcode.c_str()) )
	{
		return luaL_errorHelper(L,func + "のテンプレート" + filename + " の解析中にエラー。 Lua:" + lua_tostringHelper(L, -1));
	}
	//まずそのスクリプトを実行させて、 functionanme を登録します。
	if ( lua_pcall( L, 0, 0, 0 ) )
	{
		return luaL_errorHelper(L,func + "のテンプレート" + filename + " の実行中にエラー。 Lua:" + lua_tostringHelper(L, -1));
	}
	//functionanme を呼び出す準備に入ります。
	lua_getglobal(L, functionanme.c_str() );
	if(!lua_isfunction(L,-1) )
	{
		return luaL_errorHelper(L,func + "のテンプレート" + filename + " を実行しましたが、内部用関数" + functionanme + "が登録されていません。 Lua:" + lua_tostringHelper(L, -1));
	}
	//テンプレート引数としてのテーブルを具現化
	//thank http://logsoku.com/thread/pc2.2ch.net/tech/1063711237/824
	lua_newtable(L); 
	for(auto it = nest.safeArray.begin() ; it != nest.safeArray.end() ; ++it)
	{
		if ((*it)->type == _temp::_temp_type_value)
		{
			lua_pushstringHelper(L, (*it)->key);
			lua_pushstringHelper(L, (*it)->value);
			lua_settable(L, -3);
		}
		else if ((*it)->type == _temp::_temp_type_nest)
		{
			lua_pushstringHelper(L, (*it)->key);
			lua_newtable(L);
		}
		else //if ((*it)->type == _temp::_temp_type_up)
		{
			lua_settable(L, -3);
		}
		delete *it;
	}

	//関数呼び出し
	if ( lua_pcall( L, 1, 0, 0 ) )
	{
		return luaL_errorHelper(L,func + "のテンプレート" + filename + " の実行中にエラー。 Lua:" + lua_tostringHelper(L, -1));
	}

	return 0;             //戻り値の数を指定
}