未提供项目描述
项目描述
hotmetal
一个微小的HTML生成器
安装
从PyPI安装
pip install hotmetal
什么是hotmetal?
hotmetal
是一个微小的库,允许您直接从Python原始数据结构生成HTML,而无需使用任何基于文本的模板语言。它是 Jinja、Django模板 等的替代品。它 loosely 受到 React、Mithril 和其他JavaScript库的启发。它与 Hyperpython 类似,但更简单。它试图尽可能接近 HTML规范。
它是如何工作的?
首先,阅读 HTML 快速入门。继续吧,这很重要。
HTML文档表示一个节点树。每个节点(称为“元素”)由恰好三件事组成:一个 标签名(例如 div
、p
或 title
),一组 属性(键到值的映射)和一组 子节点(节点内的其他元素或文本)。
所以:这是一个 标签名(一个字符串)、属性(一个映射)和 子节点(一个列表)。
在Python中表示这种结构的最简单方法是三个元素的元组
("", {}, [])
以下是一个带有内部文本节点的锚元素(链接)的示例
link_element = ("a", {"href": "somewhere.html"}, ["click me!"])
以下是一个完整的HTML文档的示例(注意,缺少 DOCTYPE
,我们稍后会回到这一点)
document = (
"html",
{"lang": "en"},
[
("head", {}, [("title", {}, ["Sample page"])]),
(
"body",
{},
[
("h1", {}, ["Sample page"]),
(
"p",
{},
["This is a ", ("a", {"href": "demo.html"}, ["simple"]), " sample"],
),
],
),
],
)
一旦我们创建了这个结构,我们就可以使用 hotmetal
将它 渲染 为字符串
>>> from hotmetal import render
>>> print(render(document, indent=2))
<html lang="en">
<head>
<title>
Sample page
</title>
</head>
<body>
<h1>
Sample page
</h1>
<p>
This is a
<a href="demo.html">
simple
</a>
sample
</p>
</body>
</html>
本质上,就是这样。这就是 hotmetal
所做的。
组件
但这看起来相当麻烦,对吧?如果您总是需要费力地组装一个巨大的嵌套元组树来渲染您Web应用的每个页面,这将非常冗长且难以阅读。
这里有个巧妙的点:不是这样,而是编写具有意义名称的函数,返回元素节点,并用这些节点构建你的Web应用。让我们将这些返回节点的函数称为组件(如果你熟悉React,可能会看出它的走向)。
def menu_item(href, text):
return ("li", {}, [("a", {"href": href}, [text])])
def menu(items):
return ("ul", {}, [menu_item(href, text) for href, text in items])
menu_node = menu(
[
("/home/", "Home"),
("/blog/", "Blog"),
]
)
以此类推,直到<html>
标签。
关于如何以这种方式组合标记的一些有用模式,请阅读React文档中的组合与继承。这些概念应该可以直接迁移。
片段
之前,我们提到示例中缺少了DOCTYPE
。这是因为HTML文档的根实际上是两件事:首先是<html>
元素本身,然后是它的前面DOCTYPE
前缀。我们可以在hotmetal
中使用片段来表示这种结构。同样,在React中也有相同的概念,但在hotmetal
中语法更简单。只需使用一个空标签名和属性的节点即可。
from hotmetal import safe
document = (
"",
{},
[
safe("<!DOCTYPE html>"),
(
"html",
{"lang": "en"},
[...], # head, body etc
),
],
)
在渲染时,hotmetal
会“优化”掉空节点,只留下两个连续的子节点。这在组件组合中也很常用:你可能创建了一个接受单个子节点作为参数的组件,但仍然可以通过在片段中包裹它们来传递多个节点。
但那个safe
是什么意思?
转义
在生成HTML时,你必须小心。如果你的标记或文本内容中的任何部分可以由用户提供(这在Web应用中很常见),你可能容易受到跨站脚本(XSS)攻击。Django文档中有对为什么这很重要的良好解释。
默认情况下,hotmetal
使用Python内置的html.escape
功能相同的做法来转义它渲染的每个字符串。因此,你可以添加用户生成的内容到你的文档而不用担心。
如果你想添加一些你知道是安全的原始标记(一些手工制作的HTML,或者另一个已经转义了不安全内容的模板系统的渲染输出),你可以用safe
函数包裹这些字符串。请参阅上面的示例。
上下文
上下文提供了一种在不手动在每个级别传递props的情况下通过组件树传递数据的方法。
在hotmetal
中,也存在完全相同的概念,但实现起来又简单一些。
如果你熟悉Django或Jinja模板中的上下文概念,这并不是完全一样的东西。在这些语言中,上下文是传递变量到模板的唯一方式。在hotmetal
中不需要这样做:你只需创建接受参数的组件函数即可。
def header(title):
return ("h1", {}, [title])
所以你会在哪里使用上下文?就像在React中一样
上下文主要用于需要在不同嵌套级别访问某些数据的情况。要小心使用,因为它会使组件重用更困难。
一个很好的例子可能是如果你需要在组件树中的某个深处访问request
对象或当前登录的用户,但你不想从根明确地通过组件层次结构传递它。
要在hotmetal
中使用上下文,将字典传递给render
函数
render(some_node, context={"logged_in_user_email": "hello@example.com"})
要从树内部访问上下文,将树中的任何节点替换为返回该节点的可调用对象(一个命名函数或lambda)。在渲染过程中,hotmetal
会调用你的函数,并将context
字典作为其唯一参数传递。
def current_user_panel():
return lambda context: (
"p",
{},
[f"Hello, {context['logged_in_user_email']}"],
)
缩进控制
render
函数接受一个 indent
参数,它是一个整数,用于控制生成 HTML 中使用多少个空格作为缩进。默认值为 0,这意味着整个 HTML 字符串将在一行中返回。您可能希望在开发时使用(例如)indent=2
,在生产时使用 indent=0
(实际上是最小化您的 HTML)。
生成类名
提供了一个函数,可以用于根据各种参数生成类名字符串。这与 classnames JavaScript 库非常相似。
from hotmetal.utils.classnames import classnames
def header(title):
title_is_green = title.lower() == "green"
return ("h1", {"class": classnames("title", {"green": title_is_green})}, [title])
测试工具
在编写组件的测试时,能够遍历树以找到特定的节点并对它们进行断言通常非常有用。为了帮助实现这一点,hotmetal
提供了一个 find
函数,它接受一个节点可迭代的参数和一个可调用的谓词,并返回一个生成器,该生成器产生匹配谓词的节点(使用深度优先遍历节点,类似于浏览器的 querySelectorAll
函数)。
例如,给定以下组件
def header(title):
return ("div", {"class": "header"}, [("h1", {}, [title])])
您可以编写一个类似的测试
from hotmetal.utils.find import find
from unittest import TestCase
class HeaderTestCase(TestCase):
def test_title_appears_correctly_inside_h1(self):
node = header("hello world")
h1_elements = list(
find([node], lambda node: isinstance(node, tuple) and node[0] == "h1")
)
self.assertEqual(len(h1_elements), 1)
h1 = h1_elements[0]
children = node[2]
self.assertEqual(children, ["hello world"])
在这里,一个 lambda
被用于匹配树中的每个节点,根据该节点是否应该包含在结果中返回 True
或 False
。在编写谓词时,请记住,node
参数可能是一个树节点(一个元组),一个文本节点(一个字符串),或者一个函数(对于需要 context
的节点,见上文)。
作为编写自己的 predicate
函数的替代,提供了一系列函数,用于解决查找节点时的常见需求。上述示例中的 find
行可以重写如下
h1_elements = list(find([node], tag_is("h1")))
以下列出了这些谓词函数的完整列表
hotmetal.utils.find.tag_is(tag)
返回一个谓词函数,通过标签名称匹配节点:find(nodes, tag_is("h1"))
hotmetal.utils.find.id_is(id)
返回一个谓词函数,通过 ID 属性的值匹配节点:find(nodes, id_is("header"))
hotmetal.utils.find.has_class(cls)
返回一个谓词函数,通过类属性中的特定类名匹配节点:find(nodes, has_class("someclass"))
hotmetal.utils.find.has_attr(attr)
返回一个谓词函数,匹配具有给定属性(任何值)的节点:find(nodes, has_attr("href"))
hotmetal.utils.find.has_attr_with_value(attr, value)
返回一个谓词函数,匹配具有给定属性且该属性的值等于给定值的节点:find(nodes, has_attr_with_value("type", "hidden"))
hotmetal.utils.find.attr_value_matches(attr, predicate)
返回一个谓词函数,匹配具有给定属性且该属性的值匹配给定谓词函数的节点:find(nodes, attr_value_matches("class", lambda value: "background-" in value))
hotmetal.utils.find.text_contains(text)
返回一个谓词函数,匹配包含给定文本的文本节点(字符串):find(nodes, text_contains("hello world"))
hotmetal.utils.find.any_immediate_child_matches(predicate)
返回一个谓词函数,匹配至少有一个直接子节点匹配给定谓词的节点:find(nodes, any_immediate_child_matches(text_contains("hello world")))
hotmetal.utils.find.or_(*predicates)
对于多个谓词函数,返回一个谓词函数,匹配满足任何谓词的节点:find(nodes, or_(tag_is("h1"), id_is("title")))
hotmetal.utils.find.and_(*predicates)
对于多个谓词函数,返回一个谓词函数,匹配满足所有谓词的节点:find(nodes, and_(tag_is("input"), has_attr_with_value("type", "text")))
hotmetal.utils.find.not_(predicate)
反转谓词函数:find(nodes, and_(tag_is("input"), not_(has_attr_with_value("type", "hidden")))
hotmetal.utils.find.TAG
hotmetal.utils.find.ATTRS
hotmetal.utils.find.CHILDREN
这三个常量仅仅是节点中每个组件的索引。它们提高了可读性:你可以说 children = node[CHILDREN]
而不是 children = node[2]
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。
源分布
构建分布
hotmetal-1.0.0.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | a84539fe04ff5289e5c584e1a7310a91e2f35181128d817643bd591434501316 |
|
MD5 | 25a04a76005a96e7d17238dbafa3a778 |
|
BLAKE2b-256 | 57145d8948d83c2cd1795f9f3129e81ca688041c67f105bce090633ff6c73ccb |
hotmetal-1.0.0-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 2bc022a054c463c73ce68afc6b27c34968fdd3d038bbe71204a057859b7b55b5 |
|
MD5 | 39d741c4ef9a716527a1d811420af699 |
|
BLAKE2b-256 | 6dc703f4411fa3363a27156172046d7a62aecc6b967cca033e6063d306529fcd |