Tutorial: Markdown editor with AngularJS and marked
What I want to achieve today:
- simple web application allowing to write something in Markdown and instantly see results in HTML while writing.
- 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
- 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) ;)