Pothibo

First step when building a markdown editor

When I first started working on Ecrire, I decided to use a very simple textarea. The goal would be to write raw HTML inside the textarea and have a preview panel that shows how the rendered post would look like. Pretty basic stuff. I always had the feeling that one day, I'd migrate the dumb textarea to a Markdown editor.

After about 30 posts, I grew tired of building posts with raw HTML and started building a markdown editor. I'm done yet, but the editor is stable enough for me to start talking about it. Hell, I've been using it on this blog for the last 3 posts and everything went almost smooth. Now, when I set myself to build a markdown editor in JavaScript, I was aware of the following:

  1. Markdown is quite complex.
  2. CommonMark is still in its infancy
  3. I cannot implement the whole markdown standard by myself

The last point is quite obvious since I didn't have a year to myself to build this. I needed to build it in my spare time and so I decided that I should decide what goes in the editor.

Sure, I would look at the available solutions but there's thing that didn't exist, to my knowledge, that I wanted to implement and some others that I didn't want even if it was supported in different implementations.

My influence

iA Writer is exactly how I felt an editor should be. I really love how the parser changes the text as you type so you see the difference and the markup blends with the text. I thought it would be awesome if I could do it on the web. Granted that other people had done it in limited fashion, I believed that I could do it too.

One thing I really wanted was to be able to import images from within the editor. Without extra popup/panel or whatever. I wanted to build it so you could upload pictures directly from within the editor.

I also wanted to add full code snippet support from inside the editor so as you type your code, you can select the language and the editor will highlight it as you type, like an IDE. Eventually, I could even add auto-indenting!

So with the help of iA, my mind was set: I would build an editor.

What I didn't want to implement

You may disagree with my choices, but based on my previous posts, I decided to only include the features I would be using the most. This help me get to a working prototype sooner and focus on the bugs as they appear instead of being overwhelmed with half-ass features.

I also chose to implement features only once. That means that I wouldn't implement all the different versions you can write the same thing. For example, I would choose one way to write a header. As a rule of thumb, I would only implement the solution that I felt was the easiest to implement and maintain.

I would also not implement anything that would make my life a living hell. I know HTML quite well and I believe it is still very useful. So, I rather have to write some raw HTML here and there than fall into the abyss of spaghetti parsing.

For you folks who probably do not know me, this is the first time I'll build something that sounds like a parser. I need to keep things simple or I will fail.

The first steps

Like a MVP for a product, I list the few things that I thought was necessary for me to write posts. When I looked at the CommonMark spec, I realized that I could divide my implementation in two.

  • Inline parsing
  • Block parsing

I was aware that my over generalization of markdown in two categories may turn out to be wrong, but I didn't care. I expected to refactor down the road and since it's my very first implementation of a parser, I knew I was going to get things wrong. As long as I kept the code simple and refactor often, I wouldn't be in too much trouble.

Right there, I thought that the inline parsing would be the easiest to build from nothing. So this is were I started. I first started with headers and from there, I had to take the first decision, which you can probably guess by now.

Which type of header would I parse? I decided to go with the hashtag forms on a single line. So there I was, knowing what to parse, with zero code written.

Before parsing anything

Before parsing my very first line of markdown, I needed to decide how to implement an editor that can style itself. Basically, there were two options:

The first would be to hide a textarea behind a wrapper. I would listen to the textarea as the user type (the focus would be on the textarea) and I would parse and update the wrapper that is front of the textarea, much like curtains in front of a window. The second would be to use the contentEditable property on a <div> element.

I chose to go with content editable because of two reasons:

  1. Hidden textarea can fall out of sync with the wrapper which would be invisible to the user and
  2. content editable was somewhat made for this; there should be a way to make it work!

And frankly, I didn't know better. But even to this day, I think that using content editable was the right approach.

So now that I know what container to use, my choices of listeners for user input narrows down. Content editable container automatically builds HTML elements as you type. While those are not very predictable, it's a start.

After some research, I figured I'd use MutationObserver to listen to changes inside the container. I liked it because it gives a list of nodes that are removed, added and updated which will make my life easier.

A note about user inputs

Never use keyboard events when dealing with user inputs. The key codes are ASCII only and it's pretty much useless most of the times.

Parsing the first line

Since I would be only parsing one line, I didn't think too much about optimization, parsing patterns, etc. I just wanted to write something like "# Hello World!" and have it rendered in a <h1> tag.

So I headed over to Rubular as my go-to resource for playing with regular expression. After a few versions, I decided to go with this rule:

/^(#{1,6}) /i

Now that I had a working expression to test the line against, I needed to implement the observer.

class Editor constructor: -> @editor = document.querySelector('#editor') @observer = new MutationObserver(@outdated) observerSettings = { childList: true, subtree: true, characterData: true } @observer.observe @editor, observerSettings outdated: (mutations) => for mutation in mutations @appended(node, mutation.target) for node in mutation.addedNodes @removed(node, mutation.target) for node in mutation.removedNodes if mutation.type == 'characterData' @updated mutation.target appended: (node, line) => removed: (node, line) => updated: (node) =>

As I type in the editor, I saw that the observer was registering all the events. Small victory, but victory nonetheless!

Unless you had to deal with content editable in the past, you wouldn't know how unpredictable the rendering is. It's a mess. But, for the sake of keeping this shorter than a Stephen King's book, just know that usually, every line is a <p> element. With this in mind, my thoughts was that I would only need to look at the updated() callback for now and once the regular expression matches the line, I would replace it with a header line matching the number of hashtags the expression found.

Because of the unpredictability of content editable, I needed to always do a recursive loop to make sure the target was the <p> element. Sometimes, it's a span, sometimes, it's a text node, sometimes it's undefined. Sometimes, the element is not even in the DOM tree.

And then, for this first version, I will look at the textContent of the line and parse that. It will allow me to see the whole text without any HTML markup that the browser would inject in the line.

If the regular expression matches a header, then I replace that line and call it a success.

class Editor updated: (node) => while node? && node.parentElement != @element() node = node.parentElement return unless node? match = /^(#{1,6}) /i.exec(node.textContent) if match? el = document.createElement('div') el.innerHTML = "<h#{@match[1].length}>" header = el.children[0] header.textContent = node.textContent node.parentElement.replaceChild(header, node)

Chaos

While I was pretty sure it wouldn't work on the first try, it almost worked. It would work for the very first time the regular expression finds it but as soon as I type another character, all hell broke loose. It might not be obvious at first sight. It took me quite a while to figure it out. While the replaceChild was a good idea, it built up an infinite loop with the observer.

The observer would see a change, then I would parse that change and substitute the content which would then trigger another observer event that I would parse again and the cycle would just go on and on. Infinite loop.

Right here and there, I thought that I couldn't get out of it. Mutation observers were maybe not the right tool for the job. I seriously thought, at that time, that I would have to trash all my work and start over. How can you tell the observer to ignore changes? Should I just put a flag somewhere and use it to ignore mutations? Even if the environment is single threaded, I feared that it would bring a whole lot of issues that would just end up overwhelming me.

Worried, I was browsing the Mozilla Developer Network pointlessly until I saw it. The solution. Mutation Observer comes with a disconnect() method that allowed me to stop receiving notification and effectively breaking the infinite loop.

And because JavaScript lets you add method to objects, I thought that I should add my own to the observer instance I created: I hold() method. That method would take a callback, disconnect the observer, run the callback and once the callback method exits, reconnect the observer.

The solution

class Editor constructor: -> @editor = document.querySelector('#editor') @observer = new MutationObserver(@outdated) observerSettings = { childList: true, subtree: true, characterData: true } @observer.observe @editor, observerSettings @observer.hold = (cb) => @observer.disconnect() cb() @observer.observe @element(), observerSettings outdated: (mutations) => # Hold the observer until the changes are parsed @observer.hold => for mutation in mutations @appended(node, mutation.target) for node in mutation.addedNodes @removed(node, mutation.target) for node in mutation.removedNodes if mutation.type == 'characterData' @updated mutation.target

When I typed a header in the editor, it worked! It was very fragile, the cursor wouldn't keep its position and just pressing enter would make the whole thing go boom. But it didn't matter. I reached a milestone and I was happy. I knew that if I could at least render something that I could make everything else works. This how I got started building a markdown editor for Ecrire: baby steps.

Building a markdown editor was not easy. You probably look at all this and think I'm crazy and maybe I am. But now, two months later, I have a working markdown editor with all the features I wanted and I just love it. Rewards come in many forms. This small header rendering was just enough to keep me going!

Get more ideas like this to your inbox

You will never receive spam, ever.