Files
test-repo/src/language-html/parser-html.js
T

151 lines
3.7 KiB
JavaScript

"use strict";
const parseFrontMatter = require("../utils/front-matter");
const { HTML_TAGS, HTML_ELEMENT_ATTRIBUTES } = require("./utils");
function parse(text /*, parsers, opts*/) {
const { frontMatter, content } = parseFrontMatter(text);
// Inline the require to avoid loading all the JS if we don't use it
const Parser = require("htmlparser2/lib/Parser");
const DomHandler = require("domhandler");
/**
* modifications:
* - empty attributes (e.g., `<tag attr>`) are parsed as `{ [attr]: null }` instead of `{ [attr]: "" }`
* - trigger `Handler#onselfclosingtag()`
*/
class CustomParser extends Parser {
constructor(cbs, options) {
super(cbs, options);
this._attribvalue = null;
}
onattribdata(value) {
if (this._attribvalue === null) {
this._attribvalue = "";
}
super.onattribdata(value);
}
onattribend() {
super.onattribend();
this._attribvalue = null;
}
onselfclosingtag() {
if (this._options.xmlMode || this._options.recognizeSelfClosing) {
const name = this._tagname;
this.onopentagend();
if (this._stack[this._stack.length - 1] === name) {
this._cbs.onselfclosingtag();
this._cbs.onclosetag(name);
this._stack.pop();
}
} else {
this.onopentagend();
}
}
}
/**
* modifications:
* - add `selfClosing` field
*/
class CustomDomHandler extends DomHandler {
onselfclosingtag() {
this._tagStack[this._tagStack.length - 1].selfClosing = true;
}
}
const handler = new CustomDomHandler({
withStartIndices: true,
withEndIndices: true
});
new CustomParser(handler, {
lowerCaseTags: true, // preserve lowercase tag names to avoid false check in htmlparser2 and apply the lowercasing later
lowerCaseAttributeNames: false,
recognizeSelfClosing: true
}).end(content);
const ast = normalize({ type: "root", children: handler.dom }, text);
if (frontMatter) {
ast.children.unshift(frontMatter);
}
return ast;
}
function normalize(node, text) {
delete node.parent;
delete node.next;
delete node.prev;
if (node.type === "tag" && !(node.name in HTML_TAGS)) {
node.name = text.slice(
node.startIndex + 1, // <
node.startIndex + 1 + node.name.length
);
}
if (node.attribs) {
const CURRENT_HTML_ELEMENT_ATTRIBUTES =
HTML_ELEMENT_ATTRIBUTES[node.name] || Object.create(null);
const attributes = Object.keys(node.attribs).map(attributeKey => {
const lowerCaseAttributeKey = attributeKey.toLowerCase();
return {
type: "attribute",
key:
lowerCaseAttributeKey in HTML_ELEMENT_ATTRIBUTES["*"] ||
lowerCaseAttributeKey in CURRENT_HTML_ELEMENT_ATTRIBUTES
? lowerCaseAttributeKey
: attributeKey,
value: node.attribs[attributeKey]
};
});
const attribs = Object.create(null);
for (const attribute of attributes) {
attribs[attribute.key] = attribute.value;
}
node.attribs = attribs;
node.attributes = attributes;
}
if (node.children) {
node.children = node.children.map(child => normalize(child, text));
}
if (
node.type === "tag" &&
node.name === "textarea" &&
node.children.length === 1 &&
node.children[0].type === "text" &&
node.children[0].data === "\n" &&
!/<\/textarea>$/.test(text.slice(locStart(node), locEnd(node)))
) {
node.children = [];
}
return node;
}
function locStart(node) {
return node.startIndex;
}
function locEnd(node) {
return node.endIndex + 1;
}
module.exports = {
parsers: {
html: {
parse,
astFormat: "htmlparser2",
locStart,
locEnd
}
}
};