Commit 5ccfd533 authored by Nick Stenning's avatar Nick Stenning

Merge pull request #3017 from hypothesis/editor-markdown-commands-refactor

Refactor and add tests for annotation editor toolbar commands
parents 21f899d1 692ea39a
......@@ -5,6 +5,7 @@
var angular = require('angular');
var katex = require('katex');
var commands = require('../markdown-commands');
var mediaEmbedder = require('../media-embedder');
var loadMathJax = function() {
......@@ -42,286 +43,82 @@ module.exports = function($filter, $sanitize, $sce, $timeout) {
var inputEl = angular.element(input);
var output = elem[0].querySelector('.js-markdown-preview');
var userSelection = function() {
var selection;
if (input.selectionStart !== undefined) {
var startPos = input.selectionStart;
var endPos = input.selectionEnd;
var selectedText = input.value.substring(startPos, endPos);
var textBefore = input.value.substring(0, (startPos));
var textAfter = input.value.substring(endPos);
selection = {
before: textBefore,
after: textAfter,
selection: selectedText,
start: startPos,
end: endPos
};
}
return selection;
};
/**
* Transform the editor's input field with an editor command.
*/
function updateState(newStateFn) {
var newState = newStateFn({
text: input.value,
selectionStart: input.selectionStart,
selectionEnd: input.selectionEnd,
});
var insertMarkup = function(value, selectionStart, selectionEnd) {
// New value is set for the input
input.value = value;
// A new selection is set, or the cursor is positioned inside the input.
input.selectionStart = selectionStart;
input.selectionEnd = selectionEnd;
// Focus the input
return input.focus();
};
input.value = newState.text;
input.selectionStart = newState.selectionStart;
input.selectionEnd = newState.selectionEnd;
var applyInlineMarkup = function(markupL, innertext, markupR) {
if (!markupR) {
markupR = markupL;
}
var newtext;
var end;
var start;
var text = userSelection();
if (text.selection === "") {
newtext = text.before + markupL + innertext + markupR + text.after;
start = (text.before + markupL).length;
end = (text.before + innertext + markupR).length;
return insertMarkup(newtext, start, end);
} else {
// Check to see if markup has already been applied before to the selection.
var slice1 = text.before.slice(text.before.length - markupL.length);
var slice2 = text.after.slice(0, markupR.length);
if (slice1 === markupL && slice2 === markupR) {
// Remove markup
newtext = (
text.before.slice(0, (text.before.length - markupL.length)) +
text.selection + text.after.slice(markupR.length)
);
start = text.before.length - markupL.length;
end = (text.before + text.selection).length - markupR.length;
return insertMarkup(newtext, start, end);
} else {
// Apply markup
newtext = text.before + markupL + text.selection + markupR + text.after;
start = (text.before + markupL).length;
end = (text.before + text.selection + markupR).length;
return insertMarkup(newtext, start, end);
}
}
};
// The input field currently loses focus when the contents are
// changed. This re-focuses the input field but really it should
// happen automatically.
input.focus();
}
scope.insertBold = function() {
return applyInlineMarkup("**", "Bold");
updateState(function (state) {
return commands.toggleSpanStyle(state, '**', '**', 'Bold');
});
};
scope.insertItalic = function() {
return applyInlineMarkup("*", "Italic");
updateState(function (state) {
return commands.toggleSpanStyle(state, '*', '*', 'Italic');
});
};
scope.insertMath = function() {
var text = userSelection();
var index = text.before.length;
if (
index === 0 ||
input.value[index - 1] === '\n' ||
(input.value[index - 1] === '$' && input.value[index - 2] === '$')
) {
return applyInlineMarkup('$$', 'Insert LaTeX');
} else {
return applyInlineMarkup('\\(', 'Insert LaTeX', '\\)');
}
};
updateState(function (state) {
var before = state.text.slice(0, state.selectionStart);
scope.insertLink = function() {
var text = userSelection();
var newtext;
var start;
var end;
if (text.selection === "") {
newtext = text.before + "[Link Text](https://example.com)" + text.after;
start = text.before.length + 1;
end = text.before.length + 10;
return insertMarkup(newtext, start, end);
} else {
// Check to see if markup has already been applied to avoid double presses.
if (text.selection === "Link Text" || text.selection === "https://example.com") {
return;
if (before.length === 0 ||
before.slice(-1) === '\n' ||
before.slice(-2) === '$$') {
return commands.toggleSpanStyle(state, '$$', '$$', 'Insert LaTeX');
} else {
return commands.toggleSpanStyle(state, '\\(', '\\)',
'Insert LaTeX');
}
newtext = text.before + '[' + text.selection + '](https://example.com)' + text.after;
start = (text.before + text.selection).length + 3;
end = (text.before + text.selection).length + 22;
return insertMarkup(newtext, start, end);
}
});
};
scope.insertIMG = function() {
var text = userSelection();
var newtext;
var start;
var end;
if (text.selection === "") {
newtext = text.before + "![Image Description](https://yourimage.jpg)" + text.after;
start = text.before.length + 21;
end = text.before.length + 42;
return insertMarkup(newtext, start, end);
} else {
// Check to see if markup has already been applied to avoid double presses.
if (text.selection === "https://yourimage.jpg") {
return;
}
newtext = text.before + '![' + text.selection + '](https://yourimage.jpg)' + text.after;
start = (text.before + text.selection).length + 4;
end = (text.before + text.selection).length + 25;
return insertMarkup(newtext, start, end);
}
scope.insertLink = function() {
updateState(function (state) {
return commands.convertSelectionToLink(state);
});
};
/* jshint maxcomplexity:false */
scope.applyBlockMarkup = function(markup) {
var text = userSelection();
var ch;
var value;
var start;
var end;
var index;
var i;
var newtext;
if (text.selection !== "") {
var newstring = "";
index = text.before.length;
if (index === 0) {
// The selection takes place at the very start of the input
for (var j = 0; j < text.selection.length; j++) {
ch = text.selection[j];
if (ch === "\n") {
newstring = newstring + "\n" + markup;
} else if (index === 0) {
newstring = newstring + markup + ch;
} else {
newstring = newstring + ch;
}
index += 1;
}
} else {
var newlinedetected = false;
if (input.value.substring(index - 1).charAt(0) === "\n") {
// Look to see if the selection falls at the beginning of a new line.
newstring = newstring + markup;
newlinedetected = true;
}
for (var k = 0; k < text.selection.length; k++) {
ch = text.selection[k];
if (ch === "\n") {
newstring = newstring + "\n" + markup;
newlinedetected = true;
} else {
newstring = newstring + ch;
}
index += 1;
}
if (!newlinedetected) {
// Edge case: The selection does not include any new lines and does not start at 0.
// We need to find the newline before the currently selected text and add markup there.
i = 0;
var indexoflastnewline;
newstring = "";
var iterable = text.before + text.selection;
for (var i1 = 0; i1 < iterable.length; i1++) {
ch = iterable[i1];
if (ch === "\n") {
indexoflastnewline = i;
}
newstring = newstring + ch;
i++;
}
if (indexoflastnewline === undefined) {
// The partial selection happens to fall on the firstline
newstring = markup + newstring;
} else {
newstring = (
newstring.substring(0, (indexoflastnewline + 1)) +
markup + newstring.substring(indexoflastnewline + 1)
);
}
value = newstring + text.after;
start = (text.before + markup).length;
end = (text.before + text.selection + markup).length;
insertMarkup(value, start, end);
return;
}
}
// Sets input value and selection for cases where there are new lines in the selection
// or the selection is at the start
value = text.before + newstring + text.after;
start = (text.before + newstring).length;
end = (text.before + newstring).length;
return insertMarkup(value, start, end);
} else if (input.value.substring((text.start - 1 ), text.start) === "\n") {
// Edge case, no selection, the cursor is on a new line.
value = text.before + markup + text.selection + text.after;
start = (text.before + markup).length;
end = (text.before + markup).length;
return insertMarkup(value, start, end);
} else {
// No selection, cursor is not on new line.
// Check to see if markup has already been inserted.
if (text.before.slice(text.before.length - markup.length) === markup) {
newtext = (
text.before.substring(0, (index)) + "\n" +
text.before.substring(index + 1 + markup.length) + text.after
);
}
i = 0;
for (var i2 = 0, char; i2 < text.before.length; i2++) {
char = text.before[i2];
if (char === "\n" && i !== 0) {
index = i;
}
i += 1;
}
if (!index) { // If the line of text happens to fall on the first line and index is not set.
// Check to see if markup has already been inserted and undo it.
if (text.before.slice(0, markup.length) === markup) {
newtext = text.before.substring(markup.length) + text.after;
start = text.before.length - markup.length;
end = text.before.length - markup.length;
return insertMarkup(newtext, start, end);
} else {
newtext = markup + text.before.substring(0) + text.after;
start = (text.before + markup).length;
end = (text.before + markup).length;
return insertMarkup(newtext, start, end);
}
// Check to see if markup has already been inserted and undo it.
} else if (text.before.slice((index + 1), (index + 1 + markup.length)) === markup) {
newtext = (
text.before.substring(0, (index)) + "\n" +
text.before.substring(index + 1 + markup.length) + text.after
);
start = text.before.length - markup.length;
end = text.before.length - markup.length;
return insertMarkup(newtext, start, end);
} else {
newtext = (
text.before.substring(0, (index)) + "\n" +
markup + text.before.substring(index + 1) + text.after
);
start = (text.before + markup).length;
end = (text.before + markup).length;
return insertMarkup(newtext, start, end);
}
}
scope.insertIMG = function() {
updateState(function (state) {
return commands.convertSelectionToLink(state,
commands.LinkType.IMAGE_LINK);
});
};
scope.insertList = function() {
return scope.applyBlockMarkup("* ");
updateState(function (state) {
return commands.toggleBlockStyle(state, '* ');
});
};
scope.insertNumList = function() {
return scope.applyBlockMarkup("1. ");
updateState(function (state) {
return commands.toggleBlockStyle(state, '1. ');
});
};
scope.insertQuote = function() {
return scope.applyBlockMarkup("> ");
updateState(function (state) {
return commands.toggleBlockStyle(state, '> ');
});
};
// Keyboard shortcuts for bold, italic, and link.
......@@ -449,7 +246,7 @@ module.exports = function($filter, $sanitize, $sce, $timeout) {
// Re-render the markdown when the view needs updating.
ctrl.$render = function() {
if (!scope.readOnly && !scope.preview) {
inputEl.val((ctrl.$viewValue || ''));
input.value = ctrl.$viewValue || '';
}
var value = ctrl.$viewValue || '';
output.innerHTML = renderMathAndMarkdown(value);
......@@ -462,12 +259,10 @@ module.exports = function($filter, $sanitize, $sce, $timeout) {
// React to the changes to the input
inputEl.bind('blur change keyup', function() {
return $timeout(function() {
return ctrl.$setViewValue(inputEl.val());
});
ctrl.$setViewValue(input.value);
});
// Reset height of output div incase it has been changed.
// Reset height of output div in case it has been changed.
// Re-render when it becomes uneditable.
// Auto-focus the input box when the widget becomes editable.
return scope.$watch('readOnly', function(readOnly) {
......@@ -475,7 +270,7 @@ module.exports = function($filter, $sanitize, $sce, $timeout) {
output.style.height = "";
ctrl.$render();
if (!readOnly) {
return $timeout(function() { return input.focus(); });
input.focus();
}
});
},
......@@ -483,7 +278,7 @@ module.exports = function($filter, $sanitize, $sce, $timeout) {
require: '?ngModel',
restrict: 'E',
scope: {
readOnly: '=',
readOnly: '<',
required: '@'
},
templateUrl: 'markdown.html'
......
......@@ -39,6 +39,14 @@ describe('markdown', function () {
return contentElement.innerHTML;
}
function mockFormattingCommand() {
return {
text: 'formatted text',
selectionStart: 0,
selectionEnd: 0,
};
}
before(function () {
angular.module('app', ['ngSanitize'])
.directive('markdown', proxyquire('../markdown', {
......@@ -48,6 +56,12 @@ describe('markdown', function () {
return 'math:' + input.replace(/$$/g, '');
},
},
'../markdown-commands': {
convertSelectionToLink: mockFormattingCommand,
toggleBlockStyle: mockFormattingCommand,
toggleSpanStyle: mockFormattingCommand,
LinkType: require('../../markdown-commands').LinkType,
},
'@noCallThru': true,
}))
.filter('converter', function () {
......@@ -102,4 +116,33 @@ describe('markdown', function () {
'rendered:math:\\displaystyle {x*2}rendered:');
});
});
describe('toolbar buttons', function () {
it('should apply formatting when clicking toolbar buttons', function () {
var editor = util.createDirective(document, 'markdown', {
readOnly: false,
ngModel: 'Hello World',
});
var input = inputElement(editor);
var buttons = editor[0].querySelectorAll('.markdown-tools-button');
for (var i=0; i < buttons.length; i++) {
input.value = 'original text';
angular.element(buttons[i]).click();
assert.equal(input.value, mockFormattingCommand().text);
}
});
});
describe('editing', function () {
it('should update the input model', function () {
var editor = util.createDirective(document, 'markdown', {
readOnly: false,
ngModel: 'Hello World',
});
var input = inputElement(editor);
input.value = 'new text';
util.sendEvent(input, 'change');
assert.equal(editor.scope.ngModel, 'new text');
});
});
});
'use strict';
/* global angular */
/**
* Converts a camelCase name into hyphenated ('camel-case') form.
*
......@@ -101,7 +103,7 @@ function createDirective(document, name, attrs, initialScope, initialHtml, opts)
angular.mock.inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
})
});
var templateElement = document.createElement(hyphenate(name));
Object.keys(attrs).forEach(function (key) {
var attrName = hyphenate(key);
......@@ -145,12 +147,22 @@ function createDirective(document, name, attrs, initialScope, initialHtml, opts)
childScope.$digest();
element.ctrl = element.controller(name);
return element;
}
};
return linkDirective(initialScope);
}
/** Helper to dispatch a native event to a DOM element. */
function sendEvent(element, eventType) {
// createEvent() used instead of Event constructor
// for PhantomJS compatibility
var event = document.createEvent('Event');
event.initEvent(eventType, true /* bubbles */, true /* cancelable */);
element.dispatchEvent(event);
}
module.exports = {
createDirective: createDirective,
ngModule: ngModule,
sendEvent: sendEvent,
};
'use strict';
// Minimal set of polyfills for PhantomJS 1.x under Karma.
// this Polyfills:
//
......
'use strict';
/**
* Commands for toggling markdown formatting of a selection
* in an input field.
*
* All of the functions in this module take as input the current state
* of the input field, parameters for the operation to perform and return the
* new state of the input field.
*/
/**
* Describes the state of a plain text input field.
*
* interface EditorState {
* text: string;
* selectionStart: number;
* selectionEnd: number;
* }
*/
/**
* Types of Markdown link that can be inserted with
* convertSelectionToLink()
*/
var LinkType = {
ANCHOR_LINK: 0,
IMAGE_LINK: 1,
};
/**
* Replace text in an input field and return the new state.
*
* @param {EditorState} state - The state of the input field.
* @param {number} pos - The start position of the text to remove.
* @param {number} length - The number of characters to remove.
* @param {string} text - The replacement text to insert at @p pos.
* @return {EditorState} - The new state of the input field.
*/
function replaceText(state, pos, length, text) {
var newSelectionStart = state.selectionStart;
var newSelectionEnd = state.selectionEnd;
if (newSelectionEnd <= pos) {
// 1. Selection is before replaced text: Leave selection unchanged
} else if (newSelectionStart >= pos + length) {
// 2. Selection is after replaced text:
// Increment (start, end) by difference in length between original and
// replaced text
newSelectionStart += text.length - length;
newSelectionEnd += text.length - length;
} else if (newSelectionStart <= pos &&
newSelectionEnd >= pos + length) {
// 3. Selection fully contains replaced text:
// Increment end by difference in length between original and replaced
// text
newSelectionEnd += text.length - length;
} else if (newSelectionStart < pos &&
newSelectionEnd < pos + length) {
// 4. Selection overlaps start but not end of replaced text:
// Decrement start to start of replacement text
newSelectionStart = pos;
} else if (newSelectionStart < pos + length &&
newSelectionEnd > pos + length) {
// 5. Selection overlaps end but not start of replaced text:
// Increment end by difference in length between original and replaced
// text
newSelectionEnd += text.length - length;
} else if (pos < newSelectionStart &&
pos + length > newSelectionEnd) {
// 6. Replaced text fully contains selection:
// Expand selection to replaced text
newSelectionStart = pos;
newSelectionEnd = pos + length;
}
return {
text: state.text.slice(0, pos) + text + state.text.slice(pos + length),
selectionStart: newSelectionStart,
selectionEnd: newSelectionEnd,
};
}
/**
* Convert the selected text into a Markdown link.
*
* @param {EditorState} state - The current state of the input field.
* @param {LinkType} linkType - The type of link to insert.
* @return {EditorState} - The new state of the input field.
*/
function convertSelectionToLink(state, linkType) {
if (typeof linkType === 'undefined') {
linkType = LinkType.ANCHOR_LINK;
}
var selection = state.text.slice(state.selectionStart, state.selectionEnd);
var linkPrefix = '';
if (linkType === LinkType.IMAGE_LINK) {
linkPrefix = '!';
}
var newState;
if (selection.match(/[a-z]+:.*/)) {
// Selection is a URL, wrap it with a link and use the selection as
// the target.
var dummyLabel = 'Description';
newState = replaceText(state, state.selectionStart, selection.length,
linkPrefix + '[' + dummyLabel + '](' + selection + ')');
newState.selectionStart = state.selectionStart + linkPrefix.length + 1;
newState.selectionEnd = newState.selectionStart + dummyLabel.length;
return newState;
} else {
// Selection is not a URL, wrap it with a link and use the selection as
// the label. Change the selection to the dummy link.
var beforeURL = linkPrefix + '[' + selection + '](';
var dummyLink = 'http://insert-your-link-here.com';
newState = replaceText(state, state.selectionStart, selection.length,
beforeURL + dummyLink + ')');
newState.selectionStart = state.selectionStart + beforeURL.length;
newState.selectionEnd = newState.selectionStart + dummyLink.length;
return newState;
}
}
/**
* Toggle Markdown-style formatting around a span of text.
*
* @param {EditorState} state - The current state of the input field.
* @param {string} prefix - The prefix to add or remove
* before the selection.
* @param {string?} suffix - The suffix to add or remove after the selection,
* defaults to being the same as the prefix.
* @param {string} placeholder - The text to insert between 'prefix' and
* 'suffix' if the input text is empty.
* @return {EditorState} The new state of the input field.
*/
function toggleSpanStyle(state, prefix, suffix, placeholder) {
if (typeof suffix === 'undefined') {
suffix = prefix;
}
var selectionPrefix = state.text.slice(state.selectionStart - prefix.length,
state.selectionStart);
var selectionSuffix = state.text.slice(state.selectionEnd,
state.selectionEnd + prefix.length);
var newState = state;
if (state.selectionStart === state.selectionEnd && placeholder) {
newState = replaceText(state, state.selectionStart, 0, placeholder);
newState.selectionEnd = newState.selectionStart + placeholder.length;
}
if (selectionPrefix === prefix && selectionSuffix === suffix) {
newState = replaceText(newState, newState.selectionStart - prefix.length,
prefix.length, '');
newState = replaceText(newState, newState.selectionEnd, suffix.length, '');
} else {
newState = replaceText(newState, newState.selectionStart, 0, prefix);
newState = replaceText(newState, newState.selectionEnd, 0, suffix);
}
return newState;
}
function startOfLine(str, pos) {
var start = str.lastIndexOf('\n', pos);
if (start < 0) {
return 0;
} else {
return start + 1;
}
}
function endOfLine(str, pos) {
var end = str.indexOf('\n', pos);
if (end < 0) {
return str.length;
} else {
return end;
}
}
/**
* Transform lines between two positions in an input field.
*
* @param {EditorState} state - The initial state of the input field
* @param {number} start - The start position within the input text
* @param {number} end - The end position within the input text
* @param {(EditorState, number) => EditorState} callback
* - Callback which is invoked with the current state of the input and
* the start of the current line and returns the new state of the input.
*/
function transformLines(state, start, end, callback) {
var lineStart = startOfLine(state.text, start);
var lineEnd = endOfLine(state.text, start);
while (lineEnd <= endOfLine(state.text, end)) {
var isLastLine = lineEnd === state.text.length;
var currentLineLength = lineEnd - lineStart;
state = callback(state, lineStart, lineEnd);
var newLineLength = endOfLine(state.text, lineStart) - lineStart;
end += newLineLength - currentLineLength;
if (isLastLine) {
break;
}
lineStart = lineStart + newLineLength + 1;
lineEnd = endOfLine(state.text, lineStart);
}
return state;
}
/**
* Toggle Markdown-style formatting around a block of text.
*
* @param {EditorState} state - The current state of the input field.
* @param {string} prefix - The prefix to add or remove before each line
* of the selection.
* @return {EditorState} - The new state of the input field.
*/
function toggleBlockStyle(state, prefix) {
var start = state.selectionStart;
var end = state.selectionEnd;
// Test whether all lines in the selected range already have the style
// applied
var blockHasStyle = true;
transformLines(state, start, end, function (state, lineStart) {
if (state.text.slice(lineStart, lineStart + prefix.length) !== prefix) {
blockHasStyle = false;
}
return state;
});
if (blockHasStyle) {
// Remove the formatting.
return transformLines(state, start, end, function (state, lineStart) {
return replaceText(state, lineStart, prefix.length, '');
});
} else {
// Add the block style to any lines which do not already have it applied
return transformLines(state, start, end, function (state, lineStart) {
if (state.text.slice(lineStart, lineStart + prefix.length) === prefix) {
return state;
} else {
return replaceText(state, lineStart, 0, prefix);
}
});
}
}
module.exports = {
toggleSpanStyle: toggleSpanStyle,
toggleBlockStyle: toggleBlockStyle,
convertSelectionToLink: convertSelectionToLink,
LinkType: LinkType,
};
'use strict';
var commands = require('../markdown-commands');
/**
* Convert a string containing '<sel>' and '</sel>' markers
* to a commands.EditorState.
*/
function parseState(text) {
var startMarker = '<sel>';
var endMarker = '</sel>';
var selStart = text.indexOf(startMarker);
var selEnd = text.indexOf(endMarker);
if (selStart < 0) {
throw new Error('Input field does not contain a selection start');
}
if (selEnd < 0) {
throw new Error('Input field does not contain a selection end');
}
return {
text: text.replace(/<\/?sel>/g, ''),
selectionStart: selStart,
selectionEnd: selEnd - startMarker.length,
};
}
/**
* Convert a commands.EditorState to a string containing '<sel>'
* and '</sel>' markers.
*/
function formatState(state) {
var selectionStart = state.selectionStart;
var selectionEnd = state.selectionEnd;
var text = state.text;
return text.slice(0, selectionStart) + '<sel>' +
text.slice(selectionStart, selectionEnd) + '</sel>' +
text.slice(selectionEnd);
}
describe('markdown commands', function () {
describe('span formatting', function () {
function toggle(state, prefix, suffix, placeholder) {
prefix = prefix || '**';
suffix = suffix || '**';
return commands.toggleSpanStyle(state, prefix, suffix, placeholder);
}
it('adds formatting to spans', function () {
var output = toggle(parseState('make <sel>text</sel> bold'));
assert.equal(formatState(output), 'make **<sel>text</sel>** bold');
});
it('removes formatting from spans', function () {
var output = toggle(parseState('make **<sel>text</sel>** bold'));
assert.equal(formatState(output), 'make <sel>text</sel> bold');
});
it('adds formatting to spans when the prefix and suffix differ', function () {
var output = toggle(parseState('make <sel>math</sel> mathy'), '\\(',
'\\)');
assert.equal(formatState(output), 'make \\(<sel>math</sel>\\) mathy');
});
it('inserts placeholders if the selection is empty', function () {
var output = toggle(parseState('make <sel></sel> bold'), '**',
undefined, 'Bold');
assert.equal(formatState(output), 'make **<sel>Bold</sel>** bold');
});
});
describe('block formatting', function () {
function toggle(state) {
return commands.toggleBlockStyle(state, '> ');
}
it('adds formatting to blocks', function () {
var output = toggle(parseState('one\n<sel>two\nthree</sel>\nfour'));
assert.equal(formatState(output), 'one\n> <sel>two\n> three</sel>\nfour');
});
it('removes formatting from blocks', function () {
var output = toggle(parseState('one \n<sel>> two\n> three</sel>\nfour'));
assert.equal(formatState(output), 'one \n<sel>two\nthree</sel>\nfour');
});
it('preserves the selection', function () {
var output = toggle(parseState('one <sel>two\nthree </sel>four'));
assert.equal(formatState(output), '> one <sel>two\n> three </sel>four');
});
});
describe('link formatting', function () {
var linkify = function (text, linkType) {
return commands.convertSelectionToLink(parseState(text), linkType);
};
it('converts text to links', function () {
var output = linkify('one <sel>two</sel> three');
assert.equal(formatState(output),
'one [two](<sel>http://insert-your-link-here.com</sel>) three');
});
it('converts URLs to links', function () {
var output = linkify('one <sel>http://foobar.com</sel> three');
assert.equal(formatState(output),
'one [<sel>Description</sel>](http://foobar.com) three');
});
it('converts URLs to image links', function () {
var output = linkify('one <sel>http://foobar.com</sel> three',
commands.LinkType.IMAGE_LINK);
assert.equal(formatState(output),
'one ![<sel>Description</sel>](http://foobar.com) three');
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment