为什么DOMParser会改变空格?

5
以下对DOMParser的使用为什么会导致输出的HTML与输入的不同呢?它会移除DOCTYPE和顶级元素之间的空格,移除文档元素和head之间的空格,并在</body>前添加一个换行符。

I have tested this in Google Chrome, Firefox, and Safari; I also ran the analogous code with JSoup and got exactly the same results. So I'm pretty sure it's not a bug. My current theory is that this is caused by some sort of esoteric parsing rule from a specification somewhere. But there could be other things I misunderstand.

const html = `<!DOCTYPE html>
<html>
  <head>
    <title>1</title>
  </head>
  <body>
    <div>
      Hello, World!
    </div>
  </body>
</html>`;

const setText = function(id,string) {
  document.getElementById(id).appendChild(document.createTextNode(string));
};

const documentToString = function(d) {
  return Array.prototype.slice.call(d.childNodes).map(function(node) {
    if (node.nodeType == node.ELEMENT_NODE) return node.outerHTML;
    if (node.nodeType == node.DOCUMENT_TYPE_NODE) return new XMLSerializer().serializeToString(node);
    throw new TypeError("" + node);
  }).join("");
};

setText("raw", html);
var parsed = new DOMParser().parseFromString(html,"text/html");
setText("parsed", parsed.documentElement.outerHTML);
setText("converted", documentToString(parsed));
setText("xmlserializer", new XMLSerializer().serializeToString(parsed));
#raw, #parsed, #converted, #xmlserializer { white-space: pre; font-family: monospace; }
h1 { font-size: 110%; font-weight: bold; font-family: sans-serif; }
<body>
<h1>Raw string</h1>
<div id="raw"></div>
<h1>Parsed top-level element</h1>
<div id="parsed"></div>
<h1>Using a document-to-string converter</h1>
<div id="converted"></div>
<h1>From XMLSerializer</h1>
<div id="xmlserializer"></div>
</body>


1
标签之间的空格在语法上没有意义。一百万个空格和一个空格是一样的。 - Pointy
3
“但是有许多情况下,人们希望保留格式。”-那么,这时候就需要在文本层级上进行操作,而不是DOM层级上的操作 :-) - misorude
3
XML DOM解析器通常会保留空格,例如将纯空格文本节点放置在其他节点之间。解析文档并串行化后会返回相同的字符串。无论如何,为什么解析器会在文档主体末尾插入空格? - David P. Caldwell
2
对于那些说“空格不重要”的人,解析器并不是在删除空格。它大多数情况下都会保留空格,但在两个特定的位置,每个实现都会删除相同位置上的仅包含空格的文本节点,并插入一个仅包含空格的文本节点。 - David P. Caldwell
1
假设我想处理某人的源代码(并发出新的源代码),而不改变其他部分,例如@Scuzzy... - David P. Caldwell
显示剩余5条评论
1个回答

1
因为这是规格要求的。
HTML并非XML,会发生很多转换。例如,您可能没有意识到,但您的StackSnippet实际上包含了一个重复的<body>标签,因为片段的脚本将HTML部分包装在这样的标签中。该重复项在文档解析时被忽略。

console.log('how many bodies?', document.querySelectorAll('body').length);
<body><body><body></body></body></body>

类似的转换也会发生在TextNodes上。
是的,这不是DOMParser的事情,而是真正的HTML DOM解析器,您在文档解析时也有相同的行为:

frame.src = URL.createObjectURL(new Blob([
`<!DOCTYPE html>
<html>
  <head>
    <title>1</title>
  </head>
  <body>
    <div>
      Hello, World!
    </div>
    <script>      parent.postMessage(document.documentElement.outerHTML, "*");
    <\/script>
  </body>
</html>`], {type: 'text/html'}));

onmessage = e => console.log(e.data);
<iframe id="frame"></iframe>

现在,如果您希望检索完全相同的字符串,则应将其解析为XML:

const html = `<!DOCTYPE html>
<html>
  <head>
    <title>1</title>
  </head>
  <body>
    <div>
      Hello, World!
    </div>
  </body>
</html>`;

const setText = function(id,string) {
  document.getElementById(id).appendChild(document.createTextNode(string));
};

const documentToString = function(d) {
  return Array.prototype.slice.call(d.childNodes).map(function(node) {
    if (node.nodeType == node.ELEMENT_NODE) return node.outerHTML;
    if (node.nodeType == node.DOCUMENT_TYPE_NODE) return new XMLSerializer().serializeToString(node);
    throw new TypeError("" + node);
  }).join("");
};

setText("raw", html);
var parsed = new DOMParser().parseFromString(html,"text/xml");
setText("parsed", parsed.documentElement.outerHTML);
setText("converted", documentToString(parsed));
setText("xmlserializer", new XMLSerializer().serializeToString(parsed));
#raw, #parsed, #converted, #xmlserializer { white-space: pre; font-family: monospace; }
h1 { font-size: 110%; font-weight: bold; font-family: sans-serif; }
<h1>Raw string</h1>
<div id="raw"></div>
<h1>Parsed top-level element</h1>
<div id="parsed"></div>
<h1>Using a document-to-string converter</h1>
<div id="converted"></div>
<h1>From XMLSerializer</h1>
<div id="xmlserializer"></div>


许多有效的HTML文档并不是有效(或格式良好)的XML,因此这不是一个真正的选项。我感谢提供规范的链接,但我不太清楚该规范中的哪些解析规则导致了1.在DOCTYPE和html之间删除空格,2.在html和head之间删除空格,3.在body末尾添加换行符,4.在每个实现中都保留所有其他空格。必须有规则,否则它们不会以相同的高度特定方式行为。 - David P. Caldwell
很抱歉,我无法在您的文档中引用相关规范;-) 对于<DOCTYPE><html>,在这里,第一种情况“U+0020 SPACE忽略该字符。” 此外,我不明白您关于HTML不是有效XML的观点。当然它们都不是,这就是为什么HTML在解析规则方面比XML更加宽松的原因。但是这也意味着您拥有的DOM不是您提供的标记,并且您无法从DOM中检索此标记。 - Kaiido
抱歉,我的!DOCTYPE><html链接实际上是错误的。在标记化过程中,<!DOCTYPE>末尾的>将我们发送到数据状态,其中空格字符只是被发出。在插入时,它将被忽略,在doctype添加后,我们进入before-html-insertion-mode,这里的第三个项目告诉我们忽略该空格字符标记。 - Kaiido

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接