用于 PHP 8.4 的有主见的 HTML 序列化器

几天前,作者为 PHP 8.4 的新Dom\\HTMLDocument类写了一个简陋的美化打印器链接,之后重新编写使其更快且更符合风格。它能将给定的 HTML 代码:

<html lang="en-GB"><head><title id="something">Test</title></head><body><h1 class="top upper">Testing</h1><main><p>Some <em>HTML</em> and an <img src="example.png" alt="Alternate Text"></p>Text not in an element<ol><li>List</li><li>Another list</li></ol></main></body></html>

转换为:

<!doctype html>
<html lang=en-GB>
<head>
    <title id=something>Test</title>
</head>
<body>
    <h1 class="top upper">Testing</h1>
    <main>
        <p>
            Some 
            <em>HTML</em> 
            and an 
            <img src=example.png alt="Alternate Text">
        </p>
        Text not in an element
        <ol>
            <li>List</li>
            <li>Another list</li>
        </ol>
    </main>
</body>
</html>

作者称其“有主见”,因为它做了以下几点:

  • 属性除非必要否则不使用引号。
  • 每个元素都有逻辑缩进。
  • CSS 和 JS 的文本内容不变,不进行美化、压缩或正确性检查。
  • 元素的文本内容可能有额外的换行和制表符,浏览器通常会忽略多个空白字符,除非 CSS 另有规定,但这会破坏包含标记的<pre>块。
    该美化打印器主要用于使标记易于阅读,因为根据专家观点,程序应主要为人们阅读而编写。
    它的工作原理如下:

    当元素不是元素时?当它是一个空元素时!

    现代 HTML 有“空元素”的概念,如<a>必须有闭合标签,但空元素不需要。该打印器维护了一个不能显式关闭的元素列表。

    $void_elements = [
      "area",
      "base",
      "br",
      "col",
      "embed",
      "hr",
      "img",
      "input",
      "link",
      "meta",
      "param",
      "source",
      "track",
      "wbr",
    ];

    制表符🆚空格

    使用制表符,用户可根据个人偏好设置制表宽度,不会与语义上重要的空白混淆。

    $indent_character = "\t";

    设置 DOM

    新的HTMLDocument对使用过之前版本的人来说应该很熟悉。

    $html = '<html lang="en-GB"><head><title id="something">Test</title></head><body><h1 class="top upper">Testing</h1><main><p>Some <em>HTML</em> and an <img src="example.png" alt="Alternate Text"></p>Text not in an element<ol><li>List</li><li>Another list</li></ol></main></body></html>';
    $dom = Dom\HTMLDocument::createFromString( $html, LIBXML_NOERROR, "UTF-8" );

    若不想自动添加<head><body>元素,可使用LIBXML_HTML_NOIMPLIED标志。

    引号引还是不引?

    传统上 HTML 属性需要引号,但现代 HTML 允许属性在不包含特定字符时不使用引号。该函数用于检查属性值是否需要引号。

    function value_unquoted( $haystack ) {
      // 必须不包含特定字符
      $needles = [
          // https://infra.spec.whatwg.org/#ascii-whitespace
          "\t", "\n", "\f", "\n", " ",
          // https://html.spec.whatwg.org/multipage/syntax.html#unquoted
          "\"", "'", "=", "<", ">", "`"
      ];
      foreach ( $needles as $needle ) {
          if ( str_contains( $haystack, $needle ) ) {
              return false;
          }
      }
      // 必须不为空
      if ( $haystack == null ) {
          return false;
      }
      return true;
    }

    递归再递归

    遍历 DOM 树,正确缩进打印打开的元素及其属性,若有文本内容则打印,若元素需要关闭则打印相应的缩进。

    function serializeHTML( $node, $treeIndex = 0, $output = "") {
      global $indent_character, $preserve_internal_whitespace, $void_elements;
      // 手动添加 doctype
      if ( $output == "" ) {
          $output.= "<!doctype html>\n";
      }
      if( property_exists( $node, "localName" ) ) {
          // 这是一个元素
          // 获取所有属性
          $attributes = "";
          if ( property_exists($node, "attributes")) {
              foreach( $node->attributes as $attribute ) {
                  $value = $attribute->nodeValue;
                  // 只有值包含特定字符时才添加引号
                  $quote = value_unquoted( $value )? "" : "\"";
                  $attributes.= " {$attribute->nodeName}={$quote}{$value}{$quote}";
              }
          }
          // 打印打开的元素和所有属性
          $output.= "<{$node->localName}{$attributes}>";
      } else if( property_exists( $node, "nodeName" ) &&  $node->nodeName == "#comment" ) {
          // 注释
          $output.= "<!-- {$node->textContent} -->";
      }
      // 增加缩进
      $treeIndex++;
      $tabStart = "\n". str_repeat( $indent_character, $treeIndex );
      $tabEnd = "\n". str_repeat( $indent_character, $treeIndex - 1);
      // 节点是否有子元素?
      if( property_exists( $node, "childElementCount" ) && $node->childElementCount > 0 ) {
          // 循环遍历子元素
          $i=0;
          while( $childNode = $node->childNodes->item( $i++ ) ) {
              // 这是一个文本节点?
              if ($childNode->nodeType == 3 ) {
                  // 只有在内容中没有 HTML 时才打印输出,忽略空元素
                  if (
                     !str_contains( $childNode->textContent, "<" ) &&
                      property_exists( $childNode, "localName" ) &&
                     !in_array( $childNode->localName, $void_elements )
                  ) {
                      $output.= $tabStart. $childNode->textContent;
                  }
              } else {
                  $output.= $tabStart;
              }
              // 递归缩进所有子元素
              $output = serializeHTML( $childNode, $treeIndex, $output );
          };
          // 后缀添加"\n"和适当数量的"\t"
          $output.= "{$tabEnd}";
      } else if ( property_exists( $node, "childElementCount" ) && property_exists( $node, "innerHTML" ) ) {
          // 如果没有子元素且节点包含内容,则打印内容
          $output.= $node->innerHTML;
      }
      // 关闭元素,除非它是一个空元素
      if( property_exists( $node, "localName" ) &&!in_array( $node->localName, $void_elements ) ) {
          $output.= "</{$node->localName}>";
      }
      // 返回完全缩进的 HTML 字符串
      return $output;
    }

    打印出来

    序列化后的字符串硬编码了<!doctype html>,通过echo serializeHTML( $dom->documentElement );显示完整的 HTML。

    下一步

    请在 GitLab 上提出任何问题或留下评论。

阅读 7
0 条评论