mensbeam/html-dom

v1.0.11 2023-02-19 19:28 UTC

This package is auto-updated.

Last update: 2024-09-11 06:20:52 UTC


README

这是一个用PHP编写的现代DOM库,用于HTML文档。这个库旨在通过用户空间扩展和封装PHP内置的DOM来实现WHATWG的DOM规范WHATWG HTML DOM扩展规范。它存在的原因是PHP的DOM不准确,不足以用于任何HTML,并且极其错误。此实现旨在尽可能修复PHP DOM的不准确之处,添加现代HTML开发所需的功能,并绕过大多数错误。

要求

  • PHP 8.0.2或更高版本,并带有以下扩展
    • dom 扩展
    • ctype 扩展(可选,在解析时使用)
  • Composer 2.0或更高版本

使用

大多数库的完整文档可能不是必需的,因为它主要遵循规范,但由于库的使用方式,有一些明显的不同之处。这些将在下面概述。

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,除非 reformatWhitespacetrue,否则没有效果
    • indentWithSpaces (bool|null):是否使用空格或制表符进行缩进。默认为 true,除非 reformatWhitespacetrue,否则没有效果
    • 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,除非 reformatWhitespacetrue,否则没有效果
    • indentWithSpaces (bool|null):是否使用空格或制表符进行缩进。默认为 true,除非 reformatWhitespacetrue,否则没有效果
    • 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_TYPEMensBeam\HTML\DOM\XPathResult::UNORDERED_NODE_ITERATOR_TYPEMensBeam\HTML\DOM\XPathResult::ORDERED_NODE_SNAPSHOT_TYPEMensBeam\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库的假设,或者仅仅是由于不切实际,一些更改是必要的。下面似乎有很多与规范不符的地方,但这只是实现细节的详尽列表,其中一些甚至解释了为什么我们遵循规范而不是浏览器所做的那样。

  1. 任何关于脚本或由于脚本需要(如Document::createElement上的ElementCreationOptions选项字典)的提及将不会实现。
  2. 由于一个PHP错误严重降低了大型文档的性能,以及考虑到现有的PHP软件,以及当文档处于HTML命名空间时难以解决的奇特的xmlns属性错误,HTML文档中的HTML元素在内部被放置在空命名空间而不是HTML命名空间中。然而,从外部来看,它们将显示为具有HTML命名空间。尽管空命名空间元素在HTML规范中不存在,但可以使用DOM创建它们。然而,在本实现中,由于HTML命名空间限制,它们将被视为HTML命名空间元素。
  3. WHATWG HTML DOM扩展规范中,Document具有命名属性。在JavaScript中,通过属性表示法(document.ook)或数组表示法(document['ook'])访问它们。在PHP中,由于两种表示法之间的区别,这是不切实际的。相反,所有命名属性都需要通过数组表示法($document['ook'])访问。
  4. 规范完全是以浏览器为背景编写的,并不关心DOM在浏览器之外的用途。在浏览器中,总是通过解析序列化的标记来创建文档,DOM规范总是假定如此。在这种方式使用此PHP库的情况下是不可能的。创建新Document时的默认设置是将其内容类型设置为"application/xml"。在完全通过DOM创建HTML文档时,这不是理想的,因此此实现将默认为"text/html",除非使用XMLDocument
  5. 同样,由于规范假定实现将是浏览器,处理指令应被解析为注释。虽然对于浏览器来说这是合理的,但对于在浏览器之外使用且可能希望操纵它们的DOM库来说是不切实际的;此库在解析文档时将保留它们,但在使用Element::innerHTML时将将它们转换为注释。
  6. 根据规范,实际HTML文档不能在解析器之外创建,除非通过DOMImplementation::createHTMLDocument创建。根据规范,DOMImplementation不能通过其构造函数实例化。在库的用例中,这需要首先创建一个文档,然后通过第一个文档的实现创建一个HTML文档。这是不切实际且愚蠢的,因此在此库(如PHP DOM本身)中,可以独立于文档实例化DOMImplementation
  7. 规范显示Document可以通过其构造函数实例化,并显示XMLDocumentDocument继承。在浏览器中,XMLDocument不能通过其构造函数实例化。我们将遵循规范,并允许这样做。
  8. 根据规范,CDATA部分节点、文本节点和文档片段可以通过它们的构造函数独立于Document::createCDATASectionNodeDocument::createTextNodeDocument::createDocumentFragment方法分别实例化。由于实现它的难度以及它们在这方面与其他节点类型的差异,目前此库无法做到这一点,并且可能永远不会这样做。
  9. 按照当前DOM的规范,无法在HTML文档中创建CDATA节点的节点。然而,它们可以在XML文档中创建(并且应该如此)。然而,DOM并没有禁止将CDATA节点的导入到HTML文档中,它们将以这种方式附加到文档上。这似乎是规范维护者的一个明显的疏漏。这个库将允许将CDATA节点的导入到HTML文档中,但将它们转换为文本节点。
  10. 这个实现将不会实现NodeIteratorTreeWalker API。它们是构想糟糕且不实用的API,很少有人真正使用,因为编写递归循环遍历DOM实际上比使用这些API更容易、更快。向下遍历树已被ParentNode::walk生成器所取代,通过简单的while或do/while循环可以完成遍历相邻子元素和在DOM树中“月跳”。
  11. 由于在用户空间中创建它们的纯粹复杂性以及它如何给“核心”DOM中的节点操作带来不必要的困难,因此不会实现所有Range API。许多操作都会详细说明在操作节点时如何处理范围,这里需要添加以实现兼容性或基本兼容性——在这个过程中,已经非常重量级的库中的其他所有东西都会变慢。
  12. 不会实现DOMParserXMLSerializer API,因为它们既荒谬又具有局限性。例如,DOMParser::parseFromString不会将文档的字符集设置为UTF-8之外的内容。由于该库的使用方式,它需要能够打印到其他编码。Document::__construct将接受可选的$source$charset参数,并且有Document::loadDocument::loadFile方法分别用于从字符串或文件加载DOM。
  13. 除了HTMLElementHTMLPreElementHTMLTemplateElementHTMLUnknownElementMathMLElementSVGElement之外,不会实现任何特定的派生元素类(如HTMLAnchorElementSVGSVGElement)。前面列出的那些是元素接口算法所必需的。这个库的重点将是在这些元素上——如果有的话——在核心DOM之前。
  14. 这个类旨在与HTML一起使用,但它将“基本上”按需要与XML一起工作。加载XML使用PHP DOM的XML解析器,该解析器并不完全符合XML规范。编写一个实际符合XML规范的解析器超出了这个库的范围。这个库的一个显著特性是,它不会按照XML规范工作,即元素名称中的Unicode字符。XML允许使用大写字母,而HTML不允许。这个实现的工作方式(因为PHP的DOM在元素名称中根本不支持Unicode)将所有非ASCII字符内部转换为'Uxxxx',这将是有效的现代XML名称。对于XML来说,可能需要一个查找表,但这尚未实现,并且可能因为复杂性而不实现。
  15. 尽管实现了许多XPath扩展,但只支持XPath 1.0,因为这是PHP DOM支持的XPath。
  16. 这个库的XPath API——就像库本身的其他部分一样——是一个包装器,它包装了PHP的实现,但它按照规范工作,因此不需要手动注册命名空间。如果指定了XPathNSResolver,将查找与前缀关联的命名空间。然而,在XPath中使用PHP函数的访问并未在规范中说明,但可以通过Document::registerXPathFunctionsXPathEvaluator::registerXPathFunctions来获取。
  17. XPathEvaluatorBase::evaluate 函数有一个 result 参数,用户可以传入一个现有的结果对象来使用。我找不到任何关于其用途的可用文档,且该功能的规范描述模糊不清。因此,目前该功能不执行任何操作,直到可以推断出其需要执行的操作。
  18. 目前 XPath 表达式无法选择使用任何有效非ASCII字符的元素或属性。这是因为这些节点在内部被强制转换为在PHP的DOM中使用,而PHP的DOM不支持这些字符。可以通过强制转换XPath查询中的名称来解决这个问题,但这只能通过XPath解析器才能可靠地完成。为这种边缘情况编写整个XPath解析器并不理想。
  19. XPath API 本身是一个设计不当的API,使用起来完全不实用,因为对 XPathResult 对象进行任何操作都很繁琐且愚蠢。根据规范,即使结果类型是迭代器类型,也无法遍历结果(为什么叫这个名字呢?)。相反,必须反复调用 XPathResult::iterateNext() 方法。此实现将允许将 XPathResult 快照或迭代器类型视为数组。