Why prosemirror

Editors have always been a difficulty in the front-end domain, and a mature editor needs to involve many things.

How many things… This can see nuggets on a big brother in zhihu on the answer

The company wanted a WYSIWYG Markdown editor that did not require the markdown source code, used the same input rules as The Markdown syntax, and finally needed to output the Markdown document for storage, with some specific requirements on top of that. This requires that the selection be a flexible, configurable modular editor framework rather than an out-of-the-box application.

At the time of selection, the company had been working on some special editors using Prosemirror (however, that colleague left before I arrived), as well as Slate.js, which also published an article on Nuggets. Why not slate.js (and draft.js)? The reason is simply that our technology stack is Vue and not React. Slate.js relies on React as a view layer, and as a Vue application, it still doesn’t want to introduce a React service for Slate.js.

To sum up the reason, stepped on this sinkhole. I’ve never used slate.js, but based on popularity, star status on Github, and activity, I don’t think it’s any smaller than Slate.js, but it’s no worse than slate.js as an editor.

However, due to the high activity and other reasons, there is no Chinese information about Prosemirror when you search on Google or Baidu. I once thought that this framework was not used in China. Until one day I saw the profile picture of the tycoon mentioned above on discuss, I didn’t know that there were people who used it in China. Of course, there is no corresponding Chinese document. If you step on a hole, you can only search for a question on discuss or issue. But luckily, the author is very warm-hearted and answers almost every question, even very entry-level ones, which has helped me a lot with the development.

The following content is almost the official website document, written down through their own understanding and simplification, interested can go to the official website for more detailed content.

Prosemirror profile

If prosemirror sounds strange to you, you may have heard of the famous Codemirror. Yes, the code editor in the browser, by the same author, a very powerful German named Marijn. Some of the core concepts in SLATE like Schema are derived from Prosemirror.

Prosemirror is not a large and comprehensive framework, and even if you go to NPM and search prosemirror, there is no such package.

Prosemirror is made up of countless small modules that, according to its website, stack into a robust editor, similar to Lego

The core library is not an easy drop-in component — we are prioritizing modularity and customizeability over simplicity, with the hope that, in the future, people will distribute drop-in editors based on ProseMirror. As such, this is more of a lego set than a matchbox car.

Its core library has

  • Prosemiror-model: Defines the editor’s document model, which describes the data structure of the editor’s content

  • Prosemiror-state: Provides a data structure that describes the entire state of the editor, including selection, and the transaction system for moving from one state to the next.

  • Prosemiror-view: Implements a user interface component that displays a given editor state as an editable element in the browser and handles user interaction with that element.

  • Prosemiror-transform: Includes the ability to modify a document in a recordable and replayable manner, which is the basis for transactions in the State module and makes it possible to undo history and co-edit.

React is a core library that looks a lot like React. They form the basis of the entire editor. Of course, in addition to the core library, you need various libraries to implement the shortcut prosemiror-Commands, edit history prosemiror-History, and so on.

Implement a small editor

This is a very limited feature, just a few basic keys (e.g. enter newline, Bacakspace delete), and then we add a Ctrl-Z undo and Ctrl-Y redo.

At first I thought it was a small demo, so I used parcel to package, but I got an error when I used parcel for the first time. I don’t know if it was me or parcel.

import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
// check rules
import {schema} from "prosemirror-schema-basic"
// Record history and undo redo
import {undo, redo, history} from "prosemirror-history"
/ / a
import {keymap} from "prosemirror-keymap"
import {baseKeymap} from "prosemirror-commands"
// 
let content = document.getElementById("content")
// Generate a state
let state = EditorState.create({
    doc: DOMParser.fromSchema(schema).parse(content),
    schema,
    plugins: [
        history(),
        keymap(baseKeymap),
        keymap({"Mod-z": undo, "Mod-y": redo})
    ]
    })
// Generate a view
let view = new EditorView(document.getElementById('prosemirror'), {state})
Copy the code

This code converts the contents of the content to the initial text of the editor as the initial editing state. Can only do simple edits, such as delete, withdraw, line feed, etc.

What is Parser?

Let’s see what that code does. First, we reserve the contents of a Conetentid, which is not visible in the final display, in order to store the existing HTML document in the DOM. Next, DOMParse the HTML text along the Schema to get an object of type Node, which can be rendered into the editor’s editable text by passing in the doc attribute as an initial text data.

DOMParse here is a parser that serves as a rendering of the DOM into Node objects. In addition to DOMParse, another parser is MarkdownParser that converts Markdown documents into Node class data.

Call editorState.json () to serialize the current state doc to JSON format for easy storage.

What is schema?

Schema is a set of transformation rules that describe the relationship between documents and Dom. How do you convert Dom to Node or Node to Dom? That’s the key

/ / heading schema
heading: {
    // Optional attributes
    attrs: {level: {default: 1}},
    // The type of the node content, whether it is a row or a block
    content: "inline*".// Its own type, whether it is a row or a block
    group: "block".// Parse Dom rules and attributes
    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}}],,// Generate Dom rules
    toDOM(node) { return ["h" + node.attrs.level, 0]}},Copy the code

This is a text rule that describes a heading, but without the text rule, the parser or sequencer doesn’t know how to parse it. Any Dom that appears in the editor, and any node type that needs to be converted to the Dom, must have a corresponding schema or it will not compile.

Schemas can be created or added to existing schemas. A robust schema has high requirements on the setting of each attribute, so I will not use examples here to avoid bias. You can learn from the official website.

What is Node?

The Node class forms the Node tree of the Prosemirror document, and its children are Node classes. The Node class cannot be changed directly. It is a persistent data structure, similar to state in React. A transaction class must be applied to change the doc structure. The structure of Node is very similar to that of Virtual Dom, with tree and recursion. Dom is described by instance deconstruction, and Prosemirror also has its own set of efficient update algorithms to transform Node and Dom

Node has a wide range of attributes, such as document location, number of child nodes, Node size, text content, and so on, and in many cases these attributes can be very helpful in implementing specific functions.

What is Transaction?

Transaction is a data type that describes changes in editor state. In Prosemirror, a call to EditorView.updatestate updates the state of the entire editor, even if a space is hit, by state. So, if DOMParse is used to create a new Node each time to form a new state, things like history are not necessarily preserved, and in Prosemirror, by the time editorState.apply is actually called, There are many Plugins (if any) to process this transaction, so it is important to use editorState. apply to apply a transaction to generate a new state and then call it to change the state of the editor and save the state. The same goes for editing. Let’s start with an example

let view = new EditorView(document.body, {
  state,
  // This is a hook function, and transaction is applied at the end
  dispatchTransaction(transaction) {
    console.log("Document size went from", transaction.before.content.size,
                "to", transaction.doc.content.size)
    // Apply transaction and generate a new state
    let newState = view.state.apply(transaction)
    / / update the state
    view.updateState(newState)
  }
})
Copy the code

DispatchTransaction is actually the last method before calling EditorState.apply, which can also be done without calling dispatchTransaction and updated by default. The purpose here is that every update (be it an edit, insert, delete, etc.) logs a paragraph of text, and that’s it. If you do not apply and update, an error will be reported. You can get a real-time transaction from editor.tr.

Keymap, History

Keymap is a plug-in for keyboard input rules, and History is a plug-in for history, which is skipped.

Summary of Core Content

So far, the core content has been introduced, of course, the core content is only a superficial understanding of Prosemirror, so that we will not be confused with how it works in the future editor development.

What’s missing are the input rules that make the WYSIWYN editor work like markdown, the action bar at the top, and so on. These are all part of the editor, but I won’t cover them here because they are not core libraries. There is an example of an example setup. It is also recommended to modify the Settings to meet our requirements

Next, let’s be lazy and implement a Markdown editor. Examples are also from the official website.

Implement a Markdown editor

Simply change the parser to defaultMarkdownParser, use the plugins default Settings, and then use the prosemiror-example-setup default styles to create a WYSIWYN editor.

class ProseMirrorView {
    constructor(target, content) {
        this.view = new EditorView(target, {
        state: EditorState.create({
            // Use the default Markdown parser to parse markdown documents
            doc: defaultMarkdownParser.parse(content),
            // Set an example
            plugins: exampleSetup({schema})
            })
        })
    }
    // Expose two common methods for easy invocation
    focus() { this.view.focus() }
    destroy() { this.view.destroy() }
}
new ProseMirrorView(document.getElementById('prosemirror'), '# hello')
Copy the code

Of course, this is a very simple Markdown editor. DefaultMarkdownParser is the CommonMark standard, and many common Markdown syntax are missing. We can do a lot of customization from this.

The Markdown parser of defaultMarkdownParser uses Markdown-it. The principle is that tokens are parsed into tokens and then converted through schema. So if you want to extend Markdown, you need to know markdown-it or some other Markdown parser.

conclusion

This article gives a brief overview of some of the ideas and core content of Prosemirror, but it just scratches the surface and doesn’t fully capture its appeal. On its forums, there are many developers who contribute amazing plug-ins or mature editors that are well worth learning from. I hope you can understand prosemirror in more depth.