Merge branch 'copy-as-md' into 'master'
Copying a rendered issue/comment will paste into GFM textareas as actual GFM See merge request !8597
This commit is contained in:
commit
a24e9a0e3c
|
@ -0,0 +1,355 @@
|
||||||
|
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
|
||||||
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
|
/*= require lib/utils/common_utils */
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const gfmRules = {
|
||||||
|
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
|
||||||
|
// GitLab Flavored Markdown (GFM) to HTML.
|
||||||
|
// These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
|
||||||
|
// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
|
||||||
|
// from GFM should have a handler here, in reverse order.
|
||||||
|
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||||
|
InlineDiffFilter: {
|
||||||
|
'span.idiff.addition'(el, text) {
|
||||||
|
return `{+${text}+}`;
|
||||||
|
},
|
||||||
|
'span.idiff.deletion'(el, text) {
|
||||||
|
return `{-${text}-}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TaskListFilter: {
|
||||||
|
'input[type=checkbox].task-list-item-checkbox'(el, text) {
|
||||||
|
return `[${el.checked ? 'x' : ' '}]`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReferenceFilter: {
|
||||||
|
'a.gfm:not([data-link=true])'(el, text) {
|
||||||
|
return el.dataset.original || text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AutolinkFilter: {
|
||||||
|
'a'(el, text) {
|
||||||
|
// Fallback on the regular MarkdownFilter's `a` handler.
|
||||||
|
if (text !== el.getAttribute('href')) return false;
|
||||||
|
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TableOfContentsFilter: {
|
||||||
|
'ul.section-nav'(el, text) {
|
||||||
|
return '[[_TOC_]]';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EmojiFilter: {
|
||||||
|
'img.emoji'(el, text) {
|
||||||
|
return el.getAttribute('alt');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImageLinkFilter: {
|
||||||
|
'a.no-attachment-icon'(el, text) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VideoLinkFilter: {
|
||||||
|
'.video-container'(el, text) {
|
||||||
|
const videoEl = el.querySelector('video');
|
||||||
|
if (!videoEl) return false;
|
||||||
|
|
||||||
|
return CopyAsGFM.nodeToGFM(videoEl);
|
||||||
|
},
|
||||||
|
'video'(el, text) {
|
||||||
|
return `![${el.dataset.title}](${el.getAttribute('src')})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathFilter: {
|
||||||
|
'pre.code.math[data-math-style=display]'(el, text) {
|
||||||
|
return `\`\`\`math\n${text.trim()}\n\`\`\``;
|
||||||
|
},
|
||||||
|
'code.code.math[data-math-style=inline]'(el, text) {
|
||||||
|
return `$\`${text}\`$`;
|
||||||
|
},
|
||||||
|
'span.katex-display span.katex-mathml'(el, text) {
|
||||||
|
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
||||||
|
if (!mathAnnotation) return false;
|
||||||
|
|
||||||
|
return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
|
||||||
|
},
|
||||||
|
'span.katex-mathml'(el, text) {
|
||||||
|
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
||||||
|
if (!mathAnnotation) return false;
|
||||||
|
|
||||||
|
return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
|
||||||
|
},
|
||||||
|
'span.katex-html'(el, text) {
|
||||||
|
// We don't want to include the content of this element in the copied text.
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
'annotation[encoding="application/x-tex"]'(el, text) {
|
||||||
|
return text.trim();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SanitizationFilter: {
|
||||||
|
'dl'(el, text) {
|
||||||
|
let lines = text.trim().split('\n');
|
||||||
|
// Add two spaces to the front of subsequent list items lines,
|
||||||
|
// or leave the line entirely blank.
|
||||||
|
lines = lines.map((l) => {
|
||||||
|
const line = l.trim();
|
||||||
|
if (line.length === 0) return '';
|
||||||
|
|
||||||
|
return ` ${line}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<dl>\n${lines.join('\n')}\n</dl>`;
|
||||||
|
},
|
||||||
|
'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
|
||||||
|
const tag = el.nodeName.toLowerCase();
|
||||||
|
return `<${tag}>${text}</${tag}>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SyntaxHighlightFilter: {
|
||||||
|
'pre.code.highlight'(el, t) {
|
||||||
|
const text = t.trim();
|
||||||
|
|
||||||
|
let lang = el.getAttribute('lang');
|
||||||
|
if (lang === 'plaintext') {
|
||||||
|
lang = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixes lines with 4 spaces if the code contains triple backticks
|
||||||
|
if (lang === '' && text.match(/^```/gm)) {
|
||||||
|
return text.split('\n').map((l) => {
|
||||||
|
const line = l.trim();
|
||||||
|
if (line.length === 0) return '';
|
||||||
|
|
||||||
|
return ` ${line}`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `\`\`\`${lang}\n${text}\n\`\`\``;
|
||||||
|
},
|
||||||
|
'pre > code'(el, text) {
|
||||||
|
// Don't wrap code blocks in ``
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MarkdownFilter: {
|
||||||
|
'br'(el, text) {
|
||||||
|
// Two spaces at the end of a line are turned into a BR
|
||||||
|
return ' ';
|
||||||
|
},
|
||||||
|
'code'(el, text) {
|
||||||
|
let backtickCount = 1;
|
||||||
|
const backtickMatch = text.match(/`+/);
|
||||||
|
if (backtickMatch) {
|
||||||
|
backtickCount = backtickMatch[0].length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backticks = Array(backtickCount + 1).join('`');
|
||||||
|
const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
|
||||||
|
|
||||||
|
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
|
||||||
|
},
|
||||||
|
'blockquote'(el, text) {
|
||||||
|
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
|
||||||
|
},
|
||||||
|
'img'(el, text) {
|
||||||
|
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
|
||||||
|
},
|
||||||
|
'a.anchor'(el, text) {
|
||||||
|
// Don't render a Markdown link for the anchor link inside a heading
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
'a'(el, text) {
|
||||||
|
return `[${text}](${el.getAttribute('href')})`;
|
||||||
|
},
|
||||||
|
'li'(el, text) {
|
||||||
|
const lines = text.trim().split('\n');
|
||||||
|
const firstLine = `- ${lines.shift()}`;
|
||||||
|
// Add four spaces to the front of subsequent list items lines,
|
||||||
|
// or leave the line entirely blank.
|
||||||
|
const nextLines = lines.map((s) => {
|
||||||
|
if (s.trim().length === 0) return '';
|
||||||
|
|
||||||
|
return ` ${s}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${firstLine}\n${nextLines.join('\n')}`;
|
||||||
|
},
|
||||||
|
'ul'(el, text) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
'ol'(el, text) {
|
||||||
|
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
|
||||||
|
return text.replace(/^- /mg, '1. ');
|
||||||
|
},
|
||||||
|
'h1'(el, text) {
|
||||||
|
return `# ${text.trim()}`;
|
||||||
|
},
|
||||||
|
'h2'(el, text) {
|
||||||
|
return `## ${text.trim()}`;
|
||||||
|
},
|
||||||
|
'h3'(el, text) {
|
||||||
|
return `### ${text.trim()}`;
|
||||||
|
},
|
||||||
|
'h4'(el, text) {
|
||||||
|
return `#### ${text.trim()}`;
|
||||||
|
},
|
||||||
|
'h5'(el, text) {
|
||||||
|
return `##### ${text.trim()}`;
|
||||||
|
},
|
||||||
|
'h6'(el, text) {
|
||||||
|
return `###### ${text.trim()}`;
|
||||||
|
},
|
||||||
|
'strong'(el, text) {
|
||||||
|
return `**${text}**`;
|
||||||
|
},
|
||||||
|
'em'(el, text) {
|
||||||
|
return `_${text}_`;
|
||||||
|
},
|
||||||
|
'del'(el, text) {
|
||||||
|
return `~~${text}~~`;
|
||||||
|
},
|
||||||
|
'sup'(el, text) {
|
||||||
|
return `^${text}`;
|
||||||
|
},
|
||||||
|
'hr'(el, text) {
|
||||||
|
return '-----';
|
||||||
|
},
|
||||||
|
'table'(el, text) {
|
||||||
|
const theadEl = el.querySelector('thead');
|
||||||
|
const tbodyEl = el.querySelector('tbody');
|
||||||
|
if (!theadEl || !tbodyEl) return false;
|
||||||
|
|
||||||
|
const theadText = CopyAsGFM.nodeToGFM(theadEl);
|
||||||
|
const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
|
||||||
|
|
||||||
|
return theadText + tbodyText;
|
||||||
|
},
|
||||||
|
'thead'(el, text) {
|
||||||
|
const cells = _.map(el.querySelectorAll('th'), (cell) => {
|
||||||
|
let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
|
||||||
|
|
||||||
|
let before = '';
|
||||||
|
let after = '';
|
||||||
|
switch (cell.style.textAlign) {
|
||||||
|
case 'center':
|
||||||
|
before = ':';
|
||||||
|
after = ':';
|
||||||
|
chars -= 2;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
after = ':';
|
||||||
|
chars -= 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
chars = Math.max(chars, 3);
|
||||||
|
|
||||||
|
const middle = Array(chars + 1).join('-');
|
||||||
|
|
||||||
|
return before + middle + after;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${text}|${cells.join('|')}|`;
|
||||||
|
},
|
||||||
|
'tr'(el, text) {
|
||||||
|
const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
|
||||||
|
return `| ${cells.join(' | ')} |`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class CopyAsGFM {
|
||||||
|
constructor() {
|
||||||
|
$(document).on('copy', '.md, .wiki', this.handleCopy);
|
||||||
|
$(document).on('paste', '.js-gfm-input', this.handlePaste);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCopy(e) {
|
||||||
|
const clipboardData = e.originalEvent.clipboardData;
|
||||||
|
if (!clipboardData) return;
|
||||||
|
|
||||||
|
const documentFragment = window.gl.utils.getSelectedFragment();
|
||||||
|
if (!documentFragment) return;
|
||||||
|
|
||||||
|
// If the documentFragment contains more than just Markdown, don't copy as GFM.
|
||||||
|
if (documentFragment.querySelector('.md, .wiki')) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
clipboardData.setData('text/plain', documentFragment.textContent);
|
||||||
|
|
||||||
|
const gfm = CopyAsGFM.nodeToGFM(documentFragment);
|
||||||
|
clipboardData.setData('text/x-gfm', gfm);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePaste(e) {
|
||||||
|
const clipboardData = e.originalEvent.clipboardData;
|
||||||
|
if (!clipboardData) return;
|
||||||
|
|
||||||
|
const gfm = clipboardData.getData('text/x-gfm');
|
||||||
|
if (!gfm) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
window.gl.utils.insertText(e.target, gfm);
|
||||||
|
}
|
||||||
|
|
||||||
|
static nodeToGFM(node) {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
return node.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.innerGFM(node);
|
||||||
|
|
||||||
|
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const filter in gfmRules) {
|
||||||
|
const rules = gfmRules[filter];
|
||||||
|
|
||||||
|
for (const selector in rules) {
|
||||||
|
const func = rules[selector];
|
||||||
|
|
||||||
|
if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
|
||||||
|
|
||||||
|
const result = func(node, text);
|
||||||
|
if (result === false) continue;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static innerGFM(parentNode) {
|
||||||
|
const nodes = parentNode.childNodes;
|
||||||
|
|
||||||
|
const clonedParentNode = parentNode.cloneNode(true);
|
||||||
|
const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i += 1) {
|
||||||
|
const node = nodes[i];
|
||||||
|
const clonedNode = clonedNodes[i];
|
||||||
|
|
||||||
|
const text = this.nodeToGFM(node);
|
||||||
|
|
||||||
|
// `clonedNode.replaceWith(text)` is not yet widely supported
|
||||||
|
clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedParentNode.innerText || clonedParentNode.textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
window.gl.CopyAsGFM = CopyAsGFM;
|
||||||
|
|
||||||
|
new CopyAsGFM();
|
||||||
|
})();
|
|
@ -160,6 +160,62 @@
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
w.gl.utils.getSelectedFragment = () => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const documentFragment = selection.getRangeAt(0).cloneContents();
|
||||||
|
if (documentFragment.textContent.length === 0) return null;
|
||||||
|
|
||||||
|
return documentFragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
w.gl.utils.insertText = (target, text) => {
|
||||||
|
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
|
||||||
|
|
||||||
|
const selectionStart = target.selectionStart;
|
||||||
|
const selectionEnd = target.selectionEnd;
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
const textBefore = value.substring(0, selectionStart);
|
||||||
|
const textAfter = value.substring(selectionEnd, value.length);
|
||||||
|
const newText = textBefore + text + textAfter;
|
||||||
|
|
||||||
|
target.value = newText;
|
||||||
|
target.selectionStart = target.selectionEnd = selectionStart + text.length;
|
||||||
|
|
||||||
|
// Trigger autosave
|
||||||
|
$(target).trigger('input');
|
||||||
|
|
||||||
|
// Trigger autosize
|
||||||
|
var event = document.createEvent('Event');
|
||||||
|
event.initEvent('autosize:update', true, false);
|
||||||
|
target.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
w.gl.utils.nodeMatchesSelector = (node, selector) => {
|
||||||
|
const matches = Element.prototype.matches ||
|
||||||
|
Element.prototype.matchesSelector ||
|
||||||
|
Element.prototype.mozMatchesSelector ||
|
||||||
|
Element.prototype.msMatchesSelector ||
|
||||||
|
Element.prototype.oMatchesSelector ||
|
||||||
|
Element.prototype.webkitMatchesSelector;
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
return matches.call(node, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IE11 doesn't support `node.matches(selector)`
|
||||||
|
|
||||||
|
let parentNode = node.parentNode;
|
||||||
|
if (!parentNode) {
|
||||||
|
parentNode = document.createElement('div');
|
||||||
|
node = node.cloneNode(true);
|
||||||
|
parentNode.appendChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingNodes = parentNode.querySelectorAll(selector);
|
||||||
|
return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
this will take in the headers from an API response and normalize them
|
this will take in the headers from an API response and normalize them
|
||||||
this way we don't run into production issues when nginx gives us lowercased header keys
|
this way we don't run into production issues when nginx gives us lowercased header keys
|
||||||
|
|
|
@ -39,29 +39,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortcutsIssuable.prototype.replyWithSelectedText = function() {
|
ShortcutsIssuable.prototype.replyWithSelectedText = function() {
|
||||||
var quote, replyField, selected, separator;
|
var quote, replyField, documentFragment, selected, separator;
|
||||||
if (window.getSelection) {
|
|
||||||
selected = window.getSelection().toString();
|
documentFragment = window.gl.utils.getSelectedFragment();
|
||||||
replyField = $('.js-main-target-form #note_note');
|
if (!documentFragment) return;
|
||||||
if (selected.trim() === "") {
|
|
||||||
return;
|
// If the documentFragment contains more than just Markdown, don't copy as GFM.
|
||||||
}
|
if (documentFragment.querySelector('.md, .wiki')) return;
|
||||||
// Put a '>' character before each non-empty line in the selection
|
|
||||||
quote = _.map(selected.split("\n"), function(val) {
|
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
|
||||||
if (val.trim() !== '') {
|
|
||||||
return "> " + val + "\n";
|
replyField = $('.js-main-target-form #note_note');
|
||||||
}
|
if (selected.trim() === "") {
|
||||||
});
|
return;
|
||||||
// If replyField already has some content, add a newline before our quote
|
|
||||||
separator = replyField.val().trim() !== "" && "\n" || '';
|
|
||||||
replyField.val(function(_, current) {
|
|
||||||
return current + separator + quote.join('') + "\n";
|
|
||||||
});
|
|
||||||
// Trigger autosave for the added text
|
|
||||||
replyField.trigger('input');
|
|
||||||
// Focus the input field
|
|
||||||
return replyField.focus();
|
|
||||||
}
|
}
|
||||||
|
quote = _.map(selected.split("\n"), function(val) {
|
||||||
|
return ("> " + val).trim() + "\n";
|
||||||
|
});
|
||||||
|
// If replyField already has some content, add a newline before our quote
|
||||||
|
separator = replyField.val().trim() !== "" && "\n\n" || '';
|
||||||
|
replyField.val(function(_, current) {
|
||||||
|
return current + separator + quote.join('') + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger autosave
|
||||||
|
replyField.trigger('input');
|
||||||
|
|
||||||
|
// Trigger autosize
|
||||||
|
var event = document.createEvent('Event');
|
||||||
|
event.initEvent('autosize:update', true, false);
|
||||||
|
replyField.get(0).dispatchEvent(event);
|
||||||
|
|
||||||
|
// Focus the input field
|
||||||
|
return replyField.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
ShortcutsIssuable.prototype.editIssue = function() {
|
ShortcutsIssuable.prototype.editIssue = function() {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -153,7 +153,7 @@ module Banzai
|
||||||
title = object_link_title(object)
|
title = object_link_title(object)
|
||||||
klass = reference_class(object_sym)
|
klass = reference_class(object_sym)
|
||||||
|
|
||||||
data = data_attributes_for(link_content || match, project, object)
|
data = data_attributes_for(link_content || match, project, object, link: !!link_content)
|
||||||
|
|
||||||
if matches.names.include?("url") && matches[:url]
|
if matches.names.include?("url") && matches[:url]
|
||||||
url = matches[:url]
|
url = matches[:url]
|
||||||
|
@ -172,9 +172,10 @@ module Banzai
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def data_attributes_for(text, project, object)
|
def data_attributes_for(text, project, object, link: false)
|
||||||
data_attribute(
|
data_attribute(
|
||||||
original: text,
|
original: text,
|
||||||
|
link: link,
|
||||||
project: project.id,
|
project: project.id,
|
||||||
object_sym => object.id
|
object_sym => object.id
|
||||||
)
|
)
|
||||||
|
|
|
@ -62,7 +62,7 @@ module Banzai
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def data_attributes_for(text, project, object)
|
def data_attributes_for(text, project, object, link: false)
|
||||||
if object.is_a?(ExternalIssue)
|
if object.is_a?(ExternalIssue)
|
||||||
data_attribute(
|
data_attribute(
|
||||||
project: project.id,
|
project: project.id,
|
||||||
|
|
|
@ -20,17 +20,19 @@ module Banzai
|
||||||
code = node.text
|
code = node.text
|
||||||
css_classes = "code highlight"
|
css_classes = "code highlight"
|
||||||
lexer = lexer_for(language)
|
lexer = lexer_for(language)
|
||||||
|
lang = lexer.tag
|
||||||
|
|
||||||
begin
|
begin
|
||||||
code = format(lex(lexer, code))
|
code = format(lex(lexer, code))
|
||||||
|
|
||||||
css_classes << " js-syntax-highlight #{lexer.tag}"
|
css_classes << " js-syntax-highlight #{lang}"
|
||||||
rescue
|
rescue
|
||||||
|
lang = nil
|
||||||
# Gracefully handle syntax highlighter bugs/errors to ensure
|
# Gracefully handle syntax highlighter bugs/errors to ensure
|
||||||
# users can still access an issue/comment/etc.
|
# users can still access an issue/comment/etc.
|
||||||
end
|
end
|
||||||
|
|
||||||
highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
|
highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>)
|
||||||
|
|
||||||
# Extracted to a method to measure it
|
# Extracted to a method to measure it
|
||||||
replace_parent_pre_element(node, highlighted)
|
replace_parent_pre_element(node, highlighted)
|
||||||
|
|
|
@ -35,7 +35,8 @@ module Banzai
|
||||||
src: element['src'],
|
src: element['src'],
|
||||||
width: '400',
|
width: '400',
|
||||||
controls: true,
|
controls: true,
|
||||||
'data-setup' => '{}')
|
'data-setup' => '{}',
|
||||||
|
'data-title' => element['title'] || element['alt'])
|
||||||
|
|
||||||
link = doc.document.create_element(
|
link = doc.document.create_element(
|
||||||
'a',
|
'a',
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
module Banzai
|
module Banzai
|
||||||
module Pipeline
|
module Pipeline
|
||||||
class GfmPipeline < BasePipeline
|
class GfmPipeline < BasePipeline
|
||||||
|
# These filters convert GitLab Flavored Markdown (GFM) to HTML.
|
||||||
|
# The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6
|
||||||
|
# consequently convert that same HTML to GFM to be copied to the clipboard.
|
||||||
|
# Every filter that generates HTML from GFM should have a handler in
|
||||||
|
# app/assets/javascripts/copy_as_gfm.js.es6, in reverse order.
|
||||||
|
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||||
def self.filters
|
def self.filters
|
||||||
@filters ||= FilterArray[
|
@filters ||= FilterArray[
|
||||||
Filter::SyntaxHighlightFilter,
|
Filter::SyntaxHighlightFilter,
|
||||||
|
|
|
@ -0,0 +1,432 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Copy as GFM', feature: true, js: true do
|
||||||
|
include GitlabMarkdownHelper
|
||||||
|
include ActionView::Helpers::JavaScriptHelper
|
||||||
|
|
||||||
|
before do
|
||||||
|
@feat = MarkdownFeature.new
|
||||||
|
|
||||||
|
# `markdown` helper expects a `@project` variable
|
||||||
|
@project = @feat.project
|
||||||
|
|
||||||
|
visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
# The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
|
||||||
|
# The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM.
|
||||||
|
# To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
|
||||||
|
# by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
|
||||||
|
|
||||||
|
# These are all in a single `it` for performance reasons.
|
||||||
|
it 'works', :aggregate_failures do
|
||||||
|
verify(
|
||||||
|
'nesting',
|
||||||
|
|
||||||
|
'> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'a real world example from the gitlab-ce README',
|
||||||
|
|
||||||
|
<<-GFM.strip_heredoc
|
||||||
|
# GitLab
|
||||||
|
|
||||||
|
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
|
||||||
|
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
|
||||||
|
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
|
||||||
|
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
|
||||||
|
|
||||||
|
## Canonical source
|
||||||
|
|
||||||
|
The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
|
||||||
|
|
||||||
|
## Open source software to collaborate on code
|
||||||
|
|
||||||
|
To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
|
||||||
|
|
||||||
|
|
||||||
|
- Manage Git repositories with fine grained access controls that keep your code secure
|
||||||
|
|
||||||
|
- Perform code reviews and enhance collaboration with merge requests
|
||||||
|
|
||||||
|
- Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
|
||||||
|
|
||||||
|
- Each project can also have an issue tracker, issue board, and a wiki
|
||||||
|
|
||||||
|
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
|
||||||
|
|
||||||
|
- Completely free and open source (MIT Expat license)
|
||||||
|
GFM
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'InlineDiffFilter',
|
||||||
|
|
||||||
|
'{-Deleted text-}',
|
||||||
|
'{+Added text+}'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'TaskListFilter',
|
||||||
|
|
||||||
|
'- [ ] Unchecked task',
|
||||||
|
'- [x] Checked task',
|
||||||
|
'1. [ ] Unchecked numbered task',
|
||||||
|
'1. [x] Checked numbered task'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'ReferenceFilter',
|
||||||
|
|
||||||
|
# issue reference
|
||||||
|
@feat.issue.to_reference,
|
||||||
|
# full issue reference
|
||||||
|
@feat.issue.to_reference(full: true),
|
||||||
|
# issue URL
|
||||||
|
namespace_project_issue_url(@project.namespace, @project, @feat.issue),
|
||||||
|
# issue URL with note anchor
|
||||||
|
namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
|
||||||
|
# issue link
|
||||||
|
"[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
|
||||||
|
# issue link with note anchor
|
||||||
|
"[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'AutolinkFilter',
|
||||||
|
|
||||||
|
'https://example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'TableOfContentsFilter',
|
||||||
|
|
||||||
|
'[[_TOC_]]'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'EmojiFilter',
|
||||||
|
|
||||||
|
':thumbsup:'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'ImageLinkFilter',
|
||||||
|
|
||||||
|
'![Image](https://example.com/image.png)'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'VideoLinkFilter',
|
||||||
|
|
||||||
|
'![Video](https://example.com/video.mp4)'
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'MathFilter: math as converted from GFM to HTML',
|
||||||
|
|
||||||
|
'$`c = \pm\sqrt{a^2 + b^2}`$',
|
||||||
|
|
||||||
|
# math block
|
||||||
|
<<-GFM.strip_heredoc
|
||||||
|
```math
|
||||||
|
c = \pm\sqrt{a^2 + b^2}
|
||||||
|
```
|
||||||
|
GFM
|
||||||
|
)
|
||||||
|
|
||||||
|
aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
|
||||||
|
gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
|
||||||
|
|
||||||
|
html = <<-HTML.strip_heredoc
|
||||||
|
<span class="katex">
|
||||||
|
<span class="katex-mathml">
|
||||||
|
<math>
|
||||||
|
<semantics>
|
||||||
|
<mrow>
|
||||||
|
<mi>c</mi>
|
||||||
|
<mo>=</mo>
|
||||||
|
<mo>±</mo>
|
||||||
|
<msqrt>
|
||||||
|
<mrow>
|
||||||
|
<msup>
|
||||||
|
<mi>a</mi>
|
||||||
|
<mn>2</mn>
|
||||||
|
</msup>
|
||||||
|
<mo>+</mo>
|
||||||
|
<msup>
|
||||||
|
<mi>b</mi>
|
||||||
|
<mn>2</mn>
|
||||||
|
</msup>
|
||||||
|
</mrow>
|
||||||
|
</msqrt>
|
||||||
|
</mrow>
|
||||||
|
<annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
|
||||||
|
</semantics>
|
||||||
|
</math>
|
||||||
|
</span>
|
||||||
|
<span class="katex-html" aria-hidden="true">
|
||||||
|
<span class="strut" style="height: 0.913389em;"></span>
|
||||||
|
<span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
|
||||||
|
<span class="base textstyle uncramped">
|
||||||
|
<span class="mord mathit">c</span>
|
||||||
|
<span class="mrel">=</span>
|
||||||
|
<span class="mord">±</span>
|
||||||
|
<span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
|
||||||
|
<span class="style-wrap reset-textstyle textstyle uncramped">√</span>
|
||||||
|
</span>
|
||||||
|
<span class="vlist">
|
||||||
|
<span class="" style="top: 0em;">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 1em;"></span>
|
||||||
|
</span>
|
||||||
|
<span class="mord textstyle cramped">
|
||||||
|
<span class="mord">
|
||||||
|
<span class="mord mathit">a</span>
|
||||||
|
<span class="msupsub">
|
||||||
|
<span class="vlist">
|
||||||
|
<span class="" style="top: -0.289em; margin-right: 0.05em;">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 0em;"></span>
|
||||||
|
</span>
|
||||||
|
<span class="reset-textstyle scriptstyle cramped">
|
||||||
|
<span class="mord mathrm">2</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="baseline-fix">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 0em;"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="mbin">+</span>
|
||||||
|
<span class="mord">
|
||||||
|
<span class="mord mathit">b</span>
|
||||||
|
<span class="msupsub">
|
||||||
|
<span class="vlist">
|
||||||
|
<span class="" style="top: -0.289em; margin-right: 0.05em;">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 0em;"></span>
|
||||||
|
</span>
|
||||||
|
<span class="reset-textstyle scriptstyle cramped">
|
||||||
|
<span class="mord mathrm">2</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="baseline-fix">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 0em;"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="" style="top: -0.833389em;">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 1em;"></span>
|
||||||
|
</span>
|
||||||
|
<span class="reset-textstyle textstyle uncramped sqrt-line"></span>
|
||||||
|
</span>
|
||||||
|
<span class="baseline-fix">
|
||||||
|
<span class="fontsize-ensurer reset-size5 size5">
|
||||||
|
<span class="" style="font-size: 1em;"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
HTML
|
||||||
|
|
||||||
|
output_gfm = html_to_gfm(html)
|
||||||
|
expect(output_gfm.strip).to eq(gfm.strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'SanitizationFilter',
|
||||||
|
|
||||||
|
<<-GFM.strip_heredoc
|
||||||
|
<sub>sub</sub>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>dt</dt>
|
||||||
|
<dd>dd</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<kbd>kbd</kbd>
|
||||||
|
|
||||||
|
<q>q</q>
|
||||||
|
|
||||||
|
<samp>samp</samp>
|
||||||
|
|
||||||
|
<var>var</var>
|
||||||
|
|
||||||
|
<ruby>ruby</ruby>
|
||||||
|
|
||||||
|
<rt>rt</rt>
|
||||||
|
|
||||||
|
<rp>rp</rp>
|
||||||
|
|
||||||
|
<abbr>abbr</abbr>
|
||||||
|
GFM
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'SanitizationFilter',
|
||||||
|
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
```
|
||||||
|
Plain text
|
||||||
|
```
|
||||||
|
GFM
|
||||||
|
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
```ruby
|
||||||
|
def foo
|
||||||
|
bar
|
||||||
|
end
|
||||||
|
```
|
||||||
|
GFM
|
||||||
|
|
||||||
|
<<-GFM.strip_heredoc
|
||||||
|
Foo
|
||||||
|
|
||||||
|
This is an example of GFM
|
||||||
|
|
||||||
|
```js
|
||||||
|
Code goes here
|
||||||
|
```
|
||||||
|
GFM
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(
|
||||||
|
'MarkdownFilter',
|
||||||
|
|
||||||
|
"Line with two spaces at the end \nto insert a linebreak",
|
||||||
|
|
||||||
|
'`code`',
|
||||||
|
'`` code with ` ticks ``',
|
||||||
|
|
||||||
|
'> Quote',
|
||||||
|
|
||||||
|
# multiline quote
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
> Multiline
|
||||||
|
> Quote
|
||||||
|
>
|
||||||
|
> With multiple paragraphs
|
||||||
|
GFM
|
||||||
|
|
||||||
|
'![Image](https://example.com/image.png)',
|
||||||
|
|
||||||
|
'# Heading with no anchor link',
|
||||||
|
|
||||||
|
'[Link](https://example.com)',
|
||||||
|
|
||||||
|
'- List item',
|
||||||
|
|
||||||
|
# multiline list item
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
- Multiline
|
||||||
|
List item
|
||||||
|
GFM
|
||||||
|
|
||||||
|
# nested lists
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
- Nested
|
||||||
|
|
||||||
|
|
||||||
|
- Lists
|
||||||
|
GFM
|
||||||
|
|
||||||
|
# list with blockquote
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
- List
|
||||||
|
|
||||||
|
> Blockquote
|
||||||
|
GFM
|
||||||
|
|
||||||
|
'1. Numbered list item',
|
||||||
|
|
||||||
|
# multiline numbered list item
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
1. Multiline
|
||||||
|
Numbered list item
|
||||||
|
GFM
|
||||||
|
|
||||||
|
# nested numbered list
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
1. Nested
|
||||||
|
|
||||||
|
|
||||||
|
1. Numbered lists
|
||||||
|
GFM
|
||||||
|
|
||||||
|
'# Heading',
|
||||||
|
'## Heading',
|
||||||
|
'### Heading',
|
||||||
|
'#### Heading',
|
||||||
|
'##### Heading',
|
||||||
|
'###### Heading',
|
||||||
|
|
||||||
|
'**Bold**',
|
||||||
|
|
||||||
|
'_Italics_',
|
||||||
|
|
||||||
|
'~~Strikethrough~~',
|
||||||
|
|
||||||
|
'2^2',
|
||||||
|
|
||||||
|
'-----',
|
||||||
|
|
||||||
|
# table
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
| Centered | Right | Left |
|
||||||
|
|:--------:|------:|------|
|
||||||
|
| Foo | Bar | **Baz** |
|
||||||
|
| Foo | Bar | **Baz** |
|
||||||
|
GFM
|
||||||
|
|
||||||
|
# table with empty heading
|
||||||
|
<<-GFM.strip_heredoc,
|
||||||
|
| | x | y |
|
||||||
|
|---|---|---|
|
||||||
|
| a | 1 | 0 |
|
||||||
|
| b | 0 | 1 |
|
||||||
|
GFM
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :gfm_to_html, :markdown
|
||||||
|
|
||||||
|
def html_to_gfm(html)
|
||||||
|
js = <<-JS.strip_heredoc
|
||||||
|
(function(html) {
|
||||||
|
var node = document.createElement('div');
|
||||||
|
node.innerHTML = html;
|
||||||
|
return window.gl.CopyAsGFM.nodeToGFM(node);
|
||||||
|
})("#{escape_javascript(html)}")
|
||||||
|
JS
|
||||||
|
page.evaluate_script(js)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify(label, *gfms)
|
||||||
|
aggregate_failures(label) do
|
||||||
|
gfms.each do |gfm|
|
||||||
|
html = gfm_to_html(gfm)
|
||||||
|
output_gfm = html_to_gfm(html)
|
||||||
|
expect(output_gfm.strip).to eq(gfm.strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fake a `current_user` helper
|
||||||
|
def current_user
|
||||||
|
@feat.user
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
|
/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
|
||||||
/* global ShortcutsIssuable */
|
/* global ShortcutsIssuable */
|
||||||
|
|
||||||
|
/*= require copy_as_gfm */
|
||||||
/*= require shortcuts_issuable */
|
/*= require shortcuts_issuable */
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
@ -14,10 +15,12 @@
|
||||||
});
|
});
|
||||||
return describe('#replyWithSelectedText', function() {
|
return describe('#replyWithSelectedText', function() {
|
||||||
var stubSelection;
|
var stubSelection;
|
||||||
// Stub window.getSelection to return the provided String.
|
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
|
||||||
stubSelection = function(text) {
|
stubSelection = function(html) {
|
||||||
return window.getSelection = function() {
|
window.gl.utils.getSelectedFragment = function() {
|
||||||
return text;
|
var node = document.createElement('div');
|
||||||
|
node.innerHTML = html;
|
||||||
|
return node;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
|
@ -32,13 +35,13 @@
|
||||||
});
|
});
|
||||||
describe('with any selection', function() {
|
describe('with any selection', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
return stubSelection('Selected text.');
|
return stubSelection('<p>Selected text.</p>');
|
||||||
});
|
});
|
||||||
it('leaves existing input intact', function() {
|
it('leaves existing input intact', function() {
|
||||||
$(this.selector).val('This text was already here.');
|
$(this.selector).val('This text was already here.');
|
||||||
expect($(this.selector).val()).toBe('This text was already here.');
|
expect($(this.selector).val()).toBe('This text was already here.');
|
||||||
this.shortcut.replyWithSelectedText();
|
this.shortcut.replyWithSelectedText();
|
||||||
return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n");
|
return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
|
||||||
});
|
});
|
||||||
it('triggers `input`', function() {
|
it('triggers `input`', function() {
|
||||||
var triggered;
|
var triggered;
|
||||||
|
@ -61,16 +64,16 @@
|
||||||
});
|
});
|
||||||
describe('with a one-line selection', function() {
|
describe('with a one-line selection', function() {
|
||||||
return it('quotes the selection', function() {
|
return it('quotes the selection', function() {
|
||||||
stubSelection('This text has been selected.');
|
stubSelection('<p>This text has been selected.</p>');
|
||||||
this.shortcut.replyWithSelectedText();
|
this.shortcut.replyWithSelectedText();
|
||||||
return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
|
return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return describe('with a multi-line selection', function() {
|
return describe('with a multi-line selection', function() {
|
||||||
return it('quotes the selected lines as a group', function() {
|
return it('quotes the selected lines as a group', function() {
|
||||||
stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n");
|
stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>");
|
||||||
this.shortcut.replyWithSelectedText();
|
this.shortcut.replyWithSelectedText();
|
||||||
return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n");
|
return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
|
||||||
context "when no language is specified" do
|
context "when no language is specified" do
|
||||||
it "highlights as plaintext" do
|
it "highlights as plaintext" do
|
||||||
result = filter('<pre><code>def fun end</code></pre>')
|
result = filter('<pre><code>def fun end</code></pre>')
|
||||||
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>def fun end</code></pre>')
|
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when a valid language is specified" do
|
context "when a valid language is specified" do
|
||||||
it "highlights as that language" do
|
it "highlights as that language" do
|
||||||
result = filter('<pre><code class="ruby">def fun end</code></pre>')
|
result = filter('<pre><code class="ruby">def fun end</code></pre>')
|
||||||
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
|
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when an invalid language is specified" do
|
context "when an invalid language is specified" do
|
||||||
it "highlights as plaintext" do
|
it "highlights as plaintext" do
|
||||||
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
|
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
|
||||||
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>This is a test</code></pre>')
|
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
|
||||||
|
|
||||||
it "highlights as plaintext" do
|
it "highlights as plaintext" do
|
||||||
result = filter('<pre><code class="ruby">This is a test</code></pre>')
|
result = filter('<pre><code class="ruby">This is a test</code></pre>')
|
||||||
expect(result.to_html).to eq('<pre class="code highlight" v-pre="true"><code>This is a test</code></pre>')
|
expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue