import { Slice, Mark, Node as PMNode, NodeType, Schema, } from 'prosemirror-model'; import { isMediaBlobUrl } from '@atlaskit/media-client'; import { Selection } from 'prosemirror-state'; import { PasteSource } from '../analytics'; import { TextSelection, NodeSelection } from 'prosemirror-state'; import { findParentNodeOfType } from 'prosemirror-utils'; export function isPastedFromWord(html?: string): boolean { return !!html && html.indexOf('urn:schemas-microsoft-com:office:word') >= 0; } export function isPastedFromExcel(html?: string): boolean { return !!html && html.indexOf('urn:schemas-microsoft-com:office:excel') >= 0; } export function isPastedFromDropboxPaper(html?: string): boolean { return !!html && !!html.match(/class=\"\s?author-d-.+"/gim); } export function isPastedFromGoogleDocs(html?: string): boolean { return !!html && !!html.match(/id=\"docs-internal-guid-.+"/gim); } export function isPastedFromGoogleSpreadSheets(html?: string): boolean { return !!html && !!html.match(/data-sheets-.+=/gim); } export function isPastedFromPages(html?: string): boolean { return !!html && html.indexOf('content="Cocoa HTML Writer"') >= 0; } export function isPastedFromFabricEditor(html?: string): boolean { return !!html && html.indexOf('data-pm-slice="') >= 0; } export const isSingleLine = (text: string): boolean => { return !!text && text.trim().split('\n').length === 1; }; export function htmlContainsSingleFile(html: string): boolean { return !!html.match(//) && !isMediaBlobUrl(html); } export function getPasteSource(event: ClipboardEvent): PasteSource { const html = event.clipboardData!.getData('text/html'); if (isPastedFromDropboxPaper(html)) { return 'dropbox-paper'; } else if (isPastedFromWord(html)) { return 'microsoft-word'; } else if (isPastedFromExcel(html)) { return 'microsoft-excel'; } else if (isPastedFromGoogleDocs(html)) { return 'google-docs'; } else if (isPastedFromGoogleSpreadSheets(html)) { return 'google-spreadsheets'; } else if (isPastedFromPages(html)) { return 'apple-pages'; } else if (isPastedFromFabricEditor(html)) { return 'fabric-editor'; } return 'uncategorized'; } // TODO: Write JEST tests for this part export function isCode(str: string) { const lines = str.split(/\r?\n|\r/); if (3 > lines.length) { return false; } let weight = 0; lines.forEach((line) => { // Ends with : or ; if (/[:;]$/.test(line)) { weight++; } // Contains second and third braces if (/[{}\[\]]/.test(line)) { weight++; } // Contains or /.test(line) || /<\//.test(line)) { weight++; } // Contains () <- function calls if (/\(\)/.test(line)) { weight++; } // Contains a link if (/(^|[^!])\[(.*?)\]\((\S+)\)$/.test(line)) { weight--; } // New line starts with less than two chars. e.g- if, {, <, etc const token = /^(\s+)[a-zA-Z<{]{2,}/.exec(line); if (token && 2 <= token[1].length) { weight++; } if (/&&/.test(line)) { weight++; } }); return 4 <= weight && weight >= 0.5 * lines.length; } // @see https://product-fabric.atlassian.net/browse/ED-3159 // @see https://github.com/markdown-it/markdown-it/issues/38 export function escapeLinks(text: string) { return text.replace(/(\[([^\]]+)\]\()?((https?|ftp):\/\/[^\s]+)/g, (str) => { return str.match(/^(https?|ftp):\/\/[^\s]+$/) ? `<${str}>` : str; }); } export function hasOnlyNodesOfType( ...nodeTypes: NodeType[] ): (slice: Slice) => boolean { return (slice: Slice) => { let hasOnlyNodesOfType = true; slice.content.descendants((node: PMNode) => { hasOnlyNodesOfType = hasOnlyNodesOfType && nodeTypes.indexOf(node.type) > -1; return hasOnlyNodesOfType; }); return hasOnlyNodesOfType; }; } export function applyTextMarksToSlice( schema: Schema, marks?: Mark[], ): (slice: Slice) => Slice { return (slice: Slice) => { const { marks: { code: codeMark, link: linkMark, annotation: annotationMark }, } = schema; if (!Array.isArray(marks) || marks.length === 0) { return slice; } const sliceCopy = Slice.fromJSON(schema, slice.toJSON() || {}); // allow links and annotations to be pasted const allowedMarksToPaste = [linkMark, annotationMark]; sliceCopy.content.descendants((node, _pos, parent) => { if (node.isText && parent && parent.isBlock) { node.marks = [ // remove all marks from pasted slice when applying code mark // and exclude all marks that are not allowed to be pasted ...((node.marks && !codeMark.isInSet(marks) && node.marks.filter((mark) => allowedMarksToPaste.includes(mark.type), )) || []), // add marks to a slice if they're allowed in parent node // and exclude link marks ...parent.type .allowedMarks(marks) .filter((mark) => mark.type !== linkMark), ]; return false; } return true; }); return sliceCopy; }; } export function isEmptyNode(node: PMNode | null | undefined) { if (!node) { return false; } const { type: nodeType } = node; const emptyNode = nodeType.createAndFill(); return ( emptyNode && emptyNode.nodeSize === node.nodeSize && emptyNode.content.eq(node.content) && Mark.sameSet(emptyNode.marks, node.marks) ); } export function isCursorSelectionAtTextStartOrEnd(selection: Selection) { return ( selection instanceof TextSelection && selection.empty && selection.$cursor && (!selection.$cursor.nodeBefore || !selection.$cursor.nodeAfter) ); } export function isPanelNode(node: PMNode | null | undefined) { return Boolean(node && node.type.name === 'panel'); } export function isSelectionInsidePanel(selection: Selection): PMNode | null { if (selection instanceof NodeSelection && isPanelNode(selection.node)) { return selection.node; } const { doc: { type: { schema: { nodes: { panel }, }, }, }, } = selection.$from; const panelPosition = findParentNodeOfType(panel)(selection); if (panelPosition) { return panelPosition.node; } return null; }