@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
264 lines (232 loc) • 7.11 kB
text/typescript
import {
Fragment,
Mark,
Node as PMNode,
Schema
} from '../../';
/**
* Deduce a set of marks from a style declaration.
*/
export function marksFromStyle(schema: Schema<any, any>, style: CSSStyleDeclaration): Mark[] {
let marks: Mark[] = [];
styles: for (let i = 0; i < style.length; i++) {
const name = style.item(i);
const value = style.getPropertyValue(name);
switch (name) {
case 'text-decoration-color':
case 'text-decoration-style':
continue styles;
case 'text-decoration-line':
case 'text-decoration':
switch (value) {
case 'line-through':
marks = schema.marks.strike.create().addToSet(marks);
continue styles;
}
break;
case 'font-family':
if (value === 'monospace') {
marks = schema.marks.code.create().addToSet(marks);
continue styles;
}
}
throw new Error(`Unable to derive a mark for CSS ${name}: ${value}`);
}
return marks;
}
/**
* Create a fragment by adding a set of marks to each node.
*/
export function addMarks(fragment: Fragment, marks: Mark[]): Fragment {
let result = fragment;
for (let i = 0; i < fragment.childCount; i++) {
const child = result.child(i);
let newChild = child;
for (const mark of marks) {
newChild = newChild.mark(mark.addToSet(newChild.marks));
}
result = result.replaceChild(i, newChild);
}
return result;
}
/**
*
* Traverse the DOM node and build an array of the breadth-first-search traversal
* through the tree.
*
* Detection of supported vs unsupported content happens at this stage. Unsupported
* nodes do not have their children traversed. Doing this avoids attempting to
* decode unsupported content descendents into ProseMirror nodes.
*/
export function findTraversalPath(roots: Node[]) {
const inqueue = [...roots];
const outqueue = [] as Node[];
let elem;
while (elem = inqueue.shift()) {
outqueue.push(elem);
let children;
if (isNodeSupportedContent(elem) && (children = childrenOfNode(elem))) {
let childIndex;
for (childIndex = 0; childIndex < children.length; childIndex++) {
const child = children[childIndex];
inqueue.push(child);
}
}
}
return outqueue;
}
function childrenOfNode(node: Element): NodeList | null {
const tag = getNodeName(node);
if (tag === 'AC:STRUCTURED-MACRO') {
return getAcTagChildNodes(node, 'AC:RICH-TEXT-BODY');
}
return node.childNodes;
}
/**
* Return an array containing the child nodes in a fragment.
*
* @param fragment
*/
export function children(fragment: Fragment): PMNode[] {
const nodes: PMNode[] = [];
for (let i = 0; i < fragment.childCount; i++) {
nodes.push(fragment.child(i));
}
return nodes;
}
/**
* Quickly determine if a DOM node is supported (i.e. can be represented in the ProseMirror
* schema).
*
* When a node is not supported, its children are not traversed — instead the entire node content
* is stored inside an `unsupportedInline`.
*
* @param node
*/
function isNodeSupportedContent(node: Node): boolean {
if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.CDATA_SECTION_NODE) {
return true;
}
if (node instanceof HTMLElement || node.nodeType === Node.ELEMENT_NODE) {
const tag = getNodeName(node);
switch (tag) {
case 'DEL':
case 'S':
case 'B':
case 'STRONG':
case 'I':
case 'EM':
case 'CODE':
case 'SUB':
case 'SUP':
case 'U':
case 'BLOCKQUOTE':
case 'SPAN':
case 'H1':
case 'H2':
case 'H3':
case 'H4':
case 'H5':
case 'H6':
case 'BR':
case 'HR':
case 'UL':
case 'OL':
case 'LI':
case 'P':
case 'A':
case 'FAB:MENTION':
case 'FAB:MEDIA':
case 'AC:STRUCTURED-MACRO':
return true;
}
}
return false;
}
export function getAcName(node: Element): string | undefined {
return (node.getAttribute('ac:name') || '').toUpperCase();
}
export function getNodeName(node: Node): string {
return node.nodeName.toUpperCase();
}
export function getAcParameter(node: Element, parameter: string): string | null {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i] as Element;
if (getNodeName(child) === 'AC:PARAMETER' && getAcName(child) === parameter.toUpperCase()) {
return child.textContent;
}
}
return null;
}
export function getAcTagContent(node: Element, tagName: string): string | null {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i] as Element;
if (getNodeName(child) === tagName) {
return child.textContent;
}
}
return null;
}
export function getAcTagChildNodes(node: Element, tagName: string): NodeList | null {
const child = getAcTagNode(node, tagName);
if (child) {
// return html collection only if childNodes are found
return child.childNodes.length ? child.childNodes : null;
}
return null;
}
export function getAcTagNode(node: Element, tagName: string): Element | null {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i] as Element;
if (getNodeName(child) === tagName) {
return child;
}
}
return null;
}
export function getMacroAttribute(node: Element, attribute: string): string {
return (node.getAttribute('data-macro-' + attribute) || '');
}
export function getMacroParameters(node: Element): any {
const params = {};
getMacroAttribute(node, 'parameters').split('|').forEach(paramStr => {
const param = paramStr.split('=');
if (param.length) {
params[param[0]] = param[1];
}
});
return params;
}
export function createCodeFragment(schema: Schema<any, any>, codeContent: string, language?: string | null, title?: string | null): Fragment {
const content: PMNode[] = [];
let nodeSize = 0;
if (!!title) {
const titleNode = schema.nodes.heading.create({ level: 5 }, schema.text(title, [schema.marks.strong.create()]));
content.push(titleNode);
nodeSize += titleNode.nodeSize;
}
const codeBlockNode = schema.nodes.codeBlock.create({ language }, schema.text(codeContent));
content.push(codeBlockNode);
nodeSize += codeBlockNode.nodeSize;
return Fragment.from(content);
}
export function hasClass(node: Element, className: string): boolean {
if (node && node.className) {
return node.className.indexOf(className) > -1;
}
return false;
}
/*
* Contructs a struct string of replacement blocks and marks for a given node
*/
export function getContent(node: Node, convertedNodes: WeakMap<Node, Fragment | PMNode>): Fragment {
let fragment = Fragment.fromArray([]);
for (let childIndex = 0; childIndex < node.childNodes.length; childIndex++) {
const child = node.childNodes[childIndex];
const thing = convertedNodes.get(child);
if (thing instanceof Fragment || thing instanceof PMNode) {
fragment = fragment.append(Fragment.from(thing));
}
}
return fragment;
}