refactor: convert function expressions to declarations (#4926)

This commit is contained in:
Vladislav Shkodin
2021-02-08 20:01:21 +02:00
committed by GitHub
parent c0236536dd
commit 141a2eba56
241 changed files with 3444 additions and 2933 deletions

View File

@ -13,7 +13,8 @@ import { markdownToHtml } from '../serializers';
import { editorStyleVars, EditorControlBar } from '../styles';
import Toolbar from './Toolbar';
const rawEditorStyles = ({ minimal }) => `
function rawEditorStyles({ minimal }) {
return `
position: relative;
overflow: hidden;
overflow-x: auto;
@ -24,6 +25,7 @@ const rawEditorStyles = ({ minimal }) => `
border-top: 0;
margin-top: -${editorStyleVars.stickyDistanceBottom};
`;
}
const RawEditorContainer = styled.div`
position: relative;

View File

@ -113,7 +113,11 @@ export default class Toolbar extends React.Component {
} = this.props;
const isVisible = this.isVisible;
const showEditorComponents = !editorComponents || editorComponents.size >= 1;
const showPlugin = ({ id }) => (editorComponents ? editorComponents.includes(id) : true);
function showPlugin({ id }) {
return editorComponents ? editorComponents.includes(id) : true;
}
const pluginsList = plugins ? plugins.toList().filter(showPlugin) : List();
const headingOptions = {

View File

@ -23,16 +23,18 @@ const StyledToolbarButton = styled.button`
}
`;
const ToolbarButton = ({ type, label, icon, onClick, isActive, disabled }) => (
<StyledToolbarButton
isActive={isActive}
onClick={e => onClick && onClick(e, type)}
title={label}
disabled={disabled}
>
{icon ? <Icon type={icon} /> : label}
</StyledToolbarButton>
);
function ToolbarButton({ type, label, icon, onClick, isActive, disabled }) {
return (
<StyledToolbarButton
isActive={isActive}
onClick={e => onClick && onClick(e, type)}
title={label}
disabled={disabled}
>
{icon ? <Icon type={icon} /> : label}
</StyledToolbarButton>
);
}
ToolbarButton.propTypes = {
type: PropTypes.string,

View File

@ -15,7 +15,8 @@ import { renderBlock, renderInline, renderMark } from './renderers';
import plugins from './plugins/visual';
import schema from './schema';
const visualEditorStyles = ({ minimal }) => `
function visualEditorStyles({ minimal }) {
return `
position: relative;
overflow: hidden;
overflow-x: auto;
@ -30,26 +31,27 @@ const visualEditorStyles = ({ minimal }) => `
flex-direction: column;
z-index: ${zIndex.zIndex100};
`;
}
const InsertionPoint = styled.div`
flex: 1 1 auto;
cursor: text;
`;
const createEmptyRawDoc = () => {
function createEmptyRawDoc() {
const emptyText = Text.create('');
const emptyBlock = Block.create({ object: 'block', type: 'paragraph', nodes: [emptyText] });
return { nodes: [emptyBlock] };
};
}
const createSlateValue = (rawValue, { voidCodeBlock }) => {
function createSlateValue(rawValue, { voidCodeBlock }) {
const rawDoc = rawValue && markdownToSlate(rawValue, { voidCodeBlock });
const rawDocHasNodes = !isEmpty(get(rawDoc, 'nodes'));
const document = Document.fromJSON(rawDocHasNodes ? rawDoc : createEmptyRawDoc());
return Value.create({ document });
};
}
export const mergeMediaConfig = (editorComponents, field) => {
export function mergeMediaConfig(editorComponents, field) {
// merge editor media library config to image components
if (editorComponents.has('image')) {
const imageComponent = editorComponents.get('image');
@ -79,7 +81,7 @@ export const mergeMediaConfig = (editorComponents, field) => {
);
}
}
};
}
export default class Editor extends React.Component {
constructor(props) {

View File

@ -38,12 +38,14 @@ describe.skip('slate', () => {
</document>
</value>
);
const fn = editor => {
function fn(editor) {
editor
.deleteBackward()
.insertText('b')
.setBlocks('heading-one');
};
}
const [actual, expected] = run(input, output, fn);
expect(actual).toEqual(expected);
});

View File

@ -24,12 +24,14 @@ export default class Shortcode extends React.Component {
const EditorControl = getEditorControl();
const value = dataKey === false ? node.data : fromJS(node.data.get(dataKey));
const handleChange = (fieldName, value, metadata) => {
function handleChange(fieldName, value, metadata) {
const dataValue = dataKey === false ? value : node.data.set('shortcodeData', value);
editor.setNodeByKey(node.key, { data: dataValue || Map(), metadata });
};
}
const handleFocus = () => editor.moveToRangeOfNode(node);
function handleFocus() {
return editor.moveToRangeOfNode(node);
}
return (
!field.isEmpty() && (

View File

@ -3,23 +3,25 @@ import React from 'react';
import { css } from '@emotion/core';
import { zIndex } from 'netlify-cms-ui-default';
const InsertionPoint = props => (
<div
css={css`
height: 32px;
cursor: text;
position: relative;
z-index: ${zIndex.zIndex1};
margin-top: -16px;
`}
{...props}
/>
);
function InsertionPoint(props) {
return (
<div
css={css`
height: 32px;
cursor: text;
position: relative;
z-index: ${zIndex.zIndex1};
margin-top: -16px;
`}
{...props}
/>
);
}
const VoidBlock = ({ editor, attributes, node, children }) => {
const handleClick = event => {
function VoidBlock({ editor, attributes, node, children }) {
function handleClick(event) {
event.stopPropagation();
};
}
return (
<div {...attributes} onClick={handleClick}>
@ -32,6 +34,6 @@ const VoidBlock = ({ editor, attributes, node, children }) => {
)}
</div>
);
};
}
export default VoidBlock;

View File

@ -10,10 +10,16 @@ const MODE_STORAGE_KEY = 'cms.md-mode';
// TODO: passing the editorControl and components like this is horrible, should
// be handled through Redux and a separate registry store for instances
let editorControl;
// eslint-disable-next-line func-style
let _getEditorComponents = () => [];
export const getEditorControl = () => editorControl;
export const getEditorComponents = () => _getEditorComponents();
export function getEditorControl() {
return editorControl;
}
export function getEditorComponents() {
return _getEditorComponents();
}
export default class MarkdownControl extends React.Component {
static propTypes = {

View File

@ -1,21 +1,23 @@
import isHotkey from 'is-hotkey';
const BreakToDefaultBlock = ({ defaultType }) => ({
onKeyDown(event, editor, next) {
const { selection, startBlock } = editor.value;
const isEnter = isHotkey('enter', event);
if (!isEnter) {
function BreakToDefaultBlock({ defaultType }) {
return {
onKeyDown(event, editor, next) {
const { selection, startBlock } = editor.value;
const isEnter = isHotkey('enter', event);
if (!isEnter) {
return next();
}
if (selection.isExpanded) {
editor.delete();
return next();
}
if (selection.start.isAtEndOfNode(startBlock) && startBlock.type !== defaultType) {
return editor.insertBlock(defaultType);
}
return next();
}
if (selection.isExpanded) {
editor.delete();
return next();
}
if (selection.start.isAtEndOfNode(startBlock) && startBlock.type !== defaultType) {
return editor.insertBlock(defaultType);
}
return next();
},
});
},
};
}
export default BreakToDefaultBlock;

View File

@ -1,23 +1,25 @@
import isHotkey from 'is-hotkey';
const CloseBlock = ({ defaultType }) => ({
onKeyDown(event, editor, next) {
const { selection, startBlock } = editor.value;
const isBackspace = isHotkey('backspace', event);
if (!isBackspace) {
function CloseBlock({ defaultType }) {
return {
onKeyDown(event, editor, next) {
const { selection, startBlock } = editor.value;
const isBackspace = isHotkey('backspace', event);
if (!isBackspace) {
return next();
}
if (selection.isExpanded) {
return editor.delete();
}
if (!selection.start.isAtStartOfNode(startBlock) || startBlock.text.length > 0) {
return next();
}
if (startBlock.type !== defaultType) {
return editor.setBlocks(defaultType);
}
return next();
}
if (selection.isExpanded) {
return editor.delete();
}
if (!selection.start.isAtStartOfNode(startBlock) || startBlock.text.length > 0) {
return next();
}
if (startBlock.type !== defaultType) {
return editor.setBlocks(defaultType);
}
return next();
},
});
},
};
}
export default CloseBlock;

View File

@ -1,121 +1,123 @@
import { isArray, tail, castArray } from 'lodash';
const CommandsAndQueries = ({ defaultType }) => ({
queries: {
atStartOf(editor, node) {
const { selection } = editor.value;
return selection.isCollapsed && selection.start.isAtStartOfNode(node);
},
getAncestor(editor, firstKey, lastKey) {
if (firstKey === lastKey) {
return editor.value.document.getParent(firstKey);
}
return editor.value.document.getCommonAncestor(firstKey, lastKey);
},
getOffset(editor, node) {
const parent = editor.value.document.getParent(node.key);
return parent.nodes.indexOf(node);
},
getSelectedChildren(editor, node) {
return node.nodes.filter(child => editor.isSelected(child));
},
getCommonAncestor(editor) {
const { startBlock, endBlock, document: doc } = editor.value;
return doc.getCommonAncestor(startBlock.key, endBlock.key);
},
getClosestType(editor, node, type) {
const types = castArray(type);
return editor.value.document.getClosest(node.key, n => types.includes(n.type));
},
getBlockContainer(editor, node) {
const targetTypes = ['bulleted-list', 'numbered-list', 'list-item', 'quote', 'table-cell'];
const { startBlock, selection } = editor.value;
const target = node
? editor.value.document.getParent(node.key)
: (selection.isCollapsed && startBlock) || editor.getCommonAncestor();
if (!target) {
return editor.value.document;
}
if (targetTypes.includes(target.type)) {
return target;
}
return editor.getBlockContainer(target);
},
isSelected(editor, nodes) {
return castArray(nodes).every(node => {
return editor.value.document.isInRange(node.key, editor.value.selection);
});
},
isFirstChild(editor, node) {
return editor.value.document.getParent(node.key).nodes.first().key === node.key;
},
areSiblings(editor, nodes) {
if (!isArray(nodes) || nodes.length < 2) {
return true;
}
const parent = editor.value.document.getParent(nodes[0].key);
return tail(nodes).every(node => {
return editor.value.document.getParent(node.key).key === parent.key;
});
},
everyBlock(editor, type) {
return editor.value.blocks.every(block => block.type === type);
},
hasMark(editor, type) {
return editor.value.activeMarks.some(mark => mark.type === type);
},
hasBlock(editor, type) {
return editor.value.blocks.some(node => node.type === type);
},
hasInline(editor, type) {
return editor.value.inlines.some(node => node.type === type);
},
},
commands: {
toggleBlock(editor, type) {
switch (type) {
case 'heading-one':
case 'heading-two':
case 'heading-three':
case 'heading-four':
case 'heading-five':
case 'heading-six':
return editor.setBlocks(editor.everyBlock(type) ? defaultType : type);
case 'quote':
return editor.toggleQuoteBlock();
case 'numbered-list':
case 'bulleted-list': {
return editor.toggleList(type);
function CommandsAndQueries({ defaultType }) {
return {
queries: {
atStartOf(editor, node) {
const { selection } = editor.value;
return selection.isCollapsed && selection.start.isAtStartOfNode(node);
},
getAncestor(editor, firstKey, lastKey) {
if (firstKey === lastKey) {
return editor.value.document.getParent(firstKey);
}
}
},
unwrapBlockChildren(editor, block) {
if (!block || block.object !== 'block') {
throw Error(`Expected block but received ${block}.`);
}
const index = editor.value.document.getPath(block.key).last();
const parent = editor.value.document.getParent(block.key);
editor.withoutNormalizing(() => {
block.nodes.forEach((node, idx) => {
editor.moveNodeByKey(node.key, parent.key, index + idx);
return editor.value.document.getCommonAncestor(firstKey, lastKey);
},
getOffset(editor, node) {
const parent = editor.value.document.getParent(node.key);
return parent.nodes.indexOf(node);
},
getSelectedChildren(editor, node) {
return node.nodes.filter(child => editor.isSelected(child));
},
getCommonAncestor(editor) {
const { startBlock, endBlock, document: doc } = editor.value;
return doc.getCommonAncestor(startBlock.key, endBlock.key);
},
getClosestType(editor, node, type) {
const types = castArray(type);
return editor.value.document.getClosest(node.key, n => types.includes(n.type));
},
getBlockContainer(editor, node) {
const targetTypes = ['bulleted-list', 'numbered-list', 'list-item', 'quote', 'table-cell'];
const { startBlock, selection } = editor.value;
const target = node
? editor.value.document.getParent(node.key)
: (selection.isCollapsed && startBlock) || editor.getCommonAncestor();
if (!target) {
return editor.value.document;
}
if (targetTypes.includes(target.type)) {
return target;
}
return editor.getBlockContainer(target);
},
isSelected(editor, nodes) {
return castArray(nodes).every(node => {
return editor.value.document.isInRange(node.key, editor.value.selection);
});
editor.removeNodeByKey(block.key);
});
},
unwrapNodeToDepth(editor, node, depth) {
let currentDepth = 0;
editor.withoutNormalizing(() => {
while (currentDepth < depth) {
editor.unwrapNodeByKey(node.key);
currentDepth += 1;
},
isFirstChild(editor, node) {
return editor.value.document.getParent(node.key).nodes.first().key === node.key;
},
areSiblings(editor, nodes) {
if (!isArray(nodes) || nodes.length < 2) {
return true;
}
});
const parent = editor.value.document.getParent(nodes[0].key);
return tail(nodes).every(node => {
return editor.value.document.getParent(node.key).key === parent.key;
});
},
everyBlock(editor, type) {
return editor.value.blocks.every(block => block.type === type);
},
hasMark(editor, type) {
return editor.value.activeMarks.some(mark => mark.type === type);
},
hasBlock(editor, type) {
return editor.value.blocks.some(node => node.type === type);
},
hasInline(editor, type) {
return editor.value.inlines.some(node => node.type === type);
},
},
unwrapNodeFromAncestor(editor, node, ancestor) {
const depth = ancestor.getDepth(node.key);
editor.unwrapNodeToDepth(node, depth);
commands: {
toggleBlock(editor, type) {
switch (type) {
case 'heading-one':
case 'heading-two':
case 'heading-three':
case 'heading-four':
case 'heading-five':
case 'heading-six':
return editor.setBlocks(editor.everyBlock(type) ? defaultType : type);
case 'quote':
return editor.toggleQuoteBlock();
case 'numbered-list':
case 'bulleted-list': {
return editor.toggleList(type);
}
}
},
unwrapBlockChildren(editor, block) {
if (!block || block.object !== 'block') {
throw Error(`Expected block but received ${block}.`);
}
const index = editor.value.document.getPath(block.key).last();
const parent = editor.value.document.getParent(block.key);
editor.withoutNormalizing(() => {
block.nodes.forEach((node, idx) => {
editor.moveNodeByKey(node.key, parent.key, index + idx);
});
editor.removeNodeByKey(block.key);
});
},
unwrapNodeToDepth(editor, node, depth) {
let currentDepth = 0;
editor.withoutNormalizing(() => {
while (currentDepth < depth) {
editor.unwrapNodeByKey(node.key);
currentDepth += 1;
}
});
},
unwrapNodeFromAncestor(editor, node, ancestor) {
const depth = ancestor.getDepth(node.key);
editor.unwrapNodeToDepth(node, depth);
},
},
},
});
};
}
export default CommandsAndQueries;

View File

@ -4,15 +4,15 @@ import base64 from 'slate-base64-serializer';
import isHotkey from 'is-hotkey';
import { slateToMarkdown, markdownToSlate, htmlToSlate, markdownToHtml } from '../../serializers';
const CopyPasteVisual = ({ getAsset, resolveWidget }) => {
const handleCopy = (event, editor) => {
function CopyPasteVisual({ getAsset, resolveWidget }) {
function handleCopy(event, editor) {
const markdown = slateToMarkdown(editor.value.fragment.toJS());
const html = markdownToHtml(markdown, { getAsset, resolveWidget });
setEventTransfer(event, 'text', markdown);
setEventTransfer(event, 'html', html);
setEventTransfer(event, 'fragment', base64.serializeNode(editor.value.fragment));
event.preventDefault();
};
}
return {
onPaste(event, editor, next) {
@ -39,6 +39,6 @@ const CopyPasteVisual = ({ getAsset, resolveWidget }) => {
editor.delete();
},
};
};
}
export default CopyPasteVisual;

View File

@ -1,42 +1,44 @@
const ForceInsert = ({ defaultType }) => ({
queries: {
canInsertBeforeNode(editor, node) {
if (!editor.isVoid(node)) {
return true;
}
return !!editor.value.document.getPreviousSibling(node.key);
function ForceInsert({ defaultType }) {
return {
queries: {
canInsertBeforeNode(editor, node) {
if (!editor.isVoid(node)) {
return true;
}
return !!editor.value.document.getPreviousSibling(node.key);
},
canInsertAfterNode(editor, node) {
if (!editor.isVoid(node)) {
return true;
}
const nextSibling = editor.value.document.getNextSibling(node.key);
return nextSibling && !editor.isVoid(nextSibling);
},
},
canInsertAfterNode(editor, node) {
if (!editor.isVoid(node)) {
return true;
}
const nextSibling = editor.value.document.getNextSibling(node.key);
return nextSibling && !editor.isVoid(nextSibling);
commands: {
forceInsertBeforeNode(editor, node) {
const block = { type: defaultType, object: 'block' };
const parent = editor.value.document.getParent(node.key);
return editor
.insertNodeByKey(parent.key, 0, block)
.moveToStartOfNode(parent)
.focus();
},
forceInsertAfterNode(editor, node) {
return editor
.moveToEndOfNode(node)
.insertBlock(defaultType)
.focus();
},
moveToEndOfDocument(editor) {
const lastBlock = editor.value.document.nodes.last();
if (editor.isVoid(lastBlock)) {
editor.insertBlock(defaultType);
}
return editor.moveToEndOfNode(lastBlock).focus();
},
},
},
commands: {
forceInsertBeforeNode(editor, node) {
const block = { type: defaultType, object: 'block' };
const parent = editor.value.document.getParent(node.key);
return editor
.insertNodeByKey(parent.key, 0, block)
.moveToStartOfNode(parent)
.focus();
},
forceInsertAfterNode(editor, node) {
return editor
.moveToEndOfNode(node)
.insertBlock(defaultType)
.focus();
},
moveToEndOfDocument(editor) {
const lastBlock = editor.value.document.nodes.last();
if (editor.isVoid(lastBlock)) {
editor.insertBlock(defaultType);
}
return editor.moveToEndOfNode(lastBlock).focus();
},
},
});
};
}
export default ForceInsert;

View File

@ -14,7 +14,7 @@ export const HOT_KEY_MAP = {
link: 'mod+k',
};
const Hotkey = (key, fn) => {
function Hotkey(key, fn) {
return {
onKeyDown(event, editor, next) {
if (!isHotkey(key, event)) {
@ -24,6 +24,6 @@ const Hotkey = (key, fn) => {
editor.command(fn);
},
};
};
}
export default Hotkey;

View File

@ -1,16 +1,18 @@
import isHotkey from 'is-hotkey';
const LineBreak = () => ({
onKeyDown(event, editor, next) {
const isShiftEnter = isHotkey('shift+enter', event);
if (!isShiftEnter) {
return next();
}
return editor
.insertInline('break')
.insertText('')
.moveToStartOfNextText();
},
});
function LineBreak() {
return {
onKeyDown(event, editor, next) {
const isShiftEnter = isHotkey('shift+enter', event);
if (!isShiftEnter) {
return next();
}
return editor
.insertInline('break')
.insertText('')
.moveToStartOfNextText();
},
};
}
export default LineBreak;

View File

@ -1,27 +1,29 @@
const Link = ({ type }) => ({
commands: {
toggleLink(editor, getUrl) {
if (editor.hasInline(type)) {
editor.unwrapInline(type);
} else {
const url = getUrl();
if (!url) return;
const selection = editor.value.selection;
const isCollapsed = selection && selection.isCollapsed;
if (isCollapsed) {
// If no text is selected, use the entered URL as text.
return editor.insertInline({
type,
data: { url },
nodes: [{ object: 'text', text: url }],
});
function Link({ type }) {
return {
commands: {
toggleLink(editor, getUrl) {
if (editor.hasInline(type)) {
editor.unwrapInline(type);
} else {
return editor.wrapInline({ type, data: { url } }).moveToEnd();
const url = getUrl();
if (!url) return;
const selection = editor.value.selection;
const isCollapsed = selection && selection.isCollapsed;
if (isCollapsed) {
// If no text is selected, use the entered URL as text.
return editor.insertInline({
type,
data: { url },
nodes: [{ object: 'text', text: url }],
});
} else {
return editor.wrapInline({ type, data: { url } }).moveToEnd();
}
}
}
},
},
},
});
};
}
export default Link;

View File

@ -4,7 +4,7 @@ import { Range, Block } from 'slate';
import isHotkey from 'is-hotkey';
import { assertType } from './util';
const ListPlugin = ({ defaultType, unorderedListType, orderedListType }) => {
function ListPlugin({ defaultType, unorderedListType, orderedListType }) {
const LIST_TYPES = [orderedListType, unorderedListType];
function oppositeListType(type) {
@ -297,6 +297,6 @@ const ListPlugin = ({ defaultType, unorderedListType, orderedListType }) => {
return next();
},
};
};
}
export default ListPlugin;

View File

@ -4,98 +4,100 @@ import isHotkey from 'is-hotkey';
/**
* TODO: highlight a couple list items and hit the quote button. doesn't work.
*/
const QuoteBlock = ({ type }) => ({
commands: {
/**
* Quotes can contain other blocks, even other quotes. If a selection contains quotes, they
* shouldn't be impacted. The selection's immediate parent should be checked - if it's a
* quote, unwrap the quote (as within are only blocks), and if it's not, wrap all selected
* blocks into a quote. Make sure text is wrapped into paragraphs.
*/
toggleQuoteBlock(editor) {
const blockContainer = editor.getBlockContainer();
if (['bulleted-list', 'numbered-list'].includes(blockContainer.type)) {
const { nodes } = blockContainer;
const allItemsSelected = editor.isSelected([nodes.first(), nodes.last()]);
if (allItemsSelected) {
const nextContainer = editor.getBlockContainer(blockContainer);
if (nextContainer?.type === type) {
editor.unwrapNodeFromAncestor(blockContainer, nextContainer);
function QuoteBlock({ type }) {
return {
commands: {
/**
* Quotes can contain other blocks, even other quotes. If a selection contains quotes, they
* shouldn't be impacted. The selection's immediate parent should be checked - if it's a
* quote, unwrap the quote (as within are only blocks), and if it's not, wrap all selected
* blocks into a quote. Make sure text is wrapped into paragraphs.
*/
toggleQuoteBlock(editor) {
const blockContainer = editor.getBlockContainer();
if (['bulleted-list', 'numbered-list'].includes(blockContainer.type)) {
const { nodes } = blockContainer;
const allItemsSelected = editor.isSelected([nodes.first(), nodes.last()]);
if (allItemsSelected) {
const nextContainer = editor.getBlockContainer(blockContainer);
if (nextContainer?.type === type) {
editor.unwrapNodeFromAncestor(blockContainer, nextContainer);
} else {
editor.wrapBlockByKey(blockContainer.key, type);
}
} else {
editor.wrapBlockByKey(blockContainer.key, type);
const blockContainerParent = editor.value.document.getParent(blockContainer.key);
editor.withoutNormalizing(() => {
const selectedListItems = nodes.filter(node => editor.isSelected(node));
const newList = Block.create(blockContainer.type);
editor.unwrapNodeByKey(selectedListItems.first());
const offset = editor.getOffset(selectedListItems.first());
editor.insertNodeByKey(blockContainerParent.key, offset + 1, newList);
selectedListItems.forEach(({ key }, idx) =>
editor.moveNodeByKey(key, newList.key, idx),
);
editor.wrapBlockByKey(newList.key, type);
});
}
} else {
const blockContainerParent = editor.value.document.getParent(blockContainer.key);
editor.withoutNormalizing(() => {
const selectedListItems = nodes.filter(node => editor.isSelected(node));
const newList = Block.create(blockContainer.type);
editor.unwrapNodeByKey(selectedListItems.first());
const offset = editor.getOffset(selectedListItems.first());
editor.insertNodeByKey(blockContainerParent.key, offset + 1, newList);
selectedListItems.forEach(({ key }, idx) =>
editor.moveNodeByKey(key, newList.key, idx),
);
editor.wrapBlockByKey(newList.key, type);
});
return;
}
return;
}
const blocks = editor.value.blocks;
const firstBlockKey = blocks.first().key;
const lastBlockKey = blocks.last().key;
const ancestor = editor.getAncestor(firstBlockKey, lastBlockKey);
if (ancestor.type === type) {
editor.unwrapBlockChildren(ancestor);
} else {
editor.wrapBlock(type);
}
const blocks = editor.value.blocks;
const firstBlockKey = blocks.first().key;
const lastBlockKey = blocks.last().key;
const ancestor = editor.getAncestor(firstBlockKey, lastBlockKey);
if (ancestor.type === type) {
editor.unwrapBlockChildren(ancestor);
} else {
editor.wrapBlock(type);
}
},
},
},
onKeyDown(event, editor, next) {
if (!isHotkey('enter', event) && !isHotkey('backspace', event)) {
return next();
}
const { selection, startBlock, document: doc } = editor.value;
const parent = doc.getParent(startBlock.key);
const isQuote = parent.type === type;
if (!isQuote) {
return next();
}
if (isHotkey('enter', event)) {
if (selection.isExpanded) {
editor.delete();
}
// If the quote is empty, remove it.
if (editor.atStartOf(parent)) {
return editor.unwrapBlockByKey(parent.key);
}
if (editor.atStartOf(startBlock)) {
const offset = editor.getOffset(startBlock);
return editor
.splitNodeByKey(parent.key, offset)
.unwrapBlockByKey(editor.value.document.getParent(startBlock.key).key);
}
return next();
} else if (isHotkey('backspace', event)) {
if (selection.isExpanded) {
editor.delete();
}
if (!editor.atStartOf(parent)) {
onKeyDown(event, editor, next) {
if (!isHotkey('enter', event) && !isHotkey('backspace', event)) {
return next();
}
const previousParentSibling = doc.getPreviousSibling(parent.key);
if (previousParentSibling && previousParentSibling.type === type) {
return editor.mergeNodeByKey(parent.key);
const { selection, startBlock, document: doc } = editor.value;
const parent = doc.getParent(startBlock.key);
const isQuote = parent.type === type;
if (!isQuote) {
return next();
}
if (isHotkey('enter', event)) {
if (selection.isExpanded) {
editor.delete();
}
return editor.unwrapNodeByKey(startBlock.key);
}
return next();
},
});
// If the quote is empty, remove it.
if (editor.atStartOf(parent)) {
return editor.unwrapBlockByKey(parent.key);
}
if (editor.atStartOf(startBlock)) {
const offset = editor.getOffset(startBlock);
return editor
.splitNodeByKey(parent.key, offset)
.unwrapBlockByKey(editor.value.document.getParent(startBlock.key).key);
}
return next();
} else if (isHotkey('backspace', event)) {
if (selection.isExpanded) {
editor.delete();
}
if (!editor.atStartOf(parent)) {
return next();
}
const previousParentSibling = doc.getPreviousSibling(parent.key);
if (previousParentSibling && previousParentSibling.type === type) {
return editor.mergeNodeByKey(parent.key);
}
return editor.unwrapNodeByKey(startBlock.key);
}
return next();
},
};
}
export default QuoteBlock;

View File

@ -1,14 +1,16 @@
import isHotkey from 'is-hotkey';
const SelectAll = () => ({
onKeyDown(event, editor, next) {
const isModA = isHotkey('mod+a', event);
if (!isModA) {
return next();
}
event.preventDefault();
return editor.moveToRangeOfDocument();
},
});
function SelectAll() {
return {
onKeyDown(event, editor, next) {
const isModA = isHotkey('mod+a', event);
if (!isModA) {
return next();
}
event.preventDefault();
return editor.moveToRangeOfDocument();
},
};
}
export default SelectAll;

View File

@ -1,6 +1,6 @@
import { Text, Block } from 'slate';
const createShortcodeBlock = shortcodeConfig => {
function createShortcodeBlock(shortcodeConfig) {
// Handle code block component
if (shortcodeConfig.type === 'code-block') {
return Block.create({ type: shortcodeConfig.type, data: { shortcodeNew: true } });
@ -25,23 +25,25 @@ const createShortcodeBlock = shortcodeConfig => {
},
nodes,
});
};
}
const Shortcode = ({ defaultType }) => ({
commands: {
insertShortcode(editor, shortcodeConfig) {
const block = createShortcodeBlock(shortcodeConfig);
const { focusBlock } = editor.value;
function Shortcode({ defaultType }) {
return {
commands: {
insertShortcode(editor, shortcodeConfig) {
const block = createShortcodeBlock(shortcodeConfig);
const { focusBlock } = editor.value;
if (focusBlock.text === '' && focusBlock.type === defaultType) {
editor.replaceNodeByKey(focusBlock.key, block);
} else {
editor.insertBlock(block);
}
if (focusBlock.text === '' && focusBlock.type === defaultType) {
editor.replaceNodeByKey(focusBlock.key, block);
} else {
editor.insertBlock(block);
}
editor.focus();
editor.focus();
},
},
},
});
};
}
export default Shortcode;

View File

@ -14,39 +14,45 @@ import Shortcode from './Shortcode';
import { SLATE_DEFAULT_BLOCK_TYPE as defaultType } from '../../types';
import Hotkey, { HOT_KEY_MAP } from './Hotkey';
const plugins = ({ getAsset, resolveWidget, t }) => [
{
onKeyDown(event, editor, next) {
if (isHotkey('mod+j', event)) {
console.log(JSON.stringify(editor.value.document.toJS(), null, 2));
}
next();
function plugins({ getAsset, resolveWidget, t }) {
return [
{
onKeyDown(event, editor, next) {
if (isHotkey('mod+j', event)) {
console.log(JSON.stringify(editor.value.document.toJS(), null, 2));
}
next();
},
},
},
Hotkey(HOT_KEY_MAP['bold'], e => e.toggleMark('bold')),
Hotkey(HOT_KEY_MAP['code'], e => e.toggleMark('code')),
Hotkey(HOT_KEY_MAP['italic'], e => e.toggleMark('italic')),
Hotkey(HOT_KEY_MAP['strikethrough'], e => e.toggleMark('strikethrough')),
Hotkey(HOT_KEY_MAP['heading-one'], e => e.toggleBlock('heading-one')),
Hotkey(HOT_KEY_MAP['heading-two'], e => e.toggleBlock('heading-two')),
Hotkey(HOT_KEY_MAP['heading-three'], e => e.toggleBlock('heading-three')),
Hotkey(HOT_KEY_MAP['heading-four'], e => e.toggleBlock('heading-four')),
Hotkey(HOT_KEY_MAP['heading-five'], e => e.toggleBlock('heading-five')),
Hotkey(HOT_KEY_MAP['heading-six'], e => e.toggleBlock('heading-six')),
Hotkey(HOT_KEY_MAP['link'], e =>
e.toggleLink(() => window.prompt(t('editor.editorWidgets.markdown.linkPrompt'))),
),
CommandsAndQueries({ defaultType }),
QuoteBlock({ defaultType, type: 'quote' }),
ListPlugin({ defaultType, unorderedListType: 'bulleted-list', orderedListType: 'numbered-list' }),
Link({ type: 'link' }),
LineBreak(),
BreakToDefaultBlock({ defaultType }),
CloseBlock({ defaultType }),
SelectAll(),
ForceInsert({ defaultType }),
CopyPasteVisual({ getAsset, resolveWidget }),
Shortcode({ defaultType }),
];
Hotkey(HOT_KEY_MAP['bold'], e => e.toggleMark('bold')),
Hotkey(HOT_KEY_MAP['code'], e => e.toggleMark('code')),
Hotkey(HOT_KEY_MAP['italic'], e => e.toggleMark('italic')),
Hotkey(HOT_KEY_MAP['strikethrough'], e => e.toggleMark('strikethrough')),
Hotkey(HOT_KEY_MAP['heading-one'], e => e.toggleBlock('heading-one')),
Hotkey(HOT_KEY_MAP['heading-two'], e => e.toggleBlock('heading-two')),
Hotkey(HOT_KEY_MAP['heading-three'], e => e.toggleBlock('heading-three')),
Hotkey(HOT_KEY_MAP['heading-four'], e => e.toggleBlock('heading-four')),
Hotkey(HOT_KEY_MAP['heading-five'], e => e.toggleBlock('heading-five')),
Hotkey(HOT_KEY_MAP['heading-six'], e => e.toggleBlock('heading-six')),
Hotkey(HOT_KEY_MAP['link'], e =>
e.toggleLink(() => window.prompt(t('editor.editorWidgets.markdown.linkPrompt'))),
),
CommandsAndQueries({ defaultType }),
QuoteBlock({ defaultType, type: 'quote' }),
ListPlugin({
defaultType,
unorderedListType: 'bulleted-list',
orderedListType: 'numbered-list',
}),
Link({ type: 'link' }),
LineBreak(),
BreakToDefaultBlock({ defaultType }),
CloseBlock({ defaultType }),
SelectAll(),
ForceInsert({ defaultType }),
CopyPasteVisual({ getAsset, resolveWidget }),
Shortcode({ defaultType }),
];
}
export default plugins;

View File

@ -123,56 +123,118 @@ const StyledTd = styled.td`
/**
* Mark Components
*/
const Bold = props => <strong>{props.children}</strong>;
const Italic = props => <em>{props.children}</em>;
const Strikethrough = props => <s>{props.children}</s>;
const Code = props => <StyledCode>{props.children}</StyledCode>;
function Bold(props) {
return <strong>{props.children}</strong>;
}
function Italic(props) {
return <em>{props.children}</em>;
}
function Strikethrough(props) {
return <s>{props.children}</s>;
}
function Code(props) {
return <StyledCode>{props.children}</StyledCode>;
}
/**
* Node Components
*/
const Paragraph = props => <StyledP {...props.attributes}>{props.children}</StyledP>;
const ListItem = props => <StyledLi {...props.attributes}>{props.children}</StyledLi>;
const Quote = props => <StyledBlockQuote {...props.attributes}>{props.children}</StyledBlockQuote>;
const CodeBlock = props => (
<StyledPre>
<StyledCode {...props.attributes}>{props.children}</StyledCode>
</StyledPre>
);
const HeadingOne = props => <StyledH1 {...props.attributes}>{props.children}</StyledH1>;
const HeadingTwo = props => <StyledH2 {...props.attributes}>{props.children}</StyledH2>;
const HeadingThree = props => <StyledH3 {...props.attributes}>{props.children}</StyledH3>;
const HeadingFour = props => <StyledH4 {...props.attributes}>{props.children}</StyledH4>;
const HeadingFive = props => <StyledH5 {...props.attributes}>{props.children}</StyledH5>;
const HeadingSix = props => <StyledH6 {...props.attributes}>{props.children}</StyledH6>;
const Table = props => (
<StyledTable>
<tbody {...props.attributes}>{props.children}</tbody>
</StyledTable>
);
const TableRow = props => <tr {...props.attributes}>{props.children}</tr>;
const TableCell = props => <StyledTd {...props.attributes}>{props.children}</StyledTd>;
const ThematicBreak = props => (
<StyledHr
{...props.attributes}
css={
props.editor.isSelected(props.node) &&
css`
box-shadow: 0 0 0 2px ${colors.active};
border-radius: 8px;
color: ${colors.active};
`
}
/>
);
const Break = props => <br {...props.attributes} />;
const BulletedList = props => <StyledUl {...props.attributes}>{props.children}</StyledUl>;
const NumberedList = props => (
<StyledOl {...props.attributes} start={props.node.data.get('start') || 1}>
{props.children}
</StyledOl>
);
const Link = props => {
function Paragraph(props) {
return <StyledP {...props.attributes}>{props.children}</StyledP>;
}
function ListItem(props) {
return <StyledLi {...props.attributes}>{props.children}</StyledLi>;
}
function Quote(props) {
return <StyledBlockQuote {...props.attributes}>{props.children}</StyledBlockQuote>;
}
function CodeBlock(props) {
return (
<StyledPre>
<StyledCode {...props.attributes}>{props.children}</StyledCode>
</StyledPre>
);
}
function HeadingOne(props) {
return <StyledH1 {...props.attributes}>{props.children}</StyledH1>;
}
function HeadingTwo(props) {
return <StyledH2 {...props.attributes}>{props.children}</StyledH2>;
}
function HeadingThree(props) {
return <StyledH3 {...props.attributes}>{props.children}</StyledH3>;
}
function HeadingFour(props) {
return <StyledH4 {...props.attributes}>{props.children}</StyledH4>;
}
function HeadingFive(props) {
return <StyledH5 {...props.attributes}>{props.children}</StyledH5>;
}
function HeadingSix(props) {
return <StyledH6 {...props.attributes}>{props.children}</StyledH6>;
}
function Table(props) {
return (
<StyledTable>
<tbody {...props.attributes}>{props.children}</tbody>
</StyledTable>
);
}
function TableRow(props) {
return <tr {...props.attributes}>{props.children}</tr>;
}
function TableCell(props) {
return <StyledTd {...props.attributes}>{props.children}</StyledTd>;
}
function ThematicBreak(props) {
return (
<StyledHr
{...props.attributes}
css={
props.editor.isSelected(props.node) &&
css`
box-shadow: 0 0 0 2px ${colors.active};
border-radius: 8px;
color: ${colors.active};
`
}
/>
);
}
function Break(props) {
return <br {...props.attributes} />;
}
function BulletedList(props) {
return <StyledUl {...props.attributes}>{props.children}</StyledUl>;
}
function NumberedList(props) {
return (
<StyledOl {...props.attributes} start={props.node.data.get('start') || 1}>
{props.children}
</StyledOl>
);
}
function Link(props) {
const data = props.node.get('data');
const url = data.get('url');
const title = data.get('title');
@ -181,9 +243,9 @@ const Link = props => {
{props.children}
</StyledA>
);
};
}
const Image = props => {
function Image(props) {
const data = props.node.get('data');
const marks = data.get('marks');
const url = data.get('url');
@ -196,87 +258,93 @@ const Image = props => {
return renderMark({ mark, children: acc });
}, image);
return result;
};
}
export const renderMark = () => props => {
switch (props.mark.type) {
case 'bold':
return <Bold {...props} />;
case 'italic':
return <Italic {...props} />;
case 'strikethrough':
return <Strikethrough {...props} />;
case 'code':
return <Code {...props} />;
}
};
export function renderMark() {
return props => {
switch (props.mark.type) {
case 'bold':
return <Bold {...props} />;
case 'italic':
return <Italic {...props} />;
case 'strikethrough':
return <Strikethrough {...props} />;
case 'code':
return <Code {...props} />;
}
};
}
export const renderInline = () => props => {
switch (props.node.type) {
case 'link':
return <Link {...props} />;
case 'image':
return <Image {...props} />;
case 'break':
return <Break {...props} />;
}
};
export function renderInline() {
return props => {
switch (props.node.type) {
case 'link':
return <Link {...props} />;
case 'image':
return <Image {...props} />;
case 'break':
return <Break {...props} />;
}
};
}
export const renderBlock = ({ classNameWrapper, codeBlockComponent }) => props => {
switch (props.node.type) {
case 'paragraph':
return <Paragraph {...props} />;
case 'list-item':
return <ListItem {...props} />;
case 'quote':
return <Quote {...props} />;
case 'code-block':
if (codeBlockComponent) {
export function renderBlock({ classNameWrapper, codeBlockComponent }) {
return props => {
switch (props.node.type) {
case 'paragraph':
return <Paragraph {...props} />;
case 'list-item':
return <ListItem {...props} />;
case 'quote':
return <Quote {...props} />;
case 'code-block':
if (codeBlockComponent) {
return (
<VoidBlock {...props}>
<Shortcode
classNameWrapper={classNameWrapper}
typeOverload="code-block"
dataKey={false}
{...props}
/>
</VoidBlock>
);
}
return <CodeBlock {...props} />;
case 'heading-one':
return <HeadingOne {...props} />;
case 'heading-two':
return <HeadingTwo {...props} />;
case 'heading-three':
return <HeadingThree {...props} />;
case 'heading-four':
return <HeadingFour {...props} />;
case 'heading-five':
return <HeadingFive {...props} />;
case 'heading-six':
return <HeadingSix {...props} />;
case 'table':
return <Table {...props} />;
case 'table-row':
return <TableRow {...props} />;
case 'table-cell':
return <TableCell {...props} />;
case 'thematic-break':
return (
<VoidBlock {...props}>
<Shortcode
classNameWrapper={classNameWrapper}
typeOverload="code-block"
dataKey={false}
{...props}
/>
<ThematicBreak editor={props.editor} node={props.node} />
</VoidBlock>
);
}
return <CodeBlock {...props} />;
case 'heading-one':
return <HeadingOne {...props} />;
case 'heading-two':
return <HeadingTwo {...props} />;
case 'heading-three':
return <HeadingThree {...props} />;
case 'heading-four':
return <HeadingFour {...props} />;
case 'heading-five':
return <HeadingFive {...props} />;
case 'heading-six':
return <HeadingSix {...props} />;
case 'table':
return <Table {...props} />;
case 'table-row':
return <TableRow {...props} />;
case 'table-cell':
return <TableCell {...props} />;
case 'thematic-break':
return (
<VoidBlock {...props}>
<ThematicBreak editor={props.editor} node={props.node} />
</VoidBlock>
);
case 'bulleted-list':
return <BulletedList {...props} />;
case 'numbered-list':
return <NumberedList {...props} />;
case 'shortcode':
return (
<VoidBlock {...props}>
<Shortcode classNameWrapper={classNameWrapper} {...props} />
</VoidBlock>
);
}
};
case 'bulleted-list':
return <BulletedList {...props} />;
case 'numbered-list':
return <NumberedList {...props} />;
case 'shortcode':
return (
<VoidBlock {...props}>
<Shortcode classNameWrapper={classNameWrapper} {...props} />
</VoidBlock>
);
}
};
}

View File

@ -26,247 +26,249 @@ const codeBlockOverride = {
isVoid: true,
};
const schema = ({ voidCodeBlock } = {}) => ({
rules: [
/**
* Document
*/
{
match: [{ object: 'document' }],
nodes: [
{
match: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'bulleted-list' },
{ type: 'numbered-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
min: 1,
},
],
normalize: (editor, error) => {
switch (error.code) {
// If no blocks present, insert one.
case 'child_min_invalid': {
const node = { object: 'block', type: 'paragraph' };
editor.insertNodeByKey(error.node.key, 0, node);
return;
}
}
},
},
/**
* Block Containers
*/
{
match: [
{ object: 'block', type: 'quote' },
{ object: 'block', type: 'list-item' },
],
nodes: [
{
match: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'bulleted-list' },
{ type: 'numbered-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
},
],
},
/**
* List Items
*/
{
match: [{ object: 'block', type: 'list-item' }],
parent: [{ type: 'bulleted-list' }, { type: 'numbered-list' }],
},
/**
* Blocks
*/
{
match: [
{ object: 'block', type: 'paragraph' },
{ object: 'block', type: 'heading-one' },
{ object: 'block', type: 'heading-two' },
{ object: 'block', type: 'heading-three' },
{ object: 'block', type: 'heading-four' },
{ object: 'block', type: 'heading-five' },
{ object: 'block', type: 'heading-six' },
{ object: 'block', type: 'table-cell' },
{ object: 'inline', type: 'link' },
],
nodes: [
{
match: [{ object: 'text' }, { type: 'link' }, { type: 'image' }, { type: 'break' }],
},
],
},
/**
* Bulleted List
*/
{
match: [{ object: 'block', type: 'bulleted-list' }],
nodes: [
{
match: [{ type: 'list-item' }],
min: 1,
},
],
next: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'numbered-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
normalize: (editor, error) => {
switch (error.code) {
// If a list has no list items, remove the list
case 'child_min_invalid':
editor.removeNodeByKey(error.node.key);
return;
// If two bulleted lists are immediately adjacent, join them
case 'next_sibling_type_invalid':
if (error.next.type === 'bulleted-list') {
editor.mergeNodeByKey(error.next.key);
function schema({ voidCodeBlock } = {}) {
return {
rules: [
/**
* Document
*/
{
match: [{ object: 'document' }],
nodes: [
{
match: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'bulleted-list' },
{ type: 'numbered-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
min: 1,
},
],
normalize: (editor, error) => {
switch (error.code) {
// If no blocks present, insert one.
case 'child_min_invalid': {
const node = { object: 'block', type: 'paragraph' };
editor.insertNodeByKey(error.node.key, 0, node);
return;
}
return;
}
},
},
/**
* Numbered List
*/
{
match: [{ object: 'block', type: 'numbered-list' }],
nodes: [
{
match: [{ type: 'list-item' }],
min: 1,
},
],
next: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'bulleted-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
normalize: (editor, error) => {
switch (error.code) {
// If a list has no list items, remove the list
case 'child_min_invalid':
editor.removeNodeByKey(error.node.key);
return;
// If two numbered lists are immediately adjacent, join them
case 'next_sibling_type_invalid': {
if (error.next.type === 'numbered-list') {
editor.mergeNodeByKey(error.next.key);
}
return;
}
}
},
},
},
/**
* Voids
*/
{
match: [
{ object: 'inline', type: 'image' },
{ object: 'inline', type: 'break' },
{ object: 'block', type: 'thematic-break' },
{ object: 'block', type: 'shortcode' },
],
isVoid: true,
},
/**
* Block Containers
*/
{
match: [
{ object: 'block', type: 'quote' },
{ object: 'block', type: 'list-item' },
],
nodes: [
{
match: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'bulleted-list' },
{ type: 'numbered-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
},
],
},
/**
* Table
*/
{
match: [{ object: 'block', type: 'table' }],
nodes: [
{
match: [{ object: 'block', type: 'table-row' }],
/**
* List Items
*/
{
match: [{ object: 'block', type: 'list-item' }],
parent: [{ type: 'bulleted-list' }, { type: 'numbered-list' }],
},
/**
* Blocks
*/
{
match: [
{ object: 'block', type: 'paragraph' },
{ object: 'block', type: 'heading-one' },
{ object: 'block', type: 'heading-two' },
{ object: 'block', type: 'heading-three' },
{ object: 'block', type: 'heading-four' },
{ object: 'block', type: 'heading-five' },
{ object: 'block', type: 'heading-six' },
{ object: 'block', type: 'table-cell' },
{ object: 'inline', type: 'link' },
],
nodes: [
{
match: [{ object: 'text' }, { type: 'link' }, { type: 'image' }, { type: 'break' }],
},
],
},
/**
* Bulleted List
*/
{
match: [{ object: 'block', type: 'bulleted-list' }],
nodes: [
{
match: [{ type: 'list-item' }],
min: 1,
},
],
next: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'numbered-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
normalize: (editor, error) => {
switch (error.code) {
// If a list has no list items, remove the list
case 'child_min_invalid':
editor.removeNodeByKey(error.node.key);
return;
// If two bulleted lists are immediately adjacent, join them
case 'next_sibling_type_invalid':
if (error.next.type === 'bulleted-list') {
editor.mergeNodeByKey(error.next.key);
}
return;
}
},
],
},
},
/**
* Table Row
*/
{
match: [{ object: 'block', type: 'table-row' }],
nodes: [
{
match: [{ object: 'block', type: 'table-cell' }],
/**
* Numbered List
*/
{
match: [{ object: 'block', type: 'numbered-list' }],
nodes: [
{
match: [{ type: 'list-item' }],
min: 1,
},
],
next: [
{ type: 'paragraph' },
{ type: 'heading-one' },
{ type: 'heading-two' },
{ type: 'heading-three' },
{ type: 'heading-four' },
{ type: 'heading-five' },
{ type: 'heading-six' },
{ type: 'quote' },
{ type: 'code-block' },
{ type: 'bulleted-list' },
{ type: 'thematic-break' },
{ type: 'table' },
{ type: 'shortcode' },
],
normalize: (editor, error) => {
switch (error.code) {
// If a list has no list items, remove the list
case 'child_min_invalid':
editor.removeNodeByKey(error.node.key);
return;
// If two numbered lists are immediately adjacent, join them
case 'next_sibling_type_invalid': {
if (error.next.type === 'numbered-list') {
editor.mergeNodeByKey(error.next.key);
}
return;
}
}
},
],
},
},
/**
* Marks
*/
{
match: [
{ object: 'mark', type: 'bold' },
{ object: 'mark', type: 'italic' },
{ object: 'mark', type: 'strikethrough' },
{ object: 'mark', type: 'code' },
],
},
/**
* Voids
*/
{
match: [
{ object: 'inline', type: 'image' },
{ object: 'inline', type: 'break' },
{ object: 'block', type: 'thematic-break' },
{ object: 'block', type: 'shortcode' },
],
isVoid: true,
},
/**
* Overrides
*/
voidCodeBlock ? codeBlockOverride : codeBlock,
],
});
/**
* Table
*/
{
match: [{ object: 'block', type: 'table' }],
nodes: [
{
match: [{ object: 'block', type: 'table-row' }],
},
],
},
/**
* Table Row
*/
{
match: [{ object: 'block', type: 'table-row' }],
nodes: [
{
match: [{ object: 'block', type: 'table-cell' }],
},
],
},
/**
* Marks
*/
{
match: [
{ object: 'mark', type: 'bold' },
{ object: 'mark', type: 'italic' },
{ object: 'mark', type: 'strikethrough' },
{ object: 'mark', type: 'code' },
],
},
/**
* Overrides
*/
voidCodeBlock ? codeBlockOverride : codeBlock,
],
};
}
export default schema;

View File

@ -2,13 +2,15 @@ import controlComponent from './MarkdownControl';
import previewComponent from './MarkdownPreview';
import schema from './schema';
const Widget = (opts = {}) => ({
name: 'markdown',
controlComponent,
previewComponent,
schema,
...opts,
});
function Widget(opts = {}) {
return {
name: 'markdown',
controlComponent,
previewComponent,
schema,
...opts,
};
}
export const NetlifyCmsWidgetMarkdown = { Widget, controlComponent, previewComponent };
export default NetlifyCmsWidgetMarkdown;

View File

@ -68,10 +68,12 @@ const onlys = [
*/
const reader = new commonmark.Parser();
const writer = new commonmark.HtmlRenderer();
const parseWithCommonmark = markdown => {
function parseWithCommonmark(markdown) {
const parsed = reader.parse(markdown);
return writer.render(parsed);
};
}
const parse = flow([markdownToSlate, slateToMarkdown]);
/**

View File

@ -2,7 +2,7 @@ import unified from 'unified';
import markdownToRemark from 'remark-parse';
import remarkAllowHtmlEntities from '../remarkAllowHtmlEntities';
const process = markdown => {
function process(markdown) {
const mdast = unified()
.use(markdownToRemark)
.use(remarkAllowHtmlEntities)
@ -18,7 +18,7 @@ const process = markdown => {
* ]}
*/
return mdast.children[0].children[0].value;
};
}
describe('remarkAllowHtmlEntities', () => {
it('should not decode HTML entities', () => {

View File

@ -2,14 +2,14 @@ import unified from 'unified';
import u from 'unist-builder';
import remarkEscapeMarkdownEntities from '../remarkEscapeMarkdownEntities';
const process = text => {
function process(text) {
const tree = u('root', [u('text', text)]);
const escapedMdast = unified()
.use(remarkEscapeMarkdownEntities)
.runSync(tree);
return escapedMdast.children[0].value;
};
}
describe('remarkEscapeMarkdownEntities', () => {
it('should escape common markdown entities', () => {

View File

@ -3,18 +3,20 @@ import markdownToRemark from 'remark-parse';
import remarkToMarkdown from 'remark-stringify';
import remarkPaddedLinks from '../remarkPaddedLinks';
const input = markdown =>
unified()
function input(markdown) {
return unified()
.use(markdownToRemark)
.use(remarkPaddedLinks)
.use(remarkToMarkdown)
.processSync(markdown).contents;
}
const output = markdown =>
unified()
function output(markdown) {
return unified()
.use(markdownToRemark)
.use(remarkToMarkdown)
.processSync(markdown).contents;
}
describe('remarkPaddedLinks', () => {
it('should move leading and trailing spaces outside of a link', () => {

View File

@ -2,7 +2,10 @@ import { remarkParseShortcodes } from '../remarkShortcodes';
// Stub of Remark Parser
function process(value, plugins, processEat = () => {}) {
const eat = () => processEat;
function eat() {
return processEat;
}
function Parser() {}
Parser.prototype.blockTokenizers = {};
Parser.prototype.blockMethods = [];

View File

@ -2,14 +2,14 @@ import unified from 'unified';
import u from 'unist-builder';
import remarkStripTrailingBreaks from '../remarkStripTrailingBreaks';
const process = children => {
function process(children) {
const tree = u('root', children);
const strippedMdast = unified()
.use(remarkStripTrailingBreaks)
.runSync(tree);
return strippedMdast.children;
};
}
describe('remarkStripTrailingBreaks', () => {
it('should remove trailing breaks at the end of a block', () => {

View File

@ -58,7 +58,7 @@ import { getEditorComponents } from '../MarkdownControl';
/**
* Deserialize a Markdown string to an MDAST.
*/
export const markdownToRemark = markdown => {
export function markdownToRemark(markdown) {
/**
* Parse the Markdown string input to an MDAST.
*/
@ -77,7 +77,7 @@ export const markdownToRemark = markdown => {
.runSync(parsed);
return result;
};
}
/**
* Remove named tokenizers from the parser, effectively deactivating them.
@ -92,7 +92,7 @@ function markdownToRemarkRemoveTokenizers({ inlineTokenizers }) {
/**
* Serialize an MDAST to a Markdown string.
*/
export const remarkToMarkdown = obj => {
export function remarkToMarkdown(obj) {
/**
* Rewrite the remark-stringify text visitor to simply return the text value,
* without encoding or escaping any characters. This means we're completely
@ -143,12 +143,12 @@ export const remarkToMarkdown = obj => {
* Return markdown with trailing whitespace removed.
*/
return trimEnd(markdown);
};
}
/**
* Convert Markdown to HTML.
*/
export const markdownToHtml = (markdown, { getAsset, resolveWidget } = {}) => {
export function markdownToHtml(markdown, { getAsset, resolveWidget } = {}) {
const mdast = markdownToRemark(markdown);
const hast = unified()
@ -166,13 +166,13 @@ export const markdownToHtml = (markdown, { getAsset, resolveWidget } = {}) => {
.stringify(hast);
return html;
};
}
/**
* Deserialize an HTML string to Slate's Raw AST. Currently used for HTML
* pastes.
*/
export const htmlToSlate = html => {
export function htmlToSlate(html) {
const hast = unified()
.use(htmlToRehype, { fragment: true })
.parse(html);
@ -190,12 +190,12 @@ export const htmlToSlate = html => {
.runSync(mdast);
return slateRaw;
};
}
/**
* Convert Markdown to Slate's Raw AST.
*/
export const markdownToSlate = (markdown, { voidCodeBlock } = {}) => {
export function markdownToSlate(markdown, { voidCodeBlock } = {}) {
const mdast = markdownToRemark(markdown);
const slateRaw = unified()
@ -204,7 +204,7 @@ export const markdownToSlate = (markdown, { voidCodeBlock } = {}) => {
.runSync(mdast);
return slateRaw;
};
}
/**
* Convert a Slate Raw AST to Markdown.
@ -215,8 +215,8 @@ export const markdownToSlate = (markdown, { voidCodeBlock } = {}) => {
* MDAST. The conversion is manual because Unified can only operate on Unist
* trees.
*/
export const slateToMarkdown = (raw, { voidCodeBlock } = {}) => {
export function slateToMarkdown(raw, { voidCodeBlock } = {}) {
const mdast = slateToRemark(raw, { voidCodeBlock });
const markdown = remarkToMarkdown(mdast);
return markdown;
};
}

View File

@ -4,12 +4,13 @@
* replaces the images with the emoji characters.
*/
export default function rehypePaperEmoji() {
const transform = node => {
function transform(node) {
if (node.tagName === 'img' && node.properties.dataEmojiCh) {
return { type: 'text', value: node.properties.dataEmojiCh };
}
node.children = node.children ? node.children.map(transform) : node.children;
return node;
};
}
return transform;
}

View File

@ -236,7 +236,7 @@ function escape(delim) {
* stringification.
*/
export default function remarkEscapeMarkdownEntities() {
const transform = (node, index) => {
function transform(node, index) {
/**
* Shortcode nodes will intentionally inject markdown entities in text node
* children not be escaped.
@ -262,7 +262,7 @@ export default function remarkEscapeMarkdownEntities() {
* Always return nodes with recursively mapped children.
*/
return { ...node, ...children };
};
}
return transform;
}

View File

@ -28,11 +28,19 @@ const markMap = {
inlineCode: 'code',
};
const isInline = node => node.object === 'inline';
const isText = node => node.object === 'text';
const isMarksEqual = (node1, node2) => isEqual(node1.marks, node2.marks);
function isInline(node) {
return node.object === 'inline';
}
export const wrapInlinesWithTexts = children => {
function isText(node) {
return node.object === 'text';
}
function isMarksEqual(node1, node2) {
return isEqual(node1.marks, node2.marks);
}
export function wrapInlinesWithTexts(children) {
if (children.length <= 0) {
return children;
}
@ -63,9 +71,9 @@ export const wrapInlinesWithTexts = children => {
}
return children;
};
}
export const mergeAdjacentTexts = children => {
export function mergeAdjacentTexts(children) {
if (children.length <= 0) {
return children;
}
@ -96,7 +104,7 @@ export const mergeAdjacentTexts = children => {
}
return mergedChildren;
};
}
/**
* A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins

View File

@ -10,7 +10,7 @@ import mdastToString from 'mdast-util-to-string';
* these artifacts in resulting markdown.
*/
export default function remarkStripTrailingBreaks() {
const transform = node => {
function transform(node) {
if (node.children) {
node.children = node.children
.map((child, idx, children) => {
@ -50,6 +50,7 @@ export default function remarkStripTrailingBreaks() {
.map(transform);
}
return node;
};
}
return transform;
}

View File

@ -208,6 +208,10 @@ export default function slateToRemark(raw, { voidCodeBlock }) {
return { leadingWhitespace, centerNodes, trailingWhitespace };
}
function createText(text) {
return text && u('html', text);
}
function convertInlineAndTextChildren(nodes = []) {
const convertedNodes = [];
let remainingNodes = nodes;
@ -243,7 +247,7 @@ export default function slateToRemark(raw, { voidCodeBlock }) {
remainingNodes = remainder;
continue;
}
const createText = text => text && u('html', text);
const normalizedNodes = [
createText(leadingWhitespace),
markNode,