I had to build my own Markdown Editor because no tool was fast enough for me.
Tags: productivity, opensource, showdev, javascript
Published: 2020-02-14 17:46:44

---
title: I had to build my own Markdown Editor because no tool was fast enough for me.
published: true
description: 
tags: #productivity #opensource #showdev #javascript
---

### TL;DR
I built an **open-source** markdown editor called **Fast Author** to improve my productivity when creating written articles specific for tech tutorials involving lots screenshots.

👉 https://github.com/ExamProCo/fast-author

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1eqe2qtlg7r6628ny9h2.png)

P.S I wrote this article in this editor.

### The Pain Point
I've been working on a new version of my AWS Certified Developer course which I am publishing free-to-watch on freeCodeCamp and I had finished recording all my lecture videos with only the **follow alongs** remaining.

Follow Alongs (some may call them labs) is where I make a video where you follow along with me to get hands-on skills with AWS.

However, producing these videos has been a bottleneck on my production because I have to back-track as I discover things on the fly which could result in me re-recording 3-4 video sections back.

So it makes sense for me to tutorialize them along the way in written format which is easier to modify.

I have to create written versions anyway since on my paid platform we offer the written versions as plus to our the free-to-watch videos.

I am feeling the pain hard here because the Developer Associate is heavy hands-on and these follow alongs need more attention and complexity than any other Certification.

### Existing Editors and My Use Case

There are many markdown editors out there, but none are designed for power-users or optimized for my use case which are:

#### What did I build it in and how long did it take?

I built in 3 days. 2 days building, and 1 day QA with my co-founder Bayko. Today I am using it for its intended use and I already know I am going to get a 500% increase in productivity. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/h9dwtfd4hbo0zf2b69mf.png)

**Electron**

I already build an open-source video game called Swap-N-Pop so it was a simple as reviewing what I had done before.

**Coffeescript**

Typescript would have been a better choice if I had multiple collaborators but I wanted to get this done as fast as possible and Coffeescript gives speed like no other.

This was the same path as Swap-N-Pop where when I needed test code, and more collabs I converted it from Coffeescript from Typescript.


**MithrilJS**

I was going to use Svelte but I wanted to get this done, so I just rolled back to leveraging Mithril where I have solved lots of javascript **hard parts* and I didn't want to tack on two extra days to development.


**SharpJS**

I hate working with ImageMagick so I opted for SharpJS which is much easier to install, but we had considerable pain getting this work with Electron. I had to find the right version of both Electron and SharpJS.


#### The Editor
* The editor should use a mono-spaced font to easily align text which will render in code elements.
* Should be to quickly toggle into a publisher preview mode
* Design should be optimized for side by side preview
* Need hotkeys for custom tags for Highting, underling and marking text red.

#### The Images
* Should be able to drag in images into the editor
* Should be able to quickly edit images to resize, crop, border and draw rectangle and markers
* Should store the original images in the project for future reference or modifications

#### The Preview and Export

* Should be able to load custom css for publisher preview so I can see what it would like on DEV, Medium, freeCodeCamp, HashNode or etc
* Should rename the files in order of appearance as they are being moved around on export

#### Added Bonsues

Since this is an Electron app I should be able to add my Grammarly extension to better improve my writing.

### A Project of Distraction or Procrastination?

So far its been worth the detour. If I worked for another company and I proposed I could try to build a tool in a few days to save weeks they probably wouldn't let me do it since most people would see it as a distraction.

I could have completed my course in the days I built this but it's so easy to focus on the short term, and knowing when to put the time, in the long run, is a skill that requires lots of attempts at failed distractions.

### Interesting Code

I thought I pull out some code which was interesting:

I borrowed the online function to get the relative coordinates for a Canvas.

```js
function relMouseCoords(event){
    var totalOffsetX = 0;
    var totalOffsetY = 0;
    var canvasX = 0;
    var canvasY = 0;
    var currentElement = this;

    do{
        totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft;
        totalOffsetY += currentElement.offsetTop - currentElement.scrollTop;
    }
    while(currentElement = currentElement.offsetParent)

    canvasX = event.pageX - totalOffsetX;
    canvasY = event.pageY - totalOffsetY;

    return {x:canvasX, y:canvasY}
}
```

I would overlay a canvas on an image. Then I can capture the canvas as an image using the `toDataURL()` and the replacing the start of the string `replace(/^data:image\/png;base64,/, "")`
```js
function save(){
  console.log('saving')
  let path = "/tmp/save-drawing-overlay.png"
  const el = document.getElementById('draw')
  fs.writeFile(path, el.toDataURL().replace(/^data:image\/png;base64,/, ""), 'base64', function(err){
    console.log(err)
    ipc.send('sharp-draw',{overlay: path, source: asset.path})
  })
}
```


SharpJS can composite two files on top of each other which is how I am saving images.

```js
sharp(opts.source).composite([{input: opts.overlay}]).toFile(new_asset)
```

I set global hotkeys and just watch on keydown.

```coffee
# global hotkeys
document.addEventListener 'keydown', (e)=>
  meta =
  if os.platform() is 'darwin'
    'Meta'
  else
    'Control'
  Data.meta(true)  if e.key is meta
  Data.shift(true) if e.key is 'Shift'
  if Data.meta()
    if e.key is 'f'
      ipc.send('toggle-fullscreen')
    else if e.key is 'p'
      Data.publisher_preview !Data.publisher_preview()
      m.redraw(true)
    else if e.key is 'n'
      ipc.send('prompt-new')
    else if e.key is 's' && Data.shift()
      Data.splitview !Data.splitview()
      m.redraw(true)
    else if e.key is 'w' && Data.shift()
      Data.line_wrap !Data.line_wrap()
      m.redraw(true)
document.addEventListener 'keyup', (e)=>
  Data.meta(false)
  Data.shift(false)
```

All the data is stored in a Singleton. No reactive nonsense.

```coffee
import stream from 'mithril/stream'

class Data
  constructor:->
    # The root directory where all the markdown files are stored
    # eg. ~/fast-author/
    @home = stream('')

    # When the current file was last saved
    @last_saved = stream('')

    # the file that shows selecte in the right hand column
    @active_file = stream(null)

    # files that appear in the right hand column
    @files  = stream([])

    # assets that appear in the right hand column
    # assets only for the current markdown file that is active
    @assets  = stream([])

    # The currently selected image in the markdown to apply editing
    @active_asset = stream null

    # the contents of the markdown file
    @document = stream('')

    # whether the meta key is being held eg. Command on Mac
    @meta = stream(false)

    # whether the shift key is behind held
    @shift = stream(false)

    # whether to wrap or not wrap lines in textarea
    @line_wrap = stream(false)
    #
    # whether to split the view (show both editor or preview, or just editor)
    @splitview = stream(true)

    # when true will hide editor and center preview.
    @publisher_preview = stream(false)

    # the start and end select for markdown textarea
    @selectionStart = stream false
    @selectionEnd   = stream false

    # current selections for infobar
    @_selectionStart = stream 0
    @_selectionEnd   = stream 0
  markdown_path:(name)=>
    path = "#{@home()}/#{name}/index.md"
    console.log path
    path
  # select can be loss after certain updates to textarea.
  # This ensures our old selection remains
  keep_selection:=>
    @selectionStart @_selectionStart()
    @selectionEnd @_selectionEnd()
  get_asset:=>
    asset = null
    for a in @assets()
      if a.path is @active_asset().replace('file://','')
        asset = a
        break
    asset
export default new Data()

```