forked from decaporg/decap-cms
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
295 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
204 changes: 204 additions & 0 deletions
204
src/components/Widgets/Markdown/serializers/__tests__/remarkAssertParents.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', () => { | ||
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
src/components/Widgets/Markdown/serializers/remarkAssertParents.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters