From 5a664f8be105c9c27cfe828712867e34f001ab96 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 27 Jun 2017 12:39:23 -0400 Subject: [PATCH] remove prosemirror, reuse unified pipelines --- .../MarkdownControl/RawEditor/index.js | 50 +---- .../MarkdownControl/VisualEditor/index.js | 32 +-- .../MarkdownControl/VisualEditor/keymap.js | 92 --------- .../VisualEditor/markdownToProseMirror.js | 187 ------------------ .../MarkdownControl/VisualEditor/parser.js | 34 ---- .../MarkdownPreview/cmsPluginRehype.js | 59 ------ .../Widgets/Markdown/MarkdownPreview/index.js | 6 - src/components/Widgets/Markdown/unified.js | 33 ++++ .../Widgets/Markdown/unifiedConfig.js | 4 - 9 files changed, 41 insertions(+), 456 deletions(-) delete mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keymap.js delete mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/markdownToProseMirror.js delete mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/parser.js delete mode 100644 src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js create mode 100644 src/components/Widgets/Markdown/unified.js delete mode 100644 src/components/Widgets/Markdown/unifiedConfig.js diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 0bf25a33..ad05396a 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,24 +1,10 @@ import React, { PropTypes } from 'react'; import get from 'lodash/get'; import unified from 'unified'; -import markdownToRemark from 'remark-parse'; -import remarkToRehype from 'remark-rehype'; -import rehypeToHtml from 'rehype-stringify'; -import htmlToRehype from 'rehype-parse'; -import rehypeToRemark from 'rehype-remark'; -import remarkToMarkdown from 'remark-stringify'; -import rehypeSanitize from 'rehype-sanitize'; -import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; -import rehypeReparse from 'rehype-raw'; import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; import registry from '../../../../../lib/registry'; -import { - remarkParseConfig, - remarkStringifyConfig, - rehypeParseConfig, - rehypeStringifyConfig, -} from '../../unifiedConfig'; +import { markdownToHtml, htmlToMarkdown } from '../../unified'; import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; @@ -36,18 +22,6 @@ function processUrl(url) { return `/${ url }`; } -function cleanupPaste(paste) { - return unified() - .use(htmlToRehype, rehypeParseConfig) - .use(rehypeSanitize) - .use(rehypeReparse) - .use(rehypeToRemark) - .use(rehypeSanitize) - .use(rehypeMinifyWhitespace) - .use(remarkToMarkdown, remarkStringifyConfig) - .process(paste); -} - function getCleanPaste(e) { const transfer = e.clipboardData; return new Promise((resolve) => { @@ -58,7 +32,7 @@ function getCleanPaste(e) { // Avoid trying to clean up full HTML documents with head/body/etc if (!data.match(/^\s* { - resolve(cleanupPaste(div.innerHTML)); + resolve(htmlToMarkdown(div.innerHTML)); document.body.removeChild(div); }, 50); return null; @@ -86,12 +60,7 @@ export default class RawEditor extends React.Component { super(props); const plugins = registry.getEditorComponents(); this.state = { - value: unified() - .use(htmlToRehype) - .use(rehypeToRemark) - .use(remarkToMarkdown, remarkStringifyConfig) - .processSync(this.props.value) - .contents, + value: htmlToMarkdown(this.props.value), plugins, }; this.shortcuts = { @@ -259,16 +228,7 @@ export default class RawEditor extends React.Component { handleChange = (e) => { // handleChange may receive an event or a value const value = typeof e === 'object' ? e.target.value : e; - const html = unified() - .use(markdownToRemark, remarkParseConfig) - .use(remarkToRehype) - .use(rehypeSanitize) - .use(rehypeMinifyWhitespace) - .use(rehypeToHtml, rehypeStringifyConfig) - - .processSync(value) - .contents; - console.log(html); + const html = markdownToHtml(value); this.props.onChange(html); this.updateHeight(); this.setState({ value }); diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index d13f23d0..96afba71 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,23 +1,9 @@ import React, { Component, PropTypes } from 'react'; import { Map, List } from 'immutable'; import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw} from 'slate'; -import unified from 'unified'; -import markdownToRemark from 'remark-parse'; -import remarkToRehype from 'remark-rehype'; -import rehypeToHtml from 'rehype-stringify'; -import remarkToMarkdown from 'remark-stringify'; -import htmlToRehype from 'rehype-parse'; -import rehypeToRemark from 'rehype-remark'; +import { markdownToHtml, htmlToMarkdown } from '../../unified'; import registry from '../../../../../lib/registry'; import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; -import { - remarkParseConfig, - remarkStringifyConfig, - rehypeParseConfig, - rehypeStringifyConfig, -} from '../../unifiedConfig'; -import { buildKeymap } from './keymap'; -import createMarkdownParser from './parser'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; @@ -29,19 +15,8 @@ import styles from './index.css'; * and before persisting. */ registry.registerWidgetValueSerializer('markdown', { - serialize: value => unified() - .use(htmlToRehype, rehypeParseConfig) - .use(htmlToRehype) - .use(rehypeToRemark) - .use(remarkToMarkdown, remarkStringifyConfig) - .processSync(value) - .contents, - deserialize: value => unified() - .use(markdownToRemark, remarkParseConfig) - .use(remarkToRehype) - .use(rehypeToHtml, rehypeStringifyConfig) - .processSync(value) - .contents + serialize: htmlToMarkdown, + deserialize: markdownToHtml, }); function processUrl(url) { @@ -281,7 +256,6 @@ export default class Editor extends Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); - console.log(this.props.value); this.state = { editorState: serializer.deserialize(this.props.value || '

'), schema: { diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keymap.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keymap.js deleted file mode 100644 index bc0e1a22..00000000 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keymap.js +++ /dev/null @@ -1,92 +0,0 @@ -const { wrapIn, setBlockType, chainCommands, newlineInCode, toggleMark } = require('prosemirror-commands'); -const { selectNextCell, selectPreviousCell } = require('prosemirror-schema-table'); -const { wrapInList, splitListItem, liftListItem, sinkListItem } = require('prosemirror-schema-list'); -const { undo, redo } = require('prosemirror-history'); - -const mac = typeof navigator != 'undefined' ? /Mac/.test(navigator.platform) : false; - -// :: (Schema, ?Object) → Object -// Inspect the given schema looking for marks and nodes from the -// basic schema, and if found, add key bindings related to them. -// This will add: -// -// * **Mod-b** for toggling [strong](#schema-basic.StrongMark) -// * **Mod-i** for toggling [emphasis](#schema-basic.EmMark) -// * **Mod-`** for toggling [code font](#schema-basic.CodeMark) -// * **Ctrl-Shift-0** for making the current textblock a paragraph -// * **Ctrl-Shift-1** to **Ctrl-Shift-Digit6** for making the current -// textblock a heading of the corresponding level -// * **Ctrl-Shift-Backslash** to make the current textblock a code block -// * **Ctrl-Shift-8** to wrap the selection in an ordered list -// * **Ctrl-Shift-9** to wrap the selection in a bullet list -// * **Ctrl->** to wrap the selection in a block quote -// * **Enter** to split a non-empty textblock in a list item while at -// the same time splitting the list item -// * **Mod-Enter** to insert a hard break -// * **Mod-_** to insert a horizontal rule -// -// You can suppress or map these bindings by passing a `mapKeys` -// argument, which maps key names (say `"Mod-B"` to either `false`, to -// remove the binding, or a new key name string. -function buildKeymap(schema, mapKeys) { - let keys = {}, type; - function bind(key, cmd) { - if (mapKeys) { - const mapped = mapKeys[key]; - if (mapped === false) return; - if (mapped) key = mapped; - } - keys[key] = cmd; - } - - bind('Mod-z', undo); - bind('Mod-y', redo); - - if (type = schema.marks.strong) - bind('Mod-b', toggleMark(type)); - if (type = schema.marks.em) - bind('Mod-i', toggleMark(type)); - if (type = schema.marks.code) - bind('Mod-`', toggleMark(type)); - - if (type = schema.nodes.bullet_list) - bind('Shift-Ctrl-8', wrapInList(type)); - if (type = schema.nodes.ordered_list) - bind('Shift-Ctrl-9', wrapInList(type)); - if (type = schema.nodes.blockquote) - bind('Ctrl->', wrapIn(type)); - if (type = schema.nodes.hard_break) { - let br = type, cmd = chainCommands(newlineInCode, (state, onAction) => { - onAction(state.tr.replaceSelection(br.create()).scrollAction()); - return true; - }); - bind('Mod-Enter', cmd); - bind('Shift-Enter', cmd); - if (mac) bind('Ctrl-Enter', cmd); - } - if (type = schema.nodes.list_item) { - bind('Enter', splitListItem(type)); - bind('Mod-[', liftListItem(type)); - bind('Mod-]', sinkListItem(type)); - } - if (type = schema.nodes.paragraph) - bind('Shift-Ctrl-0', setBlockType(type)); - if (type = schema.nodes.code_block) - bind('Shift-Ctrl-\\', setBlockType(type)); - if (type = schema.nodes.heading) - for (let i = 1; i <= 6; i++) bind(`Shift-Ctrl-${ i }`, setBlockType(type, { level: i })); - if (type = schema.nodes.horizontal_rule) { - const hr = type; - bind('Mod-_', (state, onAction) => { - onAction(state.tr.replaceSelection(hr.create()).scrollAction()); - return true; - }); - } - - if (schema.nodes.table_row) { - bind('Tab', selectNextCell); - bind('Shift-Tab', selectPreviousCell); - } - return keys; -} -exports.buildKeymap = buildKeymap; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/markdownToProseMirror.js deleted file mode 100644 index 6f212121..00000000 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/markdownToProseMirror.js +++ /dev/null @@ -1,187 +0,0 @@ -import get from 'lodash/get'; -import isEmpty from 'lodash/isEmpty'; - -/** - * A remark plugin for converting an MDAST to a ProseMirror tree. - * @param {state} information to be shared across ProseMirror actions - * @returns {function} a transformer function - */ -export default function markdownToProseMirror({ state }) { - - // The state object also contains `activeMarks` and `textsArray`, but we - // may change those values from here to be shared across ProseMirror actions - // (this plugin is run for each action), so we always access them directly - // on the state object. - const { schema, plugins } = state; - - // return transform; - - return node => { - const result = transform(node); - return result; - }; - - /** - * The MDAST transformer function. - * @param {object} node an MDAST node - * @returns {Node} a ProseMirror Node - */ - function transform(node) { - if (node.type === 'text') { - processText(node.value); - return; - } - - const nodeDef = getNodeDef(node); - const processor = get(nodeDef, 'block') ? processBlock : processInline; - - return nodeDef ? processor(nodeDef, node.children, node.value) : node; - } - - /** - * Provides required information for converting an MDAST node into a ProseMirror - * Node. - * - * @param {object} node - an MDAST node - * @returns {object} conversion data node with the following shape: - * {string} pmType - the equivalent node type in the ProseMirror schema - * {boolean} block - true if the node is block level, otherwise false - * {object} attrs - passed to ProseMirror's schema mark/node creation methods - * {object} content - overrides `node.children` as node content - * {Node} defaultContent - content to use if node has no content (default: null) - * {boolean} canContainPlugins true for nodes that may contain plugins - */ - function getNodeDef({ type, ordered, lang, value, depth, url, alt }) { - switch (type) { - case 'root': - return { pmType: 'doc', block: true, defaultContent: schema.node('paragraph') }; - case 'heading': - return { pmType: type, attrs: { level: depth }, hasText: true, block: true }; - case 'paragraph': - return { pmType: type, hasText: true, block: true, canContainPlugins: true }; - case 'blockquote': - return { pmType: type, block: true }; - case 'list': - return { pmType: ordered ? 'ordered_list' : 'bullet_list', attrs: { tight: true }, block: true }; - case 'listItem': - return { pmType: 'list_item', block: true }; - case 'thematicBreak': - return { pmType: 'horizontal_rule', block: true }; - case 'break': - return { pmType: 'hard_break', block: true }; - case 'image': - return { pmType: type, block: true, attrs: { src: url, alt } }; - case 'code': - return { pmType: 'code_block', attrs: { params: lang }, content: schema.text(value), block: true }; - case 'emphasis': - return { pmType: 'em' }; - case 'strong': - return { pmType: type }; - case 'link': - return { pmType: type, attrs: { href: url } }; - case 'inlineCode': - return { pmType: 'code' }; - } - } - - /** - * Derives content from block nodes. Block nodes containing raw text, such as - * headings and paragraphs, are processed differently than block nodes - * containing other node types. - * @param {array} children child nodes - * @param {boolean} hasText if true, the node contains raw text nodes - * @returns {array} processed child nodes - */ - function getBlockContent(children, hasText) { - // children.map will return undefined for text nodes, so we filter those out - const processedChildren = children.map(transform).filter(val => val); - - if (hasText) { - const content = state.textsArray; - state.textsArray = []; - return content; - } - - return processedChildren; - } - - /** - * Processes text nodes. - * @param {string} value the node's text content - * @returns {undefined} - */ - function processText(value) { - state.textsArray.push(schema.text(value, state.activeMarks)); - return; - } - - /** - * Processes block nodes. - * @param {object} nodeModel the nodeModel for this node type via nodeModelGetters - * @param {array} children the node's child nodes - * @return {Node} a ProseMirror node - */ - function processBlock({ pmType, attrs, content, defaultContent = null, hasText, canContainPlugins }, children) { - // Plugins are just text shortcodes, so they're rendered as a text node within - // a paragraph node in the MDAST. We use a regex to determine if the text - // represents a plugin, so for performance reasons we only test text nodes that - // are the only child of a node that can contain plugins. Currently, only - // paragraphs may contain plugins. - // - // Additionally, images are handled via plugin. Because images already have a - // markdown pattern, they're represented as 'image' type in the MDAST. We - // check for those here, too. - if (canContainPlugins && children.length === 1 && ['text', 'image'].includes(children[0].type)) { - const processedPlugin = processPlugin(children[0]); - if (processedPlugin) { - return processedPlugin; - } - } - - const nodeContent = content || (isEmpty(children) ? defaultContent : getBlockContent(children, hasText)); - return schema.node(pmType, attrs, nodeContent); - } - - /** - * Processes inline nodes. - * @param {object} nodeModel the nodeModel for this node type via nodeModelGetters - * @param {array} children the node's child nodes - * @return {undefined} - */ - function processInline({ pmType, attrs }, children, value) { - const mark = schema.marks[pmType].create(attrs); - state.activeMarks = mark.addToSet(state.activeMarks); - - if (isEmpty(children)) { - state.textsArray.push(schema.text(value, state.activeMarks)); - } else { - children.forEach(childNode => transform(childNode)); - } - - state.activeMarks = mark.removeFromSet(state.activeMarks); - return; - } - - /** - * Processes plugins, which are represented as user-defined text shortcodes. - * - * The built in image plugin is handled differently because it overrides - * remark/rehype's handling of a recognized markdown/html entity. Ideally, would - * stop remark from parsing images at all, so that no special logic would be - * required, but overriding this way would require a plugin to indicate what - * entity it's overriding. - * - * @param {object} a remark node representing a user defined plugin - * @return {Node} a ProseMirror Node - */ - function processPlugin({ type, value, alt, url }) { - const isImage = type === 'image'; - const plugin = isImage ? plugins.get('image') : plugins.find(plugin => plugin.get('pattern').test(value)); - if (plugin) { - const matches = isImage ? [ , alt, url ] : value.match(plugin.get('pattern')); - const nodeType = schema.nodes[`plugin_${plugin.get('id')}`]; - const data = plugin.get('fromBlock').call(plugin, matches); - return nodeType.create(data); - } - } -} diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/parser.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/parser.js deleted file mode 100644 index 9c6a0882..00000000 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/parser.js +++ /dev/null @@ -1,34 +0,0 @@ -import unified from 'unified'; -import remarkToMarkdown from 'remark-parse'; -import { Mark } from 'prosemirror-model'; -import markdownToProseMirror from './markdownToProseMirror'; - -const state = { activeMarks: Mark.none, textsArray: [] }; - -/** - * Uses unified to parse markdown and apply plugins. - * @param {string} src raw markdown - * @returns {Node} a ProseMirror Node - */ -function parser(src) { - const result = unified() - .use(remarkToMarkdown, { fences: true, footnotes: true, pedantic: true }) - .parse(src); - - return unified() - .use(markdownToProseMirror, { state }) - .runSync(result); -} - -/** - * Gets the parser and makes schema and plugins available at top scope. - * @param {Schema} schema - a ProseMirror schema - * @param {Map} plugins - Immutable Map of registered plugins - */ -function parserGetter(schema, plugins) { - state.schema = schema; - state.plugins = plugins; - return parser; -} - -export default parserGetter; diff --git a/src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js b/src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js deleted file mode 100644 index 186b20aa..00000000 --- a/src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { PropTypes } from "react"; -import { renderToStaticMarkup } from 'react-dom/server'; -import { Map } from 'immutable'; -import isString from 'lodash/isString'; -import isEmpty from 'lodash/isEmpty'; -import unified from 'unified'; -import htmlToRehype from 'rehype-parse'; -import registry from "../../../../lib/registry"; - -const cmsPluginRehype = ({ getAsset }) => { - - const plugins = registry.getEditorComponents(); - - return transform; - - function transform(node) { - // Handle externally defined plugins (they'll be wrapped in paragraphs) - if (node.tagName === 'p' && node.children.length === 1) { - if (node.children[0].type === 'text') { - const value = node.children[0].value; - const plugin = plugins.find(plugin => plugin.get('pattern').test(value)); - if (plugin) { - const data = plugin.get('fromBlock')(value.match(plugin.get('pattern'))); - const preview = plugin.get('toPreview')(data); - const output = `
${isString(preview) ? preview : renderToStaticMarkup(preview)}
`; - return unified().use(htmlToRehype, { fragment: true }).parse(output).children[0]; - } - } - - // Handle the internally defined image plugin. At this point the token has - // already been parsed as an image by Remark, so we have to catch it by - // checking for the 'image' type. - if (node.children[0].tagName === 'img') { - const { src, alt } = node.children[0].properties; - - // Until we improve the editor components API for built in components, - // we'll mock the result of String.prototype.match to pass in to the image - // plugin's fromBlock method. - const plugin = plugins.get('image'); - if (plugin) { - const matches = [ , alt, src ]; - const data = plugin.get('fromBlock')(matches); - const extendedData = { ...data, image: getAsset(data.image).toString() }; - const preview = plugin.get('toPreview')(extendedData); - const output = `
${isString(preview) ? preview : renderToStaticMarkup(preview)}
`; - return unified().use(htmlToRehype, { fragment: true }).parse(output).children[0]; - } - } - } - - if (!isEmpty(node.children)) { - node.children = node.children.map(childNode => transform(childNode, getAsset)); - } - - return node; - } -}; - -export default cmsPluginRehype; diff --git a/src/components/Widgets/Markdown/MarkdownPreview/index.js b/src/components/Widgets/Markdown/MarkdownPreview/index.js index 3df5b23f..b7927dfd 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/index.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/index.js @@ -1,10 +1,4 @@ import React, { PropTypes } from 'react'; -import unified from 'unified'; -import markdownToRemark from 'remark-parse'; -import remarkToRehype from 'remark-rehype'; -import htmlToRehype from 'rehype-parse'; -import rehypeToReact from 'rehype-react'; -import cmsPluginToRehype from './cmsPluginRehype'; import previewStyle from '../../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js new file mode 100644 index 00000000..0d7e6957 --- /dev/null +++ b/src/components/Widgets/Markdown/unified.js @@ -0,0 +1,33 @@ +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToRehype from 'remark-rehype'; +import rehypeToHtml from 'rehype-stringify'; +import htmlToRehype from 'rehype-parse'; +import rehypeToRemark from 'rehype-remark'; +import remarkToMarkdown from 'remark-stringify'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; + +const remarkParseConfig = { fences: true }; +const remarkStringifyConfig = { listItemIndent: '1', fences: true }; +const rehypeParseConfig = { fragment: true }; + +export const markdownToHtml = markdown => + unified() + .use(markdownToRemark, remarkParseConfig) + .use(remarkToRehype) + .use(rehypeSanitize) + .use(rehypeMinifyWhitespace) + .use(rehypeToHtml) + .processSync(markdown) + .contents; + +export const htmlToMarkdown = html => + unified() + .use(htmlToRehype, rehypeParseConfig) + .use(rehypeSanitize) + .use(rehypeMinifyWhitespace) + .use(rehypeToRemark) + .use(remarkToMarkdown, remarkStringifyConfig) + .processSync(html) + .contents; diff --git a/src/components/Widgets/Markdown/unifiedConfig.js b/src/components/Widgets/Markdown/unifiedConfig.js deleted file mode 100644 index 2c2f11c4..00000000 --- a/src/components/Widgets/Markdown/unifiedConfig.js +++ /dev/null @@ -1,4 +0,0 @@ -export const remarkParseConfig = { fences: true }; -export const remarkStringifyConfig = { listItemIndent: '1', fences: true }; -export const rehypeParseConfig = { fragment: true }; -export const rehypeStringifyConfig = {};