Convert markdown-prosemirror parser/compiler to Remark
This commit is contained in:
parent
24c0a1bdb4
commit
0eb109cb73
@ -167,6 +167,7 @@
|
|||||||
"slate-drop-or-paste-images": "^0.2.0",
|
"slate-drop-or-paste-images": "^0.2.0",
|
||||||
"slug": "^0.9.1",
|
"slug": "^0.9.1",
|
||||||
"textarea-caret-position": "^0.1.1",
|
"textarea-caret-position": "^0.1.1",
|
||||||
|
"unist-util-visit": "^1.1.1",
|
||||||
"uuid": "^2.0.3",
|
"uuid": "^2.0.3",
|
||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -23,8 +23,8 @@ export default class MarkupItReactRenderer extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { value } = this.props;
|
const { value } = this.props;
|
||||||
const mast = remark.parse(value);
|
const mdast = remark.parse(value);
|
||||||
const hast = toHAST(mast, { allowDangerousHTML: true });
|
const hast = toHAST(mdast, { allowDangerousHTML: true });
|
||||||
const html = hastToHTML(hast, { allowDangerousHTML: true });
|
const html = hastToHTML(hast, { allowDangerousHTML: true });
|
||||||
return <div dangerouslySetInnerHTML={{ __html: html }} />; // eslint-disable-line react/no-danger
|
return <div dangerouslySetInnerHTML={{ __html: html }} />; // eslint-disable-line react/no-danger
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
`;
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -5,8 +5,13 @@ import { EditorState } from 'prosemirror-state';
|
|||||||
import { EditorView } from 'prosemirror-view';
|
import { EditorView } from 'prosemirror-view';
|
||||||
import history from 'prosemirror-history';
|
import history from 'prosemirror-history';
|
||||||
import {
|
import {
|
||||||
blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule,
|
blockQuoteRule,
|
||||||
inputRules, allInputRules,
|
orderedListRule,
|
||||||
|
bulletListRule,
|
||||||
|
codeBlockRule,
|
||||||
|
headingRule,
|
||||||
|
inputRules,
|
||||||
|
allInputRules,
|
||||||
} from 'prosemirror-inputrules';
|
} from 'prosemirror-inputrules';
|
||||||
import { keymap } from 'prosemirror-keymap';
|
import { keymap } from 'prosemirror-keymap';
|
||||||
import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||||
@ -56,20 +61,26 @@ function schemaWithPlugins(schema, plugins) {
|
|||||||
let nodeSpec = schema.nodeSpec;
|
let nodeSpec = schema.nodeSpec;
|
||||||
plugins.forEach((plugin) => {
|
plugins.forEach((plugin) => {
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
plugin.get('fields').forEach((field) => {
|
plugin.get("fields").forEach((field) => {
|
||||||
attrs[field.get('name')] = { default: null };
|
attrs[field.get("name")] = { default: null };
|
||||||
});
|
});
|
||||||
nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, {
|
nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get("id") }`, {
|
||||||
attrs,
|
attrs,
|
||||||
group: 'block',
|
group: "block",
|
||||||
parseDOM: [{
|
parseDOM: [
|
||||||
tag: 'div[data-plugin]',
|
{
|
||||||
|
tag: "div[data-plugin]",
|
||||||
getAttrs(dom) {
|
getAttrs(dom) {
|
||||||
return JSON.parse(dom.getAttribute('data-plugin'));
|
return JSON.parse(dom.getAttribute("data-plugin"));
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
toDOM(node) {
|
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) {
|
function createSerializer(schema, plugins) {
|
||||||
const serializer = Object.create(defaultMarkdownSerializer);
|
const serializer = Object.create(defaultMarkdownSerializer);
|
||||||
plugins.forEach((plugin) => {
|
plugins.forEach((plugin) => {
|
||||||
serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => {
|
serializer.nodes[`plugin_${ plugin.get("id") }`] = (state, node) => {
|
||||||
const toBlock = plugin.get('toBlock');
|
const toBlock = plugin.get("toBlock");
|
||||||
state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`);
|
state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -159,17 +170,31 @@ export default class Editor extends Component {
|
|||||||
const { schema, selection } = state;
|
const { schema, selection } = state;
|
||||||
if (selection.from === selection.to) {
|
if (selection.from === selection.to) {
|
||||||
const { $from } = selection;
|
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 pos = this.view.coordsAtPos(selection.from);
|
||||||
const editorPos = this.view.content.getBoundingClientRect();
|
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 });
|
this.setState({ selectionPosition });
|
||||||
|
} else {
|
||||||
|
this.setState({ showToolbar: false, showBlockMenu: false });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pos = this.view.coordsAtPos(selection.from);
|
const pos = this.view.coordsAtPos(selection.from);
|
||||||
const editorPos = this.view.content.getBoundingClientRect();
|
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 });
|
this.setState({ selectionPosition });
|
||||||
|
const selectionPosition = {
|
||||||
|
top: pos.top - editorPos.top,
|
||||||
|
left: pos.left - editorPos.left,
|
||||||
|
};
|
||||||
|
this.setState({ selectionPosition });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,8 +202,7 @@ export default class Editor extends Component {
|
|||||||
this.ref = ref;
|
this.ref = ref;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHeader = level => (
|
handleHeader = level => () => {
|
||||||
() => {
|
|
||||||
const { schema } = this.state;
|
const { schema } = this.state;
|
||||||
const state = this.view.state;
|
const state = this.view.state;
|
||||||
const { $from, to, node } = state.selection;
|
const { $from, to, node } = state.selection;
|
||||||
@ -195,8 +219,7 @@ export default class Editor extends Component {
|
|||||||
|
|
||||||
const command = setBlockType(nodeType, { level });
|
const command = setBlockType(nodeType, { level });
|
||||||
command(state, this.handleAction);
|
command(state, this.handleAction);
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
handleBold = () => {
|
handleBold = () => {
|
||||||
const command = toggleMark(this.state.schema.marks.strong);
|
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)) {
|
if (!markActive(this.view.state, this.state.schema.marks.link)) {
|
||||||
url = prompt('Link URL:'); // eslint-disable-line no-alert
|
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);
|
command(this.view.state, this.handleAction);
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePluginSubmit = (plugin, data) => {
|
handlePluginSubmit = (plugin, data) => {
|
||||||
const { schema } = this.state;
|
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())).action());
|
this.view.props.onAction(
|
||||||
|
this.view.state.tr
|
||||||
|
.replaceSelectionWith(nodeType.create(data.toJS()))
|
||||||
|
.action()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDragEnter = (e) => {
|
handleDragEnter = (e) => {
|
||||||
@ -248,31 +277,40 @@ export default class Editor extends Component {
|
|||||||
|
|
||||||
if (e.dataTransfer.files && e.dataTransfer.files.length) {
|
if (e.dataTransfer.files && e.dataTransfer.files.length) {
|
||||||
Array.from(e.dataTransfer.files).forEach((file) => {
|
Array.from(e.dataTransfer.files).forEach((file) => {
|
||||||
createAssetProxy(file.name, file)
|
createAssetProxy(file.name, file).then((assetProxy) => {
|
||||||
.then((assetProxy) => {
|
|
||||||
this.props.onAddAsset(assetProxy);
|
this.props.onAddAsset(assetProxy);
|
||||||
if (file.type.split('/')[0] === 'image') {
|
if (file.type.split("/")[0] === "image") {
|
||||||
nodes.push(
|
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 {
|
} else {
|
||||||
nodes.push(
|
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 {
|
} 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) => {
|
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 = () => {
|
handleToggle = () => {
|
||||||
this.props.onMode('raw');
|
this.props.onMode("raw");
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -283,7 +321,8 @@ export default class Editor extends Component {
|
|||||||
classNames.push(styles.dragging);
|
classNames.push(styles.dragging);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div
|
return (
|
||||||
|
<div
|
||||||
className={classNames.join(' ')}
|
className={classNames.join(' ')}
|
||||||
onDragEnter={this.handleDragEnter}
|
onDragEnter={this.handleDragEnter}
|
||||||
onDragLeave={this.handleDragLeave}
|
onDragLeave={this.handleDragLeave}
|
||||||
@ -312,7 +351,8 @@ export default class Editor extends Component {
|
|||||||
</Sticky>
|
</Sticky>
|
||||||
<div ref={this.handleRef} />
|
<div ref={this.handleRef} />
|
||||||
<div className={styles.shim} />
|
<div className={styles.shim} />
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,256 +2,90 @@
|
|||||||
/*
|
/*
|
||||||
Based closely on
|
Based closely on
|
||||||
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js
|
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")
|
const {Mark} = require("prosemirror-model")
|
||||||
|
|
||||||
function maybeMerge(a, b) {
|
let schema
|
||||||
if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks))
|
|
||||||
return a.copy(a.text + b.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pluginHandler(schema, plugins) {
|
// Setup Remark.
|
||||||
return (type, attrs, content) => {
|
const remark = new Remark({
|
||||||
if (type.name === 'paragraph' && content.length === 1 && content[0].type.name === 'text') {
|
commonmark: true,
|
||||||
const text = content[0].text;
|
footnotes: true,
|
||||||
const plugin = plugins.find(plugin => plugin.get('pattern').test(text));
|
pedantic: true,
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object used to track the context of a running parse.
|
const processMdastNode = (node) => {
|
||||||
class MarkdownParseState {
|
console.log('processMdastNode', node)
|
||||||
constructor(schema, plugins, tokenHandlers) {
|
if (node.type === 'root') {
|
||||||
this.schema = schema
|
const content = node.children.map((childNode) => (
|
||||||
this.stack = [{type: schema.nodes.doc, content: []}]
|
processMdastNode(childNode)
|
||||||
this.marks = Mark.none
|
))
|
||||||
this.tokenHandlers = tokenHandlers
|
return schema.node('doc', {}, content)
|
||||||
this.pluginHandler = pluginHandler(schema, plugins);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
top() {
|
/***
|
||||||
return this.stack[this.stack.length - 1]
|
* Block nodes
|
||||||
}
|
***/
|
||||||
|
if (node.type === 'heading') {
|
||||||
push(elt) {
|
const content = node.children.map((childNode) => (
|
||||||
if (this.stack.length) this.top().content.push(elt)
|
processMdastNode(childNode)
|
||||||
}
|
))
|
||||||
|
console.log(content)
|
||||||
// : (string)
|
return schema.node('heading', { level: node.depth }, content)
|
||||||
// Adds the given text to the current position in the document,
|
} else if (node.type === 'paragraph') {
|
||||||
// using the current marks as styling.
|
const content = node.children.map((childNode) => (
|
||||||
addText(text) {
|
processMdastNode(childNode)
|
||||||
if (!text) return
|
))
|
||||||
let nodes = this.top().content, last = nodes[nodes.length - 1]
|
return schema.node('paragraph', {}, content)
|
||||||
let node = this.schema.text(text, this.marks), merged
|
} else if (node.type === 'list') {
|
||||||
if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged
|
const content = node.children.map((childNode) => (
|
||||||
else nodes.push(node)
|
processMdastNode(childNode)
|
||||||
}
|
))
|
||||||
|
if (node.ordered) {
|
||||||
// : (Mark)
|
return schema.node('ordered_list', { tight: true, order: 1 }, content)
|
||||||
// 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 {
|
} else {
|
||||||
handlers[type + "_open"] = (state, tok) => state.openNode(nodeType, attrs(spec.attrs, tok))
|
return schema.node('bullet_list', {}, content)
|
||||||
handlers[type + "_close"] = state => state.closeNode()
|
|
||||||
}
|
}
|
||||||
} else if (spec.node) {
|
} else if (node.type === 'listItem') {
|
||||||
let nodeType = schema.nodeType(spec.node)
|
const content = node.children.map((childNode) => (
|
||||||
handlers[type] = (state, tok) => state.addNode(nodeType, attrs(spec.attrs, tok))
|
processMdastNode(childNode)
|
||||||
} else if (spec.mark) {
|
))
|
||||||
let markType = schema.marks[spec.mark]
|
return schema.node('list_item', {}, content)
|
||||||
if (noOpenClose(type)) {
|
} else if (node.type === 'thematicBreak') {
|
||||||
handlers[type] = (state, tok) => {
|
return schema.node('horizontal_rule')
|
||||||
state.openMark(markType.create(attrs(spec.attrs, tok)))
|
} else if (node.type === 'break') {
|
||||||
state.addText(withoutTrailingNewline(tok.content))
|
return schema.node('hard_break')
|
||||||
state.closeMark(markType)
|
} else if (node.type === 'image') {
|
||||||
}
|
return schema.node('image', { src: node.url, alt: node.alt })
|
||||||
} else {
|
|
||||||
handlers[type + "_open"] = (state, tok) => state.openMark(markType.create(attrs(spec.attrs, tok)))
|
|
||||||
handlers[type + "_close"] = state => state.closeMark(markType)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec))
|
|
||||||
}
|
}
|
||||||
|
/***
|
||||||
|
* 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ;; 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<Object, (MarkdownToken) → Object>`
|
|
||||||
// : 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
|
return doc
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// :: MarkdownParser
|
const compileMarkdownToProseMirror = (src) => {
|
||||||
// A parser parsing unextended [CommonMark](http://commonmark.org/),
|
console.log(src)
|
||||||
// without inline HTML, and producing a document in the basic schema.
|
const mdast = remark.parse(src)
|
||||||
export default function createMarkdownParser(schema, plugins) {
|
console.log(mdast)
|
||||||
const tokens = {
|
const doc = processMdastNode(mdast)
|
||||||
blockquote: {block: "blockquote"},
|
console.log(doc.content)
|
||||||
paragraph: {block: "paragraph"},
|
return doc
|
||||||
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
|
module.exports = (s, plugins) => {
|
||||||
// to avoid tha.
|
//console.log(s)
|
||||||
bullet_list: {block: "bullet_list", attrs: tok => ({tight: true})},
|
//console.log(s.nodes.code_block.create({ params: { language: 'javascript' } }))
|
||||||
ordered_list: {block: "ordered_list", attrs: tok => ({tight: true, order: +tok.attrGet("order") || 1})},
|
schema = s
|
||||||
heading: {block: "heading", attrs: tok => ({level: +tok.tag.slice(1)})},
|
return compileMarkdownToProseMirror
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user