benrbray / noteworthy

Markdown editor with bidirectional links and excellent math support, powered by ProseMirror. (In Development!)
https://noteworthy.ink
GNU Affero General Public License v3.0
234 stars 14 forks source link

Extract out prosemirror remark package #34

Open marekdedic opened 1 year ago

marekdedic commented 1 year ago

Hi, I'd like to use ProseMirror for my project, however, I'd prefer remark over markdown-it. After some googling I found this project and I se you have already done exactly that, if I understand it correctly.

Would it be possible to extract out a separate package for prosemirror markdown editing based on remark. Basically, an alternative to https://github.com/prosemirror/prosemirror-markdown, just with remark instead of markdown-it?

I'd be willing to help, but honestly, I don't even know where to start at this point.... :(

benrbray commented 1 year ago

Hello! Thanks for your interest! I can probably give you some pointers. Here is a high-level summary of how Noteworthy's markdown parsing&serialization are handled (actually, this will be a nice refresher for me, since I haven't touched the code in about a year :) ).

Before starting, you should be familiar with:

Parsing & Editing

At a high level, here is how Noteworthy goes from from a (remark)-Markdown string to a ProseMirror instance that supports all the same syntaxes:

  1. MarkdownAst.parseAST takes a markdown string and returns a MarkdownAst, representing an entire document.
  2. The parser is created from an EditorConfig instance.
    • EditorConfig.parseAST converts a string to an Md.Node abstract syntax tree representation
    • EditorConfig.parse converts a string to a ProseMirror Node instance, which can be used inside a ProseMirror editor (with the appropriate schema).
  3. The parser used by EditorConfig is created by the makeParser helper function in src/common/markdown/mdast2prose.ts. This is where all the magic happens.
    • The md2ast parameter is just a regular remark parser, built here. For now, Noteworthy always uses the same pre-defined set of remark extensions to parse the markdown string into an AST.
    • After calling the remark parser to convert the string into an AST, the AST needs to be converted into a ProseMirror Node. The trouble is that there is not a simple one-to-one mapping between ProseMirror nodes and the Unist AST format. As a simple example, remark/unist represent bold/italic text as nodes in the AST, but ProseMirror represents them as Marks, which are like extra annotations on a node.
    • To deal with this complexity, each EditorConfig takes in a list of "extensions" which define a mapping between ProseMirror Nodes and REmark ASTs.
  4. At the moment, there are only two kinds of extensions. Both kinds must declare a ProseMirror schema snippet, and can define new keyboard shortcuts and input rules.

I think the best way to get an understanding for how it all works is to look at some of the existing node/mark extensions. Here are some things to pay attention to:

(side note: As you can see, the division between node and mark extensions is very ProseMirror-oriented. An extension can contribute either a node or a mark, but not both. I think I chose this more out of convenience, especially since I was trying to use ProseMirror's schema type parameter to allow for richly-typed composable document formats with good type hints. But in light of #31, all of that will break anyway in new version of ProseMirror. So perhaps it is better to relax the distinction between node/mark extensions in the future.)

Serialization

At a high level, here is how Noteworthy serializes the contents of a ProseMirror editor into a Markdown string:

Comments

At the moment, I think the implementation is pretty ad-hoc and tangled up with some Noteworthy-specific stuff, but in principle it should be possible to factor all the remark-prosemirror interop into a separate package. I'm not sure it's something I have time for at the moment, but I would certainly welcome some feedback / PRs to help move it in that direction.

Hopefully this is enough to get you started. Please let me know if you have any questions and I'll do my best to reply!

marekdedic commented 1 year ago

Wow, thanks for the extremely thorough and informative response!

I hope to get started on trying this soon-ish, I'll post here if I get to it and you'll see if it makes sense :) I actually think this would make things like #31 easier for you as well...

marekdedic commented 1 year ago

Hi, I have started on this in https://github.com/marekdedic/prosemirror-remark, heavily drawing from your code, but at the same time re-architecting things to make more sense as a standalone package (and also I am using the updated ProseMirror types, so I am much more lax about types, at least for the moment...)

It is nowhere near done, but I have the necessary infrastructure in place to support both ProseMirror nodes as well as marks for both parsing and stringification. I would love to hear your feedback if you get any chance to look at it.