I was investigating the possibility of automatically converting markdown documents into HTML, and came across this article from the Visual Studio Code documentation. It details how, using the Task Runner and a Node.js module called Marked, you could set up a task that do exactly what I was after.
Since I would be having PowerShell code in my markdown documents, I was disappointed that no syntax highlighting was present in the generated HTML document. Thus began my quest of getting automatic markdown to HTML, with syntax highlighting, in Visual Studio Code.
Of course just a generated HTML document without any CSS, is a boring thing to look at, and also the main reason that proper syntax highlighting was missing from my generated HTML. I also noticed that other stuff didn’t really look good either, stuff like tables for instance, since these also rely on CSS to look good.
So it became clear that what I needed was a markdown parser that supports syntax highlighting, as well as some CSS for the styling of the finished document. And I needed a way to set it up as a task in Visual Studio Code, similar to the example I found initially.
In this blog post I will detail out how I made this happen, and show you how to go about setting this up yourself.
Node.js
The first thing you need to do is install Node.js. If you have set up Chocolatey as a package provider in PowerShell, this is as easy as entering the following command:
Install-Package nodejs
If you don’t have this option, you need to head over to the Node.js homepage and download the installer.
Visual Studio Code
Next you need to set things up in Visual Studio Code. First of, this only works if you open a folder in vscode, so start by making a new folder for you project and open it in vscode.
Create the following folder structure:
- Documentation
- Documentation\src
- Template
You can of course structure this however you like, but then you will have to remember to change the code to point at the correct folders later on.
Create a new file in the root of the project folder, called gulpfile.js.
This is how it should look at this point.
npm
Now, fire up you favorite console, navigate to your project folder, and type the command
npm init
This will start a small wizard. Don’t worry about the information you type in at this stage, it can all be changed. I usually just give it a name and let all other fields have the default values. At the end it will list the values and ask if you are satisfied.
After you confirm, a new file will be added to your project folder; package.json. This file will be used to hold information about the node packages your project depends on.
While in the console, type
npm install --save gulp gulp-front-matter gulp-layout gulp-remarkable consolidate jade
This will download and install the needed packages and save them in a sub folder in your project. It will also automatically update the package.json file for you.
If you take a look at the package.json file it should look something like this:
Notice also that the modules have been downloaded in a new folder called node_modules.
Gulp
Copy the following code into the gulpfile.js file:
var gulp = require('gulp'), markdown = require('gulp-remarkable'), frontMatter = require('gulp-front-matter'), layout = require('gulp-layout'); gulp.task('Build Docs', function() { return gulp.src('Documentation/src/*.md') .pipe(frontMatter()) .pipe(markdown({html: true})) .pipe(layout(function(file) { return file.frontMatter; })) .pipe(gulp.dest('Documentation')); }); gulp.task('default', function() { gulp.watch('Documentation/src/*.md', ['Build Docs']); });
This will define a new task called Build Docs, as well as set up the default watcher task. The build task does the following
- Get all markdown files under Documentation/src
- Read the front matter information
- Convert the markdown to HTML
- Apply the layout from information found in the front matter
- Save the generated HTML document to the Documentation folder
The watcher task just watches all markdown documents under Documentation/src and will trigger the Build Docs task every time you make any changes to the markdown files found there. It triggers on save.
Layout
Now we need to set up the layout for the generated HTML document. Start by creating the following files under the Template folder:
- default.jade
- vs.min.css
- github-markdown.min.css
The file default.jade is where we define the layout of the finished HTML document. Copy/paste the following code into this file:
html head title= title style include vs.min.css include github-markdown.min.css body .markdown-body != contents
Next we need some CSS to get colored syntax highlighting. If you go to the highligh.js CDN you will find a lot of ready made themes to choose from. I chose the one called vs, but you can of course chose whatever you want, or even make your own if you want to. If you want to follow this guide, just copy/paste the following into vs.min.css:
.hljs{display:block;overflow-x:auto;padding:0.5em;background:white;color:black}.hljs-comment,.hljs-quote,.hljs-variable{color:#008000}.hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-name,.hljs-tag{color:#00f}.hljs-string,.hljs-title,.hljs-section,.hljs-attribute,.hljs-literal,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-addition{color:#a31515}.hljs-deletion,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-meta{color:#2b91af}.hljs-doctag{color:#808080}.hljs-attr{color:#f00}.hljs-symbol,.hljs-bullet,.hljs-link{color:#00b0e8}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}
I also wanted some general CSS to make the HTML look a little better, and found that someone fellow Norwegian Sindre Sørhus, had gone to the trouble of creating a CSS specifically to make markdown-generated content look similar to how it looks on Github. I ran the CSS through a minifier to save some space though. Copy/paste the following into github-markdown.min.css:
.markdown-body hr::after,.markdown-body:after{clear:both}.markdown-body hr::after,.markdown-body hr::before,.markdown-body:after,.markdown-body:before{display:table;content:""}@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,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) format('woff')}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.6;word-wrap:break-word}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects;color:#4078c0;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{outline-width:0;text-decoration:underline}.markdown-body strong{font-weight:bolder}.markdown-body h1{margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body hr{box-sizing:content-box}.markdown-body input{margin:0;overflow:visible;font:13px/1.4 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}.markdown-body [type=button]:-moz-focusring,.markdown-body [type=reset]:-moz-focusring,.markdown-body [type=submit]:-moz-focusring,.markdown-body button:-moz-focusring{outline:ButtonText dotted 1px}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body *{box-sizing:border-box}.markdown-body blockquote{margin:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace}.markdown-body pre{font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace;word-wrap:normal}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:3px!important}.markdown-body .pl-2{padding-left:6px!important}.markdown-body .pl-3{padding-left:12px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:36px!important}.markdown-body .pl-6{padding-left:48px!important}.markdown-body .form-select::-ms-expand{opacity:0}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{display:inline-block;padding-right:2px;margin-left:-18px}.markdown-body .anchor:focus{outline:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:1em;margin-bottom:16px;font-weight:700;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2.25em;line-height:1.2}.markdown-body h1 .anchor{line-height:1}.markdown-body h2{font-size:1.75em;line-height:1.225}.markdown-body h2 .anchor{line-height:1}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor,.markdown-body h4 .anchor{line-height:1.2}.markdown-body h4{font-size:1.25em}.markdown-body h5 .anchor,.markdown-body h6 .anchor{line-height:1.1}.markdown-body h5{font-size:1em}.markdown-body h6{font-size:1em;color:#777}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{overflow:hidden;background:#e7e7e7;height:4px;padding:0;margin:16px 0;border:0}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body blockquote{padding:0 15px;color:#777;border-left:4px solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body table{border-spacing:0;border-collapse:collapse;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:.2em 0;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\00a0"}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:0 0;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body pre code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{background-color:#b52a1d;color:#f8f8f8}.markdown-body .pl-sr .pl-cce{color:#63a35c;font-weight:700}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{color:#1d3e81;font-weight:700}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{color:#333;font-style:italic}.markdown-body .pl-mb{color:#333;font-weight:700}.markdown-body .pl-md{background-color:#ffecec;color:#bd2c00}.markdown-body .pl-mi1{background-color:#eaffea;color:#55a532}.markdown-body .pl-mdr{color:#795da3;font-weight:700}.markdown-body .pl-mo{color:#1d3e81}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#4078c0;border:1px solid #4078c0}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#4078c0}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}
Task Runner
Now we need to configure the Task Runner in Visual Studio Code. Press F1 and start to write “configure” and it should show up. Choose Tasks: Configure Task Runner. Next choose Gulp. This will set up the initial tasks.json file, copy/paste the following into it to get it properly set up:
{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "0.1.0", "command": "gulp", "isShellCommand": true, "args": [ "--no-color" ], "tasks": [ { "taskName": "default", "isBuildCommand": true, "showOutput": "always", "isWatching": true } ] }
Here we are letting Visual Studio Code know that we want the default task to run when we start the Task Runner. If the task output window annoys you, just change showOutput from always to never or silent.
To start the task runner, use Ctrl + Shift + B.
You should see the output window open with information about the running job.
Markdown
The only thing missing now is to test this beauty out!
Create a new file under Documentation/src called test.md and copy/paste the following content:
--- title: Test layout: Template\default.jade --- # This is a test (h1) ## This is H2 ## Code ```ps Write-Host 'Hello World!' Get-Service | Where-Object {$_.Name -eq 'Test'} ``` ## Table | Name | Age | Number | Email | Comments | |-------|-----|--------|----------------|----------| | Peter | 45 | 892394 | test@mail.com | Good guy | | Eva | 36 | 347832 | test2@mail.com | Good gal |
Note that we need the front matter at the top of each file to tell the task what template to use for the generated HTML file.
When you save the document you should see a generated HTML file appear in the Documentation folder. If it doesn’t, and nothing appear in the log window, use Ctrl + Shift + B again and stop the running task, and restart it again right after. Make a small edit to the file and re-save it.
You should see that the task Build Docs have started (and finished) in the output window.
If you open the generated HTML file in a browser, it should look something like this:
That’s it! Hopefully everything worked smoothly. This was a long post, with lots of things to go wrong, so if you do stumble upon some errors, hopefully you are able to figure it out. If not, let me know, and I’ll help you out if I can.
And if you decide to build upon this to make it even better, such as making some killer CSS-files, please let me know!
And remember that you need to specifically start the task running every time you open the folder in Visual Studio Code. I haven’t tried it out, but there is an extension called Blade Runner on the market place that will automatically start the task whenever you open the folder, so if you tend to forget, give it a try.