mirror of https://github.com/artf/grapesjs.git
2 changed files with 243 additions and 225 deletions
@ -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…
Reference in new issue