New Upstream Release - node-prosemirror-markdown

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>&lt;<span class="string">&quot;doc&quot;</span> | <span class="string">&quot;paragraph&quot;</span> | <span class="string">&quot;blockquote&quot;</span> | <span class="string">&quot;horizontal_rule&quot;</span> | <span class="string">&quot;heading&quot;</span> | <span class="string">&quot;code_block&quot;</span> | <span class="string">&quot;ordered_list&quot;</span> | <span class="string">&quot;bullet_list&quot;</span> | <span class="string">&quot;list_item&quot;</span> | <span class="string">&quot;text&quot;</span> | <span class="string">&quot;image&quot;</span> | <span class="string">&quot;hard_break&quot;</span>, <span class="string">&quot;em&quot;</span> | <span class="string">&quot;strong&quot;</span> | <span class="string">&quot;link&quot;</span> | <span class="string">&quot;code&quot;</span>&gt;</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>&lt;<a href="#ParseSpec"><span class="type">ParseSpec</span></a>&gt;)</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>&lt;<a href="#ParseSpec"><span class="type">ParseSpec</span></a>&gt;</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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&lt;<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>)&gt;, <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>&lt;<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a>&gt;, <a id="MarkdownSerializer.constructor^options" href="#MarkdownSerializer.constructor^options"><span class=param>options</span></a>&#8288;?: <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>&#8288;?: <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>&lt;<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>)&gt;</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>&lt;<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object"><span class="type">Object</span></a>&gt;</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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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>&#8288;?: <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

Full run details

Historical runs