UNPKG

@atlaskit/adf-schema

Version:

Shared package that contains the ADF-schema (json) and ProseMirror node/mark specs

273 lines (266 loc) 9.54 kB
import { Fragment } from '@atlaskit/editor-prosemirror/model'; import { codeBlock as codeBlockFactory } from '../../next-schema/generated/nodeTypes'; import { uuid } from '../../utils'; /** * @name codeBlock_with_no_marks_node */ /** * @name codeBlock_node */ const getLanguageFromEditorStyle = dom => { return dom.getAttribute('data-language') || undefined; }; // example of BB style: // <div class="codehilite language-javascript"><pre><span>hello world</span><span>\n</span></pre></div> const getLanguageFromBitbucketStyle = dom => { if (dom && dom.classList.contains('codehilite')) { // code block html from Bitbucket always contains an extra new line return extractLanguageFromClass(dom.className); } return; }; // If there is a child code element, check that for data-language const getLanguageFromCode = dom => { const firstChild = dom.firstElementChild; if (firstChild && firstChild.nodeName === 'CODE') { return firstChild.getAttribute('data-language') || undefined; } }; // @ts-ignore TS1501: This regular expression flag is only available when targeting 'es6' or later. const LANGUAGE_CLASS_REGEX = /(?:^|\s)language-([^\s]+)/u; // @ts-ignore TS1501: This regular expression flag is only available when targeting 'es6' or later. const TRAILING_NEWLINE_REGEX = /\n$/u; const extractLanguageFromClass = className => { const result = LANGUAGE_CLASS_REGEX.exec(className); if (result && result[1]) { return result[1]; } return; }; const removeLastNewLine = dom => { const parent = dom && dom.parentElement; if (parent && parent.classList.contains('codehilite')) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion dom.textContent = dom.textContent.replace(TRAILING_NEWLINE_REGEX, ''); } return dom; }; function parseCodeFromHtml(node) { let code = ''; node.childNodes.forEach(child => { if (child.nodeType === Node.TEXT_NODE) { // append text code += child.nodeValue; } else if (child.nodeType === Node.ELEMENT_NODE && child instanceof Element) { const tagName = child.tagName.toLowerCase(); if (tagName === 'div' || tagName === 'p') { // add a newline before its content, unless it's the first child to avoid leading newlines if (child.previousElementSibling !== null) { code += '\n'; } } if (tagName === 'br') { code += '\n'; } else { code += parseCodeFromHtml(child); } } }); return code; } export const codeBlock = codeBlockFactory({ parseDOM: [{ tag: 'pre', preserveWhitespace: 'full', getAttrs: domNode => { // eslint-disable-next-line @atlaskit/editor/no-as-casting let dom = domNode; const language = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion getLanguageFromBitbucketStyle(dom.parentElement) || // eslint-disable-next-line @typescript-eslint/no-non-null-assertion getLanguageFromEditorStyle(dom.parentElement) || getLanguageFromCode(dom) || // eslint-disable-next-line @typescript-eslint/no-non-null-assertion dom.getAttribute('data-language'); dom = removeLastNewLine(dom); const wrapAttr = dom.getAttribute('data-wrap'); const isCopiedFromEditor = Boolean(dom.closest('[data-pm-slice]') || dom.hasAttribute('data-pm-slice')); // Default external HTML paste to wrapped when data-wrap is absent, but preserve // unwrapped editor-origin paste when copied content has data-pm-slice. const wrap = wrapAttr === null ? !isCopiedFromEditor : wrapAttr !== 'false'; const hideLineNumbers = dom.getAttribute('data-hide-line-numbers') === 'true'; return { language, wrap, hideLineNumbers, localId: uuid.generate() }; } }, // Handle VSCode, Android Studio paste // Checking `white-space: pre-wrap` is too aggressive @see ED-2627 { tag: 'div[style]', preserveWhitespace: 'full', getAttrs: domNode => { // eslint-disable-next-line @atlaskit/editor/no-as-casting const dom = domNode; if (dom.style.whiteSpace === 'pre' || dom.style.fontFamily && dom.style.fontFamily.toLowerCase().indexOf('monospace') > -1) { return { wrap: true }; } return false; }, getContent: (domNode, schema) => { const code = parseCodeFromHtml(domNode); return code ? Fragment.from(schema.text(code)) : Fragment.empty; } }, // Handle GitHub/Gist paste { tag: 'table[style]', preserveWhitespace: 'full', getAttrs: dom => { // eslint-disable-next-line @atlaskit/editor/no-as-casting if (dom.querySelector('td[class*="blob-code"]')) { return { wrap: true }; } return false; } }, { tag: 'div.code-block', preserveWhitespace: 'full', getAttrs: domNode => { // eslint-disable-next-line @atlaskit/editor/no-as-casting const dom = domNode; // TODO: ED-5604 - Fix it inside `react-syntax-highlighter` // Remove line numbers const lineNumber = dom.querySelectorAll('.react-syntax-highlighter-line-number'); if (lineNumber.length > 0) { // It's possible to copy without the line numbers too hence this // `react-syntax-highlighter-line-number` check, so that we don't remove real code lineNumber.forEach(line => line.remove()); } return { wrap: true }; } }], toDOM(node) { var _node$attrs; const attrs = {}; if ((node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.localId) !== undefined) { attrs['data-local-id'] = node.attrs.localId; } // Always serialize data-wrap explicitly (both true and false) so that // editor-to-editor paste can distinguish intentional wrap:false from // absent data-wrap (which parseDOM defaults to wrap:true for external HTML). attrs['data-wrap'] = node.attrs.wrap ? 'true' : 'false'; if (node.attrs.hideLineNumbers) { attrs['data-hide-line-numbers'] = 'true'; } return ['pre', attrs, ['code', { 'data-language': node.attrs.language }, 0]]; } }); export const toJSON = node => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any attrs: Object.keys(node.attrs).reduce((memo, key) => { if (key === 'uniqueId') { return memo; } if (key === 'language' && node.attrs.language === null) { return memo; } if (key === 'wrap' && node.attrs.wrap === null) { return memo; } if (key === 'hideLineNumbers' && !node.attrs.hideLineNumbers) { return memo; } memo[key] = node.attrs[key]; return memo; }, {}) }); export const codeBlockWithLocalId = codeBlockFactory({ parseDOM: [{ tag: 'pre', preserveWhitespace: 'full', getAttrs: domNode => { // eslint-disable-next-line @atlaskit/editor/no-as-casting let dom = domNode; const language = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion getLanguageFromBitbucketStyle(dom.parentElement) || // eslint-disable-next-line @typescript-eslint/no-non-null-assertion getLanguageFromEditorStyle(dom.parentElement) || getLanguageFromCode(dom) || // eslint-disable-next-line @typescript-eslint/no-non-null-assertion dom.getAttribute('data-language'); dom = removeLastNewLine(dom); return { language, localId: uuid.generate() }; } }, // Handle VSCode, Android Studio paste // Checking `white-space: pre-wrap` is too aggressive @see ED-2627 { tag: 'div[style]', preserveWhitespace: 'full', getAttrs: domNode => { // eslint-disable-next-line @atlaskit/editor/no-as-casting const dom = domNode; if (dom.style.whiteSpace === 'pre' || dom.style.fontFamily && dom.style.fontFamily.toLowerCase().indexOf('monospace') > -1) { return {}; } return false; }, getContent: (domNode, schema) => { const code = parseCodeFromHtml(domNode); return code ? Fragment.from(schema.text(code)) : Fragment.empty; } }, // Handle GitHub/Gist paste { tag: 'table[style]', preserveWhitespace: 'full', getAttrs: dom => { // eslint-disable-next-line @atlaskit/editor/no-as-casting if (dom.querySelector('td[class*="blob-code"]')) { return {}; } return false; } }, { tag: 'div.code-block', preserveWhitespace: 'full', getAttrs: domNode => { // eslint-disable-next-line @atlaskit/editor/no-as-casting const dom = domNode; // TODO: ED-5604 - Fix it inside `react-syntax-highlighter` // Remove line numbers const lineNumber = dom.querySelectorAll('.react-syntax-highlighter-line-number'); if (lineNumber.length > 0) { // It's possible to copy without the line numbers too hence this // `react-syntax-highlighter-line-number` check, so that we don't remove real code lineNumber.forEach(line => line.remove()); } return {}; } }], toDOM(node) { var _node$attrs2; const attrs = {}; if ((node === null || node === void 0 ? void 0 : (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.localId) !== undefined) { attrs['data-local-id'] = node.attrs.localId; } return ['pre', attrs, ['code', { 'data-language': node.attrs.language }, 0]]; } });