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

Migrate rich text editor to Slate backed by Unified #254

Merged
merged 79 commits into from
Aug 25, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
f3b7dc9
Update Jest to 0.19
KyleAMathews Mar 1, 2017
24c0a1b
Replace markup-it with Remark for rendering markdown in the editor pr…
KyleAMathews Mar 1, 2017
0eb109c
Convert markdown-prosemirror parser/compiler to Remark
KyleAMathews Mar 2, 2017
8763666
Update parser to support remaining node types + add inline styled tex…
KyleAMathews Mar 18, 2017
f93aa34
fix rebase incongruencies
May 22, 2017
e401f7e
remove unrelated code style improvements
May 22, 2017
514fbb3
render plugins on visual editor load
erquhart May 23, 2017
adcb215
replace remark with unified for docs and extensibility
erquhart May 24, 2017
5048c7c
convert editor component registry to Map
erquhart May 24, 2017
8bb1845
implement initial unified/remark preview update
erquhart May 25, 2017
361c3d5
improve prosemirror parser, fix new doc creation
erquhart Jun 8, 2017
b5e0be4
split off markdownToProseMirror plugin
erquhart Jun 9, 2017
b223232
handle raw editor html pastes with unified
erquhart Jun 9, 2017
bd76730
fix visual editor tests, parse/serialize consistency
erquhart Jun 10, 2017
b293b23
fix link creation in visual editor
erquhart Jun 13, 2017
e7ac3a7
switch remark options to use gfm, fences
erquhart Jun 13, 2017
49b3a62
attempt prosemirror update, troubleshooting
erquhart Jun 15, 2017
9c869be
migrate visual editor from prosemirror to slate
erquhart Jun 19, 2017
e01c077
fix empty initial state for rte
erquhart Jun 20, 2017
e682189
only render editor page controls/previews on change
erquhart Jun 21, 2017
22a8da1
fix rte link serialization
erquhart Jun 21, 2017
bc72133
set rte focus after toolbar click
erquhart Jun 21, 2017
1c0bb6a
implement widget data serialization for rte perf
erquhart Jun 21, 2017
ffbd8d2
expose widgetValueSerializer registry
erquhart Jun 22, 2017
84ed450
add visual editor serializer source doc
erquhart Jun 22, 2017
09e631d
allow nested widget previews to update
erquhart Jun 23, 2017
e0ca24c
add unified config module
erquhart Jun 23, 2017
faec38a
fix raw editor paste parsing
erquhart Jun 23, 2017
54e77bd
fix raw editor formatting controls
erquhart Jun 23, 2017
5cbc76d
improve rte pasting
erquhart Jun 23, 2017
cba631b
improve visual/raw editor consistency
erquhart Jun 27, 2017
5a664f8
remove prosemirror, reuse unified pipelines
erquhart Jun 27, 2017
a8fe57e
pre-process visual editor pastes w/ unified
erquhart Jun 29, 2017
c49d84b
add empty node and Paper emoji unified plugins
erquhart Jul 5, 2017
804ef3d
use true source maps
erquhart Jul 7, 2017
b08a9fc
improve Dropbox Paper paste handling
erquhart Jul 7, 2017
719c105
remove logic from raw markdown editor
erquhart Jul 9, 2017
ae56ef6
convert raw editor to Slate
erquhart Jul 9, 2017
24caead
add list and code toolbar buttons
erquhart Jul 9, 2017
51cd8d3
remove prosemirror dependencies
erquhart Jul 9, 2017
fe3d04b
streamline raw editor pasting
erquhart Jul 10, 2017
f22d09b
add smart soft breaks for visual editor
erquhart Jul 11, 2017
09751ef
allow raw html in markdown
erquhart Jul 12, 2017
0e50210
close blocks on backspace
erquhart Jul 12, 2017
31c9978
fix inline code serializing to blocks
erquhart Jul 12, 2017
63e93d7
improve rte list handling
erquhart Jul 12, 2017
469a50a
add idempotent markdown/html shortcode handling
erquhart Jul 13, 2017
93687d9
add shortcodes through rte toolbar
erquhart Jul 14, 2017
842c293
use mdast instead of html for rte local model
erquhart Jul 18, 2017
c95f061
fix soft break side effects
erquhart Jul 24, 2017
b7379b0
re-implement shortcode parsing to/from mdast
erquhart Jul 24, 2017
dbf14a8
re-enable shortcode insertion via toolbar
erquhart Jul 26, 2017
fbecc88
require images to be parsed as shortcodes
erquhart Jul 26, 2017
6443f5d
allow enter key to make space around void nodes
erquhart Jul 26, 2017
4ac6395
fix focus update on toolbar block click
erquhart Jul 27, 2017
82d9bdd
port history shortcuts from Slate, force focus
erquhart Jul 27, 2017
ae7bd79
re-implement visual editor html paste
erquhart Jul 27, 2017
7a744be
improve list handling
erquhart Jul 27, 2017
de1e361
allow yaml frontmatter parsing
erquhart Jul 27, 2017
28ee67c
eliminate unnecessary editor renders
erquhart Jul 27, 2017
750fbf5
re-implement visual editor link button
erquhart Jul 27, 2017
336cab2
fix html whitespace truncation
erquhart Jul 27, 2017
1f961d3
display images inserted through rte
erquhart Jul 27, 2017
6377d8c
initial refactor, some bugfixes
erquhart Jul 27, 2017
be7385d
refactor remark-shortcodes plugin
erquhart Jul 28, 2017
9174e56
refactor remarkToRehypeShortcodes
erquhart Jul 30, 2017
ca60a6b
update Slate shortcode handling to include paragraph
erquhart Jul 30, 2017
1d65466
improve shortcode handling in visual editor
erquhart Jul 30, 2017
dd51f63
improve visual editor content styling
erquhart Jul 31, 2017
9dcda7b
organize serializers
erquhart Jul 31, 2017
406ae57
add blockquote rte button
erquhart Jul 31, 2017
cf2b7be
refactor and document rte serializers
erquhart Jul 31, 2017
d84b156
update existing serialization tests
erquhart Aug 1, 2017
18b98fc
remove superfluous deps, update yarn.lock
erquhart Aug 1, 2017
2bb6732
fix visual editor heading line height
erquhart Aug 1, 2017
3d83325
add node type check to avoid errors in rte
erquhart Aug 2, 2017
9c0b726
fix small code issues in RTE implementation
erquhart Aug 2, 2017
317a876
fix html paste for visual editor
erquhart Aug 3, 2017
0ea62e0
fix rte pasted links with leading/trailing spaces
erquhart Aug 4, 2017
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
Prev Previous commit
Next Next commit
fix html paste for visual editor
  • Loading branch information
erquhart committed Aug 25, 2017
commit 317a87689178278c971c3a9efd83fac6e57d800c
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"slug": "^0.9.1",
"unified": "^6.1.4",
"unist-builder": "^1.0.2",
"unist-util-visit-parents": "^1.1.1",
"uuid": "^2.0.3",
"whatwg-fetch": "^1.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import u from 'unist-builder';
import remarkAssertParents from '../remarkAssertParents';

const transform = remarkAssertParents();

describe('remarkAssertParents', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding more tests.

it('should unnest invalidly nested blocks', () => {
const input = u('root', [
u('paragraph', [
u('paragraph', [ u('text', 'Paragraph text.') ]),
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
u('code', 'someCode()'),
u('blockquote', [ u('text', 'Quote text.') ]),
u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]),
u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]),
u('thematicBreak'),
]),
]);

const output = u('root', [
u('paragraph', [ u('text', 'Paragraph text.') ]),
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
u('code', 'someCode()'),
u('blockquote', [ u('text', 'Quote text.') ]),
u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]),
u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]),
u('thematicBreak'),
]);

expect(transform(input)).toEqual(output);
});

it('should unnest deeply nested blocks', () => {
const input = u('root', [
u('paragraph', [
u('paragraph', [
u('paragraph', [
u('paragraph', [ u('text', 'Paragraph text.') ]),
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
u('code', 'someCode()'),
u('blockquote', [
u('paragraph', [
u('strong', [
u('heading', [
u('text', 'Quote text.'),
]),
]),
]),
]),
u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]),
u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]),
u('thematicBreak'),
]),
]),
]),
]);

const output = u('root', [
u('paragraph', [ u('text', 'Paragraph text.') ]),
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
u('code', 'someCode()'),
u('blockquote', [
u('heading', [
u('text', 'Quote text.'),
]),
]),
u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]),
u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]),
u('thematicBreak'),
]);

expect(transform(input)).toEqual(output);
});

it('should remove blocks that are emptied as a result of denesting', () => {
const input = u('root', [
u('paragraph', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]),
]);

const output = u('root', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]);

expect(transform(input)).toEqual(output);
});

it('should remove blocks that are emptied as a result of denesting', () => {
const input = u('root', [
u('paragraph', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]),
]);

const output = u('root', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]);

expect(transform(input)).toEqual(output);
});

it('should handle assymetrical splits', () => {
const input = u('root', [
u('paragraph', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]),
]);

const output = u('root', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]);

expect(transform(input)).toEqual(output);
});

it('should nest invalidly nested blocks in the nearest valid ancestor', () => {
const input = u('root', [
u('paragraph', [
u('blockquote', [
u('strong', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]),
]),
]),
]);

const output = u('root', [
u('blockquote', [
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
]),
]);

expect(transform(input)).toEqual(output);
});

it('should preserve validly nested siblings of invalidly nested blocks', () => {
const input = u('root', [
u('paragraph', [
u('blockquote', [
u('strong', [
u('text', 'Deep validly nested text a.'),
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
u('text', 'Deep validly nested text b.'),
]),
]),
u('text', 'Validly nested text.'),
]),
]);

const output = u('root', [
u('blockquote', [
u('strong', [
u('text', 'Deep validly nested text a.'),
]),
u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]),
u('strong', [
u('text', 'Deep validly nested text b.'),
]),
]),
u('paragraph', [
u('text', 'Validly nested text.'),
]),
]);

expect(transform(input)).toEqual(output);
});

it('should allow intermediate parents like list and table to contain required block children', () => {
const input = u('root', [
u('blockquote', [
u('list', [
u('listItem', [
u('table', [
u('tableRow', [
u('tableCell', [
u('heading', { depth: 1 }, [ u('text', 'Validly nested heading text.') ]),
]),
]),
]),
]),
]),
]),
]);

const output = u('root', [
u('blockquote', [
u('list', [
u('listItem', [
u('table', [
u('tableRow', [
u('tableCell', [
u('heading', { depth: 1 }, [ u('text', 'Validly nested heading text.') ]),
]),
]),
]),
]),
]),
]),
]);

expect(transform(input)).toEqual(output);
});
});
4 changes: 3 additions & 1 deletion src/components/Widgets/Markdown/serializers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import htmlToRehype from 'rehype-parse';
import rehypeToRemark from 'rehype-remark';
import remarkToRehypeShortcodes from './remarkRehypeShortcodes';
import rehypePaperEmoji from './rehypePaperEmoji';
import remarkAssertParents from './remarkAssertParents';
import remarkWrapHtml from './remarkWrapHtml';
import remarkToSlatePlugin from './remarkSlate';
import remarkSquashReferences from './remarkSquashReferences';
Expand Down Expand Up @@ -199,10 +200,11 @@ export const htmlToSlate = html => {

const mdast = unified()
.use(rehypePaperEmoji)
.use(rehypeToRemark)
.use(rehypeToRemark, { minify: false })
.runSync(hast);

const slateRaw = unified()
.use(remarkAssertParents)
.use(remarkImagesToText)
.use(remarkShortcodes, { plugins: registry.getEditorComponents() })
.use(remarkWrapHtml)
Expand Down
83 changes: 83 additions & 0 deletions src/components/Widgets/Markdown/serializers/remarkAssertParents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { concat, last, nth, isEmpty, set } from 'lodash';
import visitParents from 'unist-util-visit-parents';

/**
* remarkUnwrapInvalidNest
*
* Some MDAST node types can only be nested within specific node types - for
* example, a paragraph can't be nested within another paragraph, and a heading
* can't be nested in a "strong" type node. This kind of invalid MDAST can be
* generated by rehype-remark from invalid HTML.
*
* This plugin finds instances of invalid nesting, and unwraps the invalidly
* nested nodes as far up the parental line as necessary, splitting parent nodes
* along the way. The resulting node has no invalidly nested nodes, and all
* validly nested nodes retain their ancestry. Nodes that are emptied as a
* result of unnesting nodes are removed from the tree.
*/
export default function remarkUnwrapInvalidNest() {
return transform;

function transform(tree) {
const invalidNest = findInvalidNest(tree);

if (!invalidNest) return tree;

splitTreeAtNest(tree, invalidNest);

return transform(tree);
}

/**
* visitParents uses unist-util-visit-parent to check every node in the
* tree while having access to every ancestor of the node. This is ideal
* for determining whether a block node has an ancestor that should not
* contain a block node. Note that it operates in a mutable fashion.
*/
function findInvalidNest(tree) {
/**
* Node types that are considered "blocks".
*/
const blocks = ['paragraph', 'heading', 'code', 'blockquote', 'list', 'table', 'thematicBreak'];

/**
* Node types that can contain "block" nodes as direct children. We check
*/
const canContainBlocks = ['root', 'blockquote', 'listItem', 'tableCell'];

let invalidNest;

visitParents(tree, (node, parents) => {
const parentType = !isEmpty(parents) && last(parents).type;
const isInvalidNest = blocks.includes(node.type) && !canContainBlocks.includes(parentType);

if (isInvalidNest) {
invalidNest = concat(parents, node);
return false;
}
});

return invalidNest;
}

function splitTreeAtNest(tree, nest) {
const grandparent = nth(nest, -3) || tree;
const parent = nth(nest, -2);
const node = last(nest);

const splitIndex = grandparent.children.indexOf(parent);
const splitChildren = grandparent.children;
const splitChildIndex = parent.children.indexOf(node);

const childrenBefore = parent.children.slice(0, splitChildIndex);
const childrenAfter = parent.children.slice(splitChildIndex + 1);
const nodeBefore = !isEmpty(childrenBefore) && { ...parent, children: childrenBefore };
const nodeAfter = !isEmpty(childrenAfter) && { ...parent, children: childrenAfter };

const childrenToInsert = [nodeBefore, node, nodeAfter].filter(val => !isEmpty(val));
const beforeChildren = splitChildren.slice(0, splitIndex);
const afterChildren = splitChildren.slice(splitIndex + 1);
const newChildren = concat(beforeChildren, childrenToInsert, afterChildren);
grandparent.children = newChildren;
}
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9036,6 +9036,10 @@ unist-util-stringify-position@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.1.tgz#3ccbdc53679eed6ecf3777dd7f5e3229c1b6aa3c"

unist-util-visit-parents@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-1.1.1.tgz#7d3f56b5b039a3c6e2d16e51cc093f10e4755342"

unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.3.tgz#ec268e731b9d277a79a5b5aa0643990e405d600b"
Expand Down