mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 09:52:06 +03:00
f18a23434f
refs https://github.com/TryGhost/Ghost/issues/9505 - previous attempts at toggling the markup state failed because mobiledoc-kit was automatically setting the activeMarkupState after we made the replacement - by scheduling the toggle we change the active markup state after mobiledoc has done it's thing so you can continue typing without the markdown markup being applied and without needing to insert an unexpected space
280 lines
9.8 KiB
JavaScript
280 lines
9.8 KiB
JavaScript
import {
|
|
replaceWithHeaderSection,
|
|
replaceWithListSection
|
|
} from 'mobiledoc-kit/editor/text-input-handlers';
|
|
import {run} from '@ember/runloop';
|
|
|
|
// Text expansions watch text entry events and will look for matches, replacing
|
|
// the matches with additional markup, atoms, or cards
|
|
// https://github.com/bustlelabs/mobiledoc-kit#responding-to-text-input
|
|
|
|
// TODO: this was copied from our old Koenig editor, it could do with some
|
|
// comments, cleanup, and refactoring
|
|
|
|
export default function (editor) {
|
|
// We don't want to run all our content rules on every text entry event,
|
|
// instead we check to see if this text entry event could match a content
|
|
// rule, and only then run the rules. Right now we only want to match
|
|
// content ending with *, _, ), ~, and `. This could increase as we support
|
|
// more markdown.
|
|
|
|
editor.onTextInput({
|
|
name: 'inline_markdown',
|
|
match: /[*_)~`]$/,
|
|
run(editor, matches) {
|
|
let text = editor.range.head.section.textUntil(editor.range.head);
|
|
|
|
switch (matches[0]) {
|
|
case '*':
|
|
matchStrongStar(editor, text);
|
|
matchEmStar(editor, text);
|
|
break;
|
|
case '_':
|
|
matchStrongUnderscore(editor, text);
|
|
matchEmUnderscore(editor, text);
|
|
break;
|
|
case ')':
|
|
matchLink(editor, text);
|
|
matchImage(editor, text);
|
|
break;
|
|
case '~':
|
|
matchStrikethrough(editor, text);
|
|
break;
|
|
case '`':
|
|
matchCode(editor, text);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
/* block level markdown ------------------------------------------------- */
|
|
|
|
// mobiledoc-kit has `* ` already built-in so we only need to add `- `
|
|
editor.onTextInput({
|
|
name: 'md_ul',
|
|
match: /^- $/,
|
|
run(editor) {
|
|
replaceWithListSection(editor, 'ul');
|
|
}
|
|
});
|
|
|
|
editor.onTextInput({
|
|
name: 'md_blockquote',
|
|
match: /^> $/,
|
|
run(editor) {
|
|
replaceWithHeaderSection(editor, 'blockquote');
|
|
}
|
|
});
|
|
|
|
editor.onTextInput({
|
|
name: 'md_hr',
|
|
match: /^---$/,
|
|
run(editor) {
|
|
let {range: {head, head: {section}}} = editor;
|
|
|
|
// Skip if cursor is not at end of section
|
|
if (!head.isTail()) {
|
|
return;
|
|
}
|
|
|
|
// Skip if section is a list item
|
|
if (section.isListItem) {
|
|
return;
|
|
}
|
|
|
|
editor.run((postEditor) => {
|
|
let card = postEditor.builder.createCardSection('hr');
|
|
let needsTrailingParagraph = !section.next;
|
|
|
|
postEditor.replaceSection(section, card);
|
|
|
|
// add an empty paragraph after if necessary so writing can continue
|
|
if (needsTrailingParagraph) {
|
|
let newSection = postEditor.builder.createMarkupSection('p');
|
|
postEditor.insertSectionAtEnd(newSection);
|
|
postEditor.setRange(newSection.tailPosition());
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/* inline markdown ------------------------------------------------------ */
|
|
|
|
function matchStrongStar(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|\s)\*\*([^\s*].+?[^\s*])\*\*/);
|
|
if (matches) {
|
|
let match = matches[0][0] === '*' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-(match.length));
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let bold = postEditor.builder.createMarkup('strong');
|
|
postEditor.insertTextWithMarkup(position, matches[2], [bold]);
|
|
});
|
|
}
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('strong');
|
|
});
|
|
}
|
|
|
|
function matchStrongUnderscore(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|\s)__([^\s_].+?[^\s_])__/);
|
|
if (matches) {
|
|
let match = matches[0][0] === '_' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-(match.length));
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let bold = postEditor.builder.createMarkup('strong');
|
|
postEditor.insertTextWithMarkup(position, matches[2], [bold]);
|
|
});
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('strong');
|
|
});
|
|
}
|
|
}
|
|
|
|
function matchEmStar(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|\s)\*([^\s*][^*]+?[^\s])\*/);
|
|
if (matches) {
|
|
let match = matches[0][0] === '*' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-(match.length));
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let em = postEditor.builder.createMarkup('em');
|
|
postEditor.insertTextWithMarkup(position, matches[2], [em]);
|
|
});
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('em');
|
|
});
|
|
}
|
|
}
|
|
|
|
function matchEmUnderscore(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|\s)_([^\s_][^_]+?[^\s])_/);
|
|
if (matches) {
|
|
let match = matches[0][0] === '_' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-(match.length));
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let em = postEditor.builder.createMarkup('em');
|
|
postEditor.insertTextWithMarkup(position, matches[2], [em]);
|
|
});
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('code');
|
|
});
|
|
}
|
|
}
|
|
|
|
function matchStrikethrough(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|\s)~([^\s~][^~]+?[^\s])~/);
|
|
if (matches) {
|
|
let match = matches[0][0] === '~' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-(match.length));
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let s = postEditor.builder.createMarkup('s');
|
|
postEditor.insertTextWithMarkup(position, matches[2], [s]);
|
|
});
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('s');
|
|
});
|
|
}
|
|
}
|
|
|
|
function matchCode(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|\s)`([^\s`][^`]+?[^\s])`/);
|
|
if (matches) {
|
|
let match = matches[0][0] === '`' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-(match.length));
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let code = postEditor.builder.createMarkup('code');
|
|
postEditor.insertTextWithMarkup(position, matches[2], [code]);
|
|
});
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('code');
|
|
});
|
|
}
|
|
}
|
|
|
|
function matchLink(editor, text) {
|
|
let {range} = editor;
|
|
let matches = text.match(/(^|[^!])\[(.*?)\]\((.*?)\)$/);
|
|
if (matches) {
|
|
let url = matches[3];
|
|
let text = matches[2];
|
|
let match = matches[0][0] === '[' ? matches[0] : matches[0].substr(1);
|
|
range = range.extend(-match.length);
|
|
|
|
editor.run((postEditor) => {
|
|
let position = postEditor.deleteRange(range);
|
|
let a = postEditor.builder.createMarkup('a', {href: url});
|
|
postEditor.insertTextWithMarkup(position, text, [a]);
|
|
});
|
|
|
|
// must be scheduled so that the toggle isn't reset automatically
|
|
run.schedule('actions', this, function () {
|
|
editor.toggleMarkup('code');
|
|
});
|
|
}
|
|
}
|
|
|
|
function matchImage(editor, text) {
|
|
let matches = text.match(/^!\[(.*?)\]\((.*?)\)$/);
|
|
if (matches) {
|
|
let {range: {head, head: {section}}} = editor;
|
|
let src = matches[2];
|
|
let alt = matches[1];
|
|
|
|
// skip if cursor is not at end of section
|
|
if (!head.isTail()) {
|
|
return;
|
|
}
|
|
|
|
// mobiledoc lists don't support cards
|
|
if (section.isListItem) {
|
|
return;
|
|
}
|
|
|
|
editor.run((postEditor) => {
|
|
let card = postEditor.builder.createCardSection('image', {src, alt});
|
|
// need to check the section before replacing else it will always
|
|
// add a trailing paragraph
|
|
let needsTrailingParagraph = !section.next;
|
|
|
|
editor.range.extend(-(matches[0].length));
|
|
postEditor.replaceSection(editor.range.headSection, card);
|
|
|
|
if (needsTrailingParagraph) {
|
|
let newSection = editor.builder.createMarkupSection('p');
|
|
postEditor.insertSectionAtEnd(newSection);
|
|
postEditor.setRange(newSection.tailPosition());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|