2016-11-04 11:04:54 -07:00
|
|
|
/* eslint-disable */
|
|
|
|
/*
|
|
|
|
Based closely on
|
|
|
|
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js
|
2016-11-01 23:31:20 -07:00
|
|
|
|
2016-11-04 11:04:54 -07:00
|
|
|
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 = {
|
2016-11-01 23:31:20 -07:00
|
|
|
blockquote: {block: "blockquote"},
|
|
|
|
paragraph: {block: "paragraph"},
|
|
|
|
list_item: {block: "list_item"},
|
2016-12-27 23:19:00 -08:00
|
|
|
// 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})},
|
2016-11-01 23:31:20 -07:00
|
|
|
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"}
|
2016-11-04 11:04:54 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
return new MarkdownParser(schema, plugins, markdownit("commonmark", {html: false}), tokens);
|
2016-11-01 23:31:20 -07:00
|
|
|
}
|