I had to build my own Markdown Editor because no tool was fast enough for me.
Andrew Brown 🇨🇦

Andrew Brown 🇨🇦 @andrewbrown

About: I make free cloud certification courses

Location:
Schreiber
Joined:
Oct 19, 2018

I had to build my own Markdown Editor because no tool was fast enough for me.

Publish Date: Feb 14 '20
245 18

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

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

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.



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}
}


Enter fullscreen mode Exit fullscreen mode

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,/, "")



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})
  })
}


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

I set global hotkeys and just watch on keydown.



# 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)


Enter fullscreen mode Exit fullscreen mode

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



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()



Enter fullscreen mode Exit fullscreen mode

Comments 18 total

  • Ben Halpern
    Ben HalpernFeb 14, 2020

    Wow, well done.

  • Michael "lampe" Lazarski
    Michael "lampe" LazarskiFeb 14, 2020

    Nice work :)

  • Haider Ali Punjabi
    Haider Ali PunjabiFeb 15, 2020

    Just started using Typora, but maybe it's time to switch :)

  • Marko Shiva
    Marko ShivaFeb 15, 2020

    A JavaScript error occurred in the main process
    Uncaught Exception:
    Error:
    Something went wrong installing the "sharp" module

    Module did not self-register.

    • Andrew Brown 🇨🇦
      Andrew Brown 🇨🇦Feb 15, 2020

      I spent so many hours on this.

      You may need to change 0.24 in the package json to 0.23.4 , and then

      rm -rf node_modules/sharp
      

      And ensure you do the electron-rebuild setup. That might fix it.

      I think once we build binaries things will be easier

  • Ben Sinclair
    Ben SinclairFeb 15, 2020

    Nice, but I think you built it because you couldn't find a tool that had all the features you want built-in. The line about it not being fast enough is to do with workflow, not the editor.

    I mean, Vim is pretty fast at editing markdown, but it doesn't have image editing features, and that's ok. Other people's workflows will be to use an image editor to edit images and a text editor to edit text. Building a behemoth is fine, but it's not going to speed up other people's workflows.

    • Andrew Brown 🇨🇦
      Andrew Brown 🇨🇦Feb 15, 2020

      The line about it not being fast enough is to do with workflow, not the editor.

      I thought that was obvious. I guess I'll do more exposition dumps in the future.

      Building a behemoth is fine, but it's not going to speed up other people's workflows.

      What's your point here? I built it for me. If you're doing the same kind of content creation as me then it will absolutely speed up your workflow.

  • Sean Allin Newell
    Sean Allin NewellFeb 15, 2020

    No reactive nonesense.

    Blam-o!

    I'll have to check it out.

    • Andrew Brown 🇨🇦
      Andrew Brown 🇨🇦Feb 16, 2020

      Its much more stable today. Once you make a project public you realize you can't just keep pushing random commits.

  • Ant The Developer
    Ant The DeveloperFeb 15, 2020

    I totally understand your motivation. I've written several Electron-based markdown editors in a struggle to do the same as you. Though I think I made my scope too large to satisfy.

    The current editors out there are really not good enough because many tools are great for writing but not for editing, publishing, or writing multi-markdown documents.

  • Brandon Martel
    Brandon MartelFeb 15, 2020

    Nice to see another Mithril user. One of the most underrated libs around, and my personal go to for years when I'm on a tight deadline. Really nice work.

  • Charbel Sarkis
    Charbel SarkisFeb 15, 2020

    Very nice.

  • Carlos Orelhas
    Carlos OrelhasFeb 17, 2020

    Good Job Andrew, an amazing project you built!

  • Akshay Kadam (A2K)
    Akshay Kadam (A2K)Feb 17, 2020

    Amazing job. Since I write blogs too, I wanted to build something like this but it uploads to Imgur so that I don’t have to manually upload to Imgur & link images to it. I’ll probably build it 😂

  • Rick Mills
    Rick MillsMar 2, 2020

    This is awesome, as someone in the middle of writing a ton of markdown content I share your pain with the existing editors out there so will be giving this a try!

    One thing I am curious about - grammarly. Did you manage to get this working? It's super tedious copy/pasting into their online editor to check things constantly.

    • Andrew Brown 🇨🇦
      Andrew Brown 🇨🇦Mar 2, 2020

      Not yet, but its fairly easy to load Chrome Extensions into Electron:

      Just have to download the extension and then use the Electron API addDevToolsExtension

      const path = require('path')
      const os = require('os')
      
      BrowserWindow.addDevToolsExtension(
         path.join(os.homedir(), '/Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/4.3.0_0')
      )
      

      electronjs.org/docs/tutorial/devto...

      Its a pain point for myself as well. I'm just in the middle of publishing a 10 hour course, so something that show up on Fast-Author in a week or two

  • Duy K. Bui
    Duy K. BuiMar 3, 2020

    For those who thought "SharpJS, what???" like I did, Andrew was referring to Sharp (sharp.pixelplumbing.com/), not SharpJS (iridiumion.github.io/SharpJS/)

Add comment