Replace md parser for pubstorm to make editor plugins work
This commit is contained in:
parent
038597573c
commit
8461abc1b5
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Schema } from 'prosemirror-model';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
@ -8,7 +8,6 @@ import {
|
||||
inputRules, allInputRules,
|
||||
} from 'prosemirror-inputrules';
|
||||
import { keymap } from 'prosemirror-keymap';
|
||||
import { replaceWith } from 'prosemirror-transform';
|
||||
import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
||||
import registry from '../../../../lib/registry';
|
||||
@ -28,13 +27,23 @@ function processUrl(url) {
|
||||
return `/${ url }`;
|
||||
}
|
||||
|
||||
const ruleset = {
|
||||
blockquote: [blockQuoteRule],
|
||||
ordered_list: [orderedListRule],
|
||||
bullet_list: [bulletListRule],
|
||||
code_block: [codeBlockRule],
|
||||
heading: [headingRule, 6],
|
||||
};
|
||||
|
||||
function buildInputRules(schema) {
|
||||
let result = [], type;
|
||||
if (type = schema.nodes.blockquote) result.push(blockQuoteRule(type));
|
||||
if (type = schema.nodes.ordered_list) result.push(orderedListRule(type));
|
||||
if (type = schema.nodes.bullet_list) result.push(bulletListRule(type));
|
||||
if (type = schema.nodes.code_block) result.push(codeBlockRule(type));
|
||||
if (type = schema.nodes.heading) result.push(headingRule(type, 6));
|
||||
const result = [];
|
||||
for (const rule in ruleset) {
|
||||
const type = schema.nodes[rule];
|
||||
if (type) {
|
||||
const fn = ruleset[rule];
|
||||
result.push(fn[0].apply(fn.slice(1)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -93,16 +102,17 @@ export default class Editor extends Component {
|
||||
this.state = {
|
||||
plugins,
|
||||
schema: s,
|
||||
parser: createMarkdownParser(s),
|
||||
parser: createMarkdownParser(s, plugins),
|
||||
serializer: createSerializer(s, plugins),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { schema, parser } = this.state;
|
||||
const doc = parser.parse(this.props.value || '');
|
||||
this.view = new EditorView(this.ref, {
|
||||
state: EditorState.create({
|
||||
doc: parser.parse(this.props.value || ''),
|
||||
doc,
|
||||
schema,
|
||||
plugins: [
|
||||
inputRules({
|
||||
@ -235,3 +245,12 @@ export default class Editor extends Component {
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
Editor.propTypes = {
|
||||
onAddMedia: PropTypes.func.isRequired,
|
||||
onRemoveMedia: PropTypes.func.isRequired,
|
||||
getMedia: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMode: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
@ -1,8 +1,230 @@
|
||||
import { MarkdownParser } from 'prosemirror-markdown';
|
||||
import markdownit from 'markdown-it';
|
||||
/* eslint-disable */
|
||||
/*
|
||||
Based closely on
|
||||
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js
|
||||
|
||||
export default function createMarkdownParser(schema) {
|
||||
return new MarkdownParser(schema, markdownit("commonmark", {html: false}), {
|
||||
Adds a bit of logic allowing editor plugins to hook into the parsing.
|
||||
*/
|
||||
|
||||
const markdownit = require("markdown-it")
|
||||
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)
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// :: 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"},
|
||||
@ -26,5 +248,7 @@ export default function createMarkdownParser(schema) {
|
||||
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