From 24c0a1bdb471d1cf23279ddd0eb3e1863903877f Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 1 Mar 2017 15:58:12 -0800 Subject: [PATCH] Replace markup-it with Remark for rendering markdown in the editor preview --- package.json | 4 + .../__tests__/MarkupItReactRenderer.spec.js | 199 +++--------------- .../MarkupItReactRenderer.spec.js.snap | 72 +++++-- src/components/MarkupItReactRenderer/index.js | 131 ++---------- 4 files changed, 108 insertions(+), 298 deletions(-) diff --git a/package.json b/package.json index 29506cf1..0cd64b56 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "dateformat": "^1.0.12", "deep-equal": "^1.0.1", "fuzzy": "^0.1.1", + "hast-util-to-html": "^3.0.0", "history": "^2.1.2", "immutability-helper": "^2.0.0", "immutable": "^3.7.6", @@ -112,6 +113,7 @@ "lodash": "^4.13.1", "markup-it": "^2.0.0", "material-design-icons": "^3.0.1", + "mdast-util-to-hast": "^2.4.0", "moment": "^2.11.2", "netlify-auth-js": "^0.5.5", "normalize.css": "^4.2.0", @@ -157,6 +159,8 @@ "redux-notifications": "^2.1.1", "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", + "remark": "6", + "remark-html": "^6.0.0", "selection-position": "^1.0.0", "semaphore": "^1.0.5", "slate": "^0.14.14", diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js index f9f2c158..a86d4f76 100644 --- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js @@ -1,58 +1,14 @@ /* eslint max-len:0 */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { padStart } from 'lodash'; -import { Map } from 'immutable'; -import MarkupIt from 'markup-it'; -import markdownSyntax from 'markup-it/syntaxes/markdown'; -import htmlSyntax from 'markup-it/syntaxes/html'; -import reInline from 'markup-it/syntaxes/markdown/re/inline'; -import MarkupItReactRenderer from '../'; +import React from "react"; +import { shallow } from "enzyme"; +import { padStart } from "lodash"; +import MarkupItReactRenderer from "../"; -function getAsset(path) { - return path; -} - -describe('MarkitupReactRenderer', () => { - describe('basics', () => { - it('should re-render properly after a value and syntax update', () => { - const component = shallow( - - ); - const tree1 = component.html(); - component.setProps({ - value: '

Title

', - syntax: htmlSyntax, - }); - const tree2 = component.html(); - expect(tree1).toEqual(tree2); - }); - - it('should not update the parser if syntax didn\'t change', () => { - const component = shallow( - - ); - const syntax1 = component.instance().props.syntax; - component.setProps({ - value: '## Title', - }); - const syntax2 = component.instance().props.syntax; - expect(syntax1).toEqual(syntax2); - }); - }); - - describe('Markdown rendering', () => { - describe('General', () => { - it('should render markdown', () => { +describe("MarkitupReactRenderer", () => { + describe("Markdown rendering", () => { + describe("General", () => { + it("should render markdown", () => { const value = ` # H1 @@ -79,35 +35,23 @@ Text with **bold** & _em_ elements ###### H6 `; - const component = shallow( - - ); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); - describe('Headings', () => { + describe("Headings", () => { for (const heading of [...Array(6).keys()]) { it(`should render Heading ${ heading + 1 }`, () => { - const value = padStart(' Title', heading + 7, '#'); - const component = shallow( - - ); + const value = padStart(" Title", heading + 7, "#"); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); } }); - describe('Lists', () => { - it('should render lists', () => { + describe("Lists", () => { + it("should render lists", () => { const value = ` 1. ol item 1 1. ol item 2 @@ -119,19 +63,13 @@ Text with **bold** & _em_ elements 1. Sub-Sublist 3 1. ol item 3 `; - const component = shallow( - - ); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); - describe('Links', () => { - it('should render links', () => { + describe("Links", () => { + it("should render links", () => { const value = ` I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]. @@ -139,45 +77,27 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] [2]: http://search.yahoo.com/ "Yahoo Search" [3]: http://search.msn.com/ "MSN Search" `; - const component = shallow( - - ); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); - describe('Code', () => { - it('should render code', () => { - const value = 'Use the `printf()` function.'; - const component = shallow( - - ); + describe("Code", () => { + it("should render code", () => { + const value = "Use the `printf()` function."; + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); - it('should render code 2', () => { - const value = '``There is a literal backtick (`) here.``'; - const component = shallow( - - ); + it("should render code 2", () => { + const value = "``There is a literal backtick (`) here.``"; + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); - describe('HTML', () => { - it('should render HTML as is when using Markdown', () => { + describe("HTML", () => { + it("should render HTML as is when using Markdown", () => { const value = ` # Title @@ -193,71 +113,16 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]

Test

`; - const component = shallow( - - ); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); }); - describe('custom elements', () => { - it('should extend default renderers with custom ones', () => { - const myRule = MarkupIt.Rule('mediaproxy') // eslint-disable-line - .regExp(reInline.link, (state, match) => { - if (match[0].charAt(0) !== '!') { - return null; - } - - return { - data: Map({ - alt: match[1], - src: match[2], - title: match[3], - }).filter(Boolean), - }; - }); - - const myCustomSchema = { - mediaproxy: ({ token }) => { //eslint-disable-line - const src = token.getIn(['data', 'src']); - const alt = token.getIn(['data', 'alt']); - return {alt}; - }, - }; - - const myMarkdownSyntax = markdownSyntax.addInlineRules(myRule); - const value = ` -## Title - -![mediaproxy test](http://url.to.image) -`; - const component = shallow( - - ); - expect(component.html()).toMatchSnapshot(); - }); - }); - - describe('HTML rendering', () => { - it('should render HTML', () => { - const value = '

Paragraph with inline element

'; - const component = shallow( - - ); + describe("HTML rendering", () => { + it("should render HTML", () => { + const value = "

Paragraph with inline element

"; + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); diff --git a/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap b/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap index 1495c18c..b51ff97e 100644 --- a/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap +++ b/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap @@ -1,15 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MarkitupReactRenderer HTML rendering should render HTML 1`] = `"

Paragraph with inline element

"`; +exports[`MarkitupReactRenderer HTML rendering should render HTML 1`] = `"

Paragraph with inline element

"`; -exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = `"

Use the printf() function.

"`; +exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = `"

Use the printf() function.

"`; -exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; +exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; -exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\\"alt

H6
"`; +exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = ` +"

H1

+

Text with bold & em elements

+

H2

+
    +
  • ul item 1
  • +
  • ul item 2
  • +
+

H3

+
    +
  1. ol item 1
  2. +
  3. ol item 2
  4. +
  5. ol item 3
  6. +
+

H4

+

link title

+
H5
+

\\"alt

+
H6
" +`; exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is when using Markdown 1`] = ` -"

Title

+"

Title

+ @@ -18,25 +38,41 @@ exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is
Testing HTML in Markdown
- -

Test

-
" +

Test

" `; -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 2 1`] = `"

Title

"`; +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 2 1`] = `"

Title

"`; -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 3 1`] = `"

Title

"`; +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 3 1`] = `"

Title

"`; -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 4 1`] = `"

Title

"`; +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 4 1`] = `"

Title

"`; -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 5 1`] = `"
Title
"`; +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 5 1`] = `"
Title
"`; -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; -exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; +exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `"

I get 10 times more traffic from Google than from Yahoo or MSN.

"`; -exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; - -exports[`MarkitupReactRenderer custom elements should extend default renderers with custom ones 1`] = `"

Title

\\"mediaproxy

"`; +exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = ` +"
    +
  1. ol item 1
  2. +
  3. ol item 2
  4. +
+
    +
  • Sublist 1
  • +
  • Sublist 2
  • +
  • +

    Sublist 3

    +
      +
    1. Sub-Sublist 1
    2. +
    3. Sub-Sublist 2
    4. +
    5. Sub-Sublist 3
    6. +
    +
  • +
+
    +
  1. ol item 3
  2. +
" +`; diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index 79a2bc98..03d46637 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -1,130 +1,35 @@ -import React, { PropTypes } from 'react'; -import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; -import { omit } from 'lodash'; -import registry from '../../lib/registry'; +import React, { PropTypes } from "react"; +import Remark from "remark"; +import toHAST from "mdast-util-to-hast"; +import hastToHTML from "hast-util-to-html"; +import registry from "../../lib/registry"; -const defaultSchema = { - [BLOCKS.DOCUMENT]: 'article', - [BLOCKS.TEXT]: null, - [BLOCKS.CODE]: ({ token }) => { - const className = token.getIn(['data', 'syntax']) && `language-${ token.getIn(['data', 'syntax']) }`; - return
 token.text).join('') }} />
; - }, - [BLOCKS.BLOCKQUOTE]: 'blockquote', - [BLOCKS.PARAGRAPH]: 'p', - [BLOCKS.FOOTNOTE]: 'footnote', - [BLOCKS.HTML]: ({ token }) =>
, - [BLOCKS.HR]: 'hr', - [BLOCKS.HEADING_1]: 'h1', - [BLOCKS.HEADING_2]: 'h2', - [BLOCKS.HEADING_3]: 'h3', - [BLOCKS.HEADING_4]: 'h4', - [BLOCKS.HEADING_5]: 'h5', - [BLOCKS.HEADING_6]: 'h6', - [BLOCKS.TABLE]: 'table', - [BLOCKS.TABLE_ROW]: 'tr', - [BLOCKS.TABLE_CELL]: 'td', - [BLOCKS.OL_LIST]: 'ol', - [BLOCKS.UL_LIST]: 'ul', - [BLOCKS.LIST_ITEM]: 'li', - - [STYLES.TEXT]: null, - [STYLES.BOLD]: 'strong', - [STYLES.ITALIC]: 'em', - [STYLES.CODE]: 'code', - [STYLES.STRIKETHROUGH]: 'del', - - [ENTITIES.LINK]: 'a', - [ENTITIES.IMAGE]: 'img', - [ENTITIES.FOOTNOTE_REF]: 'sup', - [ENTITIES.HARD_BREAK]: 'br', -}; - -const notAllowedAttributes = ['loose', 'image']; +// Setup Remark. +const remark = new Remark({ + commonmark: true, + footnotes: true, + pedantic: true, +}); export default class MarkupItReactRenderer extends React.Component { - constructor(props) { super(props); - const { syntax } = props; - this.parser = new MarkupIt(syntax); this.plugins = {}; + // TODO add back support for this. registry.getEditorComponents().forEach((component) => { - this.plugins[component.get('id')] = component; + this.plugins[component.get("id")] = component; }); } - componentWillReceiveProps(nextProps) { - if (nextProps.syntax != this.props.syntax) { - this.parser = new MarkupIt(nextProps.syntax); - } - } - - sanitizeProps(props) { - const { getAsset } = this.props; - - if (props.image) { - props = Object.assign({}, props, { src: getAsset(props.image).toString() }); - } - - return omit(props, notAllowedAttributes); - } - - - renderToken(schema, token, index = 0, key = '0') { - const type = token.get('type'); - const data = token.get('data'); - const text = token.get('text'); - const tokens = token.get('tokens'); - const nodeType = schema[type]; - key = `${ key }.${ index }`; - - // Only render if type is registered as renderer - if (typeof nodeType !== 'undefined') { - let children = null; - if (tokens.size) { - children = tokens.map((token, idx) => this.renderToken(schema, token, idx, key)); - } else if (type === 'text') { - children = text; - } - if (nodeType !== null) { - let props = { key, token }; - if (typeof nodeType !== 'function') { - props = { key, ...this.sanitizeProps(data.toJS()) }; - } - // If this is a react element - return React.createElement(nodeType, props, children); - } else { - // If this is a text node - return children; - } - } - - const plugin = this.plugins[token.get('type')]; - if (plugin) { - const output = plugin.toPreview(token.get('data').toJS()); - return typeof output === 'string' ? - : - output; - } - - return null; - } - - render() { - const { value, schema, getAsset } = this.props; - const content = this.parser.toContent(value); - return this.renderToken({ ...defaultSchema, ...schema }, content.get('token')); + const { value } = this.props; + const mast = remark.parse(value); + const hast = toHAST(mast, { allowDangerousHTML: true }); + const html = hastToHTML(hast, { allowDangerousHTML: true }); + return
; // eslint-disable-line react/no-danger } } MarkupItReactRenderer.propTypes = { value: PropTypes.string, - syntax: PropTypes.instanceOf(Syntax).isRequired, - schema: PropTypes.objectOf(PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ])), - getAsset: PropTypes.func.isRequired, };