thenavisapp
Version:
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
293 lines (232 loc) • 8.54 kB
text/typescript
import { type TreeAdapterTypeMap, type TreeAdapter, type Token, html } from 'parse5';
import {
type AnyNode,
type ParentNode,
type ChildNode,
Element,
Document,
ProcessingInstruction,
Comment,
Text,
isDirective,
isText,
isComment,
isTag,
} from 'domhandler';
export type Htmlparser2TreeAdapterMap = TreeAdapterTypeMap<
AnyNode,
ParentNode,
ChildNode,
Document,
Document,
Element,
Comment,
Text,
Element,
ProcessingInstruction
>;
function enquoteDoctypeId(id: string): string {
const quote = id.includes('"') ? "'" : '"';
return quote + id + quote;
}
/** @internal */
export function serializeDoctypeContent(name: string, publicId: string, systemId: string): string {
let str = '!DOCTYPE ';
if (name) {
str += name;
}
if (publicId) {
str += ` PUBLIC ${enquoteDoctypeId(publicId)}`;
} else if (systemId) {
str += ' SYSTEM';
}
if (systemId) {
str += ` ${enquoteDoctypeId(systemId)}`;
}
return str;
}
export const adapter: TreeAdapter<Htmlparser2TreeAdapterMap> = {
// Re-exports from domhandler
isCommentNode: isComment,
isElementNode: isTag,
isTextNode: isText,
//Node construction
createDocument(): Document {
const node = new Document([]);
node['x-mode'] = html.DOCUMENT_MODE.NO_QUIRKS;
return node;
},
createDocumentFragment(): Document {
return new Document([]);
},
createElement(tagName: string, namespaceURI: html.NS, attrs: Token.Attribute[]): Element {
const attribs = Object.create(null);
const attribsNamespace = Object.create(null);
const attribsPrefix = Object.create(null);
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
attribs[attrName] = attrs[i].value;
attribsNamespace[attrName] = attrs[i].namespace;
attribsPrefix[attrName] = attrs[i].prefix;
}
const node = new Element(tagName, attribs, []);
node.namespace = namespaceURI;
node['x-attribsNamespace'] = attribsNamespace;
node['x-attribsPrefix'] = attribsPrefix;
return node;
},
createCommentNode(data: string): Comment {
return new Comment(data);
},
createTextNode(value: string): Text {
return new Text(value);
},
//Tree mutation
appendChild(parentNode: ParentNode, newNode: ChildNode): void {
const prev = parentNode.children[parentNode.children.length - 1];
if (prev) {
prev.next = newNode;
newNode.prev = prev;
}
parentNode.children.push(newNode);
newNode.parent = parentNode;
},
insertBefore(parentNode: ParentNode, newNode: ChildNode, referenceNode: ChildNode): void {
const insertionIdx = parentNode.children.indexOf(referenceNode);
const { prev } = referenceNode;
if (prev) {
prev.next = newNode;
newNode.prev = prev;
}
referenceNode.prev = newNode;
newNode.next = referenceNode;
parentNode.children.splice(insertionIdx, 0, newNode);
newNode.parent = parentNode;
},
setTemplateContent(templateElement: Element, contentElement: Document): void {
adapter.appendChild(templateElement, contentElement);
},
getTemplateContent(templateElement: Element): Document {
return templateElement.children[0] as Document;
},
setDocumentType(document: Document, name: string, publicId: string, systemId: string): void {
const data = serializeDoctypeContent(name, publicId, systemId);
let doctypeNode = document.children.find(
(node): node is ProcessingInstruction => isDirective(node) && node.name === '!doctype',
);
if (doctypeNode) {
doctypeNode.data = data ?? null;
} else {
doctypeNode = new ProcessingInstruction('!doctype', data);
adapter.appendChild(document, doctypeNode);
}
doctypeNode['x-name'] = name;
doctypeNode['x-publicId'] = publicId;
doctypeNode['x-systemId'] = systemId;
},
setDocumentMode(document: Document, mode: html.DOCUMENT_MODE): void {
document['x-mode'] = mode;
},
getDocumentMode(document: Document): html.DOCUMENT_MODE {
return document['x-mode'] as html.DOCUMENT_MODE;
},
detachNode(node: ChildNode): void {
if (node.parent) {
const idx = node.parent.children.indexOf(node);
const { prev, next } = node;
node.prev = null;
node.next = null;
if (prev) {
prev.next = next;
}
if (next) {
next.prev = prev;
}
node.parent.children.splice(idx, 1);
node.parent = null;
}
},
insertText(parentNode: ParentNode, text: string): void {
const lastChild = parentNode.children[parentNode.children.length - 1];
if (lastChild && isText(lastChild)) {
lastChild.data += text;
} else {
adapter.appendChild(parentNode, adapter.createTextNode(text));
}
},
insertTextBefore(parentNode: ParentNode, text: string, referenceNode: ChildNode): void {
const prevNode = parentNode.children[parentNode.children.indexOf(referenceNode) - 1];
if (prevNode && isText(prevNode)) {
prevNode.data += text;
} else {
adapter.insertBefore(parentNode, adapter.createTextNode(text), referenceNode);
}
},
adoptAttributes(recipient: Element, attrs: Token.Attribute[]): void {
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
if (recipient.attribs[attrName] === undefined) {
recipient.attribs[attrName] = attrs[i].value;
recipient['x-attribsNamespace']![attrName] = attrs[i].namespace!;
recipient['x-attribsPrefix']![attrName] = attrs[i].prefix!;
}
}
},
//Tree traversing
getFirstChild(node: ParentNode): ChildNode | null {
return node.children[0];
},
getChildNodes(node: ParentNode): ChildNode[] {
return node.children;
},
getParentNode(node: AnyNode): ParentNode | null {
return node.parent;
},
getAttrList(element: Element): Token.Attribute[] {
return element.attributes;
},
//Node data
getTagName(element: Element): string {
return element.name;
},
getNamespaceURI(element: Element): html.NS {
return element.namespace as html.NS;
},
getTextNodeContent(textNode: Text): string {
return textNode.data;
},
getCommentNodeContent(commentNode: Comment): string {
return commentNode.data;
},
getDocumentTypeNodeName(doctypeNode: ProcessingInstruction): string {
return doctypeNode['x-name'] ?? '';
},
getDocumentTypeNodePublicId(doctypeNode: ProcessingInstruction): string {
return doctypeNode['x-publicId'] ?? '';
},
getDocumentTypeNodeSystemId(doctypeNode: ProcessingInstruction): string {
return doctypeNode['x-systemId'] ?? '';
},
//Node types
isDocumentTypeNode(node: AnyNode): node is ProcessingInstruction {
return isDirective(node) && node.name === '!doctype';
},
// Source code location
setNodeSourceCodeLocation(node: AnyNode, location: Token.ElementLocation | null): void {
if (location) {
node.startIndex = location.startOffset;
node.endIndex = location.endOffset;
}
node.sourceCodeLocation = location;
},
getNodeSourceCodeLocation(node: AnyNode): Token.ElementLocation | null | undefined {
return node.sourceCodeLocation;
},
updateNodeSourceCodeLocation(node: AnyNode, endLocation: Token.ElementLocation): void {
if (endLocation.endOffset != null) node.endIndex = endLocation.endOffset;
node.sourceCodeLocation = {
...node.sourceCodeLocation,
...endLocation,
};
},
};