Skip to content

Instantly share code, notes, and snippets.

@suzuki-kei
Created March 9, 2022 13:20
Show Gist options
  • Save suzuki-kei/7c4112c32263446de8ed624c25867c5c to your computer and use it in GitHub Desktop.
Save suzuki-kei/7c4112c32263446de8ed624c25867c5c to your computer and use it in GitHub Desktop.
"""
HTML を生成するためのテンプレートを提供する.
テンプレートには変数で値を置き換えるために,
以下のようにプレースホルダを含めることができる.
<p>こんにちは <%= name %> さん</p>
変数の値はデフォルトで HTML エスケープされるため,
意図しない XSS (Cross Site Scripting) 脆弱性を防ぐことができる.
HTML エスケープしたくない場合は, 次のように明示的に raw で修飾する.
<p>こんにちは <%= raw name %> さん</p>
具体的なコード例は HtmlTemplate クラスの docstring を参照のこと.
"""
import doctest
import html
import re
class HtmlTemplate(object):
"""
HTML テンプレート.
プレースホルダによって変数を埋め込むことができる.
XSS を防ぐために, 変数の値はデフォルトで HTML エスケープされる.
Examples
--------
>>> template = HtmlTemplate("<p>こんにちは <%= name %> さん</p>")
>>> print(template.render({"name": "Taro"}))
<p>こんにちは Taro さん</p>
>>> template = HtmlTemplate("<p>こんにちは <%= name %> さん</p>")
>>> print(template.render({"name": "<b>Taro</b>"}))
<p>こんにちは &lt;b&gt;Taro&lt;/b&gt; さん</p>
>>> template = HtmlTemplate("<p>こんにちは <%= name %> さん</p>")
>>> print(template.render({"name": SafeString("<b>Taro</b>")}))
<p>こんにちは <b>Taro</b> さん</p>
>>> template = HtmlTemplate("<p>こんにちは <%= raw name %> さん</p>")
>>> print(template.render({"name": "<b>Taro</b>"}))
<p>こんにちは <b>Taro</b> さん</p>
"""
PLACEHOLDER_PATTERN = re.compile(r"<%=\s*(?:([a-zA-Z0-9_]+)\s+)?([a-zA-Z0-9_]+)\s*%>")
"""
プレースホルダにマッチする正規表現.
"""
@staticmethod
def from_file(file_path: str) -> "HtmlTemplate":
"""
ファイルからテンプレートを読み込む.
Arguments
---------
file_path : str
テンプレートファイルのパス.
Returns
-------
template : HtmlTemplate
ファイルの内容で初期化した HtmlTemplate.
Raises
------
FileNotFoundError
ファイルが見つからない場合.
"""
with open(file_path, "r") as file:
return HtmlTemplate(file.read())
def __init__(self, template: str) -> None:
"""
テンプレート文字列で初期化する.
Arguments
---------
template : str
テンプレート文字列.
"""
self._template = template
def render(self, variables: dict[str, object]) -> str:
"""
HTML を生成する.
Arguments
---------
variables: dict[str, object]
プレースホルダとして埋め込まれた変数.
{Key=変数名, value=変数値} である辞書.
Returns
-------
html : str
生成した HTML.
"""
replace = lambda match: self._replace_placeholder(match, variables)
return re.sub(self.PLACEHOLDER_PATTERN, replace, self._template)
def _replace_placeholder(self, match: re.Match, variables: dict[str, object]) -> str:
"""
プレースホルダを変数で置換する.
Arguments
---------
match: re.Match
プレースホルダにマッチした re.Match オブジェクト.
variables : dict[str, object]
{Key=変数名, value=変数値} である辞書.
Returns
-------
value : str
プレースホルダを変数で置換して生成した文字列.
Raises
------
KeyError
変数名が variables に含まれない場合.
"""
method, name = match.groups()
value = variables[name]
if isinstance(value, SafeString):
return value.raw_string()
if method == "raw":
return str(value)
return html.escape(str(value))
class SafeString(object):
"""
そのまま HTML として出力して良い文字列.
例えば, 次のような文字列:
* 既に HTML エスケープされている文字列.
* 意図的にエスケープせずに出力したい文字列.
"""
def __init__(self, value: str):
"""
指定された文字列で初期化する.
Arguments
---------
value : str
文字列.
"""
self._value = value
def raw_string(self) -> str:
"""
str 型の値.
Returns
-------
raw_string : str
str 型の値.
"""
return self._value
class UnsafeString(object):
"""
そのまま HTML として出力してはいけない文字列.
例えば, 次のような文字列:
* テキストとして出力したいが, まだエスケープしていない文字列.
"""
def __init__(self, value: str):
"""
指定された文字列で初期化する.
Arguments
---------
value : str
文字列.
"""
self._value = value
def raw_string(self) -> str:
"""
str 型の値.
Returns
-------
raw_string : str
str 型の値.
"""
return self._value
def main():
template = """
<!DOCTYPE html>
<html>
<head>
<title>サンプルです</title>
</head>
<body>
<div>こんにちは <%= name %> さん</div>
<div>message1: <%= message1 %></div>
<div>message2: <%= raw message2 %></div>
<div>message3: <%= message3 %></div>
</body>
</html>
"""
html = HtmlTemplate(template).render({
"name" : "たろう",
"message1" : "<b>Hello</b>",
"message2" : "<b>Hello</b>",
"message3" : SafeString("<b>Hello</b>"),
})
print(html)
if __name__ == "__main__":
doctest.testmod()
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment