debian-mirror-gitlab/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
2020-10-24 23:57:45 +05:30

198 lines
6.2 KiB
JavaScript

import $ from 'jquery';
import { getSelectedFragment, insertText } from '~/lib/utils/common_utils';
export class CopyAsGFM {
constructor() {
// iOS currently does not support clipboardData.setData(). This bug should
// be fixed in iOS 12, but for now we'll disable this for all iOS browsers
// ref: https://trac.webkit.org/changeset/222228/webkit
const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
if (isIOS) return;
$(document).on('copy', '.md', e => {
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
});
$(document).on('copy', 'pre.code.highlight, table.code td.line_content', e => {
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection);
});
$(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
}
static copyAsGFM(e, transformer) {
const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const documentFragment = getSelectedFragment();
if (!documentFragment) return;
const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
if (!el) return;
e.preventDefault();
e.stopPropagation();
const div = document.createElement('div');
div.appendChild(el.cloneNode(true));
const html = div.innerHTML;
clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/html', html);
// We are also setting this as fallback to transform the selection to gfm on paste
clipboardData.setData('text/x-gfm-html', html);
CopyAsGFM.nodeToGFM(el)
.then(res => {
clipboardData.setData('text/x-gfm', res);
})
.catch(() => {
// Not showing the error as Firefox might doesn't allow
// it or other browsers who have a time limit on the execution
// of the copy event
});
}
static pasteGFM(e) {
const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
const gfm = clipboardData.getData('text/x-gfm');
const gfmHtml = clipboardData.getData('text/x-gfm-html');
if (!gfm && !gfmHtml) return;
e.preventDefault();
// We have the original selection already converted to gfm
if (gfm) {
CopyAsGFM.insertPastedText(e.target, text, gfm);
} else {
// Due to the async copy call we are not able to produce gfm so we transform the cached HTML
const div = document.createElement('div');
div.innerHTML = gfmHtml;
CopyAsGFM.nodeToGFM(div)
.then(transformedGfm => {
CopyAsGFM.insertPastedText(e.target, text, transformedGfm);
})
.catch(() => {});
}
}
static insertPastedText(target, text, gfm) {
insertText(target, textBefore => {
// If the text before the cursor contains an odd number of backticks,
// we are either inside an inline code span that starts with 1 backtick
// or a code block that starts with 3 backticks.
// This logic still holds when there are one or more _closed_ code spans
// or blocks that will have 2 or 6 backticks.
// This will break down when the actual code block contains an uneven
// number of backticks, but this is a rare edge case.
const backtickMatch = textBefore.match(/`/g);
const insideCodeBlock = backtickMatch && backtickMatch.length % 2 === 1;
if (insideCodeBlock) {
return text;
}
return gfm;
});
}
static transformGFMSelection(documentFragment) {
const gfmElements = documentFragment.querySelectorAll('.md');
switch (gfmElements.length) {
case 0: {
return documentFragment;
}
case 1: {
return gfmElements[0];
}
default: {
const allGfmElement = document.createElement('div');
for (let i = 0; i < gfmElements.length; i += 1) {
const gfmElement = gfmElements[i];
allGfmElement.appendChild(gfmElement);
allGfmElement.appendChild(document.createTextNode('\n\n'));
}
return allGfmElement;
}
}
}
static transformCodeSelection(documentFragment, target) {
let lineSelector = '.line';
if (target) {
const lineClass = ['left-side', 'right-side'].filter(name =>
target.classList.contains(name),
)[0];
if (lineClass) {
lineSelector = `.line_content.${lineClass} ${lineSelector}`;
}
}
const lineElements = documentFragment.querySelectorAll(lineSelector);
let codeElement;
if (lineElements.length > 1) {
codeElement = document.createElement('pre');
codeElement.className = 'code highlight';
const lang = lineElements[0].getAttribute('lang');
if (lang) {
codeElement.setAttribute('lang', lang);
}
} else {
codeElement = document.createElement('code');
}
if (lineElements.length > 0) {
for (let i = 0; i < lineElements.length; i += 1) {
const lineElement = lineElements[i];
codeElement.appendChild(lineElement);
codeElement.appendChild(document.createTextNode('\n'));
}
} else {
codeElement.appendChild(documentFragment);
}
return codeElement;
}
static nodeToGFM(node) {
return Promise.all([
import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
])
.then(([prosemirrorModel, schema, markdownSerializer]) => {
const { DOMParser } = prosemirrorModel;
const wrapEl = document.createElement('div');
wrapEl.appendChild(node.cloneNode(true));
const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
const res = markdownSerializer.default.serialize(doc, {
tightLists: true,
});
return res;
})
.catch(() => {});
}
static quoted(markdown) {
return `> ${markdown.split('\n').join('\n> ')}`;
}
}
// Export CopyAsGFM as a global for rspec to access
// see /spec/features/markdown/copy_as_gfm_spec.rb
if (process.env.NODE_ENV !== 'production') {
window.CopyAsGFM = CopyAsGFM;
}
export default function initCopyAsGFM() {
return new CopyAsGFM();
}