mensbeam / html-dom
Requires
- php: >=8.0.2
- ext-dom: *
- mensbeam/getters-and-setters: >=1.1
- mensbeam/html-parser: >=1.2.1
- symfony/css-selector: >=5.3
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.3
- mikey179/vfsstream: ^1.6
- nikic/php-parser: ^4.13
README
这是一个用PHP编写的现代DOM库,用于HTML文档。这个库旨在通过用户空间扩展和封装PHP内置的DOM来实现WHATWG的DOM规范和WHATWG HTML DOM扩展规范。它存在的原因是PHP的DOM不准确,不足以用于任何HTML,并且极其错误。此实现旨在尽可能修复PHP DOM的不准确之处,添加现代HTML开发所需的功能,并绕过大多数错误。
要求
使用
大多数库的完整文档可能不是必需的,因为它主要遵循规范,但由于库的使用方式,有一些明显的不同之处。这些将在下面概述。
MensBeam\HTML\DOM\Document
MensBeam\HTML\DOM\Document
实现 \ArrayAccess
,允许类通过数组语法访问命名属性
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html><body><img name="ook"><img name="eek"><img id="eek" name="ack"><embed name="eek"><object id="ook"><embed name="eek"><object name="ookeek"></object></object><iframe name="eek"></iframe><object id="eek"></object></body></html>'); echo $d['ook']::class . "\n"; echo $d['eek']->length . "\n";
输出
MensBeam\HTML\DOM\HTMLElement
5
关于被认为是命名属性的限定。有关允许以这种方式访问的内容的更多详细信息,请参阅WHATWG HTML DOM扩展规范。
namespace MensBeam\HTML\DOM; partial class Document extends Node implements \ArrayAccess { use DocumentOrElement, NonElementParentNode, ParentNode, XPathEvaluatorBase; public function __construct( ?string $source = null, ?string $charset = null ); public function destroy(): void; public function registerXPathFunctions( string|array|null $restrict = null ): void; public function serialize( ?Node $node = null, array $config = [] ): string; public function serializeInner( ?Node $node = null, array $config = [] ): string; }
MensBeam\HTML\DOM\Document::__construct
创建一个新的 MensBeam\HTML\DOM\Document
对象。
source
:表示要解析的HTML文档的字符串。charset
:用作文档编码的字符集。如果从字符串解析文档,其默认值为'windows-1251',否则为'UTF-8'。
示例
-
创建新文档
namespace MensBeam\HTML\DOM; $d = new Document();
-
从字符串创建新文档
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html><head><title>Ook</title></head><body><h1>Ook!</h1></body></html>');
或
namespace MensBeam\HTML\DOM; $d = new Document(); $d->load('<!DOCTYPE html><html><head><title>Ook</title></head><body><h1>Ook!</h1></body></html>');
-
指定字符集
namespace MensBeam\HTML\DOM; $d = new Document(null, 'GB18030'); echo $d->characterSet;
输出
gb18030
MensBeam\HTML\DOM\Document::destroy
销毁与实例关联的引用,以便PHP可以回收垃圾。由于PHP垃圾回收的方式以及基于的库PHP DOM的糟糕状态,必须在每个创建的文档中保持用户空间的引用。因此,不幸的是,每当文档不再需要时,都应该手动调用此方法。
示例
namespace MensBeam\HTML\DOM; $d = new Document(); $d->destroy(); unset($d);
MensBeam\HTML\DOM\Document::registerXPathFunctions
将PHP函数注册为XPath函数。它的工作方式与\DOMXPath::registerPhpFunctions
类似,但不需要注册php命名空间。
restrict
:使用此参数仅允许从XPath调用某些函数。此参数可以是字符串(函数名)、函数名数组或null以允许一切。
示例
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html><body><h1>Ook</h1><p class="subtitle1">Eek?</p><p class="subtitle2">Ook?</p></body></html>'); // Register PHP functions (no restrictions) $d->registerXPathFunctions(); // Call substr function on classes $result = $d->evaluate('//*[php:functionString("substr", @class, 0, 8) = "subtitle"]', $d); echo "Found " . count($result) . " nodes with classes starting with 'subtitle':\n"; foreach ($result as $node) { echo "$node\n"; }
输出
Found 2 nodes with classes starting with 'subtitle':
<p class="subtitle1">Eek?</p>
<p class="subtitle2">Ook?</p>
MensBeam\HTML\DOM\Document::serialize
将节点转换为字符串。
node
:要序列化的文档内的节点,默认为文档本身。config
:一个配置数组,可能的键和值类型为booleanAttributeValues
(bool|null
):是否在序列化HTML元素时包含布尔属性的值。根据标准,此值为true
。foreignVoidEndTags
(bool|null
):是否打印外部空元素结束标签而不是自闭合起始标签。按照标准,默认为true
groupElements
(bool|null
):将“块”等元素分组并在组之间插入额外的换行符indentStep
(int|null
):每步缩进时使用的空格或制表符(根据缩进步骤的设置)。默认为1
,除非reformatWhitespace
为true
,否则没有效果indentWithSpaces
(bool|null
):是否使用空格或制表符进行缩进。默认为true
,除非reformatWhitespace
为true
,否则没有效果reformatWhitespace
(bool|null
):是否重新格式化空白(美化打印)或否则。默认为false
示例
-
序列化文档
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html></html>'); echo $d->serialize();
或
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html></html>'); echo $d;
输出
<!DOCTYPE html><html><head></head><body></body></html>
-
序列化文档(美化打印)
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html><body><h1>Ook!</h1><p>Ook, eek? Ooooook. Ook.</body></html>'); echo $d->serialize($d, [ 'reformatWhitespace' => true ]);
输出
<!DOCTYPE html> <html> <head></head> <body> <h1>Ook!</h1> <p>Ook, eek? Ooooook. Ook.</p> </body> </html>
MensBeam\HTML\DOM\Document::serializeInner
将节点转换为字符串,但只序列化节点的内容。
node
:要序列化的文档内的节点,默认为文档本身。config
:一个配置数组,可能的键和值类型为booleanAttributeValues
(bool|null
):是否在序列化HTML元素时包含布尔属性的值。根据标准,此值为true
。foreignVoidEndTags
(bool|null
):是否打印外部空元素结束标签而不是自闭合起始标签。按照标准,默认为true
groupElements
(bool|null
):将“块”等元素分组并在组之间插入额外的换行符indentStep
(int|null
):每步缩进时使用的空格或制表符(根据缩进步骤的设置)。默认为1
,除非reformatWhitespace
为true
,否则没有效果indentWithSpaces
(bool|null
):是否使用空格或制表符进行缩进。默认为true
,除非reformatWhitespace
为true
,否则没有效果reformatWhitespace
(bool|null
):是否重新格式化空白(美化打印)或否则。默认为false
示例
-
序列化文档(与
Document::serialize
功能相同)namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html></html>'); echo $d->serializeInner();
或
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html></html>'); echo $d;
输出
<!DOCTYPE html><html><head></head><body></body></html>
-
序列化元素的内容
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html><body><h1>Ook!</h1><p>Ook, eek? Ooooook. Ook.</body></html>'); $body = $d->body; echo $body->serializeInner($body, [ 'reformatWhitespace' => true ]);
输出
<h1>Ook!</h1> <p>Ook, eek? Ooooook. Ook.</p>
MensBeam\HTML\DOM\Node
在 MensBeam\HTML\DOM\Node
中提供了常用命名空间常量,以使使用此库的命名空间不那么繁琐。此外,还提供了常量,用于与 MensBeam\HTML\DOM\ParentNode::walk
一起使用。MensBeam\HTML\DOM\Node
还实现了 \Stringable
,这意味着任何节点都可以简单地转换为字符串以进行序列化。
namespace MensBeam\HTML\DOM; partial abstract class Node implements \Stringable { public readonly \DOMNode $innerNode; // Common namespace constants provided for convenience public const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; public const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; public const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; public const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'; public const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'; public const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'; // Used with MensBeam\HTML\DOM\ParentNode::walk public const WALK_ACCEPT = 0x01; public const WALK_REJECT = 0x02; public const WALK_SKIP_CHILDREN = 0x04; public const WALK_STOP = 0x08; public function getNodePath(): ?string; }
属性
innerNode:一个只读属性,返回封装的内部元素。
警告:直接操作此节点可能导致意外行为。此功能仅公开API中提供,以便类可以与预期 \DOMDocument 对象的库(如 MensBeam\Lit)进行交互。
MensBeam\HTML\DOM\Node::getNodePath
从PHP的DOM继承而来。这是一个有用的方法,它返回节点的XPath位置路径。如果成功返回字符串,如果失败返回null。
MensBeam\HTML\DOM\ParentNode
namespace MensBeam\HTML\DOM; partial trait ParentNode { public function walk( ?\Closure $filter = null, bool $includeReferenceNode = false ): \Generator; }
MensBeam\HTML\DOM\ParentNode::walk
在遍历DOM树的同时应用回调过滤器,并以生成器形式产生匹配过滤器的节点。
filter
:用于过滤DOM树的回调方法。必须只返回以下值Node::WALK_ACCEPT
:接受节点。Node::WALK_REJECT
:拒绝节点。Node::WALK_SKIP_CHILDREN
:跳过节点的子节点。Node::WALK_STOP
:停止遍历器。
includeReferenceNode
:在遍历树时包含$this
。
示例
namespace MensBeam\HTML\DOM; $d = new Document(<<<HTML <!DOCTYPE html> <html> <body> <div><!--Ook!--> <div> <div> <!--Eek!--> </div> </div> <!--Ack!--></div> </body> </html> HTML); $walker = $d->walk(function($n) { return ($n instanceof Comment) ? Node::WALK_ACCEPT : Node::WALK_REJECT; }); echo "The following comments were found:\n"; foreach ($walker as $node) { echo "$node\n"; }
输出
The following comments were found:
<!--Ook!-->
<!--Eek!-->
<!--Ack!-->
MensBeam\HTML\DOM\XPathEvaluator
namespace MensBeam\HTML\DOM; partial class XPathEvaluator { public function registerXPathFunctions(Document $document, string|array|null $restrict = null): void; }
MensBeam\HTML\DOM\XPathEvaluator::registerXPathFunctions
将PHP函数注册为XPath函数。它的工作方式与\DOMXPath::registerPhpFunctions
类似,但不需要注册php命名空间。
document
:要注册函数的文档。restrict
:使用此参数仅允许从XPath调用某些函数。此参数可以是字符串(函数名)、函数名数组或null以允许一切。
示例
namespace MensBeam\HTML\DOM; $d = new Document('<!DOCTYPE html><html><body><h1>Ook</h1><p class="subtitle1">Eek?</p><p class="subtitle2">Ook?</p></body></html>'); $e = new XPathEvaluator(); // Register PHP functions (no restrictions) $e->registerXPathFunctions($d); // Call substr function on classes $result = $e->evaluate('//*[php:functionString("substr", @class, 0, 8) = "subtitle"]', $d); echo "Found " . count($result) . " nodes with classes starting with 'subtitle':\n"; foreach ($result as $node) { echo "$node\n"; }
输出
Found 2 nodes with classes starting with 'subtitle':
<p class="subtitle1">Eek?</p>
<p class="subtitle2">Ook?</p>
MensBeam\HTML\DOM\XPathResult
MensBeam\HTML\DOM\XPathResult
实现 \ArrayAccess
、\Countable
和 \Iterator
,并在结果类型为 MensBeam\HTML\DOM\XPathResult::ORDERED_NODE_ITERATOR_TYPE
、MensBeam\HTML\DOM\XPathResult::UNORDERED_NODE_ITERATOR_TYPE
、MensBeam\HTML\DOM\XPathResult::ORDERED_NODE_SNAPSHOT_TYPE
或 MensBeam\HTML\DOM\XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE
时允许像数组一样访问。这不在规范中,但无法简单地迭代结果是不合理的。
partial class XPathResult implements \ArrayAccess, \Countable, \Iterator {}
MensBeam\HTML\DOM\Inner\Document
这是被包装的文档对象。有几点是公开可用的。此功能仅公开API中提供,以便类可以与预期 \DOMDocument 对象的库(如 MensBeam\Lit)进行交互。
namespace MensBeam\HTML\DOM\Inner; partial abstract class Document extends \DOMDocument { public readonly \MensBeam\HTML\DOM\Node $wrapperNode; public function getWrapperNode(\DOMNode $node): ?\MensBeam\HTML\DOM\Node; }
属性
wrapperNode:一个只读属性,返回文档的包装器文档。
MensBeam\HTML\DOM\Inner\Document::getWrapperNode
返回与提供的内部节点对应的包装器节点。如果不存在,则创建。
node
:用于查找/创建包装器节点的内部节点。
限制和与规范的不同之处
本库的主要目标是准确性。然而,由于PHP的DOM的限制、规范中做出的并非适用于PHP库的假设,或者仅仅是由于不切实际,一些更改是必要的。下面似乎有很多与规范不符的地方,但这只是实现细节的详尽列表,其中一些甚至解释了为什么我们遵循规范而不是浏览器所做的那样。
- 任何关于脚本或由于脚本需要(如
Document::createElement
上的ElementCreationOptions
选项字典)的提及将不会实现。 - 由于一个PHP错误严重降低了大型文档的性能,以及考虑到现有的PHP软件,以及当文档处于HTML命名空间时难以解决的奇特的
xmlns
属性错误,HTML文档中的HTML元素在内部被放置在空命名空间而不是HTML命名空间中。然而,从外部来看,它们将显示为具有HTML命名空间。尽管空命名空间元素在HTML规范中不存在,但可以使用DOM创建它们。然而,在本实现中,由于HTML命名空间限制,它们将被视为HTML命名空间元素。 - 在WHATWG HTML DOM扩展规范中,
Document
具有命名属性。在JavaScript中,通过属性表示法(document.ook
)或数组表示法(document['ook']
)访问它们。在PHP中,由于两种表示法之间的区别,这是不切实际的。相反,所有命名属性都需要通过数组表示法($document['ook']
)访问。 - 规范完全是以浏览器为背景编写的,并不关心DOM在浏览器之外的用途。在浏览器中,总是通过解析序列化的标记来创建文档,DOM规范总是假定如此。在这种方式使用此PHP库的情况下是不可能的。创建新
Document
时的默认设置是将其内容类型设置为"application/xml"。在完全通过DOM创建HTML文档时,这不是理想的,因此此实现将默认为"text/html",除非使用XMLDocument
。 - 同样,由于规范假定实现将是浏览器,处理指令应被解析为注释。虽然对于浏览器来说这是合理的,但对于在浏览器之外使用且可能希望操纵它们的DOM库来说是不切实际的;此库在解析文档时将保留它们,但在使用
Element::innerHTML
时将将它们转换为注释。 - 根据规范,实际HTML文档不能在解析器之外创建,除非通过
DOMImplementation::createHTMLDocument
创建。根据规范,DOMImplementation
不能通过其构造函数实例化。在库的用例中,这需要首先创建一个文档,然后通过第一个文档的实现创建一个HTML文档。这是不切实际且愚蠢的,因此在此库(如PHP DOM本身)中,可以独立于文档实例化DOMImplementation
。 - 规范显示
Document
可以通过其构造函数实例化,并显示XMLDocument
从Document
继承。在浏览器中,XMLDocument
不能通过其构造函数实例化。我们将遵循规范,并允许这样做。 - 根据规范,CDATA部分节点、文本节点和文档片段可以通过它们的构造函数独立于
Document::createCDATASectionNode
、Document::createTextNode
和Document::createDocumentFragment
方法分别实例化。由于实现它的难度以及它们在这方面与其他节点类型的差异,目前此库无法做到这一点,并且可能永远不会这样做。 - 按照当前DOM的规范,无法在HTML文档中创建CDATA节点的节点。然而,它们可以在XML文档中创建(并且应该如此)。然而,DOM并没有禁止将CDATA节点的导入到HTML文档中,它们将以这种方式附加到文档上。这似乎是规范维护者的一个明显的疏漏。这个库将允许将CDATA节点的导入到HTML文档中,但将它们转换为文本节点。
- 这个实现将不会实现
NodeIterator
和TreeWalker
API。它们是构想糟糕且不实用的API,很少有人真正使用,因为编写递归循环遍历DOM实际上比使用这些API更容易、更快。向下遍历树已被ParentNode::walk
生成器所取代,通过简单的while或do/while循环可以完成遍历相邻子元素和在DOM树中“月跳”。 - 由于在用户空间中创建它们的纯粹复杂性以及它如何给“核心”DOM中的节点操作带来不必要的困难,因此不会实现所有
Range
API。许多操作都会详细说明在操作节点时如何处理范围,这里需要添加以实现兼容性或基本兼容性——在这个过程中,已经非常重量级的库中的其他所有东西都会变慢。 - 不会实现
DOMParser
和XMLSerializer
API,因为它们既荒谬又具有局限性。例如,DOMParser::parseFromString
不会将文档的字符集设置为UTF-8之外的内容。由于该库的使用方式,它需要能够打印到其他编码。Document::__construct
将接受可选的$source
和$charset
参数,并且有Document::load
和Document::loadFile
方法分别用于从字符串或文件加载DOM。 - 除了
HTMLElement
、HTMLPreElement
、HTMLTemplateElement
、HTMLUnknownElement
、MathMLElement
和SVGElement
之外,不会实现任何特定的派生元素类(如HTMLAnchorElement
或SVGSVGElement
)。前面列出的那些是元素接口算法所必需的。这个库的重点将是在这些元素上——如果有的话——在核心DOM之前。 - 这个类旨在与HTML一起使用,但它将“基本上”按需要与XML一起工作。加载XML使用PHP DOM的XML解析器,该解析器并不完全符合XML规范。编写一个实际符合XML规范的解析器超出了这个库的范围。这个库的一个显著特性是,它不会按照XML规范工作,即元素名称中的Unicode字符。XML允许使用大写字母,而HTML不允许。这个实现的工作方式(因为PHP的DOM在元素名称中根本不支持Unicode)将所有非ASCII字符内部转换为'Uxxxx',这将是有效的现代XML名称。对于XML来说,可能需要一个查找表,但这尚未实现,并且可能因为复杂性而不实现。
- 尽管实现了许多XPath扩展,但只支持XPath 1.0,因为这是PHP DOM支持的XPath。
- 这个库的XPath API——就像库本身的其他部分一样——是一个包装器,它包装了PHP的实现,但它按照规范工作,因此不需要手动注册命名空间。如果指定了
XPathNSResolver
,将查找与前缀关联的命名空间。然而,在XPath中使用PHP函数的访问并未在规范中说明,但可以通过Document::registerXPathFunctions
和XPathEvaluator::registerXPathFunctions
来获取。 XPathEvaluatorBase::evaluate
函数有一个result
参数,用户可以传入一个现有的结果对象来使用。我找不到任何关于其用途的可用文档,且该功能的规范描述模糊不清。因此,目前该功能不执行任何操作,直到可以推断出其需要执行的操作。- 目前 XPath 表达式无法选择使用任何有效非ASCII字符的元素或属性。这是因为这些节点在内部被强制转换为在PHP的DOM中使用,而PHP的DOM不支持这些字符。可以通过强制转换XPath查询中的名称来解决这个问题,但这只能通过XPath解析器才能可靠地完成。为这种边缘情况编写整个XPath解析器并不理想。
- XPath API 本身是一个设计不当的API,使用起来完全不实用,因为对
XPathResult
对象进行任何操作都很繁琐且愚蠢。根据规范,即使结果类型是迭代器类型,也无法遍历结果(为什么叫这个名字呢?)。相反,必须反复调用XPathResult::iterateNext()
方法。此实现将允许将XPathResult
快照或迭代器类型视为数组。