From f3b7dc9e2e15f3e96a4d458d16ae8abafbefcaba Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 1 Mar 2017 11:07:49 -0800 Subject: [PATCH 01/79] Update Jest to 0.19 --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 667cb132..29506cf1 100644 --- a/package.json +++ b/package.json @@ -51,38 +51,38 @@ "devDependencies": { "babel": "^6.5.2", "babel-cli": "^6.18.0", - "babel-core": "^6.5.1", + "babel-core": "^6.23.1", "babel-jest": "^20.0.3", "babel-loader": "^7.0.0", "babel-plugin-lodash": "^3.2.0", - "babel-preset-es2015": "^6.5.0", - "babel-preset-react": "^6.5.0", - "babel-preset-stage-1": "^6.16.0", - "babel-runtime": "^6.5.0", + "babel-preset-es2015": "^6.22.0", + "babel-preset-react": "^6.23.0", + "babel-preset-stage-1": "^6.22.0", + "babel-runtime": "^6.23.0", "cross-env": "^5.0.2", "css-loader": "^0.28.4", "enzyme": "^2.4.1", "eslint": "^3.7.1", "eslint-config-netlify": "github:netlify/eslint-config-netlify", "eslint-import-resolver-webpack": "^0.8.3", - "exports-loader": "^0.6.3", + "exports-loader": "^0.6.4", "extract-text-webpack-plugin": "^2.1.2", "file-loader": "^0.11.2", "identity-obj-proxy": "^3.0.0", "imports-loader": "^0.7.1", "jest": "^20.0.4", "jest-cli": "^20.0.4", - "lint-staged": "^3.1.0", + "lint-staged": "^3.3.1", "node-sass": "^3.10.0", "npm-check": "^5.2.3", "postcss-cssnext": "^2.7.0", "postcss-import": "^10.0.0", "postcss-loader": "^2.0.5", - "react-addons-test-utils": "^15.3.2", + "react-addons-test-utils": "^15.4.2", "sass-loader": "^6.0.5", "style-loader": "^0.18.2", "stylefmt": "^4.3.1", - "stylelint": "^7.3.1", + "stylelint": "^7.9.0", "stylelint-config-css-modules": "^0.1.0", "stylelint-config-standard": "^13.0.2", "stylelint-declaration-block-order": "^0.1.0", From 24c0a1bdb471d1cf23279ddd0eb3e1863903877f Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 1 Mar 2017 15:58:12 -0800 Subject: [PATCH 02/79] 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, }; From 0eb109cb738d7534407268d3be2913f074d75b93 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 2 Mar 2017 12:02:55 -0800 Subject: [PATCH 03/79] Convert markdown-prosemirror parser/compiler to Remark --- package.json | 1 + src/components/MarkupItReactRenderer/index.js | 4 +- .../__snapshots__/parser.spec.js.snap | 324 ++++++++++++++++++ .../VisualEditor/__tests__/parser.spec.js | 91 +++++ .../VisualEditor/index.js | 190 ++++++---- .../VisualEditor/parser.js | 306 ++++------------- 6 files changed, 603 insertions(+), 313 deletions(-) create mode 100644 src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap create mode 100644 src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js diff --git a/package.json b/package.json index 0cd64b56..87016e79 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "slate-drop-or-paste-images": "^0.2.0", "slug": "^0.9.1", "textarea-caret-position": "^0.1.1", + "unist-util-visit": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" }, diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index 03d46637..462170c5 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -23,8 +23,8 @@ export default class MarkupItReactRenderer extends React.Component { render() { const { value } = this.props; - const mast = remark.parse(value); - const hast = toHAST(mast, { allowDangerousHTML: true }); + const mdast = remark.parse(value); + const hast = toHAST(mdast, { allowDangerousHTML: true }); const html = hastToHTML(hast, { allowDangerousHTML: true }); return
; // eslint-disable-line react/no-danger } diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap new file mode 100644 index 00000000..e434e6da --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap @@ -0,0 +1,324 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Compile markdown to Prosemirror document structure should compile a markdown ordered list 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "H1", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "order": 1, + "tight": true, + }, + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "yo", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "bro", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "fro", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + ], + "type": "ordered_list", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile bulleted lists 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "H1", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "tight": false, + }, + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "yo", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "bro", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "fro", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + ], + "type": "bullet_list", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile hard breaks (double space) 1`] = ` +Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "blue moon", + "type": "text", + }, + Object { + "type": "hard_break", + }, + Object { + "text": "footballs", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile horizontal rules 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "H1", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "type": "horizontal_rule", + }, + Object { + "content": Array [ + Object { + "text": "blue moon", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile horizontal rules 2`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "H1", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "type": "horizontal_rule", + }, + Object { + "content": Array [ + Object { + "text": "blue moon", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile images 1`] = ` +Object { + "content": Array [ + Object { + "content": Array [ + Object { + "attrs": Object { + "alt": "super", + "src": "duper.jpg", + "title": null, + }, + "type": "image", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile multiple header levels 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "H1", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "H2", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "H3", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile simple markdown 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "H1", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "sweet body", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js new file mode 100644 index 00000000..cd188b8e --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js @@ -0,0 +1,91 @@ +import { Schema } from "prosemirror-model"; +import { schema } from "prosemirror-markdown"; + +const makeParser = require("../parser"); + +const testSchema = new Schema({ + nodes: schema.nodeSpec, + marks: schema.markSpec, +}); +const parser = makeParser(testSchema); + +describe("Compile markdown to Prosemirror document structure", () => { + it("should compile simple markdown", () => { + const value = ` +# H1 + +sweet body +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile a markdown ordered list", () => { + const value = ` +# H1 + +1. yo +2. bro +3. fro +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile bulleted lists", () => { + const value = ` +# H1 + +* yo +* bro +* fro +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile multiple header levels", () => { + const value = ` +# H1 + +## H2 + +### H3 +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile horizontal rules", () => { + const value = ` +# H1 + +--- + +blue moon +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile horizontal rules", () => { + const value = ` +# H1 + +--- + +blue moon +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile hard breaks (double space)", () => { + const value = ` +blue moon +footballs +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile images", () => { + const value = ` +![super](duper.jpg) +`; + expect(parser(value)).toMatchSnapshot(); + }); +}); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index a00620ca..775045c6 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -5,8 +5,13 @@ import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import history from 'prosemirror-history'; import { - blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule, - inputRules, allInputRules, + blockQuoteRule, + orderedListRule, + bulletListRule, + codeBlockRule, + headingRule, + inputRules, + allInputRules, } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; @@ -56,20 +61,26 @@ function schemaWithPlugins(schema, plugins) { let nodeSpec = schema.nodeSpec; plugins.forEach((plugin) => { const attrs = {}; - plugin.get('fields').forEach((field) => { - attrs[field.get('name')] = { default: null }; + plugin.get("fields").forEach((field) => { + attrs[field.get("name")] = { default: null }; }); - nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, { + nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get("id") }`, { attrs, - group: 'block', - parseDOM: [{ - tag: 'div[data-plugin]', - getAttrs(dom) { - return JSON.parse(dom.getAttribute('data-plugin')); + group: "block", + parseDOM: [ + { + tag: "div[data-plugin]", + getAttrs(dom) { + return JSON.parse(dom.getAttribute("data-plugin")); + }, }, - }], + ], toDOM(node) { - return ['div', { 'data-plugin': JSON.stringify(node.attrs) }, plugin.get('label')]; + return [ + "div", + { "data-plugin": JSON.stringify(node.attrs) }, + plugin.get("label"), + ]; }, }); }); @@ -83,8 +94,8 @@ function schemaWithPlugins(schema, plugins) { function createSerializer(schema, plugins) { const serializer = Object.create(defaultMarkdownSerializer); plugins.forEach((plugin) => { - serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => { - const toBlock = plugin.get('toBlock'); + serializer.nodes[`plugin_${ plugin.get("id") }`] = (state, node) => { + const toBlock = plugin.get("toBlock"); state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`); }; }); @@ -159,17 +170,31 @@ export default class Editor extends Component { const { schema, selection } = state; if (selection.from === selection.to) { const { $from } = selection; - if ($from.parent && $from.parent.type === schema.nodes.paragraph && $from.parent.textContent === '') { + if ( + $from.parent && + $from.parent.type === schema.nodes.paragraph && + $from.parent.textContent === "" + ) { const pos = this.view.coordsAtPos(selection.from); const editorPos = this.view.content.getBoundingClientRect(); - const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; + const selectionPosition = { + top: pos.top - editorPos.top, + left: pos.left - editorPos.left, + }; this.setState({ selectionPosition }); + } else { + this.setState({ showToolbar: false, showBlockMenu: false }); } } else { const pos = this.view.coordsAtPos(selection.from); const editorPos = this.view.content.getBoundingClientRect(); const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; this.setState({ selectionPosition }); + const selectionPosition = { + top: pos.top - editorPos.top, + left: pos.left - editorPos.left, + }; + this.setState({ selectionPosition }); } }; @@ -177,26 +202,24 @@ export default class Editor extends Component { this.ref = ref; }; - handleHeader = level => ( - () => { - const { schema } = this.state; - const state = this.view.state; - const { $from, to, node } = state.selection; - let nodeType = schema.nodes.heading; - let attrs = { level }; - let inHeader = node && node.hasMarkup(nodeType, attrs); - if (!inHeader) { - inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs); - } - if (inHeader) { - nodeType = schema.nodes.paragraph; - attrs = {}; - } - - const command = setBlockType(nodeType, { level }); - command(state, this.handleAction); + handleHeader = level => () => { + const { schema } = this.state; + const state = this.view.state; + const { $from, to, node } = state.selection; + let nodeType = schema.nodes.heading; + let attrs = { level }; + let inHeader = node && node.hasMarkup(nodeType, attrs); + if (!inHeader) { + inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs); } - ); + if (inHeader) { + nodeType = schema.nodes.paragraph; + attrs = {}; + } + + const command = setBlockType(nodeType, { level }); + command(state, this.handleAction); + }; handleBold = () => { const command = toggleMark(this.state.schema.marks.strong); @@ -213,14 +236,20 @@ export default class Editor extends Component { if (!markActive(this.view.state, this.state.schema.marks.link)) { url = prompt('Link URL:'); // eslint-disable-line no-alert } - const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null }); + const command = toggleMark(this.state.schema.marks.link, { + href: url ? processUrl(url) : null, + }); command(this.view.state, this.handleAction); }; handlePluginSubmit = (plugin, data) => { const { schema } = this.state; - const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; - this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); + const nodeType = schema.nodes[`plugin_${ plugin.get("id") }`]; + this.view.props.onAction( + this.view.state.tr + .replaceSelectionWith(nodeType.create(data.toJS())) + .action() + ); }; handleDragEnter = (e) => { @@ -248,31 +277,40 @@ export default class Editor extends Component { if (e.dataTransfer.files && e.dataTransfer.files.length) { Array.from(e.dataTransfer.files).forEach((file) => { - createAssetProxy(file.name, file) - .then((assetProxy) => { + createAssetProxy(file.name, file).then((assetProxy) => { this.props.onAddAsset(assetProxy); - if (file.type.split('/')[0] === 'image') { + if (file.type.split("/")[0] === "image") { nodes.push( - schema.nodes.image.create({ src: assetProxy.public_path, alt: file.name }) + schema.nodes.image.create({ + src: assetProxy.public_path, + alt: file.name, + }) ); } else { nodes.push( - schema.marks.link.create({ href: assetProxy.public_path, title: file.name }) + schema.marks.link.create({ + href: assetProxy.public_path, + title: file.name, + }) ); } }); }); } else { - nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain'))); + nodes.push( + schema.nodes.paragraph.create({}, e.dataTransfer.getData("text/plain")) + ); } nodes.forEach((node) => { - this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action()); + this.view.props.onAction( + this.view.state.tr.replaceSelectionWith(node).action() + ); }); }; handleToggle = () => { - this.props.onMode('raw'); + this.props.onMode("raw"); }; render() { @@ -283,36 +321,38 @@ export default class Editor extends Component { classNames.push(styles.dragging); } - return (
- - - -
-
-
); + + + +
+
+
+ ); } } diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index 6234056f..5dd54dac 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -2,256 +2,90 @@ /* Based closely on https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js - - Adds a bit of logic allowing editor plugins to hook into the parsing. */ -const markdownit = require("markdown-it") +import Remark from "remark"; +const visit = require('unist-util-visit') const {Mark} = require("prosemirror-model") -function maybeMerge(a, b) { - if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks)) - return a.copy(a.text + b.text) -} +let schema -function pluginHandler(schema, plugins) { - return (type, attrs, content) => { - if (type.name === 'paragraph' && content.length === 1 && content[0].type.name === 'text') { - const text = content[0].text; - const plugin = plugins.find(plugin => plugin.get('pattern').test(text)); - if (plugin) { - const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; - const data = plugin.get('fromBlock').call(plugin, text.match(plugin.get('pattern'))); - return nodeType.create(data); - } - } - return null; - }; -} +// Setup Remark. +const remark = new Remark({ + commonmark: true, + footnotes: true, + pedantic: true, +}); -// Object used to track the context of a running parse. -class MarkdownParseState { - constructor(schema, plugins, tokenHandlers) { - this.schema = schema - this.stack = [{type: schema.nodes.doc, content: []}] - this.marks = Mark.none - this.tokenHandlers = tokenHandlers - this.pluginHandler = pluginHandler(schema, plugins); +const processMdastNode = (node) => { + console.log('processMdastNode', node) + if (node.type === 'root') { + const content = node.children.map((childNode) => ( + processMdastNode(childNode) + )) + return schema.node('doc', {}, content) } - top() { - return this.stack[this.stack.length - 1] - } - - push(elt) { - if (this.stack.length) this.top().content.push(elt) - } - - // : (string) - // Adds the given text to the current position in the document, - // using the current marks as styling. - addText(text) { - if (!text) return - let nodes = this.top().content, last = nodes[nodes.length - 1] - let node = this.schema.text(text, this.marks), merged - if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged - else nodes.push(node) - } - - // : (Mark) - // Adds the given mark to the set of active marks. - openMark(mark) { - this.marks = mark.addToSet(this.marks) - } - - // : (Mark) - // Removes the given mark from the set of active marks. - closeMark(mark) { - this.marks = mark.removeFromSet(this.marks) - } - - parseTokens(toks) { - for (let i = 0; i < toks.length; i++) { - let tok = toks[i] - let handler = this.tokenHandlers[tok.type] - if (!handler) - throw new Error("Token type `" + tok.type + "` not supported by Markdown parser") - handler(this, tok) - } - } - - // : (NodeType, ?Object, ?[Node]) → ?Node - // Add a node at the current position. - addNode(type, attrs, content) { - const node = this.pluginHandler(type, attrs, content) || type.createAndFill(attrs, content, this.marks); - if (!node) return null - this.push(node) - return node - } - - // : (NodeType, ?Object) - // Wrap subsequent content in a node of the given type. - openNode(type, attrs) { - this.stack.push({type: type, attrs: attrs, content: []}) - } - - // : () → ?Node - // Close and return the node that is currently on top of the stack. - closeNode() { - if (this.marks.length) this.marks = Mark.none - let info = this.stack.pop() - return this.addNode(info.type, info.attrs, info.content) - } -} - -function attrs(given, token) { - return given instanceof Function ? given(token) : given -} - -// Code content is represented as a single token with a `content` -// property in Markdown-it. -function noOpenClose(type) { - return type == "code_inline" || type == "code_block" || type == "fence" -} - -function withoutTrailingNewline(str) { - return str[str.length - 1] == "\n" ? str.slice(0, str.length - 1) : str -} - -function tokenHandlers(schema, tokens) { - let handlers = Object.create(null) - for (let type in tokens) { - let spec = tokens[type] - if (spec.block) { - let nodeType =schema.nodeType(spec.block); - if (noOpenClose(type)) { - handlers[type] = (state, tok) => { - state.openNode(nodeType, attrs(spec.attrs, tok)) - state.addText(withoutTrailingNewline(tok.content)) - state.closeNode() - } - } else { - handlers[type + "_open"] = (state, tok) => state.openNode(nodeType, attrs(spec.attrs, tok)) - handlers[type + "_close"] = state => state.closeNode() - } - } else if (spec.node) { - let nodeType = schema.nodeType(spec.node) - handlers[type] = (state, tok) => state.addNode(nodeType, attrs(spec.attrs, tok)) - } else if (spec.mark) { - let markType = schema.marks[spec.mark] - if (noOpenClose(type)) { - handlers[type] = (state, tok) => { - state.openMark(markType.create(attrs(spec.attrs, tok))) - state.addText(withoutTrailingNewline(tok.content)) - state.closeMark(markType) - } - } else { - handlers[type + "_open"] = (state, tok) => state.openMark(markType.create(attrs(spec.attrs, tok))) - handlers[type + "_close"] = state => state.closeMark(markType) - } + /*** + * Block nodes + ***/ + if (node.type === 'heading') { + const content = node.children.map((childNode) => ( + processMdastNode(childNode) + )) + console.log(content) + return schema.node('heading', { level: node.depth }, content) + } else if (node.type === 'paragraph') { + const content = node.children.map((childNode) => ( + processMdastNode(childNode) + )) + return schema.node('paragraph', {}, content) + } else if (node.type === 'list') { + const content = node.children.map((childNode) => ( + processMdastNode(childNode) + )) + if (node.ordered) { + return schema.node('ordered_list', { tight: true, order: 1 }, content) } else { - throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec)) + return schema.node('bullet_list', {}, content) } + } else if (node.type === 'listItem') { + const content = node.children.map((childNode) => ( + processMdastNode(childNode) + )) + return schema.node('list_item', {}, content) + } else if (node.type === 'thematicBreak') { + return schema.node('horizontal_rule') + } else if (node.type === 'break') { + return schema.node('hard_break') + } else if (node.type === 'image') { + return schema.node('image', { src: node.url, alt: node.alt }) + } + /*** + * end block items + ***/ + + // Inline + if (node.type === 'text') { + console.log('text value', node.value) + return schema.text(node.value) } - handlers.text = (state, tok) => state.addText(tok.content) - handlers.inline = (state, tok) => state.parseTokens(tok.children) - handlers.softbreak = state => state.addText("\n") - - return handlers + return doc } -// ;; A configuration of a Markdown parser. Such a parser uses -// [markdown-it](https://github.com/markdown-it/markdown-it) to -// tokenize a file, and then runs the custom rules it is given over -// the tokens to create a ProseMirror document tree. -class MarkdownParser { - // :: (Schema, MarkdownIt, Object) - // Create a parser with the given configuration. You can configure - // the markdown-it parser to parse the dialect you want, and provide - // a description of the ProseMirror entities those tokens map to in - // the `tokens` object, which maps token names to descriptions of - // what to do with them. Such a description is an object, and may - // have the following properties: - // - // **`node`**`: ?string` - // : This token maps to a single node, whose type can be looked up - // in the schema under the given name. Exactly one of `node`, - // `block`, or `mark` must be set. - // - // **`block`**`: ?string` - // : This token comes in `_open` and `_close` variants (which are - // appended to the base token name provides a the object - // property), and wraps a block of content. The block should be - // wrapped in a node of the type named to by the property's - // value. - // - // **`mark`**`: ?string` - // : This token also comes in `_open` and `_close` variants, but - // should add a mark (named by the value) to its content, rather - // than wrapping it in a node. - // - // **`attrs`**`: ?union` - // : If the mark or node to be created needs attributes, they can - // be either given directly, or as a function that takes a - // [markdown-it - // token](https://markdown-it.github.io/markdown-it/#Token) and - // returns an attribute object. - constructor(schema, plugins, tokenizer, tokens) { - // :: Object The value of the `tokens` object used to construct - // this parser. Can be useful to copy and modify to base other - // parsers on. - this.tokens = tokens - this.schema = schema - this.tokenizer = tokenizer - this.plugins = plugins - this.tokenHandlers = tokenHandlers(schema, tokens) - } - - // :: (string) → Node - // Parse a string as [CommonMark](http://commonmark.org/) markup, - // and create a ProseMirror document as prescribed by this parser's - // rules. - parse(text) { - let state = new MarkdownParseState(this.schema, this.plugins, this.tokenHandlers), doc - state.parseTokens(this.tokenizer.parse(text, {})) - do { doc = state.closeNode() } while (state.stack.length) - return doc - } +const compileMarkdownToProseMirror = (src) => { + console.log(src) + const mdast = remark.parse(src) + console.log(mdast) + const doc = processMdastNode(mdast) + console.log(doc.content) + return doc } -// :: MarkdownParser -// A parser parsing unextended [CommonMark](http://commonmark.org/), -// without inline HTML, and producing a document in the basic schema. -export default function createMarkdownParser(schema, plugins) { - const tokens = { - blockquote: {block: "blockquote"}, - paragraph: {block: "paragraph"}, - list_item: {block: "list_item"}, - // Note - we force lists to be tight here, while that's not ProseMirror's default - // The default behavior means list elements always have a `p` inside, and we want - // to avoid tha. - bullet_list: {block: "bullet_list", attrs: tok => ({tight: true})}, - ordered_list: {block: "ordered_list", attrs: tok => ({tight: true, order: +tok.attrGet("order") || 1})}, - heading: {block: "heading", attrs: tok => ({level: +tok.tag.slice(1)})}, - code_block: {block: "code_block"}, - fence: {block: "code_block"}, - hr: {node: "horizontal_rule"}, - image: {node: "image", attrs: tok => ({ - src: tok.attrGet("src"), - title: tok.attrGet("title") || null, - alt: tok.children[0] && tok.children[0].content || null - })}, - hardbreak: {node: "hard_break"}, - - em: {mark: "em"}, - strong: {mark: "strong"}, - link: {mark: "link", attrs: tok => ({ - href: tok.attrGet("href"), - title: tok.attrGet("title") || null - })}, - code_inline: {mark: "code"} - }; - - return new MarkdownParser(schema, plugins, markdownit("commonmark", {html: false}), tokens); +module.exports = (s, plugins) => { + //console.log(s) + //console.log(s.nodes.code_block.create({ params: { language: 'javascript' } })) + schema = s + return compileMarkdownToProseMirror } From 8763666570a184759cdb2525bd0ed13c16fe25ad Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sat, 18 Mar 2017 14:13:22 -0700 Subject: [PATCH 04/79] Update parser to support remaining node types + add inline styled text support --- .../__tests__/MarkupItReactRenderer.spec.js | 48 +- .../__snapshots__/parser.spec.js.snap | 811 +++++++++++++++++- .../VisualEditor/__tests__/parser.spec.js | 132 +++ .../VisualEditor/index.js | 53 +- .../VisualEditor/parser.js | 148 ++-- 5 files changed, 1072 insertions(+), 120 deletions(-) diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js index a86d4f76..5b72258a 100644 --- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js @@ -1,14 +1,14 @@ /* eslint max-len:0 */ -import React from "react"; -import { shallow } from "enzyme"; -import { padStart } from "lodash"; -import MarkupItReactRenderer from "../"; +import React from 'react'; +import { shallow } from 'enzyme'; +import { padStart } from 'lodash'; +import MarkupItReactRenderer from '../'; -describe("MarkitupReactRenderer", () => { - describe("Markdown rendering", () => { - describe("General", () => { - it("should render markdown", () => { +describe('MarkitupReactRenderer', () => { + describe('Markdown rendering', () => { + describe('General', () => { + it('should render markdown', () => { const value = ` # H1 @@ -40,18 +40,18 @@ Text with **bold** & _em_ elements }); }); - describe("Headings", () => { + describe('Headings', () => { for (const heading of [...Array(6).keys()]) { it(`should render Heading ${ heading + 1 }`, () => { - const value = padStart(" Title", heading + 7, "#"); + 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 @@ -68,8 +68,8 @@ Text with **bold** & _em_ elements }); }); - 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]. @@ -82,22 +82,22 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] }); }); - describe("Code", () => { - it("should render code", () => { - const value = "Use the `printf()` function."; + 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.``"; + 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 @@ -119,9 +119,9 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] }); }); - describe("HTML rendering", () => { - it("should render HTML", () => { - const value = "

Paragraph with inline element

"; + 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/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap index e434e6da..4c22ae72 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap @@ -88,7 +88,7 @@ Object { }, Object { "attrs": Object { - "tight": false, + "tight": true, }, "content": Array [ Object { @@ -141,20 +141,33 @@ Object { } `; +exports[`Compile markdown to Prosemirror document structure should compile code blocks 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "params": "javascript", + }, + "content": Array [ + Object { + "text": "var a = 1;", + "type": "text", + }, + ], + "type": "code_block", + }, + ], + "type": "doc", +} +`; + exports[`Compile markdown to Prosemirror document structure should compile hard breaks (double space) 1`] = ` Object { "content": Array [ Object { "content": Array [ Object { - "text": "blue moon", - "type": "text", - }, - Object { - "type": "hard_break", - }, - Object { - "text": "footballs", + "text": "blue moonfootballs", "type": "text", }, ], @@ -233,14 +246,701 @@ exports[`Compile markdown to Prosemirror document structure should compile image Object { "content": Array [ Object { + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile inline code 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, "content": Array [ Object { - "attrs": Object { - "alt": "super", - "src": "duper.jpg", - "title": null, - }, - "type": "image", + "text": "Word", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "This is some sweet ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", + "type": "text", + }, + Object { + "text": " yo!", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile kitchen sink example 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "An exhibit of Markdown", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "This note demonstrates some of what Markdown is capable of doing.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "content": Array [ + Object { + "marks": Array [ + Object { + "type": "em", + }, + ], + "text": "Note: Feel free to play with this page. Unlike regular notes, this doesn't +automatically save itself.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Basic formatting", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "Paragraphs can be written like so. A paragraph is the basic block of Markdown. +A paragraph is what text will turn into when there is no reason it should +become anything else.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "content": Array [ + Object { + "text": "Paragraphs must be separated by a blank line. Basic formatting of ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "em", + }, + ], + "text": "italics", + "type": "text", + }, + Object { + "text": " and +", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "bold", + "type": "text", + }, + Object { + "text": " is supported. This ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "em", + }, + ], + "text": "can be ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "em", + }, + Object { + "type": "strong", + }, + ], + "text": "nested", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "em", + }, + ], + "text": " like", + "type": "text", + }, + Object { + "text": " so.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Lists", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "Ordered list", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "order": 1, + "tight": true, + }, + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "Item 1 2. A second item 3. Number 3 4. Ⅳ", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + ], + "type": "ordered_list", + }, + Object { + "content": Array [ + Object { + "marks": Array [ + Object { + "type": "em", + }, + ], + "text": "Note: the fourth item uses the Unicode character for Roman numeral four.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "Unordered list", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "tight": true, + }, + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "An item Another item Yet another item And there's more...", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + ], + "type": "bullet_list", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Paragraph modifiers", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "Code block", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "params": "", + }, + "content": Array [ + Object { + "text": "Code blocks are very useful for developers and other people who look at +code or other things that are written in plain text. As you can see, it +uses a fixed-width font.", + "type": "text", + }, + ], + "type": "code_block", + }, + Object { + "content": Array [ + Object { + "text": "You can also make ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", + "type": "text", + }, + Object { + "text": " to add code into other things.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "Quote", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "Here is a quote. What this is should be self explanatory. Quotes are +automatically indented when they are used.", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockquote", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Headings", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "There are six levels of headings. They correspond with the six levels of HTML +headings. You've probably noticed them already in the page. Each level down +uses one more hash character.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "Headings ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "em", + }, + ], + "text": "can", + "type": "text", + }, + Object { + "text": " also contain ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "formatting", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "attrs": Object { + "level": 3, + }, + "content": Array [ + Object { + "text": "They can even contain ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "Of course, demonstrating what headings look like messes up the structure of the +page.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "content": Array [ + Object { + "text": "I don't recommend using more than three or four levels of headings here, +because, when you're smallest heading isn't too small, and you're largest +heading isn't too big, and you want each size up to look noticeably larger and +more important, there there are only so many sizes that you can use.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "URLs", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "URLs can be made in a handful of ways:", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "tight": true, + }, + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "A named link to MarkItDown. The easiest way to do these is to select what you", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "text": "want to make a link and hit ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "Ctrl+L", + "type": "text", + }, + Object { + "text": ". Another named link to", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "MarkItDown", + "type": "text", + }, + Object { + "text": " Sometimes you just want a URL like", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "http://www.markitdown.net/", + "type": "text", + }, + Object { + "text": ".", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "list_item", + }, + ], + "type": "bullet_list", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Horizontal rule", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "A horizontal rule is a line that goes across the middle of the page.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "type": "horizontal_rule", + }, + Object { + "content": Array [ + Object { + "text": "It's sometimes handy for breaking things up.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Images", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "Markdown can also contain images. I'll need to add something here sometime.", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "attrs": Object { + "level": 2, + }, + "content": Array [ + Object { + "text": "Finally", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "There's actually a lot more to Markdown than this. See the official +introduction and syntax for more information. However, be aware that this is +not using the official implementation, and this might work subtly differently + in some of the little things.", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + +exports[`Compile markdown to Prosemirror document structure should compile links 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "Word", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "How far is it to ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "Google", + "type": "text", + }, + Object { + "text": " land?", + "type": "text", }, ], "type": "paragraph", @@ -294,6 +994,87 @@ Object { } `; +exports[`Compile markdown to Prosemirror document structure should compile nested inline markup 1`] = ` +Object { + "content": Array [ + Object { + "attrs": Object { + "level": 1, + }, + "content": Array [ + Object { + "text": "Word", + "type": "text", + }, + ], + "type": "heading", + }, + Object { + "content": Array [ + Object { + "text": "This is ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "some ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "em", + }, + Object { + "type": "strong", + }, + ], + "text": "hot", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": " content", + "type": "text", + }, + ], + "type": "paragraph", + }, + Object { + "content": Array [ + Object { + "text": "perhaps ", + "type": "text", + }, + Object { + "marks": Array [ + Object { + "type": "strong", + }, + ], + "text": "scalding", + "type": "text", + }, + Object { + "text": " even", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + exports[`Compile markdown to Prosemirror document structure should compile simple markdown 1`] = ` Object { "content": Array [ diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js index cd188b8e..07972fa1 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js @@ -85,6 +85,138 @@ footballs it("should compile images", () => { const value = ` ![super](duper.jpg) +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile code blocks", () => { + const value = ` +\`\`\`javascript +var a = 1; +\`\`\` +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile nested inline markup", () => { + const value = ` +# Word + +This is **some *hot* content** + +perhaps **scalding** even +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile inline code", () => { + const value = ` +# Word + +This is some sweet \`inline code\` yo! +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile links", () => { + const value = ` +# Word + +How far is it to [Google](https://google.com) land? +`; + expect(parser(value)).toMatchSnapshot(); + }); + + it("should compile kitchen sink example", () => { + const value = ` +# An exhibit of Markdown + +This note demonstrates some of what Markdown is capable of doing. + +*Note: Feel free to play with this page. Unlike regular notes, this doesn't +automatically save itself.* + +## Basic formatting + +Paragraphs can be written like so. A paragraph is the basic block of Markdown. +A paragraph is what text will turn into when there is no reason it should +become anything else. + +Paragraphs must be separated by a blank line. Basic formatting of *italics* and +**bold** is supported. This *can be **nested** like* so. + +## Lists + +### Ordered list + +1. Item 1 2. A second item 3. Number 3 4. Ⅳ + +*Note: the fourth item uses the Unicode character for Roman numeral four.* + +### Unordered list + +* An item Another item Yet another item And there's more... + +## Paragraph modifiers + +### Code block + + Code blocks are very useful for developers and other people who look at + code or other things that are written in plain text. As you can see, it + uses a fixed-width font. + +You can also make \`inline code\` to add code into other things. + +### Quote + +> Here is a quote. What this is should be self explanatory. Quotes are +automatically indented when they are used. + +## Headings + +There are six levels of headings. They correspond with the six levels of HTML +headings. You've probably noticed them already in the page. Each level down +uses one more hash character. + +### Headings *can* also contain **formatting** + +### They can even contain \`inline code\` + +Of course, demonstrating what headings look like messes up the structure of the +page. + +I don't recommend using more than three or four levels of headings here, +because, when you're smallest heading isn't too small, and you're largest +heading isn't too big, and you want each size up to look noticeably larger and +more important, there there are only so many sizes that you can use. + +## URLs + +URLs can be made in a handful of ways: + +* A named link to MarkItDown. The easiest way to do these is to select what you +* want to make a link and hit \`Ctrl+L\`. Another named link to +* [MarkItDown](http://www.markitdown.net/) Sometimes you just want a URL like +* . + +## Horizontal rule + +A horizontal rule is a line that goes across the middle of the page. + +--- + +It's sometimes handy for breaking things up. + +## Images + +Markdown can also contain images. I'll need to add something here sometime. + +## Finally + +There's actually a lot more to Markdown than this. See the official +introduction and syntax for more information. However, be aware that this is +not using the official implementation, and this might work subtly differently + in some of the little things. `; expect(parser(value)).toMatchSnapshot(); }); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index 775045c6..f8d295af 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -12,17 +12,17 @@ import { headingRule, inputRules, allInputRules, -} from 'prosemirror-inputrules'; -import { keymap } from 'prosemirror-keymap'; +} from "prosemirror-inputrules"; +import { keymap } from "prosemirror-keymap"; import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; -import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; -import registry from '../../../../lib/registry'; -import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; -import { buildKeymap } from './keymap'; -import createMarkdownParser from './parser'; -import Toolbar from '../Toolbar/Toolbar'; +import { baseKeymap, setBlockType, toggleMark } from "prosemirror-commands"; +import registry from "../../../../lib/registry"; +import { createAssetProxy } from "../../../../valueObjects/AssetProxy"; +import { buildKeymap } from "./keymap"; +import createMarkdownParser from "./parser"; +import Toolbar from "../Toolbar/Toolbar"; import { Sticky } from '../../../UI/Sticky/Sticky'; -import styles from './index.css'; +import styles from "./index.css"; function processUrl(url) { if (url.match(/^(https?:\/\/|mailto:|\/)/)) { @@ -202,24 +202,25 @@ export default class Editor extends Component { this.ref = ref; }; - handleHeader = level => () => { - const { schema } = this.state; - const state = this.view.state; - const { $from, to, node } = state.selection; - let nodeType = schema.nodes.heading; - let attrs = { level }; - let inHeader = node && node.hasMarkup(nodeType, attrs); - if (!inHeader) { - inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs); - } - if (inHeader) { - nodeType = schema.nodes.paragraph; - attrs = {}; - } + handleHeader = level => + () => { + const { schema } = this.state; + const state = this.view.state; + const { $from, to, node } = state.selection; + let nodeType = schema.nodes.heading; + let attrs = { level }; + let inHeader = node && node.hasMarkup(nodeType, attrs); + if (!inHeader) { + inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs); + } + if (inHeader) { + nodeType = schema.nodes.paragraph; + attrs = {}; + } - const command = setBlockType(nodeType, { level }); - command(state, this.handleAction); - }; + const command = setBlockType(nodeType, { level }); + command(state, this.handleAction); + }; handleBold = () => { const command = toggleMark(this.state.schema.marks.strong); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index 5dd54dac..fe015ff8 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -5,10 +5,12 @@ */ import Remark from "remark"; -const visit = require('unist-util-visit') -const {Mark} = require("prosemirror-model") +const visit = require("unist-util-visit"); +const { Mark } = require("prosemirror-model"); -let schema +let schema; +let activeMarks = Mark.none; +let textsArray = []; // Setup Remark. const remark = new Remark({ @@ -17,75 +19,111 @@ const remark = new Remark({ pedantic: true, }); -const processMdastNode = (node) => { - console.log('processMdastNode', node) - if (node.type === 'root') { - const content = node.children.map((childNode) => ( - processMdastNode(childNode) - )) - return schema.node('doc', {}, content) +const processMdastNode = node => { + if (node.type === "root") { + const content = node.children.map(childNode => processMdastNode(childNode)); + return schema.node("doc", {}, content); } /*** * Block nodes ***/ - if (node.type === 'heading') { - const content = node.children.map((childNode) => ( - processMdastNode(childNode) - )) - console.log(content) - return schema.node('heading', { level: node.depth }, content) - } else if (node.type === 'paragraph') { - const content = node.children.map((childNode) => ( - processMdastNode(childNode) - )) - return schema.node('paragraph', {}, content) - } else if (node.type === 'list') { - const content = node.children.map((childNode) => ( - processMdastNode(childNode) - )) + // heading and paragraph nodes contain raw text so we need to collect + // the flat list of text nodes. Other node types contain paragraph nodes. + if (node.type === "heading") { + node.children.forEach(childNode => processMdastNode(childNode)); + const pNode = schema.node("heading", { level: node.depth }, textsArray); + textsArray = []; + return pNode; + } else if (node.type === "paragraph") { + node.children.forEach(childNode => processMdastNode(childNode)); + const pNode = schema.node("paragraph", {}, textsArray); + textsArray = []; + return pNode; + } else if (node.type === "blockquote") { + const content = node.children.map(childNode => processMdastNode(childNode)); + return schema.node("blockquote", {}, content); + } else if (node.type === "list") { + const content = node.children.map(childNode => processMdastNode(childNode)); if (node.ordered) { - return schema.node('ordered_list', { tight: true, order: 1 }, content) + return schema.node("ordered_list", { tight: true, order: 1 }, content); } else { - return schema.node('bullet_list', {}, content) + return schema.node("bullet_list", { tight: true }, content); } - } else if (node.type === 'listItem') { - const content = node.children.map((childNode) => ( - processMdastNode(childNode) - )) - return schema.node('list_item', {}, content) - } else if (node.type === 'thematicBreak') { - return schema.node('horizontal_rule') - } else if (node.type === 'break') { - return schema.node('hard_break') - } else if (node.type === 'image') { - return schema.node('image', { src: node.url, alt: node.alt }) + } else if (node.type === "listItem") { + const content = node.children.map(childNode => processMdastNode(childNode)); + return schema.node("list_item", {}, content); + } else if (node.type === "thematicBreak") { + return schema.node("horizontal_rule"); + } else if (node.type === "break") { + return schema.node("hard_break"); + } else if (node.type === "image") { + return schema.node("image", { src: node.url, alt: node.alt }); + } else if (node.type === "code") { + return schema.node( + "code_block", + { + params: node.lang, + }, + schema.text(node.value) + ); } /*** - * end block items + * End block items ***/ // Inline - if (node.type === 'text') { - console.log('text value', node.value) - return schema.text(node.value) + if (node.type === "text") { + textsArray.push(schema.text(node.value, activeMarks)); + return; + } else if (node.type === "emphasis") { + const mark = schema.marks["em"].create(); + activeMarks = mark.addToSet(activeMarks); + node.children.forEach(childNode => processMdastNode(childNode)); + activeMarks = mark.removeFromSet(activeMarks); + return; + } else if (node.type === "strong") { + const mark = schema.marks["strong"].create(); + activeMarks = mark.addToSet(activeMarks); + node.children.forEach(childNode => processMdastNode(childNode)); + activeMarks = mark.removeFromSet(activeMarks); + return; + } else if (node.type === "link") { + const mark = schema.marks["strong"].create({ + title: node.title, + href: node.url, + }); + activeMarks = mark.addToSet(activeMarks); + node.children.forEach(childNode => processMdastNode(childNode)); + activeMarks = mark.removeFromSet(activeMarks); + return; + } else if (node.type === "inlineCode") { + // Inline code is like a text node in that it can't have children + // so we add it to textsArray immediately. + const mark = schema.marks["code"].create(); + activeMarks = mark.addToSet(activeMarks); + textsArray.push(schema.text(node.value, activeMarks)); + activeMarks = mark.removeFromSet(activeMarks); + return; } - return doc -} + return doc; +}; -const compileMarkdownToProseMirror = (src) => { - console.log(src) - const mdast = remark.parse(src) - console.log(mdast) - const doc = processMdastNode(mdast) - console.log(doc.content) - return doc -} +const compileMarkdownToProseMirror = src => { + // console.log(src); + // Clear out any old state. + let activeMarks = Mark.none; + let textsArray = []; + + const mdast = remark.parse(src); + const doc = processMdastNode(mdast); + return doc; +}; module.exports = (s, plugins) => { //console.log(s) //console.log(s.nodes.code_block.create({ params: { language: 'javascript' } })) - schema = s - return compileMarkdownToProseMirror -} + schema = s; + return compileMarkdownToProseMirror; +}; From f93aa34105b365388f0522ac026e258c2724d7df Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 22 May 2017 14:04:24 -0400 Subject: [PATCH 05/79] fix rebase incongruencies --- .../VisualEditor/index.js | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index f8d295af..fcee4d8a 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -12,17 +12,17 @@ import { headingRule, inputRules, allInputRules, -} from "prosemirror-inputrules"; -import { keymap } from "prosemirror-keymap"; +} from 'prosemirror-inputrules'; +import { keymap } from 'prosemirror-keymap'; import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; -import { baseKeymap, setBlockType, toggleMark } from "prosemirror-commands"; -import registry from "../../../../lib/registry"; -import { createAssetProxy } from "../../../../valueObjects/AssetProxy"; -import { buildKeymap } from "./keymap"; -import createMarkdownParser from "./parser"; -import Toolbar from "../Toolbar/Toolbar"; +import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; +import registry from '../../../../lib/registry'; +import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; +import { buildKeymap } from './keymap'; +import createMarkdownParser from './parser'; +import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../UI/Sticky/Sticky'; -import styles from "./index.css"; +import styles from './index.css'; function processUrl(url) { if (url.match(/^(https?:\/\/|mailto:|\/)/)) { @@ -61,25 +61,25 @@ function schemaWithPlugins(schema, plugins) { let nodeSpec = schema.nodeSpec; plugins.forEach((plugin) => { const attrs = {}; - plugin.get("fields").forEach((field) => { - attrs[field.get("name")] = { default: null }; + plugin.get('fields').forEach((field) => { + attrs[field.get('name')] = { default: null }; }); - nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get("id") }`, { + nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, { attrs, - group: "block", + group: 'block', parseDOM: [ { - tag: "div[data-plugin]", + tag: 'div[data-plugin]', getAttrs(dom) { - return JSON.parse(dom.getAttribute("data-plugin")); + return JSON.parse(dom.getAttribute('data-plugin')); }, }, ], toDOM(node) { return [ - "div", - { "data-plugin": JSON.stringify(node.attrs) }, - plugin.get("label"), + 'div', + { 'data-plugin': JSON.stringify(node.attrs) }, + plugin.get('label'), ]; }, }); @@ -94,8 +94,8 @@ function schemaWithPlugins(schema, plugins) { function createSerializer(schema, plugins) { const serializer = Object.create(defaultMarkdownSerializer); plugins.forEach((plugin) => { - serializer.nodes[`plugin_${ plugin.get("id") }`] = (state, node) => { - const toBlock = plugin.get("toBlock"); + serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => { + const toBlock = plugin.get('toBlock'); state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`); }; }); @@ -173,7 +173,7 @@ export default class Editor extends Component { if ( $from.parent && $from.parent.type === schema.nodes.paragraph && - $from.parent.textContent === "" + $from.parent.textContent === '' ) { const pos = this.view.coordsAtPos(selection.from); const editorPos = this.view.content.getBoundingClientRect(); @@ -188,8 +188,6 @@ export default class Editor extends Component { } else { const pos = this.view.coordsAtPos(selection.from); const editorPos = this.view.content.getBoundingClientRect(); - const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; - this.setState({ selectionPosition }); const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left, @@ -245,7 +243,7 @@ export default class Editor extends Component { handlePluginSubmit = (plugin, data) => { const { schema } = this.state; - const nodeType = schema.nodes[`plugin_${ plugin.get("id") }`]; + const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; this.view.props.onAction( this.view.state.tr .replaceSelectionWith(nodeType.create(data.toJS())) @@ -280,7 +278,7 @@ export default class Editor extends Component { Array.from(e.dataTransfer.files).forEach((file) => { createAssetProxy(file.name, file).then((assetProxy) => { this.props.onAddAsset(assetProxy); - if (file.type.split("/")[0] === "image") { + if (file.type.split('/')[0] === 'image') { nodes.push( schema.nodes.image.create({ src: assetProxy.public_path, @@ -299,7 +297,7 @@ export default class Editor extends Component { }); } else { nodes.push( - schema.nodes.paragraph.create({}, e.dataTransfer.getData("text/plain")) + schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain')) ); } @@ -311,7 +309,7 @@ export default class Editor extends Component { }; handleToggle = () => { - this.props.onMode("raw"); + this.props.onMode('raw'); }; render() { From e401f7ef9b72b1a189e53853f57045c899b018fb Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 22 May 2017 14:34:49 -0400 Subject: [PATCH 06/79] remove unrelated code style improvements --- .../VisualEditor/index.js | 141 +++++++----------- 1 file changed, 51 insertions(+), 90 deletions(-) diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index fcee4d8a..a00620ca 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -5,13 +5,8 @@ import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import history from 'prosemirror-history'; import { - blockQuoteRule, - orderedListRule, - bulletListRule, - codeBlockRule, - headingRule, - inputRules, - allInputRules, + blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule, + inputRules, allInputRules, } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; @@ -67,20 +62,14 @@ function schemaWithPlugins(schema, plugins) { nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, { attrs, group: 'block', - parseDOM: [ - { - tag: 'div[data-plugin]', - getAttrs(dom) { - return JSON.parse(dom.getAttribute('data-plugin')); - }, + parseDOM: [{ + tag: 'div[data-plugin]', + getAttrs(dom) { + return JSON.parse(dom.getAttribute('data-plugin')); }, - ], + }], toDOM(node) { - return [ - 'div', - { 'data-plugin': JSON.stringify(node.attrs) }, - plugin.get('label'), - ]; + return ['div', { 'data-plugin': JSON.stringify(node.attrs) }, plugin.get('label')]; }, }); }); @@ -170,28 +159,16 @@ export default class Editor extends Component { const { schema, selection } = state; if (selection.from === selection.to) { const { $from } = selection; - if ( - $from.parent && - $from.parent.type === schema.nodes.paragraph && - $from.parent.textContent === '' - ) { + if ($from.parent && $from.parent.type === schema.nodes.paragraph && $from.parent.textContent === '') { const pos = this.view.coordsAtPos(selection.from); const editorPos = this.view.content.getBoundingClientRect(); - const selectionPosition = { - top: pos.top - editorPos.top, - left: pos.left - editorPos.left, - }; + const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; this.setState({ selectionPosition }); - } else { - this.setState({ showToolbar: false, showBlockMenu: false }); } } else { const pos = this.view.coordsAtPos(selection.from); const editorPos = this.view.content.getBoundingClientRect(); - const selectionPosition = { - top: pos.top - editorPos.top, - left: pos.left - editorPos.left, - }; + const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; this.setState({ selectionPosition }); } }; @@ -200,7 +177,7 @@ export default class Editor extends Component { this.ref = ref; }; - handleHeader = level => + handleHeader = level => ( () => { const { schema } = this.state; const state = this.view.state; @@ -218,7 +195,8 @@ export default class Editor extends Component { const command = setBlockType(nodeType, { level }); command(state, this.handleAction); - }; + } + ); handleBold = () => { const command = toggleMark(this.state.schema.marks.strong); @@ -235,20 +213,14 @@ export default class Editor extends Component { if (!markActive(this.view.state, this.state.schema.marks.link)) { url = prompt('Link URL:'); // eslint-disable-line no-alert } - const command = toggleMark(this.state.schema.marks.link, { - href: url ? processUrl(url) : null, - }); + const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null }); command(this.view.state, this.handleAction); }; handlePluginSubmit = (plugin, data) => { const { schema } = this.state; const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; - this.view.props.onAction( - this.view.state.tr - .replaceSelectionWith(nodeType.create(data.toJS())) - .action() - ); + this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); }; handleDragEnter = (e) => { @@ -276,35 +248,26 @@ export default class Editor extends Component { if (e.dataTransfer.files && e.dataTransfer.files.length) { Array.from(e.dataTransfer.files).forEach((file) => { - createAssetProxy(file.name, file).then((assetProxy) => { + createAssetProxy(file.name, file) + .then((assetProxy) => { this.props.onAddAsset(assetProxy); if (file.type.split('/')[0] === 'image') { nodes.push( - schema.nodes.image.create({ - src: assetProxy.public_path, - alt: file.name, - }) + schema.nodes.image.create({ src: assetProxy.public_path, alt: file.name }) ); } else { nodes.push( - schema.marks.link.create({ - href: assetProxy.public_path, - title: file.name, - }) + schema.marks.link.create({ href: assetProxy.public_path, title: file.name }) ); } }); }); } else { - nodes.push( - schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain')) - ); + nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain'))); } nodes.forEach((node) => { - this.view.props.onAction( - this.view.state.tr.replaceSelectionWith(node).action() - ); + this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action()); }); }; @@ -320,38 +283,36 @@ export default class Editor extends Component { classNames.push(styles.dragging); } - return ( -
+ - - - -
-
-
- ); + + +
+
+
); } } From 514fbb30b8a84b448ac3c1d3dcb4d74e43099a15 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 23 May 2017 14:15:00 -0400 Subject: [PATCH 07/79] render plugins on visual editor load --- .../__snapshots__/parser.spec.js.snap | 20 +++++++ .../VisualEditor/__tests__/parser.spec.js | 56 ++++++++++++++++++- .../VisualEditor/parser.js | 36 +++++++++++- 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap index 4c22ae72..74a86aab 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap @@ -1075,6 +1075,26 @@ Object { } `; +exports[`Compile markdown to Prosemirror document structure should compile plugins 1`] = ` +Object { + "content": Array [ + Object { + "type": "paragraph", + }, + Object { + "content": Array [ + Object { + "text": "{{< test >}}", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`; + exports[`Compile markdown to Prosemirror document structure should compile simple markdown 1`] = ` Object { "content": Array [ diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js index 07972fa1..5b8fff61 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js @@ -1,3 +1,4 @@ +import { fromJS } from 'immutable'; import { Schema } from "prosemirror-model"; import { schema } from "prosemirror-markdown"; @@ -7,7 +8,51 @@ const testSchema = new Schema({ nodes: schema.nodeSpec, marks: schema.markSpec, }); -const parser = makeParser(testSchema); + +// Temporary plugins test, uses preloaded plugins from ../parser +// TODO: make the parser more testable +const testPlugins = fromJS([ + { + label: 'Image', + id: 'image', + fromBlock: match => match && { + image: match[2], + alt: match[1], + }, + toBlock: data => `![${ data.alt }](${ data.image })`, + toPreview: data => {data.alt}, + pattern: /^!\[([^\]]+)]\(([^)]+)\)$/, + fields: [{ + label: 'Image', + name: 'image', + widget: 'image', + }, { + label: 'Alt Text', + name: 'alt', + }], + }, + { + id: "youtube", + label: "Youtube", + fields: [{name: 'id', label: 'Youtube Video ID'}], + pattern: /^{{<\s?youtube (\S+)\s?>}}/, + fromBlock: function(match) { + return { + id: match[1] + }; + }, + toBlock: function(obj) { + return '{{< youtube ' + obj.id + ' >}}'; + }, + toPreview: function(obj) { + return ( + 'Youtube Video' + ); + } + }, +]); + +const parser = makeParser(testSchema, testPlugins); describe("Compile markdown to Prosemirror document structure", () => { it("should compile simple markdown", () => { @@ -127,6 +172,15 @@ How far is it to [Google](https://google.com) land? expect(parser(value)).toMatchSnapshot(); }); + it("should compile plugins", () => { + const value = ` +![test](test.png) + +{{< test >}} +`; + expect(parser(value)).toMatchSnapshot(); + }); + it("should compile kitchen sink example", () => { const value = ` # An exhibit of Markdown diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index fe015ff8..a4605fcf 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -9,6 +9,7 @@ const visit = require("unist-util-visit"); const { Mark } = require("prosemirror-model"); let schema; +let plugins let activeMarks = Mark.none; let textsArray = []; @@ -36,6 +37,38 @@ const processMdastNode = node => { textsArray = []; return pNode; } else if (node.type === "paragraph") { + + // TODO: improve plugin handling + + // Handle externally defined plugins (they'll be wrapped in paragraphs) + if (node.children.length === 1 && node.children[0].type === 'text') { + const value = node.children[0].value; + const plugin = plugins.find(plugin => plugin.get('pattern').test(value)); + if (plugin) { + const nodeType = schema.nodes[`plugin_${plugin.get('id')}`]; + const data = plugin.get('fromBlock').call(plugin, value.match(plugin.get('pattern'))); + return nodeType.create(data); + } + } + + // 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.length === 1 && node.children[0].type === 'image') { + const { url, alt } = node.children[0]; + + // 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 matches = [ , alt, url ]; + const plugin = plugins.find(plugin => plugin.id === 'image'); + if (plugin) { + const nodeType = schema.nodes.plugin_image; + const data = plugin.get('fromBlock').call(plugin, matches); + return nodeType.create(data); + } + } + node.children.forEach(childNode => processMdastNode(childNode)); const pNode = schema.node("paragraph", {}, textsArray); textsArray = []; @@ -121,9 +154,10 @@ const compileMarkdownToProseMirror = src => { return doc; }; -module.exports = (s, plugins) => { +module.exports = (s, p) => { //console.log(s) //console.log(s.nodes.code_block.create({ params: { language: 'javascript' } })) schema = s; + plugins = p; return compileMarkdownToProseMirror; }; From adcb215fbdb7f9f0d76a61aa3099d3920af6fc1e Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 24 May 2017 13:21:15 -0400 Subject: [PATCH 08/79] replace remark with unified for docs and extensibility --- package.json | 7 ++-- src/components/MarkupItReactRenderer/index.js | 25 ++++++------- .../VisualEditor/parser.js | 35 +++++++++---------- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 87016e79..09f91261 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "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", @@ -113,7 +112,6 @@ "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", @@ -159,14 +157,17 @@ "redux-notifications": "^2.1.1", "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", - "remark": "6", + "rehype-stringify": "^3.0.0", "remark-html": "^6.0.0", + "remark-parse": "^3.0.1", + "remark-rehype": "^2.0.0", "selection-position": "^1.0.0", "semaphore": "^1.0.5", "slate": "^0.14.14", "slate-drop-or-paste-images": "^0.2.0", "slug": "^0.9.1", "textarea-caret-position": "^0.1.1", + "unified": "^6.1.4", "unist-util-visit": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index 462170c5..320a0f75 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -1,16 +1,10 @@ import React, { PropTypes } from "react"; -import Remark from "remark"; -import toHAST from "mdast-util-to-hast"; -import hastToHTML from "hast-util-to-html"; +import unified from 'unified'; +import markdown from 'remark-parse'; +import rehype from 'remark-rehype'; +import html from 'rehype-stringify'; import registry from "../../lib/registry"; -// Setup Remark. -const remark = new Remark({ - commonmark: true, - footnotes: true, - pedantic: true, -}); - export default class MarkupItReactRenderer extends React.Component { constructor(props) { super(props); @@ -22,11 +16,12 @@ export default class MarkupItReactRenderer extends React.Component { } render() { - const { value } = this.props; - const mdast = remark.parse(value); - const hast = toHAST(mdast, { allowDangerousHTML: true }); - const html = hastToHTML(hast, { allowDangerousHTML: true }); - return
; // eslint-disable-line react/no-danger + const doc = unified() + .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(rehype, { allowDangerousHTML: true }) + .use(html, { allowDangerousHTML: true }) + .processSync(this.props.value); + return
; // eslint-disable-line react/no-danger } } diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index a4605fcf..98fa1ad9 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -4,22 +4,16 @@ https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js */ -import Remark from "remark"; -const visit = require("unist-util-visit"); -const { Mark } = require("prosemirror-model"); +import unified from 'unified'; +import markdown from 'remark-parse'; +import visit from 'unist-util-visit'; +import { Mark } from 'prosemirror-model'; let schema; let plugins let activeMarks = Mark.none; let textsArray = []; -// Setup Remark. -const remark = new Remark({ - commonmark: true, - footnotes: true, - pedantic: true, -}); - const processMdastNode = node => { if (node.type === "root") { const content = node.children.map(childNode => processMdastNode(childNode)); @@ -139,25 +133,28 @@ const processMdastNode = node => { activeMarks = mark.removeFromSet(activeMarks); return; } - - return doc; }; const compileMarkdownToProseMirror = src => { - // console.log(src); // Clear out any old state. let activeMarks = Mark.none; let textsArray = []; - const mdast = remark.parse(src); - const doc = processMdastNode(mdast); - return doc; + const result = unified() + .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) + .parse(src); + + const output = unified() + .use(() => processMdastNode) + .runSync(result); + + return output; }; -module.exports = (s, p) => { - //console.log(s) - //console.log(s.nodes.code_block.create({ params: { language: 'javascript' } })) +const parser = (s, p) => { schema = s; plugins = p; return compileMarkdownToProseMirror; }; + +export default parser; From 5048c7ca1dab971f3f7ea1e355c60d5114007ca3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 24 May 2017 15:44:54 -0400 Subject: [PATCH 09/79] convert editor component registry to Map --- .../Widgets/MarkdownControlElements/Toolbar/Toolbar.js | 2 +- .../Toolbar/ToolbarComponentsMenu.js | 2 +- src/lib/registry.js | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js b/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js index 59eb227e..07eb1261 100644 --- a/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js +++ b/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js @@ -18,7 +18,7 @@ export default class Toolbar extends React.Component { onLink: PropTypes.func.isRequired, onToggleMode: PropTypes.func.isRequired, rawMode: PropTypes.bool, - plugins: ImmutablePropTypes.listOf(ImmutablePropTypes.record), + plugins: ImmutablePropTypes.map, onSubmit: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired, diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js b/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js index c1d2c167..ba1e22ea 100644 --- a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js +++ b/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js @@ -6,7 +6,7 @@ import styles from './ToolbarComponentsMenu.css'; export default class ToolbarComponentsMenu extends React.Component { static PropTypes = { - plugins: ImmutablePropTypes.list.isRequired, + plugins: ImmutablePropTypes.map.isRequired, onComponentMenuItemClick: PropTypes.func.isRequired, }; diff --git a/src/lib/registry.js b/src/lib/registry.js index a656cfe7..8990c96a 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -1,11 +1,11 @@ -import { List } from 'immutable'; +import { Map } from 'immutable'; import { newEditorPlugin } from '../components/Widgets/MarkdownControlElements/plugins'; const _registry = { templates: {}, previewStyles: [], widgets: {}, - editorComponents: List([]) + editorComponents: Map(), }; export default { @@ -31,7 +31,8 @@ export default { return _registry.widgets[name]; }, registerEditorComponent(component) { - _registry.editorComponents = _registry.editorComponents.push(newEditorPlugin(component)); + const plugin = newEditorPlugin(component); + _registry.editorComponents = _registry.editorComponents.set(plugin.get('id'), plugin); }, getEditorComponents() { return _registry.editorComponents; From 8bb18452e80013067764ef738eeaab834e63507b Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 25 May 2017 13:51:43 -0400 Subject: [PATCH 10/79] implement initial unified/remark preview update --- package.json | 2 +- src/components/MarkupItReactRenderer/index.js | 92 ++++++++++++++++--- .../VisualEditor/__tests__/parser.spec.js | 3 +- .../VisualEditor/parser.js | 3 +- 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 09f91261..cbfddc32 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "redux-notifications": "^2.1.1", "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", + "rehype-parse": "^3.1.0", "rehype-stringify": "^3.0.0", "remark-html": "^6.0.0", "remark-parse": "^3.0.1", @@ -168,7 +169,6 @@ "slug": "^0.9.1", "textarea-caret-position": "^0.1.1", "unified": "^6.1.4", - "unist-util-visit": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" }, diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index 320a0f75..b152187d 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -1,30 +1,92 @@ import React, { PropTypes } from "react"; +import { renderToStaticMarkup } from 'react-dom/server'; +import { Map } from 'immutable'; import unified from 'unified'; import markdown from 'remark-parse'; import rehype from 'remark-rehype'; +import parseHtml from 'rehype-parse'; import html from 'rehype-stringify'; import registry from "../../lib/registry"; -export default class MarkupItReactRenderer extends React.Component { - constructor(props) { - super(props); - this.plugins = {}; - // TODO add back support for this. - registry.getEditorComponents().forEach((component) => { - this.plugins[component.get("id")] = component; - }); +const getPlugins = () => registry.getEditorComponents(); + +const renderEditorPlugins = ({ getAsset }) => { + return tree => { + const result = renderEditorPluginsProcessor(tree, getAsset); + return result; + }; +}; + +const renderEditorPluginsProcessor = (node, getAsset) => { + + if (node.children) { + + node.children = node.children.map(n => renderEditorPluginsProcessor(n, getAsset)); + + // Handle externally defined plugins (they'll be wrapped in paragraphs) + if (node.tagName === 'p' && node.children.length === 1 && node.children[0].type === 'text') { + const value = node.children[0].value; + const plugin = getPlugins().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 = typeof preview === 'string' ? +
: + preview; + + const result = unified() + .use(parseHtml, { fragment: true }) + .parse(renderToStaticMarkup(output)); + + return result.children[0]; + } + } } - render() { - const doc = unified() - .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) - .use(rehype, { allowDangerousHTML: true }) - .use(html, { allowDangerousHTML: true }) - .processSync(this.props.value); - return
; // eslint-disable-line react/no-danger + // 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.tagName === 'img') { + const { src, alt } = node.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 = getPlugins().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 = typeof preview === 'string' ? +
: + preview; + + const result = unified() + .use(parseHtml, { fragment: true }) + .parse(renderToStaticMarkup(output)); + + return result.children[0]; + } } + + return node; +}; + +const MarkupItReactRenderer = ({ value, getAsset }) => { + const doc = unified() + .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(rehype, { allowDangerousHTML: true }) + .use(renderEditorPlugins, { getAsset }) + .use(html, { allowDangerousHTML: true }) + .processSync(value); + + return
; // eslint-disable-line react/no-danger } +export default MarkupItReactRenderer; + MarkupItReactRenderer.propTypes = { value: PropTypes.string, + getAsset: PropTypes.func.isRequired, }; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js index 5b8fff61..594f2a32 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js @@ -1,8 +1,7 @@ import { fromJS } from 'immutable'; import { Schema } from "prosemirror-model"; import { schema } from "prosemirror-markdown"; - -const makeParser = require("../parser"); +import makeParser from '../parser'; const testSchema = new Schema({ nodes: schema.nodeSpec, diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index 98fa1ad9..96c8c3af 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -6,7 +6,6 @@ import unified from 'unified'; import markdown from 'remark-parse'; -import visit from 'unist-util-visit'; import { Mark } from 'prosemirror-model'; let schema; @@ -133,6 +132,8 @@ const processMdastNode = node => { activeMarks = mark.removeFromSet(activeMarks); return; } + + return node; }; const compileMarkdownToProseMirror = src => { From 361c3d5284cec6412d09e2b93219dec799c44068 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 7 Jun 2017 22:23:06 -0400 Subject: [PATCH 11/79] improve prosemirror parser, fix new doc creation --- src/components/MarkupItReactRenderer/index.js | 11 +- .../VisualEditor/parser.js | 323 ++++++++++-------- 2 files changed, 189 insertions(+), 145 deletions(-) diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index b152187d..f48f5888 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -30,15 +30,8 @@ const renderEditorPluginsProcessor = (node, getAsset) => { if (plugin) { const data = plugin.get('fromBlock')(value.match(plugin.get('pattern'))); const preview = plugin.get('toPreview')(data); - const output = typeof preview === 'string' ? -
: - preview; - - const result = unified() - .use(parseHtml, { fragment: true }) - .parse(renderToStaticMarkup(output)); - - return result.children[0]; + const output = `
${typeof preview === 'string' ? preview : renderToStaticMarkup(preview)}
`; + return unified().use(parseHtml, { fragment: true }).parse(output); } } } diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index 96c8c3af..6cebc077 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -1,161 +1,212 @@ -/* eslint-disable */ -/* - Based closely on - https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js -*/ - import unified from 'unified'; import markdown from 'remark-parse'; import { Mark } from 'prosemirror-model'; +import isEmpty from 'lodash/isEmpty'; let schema; let plugins let activeMarks = Mark.none; let textsArray = []; -const processMdastNode = node => { - if (node.type === "root") { - const content = node.children.map(childNode => processMdastNode(childNode)); - return schema.node("doc", {}, content); +/** + * A remark plugin for converting an MDAST to a ProseMirror tree. + * @returns {function} a transformer function + */ +function markdownToProseMirror() { + return transform; +} + +/** + * 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; } - /*** - * Block nodes - ***/ - // heading and paragraph nodes contain raw text so we need to collect - // the flat list of text nodes. Other node types contain paragraph nodes. - if (node.type === "heading") { - node.children.forEach(childNode => processMdastNode(childNode)); - const pNode = schema.node("heading", { level: node.depth }, textsArray); + const nodeDef = getNodeDef(node); + + if (!nodeDef) { + return node; + } + + return (nodeDef.block ? processBlock : processInline)(nodeDef, node.children, node.value); +} + +/** + * 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: 'strong' }; + 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 = textsArray; textsArray = []; - return pNode; - } else if (node.type === "paragraph") { - - // TODO: improve plugin handling - - // Handle externally defined plugins (they'll be wrapped in paragraphs) - if (node.children.length === 1 && node.children[0].type === 'text') { - const value = node.children[0].value; - const plugin = plugins.find(plugin => plugin.get('pattern').test(value)); - if (plugin) { - const nodeType = schema.nodes[`plugin_${plugin.get('id')}`]; - const data = plugin.get('fromBlock').call(plugin, value.match(plugin.get('pattern'))); - return nodeType.create(data); - } - } - - // 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.length === 1 && node.children[0].type === 'image') { - const { url, alt } = node.children[0]; - - // 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 matches = [ , alt, url ]; - const plugin = plugins.find(plugin => plugin.id === 'image'); - if (plugin) { - const nodeType = schema.nodes.plugin_image; - const data = plugin.get('fromBlock').call(plugin, matches); - return nodeType.create(data); - } - } - - node.children.forEach(childNode => processMdastNode(childNode)); - const pNode = schema.node("paragraph", {}, textsArray); - textsArray = []; - return pNode; - } else if (node.type === "blockquote") { - const content = node.children.map(childNode => processMdastNode(childNode)); - return schema.node("blockquote", {}, content); - } else if (node.type === "list") { - const content = node.children.map(childNode => processMdastNode(childNode)); - if (node.ordered) { - return schema.node("ordered_list", { tight: true, order: 1 }, content); - } else { - return schema.node("bullet_list", { tight: true }, content); - } - } else if (node.type === "listItem") { - const content = node.children.map(childNode => processMdastNode(childNode)); - return schema.node("list_item", {}, content); - } else if (node.type === "thematicBreak") { - return schema.node("horizontal_rule"); - } else if (node.type === "break") { - return schema.node("hard_break"); - } else if (node.type === "image") { - return schema.node("image", { src: node.url, alt: node.alt }); - } else if (node.type === "code") { - return schema.node( - "code_block", - { - params: node.lang, - }, - schema.text(node.value) - ); - } - /*** - * End block items - ***/ - - // Inline - if (node.type === "text") { - textsArray.push(schema.text(node.value, activeMarks)); - return; - } else if (node.type === "emphasis") { - const mark = schema.marks["em"].create(); - activeMarks = mark.addToSet(activeMarks); - node.children.forEach(childNode => processMdastNode(childNode)); - activeMarks = mark.removeFromSet(activeMarks); - return; - } else if (node.type === "strong") { - const mark = schema.marks["strong"].create(); - activeMarks = mark.addToSet(activeMarks); - node.children.forEach(childNode => processMdastNode(childNode)); - activeMarks = mark.removeFromSet(activeMarks); - return; - } else if (node.type === "link") { - const mark = schema.marks["strong"].create({ - title: node.title, - href: node.url, - }); - activeMarks = mark.addToSet(activeMarks); - node.children.forEach(childNode => processMdastNode(childNode)); - activeMarks = mark.removeFromSet(activeMarks); - return; - } else if (node.type === "inlineCode") { - // Inline code is like a text node in that it can't have children - // so we add it to textsArray immediately. - const mark = schema.marks["code"].create(); - activeMarks = mark.addToSet(activeMarks); - textsArray.push(schema.text(node.value, activeMarks)); - activeMarks = mark.removeFromSet(activeMarks); - return; + return content; } - return node; -}; + return processedChildren; +} -const compileMarkdownToProseMirror = src => { - // Clear out any old state. - let activeMarks = Mark.none; - let textsArray = []; +/** + * Processes text nodes. + * @param {string} value the node's text content + * @returns {undefined} + */ +function processText(value) { + textsArray.push(schema.text(value, 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); + activeMarks = mark.addToSet(activeMarks); + + if (isEmpty(children)) { + textsArray.push(schema.text(value, activeMarks)); + } else { + children.forEach(childNode => transform(childNode)); + } + + activeMarks = mark.removeFromSet(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); + } +} + +/** + * 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(markdown, { commonmark: true, footnotes: true, pedantic: true }) .parse(src); - const output = unified() - .use(() => processMdastNode) + return unified() + .use(markdownToProseMirror) .runSync(result); +} - return output; -}; - -const parser = (s, p) => { +/** + * Gets the parser and makes schema and plugins available at top scope. + * @param {Schema} s a ProseMirror schema + * @param {Map} p Immutable Map of registered plugins + */ +function parserGetter(s, p) { schema = s; plugins = p; - return compileMarkdownToProseMirror; -}; + return parser; +} -export default parser; +export default parserGetter; From b5e0be43f2dd5dcafc11fdc7f66e9dac1328aeb5 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 9 Jun 2017 17:23:58 -0400 Subject: [PATCH 12/79] split off markdownToProseMirror plugin --- .../VisualEditor/markdownToProseMirror.js | 182 ++++++++++++++++ .../VisualEditor/parser.js | 194 +----------------- 2 files changed, 190 insertions(+), 186 deletions(-) create mode 100644 src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js new file mode 100644 index 00000000..fb77f015 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js @@ -0,0 +1,182 @@ +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; + + /** + * 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: 'strong' }; + 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/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js index 6cebc077..7ef6e5a8 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js @@ -1,187 +1,9 @@ import unified from 'unified'; import markdown from 'remark-parse'; import { Mark } from 'prosemirror-model'; -import isEmpty from 'lodash/isEmpty'; +import markdownToProseMirror from './markdownToProseMirror'; -let schema; -let plugins -let activeMarks = Mark.none; -let textsArray = []; - -/** - * A remark plugin for converting an MDAST to a ProseMirror tree. - * @returns {function} a transformer function - */ -function markdownToProseMirror() { - return transform; -} - -/** - * 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); - - if (!nodeDef) { - return node; - } - - return (nodeDef.block ? processBlock : processInline)(nodeDef, node.children, node.value); -} - -/** - * 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: 'strong' }; - 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 = textsArray; - textsArray = []; - return content; - } - - return processedChildren; -} - -/** - * Processes text nodes. - * @param {string} value the node's text content - * @returns {undefined} - */ -function processText(value) { - textsArray.push(schema.text(value, 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); - activeMarks = mark.addToSet(activeMarks); - - if (isEmpty(children)) { - textsArray.push(schema.text(value, activeMarks)); - } else { - children.forEach(childNode => transform(childNode)); - } - - activeMarks = mark.removeFromSet(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); - } -} +const state = { activeMarks: Mark.none, textsArray: [] }; /** * Uses unified to parse markdown and apply plugins. @@ -194,18 +16,18 @@ function parser(src) { .parse(src); return unified() - .use(markdownToProseMirror) + .use(markdownToProseMirror, { state }) .runSync(result); } /** * Gets the parser and makes schema and plugins available at top scope. - * @param {Schema} s a ProseMirror schema - * @param {Map} p Immutable Map of registered plugins + * @param {Schema} schema - a ProseMirror schema + * @param {Map} plugins - Immutable Map of registered plugins */ -function parserGetter(s, p) { - schema = s; - plugins = p; +function parserGetter(schema, plugins) { + state.schema = schema; + state.plugins = plugins; return parser; } From b22323201dd8bd665d390f340fd2baa20d0fd140 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 9 Jun 2017 18:07:51 -0400 Subject: [PATCH 13/79] handle raw editor html pastes with unified --- package.json | 2 ++ .../MarkdownControlElements/RawEditor/index.js | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index cbfddc32..4ea7e840 100644 --- a/package.json +++ b/package.json @@ -158,10 +158,12 @@ "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", "rehype-parse": "^3.1.0", + "rehype-remark": "^2.0.0", "rehype-stringify": "^3.0.0", "remark-html": "^6.0.0", "remark-parse": "^3.0.1", "remark-rehype": "^2.0.0", + "remark-stringify": "^3.0.1", "selection-position": "^1.0.0", "semaphore": "^1.0.5", "slate": "^0.14.14", diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index 649f41af..4dba11fe 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -1,7 +1,8 @@ import React, { PropTypes } from 'react'; -import MarkupIt from 'markup-it'; -import markdownSyntax from 'markup-it/syntaxes/markdown'; -import htmlSyntax from 'markup-it/syntaxes/html'; +import unified from 'unified'; +import htmlToRehype from 'rehype-parse'; +import rehypeToRemark from 'rehype-remark'; +import remarkToMarkdown from 'remark-stringify'; import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; import registry from '../../../../lib/registry'; @@ -12,9 +13,6 @@ import styles from './index.css'; const HAS_LINE_BREAK = /\n/m; -const markdown = new MarkupIt(markdownSyntax); -const html = new MarkupIt(htmlSyntax); - function processUrl(url) { if (url.match(/^(https?:\/\/|mailto:|\/)/)) { return url; @@ -26,8 +24,11 @@ function processUrl(url) { } function cleanupPaste(paste) { - const content = html.toContent(paste); - return markdown.toText(content); + return unified() + .use(htmlToRehype) + .use(rehypeToRemark) + .use(remarkToMarkdown) + .process(paste); } function getCleanPaste(e) { From bd767308cdc82437612854cfe555962608738b40 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 9 Jun 2017 23:49:14 -0400 Subject: [PATCH 14/79] fix visual editor tests, parse/serialize consistency --- package.json | 3 + src/components/MarkupItReactRenderer/index.js | 85 ------------ .../RawEditor/index.css | 0 .../RawEditor/index.js | 8 +- .../Toolbar/Toolbar.css | 0 .../Toolbar/Toolbar.js | 0 .../Toolbar/ToolbarButton.css | 0 .../Toolbar/ToolbarButton.js | 0 .../Toolbar/ToolbarComponentsMenu.css | 0 .../Toolbar/ToolbarComponentsMenu.js | 0 .../Toolbar/ToolbarPluginForm.css | 0 .../Toolbar/ToolbarPluginForm.js | 0 .../Toolbar/ToolbarPluginFormControl.css | 0 .../Toolbar/ToolbarPluginFormControl.js | 0 .../__snapshots__/parser.spec.js.snap | 0 .../VisualEditor/__tests__/parser.spec.js | 0 .../VisualEditor/index.css | 0 .../VisualEditor/index.js | 9 +- .../VisualEditor/keymap.js | 0 .../VisualEditor/markdownToProseMirror.js | 0 .../VisualEditor/parser.js | 4 +- .../index.js} | 13 +- .../plugins.js | 0 .../RawEditor/prismMarkdown.js | 116 ---------------- src/components/Widgets/MarkdownPreview.js | 38 ----- .../__tests__/MarkupItReactRenderer.spec.js | 30 ++-- .../MarkupItReactRenderer.spec.js.snap | 0 .../MarkdownPreview/cmsPluginRehype.js | 59 ++++++++ .../Widgets/MarkdownPreview/index.js | 27 ++++ src/components/Widgets/richText.js | 131 ------------------ .../stories/MarkupItReactRenderer.js | 40 ------ src/components/stories/index.js | 1 - src/lib/registry.js | 2 +- 33 files changed, 125 insertions(+), 441 deletions(-) delete mode 100644 src/components/MarkupItReactRenderer/index.js rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/RawEditor/index.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/RawEditor/index.js (97%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/Toolbar.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/Toolbar.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarButton.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarButton.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarComponentsMenu.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarComponentsMenu.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarPluginForm.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarPluginForm.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarPluginFormControl.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/Toolbar/ToolbarPluginFormControl.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/__tests__/parser.spec.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/index.css (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/index.js (96%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/keymap.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/markdownToProseMirror.js (100%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/VisualEditor/parser.js (86%) rename src/components/Widgets/{MarkdownControl.js => MarkdownControl/index.js} (80%) rename src/components/Widgets/{MarkdownControlElements => MarkdownControl}/plugins.js (100%) delete mode 100644 src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js delete mode 100644 src/components/Widgets/MarkdownPreview.js rename src/components/{MarkupItReactRenderer => Widgets/MarkdownPreview}/__tests__/MarkupItReactRenderer.spec.js (78%) rename src/components/{MarkupItReactRenderer => Widgets/MarkdownPreview}/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap (100%) create mode 100644 src/components/Widgets/MarkdownPreview/cmsPluginRehype.js create mode 100644 src/components/Widgets/MarkdownPreview/index.js delete mode 100644 src/components/Widgets/richText.js delete mode 100644 src/components/stories/MarkupItReactRenderer.js diff --git a/package.json b/package.json index 4ea7e840..b155b71b 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,10 @@ "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", "rehype-parse": "^3.1.0", + "rehype-raw": "^1.0.0", + "rehype-react": "^3.0.0", "rehype-remark": "^2.0.0", + "rehype-sanitize": "^2.0.0", "rehype-stringify": "^3.0.0", "remark-html": "^6.0.0", "remark-parse": "^3.0.1", diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js deleted file mode 100644 index f48f5888..00000000 --- a/src/components/MarkupItReactRenderer/index.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { PropTypes } from "react"; -import { renderToStaticMarkup } from 'react-dom/server'; -import { Map } from 'immutable'; -import unified from 'unified'; -import markdown from 'remark-parse'; -import rehype from 'remark-rehype'; -import parseHtml from 'rehype-parse'; -import html from 'rehype-stringify'; -import registry from "../../lib/registry"; - -const getPlugins = () => registry.getEditorComponents(); - -const renderEditorPlugins = ({ getAsset }) => { - return tree => { - const result = renderEditorPluginsProcessor(tree, getAsset); - return result; - }; -}; - -const renderEditorPluginsProcessor = (node, getAsset) => { - - if (node.children) { - - node.children = node.children.map(n => renderEditorPluginsProcessor(n, getAsset)); - - // Handle externally defined plugins (they'll be wrapped in paragraphs) - if (node.tagName === 'p' && node.children.length === 1 && node.children[0].type === 'text') { - const value = node.children[0].value; - const plugin = getPlugins().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 = `
${typeof preview === 'string' ? preview : renderToStaticMarkup(preview)}
`; - return unified().use(parseHtml, { fragment: true }).parse(output); - } - } - } - - // 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.tagName === 'img') { - const { src, alt } = node.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 = getPlugins().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 = typeof preview === 'string' ? -
: - preview; - - const result = unified() - .use(parseHtml, { fragment: true }) - .parse(renderToStaticMarkup(output)); - - return result.children[0]; - } - } - - return node; -}; - -const MarkupItReactRenderer = ({ value, getAsset }) => { - const doc = unified() - .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) - .use(rehype, { allowDangerousHTML: true }) - .use(renderEditorPlugins, { getAsset }) - .use(html, { allowDangerousHTML: true }) - .processSync(value); - - return
; // eslint-disable-line react/no-danger -} - -export default MarkupItReactRenderer; - -MarkupItReactRenderer.propTypes = { - value: PropTypes.string, - getAsset: PropTypes.func.isRequired, -}; diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.css b/src/components/Widgets/MarkdownControl/RawEditor/index.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/RawEditor/index.css rename to src/components/Widgets/MarkdownControl/RawEditor/index.css diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControl/RawEditor/index.js similarity index 97% rename from src/components/Widgets/MarkdownControlElements/RawEditor/index.js rename to src/components/Widgets/MarkdownControl/RawEditor/index.js index 4dba11fe..c83de244 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControl/RawEditor/index.js @@ -3,6 +3,8 @@ import unified from 'unified'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import remarkToMarkdown from 'remark-stringify'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeReparse from 'rehype-raw'; import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; import registry from '../../../../lib/registry'; @@ -25,9 +27,11 @@ function processUrl(url) { function cleanupPaste(paste) { return unified() - .use(htmlToRehype) + .use(htmlToRehype, { fragment: true }) + .use(rehypeSanitize) + .use(rehypeReparse) .use(rehypeToRemark) - .use(remarkToMarkdown) + .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) .process(paste); } diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.css b/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.css rename to src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js b/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js rename to src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap rename to src/components/Widgets/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js rename to src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css b/src/components/Widgets/MarkdownControl/VisualEditor/index.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/index.css rename to src/components/Widgets/MarkdownControl/VisualEditor/index.css diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js similarity index 96% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/index.js rename to src/components/Widgets/MarkdownControl/VisualEditor/index.js index a00620ca..15265c1d 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -11,6 +11,9 @@ import { import { keymap } from 'prosemirror-keymap'; import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToMarkdown from 'remark-stringify'; import registry from '../../../../lib/registry'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { buildKeymap } from './keymap'; @@ -147,7 +150,11 @@ export default class Editor extends Component { const { serializer } = this.state; const newState = this.view.state.applyAction(action); const md = serializer.serialize(newState.doc); - this.props.onChange(md); + const processedMarkdown = unified() + .use(markdownToRemark) + .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) + .processSync(md); + this.props.onChange(processedMarkdown.contents); this.view.updateState(newState); if (newState.selection !== this.state.selection) { this.handleSelection(newState); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js b/src/components/Widgets/MarkdownControl/VisualEditor/keymap.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js rename to src/components/Widgets/MarkdownControl/VisualEditor/keymap.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js rename to src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControl/VisualEditor/parser.js similarity index 86% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js rename to src/components/Widgets/MarkdownControl/VisualEditor/parser.js index 7ef6e5a8..e06c4d2d 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/parser.js @@ -1,5 +1,5 @@ import unified from 'unified'; -import markdown from 'remark-parse'; +import remarkToMarkdown from 'remark-parse'; import { Mark } from 'prosemirror-model'; import markdownToProseMirror from './markdownToProseMirror'; @@ -12,7 +12,7 @@ const state = { activeMarks: Mark.none, textsArray: [] }; */ function parser(src) { const result = unified() - .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) .parse(src); return unified() diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl/index.js similarity index 80% rename from src/components/Widgets/MarkdownControl.js rename to src/components/Widgets/MarkdownControl/index.js index 446890b4..a3c3b0ab 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl/index.js @@ -1,9 +1,8 @@ import React, { PropTypes } from 'react'; -import registry from '../../lib/registry'; -import RawEditor from './MarkdownControlElements/RawEditor'; -import VisualEditor from './MarkdownControlElements/VisualEditor'; -import { processEditorPlugins } from './richText'; -import { StickyContainer } from '../UI/Sticky/Sticky'; +import registry from '../../../lib/registry'; +import RawEditor from './RawEditor'; +import VisualEditor from './VisualEditor'; +import { StickyContainer } from '../../UI/Sticky/Sticky'; const MODE_STORAGE_KEY = 'cms.md-mode'; @@ -21,10 +20,6 @@ export default class MarkdownControl extends React.Component { this.state = { mode: localStorage.getItem(MODE_STORAGE_KEY) || 'visual' }; } - componentWillMount() { - processEditorPlugins(registry.getEditorComponents()); - } - handleMode = (mode) => { this.setState({ mode }); localStorage.setItem(MODE_STORAGE_KEY, mode); diff --git a/src/components/Widgets/MarkdownControlElements/plugins.js b/src/components/Widgets/MarkdownControl/plugins.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/plugins.js rename to src/components/Widgets/MarkdownControl/plugins.js diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js b/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js deleted file mode 100644 index 1ea5e5d3..00000000 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js +++ /dev/null @@ -1,116 +0,0 @@ -const marks = { - 'blockquote': { - // > ... - pattern: /^>(?:[\t ]*>)*/m, - alias: 'punctuation' - }, - 'code': [ - { - // Prefixed by 4 spaces or 1 tab - pattern: /^(?: {4}|\t).+/m, - alias: 'keyword' - }, - { - // `code` - // ``code`` - pattern: /``.+?``|`[^`\n]+`/, - alias: 'keyword' - } - ], - 'title': [ - { - // title 1 - // ======= - - // title 2 - // ------- - pattern: /\w+.*(?:\r?\n|\r)(?:==+|--+)/, - alias: 'important', - inside: { - punctuation: /==+$|--+$/ - } - }, - { - // # title 1 - // ###### title 6 - pattern: /(^\s*)#+.+/m, - lookbehind: true, - alias: 'important', - inside: { - punctuation: /^#+|#+$/ - } - } - ], - 'hr': { - // *** - // --- - // * * * - // ----------- - pattern: /(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m, - lookbehind: true, - alias: 'punctuation' - }, - 'list': { - // * item - // + item - // - item - // 1. item - pattern: /(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m, - lookbehind: true, - alias: 'punctuation' - }, - 'url-reference': { - // [id]: http://example.com "Optional title" - // [id]: http://example.com 'Optional title' - // [id]: http://example.com (Optional title) - // [id]: "Optional title" - pattern: /!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/, - inside: { - 'variable': { - pattern: /^(!?\[)[^\]]+/, - lookbehind: true - }, - 'string': /(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/, - 'punctuation': /^[\[\]!:]|[<>]/ - }, - alias: 'url' - }, - 'bold': { - // **strong** - // __strong__ - - // Allow only one line break - pattern: /(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, - lookbehind: true, - inside: { - 'punctuation': /^\*\*|^__|\*\*$|__$/ - } - }, - 'italic': { - // *em* - // _em_ - - // Allow only one line break - pattern: /(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, - lookbehind: true, - inside: { - 'punctuation': /^[*_]|[*_]$/ - } - }, - 'url': { - // [example](http://example.com "Optional title") - // [example] [id] - pattern: /!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/, - inside: { - 'variable': { - pattern: /(!?\[)[^\]]+(?=\]$)/, - lookbehind: true - }, - 'string': { - pattern: /"(?:\\.|[^"\\])*"(?=\)$)/ - } - } - } -}; - -export default marks; diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js deleted file mode 100644 index 351ea9b9..00000000 --- a/src/components/Widgets/MarkdownPreview.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, { PropTypes } from 'react'; -import { getSyntaxes } from './richText'; -import MarkupItReactRenderer from '../MarkupItReactRenderer/index'; -import previewStyle from './defaultPreviewStyle'; - -const MarkdownPreview = ({ value, getAsset }) => { - if (value == null) { - return null; - } - - const schema = { - 'mediaproxy': ({ token }) => ( // eslint-disable-line - {token.getIn(['data', - ), - }; - - const { markdown } = getSyntaxes(); - return ( -
- -
- ); -}; - -MarkdownPreview.propTypes = { - getAsset: PropTypes.func.isRequired, - value: PropTypes.string, -}; - -export default MarkdownPreview; diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js similarity index 78% rename from src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js rename to src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js index 5b72258a..e8859a2a 100644 --- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js @@ -3,7 +3,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { padStart } from 'lodash'; -import MarkupItReactRenderer from '../'; +import MarkdownPreview from '../index'; describe('MarkitupReactRenderer', () => { describe('Markdown rendering', () => { @@ -35,7 +35,7 @@ Text with **bold** & _em_ elements ###### H6 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -44,7 +44,7 @@ Text with **bold** & _em_ elements for (const heading of [...Array(6).keys()]) { it(`should render Heading ${ heading + 1 }`, () => { const value = padStart(' Title', heading + 7, '#'); - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); } @@ -55,15 +55,15 @@ Text with **bold** & _em_ elements const value = ` 1. ol item 1 1. ol item 2 - * Sublist 1 - * Sublist 2 - * Sublist 3 - 1. Sub-Sublist 1 - 1. Sub-Sublist 2 - 1. Sub-Sublist 3 + * Sublist 1 + * Sublist 2 + * Sublist 3 + 1. Sub-Sublist 1 + 1. Sub-Sublist 2 + 1. Sub-Sublist 3 1. ol item 3 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -77,7 +77,7 @@ 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(); }); }); @@ -85,13 +85,13 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('Code', () => { it('should render code', () => { const value = 'Use the `printf()` function.'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); it('should render code 2', () => { const value = '``There is a literal backtick (`) here.``'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -113,7 +113,7 @@ 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(); }); }); @@ -122,7 +122,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('HTML rendering', () => { it('should render HTML', () => { const value = '

Paragraph with inline element

'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); diff --git a/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap b/src/components/Widgets/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap similarity index 100% rename from src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap rename to src/components/Widgets/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap diff --git a/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js b/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js new file mode 100644 index 00000000..3efc0700 --- /dev/null +++ b/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js @@ -0,0 +1,59 @@ +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/MarkdownPreview/index.js b/src/components/Widgets/MarkdownPreview/index.js new file mode 100644 index 00000000..cfb09370 --- /dev/null +++ b/src/components/Widgets/MarkdownPreview/index.js @@ -0,0 +1,27 @@ +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 }) => { + const Markdown = unified() + .use(markdownToRemark, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToRehype, { allowDangerousHTML: true }) + .use(cmsPluginToRehype, { getAsset }) + .use(rehypeToReact, { createElement: React.createElement }) + .processSync(value) + .contents; + + return value === null ? null :
{Markdown}
; +}; + +MarkdownPreview.propTypes = { + getAsset: PropTypes.func.isRequired, + value: PropTypes.string, +}; + +export default MarkdownPreview; diff --git a/src/components/Widgets/richText.js b/src/components/Widgets/richText.js deleted file mode 100644 index f6442449..00000000 --- a/src/components/Widgets/richText.js +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint react/prop-types: 0, react/no-multi-comp: 0 */ -import React from 'react'; -import { List, 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 { Icon } from '../UI'; - -/* - * All Rich text widgets (Markdown, for example) should use Slate for text editing and - * MarkupIt to convert between structured formats (Slate JSON, Markdown, HTML, etc.). - * This module Processes and provides Slate nodes and MarkupIt syntaxes augmented with plugins - */ - -let processedPlugins = List([]); - -const nodes = {}; -let augmentedMarkdownSyntax = markdownSyntax; -let augmentedHTMLSyntax = htmlSyntax; - -function processEditorPlugins(plugins) { - // Since the plugin list is immutable, a simple comparisson is enough - // to determine whether we need to process again. - if (plugins === processedPlugins) return; - - plugins.forEach((plugin) => { - const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => ( - { data: plugin.fromBlock(match) } - )); - - const markdownRule = basicRule.toText((state, token) => ( - `${ plugin.toBlock(token.getData().toObject()) }\n\n` - )); - - const htmlRule = basicRule.toText((state, token) => ( - plugin.toPreview(token.getData().toObject()) - )); - - const nodeRenderer = (props) => { - const { node, state } = props; - const isFocused = state.selection.hasEdgeIn(node); - const className = isFocused ? 'plugin active' : 'plugin'; - return ( -
-
-
- {plugin.fields.map(field => `${ field.label }: “${ node.data.get(field.name) }”`)} -
-
- ); - }; - - augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(markdownRule); - augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(htmlRule); - nodes[plugin.id] = nodeRenderer; - }); - - processedPlugins = plugins; -} - -function processAssetProxyPlugins(getAsset) { - const assetProxyRule = MarkupIt.Rule('assetproxy').regExp(reInline.link, (state, match) => { - if (match[0].charAt(0) !== '!') { - // Return if this is not an image - return; - } - - const imgData = Map({ - alt: match[1], - src: match[2], - title: match[3], - }).filter(Boolean); - - return { - data: imgData, - }; - }); - const assetProxyMarkdownRule = assetProxyRule.toText((state, token) => { - const data = token.getData(); - const alt = data.get('alt', ''); - const src = data.get('src', ''); - const title = data.get('title', ''); - - if (title) { - return `![${ alt }](${ src } "${ title }")`; - } else { - return `![${ alt }](${ src })`; - } - }); - const assetProxyHTMLRule = assetProxyRule.toText((state, token) => { - const data = token.getData(); - const alt = data.get('alt', ''); - const src = data.get('src', ''); - return `${`; - }); - - nodes.assetproxy = (props) => { - /* eslint react/prop-types: 0 */ - const { node, state } = props; - const isFocused = state.selection.hasEdgeIn(node); - const className = isFocused ? 'active' : null; - const src = node.data.get('src'); - return ( - - ); - }; - augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(assetProxyMarkdownRule); - augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(assetProxyHTMLRule); -} - -function getPlugins() { - return processedPlugins.map(plugin => ({ - id: plugin.id, - icon: plugin.icon, - fields: plugin.fields, - })).toArray(); -} - -function getNodes() { - return nodes; -} - -function getSyntaxes(getAsset) { - if (getAsset) { - processAssetProxyPlugins(getAsset); - } - return { markdown: augmentedMarkdownSyntax, html: augmentedHTMLSyntax }; -} - -export { processEditorPlugins, getNodes, getSyntaxes, getPlugins }; diff --git a/src/components/stories/MarkupItReactRenderer.js b/src/components/stories/MarkupItReactRenderer.js deleted file mode 100644 index a28e6949..00000000 --- a/src/components/stories/MarkupItReactRenderer.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import markdownSyntax from 'markup-it/syntaxes/markdown'; -import htmlSyntax from 'markup-it/syntaxes/html'; -import MarkupItReactRenderer from '../MarkupItReactRenderer'; -import { storiesOf } from '@kadira/storybook'; - -const mdContent = ` -# Title - -* List 1 -* List 2 -`; - -const htmlContent = ` -

Title

-
    -
  1. List item 1
  2. -
  3. List item 2
  4. -
-`; - -function getAsset(path) { - return path; -} - -storiesOf('MarkupItReactRenderer', module) - .add('Markdown', () => ( - - - )).add('HTML', () => ( - - )); diff --git a/src/components/stories/index.js b/src/components/stories/index.js index 1e73d155..c270c754 100644 --- a/src/components/stories/index.js +++ b/src/components/stories/index.js @@ -2,5 +2,4 @@ import './Card'; import './Icon'; import './Toast'; import './FindBar'; -import './MarkupItReactRenderer'; import './ScrollSync'; diff --git a/src/lib/registry.js b/src/lib/registry.js index 8990c96a..6f5254df 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -1,5 +1,5 @@ import { Map } from 'immutable'; -import { newEditorPlugin } from '../components/Widgets/MarkdownControlElements/plugins'; +import { newEditorPlugin } from '../components/Widgets/MarkdownControl/plugins'; const _registry = { templates: {}, From b293b235bb502e77383ef16bbdc6a4192be7a141 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 13 Jun 2017 11:49:09 -0400 Subject: [PATCH 15/79] fix link creation in visual editor --- .../MarkdownControl/VisualEditor/markdownToProseMirror.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js index fb77f015..911edd69 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js @@ -73,7 +73,7 @@ export default function markdownToProseMirror({ state }) { case 'strong': return { pmType: type }; case 'link': - return { pmType: 'strong' }; + return { pmType: type, attrs: { href: url } }; case 'inlineCode': return { pmType: 'code' }; } From e7ac3a7671e8c38286affacf9e6b5172a5c1c294 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 13 Jun 2017 15:30:11 -0400 Subject: [PATCH 16/79] switch remark options to use gfm, fences --- src/components/Widgets/MarkdownControl/RawEditor/index.js | 2 +- src/components/Widgets/MarkdownControl/VisualEditor/index.js | 4 +++- src/components/Widgets/MarkdownControl/VisualEditor/parser.js | 2 +- src/components/Widgets/MarkdownPreview/index.js | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/MarkdownControl/RawEditor/index.js b/src/components/Widgets/MarkdownControl/RawEditor/index.js index c83de244..18f1c794 100644 --- a/src/components/Widgets/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControl/RawEditor/index.js @@ -31,7 +31,7 @@ function cleanupPaste(paste) { .use(rehypeSanitize) .use(rehypeReparse) .use(rehypeToRemark) - .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToMarkdown, { fences: true, footnotes: true, pedantic: true }) .process(paste); } diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 15265c1d..6585442b 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -150,10 +150,12 @@ export default class Editor extends Component { const { serializer } = this.state; const newState = this.view.state.applyAction(action); const md = serializer.serialize(newState.doc); + console.log(md); const processedMarkdown = unified() .use(markdownToRemark) - .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToMarkdown, { fences: true, commonmark: true, footnotes: true, pedantic: true }) .processSync(md); + console.log(processedMarkdown.contents); this.props.onChange(processedMarkdown.contents); this.view.updateState(newState); if (newState.selection !== this.state.selection) { diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/parser.js b/src/components/Widgets/MarkdownControl/VisualEditor/parser.js index e06c4d2d..9c6a0882 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/parser.js @@ -12,7 +12,7 @@ const state = { activeMarks: Mark.none, textsArray: [] }; */ function parser(src) { const result = unified() - .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToMarkdown, { fences: true, footnotes: true, pedantic: true }) .parse(src); return unified() diff --git a/src/components/Widgets/MarkdownPreview/index.js b/src/components/Widgets/MarkdownPreview/index.js index cfb09370..9a9594ed 100644 --- a/src/components/Widgets/MarkdownPreview/index.js +++ b/src/components/Widgets/MarkdownPreview/index.js @@ -9,7 +9,7 @@ import previewStyle from '../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { const Markdown = unified() - .use(markdownToRemark, { commonmark: true, footnotes: true, pedantic: true }) + .use(markdownToRemark, { footnotes: true, pedantic: true }) .use(remarkToRehype, { allowDangerousHTML: true }) .use(cmsPluginToRehype, { getAsset }) .use(rehypeToReact, { createElement: React.createElement }) From 49b3a628238cd4ebb4293508a4e1f225ed333c19 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 15 Jun 2017 11:36:10 -0400 Subject: [PATCH 17/79] attempt prosemirror update, troubleshooting --- example/config.yml | 2 ++ package.json | 24 +++++++++---------- .../VisualEditor/__tests__/parser.spec.js | 4 ++-- .../MarkdownControl/VisualEditor/index.js | 23 ++++-------------- .../VisualEditor/markdownToProseMirror.js | 7 +++++- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/example/config.yml b/example/config.yml index 464d2c38..04a18dc0 100644 --- a/example/config.yml +++ b/example/config.yml @@ -15,6 +15,8 @@ collections: # A list of collections the CMS should be able to edit - {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"} - {label: "Cover Image", name: "image", widget: "image", required: false, tagname: ""} - {label: "Body", name: "body", widget: "markdown"} + - {label: "Body B", name: "bodyb", widget: "markdown"} + - {label: "Body C", name: "bodyc", widget: "markdown"} meta: - {label: "SEO Description", name: "description", widget: "text"} diff --git a/package.json b/package.json index b155b71b..e231f82c 100644 --- a/package.json +++ b/package.json @@ -119,18 +119,18 @@ "preliminaries-parser-toml": "1.1.0", "preliminaries-parser-yaml": "1.1.0", "prismjs": "^1.5.1", - "prosemirror-commands": "^0.16.0", - "prosemirror-history": "^0.16.0", - "prosemirror-inputrules": "^0.16.0", - "prosemirror-keymap": "^0.16.0", - "prosemirror-markdown": "^0.16.0", - "prosemirror-model": "^0.16.0", - "prosemirror-schema-basic": "^0.16.0", - "prosemirror-schema-list": "^0.16.0", - "prosemirror-schema-table": "^0.16.0", - "prosemirror-state": "^0.16.0", - "prosemirror-transform": "^0.16.0", - "prosemirror-view": "^0.16.0", + "prosemirror-commands": "^0.17.0", + "prosemirror-history": "^0.17.0", + "prosemirror-inputrules": "^0.17.0", + "prosemirror-keymap": "^0.17.0", + "prosemirror-markdown": "^0.17.0", + "prosemirror-model": "^0.17.0", + "prosemirror-schema-basic": "^0.17.0", + "prosemirror-schema-list": "^0.17.0", + "prosemirror-schema-table": "^0.17.0", + "prosemirror-state": "^0.17.0", + "prosemirror-transform": "^0.17.0", + "prosemirror-view": "^0.17.0", "react": "^15.1.0", "react-addons-css-transition-group": "^15.3.1", "react-autosuggest": "^7.0.1", diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js index 594f2a32..5d35d45d 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js @@ -4,8 +4,8 @@ import { schema } from "prosemirror-markdown"; import makeParser from '../parser'; const testSchema = new Schema({ - nodes: schema.nodeSpec, - marks: schema.markSpec, + nodes: schema.spec.nodes, + marks: schema.spec.marks, }); // Temporary plugins test, uses preloaded plugins from ../parser diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 6585442b..5adc913b 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -48,9 +48,9 @@ function buildInputRules(schema) { } function markActive(state, type) { - const { from, to, empty } = state.selection; + const { from, to, empty, $from } = state.selection; if (empty) { - return type.isInSet(state.storedMarks || state.doc.marksAt(from)); + return type.isInSet(state.storedMarks || $from.marks()); } return state.doc.rangeHasMark(from, to, type); } @@ -111,6 +111,7 @@ export default class Editor extends Component { this.view = new EditorView(this.ref, { state: this.createEditorState(), onAction: this.handleAction, + dispatchTransaction: this.handleTransaction, }); } @@ -121,18 +122,6 @@ export default class Editor extends Component { return EditorState.create({ doc, schema, - plugins: [ - inputRules({ - rules: allInputRules.concat(buildInputRules(schema)), - }), - keymap(buildKeymap(schema)), - keymap(baseKeymap), - history.history(), - keymap({ - 'Mod-z': history.undo, - 'Mod-y': history.redo, - }), - ], }); } @@ -146,16 +135,14 @@ export default class Editor extends Component { } } - handleAction = (action) => { + handleTransaction = (transaction) => { const { serializer } = this.state; - const newState = this.view.state.applyAction(action); + const newState = this.view.state.apply(transaction); const md = serializer.serialize(newState.doc); - console.log(md); const processedMarkdown = unified() .use(markdownToRemark) .use(remarkToMarkdown, { fences: true, commonmark: true, footnotes: true, pedantic: true }) .processSync(md); - console.log(processedMarkdown.contents); this.props.onChange(processedMarkdown.contents); this.view.updateState(newState); if (newState.selection !== this.state.selection) { diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js index 911edd69..6f212121 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js @@ -14,7 +14,12 @@ export default function markdownToProseMirror({ state }) { // on the state object. const { schema, plugins } = state; - return transform; + // return transform; + + return node => { + const result = transform(node); + return result; + }; /** * The MDAST transformer function. From 9c869be8facb0d96e693ff4a56fad54ed2a18864 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 19 Jun 2017 17:15:59 -0400 Subject: [PATCH 18/79] migrate visual editor from prosemirror to slate --- example/config.yml | 2 - package.json | 2 +- .../MarkdownControl/RawEditor/index.js | 12 +- .../MarkdownControl/Toolbar/Toolbar.js | 29 +- .../MarkdownControl/VisualEditor/index.css | 92 ++--- .../MarkdownControl/VisualEditor/index.js | 366 +++++++++++------- .../VisualEditor/remarkSlate.js | 12 + .../VisualEditor/slateRemark.js | 9 + 8 files changed, 299 insertions(+), 225 deletions(-) create mode 100644 src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js create mode 100644 src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js diff --git a/example/config.yml b/example/config.yml index 04a18dc0..464d2c38 100644 --- a/example/config.yml +++ b/example/config.yml @@ -15,8 +15,6 @@ collections: # A list of collections the CMS should be able to edit - {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"} - {label: "Cover Image", name: "image", widget: "image", required: false, tagname: ""} - {label: "Body", name: "body", widget: "markdown"} - - {label: "Body B", name: "bodyb", widget: "markdown"} - - {label: "Body C", name: "bodyc", widget: "markdown"} meta: - {label: "SEO Description", name: "description", widget: "text"} diff --git a/package.json b/package.json index e231f82c..4007d922 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "remark-stringify": "^3.0.1", "selection-position": "^1.0.0", "semaphore": "^1.0.5", - "slate": "^0.14.14", + "slate": "^0.20.3", "slate-drop-or-paste-images": "^0.2.0", "slug": "^0.9.1", "textarea-caret-position": "^0.1.1", diff --git a/src/components/Widgets/MarkdownControl/RawEditor/index.js b/src/components/Widgets/MarkdownControl/RawEditor/index.js index 18f1c794..ca6bcba7 100644 --- a/src/components/Widgets/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControl/RawEditor/index.js @@ -332,11 +332,13 @@ export default class RawEditor extends React.Component { > - - - - - + { buttonsConfig.map((btn, i) => ( + + ))} code { - display: block; - width: 100%; - overflow-y: auto; - background-color: #000; - color: #ccc; - border-radius: var(--borderRadius); - padding: 10px; - } + & ul, + & ol { + padding-left: 30px; } - & .ProseMirror-content { + & pre { white-space: pre-wrap; } - & .ProseMirror-drop-target { - position: absolute; - width: 1px; - background: #666; - pointer-events: none; + & pre > code { + display: block; + width: 100%; + overflow-y: auto; + background-color: #000; + color: #ccc; + border-radius: var(--borderRadius); + padding: 10px; } - & .ProseMirror-content ul, & .ProseMirror-content ol { - padding-left: 30px; - cursor: default; - } - - & .ProseMirror-content blockquote { + & blockquote { padding-left: 1em; border-left: 3px solid #eee; margin-left: 0; margin-right: 0; } - - & .ProseMirror-content pre { - white-space: pre-wrap; - } - - & .ProseMirror-content li { - position: relative; - pointer-events: none; /* Don't do weird stuff with marker clicks */ - } - & .ProseMirror-content li > * { - pointer-events: auto; - } - - & .ProseMirror-nodeselection *::selection { background: transparent; } - & .ProseMirror-nodeselection *::-moz-selection { background: transparent; } - - & .ProseMirror-selectednode { - outline: 2px solid #8cf; - } - - /* Make sure li selections wrap around markers */ - - & li.ProseMirror-selectednode { - outline: none; - } - - & li.ProseMirror-selectednode:after { - content: ""; - position: absolute; - left: -32px; - right: -2px; top: -2px; bottom: -2px; - border: 2px solid #8cf; - pointer-events: none; - } } diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 5adc913b..7cbfb7f7 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -1,19 +1,13 @@ import React, { Component, PropTypes } from 'react'; -import { Map } from 'immutable'; -import { Schema } from 'prosemirror-model'; -import { EditorState } from 'prosemirror-state'; -import { EditorView } from 'prosemirror-view'; -import history from 'prosemirror-history'; -import { - blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule, - inputRules, allInputRules, -} from 'prosemirror-inputrules'; -import { keymap } from 'prosemirror-keymap'; -import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; -import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; +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 registry from '../../../../lib/registry'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { buildKeymap } from './keymap'; @@ -32,28 +26,7 @@ function processUrl(url) { return `/${ url }`; } -const ruleset = { - blockquote: [blockQuoteRule], - ordered_list: [orderedListRule], - bullet_list: [bulletListRule], - code_block: [codeBlockRule], - heading: [headingRule, 6], -}; - -function buildInputRules(schema) { - return Map(ruleset) - .filter(rule => schema.nodes[rule]) - .map(rule => rule[0].apply(rule[0].slice(1))) - .toArray(); -} - -function markActive(state, type) { - const { from, to, empty, $from } = state.selection; - if (empty) { - return type.isInSet(state.storedMarks || $from.marks()); - } - return state.doc.rangeHasMark(from, to, type); -} +const DEFAULT_NODE = 'paragraph'; function schemaWithPlugins(schema, plugins) { let nodeSpec = schema.nodeSpec; @@ -94,115 +67,229 @@ function createSerializer(schema, plugins) { return serializer; } +const BLOCK_TAGS = { + p: 'paragraph', + li: 'list-item', + ul: 'bulleted-list', + ol: 'numbered-list', + blockquote: 'quote', + pre: 'code', + h1: 'heading-one', + h2: 'heading-two', + h3: 'heading-three', + h4: 'heading-four', + h5: 'heading-five', + h6: 'heading-six' +} + +const MARK_TAGS = { + strong: 'bold', + em: 'italic', + u: 'underline', + s: 'strikethrough', + code: 'code' +} + +const NODE_COMPONENTS = { + 'quote': props =>
{props.children}
, + 'bulleted-list': props =>
    {props.children}
, + 'heading-one': props =>

{props.children}

, + 'heading-two': props =>

{props.children}

, + 'heading-three': props =>

{props.children}

, + 'heading-four': props =>

{props.children}

, + 'heading-five': props =>
{props.children}
, + 'heading-six': props =>
{props.children}
, + 'list-item': props =>
  • {props.children}
  • , + 'numbered-list': props =>
      {props.children}
    , + 'code': props =>
    {props.children}
    , + 'link': props => {props.children}, + 'paragraph': props =>

    {props.children}

    , +}; + +const MARK_COMPONENTS = { + bold: props => {props.children}, + code: props => {props.children}, + italic: props => {props.children}, + underlined: props => {props.children}, +}; + +const RULES = [ + { + deserialize(el, next) { + const block = BLOCK_TAGS[el.tagName] + if (!block) return + return { + kind: 'block', + type: block, + nodes: next(el.children) + } + }, + serialize(entity, children) { + const component = NODE_COMPONENTS[entity.type] + if (!component) { + return; + } + return component({ children }); + } + }, + { + deserialize(el, next) { + const mark = MARK_TAGS[el.tagName] + if (!mark) return + return { + kind: 'mark', + type: mark, + nodes: next(el.children) + } + }, + serialize(entity, children) { + const component = MARK_COMPONENTS[entity.type] + if (!component) { + return; + } + return component({ children }); + } + }, + { + // Special case for code blocks, which need to grab the nested children. + deserialize(el, next) { + if (el.tagName != 'pre') return + const code = el.children[0] + const children = code && code.tagName == 'code' + ? code.children + : el.children + + return { + kind: 'block', + type: 'code', + nodes: next(children) + } + }, + }, + { + // Special case for links, to grab their href. + deserialize(el, next) { + if (el.tagName != 'a') return + return { + kind: 'inline', + type: 'link', + nodes: next(el.children), + data: { + href: el.attribs.href + } + } + }, + }, +] + +const serializer = new SlateHtml({ rules: RULES }); + export default class Editor extends Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); - const schema = schemaWithPlugins(markdownSchema, plugins); + const html = unified() + .use(markdownToRemark) + .use(remarkToRehype) + .use(rehypeToHtml) + .processSync(this.props.value || '') + .contents; this.state = { + editorState: serializer.deserialize(html), + schema: { + nodes: NODE_COMPONENTS, + marks: MARK_COMPONENTS, + }, plugins, - schema, - parser: createMarkdownParser(schema, plugins), - serializer: createSerializer(schema, plugins), }; } - componentDidMount() { - this.view = new EditorView(this.ref, { - state: this.createEditorState(), - onAction: this.handleAction, - dispatchTransaction: this.handleTransaction, - }); - } - - createEditorState() { - const { schema, parser } = this.state; - const doc = parser.parse(this.props.value || ''); - - return EditorState.create({ - doc, - schema, - }); - } - - componentDidUpdate(prevProps, prevState) { - const editorValue = this.state.serializer.serialize(this.view.state.doc); - // Check that the content of the editor is well synchronized with the props value after rendering. - // Sometimes the editor isn't well updated (eg. after items reordering) - if (editorValue !== this.props.value && editorValue !== prevProps.value) { - // If the content of the editor isn't correct, we update its state with a new one. - this.view.updateState(this.createEditorState()); - } - } - - handleTransaction = (transaction) => { - const { serializer } = this.state; - const newState = this.view.state.apply(transaction); - const md = serializer.serialize(newState.doc); - const processedMarkdown = unified() - .use(markdownToRemark) - .use(remarkToMarkdown, { fences: true, commonmark: true, footnotes: true, pedantic: true }) - .processSync(md); - this.props.onChange(processedMarkdown.contents); - this.view.updateState(newState); - if (newState.selection !== this.state.selection) { - this.handleSelection(newState); - } - this.view.focus(); + handleDocumentChange = (doc, editorState) => { + const html = serializer.serialize(editorState); + const markdown = unified() + .use(htmlToRehype) + .use(rehypeToRemark) + .use(remarkToMarkdown) + .processSync(html) + .contents; + this.props.onChange(markdown); }; - handleSelection = (state) => { - const { schema, selection } = state; - if (selection.from === selection.to) { - const { $from } = selection; - if ($from.parent && $from.parent.type === schema.nodes.paragraph && $from.parent.textContent === '') { - const pos = this.view.coordsAtPos(selection.from); - const editorPos = this.view.content.getBoundingClientRect(); - const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; - this.setState({ selectionPosition }); + hasMark = type => this.state.editorState.marks.some(mark => mark.type === type); + hasBlock = type => this.state.editorState.blocks.some(node => node.type === type); + + handleKeyDown = (e, data, state) => { + if (!data.isMod) { + return; + } + const marks = { + b: 'bold', + i: 'italic', + u: 'underlined', + '`': 'code', + }; + + const mark = marks[data.key]; + + if (mark) { + state = state.transform().toggleMark(mark).apply(); + } + return; + }; + + handleMarkClick = (event, type) => { + event.preventDefault(); + const resolvedState = this.state.editorState.transform().toggleMark(type).apply(); + this.ref.onChange(resolvedState); + this.setState({ editorState: resolvedState }); + }; + + handleBlockClick = (event, type) => { + event.preventDefault(); + let { editorState } = this.state; + const transform = editorState.transform(); + const doc = editorState.document; + const isList = this.hasBlock('list-item') + + // Handle everything except list buttons. + if (!['bulleted-list', 'numbered-list'].includes(type)) { + const isActive = this.hasBlock(type); + const transformed = transform.setBlock(isActive ? DEFAULT_NODE : type); + + if (isList) { + transformed + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list'); } - } else { - const pos = this.view.coordsAtPos(selection.from); - const editorPos = this.view.content.getBoundingClientRect(); - const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left }; - this.setState({ selectionPosition }); } - }; - handleRef = (ref) => { - this.ref = ref; - }; + // Handle the extra wrapping required for list buttons. + else { + const isType = editorState.blocks.some(block => { + return !!doc.getClosest(block.key, parent => parent.type === type); + }); - handleHeader = level => ( - () => { - const { schema } = this.state; - const state = this.view.state; - const { $from, to, node } = state.selection; - let nodeType = schema.nodes.heading; - let attrs = { level }; - let inHeader = node && node.hasMarkup(nodeType, attrs); - if (!inHeader) { - inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs); + if (isList && isType) { + transform + .setBlock(DEFAULT_NODE) + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list'); + } else if (isList) { + transform + .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') + .wrapBlock(type); + } else { + transform + .setBlock('list-item') + .wrapBlock(type); } - if (inHeader) { - nodeType = schema.nodes.paragraph; - attrs = {}; - } - - const command = setBlockType(nodeType, { level }); - command(state, this.handleAction); } - ); - handleBold = () => { - const command = toggleMark(this.state.schema.marks.strong); - command(this.view.state, this.handleAction); + const resolvedState = transform.focus().apply(); + this.ref.onChange(resolvedState); + this.setState({ editorState: resolvedState }); }; - handleItalic = () => { - const command = toggleMark(this.state.schema.marks.em); - command(this.view.state, this.handleAction); - }; handleLink = () => { let url = null; @@ -216,7 +303,7 @@ export default class Editor extends Component { handlePluginSubmit = (plugin, data) => { const { schema } = this.state; const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; - this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); + //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); }; handleDragEnter = (e) => { @@ -263,7 +350,7 @@ export default class Editor extends Component { } nodes.forEach((node) => { - this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action()); + //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action()); }); }; @@ -271,6 +358,12 @@ export default class Editor extends Component { this.props.onMode('raw'); }; + getButtonProps = (type, isBlock) => { + const handler = isBlock ? this.handleBlockClick: this.handleMarkClick; + const isActive = isBlock ? this.hasBlock : this.hasMark; + return { onAction: e => handler(e, type), active: isActive(type) }; + }; + render() { const { onAddAsset, onRemoveAsset, getAsset } = this.props; const { plugins, selectionPosition, dragging } = this.state; @@ -293,11 +386,13 @@ export default class Editor extends Component { > -
    + this.setState({ editorState })} + onDocumentChange={this.handleDocumentChange} + onKeyDown={this.onKeyDown} + ref={ref => this.ref = ref} + spellCheck + />
    ); } diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js b/src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js new file mode 100644 index 00000000..2251a9a5 --- /dev/null +++ b/src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js @@ -0,0 +1,12 @@ +function remarkToSlate(opts) { + + console.log(1); + return transform; + + function transform(node) { + console.log(2); + console.log(node); + } +} + +export default remarkToSlate; diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js b/src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js new file mode 100644 index 00000000..81cf1e44 --- /dev/null +++ b/src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js @@ -0,0 +1,9 @@ +function slateToRemark(opts) { + return transform; + + function transform(node) { + console.log(node); + } +} + +export default slateToRemark; From e01c077efb03e929998102f4a58f2ce561862e22 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 20 Jun 2017 16:52:06 -0400 Subject: [PATCH 19/79] fix empty initial state for rte --- src/components/Widgets/MarkdownControl/VisualEditor/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 7cbfb7f7..97308c10 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -195,7 +195,7 @@ export default class Editor extends Component { .processSync(this.props.value || '') .contents; this.state = { - editorState: serializer.deserialize(html), + editorState: serializer.deserialize(html || '

    '), schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, From e682189410b947db42ef57278df5ada04df6cd71 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 21 Jun 2017 15:58:56 -0400 Subject: [PATCH 20/79] only render editor page controls/previews on change --- src/components/PreviewPane/Preview.js | 19 +++++---- src/components/PreviewPane/PreviewContent.js | 22 ++++++++++ src/components/PreviewPane/PreviewPane.js | 42 ++++++++----------- src/components/Widgets/ControlHOC.js | 4 ++ .../MarkdownControl/VisualEditor/index.js | 2 + src/components/Widgets/PreviewHOC.js | 14 +++++++ 6 files changed, 71 insertions(+), 32 deletions(-) create mode 100644 src/components/PreviewPane/PreviewContent.js create mode 100644 src/components/Widgets/PreviewHOC.js diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js index 1875d1b1..e4409b5a 100644 --- a/src/components/PreviewPane/Preview.js +++ b/src/components/PreviewPane/Preview.js @@ -9,15 +9,18 @@ const style = { fontFamily: 'Roboto, "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif', }; -export default function Preview({ collection, fields, widgetFor }) { - if (!collection || !fields) { - return null; +export default class Preview extends React.Component { + render() { + const { collection, fields, widgetFor } = this.props; + if (!collection || !fields) { + return null; + } + return ( +
    + {fields.filter(isVisible).map(field => widgetFor(field.get('name')))} +
    + ); } - return ( -
    - {fields.filter(isVisible).map(field => widgetFor(field.get('name')))} -
    - ); } Preview.propTypes = { diff --git a/src/components/PreviewPane/PreviewContent.js b/src/components/PreviewPane/PreviewContent.js new file mode 100644 index 00000000..dce067fe --- /dev/null +++ b/src/components/PreviewPane/PreviewContent.js @@ -0,0 +1,22 @@ +import React, { PropTypes } from 'react'; +import { ScrollSyncPane } from '../ScrollSync'; + +// We need to create a lightweight component here so that we can +// access the context within the Frame. This allows us to attach +// the ScrollSyncPane to the body. +class PreviewContent extends React.Component { + render() { + const { previewComponent, previewProps } = this.props; + return ( + + {React.createElement(previewComponent, previewProps)} + + ); + } +} + +PreviewContent.contextTypes = { + document: PropTypes.any, +}; + +export default PreviewContent; diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index 4117b597..c704b3fc 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -2,11 +2,12 @@ import React, { PropTypes } from 'react'; import { List, Map } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Frame from 'react-frame-component'; -import { ScrollSyncPane } from '../ScrollSync'; import registry from '../../lib/registry'; import { resolveWidget } from '../Widgets'; import { selectTemplateName, selectInferedField } from '../../reducers/collections'; import { INFERABLE_FIELDS } from '../../constants/fieldInference'; +import PreviewContent from './PreviewContent.js'; +import PreviewHOC from '../Widgets/PreviewHOC'; import Preview from './Preview'; import styles from './PreviewPane.css'; @@ -16,15 +17,18 @@ export default class PreviewPane extends React.Component { const { fieldsMetaData, getAsset, entry } = props; const widget = resolveWidget(field.get('widget')); - return !widget.preview ? null : React.createElement(widget.preview, { - field, - key: field.get('name'), - value: value && Map.isMap(value) ? value.get(field.get('name')) : value, - metadata: fieldsMetaData && fieldsMetaData.get(field.get('name')), - getAsset, - entry, - fieldsMetaData, - }); + return !widget.preview ? null : ( + + ); }; inferedFields = {}; @@ -118,7 +122,9 @@ export default class PreviewPane extends React.Component { return null; } - const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview; + const previewComponent = + registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || + Preview; this.inferFields(); @@ -135,18 +141,6 @@ export default class PreviewPane extends React.Component { return ; } - // We need to create a lightweight component here so that we can - // access the context within the Frame. This allows us to attach - // the ScrollSyncPane to the body. - const PreviewContent = (props, { document: iFrameDocument }) => ( - - {React.createElement(component, previewProps)} - ); - - PreviewContent.contextTypes = { - document: PropTypes.any, - }; - return (
    `} - >); + >); } } diff --git a/src/components/Widgets/ControlHOC.js b/src/components/Widgets/ControlHOC.js index 8e358905..40a7eeb0 100644 --- a/src/components/Widgets/ControlHOC.js +++ b/src/components/Widgets/ControlHOC.js @@ -22,6 +22,10 @@ class ControlHOC extends Component { getAsset: PropTypes.func.isRequired, }; + shouldComponentUpdate(nextProps) { + return nextProps.value !== this.props.value; + } + processInnerControlRef = (wrappedControl) => { if (!wrappedControl) return; this.wrappedControlValid = wrappedControl.isValid || truthy; diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 97308c10..1239f369 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -166,6 +166,7 @@ const RULES = [ } }, }, + /* { // Special case for links, to grab their href. deserialize(el, next) { @@ -180,6 +181,7 @@ const RULES = [ } }, }, + */ ] const serializer = new SlateHtml({ rules: RULES }); diff --git a/src/components/Widgets/PreviewHOC.js b/src/components/Widgets/PreviewHOC.js new file mode 100644 index 00000000..dc25977a --- /dev/null +++ b/src/components/Widgets/PreviewHOC.js @@ -0,0 +1,14 @@ +import React from 'react'; + +class PreviewHOC extends React.Component { + shouldComponentUpdate(nextProps) { + return nextProps.value !== this.props.value; + } + + render() { + const { previewComponent, ...props } = this.props; + return React.createElement(previewComponent, props); + } +} + +export default PreviewHOC; From 22a8da11a4ccedcb030d9eac39bfbb5659b8ea9e Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 21 Jun 2017 16:53:54 -0400 Subject: [PATCH 21/79] fix rte link serialization --- .../MarkdownControl/VisualEditor/index.js | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 1239f369..81628983 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -90,27 +90,36 @@ const MARK_TAGS = { code: 'code' } -const NODE_COMPONENTS = { - 'quote': props =>
    {props.children}
    , +const BLOCK_COMPONENTS = { + 'paragraph': props =>

    {props.children}

    , + 'list-item': props =>
  • {props.children}
  • , 'bulleted-list': props =>
      {props.children}
    , + 'numbered-list': props =>
      {props.children}
    , + 'quote': props =>
    {props.children}
    , + 'code': props =>
    {props.children}
    , 'heading-one': props =>

    {props.children}

    , 'heading-two': props =>

    {props.children}

    , 'heading-three': props =>

    {props.children}

    , 'heading-four': props =>

    {props.children}

    , 'heading-five': props =>
    {props.children}
    , 'heading-six': props =>
    {props.children}
    , - 'list-item': props =>
  • {props.children}
  • , - 'numbered-list': props =>
      {props.children}
    , - 'code': props =>
    {props.children}
    , - 'link': props => {props.children}, - 'paragraph': props =>

    {props.children}

    , }; +const NODE_COMPONENTS = { + ...BLOCK_COMPONENTS, + 'link': props => { + const href = props.node && props.node.getIn(['data', 'href']) || props.href; + return {props.children}; + }, +} + + const MARK_COMPONENTS = { bold: props => {props.children}, - code: props => {props.children}, italic: props => {props.children}, underlined: props => {props.children}, + strikethrough: props => {props.children}, + code: props => {props.children}, }; const RULES = [ @@ -125,7 +134,7 @@ const RULES = [ } }, serialize(entity, children) { - const component = NODE_COMPONENTS[entity.type] + const component = BLOCK_COMPONENTS[entity.type] if (!component) { return; } @@ -166,7 +175,6 @@ const RULES = [ } }, }, - /* { // Special case for links, to grab their href. deserialize(el, next) { @@ -180,8 +188,19 @@ const RULES = [ } } }, + serialize(entity, children) { + if (entity.type !== 'link') { + return; + } + const data = entity.get('data'); + const props = { + href: data.get('href'), + attributes: data.get('attributes'), + children, + }; + return NODE_COMPONENTS.link(props); + } }, - */ ] const serializer = new SlateHtml({ rules: RULES }); From bc721337defdd3fd2a74a079e038b0de0cc83ace Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 21 Jun 2017 17:08:53 -0400 Subject: [PATCH 22/79] set rte focus after toolbar click --- .../Widgets/MarkdownControl/VisualEditor/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 81628983..2acf9849 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -260,7 +260,7 @@ export default class Editor extends Component { handleMarkClick = (event, type) => { event.preventDefault(); - const resolvedState = this.state.editorState.transform().toggleMark(type).apply(); + const resolvedState = this.state.editorState.transform().focus().toggleMark(type).apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; @@ -268,7 +268,7 @@ export default class Editor extends Component { handleBlockClick = (event, type) => { event.preventDefault(); let { editorState } = this.state; - const transform = editorState.transform(); + const transform = editorState.transform().focus(); const doc = editorState.document; const isList = this.hasBlock('list-item') @@ -306,7 +306,7 @@ export default class Editor extends Component { } } - const resolvedState = transform.focus().apply(); + const resolvedState = transform.apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; From 1c0bb6a8773feed22c5992806eaceef3e3e86033 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 21 Jun 2017 13:49:43 -0400 Subject: [PATCH 23/79] implement widget data serialization for rte perf --- src/actions/entries.js | 58 ++++++++++++++++--- src/components/Widgets/ControlHOC.js | 3 +- .../MarkdownControl/RawEditor/index.js | 46 ++++++++++----- .../MarkdownControl/VisualEditor/index.js | 34 ++++++----- .../VisualEditor/remarkSlate.js | 12 ---- .../VisualEditor/slateRemark.js | 9 --- .../Widgets/MarkdownPreview/index.js | 10 +--- src/components/Widgets/serializers.js | 5 ++ 8 files changed, 110 insertions(+), 67 deletions(-) delete mode 100644 src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js delete mode 100644 src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js create mode 100644 src/components/Widgets/serializers.js diff --git a/src/actions/entries.js b/src/actions/entries.js index ad54d99a..2fb3c4c8 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -1,10 +1,12 @@ -import { List } from 'immutable'; +import { List, Map } from 'immutable'; +import { isArray, isObject, isEmpty, isNil } from 'lodash'; import { actions as notifActions } from 'redux-notifications'; import { closeEntry } from './editor'; import { currentBackend } from '../backends/backend'; import { getIntegrationProvider } from '../integrations'; import { getAsset, selectIntegration } from '../reducers'; import { createEntry } from '../valueObjects/Entry'; +import { controlValueSerializers } from '../components/Widgets/serializers'; const { notifSend } = notifActions; @@ -216,9 +218,27 @@ export function loadEntry(collection, slug) { const backend = currentBackend(state.config); dispatch(entryLoading(collection, slug)); return backend.getEntry(collection, slug) - .then(loadedEntry => ( - dispatch(entryLoaded(collection, loadedEntry)) - )) + .then(loadedEntry => { + const deserializeValues = (values, fields) => { + return fields.reduce((acc, field) => { + const fieldName = field.get('name'); + const value = values[fieldName]; + const serializer = controlValueSerializers[field.get('widget')]; + if (isArray(value) && !isEmpty(value)) { + acc[fieldName] = value.map(val => deserializeValues(val, field.get('fields'))); + } else if (isObject(value) && !isEmpty(value)) { + acc[fieldName] = deserializeValues(value, field.get('fields')); + } else if (serializer && !isNil(value)) { + acc[fieldName] = serializer.deserialize(value); + } else if (!isNil(value)) { + acc[fieldName] = value; + } + return acc; + }, {}); + }; + loadedEntry.data = deserializeValues(loadedEntry.data, collection.get('fields')); + return dispatch(entryLoaded(collection, loadedEntry)) + }) .catch((error) => { dispatch(notifSend({ message: `Failed to load entry: ${ error.message }`, @@ -265,20 +285,40 @@ export function persistEntry(collection) { // Early return if draft contains validation errors if (!entryDraft.get('fieldsErrors').isEmpty()) return Promise.reject(); - + const backend = currentBackend(state.config); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - dispatch(entryPersisting(collection, entry)); + const serializeValues = (values, fields) => { + return fields.reduce((acc, field) => { + const fieldName = field.get('name'); + const value = values.get(fieldName); + const serializer = controlValueSerializers[field.get('widget')]; + if (List.isList(value)) { + return acc.set(fieldName, value.map(val => serializeValues(val, field.get('fields')))); + } else if (Map.isMap(value)) { + return acc.set(fieldName, serializeValues(value, field.get('fields'))); + } else if (serializer && !isNil(value)) { + return acc.set(fieldName, serializer.serialize(value)); + } else if (!isNil(value)) { + return acc.set(fieldName, value); + } + return acc; + }, Map()); + }; + const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); + const transformedEntry = entry.set('data', transformedData); + const transformedEntryDraft = entryDraft.set('entry', transformedEntry); + dispatch(entryPersisting(collection, transformedEntry)); return backend - .persistEntry(state.config, collection, entryDraft, assetProxies.toJS()) + .persistEntry(state.config, collection, transformedEntryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', kind: 'success', dismissAfter: 4000, })); - return dispatch(entryPersisted(collection, entry)); + return dispatch(entryPersisted(collection, transformedEntry)); }) .catch((error) => { dispatch(notifSend({ @@ -286,7 +326,7 @@ export function persistEntry(collection) { kind: 'danger', dismissAfter: 8000, })); - return dispatch(entryPersistFail(collection, entry, error)); + return dispatch(entryPersistFail(collection, transformedEntry, error)); }); }; } diff --git a/src/components/Widgets/ControlHOC.js b/src/components/Widgets/ControlHOC.js index 40a7eeb0..8c4d46e8 100644 --- a/src/components/Widgets/ControlHOC.js +++ b/src/components/Widgets/ControlHOC.js @@ -1,5 +1,6 @@ import React, { Component, PropTypes } from 'react'; import ImmutablePropTypes from "react-immutable-proptypes"; +import isEqual from 'lodash/isEqual'; const truthy = () => ({ error: false }); @@ -23,7 +24,7 @@ class ControlHOC extends Component { }; shouldComponentUpdate(nextProps) { - return nextProps.value !== this.props.value; + return this.props.value !== nextProps.value; } processInnerControlRef = (wrappedControl) => { diff --git a/src/components/Widgets/MarkdownControl/RawEditor/index.js b/src/components/Widgets/MarkdownControl/RawEditor/index.js index ca6bcba7..ceec01f8 100644 --- a/src/components/Widgets/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControl/RawEditor/index.js @@ -1,5 +1,8 @@ import React, { PropTypes } from 'react'; 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'; @@ -31,7 +34,7 @@ function cleanupPaste(paste) { .use(rehypeSanitize) .use(rehypeReparse) .use(rehypeToRemark) - .use(remarkToMarkdown, { fences: true, footnotes: true, pedantic: true }) + .use(remarkToMarkdown) .process(paste); } @@ -72,7 +75,10 @@ export default class RawEditor extends React.Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); - this.state = { plugins }; + this.state = { + value: this.props.value, + plugins, + }; this.shortcuts = { meta: { b: this.handleBold, @@ -84,6 +90,13 @@ export default class RawEditor extends React.Component { componentDidMount() { this.updateHeight(); this.element.addEventListener('paste', this.handlePaste, false); + const markdown = unified() + .use(htmlToRehype) + .use(rehypeToRemark) + .use(remarkToMarkdown) + .processSync(this.state.value) + .contents; + this.setState({ value: markdown }); } componentDidUpdate() { @@ -101,7 +114,7 @@ export default class RawEditor extends React.Component { getSelection() { const start = this.element.selectionStart; const end = this.element.selectionEnd; - const selected = (this.props.value || '').substr(start, end - start); + const selected = (this.state.value || '').substr(start, end - start); return { start, end, selected }; } @@ -131,22 +144,22 @@ export default class RawEditor extends React.Component { const afterSelection = value.substr(selection.end); this.newSelection = newSelection; - this.props.onChange(beforeSelection + changed + afterSelection); + this.handleChange(beforeSelection + changed + afterSelection); } replaceSelection(chars) { - const value = this.props.value || ''; + const value = this.state.value || ''; const selection = this.getSelection(); const newSelection = Object.assign({}, selection); const beforeSelection = value.substr(0, selection.start); const afterSelection = value.substr(selection.end); newSelection.end = selection.start + chars.length; this.newSelection = newSelection; - this.props.onChange(beforeSelection + chars + afterSelection); + this.handleChange(beforeSelection + chars + afterSelection); } toggleHeader(header) { - const value = this.props.value || ''; + const value = this.state.value || ''; const selection = this.getSelection(); const newSelection = Object.assign({}, selection); const lastNewline = value.lastIndexOf('\n', selection.start); @@ -167,7 +180,7 @@ export default class RawEditor extends React.Component { newSelection.end += header.length + 1; } this.newSelection = newSelection; - this.props.onChange(beforeHeader + chars + afterHeader); + this.handleChange(beforeHeader + chars + afterHeader); } updateHeight() { @@ -208,7 +221,7 @@ export default class RawEditor extends React.Component { }; handleSelection = () => { - const value = this.props.value || ''; + const value = this.state.value || ''; const selection = this.getSelection(); if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) { try { @@ -236,8 +249,15 @@ export default class RawEditor extends React.Component { }; handleChange = (e) => { - this.props.onChange(e.target.value); + const html = unified() + .use(markdownToRemark) + .use(remarkToRehype) + .use(rehypeToHtml) + .processSync(e.target.value) + .contents; + this.props.onChange(html); this.updateHeight(); + this.setState({ value: e.target.value }); }; handlePluginSubmit = (plugin, data) => { @@ -293,7 +313,7 @@ export default class RawEditor extends React.Component { }; handlePaste = (e) => { - const { value, onChange } = this.props; + const { value } = this.props; const selection = this.getSelection(); const beforeSelection = value.substr(0, selection.start); const afterSelection = value.substr(selection.end); @@ -302,7 +322,7 @@ export default class RawEditor extends React.Component { const newSelection = Object.assign({}, selection); newSelection.start = newSelection.end = beforeSelection.length + paste.length; this.newSelection = newSelection; - onChange(beforeSelection + paste + afterSelection); + this.handleChange(beforeSelection + paste + afterSelection); }); }; @@ -352,7 +372,7 @@ export default class RawEditor extends React.Component { className={styles.textarea} inputRef={this.handleRef} className={styles.textarea} - value={this.props.value || ''} + value={this.state.value || ''} onKeyDown={this.handleKey} onChange={this.handleChange} onSelect={this.handleSelection} diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 2acf9849..76e586ff 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -9,6 +9,7 @@ import remarkToMarkdown from 'remark-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import registry from '../../../../lib/registry'; +import { registerControlValueSerializer } from '../../serializers'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { buildKeymap } from './keymap'; import createMarkdownParser from './parser'; @@ -16,6 +17,23 @@ import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../UI/Sticky/Sticky'; import styles from './index.css'; +// Register handler to transform html to markdown before persist +registerControlValueSerializer('markdown', { + serialize: value => unified() + .use(htmlToRehype) + .use(htmlToRehype) + .use(rehypeToRemark) + .use(remarkToMarkdown) + .processSync(value) + .contents, + deserialize: value => unified() + .use(markdownToRemark) + .use(remarkToRehype) + .use(rehypeToHtml) + .processSync(value) + .contents +}); + function processUrl(url) { if (url.match(/^(https?:\/\/|mailto:|\/)/)) { return url; @@ -209,14 +227,8 @@ export default class Editor extends Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); - const html = unified() - .use(markdownToRemark) - .use(remarkToRehype) - .use(rehypeToHtml) - .processSync(this.props.value || '') - .contents; this.state = { - editorState: serializer.deserialize(html || '

    '), + editorState: serializer.deserialize(this.props.value || '

    '), schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, @@ -227,13 +239,7 @@ export default class Editor extends Component { handleDocumentChange = (doc, editorState) => { const html = serializer.serialize(editorState); - const markdown = unified() - .use(htmlToRehype) - .use(rehypeToRemark) - .use(remarkToMarkdown) - .processSync(html) - .contents; - this.props.onChange(markdown); + this.props.onChange(html); }; hasMark = type => this.state.editorState.marks.some(mark => mark.type === type); diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js b/src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js deleted file mode 100644 index 2251a9a5..00000000 --- a/src/components/Widgets/MarkdownControl/VisualEditor/remarkSlate.js +++ /dev/null @@ -1,12 +0,0 @@ -function remarkToSlate(opts) { - - console.log(1); - return transform; - - function transform(node) { - console.log(2); - console.log(node); - } -} - -export default remarkToSlate; diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js b/src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js deleted file mode 100644 index 81cf1e44..00000000 --- a/src/components/Widgets/MarkdownControl/VisualEditor/slateRemark.js +++ /dev/null @@ -1,9 +0,0 @@ -function slateToRemark(opts) { - return transform; - - function transform(node) { - console.log(node); - } -} - -export default slateToRemark; diff --git a/src/components/Widgets/MarkdownPreview/index.js b/src/components/Widgets/MarkdownPreview/index.js index 9a9594ed..8882157c 100644 --- a/src/components/Widgets/MarkdownPreview/index.js +++ b/src/components/Widgets/MarkdownPreview/index.js @@ -8,15 +8,7 @@ import cmsPluginToRehype from './cmsPluginRehype'; import previewStyle from '../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { - const Markdown = unified() - .use(markdownToRemark, { footnotes: true, pedantic: true }) - .use(remarkToRehype, { allowDangerousHTML: true }) - .use(cmsPluginToRehype, { getAsset }) - .use(rehypeToReact, { createElement: React.createElement }) - .processSync(value) - .contents; - - return value === null ? null :
    {Markdown}
    ; + return value === null ? null :
    ; }; MarkdownPreview.propTypes = { diff --git a/src/components/Widgets/serializers.js b/src/components/Widgets/serializers.js new file mode 100644 index 00000000..4986e294 --- /dev/null +++ b/src/components/Widgets/serializers.js @@ -0,0 +1,5 @@ +export const controlValueSerializers = {}; + +export const registerControlValueSerializer = (fieldName, serializer) => { + controlValueSerializers[fieldName] = serializer; +}; From ffbd8d22cc62236074ca7d73ad6b3210aecc25e3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 22 Jun 2017 17:35:47 -0400 Subject: [PATCH 24/79] expose widgetValueSerializer registry --- src/actions/entries.js | 6 +++--- .../Widgets/MarkdownControl/VisualEditor/index.js | 3 +-- src/components/Widgets/serializers.js | 5 ----- src/lib/registry.js | 9 ++++++++- 4 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 src/components/Widgets/serializers.js diff --git a/src/actions/entries.js b/src/actions/entries.js index 2fb3c4c8..6cae3137 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -6,7 +6,7 @@ import { currentBackend } from '../backends/backend'; import { getIntegrationProvider } from '../integrations'; import { getAsset, selectIntegration } from '../reducers'; import { createEntry } from '../valueObjects/Entry'; -import { controlValueSerializers } from '../components/Widgets/serializers'; +import registry from '../lib/registry'; const { notifSend } = notifActions; @@ -223,7 +223,7 @@ export function loadEntry(collection, slug) { return fields.reduce((acc, field) => { const fieldName = field.get('name'); const value = values[fieldName]; - const serializer = controlValueSerializers[field.get('widget')]; + const serializer = registry.getWidgetValueSerializer(field.get('widget')); if (isArray(value) && !isEmpty(value)) { acc[fieldName] = value.map(val => deserializeValues(val, field.get('fields'))); } else if (isObject(value) && !isEmpty(value)) { @@ -293,7 +293,7 @@ export function persistEntry(collection) { return fields.reduce((acc, field) => { const fieldName = field.get('name'); const value = values.get(fieldName); - const serializer = controlValueSerializers[field.get('widget')]; + const serializer = registry.getWidgetValueSerializer(field.get('widget')); if (List.isList(value)) { return acc.set(fieldName, value.map(val => serializeValues(val, field.get('fields')))); } else if (Map.isMap(value)) { diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 76e586ff..05eed69f 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -9,7 +9,6 @@ import remarkToMarkdown from 'remark-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import registry from '../../../../lib/registry'; -import { registerControlValueSerializer } from '../../serializers'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { buildKeymap } from './keymap'; import createMarkdownParser from './parser'; @@ -18,7 +17,7 @@ import { Sticky } from '../../../UI/Sticky/Sticky'; import styles from './index.css'; // Register handler to transform html to markdown before persist -registerControlValueSerializer('markdown', { +registry.registerWidgetValueSerializer('markdown', { serialize: value => unified() .use(htmlToRehype) .use(htmlToRehype) diff --git a/src/components/Widgets/serializers.js b/src/components/Widgets/serializers.js deleted file mode 100644 index 4986e294..00000000 --- a/src/components/Widgets/serializers.js +++ /dev/null @@ -1,5 +0,0 @@ -export const controlValueSerializers = {}; - -export const registerControlValueSerializer = (fieldName, serializer) => { - controlValueSerializers[fieldName] = serializer; -}; diff --git a/src/lib/registry.js b/src/lib/registry.js index 6f5254df..04176b3f 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -6,6 +6,7 @@ const _registry = { previewStyles: [], widgets: {}, editorComponents: Map(), + widgetValueSerializers: {}, }; export default { @@ -36,5 +37,11 @@ export default { }, getEditorComponents() { return _registry.editorComponents; - } + }, + registerWidgetValueSerializer(widgetName, serializer) { + _registry.widgetValueSerializers[widgetName] = serializer; + }, + getWidgetValueSerializer(widgetName) { + return _registry.widgetValueSerializers[widgetName]; + }, }; From 84ed450ac6541242fd102e9b5b4cf2e63755072a Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 22 Jun 2017 17:39:52 -0400 Subject: [PATCH 25/79] add visual editor serializer source doc --- .../Widgets/MarkdownControl/VisualEditor/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js index 05eed69f..b1194613 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -16,7 +16,12 @@ import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../UI/Sticky/Sticky'; import styles from './index.css'; -// Register handler to transform html to markdown before persist +/** + * Slate can serialize to html, but we persist the value as markdown. Serializing + * the html to markdown on every keystroke is a big perf hit, so we'll register + * functions to perform those actions only when necessary, such as after loading + * and before persisting. + */ registry.registerWidgetValueSerializer('markdown', { serialize: value => unified() .use(htmlToRehype) From 09e631ded707db1038f69c6d9cfa202a6d554889 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 23 Jun 2017 13:23:03 -0400 Subject: [PATCH 26/79] allow nested widget previews to update --- src/components/Widgets/PreviewHOC.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Widgets/PreviewHOC.js b/src/components/Widgets/PreviewHOC.js index dc25977a..1ecaa529 100644 --- a/src/components/Widgets/PreviewHOC.js +++ b/src/components/Widgets/PreviewHOC.js @@ -2,7 +2,11 @@ import React from 'react'; class PreviewHOC extends React.Component { shouldComponentUpdate(nextProps) { - return nextProps.value !== this.props.value; + // Only re-render on value change, but always re-render objects and lists. + // Their child widgets will each also be wrapped with this component, and + // will only be updated on value change. + const isWidgetContainer = ['object', 'list'].includes(nextProps.field.get('widget')); + return isWidgetContainer || this.props.value !== nextProps.value; } render() { From e0ca24c6d3cdf503d489aa975957dd76dae199e5 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 23 Jun 2017 14:42:40 -0400 Subject: [PATCH 27/79] add unified config module --- src/components/Widgets.js | 4 ++-- .../MarkdownControl/RawEditor/index.css | 2 +- .../MarkdownControl/RawEditor/index.js | 24 ++++++++++++------- .../MarkdownControl/Toolbar/Toolbar.css | 2 +- .../MarkdownControl/Toolbar/Toolbar.js | 2 +- .../MarkdownControl/Toolbar/ToolbarButton.css | 2 +- .../MarkdownControl/Toolbar/ToolbarButton.js | 2 +- .../Toolbar/ToolbarComponentsMenu.css | 0 .../Toolbar/ToolbarComponentsMenu.js | 0 .../Toolbar/ToolbarPluginForm.css | 2 +- .../Toolbar/ToolbarPluginForm.js | 0 .../Toolbar/ToolbarPluginFormControl.css | 7 ++++++ .../Toolbar/ToolbarPluginFormControl.js | 2 +- .../__snapshots__/parser.spec.js.snap | 0 .../VisualEditor/__tests__/parser.spec.js | 0 .../MarkdownControl/VisualEditor/index.css | 2 +- .../MarkdownControl/VisualEditor/index.js | 20 ++++++++++------ .../MarkdownControl/VisualEditor/keymap.js | 0 .../VisualEditor/markdownToProseMirror.js | 0 .../MarkdownControl/VisualEditor/parser.js | 0 .../{ => Markdown}/MarkdownControl/index.js | 4 ++-- .../{ => Markdown}/MarkdownControl/plugins.js | 0 .../__tests__/MarkupItReactRenderer.spec.js | 0 .../MarkupItReactRenderer.spec.js.snap | 0 .../MarkdownPreview/cmsPluginRehype.js | 2 +- .../{ => Markdown}/MarkdownPreview/index.js | 2 +- .../Widgets/Markdown/unifiedConfig.js | 4 ++++ .../Toolbar/ToolbarPluginFormControl.css | 7 ------ src/lib/registry.js | 2 +- 29 files changed, 54 insertions(+), 38 deletions(-) rename src/components/Widgets/{ => Markdown}/MarkdownControl/RawEditor/index.css (94%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/RawEditor/index.js (95%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/Toolbar.css (81%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/Toolbar.js (98%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarButton.css (85%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarButton.js (93%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarComponentsMenu.css (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarComponentsMenu.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarPluginForm.css (94%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarPluginForm.js (100%) create mode 100644 src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.css rename src/components/Widgets/{ => Markdown}/MarkdownControl/Toolbar/ToolbarPluginFormControl.js (95%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/__tests__/parser.spec.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/index.css (98%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/index.js (96%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/keymap.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/markdownToProseMirror.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/VisualEditor/parser.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/index.js (93%) rename src/components/Widgets/{ => Markdown}/MarkdownControl/plugins.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js (100%) rename src/components/Widgets/{ => Markdown}/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap (100%) rename src/components/Widgets/{ => Markdown}/MarkdownPreview/cmsPluginRehype.js (97%) rename src/components/Widgets/{ => Markdown}/MarkdownPreview/index.js (91%) create mode 100644 src/components/Widgets/Markdown/unifiedConfig.js delete mode 100644 src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css diff --git a/src/components/Widgets.js b/src/components/Widgets.js index 671152d1..db0d6d18 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -9,8 +9,8 @@ import ListControl from './Widgets/ListControl'; import ListPreview from './Widgets/ListPreview'; import TextControl from './Widgets/TextControl'; import TextPreview from './Widgets/TextPreview'; -import MarkdownControl from './Widgets/MarkdownControl'; -import MarkdownPreview from './Widgets/MarkdownPreview'; +import MarkdownControl from './Widgets/Markdown/MarkdownControl'; +import MarkdownPreview from './Widgets/Markdown/MarkdownPreview'; import ImageControl from './Widgets/ImageControl'; import ImagePreview from './Widgets/ImagePreview'; import FileControl from './Widgets/FileControl'; diff --git a/src/components/Widgets/MarkdownControl/RawEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css similarity index 94% rename from src/components/Widgets/MarkdownControl/RawEditor/index.css rename to src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css index b0fc74be..8786390c 100644 --- a/src/components/Widgets/MarkdownControl/RawEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css @@ -1,4 +1,4 @@ -@import "../../../UI/theme"; +@import "../../../../UI/theme"; .root { position: relative; diff --git a/src/components/Widgets/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js similarity index 95% rename from src/components/Widgets/MarkdownControl/RawEditor/index.js rename to src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index ceec01f8..c82bb5f6 100644 --- a/src/components/Widgets/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -10,10 +10,16 @@ import rehypeSanitize from 'rehype-sanitize'; import rehypeReparse from 'rehype-raw'; import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; -import registry from '../../../../lib/registry'; -import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; +import registry from '../../../../../lib/registry'; +import { + remarkParseConfig, + remarkStringifyConfig, + rehypeParseConfig, + rehypeStringifyConfig, +} from '../../unifiedConfig'; +import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; -import { Sticky } from '../../../UI/Sticky/Sticky'; +import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; const HAS_LINE_BREAK = /\n/m; @@ -30,11 +36,11 @@ function processUrl(url) { function cleanupPaste(paste) { return unified() - .use(htmlToRehype, { fragment: true }) + .use(htmlToRehype, rehypeParseConfig) .use(rehypeSanitize) .use(rehypeReparse) .use(rehypeToRemark) - .use(remarkToMarkdown) + .use(remarkToMarkdown, remarkStringifyConfig) .process(paste); } @@ -91,9 +97,9 @@ export default class RawEditor extends React.Component { this.updateHeight(); this.element.addEventListener('paste', this.handlePaste, false); const markdown = unified() - .use(htmlToRehype) + .use(htmlToRehype, rehypeParseConfig) .use(rehypeToRemark) - .use(remarkToMarkdown) + .use(remarkToMarkdown, remarkStringifyConfig) .processSync(this.state.value) .contents; this.setState({ value: markdown }); @@ -250,9 +256,9 @@ export default class RawEditor extends React.Component { handleChange = (e) => { const html = unified() - .use(markdownToRemark) + .use(markdownToRemark, remarkParseConfig) .use(remarkToRehype) - .use(rehypeToHtml) + .use(rehypeToHtml, rehypeStringifyConfig) .processSync(e.target.value) .contents; this.props.onChange(html); diff --git a/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.css similarity index 81% rename from src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.css index 5c7f5359..4db58bd8 100644 --- a/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.css @@ -1,4 +1,4 @@ -@import "../../../UI/theme"; +@import "../../../../UI/theme"; .Toolbar { composes: clearfix; diff --git a/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js similarity index 98% rename from src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js index 3720033b..31087c8c 100644 --- a/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js @@ -5,7 +5,7 @@ import Switch from 'react-toolbox/lib/switch'; import ToolbarButton from './ToolbarButton'; import ToolbarComponentsMenu from './ToolbarComponentsMenu'; import ToolbarPluginForm from './ToolbarPluginForm'; -import { Icon } from '../../../UI'; +import { Icon } from '../../../../UI'; import styles from './Toolbar.css'; export default class Toolbar extends React.Component { diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css similarity index 85% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css index fd822055..f09406df 100644 --- a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.css @@ -1,4 +1,4 @@ -@import "../../../UI/theme"; +@import "../../../../UI/theme"; .button { display: inline-block; diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js similarity index 93% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js index 517d028c..3a276ed8 100644 --- a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarButton.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import classnames from 'classnames'; -import { Icon } from '../../../UI'; +import { Icon } from '../../../../UI'; import styles from './ToolbarButton.css'; const ToolbarButton = ({ label, icon, action, active }) => ( diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.css b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.css similarity index 100% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.css rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.css diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js similarity index 100% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.js rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginForm.css similarity index 94% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginForm.css index b19b33d1..6852a7c9 100644 --- a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginForm.css @@ -1,4 +1,4 @@ -@import "../../../UI/theme"; +@import "../../../../UI/theme"; .pluginForm { position: absolute; diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginForm.js similarity index 100% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.js rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginForm.js diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.css b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.css new file mode 100644 index 00000000..a9e6c0ba --- /dev/null +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.css @@ -0,0 +1,7 @@ +.control { + composes: control from "../../../../ControlPanel/ControlPane.css" +} + +.label { + composes: label from "../../../../ControlPanel/ControlPane.css"; +} diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.js similarity index 95% rename from src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js rename to src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.js index ee42c943..6293475d 100644 --- a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarPluginFormControl.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { resolveWidget } from '../../../Widgets'; +import { resolveWidget } from '../../../../Widgets'; import styles from './ToolbarPluginFormControl.css'; const ToolbarPluginFormControl = ({ diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap similarity index 100% rename from src/components/Widgets/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js similarity index 100% rename from src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css similarity index 98% rename from src/components/Widgets/MarkdownControl/VisualEditor/index.css rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index d04804ef..aff7651a 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -1,4 +1,4 @@ -@import "../../../UI/theme"; +@import "../../../../UI/theme"; .editorControlBar { z-index: 1; diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js similarity index 96% rename from src/components/Widgets/MarkdownControl/VisualEditor/index.js rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index b1194613..ee678689 100644 --- a/src/components/Widgets/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -8,12 +8,18 @@ import rehypeToHtml from 'rehype-stringify'; import remarkToMarkdown from 'remark-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; -import registry from '../../../../lib/registry'; -import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; +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 { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; /** @@ -24,16 +30,16 @@ import styles from './index.css'; */ registry.registerWidgetValueSerializer('markdown', { serialize: value => unified() - .use(htmlToRehype) + .use(htmlToRehype, rehypeParseConfig) .use(htmlToRehype) .use(rehypeToRemark) - .use(remarkToMarkdown) + .use(remarkToMarkdown, remarkStringifyConfig) .processSync(value) .contents, deserialize: value => unified() - .use(markdownToRemark) + .use(markdownToRemark, remarkParseConfig) .use(remarkToRehype) - .use(rehypeToHtml) + .use(rehypeToHtml, rehypeStringifyConfig) .processSync(value) .contents }); diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/keymap.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keymap.js similarity index 100% rename from src/components/Widgets/MarkdownControl/VisualEditor/keymap.js rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keymap.js diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/markdownToProseMirror.js similarity index 100% rename from src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/markdownToProseMirror.js diff --git a/src/components/Widgets/MarkdownControl/VisualEditor/parser.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/parser.js similarity index 100% rename from src/components/Widgets/MarkdownControl/VisualEditor/parser.js rename to src/components/Widgets/Markdown/MarkdownControl/VisualEditor/parser.js diff --git a/src/components/Widgets/MarkdownControl/index.js b/src/components/Widgets/Markdown/MarkdownControl/index.js similarity index 93% rename from src/components/Widgets/MarkdownControl/index.js rename to src/components/Widgets/Markdown/MarkdownControl/index.js index a3c3b0ab..41d79763 100644 --- a/src/components/Widgets/MarkdownControl/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/index.js @@ -1,8 +1,8 @@ import React, { PropTypes } from 'react'; -import registry from '../../../lib/registry'; +import registry from '../../../../lib/registry'; import RawEditor from './RawEditor'; import VisualEditor from './VisualEditor'; -import { StickyContainer } from '../../UI/Sticky/Sticky'; +import { StickyContainer } from '../../../UI/Sticky/Sticky'; const MODE_STORAGE_KEY = 'cms.md-mode'; diff --git a/src/components/Widgets/MarkdownControl/plugins.js b/src/components/Widgets/Markdown/MarkdownControl/plugins.js similarity index 100% rename from src/components/Widgets/MarkdownControl/plugins.js rename to src/components/Widgets/Markdown/MarkdownControl/plugins.js diff --git a/src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js similarity index 100% rename from src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js rename to src/components/Widgets/Markdown/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js diff --git a/src/components/Widgets/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap similarity index 100% rename from src/components/Widgets/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap rename to src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap diff --git a/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js b/src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js similarity index 97% rename from src/components/Widgets/MarkdownPreview/cmsPluginRehype.js rename to src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js index 3efc0700..186b20aa 100644 --- a/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/cmsPluginRehype.js @@ -5,7 +5,7 @@ import isString from 'lodash/isString'; import isEmpty from 'lodash/isEmpty'; import unified from 'unified'; import htmlToRehype from 'rehype-parse'; -import registry from "../../../lib/registry"; +import registry from "../../../../lib/registry"; const cmsPluginRehype = ({ getAsset }) => { diff --git a/src/components/Widgets/MarkdownPreview/index.js b/src/components/Widgets/Markdown/MarkdownPreview/index.js similarity index 91% rename from src/components/Widgets/MarkdownPreview/index.js rename to src/components/Widgets/Markdown/MarkdownPreview/index.js index 8882157c..3df5b23f 100644 --- a/src/components/Widgets/MarkdownPreview/index.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/index.js @@ -5,7 +5,7 @@ import remarkToRehype from 'remark-rehype'; import htmlToRehype from 'rehype-parse'; import rehypeToReact from 'rehype-react'; import cmsPluginToRehype from './cmsPluginRehype'; -import previewStyle from '../defaultPreviewStyle'; +import previewStyle from '../../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { return value === null ? null :
    ; diff --git a/src/components/Widgets/Markdown/unifiedConfig.js b/src/components/Widgets/Markdown/unifiedConfig.js new file mode 100644 index 00000000..7afafaa7 --- /dev/null +++ b/src/components/Widgets/Markdown/unifiedConfig.js @@ -0,0 +1,4 @@ +export const remarkParseConfig = { pedantic: true, footnotes: true, fences: true }; +export const remarkStringifyConfig = { pedantic: true, fences: true }; +export const rehypeParseConfig = { fragment: true }; +export const rehypeStringifyConfig = {}; diff --git a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css deleted file mode 100644 index 40ee42ff..00000000 --- a/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css +++ /dev/null @@ -1,7 +0,0 @@ -.control { - composes: control from "../../../ControlPanel/ControlPane.css" -} - -.label { - composes: label from "../../../ControlPanel/ControlPane.css"; -} diff --git a/src/lib/registry.js b/src/lib/registry.js index 04176b3f..8b7dfabf 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -1,5 +1,5 @@ import { Map } from 'immutable'; -import { newEditorPlugin } from '../components/Widgets/MarkdownControl/plugins'; +import { newEditorPlugin } from '../components/Widgets/Markdown/MarkdownControl/plugins'; const _registry = { templates: {}, From faec38ac190ceacfbdaa08c302a5510fe8d0eb45 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 23 Jun 2017 14:43:00 -0400 Subject: [PATCH 28/79] fix raw editor paste parsing --- .../Widgets/Markdown/MarkdownControl/RawEditor/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index c82bb5f6..9a21b161 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import get from 'lodash/get'; import unified from 'unified'; import markdownToRemark from 'remark-parse'; import remarkToRehype from 'remark-rehype'; @@ -255,15 +256,17 @@ export default class RawEditor extends React.Component { }; handleChange = (e) => { + // handleChange may receive an event or a value + const value = get(e, ['target', 'value']) || e; const html = unified() .use(markdownToRemark, remarkParseConfig) .use(remarkToRehype) .use(rehypeToHtml, rehypeStringifyConfig) - .processSync(e.target.value) + .processSync(value) .contents; this.props.onChange(html); this.updateHeight(); - this.setState({ value: e.target.value }); + this.setState({ value }); }; handlePluginSubmit = (plugin, data) => { @@ -319,7 +322,7 @@ export default class RawEditor extends React.Component { }; handlePaste = (e) => { - const { value } = this.props; + const { value } = this.state; const selection = this.getSelection(); const beforeSelection = value.substr(0, selection.start); const afterSelection = value.substr(selection.end); From 54e77bd80c3cfcce37a5170a8372893e22b2f24f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 23 Jun 2017 14:53:35 -0400 Subject: [PATCH 29/79] fix raw editor formatting controls --- .../Widgets/Markdown/MarkdownControl/RawEditor/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 9a21b161..f9faddcf 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -128,7 +128,7 @@ export default class RawEditor extends React.Component { surroundSelection(chars) { const selection = this.getSelection(); const newSelection = Object.assign({}, selection); - const { value } = this.props; + const { value } = this.state; const escapedChars = chars.replace(/\*/g, '\\*'); const regexp = new RegExp(`^${ escapedChars }.*${ escapedChars }$`); let changed = chars + selection.selected + chars; From 5cbc76da684bf660901cd6b6d50db09fee85faa9 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 23 Jun 2017 17:04:17 -0400 Subject: [PATCH 30/79] improve rte pasting --- .../Markdown/MarkdownControl/RawEditor/index.js | 14 ++++++-------- .../Markdown/MarkdownControl/VisualEditor/index.js | 9 +++++++++ src/components/Widgets/Markdown/unifiedConfig.js | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index f9faddcf..27ce43cc 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -83,7 +83,12 @@ export default class RawEditor extends React.Component { super(props); const plugins = registry.getEditorComponents(); this.state = { - value: this.props.value, + value: unified() + .use(htmlToRehype, rehypeParseConfig) + .use(rehypeToRemark) + .use(remarkToMarkdown, remarkStringifyConfig) + .processSync(this.props.value) + .contents, plugins, }; this.shortcuts = { @@ -97,13 +102,6 @@ export default class RawEditor extends React.Component { componentDidMount() { this.updateHeight(); this.element.addEventListener('paste', this.handlePaste, false); - const markdown = unified() - .use(htmlToRehype, rehypeParseConfig) - .use(rehypeToRemark) - .use(remarkToMarkdown, remarkStringifyConfig) - .processSync(this.state.value) - .contents; - this.setState({ value: markdown }); } componentDidUpdate() { diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index ee678689..30d19275 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -247,6 +247,14 @@ export default class Editor extends Component { }; } + handlePaste = (e, data, state) => { + if (data.type !== 'html' || data.isShift) { + return; + } + const fragment = serializer.deserialize(data.html).document; + return state.transform().insertFragment(fragment).apply(); + } + handleDocumentChange = (doc, editorState) => { const html = serializer.serialize(editorState); this.props.onChange(html); @@ -445,6 +453,7 @@ export default class Editor extends Component { onChange={editorState => this.setState({ editorState })} onDocumentChange={this.handleDocumentChange} onKeyDown={this.onKeyDown} + onPaste={this.handlePaste} ref={ref => this.ref = ref} spellCheck /> diff --git a/src/components/Widgets/Markdown/unifiedConfig.js b/src/components/Widgets/Markdown/unifiedConfig.js index 7afafaa7..edb15ee8 100644 --- a/src/components/Widgets/Markdown/unifiedConfig.js +++ b/src/components/Widgets/Markdown/unifiedConfig.js @@ -1,4 +1,4 @@ -export const remarkParseConfig = { pedantic: true, footnotes: true, fences: true }; -export const remarkStringifyConfig = { pedantic: true, fences: true }; +export const remarkParseConfig = { fences: true }; +export const remarkStringifyConfig = { fences: true }; export const rehypeParseConfig = { fragment: true }; export const rehypeStringifyConfig = {}; From cba631ba1a2c5c7e50a64adba825602787b9d3e7 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 27 Jun 2017 12:21:34 -0400 Subject: [PATCH 31/79] improve visual/raw editor consistency --- package.json | 1 + .../MarkdownControl/RawEditor/index.js | 11 +- .../MarkdownControl/VisualEditor/index.js | 52 +- .../Widgets/Markdown/unifiedConfig.js | 2 +- yarn.lock | 2936 +++++++++++------ 5 files changed, 1943 insertions(+), 1059 deletions(-) diff --git a/package.json b/package.json index 4007d922..a2efaf66 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "redux-notifications": "^2.1.1", "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", + "rehype-minify-whitespace": "^2.0.0", "rehype-parse": "^3.1.0", "rehype-raw": "^1.0.0", "rehype-react": "^3.0.0", diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 27ce43cc..0bf25a33 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -8,6 +8,7 @@ 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'; @@ -41,6 +42,8 @@ function cleanupPaste(paste) { .use(rehypeSanitize) .use(rehypeReparse) .use(rehypeToRemark) + .use(rehypeSanitize) + .use(rehypeMinifyWhitespace) .use(remarkToMarkdown, remarkStringifyConfig) .process(paste); } @@ -84,7 +87,7 @@ export default class RawEditor extends React.Component { const plugins = registry.getEditorComponents(); this.state = { value: unified() - .use(htmlToRehype, rehypeParseConfig) + .use(htmlToRehype) .use(rehypeToRemark) .use(remarkToMarkdown, remarkStringifyConfig) .processSync(this.props.value) @@ -255,13 +258,17 @@ export default class RawEditor extends React.Component { handleChange = (e) => { // handleChange may receive an event or a value - const value = get(e, ['target', 'value']) || e; + 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); 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 30d19275..d13f23d0 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -107,7 +107,7 @@ const BLOCK_TAGS = { h3: 'heading-three', h4: 'heading-four', h5: 'heading-five', - h6: 'heading-six' + h6: 'heading-six', } const MARK_TAGS = { @@ -115,6 +115,7 @@ const MARK_TAGS = { em: 'italic', u: 'underline', s: 'strikethrough', + del: 'strikethrough', code: 'code' } @@ -131,6 +132,12 @@ const BLOCK_COMPONENTS = { 'heading-four': props =>

    {props.children}

    , 'heading-five': props =>
    {props.children}
    , 'heading-six': props =>
    {props.children}
    , + 'image': props => { + const data = props.node && props.node.get('data'); + const src = data && data.get('src', props.src); + const alt = data && data.get('alt', props.alt); + return {alt}; + }, }; const NODE_COMPONENTS = { @@ -139,8 +146,7 @@ const NODE_COMPONENTS = { const href = props.node && props.node.getIn(['data', 'href']) || props.href; return {props.children}; }, -} - +}; const MARK_COMPONENTS = { bold: props => {props.children}, @@ -162,6 +168,9 @@ const RULES = [ } }, serialize(entity, children) { + if (['bulleted-list', 'numbered-list'].includes(entity.type)) { + return; + } const component = BLOCK_COMPONENTS[entity.type] if (!component) { return; @@ -203,6 +212,32 @@ const RULES = [ } }, }, + { + deserialize(el, next) { + if (el.tagName != 'img') return + return { + kind: 'inline', + type: 'image', + nodes: [], + data: { + src: el.attribs.src, + alt: el.attribs.alt, + } + } + }, + serialize(entity, children) { + if (entity.type !== 'image') { + return; + } + const data = entity.get('data'); + const props = { + src: data.get('src'), + alt: data.get('alt'), + attributes: data.get('attributes'), + }; + return NODE_COMPONENTS.image(props); + } + }, { // Special case for links, to grab their href. deserialize(el, next) { @@ -229,6 +264,15 @@ const RULES = [ return NODE_COMPONENTS.link(props); } }, + { + serialize(entity, children) { + if (!['bulleted-list', 'unordered-list'].includes(entity.type)) { + return; + } + return NODE_COMPONENTS[entity.type]({ children }); + } + } + ] const serializer = new SlateHtml({ rules: RULES }); @@ -237,6 +281,7 @@ 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: { @@ -271,6 +316,7 @@ export default class Editor extends Component { b: 'bold', i: 'italic', u: 'underlined', + s: 'strikethrough', '`': 'code', }; diff --git a/src/components/Widgets/Markdown/unifiedConfig.js b/src/components/Widgets/Markdown/unifiedConfig.js index edb15ee8..2c2f11c4 100644 --- a/src/components/Widgets/Markdown/unifiedConfig.js +++ b/src/components/Widgets/Markdown/unifiedConfig.js @@ -1,4 +1,4 @@ export const remarkParseConfig = { fences: true }; -export const remarkStringifyConfig = { fences: true }; +export const remarkStringifyConfig = { listItemIndent: '1', fences: true }; export const rehypeParseConfig = { fragment: true }; export const rehypeStringifyConfig = {}; diff --git a/yarn.lock b/yarn.lock index a30ec5f7..848b2350 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,6 +52,14 @@ webpack-dev-middleware "^1.6.0" webpack-hot-middleware "^2.10.0" +"@types/node@^6.0.46": + version "6.0.88" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.88.tgz#f618f11a944f6a18d92b5c472028728a3e3d4b66" + +"@types/react@>=15": + version "16.0.5" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.5.tgz#d713cf67cc211dea20463d2a0b66005c22070c4b" + JSONStream@^0.8.4: version "0.8.4" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.8.4.tgz#91657dfe6ff857483066132b4618b62e8f4887bd" @@ -64,14 +72,14 @@ abab@^1.0.3: resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" abbrev@1: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" accepts@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + version "1.3.4" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: - mime-types "~2.1.11" + mime-types "~2.1.16" negotiator "0.6.1" acorn-dynamic-import@^2.0.0: @@ -101,23 +109,25 @@ acorn@^3.0.0, acorn@^3.0.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" acorn@^4.0.3, acorn@^4.0.4: - version "4.0.11" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.0.1: - version "5.0.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" +acorn@^5.0.0, acorn@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75" airbnb-js-shims@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-1.1.1.tgz#27224f0030f244e6570442ed1020772c1434aec2" + version "1.3.0" + resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-1.3.0.tgz#aac46d80057fb0b414f70e06d07e362fd99ee2fa" dependencies: - array-includes "^3.0.2" + array-includes "^3.0.3" es5-shim "^4.5.9" - es6-shim "^0.35.1" - object.entries "^1.0.3" + es6-shim "^0.35.3" + function.prototype.name "^1.0.3" + object.entries "^1.0.4" object.getownpropertydescriptors "^2.0.3" - object.values "^1.0.3" + object.values "^1.0.4" + promise.prototype.finally "^3.0.0" string.prototype.padend "^3.0.0" string.prototype.padstart "^3.0.0" @@ -126,17 +136,19 @@ ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" ajv@^4.7.0, ajv@^4.9.1: - version "4.11.7" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.7.tgz#8655a5d86d0824985cc471a1d913fb6729a0ec48" + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" ajv@^5.0.0: - version "5.1.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.1.5.tgz#8734931b601f00d4feef7c65738d77d1b65d1f68" + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" dependencies: co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" json-stable-stringify "^1.0.1" align-text@^0.1.1, align-text@^0.1.3: @@ -155,6 +167,12 @@ amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + dependencies: + string-width "^2.0.0" + ansi-escapes@^1.0.0, ansi-escapes@^1.1.0, ansi-escapes@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -167,22 +185,26 @@ ansi-regex@^2.0.0, ansi-regex@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.0.0.tgz#5404e93a544c4fec7f048262977bebfe3155e0c1" +ansi-styles@^3.0.0, ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: - color-convert "^1.0.0" + color-convert "^1.9.0" anymatch@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" dependencies: - arrify "^1.0.0" micromatch "^2.1.5" + normalize-path "^2.0.0" app-root-path@^2.0.0: version "2.0.1" @@ -195,8 +217,8 @@ append-transform@^0.4.0: default-require-extensions "^1.0.0" aproba@^1.0.3: - version "1.1.1" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab" + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" are-we-there-yet@~1.1.2: version "1.1.4" @@ -218,8 +240,8 @@ arr-diff@^2.0.0: arr-flatten "^1.0.1" arr-flatten@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" array-differ@^1.0.0: version "1.0.0" @@ -241,13 +263,21 @@ array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" -array-includes@^3.0.2: +array-flatten@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + +array-includes@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" dependencies: define-properties "^1.1.2" es-abstract "^1.7.0" +array-iterate@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-1.1.1.tgz#865bf7f8af39d6b0982c60902914ac76bc0108f6" + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -274,8 +304,8 @@ arrify@^1.0.0, arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" asap@^2.0.3, asap@~2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" asn1.js@^4.0.0: version "4.9.1" @@ -324,8 +354,8 @@ async@^1.3.0, async@^1.4.0, async@^1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" async@^2.1.2, async@^2.1.4, async@^2.1.5: - version "2.3.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.3.0.tgz#1013d1051047dd320fe24e494d5c66ecaf6147d9" + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" dependencies: lodash "^4.14.0" @@ -357,57 +387,57 @@ aws4@^1.2.1: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" babel-cli@^6.18.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1" dependencies: - babel-core "^6.24.1" - babel-polyfill "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - commander "^2.8.1" - convert-source-map "^1.1.0" + babel-core "^6.26.0" + babel-polyfill "^6.26.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + commander "^2.11.0" + convert-source-map "^1.5.0" fs-readdir-recursive "^1.0.0" - glob "^7.0.0" - lodash "^4.2.0" - output-file-sync "^1.1.0" - path-is-absolute "^1.0.0" + glob "^7.1.2" + lodash "^4.17.4" + output-file-sync "^1.1.2" + path-is-absolute "^1.0.1" slash "^1.0.0" - source-map "^0.5.0" - v8flags "^2.0.10" + source-map "^0.5.6" + v8flags "^2.1.1" optionalDependencies: chokidar "^1.6.1" -babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" +babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: - chalk "^1.1.0" + chalk "^1.1.3" esutils "^2.0.2" - js-tokens "^3.0.0" + js-tokens "^3.0.2" -babel-core@^6.0.0, babel-core@^6.11.4, babel-core@^6.24.1, babel-core@^6.5.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.1.tgz#8c428564dce1e1f41fb337ec34f4c3b022b5ad83" +babel-core@^6.0.0, babel-core@^6.11.4, babel-core@^6.23.1, babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" dependencies: - babel-code-frame "^6.22.0" - babel-generator "^6.24.1" + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" babel-helpers "^6.24.1" babel-messages "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - convert-source-map "^1.1.0" - debug "^2.1.1" - json5 "^0.5.0" - lodash "^4.2.0" - minimatch "^3.0.2" - path-is-absolute "^1.0.0" - private "^0.1.6" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" slash "^1.0.0" - source-map "^0.5.0" + source-map "^0.5.6" babel-eslint@^7.0.0: version "7.2.3" @@ -418,17 +448,17 @@ babel-eslint@^7.0.0: babel-types "^6.23.0" babylon "^6.17.0" -babel-generator@^6.18.0, babel-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" +babel-generator@^6.18.0, babel-generator@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" dependencies: babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" detect-indent "^4.0.0" jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" + lodash "^4.17.4" + source-map "^0.5.6" trim-right "^1.0.1" babel-helper-bindify-decorators@^6.24.1: @@ -448,12 +478,12 @@ babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: babel-types "^6.24.1" babel-helper-builder-react-jsx@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.24.1.tgz#0ad7917e33c8d751e646daca4e77cc19377d2cbc" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0" dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - esutils "^2.0.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + esutils "^2.0.2" babel-helper-call-delegate@^6.24.1: version "6.24.1" @@ -465,13 +495,13 @@ babel-helper-call-delegate@^6.24.1: babel-types "^6.24.1" babel-helper-define-map@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" dependencies: babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" babel-helper-explode-assignable-expression@^6.24.1: version "6.24.1" @@ -522,12 +552,12 @@ babel-helper-optimise-call-expression@^6.24.1: babel-types "^6.24.1" babel-helper-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" babel-helper-remap-async-to-generator@^6.24.1: version "6.24.1" @@ -575,10 +605,10 @@ babel-loader@^6.2.4: object-assign "^4.0.1" babel-loader@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.0.0.tgz#2e43a66bee1fff4470533d0402c8a4532fafbaf7" + version "7.1.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" dependencies: - find-cache-dir "^0.1.1" + find-cache-dir "^1.0.0" loader-utils "^1.0.2" mkdirp "^0.5.1" @@ -595,12 +625,12 @@ babel-plugin-check-es2015-constants@^6.22.0: babel-runtime "^6.22.0" babel-plugin-istanbul@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.3.tgz#6ee6280410dcf59c7747518c3dfd98680958f102" + version "4.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.4.tgz#18dde84bf3ce329fddf3f4103fae921456d8e587" dependencies: find-up "^2.1.0" - istanbul-lib-instrument "^1.7.1" - test-exclude "^4.1.0" + istanbul-lib-instrument "^1.7.2" + test-exclude "^4.1.1" babel-plugin-jest-hoist@^20.0.3: version "20.0.3" @@ -739,14 +769,14 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: babel-runtime "^6.22.0" babel-plugin-transform-es2015-block-scoping@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" babel-plugin-transform-es2015-classes@^6.24.1: version "6.24.1" @@ -811,13 +841,13 @@ babel-plugin-transform-es2015-modules-amd@^6.24.1: babel-template "^6.24.1" babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" dependencies: babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-types "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" babel-plugin-transform-es2015-modules-systemjs@^6.24.1: version "6.24.1" @@ -924,15 +954,15 @@ babel-plugin-transform-function-bind@^6.22.0: babel-runtime "^6.22.0" babel-plugin-transform-object-rest-spread@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" dependencies: babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.22.0" + babel-runtime "^6.26.0" babel-plugin-transform-react-display-name@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.23.0.tgz#4398910c358441dc4cef18787264d0412ed36b37" + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" dependencies: babel-runtime "^6.22.0" @@ -959,10 +989,10 @@ babel-plugin-transform-react-jsx@^6.24.1: babel-runtime "^6.22.0" babel-plugin-transform-regenerator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" dependencies: - regenerator-transform "0.9.11" + regenerator-transform "^0.10.0" babel-plugin-transform-strict-mode@^6.24.1: version "6.24.1" @@ -971,15 +1001,15 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-polyfill@^6.23.0, babel-polyfill@^6.9.1: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" +babel-polyfill@^6.26.0, babel-polyfill@^6.9.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" dependencies: - babel-runtime "^6.22.0" - core-js "^2.4.0" - regenerator-runtime "^0.10.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" -babel-preset-es2015@^6.5.0, babel-preset-es2015@^6.9.0: +babel-preset-es2015@^6.22.0, babel-preset-es2015@^6.9.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" dependencies: @@ -1020,7 +1050,7 @@ babel-preset-jest@^20.0.3: dependencies: babel-plugin-jest-hoist "^20.0.3" -babel-preset-react@^6.11.1, babel-preset-react@^6.5.0: +babel-preset-react@^6.11.1, babel-preset-react@^6.23.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380" dependencies: @@ -1039,7 +1069,7 @@ babel-preset-stage-0@^6.5.0: babel-plugin-transform-function-bind "^6.22.0" babel-preset-stage-1 "^6.24.1" -babel-preset-stage-1@^6.16.0, babel-preset-stage-1@^6.24.1: +babel-preset-stage-1@^6.22.0, babel-preset-stage-1@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0" dependencies: @@ -1066,65 +1096,69 @@ babel-preset-stage-3@^6.24.1: babel-plugin-transform-exponentiation-operator "^6.24.1" babel-plugin-transform-object-rest-spread "^6.22.0" -babel-register@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" dependencies: - babel-core "^6.24.1" - babel-runtime "^6.22.0" - core-js "^2.4.0" + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" home-or-tmp "^2.0.0" - lodash "^4.2.0" + lodash "^4.17.4" mkdirp "^0.5.1" - source-map-support "^0.4.2" + source-map-support "^0.4.15" -babel-runtime@6.x.x, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.5.0, babel-runtime@^6.6.1, babel-runtime@^6.9.2: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" +babel-runtime@6.x.x, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0, babel-runtime@^6.6.1, babel-runtime@^6.9.2: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: core-js "^2.4.0" - regenerator-runtime "^0.10.0" + regenerator-runtime "^0.11.0" -babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.3.0, babel-template@^6.7.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-template@^6.7.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" -babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.7.3: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" +babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0, babel-traverse@^6.7.3: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" dependencies: - babel-code-frame "^6.22.0" + babel-code-frame "^6.26.0" babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" -babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" dependencies: - babel-runtime "^6.22.0" + babel-runtime "^6.26.0" esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" + lodash "^4.17.4" + to-fast-properties "^1.0.3" babel@^6.5.2: version "6.23.0" resolved "https://registry.yarnpkg.com/babel/-/babel-6.23.0.tgz#d0d1e7d803e974765beea3232d4e153c0efb90f4" -babylon@^6.1.21, babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.17.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932" +babylon@^6.1.21, babylon@^6.17.0, babylon@^6.17.4, babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +bail@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.2.tgz#f7d6c1731630a9f9f0d4d35ed1f962e2074a1764" balanced-match@0.1.0: version "0.1.0" @@ -1138,17 +1172,21 @@ balanced-match@^0.4.0, balanced-match@^0.4.1, balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + base62@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084" base64-js@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" -batch@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" bcrypt-pbkdf@^1.0.0: version "1.0.1" @@ -1161,8 +1199,8 @@ big.js@^3.1.3: resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" binary-extensions@^1.0.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + version "1.10.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" block-stream@*: version "0.0.9" @@ -1175,8 +1213,19 @@ bluebird@^3.0.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.6" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" boolbase@~1.0.0: version "1.0.0" @@ -1189,25 +1238,26 @@ boom@2.x.x: hoek "2.x.x" bowser@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.6.1.tgz#9157e9498f456e937173a2918f3b2161e5353eb3" + version "1.7.2" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.7.2.tgz#b94cc6925ba6b5e07c421a58e601ce4611264572" -boxen@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.3.1.tgz#a7d898243ae622f7abb6bb604d740a76c6a5461b" +boxen@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.1.tgz#0f11e7fe344edb9397977fc13ede7f64d956481d" dependencies: - chalk "^1.1.1" - filled-array "^1.0.0" - object-assign "^4.0.1" - repeating "^2.0.0" - string-width "^1.0.1" + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" widest-line "^1.0.0" -brace-expansion@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" +brace-expansion@^1.0.0, brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" dependencies: - balanced-match "^0.4.1" + balanced-match "^1.0.0" concat-map "0.0.1" braces@^1.8.2: @@ -1304,9 +1354,9 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" -buffer-shims@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" buffer-xor@^1.0.2: version "1.0.3" @@ -1328,9 +1378,9 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" -bytes@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070" +bytes@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" caller-path@^0.1.0: version "0.1.0" @@ -1380,6 +1430,10 @@ camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" +camelcase@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + caniuse-api@^1.5.2, caniuse-api@^1.5.3: version "1.6.1" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" @@ -1390,8 +1444,8 @@ caniuse-api@^1.5.2, caniuse-api@^1.5.3: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000187, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000660" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000660.tgz#d2d57b309dc5a11bb5b46018f51855f7a41efee5" + version "1.0.30000718" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000718.tgz#86cdd97987302554934c61e106f4e470f16f993c" capture-stack-trace@^1.0.0: version "1.0.0" @@ -1401,6 +1455,10 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +ccount@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.2.tgz#53b6a2f815bb77b9c2871f7b9a72c3a25f1d8e89" + center-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" @@ -1408,7 +1466,11 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1418,6 +1480,30 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.1, chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +character-entities-html4@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.1.tgz#359a2a4a0f7e29d3dc2ac99bdbe21ee39438ea50" + +character-entities-legacy@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.1.tgz#f40779df1a101872bb510a3d295e1fccf147202f" + +character-entities@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.1.tgz#f76871be5ef66ddb7f8f8e3478ecc374c27d6dca" + +character-reference-invalid@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.1.tgz#942835f750e4ec61a308e60c2ef8cc1011202efc" + cheerio@^0.22.0: version "0.22.0" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" @@ -1439,9 +1525,9 @@ cheerio@^0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@^1.0.0, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" +chokidar@^1.0.0, chokidar@^1.6.0, chokidar@^1.6.1, chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: anymatch "^1.3.0" async-each "^1.0.0" @@ -1458,15 +1544,16 @@ ci-info@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" -cipher-base@^1.0.0, cipher-base@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" dependencies: inherits "^2.0.1" + safe-buffer "^5.0.1" circular-json@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" cjson@^0.4.0: version "0.4.0" @@ -1475,8 +1562,8 @@ cjson@^0.4.0: json-parse-helpfulerror "^1.0.3" clap@^1.0.9: - version "1.1.3" - resolved "https://registry.yarnpkg.com/clap/-/clap-1.1.3.tgz#b3bd36e93dd4cbfb395a3c26896352445265c05b" + version "1.2.0" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.0.tgz#59c90fe3e137104746ff19469a27a634ff68c857" dependencies: chalk "^1.1.3" @@ -1484,6 +1571,10 @@ classnames@^2.2.3, classnames@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + cli-cursor@^1.0.1, cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" @@ -1502,16 +1593,16 @@ cli-truncate@^0.2.1: string-width "^1.0.1" cli-width@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" clipboard@^1.5.5: - version "1.6.1" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53" + version "1.7.1" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b" dependencies: - good-listener "^1.2.0" + good-listener "^1.2.2" select "^1.1.2" - tiny-emitter "^1.0.0" + tiny-emitter "^2.0.0" cliui@^2.1.0: version "2.1.0" @@ -1529,14 +1620,13 @@ cliui@^3.0.3, cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" -clone-deep@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" +clone-deep@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" dependencies: - for-own "^0.1.3" + for-own "^1.0.0" is-plain-object "^2.0.1" - kind-of "^3.0.2" - lazy-cache "^1.0.3" + kind-of "^3.2.2" shallow-clone "^0.1.2" clone-regexp@^1.0.0: @@ -1555,8 +1645,8 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" coa@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.1.tgz#7f959346cfc8719e3f7233cd6852854a7c67d8a3" + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" dependencies: q "^1.1.2" @@ -1564,11 +1654,15 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" +collapse-white-space@^1.0.0, collapse-white-space@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c" + color-convert@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" -color-convert@^1.0.0, color-convert@^1.3.0: +color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -1579,8 +1673,8 @@ color-diff@^0.1.3: resolved "https://registry.yarnpkg.com/color-diff/-/color-diff-0.1.7.tgz#6db78cd9482a8e459d40821eaf4b503283dcb8e2" color-name@^1.0.0, color-name@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.2.tgz#5c8ab72b64bd2215d617ae9559ebb148475cf98d" + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" color-string@^0.3.0: version "0.3.0" @@ -1636,32 +1730,37 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@^2.8.1, commander@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" +comma-separated-tokens@^1.0.0, comma-separated-tokens@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.4.tgz#72083e58d4a462f01866f6617f4d98a3cd3b8a46" dependencies: - graceful-readlink ">= 1.0.0" + trim "0.0.1" + +commander@^2.11.0, commander@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" -compressible@~2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" +compressible@~2.0.10: + version "2.0.11" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.11.tgz#16718a75de283ed8e604041625a2064586797d8a" dependencies: - mime-db ">= 1.27.0 < 2" + mime-db ">= 1.29.0 < 2" compression@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3" + version "1.7.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d" dependencies: accepts "~1.3.3" - bytes "2.3.0" - compressible "~2.0.8" - debug "~2.2.0" + bytes "2.5.0" + compressible "~2.0.10" + debug "2.6.8" on-headers "~1.0.1" - vary "~1.1.0" + safe-buffer "5.1.1" + vary "~1.1.1" concat-map@0.0.1: version "0.0.1" @@ -1675,19 +1774,16 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" -configstore@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1" +configstore@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" dependencies: - dot-prop "^3.0.0" + dot-prop "^4.1.0" graceful-fs "^4.1.2" - mkdirp "^0.5.0" - object-assign "^4.0.1" - os-tmpdir "^1.0.0" - osenv "^0.1.0" - uuid "^2.0.1" - write-file-atomic "^1.1.2" - xdg-basedir "^2.0.0" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" connect-history-api-fallback@^1.3.0: version "1.3.0" @@ -1723,7 +1819,7 @@ content-type@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" -convert-source-map@^1.1.0, convert-source-map@^1.4.0: +convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" @@ -1739,11 +1835,11 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" -core-util-is@~1.0.0: +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1761,9 +1857,10 @@ cosmiconfig@^1.1.0: require-from-string "^1.1.0" cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.1.tgz#817f2c2039347a1e9bf7d090c0923e53f749ca82" + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" dependencies: + is-directory "^0.3.1" js-yaml "^3.4.3" minimist "^1.2.0" object-assign "^4.1.0" @@ -1778,38 +1875,43 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-error-class@^3.0.1: +create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" dependencies: capture-stack-trace "^1.0.0" -create-hash@^1.1.0, create-hash@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.2.tgz#51210062d7bb7479f6c65bb41a92208b1d61abad" +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" dependencies: cipher-base "^1.0.1" inherits "^2.0.1" - ripemd160 "^1.0.0" - sha.js "^2.3.6" + ripemd160 "^2.0.0" + sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.4.tgz#d3fb4ba253eb8b3f56e39ea2fbcb8af747bd3170" +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" dependencies: + cipher-base "^1.0.3" create-hash "^1.1.0" inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" -create-react-class@^15.5.1, create-react-class@^15.5.2, create-react-class@^15.5.x: - version "15.5.2" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681" +create-react-class@^15.5.1, create-react-class@^15.5.2, create-react-class@^15.5.x, create-react-class@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" dependencies: fbjs "^0.8.9" + loose-envify "^1.3.1" object-assign "^4.1.1" cross-env@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.0.2.tgz#d39fd2fa28c1b5cfb91e7058d1efe8b4fcb01334" + version "5.0.5" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.0.5.tgz#4383d364d9660873dd185b398af3bfef5efffef3" dependencies: cross-spawn "^5.1.0" is-windows "^1.0.0" @@ -1852,8 +1954,8 @@ crypto-browserify@3.3.0: sha.js "2.2.6" crypto-browserify@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" dependencies: browserify-cipher "^1.0.0" browserify-sign "^4.0.0" @@ -1866,6 +1968,10 @@ crypto-browserify@^3.11.0: public-encrypt "^4.0.0" randombytes "^2.0.0" +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + css-color-function@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" @@ -1900,8 +2006,8 @@ css-in-js-utils@^1.0.3: hyphenate-style-name "^1.0.2" css-loader@^0.28.4: - version "0.28.4" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.4.tgz#6cf3579192ce355e8b38d5f42dd7a1f2ec898d0f" + version "0.28.5" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.5.tgz#dd02bb91b94545710212ef7f6aaa66663113d754" dependencies: babel-code-frame "^6.11.0" css-selector-tokenizer "^0.7.0" @@ -1916,7 +2022,7 @@ css-loader@^0.28.4: postcss-modules-scope "^1.0.0" postcss-modules-values "^1.1.0" postcss-value-parser "^3.3.0" - source-list-map "^0.1.7" + source-list-map "^2.0.0" css-rule-stream@^1.1.0: version "1.1.0" @@ -1936,14 +2042,6 @@ css-select@~1.2.0: domutils "1.5.1" nth-check "~1.0.1" -css-selector-tokenizer@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.6.0.tgz#6445f582c7930d241dcc5007a43d6fcb8f073152" - dependencies: - cssesc "^0.1.0" - fastparse "^1.1.1" - regexpu-core "^1.0.0" - css-selector-tokenizer@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" @@ -2052,8 +2150,8 @@ data-uri-to-blob@0.0.4: resolved "https://registry.yarnpkg.com/data-uri-to-blob/-/data-uri-to-blob-0.0.4.tgz#087a7bff42f41a6cc0b2e2fb7312a7c29904fbaa" date-fns@^1.27.2: - version "1.28.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.3.tgz#145d87adc3f5a82c6bda668de97eee1132c97ea1" + version "1.28.5" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" date-now@^0.1.4: version "0.1.4" @@ -2066,19 +2164,7 @@ dateformat@^1.0.12: get-stdin "^4.0.1" meow "^3.3.0" -debug@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" - dependencies: - ms "0.7.2" - -debug@2.6.4, debug@^2.1.1, debug@^2.2.0, debug@^2.6.0: - version "2.6.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.4.tgz#7586a9b3c39741c0282ae33445c4e8ac74734fe0" - dependencies: - ms "0.7.3" - -debug@^2.6.8: +debug@2.6.8, debug@^2.1.1, debug@^2.2.0, debug@^2.3.2, debug@^2.6.0, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" dependencies: @@ -2088,12 +2174,6 @@ debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2103,8 +2183,8 @@ deep-equal@^1.0.0, deep-equal@^1.0.1, deep-equal@~1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" deep-extend@~0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" deep-is@~0.1.3: version "0.1.3" @@ -2116,7 +2196,7 @@ default-require-extensions@^1.0.0: dependencies: strip-bom "^2.0.0" -define-properties@^1.1.2: +define-properties@^1.1.1, define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" dependencies: @@ -2139,13 +2219,24 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" delegate@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe" + version "3.1.3" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.3.tgz#9a8251a777d7025faa55737bc3b071742127a9fd" delegates@^1.0.0: version "1.0.0" @@ -2167,9 +2258,9 @@ depcheck@^0.6.3: walkdir "0.0.11" yargs "^6.0.0" -depd@1.1.0, depd@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" +depd@1.1.1, depd@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" deprecate@^1.0.0: version "1.0.0" @@ -2190,9 +2281,11 @@ destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" -detect-browser@^1.3.3: - version "1.7.0" - resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-1.7.0.tgz#11758cd6aa07d76c25784036d19154ae0392c3b3" +detab@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.1.tgz#531f5e326620e2fd4f03264a905fb3bcc8af4df4" + dependencies: + repeat-string "^1.5.4" detect-indent@^4.0.0: version "4.0.0" @@ -2205,8 +2298,8 @@ detect-node@^2.0.3: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" diff@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + version "3.3.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" diffie-hellman@^5.0.0: version "5.0.2" @@ -2224,22 +2317,46 @@ disposables@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.1.tgz#064727a25b54f502bd82b89aa2dfb8df9f1b39e3" -dnd-core@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.3.0.tgz#f7b29ac37e2c78efe89e911ca5a534f280390450" +dnd-core@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.4.0.tgz#c4a5bc2aea75164f8a295d769d5f551810e7d411" dependencies: asap "^2.0.3" invariant "^2.0.0" lodash "^4.2.0" redux "^3.2.0" -doctrine@1.3.x, doctrine@^1.2.2: +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.0.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.2.2.tgz#a8a26bec7646438963fc86e06f8f8b16d6c8bf7a" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + +doctrine@1.3.x: version "1.3.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.3.0.tgz#13e75682b55518424276f7c173783456ef913d26" dependencies: esutils "^2.0.2" isarray "^1.0.0" +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2264,6 +2381,10 @@ doiuse@^2.3.0, doiuse@^2.4.1: through2 "^0.6.3" yargs "^3.5.4" +dom-helpers@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -2288,21 +2409,28 @@ domelementtype@~1.1.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" domhandler@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" dependencies: domelementtype "1" -domutils@1.5.1, domutils@^1.5.1: +domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" -dot-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" dependencies: is-obj "^1.0.0" @@ -2312,11 +2440,9 @@ duplexer2@0.0.2: dependencies: readable-stream "~1.1.9" -duplexer2@^0.1.4: +duplexer3@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - dependencies: - readable-stream "^2.0.2" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" duplexer@~0.1.1: version "0.1.1" @@ -2329,12 +2455,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" editorconfig@^0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.2.tgz#8e57926d9ee69ab6cb999f027c2171467acceb35" + version "0.13.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.3.tgz#e5219e587951d60958fd94ea9a9a008cdeff1b34" dependencies: bluebird "^3.0.5" commander "^2.9.0" lru-cache "^3.2.0" + semver "^5.1.0" sigmund "^1.0.1" ee-first@1.1.1: @@ -2342,8 +2469,8 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" electron-to-chromium@^1.2.7: - version "1.3.8" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.8.tgz#b2c8a2c79bb89fbbfd3724d9555e15095b5f5fb6" + version "1.3.18" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.18.tgz#3dcc99da3e6b665f6abbc71c28ad51a2cd731a9c" elegant-spinner@^1.0.1: version "1.0.1" @@ -2379,14 +2506,14 @@ encoding@^0.1.11: dependencies: iconv-lite "~0.4.13" -enhanced-resolve@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" +enhanced-resolve@^3.3.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" dependencies: graceful-fs "^4.1.2" memory-fs "^0.4.0" object-assign "^4.0.1" - tapable "^0.2.5" + tapable "^0.2.7" enhanced-resolve@~0.9.0: version "0.9.1" @@ -2401,21 +2528,21 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" enzyme@^2.4.1: - version "2.8.2" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.8.2.tgz#6c8bcb05012abc4aa4bc3213fb23780b9b5b1714" + version "2.9.1" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.9.1.tgz#07d5ce691241240fb817bf2c4b18d6e530240df6" dependencies: cheerio "^0.22.0" function.prototype.name "^1.0.0" is-subset "^0.1.1" - lodash "^4.17.2" + lodash "^4.17.4" object-is "^1.0.1" object.assign "^4.0.4" - object.entries "^1.0.3" - object.values "^1.0.3" - prop-types "^15.5.4" - uuid "^2.0.3" + object.entries "^1.0.4" + object.values "^1.0.4" + prop-types "^15.5.10" + uuid "^3.0.1" -"errno@>=0.1.1 <0.2.0-0", errno@^0.1.3: +errno@^0.1.3, errno@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" dependencies: @@ -2434,13 +2561,14 @@ error-stack-parser@^1.3.3, error-stack-parser@^1.3.6: stackframe "^0.3.1" es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" + version "1.8.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.0.tgz#3b00385e85729932beffa9163bbea1234e932914" dependencies: es-to-primitive "^1.1.1" function-bind "^1.1.0" + has "^1.0.1" is-callable "^1.1.3" - is-regex "^1.0.3" + is-regex "^1.0.4" es-to-primitive@^1.1.1: version "1.1.1" @@ -2459,8 +2587,8 @@ es3ify@^0.1.3: through "~2.3.4" es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.15" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6" + version "0.10.30" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939" dependencies: es6-iterator "2" es6-symbol "~3.1" @@ -2477,7 +2605,7 @@ es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: es5-ext "^0.10.14" es6-symbol "^3.1" -es6-map@^0.1.3: +es6-map@^0.1.3, es6-map@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" dependencies: @@ -2502,7 +2630,7 @@ es6-set@^0.1.4, es6-set@~0.1.5: es6-symbol "3.1.1" event-emitter "~0.3.5" -es6-shim@^0.35.1: +es6-shim@^0.35.1, es6-shim@^0.35.3: version "0.35.3" resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.3.tgz#9bfb7363feffff87a6cdb6cd93e405ec3c4b6f26" @@ -2703,10 +2831,10 @@ esmangle-evaluator@^1.0.0: resolved "https://registry.yarnpkg.com/esmangle-evaluator/-/esmangle-evaluator-1.0.1.tgz#620d866ef4861b3311f75766d52a8572bb3c6336" espree@^3.4.0: - version "3.4.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.2.tgz#38dbdedbedc95b8961a1fbf04734a8f6a9c8c592" + version "3.5.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d" dependencies: - acorn "^5.0.1" + acorn "^5.1.1" acorn-jsx "^3.0.0" esprima-fb@~15001.1001.0-dev-harmony-fb: @@ -2721,9 +2849,9 @@ esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" esquery@^1.0.0: version "1.0.0" @@ -2732,10 +2860,10 @@ esquery@^1.0.0: estraverse "^4.0.0" esrecurse@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" dependencies: - estraverse "~4.1.0" + estraverse "^4.1.0" object-assign "^4.0.1" esrever@^0.2.0: @@ -2746,15 +2874,11 @@ estraverse@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" -estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estraverse@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" - -esutils@^2.0.0, esutils@^2.0.2: +esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -2788,10 +2912,11 @@ eventsource@0.1.6: original ">=0.0.5" evp_bytestokey@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53" + version "1.0.2" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.2.tgz#f66bb88ecd57f71a766821e20283ea38c68bf80a" dependencies: - create-hash "^1.1.1" + md5.js "^1.3.4" + safe-buffer "^5.1.1" exec-sh@^0.2.0: version "0.2.0" @@ -2809,9 +2934,9 @@ execa@^0.2.2: path-key "^1.0.0" strip-eof "^1.0.0" -execa@^0.6.0: - version "0.6.3" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe" +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" dependencies: cross-spawn "^5.0.1" get-stream "^3.0.0" @@ -2847,7 +2972,7 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -exports-loader@^0.6.3: +exports-loader@^0.6.4: version "0.6.4" resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886" dependencies: @@ -2855,8 +2980,8 @@ exports-loader@^0.6.3: source-map "0.5.x" express@^4.13.3: - version "4.15.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" + version "4.15.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" dependencies: accepts "~1.3.3" array-flatten "1.1.1" @@ -2864,32 +2989,32 @@ express@^4.13.3: content-type "~1.0.2" cookie "0.3.1" cookie-signature "1.0.6" - debug "2.6.1" - depd "~1.1.0" + debug "2.6.8" + depd "~1.1.1" encodeurl "~1.0.1" escape-html "~1.0.3" etag "~1.8.0" - finalhandler "~1.0.0" + finalhandler "~1.0.4" fresh "0.5.0" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" parseurl "~1.3.1" path-to-regexp "0.1.7" - proxy-addr "~1.1.3" - qs "6.4.0" + proxy-addr "~1.1.5" + qs "6.5.0" range-parser "~1.2.0" - send "0.15.1" - serve-static "1.12.1" + send "0.15.4" + serve-static "1.12.4" setprototypeof "1.0.3" statuses "~1.3.1" - type-is "~1.6.14" + type-is "~1.6.15" utils-merge "1.0.0" - vary "~1.1.0" + vary "~1.1.1" extend@^3.0.0, extend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" extglob@^0.3.1: version "0.3.2" @@ -2906,9 +3031,9 @@ extract-text-webpack-plugin@^2.1.2: schema-utils "^0.3.0" webpack-sources "^1.0.1" -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" falafel@^1.0.1: version "1.2.0" @@ -2919,6 +3044,10 @@ falafel@^1.0.1: isarray "0.0.1" object-keys "^1.0.6" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -2951,9 +3080,9 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.4, fbjs@^0.8.9: - version "0.8.12" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" +fbjs@^0.8.9: + version "0.8.14" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c" dependencies: core-js "^1.0.0" isomorphic-fetch "^2.1.1" @@ -2984,8 +3113,8 @@ file-loader@^0.11.2: loader-utils "^1.0.2" filename-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" fileset@^2.0.2: version "2.0.3" @@ -3004,15 +3133,11 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -filled-array@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84" - -finalhandler@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.2.tgz#d0e36f9dbc557f2de14423df6261889e9d60c93a" +finalhandler@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7" dependencies: - debug "2.6.4" + debug "2.6.8" encodeurl "~1.0.1" escape-html "~1.0.3" on-finished "~2.3.0" @@ -3028,6 +3153,14 @@ find-cache-dir@^0.1.1: mkdirp "^0.5.1" pkg-dir "^1.0.0" +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + find-root@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/find-root/-/find-root-0.1.2.tgz#98d2267cff1916ccaf2743b3a0eea81d79d7dcd1" @@ -3072,12 +3205,18 @@ for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" -for-own@^0.1.3, for-own@^0.1.4: +for-own@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" dependencies: for-in "^1.0.1" +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + foreach@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -3111,11 +3250,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" fsevents@^1.0.0, fsevents@^1.0.14: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.1.tgz#f19fd28f43eeaf761680e519a203c4d0b3d31aff" + version "1.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" dependencies: nan "^2.3.0" - node-pre-gyp "^0.6.29" + node-pre-gyp "^0.6.36" fstream-ignore@^1.0.5: version "1.0.5" @@ -3138,17 +3277,17 @@ function-bind@^1.0.2, function-bind@^1.1.0, function-bind@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" -function.prototype.name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.0.tgz#5f523ca64e491a5f95aba80cc1e391080a14482e" +function.prototype.name@^1.0.0, function.prototype.name@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.3.tgz#0099ae5572e9dd6f03c97d023fd92bcc5e639eac" dependencies: define-properties "^1.1.2" function-bind "^1.1.0" - is-callable "^1.1.2" + is-callable "^1.1.3" fuse.js@^2.2.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-2.6.2.tgz#d5d994fda96f543b5a51df38b72cec9cc60d9dea" + version "2.7.4" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-2.7.4.tgz#96e420fde7ef011ac49c258a621314fe576536f9" fuzzy@^0.1.1: version "0.1.3" @@ -3162,7 +3301,7 @@ gather-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b" -gauge@~2.7.1: +gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" dependencies: @@ -3218,8 +3357,8 @@ get-window@^1.1.1: get-document "1" getpass@^0.1.1: - version "0.1.6" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" dependencies: assert-plus "^1.0.0" @@ -3250,14 +3389,14 @@ glob@^6.0.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.2" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -3284,9 +3423,9 @@ global@^4.3.0: min-document "^2.19.0" process "~0.5.1" -globals@^9.0.0, globals@^9.14.0: - version "9.17.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" +globals@^9.14.0, globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" globby@^4.0.0: version "4.1.0" @@ -3310,7 +3449,7 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^6.0.0: +globby@^6.0.0, globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" dependencies: @@ -3325,47 +3464,39 @@ globjoin@^0.1.2, globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" globule@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.1.0.tgz#c49352e4dc183d85893ee825385eb994bb6df45f" + version "1.2.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" dependencies: glob "~7.1.1" - lodash "~4.16.4" + lodash "~4.17.4" minimatch "~3.0.2" -good-listener@^1.2.0: +good-listener@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" dependencies: delegate "^3.1.2" -got@^5.0.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" dependencies: - create-error-class "^3.0.1" - duplexer2 "^0.1.4" + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" is-redirect "^1.0.0" is-retry-allowed "^1.0.0" is-stream "^1.0.0" lowercase-keys "^1.0.0" - node-status-codes "^1.0.0" - object-assign "^4.0.1" - parse-json "^2.1.0" - pinkie-promise "^2.0.0" - read-all-stream "^3.0.0" - readable-stream "^2.0.5" - timed-out "^3.0.0" - unzip-response "^1.0.2" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" url-parse-lax "^1.0.0" graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -3375,8 +3506,8 @@ handle-thing@^1.2.5: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" handlebars@^4.0.3: - version "4.0.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" + version "4.0.10" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" dependencies: async "^1.4.0" optimist "^0.6.1" @@ -3409,6 +3540,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3419,12 +3554,147 @@ has@^1.0.1, has@~1.0.1: dependencies: function-bind "^1.0.2" -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573" +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" dependencies: inherits "^2.0.1" +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hast-to-hyperscript@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-3.0.2.tgz#8468ed08b8382f130e003a38ef735bcf29737336" + dependencies: + comma-separated-tokens "^1.0.0" + is-nan "^1.2.1" + kebab-case "^1.0.0" + property-information "^3.0.0" + space-separated-tokens "^1.0.0" + trim "0.0.1" + unist-util-is "^2.0.0" + +hast-util-embedded@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hast-util-embedded/-/hast-util-embedded-1.0.0.tgz#49d6114b40933a9d0bd708a3b012378f2cd6e86c" + dependencies: + hast-util-is-element "^1.0.0" + +hast-util-from-parse5@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-1.1.0.tgz#359cc339dc8ccf1dfaca41915ad63fd546130acd" + dependencies: + camelcase "^3.0.0" + has "^1.0.1" + hastscript "^3.0.0" + property-information "^3.1.0" + vfile-location "^2.0.0" + +hast-util-has-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-1.0.0.tgz#211f9d7f7640898244a33f5d16f5c5d1880c8e40" + +hast-util-is-body-ok-link@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-1.0.1.tgz#f5d8893f4f21fa1ae51c059ac29abdbc8e6e6046" + dependencies: + hast-util-has-property "^1.0.0" + hast-util-is-element "^1.0.0" + +hast-util-is-element@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.0.0.tgz#3f7216978b2ae14d98749878782675f33be3ce00" + +hast-util-parse-selector@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.1.0.tgz#b55c0f4bb7bb2040c889c325ef87ab29c38102b4" + +hast-util-raw@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-1.2.0.tgz#99b69c0a3ca307c5472ef372888bf6cdc1f48eaa" + dependencies: + hast-util-from-parse5 "^1.0.0" + hast-util-to-parse5 "^2.0.0" + html-void-elements "^1.0.1" + parse5 "^3.0.0" + unist-util-position "^3.0.0" + web-namespaces "^1.0.0" + zwitch "^1.0.0" + +hast-util-sanitize@^1.0.0, hast-util-sanitize@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-1.1.1.tgz#c439852d9db7ff554ecd6be96435a6a8274ade32" + dependencies: + xtend "^4.0.1" + +hast-util-to-html@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-3.1.0.tgz#882c99849e40130e991c042e456d453d95c36cff" + dependencies: + ccount "^1.0.0" + comma-separated-tokens "^1.0.1" + hast-util-is-element "^1.0.0" + hast-util-whitespace "^1.0.0" + html-void-elements "^1.0.0" + kebab-case "^1.0.0" + property-information "^3.1.0" + space-separated-tokens "^1.0.0" + stringify-entities "^1.0.1" + unist-util-is "^2.0.0" + xtend "^4.0.1" + +hast-util-to-mdast@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hast-util-to-mdast/-/hast-util-to-mdast-1.2.0.tgz#2812f744286186043d5d526dfc964ff026be07b7" + dependencies: + hast-util-has-property "^1.0.0" + hast-util-is-element "^1.0.0" + hast-util-to-string "^1.0.0" + rehype-minify-whitespace "^2.0.0" + unist-util-is "^2.1.0" + unist-util-visit "^1.1.1" + xtend "^4.0.1" + +hast-util-to-parse5@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-2.2.0.tgz#48c8f7f783020c04c3625db06109d02017033cbc" + dependencies: + hast-to-hyperscript "^3.0.0" + mapz "^1.0.0" + web-namespaces "^1.0.0" + xtend "^4.0.1" + zwitch "^1.0.0" + +hast-util-to-string@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-1.0.1.tgz#b28055cdca012d3c8fd048757c8483d0de0d002c" + +hast-util-whitespace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.0.tgz#bd096919625d2936e1ff17bc4df7fd727f17ece9" + +hastscript@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-3.1.0.tgz#66628ba6d7f1ad07d9277dd09028aba7f4934599" + dependencies: + camelcase "^3.0.0" + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.0.0" + property-information "^3.0.0" + space-separated-tokens "^1.0.0" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -3467,6 +3737,10 @@ hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.0.3, hoist-non-react-s version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" +hoist-non-react-statics@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.0.tgz#ede16318c2ff1f9fe3a025396ba06fd4c44608bb" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -3481,8 +3755,8 @@ homedir-polyfill@^1.0.0: parse-passwd "^1.0.0" hosted-git-info@^2.1.4: - version "2.4.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67" + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" hpack.js@^2.1.6: version "2.1.6" @@ -3508,8 +3782,20 @@ html-entities@^1.2.0: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" html-tags@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.1.1.tgz#869f43859f12d9bdc3892419e494a628aa1b204e" + version "1.2.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.2.0.tgz#c78de65b5663aa597989dd2b7ab49200d7e4db98" + +html-tags@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" + +html-void-elements@^1.0.0, html-void-elements@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.2.tgz#9d22e0ca32acc95b3f45b8d5b4f6fbdc05affd55" + +html-whitespace-sensitive-tag-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-1.0.0.tgz#fd6ed3a3d631ce29341aefe26a8fea720d3adfa7" htmlparser2@^3.9.0, htmlparser2@^3.9.1: version "3.9.2" @@ -3526,19 +3812,11 @@ http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" -http-errors@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" +http-errors@~1.6.1, http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" dependencies: - inherits "2.0.3" - setprototypeof "1.0.2" - statuses ">= 1.3.1 < 2" - -http-errors@~1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" - dependencies: - depd "1.1.0" + depd "1.1.1" inherits "2.0.3" setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" @@ -3575,13 +3853,17 @@ hyphenate-style-name@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b" -iconv-lite@0.4.13, iconv-lite@~0.4.13: +iconv-lite@0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" -icss-replace-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5" +iconv-lite@~0.4.13: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" icss-utils@^2.1.0: version "2.1.0" @@ -3600,8 +3882,8 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" ignore@^3.2.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" + version "3.3.3" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d" image-extensions@^1.0.1: version "1.1.0" @@ -3616,8 +3898,8 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" immutability-helper@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.1.2.tgz#734506440d7209b74664dcadaa8ba14e73f2185b" + version "2.3.1" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.3.1.tgz#8ccfce92157208c120b2afad7ed05c11114c086e" dependencies: invariant "^2.2.0" @@ -3625,6 +3907,10 @@ immutable@^3.7.6, immutable@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + imports-loader@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.7.1.tgz#f204b5f34702a32c1db7d48d89d5e867a0441253" @@ -3647,8 +3933,8 @@ indent-string@^2.1.0: repeating "^2.0.0" indent-string@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.1.0.tgz#08ff4334603388399b329e6b9538dc7a3cf5de7d" + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" indexes-of@^1.0.1: version "1.0.1" @@ -3684,9 +3970,9 @@ inline-process-browser@^1.0.0: falafel "^1.0.1" through2 "^0.6.5" -inline-style-prefixer@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-3.0.2.tgz#989865e0c5de7a946acbea71e16e02741efe0dd7" +inline-style-prefixer@^3.0.6: + version "3.0.7" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-3.0.7.tgz#0ccc92e5902fe6e0d28d975c4258443f880615f8" dependencies: bowser "^1.6.0" css-in-js-utils "^1.0.3" @@ -3709,6 +3995,12 @@ inquirer@^0.12.0: strip-ansi "^3.0.0" through "^2.3.6" +internal-ip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + interpret@^0.6.4: version "0.6.6" resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" @@ -3717,7 +4009,7 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@2.x.x, invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1: +invariant@2.x.x, invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3727,13 +4019,17 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" -ipaddr.js@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +ipaddr.js@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" irregular-plurals@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.2.0.tgz#38f299834ba8c00c30be9c554e137269752ff3ac" + version "1.3.0" + resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.3.0.tgz#7af06931bdf74be33dcf585a13e06fccc16caecf" is-absolute-url@^2.0.0: version "2.1.0" @@ -3746,6 +4042,21 @@ is-absolute@^0.2.3: is-relative "^0.2.1" is-windows "^0.2.0" +is-alphabetical@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.1.tgz#c77079cc91d4efac775be1034bf2d243f95e6f08" + +is-alphanumeric@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" + +is-alphanumerical@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.1.tgz#dfb4aa4d1085e33bdb61c2dee9c80e9c6c19f53b" + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -3756,7 +4067,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.0.2: +is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" @@ -3766,7 +4077,7 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-callable@^1.1.1, is-callable@^1.1.2, is-callable@^1.1.3: +is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" @@ -3786,13 +4097,21 @@ is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" +is-decimal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.1.tgz#f5fb6a94996ad9e8e3761fbfbd091f1fca8c4e82" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + is-dom@^1.0.5: version "1.0.9" resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.0.9.tgz#483832d52972073de12b9fe3f60320870da8370d" is-dotfile@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" is-empty@^1.0.0: version "1.2.0" @@ -3852,31 +4171,51 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-hexadecimal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" + is-image@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-image/-/is-image-1.0.1.tgz#6fd51a752a1a111506d060d952118b0b989b426e" dependencies: image-extensions "^1.0.1" +is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + is-my-json-valid@^2.10.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693" + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" dependencies: generate-function "^2.0.0" generate-object-property "^1.1.0" jsonpointer "^4.0.0" xtend "^4.0.0" +is-nan@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" + dependencies: + define-properties "^1.1.1" + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" -is-number@^2.0.2, is-number@^2.1.0: +is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" dependencies: kind-of "^3.0.2" +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -3902,10 +4241,10 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" is-plain-object@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.3.tgz#c15bf3e4b66b62d72efaf2925848663ecbc619b6" + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" dependencies: - isobject "^3.0.0" + isobject "^3.0.1" is-posix-bracket@^0.1.0: version "0.1.1" @@ -3927,7 +4266,7 @@ is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" -is-regex@^1.0.3: +is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" dependencies: @@ -3993,6 +4332,14 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +is-whitespace-character@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.1.tgz#9ae0176f3282b65457a1992cdb084f8a5f833e3b" + +is-window@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" + is-windows@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" @@ -4001,6 +4348,10 @@ is-windows@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" +is-word-character@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.1.tgz#5a03fa1ea91ace8a6eb0c7cd770eb86d65c8befb" + is@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" @@ -4027,9 +4378,9 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" -isobject@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.0.tgz#39565217f3661789e8a0a0c080d5f7e6bc46e1a0" +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" isomorphic-fetch@^2.1.1: version "2.2.1" @@ -4043,64 +4394,65 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" istanbul-api@^1.1.1: - version "1.1.7" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.7.tgz#f6f37f09f8002b130f891c646b70ee4a8e7345ae" + version "1.1.12" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.12.tgz#92d67e9d8f9ea87349a64a70ddf5a7a8cdf97f21" dependencies: async "^2.1.4" fileset "^2.0.2" - istanbul-lib-coverage "^1.0.2" - istanbul-lib-hook "^1.0.5" - istanbul-lib-instrument "^1.7.0" - istanbul-lib-report "^1.0.0" - istanbul-lib-source-maps "^1.1.1" - istanbul-reports "^1.0.2" + istanbul-lib-coverage "^1.1.1" + istanbul-lib-hook "^1.0.7" + istanbul-lib-instrument "^1.7.5" + istanbul-lib-report "^1.1.1" + istanbul-lib-source-maps "^1.2.1" + istanbul-reports "^1.1.1" js-yaml "^3.7.0" mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.0.2, istanbul-lib-coverage@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz#caca19decaef3525b5d6331d701f3f3b7ad48528" +istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" -istanbul-lib-hook@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.5.tgz#6ca3d16d60c5f4082da39f7c5cd38ea8a772b88e" +istanbul-lib-hook@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc" dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.0, istanbul-lib-instrument@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.1.tgz#169e31bc62c778851a99439dd99c3cc12184d360" +istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.2, istanbul-lib-instrument@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.5.tgz#adb596f8f0cb8b95e739206351a38a586af21b1e" dependencies: babel-generator "^6.18.0" babel-template "^6.16.0" babel-traverse "^6.18.0" babel-types "^6.18.0" - babylon "^6.13.0" - istanbul-lib-coverage "^1.1.0" + babylon "^6.17.4" + istanbul-lib-coverage "^1.1.1" semver "^5.3.0" -istanbul-lib-report@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.0.0.tgz#d83dac7f26566b521585569367fe84ccfc7aaecb" +istanbul-lib-report@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9" dependencies: - istanbul-lib-coverage "^1.0.2" + istanbul-lib-coverage "^1.1.1" mkdirp "^0.5.1" path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.1.1.tgz#f8c8c2e8f2160d1d91526d97e5bd63b2079af71c" +istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c" dependencies: - istanbul-lib-coverage "^1.0.2" + debug "^2.6.3" + istanbul-lib-coverage "^1.1.1" mkdirp "^0.5.1" - rimraf "^2.4.4" + rimraf "^2.6.1" source-map "^0.5.3" -istanbul-reports@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.0.2.tgz#4e8366abe6fa746cc1cd6633f108de12cc6ac6fa" +istanbul-reports@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.1.tgz#042be5c89e175bc3f86523caab29c014e77fee4e" dependencies: handlebars "^4.0.3" @@ -4187,8 +4539,8 @@ jest-environment-node@^20.0.3: jest-util "^20.0.3" jest-haste-map@^20.0.4: - version "20.0.4" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.4.tgz#653eb55c889ce3c021f7b94693f20a4159badf03" + version "20.0.5" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112" dependencies: fb-watchman "^2.0.0" graceful-fs "^4.1.11" @@ -4319,26 +4671,20 @@ jju@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa" -jodid25519@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" - dependencies: - jsbn "~0.1.0" - -js-base64@^2.1.9, js-base64@~2.1.8: +js-base64@^2.1.8, js-base64@^2.1.9, js-base64@~2.1.8: version "2.1.9" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" -js-tokens@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" js-yaml@^3.4.2, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0, js-yaml@^3.8.1: - version "3.8.3" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" + version "3.9.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: argparse "^1.0.7" - esprima "^3.1.1" + esprima "^4.0.0" js-yaml@~3.7.0: version "3.7.0" @@ -4384,8 +4730,8 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" json-loader@^0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" json-parse-helpfulerror@^1.0.3: version "1.0.3" @@ -4393,6 +4739,10 @@ json-parse-helpfulerror@^1.0.3: dependencies: jju "^1.1.0" +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -4437,13 +4787,13 @@ jsonpointer@^4.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" jsprim@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" dependencies: assert-plus "1.0.0" - extsprintf "1.0.2" + extsprintf "1.3.0" json-schema "0.2.3" - verror "1.3.6" + verror "1.10.0" jstransform@~3.0.0: version "3.0.0" @@ -4461,9 +4811,13 @@ jwt-decode@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" +kebab-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/kebab-case/-/kebab-case-1.0.0.tgz#3f9e4990adcad0c686c0e701f7645868f75f91eb" + keycode@^2.1.1, keycode@^2.1.2: - version "2.1.8" - resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.8.tgz#94d2b7098215eff0e8f9a8931d5a59076c4532fb" + version "2.1.9" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" kind-of@^2.0.1: version "2.0.1" @@ -4471,21 +4825,27 @@ kind-of@^2.0.1: dependencies: is-buffer "^1.0.2" -kind-of@^3.0.2: +kind-of@^3.0.2, kind-of@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +known-css-properties@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.2.0.tgz#899c94be368e55b42d7db8d5be7d73a4a4a41454" + +latest-version@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" dependencies: - is-buffer "^1.0.2" - -known-css-properties@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.0.7.tgz#9104343a2adfd8ef3b07bdee7a325e4d44ed9371" - -latest-version@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b" - dependencies: - package-json "^2.0.0" + package-json "^4.0.0" lazy-cache@^0.2.3: version "0.2.7" @@ -4534,16 +4894,18 @@ linkify-it@~1.2.2: dependencies: uc.micro "^1.0.1" -lint-staged@^3.1.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.4.0.tgz#52fa85dfc92bb1c6fe8ad0d0d98ca13924e03e4b" +lint-staged@^3.3.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.6.1.tgz#24423c8b7bd99d96e15acd1ac8cb392a78e58582" dependencies: app-root-path "^2.0.0" cosmiconfig "^1.1.0" - execa "^0.6.0" - listr "^0.11.0" + execa "^0.7.0" + listr "^0.12.0" + lodash.chunk "^4.2.0" minimatch "^3.0.0" npm-which "^3.0.1" + p-map "^1.1.1" staged-git-files "0.0.4" listr-silent-renderer@^1.1.1: @@ -4572,9 +4934,9 @@ listr-verbose-renderer@^0.4.0: date-fns "^1.27.2" figures "^1.7.0" -listr@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.11.0.tgz#5e778bc23806ac3ab984ed75564458151f39b03e" +listr@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" dependencies: chalk "^1.1.3" cli-truncate "^0.2.1" @@ -4588,6 +4950,7 @@ listr@^0.11.0: log-symbols "^1.0.2" log-update "^1.0.2" ora "^0.2.3" + p-map "^1.1.1" rxjs "^5.0.0-beta.11" stream-to-observable "^0.1.0" strip-ansi "^3.0.1" @@ -4615,7 +4978,7 @@ loader-utils@^0.2.11, loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.x: +loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -4674,6 +5037,10 @@ lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + lodash.clonedeep@^4.3.2: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -4786,11 +5153,15 @@ lodash.throttle@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -"lodash@4.6.1 || ^4.16.1", lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1, lodash@^4.7.0: +"lodash@4.6.1 || ^4.16.1", lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1, lodash@^4.7.0, lodash@~4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4798,10 +5169,6 @@ lodash@^3.7.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@~4.16.4: - version "4.16.6" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" - log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -4815,11 +5182,19 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" +loglevel@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd" + +longest-streak@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.1.tgz#42d291b5411e40365c00e63193497e2247316e35" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -4843,11 +5218,11 @@ lru-cache@^3.2.0: pseudomap "^1.0.1" lru-cache@^4.0.0, lru-cache@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" dependencies: - pseudomap "^1.0.1" - yallist "^2.0.0" + pseudomap "^1.0.2" + yallist "^2.1.2" ltrim@0.0.3: version "0.0.3" @@ -4857,6 +5232,12 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -4875,6 +5256,16 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +mapz@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mapz/-/mapz-1.0.1.tgz#9ecec757d3c3fe0a8a6f363e328eaee69a428441" + dependencies: + x-is-array "^0.1.0" + +markdown-escapes@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.1.tgz#1994df2d3af4811de59a6714934c2b2292734518" + markdown-it@^6.0.4: version "6.1.1" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-6.1.1.tgz#ced037f4473ee9f5153ac414f77dc83c91ba927c" @@ -4885,6 +5276,10 @@ markdown-it@^6.0.4: mdurl "~1.0.1" uc.micro "^1.0.1" +markdown-table@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.1.tgz#4b3dd3a133d1518b8ef0dbc709bf2a1b4824bc8c" + markup-it@^2.0.0: version "2.5.0" resolved "https://registry.yarnpkg.com/markup-it/-/markup-it-2.5.0.tgz#239bb2f445b4c8664af8527168ee3c00bc0d451c" @@ -4905,8 +5300,48 @@ material-design-icons@^3.0.1: resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf" math-expression-evaluator@^1.2.14: - version "1.2.16" - resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9" + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +mathml-tag-names@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz#8d41268168bf86d1102b98109e28e531e7a34578" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mdast-util-compact@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.1.tgz#cdb5f84e2b6a2d3114df33bd05d9cb32e3c4083a" + dependencies: + unist-util-modify-children "^1.0.0" + unist-util-visit "^1.1.0" + +mdast-util-definitions@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.2.tgz#673f4377c3e23d3de7af7a4fe2214c0e221c5ac7" + dependencies: + unist-util-visit "^1.0.0" + +mdast-util-to-hast@^2.1.1, mdast-util-to-hast@^2.2.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-2.4.2.tgz#f116e8bf3da772ba5a397a92dab090f5ba91caa0" + dependencies: + collapse-white-space "^1.0.0" + detab "^2.0.0" + mdast-util-definitions "^1.2.0" + normalize-uri "^1.0.0" + trim "0.0.1" + trim-lines "^1.0.0" + unist-builder "^1.0.1" + unist-util-generated "^1.1.0" + unist-util-position "^3.0.0" + unist-util-visit "^1.1.0" + xtend "^4.0.1" mdurl@~1.0.1: version "1.0.1" @@ -4996,20 +5431,24 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.27.0 < 2", mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" +"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" -mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" +mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" dependencies: - mime-db "~1.27.0" + mime-db "~1.29.0" -mime@1.3.4, mime@1.3.x, mime@^1.3.4: +mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mime@1.3.x, mime@^1.3.4: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -5024,13 +5463,19 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@~3.0.2: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimatch@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: brace-expansion "^1.0.0" -minimist@0.0.8, minimist@~0.0.1: +minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5038,6 +5483,10 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1. version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + mixin-object@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" @@ -5059,22 +5508,21 @@ moment@^2.11.2: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" - -ms@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde" + dependencies: + dns-packet "^1.0.1" + thunky "^0.1.0" + multimatch@^2.0.0, multimatch@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" @@ -5107,21 +5555,25 @@ netlify-auth-js@^0.5.5: micro-api-client "^2.0.0" node-emoji@^1.0.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.1.tgz#fd918e412769bf8c448051238233840b2aff16a1" + version "1.8.1" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" dependencies: - string.prototype.codepointat "^0.2.0" + lodash.toarray "^4.4.0" node-fetch@^1.0.1: - version "1.6.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" + version "1.7.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7" dependencies: encoding "^0.1.11" is-stream "^1.0.1" +node-forge@0.6.33: + version "0.6.33" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" + node-gyp@^3.3.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.0.tgz#7474f63a3a0501161dda0b6341f022f14c423fa6" + version "3.6.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" dependencies: fstream "^1.0.0" glob "^7.0.3" @@ -5234,9 +5686,9 @@ node-notifier@^5.0.2: shellwords "^0.1.0" which "^1.2.12" -node-pre-gyp@^0.6.29: - version "0.6.34" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.34.tgz#94ad1c798a11d7fc67381b50d47f8cc18d9799f7" +node-pre-gyp@^0.6.36: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" dependencies: mkdirp "^0.5.1" nopt "^4.0.1" @@ -5269,10 +5721,6 @@ node-sass@^3.10.0: request "^2.61.0" sass-graph "^2.1.1" -node-status-codes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" - "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -5287,15 +5735,15 @@ nopt@^4.0.1: osenv "^0.1.4" normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.3.8" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.8.tgz#d819eda2a9dedbd1ffa563ea4071d936782295bb" + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: hosted-git-info "^2.1.4" is-builtin-module "^1.0.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.1: +normalize-path@^2.0.0, normalize-path@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: @@ -5309,6 +5757,10 @@ normalize-selector@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" +normalize-uri@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/normalize-uri/-/normalize-uri-1.1.0.tgz#01fb440c7fd059b9d9be8645aac14341efd059dd" + normalize-url@^1.4.0: version "1.9.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" @@ -5323,8 +5775,8 @@ normalize.css@^4.2.0: resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-4.2.0.tgz#21d66cc557154d4379fd1e079ec7de58a379b099" npm-check@^5.2.3: - version "5.4.0" - resolved "https://registry.yarnpkg.com/npm-check/-/npm-check-5.4.0.tgz#21f537e474616458beaccd6ecfc9f73f246ee7bd" + version "5.4.5" + resolved "https://registry.yarnpkg.com/npm-check/-/npm-check-5.4.5.tgz#ee2c601b7015ed892b87c923c98e000ee36736ef" dependencies: babel-runtime "^6.6.1" callsite-record "^3.0.0" @@ -5343,14 +5795,14 @@ npm-check@^5.2.3: minimatch "^3.0.2" node-emoji "^1.0.3" ora "^0.2.1" - package-json "^2.0.1" + package-json "^4.0.1" path-exists "^2.1.0" pkg-dir "^1.0.0" semver "^5.0.1" semver-diff "^2.0.0" text-table "^0.2.0" throat "^2.0.2" - update-notifier "^0.6.3" + update-notifier "^2.1.0" npm-path@^2.0.2: version "2.0.3" @@ -5379,12 +5831,12 @@ npm-which@^3.0.1: which "^1.2.10" "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f" + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: are-we-there-yet "~1.1.2" console-control-strings "~1.1.0" - gauge "~2.7.1" + gauge "~2.7.3" set-blocking "~2.0.0" nth-check@~1.0.1: @@ -5402,8 +5854,8 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" "nwmatcher@>= 1.3.9 < 2.0.0": - version "1.3.9" - resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.3.9.tgz#8bab486ff7fa3dfd086656bbe8b17116d3692d2a" + version "1.4.1" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.1.tgz#7ae9b07b0ea804db7e25f05cb5fe4097d4e4949f" oauth-sign@~0.8.1: version "0.8.2" @@ -5417,9 +5869,9 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -object-inspect@~1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.2.2.tgz#c82115e4fcc888aea14d64c22e4f17f6a70d5e5a" +object-inspect@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d" object-is@^1.0.1: version "1.0.1" @@ -5441,7 +5893,7 @@ object.assign@^4.0.4: function-bind "^1.1.0" object-keys "^1.0.10" -object.entries@^1.0.3: +object.entries@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f" dependencies: @@ -5464,7 +5916,7 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" -object.values@^1.0.3: +object.values@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" dependencies: @@ -5567,14 +6019,14 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" -osenv@0, osenv@^0.1.0, osenv@^0.1.4: +osenv@0, osenv@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -output-file-sync@^1.1.0: +output-file-sync@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" dependencies: @@ -5600,11 +6052,11 @@ p-map@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" -package-json@^2.0.0, package-json@^2.0.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb" +package-json@^4.0.0, package-json@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" dependencies: - got "^5.0.0" + got "^6.7.1" registry-auth-token "^3.0.1" registry-url "^3.0.3" semver "^5.1.0" @@ -5629,6 +6081,17 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-entities@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -5638,7 +6101,7 @@ parse-glob@^3.0.4: is-extglob "^1.0.0" is-glob "^2.0.0" -parse-json@^2.1.0, parse-json@^2.2.0: +parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" dependencies: @@ -5652,6 +6115,16 @@ parse5@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" +parse5@^2.1.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-2.2.3.tgz#0c4fc41c1000c5e6b93d48b03f8083837834e9f6" + +parse5@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" + dependencies: + "@types/node" "^6.0.46" + parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" @@ -5670,7 +6143,7 @@ path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" -path-is-absolute@^1.0.0: +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5707,10 +6180,14 @@ pbkdf2-compat@2.0.1: resolved "https://registry.yarnpkg.com/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz#b6e0c8fa99494d94e0511575802a59a5c142f288" pbkdf2@^3.0.3: - version "3.0.9" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693" + version "3.0.13" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.13.tgz#c37d295531e786b1da3e3eadc840426accb0ae25" dependencies: - create-hmac "^1.1.2" + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" performance-now@^0.2.0: version "0.2.0" @@ -5720,6 +6197,10 @@ pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -5751,6 +6232,12 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + pkg-up@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26" @@ -5885,8 +6372,8 @@ postcss-convert-values@^2.3.4: postcss-value-parser "^3.1.2" postcss-cssnext@^2.7.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-2.10.0.tgz#30e0dddcfb978eae2523a340aa2c8ba49c5d7103" + version "2.11.0" + resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-2.11.0.tgz#31e68f001e409604da703b66de14b8b8c8c9f2b1" dependencies: autoprefixer "^6.0.2" caniuse-api "^1.5.3" @@ -5910,6 +6397,7 @@ postcss-cssnext@^2.7.0: postcss-custom-selectors "^3.0.0" postcss-font-family-system-ui "^1.0.1" postcss-font-variant "^2.0.0" + postcss-image-set-polyfill "^0.3.3" postcss-initial "^1.3.1" postcss-media-minmax "^2.1.0" postcss-nesting "^2.0.5" @@ -5992,6 +6480,13 @@ postcss-font-variant@^2.0.0: dependencies: postcss "^5.0.4" +postcss-image-set-polyfill@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz#0f193413700cf1f82bd39066ef016d65a4a18181" + dependencies: + postcss "^6.0.1" + postcss-media-query-parser "^0.2.3" + postcss-import@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-10.0.0.tgz#4c85c97b099136cc5ea0240dc1dfdbfde4e2ebbe" @@ -6015,7 +6510,7 @@ postcss-less@^0.14.0: dependencies: postcss "^5.0.21" -postcss-load-config@^1.x: +postcss-load-config@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" dependencies: @@ -6039,13 +6534,13 @@ postcss-load-plugins@^2.3.0: object-assign "^4.1.0" postcss-loader@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.5.tgz#c19d3e8b83eb1ac316f5621ef4c0ef5b3d1b8b3a" + version "2.0.6" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.6.tgz#8c7e0055a3df1889abc6bad52dd45b2f41bbc6fc" dependencies: - loader-utils "^1.x" - postcss "^6.x" - postcss-load-config "^1.x" - schema-utils "^0.x" + loader-utils "^1.1.0" + postcss "^6.0.2" + postcss-load-config "^1.2.0" + schema-utils "^0.3.0" postcss-media-minmax@^2.1.0: version "2.1.2" @@ -6053,7 +6548,7 @@ postcss-media-minmax@^2.1.0: dependencies: postcss "^5.0.4" -postcss-media-query-parser@^0.2.0: +postcss-media-query-parser@^0.2.0, postcss-media-query-parser@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" @@ -6119,31 +6614,31 @@ postcss-minify-selectors@^2.0.4: postcss-selector-parser "^2.0.0" postcss-modules-extract-imports@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.0.1.tgz#8fb3fef9a6dd0420d3f6d4353cf1ff73f2b2a341" + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" dependencies: - postcss "^5.0.4" + postcss "^6.0.1" postcss-modules-local-by-default@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.1.1.tgz#29a10673fa37d19251265ca2ba3150d9040eb4ce" + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" dependencies: - css-selector-tokenizer "^0.6.0" - postcss "^5.0.4" + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" postcss-modules-scope@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.0.2.tgz#ff977395e5e06202d7362290b88b1e8cd049de29" + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" dependencies: - css-selector-tokenizer "^0.6.0" - postcss "^5.0.4" + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" postcss-modules-values@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.2.2.tgz#f0e7d476fe1ed88c5e4c7f97533a3e772ad94ca1" + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" dependencies: - icss-replace-symbols "^1.0.2" - postcss "^5.0.14" + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" postcss-nesting@^2.0.5: version "2.3.1" @@ -6330,13 +6825,13 @@ postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0. source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.1, postcss@^6.x: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.2.tgz#5c4fea589f0ac3b00caa75b1cbc3a284195b7e5d" +postcss@^6.0.1, postcss@^6.0.2: + version "6.0.9" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.9.tgz#54819766784a51c65b1ec4d54c2f93765438c35a" dependencies: - chalk "^1.1.3" + chalk "^2.1.0" source-map "^0.5.6" - supports-color "^3.2.3" + supports-color "^4.2.1" preliminaries-parser-toml@1.1.0: version "1.1.0" @@ -6380,7 +6875,7 @@ prismjs@^1.5.1: optionalDependencies: clipboard "^1.5.5" -private@^0.1.6, private@~0.1.5: +private@^0.1.6, private@^0.1.7, private@~0.1.5: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -6389,8 +6884,8 @@ process-nextick-args@~1.0.6: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" process@^0.11.0, process@~0.11.0: - version "0.11.9" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" process@~0.5.1: version "0.5.2" @@ -6400,115 +6895,128 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +promise.prototype.finally@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.0.0.tgz#afb1710ff2068562966f6d006d12c3107c7a4f39" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + function-bind "^1.1.0" + promise@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" dependencies: asap "~2.0.3" -prop-types@^15.0.0, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: - version "15.5.8" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" dependencies: fbjs "^0.8.9" + loose-envify "^1.3.1" -prosemirror-commands@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-0.16.0.tgz#061ec07de7a695cc3afbf0fb8b1ce7b8feaafb3b" - dependencies: - prosemirror-model "^0.16.0" - prosemirror-state "^0.16.0" - prosemirror-transform "^0.16.0" +property-information@^3.0.0, property-information@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-3.2.0.tgz#fd1483c8fbac61808f5fe359e7693a1f48a58331" -prosemirror-history@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-0.16.0.tgz#62babd5e048f9b035b539b297514350856b3e27f" +prosemirror-commands@^0.17.0: + version "0.17.1" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-0.17.1.tgz#c27f74f76230a41e26620bfcda7e1e34b1f99dbf" dependencies: - prosemirror-state "^0.16.0" - prosemirror-transform "^0.16.0" + prosemirror-model "^0.17.0" + prosemirror-state "^0.17.0" + prosemirror-transform "^0.17.0" + +prosemirror-history@^0.17.0: + version "0.17.2" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-0.17.2.tgz#2ebdd52b606a48eb49f0f608b561ebec2a4eaf11" + dependencies: + prosemirror-state "^0.17.0" + prosemirror-transform "^0.17.0" rope-sequence "^1.2.0" -prosemirror-inputrules@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-0.16.0.tgz#dc2d3f3e79c4c28ed7e1208608fa854f25f88aba" +prosemirror-inputrules@^0.17.0: + version "0.17.1" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-0.17.1.tgz#eb0eb909b9509448cba838a3443cb5d32e9b1e99" dependencies: - prosemirror-state "^0.16.0" - prosemirror-transform "^0.16.0" + prosemirror-state "^0.17.0" + prosemirror-transform "^0.17.0" -prosemirror-keymap@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-0.16.0.tgz#00d98e62161f78d17bcec88dbe0dde822dfd461d" +prosemirror-keymap@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-0.17.0.tgz#760ed65586e6116079730b68f46bff0ba0c19031" dependencies: - prosemirror-state "^0.16.0" + prosemirror-state "^0.17.0" w3c-keyname "^1.1.0" -prosemirror-markdown@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-0.16.0.tgz#3d5c66841340f5e4e3f81109faaa1760d93b1dfd" +prosemirror-markdown@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-0.17.0.tgz#56ff74671e01b0f6109bf73b0abe98196151a3c7" dependencies: markdown-it "^6.0.4" - prosemirror-model "~0.16.0" + prosemirror-model "~0.17.0" -prosemirror-model@^0.16.0, prosemirror-model@~0.16.0: - version "0.16.1" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-0.16.1.tgz#02bb4d0912d9f8847c002902304b784124736f6d" +prosemirror-model@^0.17.0, prosemirror-model@~0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-0.17.0.tgz#ba81887038290833b032fd4e70ee526b07ca8ebf" dependencies: orderedmap "^1.0.0" -prosemirror-schema-basic@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-0.16.0.tgz#1a7c45fd25b5307e361edb16ab31f308dbacde41" +prosemirror-schema-basic@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-0.17.0.tgz#4f0e16459d68fb3ad909d620a97e554fe11ecbe8" dependencies: - prosemirror-model "^0.16.0" + prosemirror-model "^0.17.0" -prosemirror-schema-list@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-0.16.0.tgz#b7dd1e7f42da4e487b1eb0199a9c77e78dcadc93" +prosemirror-schema-list@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-0.17.0.tgz#fd78f7ecb5652913cf4e1f322845730b6eefbe28" dependencies: - prosemirror-model "^0.16.0" - prosemirror-transform "^0.16.0" + prosemirror-model "^0.17.0" + prosemirror-transform "^0.17.0" -prosemirror-schema-table@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-table/-/prosemirror-schema-table-0.16.0.tgz#b94dc190dd86b3a976faa8d6444b7a23821bb30f" +prosemirror-schema-table@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-schema-table/-/prosemirror-schema-table-0.17.0.tgz#60cb24c57a96c1c99147073d6980440fc929f5ce" dependencies: - prosemirror-model "^0.16.0" - prosemirror-state "^0.16.0" - prosemirror-transform "^0.16.0" + prosemirror-model "^0.17.0" + prosemirror-state "^0.17.0" + prosemirror-transform "^0.17.0" -prosemirror-state@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-0.16.0.tgz#51959ff8c0624058ce2abf2481f988a7bf33fe78" +prosemirror-state@^0.17.0: + version "0.17.1" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-0.17.1.tgz#712efb9485a945d4b42d01ce63fd116472e16038" dependencies: - prosemirror-model "^0.16.0" - prosemirror-transform "^0.16.0" + prosemirror-model "^0.17.0" + prosemirror-transform "^0.17.0" -prosemirror-transform@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-0.16.0.tgz#fec4296d0876fa7e8f698773fa0d7008e2779317" +prosemirror-transform@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-0.17.0.tgz#565b53f533fa30c7292354f8ba09f18916e5d939" dependencies: - prosemirror-model "^0.16.0" + prosemirror-model "^0.17.0" -prosemirror-view@^0.16.0: - version "0.16.2" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-0.16.2.tgz#63ed4cdfd000242d769b8cac6ea94b6de22b1954" +prosemirror-view@^0.17.0: + version "0.17.7" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-0.17.7.tgz#0e4007658d0f16c280173a57ed57a4a5d7a47bf2" dependencies: - prosemirror-model "^0.16.0" - prosemirror-state "^0.16.0" - prosemirror-transform "^0.16.0" + prosemirror-model "^0.17.0" + prosemirror-state "^0.17.0" + prosemirror-transform "^0.17.0" -proxy-addr@~1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" +proxy-addr@~1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" dependencies: forwarded "~0.1.0" - ipaddr.js "1.3.0" + ipaddr.js "1.4.0" prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" -pseudomap@^1.0.1: +pseudomap@^1.0.1, pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -6534,7 +7042,11 @@ q@^1.1.2: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" -qs@6.4.0, qs@^6.1.0, qs@^6.2.0, qs@~6.4.0: +qs@6.5.0, qs@^6.1.0, qs@^6.2.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" + +qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -6563,16 +7075,22 @@ querystringify@0.0.x: version "0.0.4" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" +querystringify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" + randomatic@^1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" dependencies: - is-number "^2.0.2" - kind-of "^3.0.2" + is-number "^3.0.0" + kind-of "^4.0.0" randombytes@^2.0.0, randombytes@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec" + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" range-parser@^1.0.3, range-parser@~1.2.0: version "1.2.0" @@ -6595,18 +7113,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: strip-json-comments "~2.0.1" react-addons-css-transition-group@^15.0.2, react-addons-css-transition-group@^15.3.1: - version "15.5.2" - resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.5.2.tgz#ea7e0a9f0e1c27ca426da4efd3559915bd42ead2" + version "15.6.0" + resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.6.0.tgz#69887cf6e4874d25cd66e22a699e29f0d648aba0" dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" + react-transition-group "^1.2.0" -react-addons-test-utils@^15.3.2: - version "15.5.1" - resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.5.1.tgz#e0d258cda2a122ad0dff69f838260d0c3958f5f7" - dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" +react-addons-test-utils@^15.4.2: + version "15.6.0" + resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.0.tgz#062d36117fe8d18f3ba5e06eb33383b0b85ea5b9" react-autosuggest@^7.0.1: version "7.1.0" @@ -6631,46 +7145,51 @@ react-css-themr@^1.7.1: invariant "^2.2.1" react-datetime@^2.6.0: - version "2.8.10" - resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.8.10.tgz#06d4317b7734310e0e8109555526656128346132" + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.9.0.tgz#9ec80060cbb8e5c5d8f98f0acebb6f4712ce449a" dependencies: - create-react-class "^15.5.2" + "@types/react" ">=15" object-assign "^3.0.0" prop-types "^15.5.7" react-onclickoutside "^5.9.0" react-deep-force-update@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-2.0.1.tgz#4f7f6c12c3e7de42f345992a3c518236fa1ecad3" + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-2.1.1.tgz#8ea4263cd6455a050b37445b3f08fd839d86e909" react-dnd-html5-backend@^2.1.2: - version "2.3.0" - resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.3.0.tgz#a45ce593f5c6944aa01114b368117c56c954804e" + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.4.1.tgz#439d2bcaf8bd8b87a51386beb51c128826182ddd" dependencies: lodash "^4.2.0" react-dnd@^2.1.4: - version "2.3.0" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.3.0.tgz#aede61c06b968554dcf2a2445657cdbb3100be49" + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.4.0.tgz#96f0042cd4cd375b4f0c3413f6ec84d267b7d792" dependencies: disposables "^1.0.1" - dnd-core "^2.3.0" + dnd-core "^2.4.0" hoist-non-react-statics "^1.2.0" invariant "^2.1.0" lodash "^4.2.0" + prop-types "^15.5.8" + +react-dom-factories@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.1.tgz#c50692ac5ff1adb39d86dfe6dbe3485dacf58455" react-dom@^15.1.0: - version "15.5.4" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da" + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470" dependencies: fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" - prop-types "~15.5.7" + prop-types "^15.5.10" react-frame-component@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-1.0.3.tgz#00a5deea81671927ea973954a0d8eb19ecc339de" + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-1.1.1.tgz#05b7f5689a2d373f25baf0c9adb0e59d78103388" react-fuzzy@^0.2.3: version "0.2.3" @@ -6722,14 +7241,15 @@ react-lazy-load@^3.0.3: prop-types "^15.5.8" react-modal@^1.2.1: - version "1.7.7" - resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-1.7.7.tgz#70205f51c58708c487aff681ba3fed7946e391d9" + version "1.9.7" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-1.9.7.tgz#07ef56790b953e3b98ef1e2989e347983c72871d" dependencies: create-react-class "^15.5.2" element-class "^0.2.0" exenv "1.2.0" lodash.assign "^4.2.0" prop-types "^15.5.7" + react-dom-factories "^1.0.0" react-onclickoutside@^5.9.0: version "5.11.1" @@ -6741,6 +7261,12 @@ react-portal@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-2.2.1.tgz#0cde8c35eeb0cce9a67b1e1255d5e4a2d147d1cf" +react-portal@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.1.0.tgz#865c44fb72a1da106c649206936559ce891ee899" + dependencies: + prop-types "^15.5.8" + react-proxy@^3.0.0-alpha.0: version "3.0.0-alpha.1" resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-3.0.0-alpha.1.tgz#4400426bcfa80caa6724c7755695315209fa4b07" @@ -6763,16 +7289,15 @@ react-redux@^4.4.0: prop-types "^15.5.4" react-redux@^5.0.1: - version "5.0.4" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.4.tgz#1563babadcfb2672f57f9ceaa439fb16bf85d55b" + version "5.0.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" dependencies: - create-react-class "^15.5.1" - hoist-non-react-statics "^1.0.3" + hoist-non-react-statics "^2.2.1" invariant "^2.0.0" lodash "^4.2.0" lodash-es "^4.2.0" loose-envify "^1.1.0" - prop-types "^15.0.0" + prop-types "^15.5.10" react-router-redux@^4.0.5: version "4.0.8" @@ -6811,11 +7336,11 @@ react-sortable@^1.2.0: resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849" react-split-pane@^0.1.57: - version "0.1.63" - resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.63.tgz#fadb3960cc659911dd05ffbc88acee4be9f53583" + version "0.1.66" + resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.66.tgz#369085dd07ec1237bda123e73813dcc7dc6502c1" dependencies: - inline-style-prefixer "^3.0.2" - prop-types "^15.5.8" + inline-style-prefixer "^3.0.6" + prop-types "^15.5.10" react-style-proptype "^3.0.0" react-style-proptype@^3.0.0: @@ -6852,25 +7377,29 @@ react-topbar-progress-indicator@^1.0.0: dependencies: topbar "^0.1.3" +react-transition-group@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.0.tgz#b51fc921b0c3835a7ef7c571c79fc82c73e9204f" + dependencies: + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" + react-waypoint@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-3.1.3.tgz#1101fb8a27556a199150c7bfd34428606b5fc7e4" react@^15.1.0: - version "15.5.4" - resolved "https://registry.yarnpkg.com/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047" + version "15.6.1" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" dependencies: + create-react-class "^15.6.0" fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" - prop-types "^15.5.7" - -read-all-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" - dependencies: - pinkie-promise "^2.0.0" - readable-stream "^2.0.0" + prop-types "^15.5.10" read-cache@^1.0.0: version "1.0.0" @@ -6917,16 +7446,16 @@ readable-stream@^1.0.33, readable-stream@~1.1.9: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9: - version "2.2.9" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8" +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: - buffer-shims "~1.0.0" core-util-is "~1.0.0" - inherits "~2.0.1" + inherits "~2.0.3" isarray "~1.0.0" process-nextick-args "~1.0.6" - string_decoder "~1.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" util-deprecate "~1.0.1" readdirp@^2.0.0: @@ -6962,18 +7491,19 @@ rechoir@^0.6.2: resolve "^1.1.6" recursive-readdir@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.1.1.tgz#a01cfc7f7f38a53ec096a096f63a50489c3e297c" + version "2.2.1" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" dependencies: minimatch "3.0.3" redbox-react@^1.2.2, redbox-react@^1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/redbox-react/-/redbox-react-1.3.6.tgz#70314c57c066257eb70b0a24dc794b5cef4f1c4e" + version "1.5.0" + resolved "https://registry.yarnpkg.com/redbox-react/-/redbox-react-1.5.0.tgz#04dab11557d26651bf3562a67c22ace56c5d3967" dependencies: error-stack-parser "^1.3.6" object-assign "^4.0.1" prop-types "^15.5.4" + sourcemapped-stacktrace "^1.1.6" redent@^1.0.0: version "1.0.0" @@ -7011,25 +7541,29 @@ redux-thunk@^1.0.3: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-1.0.3.tgz#778aa0099eea0595031ab6b39165f6670d8d26bd" redux@^3.2.0, redux@^3.3.1, redux@^3.5.2, redux@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" dependencies: lodash "^4.2.1" lodash-es "^4.2.1" loose-envify "^1.1.0" - symbol-observable "^1.0.2" + symbol-observable "^1.0.3" regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" -regenerator-runtime@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz#8c4367a904b51ea62a908ac310bf99ff90a82a3e" +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" -regenerator-transform@0.9.11: - version "0.9.11" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" +regenerator-runtime@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" dependencies: babel-runtime "^6.18.0" babel-types "^6.19.0" @@ -7059,8 +7593,8 @@ regexpu-core@^2.0.0: regjsparser "^0.1.4" registry-auth-token@^3.0.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.0.tgz#57ae67347e73d96345ed1bc01294c7237c02aa63" + version "3.3.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.1.tgz#fb0d3289ee0d9ada2cbb52af5dfe66cb070d3006" dependencies: rc "^1.1.6" safe-buffer "^5.0.1" @@ -7081,15 +7615,123 @@ regjsparser@^0.1.4: dependencies: jsesc "~0.5.0" +rehype-minify-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rehype-minify-whitespace/-/rehype-minify-whitespace-2.0.0.tgz#30d83de99bc0adc59accd889223fe511921c6045" + dependencies: + collapse-white-space "^1.0.0" + hast-util-embedded "^1.0.0" + hast-util-has-property "^1.0.0" + hast-util-is-body-ok-link "^1.0.0" + hast-util-is-element "^1.0.0" + html-whitespace-sensitive-tag-names "^1.0.0" + unist-util-is "^2.0.0" + unist-util-modify-children "^1.0.0" + +rehype-parse@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-3.1.0.tgz#7f5227a597a3f39fc4b938646161539c444ee728" + dependencies: + hast-util-from-parse5 "^1.0.0" + parse5 "^2.1.5" + xtend "^4.0.1" + +rehype-raw@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-1.0.0.tgz#6f2f8ebe6858f8304dfe2704ccc6cb29ae8d858e" + dependencies: + hast-util-raw "^1.0.0" + +rehype-react@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-3.0.1.tgz#a54f3a40969a059b5b9aa7c591b13e0344a8bc60" + dependencies: + has "^1.0.1" + hast-to-hyperscript "^3.0.0" + +rehype-remark@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rehype-remark/-/rehype-remark-2.1.0.tgz#84cadd41410d23de8f83e141e92342c2df94c1c8" + dependencies: + hast-util-to-mdast "^1.1.0" + +rehype-sanitize@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-2.0.1.tgz#ab2866cacc51b45c30696cfee7b7b30cf918465e" + dependencies: + hast-util-sanitize "^1.1.0" + +rehype-stringify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-3.0.0.tgz#9fef0868213c2dce2f780b76f3d0488c85e819eb" + dependencies: + hast-util-to-html "^3.0.0" + xtend "^4.0.1" + +remark-html@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/remark-html/-/remark-html-6.0.1.tgz#5094d2c71f7941fdb2ae865bac76627757ce09c1" + dependencies: + hast-util-sanitize "^1.0.0" + hast-util-to-html "^3.0.0" + mdast-util-to-hast "^2.1.1" + xtend "^4.0.1" + +remark-parse@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-3.0.1.tgz#1b9f841a44d8f4fbf2246850265459a4eb354c80" + dependencies: + collapse-white-space "^1.0.2" + has "^1.0.1" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^1.0.0" + vfile-location "^2.0.0" + xtend "^4.0.1" + +remark-rehype@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-2.0.1.tgz#13e989f89ee15444bd2354dffd767da922b985e3" + dependencies: + mdast-util-to-hast "^2.2.0" + +remark-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-3.0.1.tgz#79242bebe0a752081b5809516fa0c06edec069cf" + dependencies: + ccount "^1.0.0" + is-alphanumeric "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + longest-streak "^2.0.1" + markdown-escapes "^1.0.0" + markdown-table "^1.1.0" + mdast-util-compact "^1.0.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + stringify-entities "^1.0.1" + unherit "^1.0.4" + xtend "^4.0.1" + remove-trailing-separator@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4" + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" repeat-element@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" -repeat-string@^1.5.2: +repeat-string@^1.5.2, repeat-string@^1.5.4: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -7099,6 +7741,10 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace-ext@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + request@2, request@^2.61.0, request@^2.79.0, request@^2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -7161,13 +7807,17 @@ resolve-from@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" -resolve@1.1.7, resolve@~1.1.7: +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + +resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.2.0, resolve@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.2.0, resolve@^1.3.2, resolve@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" dependencies: path-parse "^1.0.5" @@ -7198,7 +7848,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: @@ -7208,9 +7858,12 @@ ripemd160@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce" -ripemd160@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" rope-sequence@^1.2.0: version "1.2.2" @@ -7231,14 +7884,14 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" rxjs@^5.0.0-beta.11: - version "5.3.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.3.0.tgz#d88ccbdd46af290cbdb97d5d8055e52453fabe2d" + version "5.4.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" dependencies: symbol-observable "^1.0.1" -safe-buffer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" sane@~1.6.0: version "1.6.0" @@ -7253,33 +7906,41 @@ sane@~1.6.0: watch "~0.10.0" sass-graph@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.1.2.tgz#965104be23e8103cb7e5f710df65935b317da57b" + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" dependencies: glob "^7.0.0" lodash "^4.0.0" - yargs "^4.7.1" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" sass-loader@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.5.tgz#a847910f36442aa56c5985879d54eb519e24a328" + version "6.0.6" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" dependencies: async "^2.1.5" - clone-deep "^0.2.4" + clone-deep "^0.3.0" loader-utils "^1.0.1" lodash.tail "^4.1.1" - pify "^2.3.0" + pify "^3.0.0" sax@^1.2.1, sax@~1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -schema-utils@^0.3.0, schema-utils@^0.x: +schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" dependencies: ajv "^5.0.0" +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + section-iterator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" @@ -7292,13 +7953,23 @@ select@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" +selection-is-backward@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1" + selection-position@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/selection-position/-/selection-position-1.0.0.tgz#e43f87151d94957efa170e10e02c901b47f703c7" +selfsigned@^1.9.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52" + dependencies: + node-forge "0.6.33" + semaphore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.0.5.tgz#b492576e66af193db95d65e25ec53f5f19798d60" + version "1.1.0" + resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" semver-diff@^2.0.0: version "2.1.0" @@ -7306,48 +7977,52 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@~5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" -send@0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" +send@0.15.4: + version "0.15.4" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" dependencies: - debug "2.6.1" - depd "~1.1.0" + debug "2.6.8" + depd "~1.1.1" destroy "~1.0.4" encodeurl "~1.0.1" escape-html "~1.0.3" etag "~1.8.0" fresh "0.5.0" - http-errors "~1.6.1" + http-errors "~1.6.2" mime "1.3.4" - ms "0.7.2" + ms "2.0.0" on-finished "~2.3.0" range-parser "~1.2.0" statuses "~1.3.1" serve-index@^1.7.2: - version "1.8.0" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b" + version "1.9.0" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7" dependencies: accepts "~1.3.3" - batch "0.5.3" - debug "~2.2.0" + batch "0.6.1" + debug "2.6.8" escape-html "~1.0.3" - http-errors "~1.5.0" - mime-types "~2.1.11" + http-errors "~1.6.1" + mime-types "~2.1.15" parseurl "~1.3.1" -serve-static@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.1.tgz#7443a965e3ced647aceb5639fa06bf4d1bbe0039" +serve-static@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" dependencies: encodeurl "~1.0.1" escape-html "~1.0.3" parseurl "~1.3.1" - send "0.15.1" + send "0.15.4" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -7361,10 +8036,6 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" -setprototypeof@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" - setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" @@ -7373,7 +8044,7 @@ sha.js@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" -sha.js@^2.3.6: +sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.8" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" dependencies: @@ -7413,22 +8084,22 @@ shelljs@^0.6.0: resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" shelljs@^0.7.5: - version "0.7.7" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" + version "0.7.8" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" dependencies: glob "^7.0.0" interpret "^1.0.0" rechoir "^0.6.2" shellwords@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.0.tgz#66afd47b6a12932d9071cbfd98a52e785cd0ba14" + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" -signal-exit@^3.0.0: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -7447,32 +8118,30 @@ slate-drop-or-paste-images@^0.2.0: is-url "^1.2.2" mime-types "^2.1.11" -slate@^0.14.14: - version "0.14.19" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.14.19.tgz#ea54b57c55acabfa02b4e9b6fb99948669734921" +slate@^0.20.3: + version "0.20.7" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.20.7.tgz#083ca9074dc7fd3ad8863985e6d92ed76bdc9eff" dependencies: - uid "0.0.2" cheerio "^0.22.0" - debug "^2.2.0" - detect-browser "^1.3.3" + debug "^2.3.2" direction "^0.1.5" + es6-map "^0.1.4" esrever "^0.2.0" get-window "^1.1.1" immutable "^3.8.1" is-empty "^1.0.0" + is-in-browser "^1.1.3" + is-window "^1.0.2" keycode "^2.1.2" - lodash "^4.13.1" + prop-types "^15.5.8" + react-portal "^3.1.0" + selection-is-backward "^1.0.0" type-of "^2.0.1" - ua-parser-js "^0.7.10" slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" -slide@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" - slug@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.1.tgz#af08f608a7c11516b61778aa800dce84c518cfda" @@ -7485,16 +8154,16 @@ sntp@1.x.x: dependencies: hoek "2.x.x" -sockjs-client@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5" +sockjs-client@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" dependencies: - debug "^2.2.0" + debug "^2.6.6" eventsource "0.1.6" faye-websocket "~0.11.0" inherits "^2.0.1" json3 "^3.3.2" - url-parse "^1.1.1" + url-parse "^1.1.8" sockjs@0.3.18: version "0.3.18" @@ -7509,21 +8178,17 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@^0.1.7, source-list-map@~0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" - -source-list-map@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.2.tgz#9889019d1024cce55cdc069498337ef6186a11a1" - source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" -source-map-support@^0.4.2: - version "0.4.14" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.14.tgz#9d4463772598b86271b4f523f6c1f4e02a7d6aef" +source-list-map@~0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" + +source-map-support@^0.4.15: + version "0.4.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.16.tgz#16fecf98212467d017d586a2af68d628b9421cd8" dependencies: source-map "^0.5.6" @@ -7533,10 +8198,14 @@ source-map@0.1.31: dependencies: amdefine ">=0.0.4" -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" +source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + source-map@^0.4.2, source-map@^0.4.4, source-map@~0.4.1, source-map@~0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" @@ -7549,6 +8218,18 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" +sourcemapped-stacktrace@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.7.tgz#17e05374ff78b71a9d89ad3975a49f22725ba935" + dependencies: + source-map "0.5.6" + +space-separated-tokens@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.1.tgz#9695b9df9e65aec1811d4c3f9ce52520bc2f7e4d" + dependencies: + trim "0.0.1" + spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -7591,8 +8272,8 @@ specificity@^0.2.1: resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.2.1.tgz#3a7047c2a179f35362e3990745cea539f15161b8" specificity@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.0.tgz#332472d4e5eb5af20821171933998a6bc3b1ce6f" + version "0.3.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.1.tgz#f1b068424ce317ae07478d95de3c21cf85e8d567" split2@^0.2.1: version "0.2.1" @@ -7605,8 +8286,8 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" sshpk@^1.7.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.0.tgz#ff2a3e4fd04497555fed97b39a0fd82fafb3a33c" + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -7615,13 +8296,12 @@ sshpk@^1.7.0: optionalDependencies: bcrypt-pbkdf "^1.0.0" ecc-jsbn "~0.1.1" - jodid25519 "^1.0.0" jsbn "~0.1.0" tweetnacl "~0.14.0" stack-source-map@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/stack-source-map/-/stack-source-map-1.0.6.tgz#60216e4d4d0f2b15f3c6bd56abeca5b4e0f0d0d4" + version "1.0.7" + resolved "https://registry.yarnpkg.com/stack-source-map/-/stack-source-map-1.0.7.tgz#ca2d72aafbcc340c5474de3b6a158d0f3495f4bc" dependencies: path-browserify "0.0.0" source-map "^0.5.6" @@ -7634,6 +8314,10 @@ staged-git-files@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" +state-toggle@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" + "statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -7657,8 +8341,8 @@ stream-combiner@^0.2.1: through "~2.3.4" stream-http@^2.3.1: - version "2.7.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.0.tgz#cec1f4e3b494bc4a81b451808970f8b20b4ed5f6" + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" dependencies: builtin-status-codes "^3.0.0" inherits "^2.0.1" @@ -7689,15 +8373,11 @@ string-width@^1.0.1, string-width@^1.0.2: strip-ansi "^3.0.0" string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" - -string.prototype.codepointat@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" + strip-ansi "^4.0.0" string.prototype.padend@^3.0.0: version "3.0.0" @@ -7727,11 +8407,20 @@ string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.0.tgz#f06f41157b664d86069f84bdbdc9b0d8ab281667" +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: - buffer-shims "~1.0.0" + safe-buffer "~5.1.0" + +stringify-entities@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-1.3.1.tgz#b150ec2d72ac4c1b5f324b51fb6b28c9cdff058c" + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-hexadecimal "^1.0.0" stringstream@~0.0.4: version "0.0.5" @@ -7743,6 +8432,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -7875,13 +8570,13 @@ stylelint@^6.8.0: svg-tags "^1.0.0" table "^3.7.8" -stylelint@^7.3.1, stylelint@^7.5.0: - version "7.10.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-7.10.1.tgz#209a7ce5e781fc2a62489fbb31ec0201ec675db2" +stylelint@^7.3.1, stylelint@^7.5.0, stylelint@^7.9.0: + version "7.13.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-7.13.0.tgz#111f97b6da72e775c80800d6bb6f5f869997785d" dependencies: autoprefixer "^6.0.0" balanced-match "^0.4.0" - chalk "^1.1.1" + chalk "^2.0.1" colorguard "^1.2.0" cosmiconfig "^2.1.1" debug "^2.6.0" @@ -7891,15 +8586,17 @@ stylelint@^7.3.1, stylelint@^7.5.0: get-stdin "^5.0.0" globby "^6.0.0" globjoin "^0.1.4" - html-tags "^1.1.1" + html-tags "^2.0.0" ignore "^3.2.0" imurmurhash "^0.1.4" - known-css-properties "^0.0.7" + known-css-properties "^0.2.0" lodash "^4.17.4" log-symbols "^1.0.2" + mathml-tag-names "^2.0.0" meow "^3.3.0" micromatch "^2.3.11" normalize-selector "^0.2.0" + pify "^2.3.0" postcss "^5.0.20" postcss-less "^0.14.0" postcss-media-query-parser "^0.2.0" @@ -7908,7 +8605,7 @@ stylelint@^7.3.1, stylelint@^7.5.0: postcss-scss "^0.4.0" postcss-selector-parser "^2.1.1" postcss-value-parser "^3.1.1" - resolve-from "^2.0.0" + resolve-from "^3.0.0" specificity "^0.3.0" string-width "^2.0.0" style-search "^0.1.0" @@ -7939,6 +8636,12 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-co dependencies: has-flag "^1.0.0" +supports-color@^4.0.0, supports-color@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" + dependencies: + has-flag "^2.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -7955,7 +8658,7 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" -symbol-observable@^1.0.1, symbol-observable@^1.0.2: +symbol-observable@^1.0.1, symbol-observable@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" @@ -7995,24 +8698,24 @@ tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" -tapable@^0.2.5, tapable@~0.2.5: - version "0.2.6" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" +tapable@^0.2.7, tapable@~0.2.5: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" tape@^4.2.0: - version "4.6.3" - resolved "https://registry.yarnpkg.com/tape/-/tape-4.6.3.tgz#637e77581e9ab2ce17577e9bd4ce4f575806d8b6" + version "4.8.0" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e" dependencies: deep-equal "~1.0.1" defined "~1.0.0" for-each "~0.3.2" function-bind "~1.1.0" - glob "~7.1.1" + glob "~7.1.2" has "~1.0.1" inherits "~2.0.3" minimist "~1.2.0" - object-inspect "~1.2.1" - resolve "~1.1.7" + object-inspect "~1.3.0" + resolve "~1.4.0" resumer "~0.0.0" string.prototype.trim "~1.1.2" through "~2.3.8" @@ -8038,9 +8741,15 @@ tar@^2.0.0, tar@^2.2.1: fstream "^1.0.2" inherits "2" -test-exclude@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.0.tgz#04ca70b7390dd38c98d4a003a173806ca7991c91" +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + dependencies: + execa "^0.7.0" + +test-exclude@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" dependencies: arrify "^1.0.1" micromatch "^2.3.11" @@ -8061,8 +8770,8 @@ throat@^2.0.2: resolved "https://registry.yarnpkg.com/throat/-/throat-2.0.2.tgz#a9fce808b69e133a632590780f342c30a6249b02" throat@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-3.0.0.tgz#e7c64c867cbb3845f10877642f7b60055b8ec0d6" + version "3.2.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-3.2.0.tgz#50cb0670edbc40237b9e347d7e1f88e4620af836" through2@^0.6.1, through2@^0.6.2, through2@^0.6.3, through2@^0.6.5, through2@~0.6.1: version "0.6.5" @@ -8075,9 +8784,17 @@ through2@^0.6.1, through2@^0.6.2, through2@^0.6.3, through2@^0.6.5, through2@~0. version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" -timed-out@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" +thunky@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" + +time-stamp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" timers-browserify@^1.4.2: version "1.4.2" @@ -8086,14 +8803,14 @@ timers-browserify@^1.4.2: process "~0.11.0" timers-browserify@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86" + version "2.0.4" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" dependencies: setimmediate "^1.0.4" -tiny-emitter@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.2.0.tgz#6dc845052cb08ebefc1874723b58f24a648c3b6f" +tiny-emitter@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" tmp@0.0.30: version "0.0.30" @@ -8109,9 +8826,9 @@ to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" -to-fast-properties@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" toml-js@0.0.8: version "0.0.8" @@ -8135,6 +8852,10 @@ tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" +trim-lines@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.0.tgz#9926d03ede13ba18f7d42222631fb04c79ff26fe" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -8143,6 +8864,18 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +trim-trailing-lines@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.0.tgz#7aefbb7808df9d669f6da2e438cac8c46ada7684" + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + +trough@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.1.tgz#a9fd8b0394b0ae8fff82e0633a0a36ccad5b5f86" + tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" @@ -8167,7 +8900,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.14: +type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: @@ -8182,17 +8915,17 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -ua-parser-js@^0.7.10, ua-parser-js@^0.7.9: - version "0.7.12" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb" +ua-parser-js@^0.7.9: + version "0.7.14" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" uc.micro@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" uglify-js@^2.6, uglify-js@^2.8.27: - version "2.8.28" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.28.tgz#e335032df9bb20dcb918f164589d5af47f38834a" + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" dependencies: source-map "~0.5.1" yargs "~3.10.0" @@ -8216,18 +8949,33 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" -uid@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/uid/-/uid-0.0.2.tgz#5e4a5d4b78138b4f70f89fd3c76fc59aa9d2f103" - unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" +unherit@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.0.tgz#6b9aaedfbf73df1756ad9e316dd981885840cd7d" + dependencies: + inherits "^2.0.1" + xtend "^4.0.1" + "unicode@>= 0.3.1": version "9.0.1" resolved "https://registry.yarnpkg.com/unicode/-/unicode-9.0.1.tgz#104706272c6464c574801be1b086f7245cf25158" +unified@^6.1.4: + version "6.1.5" + resolved "https://registry.yarnpkg.com/unified/-/unified-6.1.5.tgz#716937872621a63135e62ced2f3ac6a063c6fb87" + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-plain-obj "^1.1.0" + trough "^1.0.0" + vfile "^2.0.0" + x-is-function "^1.0.4" + x-is-string "^0.1.0" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -8242,6 +8990,50 @@ uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + +unist-builder@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-1.0.2.tgz#8c3b9903ef64bcfb117dd7cf6a5d98fc1b3b27b6" + dependencies: + object-assign "^4.1.0" + +unist-util-generated@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.1.tgz#99f16c78959ac854dee7c615c291924c8bf4de7f" + +unist-util-is@^2.0.0, unist-util-is@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" + +unist-util-modify-children@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d" + dependencies: + array-iterate "^1.0.0" + +unist-util-position@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.0.0.tgz#e6e1e03eeeb81c5e1afe553e8d4adfbd7c0d8f82" + +unist-util-remove-position@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.1.tgz#5a85c1555fc1ba0c101b86707d15e50fa4c871bb" + dependencies: + unist-util-visit "^1.1.0" + +unist-util-stringify-position@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.1.tgz#3ccbdc53679eed6ecf3777dd7f5e3229c1b6aa3c" + +unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.3.tgz#ec268e731b9d277a79a5b5aa0643990e405d600b" + units-css@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" @@ -8261,20 +9053,22 @@ unreachable-branch-transform@^0.3.0: recast "^0.10.1" through2 "^0.6.2" -unzip-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -update-notifier@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.6.3.tgz#776dec8daa13e962a341e8a1d98354306b67ae08" +update-notifier@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.2.0.tgz#1b5837cf90c0736d88627732b661c138f86de72f" dependencies: - boxen "^0.3.1" + boxen "^1.0.0" chalk "^1.0.0" - configstore "^2.0.0" + configstore "^3.0.0" + import-lazy "^2.1.0" is-npm "^1.0.0" - latest-version "^2.0.0" + latest-version "^3.0.0" semver-diff "^2.0.0" + xdg-basedir "^3.0.0" url-loader@^0.5.9: version "0.5.9" @@ -8296,11 +9090,11 @@ url-parse@1.0.x: querystringify "0.0.x" requires-port "1.0.x" -url-parse@^1.1.1: - version "1.1.8" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.8.tgz#7a65b3a8d57a1e86af6b4e2276e34774167c0156" +url-parse@^1.1.8: + version "1.1.9" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.9.tgz#c67f1d775d51f0a18911dd7b3ffad27bb9e5bd19" dependencies: - querystringify "0.0.x" + querystringify "~1.0.0" requires-port "1.0.x" url@^0.11.0: @@ -8338,11 +9132,11 @@ uuid@^2.0.1, uuid@^2.0.2, uuid@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" +uuid@^3.0.0, uuid@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -v8flags@^2.0.10: +v8flags@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" dependencies: @@ -8355,7 +9149,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -vary@~1.1.0: +vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" @@ -8363,11 +9157,25 @@ vendors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" dependencies: - extsprintf "1.0.2" + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vfile-location@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.2.tgz#d3675c59c877498e492b4756ff65e4af1a752255" + +vfile@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.2.0.tgz#ce47a4fb335922b233e535db0f7d8121d8fced4e" + dependencies: + is-buffer "^1.1.4" + replace-ext "1.0.0" + unist-util-stringify-position "^1.0.0" viewport-dimensions@^0.2.0: version "0.2.0" @@ -8418,11 +9226,11 @@ watchpack@^0.2.1: graceful-fs "^4.1.2" watchpack@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" dependencies: async "^2.1.2" - chokidar "^1.4.3" + chokidar "^1.7.0" graceful-fs "^4.1.2" wbuf@^1.1.0, wbuf@^1.7.2: @@ -8431,13 +9239,17 @@ wbuf@^1.1.0, wbuf@^1.7.2: dependencies: minimalistic-assert "^1.0.0" +web-namespaces@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.1.tgz#742d9fff61ff84f4164f677244f42d29c10c451d" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" webidl-conversions@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0" + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" webpack-core@~0.6.9: version "0.6.9" @@ -8446,40 +9258,47 @@ webpack-core@~0.6.9: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@^1.10.2, webpack-dev-middleware@^1.6.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.2.tgz#2e252ce1dfb020dbda1ccb37df26f30ab014dbd1" +webpack-dev-middleware@^1.11.0, webpack-dev-middleware@^1.6.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" dependencies: memory-fs "~0.4.1" mime "^1.3.4" path-is-absolute "^1.0.0" range-parser "^1.0.3" + time-stamp "^2.0.0" webpack-dev-server@^2.4.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.4.5.tgz#31384ce81136be1080b4b4cde0eb9b90e54ee6cf" + version "2.7.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8" dependencies: ansi-html "0.0.7" + bonjour "^3.5.0" chokidar "^1.6.0" compression "^1.5.2" connect-history-api-fallback "^1.3.0" + del "^3.0.0" express "^4.13.3" html-entities "^1.2.0" http-proxy-middleware "~0.17.4" + internal-ip "^1.2.0" + ip "^1.1.5" + loglevel "^1.4.1" opn "4.0.2" portfinder "^1.0.9" + selfsigned "^1.9.1" serve-index "^1.7.2" sockjs "0.3.18" - sockjs-client "1.1.2" + sockjs-client "1.1.4" spdy "^3.4.1" strip-ansi "^3.0.0" supports-color "^3.1.1" - webpack-dev-middleware "^1.10.2" + webpack-dev-middleware "^1.11.0" yargs "^6.0.0" webpack-hot-middleware@^2.10.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.18.0.tgz#a16bb535b83a6ac94a78ac5ebce4f3059e8274d3" + version "2.18.2" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.18.2.tgz#84dee643f037c3d59c9de142548430371aa8d3b2" dependencies: ansi-html "0.0.7" html-entities "^1.2.0" @@ -8500,13 +9319,6 @@ webpack-postcss-tools@^1.1.1: postcss "^4.1.7" resolve "^1.1.6" -webpack-sources@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb" - dependencies: - source-list-map "^1.1.1" - source-map "~0.5.3" - webpack-sources@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" @@ -8535,15 +9347,15 @@ webpack@^1.12.11: webpack-core "~0.6.9" webpack@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.6.1.tgz#2e0457f0abb1ac5df3ab106c69c672f236785f07" + version "2.7.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.7.0.tgz#b2a1226804373ffd3d03ea9c6bd525067034f6b1" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" ajv "^4.7.0" ajv-keywords "^1.1.1" async "^2.1.2" - enhanced-resolve "^3.0.0" + enhanced-resolve "^3.3.0" interpret "^1.0.0" json-loader "^0.5.4" json5 "^0.5.1" @@ -8557,7 +9369,7 @@ webpack@^2.6.1: tapable "~0.2.5" uglify-js "^2.8.27" watchpack "^1.3.1" - webpack-sources "^0.2.3" + webpack-sources "^1.0.1" yargs "^6.0.0" websocket-driver@>=0.5.1: @@ -8576,13 +9388,17 @@ whatwg-encoding@^1.0.1: dependencies: iconv-lite "0.4.13" -whatwg-fetch@>=0.10.0, whatwg-fetch@^1.0.0: +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + +whatwg-fetch@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz#ac3c9d39f320c6dce5339969d054ef43dd333319" whatwg-url@^4.3.0: - version "4.7.1" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.7.1.tgz#df4dc2e3f25a63b1fa5b32ed6d6c139577d690de" + version "4.8.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" dependencies: tr46 "~0.0.3" webidl-conversions "^3.0.0" @@ -8596,16 +9412,16 @@ which-module@^1.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" which@1, which@^1.2.10, which@^1.2.12, which@^1.2.8, which@^1.2.9: - version "1.2.14" - resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: isexe "^2.0.0" wide-align@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" dependencies: - string-width "^1.0.1" + string-width "^1.0.2" widest-line@^1.0.0: version "1.0.0" @@ -8638,11 +9454,11 @@ wordwrap@~1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" worker-farm@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.3.1.tgz#4333112bb49b17aa050b87895ca6b2cacf40e5ff" + version "1.5.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" dependencies: - errno ">=0.1.1 <0.2.0-0" - xtend ">=4.0.0 <4.1.0-0" + errno "^0.1.4" + xtend "^4.0.1" wrap-ansi@^2.0.0: version "2.1.0" @@ -8655,13 +9471,13 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -write-file-atomic@^1.1.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.3.tgz#831dd22d491bdc135180bb996a0eb3f8bf587791" +write-file-atomic@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" dependencies: graceful-fs "^4.1.11" imurmurhash "^0.1.4" - slide "^1.1.5" + signal-exit "^3.0.2" write-file-stdout@0.0.2: version "0.0.2" @@ -8673,17 +9489,27 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -xdg-basedir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" - dependencies: - os-homedir "^1.0.0" +x-is-array@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/x-is-array/-/x-is-array-0.1.0.tgz#de520171d47b3f416f5587d629b89d26b12dc29d" + +x-is-function@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/x-is-function/-/x-is-function-1.0.4.tgz#5d294dc3d268cbdd062580e0c5df77a391d1fa1e" + +x-is-string@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" -"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0: +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -8691,7 +9517,7 @@ y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" -yallist@^2.0.0: +yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -8767,7 +9593,7 @@ yargs@^6.0.0: y18n "^3.2.1" yargs-parser "^4.2.0" -yargs@^7.0.2: +yargs@^7.0.0, yargs@^7.0.2: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" dependencies: @@ -8793,3 +9619,7 @@ yargs@~3.10.0: cliui "^2.1.0" decamelize "^1.0.0" window-size "0.1.0" + +zwitch@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.2.tgz#9b059541bfa844799fe2d903bde609de2503a041" From 5a664f8be105c9c27cfe828712867e34f001ab96 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 27 Jun 2017 12:39:23 -0400 Subject: [PATCH 32/79] 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 = {}; From a8fe57e5d6d9e48825026fc051d819bf812f3dd6 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 29 Jun 2017 17:56:20 -0400 Subject: [PATCH 33/79] pre-process visual editor pastes w/ unified --- .../Widgets/Markdown/MarkdownControl/VisualEditor/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 96afba71..26f36ee0 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -270,7 +270,9 @@ export default class Editor extends Component { if (data.type !== 'html' || data.isShift) { return; } - const fragment = serializer.deserialize(data.html).document; + const markdown = htmlToMarkdown(data.html); + const html = markdownToHtml(markdown); + const fragment = serializer.deserialize(html).document; return state.transform().insertFragment(fragment).apply(); } From c49d84b2ebed85b3a34fca4c7097237af6dde777 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 5 Jul 2017 15:21:12 -0400 Subject: [PATCH 34/79] add empty node and Paper emoji unified plugins --- .../MarkdownControl/VisualEditor/index.js | 1 + src/components/Widgets/Markdown/unified.js | 56 ++++++++++++++++--- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 26f36ee0..b801510f 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -193,6 +193,7 @@ const RULES = [ return { kind: 'inline', type: 'image', + isVoid: true, nodes: [], data: { src: el.attribs.src, diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 0d7e6957..6a306e35 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -1,3 +1,4 @@ +import find from 'lodash/find'; import unified from 'unified'; import markdownToRemark from 'remark-parse'; import remarkToRehype from 'remark-rehype'; @@ -12,22 +13,61 @@ const remarkParseConfig = { fences: true }; const remarkStringifyConfig = { listItemIndent: '1', fences: true }; const rehypeParseConfig = { fragment: true }; -export const markdownToHtml = markdown => - unified() +const rehypeRemoveEmpty = () => { + const isVoidElement = node => ['img', 'hr'].includes(node.tagName); + const isNonEmptyText = node => node.type === 'text' && node.value; + const isNonEmptyNode = node => { + return isVoidElement(node) || isNonEmptyText(node) || find(node.children, isNonEmptyNode); + }; + + const transform = node => { + if (isVoidElement(node) || isNonEmptyText(node)) { + return node; + } + if (node.children) { + node.children = node.children.reduce((acc, childNode) => { + if (isVoidElement(childNode) || isNonEmptyText(childNode)) { + return acc.concat(childNode); + } + return find(childNode.children, isNonEmptyNode) ? acc.concat(transform(childNode)) : acc; + }, []); + } + return node; + }; + return transform; +}; + +const rehypePaperEmoji = () => { + const 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; +}; + +export const markdownToHtml = markdown => { + console.log('markdownToHtml input', markdown); + const result = unified() .use(markdownToRemark, remarkParseConfig) .use(remarkToRehype) - .use(rehypeSanitize) - .use(rehypeMinifyWhitespace) .use(rehypeToHtml) .processSync(markdown) .contents; + console.log('markdownToHtml output', result); + return result; +} -export const htmlToMarkdown = html => - unified() +export const htmlToMarkdown = html => { + console.log('htmlToMarkdown input', html); + const result = unified() .use(htmlToRehype, rehypeParseConfig) - .use(rehypeSanitize) - .use(rehypeMinifyWhitespace) .use(rehypeToRemark) .use(remarkToMarkdown, remarkStringifyConfig) .processSync(html) .contents; + console.log('htmlToMarkdown output', result); + return result; +}; From 804ef3d4b3e48e0149538ec6a7d69a619e1be9b6 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 7 Jul 2017 18:14:43 -0400 Subject: [PATCH 35/79] use true source maps --- webpack.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.dev.js b/webpack.dev.js index 2782961b..44dbace0 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -53,7 +53,7 @@ module.exports = merge.smart(require('./webpack.base.js'), { disable: true, }), ], - devtool: 'cheap-module-source-map', + devtool: 'source-map', devServer: { hot: true, contentBase: 'example/', From b08a9fcaa8cfe672ee5b8c5e2a4cc7b9336a3099 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 7 Jul 2017 18:28:15 -0400 Subject: [PATCH 36/79] improve Dropbox Paper paste handling --- src/components/Widgets/Markdown/unified.js | 53 ++++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 6a306e35..f91eb5e1 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -13,6 +13,9 @@ const remarkParseConfig = { fences: true }; const remarkStringifyConfig = { listItemIndent: '1', fences: true }; const rehypeParseConfig = { fragment: true }; +/** + * Remove empty nodes, including the top level parents of deeply nested empty nodes. + */ const rehypeRemoveEmpty = () => { const isVoidElement = node => ['img', 'hr'].includes(node.tagName); const isNonEmptyText = node => node.type === 'text' && node.value; @@ -37,6 +40,44 @@ const rehypeRemoveEmpty = () => { return transform; }; +/** + * If the first child of a list item is a list, include it in the previous list + * item. Otherwise it translates to markdown as having two bullets. When + * rehype-remark processes a list and finds children that are not list items, it + * wraps them in list items, which leads to the condition this plugin addresses. + * Dropbox Paper currently outputs this kind of HTML, which is invalid. We have + * a support issue open for it, and this plugin can potentially be removed when + * that's resolved. + */ +const remarkNestedList = () => { + const transform = node => { + if (node.type === 'list' && node.children && node.children.length > 1) { + node.children = node.children.reduce((acc, childNode, index) => { + if (index && childNode.children && childNode.children[0].type === 'list') { + acc[acc.length - 1].children.push(transform(childNode.children.shift())) + if (childNode.children.length) { + acc.push(transform(childNode)); + } + } else { + acc.push(transform(childNode)); + } + return acc; + }, []); + return node; + } + if (node.children) { + node.children = node.children.map(transform); + } + return node; + }; + return transform; +}; + +/** + * Dropbox Paper outputs emoji characters as images, and stores the actual + * emoji character in a `data-emoji-ch` attribute on the image. This plugin + * replaces the images with the emoji characters. + */ const rehypePaperEmoji = () => { const transform = node => { if (node.tagName === 'img' && node.properties.dataEmojiCh) { @@ -49,25 +90,29 @@ const rehypePaperEmoji = () => { }; export const markdownToHtml = markdown => { - console.log('markdownToHtml input', markdown); const result = unified() .use(markdownToRemark, remarkParseConfig) .use(remarkToRehype) + .use(rehypeRemoveEmpty) + .use(rehypeSanitize) + .use(rehypeMinifyWhitespace) .use(rehypeToHtml) .processSync(markdown) .contents; - console.log('markdownToHtml output', result); return result; } export const htmlToMarkdown = html => { - console.log('htmlToMarkdown input', html); const result = unified() .use(htmlToRehype, rehypeParseConfig) + .use(rehypePaperEmoji) + .use(rehypeSanitize) + .use(rehypeRemoveEmpty) + .use(rehypeMinifyWhitespace) .use(rehypeToRemark) + .use(remarkNestedList) .use(remarkToMarkdown, remarkStringifyConfig) .processSync(html) .contents; - console.log('htmlToMarkdown output', result); return result; }; From 719c10584455def95d43bc63ae83ddd9ab880247 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Sat, 8 Jul 2017 22:09:47 -0400 Subject: [PATCH 37/79] remove logic from raw markdown editor --- .../MarkdownControl/RawEditor/index.css | 19 - .../MarkdownControl/RawEditor/index.js | 354 +----------------- .../MarkdownControl/Toolbar/Toolbar.js | 24 +- .../MarkdownControl/Toolbar/ToolbarButton.css | 5 +- .../MarkdownControl/Toolbar/ToolbarButton.js | 3 +- .../Toolbar/ToolbarComponentsMenu.js | 13 +- 6 files changed, 51 insertions(+), 367 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css index 8786390c..d200d47b 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css @@ -12,25 +12,6 @@ composes: editorControlBarSticky from "../VisualEditor/index.css"; } -.dragging { } - -.shim { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: none; - border: 2px dashed #aaa; - background: rgba(0,0,0,0.2); -} - -.dragging .shim { - z-index: 1000; - display: block; - pointer-events: none; -} - .textarea { overflow: hidden; resize: none; diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index ad05396a..a812c004 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,365 +1,49 @@ import React, { PropTypes } from 'react'; -import get from 'lodash/get'; -import unified from 'unified'; -import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; -import registry from '../../../../../lib/registry'; import { markdownToHtml, htmlToMarkdown } from '../../unified'; -import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; -const HAS_LINE_BREAK = /\n/m; - -function processUrl(url) { - if (url.match(/^(https?:\/\/|mailto:|\/)/)) { - return url; - } - if (url.match(/^[^/]+\.[^/]+/)) { - return `https://${ url }`; - } - return `/${ url }`; -} - -function getCleanPaste(e) { - const transfer = e.clipboardData; - return new Promise((resolve) => { - const isHTML = !!Array.from(transfer.types).find(type => type === 'text/html'); - - if (isHTML) { - const data = transfer.getData('text/html'); - // Avoid trying to clean up full HTML documents with head/body/etc - if (!data.match(/^\s* { - resolve(htmlToMarkdown(div.innerHTML)); - document.body.removeChild(div); - }, 50); - return null; - } - } - - e.preventDefault(); - return resolve(transfer.getData(transfer.types[0])); - }); -} - export default class RawEditor extends React.Component { constructor(props) { super(props); - const plugins = registry.getEditorComponents(); this.state = { - value: htmlToMarkdown(this.props.value), - plugins, - }; - this.shortcuts = { - meta: { - b: this.handleBold, - i: this.handleItalic, - }, + value: htmlToMarkdown(this.props.value) || '', }; } - componentDidMount() { - this.updateHeight(); - this.element.addEventListener('paste', this.handlePaste, false); - } - - componentDidUpdate() { - if (this.newSelection) { - this.element.selectionStart = this.newSelection.start; - this.element.selectionEnd = this.newSelection.end; - this.newSelection = null; - } - } - - componentWillUnmount() { - this.element.removeEventListener('paste', this.handlePaste); - } - - getSelection() { - const start = this.element.selectionStart; - const end = this.element.selectionEnd; - const selected = (this.state.value || '').substr(start, end - start); - return { start, end, selected }; - } - - surroundSelection(chars) { - const selection = this.getSelection(); - const newSelection = Object.assign({}, selection); - const { value } = this.state; - const escapedChars = chars.replace(/\*/g, '\\*'); - const regexp = new RegExp(`^${ escapedChars }.*${ escapedChars }$`); - let changed = chars + selection.selected + chars; - - if (regexp.test(selection.selected)) { - changed = selection.selected.substr(chars.length, selection.selected.length - (chars.length * 2)); - newSelection.end = selection.end - (chars.length * 2); - } else if ( - value.substr(selection.start - chars.length, chars.length) === chars && - value.substr(selection.end, chars.length) === chars - ) { - newSelection.start = selection.start - chars.length; - newSelection.end = selection.end + chars.length; - changed = selection.selected; - } else { - newSelection.end = selection.end + (chars.length * 2); - } - - const beforeSelection = value.substr(0, selection.start); - const afterSelection = value.substr(selection.end); - - this.newSelection = newSelection; - this.handleChange(beforeSelection + changed + afterSelection); - } - - replaceSelection(chars) { - const value = this.state.value || ''; - const selection = this.getSelection(); - const newSelection = Object.assign({}, selection); - const beforeSelection = value.substr(0, selection.start); - const afterSelection = value.substr(selection.end); - newSelection.end = selection.start + chars.length; - this.newSelection = newSelection; - this.handleChange(beforeSelection + chars + afterSelection); - } - - toggleHeader(header) { - const value = this.state.value || ''; - const selection = this.getSelection(); - const newSelection = Object.assign({}, selection); - const lastNewline = value.lastIndexOf('\n', selection.start); - const currentMatch = value.substr(lastNewline + 1).match(/^(#+)\s/); - const beforeHeader = value.substr(0, lastNewline + 1); - let afterHeader; - let chars; - if (currentMatch) { - afterHeader = value.substr(lastNewline + 1 + currentMatch[0].length); - chars = currentMatch[1] === header ? '' : `${ header } `; - const diff = chars.length - currentMatch[0].length; - newSelection.start += diff; - newSelection.end += diff; - } else { - afterHeader = value.substr(lastNewline + 1); - chars = `${ header } `; - newSelection.start += header.length + 1; - newSelection.end += header.length + 1; - } - this.newSelection = newSelection; - this.handleChange(beforeHeader + chars + afterHeader); - } - - updateHeight() { - if (this.element.scrollHeight > this.element.clientHeight) { - this.element.style.height = `${ this.element.scrollHeight }px`; - } - } - - handleRef = (ref) => { - this.element = ref; - if (ref) { - this.caretPosition = new CaretPosition(ref); - } - }; - - handleKey = (e) => { - if (e.metaKey) { - const action = this.shortcuts.meta[e.key]; - if (action) { - e.preventDefault(); - action(); - } - } - }; - - handleBold = () => { - this.surroundSelection('**'); - }; - - handleItalic = () => { - this.surroundSelection('*'); - }; - - handleLink = () => { - const url = prompt('URL:'); // eslint-disable-line no-alert - const selection = this.getSelection(); - this.replaceSelection(`[${ selection.selected }](${ processUrl(url) })`); - }; - - handleSelection = () => { - const value = this.state.value || ''; - const selection = this.getSelection(); - if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) { - try { - const selectionPosition = this.caretPosition.get(selection.start, selection.end); - this.setState({ selectionPosition }); - } catch (e) { - console.log(e); // eslint-disable-line no-console - } - } else if (selection.start === selection.end) { - const newBlock = - ( - (selection.start === 0 && value.substr(0, 1).match(/^\n?$/)) || - value.substr(selection.start - 2, 2) === '\n\n') && - ( - selection.end === (value.length - 1) || - value.substr(selection.end, 2) === '\n\n' || - value.substr(selection.end).match(/\n*$/m) - ); - - if (newBlock) { - const position = this.caretPosition.get(selection.start, selection.end); - this.setState({ selectionPosition: position }); - } - } - }; - handleChange = (e) => { - // handleChange may receive an event or a value - const value = typeof e === 'object' ? e.target.value : e; - const html = markdownToHtml(value); + const html = markdownToHtml(e.target.value); this.props.onChange(html); - this.updateHeight(); - this.setState({ value }); + this.setState({ value: e.target.value }); }; - handlePluginSubmit = (plugin, data) => { - const toBlock = plugin.get('toBlock'); - this.replaceSelection(toBlock.call(toBlock, data.toJS())); - }; - - handleHeader(header) { - return () => { - this.toggleHeader(header); - }; - } - - handleDragEnter = (e) => { - e.preventDefault(); - this.setState({ dragging: true }); - }; - - handleDragLeave = (e) => { - e.preventDefault(); - this.setState({ dragging: false }); - }; - - handleDragOver = (e) => { - e.preventDefault(); - }; - - handleDrop = (e) => { - e.preventDefault(); - - this.setState({ dragging: false }); - - let data; - - if (e.dataTransfer.files && e.dataTransfer.files.length) { - data = Array.from(e.dataTransfer.files).map((file) => { - const link = `[Uploading ${ file.name }...]()`; - if (file.type.split('/')[0] === 'image') { - return `!${ link }`; - } - - createAssetProxy(file.name, file) - .then((assetProxy) => { - this.props.onAddAsset(assetProxy); - // TODO: Change the link text - }); - return link; - }).join('\n\n'); - } else { - data = e.dataTransfer.getData('text/plain'); - } - this.replaceSelection(data); - }; - - handlePaste = (e) => { - const { value } = this.state; - const selection = this.getSelection(); - const beforeSelection = value.substr(0, selection.start); - const afterSelection = value.substr(selection.end); - - getCleanPaste(e).then((paste) => { - const newSelection = Object.assign({}, selection); - newSelection.start = newSelection.end = beforeSelection.length + paste.length; - this.newSelection = newSelection; - this.handleChange(beforeSelection + paste + afterSelection); - }); - }; - - handleToggle = () => { + handleToggleMode = () => { this.props.onMode('visual'); }; render() { - const { onAddAsset, onRemoveAsset, getAsset } = this.props; - const { plugins, selectionPosition, dragging } = this.state; - const classNames = [styles.root]; - if (dragging) { - classNames.push(styles.dragging); - } - - return (
    - - + + + + - - -
    -
    ); +
    + ); } } RawEditor.propTypes = { - onAddAsset: PropTypes.func.isRequired, - onRemoveAsset: PropTypes.func.isRequired, - getAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, value: PropTypes.node, diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js index 31087c8c..93d673c4 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js @@ -10,14 +10,15 @@ import styles from './Toolbar.css'; export default class Toolbar extends React.Component { static propTypes = { - buttons: PropTypes.object.isRequired, + buttons: PropTypes.object, onToggleMode: PropTypes.func.isRequired, rawMode: PropTypes.bool, plugins: ImmutablePropTypes.map, - onSubmit: PropTypes.func.isRequired, - onAddAsset: PropTypes.func.isRequired, - onRemoveAsset: PropTypes.func.isRequired, - getAsset: PropTypes.func.isRequired, + onSubmit: PropTypes.func, + onAddAsset: PropTypes.func, + onRemoveAsset: PropTypes.func, + getAsset: PropTypes.func, + disabled: PropTypes.bool, }; constructor(props) { @@ -42,15 +43,17 @@ export default class Toolbar extends React.Component { render() { const { - buttons, onToggleMode, rawMode, plugins, onAddAsset, onRemoveAsset, getAsset, + disabled, } = this.props; + const buttons = this.props.buttons || {}; + const { activePlugin } = this.state; const buttonsConfig = [ @@ -64,11 +67,18 @@ export default class Toolbar extends React.Component { return (
    { buttonsConfig.map((btn, i) => ( - + {})} + active={btn.state && btn.state.active} + disabled={disabled} + {...btn} + /> ))} {activePlugin && ( +const ToolbarButton = ({ label, icon, action, active, disabled }) => ( diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js index ba1e22ea..e6bf3a2d 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/ToolbarComponentsMenu.js @@ -6,7 +6,7 @@ import styles from './ToolbarComponentsMenu.css'; export default class ToolbarComponentsMenu extends React.Component { static PropTypes = { - plugins: ImmutablePropTypes.map.isRequired, + plugins: ImmutablePropTypes.map, onComponentMenuItemClick: PropTypes.func.isRequired, }; @@ -26,17 +26,22 @@ export default class ToolbarComponentsMenu extends React.Component { }; render() { - const { plugins, onComponentMenuItemClick } = this.props; + const { plugins, onComponentMenuItemClick, disabled } = this.props; return (
    - + - {plugins.map(plugin => ( + {plugins && plugins.map(plugin => ( Date: Sat, 8 Jul 2017 23:23:14 -0400 Subject: [PATCH 38/79] convert raw editor to Slate --- src/components/ControlPanel/ControlPane.css | 10 +++++++-- src/components/UI/theme.css | 2 ++ .../MarkdownControl/RawEditor/index.css | 6 +++-- .../MarkdownControl/RawEditor/index.js | 22 ++++++++++++------- .../MarkdownControl/VisualEditor/index.css | 5 +---- src/index.css | 4 ++-- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/components/ControlPanel/ControlPane.css b/src/components/ControlPanel/ControlPane.css index ea8d67d3..b3c95378 100644 --- a/src/components/ControlPanel/ControlPane.css +++ b/src/components/ControlPanel/ControlPane.css @@ -8,8 +8,8 @@ & input, & textarea, - & select { - font-family: 'SFMono-Regular', Consolas, "Liberation Mono", Menlo, Courier, monospace; + & select, + & div[contenteditable=true] { display: block; width: 100%; padding: 12px; @@ -28,6 +28,12 @@ border-color: var(--primaryColor); } } + + & input, + & textarea, + & select { + font-family: var(--fontFamilyMono); + } } .label { diff --git a/src/components/UI/theme.css b/src/components/UI/theme.css index d1a3cb8c..d28ee61e 100644 --- a/src/components/UI/theme.css +++ b/src/components/UI/theme.css @@ -1,4 +1,6 @@ :root { + --fontFamily: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --fontFamilyMono: 'SFMono-Regular', Consolas, "Liberation Mono", Menlo, Courier, monospace; --defaultColor: #333; --defaultColorLight: #fff; --backgroundColor: #fff; diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css index d200d47b..aa2fec15 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css @@ -12,8 +12,10 @@ composes: editorControlBarSticky from "../VisualEditor/index.css"; } -.textarea { +.SlateEditor { + position: relative; overflow: hidden; - resize: none; + overflow-x: auto; min-height: var(--richTextEditorMinHeight); + font-family: var(--fontFamilyMono); } diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index a812c004..e441f850 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import TextareaAutosize from 'react-textarea-autosize'; +import { Editor as SlateEditor, Plain as SlatePlain } from 'slate'; import { markdownToHtml, htmlToMarkdown } from '../../unified'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; @@ -8,15 +8,20 @@ import styles from './index.css'; export default class RawEditor extends React.Component { constructor(props) { super(props); + const value = htmlToMarkdown(this.props.value); this.state = { - value: htmlToMarkdown(this.props.value) || '', + editorState: SlatePlain.deserialize(value || ''), }; } - handleChange = (e) => { - const html = markdownToHtml(e.target.value); + handleChange = editorState => { + this.setState({ editorState }); + } + + handleDocumentChange = (doc, editorState) => { + const value = SlatePlain.serialize(editorState); + const html = markdownToHtml(value); this.props.onChange(html); - this.setState({ value: e.target.value }); }; handleToggleMode = () => { @@ -33,10 +38,11 @@ export default class RawEditor extends React.Component { > -
    ); diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index aff7651a..9a1ec80b 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -70,13 +70,10 @@ .slateEditor { position: relative; - background-color: var(--controlBGColor); - padding: 12px; overflow: hidden; - border-radius: var(--borderRadius); overflow-x: auto; - border: var(--textFieldBorder); min-height: var(--richTextEditorMinHeight); + font-family: var(--fontFamily); & ul, & ol { diff --git a/src/index.css b/src/index.css index 9880debb..53705baf 100644 --- a/src/index.css +++ b/src/index.css @@ -13,7 +13,7 @@ html { } body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-family: var(--fontFamily); height: 100%; background-color: #fff; color: #7c8382; @@ -22,7 +22,7 @@ body { h1, h2, h3, h4, h5, h6, p { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-family: var(--fontFamily); } h1 { From 24caeadfa4a2699a391217effef13a43f8b02d0e Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Sun, 9 Jul 2017 00:17:10 -0400 Subject: [PATCH 39/79] add list and code toolbar buttons --- .../Markdown/MarkdownControl/Toolbar/Toolbar.js | 8 ++++++-- .../Markdown/MarkdownControl/VisualEditor/index.js | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js index 93d673c4..26ac0145 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js @@ -57,10 +57,14 @@ export default class Toolbar extends React.Component { const { activePlugin } = this.state; const buttonsConfig = [ - { label: 'Header 1', icon: 'h1', state: buttons.h1 }, - { label: 'Header 2', icon: 'h2', state: buttons.h2 }, { label: 'Bold', icon: 'bold', state: buttons.bold }, { label: 'Italic', icon: 'italic', state: buttons.italic }, + { label: 'Code', icon: 'code-alt', state: buttons.code }, + { label: 'Header 1', icon: 'h1', state: buttons.h1 }, + { label: 'Header 2', icon: 'h2', state: buttons.h2 }, + { label: 'Bullet List', icon: 'list-bullet', state: buttons.list }, + { label: 'Numbered List', icon: 'list-numbered', state: buttons.listNumbered }, + { label: 'Code Block', icon: 'code', state: buttons.codeBlock }, { label: 'Link', icon: 'link', state: buttons.link }, ]; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index b801510f..8cbd90ab 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -242,7 +242,7 @@ const RULES = [ }, { serialize(entity, children) { - if (!['bulleted-list', 'unordered-list'].includes(entity.type)) { + if (!['bulleted-list', 'numbered-list'].includes(entity.type)) { return; } return NODE_COMPONENTS[entity.type]({ children }); @@ -455,11 +455,15 @@ export default class Editor extends Component { Date: Sun, 9 Jul 2017 00:31:07 -0400 Subject: [PATCH 40/79] remove prosemirror dependencies --- package.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/package.json b/package.json index a2efaf66..1f86e5ee 100644 --- a/package.json +++ b/package.json @@ -119,18 +119,6 @@ "preliminaries-parser-toml": "1.1.0", "preliminaries-parser-yaml": "1.1.0", "prismjs": "^1.5.1", - "prosemirror-commands": "^0.17.0", - "prosemirror-history": "^0.17.0", - "prosemirror-inputrules": "^0.17.0", - "prosemirror-keymap": "^0.17.0", - "prosemirror-markdown": "^0.17.0", - "prosemirror-model": "^0.17.0", - "prosemirror-schema-basic": "^0.17.0", - "prosemirror-schema-list": "^0.17.0", - "prosemirror-schema-table": "^0.17.0", - "prosemirror-state": "^0.17.0", - "prosemirror-transform": "^0.17.0", - "prosemirror-view": "^0.17.0", "react": "^15.1.0", "react-addons-css-transition-group": "^15.3.1", "react-autosuggest": "^7.0.1", From fe3d04b7220cfe597cb697088e39cb33004fece3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 10 Jul 2017 18:26:07 -0400 Subject: [PATCH 41/79] streamline raw editor pasting --- .../Widgets/Markdown/MarkdownControl/RawEditor/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index e441f850..b421691e 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -24,6 +24,13 @@ export default class RawEditor extends React.Component { this.props.onChange(html); }; + handlePaste = (e, data, state) => { + if (data.text) { + const fragment = SlatePlain.deserialize(data.text).document; + return state.transform().insertFragment(fragment).apply(); + } + }; + handleToggleMode = () => { this.props.onMode('visual'); }; @@ -43,6 +50,7 @@ export default class RawEditor extends React.Component { state={this.state.editorState} onChange={this.handleChange} onDocumentChange={this.handleDocumentChange} + onPaste={this.handlePaste} />
    ); From f22d09b7818f093c8f0c7e93962dc227b848d0ea Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 11 Jul 2017 18:56:32 -0400 Subject: [PATCH 42/79] add smart soft breaks for visual editor --- .../MarkdownControl/VisualEditor/index.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 8cbd90ab..b69137a3 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -253,6 +253,35 @@ const RULES = [ const serializer = new SlateHtml({ rules: RULES }); +const SoftBreak = (options = {}) => ({ + onKeyDown(e, data, state) { + if (data.key != 'enter') return; + if (options.shift && e.shiftKey == false) return; + + const { onlyIn, ignoreIn, closeAfter, unwrapBlocks, defaultBlock = 'paragraph' } = options; + const { type, nodes } = state.startBlock; + if (onlyIn && !onlyIn.includes(type)) return; + if (ignoreIn && ignoreIn.includes(type)) return; + + const shouldClose = nodes.last().characters.takeLast(closeAfter).every(c => c.text === '\n'); + if (closeAfter && shouldClose) { + const trimmed = state.transform().deleteBackward(closeAfter); + const unwrapped = unwrapBlocks + ? unwrapBlocks.reduce((acc, blockType) => acc.unwrapBlock(blockType), trimmed) + : trimmed; + return unwrapped.insertBlock(defaultBlock).apply(); + } + + return state.transform().insertText('\n').apply(); + } +}); + +const slatePlugins = [ + SoftBreak({ ignoreIn: ['paragraph', 'list-item'], closeAfter: 2 }), + SoftBreak({ onlyIn: ['list-item'], shift: true}), + SoftBreak({ onlyIn: ['paragraph'], closeAfter: 1 }), +]; + export default class Editor extends Component { constructor(props) { super(props); @@ -477,6 +506,7 @@ export default class Editor extends Component { className={styles.slateEditor} state={this.state.editorState} schema={this.state.schema} + plugins={slatePlugins} onChange={editorState => this.setState({ editorState })} onDocumentChange={this.handleDocumentChange} onKeyDown={this.onKeyDown} From 09751efe4135ecc5a522697833dadcbed8732dd2 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 12 Jul 2017 17:18:25 -0400 Subject: [PATCH 43/79] allow raw html in markdown --- src/components/Widgets/Markdown/unified.js | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index f91eb5e1..b7e2b467 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -7,29 +7,29 @@ import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import remarkToMarkdown from 'remark-stringify'; import rehypeSanitize from 'rehype-sanitize'; +import rehypeReparse from 'rehype-raw'; import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; -const remarkParseConfig = { fences: true }; -const remarkStringifyConfig = { listItemIndent: '1', fences: true }; -const rehypeParseConfig = { fragment: true }; /** * Remove empty nodes, including the top level parents of deeply nested empty nodes. */ const rehypeRemoveEmpty = () => { const isVoidElement = node => ['img', 'hr'].includes(node.tagName); - const isNonEmptyText = node => node.type === 'text' && node.value; + const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value; const isNonEmptyNode = node => { - return isVoidElement(node) || isNonEmptyText(node) || find(node.children, isNonEmptyNode); + return isVoidElement(node) + || isNonEmptyLeaf(node) + || find(node.children, isNonEmptyNode); }; const transform = node => { - if (isVoidElement(node) || isNonEmptyText(node)) { + if (isVoidElement(node) || isNonEmptyLeaf(node)) { return node; } if (node.children) { node.children = node.children.reduce((acc, childNode) => { - if (isVoidElement(childNode) || isNonEmptyText(childNode)) { + if (isVoidElement(childNode) || isNonEmptyLeaf(childNode)) { return acc.concat(childNode); } return find(childNode.children, isNonEmptyNode) ? acc.concat(transform(childNode)) : acc; @@ -91,12 +91,13 @@ const rehypePaperEmoji = () => { export const markdownToHtml = markdown => { const result = unified() - .use(markdownToRemark, remarkParseConfig) - .use(remarkToRehype) + .use(markdownToRemark, { fences: true }) + .use(remarkToRehype, { allowDangerousHTML: true }) + .use(rehypeReparse) .use(rehypeRemoveEmpty) .use(rehypeSanitize) .use(rehypeMinifyWhitespace) - .use(rehypeToHtml) + .use(rehypeToHtml, { allowDangerousHTML: true }) .processSync(markdown) .contents; return result; @@ -104,14 +105,14 @@ export const markdownToHtml = markdown => { export const htmlToMarkdown = html => { const result = unified() - .use(htmlToRehype, rehypeParseConfig) + .use(htmlToRehype, { fragment: true }) .use(rehypePaperEmoji) .use(rehypeSanitize) .use(rehypeRemoveEmpty) .use(rehypeMinifyWhitespace) .use(rehypeToRemark) .use(remarkNestedList) - .use(remarkToMarkdown, remarkStringifyConfig) + .use(remarkToMarkdown, { listItemIndent: '1', fences: true }) .processSync(html) .contents; return result; From 0e50210dcf88df8ae8acaadd36a17ac9b0f2c508 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 12 Jul 2017 18:31:05 -0400 Subject: [PATCH 44/79] close blocks on backspace --- .../MarkdownControl/VisualEditor/index.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index b69137a3..f29654f3 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -276,10 +276,38 @@ const SoftBreak = (options = {}) => ({ } }); +const BackspaceCloseBlock = (options = {}) => ({ + onKeyDown(e, data, state) { + if (data.key != 'backspace' || state.startBlock.type === 'paragraph') return; + + const { defaultBlock = 'paragraph', wrapped = {} } = options; + const { startBlock } = state; + const { type } = startBlock; + + const characters = startBlock.getFirstText().characters; + const isEmpty = !characters || characters.isEmpty(); + + if (isEmpty) { + const transform = state.transform(); + + if (wrapped[type] && state.document.getPreviousSibling(startBlock.key)) { + return; + } + + if (wrapped[type]) { + wrapped[type].forEach(wrapper => transform.unwrapBlock(wrapper)); + } + + return transform.insertBlock(defaultBlock).focus().apply(); + } + } +}); + const slatePlugins = [ SoftBreak({ ignoreIn: ['paragraph', 'list-item'], closeAfter: 2 }), SoftBreak({ onlyIn: ['list-item'], shift: true}), SoftBreak({ onlyIn: ['paragraph'], closeAfter: 1 }), + BackspaceCloseBlock({ wrapped: { 'list-item': ['bulleted-list', 'numbered-list'] } }), ]; export default class Editor extends Component { From 31c997897f925b8e216d6ba88037dd307fe71f62 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 12 Jul 2017 18:51:28 -0400 Subject: [PATCH 45/79] fix inline code serializing to blocks --- .../Widgets/Markdown/MarkdownControl/VisualEditor/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index f29654f3..2e615fb5 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -146,6 +146,9 @@ const RULES = [ if (['bulleted-list', 'numbered-list'].includes(entity.type)) { return; } + if (entity.kind !== 'block') { + return; + } const component = BLOCK_COMPONENTS[entity.type] if (!component) { return; @@ -164,6 +167,9 @@ const RULES = [ } }, serialize(entity, children) { + if (entity.kind !== 'mark') { + return; + } const component = MARK_COMPONENTS[entity.type] if (!component) { return; From 63e93d79caf55f26d0274d58d3aea43d0c54ee4f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 12 Jul 2017 19:22:17 -0400 Subject: [PATCH 46/79] improve rte list handling --- package.json | 1 + .../MarkdownControl/VisualEditor/index.js | 27 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 1f86e5ee..4a51414d 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "semaphore": "^1.0.5", "slate": "^0.20.3", "slate-drop-or-paste-images": "^0.2.0", + "slate-edit-list": "^0.7.1", "slug": "^0.9.1", "textarea-caret-position": "^0.1.1", "unified": "^6.1.4", diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 2e615fb5..af58117a 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,6 +1,7 @@ import React, { Component, PropTypes } from 'react'; import { Map, List } from 'immutable'; import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw} from 'slate'; +import EditList from 'slate-edit-list'; import { markdownToHtml, htmlToMarkdown } from '../../unified'; import registry from '../../../../../lib/registry'; import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; @@ -284,36 +285,28 @@ const SoftBreak = (options = {}) => ({ const BackspaceCloseBlock = (options = {}) => ({ onKeyDown(e, data, state) { - if (data.key != 'backspace' || state.startBlock.type === 'paragraph') return; + if (data.key != 'backspace') return; - const { defaultBlock = 'paragraph', wrapped = {} } = options; + const { defaultBlock = 'paragraph', ignoreIn, onlyIn } = options; const { startBlock } = state; const { type } = startBlock; + if (onlyIn && !onlyIn.includes(type)) return; + if (ignoreIn && ignoreIn.includes(type)) return; + const characters = startBlock.getFirstText().characters; const isEmpty = !characters || characters.isEmpty(); if (isEmpty) { - const transform = state.transform(); - - if (wrapped[type] && state.document.getPreviousSibling(startBlock.key)) { - return; - } - - if (wrapped[type]) { - wrapped[type].forEach(wrapper => transform.unwrapBlock(wrapper)); - } - - return transform.insertBlock(defaultBlock).focus().apply(); + return state.transform().insertBlock(defaultBlock).focus().apply(); } } }); const slatePlugins = [ - SoftBreak({ ignoreIn: ['paragraph', 'list-item'], closeAfter: 2 }), - SoftBreak({ onlyIn: ['list-item'], shift: true}), - SoftBreak({ onlyIn: ['paragraph'], closeAfter: 1 }), - BackspaceCloseBlock({ wrapped: { 'list-item': ['bulleted-list', 'numbered-list'] } }), + SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list'], closeAfter: 1 }), + BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list'] }), + EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }), ]; export default class Editor extends Component { From 469a50afa4b6cd3f10f123bbe8d112be6ee13e0f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 12 Jul 2017 23:15:42 -0400 Subject: [PATCH 47/79] add idempotent markdown/html shortcode handling --- package.json | 4 + src/components/Widgets/Markdown/unified.js | 118 +++++++++++++++++++-- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4a51414d..5ba48323 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,11 @@ "classnames": "^2.2.5", "dateformat": "^1.0.12", "deep-equal": "^1.0.1", + "deepmerge": "^1.5.0", "fuzzy": "^0.1.1", + "hast-util-from-string": "^1.0.0", + "hast-util-sanitize": "^1.1.1", + "hast-util-to-mdast": "^1.2.0", "history": "^2.1.2", "immutability-helper": "^2.0.0", "immutable": "^3.7.6", diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index b7e2b467..046ee2c5 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -9,7 +9,15 @@ import remarkToMarkdown from 'remark-stringify'; import rehypeSanitize from 'rehype-sanitize'; import rehypeReparse from 'rehype-raw'; import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; +import ReactDOMServer from 'react-dom/server'; +import registry from '../../../lib/registry'; +import merge from 'deepmerge'; +import rehypeSanitizeSchemaDefault from 'hast-util-sanitize/lib/github'; +import hastFromString from 'hast-util-from-string'; +import hastToMdastHandlerAll from 'hast-util-to-mdast/all'; +import { reduce, capitalize } from 'lodash'; +const shortcodeAttributePrefix = 'ncp'; /** * Remove empty nodes, including the top level parents of deeply nested empty nodes. @@ -17,19 +25,21 @@ import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; const rehypeRemoveEmpty = () => { const isVoidElement = node => ['img', 'hr'].includes(node.tagName); const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value; + const isShortcode = node => node.properties && node.properties[`data${capitalize(shortcodeAttributePrefix)}`]; const isNonEmptyNode = node => { return isVoidElement(node) || isNonEmptyLeaf(node) + || isShortcode(node) || find(node.children, isNonEmptyNode); }; const transform = node => { - if (isVoidElement(node) || isNonEmptyLeaf(node)) { + if (isVoidElement(node) || isNonEmptyLeaf(node) || isShortcode(node)) { return node; } if (node.children) { node.children = node.children.reduce((acc, childNode) => { - if (isVoidElement(childNode) || isNonEmptyLeaf(childNode)) { + if (isVoidElement(childNode) || isNonEmptyLeaf(childNode) || isShortcode(node)) { return acc.concat(childNode); } return find(childNode.children, isNonEmptyNode) ? acc.concat(transform(childNode)) : acc; @@ -89,16 +99,91 @@ const rehypePaperEmoji = () => { return transform; }; +const rehypeShortcodes = () => { + const plugins = registry.getEditorComponents(); + const transform = node => { + const { properties } = node; + const dataPrefix = `data${capitalize(shortcodeAttributePrefix)}`; + const pluginId = properties && properties[dataPrefix]; + const plugin = plugins.get(pluginId); + + if (plugin) { + const data = reduce(properties, (acc, value, key) => { + if (key.startsWith(dataPrefix)) { + const dataKey = key.slice(dataPrefix.length).toLowerCase(); + if (dataKey) { + acc[dataKey] = value; + } + } + return acc; + }, {}); + + node.data = node.data || {}; + node.data[shortcodeAttributePrefix] = true; + + return hastFromString(node, plugin.toBlock(data)); + } + + node.children = node.children ? node.children.map(transform) : node.children; + + return node; + }; + return transform; +} + +function remarkPrecompileShortcodes() { + const Compiler = this.Compiler; + const visitors = Compiler.prototype.visitors; + const textVisitor = visitors.text; + + visitors.text = newTextVisitor; + + function newTextVisitor(node, parent) { + if (parent.data && parent.data[shortcodeAttributePrefix]) { + return node.value; + } + return textVisitor.call(this, node, parent); + } +} + +const parseShortcodesFromMarkdown = markdown => { + const plugins = registry.getEditorComponents(); + const markdownLines = markdown.split('\n'); + const markdownLinesParsed = plugins.reduce((lines, plugin) => { + const result = lines.map(line => { + return line.replace(plugin.pattern, (...match) => { + const data = plugin.fromBlock(match); + const preview = plugin.toPreview(data); + const html = typeof preview === 'string' ? preview : ReactDOMServer.renderToStaticMarkup(preview); + const dataAttrs = reduce(data, (attrs, val, key) => { + attrs.push(`data-${shortcodeAttributePrefix}-${key}="${val}"`); + return attrs; + }, [`data-${shortcodeAttributePrefix}="${plugin.id}"`]); + const result = `
    ${html}
    `; + return result; + }); + }); + return result; + }, markdownLines); + return markdownLinesParsed.join('\n'); +}; + +const rehypeSanitizeSchema = merge(rehypeSanitizeSchemaDefault, { attributes: { '*': [ 'data*' ] } }); + export const markdownToHtml = markdown => { + // Parse shortcodes from the raw markdown rather than via Unified plugin. + // This ensures against conflicts between shortcode syntax and Unified + // parsing rules. + const markdownWithParsedShortcodes = parseShortcodesFromMarkdown(markdown); const result = unified() .use(markdownToRemark, { fences: true }) .use(remarkToRehype, { allowDangerousHTML: true }) .use(rehypeReparse) .use(rehypeRemoveEmpty) - .use(rehypeSanitize) + .use(rehypeSanitize, rehypeSanitizeSchema) .use(rehypeMinifyWhitespace) .use(rehypeToHtml, { allowDangerousHTML: true }) - .processSync(markdown) + .processSync(markdownWithParsedShortcodes) .contents; return result; } @@ -106,13 +191,32 @@ export const markdownToHtml = markdown => { export const htmlToMarkdown = html => { const result = unified() .use(htmlToRehype, { fragment: true }) - .use(rehypePaperEmoji) - .use(rehypeSanitize) + .use(rehypeSanitize, rehypeSanitizeSchema) .use(rehypeRemoveEmpty) .use(rehypeMinifyWhitespace) - .use(rehypeToRemark) + .use(rehypePaperEmoji) + .use(rehypeShortcodes) + .use(rehypeToRemark, { handlers: { div: (h, node) => { + const dataPrefix = `data${capitalize(shortcodeAttributePrefix)}`; + const isShortcode = node.properties[dataPrefix]; + if (isShortcode) { + const paragraph = h(node, 'paragraph', hastToMdastHandlerAll(h, node)); + paragraph.data = paragraph.data || {}; + paragraph.data[shortcodeAttributePrefix] = true; + return paragraph; + } + }}}) + .use(() => node => { + return node; + }) .use(remarkNestedList) .use(remarkToMarkdown, { listItemIndent: '1', fences: true }) + .use(remarkPrecompileShortcodes) + /* + .use(() => node => { + return node; + }) + */ .processSync(html) .contents; return result; From 93687d91570c858169378a162eb28d11fdfa9630 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 13 Jul 2017 21:33:50 -0400 Subject: [PATCH 48/79] add shortcodes through rte toolbar --- package.json | 2 +- .../MarkdownControl/VisualEditor/index.css | 12 + .../MarkdownControl/VisualEditor/index.js | 268 +++++++++++------- src/components/Widgets/Markdown/unified.js | 12 + 4 files changed, 184 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index 5ba48323..3ba523ce 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "remark-stringify": "^3.0.1", "selection-position": "^1.0.0", "semaphore": "^1.0.5", - "slate": "^0.20.3", + "slate": "^0.20.6", "slate-drop-or-paste-images": "^0.2.0", "slate-edit-list": "^0.7.1", "slug": "^0.9.1", diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index 9a1ec80b..81ab24db 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -100,3 +100,15 @@ margin-left: 0; margin-right: 0; } } + +.shortcode { + border: 2px solid black; + padding: 8px; + margin: 2px 0; + cursor: pointer; +} + +.shortcodeSelected { + border-color: var(--primaryColor); + color: var(--primaryColor); +} diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index af58117a..f0871fd3 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,6 +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 ReactDOMServer from 'react-dom/server'; +import { Map, List, fromJS } from 'immutable'; +import { reduce, mapValues } from 'lodash'; +import cn from 'classnames'; +import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate'; import EditList from 'slate-edit-list'; import { markdownToHtml, htmlToMarkdown } from '../../unified'; import registry from '../../../../../lib/registry'; @@ -96,7 +99,8 @@ const MARK_TAGS = { } const BLOCK_COMPONENTS = { - 'paragraph': props =>

    {props.children}

    , + 'container': props =>
    {props.children}
    , + 'paragraph': props =>

    {props.children}

    , 'list-item': props =>
  • {props.children}
  • , 'bulleted-list': props =>
      {props.children}
    , 'numbered-list': props =>
      {props.children}
    , @@ -110,11 +114,20 @@ const BLOCK_COMPONENTS = { 'heading-six': props =>
    {props.children}
    , 'image': props => { const data = props.node && props.node.get('data'); - const src = data && data.get('src', props.src); - const alt = data && data.get('alt', props.alt); + const src = data && data.get('src') || props.src; + const alt = data && data.get('alt') || props.alt; return {alt}; }, }; +const getShortcodeId = props => { + if (props.node) { + const result = props.node.getIn(['data', 'shortcode', 'shortcodeId']); + return result || props.node.getIn(['data', 'shortcode']).shortcodeId; + } + return null; +} + +const shortcodeStyles = {border: '2px solid black', padding: '8px', margin: '2px 0', cursor: 'pointer'}; const NODE_COMPONENTS = { ...BLOCK_COMPONENTS, @@ -122,6 +135,19 @@ const NODE_COMPONENTS = { const href = props.node && props.node.getIn(['data', 'href']) || props.href; return {props.children}; }, + 'shortcode': props => { + const { attributes, node, state: editorState } = props; + const isSelected = editorState.selection.hasFocusIn(node); + return ( +
    + {getShortcodeId(props)} +
    + ); + }, }; const MARK_COMPONENTS = { @@ -133,6 +159,50 @@ const MARK_COMPONENTS = { }; const RULES = [ + { + deserialize(el, next) { + const shortcodeId = el.attribs && el.attribs['data-ncp']; + if (!shortcodeId) { + return; + } + const plugin = registry.getEditorComponents().get(shortcodeId); + if (!plugin) { + return; + } + const shortcodeData = Map(el.attribs).reduce((acc, value, key) => { + if (key.startsWith('data-ncp-')) { + const dataKey = key.slice('data-ncp-'.length).toLowerCase(); + if (dataKey) { + return acc.set(dataKey, value); + } + } + return acc; + }, Map({ shortcodeId })); + + const result = { + kind: 'block', + isVoid: true, + type: 'shortcode', + data: { shortcode: shortcodeData }, + }; + return result; + }, + serialize(entity, children) { + if (entity.type !== 'shortcode') { + return; + } + + const data = Map(entity.data.get('shortcode')); + const shortcodeId = data.get('shortcodeId'); + const plugin = registry.getEditorComponents().get(shortcodeId); + const dataAttrs = data.delete('shortcodeId').mapKeys(key => `data-ncp-${key}`).set('data-ncp', shortcodeId); + const preview = plugin.toPreview(data.toJS()); + const component = typeof preview === 'string' + ?
    + :
    {preview}
    ; + return component; + }, + }, { deserialize(el, next) { const block = BLOCK_TAGS[el.tagName] @@ -216,9 +286,9 @@ const RULES = [ const props = { src: data.get('src'), alt: data.get('alt'), - attributes: data.get('attributes'), }; - return NODE_COMPONENTS.image(props); + const result = NODE_COMPONENTS.image(props); + return result; } }, { @@ -313,11 +383,44 @@ export default class Editor extends Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); + // Wrap value in div to ensure against trailing text outside of top level html element + const initialValue = this.props.value ? `
    ${this.props.value}
    ` : '

    '; this.state = { - editorState: serializer.deserialize(this.props.value || '

    '), + editorState: serializer.deserialize(initialValue), schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, + rules: [ + { + match: object => object.kind === 'document', + validate: doc => { + const blocks = doc.getBlocks(); + const firstBlock = blocks.first(); + const lastBlock = blocks.last(); + const firstBlockIsVoid = firstBlock.isVoid; + const lastBlockIsVoid = lastBlock.isVoid; + + if (firstBlockIsVoid || lastBlockIsVoid) { + return { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid }; + } + }, + normalize: (transform, doc, { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid }) => { + const block = SlateBlock.create({ + type: 'paragraph', + nodes: [SlateText.createFromString('')], + }); + if (firstBlockIsVoid) { + const { key } = transform.state.document; + transform.insertNodeByKey(key, 0, block); + } + if (lastBlockIsVoid) { + const { key, nodes } = transform.state.document; + transform.insertNodeByKey(key, nodes.size, block); + } + return transform; + }, + } + ], }, plugins, }; @@ -425,57 +528,13 @@ export default class Editor extends Component { }; handlePluginSubmit = (plugin, data) => { - const { schema } = this.state; - const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; - //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); - }; - - handleDragEnter = (e) => { - e.preventDefault(); - this.setState({ dragging: true }); - }; - - handleDragLeave = (e) => { - e.preventDefault(); - this.setState({ dragging: false }); - }; - - handleDragOver = (e) => { - e.preventDefault(); - }; - - handleDrop = (e) => { - e.preventDefault(); - - this.setState({ dragging: false }); - - const { schema } = this.state; - - const nodes = []; - - if (e.dataTransfer.files && e.dataTransfer.files.length) { - Array.from(e.dataTransfer.files).forEach((file) => { - createAssetProxy(file.name, file) - .then((assetProxy) => { - this.props.onAddAsset(assetProxy); - if (file.type.split('/')[0] === 'image') { - nodes.push( - schema.nodes.image.create({ src: assetProxy.public_path, alt: file.name }) - ); - } else { - nodes.push( - schema.marks.link.create({ href: assetProxy.public_path, title: file.name }) - ); - } - }); - }); - } else { - nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain'))); - } - - nodes.forEach((node) => { - //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action()); - }); + const { editorState } = this.state; + const markdown = plugin.toBlock(data.toJS()); + const html = markdownToHtml(markdown); + const block = serializer.deserialize(html).document.getBlocks().first(); + const resolvedState = editorState.transform().insertBlock(block).apply(); + this.ref.onChange(resolvedState); + this.setState({ editorState: resolvedState }); }; handleToggle = () => { @@ -491,58 +550,49 @@ export default class Editor extends Component { render() { const { onAddAsset, onRemoveAsset, getAsset } = this.props; const { plugins, selectionPosition, dragging } = this.state; - const classNames = [styles.editor]; - if (dragging) { - classNames.push(styles.dragging); - } - return (
    - - + + + + this.setState({ editorState })} + onDocumentChange={this.handleDocumentChange} + onKeyDown={this.onKeyDown} + onPaste={this.handlePaste} + ref={ref => this.ref = ref} + spellCheck /> - - this.setState({ editorState })} - onDocumentChange={this.handleDocumentChange} - onKeyDown={this.onKeyDown} - onPaste={this.handlePaste} - ref={ref => this.ref = ref} - spellCheck - /> -
    -
    ); +
    + ); } } diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 046ee2c5..ba011474 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -103,6 +103,9 @@ const rehypeShortcodes = () => { const plugins = registry.getEditorComponents(); const transform = node => { const { properties } = node; + + // Convert this logic into a parseShortcodeDataFromHtml shared function, as + // this is also used in the visual editor serializer const dataPrefix = `data${capitalize(shortcodeAttributePrefix)}`; const pluginId = properties && properties[dataPrefix]; const plugin = plugins.get(pluginId); @@ -131,6 +134,15 @@ const rehypeShortcodes = () => { return transform; } +/** + * we can't escape the less than symbol + * which means how do we know {{}} from ? + * maybe we escape nothing + * then we can check for shortcodes in a unified plugin + * and only check against text nodes + * and maybe narrow the target text nodes even further somehow + * and make shortcode parsing faster + */ function remarkPrecompileShortcodes() { const Compiler = this.Compiler; const visitors = Compiler.prototype.visitors; From 842c2935e99de7dd666cd26e4f859b83209b26f3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 18 Jul 2017 19:14:40 -0400 Subject: [PATCH 49/79] use mdast instead of html for rte local model markdown is currently serialized to html at load time, which makes it near impossible to support arbitrary html in the markdown. This also means we're stringifying to html on every change. This commit moves to Remark's MDAST for local serialization, including parsing from MDAST to Slates's Raw AST. It brings much more control over the editing experience and full support for processing unescaped HTML. --- package.json | 4 + src/actions/editorialWorkflow.js | 11 +- src/actions/entries.js | 42 +-- .../MarkdownControl/RawEditor/index.js | 8 +- .../MarkdownControl/VisualEditor/index.css | 11 + .../MarkdownControl/VisualEditor/index.js | 108 +++--- .../Widgets/Markdown/MarkdownControl/index.js | 14 +- .../Widgets/Markdown/MarkdownPreview/index.js | 9 +- src/components/Widgets/Markdown/unified.js | 344 ++++++++++++++++-- src/containers/EntryPage.js | 12 +- src/lib/serializeEntryValues.js | 49 +++ src/reducers/entries.js | 3 +- 12 files changed, 465 insertions(+), 150 deletions(-) create mode 100644 src/lib/serializeEntryValues.js diff --git a/package.json b/package.json index 3ba523ce..a2405c4c 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "lodash": "^4.13.1", "markup-it": "^2.0.0", "material-design-icons": "^3.0.1", + "mdast-util-definitions": "^1.2.2", "moment": "^2.11.2", "netlify-auth-js": "^0.5.5", "normalize.css": "^4.2.0", @@ -165,9 +166,12 @@ "slate": "^0.20.6", "slate-drop-or-paste-images": "^0.2.0", "slate-edit-list": "^0.7.1", + "slate-edit-table": "^0.10.1", "slug": "^0.9.1", "textarea-caret-position": "^0.1.1", "unified": "^6.1.4", + "unist-builder": "^1.0.2", + "unist-util-modify-children": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" }, diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index d04903a8..8127eee6 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -228,20 +228,23 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) { if (!entryDraft.get('fieldsErrors').isEmpty()) return Promise.resolve(); const backend = currentBackend(state.config); + const transactionID = uuid.v4(); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - const transactionID = uuid.v4(); + const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); + const transformedEntry = entry.set('data', transformedData); + const transformedEntryDraft = entryDraft.set('entry', transformedEntry); - dispatch(unpublishedEntryPersisting(collection, entry, transactionID)); + dispatch(unpublishedEntryPersisting(collection, transformedEntry, transactionID)); const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry; - return persistAction.call(backend, state.config, collection, entryDraft, assetProxies.toJS()) + return persistAction.call(backend, state.config, collection, transformedEntryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', kind: 'success', dismissAfter: 4000, })); - return dispatch(unpublishedEntryPersisted(collection, entry, transactionID)); + return dispatch(unpublishedEntryPersisted(collection, transformedEntry, transactionID)); }) .catch((error) => { dispatch(notifSend({ diff --git a/src/actions/entries.js b/src/actions/entries.js index 6cae3137..6bdfbf7c 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -1,12 +1,11 @@ -import { List, Map } from 'immutable'; -import { isArray, isObject, isEmpty, isNil } from 'lodash'; +import { List } from 'immutable'; import { actions as notifActions } from 'redux-notifications'; +import { serializeValues } from '../lib/serializeEntryValues'; import { closeEntry } from './editor'; import { currentBackend } from '../backends/backend'; import { getIntegrationProvider } from '../integrations'; import { getAsset, selectIntegration } from '../reducers'; import { createEntry } from '../valueObjects/Entry'; -import registry from '../lib/registry'; const { notifSend } = notifActions; @@ -219,27 +218,10 @@ export function loadEntry(collection, slug) { dispatch(entryLoading(collection, slug)); return backend.getEntry(collection, slug) .then(loadedEntry => { - const deserializeValues = (values, fields) => { - return fields.reduce((acc, field) => { - const fieldName = field.get('name'); - const value = values[fieldName]; - const serializer = registry.getWidgetValueSerializer(field.get('widget')); - if (isArray(value) && !isEmpty(value)) { - acc[fieldName] = value.map(val => deserializeValues(val, field.get('fields'))); - } else if (isObject(value) && !isEmpty(value)) { - acc[fieldName] = deserializeValues(value, field.get('fields')); - } else if (serializer && !isNil(value)) { - acc[fieldName] = serializer.deserialize(value); - } else if (!isNil(value)) { - acc[fieldName] = value; - } - return acc; - }, {}); - }; - loadedEntry.data = deserializeValues(loadedEntry.data, collection.get('fields')); return dispatch(entryLoaded(collection, loadedEntry)) }) .catch((error) => { + console.error(error); dispatch(notifSend({ message: `Failed to load entry: ${ error.message }`, kind: 'danger', @@ -289,23 +271,6 @@ export function persistEntry(collection) { const backend = currentBackend(state.config); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - const serializeValues = (values, fields) => { - return fields.reduce((acc, field) => { - const fieldName = field.get('name'); - const value = values.get(fieldName); - const serializer = registry.getWidgetValueSerializer(field.get('widget')); - if (List.isList(value)) { - return acc.set(fieldName, value.map(val => serializeValues(val, field.get('fields')))); - } else if (Map.isMap(value)) { - return acc.set(fieldName, serializeValues(value, field.get('fields'))); - } else if (serializer && !isNil(value)) { - return acc.set(fieldName, serializer.serialize(value)); - } else if (!isNil(value)) { - return acc.set(fieldName, value); - } - return acc; - }, Map()); - }; const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); const transformedEntry = entry.set('data', transformedData); const transformedEntryDraft = entryDraft.set('entry', transformedEntry); @@ -321,6 +286,7 @@ export function persistEntry(collection) { return dispatch(entryPersisted(collection, transformedEntry)); }) .catch((error) => { + console.error(error); dispatch(notifSend({ message: `Failed to persist entry: ${ error }`, kind: 'danger', diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index b421691e..e6cd1571 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { Editor as SlateEditor, Plain as SlatePlain } from 'slate'; -import { markdownToHtml, htmlToMarkdown } from '../../unified'; +import { markdownToRemark, remarkToMarkdown } from '../../unified'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; @@ -8,7 +8,7 @@ import styles from './index.css'; export default class RawEditor extends React.Component { constructor(props) { super(props); - const value = htmlToMarkdown(this.props.value); + const value = remarkToMarkdown(this.props.value); this.state = { editorState: SlatePlain.deserialize(value || ''), }; @@ -20,7 +20,7 @@ export default class RawEditor extends React.Component { handleDocumentChange = (doc, editorState) => { const value = SlatePlain.serialize(editorState); - const html = markdownToHtml(value); + const html = markdownToRemark(value); this.props.onChange(html); }; @@ -60,5 +60,5 @@ export default class RawEditor extends React.Component { RawEditor.propTypes = { onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, - value: PropTypes.node, + value: PropTypes.object, }; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index 81ab24db..b7a3aafb 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -99,6 +99,17 @@ border-left: 3px solid #eee; margin-left: 0; margin-right: 0; } + + & table { + border-collapse: collapse; + } + + & td, + & th { + border: 2px solid black; + padding: 8px; + text-align: left; + } } .shortcode { diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index f0871fd3..f6b5d1ca 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,27 +1,18 @@ import React, { Component, PropTypes } from 'react'; import ReactDOMServer from 'react-dom/server'; import { Map, List, fromJS } from 'immutable'; -import { reduce, mapValues } from 'lodash'; +import { get, reduce, mapValues } from 'lodash'; import cn from 'classnames'; import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate'; import EditList from 'slate-edit-list'; -import { markdownToHtml, htmlToMarkdown } from '../../unified'; +import EditTable from 'slate-edit-table'; +import { markdownToRemark, remarkToMarkdown, slateToRemark, remarkToSlate, markdownToHtml, htmlToMarkdown } from '../../unified'; import registry from '../../../../../lib/registry'; import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; -/** - * Slate can serialize to html, but we persist the value as markdown. Serializing - * the html to markdown on every keystroke is a big perf hit, so we'll register - * functions to perform those actions only when necessary, such as after loading - * and before persisting. - */ -registry.registerWidgetValueSerializer('markdown', { - serialize: htmlToMarkdown, - deserialize: markdownToHtml, -}); function processUrl(url) { if (url.match(/^(https?:\/\/|mailto:|\/)/)) { @@ -102,10 +93,14 @@ const BLOCK_COMPONENTS = { 'container': props =>
    {props.children}
    , 'paragraph': props =>

    {props.children}

    , 'list-item': props =>
  • {props.children}
  • , + 'numbered-list': props => { + const { data } = props.node; + const start = data.get('start') || 1; + return
      {props.children}
    ; + }, 'bulleted-list': props =>
      {props.children}
    , - 'numbered-list': props =>
      {props.children}
    , 'quote': props =>
    {props.children}
    , - 'code': props =>
    {props.children}
    , + 'code': props =>
    {props.children}
    , 'heading-one': props =>

    {props.children}

    , 'heading-two': props =>

    {props.children}

    , 'heading-three': props =>

    {props.children}

    , @@ -116,8 +111,13 @@ const BLOCK_COMPONENTS = { const data = props.node && props.node.get('data'); const src = data && data.get('src') || props.src; const alt = data && data.get('alt') || props.alt; - return {alt}; + const title = data && data.get('title') || props.title; + return
    {alt}
    ; }, + 'table': props => {props.children}
    , + 'table-row': props => {props.children}, + 'table-cell': props => {props.children}, + 'thematic-break': props =>
    , }; const getShortcodeId = props => { if (props.node) { @@ -132,8 +132,10 @@ const shortcodeStyles = {border: '2px solid black', padding: '8px', margin: '2px const NODE_COMPONENTS = { ...BLOCK_COMPONENTS, 'link': props => { - const href = props.node && props.node.getIn(['data', 'href']) || props.href; - return {props.children}; + const data = props.node.get('data'); + const href = data && data.get('url') || props.href; + const title = data && data.get('title') || props.title; + return {props.children}; }, 'shortcode': props => { const { attributes, node, state: editorState } = props; @@ -153,7 +155,6 @@ const NODE_COMPONENTS = { const MARK_COMPONENTS = { bold: props => {props.children}, italic: props => {props.children}, - underlined: props => {props.children}, strikethrough: props => {props.children}, code: props => {props.children}, }; @@ -217,9 +218,6 @@ const RULES = [ if (['bulleted-list', 'numbered-list'].includes(entity.type)) { return; } - if (entity.kind !== 'block') { - return; - } const component = BLOCK_COMPONENTS[entity.type] if (!component) { return; @@ -242,9 +240,6 @@ const RULES = [ return; } const component = MARK_COMPONENTS[entity.type] - if (!component) { - return; - } return component({ children }); } }, @@ -268,13 +263,14 @@ const RULES = [ deserialize(el, next) { if (el.tagName != 'img') return return { - kind: 'inline', + kind: 'block', type: 'image', isVoid: true, nodes: [], data: { src: el.attribs.src, alt: el.attribs.alt, + title: el.attribs.title, } } }, @@ -286,6 +282,7 @@ const RULES = [ const props = { src: data.get('src'), alt: data.get('alt'), + title: data.get('title'), }; const result = NODE_COMPONENTS.image(props); return result; @@ -300,7 +297,8 @@ const RULES = [ type: 'link', nodes: next(el.children), data: { - href: el.attribs.href + href: el.attribs.href, + title: el.attribs.title, } } }, @@ -311,6 +309,7 @@ const RULES = [ const data = entity.get('data'); const props = { href: data.get('href'), + title: data.get('title'), attributes: data.get('attributes'), children, }; @@ -328,7 +327,7 @@ const RULES = [ ] -const serializer = new SlateHtml({ rules: RULES }); +const htmlSerializer = new SlateHtml({ rules: RULES }); const SoftBreak = (options = {}) => ({ onKeyDown(e, data, state) { @@ -374,53 +373,29 @@ const BackspaceCloseBlock = (options = {}) => ({ }); const slatePlugins = [ - SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list'], closeAfter: 1 }), - BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list'] }), + SoftBreak({ ignoreIn: ['list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }), + BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list', 'table', 'table-row', 'table-cell'] }), EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }), + EditTable({ typeTable: 'table', typeRow: 'table-row', typeCell: 'table-cell' }), ]; export default class Editor extends Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); - // Wrap value in div to ensure against trailing text outside of top level html element - const initialValue = this.props.value ? `
    ${this.props.value}
    ` : '

    '; + const emptyRaw = { + nodes: [{ kind: 'block', type: 'paragraph', nodes: [ + { kind: 'text', ranges: [{ text: '' }] } + ]}], + }; + const remark = this.props.value && remarkToSlate(this.props.value); + const initialValue = get(remark, ['nodes', 'length']) ? remark : emptyRaw; + const editorState = SlateRaw.deserialize(initialValue, { terse: true }); this.state = { - editorState: serializer.deserialize(initialValue), + editorState, schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, - rules: [ - { - match: object => object.kind === 'document', - validate: doc => { - const blocks = doc.getBlocks(); - const firstBlock = blocks.first(); - const lastBlock = blocks.last(); - const firstBlockIsVoid = firstBlock.isVoid; - const lastBlockIsVoid = lastBlock.isVoid; - - if (firstBlockIsVoid || lastBlockIsVoid) { - return { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid }; - } - }, - normalize: (transform, doc, { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid }) => { - const block = SlateBlock.create({ - type: 'paragraph', - nodes: [SlateText.createFromString('')], - }); - if (firstBlockIsVoid) { - const { key } = transform.state.document; - transform.insertNodeByKey(key, 0, block); - } - if (lastBlockIsVoid) { - const { key, nodes } = transform.state.document; - transform.insertNodeByKey(key, nodes.size, block); - } - return transform; - }, - } - ], }, plugins, }; @@ -437,8 +412,9 @@ export default class Editor extends Component { } handleDocumentChange = (doc, editorState) => { - const html = serializer.serialize(editorState); - this.props.onChange(html); + const raw = SlateRaw.serialize(editorState, { terse: true }); + const mdast = slateToRemark(raw); + this.props.onChange(mdast); }; hasMark = type => this.state.editorState.marks.some(mark => mark.type === type); @@ -602,5 +578,5 @@ Editor.propTypes = { getAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, - value: PropTypes.node, + value: PropTypes.object, }; diff --git a/src/components/Widgets/Markdown/MarkdownControl/index.js b/src/components/Widgets/Markdown/MarkdownControl/index.js index 41d79763..6ed3df10 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/index.js @@ -1,18 +1,30 @@ import React, { PropTypes } from 'react'; import registry from '../../../../lib/registry'; +import { markdownToRemark, remarkToMarkdown } from '../unified'; import RawEditor from './RawEditor'; import VisualEditor from './VisualEditor'; import { StickyContainer } from '../../../UI/Sticky/Sticky'; const MODE_STORAGE_KEY = 'cms.md-mode'; +/** + * Slate can serialize to html, but we persist the value as markdown. Serializing + * the html to markdown on every keystroke is a big perf hit, so we'll register + * functions to perform those actions only when necessary, such as after loading + * and before persisting. + */ +registry.registerWidgetValueSerializer('markdown', { + serialize: remarkToMarkdown, + deserialize: markdownToRemark, +}); + export default class MarkdownControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired, - value: PropTypes.node, + value: PropTypes.object, }; constructor(props) { diff --git a/src/components/Widgets/Markdown/MarkdownPreview/index.js b/src/components/Widgets/Markdown/MarkdownPreview/index.js index b7927dfd..c3127f6a 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/index.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/index.js @@ -1,13 +1,18 @@ import React, { PropTypes } from 'react'; +import { remarkToHtml } from '../unified'; import previewStyle from '../../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { - return value === null ? null :
    ; + if (value === null) { + return null; + } + const html = remarkToHtml(value); + return
    ; }; MarkdownPreview.propTypes = { getAsset: PropTypes.func.isRequired, - value: PropTypes.string, + value: PropTypes.object, }; export default MarkdownPreview; diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index ba011474..434f2b34 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -1,29 +1,34 @@ -import find from 'lodash/find'; +import { get, find, isEmpty } from 'lodash'; import unified from 'unified'; -import markdownToRemark from 'remark-parse'; +import u from 'unist-builder'; +import markdownToRemarkPlugin from 'remark-parse'; +import remarkToMarkdownPlugin from 'remark-stringify'; +import mdastDefinitions from 'mdast-util-definitions'; +import modifyChildren from 'unist-util-modify-children'; 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 rehypeReparse from 'rehype-raw'; import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; import ReactDOMServer from 'react-dom/server'; import registry from '../../../lib/registry'; import merge from 'deepmerge'; -import rehypeSanitizeSchemaDefault from 'hast-util-sanitize/lib/github'; import hastFromString from 'hast-util-from-string'; import hastToMdastHandlerAll from 'hast-util-to-mdast/all'; import { reduce, capitalize } from 'lodash'; +// Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter +delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter; +console.log(markdownToRemarkPlugin.Parser.prototype.blockTokenizers); + const shortcodeAttributePrefix = 'ncp'; /** * Remove empty nodes, including the top level parents of deeply nested empty nodes. */ const rehypeRemoveEmpty = () => { - const isVoidElement = node => ['img', 'hr'].includes(node.tagName); + const isVoidElement = node => ['img', 'hr', 'br'].includes(node.tagName); const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value; const isShortcode = node => node.properties && node.properties[`data${capitalize(shortcodeAttributePrefix)}`]; const isNonEmptyNode = node => { @@ -135,28 +140,15 @@ const rehypeShortcodes = () => { } /** - * we can't escape the less than symbol - * which means how do we know {{}} from ? - * maybe we escape nothing - * then we can check for shortcodes in a unified plugin - * and only check against text nodes - * and maybe narrow the target text nodes even further somehow - * and make shortcode parsing faster + * Rewrite the remark-stringify text visitor to simply return the text value, + * without encoding or escaping any characters. This means we're completely + * trusting the markdown that we receive. */ function remarkPrecompileShortcodes() { const Compiler = this.Compiler; const visitors = Compiler.prototype.visitors; - const textVisitor = visitors.text; - - visitors.text = newTextVisitor; - - function newTextVisitor(node, parent) { - if (parent.data && parent.data[shortcodeAttributePrefix]) { - return node.value; - } - return textVisitor.call(this, node, parent); - } -} + visitors.text = node => node.value; +}; const parseShortcodesFromMarkdown = markdown => { const plugins = registry.getEditorComponents(); @@ -180,7 +172,302 @@ const parseShortcodesFromMarkdown = markdown => { return markdownLinesParsed.join('\n'); }; -const rehypeSanitizeSchema = merge(rehypeSanitizeSchemaDefault, { attributes: { '*': [ 'data*' ] } }); +const remarkToSlatePlugin = () => { + const typeMap = { + paragraph: 'paragraph', + blockquote: 'quote', + code: 'code', + listItem: 'list-item', + table: 'table', + tableRow: 'table-row', + tableCell: 'table-cell', + thematicBreak: 'thematic-break', + link: 'link', + image: 'image', + }; + const markMap = { + strong: 'bold', + emphasis: 'italic', + delete: 'strikethrough', + inlineCode: 'code', + }; + const toTextNode = text => ({ kind: 'text', text }); + const wrapText = (node, index, parent) => { + if (['text', 'html'].includes(node.type)) { + parent.children.splice(index, 1, u('paragraph', [node])); + } + }; + + let getDefinition; + const transform = node => { + let nodes; + + if (node.type === 'root') { + getDefinition = mdastDefinitions(node); + modifyChildren(wrapText)(node); + } + + if (isEmpty(node.children)) { + nodes = node.children; + } else { + // If a node returns a falsey value, exclude it. Some nodes do not + // translate from MDAST to Slate, such as definitions for link/image + // references or footnotes. + nodes = node.children.reduce((acc, childNode) => { + const transformed = transform(childNode); + if (transformed) { + acc.push(transformed); + } + return acc; + }, []); + } + + if (node.type === 'root') { + return { nodes }; + } + + // Process raw html as text, since it's valid markdown + if (['text', 'html'].includes(node.type)) { + return toTextNode(node.value); + } + + if (node.type === 'inlineCode') { + return { kind: 'text', ranges: [{ text: node.value, marks: [{ type: 'code' }] }] }; + } + + if (['strong', 'emphasis', 'delete'].includes(node.type)) { + const remarkToSlateMarks = (markNode, parentMarks = []) => { + const marks = [...parentMarks, { type: markMap[markNode.type] }]; + const ranges = []; + markNode.children.forEach(childNode => { + if (['html', 'text'].includes(childNode.type)) { + ranges.push({ text: childNode.value, marks }); + return; + } + const nestedRanges = remarkToSlateMarks(childNode, marks); + ranges.push(...nestedRanges); + }); + return ranges; + }; + + return { kind: 'text', ranges: remarkToSlateMarks(node) }; + } + + if (node.type === 'heading') { + const depths = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; + return { kind: 'block', type: `heading-${depths[node.depth]}`, nodes }; + } + + if (['paragraph', 'blockquote', 'tableRow', 'tableCell'].includes(node.type)) { + return { kind: 'block', type: typeMap[node.type], nodes }; + } + + if (node.type === 'code') { + const data = { lang: node.lang }; + const text = toTextNode(node.value); + const nodes = [text]; + return { kind: 'block', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'list') { + const slateType = node.ordered ? 'numbered-list' : 'bulleted-list'; + const data = { start: node.start }; + return { kind: 'block', type: slateType, data, nodes }; + } + + if (node.type === 'listItem') { + const data = { checked: node.checked }; + return { kind: 'block', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'table') { + const data = { align: node.align }; + return { kind: 'block', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'thematicBreak') { + return { kind: 'block', type: typeMap[node.type], isVoid: true }; + } + + if (node.type === 'link') { + const { title, url } = node; + const data = { title, url }; + return { kind: 'inline', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'linkReference') { + const definition = getDefinition(node.identifier); + const { title, url } = definition; + const data = { title, url }; + return { kind: 'inline', type: typeMap['link'], data, nodes }; + } + + if (node.type === 'image') { + const { title, url, alt } = node; + const data = { title, url, alt }; + return { kind: 'block', type: typeMap[node.type], data }; + } + + if (node.type === 'imageReference') { + const definition = getDefinition(node.identifier); + const { title, url } = definition; + const data = { title, url }; + return { kind: 'block', type: typeMap['image'], data }; + } + }; + return transform; +}; + +const slateToRemarkPlugin = () => { + const transform = node => { + console.log(node); + return node; + }; + return transform; +}; + +export const markdownToRemark = markdown => { + const result = unified() + .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) + .parse(markdown); + return result; +}; + +export const remarkToMarkdown = obj => { + const mdast = obj || u('root', [u('paragraph', [u('text', '')])]); + + const result = unified() + .use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true }) + .use(remarkPrecompileShortcodes) + .stringify(mdast); + return result; +}; + +export const remarkToSlate = mdast => { + const result = unified() + .use(remarkToSlatePlugin) + .runSync(mdast); + return result; +}; + +export const slateToRemark = raw => { + const typeMap = { + 'paragraph': 'paragraph', + 'heading-one': 'heading', + 'heading-two': 'heading', + 'heading-three': 'heading', + 'heading-four': 'heading', + 'heading-five': 'heading', + 'heading-six': 'heading', + 'quote': 'blockquote', + 'code': 'code', + 'numbered-list': 'list', + 'bulleted-list': 'list', + 'list-item': 'listItem', + 'table': 'table', + 'table-row': 'tableRow', + 'table-cell': 'tableCell', + 'thematic-break': 'thematicBreak', + 'link': 'link', + 'image': 'image', + }; + const markMap = { + bold: 'strong', + italic: 'emphasis', + strikethrough: 'delete', + code: 'inlineCode', + }; + const transform = node => { + const children = isEmpty(node.nodes) ? node.nodes : node.nodes.reduce((acc, childNode) => { + if (childNode.kind !== 'text') { + acc.push(transform(childNode)); + return acc; + } + if (childNode.ranges) { + childNode.ranges.forEach(range => { + const { marks = [], text } = range; + const markTypes = marks.map(mark => markMap[mark.type]); + if (markTypes.includes('inlineCode')) { + acc.push(u('inlineCode', text)); + } else { + const textNode = u('html', text); + const nestedText = !markTypes.length ? textNode : markTypes.reduce((acc, markType) => { + const nested = u(markType, [acc]); + return nested; + }, textNode); + acc.push(nestedText); + } + }); + } else { + acc.push(u('html', childNode.text)); + } + return acc; + }, []); + + if (node.type === 'root') { + return u('root', children); + } + + if (node.type.startsWith('heading')) { + const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; + const depth = node.type.split('-')[1]; + const props = { depth: depths[depth] }; + return u(typeMap[node.type], props, children); + } + + if (['paragraph', 'quote', 'list-item', 'table', 'table-row', 'table-cell'].includes(node.type)) { + return u(typeMap[node.type], children); + } + + if (node.type === 'code') { + const value = get(node.nodes, [0, 'text']); + const props = { lang: get(node.data, 'lang') }; + return u(typeMap[node.type], props, value); + } + + if (['numbered-list', 'bulleted-list'].includes(node.type)) { + const ordered = node.type === 'numbered-list'; + const props = { ordered, start: get(node.data, 'start') || 1 }; + return u(typeMap[node.type], props, children); + } + + if (node.type === 'thematic-break') { + return u(typeMap[node.type]); + } + + if (node.type === 'link') { + const data = get(node, 'data', {}); + const { url, title } = data; + return u(typeMap[node.type], data, children); + } + + if (node.type === 'image') { + const data = get(node, 'data', {}); + const { url, title, alt } = data; + return u(typeMap[node.type], data); + } + } + raw.type = 'root'; + const result = transform(raw); + return result; +}; + +export const remarkToHtml = mdast => { + const result = unified() + .use(remarkToRehype, { allowDangerousHTML: true }) + .use(rehypeReparse) + .use(rehypeRemoveEmpty) + .use(rehypeMinifyWhitespace) + .use(() => node => { + return node; + }) + .runSync(mdast); + + const output = unified() + .use(rehypeToHtml, { allowDangerousHTML: true, allowDangerousCharacters: true, entities: { subset: [] } }) + .stringify(result); + return output +} export const markdownToHtml = markdown => { // Parse shortcodes from the raw markdown rather than via Unified plugin. @@ -188,11 +475,9 @@ export const markdownToHtml = markdown => { // parsing rules. const markdownWithParsedShortcodes = parseShortcodesFromMarkdown(markdown); const result = unified() - .use(markdownToRemark, { fences: true }) + .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) .use(remarkToRehype, { allowDangerousHTML: true }) - .use(rehypeReparse) .use(rehypeRemoveEmpty) - .use(rehypeSanitize, rehypeSanitizeSchema) .use(rehypeMinifyWhitespace) .use(rehypeToHtml, { allowDangerousHTML: true }) .processSync(markdownWithParsedShortcodes) @@ -203,7 +488,6 @@ export const markdownToHtml = markdown => { export const htmlToMarkdown = html => { const result = unified() .use(htmlToRehype, { fragment: true }) - .use(rehypeSanitize, rehypeSanitizeSchema) .use(rehypeRemoveEmpty) .use(rehypeMinifyWhitespace) .use(rehypePaperEmoji) @@ -222,7 +506,7 @@ export const htmlToMarkdown = html => { return node; }) .use(remarkNestedList) - .use(remarkToMarkdown, { listItemIndent: '1', fences: true }) + .use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true }) .use(remarkPrecompileShortcodes) /* .use(() => node => { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 059fa947..5bc3a944 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -13,6 +13,7 @@ import { deleteEntry, } from '../actions/entries'; import { closeEntry } from '../actions/editor'; +import { deserializeValues } from '../lib/serializeEntryValues'; import { addAsset, removeAsset } from '../actions/media'; import { openSidebar } from '../actions/globalUI'; import { selectEntry, getAsset } from '../reducers'; @@ -64,11 +65,14 @@ class EntryPage extends React.Component { componentWillReceiveProps(nextProps) { if (this.props.entry === nextProps.entry) return; + const { entry, newEntry, fields, collection } = nextProps; - if (nextProps.entry && !nextProps.entry.get('isFetching') && !nextProps.entry.get('error')) { - this.createDraft(nextProps.entry); - } else if (nextProps.newEntry) { - this.props.createEmptyDraft(nextProps.collection); + if (entry && !entry.get('isFetching') && !entry.get('error')) { + const values = deserializeValues(entry.get('data'), fields); + const deserializedEntry = entry.set('data', values); + this.createDraft(deserializedEntry); + } else if (newEntry) { + this.props.createEmptyDraft(collection); } } diff --git a/src/lib/serializeEntryValues.js b/src/lib/serializeEntryValues.js new file mode 100644 index 00000000..f6f9ae05 --- /dev/null +++ b/src/lib/serializeEntryValues.js @@ -0,0 +1,49 @@ +import { isArray, isObject, isEmpty, isNil } from 'lodash'; +import { Map, List } from 'immutable'; +import registry from './registry'; + +/** + * Methods for serializing/deserializing entry field values. Most widgets don't + * require this for their values, and those that do can typically serialize/ + * deserialize on every change from within the widget. The serialization + * handlers here are for widgets whose values require heavy serialization that + * would hurt performance if run for every change. + + * An example of this is the markdown widget, whose value is stored as a + * markdown string. Instead of stringifying on every change of that field, a + * deserialization method is registered from the widget's control module that + * converts the stored markdown string to an AST, and that AST serves as the + * widget model during editing. + * + * Serialization handlers should be registered for each widget that requires + * them, and the registration method is exposed through the registry. Any + * registered deserialization handlers run on entry load, and serialization + * handlers run on persist. + */ + +const runSerializer = (values, fields, method) => { + return fields.reduce((acc, field) => { + const fieldName = field.get('name'); + const value = values.get(fieldName); + const serializer = registry.getWidgetValueSerializer(field.get('widget')); + const nestedFields = field.get('fields'); + if (nestedFields && List.isList(value)) { + return acc.set(fieldName, value.map(val => runSerializer(val, nestedFields, method))); + } else if (nestedFields && Map.isMap(value)) { + return acc.set(fieldName, runSerializer(value, nestedFields, method)); + } else if (serializer && !isNil(value)) { + return acc.set(fieldName, serializer[method](value)); + } else if (!isNil(value)) { + return acc.set(fieldName, value); + } + return acc; + }, Map()); +}; + +export const serializeValues = (values, fields) => { + return runSerializer(values, fields, 'serialize'); +}; + +export const deserializeValues = (values, fields) => { + return runSerializer(values, fields, 'deserialize'); +}; diff --git a/src/reducers/entries.js b/src/reducers/entries.js index acd891bf..49c20bf5 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -21,10 +21,11 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); case ENTRY_SUCCESS: - return state.setIn( + const result = state.setIn( ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], fromJS(action.payload.entry) ); + return result; case ENTRIES_REQUEST: return state.setIn(['pages', action.payload.collection, 'isFetching'], true); From c95f06138a7f9fd8a4d311cd395acf8ea339774f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 24 Jul 2017 13:36:14 -0400 Subject: [PATCH 50/79] fix soft break side effects --- .../Widgets/Markdown/MarkdownControl/VisualEditor/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index f6b5d1ca..c49cf597 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -373,7 +373,7 @@ const BackspaceCloseBlock = (options = {}) => ({ }); const slatePlugins = [ - SoftBreak({ ignoreIn: ['list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }), + SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }), BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list', 'table', 'table-row', 'table-cell'] }), EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }), EditTable({ typeTable: 'table', typeRow: 'table-row', typeCell: 'table-cell' }), From b7379b019e796884874d02a47cfa60aec9d4546b Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 24 Jul 2017 11:12:47 -0400 Subject: [PATCH 51/79] re-implement shortcode parsing to/from mdast --- package.json | 1 + .../MarkdownControl/VisualEditor/index.js | 3 +- src/components/Widgets/Markdown/unified.js | 141 ++++++++++++++++-- yarn.lock | 10 +- 4 files changed, 138 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index a2405c4c..67d4629b 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "textarea-caret-position": "^0.1.1", "unified": "^6.1.4", "unist-builder": "^1.0.2", + "unist-util-map": "^1.0.3", "unist-util-modify-children": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index c49cf597..ec2070fe 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -139,6 +139,7 @@ const NODE_COMPONENTS = { }, 'shortcode': props => { const { attributes, node, state: editorState } = props; + const { data } = node; const isSelected = editorState.selection.hasFocusIn(node); return (
    - {getShortcodeId(props)} + {data.get('shortcode')}
    ); }, diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 434f2b34..139c485d 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -1,4 +1,5 @@ -import { get, find, isEmpty } from 'lodash'; +import { get, has, find, isEmpty } from 'lodash'; +import { renderToString } from 'react-dom/server'; import unified from 'unified'; import u from 'unist-builder'; import markdownToRemarkPlugin from 'remark-parse'; @@ -20,7 +21,6 @@ import { reduce, capitalize } from 'lodash'; // Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter; -console.log(markdownToRemarkPlugin.Parser.prototype.blockTokenizers); const shortcodeAttributePrefix = 'ncp'; @@ -150,6 +150,90 @@ function remarkPrecompileShortcodes() { visitors.text = node => node.value; }; +const remarkShortcodes = ({ plugins }) => { + return transform; + + function transform(node) { + if (node.children) { + node.children = node.children.reduce(reducer, []); + } + return node; + + function reducer(newChildren, childNode) { + if (!['text', 'html'].includes(childNode.type)) { + const processedNode = childNode.children ? transform(childNode) : childNode; + newChildren.push(processedNode); + return newChildren; + } + + const text = childNode.value; + let lastPlugin; + let match; + const plugin = plugins.find(p => { + match = text.match(p.pattern); + return match; + }); + if (!plugin) { + newChildren.push(childNode); + return newChildren; + } + const matchValue = match[0]; + const matchLength = matchValue.length; + const matchAll = matchLength === text.length; + + if (matchAll) { + const shortcodeNode = createShortcodeNode(text, plugin, match); + newChildren.push(shortcodeNode); + return newChildren; + } + + const tempChildren = []; + const matchAtStart = match.index === 0; + const matchAtEnd = match.index + matchLength === text.length; + + if (!matchAtStart) { + const textBeforeMatch = text.slice(0, match.index); + const result = reducer([], { type: 'text', value: textBeforeMatch }); + tempChildren.push(...result); + } + + const matchNode = createShortcodeNode(matchValue, plugin, match); + tempChildren.push(matchNode); + + if (!matchAtEnd) { + const textAfterMatch = text.slice(match.index + matchLength); + const result = reducer([], { type: 'text', value: textAfterMatch }); + tempChildren.push(...result); + } + + newChildren.push(...tempChildren); + return newChildren; + } + + function createShortcodeNode(text, plugin, match) { + const shortcode = plugin.id; + const shortcodeData = plugin.fromBlock(match); + return { type: 'html', value: text, data: { shortcode, shortcodeData } }; + } + } +}; + +const remarkToRehypeShortcodes = ({ plugins }) => { + return transform; + + function transform(node) { + const children = node.children ? node.children.map(transform) : node.children; + if (!has(node, ['data', 'shortcode'])) { + return { ...node, children }; + } + const { shortcode, shortcodeData } = node.data; + const plugin = plugins.get(shortcode); + const value = plugin.toPreview(shortcodeData); + const valueHtml = typeof value === 'string' ? value : renderToString(value); + return { ...node, value: valueHtml }; + } +}; + const parseShortcodesFromMarkdown = markdown => { const plugins = registry.getEditorComponents(); const markdownLines = markdown.split('\n'); @@ -191,7 +275,7 @@ const remarkToSlatePlugin = () => { delete: 'strikethrough', inlineCode: 'code', }; - const toTextNode = text => ({ kind: 'text', text }); + const toTextNode = (text, data) => ({ kind: 'text', text, data }); const wrapText = (node, index, parent) => { if (['text', 'html'].includes(node.type)) { parent.children.splice(index, 1, u('paragraph', [node])); @@ -199,11 +283,13 @@ const remarkToSlatePlugin = () => { }; let getDefinition; - const transform = node => { + const transform = (node, index, siblings, parent) => { let nodes; if (node.type === 'root') { + // Create definition getter for link and image references getDefinition = mdastDefinitions(node); + // Ensure top level text nodes are wrapped in paragraphs modifyChildren(wrapText)(node); } @@ -213,8 +299,10 @@ const remarkToSlatePlugin = () => { // If a node returns a falsey value, exclude it. Some nodes do not // translate from MDAST to Slate, such as definitions for link/image // references or footnotes. - nodes = node.children.reduce((acc, childNode) => { - const transformed = transform(childNode); + // + // Consider using unist-util-remove instead for this. + nodes = node.children.reduce((acc, childNode, idx, sibs) => { + const transformed = transform(childNode, idx, sibs, node); if (transformed) { acc.push(transformed); } @@ -228,7 +316,17 @@ const remarkToSlatePlugin = () => { // Process raw html as text, since it's valid markdown if (['text', 'html'].includes(node.type)) { - return toTextNode(node.value); + const { value, data } = node; + const shortcode = get(data, 'shortcode'); + if (shortcode) { + const isBlock = parent.type === 'paragraph' && siblings.length === 1; + data.shortcodeValue = value; + + if (isBlock) { + return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes: [toTextNode('')] }; + } + } + return toTextNode(value, data); } if (node.type === 'inlineCode') { @@ -315,7 +413,10 @@ const remarkToSlatePlugin = () => { return { kind: 'block', type: typeMap['image'], data }; } }; - return transform; + + // Since `transform` is used for recursive child mapping, ensure that only the + // first argument is supplied on the initial call. + return node => transform(node); }; const slateToRemarkPlugin = () => { @@ -327,9 +428,14 @@ const slateToRemarkPlugin = () => { }; export const markdownToRemark = markdown => { - const result = unified() + const parsed = unified() .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) .parse(markdown); + + const result = unified() + .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) + .runSync(parsed); + return result; }; @@ -399,6 +505,7 @@ export const slateToRemark = raw => { } }); } else { + acc.push(u('html', childNode.text)); } return acc; @@ -408,6 +515,10 @@ export const slateToRemark = raw => { return u('root', children); } + if (node.type === 'shortcode') { + return u('html', { data: node.data }, node.data.shortcodeValue); + } + if (node.type.startsWith('heading')) { const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; const depth = node.type.split('-')[1]; @@ -448,19 +559,21 @@ export const slateToRemark = raw => { } } raw.type = 'root'; - const result = transform(raw); + const mdast = transform(raw); + + const result = unified() + .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) + .runSync(mdast); + return result; }; export const remarkToHtml = mdast => { const result = unified() + .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents() }) .use(remarkToRehype, { allowDangerousHTML: true }) - .use(rehypeReparse) .use(rehypeRemoveEmpty) .use(rehypeMinifyWhitespace) - .use(() => node => { - return node; - }) .runSync(mdast); const output = unified() diff --git a/yarn.lock b/yarn.lock index 848b2350..97a6a911 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5321,7 +5321,7 @@ mdast-util-compact@^1.0.0: unist-util-modify-children "^1.0.0" unist-util-visit "^1.1.0" -mdast-util-definitions@^1.2.0: +mdast-util-definitions@^1.2.0, mdast-util-definitions@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.2.tgz#673f4377c3e23d3de7af7a4fe2214c0e221c5ac7" dependencies: @@ -9010,7 +9010,13 @@ unist-util-is@^2.0.0, unist-util-is@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" -unist-util-modify-children@^1.0.0: +unist-util-map@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/unist-util-map/-/unist-util-map-1.0.3.tgz#26a913d7cddb3cd3e9a886d135d37a3d1f54e514" + dependencies: + object-assign "^4.0.1" + +unist-util-modify-children@^1.0.0, unist-util-modify-children@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d" dependencies: From dbf14a8f7b2e475b27ab6f0e051be0133901ae31 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 25 Jul 2017 21:45:33 -0400 Subject: [PATCH 52/79] re-enable shortcode insertion via toolbar --- .../MarkdownControl/VisualEditor/index.js | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index ec2070fe..78a42cd9 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -397,6 +397,23 @@ export default class Editor extends Component { schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, + rules: [ + { + match: object => object.kind === 'document', + validate: doc => { + const hasBlocks = !doc.getBlocks().isEmpty(); + return hasBlocks ? null : {}; + }, + normalize: transform => { + const block = SlateBlock.create({ + type: 'paragraph', + nodes: [SlateText.createFromString('')], + }); + const { key } = transform.state.document; + return transform.insertNodeByKey(key, 0, block).focus(); + }, + }, + ], }, plugins, }; @@ -504,11 +521,15 @@ export default class Editor extends Component { command(this.view.state, this.handleAction); }; - handlePluginSubmit = (plugin, data) => { + handlePluginSubmit = (plugin, shortcodeData) => { const { editorState } = this.state; - const markdown = plugin.toBlock(data.toJS()); - const html = markdownToHtml(markdown); - const block = serializer.deserialize(html).document.getBlocks().first(); + const data = { + shortcode: plugin.id, + shortcodeValue: plugin.toBlock(shortcodeData.toJS()), + shortcodeData, + }; + const nodes = [SlateText.createFromString('')]; + const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; const resolvedState = editorState.transform().insertBlock(block).apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); From fbecc887b85f6fa48c791b7eb26215eeeeffca6d Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 12:47:32 -0400 Subject: [PATCH 53/79] require images to be parsed as shortcodes --- src/components/Widgets/Markdown/unified.js | 28 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 139c485d..8f343f03 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -19,9 +19,6 @@ import hastFromString from 'hast-util-from-string'; import hastToMdastHandlerAll from 'hast-util-to-mdast/all'; import { reduce, capitalize } from 'lodash'; -// Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter -delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter; - const shortcodeAttributePrefix = 'ncp'; /** @@ -427,12 +424,37 @@ const slateToRemarkPlugin = () => { return transform; }; +/** + * Images must be parsed as shortcodes for asset proxying. This plugin converts + * MDAST image nodes back to text to allow shortcode pattern matching. + */ +const remarkImagesToText = () => { + return transform; + + function transform(node) { + const children = node.children ? node.children.map(transform) : node.children; + if (node.type === 'image') { + const alt = node.alt || ''; + const url = node.url || ''; + const title = node.title ? ` "${node.title}"` : ''; + return { type: 'text', value: `![${alt}](${url}${title})` }; + } + return { ...node, children }; + } +} + export const markdownToRemark = markdown => { const parsed = unified() .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) + .use(function() { + const { blockMethods } = this.Parser.prototype; + // Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter + blockMethods.splice(blockMethods.indexOf('yamlFrontMatter'), 1); + }) .parse(markdown); const result = unified() + .use(remarkImagesToText) .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) .runSync(parsed); From 6443f5d808e67284bc7c7f2f68619375f731b759 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 17:11:23 -0400 Subject: [PATCH 54/79] allow enter key to make space around void nodes --- .../MarkdownControl/VisualEditor/index.js | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 78a42cd9..9ad1020e 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -439,23 +439,55 @@ export default class Editor extends Component { hasBlock = type => this.state.editorState.blocks.some(node => node.type === type); handleKeyDown = (e, data, state) => { - if (!data.isMod) { - return; - } - const marks = { - b: 'bold', - i: 'italic', - u: 'underlined', - s: 'strikethrough', - '`': 'code', + const createDefaultBlock = () => { + return SlateBlock.create({ + type: 'paragraph', + nodes: [SlateText.createFromString('')] + }); }; + if (data.key === 'enter') { + /** + * If a single void block is selected, and it's a direct descendant of the + * document (top level), a new paragraph should be added above or below it + * when 'Enter' is pressed, and the current selection should move to the + * new paragraph. + * + * If the selected block is the first block in the document, create the + * new block above it. If not, create the new block below it. + */ + const { document: doc, selection, anchorBlock, focusBlock } = state; + const focusBlockIndex = doc.nodes.indexOf(focusBlock); + const focusBlockIsTopLevel = focusBlockIndex > -1; + const focusBlockIsFirstChild = focusBlockIndex === 0; + const singleBlockSelected = anchorBlock === focusBlock; - const mark = marks[data.key]; - - if (mark) { - state = state.transform().toggleMark(mark).apply(); + if (focusBlock.isVoid && focusBlockIsTopLevel && singleBlockSelected) { + e.preventDefault(); + const newBlock = createDefaultBlock(); + const newBlockIndex = focusBlockIsFirstChild ? 0 : focusBlockIndex + 1; + return state.transform() + .insertNodeByKey(doc.key, newBlockIndex, newBlock) + .collapseToStartOf(newBlock) + .apply(); + } + } + + if (data.isMod) { + const marks = { + b: 'bold', + i: 'italic', + u: 'underlined', + s: 'strikethrough', + '`': 'code', + }; + + const mark = marks[data.key]; + + if (mark) { + e.preventDefault(); + return state.transform().toggleMark(mark).apply(); + } } - return; }; handleMarkClick = (event, type) => { @@ -584,7 +616,7 @@ export default class Editor extends Component { plugins={slatePlugins} onChange={editorState => this.setState({ editorState })} onDocumentChange={this.handleDocumentChange} - onKeyDown={this.onKeyDown} + onKeyDown={this.handleKeyDown} onPaste={this.handlePaste} ref={ref => this.ref = ref} spellCheck From 4ac63954ca81db22673c0cea3f76794328012efa Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 20:06:53 -0400 Subject: [PATCH 55/79] fix focus update on toolbar block click --- .../Widgets/Markdown/MarkdownControl/VisualEditor/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 9ad1020e..b5fb4cda 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -500,7 +500,7 @@ export default class Editor extends Component { handleBlockClick = (event, type) => { event.preventDefault(); let { editorState } = this.state; - const transform = editorState.transform().focus(); + const transform = editorState.transform(); const doc = editorState.document; const isList = this.hasBlock('list-item') @@ -538,7 +538,7 @@ export default class Editor extends Component { } } - const resolvedState = transform.apply(); + const resolvedState = transform.focus().apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; From 82d9bdd7ae22c8f64cdcfe837f5f0c5ff2b91d6a Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 20:29:19 -0400 Subject: [PATCH 56/79] port history shortcuts from Slate, force focus --- .../Markdown/MarkdownControl/VisualEditor/index.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index b5fb4cda..60569408 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -473,6 +473,17 @@ export default class Editor extends Component { } if (data.isMod) { + + if (data.key === 'y') { + e.preventDefault(); + return state.transform().redo().focus().apply({ save: false }); + } + + if (data.key === 'z') { + e.preventDefault(); + return state.transform()[data.isShift ? 'redo' : 'undo']().focus().apply({ save: false }); + } + const marks = { b: 'bold', i: 'italic', From ae7bd79c7ac62afa61e0dca211bb82cb2852cd4f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 20:55:30 -0400 Subject: [PATCH 57/79] re-implement visual editor html paste --- .../MarkdownControl/VisualEditor/index.js | 181 +----------------- src/components/Widgets/Markdown/unified.js | 33 +--- 2 files changed, 14 insertions(+), 200 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 60569408..59873ef9 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -3,10 +3,10 @@ import ReactDOMServer from 'react-dom/server'; import { Map, List, fromJS } from 'immutable'; import { get, reduce, mapValues } from 'lodash'; import cn from 'classnames'; -import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate'; +import { Editor as SlateEditor, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate'; import EditList from 'slate-edit-list'; import EditTable from 'slate-edit-table'; -import { markdownToRemark, remarkToMarkdown, slateToRemark, remarkToSlate, markdownToHtml, htmlToMarkdown } from '../../unified'; +import { markdownToRemark, remarkToMarkdown, slateToRemark, remarkToSlate, markdownToHtml, htmlToSlate } from '../../unified'; import registry from '../../../../../lib/registry'; import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; @@ -160,176 +160,6 @@ const MARK_COMPONENTS = { code: props => {props.children}, }; -const RULES = [ - { - deserialize(el, next) { - const shortcodeId = el.attribs && el.attribs['data-ncp']; - if (!shortcodeId) { - return; - } - const plugin = registry.getEditorComponents().get(shortcodeId); - if (!plugin) { - return; - } - const shortcodeData = Map(el.attribs).reduce((acc, value, key) => { - if (key.startsWith('data-ncp-')) { - const dataKey = key.slice('data-ncp-'.length).toLowerCase(); - if (dataKey) { - return acc.set(dataKey, value); - } - } - return acc; - }, Map({ shortcodeId })); - - const result = { - kind: 'block', - isVoid: true, - type: 'shortcode', - data: { shortcode: shortcodeData }, - }; - return result; - }, - serialize(entity, children) { - if (entity.type !== 'shortcode') { - return; - } - - const data = Map(entity.data.get('shortcode')); - const shortcodeId = data.get('shortcodeId'); - const plugin = registry.getEditorComponents().get(shortcodeId); - const dataAttrs = data.delete('shortcodeId').mapKeys(key => `data-ncp-${key}`).set('data-ncp', shortcodeId); - const preview = plugin.toPreview(data.toJS()); - const component = typeof preview === 'string' - ?
    - :
    {preview}
    ; - return component; - }, - }, - { - deserialize(el, next) { - const block = BLOCK_TAGS[el.tagName] - if (!block) return - return { - kind: 'block', - type: block, - nodes: next(el.children) - } - }, - serialize(entity, children) { - if (['bulleted-list', 'numbered-list'].includes(entity.type)) { - return; - } - const component = BLOCK_COMPONENTS[entity.type] - if (!component) { - return; - } - return component({ children }); - } - }, - { - deserialize(el, next) { - const mark = MARK_TAGS[el.tagName] - if (!mark) return - return { - kind: 'mark', - type: mark, - nodes: next(el.children) - } - }, - serialize(entity, children) { - if (entity.kind !== 'mark') { - return; - } - const component = MARK_COMPONENTS[entity.type] - return component({ children }); - } - }, - { - // Special case for code blocks, which need to grab the nested children. - deserialize(el, next) { - if (el.tagName != 'pre') return - const code = el.children[0] - const children = code && code.tagName == 'code' - ? code.children - : el.children - - return { - kind: 'block', - type: 'code', - nodes: next(children) - } - }, - }, - { - deserialize(el, next) { - if (el.tagName != 'img') return - return { - kind: 'block', - type: 'image', - isVoid: true, - nodes: [], - data: { - src: el.attribs.src, - alt: el.attribs.alt, - title: el.attribs.title, - } - } - }, - serialize(entity, children) { - if (entity.type !== 'image') { - return; - } - const data = entity.get('data'); - const props = { - src: data.get('src'), - alt: data.get('alt'), - title: data.get('title'), - }; - const result = NODE_COMPONENTS.image(props); - return result; - } - }, - { - // Special case for links, to grab their href. - deserialize(el, next) { - if (el.tagName != 'a') return - return { - kind: 'inline', - type: 'link', - nodes: next(el.children), - data: { - href: el.attribs.href, - title: el.attribs.title, - } - } - }, - serialize(entity, children) { - if (entity.type !== 'link') { - return; - } - const data = entity.get('data'); - const props = { - href: data.get('href'), - title: data.get('title'), - attributes: data.get('attributes'), - children, - }; - return NODE_COMPONENTS.link(props); - } - }, - { - serialize(entity, children) { - if (!['bulleted-list', 'numbered-list'].includes(entity.type)) { - return; - } - return NODE_COMPONENTS[entity.type]({ children }); - } - } - -] - -const htmlSerializer = new SlateHtml({ rules: RULES }); - const SoftBreak = (options = {}) => ({ onKeyDown(e, data, state) { if (data.key != 'enter') return; @@ -423,10 +253,9 @@ export default class Editor extends Component { if (data.type !== 'html' || data.isShift) { return; } - const markdown = htmlToMarkdown(data.html); - const html = markdownToHtml(markdown); - const fragment = serializer.deserialize(html).document; - return state.transform().insertFragment(fragment).apply(); + const ast = htmlToSlate(data.html); + const { document: doc } = SlateRaw.deserialize(ast, { terse: true }); + return state.transform().insertFragment(doc).apply(); } handleDocumentChange = (doc, editorState) => { diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 8f343f03..b9a68160 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -620,35 +620,20 @@ export const markdownToHtml = markdown => { return result; } -export const htmlToMarkdown = html => { - const result = unified() +export const htmlToSlate = html => { + const hast = unified() .use(htmlToRehype, { fragment: true }) + .parse(html); + + const result = unified() .use(rehypeRemoveEmpty) .use(rehypeMinifyWhitespace) .use(rehypePaperEmoji) .use(rehypeShortcodes) - .use(rehypeToRemark, { handlers: { div: (h, node) => { - const dataPrefix = `data${capitalize(shortcodeAttributePrefix)}`; - const isShortcode = node.properties[dataPrefix]; - if (isShortcode) { - const paragraph = h(node, 'paragraph', hastToMdastHandlerAll(h, node)); - paragraph.data = paragraph.data || {}; - paragraph.data[shortcodeAttributePrefix] = true; - return paragraph; - } - }}}) - .use(() => node => { - return node; - }) + .use(rehypeToRemark) .use(remarkNestedList) - .use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true }) - .use(remarkPrecompileShortcodes) - /* - .use(() => node => { - return node; - }) - */ - .processSync(html) - .contents; + .use(remarkToSlatePlugin) + .runSync(hast); + return result; }; From 7a744bef840663b6de44ea04f3efaa13b0ea7e5b Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 21:44:39 -0400 Subject: [PATCH 58/79] improve list handling --- .../MarkdownControl/VisualEditor/index.js | 72 +++++++++---------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 59873ef9..6a862551 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -203,10 +203,12 @@ const BackspaceCloseBlock = (options = {}) => ({ } }); +const EditListPlugin = EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }); + const slatePlugins = [ SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }), BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list', 'table', 'table-row', 'table-cell'] }), - EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }), + EditListPlugin, EditTable({ typeTable: 'table', typeRow: 'table-row', typeCell: 'table-cell' }), ]; @@ -228,6 +230,10 @@ export default class Editor extends Component { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, rules: [ + /** + * If the editor is ever in an empty state, insert an empty + * paragraph block. + */ { match: object => object.kind === 'document', validate: doc => { @@ -276,29 +282,30 @@ export default class Editor extends Component { }; if (data.key === 'enter') { /** - * If a single void block is selected, and it's a direct descendant of the - * document (top level), a new paragraph should be added above or below it - * when 'Enter' is pressed, and the current selection should move to the - * new paragraph. + * If "Enter" is pressed while a single void block is selected, a new + * paragraph should be added above or below it, and the current selection + * should be collapsed to the start of the new paragraph. * * If the selected block is the first block in the document, create the * new block above it. If not, create the new block below it. */ const { document: doc, selection, anchorBlock, focusBlock } = state; - const focusBlockIndex = doc.nodes.indexOf(focusBlock); - const focusBlockIsTopLevel = focusBlockIndex > -1; - const focusBlockIsFirstChild = focusBlockIndex === 0; const singleBlockSelected = anchorBlock === focusBlock; + if (!singleBlockSelected || !focusBlock.isVoid) return; - if (focusBlock.isVoid && focusBlockIsTopLevel && singleBlockSelected) { - e.preventDefault(); - const newBlock = createDefaultBlock(); - const newBlockIndex = focusBlockIsFirstChild ? 0 : focusBlockIndex + 1; - return state.transform() - .insertNodeByKey(doc.key, newBlockIndex, newBlock) - .collapseToStartOf(newBlock) - .apply(); - } + e.preventDefault(); + + const focusBlockParent = doc.getParent(focusBlock.key); + const focusBlockIndex = focusBlockParent.nodes.indexOf(focusBlock); + const focusBlockIsFirstChild = focusBlockIndex === 0; + + const newBlock = createDefaultBlock(); + const newBlockIndex = focusBlockIsFirstChild ? 0 : focusBlockIndex + 1; + + return state.transform() + .insertNodeByKey(focusBlockParent.key, newBlockIndex, newBlock) + .collapseToStartOf(newBlock) + .apply(); } if (data.isMod) { @@ -340,41 +347,30 @@ export default class Editor extends Component { handleBlockClick = (event, type) => { event.preventDefault(); let { editorState } = this.state; + const { document: doc, selection } = editorState; const transform = editorState.transform(); - const doc = editorState.document; - const isList = this.hasBlock('list-item') // Handle everything except list buttons. if (!['bulleted-list', 'numbered-list'].includes(type)) { const isActive = this.hasBlock(type); const transformed = transform.setBlock(isActive ? DEFAULT_NODE : type); - - if (isList) { - transformed - .unwrapBlock('bulleted-list') - .unwrapBlock('numbered-list'); - } } // Handle the extra wrapping required for list buttons. else { - const isType = editorState.blocks.some(block => { + const isSameListType = editorState.blocks.some(block => { return !!doc.getClosest(block.key, parent => parent.type === type); }); + const isInList = EditListPlugin.utils.isSelectionInList(editorState); - if (isList && isType) { - transform - .setBlock(DEFAULT_NODE) - .unwrapBlock('bulleted-list') - .unwrapBlock('numbered-list'); - } else if (isList) { - transform - .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') - .wrapBlock(type); + if (isInList && isSameListType) { + EditListPlugin.transforms.unwrapList(transform, type); + } else if (isInList) { + const currentListType = type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list'; + EditListPlugin.transforms.unwrapList(transform, currentListType); + EditListPlugin.transforms.wrapInList(transform, type); } else { - transform - .setBlock('list-item') - .wrapBlock(type); + EditListPlugin.transforms.wrapInList(transform, type); } } From de1e36108d53a0d97031f6dff95f2000a2628fb2 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 26 Jul 2017 22:10:19 -0400 Subject: [PATCH 59/79] allow yaml frontmatter parsing --- src/components/Widgets/Markdown/unified.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index b9a68160..3ebd6d20 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -446,11 +446,6 @@ const remarkImagesToText = () => { export const markdownToRemark = markdown => { const parsed = unified() .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) - .use(function() { - const { blockMethods } = this.Parser.prototype; - // Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter - blockMethods.splice(blockMethods.indexOf('yamlFrontMatter'), 1); - }) .parse(markdown); const result = unified() From 28ee67c35e271211a82ffdd9e8cb620ae02a53de Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 27 Jul 2017 08:46:53 -0400 Subject: [PATCH 60/79] eliminate unnecessary editor renders --- .../Widgets/Markdown/MarkdownControl/RawEditor/index.js | 7 +++++++ .../Widgets/Markdown/MarkdownControl/VisualEditor/index.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index e6cd1571..15bef1b6 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -14,6 +14,13 @@ export default class RawEditor extends React.Component { }; } + shouldComponentUpdate(nextProps, nextState) { + if (this.state.editorState.equals(nextState.editorState)) { + return false + } + return true; + } + handleChange = editorState => { this.setState({ editorState }); } diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 6a862551..2406ceab 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -255,6 +255,13 @@ export default class Editor extends Component { }; } + shouldComponentUpdate(nextProps, nextState) { + if (this.state.editorState.equals(nextState.editorState)) { + return false + } + return true; + } + handlePaste = (e, data, state) => { if (data.type !== 'html' || data.isShift) { return; From 750fbf5e3dcb5a1fe9bbe417d392a223d8b1ad82 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 27 Jul 2017 11:02:17 -0400 Subject: [PATCH 61/79] re-implement visual editor link button --- .../MarkdownControl/VisualEditor/index.js | 117 +++++++----------- 1 file changed, 48 insertions(+), 69 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 2406ceab..75f3072f 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -13,58 +13,8 @@ import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; - -function processUrl(url) { - if (url.match(/^(https?:\/\/|mailto:|\/)/)) { - return url; - } - if (url.match(/^[^/]+\.[^/]+/)) { - return `https://${ url }`; - } - return `/${ url }`; -} - const DEFAULT_NODE = 'paragraph'; -function schemaWithPlugins(schema, plugins) { - let nodeSpec = schema.nodeSpec; - plugins.forEach((plugin) => { - const attrs = {}; - plugin.get('fields').forEach((field) => { - attrs[field.get('name')] = { default: null }; - }); - nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, { - attrs, - group: 'block', - parseDOM: [{ - tag: 'div[data-plugin]', - getAttrs(dom) { - return JSON.parse(dom.getAttribute('data-plugin')); - }, - }], - toDOM(node) { - return ['div', { 'data-plugin': JSON.stringify(node.attrs) }, plugin.get('label')]; - }, - }); - }); - - return new Schema({ - nodes: nodeSpec, - marks: schema.markSpec, - }); -} - -function createSerializer(schema, plugins) { - const serializer = Object.create(defaultMarkdownSerializer); - plugins.forEach((plugin) => { - serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => { - const toBlock = plugin.get('toBlock'); - state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`); - }; - }); - return serializer; -} - const BLOCK_TAGS = { p: 'paragraph', li: 'list-item', @@ -109,9 +59,9 @@ const BLOCK_COMPONENTS = { 'heading-six': props =>
    {props.children}
    , 'image': props => { const data = props.node && props.node.get('data'); - const src = data && data.get('src') || props.src; - const alt = data && data.get('alt') || props.alt; - const title = data && data.get('title') || props.title; + const src = data.get('url'); + const alt = data.get('alt'); + const title = data.get('title'); return
    {alt}
    ; }, 'table': props => {props.children}
    , @@ -133,8 +83,8 @@ const NODE_COMPONENTS = { ...BLOCK_COMPONENTS, 'link': props => { const data = props.node.get('data'); - const href = data && data.get('url') || props.href; - const title = data && data.get('title') || props.title; + const href = data.get('url'); + const title = data.get('title'); return {props.children}; }, 'shortcode': props => { @@ -386,14 +336,42 @@ export default class Editor extends Component { this.setState({ editorState: resolvedState }); }; + hasLinks = () => { + return this.state.editorState.inlines.some(inline => inline.type === 'link'); + }; handleLink = () => { - let url = null; - if (!markActive(this.view.state, this.state.schema.marks.link)) { - url = prompt('Link URL:'); // eslint-disable-line no-alert + let { editorState } = this.state; + + // If the current selection contains links, clicking the "link" button + // should simply unlink them. + if (this.hasLinks()) { + editorState = editorState.transform().unwrapInline('link').apply(); } - const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null }); - command(this.view.state, this.handleAction); + + else { + const url = window.prompt('Enter the URL of the link'); + + // If nothing is entered in the URL prompt, do nothing. + if (!url) return; + + let transform = editorState.transform(); + + // If no text is selected, use the entered URL as text. + if (editorState.isCollapsed) { + transform = transform + .insertText(url) + .extend(0 - url.length); + } + + editorState = transform + .wrapInline({ type: 'link', data: { url } }) + .collapseToEnd() + .apply(); + } + + this.ref.onChange(editorState); + this.setState({ editorState }); }; handlePluginSubmit = (plugin, shortcodeData) => { @@ -414,9 +392,10 @@ export default class Editor extends Component { this.props.onMode('raw'); }; - getButtonProps = (type, isBlock) => { - const handler = isBlock ? this.handleBlockClick: this.handleMarkClick; - const isActive = isBlock ? this.hasBlock : this.hasMark; + getButtonProps = (type, opts = {}) => { + const { isBlock } = opts; + const handler = opts.handler || (isBlock ? this.handleBlockClick: this.handleMarkClick); + const isActive = opts.isActive || (isBlock ? this.hasBlock : this.hasMark); return { onAction: e => handler(e, type), active: isActive(type) }; }; @@ -437,12 +416,12 @@ export default class Editor extends Component { bold: this.getButtonProps('bold'), italic: this.getButtonProps('italic'), code: this.getButtonProps('code'), - link: this.getButtonProps('link'), - h1: this.getButtonProps('heading-one', true), - h2: this.getButtonProps('heading-two', true), - list: this.getButtonProps('bulleted-list', true), - listNumbered: this.getButtonProps('numbered-list', true), - codeBlock: this.getButtonProps('code', true), + link: this.getButtonProps('link', { handler: this.handleLink, isActive: this.hasLinks }), + h1: this.getButtonProps('heading-one', { isBlock: true }), + h2: this.getButtonProps('heading-two', { isBlock: true }), + list: this.getButtonProps('bulleted-list', { isBlock: true }), + listNumbered: this.getButtonProps('numbered-list', { isBlock: true }), + codeBlock: this.getButtonProps('code', { isBlock: true }), }} onToggleMode={this.handleToggle} plugins={plugins} From 336cab25923a92fb953db35c0508384f1b2e67e3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 27 Jul 2017 11:02:34 -0400 Subject: [PATCH 62/79] fix html whitespace truncation --- src/components/Widgets/Markdown/unified.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 3ebd6d20..85b91a14 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -589,8 +589,6 @@ export const remarkToHtml = mdast => { const result = unified() .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents() }) .use(remarkToRehype, { allowDangerousHTML: true }) - .use(rehypeRemoveEmpty) - .use(rehypeMinifyWhitespace) .runSync(mdast); const output = unified() From 1f961d36cf6e9a8e6adf835e482c38ee09fd43d2 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 27 Jul 2017 13:11:54 -0400 Subject: [PATCH 63/79] display images inserted through rte --- src/components/Widgets/Markdown/MarkdownPreview/index.js | 2 +- src/components/Widgets/Markdown/unified.js | 8 ++++---- src/index.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownPreview/index.js b/src/components/Widgets/Markdown/MarkdownPreview/index.js index c3127f6a..cb4a532d 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/index.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/index.js @@ -6,7 +6,7 @@ const MarkdownPreview = ({ value, getAsset }) => { if (value === null) { return null; } - const html = remarkToHtml(value); + const html = remarkToHtml(value, getAsset); return
    ; }; diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 85b91a14..b5f5dfc7 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -215,7 +215,7 @@ const remarkShortcodes = ({ plugins }) => { } }; -const remarkToRehypeShortcodes = ({ plugins }) => { +const remarkToRehypeShortcodes = ({ plugins, getAsset }) => { return transform; function transform(node) { @@ -225,7 +225,7 @@ const remarkToRehypeShortcodes = ({ plugins }) => { } const { shortcode, shortcodeData } = node.data; const plugin = plugins.get(shortcode); - const value = plugin.toPreview(shortcodeData); + const value = plugin.toPreview(shortcodeData, getAsset); const valueHtml = typeof value === 'string' ? value : renderToString(value); return { ...node, value: valueHtml }; } @@ -585,9 +585,9 @@ export const slateToRemark = raw => { return result; }; -export const remarkToHtml = mdast => { +export const remarkToHtml = (mdast, getAsset) => { const result = unified() - .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents() }) + .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents(), getAsset }) .use(remarkToRehype, { allowDangerousHTML: true }) .runSync(mdast); diff --git a/src/index.js b/src/index.js index 52bd7218..3884354c 100644 --- a/src/index.js +++ b/src/index.js @@ -37,7 +37,7 @@ const buildtInPlugins = [{ alt: match[1], }, toBlock: data => `![${ data.alt }](${ data.image })`, - toPreview: data => {data.alt}, + toPreview: (data, getAsset) => {data.alt}, pattern: /^!\[([^\]]+)]\(([^)]+)\)$/, fields: [{ label: 'Image', From 6377d8c73e72380f9d6a50b4eb921a67e558a74f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 27 Jul 2017 18:03:13 -0400 Subject: [PATCH 64/79] initial refactor, some bugfixes --- .../MarkdownControl/RawEditor/index.css | 4 +- .../MarkdownControl/RawEditor/index.js | 38 ++- .../VisualEditor/components.js | 49 +++ .../MarkdownControl/VisualEditor/index.css | 23 +- .../MarkdownControl/VisualEditor/index.js | 304 ++---------------- .../MarkdownControl/VisualEditor/keys.js | 67 ++++ .../MarkdownControl/VisualEditor/plugins.js | 90 ++++++ .../MarkdownControl/VisualEditor/rules.js | 30 ++ .../Widgets/Markdown/MarkdownControl/index.js | 8 +- src/components/Widgets/Markdown/unified.js | 34 +- 10 files changed, 313 insertions(+), 334 deletions(-) create mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js create mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keys.js create mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/plugins.js create mode 100644 src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css index aa2fec15..30b75f9c 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css @@ -1,6 +1,6 @@ @import "../../../../UI/theme"; -.root { +.rawWrapper { position: relative; } @@ -12,7 +12,7 @@ composes: editorControlBarSticky from "../VisualEditor/index.css"; } -.SlateEditor { +.rawEditor { position: relative; overflow: hidden; overflow-x: auto; diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 15bef1b6..6615a1f2 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Editor as SlateEditor, Plain as SlatePlain } from 'slate'; +import { Editor as Slate, Plain } from 'slate'; import { markdownToRemark, remarkToMarkdown } from '../../unified'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; @@ -8,32 +8,44 @@ import styles from './index.css'; export default class RawEditor extends React.Component { constructor(props) { super(props); + /** + * The value received is a Remark AST (MDAST), and must be stringified + * to plain text before Slate's Plain serializer can convert it to the + * Slate AST. + */ const value = remarkToMarkdown(this.props.value); this.state = { - editorState: SlatePlain.deserialize(value || ''), + editorState: Plain.deserialize(value || ''), }; } shouldComponentUpdate(nextProps, nextState) { - if (this.state.editorState.equals(nextState.editorState)) { - return false - } - return true; + return !this.state.editorState.equals(nextState.editorState); } handleChange = editorState => { this.setState({ editorState }); } + /** + * When the document value changes, serialize from Slate's AST back to plain + * text (which is Markdown), and then deserialize from that to a Remark MDAST, + * before passing up as the new value. + */ handleDocumentChange = (doc, editorState) => { - const value = SlatePlain.serialize(editorState); - const html = markdownToRemark(value); - this.props.onChange(html); + const value = Plain.serialize(editorState); + const mdast = markdownToRemark(value); + this.props.onChange(mdast); }; + /** + * If a paste contains plain text, deserialize it to Slate's AST and insert + * to the document. Selection logic (where to insert, whether to replace) is + * handled by Slate. + */ handlePaste = (e, data, state) => { if (data.text) { - const fragment = SlatePlain.deserialize(data.text).document; + const fragment = Plain.deserialize(data.text).document; return state.transform().insertFragment(fragment).apply(); } }; @@ -44,7 +56,7 @@ export default class RawEditor extends React.Component { render() { return ( -
    +
    - {props.children}, + italic: props => {props.children}, + strikethrough: props => {props.children}, + code: props => {props.children}, +}; + +export const NODE_COMPONENTS = { + paragraph: props =>

    {props.children}

    , + 'list-item': props =>
  • {props.children}
  • , + 'bulleted-list': props =>
      {props.children}
    , + 'numbered-list': props => +
      {props.children}
    , + quote: props =>
    {props.children}
    , + code: props =>
    {props.children}
    , + 'heading-one': props =>

    {props.children}

    , + 'heading-two': props =>

    {props.children}

    , + 'heading-three': props =>

    {props.children}

    , + 'heading-four': props =>

    {props.children}

    , + 'heading-five': props =>
    {props.children}
    , + 'heading-six': props =>
    {props.children}
    , + table: props => {props.children}
    , + 'table-row': props => {props.children}, + 'table-cell': props => {props.children}, + 'thematic-break': props =>
    , + 'shortcode-wrapper': props =>
    {props.children}
    , + link: props => { + const data = props.node.get('data'); + const url = data.get('url'); + const title = data.get('title'); + return {props.children}; + }, + shortcode: props => { + const { attributes, node, state: editorState } = props; + const isSelected = editorState.selection.hasFocusIn(node); + const className = cn(styles.shortcode, { [styles.shortcodeSelected]: isSelected }); + return {node.data.get('shortcode')}; + }, +}; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index b7a3aafb..6c40e7c4 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -11,7 +11,7 @@ border-color: var(--textFieldBorderColor); } -.editor { +.wrapper { position: relative; & h1, & h2, & h3 { padding: 0; @@ -49,26 +49,7 @@ } } -.dragging { } - -.shim { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: none; - border: 2px dashed #aaa; - background: rgba(0,0,0,0.2); -} - -.dragging .shim { - z-index: 1000; - display: block; - pointer-events: none; -} - -.slateEditor { +.editor { position: relative; overflow: hidden; overflow-x: auto; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 75f3072f..70c067e2 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,215 +1,37 @@ import React, { Component, PropTypes } from 'react'; -import ReactDOMServer from 'react-dom/server'; -import { Map, List, fromJS } from 'immutable'; -import { get, reduce, mapValues } from 'lodash'; -import cn from 'classnames'; -import { Editor as SlateEditor, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate'; -import EditList from 'slate-edit-list'; -import EditTable from 'slate-edit-table'; -import { markdownToRemark, remarkToMarkdown, slateToRemark, remarkToSlate, markdownToHtml, htmlToSlate } from '../../unified'; +import { get, isEmpty } from 'lodash'; +import { Editor as Slate, Raw, Block, Text } from 'slate'; +import { slateToRemark, remarkToSlate, htmlToSlate } from '../../unified'; import registry from '../../../../../lib/registry'; -import { createAssetProxy } from '../../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; +import { MARK_COMPONENTS, NODE_COMPONENTS } from './components'; +import RULES from './rules'; +import plugins, { EditListConfigured } from './plugins'; +import onKeyDown from './keys'; import styles from './index.css'; -const DEFAULT_NODE = 'paragraph'; - -const BLOCK_TAGS = { - p: 'paragraph', - li: 'list-item', - ul: 'bulleted-list', - ol: 'numbered-list', - blockquote: 'quote', - pre: 'code', - h1: 'heading-one', - h2: 'heading-two', - h3: 'heading-three', - h4: 'heading-four', - h5: 'heading-five', - h6: 'heading-six', -} - -const MARK_TAGS = { - strong: 'bold', - em: 'italic', - u: 'underline', - s: 'strikethrough', - del: 'strikethrough', - code: 'code' -} - -const BLOCK_COMPONENTS = { - 'container': props =>
    {props.children}
    , - 'paragraph': props =>

    {props.children}

    , - 'list-item': props =>
  • {props.children}
  • , - 'numbered-list': props => { - const { data } = props.node; - const start = data.get('start') || 1; - return
      {props.children}
    ; - }, - 'bulleted-list': props =>
      {props.children}
    , - 'quote': props =>
    {props.children}
    , - 'code': props =>
    {props.children}
    , - 'heading-one': props =>

    {props.children}

    , - 'heading-two': props =>

    {props.children}

    , - 'heading-three': props =>

    {props.children}

    , - 'heading-four': props =>

    {props.children}

    , - 'heading-five': props =>
    {props.children}
    , - 'heading-six': props =>
    {props.children}
    , - 'image': props => { - const data = props.node && props.node.get('data'); - const src = data.get('url'); - const alt = data.get('alt'); - const title = data.get('title'); - return
    {alt}
    ; - }, - 'table': props => {props.children}
    , - 'table-row': props => {props.children}, - 'table-cell': props => {props.children}, - 'thematic-break': props =>
    , -}; -const getShortcodeId = props => { - if (props.node) { - const result = props.node.getIn(['data', 'shortcode', 'shortcodeId']); - return result || props.node.getIn(['data', 'shortcode']).shortcodeId; - } - return null; -} - -const shortcodeStyles = {border: '2px solid black', padding: '8px', margin: '2px 0', cursor: 'pointer'}; - -const NODE_COMPONENTS = { - ...BLOCK_COMPONENTS, - 'link': props => { - const data = props.node.get('data'); - const href = data.get('url'); - const title = data.get('title'); - return {props.children}; - }, - 'shortcode': props => { - const { attributes, node, state: editorState } = props; - const { data } = node; - const isSelected = editorState.selection.hasFocusIn(node); - return ( -
    - {data.get('shortcode')} -
    - ); - }, -}; - -const MARK_COMPONENTS = { - bold: props => {props.children}, - italic: props => {props.children}, - strikethrough: props => {props.children}, - code: props => {props.children}, -}; - -const SoftBreak = (options = {}) => ({ - onKeyDown(e, data, state) { - if (data.key != 'enter') return; - if (options.shift && e.shiftKey == false) return; - - const { onlyIn, ignoreIn, closeAfter, unwrapBlocks, defaultBlock = 'paragraph' } = options; - const { type, nodes } = state.startBlock; - if (onlyIn && !onlyIn.includes(type)) return; - if (ignoreIn && ignoreIn.includes(type)) return; - - const shouldClose = nodes.last().characters.takeLast(closeAfter).every(c => c.text === '\n'); - if (closeAfter && shouldClose) { - const trimmed = state.transform().deleteBackward(closeAfter); - const unwrapped = unwrapBlocks - ? unwrapBlocks.reduce((acc, blockType) => acc.unwrapBlock(blockType), trimmed) - : trimmed; - return unwrapped.insertBlock(defaultBlock).apply(); - } - - return state.transform().insertText('\n').apply(); - } -}); - -const BackspaceCloseBlock = (options = {}) => ({ - onKeyDown(e, data, state) { - if (data.key != 'backspace') return; - - const { defaultBlock = 'paragraph', ignoreIn, onlyIn } = options; - const { startBlock } = state; - const { type } = startBlock; - - if (onlyIn && !onlyIn.includes(type)) return; - if (ignoreIn && ignoreIn.includes(type)) return; - - const characters = startBlock.getFirstText().characters; - const isEmpty = !characters || characters.isEmpty(); - - if (isEmpty) { - return state.transform().insertBlock(defaultBlock).focus().apply(); - } - } -}); - -const EditListPlugin = EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }); - -const slatePlugins = [ - SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }), - BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list', 'table', 'table-row', 'table-cell'] }), - EditListPlugin, - EditTable({ typeTable: 'table', typeRow: 'table-row', typeCell: 'table-cell' }), -]; - export default class Editor extends Component { constructor(props) { super(props); - const plugins = registry.getEditorComponents(); - const emptyRaw = { - nodes: [{ kind: 'block', type: 'paragraph', nodes: [ - { kind: 'text', ranges: [{ text: '' }] } - ]}], - }; - const remark = this.props.value && remarkToSlate(this.props.value); - const initialValue = get(remark, ['nodes', 'length']) ? remark : emptyRaw; - const editorState = SlateRaw.deserialize(initialValue, { terse: true }); + const emptyBlock = Block.create({ kind: 'block', type: 'paragraph'}); + const emptyRaw = { nodes: [emptyBlock] }; + const mdast = this.props.value && remarkToSlate(this.props.value); + const mdastHasNodes = !isEmpty(get(mdast, 'nodes')) + const editorState = Raw.deserialize(mdastHasNodes ? mdast : emptyRaw, { terse: true }); this.state = { editorState, schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, - rules: [ - /** - * If the editor is ever in an empty state, insert an empty - * paragraph block. - */ - { - match: object => object.kind === 'document', - validate: doc => { - const hasBlocks = !doc.getBlocks().isEmpty(); - return hasBlocks ? null : {}; - }, - normalize: transform => { - const block = SlateBlock.create({ - type: 'paragraph', - nodes: [SlateText.createFromString('')], - }); - const { key } = transform.state.document; - return transform.insertNodeByKey(key, 0, block).focus(); - }, - }, - ], + rules: RULES, }, - plugins, + shortcodes: registry.getEditorComponents(), }; } shouldComponentUpdate(nextProps, nextState) { - if (this.state.editorState.equals(nextState.editorState)) { - return false - } - return true; + return !this.state.editorState.equals(nextState.editorState); } handlePaste = (e, data, state) => { @@ -217,12 +39,12 @@ export default class Editor extends Component { return; } const ast = htmlToSlate(data.html); - const { document: doc } = SlateRaw.deserialize(ast, { terse: true }); + const { document: doc } = Raw.deserialize(ast, { terse: true }); return state.transform().insertFragment(doc).apply(); } handleDocumentChange = (doc, editorState) => { - const raw = SlateRaw.serialize(editorState, { terse: true }); + const raw = Raw.serialize(editorState, { terse: true }); const mdast = slateToRemark(raw); this.props.onChange(mdast); }; @@ -230,70 +52,6 @@ export default class Editor extends Component { hasMark = type => this.state.editorState.marks.some(mark => mark.type === type); hasBlock = type => this.state.editorState.blocks.some(node => node.type === type); - handleKeyDown = (e, data, state) => { - const createDefaultBlock = () => { - return SlateBlock.create({ - type: 'paragraph', - nodes: [SlateText.createFromString('')] - }); - }; - if (data.key === 'enter') { - /** - * If "Enter" is pressed while a single void block is selected, a new - * paragraph should be added above or below it, and the current selection - * should be collapsed to the start of the new paragraph. - * - * If the selected block is the first block in the document, create the - * new block above it. If not, create the new block below it. - */ - const { document: doc, selection, anchorBlock, focusBlock } = state; - const singleBlockSelected = anchorBlock === focusBlock; - if (!singleBlockSelected || !focusBlock.isVoid) return; - - e.preventDefault(); - - const focusBlockParent = doc.getParent(focusBlock.key); - const focusBlockIndex = focusBlockParent.nodes.indexOf(focusBlock); - const focusBlockIsFirstChild = focusBlockIndex === 0; - - const newBlock = createDefaultBlock(); - const newBlockIndex = focusBlockIsFirstChild ? 0 : focusBlockIndex + 1; - - return state.transform() - .insertNodeByKey(focusBlockParent.key, newBlockIndex, newBlock) - .collapseToStartOf(newBlock) - .apply(); - } - - if (data.isMod) { - - if (data.key === 'y') { - e.preventDefault(); - return state.transform().redo().focus().apply({ save: false }); - } - - if (data.key === 'z') { - e.preventDefault(); - return state.transform()[data.isShift ? 'redo' : 'undo']().focus().apply({ save: false }); - } - - const marks = { - b: 'bold', - i: 'italic', - u: 'underlined', - s: 'strikethrough', - '`': 'code', - }; - - const mark = marks[data.key]; - - if (mark) { - e.preventDefault(); - return state.transform().toggleMark(mark).apply(); - } - } - }; - handleMarkClick = (event, type) => { event.preventDefault(); const resolvedState = this.state.editorState.transform().focus().toggleMark(type).apply(); @@ -310,7 +68,7 @@ export default class Editor extends Component { // Handle everything except list buttons. if (!['bulleted-list', 'numbered-list'].includes(type)) { const isActive = this.hasBlock(type); - const transformed = transform.setBlock(isActive ? DEFAULT_NODE : type); + const transformed = transform.setBlock(isActive ? 'paragraph' : type); } // Handle the extra wrapping required for list buttons. @@ -318,16 +76,16 @@ export default class Editor extends Component { const isSameListType = editorState.blocks.some(block => { return !!doc.getClosest(block.key, parent => parent.type === type); }); - const isInList = EditListPlugin.utils.isSelectionInList(editorState); + const isInList = EditListConfigured.utils.isSelectionInList(editorState); if (isInList && isSameListType) { - EditListPlugin.transforms.unwrapList(transform, type); + EditListConfigured.transforms.unwrapList(transform, type); } else if (isInList) { const currentListType = type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list'; - EditListPlugin.transforms.unwrapList(transform, currentListType); - EditListPlugin.transforms.wrapInList(transform, type); + EditListConfigured.transforms.unwrapList(transform, currentListType); + EditListConfigured.transforms.wrapInList(transform, type); } else { - EditListPlugin.transforms.wrapInList(transform, type); + EditListConfigured.transforms.wrapInList(transform, type); } } @@ -381,7 +139,7 @@ export default class Editor extends Component { shortcodeValue: plugin.toBlock(shortcodeData.toJS()), shortcodeData, }; - const nodes = [SlateText.createFromString('')]; + const nodes = [Text.createFromString('')]; const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; const resolvedState = editorState.transform().insertBlock(block).apply(); this.ref.onChange(resolvedState); @@ -401,17 +159,15 @@ export default class Editor extends Component { render() { const { onAddAsset, onRemoveAsset, getAsset } = this.props; - const { plugins, selectionPosition, dragging } = this.state; return ( -
    +
    - this.setState({ editorState })} onDocumentChange={this.handleDocumentChange} - onKeyDown={this.handleKeyDown} + onKeyDown={onKeyDown} onPaste={this.handlePaste} ref={ref => this.ref = ref} spellCheck diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keys.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keys.js new file mode 100644 index 00000000..3a5f839d --- /dev/null +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/keys.js @@ -0,0 +1,67 @@ +import { Block, Text } from 'slate'; + +export default onKeyDown; + +function onKeyDown(e, data, state) { + const createDefaultBlock = () => { + return Block.create({ + type: 'paragraph', + nodes: [Text.createFromString('')] + }); + }; + if (data.key === 'enter') { + /** + * If "Enter" is pressed while a single void block is selected, a new + * paragraph should be added above or below it, and the current selection + * should be collapsed to the start of the new paragraph. + * + * If the selected block is the first block in the document, create the + * new block above it. If not, create the new block below it. + */ + const { document: doc, selection, anchorBlock, focusBlock } = state; + const singleBlockSelected = anchorBlock === focusBlock; + if (!singleBlockSelected || !focusBlock.isVoid) return; + + e.preventDefault(); + + const focusBlockParent = doc.getParent(focusBlock.key); + const focusBlockIndex = focusBlockParent.nodes.indexOf(focusBlock); + const focusBlockIsFirstChild = focusBlockIndex === 0; + + const newBlock = createDefaultBlock(); + const newBlockIndex = focusBlockIsFirstChild ? 0 : focusBlockIndex + 1; + + return state.transform() + .insertNodeByKey(focusBlockParent.key, newBlockIndex, newBlock) + .collapseToStartOf(newBlock) + .apply(); + } + + if (data.isMod) { + + if (data.key === 'y') { + e.preventDefault(); + return state.transform().redo().focus().apply({ save: false }); + } + + if (data.key === 'z') { + e.preventDefault(); + return state.transform()[data.isShift ? 'redo' : 'undo']().focus().apply({ save: false }); + } + + const marks = { + b: 'bold', + i: 'italic', + u: 'underlined', + s: 'strikethrough', + '`': 'code', + }; + + const mark = marks[data.key]; + + if (mark) { + e.preventDefault(); + return state.transform().toggleMark(mark).apply(); + } + } +}; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/plugins.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/plugins.js new file mode 100644 index 00000000..308a403f --- /dev/null +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/plugins.js @@ -0,0 +1,90 @@ +import EditList from 'slate-edit-list'; +import EditTable from 'slate-edit-table'; + +const SoftBreak = (options = {}) => ({ + onKeyDown(e, data, state) { + if (data.key != 'enter') return; + if (options.shift && e.shiftKey == false) return; + + const { onlyIn, ignoreIn, closeAfter, unwrapBlocks, defaultBlock = 'paragraph' } = options; + const { type, nodes } = state.startBlock; + if (onlyIn && !onlyIn.includes(type)) return; + if (ignoreIn && ignoreIn.includes(type)) return; + + const shouldClose = nodes.last().characters.takeLast(closeAfter).every(c => c.text === '\n'); + if (closeAfter && shouldClose) { + const trimmed = state.transform().deleteBackward(closeAfter); + const unwrapped = unwrapBlocks + ? unwrapBlocks.reduce((acc, blockType) => acc.unwrapBlock(blockType), trimmed) + : trimmed; + return unwrapped.insertBlock(defaultBlock).apply(); + } + + return state.transform().insertText('\n').apply(); + } +}); + +const SoftBreakOpts = { + onlyIn: ['quote', 'code'], + closeAfter: 1 +}; + +export const SoftBreakConfigured = SoftBreak(SoftBreakOpts); + +const BackspaceCloseBlock = (options = {}) => ({ + onKeyDown(e, data, state) { + if (data.key != 'backspace') return; + + const { defaultBlock = 'paragraph', ignoreIn, onlyIn } = options; + const { startBlock } = state; + const { type } = startBlock; + + if (onlyIn && !onlyIn.includes(type)) return; + if (ignoreIn && ignoreIn.includes(type)) return; + + const characters = startBlock.getFirstText().characters; + const isEmpty = !characters || characters.isEmpty(); + + if (isEmpty) { + return state.transform().insertBlock(defaultBlock).focus().apply(); + } + } +}); + +const BackspaceCloseBlockOpts = { + ignoreIn: [ + 'paragraph', + 'list-item', + 'bulleted-list', + 'numbered-list', + 'table', + 'table-row', + 'table-cell', + ], +}; + +export const BackspaceCloseBlockConfigured = BackspaceCloseBlock(BackspaceCloseBlockOpts); + +const EditListOpts = { + types: ['bulleted-list', 'numbered-list'], + typeItem: 'list-item', +}; + +export const EditListConfigured = EditList(EditListOpts); + +const EditTableOpts = { + typeTable: 'table', + typeRow: 'table-row', + typeCell: 'table-cell', +}; + +export const EditTableConfigured = EditTable(EditTableOpts); + +const plugins = [ + SoftBreakConfigured, + BackspaceCloseBlockConfigured, + EditListConfigured, + EditTableConfigured, +]; + +export default plugins; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js new file mode 100644 index 00000000..261f9e43 --- /dev/null +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js @@ -0,0 +1,30 @@ +import { Block, Text } from 'slate'; + +/** + * Rules are used to validate the editor state each time it changes, to ensure + * it is never rendered in an undesirable state. + */ + +/** + * If the editor is ever in an empty state, insert an empty + * paragraph block. + */ +const enforceNeverEmpty = { + match: object => object.kind === 'document', + validate: doc => { + const hasBlocks = !doc.getBlocks().isEmpty(); + return hasBlocks ? null : {}; + }, + normalize: transform => { + const block = Block.create({ + type: 'paragraph', + nodes: [Text.createFromString('')], + }); + const { key } = transform.state.document; + return transform.insertNodeByKey(key, 0, block).focus(); + }, +}; + +const rules = [ enforceNeverEmpty ]; + +export default rules; diff --git a/src/components/Widgets/Markdown/MarkdownControl/index.js b/src/components/Widgets/Markdown/MarkdownControl/index.js index 6ed3df10..e2063020 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/index.js @@ -8,10 +8,10 @@ import { StickyContainer } from '../../../UI/Sticky/Sticky'; const MODE_STORAGE_KEY = 'cms.md-mode'; /** - * Slate can serialize to html, but we persist the value as markdown. Serializing - * the html to markdown on every keystroke is a big perf hit, so we'll register - * functions to perform those actions only when necessary, such as after loading - * and before persisting. + * The markdown field value is persisted as a markdown string, but stringifying + * on every keystroke is a big perf hit, so we'll register functions to perform + * those actions only when necessary, such as after loading and before + * persisting. */ registry.registerWidgetValueSerializer('markdown', { serialize: remarkToMarkdown, diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index b5f5dfc7..60ea8abd 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -392,8 +392,11 @@ const remarkToSlatePlugin = () => { if (node.type === 'linkReference') { const definition = getDefinition(node.identifier); - const { title, url } = definition; - const data = { title, url }; + const data = {}; + if (definition) { + data.title = definition.title; + data.url = definition.url; + } return { kind: 'inline', type: typeMap['link'], data, nodes }; } @@ -405,8 +408,11 @@ const remarkToSlatePlugin = () => { if (node.type === 'imageReference') { const definition = getDefinition(node.identifier); - const { title, url } = definition; - const data = { title, url }; + const data = {}; + if (definition) { + data.title = definition.title; + data.url = definition.url; + } return { kind: 'block', type: typeMap['image'], data }; } }; @@ -536,6 +542,10 @@ export const slateToRemark = raw => { return u('html', { data: node.data }, node.data.shortcodeValue); } + if (node.type === 'shortcode-wrapper') { + return u('paragraph', children); + } + if (node.type.startsWith('heading')) { const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; const depth = node.type.split('-')[1]; @@ -597,22 +607,6 @@ export const remarkToHtml = (mdast, getAsset) => { return output } -export const markdownToHtml = markdown => { - // Parse shortcodes from the raw markdown rather than via Unified plugin. - // This ensures against conflicts between shortcode syntax and Unified - // parsing rules. - const markdownWithParsedShortcodes = parseShortcodesFromMarkdown(markdown); - const result = unified() - .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) - .use(remarkToRehype, { allowDangerousHTML: true }) - .use(rehypeRemoveEmpty) - .use(rehypeMinifyWhitespace) - .use(rehypeToHtml, { allowDangerousHTML: true }) - .processSync(markdownWithParsedShortcodes) - .contents; - return result; -} - export const htmlToSlate = html => { const hast = unified() .use(htmlToRehype, { fragment: true }) From be7385de292f9dbb87eb1fc7cb2db11f1c2175f3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 28 Jul 2017 18:22:05 -0400 Subject: [PATCH 65/79] refactor remark-shortcodes plugin --- package.json | 1 + src/components/Widgets/Markdown/unified.js | 196 +++++++++------------ 2 files changed, 81 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index 67d4629b..8eb42be8 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "markup-it": "^2.0.0", "material-design-icons": "^3.0.1", "mdast-util-definitions": "^1.2.2", + "mdast-util-to-string": "^1.0.4", "moment": "^2.11.2", "netlify-auth-js": "^0.5.5", "normalize.css": "^4.2.0", diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 60ea8abd..e63c1342 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -1,10 +1,11 @@ -import { get, has, find, isEmpty } from 'lodash'; +import { get, has, find, isEmpty, every, map } from 'lodash'; import { renderToString } from 'react-dom/server'; import unified from 'unified'; import u from 'unist-builder'; import markdownToRemarkPlugin from 'remark-parse'; import remarkToMarkdownPlugin from 'remark-stringify'; import mdastDefinitions from 'mdast-util-definitions'; +import mdastToString from 'mdast-util-to-string'; import modifyChildren from 'unist-util-modify-children'; import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; @@ -101,41 +102,6 @@ const rehypePaperEmoji = () => { return transform; }; -const rehypeShortcodes = () => { - const plugins = registry.getEditorComponents(); - const transform = node => { - const { properties } = node; - - // Convert this logic into a parseShortcodeDataFromHtml shared function, as - // this is also used in the visual editor serializer - const dataPrefix = `data${capitalize(shortcodeAttributePrefix)}`; - const pluginId = properties && properties[dataPrefix]; - const plugin = plugins.get(pluginId); - - if (plugin) { - const data = reduce(properties, (acc, value, key) => { - if (key.startsWith(dataPrefix)) { - const dataKey = key.slice(dataPrefix.length).toLowerCase(); - if (dataKey) { - acc[dataKey] = value; - } - } - return acc; - }, {}); - - node.data = node.data || {}; - node.data[shortcodeAttributePrefix] = true; - - return hastFromString(node, plugin.toBlock(data)); - } - - node.children = node.children ? node.children.map(transform) : node.children; - - return node; - }; - return transform; -} - /** * Rewrite the remark-stringify text visitor to simply return the text value, * without encoding or escaping any characters. This means we're completely @@ -147,71 +113,92 @@ function remarkPrecompileShortcodes() { visitors.text = node => node.value; }; + +/** + * Parse shortcodes from an MDAST. + * + * Shortcodes are plain text, and must be the lone content of a paragraph. The + * paragraph must also be a direct child of the root node. When a shortcode is + * found, we just need to add data to the node so the shortcode can be + * identified and processed when serializing to a new format. The paragraph + * containing the node is also recreated to ensure normalization. + */ const remarkShortcodes = ({ plugins }) => { return transform; - function transform(node) { - if (node.children) { - node.children = node.children.reduce(reducer, []); - } - return node; + /** + * Map over children of the root node and convert any found shortcode nodes. + */ + function transform(root) { + const transformedChildren = map(root.children, processShortcodes); + return { ...root, children: transformedChildren }; + } - function reducer(newChildren, childNode) { - if (!['text', 'html'].includes(childNode.type)) { - const processedNode = childNode.children ? transform(childNode) : childNode; - newChildren.push(processedNode); - return newChildren; - } + /** + * Mapping function to transform nodes that contain shortcodes. + */ + function processShortcodes(node) { + /** + * If the node is not eligible to contain a shortcode, return the original + * node unchanged. + */ + if (!nodeMayContainShortcode(node)) return node; - const text = childNode.value; - let lastPlugin; - let match; - const plugin = plugins.find(p => { - match = text.match(p.pattern); - return match; + /** + * Combine the text values of all children to a single string, then + * check that string for a shortcode pattern match. + */ + const text = mdastToString(node); + const { plugin, match } = matchTextToPlugin(text); + + /** + * If a matching shortcode plugin is found, return a new node with shortcode + * data included. Otherwise, return the original node. + */ + return plugin ? createShortcodeNode(text, plugin, match) : node; + }; + + /** + * Ensure that the node and it's children are acceptable types to contain + * shortcodes. Currently, only a paragraph containing text and/or html nodes + * may contain shortcodes. + */ + function nodeMayContainShortcode(node) { + const validNodeTypes = ['paragraph']; + const validChildTypes = ['text', 'html']; + + if (validNodeTypes.includes(node.type)) { + return every(node.children, child => { + return validChildTypes.includes(child.type); }); - if (!plugin) { - newChildren.push(childNode); - return newChildren; - } - const matchValue = match[0]; - const matchLength = matchValue.length; - const matchAll = matchLength === text.length; - - if (matchAll) { - const shortcodeNode = createShortcodeNode(text, plugin, match); - newChildren.push(shortcodeNode); - return newChildren; - } - - const tempChildren = []; - const matchAtStart = match.index === 0; - const matchAtEnd = match.index + matchLength === text.length; - - if (!matchAtStart) { - const textBeforeMatch = text.slice(0, match.index); - const result = reducer([], { type: 'text', value: textBeforeMatch }); - tempChildren.push(...result); - } - - const matchNode = createShortcodeNode(matchValue, plugin, match); - tempChildren.push(matchNode); - - if (!matchAtEnd) { - const textAfterMatch = text.slice(match.index + matchLength); - const result = reducer([], { type: 'text', value: textAfterMatch }); - tempChildren.push(...result); - } - - newChildren.push(...tempChildren); - return newChildren; } + } - function createShortcodeNode(text, plugin, match) { - const shortcode = plugin.id; - const shortcodeData = plugin.fromBlock(match); - return { type: 'html', value: text, data: { shortcode, shortcodeData } }; - } + /** + * Return the plugin and RegExp.match result from the first plugin with a + * pattern that matches the given text. + */ + function matchTextToPlugin(text) { + let match; + const plugin = plugins.find(p => { + match = text.match(p.pattern); + return !!match; + }); + return { plugin, match }; + } + + /** + * Create a new node with shortcode data included. Use an 'html' node instead + * of a 'text' node as the child to ensure the node content is not parsed by + * Remark or Rehype. Include the child as an array because an MDAST paragraph + * node must have it's children in an array. + */ + function createShortcodeNode(text, plugin, match) { + const shortcode = plugin.id; + const shortcodeData = plugin.fromBlock(match); + const data = { shortcode, shortcodeData }; + const textNode = u('html', text); + return u('paragraph', { data }, [textNode]); } }; @@ -231,28 +218,6 @@ const remarkToRehypeShortcodes = ({ plugins, getAsset }) => { } }; -const parseShortcodesFromMarkdown = markdown => { - const plugins = registry.getEditorComponents(); - const markdownLines = markdown.split('\n'); - const markdownLinesParsed = plugins.reduce((lines, plugin) => { - const result = lines.map(line => { - return line.replace(plugin.pattern, (...match) => { - const data = plugin.fromBlock(match); - const preview = plugin.toPreview(data); - const html = typeof preview === 'string' ? preview : ReactDOMServer.renderToStaticMarkup(preview); - const dataAttrs = reduce(data, (attrs, val, key) => { - attrs.push(`data-${shortcodeAttributePrefix}-${key}="${val}"`); - return attrs; - }, [`data-${shortcodeAttributePrefix}="${plugin.id}"`]); - const result = `
    ${html}
    `; - return result; - }); - }); - return result; - }, markdownLines); - return markdownLinesParsed.join('\n'); -}; - const remarkToSlatePlugin = () => { const typeMap = { paragraph: 'paragraph', @@ -616,7 +581,6 @@ export const htmlToSlate = html => { .use(rehypeRemoveEmpty) .use(rehypeMinifyWhitespace) .use(rehypePaperEmoji) - .use(rehypeShortcodes) .use(rehypeToRemark) .use(remarkNestedList) .use(remarkToSlatePlugin) From 9174e56414e24a6a651468ddf42a1375fae5d0f3 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Sat, 29 Jul 2017 21:58:20 -0400 Subject: [PATCH 66/79] refactor remarkToRehypeShortcodes --- src/components/Widgets/Markdown/unified.js | 42 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index e63c1342..bade1642 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -202,19 +202,49 @@ const remarkShortcodes = ({ plugins }) => { } }; + +/** + * This plugin doesn't actually transform Remark (MDAST) nodes to Rehype + * (HAST) nodes, but rather, it prepares an MDAST shortcode node for HAST + * conversion by replacing the shortcode text with stringified HTML for + * previewing the shortcode output. + */ const remarkToRehypeShortcodes = ({ plugins, getAsset }) => { return transform; - function transform(node) { - const children = node.children ? node.children.map(transform) : node.children; - if (!has(node, ['data', 'shortcode'])) { - return { ...node, children }; - } + function transform(root) { + const transformedChildren = map(root.children, processShortcodes); + return { ...root, children: transformedChildren }; + } + + /** + * Mapping function to transform nodes that contain shortcodes. + */ + function processShortcodes(node) { + /** + * If the node doesn't contain shortcode data, return the original node. + */ + if (!has(node, ['data', 'shortcode'])) return node; + + /** + * Get shortcode data from the node, and retrieve the matching plugin by + * key. + */ const { shortcode, shortcodeData } = node.data; const plugin = plugins.get(shortcode); + + /** + * Run the shortcode plugin's `toPreview` method, which will return either + * an HTML string or a React component. If a React component is returned, + * render it to an HTML string. + */ const value = plugin.toPreview(shortcodeData, getAsset); const valueHtml = typeof value === 'string' ? value : renderToString(value); - return { ...node, value: valueHtml }; + + /** + * Return a new 'html' type node containing the shortcode preview markup. + */ + return u('html', valueHtml); } }; From ca60a6b8c9b2c87684d72d211cae18fccd6256cf Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Sat, 29 Jul 2017 23:03:03 -0400 Subject: [PATCH 67/79] update Slate shortcode handling to include paragraph --- src/components/Widgets/Markdown/unified.js | 31 +++++++++------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index bade1642..2683850b 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -20,8 +20,6 @@ import hastFromString from 'hast-util-from-string'; import hastToMdastHandlerAll from 'hast-util-to-mdast/all'; import { reduce, capitalize } from 'lodash'; -const shortcodeAttributePrefix = 'ncp'; - /** * Remove empty nodes, including the top level parents of deeply nested empty nodes. */ @@ -306,19 +304,18 @@ const remarkToSlatePlugin = () => { return { nodes }; } + /** + * Convert MDAST shortcode nodes to Slate 'shortcode' type nodes. + */ + if (get(node, ['data', 'shortcode'])) { + const { data } = node; + const nodes = [ toTextNode('') ]; + return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; + } + // Process raw html as text, since it's valid markdown if (['text', 'html'].includes(node.type)) { - const { value, data } = node; - const shortcode = get(data, 'shortcode'); - if (shortcode) { - const isBlock = parent.type === 'paragraph' && siblings.length === 1; - data.shortcodeValue = value; - - if (isBlock) { - return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes: [toTextNode('')] }; - } - } - return toTextNode(value, data); + return toTextNode(node.value, node.data); } if (node.type === 'inlineCode') { @@ -534,11 +531,9 @@ export const slateToRemark = raw => { } if (node.type === 'shortcode') { - return u('html', { data: node.data }, node.data.shortcodeValue); - } - - if (node.type === 'shortcode-wrapper') { - return u('paragraph', children); + const { data } = node; + const textNode = u('html', data.shortcodeValue); + return u('paragraph', { data }, [ textNode ]); } if (node.type.startsWith('heading')) { From 1d654662d28f66814a1f4345c80a6216986af364 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Sun, 30 Jul 2017 11:09:35 -0400 Subject: [PATCH 68/79] improve shortcode handling in visual editor --- .../VisualEditor/components.js | 9 +----- .../MarkdownControl/VisualEditor/index.css | 2 +- .../MarkdownControl/VisualEditor/index.js | 10 +++---- .../MarkdownControl/VisualEditor/rules.js | 17 ++++++++++- src/components/Widgets/Markdown/unified.js | 30 +++++++++++++------ 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js index 3a06e026..4d891424 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js @@ -33,17 +33,10 @@ export const NODE_COMPONENTS = { 'table-row': props => {props.children}, 'table-cell': props => {props.children}, 'thematic-break': props =>
    , - 'shortcode-wrapper': props =>
    {props.children}
    , - link: props => { - const data = props.node.get('data'); - const url = data.get('url'); - const title = data.get('title'); - return {props.children}; - }, shortcode: props => { const { attributes, node, state: editorState } = props; const isSelected = editorState.selection.hasFocusIn(node); const className = cn(styles.shortcode, { [styles.shortcodeSelected]: isSelected }); - return {node.data.get('shortcode')}; + return
    {node.data.get('shortcode')}
    ; }, }; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index 6c40e7c4..6a13efba 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -96,7 +96,7 @@ .shortcode { border: 2px solid black; padding: 8px; - margin: 2px 0; + margin: 16px 0; cursor: pointer; } diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 70c067e2..ebdaf6b1 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -26,7 +26,7 @@ export default class Editor extends Component { marks: MARK_COMPONENTS, rules: RULES, }, - shortcodes: registry.getEditorComponents(), + shortcodePlugins: registry.getEditorComponents(), }; } @@ -45,7 +45,8 @@ export default class Editor extends Component { handleDocumentChange = (doc, editorState) => { const raw = Raw.serialize(editorState, { terse: true }); - const mdast = slateToRemark(raw); + const plugins = this.state.shortcodePlugins; + const mdast = slateToRemark(raw, plugins); this.props.onChange(mdast); }; @@ -136,12 +137,11 @@ export default class Editor extends Component { const { editorState } = this.state; const data = { shortcode: plugin.id, - shortcodeValue: plugin.toBlock(shortcodeData.toJS()), shortcodeData, }; const nodes = [Text.createFromString('')]; const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; - const resolvedState = editorState.transform().insertBlock(block).apply(); + const resolvedState = editorState.transform().insertBlock(block).focus().apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; @@ -180,7 +180,7 @@ export default class Editor extends Component { codeBlock: this.getButtonProps('code', { isBlock: true }), }} onToggleMode={this.handleToggle} - plugins={this.state.shortcodes} + plugins={this.state.shortcodePlugins} onSubmit={this.handlePluginSubmit} onAddAsset={onAddAsset} onRemoveAsset={onRemoveAsset} diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js index 261f9e43..a367b875 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js @@ -25,6 +25,21 @@ const enforceNeverEmpty = { }, }; -const rules = [ enforceNeverEmpty ]; +/** + * Ensure that shortcodes are children of the root node. + */ +const shortcodesAtRoot = { + match: object => object.kind === 'document', + validate: doc => { + return doc.findDescendant(node => { + return node.type === 'shortcode' && doc.getParent(node.key).key !== doc.key; + }); + }, + normalize: (transform, doc, node) => { + return transform.unwrapNodeByKey(node.key); + }, +}; + +const rules = [ enforceNeverEmpty, shortcodesAtRoot ]; export default rules; diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 2683850b..6a5071ba 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -143,17 +143,18 @@ const remarkShortcodes = ({ plugins }) => { if (!nodeMayContainShortcode(node)) return node; /** - * Combine the text values of all children to a single string, then - * check that string for a shortcode pattern match. + * Combine the text values of all children to a single string, check the + * string for a shortcode pattern match, and validate the match. */ - const text = mdastToString(node); + const text = mdastToString(node).trim(); const { plugin, match } = matchTextToPlugin(text); + const matchIsValid = validateMatch(text, match); /** - * If a matching shortcode plugin is found, return a new node with shortcode - * data included. Otherwise, return the original node. + * If a valid match is found, return a new node with shortcode data + * included. Otherwise, return the original node. */ - return plugin ? createShortcodeNode(text, plugin, match) : node; + return matchIsValid ? createShortcodeNode(text, plugin, match) : node; }; /** @@ -185,6 +186,13 @@ const remarkShortcodes = ({ plugins }) => { return { plugin, match }; } + /** + * A match is only valid if it takes up the entire paragraph. + */ + function validateMatch(text, match) { + return match && match[0].length === text.length; + } + /** * Create a new node with shortcode data included. Use an 'html' node instead * of a 'text' node as the child to ensure the node content is not parsed by @@ -242,7 +250,9 @@ const remarkToRehypeShortcodes = ({ plugins, getAsset }) => { /** * Return a new 'html' type node containing the shortcode preview markup. */ - return u('html', valueHtml); + const textNode = u('html', valueHtml); + const children = [ textNode ]; + return { ...node, children }; } }; @@ -471,7 +481,7 @@ export const remarkToSlate = mdast => { return result; }; -export const slateToRemark = raw => { +export const slateToRemark = (raw, shortcodePlugins) => { const typeMap = { 'paragraph': 'paragraph', 'heading-one': 'heading', @@ -532,7 +542,9 @@ export const slateToRemark = raw => { if (node.type === 'shortcode') { const { data } = node; - const textNode = u('html', data.shortcodeValue); + const plugin = shortcodePlugins.get(data.shortcode); + const text = plugin.toBlock(data.shortcodeData); + const textNode = u('html', text); return u('paragraph', { data }, [ textNode ]); } From dd51f6365c04675dcc2501a97420605ee029823f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 31 Jul 2017 11:37:46 -0400 Subject: [PATCH 69/79] improve visual editor content styling --- .../VisualEditor/components.js | 6 ++ .../MarkdownControl/VisualEditor/index.css | 102 +++++++++++------- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js index 4d891424..ad8945d8 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js @@ -33,6 +33,12 @@ export const NODE_COMPONENTS = { 'table-row': props => {props.children}, 'table-cell': props => {props.children}, 'thematic-break': props =>
    , + link: props => { + const data = props.node.get('data'); + const url = data.get('url'); + const title = data.get('title'); + return {props.children}; + }, shortcode: props => { const { attributes, node, state: editorState } = props; const isSelected = editorState.selection.hasFocusIn(node); diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index 6a13efba..396f7302 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -13,40 +13,6 @@ .wrapper { position: relative; - & h1, & h2, & h3 { - padding: 0; - color: #7c8382; - text-decoration: none; - border-bottom: none; - margin-bottom: 20px; - line-height: 1.45; - } - & h1 { - font-size: 2.5rem; - } - & h2 { - font-size: 2rem; - } - & h3 { - font-size: 1.8rem; - } - & p { - margin-top: 20px; - margin-bottom: 20px; - } - & hr { - border: 1px solid; - margin-bottom: 20px; - } - & li > p { - margin: 0; - } - & div[data-plugin] { - background: #fff; - border: 1px solid #aaa; - padding: 10px; - margin-bottom: 20px; - } } .editor { @@ -56,6 +22,58 @@ min-height: var(--richTextEditorMinHeight); font-family: var(--fontFamily); + & h1 { + font-size: 32px; + margin-top: 16px; + } + + & h2 { + font-size: 24px; + margin-top: 12px; + } + + & h3 { + font-size: 20px; + margin-top: 8px; + } + + & h4 { + font-size: 18px; + margin-top: 8px; + } + + & h5, + & h6 { + font-size: 16px; + margin-top: 8px; + } + + & h1, & h2, & h3, & h4, & h5, & h6 { + font-weight: 700; + } + + & p, + & pre, + & blockquote, + & ul, + & ol { + margin-top: 16px; + margin-bottom: 16px; + } + + & a { + text-decoration: underline; + } + + & hr { + border: 1px solid; + margin-bottom: 16px; + } + + & li > p { + margin: 0; + } + & ul, & ol { padding-left: 30px; @@ -75,10 +93,18 @@ padding: 10px; } + & code { + background-color: var(--backgroundColorShaded); + border-radius: var(--borderRadius); + padding: 0 2px; + font-size: 85%; + } + & blockquote { - padding-left: 1em; - border-left: 3px solid #eee; - margin-left: 0; margin-right: 0; + padding-left: 16px; + border-left: 3px solid var(--backgroundColorShaded); + margin-left: 0; + margin-right: 0; } & table { @@ -96,7 +122,7 @@ .shortcode { border: 2px solid black; padding: 8px; - margin: 16px 0; + margin: 2px 0; cursor: pointer; } From 9dcda7b0b9054ff4f0ce05d9baa43455be141a86 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 31 Jul 2017 12:58:45 -0400 Subject: [PATCH 70/79] organize serializers --- .../MarkdownControl/RawEditor/index.js | 2 +- .../MarkdownControl/VisualEditor/index.js | 2 +- .../Widgets/Markdown/MarkdownControl/index.js | 2 +- .../Widgets/Markdown/MarkdownPreview/index.js | 2 +- .../Widgets/Markdown/serializers/index.js | 203 ++++++ .../serializers/rehype-paper-emoji.js | 15 + .../serializers/rehype-remove-empty.js | 32 + .../serializers/remark-images-to-text.js | 18 + .../serializers/remark-nested-list.js | 33 + .../serializers/remark-rehype-shortcodes.js | 50 ++ .../Markdown/serializers/remark-shortcodes.js | 99 +++ .../Markdown/serializers/remark-slate.js | 172 +++++ src/components/Widgets/Markdown/unified.js | 627 ------------------ 13 files changed, 626 insertions(+), 631 deletions(-) create mode 100644 src/components/Widgets/Markdown/serializers/index.js create mode 100644 src/components/Widgets/Markdown/serializers/rehype-paper-emoji.js create mode 100644 src/components/Widgets/Markdown/serializers/rehype-remove-empty.js create mode 100644 src/components/Widgets/Markdown/serializers/remark-images-to-text.js create mode 100644 src/components/Widgets/Markdown/serializers/remark-nested-list.js create mode 100644 src/components/Widgets/Markdown/serializers/remark-rehype-shortcodes.js create mode 100644 src/components/Widgets/Markdown/serializers/remark-shortcodes.js create mode 100644 src/components/Widgets/Markdown/serializers/remark-slate.js delete mode 100644 src/components/Widgets/Markdown/unified.js diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 6615a1f2..1ef4ae0f 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { Editor as Slate, Plain } from 'slate'; -import { markdownToRemark, remarkToMarkdown } from '../../unified'; +import { markdownToRemark, remarkToMarkdown } from '../../serializers'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index ebdaf6b1..7d8116fb 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,7 +1,7 @@ import React, { Component, PropTypes } from 'react'; import { get, isEmpty } from 'lodash'; import { Editor as Slate, Raw, Block, Text } from 'slate'; -import { slateToRemark, remarkToSlate, htmlToSlate } from '../../unified'; +import { slateToRemark, remarkToSlate, htmlToSlate } from '../../serializers'; import registry from '../../../../../lib/registry'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; diff --git a/src/components/Widgets/Markdown/MarkdownControl/index.js b/src/components/Widgets/Markdown/MarkdownControl/index.js index e2063020..423bafd8 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/index.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import registry from '../../../../lib/registry'; -import { markdownToRemark, remarkToMarkdown } from '../unified'; +import { markdownToRemark, remarkToMarkdown } from '../serializers' import RawEditor from './RawEditor'; import VisualEditor from './VisualEditor'; import { StickyContainer } from '../../../UI/Sticky/Sticky'; diff --git a/src/components/Widgets/Markdown/MarkdownPreview/index.js b/src/components/Widgets/Markdown/MarkdownPreview/index.js index cb4a532d..461b0b40 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/index.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/index.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { remarkToHtml } from '../unified'; +import { remarkToHtml } from '../serializers'; import previewStyle from '../../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { diff --git a/src/components/Widgets/Markdown/serializers/index.js b/src/components/Widgets/Markdown/serializers/index.js new file mode 100644 index 00000000..b2b596bc --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/index.js @@ -0,0 +1,203 @@ +import { get, isEmpty, reduce } from 'lodash'; +import unified from 'unified'; +import u from 'unist-builder'; +import markdownToRemarkPlugin from 'remark-parse'; +import remarkToMarkdownPlugin from 'remark-stringify'; +import remarkToRehype from 'remark-rehype'; +import rehypeToHtml from 'rehype-stringify'; +import htmlToRehype from 'rehype-parse'; +import rehypeToRemark from 'rehype-remark'; +import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; +import remarkToRehypeShortcodes from './remark-rehype-shortcodes'; +import rehypeRemoveEmpty from './rehype-remove-empty'; +import rehypePaperEmoji from './rehype-paper-emoji'; +import remarkNestedList from './remark-nested-list'; +import remarkToSlatePlugin from './remark-slate'; +import remarkImagesToText from './remark-images-to-text'; +import remarkShortcodes from './remark-shortcodes'; +import registry from '../../../../lib/registry'; + +export const remarkToHtml = (mdast, getAsset) => { + const result = unified() + .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents(), getAsset }) + .use(remarkToRehype, { allowDangerousHTML: true }) + .runSync(mdast); + + const output = unified() + .use(rehypeToHtml, { allowDangerousHTML: true, allowDangerousCharacters: true }) + .stringify(result); + return output +} + +export const htmlToSlate = html => { + const hast = unified() + .use(htmlToRehype, { fragment: true }) + .parse(html); + + const result = unified() + .use(rehypeRemoveEmpty) + .use(rehypeMinifyWhitespace) + .use(rehypePaperEmoji) + .use(rehypeToRemark) + .use(remarkNestedList) + .use(remarkToSlatePlugin) + .runSync(hast); + + return result; +}; + +export const markdownToRemark = markdown => { + const parsed = unified() + .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) + .parse(markdown); + + const result = unified() + .use(remarkImagesToText) + .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) + .runSync(parsed); + + return result; +}; + +export const 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 + * trusting the markdown that we receive. + */ + function remarkAllowAllText() { + const Compiler = this.Compiler; + const visitors = Compiler.prototype.visitors; + visitors.text = node => node.value; + }; + + const mdast = obj || u('root', [u('paragraph', [u('text', '')])]); + const result = unified() + .use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true }) + .use(remarkAllowAllText) + .stringify(mdast); + return result; +}; + +export const remarkToSlate = mdast => { + const result = unified() + .use(remarkToSlatePlugin) + .runSync(mdast); + return result; +}; + +export const slateToRemark = (raw, shortcodePlugins) => { + const typeMap = { + 'paragraph': 'paragraph', + 'heading-one': 'heading', + 'heading-two': 'heading', + 'heading-three': 'heading', + 'heading-four': 'heading', + 'heading-five': 'heading', + 'heading-six': 'heading', + 'quote': 'blockquote', + 'code': 'code', + 'numbered-list': 'list', + 'bulleted-list': 'list', + 'list-item': 'listItem', + 'table': 'table', + 'table-row': 'tableRow', + 'table-cell': 'tableCell', + 'thematic-break': 'thematicBreak', + 'link': 'link', + 'image': 'image', + }; + const markMap = { + bold: 'strong', + italic: 'emphasis', + strikethrough: 'delete', + code: 'inlineCode', + }; + const transform = node => { + const children = isEmpty(node.nodes) ? node.nodes : node.nodes.reduce((acc, childNode) => { + if (childNode.kind !== 'text') { + acc.push(transform(childNode)); + return acc; + } + if (childNode.ranges) { + childNode.ranges.forEach(range => { + const { marks = [], text } = range; + const markTypes = marks.map(mark => markMap[mark.type]); + if (markTypes.includes('inlineCode')) { + acc.push(u('inlineCode', text)); + } else { + const textNode = u('html', text); + const nestedText = !markTypes.length ? textNode : markTypes.reduce((acc, markType) => { + const nested = u(markType, [acc]); + return nested; + }, textNode); + acc.push(nestedText); + } + }); + } else { + + acc.push(u('html', childNode.text)); + } + return acc; + }, []); + + if (node.type === 'root') { + return u('root', children); + } + + if (node.type === 'shortcode') { + const { data } = node; + const plugin = shortcodePlugins.get(data.shortcode); + const text = plugin.toBlock(data.shortcodeData); + const textNode = u('html', text); + return u('paragraph', { data }, [ textNode ]); + } + + if (node.type.startsWith('heading')) { + const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; + const depth = node.type.split('-')[1]; + const props = { depth: depths[depth] }; + return u(typeMap[node.type], props, children); + } + + if (['paragraph', 'quote', 'list-item', 'table', 'table-row', 'table-cell'].includes(node.type)) { + return u(typeMap[node.type], children); + } + + if (node.type === 'code') { + const value = get(node.nodes, [0, 'text']); + const props = { lang: get(node.data, 'lang') }; + return u(typeMap[node.type], props, value); + } + + if (['numbered-list', 'bulleted-list'].includes(node.type)) { + const ordered = node.type === 'numbered-list'; + const props = { ordered, start: get(node.data, 'start') || 1 }; + return u(typeMap[node.type], props, children); + } + + if (node.type === 'thematic-break') { + return u(typeMap[node.type]); + } + + if (node.type === 'link') { + const data = get(node, 'data', {}); + const { url, title } = data; + return u(typeMap[node.type], data, children); + } + + if (node.type === 'image') { + const data = get(node, 'data', {}); + const { url, title, alt } = data; + return u(typeMap[node.type], data); + } + } + raw.type = 'root'; + const mdast = transform(raw); + + const result = unified() + .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) + .runSync(mdast); + + return result; +}; diff --git a/src/components/Widgets/Markdown/serializers/rehype-paper-emoji.js b/src/components/Widgets/Markdown/serializers/rehype-paper-emoji.js new file mode 100644 index 00000000..873c6b79 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/rehype-paper-emoji.js @@ -0,0 +1,15 @@ +/** + * Dropbox Paper outputs emoji characters as images, and stores the actual + * emoji character in a `data-emoji-ch` attribute on the image. This plugin + * replaces the images with the emoji characters. + */ +export default function rehypePaperEmoji() { + const 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; +} diff --git a/src/components/Widgets/Markdown/serializers/rehype-remove-empty.js b/src/components/Widgets/Markdown/serializers/rehype-remove-empty.js new file mode 100644 index 00000000..4d59b6a5 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/rehype-remove-empty.js @@ -0,0 +1,32 @@ +import { find, capitalize } from 'lodash'; + +/** + * Remove empty nodes, including the top level parents of deeply nested empty nodes. + */ +export default function rehypeRemoveEmpty() { + const isVoidElement = node => ['img', 'hr', 'br'].includes(node.tagName); + const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value; + const isShortcode = node => node.properties && node.properties[`data${capitalize(shortcodeAttributePrefix)}`]; + const isNonEmptyNode = node => { + return isVoidElement(node) + || isNonEmptyLeaf(node) + || isShortcode(node) + || find(node.children, isNonEmptyNode); + }; + + const transform = node => { + if (isVoidElement(node) || isNonEmptyLeaf(node) || isShortcode(node)) { + return node; + } + if (node.children) { + node.children = node.children.reduce((acc, childNode) => { + if (isVoidElement(childNode) || isNonEmptyLeaf(childNode) || isShortcode(node)) { + return acc.concat(childNode); + } + return find(childNode.children, isNonEmptyNode) ? acc.concat(transform(childNode)) : acc; + }, []); + } + return node; + }; + return transform; +} diff --git a/src/components/Widgets/Markdown/serializers/remark-images-to-text.js b/src/components/Widgets/Markdown/serializers/remark-images-to-text.js new file mode 100644 index 00000000..f63d3820 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remark-images-to-text.js @@ -0,0 +1,18 @@ +/** + * Images must be parsed as shortcodes for asset proxying. This plugin converts + * MDAST image nodes back to text to allow shortcode pattern matching. + */ +export default function remarkImagesToText() { + return transform; + + function transform(node) { + const children = node.children ? node.children.map(transform) : node.children; + if (node.type === 'image') { + const alt = node.alt || ''; + const url = node.url || ''; + const title = node.title ? ` "${node.title}"` : ''; + return { type: 'text', value: `![${alt}](${url}${title})` }; + } + return { ...node, children }; + } +} diff --git a/src/components/Widgets/Markdown/serializers/remark-nested-list.js b/src/components/Widgets/Markdown/serializers/remark-nested-list.js new file mode 100644 index 00000000..930daeb7 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remark-nested-list.js @@ -0,0 +1,33 @@ +/** + * If the first child of a list item is a list, include it in the previous list + * item. Otherwise it translates to markdown as having two bullets. When + * rehype-remark processes a list and finds children that are not list items, it + * wraps them in list items, which leads to the condition this plugin addresses. + * Dropbox Paper currently outputs this kind of HTML, which is invalid. We have + * a support issue open for it, and this plugin can potentially be removed when + * that's resolved. + */ + +export default function remarkNestedList() { + const transform = node => { + if (node.type === 'list' && node.children && node.children.length > 1) { + node.children = node.children.reduce((acc, childNode, index) => { + if (index && childNode.children && childNode.children[0].type === 'list') { + acc[acc.length - 1].children.push(transform(childNode.children.shift())) + if (childNode.children.length) { + acc.push(transform(childNode)); + } + } else { + acc.push(transform(childNode)); + } + return acc; + }, []); + return node; + } + if (node.children) { + node.children = node.children.map(transform); + } + return node; + }; + return transform; +} diff --git a/src/components/Widgets/Markdown/serializers/remark-rehype-shortcodes.js b/src/components/Widgets/Markdown/serializers/remark-rehype-shortcodes.js new file mode 100644 index 00000000..7e6507a7 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remark-rehype-shortcodes.js @@ -0,0 +1,50 @@ +import { map, has } from 'lodash'; +import { renderToString } from 'react-dom/server'; +import u from 'unist-builder'; + +/** + * This plugin doesn't actually transform Remark (MDAST) nodes to Rehype + * (HAST) nodes, but rather, it prepares an MDAST shortcode node for HAST + * conversion by replacing the shortcode text with stringified HTML for + * previewing the shortcode output. + */ +export default function remarkToRehypeShortcodes({ plugins, getAsset }) { + return transform; + + function transform(root) { + const transformedChildren = map(root.children, processShortcodes); + return { ...root, children: transformedChildren }; + } + + /** + * Mapping function to transform nodes that contain shortcodes. + */ + function processShortcodes(node) { + /** + * If the node doesn't contain shortcode data, return the original node. + */ + if (!has(node, ['data', 'shortcode'])) return node; + + /** + * Get shortcode data from the node, and retrieve the matching plugin by + * key. + */ + const { shortcode, shortcodeData } = node.data; + const plugin = plugins.get(shortcode); + + /** + * Run the shortcode plugin's `toPreview` method, which will return either + * an HTML string or a React component. If a React component is returned, + * render it to an HTML string. + */ + const value = plugin.toPreview(shortcodeData, getAsset); + const valueHtml = typeof value === 'string' ? value : renderToString(value); + + /** + * Return a new 'html' type node containing the shortcode preview markup. + */ + const textNode = u('html', valueHtml); + const children = [ textNode ]; + return { ...node, children }; + } +} diff --git a/src/components/Widgets/Markdown/serializers/remark-shortcodes.js b/src/components/Widgets/Markdown/serializers/remark-shortcodes.js new file mode 100644 index 00000000..2b07759a --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remark-shortcodes.js @@ -0,0 +1,99 @@ +import { map, every } from 'lodash'; +import u from 'unist-builder'; +import mdastToString from 'mdast-util-to-string'; + +/** + * Parse shortcodes from an MDAST. + * + * Shortcodes are plain text, and must be the lone content of a paragraph. The + * paragraph must also be a direct child of the root node. When a shortcode is + * found, we just need to add data to the node so the shortcode can be + * identified and processed when serializing to a new format. The paragraph + * containing the node is also recreated to ensure normalization. + */ +export default function remarkShortcodes({ plugins }) { + return transform; + + /** + * Map over children of the root node and convert any found shortcode nodes. + */ + function transform(root) { + const transformedChildren = map(root.children, processShortcodes); + return { ...root, children: transformedChildren }; + } + + /** + * Mapping function to transform nodes that contain shortcodes. + */ + function processShortcodes(node) { + /** + * If the node is not eligible to contain a shortcode, return the original + * node unchanged. + */ + if (!nodeMayContainShortcode(node)) return node; + + /** + * Combine the text values of all children to a single string, check the + * string for a shortcode pattern match, and validate the match. + */ + const text = mdastToString(node).trim(); + const { plugin, match } = matchTextToPlugin(text); + const matchIsValid = validateMatch(text, match); + + /** + * If a valid match is found, return a new node with shortcode data + * included. Otherwise, return the original node. + */ + return matchIsValid ? createShortcodeNode(text, plugin, match) : node; + }; + + /** + * Ensure that the node and it's children are acceptable types to contain + * shortcodes. Currently, only a paragraph containing text and/or html nodes + * may contain shortcodes. + */ + function nodeMayContainShortcode(node) { + const validNodeTypes = ['paragraph']; + const validChildTypes = ['text', 'html']; + + if (validNodeTypes.includes(node.type)) { + return every(node.children, child => { + return validChildTypes.includes(child.type); + }); + } + } + + /** + * Return the plugin and RegExp.match result from the first plugin with a + * pattern that matches the given text. + */ + function matchTextToPlugin(text) { + let match; + const plugin = plugins.find(p => { + match = text.match(p.pattern); + return !!match; + }); + return { plugin, match }; + } + + /** + * A match is only valid if it takes up the entire paragraph. + */ + function validateMatch(text, match) { + return match && match[0].length === text.length; + } + + /** + * Create a new node with shortcode data included. Use an 'html' node instead + * of a 'text' node as the child to ensure the node content is not parsed by + * Remark or Rehype. Include the child as an array because an MDAST paragraph + * node must have it's children in an array. + */ + function createShortcodeNode(text, plugin, match) { + const shortcode = plugin.id; + const shortcodeData = plugin.fromBlock(match); + const data = { shortcode, shortcodeData }; + const textNode = u('html', text); + return u('paragraph', { data }, [textNode]); + } +} diff --git a/src/components/Widgets/Markdown/serializers/remark-slate.js b/src/components/Widgets/Markdown/serializers/remark-slate.js new file mode 100644 index 00000000..f979916b --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remark-slate.js @@ -0,0 +1,172 @@ +import { get, isEmpty } from 'lodash'; +import u from 'unist-builder'; +import mdastDefinitions from 'mdast-util-definitions'; +import modifyChildren from 'unist-util-modify-children'; + +export default function remarkToSlatePlugin() { + const typeMap = { + paragraph: 'paragraph', + blockquote: 'quote', + code: 'code', + listItem: 'list-item', + table: 'table', + tableRow: 'table-row', + tableCell: 'table-cell', + thematicBreak: 'thematic-break', + link: 'link', + image: 'image', + }; + const markMap = { + strong: 'bold', + emphasis: 'italic', + delete: 'strikethrough', + inlineCode: 'code', + }; + const toTextNode = (text, data) => ({ kind: 'text', text, data }); + const wrapText = (node, index, parent) => { + if (['text', 'html'].includes(node.type)) { + parent.children.splice(index, 1, u('paragraph', [node])); + } + }; + + let getDefinition; + const transform = (node, index, siblings, parent) => { + let nodes; + + if (node.type === 'root') { + // Create definition getter for link and image references + getDefinition = mdastDefinitions(node); + // Ensure top level text nodes are wrapped in paragraphs + modifyChildren(wrapText)(node); + } + + if (isEmpty(node.children)) { + nodes = node.children; + } else { + // If a node returns a falsey value, exclude it. Some nodes do not + // translate from MDAST to Slate, such as definitions for link/image + // references or footnotes. + // + // Consider using unist-util-remove instead for this. + nodes = node.children.reduce((acc, childNode, idx, sibs) => { + const transformed = transform(childNode, idx, sibs, node); + if (transformed) { + acc.push(transformed); + } + return acc; + }, []); + } + + if (node.type === 'root') { + return { nodes }; + } + + /** + * Convert MDAST shortcode nodes to Slate 'shortcode' type nodes. + */ + if (get(node, ['data', 'shortcode'])) { + const { data } = node; + const nodes = [ toTextNode('') ]; + return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; + } + + // Process raw html as text, since it's valid markdown + if (['text', 'html'].includes(node.type)) { + return toTextNode(node.value, node.data); + } + + if (node.type === 'inlineCode') { + return { kind: 'text', ranges: [{ text: node.value, marks: [{ type: 'code' }] }] }; + } + + if (['strong', 'emphasis', 'delete'].includes(node.type)) { + const remarkToSlateMarks = (markNode, parentMarks = []) => { + const marks = [...parentMarks, { type: markMap[markNode.type] }]; + const ranges = []; + markNode.children.forEach(childNode => { + if (['html', 'text'].includes(childNode.type)) { + ranges.push({ text: childNode.value, marks }); + return; + } + const nestedRanges = remarkToSlateMarks(childNode, marks); + ranges.push(...nestedRanges); + }); + return ranges; + }; + + return { kind: 'text', ranges: remarkToSlateMarks(node) }; + } + + if (node.type === 'heading') { + const depths = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; + return { kind: 'block', type: `heading-${depths[node.depth]}`, nodes }; + } + + if (['paragraph', 'blockquote', 'tableRow', 'tableCell'].includes(node.type)) { + return { kind: 'block', type: typeMap[node.type], nodes }; + } + + if (node.type === 'code') { + const data = { lang: node.lang }; + const text = toTextNode(node.value); + const nodes = [text]; + return { kind: 'block', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'list') { + const slateType = node.ordered ? 'numbered-list' : 'bulleted-list'; + const data = { start: node.start }; + return { kind: 'block', type: slateType, data, nodes }; + } + + if (node.type === 'listItem') { + const data = { checked: node.checked }; + return { kind: 'block', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'table') { + const data = { align: node.align }; + return { kind: 'block', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'thematicBreak') { + return { kind: 'block', type: typeMap[node.type], isVoid: true }; + } + + if (node.type === 'link') { + const { title, url } = node; + const data = { title, url }; + return { kind: 'inline', type: typeMap[node.type], data, nodes }; + } + + if (node.type === 'linkReference') { + const definition = getDefinition(node.identifier); + const data = {}; + if (definition) { + data.title = definition.title; + data.url = definition.url; + } + return { kind: 'inline', type: typeMap['link'], data, nodes }; + } + + if (node.type === 'image') { + const { title, url, alt } = node; + const data = { title, url, alt }; + return { kind: 'block', type: typeMap[node.type], data }; + } + + if (node.type === 'imageReference') { + const definition = getDefinition(node.identifier); + const data = {}; + if (definition) { + data.title = definition.title; + data.url = definition.url; + } + return { kind: 'block', type: typeMap['image'], data }; + } + }; + + // Since `transform` is used for recursive child mapping, ensure that only the + // first argument is supplied on the initial call. + return node => transform(node); +} diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js deleted file mode 100644 index 6a5071ba..00000000 --- a/src/components/Widgets/Markdown/unified.js +++ /dev/null @@ -1,627 +0,0 @@ -import { get, has, find, isEmpty, every, map } from 'lodash'; -import { renderToString } from 'react-dom/server'; -import unified from 'unified'; -import u from 'unist-builder'; -import markdownToRemarkPlugin from 'remark-parse'; -import remarkToMarkdownPlugin from 'remark-stringify'; -import mdastDefinitions from 'mdast-util-definitions'; -import mdastToString from 'mdast-util-to-string'; -import modifyChildren from 'unist-util-modify-children'; -import remarkToRehype from 'remark-rehype'; -import rehypeToHtml from 'rehype-stringify'; -import htmlToRehype from 'rehype-parse'; -import rehypeToRemark from 'rehype-remark'; -import rehypeReparse from 'rehype-raw'; -import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; -import ReactDOMServer from 'react-dom/server'; -import registry from '../../../lib/registry'; -import merge from 'deepmerge'; -import hastFromString from 'hast-util-from-string'; -import hastToMdastHandlerAll from 'hast-util-to-mdast/all'; -import { reduce, capitalize } from 'lodash'; - -/** - * Remove empty nodes, including the top level parents of deeply nested empty nodes. - */ -const rehypeRemoveEmpty = () => { - const isVoidElement = node => ['img', 'hr', 'br'].includes(node.tagName); - const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value; - const isShortcode = node => node.properties && node.properties[`data${capitalize(shortcodeAttributePrefix)}`]; - const isNonEmptyNode = node => { - return isVoidElement(node) - || isNonEmptyLeaf(node) - || isShortcode(node) - || find(node.children, isNonEmptyNode); - }; - - const transform = node => { - if (isVoidElement(node) || isNonEmptyLeaf(node) || isShortcode(node)) { - return node; - } - if (node.children) { - node.children = node.children.reduce((acc, childNode) => { - if (isVoidElement(childNode) || isNonEmptyLeaf(childNode) || isShortcode(node)) { - return acc.concat(childNode); - } - return find(childNode.children, isNonEmptyNode) ? acc.concat(transform(childNode)) : acc; - }, []); - } - return node; - }; - return transform; -}; - -/** - * If the first child of a list item is a list, include it in the previous list - * item. Otherwise it translates to markdown as having two bullets. When - * rehype-remark processes a list and finds children that are not list items, it - * wraps them in list items, which leads to the condition this plugin addresses. - * Dropbox Paper currently outputs this kind of HTML, which is invalid. We have - * a support issue open for it, and this plugin can potentially be removed when - * that's resolved. - */ -const remarkNestedList = () => { - const transform = node => { - if (node.type === 'list' && node.children && node.children.length > 1) { - node.children = node.children.reduce((acc, childNode, index) => { - if (index && childNode.children && childNode.children[0].type === 'list') { - acc[acc.length - 1].children.push(transform(childNode.children.shift())) - if (childNode.children.length) { - acc.push(transform(childNode)); - } - } else { - acc.push(transform(childNode)); - } - return acc; - }, []); - return node; - } - if (node.children) { - node.children = node.children.map(transform); - } - return node; - }; - return transform; -}; - -/** - * Dropbox Paper outputs emoji characters as images, and stores the actual - * emoji character in a `data-emoji-ch` attribute on the image. This plugin - * replaces the images with the emoji characters. - */ -const rehypePaperEmoji = () => { - const 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; -}; - -/** - * Rewrite the remark-stringify text visitor to simply return the text value, - * without encoding or escaping any characters. This means we're completely - * trusting the markdown that we receive. - */ -function remarkPrecompileShortcodes() { - const Compiler = this.Compiler; - const visitors = Compiler.prototype.visitors; - visitors.text = node => node.value; -}; - - -/** - * Parse shortcodes from an MDAST. - * - * Shortcodes are plain text, and must be the lone content of a paragraph. The - * paragraph must also be a direct child of the root node. When a shortcode is - * found, we just need to add data to the node so the shortcode can be - * identified and processed when serializing to a new format. The paragraph - * containing the node is also recreated to ensure normalization. - */ -const remarkShortcodes = ({ plugins }) => { - return transform; - - /** - * Map over children of the root node and convert any found shortcode nodes. - */ - function transform(root) { - const transformedChildren = map(root.children, processShortcodes); - return { ...root, children: transformedChildren }; - } - - /** - * Mapping function to transform nodes that contain shortcodes. - */ - function processShortcodes(node) { - /** - * If the node is not eligible to contain a shortcode, return the original - * node unchanged. - */ - if (!nodeMayContainShortcode(node)) return node; - - /** - * Combine the text values of all children to a single string, check the - * string for a shortcode pattern match, and validate the match. - */ - const text = mdastToString(node).trim(); - const { plugin, match } = matchTextToPlugin(text); - const matchIsValid = validateMatch(text, match); - - /** - * If a valid match is found, return a new node with shortcode data - * included. Otherwise, return the original node. - */ - return matchIsValid ? createShortcodeNode(text, plugin, match) : node; - }; - - /** - * Ensure that the node and it's children are acceptable types to contain - * shortcodes. Currently, only a paragraph containing text and/or html nodes - * may contain shortcodes. - */ - function nodeMayContainShortcode(node) { - const validNodeTypes = ['paragraph']; - const validChildTypes = ['text', 'html']; - - if (validNodeTypes.includes(node.type)) { - return every(node.children, child => { - return validChildTypes.includes(child.type); - }); - } - } - - /** - * Return the plugin and RegExp.match result from the first plugin with a - * pattern that matches the given text. - */ - function matchTextToPlugin(text) { - let match; - const plugin = plugins.find(p => { - match = text.match(p.pattern); - return !!match; - }); - return { plugin, match }; - } - - /** - * A match is only valid if it takes up the entire paragraph. - */ - function validateMatch(text, match) { - return match && match[0].length === text.length; - } - - /** - * Create a new node with shortcode data included. Use an 'html' node instead - * of a 'text' node as the child to ensure the node content is not parsed by - * Remark or Rehype. Include the child as an array because an MDAST paragraph - * node must have it's children in an array. - */ - function createShortcodeNode(text, plugin, match) { - const shortcode = plugin.id; - const shortcodeData = plugin.fromBlock(match); - const data = { shortcode, shortcodeData }; - const textNode = u('html', text); - return u('paragraph', { data }, [textNode]); - } -}; - - -/** - * This plugin doesn't actually transform Remark (MDAST) nodes to Rehype - * (HAST) nodes, but rather, it prepares an MDAST shortcode node for HAST - * conversion by replacing the shortcode text with stringified HTML for - * previewing the shortcode output. - */ -const remarkToRehypeShortcodes = ({ plugins, getAsset }) => { - return transform; - - function transform(root) { - const transformedChildren = map(root.children, processShortcodes); - return { ...root, children: transformedChildren }; - } - - /** - * Mapping function to transform nodes that contain shortcodes. - */ - function processShortcodes(node) { - /** - * If the node doesn't contain shortcode data, return the original node. - */ - if (!has(node, ['data', 'shortcode'])) return node; - - /** - * Get shortcode data from the node, and retrieve the matching plugin by - * key. - */ - const { shortcode, shortcodeData } = node.data; - const plugin = plugins.get(shortcode); - - /** - * Run the shortcode plugin's `toPreview` method, which will return either - * an HTML string or a React component. If a React component is returned, - * render it to an HTML string. - */ - const value = plugin.toPreview(shortcodeData, getAsset); - const valueHtml = typeof value === 'string' ? value : renderToString(value); - - /** - * Return a new 'html' type node containing the shortcode preview markup. - */ - const textNode = u('html', valueHtml); - const children = [ textNode ]; - return { ...node, children }; - } -}; - -const remarkToSlatePlugin = () => { - const typeMap = { - paragraph: 'paragraph', - blockquote: 'quote', - code: 'code', - listItem: 'list-item', - table: 'table', - tableRow: 'table-row', - tableCell: 'table-cell', - thematicBreak: 'thematic-break', - link: 'link', - image: 'image', - }; - const markMap = { - strong: 'bold', - emphasis: 'italic', - delete: 'strikethrough', - inlineCode: 'code', - }; - const toTextNode = (text, data) => ({ kind: 'text', text, data }); - const wrapText = (node, index, parent) => { - if (['text', 'html'].includes(node.type)) { - parent.children.splice(index, 1, u('paragraph', [node])); - } - }; - - let getDefinition; - const transform = (node, index, siblings, parent) => { - let nodes; - - if (node.type === 'root') { - // Create definition getter for link and image references - getDefinition = mdastDefinitions(node); - // Ensure top level text nodes are wrapped in paragraphs - modifyChildren(wrapText)(node); - } - - if (isEmpty(node.children)) { - nodes = node.children; - } else { - // If a node returns a falsey value, exclude it. Some nodes do not - // translate from MDAST to Slate, such as definitions for link/image - // references or footnotes. - // - // Consider using unist-util-remove instead for this. - nodes = node.children.reduce((acc, childNode, idx, sibs) => { - const transformed = transform(childNode, idx, sibs, node); - if (transformed) { - acc.push(transformed); - } - return acc; - }, []); - } - - if (node.type === 'root') { - return { nodes }; - } - - /** - * Convert MDAST shortcode nodes to Slate 'shortcode' type nodes. - */ - if (get(node, ['data', 'shortcode'])) { - const { data } = node; - const nodes = [ toTextNode('') ]; - return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; - } - - // Process raw html as text, since it's valid markdown - if (['text', 'html'].includes(node.type)) { - return toTextNode(node.value, node.data); - } - - if (node.type === 'inlineCode') { - return { kind: 'text', ranges: [{ text: node.value, marks: [{ type: 'code' }] }] }; - } - - if (['strong', 'emphasis', 'delete'].includes(node.type)) { - const remarkToSlateMarks = (markNode, parentMarks = []) => { - const marks = [...parentMarks, { type: markMap[markNode.type] }]; - const ranges = []; - markNode.children.forEach(childNode => { - if (['html', 'text'].includes(childNode.type)) { - ranges.push({ text: childNode.value, marks }); - return; - } - const nestedRanges = remarkToSlateMarks(childNode, marks); - ranges.push(...nestedRanges); - }); - return ranges; - }; - - return { kind: 'text', ranges: remarkToSlateMarks(node) }; - } - - if (node.type === 'heading') { - const depths = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; - return { kind: 'block', type: `heading-${depths[node.depth]}`, nodes }; - } - - if (['paragraph', 'blockquote', 'tableRow', 'tableCell'].includes(node.type)) { - return { kind: 'block', type: typeMap[node.type], nodes }; - } - - if (node.type === 'code') { - const data = { lang: node.lang }; - const text = toTextNode(node.value); - const nodes = [text]; - return { kind: 'block', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'list') { - const slateType = node.ordered ? 'numbered-list' : 'bulleted-list'; - const data = { start: node.start }; - return { kind: 'block', type: slateType, data, nodes }; - } - - if (node.type === 'listItem') { - const data = { checked: node.checked }; - return { kind: 'block', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'table') { - const data = { align: node.align }; - return { kind: 'block', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'thematicBreak') { - return { kind: 'block', type: typeMap[node.type], isVoid: true }; - } - - if (node.type === 'link') { - const { title, url } = node; - const data = { title, url }; - return { kind: 'inline', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'linkReference') { - const definition = getDefinition(node.identifier); - const data = {}; - if (definition) { - data.title = definition.title; - data.url = definition.url; - } - return { kind: 'inline', type: typeMap['link'], data, nodes }; - } - - if (node.type === 'image') { - const { title, url, alt } = node; - const data = { title, url, alt }; - return { kind: 'block', type: typeMap[node.type], data }; - } - - if (node.type === 'imageReference') { - const definition = getDefinition(node.identifier); - const data = {}; - if (definition) { - data.title = definition.title; - data.url = definition.url; - } - return { kind: 'block', type: typeMap['image'], data }; - } - }; - - // Since `transform` is used for recursive child mapping, ensure that only the - // first argument is supplied on the initial call. - return node => transform(node); -}; - -const slateToRemarkPlugin = () => { - const transform = node => { - console.log(node); - return node; - }; - return transform; -}; - -/** - * Images must be parsed as shortcodes for asset proxying. This plugin converts - * MDAST image nodes back to text to allow shortcode pattern matching. - */ -const remarkImagesToText = () => { - return transform; - - function transform(node) { - const children = node.children ? node.children.map(transform) : node.children; - if (node.type === 'image') { - const alt = node.alt || ''; - const url = node.url || ''; - const title = node.title ? ` "${node.title}"` : ''; - return { type: 'text', value: `![${alt}](${url}${title})` }; - } - return { ...node, children }; - } -} - -export const markdownToRemark = markdown => { - const parsed = unified() - .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) - .parse(markdown); - - const result = unified() - .use(remarkImagesToText) - .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) - .runSync(parsed); - - return result; -}; - -export const remarkToMarkdown = obj => { - const mdast = obj || u('root', [u('paragraph', [u('text', '')])]); - - const result = unified() - .use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true }) - .use(remarkPrecompileShortcodes) - .stringify(mdast); - return result; -}; - -export const remarkToSlate = mdast => { - const result = unified() - .use(remarkToSlatePlugin) - .runSync(mdast); - return result; -}; - -export const slateToRemark = (raw, shortcodePlugins) => { - const typeMap = { - 'paragraph': 'paragraph', - 'heading-one': 'heading', - 'heading-two': 'heading', - 'heading-three': 'heading', - 'heading-four': 'heading', - 'heading-five': 'heading', - 'heading-six': 'heading', - 'quote': 'blockquote', - 'code': 'code', - 'numbered-list': 'list', - 'bulleted-list': 'list', - 'list-item': 'listItem', - 'table': 'table', - 'table-row': 'tableRow', - 'table-cell': 'tableCell', - 'thematic-break': 'thematicBreak', - 'link': 'link', - 'image': 'image', - }; - const markMap = { - bold: 'strong', - italic: 'emphasis', - strikethrough: 'delete', - code: 'inlineCode', - }; - const transform = node => { - const children = isEmpty(node.nodes) ? node.nodes : node.nodes.reduce((acc, childNode) => { - if (childNode.kind !== 'text') { - acc.push(transform(childNode)); - return acc; - } - if (childNode.ranges) { - childNode.ranges.forEach(range => { - const { marks = [], text } = range; - const markTypes = marks.map(mark => markMap[mark.type]); - if (markTypes.includes('inlineCode')) { - acc.push(u('inlineCode', text)); - } else { - const textNode = u('html', text); - const nestedText = !markTypes.length ? textNode : markTypes.reduce((acc, markType) => { - const nested = u(markType, [acc]); - return nested; - }, textNode); - acc.push(nestedText); - } - }); - } else { - - acc.push(u('html', childNode.text)); - } - return acc; - }, []); - - if (node.type === 'root') { - return u('root', children); - } - - if (node.type === 'shortcode') { - const { data } = node; - const plugin = shortcodePlugins.get(data.shortcode); - const text = plugin.toBlock(data.shortcodeData); - const textNode = u('html', text); - return u('paragraph', { data }, [ textNode ]); - } - - if (node.type.startsWith('heading')) { - const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; - const depth = node.type.split('-')[1]; - const props = { depth: depths[depth] }; - return u(typeMap[node.type], props, children); - } - - if (['paragraph', 'quote', 'list-item', 'table', 'table-row', 'table-cell'].includes(node.type)) { - return u(typeMap[node.type], children); - } - - if (node.type === 'code') { - const value = get(node.nodes, [0, 'text']); - const props = { lang: get(node.data, 'lang') }; - return u(typeMap[node.type], props, value); - } - - if (['numbered-list', 'bulleted-list'].includes(node.type)) { - const ordered = node.type === 'numbered-list'; - const props = { ordered, start: get(node.data, 'start') || 1 }; - return u(typeMap[node.type], props, children); - } - - if (node.type === 'thematic-break') { - return u(typeMap[node.type]); - } - - if (node.type === 'link') { - const data = get(node, 'data', {}); - const { url, title } = data; - return u(typeMap[node.type], data, children); - } - - if (node.type === 'image') { - const data = get(node, 'data', {}); - const { url, title, alt } = data; - return u(typeMap[node.type], data); - } - } - raw.type = 'root'; - const mdast = transform(raw); - - const result = unified() - .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) - .runSync(mdast); - - return result; -}; - -export const remarkToHtml = (mdast, getAsset) => { - const result = unified() - .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents(), getAsset }) - .use(remarkToRehype, { allowDangerousHTML: true }) - .runSync(mdast); - - const output = unified() - .use(rehypeToHtml, { allowDangerousHTML: true, allowDangerousCharacters: true, entities: { subset: [] } }) - .stringify(result); - return output -} - -export const htmlToSlate = html => { - const hast = unified() - .use(htmlToRehype, { fragment: true }) - .parse(html); - - const result = unified() - .use(rehypeRemoveEmpty) - .use(rehypeMinifyWhitespace) - .use(rehypePaperEmoji) - .use(rehypeToRemark) - .use(remarkNestedList) - .use(remarkToSlatePlugin) - .runSync(hast); - - return result; -}; From 406ae57d3ee75bd850c01ba9f1922825c979e431 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 31 Jul 2017 13:08:56 -0400 Subject: [PATCH 71/79] add blockquote rte button --- .../Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js | 3 ++- .../Widgets/Markdown/MarkdownControl/VisualEditor/index.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js index 26ac0145..99ed3249 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js +++ b/src/components/Widgets/Markdown/MarkdownControl/Toolbar/Toolbar.js @@ -62,9 +62,10 @@ export default class Toolbar extends React.Component { { label: 'Code', icon: 'code-alt', state: buttons.code }, { label: 'Header 1', icon: 'h1', state: buttons.h1 }, { label: 'Header 2', icon: 'h2', state: buttons.h2 }, + { label: 'Code Block', icon: 'code', state: buttons.codeBlock }, + { label: 'Quote', icon: 'quote', state: buttons.quote }, { label: 'Bullet List', icon: 'list-bullet', state: buttons.list }, { label: 'Numbered List', icon: 'list-numbered', state: buttons.listNumbered }, - { label: 'Code Block', icon: 'code', state: buttons.codeBlock }, { label: 'Link', icon: 'link', state: buttons.link }, ]; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 7d8116fb..33fbcd26 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -178,6 +178,7 @@ export default class Editor extends Component { list: this.getButtonProps('bulleted-list', { isBlock: true }), listNumbered: this.getButtonProps('numbered-list', { isBlock: true }), codeBlock: this.getButtonProps('code', { isBlock: true }), + quote: this.getButtonProps('quote', { isBlock: true }), }} onToggleMode={this.handleToggle} plugins={this.state.shortcodePlugins} From cf2b7be25f8f4824d6e7e3a130a892e95db6b378 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 31 Jul 2017 16:41:40 -0400 Subject: [PATCH 72/79] refactor and document rte serializers --- .../Widgets/Markdown/serializers/index.js | 341 ++++++++++-------- .../serializers/rehype-remove-empty.js | 32 -- ...ype-paper-emoji.js => rehypePaperEmoji.js} | 0 .../serializers/remark-nested-list.js | 33 -- .../Markdown/serializers/remark-slate.js | 172 --------- ...mages-to-text.js => remarkImagesToText.js} | 0 ...hortcodes.js => remarkRehypeShortcodes.js} | 0 ...mark-shortcodes.js => remarkShortcodes.js} | 0 .../Markdown/serializers/remarkSlate.js | 293 +++++++++++++++ .../serializers/remarkSquashReferences.js | 65 ++++ .../Markdown/serializers/remarkWrapHtml.js | 21 ++ .../Markdown/serializers/slateRemark.js | 330 +++++++++++++++++ 12 files changed, 898 insertions(+), 389 deletions(-) delete mode 100644 src/components/Widgets/Markdown/serializers/rehype-remove-empty.js rename src/components/Widgets/Markdown/serializers/{rehype-paper-emoji.js => rehypePaperEmoji.js} (100%) delete mode 100644 src/components/Widgets/Markdown/serializers/remark-nested-list.js delete mode 100644 src/components/Widgets/Markdown/serializers/remark-slate.js rename src/components/Widgets/Markdown/serializers/{remark-images-to-text.js => remarkImagesToText.js} (100%) rename src/components/Widgets/Markdown/serializers/{remark-rehype-shortcodes.js => remarkRehypeShortcodes.js} (100%) rename src/components/Widgets/Markdown/serializers/{remark-shortcodes.js => remarkShortcodes.js} (100%) create mode 100644 src/components/Widgets/Markdown/serializers/remarkSlate.js create mode 100644 src/components/Widgets/Markdown/serializers/remarkSquashReferences.js create mode 100644 src/components/Widgets/Markdown/serializers/remarkWrapHtml.js create mode 100644 src/components/Widgets/Markdown/serializers/slateRemark.js diff --git a/src/components/Widgets/Markdown/serializers/index.js b/src/components/Widgets/Markdown/serializers/index.js index b2b596bc..c66fc890 100644 --- a/src/components/Widgets/Markdown/serializers/index.js +++ b/src/components/Widgets/Markdown/serializers/index.js @@ -1,4 +1,4 @@ -import { get, isEmpty, reduce } from 'lodash'; +import { get, isEmpty, reduce, pull } from 'lodash'; import unified from 'unified'; import u from 'unist-builder'; import markdownToRemarkPlugin from 'remark-parse'; @@ -7,51 +7,133 @@ import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; -import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; -import remarkToRehypeShortcodes from './remark-rehype-shortcodes'; -import rehypeRemoveEmpty from './rehype-remove-empty'; -import rehypePaperEmoji from './rehype-paper-emoji'; -import remarkNestedList from './remark-nested-list'; -import remarkToSlatePlugin from './remark-slate'; -import remarkImagesToText from './remark-images-to-text'; -import remarkShortcodes from './remark-shortcodes'; +import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; +import rehypePaperEmoji from './rehypePaperEmoji'; +import remarkWrapHtml from './remarkWrapHtml'; +import remarkToSlatePlugin from './remarkSlate'; +import remarkSquashReferences from './remarkSquashReferences'; +import remarkImagesToText from './remarkImagesToText'; +import remarkShortcodes from './remarkShortcodes'; +import slateToRemarkParser from './slateRemark'; import registry from '../../../../lib/registry'; -export const remarkToHtml = (mdast, getAsset) => { - const result = unified() - .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents(), getAsset }) - .use(remarkToRehype, { allowDangerousHTML: true }) - .runSync(mdast); +/** + * This module contains all serializers for the Markdown widget. + * + * The value of a Markdown widget is transformed to various formats during + * editing, and these formats are referenced throughout serializer source + * documentation. Below is brief glossary of the formats used. + * + * - Markdown {string} + * The stringified Markdown value. The value of the field is persisted + * (stored) in this format, and the stringified value is also used when the + * editor is in "raw" Markdown mode. + * + * - MDAST {object} + * Also loosely referred to as "Remark". MDAST stands for MarkDown AST + * (Abstract Syntax Tree), and is an object representation of a Markdown + * document. Underneath, it's a Unist tree with a Markdown-specific schema. An + * MDAST is used as the source of truth for any Markdown field within the CMS + * once the Markdown string value is loaded. MDAST syntax is a part of the + * Unified ecosystem, and powers the Remark processor, so Remark plugins may + * be used. + * + * - HAST {object} + * Also loosely referred to as "Rehype". HAST, similar to MDAST, is an object + * representation of an HTML document. The field value takes this format + * temporarily before the document is stringified to HTML. + * + * - HTML {string} + * The field value is stringifed to HTML for preview purposes - the HTML value + * is never parsed, it is output only. + * + * - Slate Raw AST {object} + * Slate's Raw AST is a very simple and unopinionated object representation of + * a document in a Slate editor. We define our own Markdown-specific schema + * for serialization to/from Slate's Raw AST and MDAST. + * + * Overview of the Markdown widget serialization life cycle: + * + * - Entry Load + * When an entry is loaded, all Markdown widget values are serialized to + * MDAST within the entry draft. + * + * - Visual Editor Render + * When a Markdown widget using the visual editor renders, it converts the + * MDAST value from the entry draft to Slate's Raw AST, and renders that. + * + * - Visual Editor Update + * When the value of a Markdown field is changed in the visual editor, the + * resulting Slate Raw AST is converted back to MDAST, and the MDAST value is + * set as the new state of the field in the entry draft. + * + * - Visual Editor Paste + * When a value is pasted to the visual editor, the pasted value is checked + * for HTML data. If HTML is found, the value is deserialized to an HAST, then + * to MDAST, and finally to Slate's Raw AST. If no HTML is found, the plain + * text value of the paste is serialized to Slate's Raw AST via the Slate + * Plain serializer. The deserialized fragment is then inserted to the Slate + * document. + * + * - Raw Editor Render + * When a Markdown widget using the raw editor (Markdown switch activated), + * it stringifies the MDAST from the entry draft to Markdown, and runs the + * stringified Markdown through Slate's Plain serializer, which outputs a + * Slate Raw AST of the plain text, which is then rendered in the editor. + * + * - Raw Editor Update + * When the value of a Markdown field is changed in the raw editor, the + * resulting Slate Raw AST is stringified back to a string, and the string + * value is then parsed as Markdown into an MDAST. The MDAST value is + * set as the new state of the field in the entry draft. + * + * - Raw Editor Paste + * When a value is pasted to the raw editor, the text value of the paste is + * serialized to Slate's Raw AST via the Slate Plain serializer. The + * deserialized fragment is then inserted to the Slate document. + * + * - Preview Pane Render + * When the preview pane renders the value of a Markdown widget, it first + * converts the MDAST value to HAST, stringifies the HAST to HTML, and + * renders that. + * + * - Entry Persist (Save) + * On persist, the MDAST value in the entry draft is stringified back to + * a Markdown string for storage. + */ - const output = unified() - .use(rehypeToHtml, { allowDangerousHTML: true, allowDangerousCharacters: true }) - .stringify(result); - return output -} - -export const htmlToSlate = html => { - const hast = unified() - .use(htmlToRehype, { fragment: true }) - .parse(html); - - const result = unified() - .use(rehypeRemoveEmpty) - .use(rehypeMinifyWhitespace) - .use(rehypePaperEmoji) - .use(rehypeToRemark) - .use(remarkNestedList) - .use(remarkToSlatePlugin) - .runSync(hast); - - return result; -}; +/** + * Deserialize a Markdown string to an MDAST. + */ export const markdownToRemark = markdown => { + + /** + * Disabling tokenizers allows us to turn off features within the Remark + * parser. + */ + function disableTokenizers() { + + /** + * Turn off soft breaks until we can properly support them across both + * editors. + */ + pull(this.Parser.prototype.inlineMethods, 'break'); + } + + /** + * Parse the Markdown string input to an MDAST. + */ const parsed = unified() - .use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true }) + .use(markdownToRemarkPlugin, { fences: true, pedantic: true, commonmark: true }) + .use(disableTokenizers) .parse(markdown); + /** + * Further transform the MDAST with plugins. + */ const result = unified() + .use(remarkSquashReferences) .use(remarkImagesToText) .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) .runSync(parsed); @@ -59,6 +141,10 @@ export const markdownToRemark = markdown => { return result; }; + +/** + * Serialize an MDAST to a Markdown string. + */ export const remarkToMarkdown = obj => { /** * Rewrite the remark-stringify text visitor to simply return the text value, @@ -71,133 +157,84 @@ export const remarkToMarkdown = obj => { visitors.text = node => node.value; }; + /** + * Provide an empty MDAST if no value is provided. + */ const mdast = obj || u('root', [u('paragraph', [u('text', '')])]); - const result = unified() + + const markdown = unified() .use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true }) .use(remarkAllowAllText) .stringify(mdast); - return result; + + return markdown; }; + +/** + * Convert an MDAST to an HTML string. + */ +export const remarkToHtml = (mdast, getAsset) => { + const hast = unified() + .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents(), getAsset }) + .use(remarkToRehype, { allowDangerousHTML: true }) + .runSync(mdast); + + const html = unified() + .use(rehypeToHtml, { allowDangerousHTML: true, allowDangerousCharacters: true }) + .stringify(hast); + + return html; +} + + +/** + * Deserialize an HTML string to Slate's Raw AST. Currently used for HTML + * pastes. + */ +export const htmlToSlate = html => { + const hast = unified() + .use(htmlToRehype, { fragment: true }) + .parse(html); + + const mdast = unified() + .use(rehypePaperEmoji) + .use(rehypeToRemark) + .runSync(hast); + + const slateRaw = unified() + .use(remarkImagesToText) + .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) + .use(remarkWrapHtml) + .use(remarkToSlatePlugin) + .runSync(mdast); + + return slateRaw; +}; + + +/** + * Convert an MDAST to Slate's Raw AST. + */ export const remarkToSlate = mdast => { const result = unified() + .use(remarkWrapHtml) .use(remarkToSlatePlugin) .runSync(mdast); return result; }; -export const slateToRemark = (raw, shortcodePlugins) => { - const typeMap = { - 'paragraph': 'paragraph', - 'heading-one': 'heading', - 'heading-two': 'heading', - 'heading-three': 'heading', - 'heading-four': 'heading', - 'heading-five': 'heading', - 'heading-six': 'heading', - 'quote': 'blockquote', - 'code': 'code', - 'numbered-list': 'list', - 'bulleted-list': 'list', - 'list-item': 'listItem', - 'table': 'table', - 'table-row': 'tableRow', - 'table-cell': 'tableCell', - 'thematic-break': 'thematicBreak', - 'link': 'link', - 'image': 'image', - }; - const markMap = { - bold: 'strong', - italic: 'emphasis', - strikethrough: 'delete', - code: 'inlineCode', - }; - const transform = node => { - const children = isEmpty(node.nodes) ? node.nodes : node.nodes.reduce((acc, childNode) => { - if (childNode.kind !== 'text') { - acc.push(transform(childNode)); - return acc; - } - if (childNode.ranges) { - childNode.ranges.forEach(range => { - const { marks = [], text } = range; - const markTypes = marks.map(mark => markMap[mark.type]); - if (markTypes.includes('inlineCode')) { - acc.push(u('inlineCode', text)); - } else { - const textNode = u('html', text); - const nestedText = !markTypes.length ? textNode : markTypes.reduce((acc, markType) => { - const nested = u(markType, [acc]); - return nested; - }, textNode); - acc.push(nestedText); - } - }); - } else { - acc.push(u('html', childNode.text)); - } - return acc; - }, []); - - if (node.type === 'root') { - return u('root', children); - } - - if (node.type === 'shortcode') { - const { data } = node; - const plugin = shortcodePlugins.get(data.shortcode); - const text = plugin.toBlock(data.shortcodeData); - const textNode = u('html', text); - return u('paragraph', { data }, [ textNode ]); - } - - if (node.type.startsWith('heading')) { - const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; - const depth = node.type.split('-')[1]; - const props = { depth: depths[depth] }; - return u(typeMap[node.type], props, children); - } - - if (['paragraph', 'quote', 'list-item', 'table', 'table-row', 'table-cell'].includes(node.type)) { - return u(typeMap[node.type], children); - } - - if (node.type === 'code') { - const value = get(node.nodes, [0, 'text']); - const props = { lang: get(node.data, 'lang') }; - return u(typeMap[node.type], props, value); - } - - if (['numbered-list', 'bulleted-list'].includes(node.type)) { - const ordered = node.type === 'numbered-list'; - const props = { ordered, start: get(node.data, 'start') || 1 }; - return u(typeMap[node.type], props, children); - } - - if (node.type === 'thematic-break') { - return u(typeMap[node.type]); - } - - if (node.type === 'link') { - const data = get(node, 'data', {}); - const { url, title } = data; - return u(typeMap[node.type], data, children); - } - - if (node.type === 'image') { - const data = get(node, 'data', {}); - const { url, title, alt } = data; - return u(typeMap[node.type], data); - } - } - raw.type = 'root'; - const mdast = transform(raw); - - const result = unified() - .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) - .runSync(mdast); - - return result; +/** + * Convert a Slate Raw AST to MDAST. + * + * Requires shortcode plugins to parse shortcode nodes back to text. + * + * Note that Unified is not utilized for the conversion from Slate's Raw AST to + * MDAST. The conversion is manual because Unified can only operate on Unist + * trees. + */ +export const slateToRemark = (raw) => { + const mdast = slateToRemarkParser(raw, { shortcodePlugins: registry.getEditorComponents() }); + return mdast; }; diff --git a/src/components/Widgets/Markdown/serializers/rehype-remove-empty.js b/src/components/Widgets/Markdown/serializers/rehype-remove-empty.js deleted file mode 100644 index 4d59b6a5..00000000 --- a/src/components/Widgets/Markdown/serializers/rehype-remove-empty.js +++ /dev/null @@ -1,32 +0,0 @@ -import { find, capitalize } from 'lodash'; - -/** - * Remove empty nodes, including the top level parents of deeply nested empty nodes. - */ -export default function rehypeRemoveEmpty() { - const isVoidElement = node => ['img', 'hr', 'br'].includes(node.tagName); - const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value; - const isShortcode = node => node.properties && node.properties[`data${capitalize(shortcodeAttributePrefix)}`]; - const isNonEmptyNode = node => { - return isVoidElement(node) - || isNonEmptyLeaf(node) - || isShortcode(node) - || find(node.children, isNonEmptyNode); - }; - - const transform = node => { - if (isVoidElement(node) || isNonEmptyLeaf(node) || isShortcode(node)) { - return node; - } - if (node.children) { - node.children = node.children.reduce((acc, childNode) => { - if (isVoidElement(childNode) || isNonEmptyLeaf(childNode) || isShortcode(node)) { - return acc.concat(childNode); - } - return find(childNode.children, isNonEmptyNode) ? acc.concat(transform(childNode)) : acc; - }, []); - } - return node; - }; - return transform; -} diff --git a/src/components/Widgets/Markdown/serializers/rehype-paper-emoji.js b/src/components/Widgets/Markdown/serializers/rehypePaperEmoji.js similarity index 100% rename from src/components/Widgets/Markdown/serializers/rehype-paper-emoji.js rename to src/components/Widgets/Markdown/serializers/rehypePaperEmoji.js diff --git a/src/components/Widgets/Markdown/serializers/remark-nested-list.js b/src/components/Widgets/Markdown/serializers/remark-nested-list.js deleted file mode 100644 index 930daeb7..00000000 --- a/src/components/Widgets/Markdown/serializers/remark-nested-list.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * If the first child of a list item is a list, include it in the previous list - * item. Otherwise it translates to markdown as having two bullets. When - * rehype-remark processes a list and finds children that are not list items, it - * wraps them in list items, which leads to the condition this plugin addresses. - * Dropbox Paper currently outputs this kind of HTML, which is invalid. We have - * a support issue open for it, and this plugin can potentially be removed when - * that's resolved. - */ - -export default function remarkNestedList() { - const transform = node => { - if (node.type === 'list' && node.children && node.children.length > 1) { - node.children = node.children.reduce((acc, childNode, index) => { - if (index && childNode.children && childNode.children[0].type === 'list') { - acc[acc.length - 1].children.push(transform(childNode.children.shift())) - if (childNode.children.length) { - acc.push(transform(childNode)); - } - } else { - acc.push(transform(childNode)); - } - return acc; - }, []); - return node; - } - if (node.children) { - node.children = node.children.map(transform); - } - return node; - }; - return transform; -} diff --git a/src/components/Widgets/Markdown/serializers/remark-slate.js b/src/components/Widgets/Markdown/serializers/remark-slate.js deleted file mode 100644 index f979916b..00000000 --- a/src/components/Widgets/Markdown/serializers/remark-slate.js +++ /dev/null @@ -1,172 +0,0 @@ -import { get, isEmpty } from 'lodash'; -import u from 'unist-builder'; -import mdastDefinitions from 'mdast-util-definitions'; -import modifyChildren from 'unist-util-modify-children'; - -export default function remarkToSlatePlugin() { - const typeMap = { - paragraph: 'paragraph', - blockquote: 'quote', - code: 'code', - listItem: 'list-item', - table: 'table', - tableRow: 'table-row', - tableCell: 'table-cell', - thematicBreak: 'thematic-break', - link: 'link', - image: 'image', - }; - const markMap = { - strong: 'bold', - emphasis: 'italic', - delete: 'strikethrough', - inlineCode: 'code', - }; - const toTextNode = (text, data) => ({ kind: 'text', text, data }); - const wrapText = (node, index, parent) => { - if (['text', 'html'].includes(node.type)) { - parent.children.splice(index, 1, u('paragraph', [node])); - } - }; - - let getDefinition; - const transform = (node, index, siblings, parent) => { - let nodes; - - if (node.type === 'root') { - // Create definition getter for link and image references - getDefinition = mdastDefinitions(node); - // Ensure top level text nodes are wrapped in paragraphs - modifyChildren(wrapText)(node); - } - - if (isEmpty(node.children)) { - nodes = node.children; - } else { - // If a node returns a falsey value, exclude it. Some nodes do not - // translate from MDAST to Slate, such as definitions for link/image - // references or footnotes. - // - // Consider using unist-util-remove instead for this. - nodes = node.children.reduce((acc, childNode, idx, sibs) => { - const transformed = transform(childNode, idx, sibs, node); - if (transformed) { - acc.push(transformed); - } - return acc; - }, []); - } - - if (node.type === 'root') { - return { nodes }; - } - - /** - * Convert MDAST shortcode nodes to Slate 'shortcode' type nodes. - */ - if (get(node, ['data', 'shortcode'])) { - const { data } = node; - const nodes = [ toTextNode('') ]; - return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; - } - - // Process raw html as text, since it's valid markdown - if (['text', 'html'].includes(node.type)) { - return toTextNode(node.value, node.data); - } - - if (node.type === 'inlineCode') { - return { kind: 'text', ranges: [{ text: node.value, marks: [{ type: 'code' }] }] }; - } - - if (['strong', 'emphasis', 'delete'].includes(node.type)) { - const remarkToSlateMarks = (markNode, parentMarks = []) => { - const marks = [...parentMarks, { type: markMap[markNode.type] }]; - const ranges = []; - markNode.children.forEach(childNode => { - if (['html', 'text'].includes(childNode.type)) { - ranges.push({ text: childNode.value, marks }); - return; - } - const nestedRanges = remarkToSlateMarks(childNode, marks); - ranges.push(...nestedRanges); - }); - return ranges; - }; - - return { kind: 'text', ranges: remarkToSlateMarks(node) }; - } - - if (node.type === 'heading') { - const depths = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; - return { kind: 'block', type: `heading-${depths[node.depth]}`, nodes }; - } - - if (['paragraph', 'blockquote', 'tableRow', 'tableCell'].includes(node.type)) { - return { kind: 'block', type: typeMap[node.type], nodes }; - } - - if (node.type === 'code') { - const data = { lang: node.lang }; - const text = toTextNode(node.value); - const nodes = [text]; - return { kind: 'block', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'list') { - const slateType = node.ordered ? 'numbered-list' : 'bulleted-list'; - const data = { start: node.start }; - return { kind: 'block', type: slateType, data, nodes }; - } - - if (node.type === 'listItem') { - const data = { checked: node.checked }; - return { kind: 'block', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'table') { - const data = { align: node.align }; - return { kind: 'block', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'thematicBreak') { - return { kind: 'block', type: typeMap[node.type], isVoid: true }; - } - - if (node.type === 'link') { - const { title, url } = node; - const data = { title, url }; - return { kind: 'inline', type: typeMap[node.type], data, nodes }; - } - - if (node.type === 'linkReference') { - const definition = getDefinition(node.identifier); - const data = {}; - if (definition) { - data.title = definition.title; - data.url = definition.url; - } - return { kind: 'inline', type: typeMap['link'], data, nodes }; - } - - if (node.type === 'image') { - const { title, url, alt } = node; - const data = { title, url, alt }; - return { kind: 'block', type: typeMap[node.type], data }; - } - - if (node.type === 'imageReference') { - const definition = getDefinition(node.identifier); - const data = {}; - if (definition) { - data.title = definition.title; - data.url = definition.url; - } - return { kind: 'block', type: typeMap['image'], data }; - } - }; - - // Since `transform` is used for recursive child mapping, ensure that only the - // first argument is supplied on the initial call. - return node => transform(node); -} diff --git a/src/components/Widgets/Markdown/serializers/remark-images-to-text.js b/src/components/Widgets/Markdown/serializers/remarkImagesToText.js similarity index 100% rename from src/components/Widgets/Markdown/serializers/remark-images-to-text.js rename to src/components/Widgets/Markdown/serializers/remarkImagesToText.js diff --git a/src/components/Widgets/Markdown/serializers/remark-rehype-shortcodes.js b/src/components/Widgets/Markdown/serializers/remarkRehypeShortcodes.js similarity index 100% rename from src/components/Widgets/Markdown/serializers/remark-rehype-shortcodes.js rename to src/components/Widgets/Markdown/serializers/remarkRehypeShortcodes.js diff --git a/src/components/Widgets/Markdown/serializers/remark-shortcodes.js b/src/components/Widgets/Markdown/serializers/remarkShortcodes.js similarity index 100% rename from src/components/Widgets/Markdown/serializers/remark-shortcodes.js rename to src/components/Widgets/Markdown/serializers/remarkShortcodes.js diff --git a/src/components/Widgets/Markdown/serializers/remarkSlate.js b/src/components/Widgets/Markdown/serializers/remarkSlate.js new file mode 100644 index 00000000..bc53ac66 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remarkSlate.js @@ -0,0 +1,293 @@ +import { get, isEmpty, isArray } from 'lodash'; +import u from 'unist-builder'; +import modifyChildren from 'unist-util-modify-children'; + +/** + * Map of MDAST node types to Slate node types. + */ +const typeMap = { + root: 'root', + paragraph: 'paragraph', + blockquote: 'quote', + code: 'code', + listItem: 'list-item', + table: 'table', + tableRow: 'table-row', + tableCell: 'table-cell', + thematicBreak: 'thematic-break', + link: 'link', + image: 'image', + shortcode: 'shortcode', +}; + + +/** + * Map of MDAST node types to Slate mark types. + */ +const markMap = { + strong: 'bold', + emphasis: 'italic', + delete: 'strikethrough', + inlineCode: 'code', +}; + + +/** + * Create a Slate Inline node. + */ +function createBlock(type, nodes, props = {}) { + if (!isArray(nodes)) { + props = nodes; + nodes = undefined; + } + + return { kind: 'block', type, nodes, ...props }; +} + + +/** + * Create a Slate Block node. + */ +function createInline(type, nodes, props = {}) { + return { kind: 'inline', type, nodes, ...props }; +} + + +/** + * Create a Slate Raw text node. + */ +function createText(value, data) { + const node = { kind: 'text', data }; + if (isArray(value)) { + return { ...node, ranges: value }; + } + return {...node, text: value }; +} + +function convertMarkNode(node, parentMarks = []) { + + /** + * Add the current node's mark type to the marks collected from parent + * mark nodes, if any. + */ + const marks = [...parentMarks, { type: markMap[node.type] }]; + + /** + * Set an array to collect sections of text. + */ + const ranges = []; + + node.children.forEach(childNode => { + + /** + * If a text node is a direct child of the current node, it should be + * set aside as a range, and all marks that have been collected in the + * `marks` array should apply to that specific range. + */ + if (['html', 'text'].includes(childNode.type)) { + ranges.push({ text: childNode.value, marks }); + return; + } + + /** + * Any non-text child node should be processed as a parent node. The + * recursive results should be pushed into the ranges array. This way, + * every MDAST nested text structure becomes a flat array of ranges + * that can serve as the value of a single Slate Raw text node. + */ + const nestedRanges = convertMarkNode(childNode, marks); + ranges.push(...nestedRanges); + }); + + return ranges; +} + +/** + * Convert a single MDAST node to a Slate Raw node. Uses local node factories + * that mimic the unist-builder function utilized in the slateRemark + * transformer. + */ +function convertNode(node, nodes) { + + /** + * Unified/Remark processors use mutable operations, so we don't want to + * change a node's type directly for conversion purposes, as that tends to + * unexpected errors. + */ + const type = get(node, ['data', 'shortcode']) ? 'shortcode' : node.type; + + switch (type) { + + /** + * General + * + * Convert simple cases that only require a type and children, with no + * additional properties. + */ + case 'root': + case 'paragraph': + case 'listItem': + case 'blockquote': + case 'tableRow': + case 'tableCell': { + return createBlock(typeMap[type], nodes); + } + + + /** + * Shortcodes + * + * Shortcode nodes are represented as "void" blocks in the Slate AST. They + * maintain the same data as MDAST shortcode nodes. Slate void blocks must + * contain a blank text node. + */ + case 'shortcode': { + const { data } = node; + const nodes = [ createText('') ]; + return createBlock(typeMap[type], nodes, { data, isVoid: true }); + } + + /** + * Text + * + * Text and HTML nodes are both used to render text, and should be treated + * the same. HTML is treated as text because we never want to escape or + * encode it. + */ + case 'text': + case 'html': { + return createText(node.value, node.data); + } + + /** + * Inline Code + * + * Inline code nodes from an MDAST are represented in our Slate schema as + * text nodes with a "code" mark. We manually create the "range" containing + * the inline code value and a "code" mark, and place it in an array for use + * as a Slate text node's children array. + */ + case 'inlineCode': { + const range = { + text: node.value, + marks: [{ type: 'code' }], + }; + return createText([ range ]); + } + + /** + * Marks + * + * Marks are typically decorative sub-types that apply to text nodes. In an + * MDAST, marks are nodes that can contain other nodes. This nested + * hierarchy has to be flattened and split into distinct text nodes with + * their own set of marks. + */ + case 'strong': + case 'emphasis': + case 'delete': { + return createText(convertMarkNode(node)); + } + + /** + * Headings + * + * MDAST headings use a single type with a separate "depth" property to + * indicate the heading level, while the Slate schema uses a separate node + * type for each heading level. Here we get the proper Slate node name based + * on the MDAST node depth. + */ + case 'heading': { + const depthMap = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; + const slateType = `heading-${depthMap[node.depth]}`; + return createBlock(slateType, nodes); + } + + /** + * Code Blocks + * + * MDAST code blocks are a distinct node type with a simple text value. We + * convert that value into a nested child text node for Slate. We also carry + * over the "lang" data property if it's defined. + */ + case 'code': { + const data = { lang: node.lang }; + const text = createText(node.value); + const nodes = [text]; + return createBlock(typeMap[type], nodes, { data }); + } + + /** + * Lists + * + * MDAST has a single list type and an "ordered" property. We derive that + * information into the Slate schema's distinct list node types. We also + * include the "start" property, which indicates the number an ordered list + * starts at, if defined. + */ + case 'list': { + const slateType = node.ordered ? 'numbered-list' : 'bulleted-list'; + const data = { start: node.start }; + return createBlock(slateType, nodes, { data }); + } + + + /** + * Thematic Breaks + * + * Thematic breaks are void nodes in the Slate schema. + */ + case 'thematicBreak': { + return createBlock(typeMap[type], { isVoid: true }); + } + + /** + * Links + * + * MDAST stores the link attributes directly on the node, while our Slate + * schema references them in the data object. + */ + case 'link': { + const { title, url } = node; + const data = { title, url }; + return createInline(typeMap[type], nodes, { data }); + } + + /** + * Tables + * + * Tables are parsed separately because they may include an "align" + * property, which should be passed to the Slate node. + */ + case 'table': { + const data = { align: node.align }; + return createBlock(typeMap[type], nodes, { data }); + } + } +} + + +/** + * A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins + * return a `transform` function that receives the MDAST as it's first argument. + */ +export default function remarkToSlatePlugin() { + function transform(node) { + + /** + * Call `transform` recursively on child nodes. + * + * If a node returns a falsey value, filter it out. Some nodes do not + * translate from MDAST to Slate, such as definitions for link/image + * references or footnotes. + */ + const children = !isEmpty(node.children) && node.children.map(transform).filter(val => val); + + /** + * Run individual nodes through the conversion factory. + */ + return convertNode(node, children); + } + + return transform; +} diff --git a/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js b/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js new file mode 100644 index 00000000..53762255 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js @@ -0,0 +1,65 @@ +import { without } from 'lodash'; +import u from 'unist-builder'; +import mdastDefinitions from 'mdast-util-definitions'; + +/** + * Raw markdown may contain image references or link references. Because there + * is no way to maintain these references within the Slate AST, we convert image + * and link references to standard images and links by putting their url's + * inline. The definitions are then removed from the document. + * + * For example, the following markdown: + * + * ``` + * ![alpha][bravo] + * + * [bravo]: http://example.com/example.jpg + * ``` + * + * Yields: + * + * ``` + * ![alpha][http://example.com/example.jpg] + * ``` + * + */ +export default function remarkSquashReferences() { + return getTransform; + + function getTransform(node) { + const getDefinition = mdastDefinitions(node); + return transform.call(null, getDefinition, node); + } + + function transform(getDefinition, node) { + + /** + * Bind the `getDefinition` function to `transform` and recursively map all + * nodes. + */ + const boundTransform = transform.bind(null, getDefinition); + const children = node.children ? node.children.map(boundTransform) : node.children; + + /** + * Combine reference and definition nodes into standard image and link + * nodes. + */ + if (['imageReference', 'linkReference'].includes(node.type)) { + const type = node.type === 'imageReference' ? 'image' : 'link'; + const { title, url } = getDefinition(node.identifier) || {}; + return u(type, { title, url, alt: node.alt }, children); + } + + /** + * Remove definition nodes and filter the resulting null values from the + * filtered children array. + */ + if(node.type === 'definition') { + return null; + } + + const filteredChildren = without(children, null); + + return { ...node, children: filteredChildren }; + } +} diff --git a/src/components/Widgets/Markdown/serializers/remarkWrapHtml.js b/src/components/Widgets/Markdown/serializers/remarkWrapHtml.js new file mode 100644 index 00000000..baee06bb --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remarkWrapHtml.js @@ -0,0 +1,21 @@ +import u from 'unist-builder'; + +/** + * Ensure that top level 'html' type nodes are wrapped in paragraphs. Html nodes + * are used for text nodes that we don't want Remark or Rehype to parse. + */ +export default function remarkWrapHtml() { + + function transform(tree) { + tree.children = tree.children.map(node => { + if (node.type === 'html') { + return u('paragraph', [node]); + } + return node; + }); + + return tree; + } + + return transform; +} diff --git a/src/components/Widgets/Markdown/serializers/slateRemark.js b/src/components/Widgets/Markdown/serializers/slateRemark.js new file mode 100644 index 00000000..21853abc --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/slateRemark.js @@ -0,0 +1,330 @@ +import { get, isEmpty, concat, without, flatten } from 'lodash'; +import u from 'unist-builder'; + +/** + * Map of Slate node types to MDAST/Remark node types. + */ +const typeMap = { + 'root': 'root', + 'paragraph': 'paragraph', + 'heading-one': 'heading', + 'heading-two': 'heading', + 'heading-three': 'heading', + 'heading-four': 'heading', + 'heading-five': 'heading', + 'heading-six': 'heading', + 'quote': 'blockquote', + 'code': 'code', + 'numbered-list': 'list', + 'bulleted-list': 'list', + 'list-item': 'listItem', + 'table': 'table', + 'table-row': 'tableRow', + 'table-cell': 'tableCell', + 'thematic-break': 'thematicBreak', + 'link': 'link', + 'image': 'image', +}; + + +/** + * Map of Slate mark types to MDAST/Remark node types. + */ +const markMap = { + bold: 'strong', + italic: 'emphasis', + strikethrough: 'delete', + code: 'inlineCode', +}; + + +/** + * Slate treats inline code decoration as a standard mark, but MDAST does + * not allow inline code nodes to contain children, only a single text + * value. An MDAST inline code node can be nested within mark nodes such + * as "emphasis" and "strong", but it cannot contain them. + * + * Because of this, if a "code" mark (translated to MDAST "inlineCode") is + * in the markTypes array, we make the base text node an "inlineCode" type + * instead of a standard text node. + */ +function processCodeMark(markTypes) { + const isInlineCode = markTypes.includes('inlineCode'); + const filteredMarkTypes = isInlineCode ? without(markTypes, 'inlineCode') : markTypes; + const textNodeType = isInlineCode ? 'inlineCode' : 'html'; + return { filteredMarkTypes, textNodeType }; +} + + +/** + * Wraps a text node in one or more mark nodes by placing the text node in an + * array and using that as the `children` value of a mark node. The resulting + * mark node is then placed in an array and used as the child of a mark node for + * the next mark type in `markTypes`. This continues for each member of + * `markTypes`. If `markTypes` is empty, the original text node is returned. + */ +function wrapTextWithMarks(textNode, markTypes) { + const wrapTextWithMark = (childNode, markType) => u(markType, [childNode]); + return markTypes.reduce(wrapTextWithMark, textNode); +} + +/** + * Converts a Slate Raw text node to an MDAST text node. + * + * Slate text nodes without marks often simply have a "text" property with + * the value. In this case the conversion to MDAST is simple. If a Slate + * text node does not have a "text" property, it will instead have a + * "ranges" property containing an array of objects, each with an array of + * marks, such as "bold" or "italic", along with a "text" property. + * + * MDAST instead expresses such marks in a nested structure, with individual + * nodes for each mark type nested until the deepest mark node, which will + * contain the text node. + * + * To convert a Slate text node's marks to MDAST, we treat each "range" as a + * separate text node, convert the text node itself to an MDAST text node, + * and then recursively wrap the text node for each mark, collecting the results + * of each range in a single array of child nodes. + * + * For example, this Slate text node: + * + * { + * kind: 'text', + * ranges: [ + * { + * text: 'test', + * marks: ['bold', 'italic'] + * }, + * { + * text: 'test two' + * } + * ] + * } + * + * ...would be converted to this MDAST nested structure: + * + * [ + * { + * type: 'strong', + * children: [{ + * type: 'emphasis', + * children: [{ + * type: 'text', + * value: 'test' + * }] + * }] + * }, + * { + * type: 'text', + * value: 'test two' + * } + * ] + * + * This example also demonstrates how a single Slate node may need to be + * replaced with multiple MDAST nodes, so the resulting array must be flattened. + */ +function convertTextNode(node) { + + /** + * If the Slate text node has no "ranges" property, just return an equivalent + * MDAST node. + */ + if (!node.ranges) { + return u('html', node.text); + } + + /** + * If there is no "text" property, convert the text range(s) to an array of + * one or more nested MDAST nodes. + */ + const textNodes = node.ranges.map(range => { + /** + * Get an array of the mark types, converted to their MDAST equivalent + * types. + */ + const { marks = [], text } = range; + const markTypes = marks.map(mark => markMap[mark.type]); + + /** + * Code marks must be removed from the marks array, and the presence of a + * code mark changes the text node type that should be used. + */ + const { filteredMarkTypes, textNodeType } = processCodeMark(markTypes); + + /** + * Create the base text node. + */ + const textNode = u(textNodeType, text); + + /** + * Recursively wrap the base text node in the individual mark nodes, if + * any exist. + */ + return wrapTextWithMarks(textNode, filteredMarkTypes); + }); + + /** + * Since each range will be mapped into an array, we flatten the result to + * return a single array of all nodes. + */ + return flatten(textNodes); +} + + +/** + * Convert a single Slate Raw node to an MDAST node. Uses the unist-builder `u` + * function to create MDAST nodes and parses shortcodes. + */ +function convertNode(node, children, shortcodePlugins) { + switch (node.type) { + + /** + * General + * + * Convert simple cases that only require a type and children, with no + * additional properties. + */ + case 'root': + case 'paragraph': + case 'quote': + case 'list-item': + case 'table': + case 'table-row': + case 'table-cell': { + return u(typeMap[node.type], children); + } + + /** + * Shortcodes + * + * Shortcode nodes only exist in Slate's Raw AST if they were inserted + * via the plugin toolbar in memory, so they should always have + * shortcode data attached. The "shortcode" data property contains the + * name of the registered shortcode plugin, and the "shortcodeData" data + * property contains the data received from the shortcode plugin's + * `fromBlock` method when the shortcode node was created. + * + * Here we get the shortcode plugin from the registry and use it's + * `toBlock` method to recreate the original markdown shortcode. We then + * insert that text into a new "html" type node (a "text" type node + * might get encoded or escaped by remark-stringify). Finally, we wrap + * the "html" node in a "paragraph" type node, as shortcode nodes must + * be alone in their own paragraph. + */ + case 'shortcode': { + const { data } = node; + const plugin = shortcodePlugins.get(data.shortcode); + const text = plugin.toBlock(data.shortcodeData); + const textNode = u('html', text); + return u('paragraph', { data }, [ textNode ]); + } + + /** + * Headings + * + * Slate schemas don't usually infer basic type info from data, so each + * level of heading is a separately named type. The MDAST schema just + * has a single "heading" type with the depth stored in a "depth" + * property on the node. Here we derive the depth from the Slate node + * type - e.g., for "heading-two", we need a depth value of "2". + */ + case 'heading-one': + case 'heading-two': + case 'heading-three': + case 'heading-four': + case 'heading-five': + case 'heading-six': { + const depthMap = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 }; + const depthText = node.type.split('-')[1]; + const depth = depthMap[depthText]; + return u(typeMap[node.type], { depth }, children); + } + + /** + * Code Blocks + * + * Code block nodes have a single text child, and may have a code language + * stored in the "lang" data property. Here we transfer both the node + * value and the "lang" data property to the new MDAST node. + */ + case 'code': { + const value = get(node.nodes, [0, 'text']); + const lang = get(node.data, 'lang'); + return u(typeMap[node.type], { lang }, value); + } + + /** + * Lists + * + * Our Slate schema has separate node types for ordered and unordered + * lists, but the MDAST spec uses a single type with a boolean "ordered" + * property to indicate whether the list is numbered. The MDAST spec also + * allows for a "start" property to indicate the first number used for an + * ordered list. Here we translate both values to our Slate schema. + */ + case 'numbered-list': + case 'bulleted-list': { + const ordered = node.type === 'numbered-list'; + const props = { ordered, start: get(node.data, 'start') || 1 }; + return u(typeMap[node.type], props, children); + } + + /** + * Thematic Breaks + * + * Thematic breaks don't have children. We parse them separately for + * clarity. + */ + case 'thematic-break': { + return u(typeMap[node.type]); + } + + /** + * Links + * + * The url and title attributes of link nodes are stored in properties on + * the node for both Slate and Remark schemas. + */ + case 'link': { + const { url, title } = get(node, 'data', {}); + return u(typeMap[node.type], { url, title }, children); + } + + /** + * No default case is supplied because an unhandled case should never + * occur. In the event that it does, let the error throw (for now). + */ + } +} + + +export default function slateToRemark(raw, { shortcodePlugins }) { + /** + * The transform function mimics the approach of a Remark plugin for + * conformity with the other serialization functions. This function converts + * Slate nodes to MDAST nodes, and recursively calls itself to process child + * nodes to arbitrary depth. + */ + function transform(node) { + + /** + * Call `transform` recursively on child nodes, and flatten the resulting + * array. + */ + const children = !isEmpty(node.nodes) && flatten(node.nodes.map(transform)); + + /** + * Run individual nodes through conversion factories. + */ + return node.kind === 'text' ? convertTextNode(node) : convertNode(node, children, shortcodePlugins); + } + + /** + * The Slate Raw AST generally won't have a top level type, so we set it to + * "root" for clarity. + */ + raw.type = 'root'; + + const mdast = transform(raw); + return mdast; +} From d84b156b0a915032e24239191f214b9ae32e5c32 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 1 Aug 2017 18:51:30 -0400 Subject: [PATCH 73/79] update existing serialization tests --- .../__snapshots__/parser.spec.js.snap | 1041 ++++++++++------- .../VisualEditor/__tests__/parser.spec.js | 11 +- .../VisualEditor/components.js | 18 +- .../MarkupItReactRenderer.spec.js.snap | 78 -- .../__snapshots__/renderer.spec.js.snap | 78 ++ ...ReactRenderer.spec.js => renderer.spec.js} | 19 +- 6 files changed, 706 insertions(+), 539 deletions(-) delete mode 100644 src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap create mode 100644 src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/renderer.spec.js.snap rename src/components/Widgets/Markdown/MarkdownPreview/__tests__/{MarkupItReactRenderer.spec.js => renderer.spec.js} (77%) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap index 74a86aab..7d772fb1 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap @@ -2,1124 +2,1297 @@ exports[`Compile markdown to Prosemirror document structure should compile a markdown ordered list 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H1", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "attrs": Object { - "order": 1, - "tight": true, + "data": Object { + "start": 1, }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "yo", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "bro", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "fro", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, ], - "type": "ordered_list", + "type": "numbered-list", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile bulleted lists 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H1", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "attrs": Object { - "tight": true, + "data": Object { + "start": null, }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "yo", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "bro", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "fro", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, ], - "type": "bullet_list", + "type": "bulleted-list", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile code blocks 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "params": "javascript", + "data": Object { + "lang": "javascript", }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "var a = 1;", - "type": "text", }, ], - "type": "code_block", + "type": "code", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile hard breaks (double space) 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "text": "blue moonfootballs", - "type": "text", + "data": undefined, + "kind": "text", + "text": "blue moon +footballs", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile horizontal rules 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H1", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "type": "horizontal_rule", + "isVoid": true, + "kind": "block", + "nodes": undefined, + "type": "thematic-break", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "blue moon", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile horizontal rules 2`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H1", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "type": "horizontal_rule", + "isVoid": true, + "kind": "block", + "nodes": undefined, + "type": "thematic-break", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "blue moon", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile images 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "kind": "block", + "nodes": Array [ + Object { + "data": undefined, + "kind": "text", + "text": "![super](duper.jpg)", + }, + ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile inline code 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Word", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "This is some sweet ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "code", + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", }, ], - "text": "inline code", - "type": "text", }, Object { + "data": undefined, + "kind": "text", "text": " yo!", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile kitchen sink example 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "An exhibit of Markdown", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "This note demonstrates some of what Markdown is capable of doing.", - "type": "text", }, ], "type": "paragraph", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "em", + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "Note: Feel free to play with this page. Unlike regular notes, this doesn't +automatically save itself.", }, ], - "text": "Note: Feel free to play with this page. Unlike regular notes, this doesn't -automatically save itself.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Basic formatting", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Paragraphs can be written like so. A paragraph is the basic block of Markdown. A paragraph is what text will turn into when there is no reason it should become anything else.", - "type": "text", }, ], "type": "paragraph", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Paragraphs must be separated by a blank line. Basic formatting of ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "em", + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "italics", }, ], - "text": "italics", - "type": "text", }, Object { + "data": undefined, + "kind": "text", "text": " and ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "strong", + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "bold", }, ], - "text": "bold", - "type": "text", }, Object { - "text": " is supported. This ", - "type": "text", + "data": undefined, + "kind": "text", + "text": " is supported. This *can be ", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "em", + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "nested", }, ], - "text": "can be ", - "type": "text", }, Object { - "marks": Array [ - Object { - "type": "em", - }, - Object { - "type": "strong", - }, - ], - "text": "nested", - "type": "text", - }, - Object { - "marks": Array [ - Object { - "type": "em", - }, - ], - "text": " like", - "type": "text", - }, - Object { - "text": " so.", - "type": "text", + "data": undefined, + "kind": "text", + "text": " like* so.", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Lists", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Ordered list", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, Object { - "attrs": Object { - "order": 1, - "tight": true, + "data": Object { + "start": 1, }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Item 1 2. A second item 3. Number 3 4. Ⅳ", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, ], - "type": "ordered_list", + "type": "numbered-list", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "em", + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "Note: the fourth item uses the Unicode character for Roman numeral four.", }, ], - "text": "Note: the fourth item uses the Unicode character for Roman numeral four.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Unordered list", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, Object { - "attrs": Object { - "tight": true, + "data": Object { + "start": null, }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "An item Another item Yet another item And there's more...", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, ], - "type": "bullet_list", + "type": "bulleted-list", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Paragraph modifiers", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Code block", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, Object { - "attrs": Object { - "params": "", + "data": Object { + "lang": null, }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Code blocks are very useful for developers and other people who look at code or other things that are written in plain text. As you can see, it uses a fixed-width font.", - "type": "text", }, ], - "type": "code_block", + "type": "code", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "You can also make ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "code", + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", }, ], - "text": "inline code", - "type": "text", }, Object { + "data": undefined, + "kind": "text", "text": " to add code into other things.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Quote", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Here is a quote. What this is should be self explanatory. Quotes are automatically indented when they are used.", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "blockquote", + "type": "quote", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Headings", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "There are six levels of headings. They correspond with the six levels of HTML headings. You've probably noticed them already in the page. Each level down uses one more hash character.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Headings ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "em", + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "can", }, ], - "text": "can", - "type": "text", }, Object { + "data": undefined, + "kind": "text", "text": " also contain ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "strong", + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "formatting", }, ], - "text": "formatting", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "They can even contain ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "code", + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", }, ], - "text": "inline code", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Of course, demonstrating what headings look like messes up the structure of the page.", - "type": "text", }, ], "type": "paragraph", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "I don't recommend using more than three or four levels of headings here, because, when you're smallest heading isn't too small, and you're largest heading isn't too big, and you want each size up to look noticeably larger and more important, there there are only so many sizes that you can use.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "URLs", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "URLs can be made in a handful of ways:", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "tight": true, + "data": Object { + "start": null, }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "A named link to MarkItDown. The easiest way to do these is to select what you", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "want to make a link and hit ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "code", + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "Ctrl+L", }, ], - "text": "Ctrl+L", - "type": "text", }, Object { + "data": undefined, + "kind": "text", "text": ". Another named link to", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "marks": Array [ + "data": Object { + "title": null, + "url": "http://www.markitdown.net/", + }, + "kind": "inline", + "nodes": Array [ Object { - "type": "strong", + "data": undefined, + "kind": "text", + "text": "MarkItDown", }, ], - "text": "MarkItDown", - "type": "text", + "type": "link", }, Object { + "data": undefined, + "kind": "text", "text": " Sometimes you just want a URL like", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "marks": Array [ + "data": Object { + "title": null, + "url": "http://www.markitdown.net/", + }, + "kind": "inline", + "nodes": Array [ Object { - "type": "strong", + "data": undefined, + "kind": "text", + "text": "http://www.markitdown.net/", }, ], - "text": "http://www.markitdown.net/", - "type": "text", + "type": "link", }, Object { + "data": undefined, + "kind": "text", "text": ".", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "list_item", + "type": "list-item", }, ], - "type": "bullet_list", + "type": "bulleted-list", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Horizontal rule", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "A horizontal rule is a line that goes across the middle of the page.", - "type": "text", }, ], "type": "paragraph", }, Object { - "type": "horizontal_rule", + "isVoid": true, + "kind": "block", + "nodes": undefined, + "type": "thematic-break", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "It's sometimes handy for breaking things up.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Images", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Markdown can also contain images. I'll need to add something here sometime.", - "type": "text", }, ], "type": "paragraph", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Finally", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "There's actually a lot more to Markdown than this. See the official introduction and syntax for more information. However, be aware that this is not using the official implementation, and this might work subtly differently in some of the little things.", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile links 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Word", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "How far is it to ", - "type": "text", }, Object { - "marks": Array [ + "data": Object { + "title": null, + "url": "https://google.com", + }, + "kind": "inline", + "nodes": Array [ Object { - "type": "strong", + "data": undefined, + "kind": "text", + "text": "Google", }, ], - "text": "Google", - "type": "text", + "type": "link", }, Object { + "data": undefined, + "kind": "text", "text": " land?", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile multiple header levels 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H1", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "attrs": Object { - "level": 2, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H2", - "type": "text", }, ], - "type": "heading", + "type": "heading-two", }, Object { - "attrs": Object { - "level": 3, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H3", - "type": "text", }, ], - "type": "heading", + "type": "heading-three", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile nested inline markup 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "Word", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "This is ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "strong", + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "some ", + }, + Object { + "marks": Array [ + Object { + "type": "bold", + }, + Object { + "type": "italic", + }, + ], + "text": "hot", + }, + Object { + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": " content", }, ], - "text": "some ", - "type": "text", - }, - Object { - "marks": Array [ - Object { - "type": "em", - }, - Object { - "type": "strong", - }, - ], - "text": "hot", - "type": "text", - }, - Object { - "marks": Array [ - Object { - "type": "strong", - }, - ], - "text": " content", - "type": "text", }, ], "type": "paragraph", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "perhaps ", - "type": "text", }, Object { - "marks": Array [ + "data": undefined, + "kind": "text", + "ranges": Array [ Object { - "type": "strong", + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "scalding", }, ], - "text": "scalding", - "type": "text", }, Object { + "data": undefined, + "kind": "text", "text": " even", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile plugins 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "kind": "block", + "nodes": Array [ + Object { + "data": undefined, + "kind": "text", + "text": "![test](test.png)", + }, + ], "type": "paragraph", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "{{< test >}}", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; exports[`Compile markdown to Prosemirror document structure should compile simple markdown 1`] = ` Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { - "attrs": Object { - "level": 1, - }, - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "H1", - "type": "text", }, ], - "type": "heading", + "type": "heading-one", }, Object { - "content": Array [ + "kind": "block", + "nodes": Array [ Object { + "data": undefined, + "kind": "text", "text": "sweet body", - "type": "text", }, ], "type": "paragraph", }, ], - "type": "doc", + "type": "root", } `; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js index 5d35d45d..9006c9ff 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js @@ -1,12 +1,5 @@ import { fromJS } from 'immutable'; -import { Schema } from "prosemirror-model"; -import { schema } from "prosemirror-markdown"; -import makeParser from '../parser'; - -const testSchema = new Schema({ - nodes: schema.spec.nodes, - marks: schema.spec.marks, -}); +import { markdownToRemark, remarkToSlate } from '../../../serializers'; // Temporary plugins test, uses preloaded plugins from ../parser // TODO: make the parser more testable @@ -51,7 +44,7 @@ const testPlugins = fromJS([ }, ]); -const parser = makeParser(testSchema, testPlugins); +const parser = markdown => remarkToSlate(markdownToRemark(markdown)); describe("Compile markdown to Prosemirror document structure", () => { it("should compile simple markdown", () => { diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js index ad8945d8..2f19dcc7 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js @@ -16,30 +16,30 @@ export const MARK_COMPONENTS = { }; export const NODE_COMPONENTS = { - paragraph: props =>

    {props.children}

    , + 'paragraph': props =>

    {props.children}

    , 'list-item': props =>
  • {props.children}
  • , - 'bulleted-list': props =>
      {props.children}
    , - 'numbered-list': props => -
      {props.children}
    , - quote: props =>
    {props.children}
    , - code: props =>
    {props.children}
    , + 'quote': props =>
    {props.children}
    , + 'code': props =>
    {props.children}
    , 'heading-one': props =>

    {props.children}

    , 'heading-two': props =>

    {props.children}

    , 'heading-three': props =>

    {props.children}

    , 'heading-four': props =>

    {props.children}

    , 'heading-five': props =>
    {props.children}
    , 'heading-six': props =>
    {props.children}
    , - table: props => {props.children}
    , + 'table': props => {props.children}
    , 'table-row': props => {props.children}, 'table-cell': props => {props.children}, 'thematic-break': props =>
    , - link: props => { + 'bulleted-list': props =>
      {props.children}
    , + 'numbered-list': props => +
      {props.children}
    , + 'link': props => { const data = props.node.get('data'); const url = data.get('url'); const title = data.get('title'); return {props.children}; }, - shortcode: props => { + 'shortcode': props => { const { attributes, node, state: editorState } = props; const isSelected = editorState.selection.hasFocusIn(node); const className = cn(styles.shortcode, { [styles.shortcodeSelected]: isSelected }); diff --git a/src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap deleted file mode 100644 index b51ff97e..00000000 --- a/src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -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 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. -
    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

    -
    - -
    -
    Test HTML content
    -
    Testing HTML in Markdown
    -
    -
    -

    Test

    " -`; - -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 3 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 6 1`] = `"
    Title
    "`; - -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. -
    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/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/renderer.spec.js.snap b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/renderer.spec.js.snap new file mode 100644 index 00000000..2d901185 --- /dev/null +++ b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/__snapshots__/renderer.spec.js.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Markdown Preview renderer HTML rendering should render HTML 1`] = `"

    Paragraph with inline element

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

    Use the printf() function.

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

    There is a literal backtick (\`) here.

    "`; + +exports[`Markdown Preview renderer 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 text](https://pbs.twimg.com/profile_images/678903331176214528/TQTdqGwD.jpg)

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

    Title

    +
    + +
    +
    Test HTML content
    +
    Testing HTML in Markdown
    +
    +
    +

    Test

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

    Title

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

    Title

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

    Title

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

    Title

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

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

    "`; + +exports[`Markdown Preview renderer Markdown rendering Lists should render lists 1`] = ` +"
      +
    1. ol item 1
    2. +
    3. +

      ol item 2

      +
        +
      • Sublist 1
      • +
      • Sublist 2
      • +
      • +

        Sublist 3

        +
          +
        1. Sub-Sublist 1
        2. +
        3. Sub-Sublist 2
        4. +
        5. Sub-Sublist 3
        6. +
        +
      • +
      +
    4. +
    5. ol item 3
    6. +
    " +`; diff --git a/src/components/Widgets/Markdown/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js similarity index 77% rename from src/components/Widgets/Markdown/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js rename to src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js index e8859a2a..02bb94c2 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js @@ -4,8 +4,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { padStart } from 'lodash'; import MarkdownPreview from '../index'; +import { markdownToRemark } from '../../serializers'; -describe('MarkitupReactRenderer', () => { +describe('Markdown Preview renderer', () => { describe('Markdown rendering', () => { describe('General', () => { it('should render markdown', () => { @@ -35,7 +36,7 @@ Text with **bold** & _em_ elements ###### H6 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -44,7 +45,7 @@ Text with **bold** & _em_ elements for (const heading of [...Array(6).keys()]) { it(`should render Heading ${ heading + 1 }`, () => { const value = padStart(' Title', heading + 7, '#'); - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); } @@ -63,7 +64,7 @@ Text with **bold** & _em_ elements 1. Sub-Sublist 3 1. ol item 3 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -77,7 +78,7 @@ 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(); }); }); @@ -85,13 +86,13 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('Code', () => { it('should render code', () => { const value = 'Use the `printf()` function.'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); it('should render code 2', () => { const value = '``There is a literal backtick (`) here.``'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -113,7 +114,7 @@ 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(); }); }); @@ -122,7 +123,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('HTML rendering', () => { it('should render HTML', () => { const value = '

    Paragraph with inline element

    '; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); From 18b98fc1c9d1ecbb95b276e86ad4a52d8e14ab8c Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 1 Aug 2017 19:47:45 -0400 Subject: [PATCH 74/79] remove superfluous deps, update yarn.lock --- package.json | 17 +---------------- .../Widgets/Markdown/serializers/remarkSlate.js | 1 - 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/package.json b/package.json index 8eb42be8..38cf8b04 100644 --- a/package.json +++ b/package.json @@ -100,11 +100,7 @@ "classnames": "^2.2.5", "dateformat": "^1.0.12", "deep-equal": "^1.0.1", - "deepmerge": "^1.5.0", "fuzzy": "^0.1.1", - "hast-util-from-string": "^1.0.0", - "hast-util-sanitize": "^1.1.1", - "hast-util-to-mdast": "^1.2.0", "history": "^2.1.2", "immutability-helper": "^2.0.0", "immutable": "^3.7.6", @@ -114,7 +110,6 @@ "jwt-decode": "^2.1.0", "localforage": "^1.4.2", "lodash": "^4.13.1", - "markup-it": "^2.0.0", "material-design-icons": "^3.0.1", "mdast-util-definitions": "^1.2.2", "mdast-util-to-string": "^1.0.4", @@ -151,29 +146,19 @@ "redux-notifications": "^2.1.1", "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", - "rehype-minify-whitespace": "^2.0.0", "rehype-parse": "^3.1.0", - "rehype-raw": "^1.0.0", - "rehype-react": "^3.0.0", "rehype-remark": "^2.0.0", - "rehype-sanitize": "^2.0.0", "rehype-stringify": "^3.0.0", - "remark-html": "^6.0.0", "remark-parse": "^3.0.1", "remark-rehype": "^2.0.0", "remark-stringify": "^3.0.1", - "selection-position": "^1.0.0", "semaphore": "^1.0.5", - "slate": "^0.20.6", - "slate-drop-or-paste-images": "^0.2.0", + "slate": "^0.21.0", "slate-edit-list": "^0.7.1", "slate-edit-table": "^0.10.1", "slug": "^0.9.1", - "textarea-caret-position": "^0.1.1", "unified": "^6.1.4", "unist-builder": "^1.0.2", - "unist-util-map": "^1.0.3", - "unist-util-modify-children": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" }, diff --git a/src/components/Widgets/Markdown/serializers/remarkSlate.js b/src/components/Widgets/Markdown/serializers/remarkSlate.js index bc53ac66..0064365e 100644 --- a/src/components/Widgets/Markdown/serializers/remarkSlate.js +++ b/src/components/Widgets/Markdown/serializers/remarkSlate.js @@ -1,6 +1,5 @@ import { get, isEmpty, isArray } from 'lodash'; import u from 'unist-builder'; -import modifyChildren from 'unist-util-modify-children'; /** * Map of MDAST node types to Slate node types. From 2bb67321f909e70abbd30e41983eab8dd6ccc9f6 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 1 Aug 2017 19:53:23 -0400 Subject: [PATCH 75/79] fix visual editor heading line height --- .../Widgets/Markdown/MarkdownControl/VisualEditor/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index 396f7302..69b85d6c 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -50,6 +50,7 @@ & h1, & h2, & h3, & h4, & h5, & h6 { font-weight: 700; + line-height: 1; } & p, From 3d83325afc5cd2497452802430bc591726dd5c3f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Tue, 1 Aug 2017 21:39:13 -0400 Subject: [PATCH 76/79] add node type check to avoid errors in rte --- src/components/Widgets/Markdown/serializers/remarkSlate.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Widgets/Markdown/serializers/remarkSlate.js b/src/components/Widgets/Markdown/serializers/remarkSlate.js index 0064365e..321f0e89 100644 --- a/src/components/Widgets/Markdown/serializers/remarkSlate.js +++ b/src/components/Widgets/Markdown/serializers/remarkSlate.js @@ -69,14 +69,15 @@ function convertMarkNode(node, parentMarks = []) { * Add the current node's mark type to the marks collected from parent * mark nodes, if any. */ - const marks = [...parentMarks, { type: markMap[node.type] }]; + const markType = markMap[node.type]; + const marks = markType ? [...parentMarks, { type: markMap[node.type] }] : parentMarks; /** * Set an array to collect sections of text. */ const ranges = []; - node.children.forEach(childNode => { + node.children && node.children.forEach(childNode => { /** * If a text node is a direct child of the current node, it should be From 9c0b7262efe8198429d5a1928a1abe7948c27046 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Wed, 2 Aug 2017 13:11:43 -0400 Subject: [PATCH 77/79] fix small code issues in RTE implementation --- src/actions/editorialWorkflow.js | 17 +++++++----- src/actions/entries.js | 19 +++++++++----- src/components/PreviewPane/Preview.js | 4 +++ src/components/PreviewPane/PreviewContent.js | 8 +++--- src/components/PreviewPane/PreviewPane.js | 3 +++ src/components/Widgets/ControlHOC.js | 1 - .../serializers/remarkSquashReferences.js | 2 +- src/components/Widgets/PreviewHOC.js | 9 ++++--- src/containers/EntryPage.js | 5 ++++ src/lib/serializeEntryValues.js | 26 ++++++++++++++++--- src/reducers/entries.js | 3 +-- 11 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index 8127eee6..7c1afdd3 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -231,20 +231,25 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) { const transactionID = uuid.v4(); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); - const transformedEntry = entry.set('data', transformedData); - const transformedEntryDraft = entryDraft.set('entry', transformedEntry); - dispatch(unpublishedEntryPersisting(collection, transformedEntry, transactionID)); + /** + * Serialize the values of any fields with registered serializers, and + * update the entry and entryDraft with the serialized values. + */ + const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); + const serializedEntry = entry.set('data', serializedData); + const serializedEntryDraft = entryDraft.set('entry', serializedEntry); + + dispatch(unpublishedEntryPersisting(collection, serializedEntry, transactionID)); const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry; - return persistAction.call(backend, state.config, collection, transformedEntryDraft, assetProxies.toJS()) + return persistAction.call(backend, state.config, collection, serializedEntryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', kind: 'success', dismissAfter: 4000, })); - return dispatch(unpublishedEntryPersisted(collection, transformedEntry, transactionID)); + return dispatch(unpublishedEntryPersisted(collection, serializedEntry, transactionID)); }) .catch((error) => { dispatch(notifSend({ diff --git a/src/actions/entries.js b/src/actions/entries.js index 6bdfbf7c..26235db2 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -271,19 +271,24 @@ export function persistEntry(collection) { const backend = currentBackend(state.config); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); - const transformedEntry = entry.set('data', transformedData); - const transformedEntryDraft = entryDraft.set('entry', transformedEntry); - dispatch(entryPersisting(collection, transformedEntry)); + + /** + * Serialize the values of any fields with registered serializers, and + * update the entry and entryDraft with the serialized values. + */ + const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); + const serializedEntry = entry.set('data', serializedData); + const serializedEntryDraft = entryDraft.set('entry', serializedEntry); + dispatch(entryPersisting(collection, serializedEntry)); return backend - .persistEntry(state.config, collection, transformedEntryDraft, assetProxies.toJS()) + .persistEntry(state.config, collection, serializedEntryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', kind: 'success', dismissAfter: 4000, })); - return dispatch(entryPersisted(collection, transformedEntry)); + return dispatch(entryPersisted(collection, serializedEntry)); }) .catch((error) => { console.error(error); @@ -292,7 +297,7 @@ export function persistEntry(collection) { kind: 'danger', dismissAfter: 8000, })); - return dispatch(entryPersistFail(collection, transformedEntry, error)); + return dispatch(entryPersistFail(collection, serializedEntry, error)); }); }; } diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js index e4409b5a..93ea02c3 100644 --- a/src/components/PreviewPane/Preview.js +++ b/src/components/PreviewPane/Preview.js @@ -9,6 +9,10 @@ const style = { fontFamily: 'Roboto, "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif', }; +/** + * Use a stateful component so that child components can effectively utilize + * `shouldComponentUpdate`. + */ export default class Preview extends React.Component { render() { const { collection, fields, widgetFor } = this.props; diff --git a/src/components/PreviewPane/PreviewContent.js b/src/components/PreviewPane/PreviewContent.js index dce067fe..2baec537 100644 --- a/src/components/PreviewPane/PreviewContent.js +++ b/src/components/PreviewPane/PreviewContent.js @@ -1,9 +1,11 @@ import React, { PropTypes } from 'react'; import { ScrollSyncPane } from '../ScrollSync'; -// We need to create a lightweight component here so that we can -// access the context within the Frame. This allows us to attach -// the ScrollSyncPane to the body. +/** + * We need to create a lightweight component here so that we can access the + * context within the Frame. This allows us to attach the ScrollSyncPane to the + * body. + */ class PreviewContent extends React.Component { render() { const { previewComponent, previewProps } = this.props; diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index c704b3fc..d849eac6 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -17,6 +17,9 @@ export default class PreviewPane extends React.Component { const { fieldsMetaData, getAsset, entry } = props; const widget = resolveWidget(field.get('widget')); + /** + * Use an HOC to provide conditional updates for all previews. + */ return !widget.preview ? null : ( ({ error: false }); diff --git a/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js b/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js index 53762255..049e69cb 100644 --- a/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js +++ b/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js @@ -19,7 +19,7 @@ import mdastDefinitions from 'mdast-util-definitions'; * Yields: * * ``` - * ![alpha][http://example.com/example.jpg] + * ![alpha](http://example.com/example.jpg) * ``` * */ diff --git a/src/components/Widgets/PreviewHOC.js b/src/components/Widgets/PreviewHOC.js index 1ecaa529..27bdccb0 100644 --- a/src/components/Widgets/PreviewHOC.js +++ b/src/components/Widgets/PreviewHOC.js @@ -1,10 +1,13 @@ import React from 'react'; class PreviewHOC extends React.Component { + + /** + * Only re-render on value change, but always re-render objects and lists. + * Their child widgets will each also be wrapped with this component, and + * will only be updated on value change. + */ shouldComponentUpdate(nextProps) { - // Only re-render on value change, but always re-render objects and lists. - // Their child widgets will each also be wrapped with this component, and - // will only be updated on value change. const isWidgetContainer = ['object', 'list'].includes(nextProps.field.get('widget')); return isWidgetContainer || this.props.value !== nextProps.value; } diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 5bc3a944..7a85c08d 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -68,6 +68,11 @@ class EntryPage extends React.Component { const { entry, newEntry, fields, collection } = nextProps; if (entry && !entry.get('isFetching') && !entry.get('error')) { + + /** + * Deserialize entry values for widgets with registered serializers before + * creating the entry draft. + */ const values = deserializeValues(entry.get('data'), fields); const deserializedEntry = entry.set('data', values); this.createDraft(deserializedEntry); diff --git a/src/lib/serializeEntryValues.js b/src/lib/serializeEntryValues.js index f6f9ae05..878b9524 100644 --- a/src/lib/serializeEntryValues.js +++ b/src/lib/serializeEntryValues.js @@ -20,22 +20,40 @@ import registry from './registry'; * registered deserialization handlers run on entry load, and serialization * handlers run on persist. */ - const runSerializer = (values, fields, method) => { + + /** + * Reduce the list of fields to a map where keys are field names and values + * are field values, serializing the values of fields whose widgets have + * registered serializers. If the field is a list or object, call recursively + * for nested fields. + */ return fields.reduce((acc, field) => { const fieldName = field.get('name'); const value = values.get(fieldName); const serializer = registry.getWidgetValueSerializer(field.get('widget')); const nestedFields = field.get('fields'); + + // Call recursively for fields within lists if (nestedFields && List.isList(value)) { return acc.set(fieldName, value.map(val => runSerializer(val, nestedFields, method))); - } else if (nestedFields && Map.isMap(value)) { + } + + // Call recursively for fields within objects + if (nestedFields && Map.isMap(value)) { return acc.set(fieldName, runSerializer(value, nestedFields, method)); - } else if (serializer && !isNil(value)) { + } + + // Run serialization method on value if not null or undefined + if (serializer && !isNil(value)) { return acc.set(fieldName, serializer[method](value)); - } else if (!isNil(value)) { + } + + // If no serializer is registered for the field's widget, use the field as is + if (!isNil(value)) { return acc.set(fieldName, value); } + return acc; }, Map()); }; diff --git a/src/reducers/entries.js b/src/reducers/entries.js index 49c20bf5..acd891bf 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -21,11 +21,10 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); case ENTRY_SUCCESS: - const result = state.setIn( + return state.setIn( ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], fromJS(action.payload.entry) ); - return result; case ENTRIES_REQUEST: return state.setIn(['pages', action.payload.collection, 'isFetching'], true); From 317a87689178278c971c3a9efd83fac6e57d800c Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Thu, 3 Aug 2017 17:01:52 -0400 Subject: [PATCH 78/79] fix html paste for visual editor --- package.json | 1 + .../__tests__/remarkAssertParents.spec.js | 204 ++++++++++++++++++ .../Widgets/Markdown/serializers/index.js | 4 +- .../serializers/remarkAssertParents.js | 83 +++++++ yarn.lock | 4 + 5 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/components/Widgets/Markdown/serializers/__tests__/remarkAssertParents.spec.js create mode 100644 src/components/Widgets/Markdown/serializers/remarkAssertParents.js diff --git a/package.json b/package.json index 38cf8b04..9d025019 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "slug": "^0.9.1", "unified": "^6.1.4", "unist-builder": "^1.0.2", + "unist-util-visit-parents": "^1.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" }, diff --git a/src/components/Widgets/Markdown/serializers/__tests__/remarkAssertParents.spec.js b/src/components/Widgets/Markdown/serializers/__tests__/remarkAssertParents.spec.js new file mode 100644 index 00000000..afccd2ed --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/__tests__/remarkAssertParents.spec.js @@ -0,0 +1,204 @@ +import u from 'unist-builder'; +import remarkAssertParents from '../remarkAssertParents'; + +const transform = remarkAssertParents(); + +describe('remarkAssertParents', () => { + it('should unnest invalidly nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('paragraph', [ u('text', 'Paragraph text.') ]), + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('code', 'someCode()'), + u('blockquote', [ u('text', 'Quote text.') ]), + u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), + u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('thematicBreak'), + ]), + ]); + + const output = u('root', [ + u('paragraph', [ u('text', 'Paragraph text.') ]), + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('code', 'someCode()'), + u('blockquote', [ u('text', 'Quote text.') ]), + u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), + u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('thematicBreak'), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should unnest deeply nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('paragraph', [ + u('paragraph', [ + u('paragraph', [ u('text', 'Paragraph text.') ]), + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('code', 'someCode()'), + u('blockquote', [ + u('paragraph', [ + u('strong', [ + u('heading', [ + u('text', 'Quote text.'), + ]), + ]), + ]), + ]), + u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), + u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('thematicBreak'), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('paragraph', [ u('text', 'Paragraph text.') ]), + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('code', 'someCode()'), + u('blockquote', [ + u('heading', [ + u('text', 'Quote text.'), + ]), + ]), + u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), + u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('thematicBreak'), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should remove blocks that are emptied as a result of denesting', () => { + const input = u('root', [ + u('paragraph', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]), + ]); + + const output = u('root', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should remove blocks that are emptied as a result of denesting', () => { + const input = u('root', [ + u('paragraph', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]), + ]); + + const output = u('root', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should handle assymetrical splits', () => { + const input = u('root', [ + u('paragraph', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]), + ]); + + const output = u('root', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should nest invalidly nested blocks in the nearest valid ancestor', () => { + const input = u('root', [ + u('paragraph', [ + u('blockquote', [ + u('strong', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + ]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should preserve validly nested siblings of invalidly nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('blockquote', [ + u('strong', [ + u('text', 'Deep validly nested text a.'), + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('text', 'Deep validly nested text b.'), + ]), + ]), + u('text', 'Validly nested text.'), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('strong', [ + u('text', 'Deep validly nested text a.'), + ]), + u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('strong', [ + u('text', 'Deep validly nested text b.'), + ]), + ]), + u('paragraph', [ + u('text', 'Validly nested text.'), + ]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should allow intermediate parents like list and table to contain required block children', () => { + const input = u('root', [ + u('blockquote', [ + u('list', [ + u('listItem', [ + u('table', [ + u('tableRow', [ + u('tableCell', [ + u('heading', { depth: 1 }, [ u('text', 'Validly nested heading text.') ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('list', [ + u('listItem', [ + u('table', [ + u('tableRow', [ + u('tableCell', [ + u('heading', { depth: 1 }, [ u('text', 'Validly nested heading text.') ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + expect(transform(input)).toEqual(output); + }); +}); diff --git a/src/components/Widgets/Markdown/serializers/index.js b/src/components/Widgets/Markdown/serializers/index.js index c66fc890..d7663450 100644 --- a/src/components/Widgets/Markdown/serializers/index.js +++ b/src/components/Widgets/Markdown/serializers/index.js @@ -9,6 +9,7 @@ import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; import rehypePaperEmoji from './rehypePaperEmoji'; +import remarkAssertParents from './remarkAssertParents'; import remarkWrapHtml from './remarkWrapHtml'; import remarkToSlatePlugin from './remarkSlate'; import remarkSquashReferences from './remarkSquashReferences'; @@ -199,10 +200,11 @@ export const htmlToSlate = html => { const mdast = unified() .use(rehypePaperEmoji) - .use(rehypeToRemark) + .use(rehypeToRemark, { minify: false }) .runSync(hast); const slateRaw = unified() + .use(remarkAssertParents) .use(remarkImagesToText) .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) .use(remarkWrapHtml) diff --git a/src/components/Widgets/Markdown/serializers/remarkAssertParents.js b/src/components/Widgets/Markdown/serializers/remarkAssertParents.js new file mode 100644 index 00000000..afbc20b8 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remarkAssertParents.js @@ -0,0 +1,83 @@ +import { concat, last, nth, isEmpty, set } from 'lodash'; +import visitParents from 'unist-util-visit-parents'; + +/** + * remarkUnwrapInvalidNest + * + * Some MDAST node types can only be nested within specific node types - for + * example, a paragraph can't be nested within another paragraph, and a heading + * can't be nested in a "strong" type node. This kind of invalid MDAST can be + * generated by rehype-remark from invalid HTML. + * + * This plugin finds instances of invalid nesting, and unwraps the invalidly + * nested nodes as far up the parental line as necessary, splitting parent nodes + * along the way. The resulting node has no invalidly nested nodes, and all + * validly nested nodes retain their ancestry. Nodes that are emptied as a + * result of unnesting nodes are removed from the tree. + */ +export default function remarkUnwrapInvalidNest() { + return transform; + + function transform(tree) { + const invalidNest = findInvalidNest(tree); + + if (!invalidNest) return tree; + + splitTreeAtNest(tree, invalidNest); + + return transform(tree); + } + + /** + * visitParents uses unist-util-visit-parent to check every node in the + * tree while having access to every ancestor of the node. This is ideal + * for determining whether a block node has an ancestor that should not + * contain a block node. Note that it operates in a mutable fashion. + */ + function findInvalidNest(tree) { + /** + * Node types that are considered "blocks". + */ + const blocks = ['paragraph', 'heading', 'code', 'blockquote', 'list', 'table', 'thematicBreak']; + + /** + * Node types that can contain "block" nodes as direct children. We check + */ + const canContainBlocks = ['root', 'blockquote', 'listItem', 'tableCell']; + + let invalidNest; + + visitParents(tree, (node, parents) => { + const parentType = !isEmpty(parents) && last(parents).type; + const isInvalidNest = blocks.includes(node.type) && !canContainBlocks.includes(parentType); + + if (isInvalidNest) { + invalidNest = concat(parents, node); + return false; + } + }); + + return invalidNest; + } + + function splitTreeAtNest(tree, nest) { + const grandparent = nth(nest, -3) || tree; + const parent = nth(nest, -2); + const node = last(nest); + + const splitIndex = grandparent.children.indexOf(parent); + const splitChildren = grandparent.children; + const splitChildIndex = parent.children.indexOf(node); + + const childrenBefore = parent.children.slice(0, splitChildIndex); + const childrenAfter = parent.children.slice(splitChildIndex + 1); + const nodeBefore = !isEmpty(childrenBefore) && { ...parent, children: childrenBefore }; + const nodeAfter = !isEmpty(childrenAfter) && { ...parent, children: childrenAfter }; + + const childrenToInsert = [nodeBefore, node, nodeAfter].filter(val => !isEmpty(val)); + const beforeChildren = splitChildren.slice(0, splitIndex); + const afterChildren = splitChildren.slice(splitIndex + 1); + const newChildren = concat(beforeChildren, childrenToInsert, afterChildren); + grandparent.children = newChildren; + } +} diff --git a/yarn.lock b/yarn.lock index 97a6a911..2bdc0ff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9036,6 +9036,10 @@ unist-util-stringify-position@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.1.tgz#3ccbdc53679eed6ecf3777dd7f5e3229c1b6aa3c" +unist-util-visit-parents@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-1.1.1.tgz#7d3f56b5b039a3c6e2d16e51cc093f10e4755342" + unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.3.tgz#ec268e731b9d277a79a5b5aa0643990e405d600b" From 0ea62e0f9dfc3df819650e51152d7503dd8cae0f Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 4 Aug 2017 15:57:23 -0400 Subject: [PATCH 79/79] fix rte pasted links with leading/trailing spaces --- .../__tests__/remarkPaddedLinks.spec.js | 45 +++ .../Widgets/Markdown/serializers/index.js | 2 + .../Markdown/serializers/remarkPaddedLinks.js | 120 ++++++ yarn.lock | 380 ++---------------- 4 files changed, 190 insertions(+), 357 deletions(-) create mode 100644 src/components/Widgets/Markdown/serializers/__tests__/remarkPaddedLinks.spec.js create mode 100644 src/components/Widgets/Markdown/serializers/remarkPaddedLinks.js diff --git a/src/components/Widgets/Markdown/serializers/__tests__/remarkPaddedLinks.spec.js b/src/components/Widgets/Markdown/serializers/__tests__/remarkPaddedLinks.spec.js new file mode 100644 index 00000000..40471ec1 --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/__tests__/remarkPaddedLinks.spec.js @@ -0,0 +1,45 @@ +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToMarkdown from 'remark-stringify'; +import remarkPaddedLinks from '../remarkPaddedLinks'; + +const input = markdown => + unified() + .use(markdownToRemark) + .use(remarkPaddedLinks) + .use(remarkToMarkdown) + .processSync(markdown) + .contents; + +const output = markdown => + unified() + .use(markdownToRemark) + .use(remarkToMarkdown) + .processSync(markdown) + .contents; + +describe('remarkPaddedLinks', () => { + it('should move leading and trailing spaces outside of a link', () => { + expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); + }); + + it('should convert multiple leading or trailing spaces to a single space', () => { + expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); + }); + + it('should work with only a leading space or only a trailing space', () => { + expect(input('[ a](b)[c ](d)')).toEqual(output(' [a](b)[c](d) ')); + }); + + it('should work for nested links', () => { + expect(input('* # a[ b ](c)d')).toEqual(output('* # a [b](c) d')); + }); + + it('should work for parents with multiple links that are not siblings', () => { + expect(input('# a[ b ](c)d **[ e ](f)**')).toEqual(output('# a [b](c) d ** [e](f) **')); + }); + + it('should work for links with arbitrarily nested children', () => { + expect(input('[ a __*b*__ _c_ ](d)')).toEqual(output(' [a __*b*__ _c_](d) ')); + }); +}); diff --git a/src/components/Widgets/Markdown/serializers/index.js b/src/components/Widgets/Markdown/serializers/index.js index d7663450..e524db50 100644 --- a/src/components/Widgets/Markdown/serializers/index.js +++ b/src/components/Widgets/Markdown/serializers/index.js @@ -10,6 +10,7 @@ import rehypeToRemark from 'rehype-remark'; import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; import rehypePaperEmoji from './rehypePaperEmoji'; import remarkAssertParents from './remarkAssertParents'; +import remarkPaddedLinks from './remarkPaddedLinks'; import remarkWrapHtml from './remarkWrapHtml'; import remarkToSlatePlugin from './remarkSlate'; import remarkSquashReferences from './remarkSquashReferences'; @@ -205,6 +206,7 @@ export const htmlToSlate = html => { const slateRaw = unified() .use(remarkAssertParents) + .use(remarkPaddedLinks) .use(remarkImagesToText) .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) .use(remarkWrapHtml) diff --git a/src/components/Widgets/Markdown/serializers/remarkPaddedLinks.js b/src/components/Widgets/Markdown/serializers/remarkPaddedLinks.js new file mode 100644 index 00000000..7b2b752b --- /dev/null +++ b/src/components/Widgets/Markdown/serializers/remarkPaddedLinks.js @@ -0,0 +1,120 @@ +import { + get, + set, + find, + findLast, + startsWith, + endsWith, + trimStart, + trimEnd, + concat, + flatMap +} from 'lodash'; +import u from 'unist-builder'; +import toString from 'mdast-util-to-string'; + +/** + * Convert leading and trailing spaces in a link to single spaces outside of the + * link. MDASTs derived from pasted Google Docs HTML require this treatment. + * + * Note that, because we're potentially replacing characters in a link node's + * children with character's in a link node's siblings, we have to operate on a + * parent (link) node and its children at once, rather than just processing + * children one at a time. + */ +export default function remarkPaddedLinks() { + + function transform(node) { + + /** + * Because we're operating on link nodes and their children at once, we can + * exit if the current node has no children. + */ + if (!node.children) return node; + + /** + * Process a node's children if any of them are links. If a node is a link + * with leading or trailing spaces, we'll get back an array of nodes instead + * of a single node, so we use `flatMap` to keep those nodes as siblings + * with the other children. + * + * If performance improvements are found desirable, we could change this to + * only pass in the link nodes instead of the entire array of children, but + * this seems unlikely to produce a noticeable perf gain. + */ + const hasLinkChild = node.children.some(child => child.type === 'link'); + const processedChildren = hasLinkChild ? flatMap(node.children, transformChildren) : node.children; + + /** + * Run all children through the transform recursively. + */ + const children = processedChildren.map(transform); + + return { ...node, children }; + }; + + function transformChildren(node) { + if (node.type !== 'link') return node; + + /** + * Get the node's complete string value, check for leading and trailing + * whitespace, and get nodes from each edge where whitespace is found. + */ + const text = toString(node); + const leadingWhitespaceNode = startsWith(text, ' ') && getEdgeTextChild(node); + const trailingWhitespaceNode = endsWith(text, ' ') && getEdgeTextChild(node, true); + + if (!leadingWhitespaceNode && !trailingWhitespaceNode) return node; + + /** + * Trim the edge nodes in place. Unified handles everything in a mutable + * fashion, so it's often simpler to do the same when working with Unified + * ASTs. + */ + if (leadingWhitespaceNode) { + leadingWhitespaceNode.value = trimStart(leadingWhitespaceNode.value); + } + + if (trailingWhitespaceNode) { + trailingWhitespaceNode.value = trimEnd(trailingWhitespaceNode.value); + } + + /** + * Create an array of nodes. The first and last child will either be `false` + * or a text node. We filter out the false values before returning. + */ + const nodes = [ + leadingWhitespaceNode && u('text', ' '), + node, + trailingWhitespaceNode && u('text', ' ') + ]; + + return nodes.filter(val => val); + } + + /** + * Get the first or last non-blank text child of a node, regardless of + * nesting. If `end` is truthy, get the last node, otherwise first. + */ + function getEdgeTextChild(node, end) { + const findFn = end ? findLast : find; + + let edgeChildWithValue; + setEdgeChildWithValue(node); + return edgeChildWithValue; + + /** + * searchChildren checks a node and all of it's children deeply to find a + * non-blank text value. When the text node is found, we set it in an outside + * variable, as it may be deep in the tree and therefore wouldn't be returned + * by `find`/`findLast`. + */ + function setEdgeChildWithValue(child) { + if (!edgeChildWithValue && child.value) { + edgeChildWithValue = child; + } + findFn(child.children, setEdgeChildWithValue); + } + } + return transform; +} diff --git a/yarn.lock b/yarn.lock index 2bdc0ff8..4aa24746 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,10 +52,6 @@ webpack-dev-middleware "^1.6.0" webpack-hot-middleware "^2.10.0" -"@types/node@^6.0.46": - version "6.0.88" - resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.88.tgz#f618f11a944f6a18d92b5c472028728a3e3d4b66" - "@types/react@>=15": version "16.0.5" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.5.tgz#d713cf67cc211dea20463d2a0b66005c22070c4b" @@ -2141,14 +2137,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-regex@^0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/data-uri-regex/-/data-uri-regex-0.1.4.tgz#1e1db6c8397eca8a48ecdb55ad1b927ec0bbac2e" - -data-uri-to-blob@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/data-uri-to-blob/-/data-uri-to-blob-0.0.4.tgz#087a7bff42f41a6cc0b2e2fb7312a7c29904fbaa" - date-fns@^1.27.2: version "1.28.5" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" @@ -2196,7 +2184,7 @@ default-require-extensions@^1.0.0: dependencies: strip-bom "^2.0.0" -define-properties@^1.1.1, define-properties@^1.1.2: +define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" dependencies: @@ -3574,18 +3562,6 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hast-to-hyperscript@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-3.0.2.tgz#8468ed08b8382f130e003a38ef735bcf29737336" - dependencies: - comma-separated-tokens "^1.0.0" - is-nan "^1.2.1" - kebab-case "^1.0.0" - property-information "^3.0.0" - space-separated-tokens "^1.0.0" - trim "0.0.1" - unist-util-is "^2.0.0" - hast-util-embedded@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hast-util-embedded/-/hast-util-embedded-1.0.0.tgz#49d6114b40933a9d0bd708a3b012378f2cd6e86c" @@ -3621,24 +3597,6 @@ hast-util-parse-selector@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.1.0.tgz#b55c0f4bb7bb2040c889c325ef87ab29c38102b4" -hast-util-raw@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-1.2.0.tgz#99b69c0a3ca307c5472ef372888bf6cdc1f48eaa" - dependencies: - hast-util-from-parse5 "^1.0.0" - hast-util-to-parse5 "^2.0.0" - html-void-elements "^1.0.1" - parse5 "^3.0.0" - unist-util-position "^3.0.0" - web-namespaces "^1.0.0" - zwitch "^1.0.0" - -hast-util-sanitize@^1.0.0, hast-util-sanitize@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-1.1.1.tgz#c439852d9db7ff554ecd6be96435a6a8274ade32" - dependencies: - xtend "^4.0.1" - hast-util-to-html@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-3.1.0.tgz#882c99849e40130e991c042e456d453d95c36cff" @@ -3667,16 +3625,6 @@ hast-util-to-mdast@^1.1.0: unist-util-visit "^1.1.1" xtend "^4.0.1" -hast-util-to-parse5@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-2.2.0.tgz#48c8f7f783020c04c3625db06109d02017033cbc" - dependencies: - hast-to-hyperscript "^3.0.0" - mapz "^1.0.0" - web-namespaces "^1.0.0" - xtend "^4.0.1" - zwitch "^1.0.0" - hast-util-to-string@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-1.0.1.tgz#b28055cdca012d3c8fd048757c8483d0de0d002c" @@ -3789,7 +3737,7 @@ html-tags@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" -html-void-elements@^1.0.0, html-void-elements@^1.0.1: +html-void-elements@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.2.tgz#9d22e0ca32acc95b3f45b8d5b4f6fbdc05affd55" @@ -3885,14 +3833,6 @@ ignore@^3.2.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d" -image-extensions@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/image-extensions/-/image-extensions-1.1.0.tgz#b8e6bf6039df0056e333502a00b6637a3105d894" - -image-to-data-uri@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/image-to-data-uri/-/image-to-data-uri-1.1.0.tgz#23f9d7f17b6562ca6a8145e9779c9a166b829f6e" - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -4087,12 +4027,6 @@ is-ci@^1.0.10, is-ci@^1.0.8: dependencies: ci-info "^1.0.0" -is-data-uri@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-data-uri/-/is-data-uri-0.1.0.tgz#46ee67b63c18c1ffa0bd4dfab2cd2c81c728237f" - dependencies: - data-uri-regex "^0.1.2" - is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" @@ -4175,12 +4109,6 @@ is-hexadecimal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" -is-image@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-image/-/is-image-1.0.1.tgz#6fd51a752a1a111506d060d952118b0b989b426e" - dependencies: - image-extensions "^1.0.1" - is-in-browser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" @@ -4194,12 +4122,6 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" -is-nan@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" - dependencies: - define-properties "^1.1.1" - is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -4324,10 +4246,6 @@ is-unc-path@^0.1.1: dependencies: unc-path-regex "^0.1.0" -is-url@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.2.tgz#498905a593bf47cc2d9e7f738372bbf7696c7f26" - is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -4352,10 +4270,6 @@ is-word-character@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.1.tgz#5a03fa1ea91ace8a6eb0c7cd770eb86d65c8befb" -is@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -4888,12 +4802,6 @@ lie@3.0.2: inline-process-browser "^1.0.0" unreachable-branch-transform "^0.3.0" -linkify-it@~1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a" - dependencies: - uc.micro "^1.0.1" - lint-staged@^3.3.1: version "3.6.1" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.6.1.tgz#24423c8b7bd99d96e15acd1ac8cb392a78e58582" @@ -5021,7 +4929,7 @@ lodash._topath@^3.0.0: dependencies: lodash.isarray "^3.0.0" -lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0: +lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" @@ -5224,10 +5132,6 @@ lru-cache@^4.0.0, lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" -ltrim@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/ltrim/-/ltrim-0.0.3.tgz#fb45cd0789bde93f1523d12ce5adbeb4af7e59d4" - macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" @@ -5256,45 +5160,14 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" -mapz@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mapz/-/mapz-1.0.1.tgz#9ecec757d3c3fe0a8a6f363e328eaee69a428441" - dependencies: - x-is-array "^0.1.0" - markdown-escapes@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.1.tgz#1994df2d3af4811de59a6714934c2b2292734518" -markdown-it@^6.0.4: - version "6.1.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-6.1.1.tgz#ced037f4473ee9f5153ac414f77dc83c91ba927c" - dependencies: - argparse "^1.0.7" - entities "~1.1.1" - linkify-it "~1.2.2" - mdurl "~1.0.1" - uc.micro "^1.0.1" - markdown-table@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.1.tgz#4b3dd3a133d1518b8ef0dbc709bf2a1b4824bc8c" -markup-it@^2.0.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/markup-it/-/markup-it-2.5.0.tgz#239bb2f445b4c8664af8527168ee3c00bc0d451c" - dependencies: - escape-string-regexp "^1.0.5" - html-entities "^1.2.0" - htmlparser2 "^3.9.0" - immutable "^3.7.6" - is "^3.1.0" - ltrim "0.0.3" - object-values "^1.0.0" - range-utils "^1.1.0" - rtrim "0.0.3" - yargs "^4.7.1" - material-design-icons@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf" @@ -5327,7 +5200,7 @@ mdast-util-definitions@^1.2.0, mdast-util-definitions@^1.2.2: dependencies: unist-util-visit "^1.0.0" -mdast-util-to-hast@^2.1.1, mdast-util-to-hast@^2.2.0: +mdast-util-to-hast@^2.2.0: version "2.4.2" resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-2.4.2.tgz#f116e8bf3da772ba5a397a92dab090f5ba91caa0" dependencies: @@ -5343,9 +5216,9 @@ mdast-util-to-hast@^2.1.1, mdast-util-to-hast@^2.2.0: unist-util-visit "^1.1.0" xtend "^4.0.1" -mdurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" +mdast-util-to-string@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.0.4.tgz#5c455c878c9355f0c1e7f3e8b719cf583691acfb" media-typer@0.3.0: version "0.3.0" @@ -5435,7 +5308,7 @@ miller-rabin@^4.0.0: version "1.29.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" -mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: version "2.1.16" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" dependencies: @@ -5881,10 +5754,6 @@ object-keys@^1.0.10, object-keys@^1.0.6, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" -object-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/object-values/-/object-values-1.0.0.tgz#72af839630119e5b98c3b02bb8c27e3237158105" - object.assign@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" @@ -5991,10 +5860,6 @@ ora@^0.2.1, ora@^0.2.3: cli-spinners "^0.1.2" object-assign "^4.0.1" -orderedmap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba" - original@>=0.0.5: version "1.0.0" resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" @@ -6119,12 +5984,6 @@ parse5@^2.1.5: version "2.2.3" resolved "https://registry.yarnpkg.com/parse5/-/parse5-2.2.3.tgz#0c4fc41c1000c5e6b93d48b03f8083837834e9f6" -parse5@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" - dependencies: - "@types/node" "^6.0.46" - parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" @@ -6920,91 +6779,6 @@ property-information@^3.0.0, property-information@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-3.2.0.tgz#fd1483c8fbac61808f5fe359e7693a1f48a58331" -prosemirror-commands@^0.17.0: - version "0.17.1" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-0.17.1.tgz#c27f74f76230a41e26620bfcda7e1e34b1f99dbf" - dependencies: - prosemirror-model "^0.17.0" - prosemirror-state "^0.17.0" - prosemirror-transform "^0.17.0" - -prosemirror-history@^0.17.0: - version "0.17.2" - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-0.17.2.tgz#2ebdd52b606a48eb49f0f608b561ebec2a4eaf11" - dependencies: - prosemirror-state "^0.17.0" - prosemirror-transform "^0.17.0" - rope-sequence "^1.2.0" - -prosemirror-inputrules@^0.17.0: - version "0.17.1" - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-0.17.1.tgz#eb0eb909b9509448cba838a3443cb5d32e9b1e99" - dependencies: - prosemirror-state "^0.17.0" - prosemirror-transform "^0.17.0" - -prosemirror-keymap@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-0.17.0.tgz#760ed65586e6116079730b68f46bff0ba0c19031" - dependencies: - prosemirror-state "^0.17.0" - w3c-keyname "^1.1.0" - -prosemirror-markdown@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-0.17.0.tgz#56ff74671e01b0f6109bf73b0abe98196151a3c7" - dependencies: - markdown-it "^6.0.4" - prosemirror-model "~0.17.0" - -prosemirror-model@^0.17.0, prosemirror-model@~0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-0.17.0.tgz#ba81887038290833b032fd4e70ee526b07ca8ebf" - dependencies: - orderedmap "^1.0.0" - -prosemirror-schema-basic@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-0.17.0.tgz#4f0e16459d68fb3ad909d620a97e554fe11ecbe8" - dependencies: - prosemirror-model "^0.17.0" - -prosemirror-schema-list@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-0.17.0.tgz#fd78f7ecb5652913cf4e1f322845730b6eefbe28" - dependencies: - prosemirror-model "^0.17.0" - prosemirror-transform "^0.17.0" - -prosemirror-schema-table@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-table/-/prosemirror-schema-table-0.17.0.tgz#60cb24c57a96c1c99147073d6980440fc929f5ce" - dependencies: - prosemirror-model "^0.17.0" - prosemirror-state "^0.17.0" - prosemirror-transform "^0.17.0" - -prosemirror-state@^0.17.0: - version "0.17.1" - resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-0.17.1.tgz#712efb9485a945d4b42d01ce63fd116472e16038" - dependencies: - prosemirror-model "^0.17.0" - prosemirror-transform "^0.17.0" - -prosemirror-transform@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-0.17.0.tgz#565b53f533fa30c7292354f8ba09f18916e5d939" - dependencies: - prosemirror-model "^0.17.0" - -prosemirror-view@^0.17.0: - version "0.17.7" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-0.17.7.tgz#0e4007658d0f16c280173a57ed57a4a5d7a47bf2" - dependencies: - prosemirror-model "^0.17.0" - prosemirror-state "^0.17.0" - prosemirror-transform "^0.17.0" - proxy-addr@~1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" @@ -7096,13 +6870,6 @@ range-parser@^1.0.3, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" -range-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/range-utils/-/range-utils-1.1.0.tgz#b27a9e8669d76eab7f7611f3120b39655b2e9495" - dependencies: - extend "^3.0.0" - is "^3.1.0" - rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: version "1.2.1" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" @@ -7636,31 +7403,12 @@ rehype-parse@^3.1.0: parse5 "^2.1.5" xtend "^4.0.1" -rehype-raw@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-1.0.0.tgz#6f2f8ebe6858f8304dfe2704ccc6cb29ae8d858e" - dependencies: - hast-util-raw "^1.0.0" - -rehype-react@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-3.0.1.tgz#a54f3a40969a059b5b9aa7c591b13e0344a8bc60" - dependencies: - has "^1.0.1" - hast-to-hyperscript "^3.0.0" - rehype-remark@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/rehype-remark/-/rehype-remark-2.1.0.tgz#84cadd41410d23de8f83e141e92342c2df94c1c8" dependencies: hast-util-to-mdast "^1.1.0" -rehype-sanitize@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-2.0.1.tgz#ab2866cacc51b45c30696cfee7b7b30cf918465e" - dependencies: - hast-util-sanitize "^1.1.0" - rehype-stringify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-3.0.0.tgz#9fef0868213c2dce2f780b76f3d0488c85e819eb" @@ -7668,15 +7416,6 @@ rehype-stringify@^3.0.0: hast-util-to-html "^3.0.0" xtend "^4.0.1" -remark-html@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/remark-html/-/remark-html-6.0.1.tgz#5094d2c71f7941fdb2ae865bac76627757ce09c1" - dependencies: - hast-util-sanitize "^1.0.0" - hast-util-to-html "^3.0.0" - mdast-util-to-hast "^2.1.1" - xtend "^4.0.1" - remark-parse@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-3.0.1.tgz#1b9f841a44d8f4fbf2246850265459a4eb354c80" @@ -7865,14 +7604,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -rope-sequence@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce" - -rtrim@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/rtrim/-/rtrim-0.0.3.tgz#5385688397601728335d77b15dfd49e11c2ac099" - run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -7957,10 +7688,6 @@ selection-is-backward@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1" -selection-position@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/selection-position/-/selection-position-1.0.0.tgz#e43f87151d94957efa170e10e02c901b47f703c7" - selfsigned@^1.9.1: version "1.10.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52" @@ -8107,22 +7834,20 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" -slate-drop-or-paste-images@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.2.0.tgz#a866d9c19155cef7e21ef72075cd624eb63dc189" - dependencies: - data-uri-to-blob "0.0.4" - image-to-data-uri "^1.0.0" - is-data-uri "^0.1.0" - is-image "^1.0.1" - is-url "^1.2.2" - mime-types "^2.1.11" +slate-edit-list@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.7.1.tgz#84ee960d2d5b5a20ce267ad9df894395a91b93d5" -slate@^0.20.3: - version "0.20.7" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.20.7.tgz#083ca9074dc7fd3ad8863985e6d92ed76bdc9eff" +slate-edit-table@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/slate-edit-table/-/slate-edit-table-0.10.1.tgz#4f01bf26bac2de26e8d25bfbe7116a2f643e5934" + dependencies: + immutable "^3.8.1" + +slate@^0.21.0: + version "0.21.4" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.21.4.tgz#ae6113379cd838b7ec68ecd94834ce9741bc36f3" dependencies: - cheerio "^0.22.0" debug "^2.3.2" direction "^0.1.5" es6-map "^0.1.4" @@ -8133,6 +7858,7 @@ slate@^0.20.3: is-in-browser "^1.1.3" is-window "^1.0.2" keycode "^2.1.2" + lodash "^4.17.4" prop-types "^15.5.8" react-portal "^3.1.0" selection-is-backward "^1.0.0" @@ -8761,10 +8487,6 @@ text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" -textarea-caret-position@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/textarea-caret-position/-/textarea-caret-position-0.1.1.tgz#34372aa755008b1a8451b8a1bb8f07795270fd70" - throat@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/throat/-/throat-2.0.2.tgz#a9fce808b69e133a632590780f342c30a6249b02" @@ -8919,10 +8641,6 @@ ua-parser-js@^0.7.9: version "0.7.14" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" -uc.micro@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" - uglify-js@^2.6, uglify-js@^2.8.27: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -8996,7 +8714,7 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" -unist-builder@^1.0.1: +unist-builder@^1.0.1, unist-builder@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-1.0.2.tgz#8c3b9903ef64bcfb117dd7cf6a5d98fc1b3b27b6" dependencies: @@ -9010,13 +8728,7 @@ unist-util-is@^2.0.0, unist-util-is@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" -unist-util-map@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/unist-util-map/-/unist-util-map-1.0.3.tgz#26a913d7cddb3cd3e9a886d135d37a3d1f54e514" - dependencies: - object-assign "^4.0.1" - -unist-util-modify-children@^1.0.0, unist-util-modify-children@^1.1.1: +unist-util-modify-children@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d" dependencies: @@ -9197,10 +8909,6 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" -w3c-keyname@^1.1.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.6.tgz#f835231f26b36cf4fb2f7aa3ee3be3db266d4b35" - walkdir@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532" @@ -9249,10 +8957,6 @@ wbuf@^1.1.0, wbuf@^1.7.2: dependencies: minimalistic-assert "^1.0.0" -web-namespaces@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.1.tgz#742d9fff61ff84f4164f677244f42d29c10c451d" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -9447,10 +9151,6 @@ window-size@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" -window-size@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" - wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -9499,10 +9199,6 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -x-is-array@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/x-is-array/-/x-is-array-0.1.0.tgz#de520171d47b3f416f5587d629b89d26b12dc29d" - x-is-function@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/x-is-function/-/x-is-function-1.0.4.tgz#5d294dc3d268cbdd062580e0c5df77a391d1fa1e" @@ -9531,13 +9227,6 @@ yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" -yargs-parser@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" - dependencies: - camelcase "^3.0.0" - lodash.assign "^4.0.6" - yargs-parser@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" @@ -9566,25 +9255,6 @@ yargs@^3.5.4: window-size "^0.1.4" y18n "^3.2.0" -yargs@^4.7.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" - dependencies: - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - lodash.assign "^4.0.3" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.1" - which-module "^1.0.0" - window-size "^0.2.0" - y18n "^3.2.1" - yargs-parser "^2.4.1" - yargs@^6.0.0: version "6.6.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" @@ -9629,7 +9299,3 @@ yargs@~3.10.0: cliui "^2.1.0" decamelize "^1.0.0" window-size "0.1.0" - -zwitch@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.2.tgz#9b059541bfa844799fe2d903bde609de2503a041"