feat: update prosemirror-markdown (#37698)

This commit is contained in:
Ifiok Jr 2019-08-22 18:58:58 +01:00 committed by Sheetal Nandi
parent 859c748181
commit ef3fae5058
2 changed files with 395 additions and 148 deletions

View File

@ -4,11 +4,57 @@
// David Hahn <https://github.com/davidka>
// Tim Baumann <https://github.com/timjb>
// Patrick Simmelbauer <https://github.com/patsimm>
// Ifiokj Jr. <https://github.com/ifiokjr>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
import MarkdownIt = require('markdown-it');
import { Node as ProsemirrorNode, Schema } from 'prosemirror-model';
import Token = require('markdown-it/lib/token');
import { Fragment, Mark, Node as ProsemirrorNode, Schema } from 'prosemirror-model';
export interface TokenConfig {
/**
* 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.
*/
node?: 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.
*/
mark?: 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.
*/
block?: string;
/**
* Attributes for the node or mark. When `getAttrs` is provided,
* it takes precedence.
*/
attrs?: Record<string, any>;
/**
* A function used to compute the attributes for the node or mark
* that takes a [markdown-it
* token](https://markdown-it.github.io/markdown-it/#Token) and
* returns an attribute object.
*/
getAttrs?(token: Token): Record<string, any>;
/**
* When true, ignore content for the matched token.
*/
ignore?: boolean;
}
/**
* A configuration of a Markdown parser. Such a parser uses
@ -17,93 +63,110 @@ import { Node as ProsemirrorNode, Schema } from 'prosemirror-model';
* the tokens to create a ProseMirror document tree.
*/
export class MarkdownParser<S extends Schema = any> {
/**
* 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`**`: ?Object`
* : Attributes for the node or mark. When `getAttrs` is provided,
* it takes precedence.
*
* **`getAttrs`**`: ?(MarkdownToken) → Object`
* : A function used to compute the attributes for the node or mark
* that takes a [markdown-it
* token](https://markdown-it.github.io/markdown-it/#Token) and
* returns an attribute object.
*
* **`ignore`**`: ?bool`
* : When true, ignore content for the matched token.
*/
constructor(schema: S, tokenizer: MarkdownIt, tokens: { [key: string]: any });
/**
* The value of the `tokens` object used to construct
* this parser. Can be useful to copy and modify to base other
* parsers on.
*/
tokens: { [key: string]: any };
/**
* Parse a string as [CommonMark](http://commonmark.org/) markup,
* and create a ProseMirror document as prescribed by this parser's
* rules.
*/
parse(text: string): ProsemirrorNode<S>;
/**
* 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`**`: ?Object`
* : Attributes for the node or mark. When `getAttrs` is provided,
* it takes precedence.
*
* **`getAttrs`**`: ?(MarkdownToken) → Object`
* : A function used to compute the attributes for the node or mark
* that takes a [markdown-it
* token](https://markdown-it.github.io/markdown-it/#Token) and
* returns an attribute object.
*
* **`ignore`**`: ?bool`
* : When true, ignore content for the matched token.
*/
constructor(schema: S, tokenizer: MarkdownIt, tokens: { [key: string]: TokenConfig });
/**
* The value of the `tokens` object used to construct
* this parser. Can be useful to copy and modify to base other
* parsers on.
*/
tokens: { [key: string]: Token };
/**
* Parse a string as [CommonMark](http://commonmark.org/) markup,
* and create a ProseMirror document as prescribed by this parser's
* rules.
*/
parse(text: string): ProsemirrorNode<S>;
}
/**
* A parser parsing unextended [CommonMark](http://commonmark.org/),
* without inline HTML, and producing a document in the basic schema.
*/
export let defaultMarkdownParser: MarkdownParser;
export type MarkSerializerMethod<S extends Schema = any> = (
state: MarkdownSerializerState<S>,
mark: Mark<S>,
parent: Fragment<S>,
index: number,
) => void;
export interface MarkSerializerConfig<S extends Schema = any> {
open: string | MarkSerializerMethod<S>;
close: string | MarkSerializerMethod<S>;
mixable?: boolean;
expelEnclosingWhitespace?: boolean;
escape?: boolean;
}
/**
* A specification for serializing a ProseMirror document as
* Markdown/CommonMark text.
*/
export class MarkdownSerializer<S extends Schema = any> {
constructor(
nodes: {
[name: string]: (
state: MarkdownSerializerState<S>,
node: ProsemirrorNode<S>,
parent: ProsemirrorNode<S>,
index: number
) => void;
},
marks: { [key: string]: any }
);
/**
* The node serializer
* functions for this serializer.
*/
nodes: { [name: string]: (p1: MarkdownSerializerState<S>, p2: ProsemirrorNode<S>) => void };
/**
* The mark serializer info.
*/
marks: { [key: string]: any };
/**
* Serialize the content of the given node to
* [CommonMark](http://commonmark.org/).
*/
serialize(content: ProsemirrorNode<S>, options?: { [key: string]: any }): string;
constructor(
nodes: {
[name: string]: (
state: MarkdownSerializerState<S>,
node: ProsemirrorNode<S>,
parent: ProsemirrorNode<S>,
index: number,
) => void;
},
marks: {
[key: string]: MarkSerializerConfig;
},
);
/**
* The node serializer
* functions for this serializer.
*/
nodes: { [name: string]: (p1: MarkdownSerializerState<S>, p2: ProsemirrorNode<S>) => void };
/**
* The mark serializer info.
*/
marks: { [key: string]: any };
/**
* Serialize the content of the given node to
* [CommonMark](http://commonmark.org/).
*/
serialize(content: ProsemirrorNode<S>, options?: { [key: string]: any }): string;
}
/**
* A serializer for the [basic schema](#schema).
@ -115,74 +178,81 @@ export let defaultMarkdownSerializer: MarkdownSerializer;
* node and mark serialization methods (see `toMarkdown`).
*/
export class MarkdownSerializerState<S extends Schema = any> {
/**
* The options passed to the serializer.
*/
options: { tightLists?: boolean | null };
/**
* Render a block, prefixing each line with `delim`, and the first
* line in `firstDelim`. `node` should be the node that is closed at
* the end of the block, and `f` is a function that renders the
* content of the block.
*/
wrapBlock(
delim: string,
firstDelim: string | undefined,
node: ProsemirrorNode<S>,
f: () => void
): void;
/**
* Ensure the current content ends with a newline.
*/
ensureNewLine(): void;
/**
* Prepare the state for writing output (closing closed paragraphs,
* adding delimiters, and so on), and then optionally add content
* (unescaped) to the output.
*/
write(content?: string): void;
/**
* Close the block for the given node.
*/
closeBlock(node: ProsemirrorNode<S>): void;
/**
* Add the given text to the document. When escape is not `false`,
* it will be escaped.
*/
text(text: string, escape?: boolean): void;
/**
* Render the given node as a block.
*/
render(node: ProsemirrorNode<S>): void;
/**
* Render the contents of `parent` as block nodes.
*/
renderContent(parent: ProsemirrorNode<S>): void;
/**
* Render the contents of `parent` as inline content.
*/
renderInline(parent: ProsemirrorNode<S>): void;
/**
* Render a node's content as a list. `delim` should be the extra
* indentation added to all lines except the first in an item,
* `firstDelim` is a function going from an item index to a
* delimiter for the first line of the item.
*/
renderList(node: ProsemirrorNode<S>, delim: string, firstDelim: (p: number) => string): void;
/**
* Escape the given string so that it can safely appear in Markdown
* content. If `startOfLine` is true, also escape characters that
* has special meaning only at the start of the line.
*/
esc(str: string, startOfLine?: boolean): string;
/**
* Repeat the given string `n` times.
*/
repeat(str: string, n: number): string;
/**
* Get leading and trailing whitespace from a string. Values of
* leading or trailing property of the return object will be undefined
* if there is no match.
*/
getEnclosingWhitespace(text: string): { leading?: string | null; trailing?: string | null };
/**
* The options passed to the serializer.
*/
options: { tightLists?: boolean | null };
/**
* Render a block, prefixing each line with `delim`, and the first
* line in `firstDelim`. `node` should be the node that is closed at
* the end of the block, and `f` is a function that renders the
* content of the block.
*/
wrapBlock(delim: string, firstDelim: string | undefined, node: ProsemirrorNode<S>, f: () => void): void;
/**
* Ensure the current content ends with a newline.
*/
ensureNewLine(): void;
/**
* Prepare the state for writing output (closing closed paragraphs,
* adding delimiters, and so on), and then optionally add content
* (unescaped) to the output.
*/
write(content?: string): void;
/**
* Close the block for the given node.
*/
closeBlock(node: ProsemirrorNode<S>): void;
/**
* Add the given text to the document. When escape is not `false`,
* it will be escaped.
*/
text(text: string, escape?: boolean): void;
/**
* Render the given node as a block.
*/
render(node: ProsemirrorNode<S>): void;
/**
* Render the contents of `parent` as block nodes.
*/
renderContent(parent: ProsemirrorNode<S>): void;
/**
* Render the contents of `parent` as inline content.
*/
renderInline(parent: ProsemirrorNode<S>): void;
/**
* Render a node's content as a list. `delim` should be the extra
* indentation added to all lines except the first in an item,
* `firstDelim` is a function going from an item index to a
* delimiter for the first line of the item.
*/
renderList(node: ProsemirrorNode<S>, delim: string, firstDelim: (p: number) => string): void;
/**
* Escape the given string so that it can safely appear in Markdown
* content. If `startOfLine` is true, also escape characters that
* has special meaning only at the start of the line.
*/
esc(str: string, startOfLine?: boolean): string;
/**
* Repeat the given string `n` times.
*/
repeat(str: string, n: number): string;
/**
* Get leading and trailing whitespace from a string. Values of
* leading or trailing property of the return object will be undefined
* if there is no match.
*/
getEnclosingWhitespace(text: string): { leading?: string | null; trailing?: string | null };
/**
* Wraps the passed string in a string of its own
*/
quote(str: string): string;
}

View File

@ -1 +1,178 @@
import * as markdown from 'prosemirror-markdown';
import { MarkdownParser, MarkdownSerializer } from 'prosemirror-markdown';
import { Schema, Node as ProsemirrorNode, Mark, Fragment } from 'prosemirror-model';
import md = require('markdown-it');
/**
* Parses markdown content into a ProsemirrorNode compatible with the provided schema.
*/
export const fromMarkdown = (markdown: string, schema: Schema) =>
new MarkdownParser(schema, md('commonmark'), {
blockquote: { block: 'blockquote' },
paragraph: { block: 'paragraph' },
list_item: { block: 'listItem' },
bullet_list: { block: 'bulletList' },
ordered_list: {
block: 'orderedList',
getAttrs: tok => ({ order: parseInt(tok.attrGet('order') || '1', 10) }),
},
heading: { block: 'heading', getAttrs: tok => ({ level: +tok.tag.slice(1) }) },
code_block: { block: 'codeBlock' },
fence: { block: 'codeBlock', getAttrs: tok => ({ params: tok.info || '' }) },
hr: { node: 'horizontalRule' },
image: {
node: 'image',
getAttrs: tok => ({
src: tok.attrGet('src'),
title: tok.attrGet('title') || null,
alt: (tok.children[0] && tok.children[0].content) || null,
}),
},
hardbreak: { node: 'hardBreak' },
em: { mark: 'italic' },
strong: { mark: 'bold' },
link: {
mark: 'link',
getAttrs: tok => ({
href: tok.attrGet('href'),
title: tok.attrGet('title') || null,
}),
},
code_inline: { mark: 'code' },
}).parse(markdown);
export const toMarkdown = (content: ProsemirrorNode) =>
new MarkdownSerializer(
{
blockquote(state, node) {
state.wrapBlock('> ', undefined, node, () => state.renderContent(node));
},
codeBlock(state, node) {
// tslint:disable-next-line: prefer-template
state.write('```' + (node.attrs.language || '') + '\n');
state.text(node.textContent, false);
state.ensureNewLine();
state.write('```');
state.closeBlock(node);
},
heading(state, node) {
state.write(state.repeat('#', node.attrs.level) + ' ');
state.renderInline(node);
state.closeBlock(node);
},
horizontalRule(state, node) {
state.write(node.attrs.markup || '---');
state.closeBlock(node);
},
bulletList(state, node) {
state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ');
},
orderedList(state, node) {
const start = node.attrs.order || 1;
const maxW = String(start + node.childCount - 1).length;
const space = state.repeat(' ', maxW + 2);
state.renderList(node, space, i => {
const nStr = String(start + i);
// tslint:disable-next-line: prefer-template
return state.repeat(' ', maxW - nStr.length) + nStr + '. ';
});
},
listItem(state, node) {
state.renderContent(node);
},
paragraph(state, node) {
console.log(state, node);
state.renderInline(node);
state.closeBlock(node);
},
image(state, node) {
state.write(
// tslint:disable-next-line: prefer-template
'![' +
state.esc(node.attrs.alt || '') +
'](' +
state.esc(node.attrs.src) +
(node.attrs.title ? ' ' + state.quote(node.attrs.title) : '') +
')',
);
},
hardBreak(state, node, parent, index) {
for (let i = index + 1; i < parent.childCount; i++) {
if (parent.child(i).type !== node.type) {
state.write('\\\n');
return;
}
}
},
text(state, node) {
if (!node.text) {
return;
}
state.text(node.text);
},
},
{
italic: { open: '*', close: '*', mixable: true, expelEnclosingWhitespace: true },
bold: { open: '**', close: '**', mixable: true, expelEnclosingWhitespace: true },
link: {
open(_state, mark, parent, index) {
return isPlainURL(mark, parent, index, 1) ? '<' : '[';
},
close(state, mark, parent, index) {
return isPlainURL(mark, parent, index, -1)
? '>'
: // tslint:disable-next-line: prefer-template
'](' +
state.esc(mark.attrs.href) +
(mark.attrs.title ? ' ' + state.quote(mark.attrs.title) : '') +
')';
},
},
code: {
open(_state, _mark, parent, index) {
return backticksFor(parent.child(index), -1);
},
close(_state, _mark, parent, index) {
return backticksFor(parent.child(index - 1), 1);
},
escape: false,
},
},
).serialize(content);
type Side = -1 | 1;
function isPlainURL(link: Mark, parent: Fragment, index: number, side: Side) {
if (link.attrs.title) {
return false;
}
const content = parent.child(index + (side < 0 ? -1 : 0));
if (!content.isText || content.text !== link.attrs.href || content.marks[content.marks.length - 1] !== link) {
return false;
}
if (index === (side < 0 ? 1 : parent.childCount - 1)) {
return true;
}
const next = parent.child(index + (side < 0 ? -2 : 1));
return !link.isInSet(next.marks);
}
function backticksFor(node: ProsemirrorNode, side: Side) {
const ticks = /`+/g;
let m;
let len = 0;
if (node.isText) {
// tslint:disable-next-line:no-conditional-assignment
while ((m = ticks.exec(node.text!))) {
len = Math.max(len, m[0].length);
}
}
let result = len > 0 && side > 0 ? ' `' : '`';
for (let i = 0; i < len; i++) {
result += '`';
}
if (len > 0 && side < 0) {
result += ' ';
}
return result;
}