Created
March 9, 2022 13:20
-
-
Save suzuki-kei/7c4112c32263446de8ed624c25867c5c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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>こんにちは <b>Taro</b> さん</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