Browse Source

Move utils

custom-code-parser
Artur Arseniev 3 months ago
parent
commit
ff7e90013e
  1. 241
      packages/core/src/parser/model/ParserHtml.ts
  2. 227
      packages/core/src/parser/model/utils.ts

241
packages/core/src/parser/model/ParserHtml.ts

@ -2,13 +2,12 @@ import { each, isArray, isFunction, isUndefined, result } from 'underscore';
import { ObjectAny, ObjectStrings } from '../../common';
import { ComponentDefinitionDefined, ComponentStackItem } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';
import { doctypeToString, processDataGjsAttributeHyphen } from '../../utils/dom';
import { processDataGjsAttributeHyphen } from '../../utils/dom';
import { isDef } from '../../utils/mixins';
import { HTMLParserOptions, ParseNodeOptions, ParserConfig } from '../config/config';
import {
HTMLParseResult,
ParsedElementNode,
ParsedNode,
ParsedNodeMeta,
ParsedNodeNamespace,
ParsedNodeType,
@ -17,6 +16,19 @@ import {
} from '../types';
import BrowserParserHtml from './BrowserParserHtml';
import { getSyntheticElementCtor } from './SyntheticElement';
import {
createElementNode,
createFragmentRoot,
domDocumentToParsedNode,
domRootToFragment,
findChildElement,
getNodeChildNodes,
getNodeTextContent,
getSourceNode,
normalizeDocumentRoot,
removeElementNodes,
sanitizeNode,
} from './utils';
const modelAttrStart = 'data-gjs-';
@ -27,226 +39,6 @@ interface ParserHtmlInternalOptions extends ParseNodeOptions {
const hasOwn = (obj: object, key: string) => Object.prototype.hasOwnProperty.call(obj, key);
const getNodeChildNodes = (node: ParsedNodeMeta) => node.childNodes || [];
const getNodeTagName = (node: ParsedNodeMeta) => `${node.tagName || ''}`.toLowerCase();
const getSourceNode = (node: ParsedNodeMeta) => node.__domNode || node;
const getDomChildNodes = (node: Node) => {
const template = node as HTMLTemplateElement;
const childNodes = template.content?.childNodes || node.childNodes || [];
return Array.from(childNodes);
};
const cloneParsedNode = (node: ParsedNode): ParsedNodeMeta => {
const cloned: ParsedNodeMeta = {
nodeType: node.nodeType,
};
node.tagName && (cloned.tagName = node.tagName);
node.namespaceURI && (cloned.namespaceURI = node.namespaceURI);
node.textContent !== undefined && (cloned.textContent = node.textContent);
node.attributes && (cloned.attributes = { ...node.attributes });
node.childNodes && (cloned.childNodes = node.childNodes.map((child) => cloneParsedNode(child)));
const meta = node as ParsedNodeMeta & { doctype?: string; selfClosing?: boolean };
meta.__doctype && (cloned.__doctype = meta.__doctype);
meta.doctype && (cloned.__doctype = meta.doctype);
meta.__selfClosing && (cloned.__selfClosing = meta.__selfClosing);
meta.selfClosing && (cloned.__selfClosing = meta.selfClosing);
meta.__boolAttributes && (cloned.__boolAttributes = [...meta.__boolAttributes]);
return cloned;
};
const createElementNode = (tagName: string, childNodes: ParsedNodeMeta[] = []): ParsedNodeMeta => ({
nodeType: ParsedNodeType.element,
tagName,
namespaceURI: ParsedNodeNamespace.html,
childNodes,
});
const createFragmentRoot = (nodes: ParsedNode[]): ParsedNodeMeta => ({
nodeType: ParsedNodeType.fragment,
childNodes: nodes.map((node) => cloneParsedNode(node)),
});
const appendChildElement = (node: ParsedNodeMeta, tagName: string) => {
const child = createElementNode(tagName);
node.childNodes = [...getNodeChildNodes(node), child];
return child;
};
const findChildElement = (node: ParsedNodeMeta, tagName: string) =>
getNodeChildNodes(node).find((child) => getNodeTagName(child) === tagName);
const getNodeTextContent = (node: ParsedNodeMeta): string => {
if (node.nodeType === ParsedNodeType.text || node.nodeType === ParsedNodeType.comment) {
return node.textContent ?? '';
}
if (node.textContent && !node.childNodes?.length) {
return node.textContent;
}
return getNodeChildNodes(node)
.map((child) => getNodeTextContent(child))
.join('');
};
const removeElementNodes = (root: ParsedNodeMeta, tagName: string) => {
const removed: ParsedNodeMeta[] = [];
const remove = (node: ParsedNodeMeta) => {
if (!node.childNodes?.length) return;
const nextNodes: ParsedNodeMeta[] = [];
node.childNodes.forEach((child) => {
if (getNodeTagName(child) === tagName) {
removed.push(child);
return;
}
remove(child);
nextNodes.push(child);
});
node.childNodes = nextNodes;
};
remove(root);
return removed;
};
const sanitizeNode = (node: ParsedNodeMeta, opts: HTMLParserOptions) => {
const attrs = node.attributes || {};
const cleanAttrs: Record<string, string> = {};
each(attrs, (value, name) => {
const attrValue = `${value}`;
const isUnsafeAttr = !opts.allowUnsafeAttr && name.startsWith('on');
const isUnsafeValue = !opts.allowUnsafeAttrValue && attrValue.startsWith('javascript:');
if (!isUnsafeAttr && !isUnsafeValue) {
cleanAttrs[name] = attrValue;
}
});
if (Object.keys(cleanAttrs).length) {
node.attributes = cleanAttrs;
} else {
delete node.attributes;
}
each(getNodeChildNodes(node), (child) => sanitizeNode(child, opts));
};
const domDocumentToParsedNode = (doc: Document): ParsedNodeMeta => ({
nodeType: ParsedNodeType.document,
__domNode: doc,
__doctype: doctypeToString(doc.doctype),
childNodes: doc.documentElement ? [domToParsedNode(doc.documentElement)] : [],
});
const domToParsedNode = (node: Node): ParsedNodeMeta => {
if (node.nodeType === ParsedNodeType.document) {
return domDocumentToParsedNode(node as Document);
}
const parsedNode: ParsedNodeMeta = {
nodeType: node.nodeType,
__domNode: node,
};
if (node.nodeType === ParsedNodeType.text || node.nodeType === ParsedNodeType.comment) {
parsedNode.textContent = node.textContent ?? '';
}
if (node.nodeType === ParsedNodeType.element) {
const el = node as HTMLElement;
parsedNode.tagName = el.tagName || '';
parsedNode.namespaceURI = el.namespaceURI || undefined;
const attrs = el.attributes || [];
if (attrs.length) {
parsedNode.attributes = {};
}
const boolAttributes: string[] = [];
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
parsedNode.attributes![attr.nodeName] = attr.nodeValue || '';
if (attr.nodeValue === '' && (el as any)[attr.nodeName] === true) {
boolAttributes.push(attr.nodeName);
}
}
boolAttributes.length && (parsedNode.__boolAttributes = boolAttributes);
const childNodes = getDomChildNodes(el);
childNodes.length && (parsedNode.childNodes = childNodes.map((child) => domToParsedNode(child)));
parsedNode.__selfClosing = `${el.outerHTML || ''}`.slice(-2) === '/>';
}
return parsedNode;
};
const domRootToFragment = (root: HTMLElement): ParsedNodeMeta => ({
nodeType: ParsedNodeType.fragment,
__domNode: root,
childNodes: getDomChildNodes(root).map((node) => domToParsedNode(node)),
});
const normalizeDocumentRoot = (nodes: ParsedNode[]) => {
const flatNodes = nodes
.map((node) => cloneParsedNode(node))
.flatMap((node) => (node.nodeType === ParsedNodeType.fragment ? getNodeChildNodes(node) : [node]));
const documentNode = flatNodes.find((node) => node.nodeType === ParsedNodeType.document);
if (documentNode) {
return documentNode;
}
const htmlNode = flatNodes.find(
(node) => node.nodeType === ParsedNodeType.element && getNodeTagName(node) === 'html',
);
const documentRoot: ParsedNodeMeta = {
nodeType: ParsedNodeType.document,
childNodes: [],
};
if (htmlNode) {
const extraNodes = flatNodes.filter((node) => node !== htmlNode);
if (extraNodes.length) {
const bodyNode = findChildElement(htmlNode, 'body') || appendChildElement(htmlNode, 'body');
bodyNode.childNodes = [...getNodeChildNodes(bodyNode), ...extraNodes];
}
documentRoot.childNodes = [htmlNode];
return documentRoot;
}
const remaining = [...flatNodes];
const headIndex = remaining.findIndex(
(node) => node.nodeType === ParsedNodeType.element && getNodeTagName(node) === 'head',
);
const bodyIndex = remaining.findIndex(
(node) => node.nodeType === ParsedNodeType.element && getNodeTagName(node) === 'body',
);
const headNode = headIndex >= 0 ? remaining.splice(headIndex, 1)[0] : undefined;
const bodyNode =
bodyIndex >= 0
? remaining.splice(bodyIndex > headIndex && headIndex >= 0 ? bodyIndex - 1 : bodyIndex, 1)[0]
: undefined;
const htmlRoot = createElementNode('html');
const htmlChildren: ParsedNodeMeta[] = [];
headNode && htmlChildren.push(headNode);
const normalizedBody = bodyNode || createElementNode('body');
normalizedBody.childNodes = [...getNodeChildNodes(normalizedBody), ...remaining];
htmlChildren.push(normalizedBody);
htmlRoot.childNodes = htmlChildren;
documentRoot.childNodes = [htmlRoot];
return documentRoot;
};
const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boolean } = {}) => {
return {
compTypes: [] as ComponentStackItem[],
@ -626,12 +418,11 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
const inputOptions = { input: isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str };
Parser?.__emitEvent(ParserEvents.htmlBefore, inputOptions);
const { input } = inputOptions;
const parseRes = this.__parseInput(input, options, cf, parserCode);
const root = parseRes.root;
const { root, isParsedMode } = this.__parseInput(input, options, cf, parserCode);
const parserConfig = Parser?.getConfig() || config;
const parseOptions: ParserHtmlInternalOptions = {
...cf,
__parsedMode: parseRes.isParsedMode,
__parsedMode: isParsedMode,
__syntheticElementCtor: getSyntheticElementCtor(parserConfig.customSyntheticElement),
};

227
packages/core/src/parser/model/utils.ts

@ -0,0 +1,227 @@
import { each } from 'underscore';
import { doctypeToString } from '../../utils/dom';
import { HTMLParserOptions } from '../config/config';
import { ParsedNode, ParsedNodeMeta, ParsedNodeNamespace, ParsedNodeType } from '../types';
export const getNodeChildNodes = (node: ParsedNodeMeta) => node.childNodes || [];
export const getNodeTagName = (node: ParsedNodeMeta) => `${node.tagName || ''}`.toLowerCase();
export const getSourceNode = (node: ParsedNodeMeta) => node.__domNode || node;
export const getDomChildNodes = (node: Node) => {
const template = node as HTMLTemplateElement;
const childNodes = template.content?.childNodes || node.childNodes || [];
return Array.from(childNodes);
};
export const cloneParsedNode = (node: ParsedNode): ParsedNodeMeta => {
const cloned: ParsedNodeMeta = {
nodeType: node.nodeType,
};
node.tagName && (cloned.tagName = node.tagName);
node.namespaceURI && (cloned.namespaceURI = node.namespaceURI);
node.textContent !== undefined && (cloned.textContent = node.textContent);
node.attributes && (cloned.attributes = { ...node.attributes });
node.childNodes && (cloned.childNodes = node.childNodes.map((child) => cloneParsedNode(child)));
const meta = node as ParsedNodeMeta & { doctype?: string; selfClosing?: boolean };
meta.__doctype && (cloned.__doctype = meta.__doctype);
meta.doctype && (cloned.__doctype = meta.doctype);
meta.__selfClosing && (cloned.__selfClosing = meta.__selfClosing);
meta.selfClosing && (cloned.__selfClosing = meta.selfClosing);
meta.__boolAttributes && (cloned.__boolAttributes = [...meta.__boolAttributes]);
return cloned;
};
export const createElementNode = (tagName: string, childNodes: ParsedNodeMeta[] = []): ParsedNodeMeta => ({
nodeType: ParsedNodeType.element,
tagName,
namespaceURI: ParsedNodeNamespace.html,
childNodes,
});
export const createFragmentRoot = (nodes: ParsedNode[]): ParsedNodeMeta => ({
nodeType: ParsedNodeType.fragment,
childNodes: nodes.map((node) => cloneParsedNode(node)),
});
export const appendChildElement = (node: ParsedNodeMeta, tagName: string) => {
const child = createElementNode(tagName);
node.childNodes = [...getNodeChildNodes(node), child];
return child;
};
export const findChildElement = (node: ParsedNodeMeta, tagName: string) =>
getNodeChildNodes(node).find((child) => getNodeTagName(child) === tagName);
export const getNodeTextContent = (node: ParsedNodeMeta): string => {
if (node.nodeType === ParsedNodeType.text || node.nodeType === ParsedNodeType.comment) {
return node.textContent ?? '';
}
if (node.textContent && !node.childNodes?.length) {
return node.textContent;
}
return getNodeChildNodes(node)
.map((child) => getNodeTextContent(child))
.join('');
};
export const removeElementNodes = (root: ParsedNodeMeta, tagName: string) => {
const removed: ParsedNodeMeta[] = [];
const remove = (node: ParsedNodeMeta) => {
if (!node.childNodes?.length) return;
const nextNodes: ParsedNodeMeta[] = [];
node.childNodes.forEach((child) => {
if (getNodeTagName(child) === tagName) {
removed.push(child);
return;
}
remove(child);
nextNodes.push(child);
});
node.childNodes = nextNodes;
};
remove(root);
return removed;
};
export const sanitizeNode = (node: ParsedNodeMeta, opts: HTMLParserOptions) => {
const attrs = node.attributes || {};
const cleanAttrs: Record<string, string> = {};
each(attrs, (value, name) => {
const attrValue = `${value}`;
const isUnsafeAttr = !opts.allowUnsafeAttr && name.startsWith('on');
const isUnsafeValue = !opts.allowUnsafeAttrValue && attrValue.startsWith('javascript:');
if (!isUnsafeAttr && !isUnsafeValue) {
cleanAttrs[name] = attrValue;
}
});
if (Object.keys(cleanAttrs).length) {
node.attributes = cleanAttrs;
} else {
delete node.attributes;
}
each(getNodeChildNodes(node), (child) => sanitizeNode(child, opts));
};
export const domDocumentToParsedNode = (doc: Document): ParsedNodeMeta => ({
nodeType: ParsedNodeType.document,
__domNode: doc,
__doctype: doctypeToString(doc.doctype),
childNodes: doc.documentElement ? [domToParsedNode(doc.documentElement)] : [],
});
export const domToParsedNode = (node: Node): ParsedNodeMeta => {
if (node.nodeType === ParsedNodeType.document) {
return domDocumentToParsedNode(node as Document);
}
const parsedNode: ParsedNodeMeta = {
nodeType: node.nodeType,
__domNode: node,
};
if (node.nodeType === ParsedNodeType.text || node.nodeType === ParsedNodeType.comment) {
parsedNode.textContent = node.textContent ?? '';
}
if (node.nodeType === ParsedNodeType.element) {
const el = node as HTMLElement;
parsedNode.tagName = el.tagName || '';
parsedNode.namespaceURI = el.namespaceURI || undefined;
const attrs = el.attributes || [];
if (attrs.length) {
parsedNode.attributes = {};
}
const boolAttributes: string[] = [];
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
parsedNode.attributes![attr.nodeName] = attr.nodeValue || '';
if (attr.nodeValue === '' && (el as any)[attr.nodeName] === true) {
boolAttributes.push(attr.nodeName);
}
}
boolAttributes.length && (parsedNode.__boolAttributes = boolAttributes);
const childNodes = getDomChildNodes(el);
childNodes.length && (parsedNode.childNodes = childNodes.map((child) => domToParsedNode(child)));
parsedNode.__selfClosing = `${el.outerHTML || ''}`.slice(-2) === '/>';
}
return parsedNode;
};
export const domRootToFragment = (root: HTMLElement): ParsedNodeMeta => ({
nodeType: ParsedNodeType.fragment,
__domNode: root,
childNodes: getDomChildNodes(root).map((node) => domToParsedNode(node)),
});
export const normalizeDocumentRoot = (nodes: ParsedNode[]) => {
const flatNodes = nodes
.map((node) => cloneParsedNode(node))
.flatMap((node) => (node.nodeType === ParsedNodeType.fragment ? getNodeChildNodes(node) : [node]));
const documentNode = flatNodes.find((node) => node.nodeType === ParsedNodeType.document);
if (documentNode) {
return documentNode;
}
const htmlNode = flatNodes.find(
(node) => node.nodeType === ParsedNodeType.element && getNodeTagName(node) === 'html',
);
const documentRoot: ParsedNodeMeta = {
nodeType: ParsedNodeType.document,
childNodes: [],
};
if (htmlNode) {
const extraNodes = flatNodes.filter((node) => node !== htmlNode);
if (extraNodes.length) {
const bodyNode = findChildElement(htmlNode, 'body') || appendChildElement(htmlNode, 'body');
bodyNode.childNodes = [...getNodeChildNodes(bodyNode), ...extraNodes];
}
documentRoot.childNodes = [htmlNode];
return documentRoot;
}
const remaining = [...flatNodes];
const headIndex = remaining.findIndex(
(node) => node.nodeType === ParsedNodeType.element && getNodeTagName(node) === 'head',
);
const bodyIndex = remaining.findIndex(
(node) => node.nodeType === ParsedNodeType.element && getNodeTagName(node) === 'body',
);
const headNode = headIndex >= 0 ? remaining.splice(headIndex, 1)[0] : undefined;
const bodyNode =
bodyIndex >= 0
? remaining.splice(bodyIndex > headIndex && headIndex >= 0 ? bodyIndex - 1 : bodyIndex, 1)[0]
: undefined;
const htmlRoot = createElementNode('html');
const htmlChildren: ParsedNodeMeta[] = [];
headNode && htmlChildren.push(headNode);
const normalizedBody = bodyNode || createElementNode('body');
normalizedBody.childNodes = [...getNodeChildNodes(normalizedBody), ...remaining];
htmlChildren.push(normalizedBody);
htmlRoot.childNodes = htmlChildren;
documentRoot.childNodes = [htmlRoot];
return documentRoot;
};
Loading…
Cancel
Save