New Upstream Release - node-prosemirror-markdown
Recent merge proposals
Ready changes
Summary
Merged new upstream version: 1.11.0 (was: 1.8.0).
Diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6b7307a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/node_modules
+.tern-port
+/dist
+/test/*.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36010bc..42545f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,69 @@
+## 1.11.0 (2023-05-17)
+
+### Bug fixes
+
+Make sure blank lines at the end of code blocks are properly serialized.
+
+Convert soft breaks (single newlines) in Markdown to spaces, rather than newlines in the ProseMirror document, because newlines tend to behave awkwardly in the editor.
+
+Fix a bug that cause the object passed as configuration to `MarkdownSerializer` to be mutated. Add release note
+
+Include CommonJS type declarations in the package to please new TypeScript resolution settings.
+
+### New features
+
+A new option to `MarkdownSerializer` allows client code to configure which node type should be treated as hard breaks during mark serialization. Remove the extra left bracket
+
+## 1.10.1 (2022-10-28)
+
+### Bug fixes
+
+Don't treat the empty string the same as `null` in `wrapBlock`'s `firstDelim` argument. Check content of code blocks for any sequence of backticks
+
+Use longer sequences of backticks when serializing a code block that contains three or more backticks in a row.
+
+## 1.10.0 (2022-10-05)
+
+### New features
+
+You can now pass an optional markdown-it environment object to .
+
+## 1.9.4 (2022-08-19)
+
+### Bug fixes
+
+Don't escape colon characters at the start of a line.
+
+Escape parentheses in images and links.
+
+Allow links to wrap emphasis markers when serializing Markdown.
+
+## 1.9.3 (2022-07-05)
+
+### Bug fixes
+
+Make sure '\!' characters in front of links are escaped.
+
+## 1.9.2 (2022-07-04)
+
+### Bug fixes
+
+Don't escape characters in autolinks.
+
+Fix a bug that caused the serializer to not escape start-of-line markup when inside a list.
+
+## 1.9.1 (2022-06-02)
+
+### Bug fixes
+
+Fix a bug where inline nodes with content would reset the marks in their parent node during Markdown parsing.
+
+## 1.9.0 (2022-05-30)
+
+### New features
+
+Include TypeScript type declarations.
+
## 1.8.0 (2022-03-14)
### New features
diff --git a/LICENSE b/LICENSE
index ef9326c..7e2295b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (C) 2015-2017 by Marijn Haverbeke <marijnh@gmail.com> and others
+Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 90c4d20..8395ec4 100644
--- a/README.md
+++ b/README.md
@@ -1,232 +1,276 @@
-# prosemirror-markdown
-
-[ [**WEBSITE**](http://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror-markdown/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**GITTER**](https://gitter.im/ProseMirror/prosemirror) ]
-
-This is a (non-core) module for [ProseMirror](http://prosemirror.net).
+<h1>prosemirror-markdown</h1>
+<p>[ <a href="http://prosemirror.net"><strong>WEBSITE</strong></a> | <a href="https://github.com/prosemirror/prosemirror-markdown/issues"><strong>ISSUES</strong></a> | <a href="https://discuss.prosemirror.net"><strong>FORUM</strong></a> | <a href="https://gitter.im/ProseMirror/prosemirror"><strong>GITTER</strong></a> ]</p>
+<p>This is a (non-core) module for <a href="http://prosemirror.net">ProseMirror</a>.
ProseMirror is a well-behaved rich semantic content editor based on
contentEditable, with support for collaborative editing and custom
-document schemas.
-
-This module implements a ProseMirror
-[schema](https://prosemirror.net/docs/guide/#schema) that corresponds to
-the document schema used by [CommonMark](http://commonmark.org/), and
+document schemas.</p>
+<p>This module implements a ProseMirror
+<a href="https://prosemirror.net/docs/guide/#schema">schema</a> that corresponds to
+the document schema used by <a href="http://commonmark.org/">CommonMark</a>, and
a parser and serializer to convert between ProseMirror documents in
-that schema and CommonMark/Markdown text.
-
-This code is released under an
-[MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE).
-There's a [forum](http://discuss.prosemirror.net) for general
+that schema and CommonMark/Markdown text.</p>
+<p>This code is released under an
+<a href="https://github.com/prosemirror/prosemirror/tree/master/LICENSE">MIT license</a>.
+There's a <a href="http://discuss.prosemirror.net">forum</a> for general
discussion and support requests, and the
-[Github bug tracker](https://github.com/prosemirror/prosemirror/issues)
-is the place to report issues.
-
-We aim to be an inclusive, welcoming community. To make that explicit,
-we have a [code of
-conduct](http://contributor-covenant.org/version/1/1/0/) that applies
-to communication around the project.
-
-## Documentation
-
- * **`schema`**`: Schema`\
- Document schema for the data model used by CommonMark.
-
-
-### class MarkdownParser
-
-A configuration of a Markdown parser. Such a parser uses
-[markdown-it](https://github.com/markdown-it/markdown-it) to
+<a href="https://github.com/prosemirror/prosemirror/issues">Github bug tracker</a>
+is the place to report issues.</p>
+<p>We aim to be an inclusive, welcoming community. To make that explicit,
+we have a <a href="http://contributor-covenant.org/version/1/1/0/">code of
+conduct</a> that applies
+to communication around the project.</p>
+<h2>Documentation</h2>
+<dl>
+<dt id="schema">
+ <code><strong><a href="#schema">schema</a></strong>: <span class="type">Schema</span><<span class="string">"doc"</span> | <span class="string">"paragraph"</span> | <span class="string">"blockquote"</span> | <span class="string">"horizontal_rule"</span> | <span class="string">"heading"</span> | <span class="string">"code_block"</span> | <span class="string">"ordered_list"</span> | <span class="string">"bullet_list"</span> | <span class="string">"list_item"</span> | <span class="string">"text"</span> | <span class="string">"image"</span> | <span class="string">"hard_break"</span>, <span class="string">"em"</span> | <span class="string">"strong"</span> | <span class="string">"link"</span> | <span class="string">"code"</span>></code></dt>
+
+<dd><p>Document schema for the data model used by CommonMark.</p>
+</dd>
+<dt id="MarkdownParser">
+ <h4>
+ <code><span class=keyword>class</span></code>
+ <a href="#MarkdownParser">MarkdownParser</a></h4>
+</dt>
+
+<dd><p>A configuration of a Markdown parser. Such a parser uses
+<a href="https://github.com/markdown-it/markdown-it">markdown-it</a> to
tokenize a file, and then runs the custom rules it is given over
-the tokens to create a ProseMirror document tree.
-
- * `new `**`MarkdownParser`**`(schema: Schema, tokenizer: MarkdownIt, tokens: 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 (unless `noCloseToken` is true) 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. If the token does not have
- `_open` or `_close`, use the `noCloseToken` option.
-
- **`mark`**`: ?string`
- : This token (again, unless `noCloseToken` is true) 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.
-
- **`noCloseToken`**`: ?boolean`
- : Indicates that the [markdown-it
- token](https://markdown-it.github.io/markdown-it/#Token) has
- no `_open` or `_close` for the nodes. This defaults to `true`
- for `code_inline`, `code_block` and `fence`.
-
- **`ignore`**`: ?bool`
- : When true, ignore content for the matched token.
-
- * **`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.
-
- * **`tokenizer`**`: This`\
- parser's markdown-it tokenizer.
-
- * **`parse`**`(text: string) → Node`\
- Parse a string as [CommonMark](http://commonmark.org/) markup,
- and create a ProseMirror document as prescribed by this parser's
- rules.
-
-
- * **`defaultMarkdownParser`**`: MarkdownParser`\
- A parser parsing unextended [CommonMark](http://commonmark.org/),
- without inline HTML, and producing a document in the basic schema.
-
-
-### class MarkdownSerializer
-
-A specification for serializing a ProseMirror document as
-Markdown/CommonMark text.
-
- * `new `**`MarkdownSerializer`**`(nodes: Object< fn(state: MarkdownSerializerState, node: Node, parent: Node, index: number) >, marks: Object, options: ?Object)`\
- Construct a serializer with the given configuration. The `nodes`
- object should map node names in a given schema to function that
- take a serializer state and such a node, and serialize the node.
-
- The `marks` object should hold objects with `open` and `close`
- properties, which hold the strings that should appear before and
- after a piece of text marked that way, either directly or as a
- function that takes a serializer state and a mark, and returns a
- string. `open` and `close` can also be functions, which will be
- called as
-
- (state: MarkdownSerializerState, mark: Mark,
- parent: Fragment, index: number) → string
-
- Where `parent` and `index` allow you to inspect the mark's
- context to see which nodes it applies to.
-
- Mark information objects can also have a `mixable` property
- which, when `true`, indicates that the order in which the mark's
- opening and closing syntax appears relative to other mixable
- marks can be varied. (For example, you can say `**a *b***` and
- `*a **b***`, but not `` `a *b*` ``.)
-
- To disable character escaping in a mark, you can give it an
- `escape` property of `false`. Such a mark has to have the highest
- precedence (must always be the innermost mark).
-
- The `expelEnclosingWhitespace` mark property causes the
- serializer to move enclosing whitespace from inside the marks to
- outside the marks. This is necessary for emphasis marks as
- CommonMark does not permit enclosing whitespace inside emphasis
- marks, see: http://spec.commonmark.org/0.26/#example-330
-
- * **`options`**`: ?Object`\
- Optional additional options.
-
- * **`escapeExtraCharacters`**`: ?RegExp`\
- Extra characters can be added for escaping. This is passed
- directly to String.replace(), and the matching characters are
- preceded by a backslash.
-
- * **`nodes`**`: Object< fn(MarkdownSerializerState, Node) >`\
- The node serializer
- functions for this serializer.
-
- * **`marks`**`: Object`\
- The mark serializer info.
-
- * **`serialize`**`(content: Node, options: ?Object) → string`\
- Serialize the content of the given node to
- [CommonMark](http://commonmark.org/).
-
-
-### class MarkdownSerializerState
-
-This is an object used to track state and expose
+the tokens to create a ProseMirror document tree.</p>
+<dl><dt id="MarkdownParser.constructor">
+ <code><span class=keyword>new</span> <strong><a href="#MarkdownParser.constructor">MarkdownParser</a></strong>(<a id="MarkdownParser.constructor^schema" href="#MarkdownParser.constructor^schema"><span class=param>schema</span></a>: <span class="type">Schema</span>, <a id="MarkdownParser.constructor^tokenizer" href="#MarkdownParser.constructor^tokenizer"><span class=param>tokenizer</span></a>: <span class="type">any</span>, <a id="MarkdownParser.constructor^tokens" href="#MarkdownParser.constructor^tokens"><span class=param>tokens</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><<a href="#ParseSpec"><span class="type">ParseSpec</span></a>>)</code></dt>
+
+<dd><p>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 <code>tokens</code> 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:</p>
+</dd><dt id="MarkdownParser.schema">
+ <code><strong><a href="#MarkdownParser.schema">schema</a></strong>: <span class="type">Schema</span></code></dt>
+
+<dd><p>The parser's document schema.</p>
+</dd><dt id="MarkdownParser.tokenizer">
+ <code><strong><a href="#MarkdownParser.tokenizer">tokenizer</a></strong>: <span class="type">any</span></code></dt>
+
+<dd><p>This parser's markdown-it tokenizer.</p>
+</dd><dt id="MarkdownParser.tokens">
+ <code><strong><a href="#MarkdownParser.tokens">tokens</a></strong>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><<a href="#ParseSpec"><span class="type">ParseSpec</span></a>></code></dt>
+
+<dd><p>The value of the <code>tokens</code> object used to construct this
+parser. Can be useful to copy and modify to base other parsers
+on.</p>
+</dd><dt id="MarkdownParser.parse">
+ <code><strong><a href="#MarkdownParser.parse">parse</a></strong>(<a id="MarkdownParser.parse^text" href="#MarkdownParser.parse^text"><span class=param>text</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>) → <span class="type">any</span></code></dt>
+
+<dd><p>Parse a string as <a href="http://commonmark.org/">CommonMark</a> markup,
+and create a ProseMirror document as prescribed by this parser's
+rules.</p>
+</dd></dl>
+
+</dd>
+<dt id="ParseSpec">
+ <h4>
+ <code><span class=keyword>interface</span></code>
+ <a href="#ParseSpec">ParseSpec</a></h4>
+</dt>
+
+<dd><p>Object type used to specify how Markdown tokens should be parsed.</p>
+<dl><dt id="ParseSpec.node">
+ <code><strong><a href="#ParseSpec.node">node</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>This token maps to a single node, whose type can be looked up
+in the schema under the given name. Exactly one of <code>node</code>,
+<code>block</code>, or <code>mark</code> must be set.</p>
+</dd><dt id="ParseSpec.block">
+ <code><strong><a href="#ParseSpec.block">block</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>This token (unless <code>noCloseToken</code> is true) comes in <code>_open</code>
+and <code>_close</code> 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. If the token does not have
+<code>_open</code> or <code>_close</code>, use the <code>noCloseToken</code> option.</p>
+</dd><dt id="ParseSpec.mark">
+ <code><strong><a href="#ParseSpec.mark">mark</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>This token (again, unless <code>noCloseToken</code> is true) also comes
+in <code>_open</code> and <code>_close</code> variants, but should add a mark
+(named by the value) to its content, rather than wrapping it
+in a node.</p>
+</dd><dt id="ParseSpec.attrs">
+ <code><strong><a href="#ParseSpec.attrs">attrs</a></strong>⁠?: <span class="type">Attrs</span></code></dt>
+
+<dd><p>Attributes for the node or mark. When <code>getAttrs</code> is provided,
+it takes precedence.</p>
+</dd><dt id="ParseSpec.getAttrs">
+ <code><strong><a href="#ParseSpec.getAttrs">getAttrs</a></strong>⁠?: <span class=fn>fn</span>(<a id="ParseSpec.getAttrs^token" href="#ParseSpec.getAttrs^token"><span class=param>token</span></a>: <span class="type">any</span>, <a id="ParseSpec.getAttrs^tokenStream" href="#ParseSpec.getAttrs^tokenStream"><span class=param>tokenStream</span></a>: <span class="type">any</span>[], <a id="ParseSpec.getAttrs^index" href="#ParseSpec.getAttrs^index"><span class=param>index</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>) → <span class="type">Attrs</span></code></dt>
+
+<dd><p>A function used to compute the attributes for the node or mark
+that takes a <a href="https://markdown-it.github.io/markdown-it/#Token">markdown-it
+token</a> and
+returns an attribute object.</p>
+</dd><dt id="ParseSpec.noCloseToken">
+ <code><strong><a href="#ParseSpec.noCloseToken">noCloseToken</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a></code></dt>
+
+<dd><p>Indicates that the <a href="https://markdown-it.github.io/markdown-it/#Token">markdown-it
+token</a> has
+no <code>_open</code> or <code>_close</code> for the nodes. This defaults to <code>true</code>
+for <code>code_inline</code>, <code>code_block</code> and <code>fence</code>.</p>
+</dd><dt id="ParseSpec.ignore">
+ <code><strong><a href="#ParseSpec.ignore">ignore</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a></code></dt>
+
+<dd><p>When true, ignore content for the matched token.</p>
+</dd></dl>
+
+</dd>
+<dt id="defaultMarkdownParser">
+ <code><strong><a href="#defaultMarkdownParser">defaultMarkdownParser</a></strong>: <a href="#MarkdownParser"><span class="type">MarkdownParser</span></a></code></dt>
+
+<dd><p>A parser parsing unextended <a href="http://commonmark.org/">CommonMark</a>,
+without inline HTML, and producing a document in the basic schema.</p>
+</dd>
+<dt id="MarkdownSerializer">
+ <h4>
+ <code><span class=keyword>class</span></code>
+ <a href="#MarkdownSerializer">MarkdownSerializer</a></h4>
+</dt>
+
+<dd><p>A specification for serializing a ProseMirror document as
+Markdown/CommonMark text.</p>
+<dl><dt id="MarkdownSerializer.constructor">
+ <code><span class=keyword>new</span> <strong><a href="#MarkdownSerializer.constructor">MarkdownSerializer</a></strong>(<a id="MarkdownSerializer.constructor^nodes" href="#MarkdownSerializer.constructor^nodes"><span class=param>nodes</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><<span class=fn>fn</span>(<a id="MarkdownSerializer.constructor^nodes^state" href="#MarkdownSerializer.constructor^nodes^state"><span class=param>state</span></a>: <a href="#MarkdownSerializerState"><span class="type">MarkdownSerializerState</span></a>, <a id="MarkdownSerializer.constructor^nodes^node" href="#MarkdownSerializer.constructor^nodes^node"><span class=param>node</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializer.constructor^nodes^parent" href="#MarkdownSerializer.constructor^nodes^parent"><span class=param>parent</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializer.constructor^nodes^index" href="#MarkdownSerializer.constructor^nodes^index"><span class=param>index</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>)>, <a id="MarkdownSerializer.constructor^marks" href="#MarkdownSerializer.constructor^marks"><span class=param>marks</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a>>, <a id="MarkdownSerializer.constructor^options" href="#MarkdownSerializer.constructor^options"><span class=param>options</span></a>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><span class=defaultvalue> = {}</span>)</code></dt>
+
+<dd><p>Construct a serializer with the given configuration. The <code>nodes</code>
+object should map node names in a given schema to function that
+take a serializer state and such a node, and serialize the node.</p>
+<dl><dt id="MarkdownSerializer.constructor^options">
+ <code><strong><a href="#MarkdownSerializer.constructor^options">options</a></strong></code></dt>
+
+<dd><dl><dt id="MarkdownSerializer.constructor^options.escapeExtraCharacters">
+ <code><strong><a href="#MarkdownSerializer.constructor^options.escapeExtraCharacters">escapeExtraCharacters</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp"><span class="type">RegExp</span></a></code></dt>
+
+<dd><p>Extra characters can be added for escaping. This is passed
+directly to String.replace(), and the matching characters are
+preceded by a backslash.</p>
+</dd></dl></dd></dl></dd><dt id="MarkdownSerializer.nodes">
+ <code><strong><a href="#MarkdownSerializer.nodes">nodes</a></strong>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><<span class=fn>fn</span>(<a id="MarkdownSerializer.nodes^state" href="#MarkdownSerializer.nodes^state"><span class=param>state</span></a>: <a href="#MarkdownSerializerState"><span class="type">MarkdownSerializerState</span></a>, <a id="MarkdownSerializer.nodes^node" href="#MarkdownSerializer.nodes^node"><span class=param>node</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializer.nodes^parent" href="#MarkdownSerializer.nodes^parent"><span class=param>parent</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializer.nodes^index" href="#MarkdownSerializer.nodes^index"><span class=param>index</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>)></code></dt>
+
+<dd><p>The node serializer functions for this serializer.</p>
+</dd><dt id="MarkdownSerializer.marks">
+ <code><strong><a href="#MarkdownSerializer.marks">marks</a></strong>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a>></code></dt>
+
+<dd><p>The mark serializer info.</p>
+</dd><dt id="MarkdownSerializer.options">
+ <code><strong><a href="#MarkdownSerializer.options">options</a></strong>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a></code></dt>
+
+<dd><dl><dt id="MarkdownSerializer.options.escapeExtraCharacters">
+ <code><strong><a href="#MarkdownSerializer.options.escapeExtraCharacters">escapeExtraCharacters</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp"><span class="type">RegExp</span></a></code></dt>
+
+<dd><p>Extra characters can be added for escaping. This is passed
+directly to String.replace(), and the matching characters are
+preceded by a backslash.</p>
+</dd></dl></dd><dt id="MarkdownSerializer.serialize">
+ <code><strong><a href="#MarkdownSerializer.serialize">serialize</a></strong>(<a id="MarkdownSerializer.serialize^content" href="#MarkdownSerializer.serialize^content"><span class=param>content</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializer.serialize^options" href="#MarkdownSerializer.serialize^options"><span class=param>options</span></a>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a><span class=defaultvalue> = {}</span>) → <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>Serialize the content of the given node to
+<a href="http://commonmark.org/">CommonMark</a>.</p>
+<dl><dt id="MarkdownSerializer.serialize^options">
+ <code><strong><a href="#MarkdownSerializer.serialize^options">options</a></strong></code></dt>
+
+<dd><dl><dt id="MarkdownSerializer.serialize^options.tightLists">
+ <code><strong><a href="#MarkdownSerializer.serialize^options.tightLists">tightLists</a></strong>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a></code></dt>
+
+<dd><p>Whether to render lists in a tight style. This can be overridden
+on a node level by specifying a tight attribute on the node.
+Defaults to false.</p>
+</dd></dl></dd></dl></dd></dl>
+
+</dd>
+<dt id="MarkdownSerializerState">
+ <h4>
+ <code><span class=keyword>class</span></code>
+ <a href="#MarkdownSerializerState">MarkdownSerializerState</a></h4>
+</dt>
+
+<dd><p>This is an object used to track state and expose
methods related to markdown serialization. Instances are passed to
-node and mark serialization methods (see `toMarkdown`).
-
- * **`options`**`: Object`\
- The options passed to the serializer.
-
- * **`tightLists`**`: ?bool`\
- Whether to render lists in a tight style. This can be overridden
- on a node level by specifying a tight attribute on the node.
- Defaults to false.
-
- * **`wrapBlock`**`(delim: string, firstDelim: ?string, node: Node, f: fn())`\
- 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.
-
- * **`ensureNewLine`**`()`\
- Ensure the current content ends with a newline.
-
- * **`write`**`(content: ?string)`\
- Prepare the state for writing output (closing closed paragraphs,
- adding delimiters, and so on), and then optionally add content
- (unescaped) to the output.
-
- * **`closeBlock`**`(node: Node)`\
- Close the block for the given node.
-
- * **`text`**`(text: string, escape: ?bool)`\
- Add the given text to the document. When escape is not `false`,
- it will be escaped.
-
- * **`render`**`(node: Node)`\
- Render the given node as a block.
-
- * **`renderContent`**`(parent: Node)`\
- Render the contents of `parent` as block nodes.
-
- * **`renderInline`**`(parent: Node)`\
- Render the contents of `parent` as inline content.
-
- * **`renderList`**`(node: Node, delim: string, firstDelim: fn(number) → string)`\
- 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.
-
- * **`esc`**`(str: string, startOfLine: ?bool) → string`\
- Escape the given string so that it can safely appear in Markdown
- content. If `startOfLine` is true, also escape characters that
- have special meaning only at the start of the line.
-
- * **`repeat`**`(str: string, n: number) → string`\
- Repeat the given string `n` times.
-
- * **`getEnclosingWhitespace`**`(text: string) → {leading: ?string, trailing: ?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.
-
-
- * **`defaultMarkdownSerializer`**`: MarkdownSerializer`\
- A serializer for the [basic schema](#schema).
-
+node and mark serialization methods (see <code>toMarkdown</code>).</p>
+<dl><dt id="MarkdownSerializerState.options">
+ <code><strong><a href="#MarkdownSerializerState.options">options</a></strong>: {<span class=prop>tightLists</span>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a>, <span class=prop>escapeExtraCharacters</span>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp"><span class="type">RegExp</span></a>}</code></dt>
+
+<dd><p>The options passed to the serializer.</p>
+</dd><dt id="MarkdownSerializerState.wrapBlock">
+ <code><strong><a href="#MarkdownSerializerState.wrapBlock">wrapBlock</a></strong>(<a id="MarkdownSerializerState.wrapBlock^delim" href="#MarkdownSerializerState.wrapBlock^delim"><span class=param>delim</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <a id="MarkdownSerializerState.wrapBlock^firstDelim" href="#MarkdownSerializerState.wrapBlock^firstDelim"><span class=param>firstDelim</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <a id="MarkdownSerializerState.wrapBlock^node" href="#MarkdownSerializerState.wrapBlock^node"><span class=param>node</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializerState.wrapBlock^f" href="#MarkdownSerializerState.wrapBlock^f"><span class=param>f</span></a>: <span class=fn>fn</span>())</code></dt>
+
+<dd><p>Render a block, prefixing each line with <code>delim</code>, and the first
+line in <code>firstDelim</code>. <code>node</code> should be the node that is closed at
+the end of the block, and <code>f</code> is a function that renders the
+content of the block.</p>
+</dd><dt id="MarkdownSerializerState.ensureNewLine">
+ <code><strong><a href="#MarkdownSerializerState.ensureNewLine">ensureNewLine</a></strong>()</code></dt>
+
+<dd><p>Ensure the current content ends with a newline.</p>
+</dd><dt id="MarkdownSerializerState.write">
+ <code><strong><a href="#MarkdownSerializerState.write">write</a></strong>(<a id="MarkdownSerializerState.write^content" href="#MarkdownSerializerState.write^content"><span class=param>content</span></a>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>)</code></dt>
+
+<dd><p>Prepare the state for writing output (closing closed paragraphs,
+adding delimiters, and so on), and then optionally add content
+(unescaped) to the output.</p>
+</dd><dt id="MarkdownSerializerState.closeBlock">
+ <code><strong><a href="#MarkdownSerializerState.closeBlock">closeBlock</a></strong>(<a id="MarkdownSerializerState.closeBlock^node" href="#MarkdownSerializerState.closeBlock^node"><span class=param>node</span></a>: <span class="type">Node</span>)</code></dt>
+
+<dd><p>Close the block for the given node.</p>
+</dd><dt id="MarkdownSerializerState.text">
+ <code><strong><a href="#MarkdownSerializerState.text">text</a></strong>(<a id="MarkdownSerializerState.text^text" href="#MarkdownSerializerState.text^text"><span class=param>text</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <a id="MarkdownSerializerState.text^escape" href="#MarkdownSerializerState.text^escape"><span class=param>escape</span></a>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a><span class=defaultvalue> = true</span>)</code></dt>
+
+<dd><p>Add the given text to the document. When escape is not <code>false</code>,
+it will be escaped.</p>
+</dd><dt id="MarkdownSerializerState.render">
+ <code><strong><a href="#MarkdownSerializerState.render">render</a></strong>(<a id="MarkdownSerializerState.render^node" href="#MarkdownSerializerState.render^node"><span class=param>node</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializerState.render^parent" href="#MarkdownSerializerState.render^parent"><span class=param>parent</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializerState.render^index" href="#MarkdownSerializerState.render^index"><span class=param>index</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>)</code></dt>
+
+<dd><p>Render the given node as a block.</p>
+</dd><dt id="MarkdownSerializerState.renderContent">
+ <code><strong><a href="#MarkdownSerializerState.renderContent">renderContent</a></strong>(<a id="MarkdownSerializerState.renderContent^parent" href="#MarkdownSerializerState.renderContent^parent"><span class=param>parent</span></a>: <span class="type">Node</span>)</code></dt>
+
+<dd><p>Render the contents of <code>parent</code> as block nodes.</p>
+</dd><dt id="MarkdownSerializerState.renderInline">
+ <code><strong><a href="#MarkdownSerializerState.renderInline">renderInline</a></strong>(<a id="MarkdownSerializerState.renderInline^parent" href="#MarkdownSerializerState.renderInline^parent"><span class=param>parent</span></a>: <span class="type">Node</span>)</code></dt>
+
+<dd><p>Render the contents of <code>parent</code> as inline content.</p>
+</dd><dt id="MarkdownSerializerState.renderList">
+ <code><strong><a href="#MarkdownSerializerState.renderList">renderList</a></strong>(<a id="MarkdownSerializerState.renderList^node" href="#MarkdownSerializerState.renderList^node"><span class=param>node</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializerState.renderList^delim" href="#MarkdownSerializerState.renderList^delim"><span class=param>delim</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <a id="MarkdownSerializerState.renderList^firstDelim" href="#MarkdownSerializerState.renderList^firstDelim"><span class=param>firstDelim</span></a>: <span class=fn>fn</span>(<a id="MarkdownSerializerState.renderList^firstDelim^index" href="#MarkdownSerializerState.renderList^firstDelim^index"><span class=param>index</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>) → <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>)</code></dt>
+
+<dd><p>Render a node's content as a list. <code>delim</code> should be the extra
+indentation added to all lines except the first in an item,
+<code>firstDelim</code> is a function going from an item index to a
+delimiter for the first line of the item.</p>
+</dd><dt id="MarkdownSerializerState.esc">
+ <code><strong><a href="#MarkdownSerializerState.esc">esc</a></strong>(<a id="MarkdownSerializerState.esc^str" href="#MarkdownSerializerState.esc^str"><span class=param>str</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <a id="MarkdownSerializerState.esc^startOfLine" href="#MarkdownSerializerState.esc^startOfLine"><span class=param>startOfLine</span></a>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a><span class=defaultvalue> = false</span>) → <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>Escape the given string so that it can safely appear in Markdown
+content. If <code>startOfLine</code> is true, also escape characters that
+have special meaning only at the start of the line.</p>
+</dd><dt id="MarkdownSerializerState.repeat">
+ <code><strong><a href="#MarkdownSerializerState.repeat">repeat</a></strong>(<a id="MarkdownSerializerState.repeat^str" href="#MarkdownSerializerState.repeat^str"><span class=param>str</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <a id="MarkdownSerializerState.repeat^n" href="#MarkdownSerializerState.repeat^n"><span class=param>n</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>) → <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>Repeat the given string <code>n</code> times.</p>
+</dd><dt id="MarkdownSerializerState.markString">
+ <code><strong><a href="#MarkdownSerializerState.markString">markString</a></strong>(<a id="MarkdownSerializerState.markString^mark" href="#MarkdownSerializerState.markString^mark"><span class=param>mark</span></a>: <span class="type">Mark</span>, <a id="MarkdownSerializerState.markString^open" href="#MarkdownSerializerState.markString^open"><span class=param>open</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean"><span class="prim">boolean</span></a>, <a id="MarkdownSerializerState.markString^parent" href="#MarkdownSerializerState.markString^parent"><span class=param>parent</span></a>: <span class="type">Node</span>, <a id="MarkdownSerializerState.markString^index" href="#MarkdownSerializerState.markString^index"><span class=param>index</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><span class="prim">number</span></a>) → <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a></code></dt>
+
+<dd><p>Get the markdown string for a given opening or closing mark.</p>
+</dd><dt id="MarkdownSerializerState.getEnclosingWhitespace">
+ <code><strong><a href="#MarkdownSerializerState.getEnclosingWhitespace">getEnclosingWhitespace</a></strong>(<a id="MarkdownSerializerState.getEnclosingWhitespace^text" href="#MarkdownSerializerState.getEnclosingWhitespace^text"><span class=param>text</span></a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>) → {<span class=prop>leading</span>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>, <span class=prop>trailing</span>⁠?: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String"><span class="prim">string</span></a>}</code></dt>
+
+<dd><p>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.</p>
+</dd></dl>
+
+</dd>
+<dt id="defaultMarkdownSerializer">
+ <code><strong><a href="#defaultMarkdownSerializer">defaultMarkdownSerializer</a></strong>: <a href="#MarkdownSerializer"><span class="type">MarkdownSerializer</span></a></code></dt>
+
+<dd><p>A serializer for the <a href="#schema">basic schema</a>.</p>
+</dd>
+</dl>
diff --git a/debian/changelog b/debian/changelog
index ea420c4..6b43edd 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+node-prosemirror-markdown (1.11.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Wed, 24 May 2023 07:21:37 -0000
+
node-prosemirror-markdown (1.8.0-1) unstable; urgency=medium
* Team Upload
diff --git a/package.json b/package.json
index b215956..2f11932 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,21 @@
{
"name": "prosemirror-markdown",
- "version": "1.8.0",
+ "version": "1.11.0",
"description": "ProseMirror Markdown integration",
- "main": "dist/index.js",
- "module": "dist/index.es.js",
+ "type": "module",
+ "main": "dist/index.cjs",
+ "module": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "sideEffects": false,
"license": "MIT",
"maintainers": [
{
"name": "Marijn Haverbeke",
- "email": "marijnh@gmail.com",
+ "email": "marijn@haverbeke.berlin",
"web": "http://marijnhaverbeke.nl"
}
],
@@ -17,23 +24,17 @@
"url": "git://github.com/prosemirror/prosemirror-markdown.git"
},
"dependencies": {
- "markdown-it": "^12.0.0",
+ "markdown-it": "^13.0.1",
"prosemirror-model": "^1.0.0"
},
"devDependencies": {
- "ist": "1.0.0",
- "mocha": "^9.1.2",
+ "@prosemirror/buildhelper": "^0.1.5",
+ "@types/markdown-it": "^12.2.3",
"prosemirror-test-builder": "^1.0.0",
- "punycode": "^1.4.0",
- "rollup": "^2.26.3",
- "@rollup/plugin-buble": "^0.21.3",
- "builddocs": "^0.3.0"
+ "punycode": "^1.4.0"
},
"scripts": {
- "test": "mocha test/test-*.js",
- "build": "rollup -c",
- "watch": "rollup -c -w",
- "prepare": "npm run build",
- "build_readme": "builddocs --name markdown --format markdown --main src/README.md src/*.js > README.md"
+ "test": "pm-runtests",
+ "prepare": "pm-buildhelper src/index.ts"
}
}
diff --git a/rollup.config.js b/rollup.config.js
deleted file mode 100644
index caa106f..0000000
--- a/rollup.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
- input: './src/index.js',
- output: [{
- file: 'dist/index.js',
- format: 'cjs',
- sourcemap: true
- }, {
- file: 'dist/index.es.js',
- format: 'es',
- sourcemap: true
- }],
- plugins: [require('@rollup/plugin-buble')()],
- external(id) { return id[0] != "." && !require("path").isAbsolute(id) }
-}
diff --git a/src/README.md b/src/README.md
index a2ed1e1..4185511 100644
--- a/src/README.md
+++ b/src/README.md
@@ -31,6 +31,8 @@ to communication around the project.
@MarkdownParser
+@ParseSpec
+
@defaultMarkdownParser
@MarkdownSerializer
diff --git a/src/from_markdown.js b/src/from_markdown.js
deleted file mode 100644
index 38f067b..0000000
--- a/src/from_markdown.js
+++ /dev/null
@@ -1,262 +0,0 @@
-import markdownit from "markdown-it"
-import {schema} from "./schema"
-import {Mark} from "prosemirror-model"
-
-function maybeMerge(a, b) {
- if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks))
- return a.withText(a.text + b.text)
-}
-
-// Object used to track the context of a running parse.
-class MarkdownParseState {
- constructor(schema, tokenHandlers) {
- this.schema = schema
- this.stack = [{type: schema.topNodeType, content: []}]
- this.marks = Mark.none
- this.tokenHandlers = tokenHandlers
- }
-
- 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, toks, i)
- }
- }
-
- // : (NodeType, ?Object, ?[Node]) → ?Node
- // Add a node at the current position.
- addNode(type, attrs, content) {
- let node = 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(spec, token, tokens, i) {
- if (spec.getAttrs) return spec.getAttrs(token, tokens, i)
- // For backwards compatibility when `attrs` is a Function
- else if (spec.attrs instanceof Function) return spec.attrs(token)
- else return spec.attrs
-}
-
-// Code content is represented as a single token with a `content`
-// property in Markdown-it.
-function noCloseToken(spec, type) {
- return spec.noCloseToken || 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 noOp() {}
-
-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 (noCloseToken(spec, type)) {
- handlers[type] = (state, tok, tokens, i) => {
- state.openNode(nodeType, attrs(spec, tok, tokens, i))
- state.addText(withoutTrailingNewline(tok.content))
- state.closeNode()
- }
- } else {
- handlers[type + "_open"] = (state, tok, tokens, i) => state.openNode(nodeType, attrs(spec, tok, tokens, i))
- handlers[type + "_close"] = state => state.closeNode()
- }
- } else if (spec.node) {
- let nodeType = schema.nodeType(spec.node)
- handlers[type] = (state, tok, tokens, i) => state.addNode(nodeType, attrs(spec, tok, tokens, i))
- } else if (spec.mark) {
- let markType = schema.marks[spec.mark]
- if (noCloseToken(spec, type)) {
- handlers[type] = (state, tok, tokens, i) => {
- state.openMark(markType.create(attrs(spec, tok, tokens, i)))
- state.addText(withoutTrailingNewline(tok.content))
- state.closeMark(markType)
- }
- } else {
- handlers[type + "_open"] = (state, tok, tokens, i) => state.openMark(markType.create(attrs(spec, tok, tokens, i)))
- handlers[type + "_close"] = state => state.closeMark(markType)
- }
- } else if (spec.ignore) {
- if (noCloseToken(spec, type)) {
- handlers[type] = noOp
- } else {
- handlers[type + "_open"] = noOp
- handlers[type + "_close"] = noOp
- }
- } 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 = 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.
-export 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 (unless `noCloseToken` is true) 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. If the token does not have
- // `_open` or `_close`, use the `noCloseToken` option.
- //
- // **`mark`**`: ?string`
- // : This token (again, unless `noCloseToken` is true) 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.
- //
- // **`noCloseToken`**`: ?boolean`
- // : Indicates that the [markdown-it
- // token](https://markdown-it.github.io/markdown-it/#Token) has
- // no `_open` or `_close` for the nodes. This defaults to `true`
- // for `code_inline`, `code_block` and `fence`.
- //
- // **`ignore`**`: ?bool`
- // : When true, ignore content for the matched token.
- constructor(schema, 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 parser's markdown-it tokenizer.
- this.tokenizer = tokenizer
- 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.tokenHandlers), doc
- state.parseTokens(this.tokenizer.parse(text, {}))
- do { doc = state.closeNode() } while (state.stack.length)
- return doc || this.schema.topNodeType.createAndFill()
- }
-}
-
-function listIsTight(tokens, i) {
- while (++i < tokens.length)
- if (tokens[i].type != "list_item_open") return tokens[i].hidden
- return false
-}
-
-// :: MarkdownParser
-// A parser parsing unextended [CommonMark](http://commonmark.org/),
-// without inline HTML, and producing a document in the basic schema.
-export const defaultMarkdownParser = new MarkdownParser(schema, markdownit("commonmark", {html: false}), {
- blockquote: {block: "blockquote"},
- paragraph: {block: "paragraph"},
- list_item: {block: "list_item"},
- bullet_list: {block: "bullet_list", getAttrs: (_, tokens, i) => ({tight: listIsTight(tokens, i)})},
- ordered_list: {block: "ordered_list", getAttrs: (tok, tokens, i) => ({
- order: +tok.attrGet("start") || 1,
- tight: listIsTight(tokens, i)
- })},
- heading: {block: "heading", getAttrs: tok => ({level: +tok.tag.slice(1)})},
- code_block: {block: "code_block", noCloseToken: true},
- fence: {block: "code_block", getAttrs: tok => ({params: tok.info || ""}), noCloseToken: true},
- hr: {node: "horizontal_rule"},
- 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: "hard_break"},
-
- em: {mark: "em"},
- strong: {mark: "strong"},
- link: {mark: "link", getAttrs: tok => ({
- href: tok.attrGet("href"),
- title: tok.attrGet("title") || null
- })},
- code_inline: {mark: "code", noCloseToken: true}
-})
diff --git a/src/from_markdown.ts b/src/from_markdown.ts
new file mode 100644
index 0000000..40a63db
--- /dev/null
+++ b/src/from_markdown.ts
@@ -0,0 +1,272 @@
+// @ts-ignore
+import MarkdownIt from "markdown-it"
+import Token from "markdown-it/lib/token"
+import {schema} from "./schema"
+import {Mark, MarkType, Node, Attrs, Schema, NodeType} from "prosemirror-model"
+
+function maybeMerge(a: Node, b: Node): Node | undefined {
+ if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks))
+ return (a as any).withText(a.text! + b.text!)
+}
+
+// Object used to track the context of a running parse.
+class MarkdownParseState {
+ stack: {type: NodeType, attrs: Attrs | null, content: Node[], marks: readonly Mark[]}[]
+
+ constructor(
+ readonly schema: Schema,
+ readonly tokenHandlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void}
+ ) {
+ this.stack = [{type: schema.topNodeType, attrs: null, content: [], marks: Mark.none}]
+ }
+
+ top() {
+ return this.stack[this.stack.length - 1]
+ }
+
+ push(elt: Node) {
+ if (this.stack.length) this.top().content.push(elt)
+ }
+
+ // Adds the given text to the current position in the document,
+ // using the current marks as styling.
+ addText(text: string) {
+ if (!text) return
+ let top = this.top(), nodes = top.content, last = nodes[nodes.length - 1]
+ let node = this.schema.text(text, top.marks), merged
+ if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged
+ else nodes.push(node)
+ }
+
+ // Adds the given mark to the set of active marks.
+ openMark(mark: Mark) {
+ let top = this.top()
+ top.marks = mark.addToSet(top.marks)
+ }
+
+ // Removes the given mark from the set of active marks.
+ closeMark(mark: MarkType) {
+ let top = this.top()
+ top.marks = mark.removeFromSet(top.marks)
+ }
+
+ parseTokens(toks: Token[]) {
+ 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, toks, i)
+ }
+ }
+
+ // Add a node at the current position.
+ addNode(type: NodeType, attrs: Attrs | null, content?: readonly Node[]) {
+ let top = this.top()
+ let node = type.createAndFill(attrs, content, top ? top.marks : [])
+ if (!node) return null
+ this.push(node)
+ return node
+ }
+
+ // Wrap subsequent content in a node of the given type.
+ openNode(type: NodeType, attrs: Attrs | null) {
+ this.stack.push({type: type, attrs: attrs, content: [], marks: Mark.none})
+ }
+
+ // Close and return the node that is currently on top of the stack.
+ closeNode() {
+ let info = this.stack.pop()!
+ return this.addNode(info.type, info.attrs, info.content)
+ }
+}
+
+function attrs(spec: ParseSpec, token: Token, tokens: Token[], i: number) {
+ if (spec.getAttrs) return spec.getAttrs(token, tokens, i)
+ // For backwards compatibility when `attrs` is a Function
+ else if (spec.attrs instanceof Function) return spec.attrs(token)
+ else return spec.attrs
+}
+
+// Code content is represented as a single token with a `content`
+// property in Markdown-it.
+function noCloseToken(spec: ParseSpec, type: string) {
+ return spec.noCloseToken || type == "code_inline" || type == "code_block" || type == "fence"
+}
+
+function withoutTrailingNewline(str: string) {
+ return str[str.length - 1] == "\n" ? str.slice(0, str.length - 1) : str
+}
+
+function noOp() {}
+
+function tokenHandlers(schema: Schema, tokens: {[token: string]: ParseSpec}) {
+ let handlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void} =
+ Object.create(null)
+ for (let type in tokens) {
+ let spec = tokens[type]
+ if (spec.block) {
+ let nodeType = schema.nodeType(spec.block)
+ if (noCloseToken(spec, type)) {
+ handlers[type] = (state, tok, tokens, i) => {
+ state.openNode(nodeType, attrs(spec, tok, tokens, i))
+ state.addText(withoutTrailingNewline(tok.content))
+ state.closeNode()
+ }
+ } else {
+ handlers[type + "_open"] = (state, tok, tokens, i) => state.openNode(nodeType, attrs(spec, tok, tokens, i))
+ handlers[type + "_close"] = state => state.closeNode()
+ }
+ } else if (spec.node) {
+ let nodeType = schema.nodeType(spec.node)
+ handlers[type] = (state, tok, tokens, i) => state.addNode(nodeType, attrs(spec, tok, tokens, i))
+ } else if (spec.mark) {
+ let markType = schema.marks[spec.mark]
+ if (noCloseToken(spec, type)) {
+ handlers[type] = (state, tok, tokens, i) => {
+ state.openMark(markType.create(attrs(spec, tok, tokens, i)))
+ state.addText(withoutTrailingNewline(tok.content))
+ state.closeMark(markType)
+ }
+ } else {
+ handlers[type + "_open"] = (state, tok, tokens, i) => state.openMark(markType.create(attrs(spec, tok, tokens, i)))
+ handlers[type + "_close"] = state => state.closeMark(markType)
+ }
+ } else if (spec.ignore) {
+ if (noCloseToken(spec, type)) {
+ handlers[type] = noOp
+ } else {
+ handlers[type + "_open"] = noOp
+ handlers[type + "_close"] = noOp
+ }
+ } 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 = handlers.softbreak || (state => state.addText(" "))
+
+ return handlers
+}
+
+/// Object type used to specify how Markdown tokens should be parsed.
+export interface ParseSpec {
+ /// 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 (unless `noCloseToken` is true) 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. If the token does not have
+ /// `_open` or `_close`, use the `noCloseToken` option.
+ block?: string
+
+ /// This token (again, unless `noCloseToken` is true) 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
+
+ /// Attributes for the node or mark. When `getAttrs` is provided,
+ /// it takes precedence.
+ attrs?: Attrs | null
+
+ /// 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, tokenStream: Token[], index: number) => Attrs | null
+
+ /// Indicates that the [markdown-it
+ /// token](https://markdown-it.github.io/markdown-it/#Token) has
+ /// no `_open` or `_close` for the nodes. This defaults to `true`
+ /// for `code_inline`, `code_block` and `fence`.
+ noCloseToken?: boolean
+
+ /// When true, ignore content for the matched token.
+ ignore?: boolean
+}
+
+/// 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.
+export class MarkdownParser {
+ /// @internal
+ tokenHandlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void}
+
+ /// 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:
+ constructor(
+ /// The parser's document schema.
+ readonly schema: Schema,
+ /// This parser's markdown-it tokenizer.
+ readonly tokenizer: MarkdownIt,
+ /// The value of the `tokens` object used to construct this
+ /// parser. Can be useful to copy and modify to base other parsers
+ /// on.
+ readonly tokens: {[name: string]: ParseSpec}
+ ) {
+ this.tokenHandlers = tokenHandlers(schema, tokens)
+ }
+
+ /// Parse a string as [CommonMark](http://commonmark.org/) markup,
+ /// and create a ProseMirror document as prescribed by this parser's
+ /// rules.
+ ///
+ /// The second argument, when given, is passed through to the
+ /// [Markdown
+ /// parser](https://markdown-it.github.io/markdown-it/#MarkdownIt.parse).
+ parse(text: string, markdownEnv: Object = {}) {
+ let state = new MarkdownParseState(this.schema, this.tokenHandlers), doc
+ state.parseTokens(this.tokenizer.parse(text, markdownEnv))
+ do { doc = state.closeNode() } while (state.stack.length)
+ return doc || this.schema.topNodeType.createAndFill()
+ }
+}
+
+function listIsTight(tokens: readonly Token[], i: number) {
+ while (++i < tokens.length)
+ if (tokens[i].type != "list_item_open") return tokens[i].hidden
+ return false
+}
+
+/// A parser parsing unextended [CommonMark](http://commonmark.org/),
+/// without inline HTML, and producing a document in the basic schema.
+export const defaultMarkdownParser = new MarkdownParser(schema, MarkdownIt("commonmark", {html: false}), {
+ blockquote: {block: "blockquote"},
+ paragraph: {block: "paragraph"},
+ list_item: {block: "list_item"},
+ bullet_list: {block: "bullet_list", getAttrs: (_, tokens, i) => ({tight: listIsTight(tokens, i)})},
+ ordered_list: {block: "ordered_list", getAttrs: (tok, tokens, i) => ({
+ order: +tok.attrGet("start")! || 1,
+ tight: listIsTight(tokens, i)
+ })},
+ heading: {block: "heading", getAttrs: tok => ({level: +tok.tag.slice(1)})},
+ code_block: {block: "code_block", noCloseToken: true},
+ fence: {block: "code_block", getAttrs: tok => ({params: tok.info || ""}), noCloseToken: true},
+ hr: {node: "horizontal_rule"},
+ 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: "hard_break"},
+
+ em: {mark: "em"},
+ strong: {mark: "strong"},
+ link: {mark: "link", getAttrs: tok => ({
+ href: tok.attrGet("href"),
+ title: tok.attrGet("title") || null
+ })},
+ code_inline: {mark: "code", noCloseToken: true}
+})
diff --git a/src/index.js b/src/index.ts
similarity index 72%
rename from src/index.js
rename to src/index.ts
index 7e67947..bca4892 100644
--- a/src/index.js
+++ b/src/index.ts
@@ -1,5 +1,5 @@
// Defines a parser and serializer for [CommonMark](http://commonmark.org/) text.
export {schema} from "./schema"
-export {defaultMarkdownParser, MarkdownParser} from "./from_markdown"
+export {defaultMarkdownParser, MarkdownParser, ParseSpec} from "./from_markdown"
export {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "./to_markdown"
diff --git a/src/schema.js b/src/schema.ts
similarity index 81%
rename from src/schema.js
rename to src/schema.ts
index bcf0934..f3870e3 100644
--- a/src/schema.js
+++ b/src/schema.ts
@@ -1,6 +1,6 @@
import {Schema} from "prosemirror-model"
-// ::Schema Document schema for the data model used by CommonMark.
+/// Document schema for the data model used by CommonMark.
export const schema = new Schema({
nodes: {
doc: {
@@ -49,7 +49,7 @@ export const schema = new Schema({
marks: "",
attrs: {params: {default: ""}},
parseDOM: [{tag: "pre", preserveWhitespace: "full", getAttrs: node => (
- {params: node.getAttribute("data-params") || ""}
+ {params: (node as HTMLElement).getAttribute("data-params") || ""}
)}],
toDOM(node) { return ["pre", node.attrs.params ? {"data-params": node.attrs.params} : {}, ["code", 0]] }
},
@@ -59,8 +59,8 @@ export const schema = new Schema({
group: "block",
attrs: {order: {default: 1}, tight: {default: false}},
parseDOM: [{tag: "ol", getAttrs(dom) {
- return {order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1,
- tight: dom.hasAttribute("data-tight")}
+ return {order: (dom as HTMLElement).hasAttribute("start") ? +(dom as HTMLElement).getAttribute("start")! : 1,
+ tight: (dom as HTMLElement).hasAttribute("data-tight")}
}}],
toDOM(node) {
return ["ol", {start: node.attrs.order == 1 ? null : node.attrs.order,
@@ -72,7 +72,7 @@ export const schema = new Schema({
content: "list_item+",
group: "block",
attrs: {tight: {default: false}},
- parseDOM: [{tag: "ul", getAttrs: dom => ({tight: dom.hasAttribute("data-tight")})}],
+ parseDOM: [{tag: "ul", getAttrs: dom => ({tight: (dom as HTMLElement).hasAttribute("data-tight")})}],
toDOM(node) { return ["ul", {"data-tight": node.attrs.tight ? "true" : null}, 0] }
},
@@ -98,9 +98,9 @@ export const schema = new Schema({
draggable: true,
parseDOM: [{tag: "img[src]", getAttrs(dom) {
return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt")
+ src: (dom as HTMLElement).getAttribute("src"),
+ title: (dom as HTMLElement).getAttribute("title"),
+ alt: (dom as HTMLElement).getAttribute("alt")
}
}}],
toDOM(node) { return ["img", node.attrs] }
@@ -124,7 +124,7 @@ export const schema = new Schema({
strong: {
parseDOM: [{tag: "b"}, {tag: "strong"},
- {style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}],
+ {style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null}],
toDOM() { return ["strong"] }
},
@@ -135,7 +135,7 @@ export const schema = new Schema({
},
inclusive: false,
parseDOM: [{tag: "a[href]", getAttrs(dom) {
- return {href: dom.getAttribute("href"), title: dom.getAttribute("title")}
+ return {href: (dom as HTMLElement).getAttribute("href"), title: (dom as HTMLElement).getAttribute("title")}
}}],
toDOM(node) { return ["a", node.attrs] }
},
diff --git a/src/to_markdown.js b/src/to_markdown.js
deleted file mode 100644
index 6863344..0000000
--- a/src/to_markdown.js
+++ /dev/null
@@ -1,415 +0,0 @@
-// ::- A specification for serializing a ProseMirror document as
-// Markdown/CommonMark text.
-export class MarkdownSerializer {
- // :: (Object<(state: MarkdownSerializerState, node: Node, parent: Node, index: number)>, Object, ?Object)
- // Construct a serializer with the given configuration. The `nodes`
- // object should map node names in a given schema to function that
- // take a serializer state and such a node, and serialize the node.
- //
- // The `marks` object should hold objects with `open` and `close`
- // properties, which hold the strings that should appear before and
- // after a piece of text marked that way, either directly or as a
- // function that takes a serializer state and a mark, and returns a
- // string. `open` and `close` can also be functions, which will be
- // called as
- //
- // (state: MarkdownSerializerState, mark: Mark,
- // parent: Fragment, index: number) → string
- //
- // Where `parent` and `index` allow you to inspect the mark's
- // context to see which nodes it applies to.
- //
- // Mark information objects can also have a `mixable` property
- // which, when `true`, indicates that the order in which the mark's
- // opening and closing syntax appears relative to other mixable
- // marks can be varied. (For example, you can say `**a *b***` and
- // `*a **b***`, but not `` `a *b*` ``.)
- //
- // To disable character escaping in a mark, you can give it an
- // `escape` property of `false`. Such a mark has to have the highest
- // precedence (must always be the innermost mark).
- //
- // The `expelEnclosingWhitespace` mark property causes the
- // serializer to move enclosing whitespace from inside the marks to
- // outside the marks. This is necessary for emphasis marks as
- // CommonMark does not permit enclosing whitespace inside emphasis
- // marks, see: http://spec.commonmark.org/0.26/#example-330
- //
- // options::- Optional additional options.
- // escapeExtraCharacters:: ?RegExp
- // Extra characters can be added for escaping. This is passed
- // directly to String.replace(), and the matching characters are
- // preceded by a backslash.
- constructor(nodes, marks, options) {
- // :: Object<(MarkdownSerializerState, Node)> The node serializer
- // functions for this serializer.
- this.nodes = nodes
- // :: Object The mark serializer info.
- this.marks = marks
- this.options = options || {}
- }
-
- // :: (Node, ?Object) → string
- // Serialize the content of the given node to
- // [CommonMark](http://commonmark.org/).
- serialize(content, options) {
- options = Object.assign(this.options, options)
- let state = new MarkdownSerializerState(this.nodes, this.marks, options)
- state.renderContent(content)
- return state.out
- }
-}
-
-// :: MarkdownSerializer
-// A serializer for the [basic schema](#schema).
-export const defaultMarkdownSerializer = new MarkdownSerializer({
- blockquote(state, node) {
- state.wrapBlock("> ", null, node, () => state.renderContent(node))
- },
- code_block(state, node) {
- state.write("```" + (node.attrs.params || "") + "\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)
- },
- horizontal_rule(state, node) {
- state.write(node.attrs.markup || "---")
- state.closeBlock(node)
- },
- bullet_list(state, node) {
- state.renderList(node, " ", () => (node.attrs.bullet || "*") + " ")
- },
- ordered_list(state, node) {
- let start = node.attrs.order || 1
- let maxW = String(start + node.childCount - 1).length
- let space = state.repeat(" ", maxW + 2)
- state.renderList(node, space, i => {
- let nStr = String(start + i)
- return state.repeat(" ", maxW - nStr.length) + nStr + ". "
- })
- },
- list_item(state, node) {
- state.renderContent(node)
- },
- paragraph(state, node) {
- state.renderInline(node)
- state.closeBlock(node)
- },
-
- image(state, node) {
- state.write("![" + state.esc(node.attrs.alt || "") + "](" + node.attrs.src +
- (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")")
- },
- hard_break(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) {
- state.text(node.text)
- }
-}, {
- em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true},
- strong: {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) ? ">"
- : "](" + mark.attrs.href + (mark.attrs.title ? ' "' + mark.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")"
- }
- },
- 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}
-})
-
-function backticksFor(node, side) {
- let ticks = /`+/g, m, len = 0
- if (node.isText) 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
-}
-
-function isPlainURL(link, parent, index, side) {
- if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false
- let 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
- let next = parent.child(index + (side < 0 ? -2 : 1))
- return !link.isInSet(next.marks)
-}
-
-// ::- This is an object used to track state and expose
-// methods related to markdown serialization. Instances are passed to
-// node and mark serialization methods (see `toMarkdown`).
-export class MarkdownSerializerState {
- constructor(nodes, marks, options) {
- this.nodes = nodes
- this.marks = marks
- this.delim = this.out = ""
- this.closed = false
- this.inTightList = false
- // :: Object
- // The options passed to the serializer.
- // tightLists:: ?bool
- // Whether to render lists in a tight style. This can be overridden
- // on a node level by specifying a tight attribute on the node.
- // Defaults to false.
- this.options = options || {}
- if (typeof this.options.tightLists == "undefined")
- this.options.tightLists = false
- }
-
- flushClose(size) {
- if (this.closed) {
- if (!this.atBlank()) this.out += "\n"
- if (size == null) size = 2
- if (size > 1) {
- let delimMin = this.delim
- let trim = /\s+$/.exec(delimMin)
- if (trim) delimMin = delimMin.slice(0, delimMin.length - trim[0].length)
- for (let i = 1; i < size; i++)
- this.out += delimMin + "\n"
- }
- this.closed = false
- }
- }
-
- // :: (string, ?string, Node, ())
- // 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, firstDelim, node, f) {
- let old = this.delim
- this.write(firstDelim || delim)
- this.delim += delim
- f()
- this.delim = old
- this.closeBlock(node)
- }
-
- atBlank() {
- return /(^|\n)$/.test(this.out)
- }
-
- // :: ()
- // Ensure the current content ends with a newline.
- ensureNewLine() {
- if (!this.atBlank()) this.out += "\n"
- }
-
- // :: (?string)
- // 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) {
- this.flushClose()
- if (this.delim && this.atBlank())
- this.out += this.delim
- if (content) this.out += content
- }
-
- // :: (Node)
- // Close the block for the given node.
- closeBlock(node) {
- this.closed = node
- }
-
- // :: (string, ?bool)
- // Add the given text to the document. When escape is not `false`,
- // it will be escaped.
- text(text, escape) {
- let lines = text.split("\n")
- for (let i = 0; i < lines.length; i++) {
- var startOfLine = this.atBlank() || this.closed
- this.write()
- this.out += escape !== false ? this.esc(lines[i], startOfLine) : lines[i]
- if (i != lines.length - 1) this.out += "\n"
- }
- }
-
- // :: (Node)
- // Render the given node as a block.
- render(node, parent, index) {
- if (typeof parent == "number") throw new Error("!")
- if (!this.nodes[node.type.name]) throw new Error("Token type `" + node.type.name + "` not supported by Markdown renderer")
- this.nodes[node.type.name](this, node, parent, index)
- }
-
- // :: (Node)
- // Render the contents of `parent` as block nodes.
- renderContent(parent) {
- parent.forEach((node, _, i) => this.render(node, parent, i))
- }
-
- // :: (Node)
- // Render the contents of `parent` as inline content.
- renderInline(parent) {
- let active = [], trailing = ""
- let progress = (node, _, index) => {
- let marks = node ? node.marks : []
-
- // Remove marks from `hard_break` that are the last node inside
- // that mark to prevent parser edge cases with new lines just
- // before closing marks.
- // (FIXME it'd be nice if we had a schema-agnostic way to
- // identify nodes that serialize as hard breaks)
- if (node && node.type.name === "hard_break")
- marks = marks.filter(m => {
- if (index + 1 == parent.childCount) return false
- let next = parent.child(index + 1)
- return m.isInSet(next.marks) && (!next.isText || /\S/.test(next.text))
- })
-
- let leading = trailing
- trailing = ""
- // If whitespace has to be expelled from the node, adjust
- // leading and trailing accordingly.
- if (node && node.isText && marks.some(mark => {
- let info = this.marks[mark.type.name]
- return info && info.expelEnclosingWhitespace
- })) {
- let [_, lead, inner, trail] = /^(\s*)(.*?)(\s*)$/m.exec(node.text)
- leading += lead
- trailing = trail
- if (lead || trail) {
- node = inner ? node.withText(inner) : null
- if (!node) marks = active
- }
- }
-
- let inner = marks.length && marks[marks.length - 1], noEsc = inner && this.marks[inner.type.name].escape === false
- let len = marks.length - (noEsc ? 1 : 0)
-
- // Try to reorder 'mixable' marks, such as em and strong, which
- // in Markdown may be opened and closed in different order, so
- // that order of the marks for the token matches the order in
- // active.
- outer: for (let i = 0; i < len; i++) {
- let mark = marks[i]
- if (!this.marks[mark.type.name].mixable) break
- for (let j = 0; j < active.length; j++) {
- let other = active[j]
- if (!this.marks[other.type.name].mixable) break
- if (mark.eq(other)) {
- if (i > j)
- marks = marks.slice(0, j).concat(mark).concat(marks.slice(j, i)).concat(marks.slice(i + 1, len))
- else if (j > i)
- marks = marks.slice(0, i).concat(marks.slice(i + 1, j)).concat(mark).concat(marks.slice(j, len))
- continue outer
- }
- }
- }
-
- // Find the prefix of the mark set that didn't change
- let keep = 0
- while (keep < Math.min(active.length, len) && marks[keep].eq(active[keep])) ++keep
-
- // Close the marks that need to be closed
- while (keep < active.length)
- this.text(this.markString(active.pop(), false, parent, index), false)
-
- // Output any previously expelled trailing whitespace outside the marks
- if (leading) this.text(leading)
-
- // Open the marks that need to be opened
- if (node) {
- while (active.length < len) {
- let add = marks[active.length]
- active.push(add)
- this.text(this.markString(add, true, parent, index), false)
- }
-
- // Render the node. Special case code marks, since their content
- // may not be escaped.
- if (noEsc && node.isText)
- this.text(this.markString(inner, true, parent, index) + node.text +
- this.markString(inner, false, parent, index + 1), false)
- else
- this.render(node, parent, index)
- }
- }
- parent.forEach(progress)
- progress(null, null, parent.childCount)
- }
-
- // :: (Node, string, (number) → string)
- // 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, delim, firstDelim) {
- if (this.closed && this.closed.type == node.type)
- this.flushClose(3)
- else if (this.inTightList)
- this.flushClose(1)
-
- let isTight = typeof node.attrs.tight != "undefined" ? node.attrs.tight : this.options.tightLists
- let prevTight = this.inTightList
- this.inTightList = isTight
- node.forEach((child, _, i) => {
- if (i && isTight) this.flushClose(1)
- this.wrapBlock(delim, firstDelim(i), node, () => this.render(child, node, i))
- })
- this.inTightList = prevTight
- }
-
- // :: (string, ?bool) → string
- // Escape the given string so that it can safely appear in Markdown
- // content. If `startOfLine` is true, also escape characters that
- // have special meaning only at the start of the line.
- esc(str, startOfLine) {
- str = str.replace(
- /[`*\\~\[\]_]/g,
- (m, i) => m == "_" && i > 0 && i + 1 < str.length && str[i-1].match(/\w/) && str[i+1].match(/\w/) ? m : "\\" + m
- )
- if (startOfLine) str = str.replace(/^[:#\-*+>]/, "\\$&").replace(/^(\s*\d+)\./, "$1\\.")
- if (this.options.escapeExtraCharacters) str = str.replace(this.options.escapeExtraCharacters, "\\$&")
- return str
- }
-
- quote(str) {
- var wrap = str.indexOf('"') == -1 ? '""' : str.indexOf("'") == -1 ? "''" : "()"
- return wrap[0] + str + wrap[1]
- }
-
- // :: (string, number) → string
- // Repeat the given string `n` times.
- repeat(str, n) {
- let out = ""
- for (let i = 0; i < n; i++) out += str
- return out
- }
-
- // : (Mark, bool, string?) → string
- // Get the markdown string for a given opening or closing mark.
- markString(mark, open, parent, index) {
- let info = this.marks[mark.type.name]
- let value = open ? info.open : info.close
- return typeof value == "string" ? value : value(this, mark, parent, index)
- }
-
- // :: (string) → { leading: ?string, trailing: ?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) {
- return {
- leading: (text.match(/^(\s+)/) || [])[0],
- trailing: (text.match(/(\s+)$/) || [])[0]
- }
- }
-}
diff --git a/src/to_markdown.ts b/src/to_markdown.ts
new file mode 100644
index 0000000..8a7686e
--- /dev/null
+++ b/src/to_markdown.ts
@@ -0,0 +1,427 @@
+import {Node, Mark} from "prosemirror-model"
+
+type MarkSerializerSpec = {
+ /// The string that should appear before a piece of content marked
+ /// by this mark, either directly or as a function that returns an
+ /// appropriate string.
+ open: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string),
+ /// The string that should appear after a piece of content marked by
+ /// this mark.
+ close: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string),
+ /// When `true`, this indicates that the order in which the mark's
+ /// opening and closing syntax appears relative to other mixable
+ /// marks can be varied. (For example, you can say `**a *b***` and
+ /// `*a **b***`, but not `` `a *b*` ``.)
+ mixable?: boolean,
+ /// When enabled, causes the serializer to move enclosing whitespace
+ /// from inside the marks to outside the marks. This is necessary
+ /// for emphasis marks as CommonMark does not permit enclosing
+ /// whitespace inside emphasis marks, see:
+ /// http:///spec.commonmark.org/0.26/#example-330
+ expelEnclosingWhitespace?: boolean,
+ /// Can be set to `false` to disable character escaping in a mark. A
+ /// non-escaping mark has to have the highest precedence (must
+ /// always be the innermost mark).
+ escape?: boolean
+}
+
+/// A specification for serializing a ProseMirror document as
+/// Markdown/CommonMark text.
+export class MarkdownSerializer {
+ /// Construct a serializer with the given configuration. The `nodes`
+ /// object should map node names in a given schema to function that
+ /// take a serializer state and such a node, and serialize the node.
+ constructor(
+ /// The node serializer functions for this serializer.
+ readonly nodes: {[node: string]: (state: MarkdownSerializerState, node: Node, parent: Node, index: number) => void},
+ /// The mark serializer info.
+ readonly marks: {[mark: string]: MarkSerializerSpec},
+ readonly options: {
+ /// Extra characters can be added for escaping. This is passed
+ /// directly to String.replace(), and the matching characters are
+ /// preceded by a backslash.
+ escapeExtraCharacters?: RegExp,
+ /// Specify the node name of hard breaks.
+ /// Defaults to "hard_break"
+ hardBreakNodeName?: string
+ } = {}
+ ) {}
+
+ /// Serialize the content of the given node to
+ /// [CommonMark](http://commonmark.org/).
+ serialize(content: Node, options: {
+ /// Whether to render lists in a tight style. This can be overridden
+ /// on a node level by specifying a tight attribute on the node.
+ /// Defaults to false.
+ tightLists?: boolean
+ } = {}) {
+ options = Object.assign({}, this.options, options)
+ let state = new MarkdownSerializerState(this.nodes, this.marks, options)
+ state.renderContent(content)
+ return state.out
+ }
+}
+
+/// A serializer for the [basic schema](#schema).
+export const defaultMarkdownSerializer = new MarkdownSerializer({
+ blockquote(state, node) {
+ state.wrapBlock("> ", null, node, () => state.renderContent(node))
+ },
+ code_block(state, node) {
+ // Make sure the front matter fences are longer than any dash sequence within it
+ const backticks = node.textContent.match(/`{3,}/gm)
+ const fence = backticks ? (backticks.sort().slice(-1)[0] + "`") : "```"
+
+ state.write(fence + (node.attrs.params || "") + "\n")
+ state.text(node.textContent, false)
+ // Add a newline to the current content before adding closing marker
+ state.write("\n")
+ state.write(fence)
+ state.closeBlock(node)
+ },
+ heading(state, node) {
+ state.write(state.repeat("#", node.attrs.level) + " ")
+ state.renderInline(node)
+ state.closeBlock(node)
+ },
+ horizontal_rule(state, node) {
+ state.write(node.attrs.markup || "---")
+ state.closeBlock(node)
+ },
+ bullet_list(state, node) {
+ state.renderList(node, " ", () => (node.attrs.bullet || "*") + " ")
+ },
+ ordered_list(state, node) {
+ let start = node.attrs.order || 1
+ let maxW = String(start + node.childCount - 1).length
+ let space = state.repeat(" ", maxW + 2)
+ state.renderList(node, space, i => {
+ let nStr = String(start + i)
+ return state.repeat(" ", maxW - nStr.length) + nStr + ". "
+ })
+ },
+ list_item(state, node) {
+ state.renderContent(node)
+ },
+ paragraph(state, node) {
+ state.renderInline(node)
+ state.closeBlock(node)
+ },
+
+ image(state, node) {
+ state.write("![" + state.esc(node.attrs.alt || "") + "](" + node.attrs.src.replace(/[\(\)]/g, "\\$&") +
+ (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")")
+ },
+ hard_break(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) {
+ state.text(node.text!, !state.inAutolink)
+ }
+}, {
+ em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true},
+ strong: {open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true},
+ link: {
+ open(state, mark, parent, index) {
+ state.inAutolink = isPlainURL(mark, parent, index)
+ return state.inAutolink ? "<" : "["
+ },
+ close(state, mark, parent, index) {
+ let {inAutolink} = state
+ state.inAutolink = undefined
+ return inAutolink ? ">"
+ : "](" + mark.attrs.href.replace(/[\(\)"]/g, "\\$&") + (mark.attrs.title ? ` "${mark.attrs.title.replace(/"/g, '\\"')}"` : "") + ")"
+ },
+ mixable: true
+ },
+ 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}
+})
+
+function backticksFor(node: Node, side: number) {
+ let ticks = /`+/g, m, len = 0
+ if (node.isText) 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
+}
+
+function isPlainURL(link: Mark, parent: Node, index: number) {
+ if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false
+ let content = parent.child(index)
+ if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) return false
+ return index == parent.childCount - 1 || !link.isInSet(parent.child(index + 1).marks)
+}
+
+/// This is an object used to track state and expose
+/// methods related to markdown serialization. Instances are passed to
+/// node and mark serialization methods (see `toMarkdown`).
+export class MarkdownSerializerState {
+ /// @internal
+ delim: string = ""
+ /// @internal
+ out: string = ""
+ /// @internal
+ closed: Node | null = null
+ /// @internal
+ inAutolink: boolean | undefined = undefined
+ /// @internal
+ atBlockStart: boolean = false
+ /// @internal
+ inTightList: boolean = false
+
+ /// @internal
+ constructor(
+ /// @internal
+ readonly nodes: {[node: string]: (state: MarkdownSerializerState, node: Node, parent: Node, index: number) => void},
+ /// @internal
+ readonly marks: {[mark: string]: MarkSerializerSpec},
+ /// The options passed to the serializer.
+ readonly options: {tightLists?: boolean, escapeExtraCharacters?: RegExp, hardBreakNodeName?: string}
+ ) {
+ if (typeof this.options.tightLists == "undefined")
+ this.options.tightLists = false
+ if (typeof this.options.hardBreakNodeName == "undefined")
+ this.options.hardBreakNodeName = "hard_break"
+ }
+
+ /// @internal
+ flushClose(size: number = 2) {
+ if (this.closed) {
+ if (!this.atBlank()) this.out += "\n"
+ if (size > 1) {
+ let delimMin = this.delim
+ let trim = /\s+$/.exec(delimMin)
+ if (trim) delimMin = delimMin.slice(0, delimMin.length - trim[0].length)
+ for (let i = 1; i < size; i++)
+ this.out += delimMin + "\n"
+ }
+ this.closed = 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 | null, node: Node, f: () => void) {
+ let old = this.delim
+ this.write(firstDelim != null ? firstDelim : delim)
+ this.delim += delim
+ f()
+ this.delim = old
+ this.closeBlock(node)
+ }
+
+ /// @internal
+ atBlank() {
+ return /(^|\n)$/.test(this.out)
+ }
+
+ /// Ensure the current content ends with a newline.
+ ensureNewLine() {
+ if (!this.atBlank()) this.out += "\n"
+ }
+
+ /// 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) {
+ this.flushClose()
+ if (this.delim && this.atBlank())
+ this.out += this.delim
+ if (content) this.out += content
+ }
+
+ /// Close the block for the given node.
+ closeBlock(node: Node) {
+ this.closed = node
+ }
+
+ /// Add the given text to the document. When escape is not `false`,
+ /// it will be escaped.
+ text(text: string, escape = true) {
+ let lines = text.split("\n")
+ for (let i = 0; i < lines.length; i++) {
+ this.write()
+ // Escape exclamation marks in front of links
+ if (!escape && lines[i][0] == "[" && /(^|[^\\])\!$/.test(this.out))
+ this.out = this.out.slice(0, this.out.length - 1) + "\\!"
+ this.out += escape ? this.esc(lines[i], this.atBlockStart) : lines[i]
+ if (i != lines.length - 1) this.out += "\n"
+ }
+ }
+
+ /// Render the given node as a block.
+ render(node: Node, parent: Node, index: number) {
+ if (typeof parent == "number") throw new Error("!")
+ if (!this.nodes[node.type.name]) throw new Error("Token type `" + node.type.name + "` not supported by Markdown renderer")
+ this.nodes[node.type.name](this, node, parent, index)
+ }
+
+ /// Render the contents of `parent` as block nodes.
+ renderContent(parent: Node) {
+ parent.forEach((node, _, i) => this.render(node, parent, i))
+ }
+
+ /// Render the contents of `parent` as inline content.
+ renderInline(parent: Node) {
+ this.atBlockStart = true
+ let active: Mark[] = [], trailing = ""
+ let progress = (node: Node | null, offset: number, index: number) => {
+ let marks = node ? node.marks : []
+
+ // Remove marks from `hard_break` that are the last node inside
+ // that mark to prevent parser edge cases with new lines just
+ // before closing marks.
+ if (node && node.type.name === this.options.hardBreakNodeName)
+ marks = marks.filter(m => {
+ if (index + 1 == parent.childCount) return false
+ let next = parent.child(index + 1)
+ return m.isInSet(next.marks) && (!next.isText || /\S/.test(next.text!))
+ })
+
+ let leading = trailing
+ trailing = ""
+ // If whitespace has to be expelled from the node, adjust
+ // leading and trailing accordingly.
+ if (node && node.isText && marks.some(mark => {
+ let info = this.marks[mark.type.name]
+ return info && info.expelEnclosingWhitespace &&
+ !(mark.isInSet(active) || index < parent.childCount - 1 && mark.isInSet(parent.child(index + 1).marks))
+ })) {
+ let [_, lead, inner, trail] = /^(\s*)(.*?)(\s*)$/m.exec(node.text!)!
+ leading += lead
+ trailing = trail
+ if (lead || trail) {
+ node = inner ? (node as any).withText(inner) : null
+ if (!node) marks = active
+ }
+ }
+
+ let inner = marks.length ? marks[marks.length - 1] : null
+ let noEsc = inner && this.marks[inner.type.name].escape === false
+ let len = marks.length - (noEsc ? 1 : 0)
+
+ // Try to reorder 'mixable' marks, such as em and strong, which
+ // in Markdown may be opened and closed in different order, so
+ // that order of the marks for the token matches the order in
+ // active.
+ outer: for (let i = 0; i < len; i++) {
+ let mark = marks[i]
+ if (!this.marks[mark.type.name].mixable) break
+ for (let j = 0; j < active.length; j++) {
+ let other = active[j]
+ if (!this.marks[other.type.name].mixable) break
+ if (mark.eq(other)) {
+ if (i > j)
+ marks = marks.slice(0, j).concat(mark).concat(marks.slice(j, i)).concat(marks.slice(i + 1, len))
+ else if (j > i)
+ marks = marks.slice(0, i).concat(marks.slice(i + 1, j)).concat(mark).concat(marks.slice(j, len))
+ continue outer
+ }
+ }
+ }
+
+ // Find the prefix of the mark set that didn't change
+ let keep = 0
+ while (keep < Math.min(active.length, len) && marks[keep].eq(active[keep])) ++keep
+
+ // Close the marks that need to be closed
+ while (keep < active.length)
+ this.text(this.markString(active.pop()!, false, parent, index), false)
+
+ // Output any previously expelled trailing whitespace outside the marks
+ if (leading) this.text(leading)
+
+ // Open the marks that need to be opened
+ if (node) {
+ while (active.length < len) {
+ let add = marks[active.length]
+ active.push(add)
+ this.text(this.markString(add, true, parent, index), false)
+ }
+
+ // Render the node. Special case code marks, since their content
+ // may not be escaped.
+ if (noEsc && node.isText)
+ this.text(this.markString(inner!, true, parent, index) + node.text +
+ this.markString(inner!, false, parent, index + 1), false)
+ else
+ this.render(node, parent, index)
+ }
+ }
+ parent.forEach(progress)
+ progress(null, 0, parent.childCount)
+ this.atBlockStart = false
+ }
+
+ /// 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: Node, delim: string, firstDelim: (index: number) => string) {
+ if (this.closed && this.closed.type == node.type)
+ this.flushClose(3)
+ else if (this.inTightList)
+ this.flushClose(1)
+
+ let isTight = typeof node.attrs.tight != "undefined" ? node.attrs.tight : this.options.tightLists
+ let prevTight = this.inTightList
+ this.inTightList = isTight
+ node.forEach((child, _, i) => {
+ if (i && isTight) this.flushClose(1)
+ this.wrapBlock(delim, firstDelim(i), node, () => this.render(child, node, i))
+ })
+ this.inTightList = prevTight
+ }
+
+ /// Escape the given string so that it can safely appear in Markdown
+ /// content. If `startOfLine` is true, also escape characters that
+ /// have special meaning only at the start of the line.
+ esc(str: string, startOfLine = false) {
+ str = str.replace(
+ /[`*\\~\[\]_]/g,
+ (m, i) => m == "_" && i > 0 && i + 1 < str.length && str[i-1].match(/\w/) && str[i+1].match(/\w/) ? m : "\\" + m
+ )
+ if (startOfLine) str = str.replace(/^[#\-*+>]/, "\\$&").replace(/^(\s*\d+)\./, "$1\\.")
+ if (this.options.escapeExtraCharacters) str = str.replace(this.options.escapeExtraCharacters, "\\$&")
+ return str
+ }
+
+ /// @internal
+ quote(str: string) {
+ let wrap = str.indexOf('"') == -1 ? '""' : str.indexOf("'") == -1 ? "''" : "()"
+ return wrap[0] + str + wrap[1]
+ }
+
+ /// Repeat the given string `n` times.
+ repeat(str: string, n: number) {
+ let out = ""
+ for (let i = 0; i < n; i++) out += str
+ return out
+ }
+
+ /// Get the markdown string for a given opening or closing mark.
+ markString(mark: Mark, open: boolean, parent: Node, index: number) {
+ let info = this.marks[mark.type.name]
+ let value = open ? info.open : info.close
+ return typeof value == "string" ? value : value(this, mark, parent, index)
+ }
+
+ /// 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, trailing?: string} {
+ return {
+ leading: (text.match(/^(\s+)/) || [undefined])[0],
+ trailing: (text.match(/(\s+)$/) || [undefined])[0]
+ }
+ }
+}
diff --git a/test/build.js b/test/build.js
deleted file mode 100644
index 25a108b..0000000
--- a/test/build.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const {builders} = require("prosemirror-test-builder")
-const {schema} = require("..")
-
-module.exports = builders(schema, {
- p: {nodeType: "paragraph"},
- h1: {nodeType: "heading", level: 1},
- h2: {nodeType: "heading", level: 2},
- hr: {nodeType: "horizontal_rule"},
- li: {nodeType: "list_item"},
- ol: {nodeType: "ordered_list"},
- ol3: {nodeType: "ordered_list", order: 3},
- ul: {nodeType: "bullet_list"},
- pre: {nodeType: "code_block"},
- a: {markType: "link", href: "foo"},
- br: {nodeType: "hard_break"},
- img: {nodeType: "image", src: "img.png", alt: "x"}
-})
diff --git a/test/build.ts b/test/build.ts
new file mode 100644
index 0000000..808590f
--- /dev/null
+++ b/test/build.ts
@@ -0,0 +1,36 @@
+import {builders, NodeBuilder, MarkBuilder} from "prosemirror-test-builder"
+import {schema} from "prosemirror-markdown"
+
+const b = builders(schema, {
+ p: {nodeType: "paragraph"},
+ h1: {nodeType: "heading", level: 1},
+ h2: {nodeType: "heading", level: 2},
+ hr: {nodeType: "horizontal_rule"},
+ li: {nodeType: "list_item"},
+ ol: {nodeType: "ordered_list"},
+ ol3: {nodeType: "ordered_list", order: 3},
+ ul: {nodeType: "bullet_list"},
+ pre: {nodeType: "code_block"},
+ a: {markType: "link", href: "foo"},
+ br: {nodeType: "hard_break"},
+ img: {nodeType: "image", src: "img.png", alt: "x"}
+}) as any
+
+export const doc: NodeBuilder = b.doc
+export const p: NodeBuilder = b.p
+export const h1: NodeBuilder = b.h1
+export const h2: NodeBuilder = b.h2
+export const hr: NodeBuilder = b.hr
+export const li: NodeBuilder = b.li
+export const ol: NodeBuilder = b.ol
+export const ol3: NodeBuilder = b.ol3
+export const ul: NodeBuilder = b.ul
+export const pre: NodeBuilder = b.pre
+export const blockquote: NodeBuilder = b.blockquote
+export const br: NodeBuilder = b.br
+export const img: NodeBuilder = b.img
+export const a: MarkBuilder = b.a
+export const link: MarkBuilder = b.link
+export const em: MarkBuilder = b.em
+export const strong: MarkBuilder = b.strong
+export const code: MarkBuilder = b.code
diff --git a/test/test-custom-parser.js b/test/test-custom-parser.ts
similarity index 58%
rename from test/test-custom-parser.js
rename to test/test-custom-parser.ts
index 6d7d9d0..23a4bc3 100644
--- a/test/test-custom-parser.js
+++ b/test/test-custom-parser.ts
@@ -1,10 +1,11 @@
-const {eq} = require("prosemirror-test-builder")
-const ist = require("ist")
+import {eq} from "prosemirror-test-builder"
+import ist from "ist"
+// @ts-ignore
+import markdownit from "markdown-it"
+import {Node} from "prosemirror-model"
+import {schema, MarkdownParser} from "prosemirror-markdown"
-const markdownit = require("markdown-it")
-const {schema, MarkdownParser} = require("..")
-
-const {doc, p, hard_break} = require("./build")
+import {doc, p, br} from "./build.js"
const md = markdownit("commonmark", {html: false})
const ignoreBlockquoteParser = new MarkdownParser(schema, md, {
@@ -13,8 +14,8 @@ const ignoreBlockquoteParser = new MarkdownParser(schema, md, {
softbreak: {node: 'hard_break'}
})
-function parseWith(parser) {
- return (text, doc) => {
+function parseWith(parser: MarkdownParser) {
+ return (text: string, doc: Node) => {
ist(parser.parse(text), doc, eq)
}
}
@@ -26,5 +27,5 @@ describe("custom markdown parser", () => {
it("converts softbreaks to hard_break nodes", () =>
parseWith(ignoreBlockquoteParser)("hello\nworld!",
- doc(p("hello", hard_break(), 'world!'))))
+ doc(p("hello", br(), 'world!'))))
})
diff --git a/test/test-parse.js b/test/test-parse.ts
similarity index 63%
rename from test/test-parse.js
rename to test/test-parse.ts
index 1604dbc..c60cb39 100644
--- a/test/test-parse.js
+++ b/test/test-parse.ts
@@ -1,19 +1,20 @@
-const {eq} = require("prosemirror-test-builder")
-const ist = require("ist")
+import {eq} from "prosemirror-test-builder"
+import {Node} from "prosemirror-model"
+import ist from "ist"
-const {schema, defaultMarkdownParser, defaultMarkdownSerializer, MarkdownSerializer} = require("..")
+import {schema, defaultMarkdownParser, defaultMarkdownSerializer, MarkdownSerializer} from "prosemirror-markdown"
-const {doc, blockquote, h1, h2, p, hr, li, ol, ol3, ul, pre, em, strong, code, a, link, br, img} = require("./build")
+import {doc, blockquote, h1, h2, p, hr, li, ol, ol3, ul, pre, em, strong, code, a, link, br, img} from "./build.js"
-function parse(text, doc) {
+function parse(text: string, doc: Node) {
ist(defaultMarkdownParser.parse(text), doc, eq)
}
-function serialize(doc, text) {
+function serialize(doc: Node, text: string) {
ist(defaultMarkdownSerializer.serialize(doc), text)
}
-function same(text, doc) {
+function same(text: string, doc: Node) {
parse(text, doc)
serialize(doc, text)
}
@@ -70,6 +71,10 @@ describe("markdown", () => {
same("**[link](foo) is bold**",
doc(p(strong(a("link"), " is bold")))))
+ it("parses emphasis inside links", () =>
+ same("[link *foo **bar** `#`*](foo)",
+ doc(p(a("link ", em("foo ", strong("bar"), " ", code("#")))))))
+
it("parses code mark inside strong text", () =>
same("**`code` is bold**",
doc(p(strong(code("code"), " is bold")))))
@@ -82,6 +87,11 @@ describe("markdown", () => {
serialize(doc(p("Three spaces: ", code(" "))),
"Three spaces: ` `"))
+ it("parses hard breaks", () => {
+ same("foo\\\nbar", doc(p("foo", br(), "bar")))
+ same("*foo\\\nbar*", doc(p(em("foo", br(), "bar"))))
+ })
+
it("parses links", () =>
same("My [link](foo) goes to foo",
doc(p("My ", a("link"), " goes to foo"))))
@@ -111,15 +121,15 @@ describe("markdown", () => {
it("parses an image", () =>
same("Here's an image: ![x](img.png)",
- doc(p("Here's an image: ", img))))
+ doc(p("Here's an image: ", img()))))
it("parses a line break", () =>
same("line one\\\nline two",
- doc(p("line one", br, "line two"))))
+ doc(p("line one", br(), "line two"))))
it("parses a horizontal rule", () =>
same("one two\n\n---\n\nthree",
- doc(p("one two"), hr, p("three"))))
+ doc(p("one two"), hr(), p("three"))))
it("ignores HTML tags", () =>
same("Foo < img> bar",
@@ -130,10 +140,10 @@ describe("markdown", () => {
doc(p("1. foo"))))
it("doesn't fail with line break inside inline mark", () =>
- same("**text1\ntext2**", doc(p(strong("text1\ntext2")))))
+ serialize(doc(p(strong("text1\ntext2"))), "**text1\ntext2**"))
it("drops trailing hard breaks", () =>
- serialize(doc(p("a", br, br)), "a"))
+ serialize(doc(p("a", br(), br())), "a"))
it("expels enclosing whitespace from inside emphasis", () =>
serialize(doc(p("Some emphasized text with", strong(em(" whitespace ")), "surrounding the emphasis.")),
@@ -156,44 +166,58 @@ describe("markdown", () => {
same("foo`*`", doc(p("foo", code("*")))))
it("doesn't escape underscores between word characters", () =>
- same(
- "abc_def",
- doc(p("abc_def"))
- )
- )
+ same("abc_def", doc(p("abc_def"))))
it("doesn't escape strips of underscores between word characters", () =>
- same(
- "abc___def",
- doc(p("abc___def"))
- )
- )
+ same("abc___def", doc(p("abc___def"))))
it("escapes underscores at word boundaries", () =>
- same(
- "\\_abc\\_",
- doc(p("_abc_"))
- )
- )
+ same("\\_abc\\_", doc(p("_abc_"))))
it("escapes underscores surrounded by non-word characters", () =>
- same(
- "/\\_abc\\_)",
- doc(p("/_abc_)"))
- )
- )
-
- context("custom serializer", () => {
- let markdownSerializer = new MarkdownSerializer(
- defaultMarkdownSerializer.nodes,
- defaultMarkdownSerializer.marks,
- {
- escapeExtraCharacters: /[\|!]/g,
- }
- );
-
- it("escapes extra characters from options", () => {
- ist(markdownSerializer.serialize(doc(p("foo|bar!"))), "foo\\|bar\\!");
- });
- });
+ same("/\\_abc\\_)", doc(p("/_abc_)"))))
+
+ it("ensure no escapes in url", () =>
+ parse("[text](https://example.com/_file/#~anchor)",
+ doc(p(a({href: "https://example.com/_file/#~anchor"}, "text")))))
+
+ // Issue #65
+ it("ensure no escapes in autolinks", () =>
+ same("<https://example.com/_file/#~anchor>",
+ doc(p(a({href: "https://example.com/_file/#~anchor"}, "https://example.com/_file/#~anchor")))))
+
+ // Issue #73
+ it("escape ! in front of links", () =>
+ serialize(doc(p("!", a("text"))), "\\![text](foo)"))
+
+ // Issue #78
+ it("escape of URL in links and images", () => {
+ serialize(doc(p(a({href: "foo):"}, "link"))), "[link](foo\\):)")
+ serialize(doc(p(a({href: "(foo"}, "link"))), "[link](\\(foo)")
+ serialize(doc(p(img({src: "foo):"}))), "![x](foo\\):)")
+ serialize(doc(p(img({src: "(foo"}))), "![x](\\(foo)")
+ serialize(doc(p(a({title: "bar", href: "foo%20\""}, "link"))), "[link](foo%20\\\" \"bar\")")
+ })
+
+ it("escapes extra characters from options", () => {
+ let markdownSerializer = new MarkdownSerializer(defaultMarkdownSerializer.nodes,
+ defaultMarkdownSerializer.marks,
+ {escapeExtraCharacters: /[\|!]/g})
+ ist(markdownSerializer.serialize(doc(p("foo|bar!"))), "foo\\|bar\\!")
+ })
+
+ it("escapes list markers inside lists", () => {
+ same("* 1\\. hi\n\n* x", doc(ul(li(p("1. hi")), li(p("x")))))
+ })
+
+ // Issue #88
+ it("code block fence adjusts to content", () => {
+ same("````\n```\ncode\n```\n````", doc(pre("```\ncode\n```")))
+ })
+
+ it("parses a code block ends with empty line", () => {
+ const originalText = "1\n"
+ const mdText = defaultMarkdownSerializer.serialize(doc(schema.node("code_block", {params: ""}, [schema.text(originalText)])))
+ same(mdText, doc(schema.node("code_block", {params: ""}, [schema.text(originalText)])))
+ })
})
More details
Historical runs
- failed: [1m[31m[!] [1mError: Cannot find module '/<<PKGBUILDDIR>>/rollup.config.js' imported from /usr/share/nodejs/rollup/dist/shared/loadConfigFile.js[22m[1m[39m[22m
- failed: [1m[31m[!] [1mError: Cannot find module '/<<PKGBUILDDIR>>/rollup.config.js' imported from /usr/share/nodejs/rollup/dist/shared/loadConfigFile.js[22m[1m[39m[22m
- failed: [1m[31m[!] [1mError: Cannot find module '/<<PKGBUILDDIR>>/rollup.config.js' imported from /usr/share/nodejs/rollup/dist/shared/loadConfigFile.js[22m[1m[39m[22m
- aborted: Killed by signal
- build-failed-stage-build: dh_auto_build: error: cd ./. && sh -ex debian/nodejs/./build returned exit code 1
- nothing-to-do: Last upstream version 1.8.0 already imported.
- nothing-to-do: Last upstream version 1.8.0 already imported.
- nothing-new-to-do: Last upstream version 1.8.0 already merged.
- success: Merged new upstream version 1.7.1