pax_global_header00006660000000000000000000000064152663703310014520gustar00rootroot0000000000000052 comment=9a7d517c12295db8607bdabd68dd499219518ade prosemirror-markdown/000077500000000000000000000000001526637033100152675ustar00rootroot00000000000000prosemirror-markdown/.gitignore000066400000000000000000000000521526637033100172540ustar00rootroot00000000000000/node_modules .tern-port /dist /test/*.js prosemirror-markdown/.npmignore000066400000000000000000000000371526637033100172660ustar00rootroot00000000000000/node_modules .tern-port /test prosemirror-markdown/.npmrc000066400000000000000000000000231526637033100164020ustar00rootroot00000000000000package-lock=false prosemirror-markdown/.tern-project000066400000000000000000000001571526637033100177070ustar00rootroot00000000000000{ "libs": ["browser"], "plugins": { "node": {}, "complete_strings": {}, "es_modules": {} } } prosemirror-markdown/CHANGELOG.md000066400000000000000000000177341526637033100171140ustar00rootroot00000000000000## 1.13.9 (2026-10-22) ### Bug fixes Improve Markdown serializer performance. ## 1.13.8 (2026-09-21) ### Bug fixes Add a validator that makes sure the heading node's `level` attribute is validated as a number. ## 1.13.7 (2026-08-31) ### Bug fixes Make sure trailing whitespace is expelled from strong/emphasis marks when they are broken up by other marks. ## 1.13.6 (2026-08-16) ### Bug fixes Fix an issue where `MarkdownSerializer` could emit invalid strong/emphasis marks when the mark was broken up by another mark right before whitespace. ## 1.13.5 (2026-07-06) ### Bug fixes Don't drop an `order` attribute of 0 for numbered lists when serializing. ## 1.13.4 (2026-02-04) ### Bug fixes Fix a regression that caused the serializer to crash when a block ends in a hard break. ## 1.13.3 (2026-01-20) ### Bug fixes Properly move trailing whitespace out of marks when they end in a hard break node. ## 1.13.2 (2025-03-18) ### Bug fixes Add a `code` flag to the code mark. ## 1.13.1 (2024-09-26) ### Bug fixes Fix a type error caused by use of an older markdown-it type package. ## 1.13.0 (2024-05-20) ### Bug fixes Fix the type of `MarkdownParser.parse` to be non-nullable. Add a strict option to MarkdownSerializer ### New features The new `strict` option to `MarkdownSerializer` makes it possible to make the serializer ignore node and mark types it doesn't know. ## 1.12.0 (2023-12-11) ### Bug fixes Block-level markup inside a heading is no longer escaped by the serializer. Do not backslash-escape a `+` at the start of line when it isn't followed by a space. Upgrade to markdown-it 14 ### New features `MarkdownSerializerState.renderInline` now takes a parameter that controls whether block-level markup should be escaped. Upgrade to markdown-it version 14, which provides ES modules. ## 1.11.2 (2023-08-04) ### Bug fixes Fix some unnecessary escapes for period characters in Markdown serialization. Only escape `#` signs if they would otherwise create a heading. Add a test for headings in list items Fix a bug in `MarkdownSerializer` that broken expelling of whitespace from marks when the mark spanned multiple nodes. ## 1.11.1 (2023-06-30) ### Bug fixes Allow any blocks as first child of list items to align with what Markdown itself does. Add parse rules that clear `strong` and `em` marks when inline CSS resets it. ## 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 `MarkdownSerializer` now takes an `escapeExtraCharacters` option that can be used to control backslash-escaping behavior. Fix types for new option ## 1.7.1 (2022-02-16) ### Bug fixes Avoid escaping underscores surrounded by word characters. ## 1.7.0 (2022-01-06) ### New features Upgrade markdown-it to version 12. ## 1.6.2 (2022-01-04) ### Bug fixes Fix a bug where URL text in links and images was overzealously escaped. ## 1.6.1 (2021-12-16) ### Bug fixes Fix a bug where `MarkdownParser.parse` could return null when the parsed content doesn't fit the schema. Make sure underscores are escaped when serializing to Markdown. ## 1.6.0 (2021-09-21) ### New features `MarkdownParser.tokenizer` is now public, for easier creation of parsers that base on other parsers. ## 1.5.2 (2021-09-03) ### Bug fixes Serializing to Markdown now properly escapes '>' characters at the start of the line. ## 1.5.1 (2021-01-06) ### Bug fixes The Markdown parser will now correctly set the `tight` attribute on list nodes. ## 1.5.0 (2020-07-17) ### New features Markdown parse specs can now be specified as `noCloseToken`, which will cause the parser to treat them as a single token, rather than a pair of `_open`/`_close` tokens. ## 1.4.5 (2020-05-14) ### Bug fixes Don't allow hard_break nodes in headings. ## 1.4.4 (2019-12-19) ### Bug fixes Fix issue that broke parsing ordered lists with a starting number other than 1. ## 1.4.3 (2019-12-17) ### Bug fixes Don't use short-hand angle bracket syntax when outputting self-linking URLs that are relative. ## 1.4.2 (2019-11-20) ### Bug fixes Rename ES module files to use a .js extension, since Webpack gets confused by .mjs ## 1.4.1 (2019-11-19) ### Bug fixes The file referred to in the package's `module` field now is compiled down to ES5. ## 1.4.0 (2019-11-08) ### New features Add a `module` field to package json file. ## 1.3.2 (2019-10-30) ### Bug fixes Code blocks in the schema no longer allow marks inside them. Code blocks are now parsed with `preserveWhiteSpace: full`, preventing removal of newline characters. ## 1.3.1 (2019-06-08) ### Bug fixes Fix a bug that could occur when parsing multiple adjacent pieces of text with the same style. ## 1.3.0 (2019-01-22) ### Bug fixes Inline code containing backticks is now serialized wrapped in the appropriate amount of backticks. ### New features The serializer now serializes links whose target is the same as their text content using \< \> syntax. Mark opening and close string callbacks now get passed the mark's context (parent fragment and index). ## 1.2.2 (2018-11-22) ### Bug fixes Hard breaks at the end of an emphasized or strong mark are no longer serialized to invalid Markdown text. ## 1.2.1 (2018-10-19) ### Bug fixes Fixes a bug where inline mark delimiters were serialized incorrectly (the closing and opening marks were swapped, which was only noticeable when they are different). ## 1.2.0 (2018-10-08) ### Bug fixes Fixes an issue where the Markdown serializer would escape special characters in inline code. ### New features Upgrade the markdown-it dependency to version 8. ## 1.1.1 (2018-07-08) ### Bug fixes Fix bug that caused superfluous backslashes to be inserted at the start of some lines when serializing to Markdown. ## 1.1.0 (2018-06-20) ### New features You can now override the handling of softbreak tokens in a custom handler. ## 1.0.4 (2018-04-17) ### Bug fixes Fix crash when serializing marks with line breaks inside of them. ## 1.0.3 (2018-01-10) ### Bug fixes Fix dependency version range for prosemirror-model. ## 1.0.2 (2017-12-07) ### Bug fixes Code blocks are always wrapped in triple backticks when serializing, to avoid parsing corner cases around indented code blocks. ## 1.0.1 (2017-11-05) ### Bug fixes Link marks are now non-inclusive (typing after them produces non-linked text). ## 1.0.0 (2017-10-13) First stable release. prosemirror-markdown/CONTRIBUTING.md000066400000000000000000000073301526637033100175230ustar00rootroot00000000000000# How to contribute - [Getting help](#getting-help) - [Submitting bug reports](#submitting-bug-reports) - [Contributing code](#contributing-code) ## Getting help Community discussion, questions, and informal bug reporting is done on the [discuss.ProseMirror forum](http://discuss.prosemirror.net). ## Submitting bug reports Report bugs on the [issue tracker](https://code.haverbeke.berlin/prosemirror/prosemirror/issues). Before reporting a bug, please read these pointers. - The issue tracker is for *bugs*, not requests for help. Questions should be asked on the [forum](http://discuss.prosemirror.net). - Include information about the version of the code that exhibits the problem. For browser-related issues, include the browser and browser version on which the problem occurred. - Mention very precisely what went wrong. "X is broken" is not a good bug report. What did you expect to happen? What happened instead? Describe the exact steps a maintainer has to take to make the problem occur. A screencast can be useful, but is no substitute for a textual description. - A great way to make it easy to reproduce your problem, if it can not be trivially reproduced on the website demos, is to submit a script that triggers the issue. ## Contributing code Code generated by a language model is not welcome in this project. Please don't waste my time with it. If you want to make a change that involves a significant overhaul of the code or introduces a user-visible new feature, create an [issue](https://code.haverbeke.berlin/prosemirror/prosemirror/issues/) first with your proposal. - Make sure you have a [Codeberg](https://codeberg.org/user/sign_up) or [GitHub](https://github.com/signup/free) account. - Use that to create a [code.haverbeke.berlin account](https://code.haverbeke.berlin/user/login). - Fork the relevant repository. - Create a local checkout of the code. You can use the [main repository](https://code.haverbeke.berlin/prosemirror/prosemirror) to easily check out all core modules. - Make your changes, and commit them - If your changes are easy to test or likely to regress, add tests in the relevant `test/` directory. Either put them in an existing `test-*.js` file, if they fit there, or add a new file. - Make sure all tests pass. Run `npm run test` to verify tests pass (you will need Node.js v6+). - Submit a pull request. Don't put more than one feature/fix in a single pull request. By contributing code to ProseMirror you - Agree to license the contributed code under the project's [MIT license](https://code.haverbeke.berlin/prosemirror/prosemirror/blob/main/LICENSE). - Confirm that you have the right to contribute and license the code in question. (Either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.) ### Coding standards - ES6 syntax, targeting an ES5 runtime (i.e. don't use library elements added by ES6, don't use ES7/ES.next syntax). - 2 spaces per indentation level, no tabs. - No semicolons except when necessary. - Follow the surrounding code when it comes to spacing, brace placement, etc. - Brace-less single-statement bodies are encouraged whenever they don't impact readability. - [getdocs-ts](https://code.haverbeke.berlin/marijn/getdocs-ts)-style doc comments above items that are part of the public API. - When documenting non-public items, you can put the type after a single colon, so that getdocs doesn't pick it up and add it to the API reference. - ProseMirror does *not* follow JSHint or JSLint prescribed style. Patches that try to 'fix' code to pass one of these linters will not be accepted. prosemirror-markdown/LICENSE000066400000000000000000000021211526637033100162700ustar00rootroot00000000000000Copyright (C) 2015-2017 by Marijn Haverbeke 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 in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. prosemirror-markdown/README.md000066400000000000000000000674401526637033100165610ustar00rootroot00000000000000

prosemirror-markdown

[ WEBSITE | ISSUES | FORUM | GITTER ]

This is a (non-core) module for ProseMirror. 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 that corresponds to the document schema used by CommonMark, 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. There's a forum for general discussion and support requests, and the bug tracker is the place to report issues.

We aim to be an inclusive, welcoming community. To make that explicit, we have a code of conduct that applies to communication around the project.

Documentation

schema: Schema<"doc" | "paragraph" | "blockquote" | "horizontal_rule" | "heading" | "code_block" | "ordered_list" | "bullet_list" | "list_item" | "text" | "image" | "hard_break", "em" | "strong" | "link" | "code">

Document schema for the data model used by CommonMark.

class MarkdownParser

A configuration of a Markdown parser. Such a parser uses markdown-it 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: any, tokens: Object<ParseSpec>)

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:

schema: Schema

The parser's document schema.

tokenizer: any

This parser's markdown-it tokenizer.

tokens: Object<ParseSpec>

The value of the tokens object used to construct this parser. Can be useful to copy and modify to base other parsers on.

parse(text: string) → any

Parse a string as CommonMark markup, and create a ProseMirror document as prescribed by this parser's rules.

interface ParseSpec

Object type used to specify how Markdown tokens should be parsed.

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⁠?: Attrs

Attributes for the node or mark. When getAttrs is provided, it takes precedence.

getAttrs⁠?: fn(token: any, tokenStream: any[], index: number) → Attrs

A function used to compute the attributes for the node or mark that takes a markdown-it token and returns an attribute object.

noCloseToken⁠?: boolean

Indicates that the markdown-it token has no _open or _close for the nodes. This defaults to true for code_inline, code_block and fence.

ignore⁠?: boolean

When true, ignore content for the matched token.

defaultMarkdownParser: MarkdownParser

A parser parsing unextended CommonMark, 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<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.

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(state: MarkdownSerializerState, node: Node, parent: Node, index: number)>

The node serializer functions for this serializer.

marks: Object<Object>

The mark serializer info.

options: Object
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.

serialize(content: Node, options⁠?: Object = {}) → string

Serialize the content of the given node to CommonMark.

options
tightLists⁠?: boolean

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.

class MarkdownSerializerState

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: {tightLists⁠?: boolean, escapeExtraCharacters⁠?: RegExp}

The options passed to the serializer.

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⁠?: boolean = true)

Add the given text to the document. When escape is not false, it will be escaped.

render(node: Node, parent: Node, index: number)

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(index: 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⁠?: boolean = false) → 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.

markString(mark: Mark, open: boolean, parent: Node, index: number) → string

Get the markdown string for a given opening or closing mark.

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.

prosemirror-markdown/package.json000066400000000000000000000017321526637033100175600ustar00rootroot00000000000000{ "name": "prosemirror-markdown", "version": "1.13.9", "description": "ProseMirror Markdown integration", "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": "marijn@haverbeke.berlin", "web": "http://marijnhaverbeke.nl" } ], "repository": { "type": "git", "url": "git+https://code.haverbeke.berlin/prosemirror/prosemirror-markdown.git" }, "dependencies": { "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0", "@types/markdown-it": "^14.0.0" }, "devDependencies": { "@prosemirror/buildhelper": "^0.1.5", "prosemirror-test-builder": "^1.0.0", "punycode": "^1.4.0" }, "scripts": { "test": "pm-runtests", "prepare": "pm-buildhelper src/index.ts" } } prosemirror-markdown/src/000077500000000000000000000000001526637033100160565ustar00rootroot00000000000000prosemirror-markdown/src/README.md000066400000000000000000000026661526637033100173470ustar00rootroot00000000000000# prosemirror-markdown [ [**WEBSITE**](http://prosemirror.net) | [**ISSUES**](https://code.haverbeke.berlin/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). 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 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://code.haverbeke.berlin/prosemirror/prosemirror/src/branch/main/LICENSE). There's a [forum](http://discuss.prosemirror.net) for general discussion and support requests, and the [bug tracker](https://code.haverbeke.berlin/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 @MarkdownParser @ParseSpec @defaultMarkdownParser @MarkdownSerializer @MarkdownSerializerState @defaultMarkdownSerializer prosemirror-markdown/src/from_markdown.ts000066400000000000000000000242501526637033100212760ustar00rootroot00000000000000// @ts-ignore import MarkdownIt from "markdown-it" import Token from "markdown-it/lib/token.mjs" 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} }) prosemirror-markdown/src/index.ts000066400000000000000000000004511526637033100175350ustar00rootroot00000000000000// Defines a parser and serializer for [CommonMark](http://commonmark.org/) text. export {schema} from "./schema" export {defaultMarkdownParser, MarkdownParser, ParseSpec} from "./from_markdown" export {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "./to_markdown" prosemirror-markdown/src/schema.ts000066400000000000000000000103631526637033100176710ustar00rootroot00000000000000import {Schema, MarkSpec} from "prosemirror-model" /// Document schema for the data model used by CommonMark. export const schema = new Schema({ nodes: { doc: { content: "block+" }, paragraph: { content: "inline*", group: "block", parseDOM: [{tag: "p"}], toDOM() { return ["p", 0] } }, blockquote: { content: "block+", group: "block", parseDOM: [{tag: "blockquote"}], toDOM() { return ["blockquote", 0] } }, horizontal_rule: { group: "block", parseDOM: [{tag: "hr"}], toDOM() { return ["div", ["hr"]] } }, heading: { attrs: {level: {default: 1, validate: "number"}}, content: "(text | image)*", group: "block", defining: true, parseDOM: [{tag: "h1", attrs: {level: 1}}, {tag: "h2", attrs: {level: 2}}, {tag: "h3", attrs: {level: 3}}, {tag: "h4", attrs: {level: 4}}, {tag: "h5", attrs: {level: 5}}, {tag: "h6", attrs: {level: 6}}], toDOM(node) { return ["h" + +node.attrs.level, 0] } }, code_block: { content: "text*", group: "block", code: true, defining: true, marks: "", attrs: {params: {default: ""}}, parseDOM: [{tag: "pre", preserveWhitespace: "full", getAttrs: node => ( {params: (node as HTMLElement).getAttribute("data-params") || ""} )}], toDOM(node) { return ["pre", node.attrs.params ? {"data-params": node.attrs.params} : {}, ["code", 0]] } }, ordered_list: { content: "list_item+", group: "block", attrs: {order: {default: 1}, tight: {default: false}}, parseDOM: [{tag: "ol", getAttrs(dom) { 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, "data-tight": node.attrs.tight ? "true" : null}, 0] } }, bullet_list: { content: "list_item+", group: "block", attrs: {tight: {default: false}}, parseDOM: [{tag: "ul", getAttrs: dom => ({tight: (dom as HTMLElement).hasAttribute("data-tight")})}], toDOM(node) { return ["ul", {"data-tight": node.attrs.tight ? "true" : null}, 0] } }, list_item: { content: "block+", defining: true, parseDOM: [{tag: "li"}], toDOM() { return ["li", 0] } }, text: { group: "inline" }, image: { inline: true, attrs: { src: {}, alt: {default: null}, title: {default: null} }, group: "inline", draggable: true, parseDOM: [{tag: "img[src]", getAttrs(dom) { return { src: (dom as HTMLElement).getAttribute("src"), title: (dom as HTMLElement).getAttribute("title"), alt: (dom as HTMLElement).getAttribute("alt") } }}], toDOM(node) { return ["img", node.attrs] } }, hard_break: { inline: true, group: "inline", selectable: false, parseDOM: [{tag: "br"}], toDOM() { return ["br"] } } }, marks: { em: { parseDOM: [ {tag: "i"}, {tag: "em"}, {style: "font-style=italic"}, {style: "font-style=normal", clearMark: m => m.type.name == "em"} ], toDOM() { return ["em"] } }, strong: { parseDOM: [ {tag: "strong"}, {tag: "b", getAttrs: node => node.style.fontWeight != "normal" && null}, {style: "font-weight=400", clearMark: m => m.type.name == "strong"}, {style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null} ], toDOM() { return ["strong"] } } as MarkSpec, link: { attrs: { href: {}, title: {default: null} }, inclusive: false, parseDOM: [{tag: "a[href]", getAttrs(dom) { return {href: (dom as HTMLElement).getAttribute("href"), title: dom.getAttribute("title")} }}], toDOM(node) { return ["a", node.attrs] } }, code: { code: true, parseDOM: [{tag: "code"}], toDOM() { return ["code"] } } } }) prosemirror-markdown/src/to_markdown.ts000066400000000000000000000442061526637033100207600ustar00rootroot00000000000000import {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 } const blankMark: MarkSerializerSpec = {open: "", close: "", mixable: true} /// 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, /// By default, the serializer raises an error when it finds a /// node or mark type for which no serializer is defined. Set /// this to `false` to make it just ignore such elements, /// rendering only their content. strict?: boolean } = {} ) {} /// 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, false) 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: RegExpExecArray | null, 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, strict?: boolean} ) { 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 } } /// @internal getMark(name: string) { let info = this.marks[name] if (!info) { if (this.options.strict !== false) throw new Error(`Mark type \`${name}\` not supported by Markdown renderer`) info = blankMark } return info } /// 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 !this.out || this.out[this.out.length - 1] == "\n" } /// 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.slice(-2))) 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 (this.nodes[node.type.name]) { this.nodes[node.type.name](this, node, parent, index) } else { if (this.options.strict !== false) { throw new Error("Token type `" + node.type.name + "` not supported by Markdown renderer") } else if (!node.type.isLeaf) { if (node.type.inlineContent) this.renderInline(node) else this.renderContent(node) if (node.isBlock) this.closeBlock(node) } } } /// 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, fromBlockStart = true) { this.atBlockStart = fromBlockStart 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 inner = marks.length ? marks[marks.length - 1] : null let noEsc = inner && this.getMark(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.getMark(mark.type.name).mixable) break for (let j = 0; j < active.length; j++) { let other = active[j] if (!this.getMark(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 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.getMark(mark.type.name) return info && info.expelEnclosingWhitespace && !active.some((m, i) => i < keep && m.eq(mark)) })) { let [_, lead, rest] = /^(\s*)(.*)$/m.exec(node.text!)! if (lead) { leading += lead node = rest ? (node as any).withText(rest) : null if (!node) marks = active } } if (node && node.isText && marks.some((mark, i) => { let info = this.getMark(mark.type.name) return info && info.expelEnclosingWhitespace && !this.isMarkAhead(parent, index + 1, marks.slice(0, i + 1)) })) { let [_, rest, trail] = /^(.*?)(\s*)$/m.exec(node.text!)! if (trail) { trailing = trail node = rest ? (node as any).withText(rest) : null if (!node) marks = active } } // Close the marks that need to be closed if (node || index == parent.childCount) 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) this.atBlockStart = 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) this.atBlockStart = false // After the first non-empty text node is rendered, the end of output // is no longer at block start. // // FIXME: If a non-text node writes something to the output for this // block, the end of output is also no longer at block start. But how // can we detect that? if (node.isText && node.nodeSize > 0) this.atBlockStart = false } } 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*)(#{1,6})(\s|$)/, '$1\\$2$3').replace(/^(\s*\d+)\.\s/, "$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.getMark(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] } } /// @internal isMarkAhead(parent: Node, index: number, marks: readonly Mark[]) { for (;; index++) { if (index >= parent.childCount) return false let next = parent.child(index) if (next.type.name != this.options.hardBreakNodeName) { return next.marks.length >= marks.length && Mark.sameSet(next.marks.slice(0, marks.length), marks) } index++ } } } prosemirror-markdown/test/000077500000000000000000000000001526637033100162465ustar00rootroot00000000000000prosemirror-markdown/test/build.ts000066400000000000000000000024111526637033100177130ustar00rootroot00000000000000import {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 prosemirror-markdown/test/test-custom-parser.ts000066400000000000000000000016251526637033100224030ustar00rootroot00000000000000import {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" import {doc, p, br} from "./build.js" const md = markdownit("commonmark", {html: false}) const ignoreBlockquoteParser = new MarkdownParser(schema, md, { blockquote: {ignore: true}, paragraph: {block: "paragraph"}, softbreak: {node: 'hard_break'} }) function parseWith(parser: MarkdownParser) { return (text: string, doc: Node) => { ist(parser.parse(text), doc, eq) } } describe("custom markdown parser", () => { it("ignores a blockquote", () => parseWith(ignoreBlockquoteParser)("> hello!", doc(p("hello!")))) it("converts softbreaks to hard_break nodes", () => parseWith(ignoreBlockquoteParser)("hello\nworld!", doc(p("hello", br(), 'world!')))) }) prosemirror-markdown/test/test-parse.ts000066400000000000000000000244201526637033100207070ustar00rootroot00000000000000import {eq} from "prosemirror-test-builder" import {Node} from "prosemirror-model" import ist from "ist" import {schema, defaultMarkdownParser, defaultMarkdownSerializer, MarkdownSerializer} from "prosemirror-markdown" 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: string, doc: Node) { ist(defaultMarkdownParser.parse(text), doc, eq) } function serialize(doc: Node, text: string) { ist(defaultMarkdownSerializer.serialize(doc), text) } function same(text: string, doc: Node) { parse(text, doc) serialize(doc, text) } describe("markdown", () => { it("parses a paragraph", () => same("hello!", doc(p("hello!")))) it("parses headings", () => same("# one\n\n## two\n\nthree", doc(h1("one"), h2("two"), p("three")))) it("parses a blockquote", () => same("> once\n\n> > twice", doc(blockquote(p("once")), blockquote(blockquote(p("twice")))))) // FIXME bring back testing for preserving bullets and tight attrs // when supported again it("parses a bullet list", () => same("* foo\n\n * bar\n\n * baz\n\n* quux", doc(ul(li(p("foo"), ul(li(p("bar")), li(p("baz")))), li(p("quux")))))) it("parses an ordered list", () => same("1. Hello\n\n2. Goodbye\n\n3. Nest\n\n 1. Hey\n\n 2. Aye", doc(ol(li(p("Hello")), li(p("Goodbye")), li(p("Nest"), ol(li(p("Hey")), li(p("Aye")))))))) it("preserves ordered list start number", () => same("3. Foo\n\n4. Bar", doc(ol3(li(p("Foo")), li(p("Bar")))))) it("can parse a heading in a list", () => same("* # Foo", doc(ul(li(h1("Foo")))))) it("parses a code block", () => same("Some code:\n\n```\nHere it is\n```\n\nPara", doc(p("Some code:"), schema.node("code_block", {params: ""}, [schema.text("Here it is")]), p("Para")))) it("parses an intended code block", () => parse("Some code:\n\n Here it is\n\nPara", doc(p("Some code:"), pre("Here it is"), p("Para")))) it("parses a fenced code block with info string", () => same("foo\n\n```javascript\n1\n```", doc(p("foo"), schema.node("code_block", {params: "javascript"}, [schema.text("1")])))) it("parses inline marks", () => same("Hello. Some *em* text, some **strong** text, and some `code`", doc(p("Hello. Some ", em("em"), " text, some ", strong("strong"), " text, and some ", code("code"))))) it("parses overlapping inline marks", () => same("This is **strong *emphasized text with `code` in* it**", doc(p("This is ", strong("strong ", em("emphasized text with ", code("code"), " in"), " it"))))) it("parses links inside strong text", () => 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"))))) it("parses code mark containing backticks", () => same("``` one backtick: ` two backticks: `` ```", doc(p(code("one backtick: ` two backticks: ``"))))) it("parses code mark containing only whitespace", () => 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")))) it("parses urls", () => same("Link to ", doc(p("Link to ", link({href: "https://prosemirror.net"}, "https://prosemirror.net"))))) it("correctly serializes relative urls", () => { same("[foo.html](foo.html)", doc(p(link({href: "foo.html"}, "foo.html")))) }) it("can handle link titles", () => { same('[a](x.html "title \\"quoted\\"")', doc(p(link({href: "x.html", title: 'title "quoted"'}, "a")))) }) it("doesn't escape underscores in link", () => { same('[link](http://foo.com/a_b_c)', doc(p(link({href: "http://foo.com/a_b_c"}, "link")))) }) it("parses emphasized urls", () => same("Link to **", doc(p("Link to ", em(link({href: "https://prosemirror.net"}, "https://prosemirror.net")))))) it("parses an image", () => same("Here's an image: ![x](img.png)", doc(p("Here's an image: ", img())))) it("parses a line break", () => same("line one\\\nline 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")))) it("ignores HTML tags", () => same("Foo < img> bar", doc(p("Foo < img> bar")))) it("doesn't accidentally generate list markup", () => same("1\\. foo", doc(p("1. foo")))) it("doesn't fail with line break inside inline mark", () => serialize(doc(p(strong("text1\ntext2"))), "**text1\ntext2**")) it("drops trailing hard breaks", () => 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.")), "Some emphasized text with ***whitespace*** surrounding the emphasis.")) it("expels whitespace from emphasis with a nested mark", () => serialize(doc(p("One", em(" two ", a("three"), " four "), "five")), "One *two [three](foo) four* five")) it("properly expels whitespace before a hard break", () => serialize(doc(p(strong("foo ", br()), "bar")), "**foo** \\\nbar")) it("doesn't crash when a block ends in a hard break", () => serialize(doc(p(strong("foo", br()))), "**foo**")) it("drops nodes when all whitespace is expelled from them", () => serialize(doc(p("Text with", em(" "), "an emphasized space")), "Text with an emphasized space")) it("preserves list tightness", () => { same("* foo\n* bar", doc(ul({tight: true}, li(p("foo")), li(p("bar"))))) same("1. foo\n2. bar", doc(ol({tight: true}, li(p("foo")), li(p("bar"))))) }) it("doesn't put a code block after a list item inside the list item", () => same("* list item\n\n```\ncode\n```", doc(ul({tight: true}, li(p("list item"))), pre("code")))) it("doesn't escape characters in code", () => same("foo`*`", doc(p("foo", code("*"))))) it("doesn't escape underscores between word characters", () => same("abc_def", doc(p("abc_def")))) it("doesn't escape strips of underscores between word characters", () => same("abc___def", doc(p("abc___def")))) it("escapes underscores at word boundaries", () => same("\\_abc\\_", doc(p("_abc_")))) it("escapes underscores surrounded by non-word characters", () => 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("", 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"))))) }) it("does not escape list markers in the middle of paragraphs", () => { same("123 [0. com](foo)\n\n123 [2. 2](foo)", doc(p("123 ", a("0. com")), p("123 ", a("2. 2")))) }) it("does not escape list markers without space after them", () => { same("1.2kg", doc(p("1.2kg"))) }) // Issue #105 it("escapes ATX heading markers with space after them", () => { same("\\### text", doc(p("### text"))) }) it("escapes ATX heading markers followed by the end of line", () => { same("\\###", doc(p("###"))) }) it("does not escape ATX heading markers without space after them", () => { same("#hashtag", doc(p("#hashtag"))) }) it("does not escape ATX heading markers consisting of more than 6 in a sequence", () => { same("#######", doc(p("#######"))) }) it("keeps Unicode space after ATX heading markers when escaping", () => { same("\\# こんにちは", doc(p("# こんにちは"))) }) it("doesn't escape block-start characters in header", () => { same("# 1. foo", doc(h1("1. foo"))) }) it("doesn't escape +++", () => { same("+++", doc(p("+++"))) }) // 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)]))) }) it("can expel leading whitespace from overlapping emphasis", () => { serialize(doc(p(strong("f"), em(strong("oo"), " bar"))), "**f*oo*** *bar*") }) it("can expel trailing whitespace from overlapping emphasis", () => { serialize(doc(p(strong("f"), em(strong("oo "), "bar"))), "**f*oo*** *bar*") }) })