Tutorial: Markdown editor with AngularJS and marked

Posted by Peter
tags: angularjs javascript markdown

What I want to achieve today:

  1. simple web application allowing to write something in Markdown and instantly see results in HTML while writing.
  2. Most of the time I use Markdown to create GitHub README.md files, so I would like my app to be able to show me how it will look exactly before commiting and pushing to repo
  3. finally, I’d like my code to be highlighted appropriately

OK, this does not seem to complicated, so let’s get to work.

What I’ll be using today:

  • AngularJS - as a framework and real-time content update
  • marked - library compiling Markdown to HTML, also supporting 3rd party libs for code highlight
  • highlight.js - code highlighting
  • Bootstrap 3 - so app will look at least decent
  • bower - because It makes managing dependencies a breeze :)

Let’s start with bower.json - a definition file specifying which libs our application will be using.

1
2
3
4
5
6
7
8
9
10
11
{
  "name": "GitHub flavored markdown with angular.js and marked",
  "dependencies": {
      "angular": "1.3.x",
      "angular-sanitize": "~1.3.x",
      "bootstrap": "~3.x",
      "marked": "~0.3.x",
      "highlightjs": "~8.4.0"
  }
}

As you can see, we’ll be using AngulrJS with Sanitize, Bootstrap, marked and highlight.js.

Before you start anything else, install all dependecies with:

1
bower install

When it’s ready, we can start building application.

I’ll put all the code in index.html - it’s simple app, and there is no need to complicate things.

First: simple HTML5 template with CSS and JS links, it’ll look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html>
<head>
    <title>GHFM</title>

    <link rel="stylesheet" href="./bower_components/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="./bower_components/highlightjs/styles/github.css">

    <style type="text/css" media="screen">
      body {
        padding-top: 100px;
      }

      textarea {
        overflow: hidden;
      }
    </style>
</head>
<body>

<nav class="navbar navbar-default navbar-fixed-top">
  <div class="container-fluid">
    <h2>GitHub flavored markdown with angular.js and marked</h2>
  </div>
</nav>

<!-- application HTML will go here -->

<script type="text/javascript" src="./bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="./bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script type="text/javascript" src="./bower_components/marked/lib/marked.js"></script>
<script type="text/javascript" src="./bower_components/highlightjs/highlight.pack.js"></script>

<script type="text/javascript">
// application code will go here
</script>

</body>
</html>

Now it’s time to attach Angular application and controller to HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html ng-app="markedApp"> <!-- added: ng-app="markedApp" -->
  .....

  <!-- this DIV will hold application controller -->
  <div class="container-fluid" ng-controller="markedController as marked"></div>

  .....

  <script type="text/javascript">

  var markedApp = angular.module('markedApp', []);

  markedApp.controller('markedController', [function() {

  }]);

  </script>
</html>

Now we have an AngularJS application and controller. It’s time to make it start working for us. First there has to be a place where Markdown will be entered and a place where compiled HTML will be displayed. I’ll put necessary code in DIV with controller attached:

1
2
3
4
5
6
7
8
9
10
<div class="container-fluid" ng-controller="markedController as marked">
  <div class="row">
    <div class="col-sm-6">
      <textarea class="form-control" id="inputText" ng-model="marked.inputText"></textarea>
    </div>
    <div class="col-sm-6">
      <div id="outputText" ng-bind-html="marked.outputText"></div>
    </div>
  </div>
</div>

I’m using “controllerAs” syntax instead of $scope - you can read more about it here.

It’s time to fill-in controller code. I want it to do few things:

  • init scope value for inputText (textarea)
  • watch inputText for changes and update output DIV content accordingly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// basic config for marked library - this will provide us with GitHub flavored markdown
marked.setOptions({
    renderer: new marked.Renderer(),
    gfm: true,
    tables: true,
    breaks: false,
    pedantic: false,
    sanitize: false, // if false -> allow plain old HTML ;)
    smartLists: true,
    smartypants: false
  });

markedApp.controller('markedController', ['$scope', '$http', function($scope, $http) {
    var markdown = this;  // alias for this, so we can access it in $scope.$watch

    this.inputText = '';

    $scope.$watch('marked.inputText', function(current, original) {
        markdown.outputText = marked(current);
    });
}]);

You might notice, that something is wrong - there is an error in console. Angular is complaining, that content is unsafe. It’s because we used ng-bind-html. In order to make it work, I’ll use already attached angularjs-sanitize module by injecting it in application:

1
  var markedApp = angular.module('markedApp', ['ngSanitize']); // injected 'ngSanitize'

Cool - now we have our application working - whatever we will write in textarea, will be instantly compiled into HTML and displayed in DIV on the right.

We need to do one more thing, though - syntax highlighting. This is why I already linked to highlight.js and CSS file for GitHub color scheme. Marked lib provides a convenient way to process any code and sort of pipe it through highlight.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  marked.setOptions({
    ....

    // this function pipes code through highlight.js
    highlight: function (code, lang) {

      // in case, there is code without language specified
      if (lang) {
        return hljs.highlight(lang, code).value;
      } else {
        return hljs.highlightAuto(code).value;
      }
    }
  });

One final touch: just to make everything look nice, I’ll use a directive, which will handle textarea auto expanding while writing. I found nice piece of code by enapupe in comments here. In order to use it add HTML auto-grow attribute here:

1
  <textarea class="form-control" id="inputText" ng-model="marked.inputText" auto-grow></textarea>

and this directive code:

1
2
3
4
5
6
7
8
9
10
11
12
  markedApp.directive("autoGrow", function(){
      return function(scope, element, attr){
          var update = function(){
              element.css("height", "auto");
              element.css("height", element[0].scrollHeight + "px");
          };
          scope.$watch(attr.ngModel, function(){
              update();
          });
          attr.$set("ngTrim", "false");
      };
  });

That’s it - application is working and looks not that bad :) If you want to see it in action you can get source from GitHub and run it yourself or if you’re really lazy - demo is here (or try live thing below)¬†;)