|
|
@@ -22,28 +22,32 @@
|
|
|
|
|
|
this.serializer = this.serializer || (() => {
|
|
|
|
|
|
- //omittedEndTag.followings && omittedEndTag.followings.includes(nextSibling.tagName.toLowerCase())
|
|
|
-
|
|
|
const SELF_CLOSED_TAG_NAMES = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
|
|
|
+
|
|
|
+ // see https://www.w3.org/TR/html5/syntax.html#optional-tags
|
|
|
+ const OMITTED_START_TAGS = [
|
|
|
+ { tagName: "head", accept: element => !element.childNodes.length || element.childNodes[0].nodeType == Node.ELEMENT_NODE },
|
|
|
+ { tagName: "body", accept: element => !element.childNodes.length },
|
|
|
+ ];
|
|
|
const OMITTED_END_TAGS = [
|
|
|
- { tagName: "html", acceptNode: next => !next || next.nodeType != Node.COMMENT_NODE },
|
|
|
- { tagName: "head", acceptNode: next => !next || next.nodeType != Node.COMMENT_NODE && (next.nodeType != Node.TEXT_NODE || !spaceFirstCharacter(next.textContent)) },
|
|
|
- { tagName: "body", acceptNode: next => !next || next.nodeType != Node.COMMENT_NODE },
|
|
|
- { tagName: "li", acceptNode: next => !next || ["LI"].includes(next.tagName) },
|
|
|
- { tagName: "dt", acceptNode: next => !next || ["DT", "DD"].includes(next.tagName) },
|
|
|
- { tagName: "dd", acceptNode: next => !next || ["DT", "DD"].includes(next.tagName) },
|
|
|
- { tagName: "rt", acceptNode: next => !next || ["RT", "RP"].includes(next.tagName) },
|
|
|
- { tagName: "rp", acceptNode: next => !next || ["RT", "RP"].includes(next.tagName) },
|
|
|
- { tagName: "optgroup", acceptNode: next => !next || ["OPTGROUP"].includes(next.tagName) },
|
|
|
- { tagName: "option", acceptNode: next => !next || ["OPTION", "OPTGROUP"].includes(next.tagName) },
|
|
|
- { tagName: "colgroup", acceptNode: next => !next || next.nodeType != Node.COMMENT_NODE && (next.nodeType != Node.TEXT_NODE || !spaceFirstCharacter(next.textContent)) },
|
|
|
- { tagName: "caption", acceptNode: next => !next || next.nodeType != Node.COMMENT_NODE && (next.nodeType != Node.TEXT_NODE || !spaceFirstCharacter(next.textContent)) },
|
|
|
- { tagName: "thead", acceptNode: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
|
|
|
- { tagName: "tbody", acceptNode: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
|
|
|
- { tagName: "tfoot", acceptNode: next => !next },
|
|
|
- { tagName: "tr", acceptNode: next => !next || ["TR"].includes(next.tagName) },
|
|
|
- { tagName: "td", acceptNode: next => !next || ["TD", "TH"].includes(next.tagName) },
|
|
|
- { tagName: "th", acceptNode: next => !next || ["TD", "TH"].includes(next.tagName) },
|
|
|
+ { tagName: "html", accept: next => !next || next.nodeType != Node.COMMENT_NODE },
|
|
|
+ { tagName: "head", accept: next => !next || next.nodeType != Node.COMMENT_NODE && (next.nodeType != Node.TEXT_NODE || !spaceFirstCharacter(next.textContent)) },
|
|
|
+ { tagName: "body", accept: next => !next || next.nodeType != Node.COMMENT_NODE },
|
|
|
+ { tagName: "li", accept: next => !next || ["LI"].includes(next.tagName) },
|
|
|
+ { tagName: "dt", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
|
|
|
+ { tagName: "dd", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
|
|
|
+ { tagName: "rt", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
|
|
|
+ { tagName: "rp", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
|
|
|
+ { tagName: "optgroup", accept: next => !next || ["OPTGROUP"].includes(next.tagName) },
|
|
|
+ { tagName: "option", accept: next => !next || ["OPTION", "OPTGROUP"].includes(next.tagName) },
|
|
|
+ { tagName: "colgroup", accept: next => !next || next.nodeType != Node.COMMENT_NODE && (next.nodeType != Node.TEXT_NODE || !spaceFirstCharacter(next.textContent)) },
|
|
|
+ { tagName: "caption", accept: next => !next || next.nodeType != Node.COMMENT_NODE && (next.nodeType != Node.TEXT_NODE || !spaceFirstCharacter(next.textContent)) },
|
|
|
+ { tagName: "thead", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
|
|
|
+ { tagName: "tbody", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
|
|
|
+ { tagName: "tfoot", accept: next => !next },
|
|
|
+ { tagName: "tr", accept: next => !next || ["TR"].includes(next.tagName) },
|
|
|
+ { tagName: "td", accept: next => !next || ["TD", "TH"].includes(next.tagName) },
|
|
|
+ { tagName: "th", accept: next => !next || ["TD", "TH"].includes(next.tagName) },
|
|
|
];
|
|
|
|
|
|
return {
|
|
|
@@ -90,46 +94,50 @@ this.serializer = this.serializer || (() => {
|
|
|
|
|
|
function serializeElement(element) {
|
|
|
const tagName = element.tagName.toLowerCase();
|
|
|
- let content = "<" + tagName;
|
|
|
- Array.from(element.attributes).forEach(attribute => {
|
|
|
- const name = attribute.name.replace(/["'>/=]/g, "");
|
|
|
- let value = attribute.value;
|
|
|
- if (name == "class") {
|
|
|
- value = Array.from(element.classList).map(className => className.trim()).join(" ");
|
|
|
- }
|
|
|
- value = value.replace(/&/g, "&").replace(/\u00a0/g, " ").replace(/"/g, """);
|
|
|
- const validUnquotedValue = value.match(/^[^ \t\n\f\r"'`=<>]+$/);
|
|
|
- content += " ";
|
|
|
- if (!attribute.namespace) {
|
|
|
- content += name;
|
|
|
- } else if (attribute.namespaceURI == "http://www.w3.org/XML/1998/namespace") {
|
|
|
- content += "xml:" + name;
|
|
|
- } else if (attribute.namespaceURI == "http://www.w3.org/2000/xmlns/") {
|
|
|
- if (name !== "xmlns") {
|
|
|
- content += "xmlns:";
|
|
|
+ const omittedStartTag = OMITTED_START_TAGS.find(omittedStartTag => tagName == omittedStartTag.tagName && omittedStartTag.accept(element));
|
|
|
+ let content = "";
|
|
|
+ if (!omittedStartTag || element.attributes.length) {
|
|
|
+ content = "<" + tagName;
|
|
|
+ Array.from(element.attributes).forEach(attribute => {
|
|
|
+ const name = attribute.name.replace(/["'>/=]/g, "");
|
|
|
+ let value = attribute.value;
|
|
|
+ if (name == "class") {
|
|
|
+ value = Array.from(element.classList).map(className => className.trim()).join(" ");
|
|
|
}
|
|
|
- content += name;
|
|
|
- } else if (attribute.namespaceURI == "http://www.w3.org/1999/xlink") {
|
|
|
- content += "xlink:" + name;
|
|
|
- } else {
|
|
|
- content += name;
|
|
|
- }
|
|
|
- if (value != "") {
|
|
|
- content += "=";
|
|
|
- if (!validUnquotedValue) {
|
|
|
- content += "\"";
|
|
|
+ value = value.replace(/&/g, "&").replace(/\u00a0/g, " ").replace(/"/g, """);
|
|
|
+ const validUnquotedValue = value.match(/^[^ \t\n\f\r"'`=<>]+$/);
|
|
|
+ content += " ";
|
|
|
+ if (!attribute.namespace) {
|
|
|
+ content += name;
|
|
|
+ } else if (attribute.namespaceURI == "http://www.w3.org/XML/1998/namespace") {
|
|
|
+ content += "xml:" + name;
|
|
|
+ } else if (attribute.namespaceURI == "http://www.w3.org/2000/xmlns/") {
|
|
|
+ if (name !== "xmlns") {
|
|
|
+ content += "xmlns:";
|
|
|
+ }
|
|
|
+ content += name;
|
|
|
+ } else if (attribute.namespaceURI == "http://www.w3.org/1999/xlink") {
|
|
|
+ content += "xlink:" + name;
|
|
|
+ } else {
|
|
|
+ content += name;
|
|
|
}
|
|
|
- content += value;
|
|
|
- if (!validUnquotedValue) {
|
|
|
- content += "\"";
|
|
|
+ if (value != "") {
|
|
|
+ content += "=";
|
|
|
+ if (!validUnquotedValue) {
|
|
|
+ content += "\"";
|
|
|
+ }
|
|
|
+ content += value;
|
|
|
+ if (!validUnquotedValue) {
|
|
|
+ content += "\"";
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- });
|
|
|
- content += ">";
|
|
|
+ });
|
|
|
+ content += ">";
|
|
|
+ }
|
|
|
Array.from(element.childNodes).forEach(childNode => content += serialize(childNode));
|
|
|
const omittedEndTag = OMITTED_END_TAGS.find(omittedEndTag => {
|
|
|
const nextSibling = element.nextSibling;
|
|
|
- return tagName == omittedEndTag.tagName && omittedEndTag.acceptNode(nextSibling);
|
|
|
+ return tagName == omittedEndTag.tagName && omittedEndTag.accept(nextSibling);
|
|
|
});
|
|
|
if (!omittedEndTag && !SELF_CLOSED_TAG_NAMES.includes(tagName)) {
|
|
|
content += "</" + tagName + ">";
|