Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor fast follow #653

Merged
merged 8 commits into from
Oct 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,6 @@ Object {
Object {
"isVoid": true,
"kind": "block",
"nodes": undefined,
"type": "thematic-break",
},
Object {
Expand Down Expand Up @@ -286,7 +285,6 @@ Object {
Object {
"isVoid": true,
"kind": "block",
"nodes": undefined,
"type": "thematic-break",
},
Object {
Expand Down Expand Up @@ -1200,7 +1198,6 @@ more important, there there are only so many sizes that you can use.",
Object {
"isVoid": true,
"kind": "block",
"nodes": undefined,
"type": "thematic-break",
},
Object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,42 @@ import { Block, Text } from 'slate';

export default onKeyDown;

/**
* Minimal re-implementation of Slate's undo/redo functionality, but with focus
* forced back into editor afterward.
*/
function changeHistory(change, type) {

/**
* Get the history for undo or redo (determined via `type` param).
*/
const { history } = change.state;
if (!history) return;
const historyOfType = history[`${type}s`];

/**
* If there is a next history item to apply, and it's valid, apply it.
*/
const next = historyOfType.first();
const historyOfTypeIsValid = historyOfType.size > 1
|| next.length > 1
|| next[0].type !== 'set_selection';

if (next && historyOfTypeIsValid) {
change[type]();
}

/**
* Always ensure focus is set.
*/
return change.focus();
}

function onKeyDown(e, data, change) {
const createDefaultBlock = () => {
return Block.create({
type: 'paragraph',
nodes: [Text.create('')]
nodes: [Text.create('')],
});
};
if (data.key === 'enter') {
Expand Down Expand Up @@ -37,6 +68,22 @@ function onKeyDown(e, data, change) {
}

if (data.isMod) {

/**
* Undo and redo work automatically with Slate, but focus is lost in certain
* actions. We override Slate's built in undo/redo here and force focus
* back to the editor each time.
*/
if (data.key === 'y') {
e.preventDefault();
return changeHistory(change, 'redo');
}

if (data.key === 'z') {
e.preventDefault();
return changeHistory(change, data.isShift ? 'redo' : 'undo');
}

const marks = {
b: 'bold',
i: 'italic',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ const SoftBreak = (options = {}) => ({
if (options.shift && e.shiftKey == false) return;

const { onlyIn, ignoreIn, defaultBlock = 'paragraph' } = options;
const { type, nodes } = change.state.startBlock;
const { type, text } = change.state.startBlock;
if (onlyIn && !onlyIn.includes(type)) return;
if (ignoreIn && ignoreIn.includes(type)) return;

const shouldClose = nodes.last().characters.last() === '\n';
const shouldClose = text.endsWith('\n');
if (shouldClose) {
const trimmed = change.deleteBackward(1);
return trimmed.insertBlock(defaultBlock);
return change
.deleteBackward(1)
.insertBlock(defaultBlock);
}

const textNode = Text.create('\n');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,39 @@ const shortcodesAtRoot = {
},
};

const rules = [ enforceNeverEmpty, shortcodesAtRoot ];
/**
* Ensure that trailing shortcodes are followed by an empty paragraph.
*/
const noTrailingShortcodes = {
match: object => object.kind === 'document',
validate: doc => {
return doc.findDescendant(node => {
return node.type === 'shortcode' && doc.getBlocks().last().key === node.key;
});
},
normalize: (change, doc, node) => {
const text = Text.create('');
const block = Block.create({ type: 'paragraph', nodes: [ text ] });
return change.insertNodeByKey(doc.key, doc.get('nodes').size, block);
},
};

/**
* Ensure that code blocks contain no marks.
*/
const codeBlocksContainPlainText = {
match: node => node.type === 'code',
validate: node => {
const invalidChild = node.getTexts().find(text => !text.getMarks().isEmpty());
return invalidChild || null;
},
normalize: (change, node, invalidChild) => {
invalidChild.getMarks().forEach(mark => {
change.removeMarkByKey(invalidChild.key, 0, invalidChild.get('characters').size, mark);
});
},
};

const rules = [ enforceNeverEmpty, shortcodesAtRoot, noTrailingShortcodes, codeBlocksContainPlainText ];

export default rules;
28 changes: 12 additions & 16 deletions src/components/Widgets/Markdown/serializers/__tests__/slate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,33 @@ import { markdownToSlate, slateToMarkdown } from '../index';
const process = flow([markdownToSlate, slateToMarkdown]);

describe('slate', () => {
it('should distinguish between newlines and hard breaks', () => {
expect(process('a\n')).toEqual('a\n');
});

it('should not decode encoded html entities in inline code', () => {
expect(process('<code>&lt;div&gt;</code>')).toEqual('<code>&lt;div&gt;</code>\n');
expect(process('<code>&lt;div&gt;</code>')).toEqual('<code>&lt;div&gt;</code>');
});

it('should parse non-text children of mark nodes', () => {
expect(process('**a[b](c)d**')).toEqual('**a[b](c)d**\n');
expect(process('**[a](b)**')).toEqual('**[a](b)**\n');
expect(process('**![a](b)**')).toEqual('**![a](b)**\n');
expect(process('_`a`_')).toEqual('_`a`_\n');
expect(process('_`a`b_')).toEqual('_`a`b_\n');
expect(process('**a[b](c)d**')).toEqual('**a[b](c)d**');
expect(process('**[a](b)**')).toEqual('**[a](b)**');
expect(process('**![a](b)**')).toEqual('**![a](b)**');
expect(process('_`a`_')).toEqual('_`a`_');
expect(process('_`a`b_')).toEqual('_`a`b_');
});

it('should condense adjacent, identically styled text and inline nodes', () => {
expect(process('**a ~~b~~~~c~~**')).toEqual('**a ~~bc~~**\n');
expect(process('**a ~~b~~~~[c](d)~~**')).toEqual('**a ~~b[c](d)~~**\n');
expect(process('**a ~~b~~~~c~~**')).toEqual('**a ~~bc~~**');
expect(process('**a ~~b~~~~[c](d)~~**')).toEqual('**a ~~b[c](d)~~**');
});

it('should handle nested markdown entities', () => {
expect(process('**a**b**c**')).toEqual('**a**b**c**\n');
expect(process('**a _b_ c**')).toEqual('**a _b_ c**\n');
expect(process('**a**b**c**')).toEqual('**a**b**c**');
expect(process('**a _b_ c**')).toEqual('**a _b_ c**');
});

it('should parse inline images as images', () => {
expect(process('a ![b](c)')).toEqual('a ![b](c)\n');
expect(process('a ![b](c)')).toEqual('a ![b](c)');
});

it('should not escape markdown entities in html', () => {
expect(process('<span>*</span>')).toEqual('<span>*</span>\n');
expect(process('<span>*</span>')).toEqual('<span>*</span>');
});
});
19 changes: 12 additions & 7 deletions src/components/Widgets/Markdown/serializers/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { get, isEmpty, reduce, pull } from 'lodash';
import { get, isEmpty, reduce, pull, trimEnd } from 'lodash';
import unified from 'unified';
import u from 'unist-builder';
import markdownToRemarkPlugin from 'remark-parse';
Expand Down Expand Up @@ -118,27 +118,32 @@ export const remarkToMarkdown = obj => {
fences: true,
listItemIndent: '1',

// Settings to emulate the defaults from the Prosemirror editor, not
// necessarily optimal. Should eventually be configurable.
/**
* Settings to emulate the defaults from the Prosemirror editor, not
* necessarily optimal. Should eventually be configurable.
*/
bullet: '*',
strong: '*',
rule: '-',
};

/**
* Escape markdown entities found in text and html nodes within the MDAST.
* Transform the MDAST with plugins.
*/
const escapedMdast = unified()
const processedMdast = unified()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 This makes a lot more sense.

.use(remarkEscapeMarkdownEntities)
.use(remarkStripTrailingBreaks)
.runSync(mdast);

const markdown = unified()
.use(remarkToMarkdownPlugin, remarkToMarkdownPluginOpts)
.use(remarkAllowAllText)
.stringify(escapedMdast);
.stringify(processedMdast);

return markdown;
/**
* Return markdown with trailing whitespace removed.
*/
return trimEnd(markdown);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes at least two annoying bugs I've been having, thank you!

};


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { flow, partial, flatMap, flatten, map } from 'lodash';
import { has, flow, partial, flatMap, flatten, map } from 'lodash';
import { joinPatternSegments, combinePatterns, replaceWhen } from '../../../../lib/regexHelper';

/**
Expand Down Expand Up @@ -248,6 +248,12 @@ function escape(delim) {
*/
export default function remarkEscapeMarkdownEntities() {
const transform = (node, index) => {
/**
* Shortcode nodes will intentionally inject markdown entities in text node
* children not be escaped.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a typo here, or do I just not understand what it means?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, that's a typo.

*/
if (has(node.data, 'shortcode')) return node;

const children = node.children && node.children.map(transform);

/**
Expand Down
16 changes: 13 additions & 3 deletions src/components/Widgets/Markdown/serializers/remarkSlate.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ const markMap = {
};


/**
* Add nodes to a parent node only if `nodes` is truthy.
*/
function addNodes(parent, nodes) {
return nodes ? { ...parent, nodes } : parent;
}


/**
* Create a Slate Inline node.
*/
Expand All @@ -67,15 +75,17 @@ function createBlock(type, nodes, props = {}) {
nodes = undefined;
}

return { kind: 'block', type, nodes, ...props };
const node = { kind: 'block', type, ...props };
return addNodes(node, nodes);
}


/**
* Create a Slate Block node.
*/
function createInline(type, props = {}, nodes) {
return { kind: 'inline', type, nodes, ...props };
const node = { kind: 'inline', type, ...props };
return addNodes(node, nodes);
}


Expand Down Expand Up @@ -143,7 +153,7 @@ function processMarkNode(node, parentMarks = []) {
function convertMarkNode(node) {
const slateNodes = processMarkNode(node);

const convertedSlateNodes = slateNodes.reduce((acc, node, idx, nodes) => {
const convertedSlateNodes = slateNodes.reduce((acc, node) => {
const lastConvertedNode = last(acc);
if (node.text && lastConvertedNode && lastConvertedNode.ranges) {
lastConvertedNode.ranges.push(node);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { without } from 'lodash';
import { without, flatten } from 'lodash';
import u from 'unist-builder';
import mdastDefinitions from 'mdast-util-definitions';

Expand Down Expand Up @@ -46,8 +46,17 @@ export default function remarkSquashReferences() {
*/
if (['imageReference', 'linkReference'].includes(node.type)) {
const type = node.type === 'imageReference' ? 'image' : 'link';
const { title, url } = getDefinition(node.identifier) || {};
return u(type, { title, url, alt: node.alt }, children);
const definition = getDefinition(node.identifier);

if (definition) {
const { title, url } = definition;
return u(type, { title, url, alt: node.alt }, children);
}

const pre = u('text', node.type === 'imageReference' ? '![' : '[');
const post = u('text', ']');
const nodes = children || [ u('text', node.alt) ];
return [ pre, ...nodes, post];
}

/**
Expand All @@ -60,6 +69,6 @@ export default function remarkSquashReferences() {

const filteredChildren = without(children, null);

return { ...node, children: filteredChildren };
return { ...node, children: flatten(filteredChildren) };
}
}