Initial import of Markdown Renderer.

This commit is contained in:
Yi Wang 2013-11-02 23:07:52 +08:00
commit 05cc531ee2
4 changed files with 442 additions and 0 deletions

104
README.md Normal file
View File

@ -0,0 +1,104 @@
# Markdown Renderer
Markdown Renderer is a very simple HTTP server written in Go. It
renders Markdown documents retrieved from another (specified) HTTP
server into HTML.
Markdown Renderer uses package
[`github.com/knieriem/markdown`](https://github.com/knieriem/markdown)
to render Markdown documents. It can apply a CSS with the output
HTML. You can write your own CSS, or download one from the Internet,
like [this](http://kevinburke.bitbucket.org/markdowncss/).
### A Use Case: Render Markdown Documents in SVN
This is a real use case, and the one that motivated me to write
Markdown Renderer.
In our company, we have an SVN server, on which our code and documents
reside. We would like to be able to browse our documents from the Web
browser, in particular, we want those documents in Markdown syntax
being renderred to HTML. However, the SVN server is not smart enough
to render Markdown documents; more than that, it does not even
recognizes file types of documents and returns all documents with
`Content-Type: text/plain` anyway.
This inspires me to set up an Nginx server, which `proxy_pass`es all
requests to the SVN server, and set the correct `Content-Type` by the
file extension name of corresponding document. This can be done using
the `more_set_headers` directive provided by Nginx module
[`HttpHeadersMoreModule`](http://wiki.nginx.org/HttpHeadersMoreModule).
Any example Nginx configuration should be like this:
server {
location ~ \.docx$ {
more_set_headers application/msword;
}
location ~ \.xlsx$ {
more_set_headers application/vnd.ms-excel;
}
}
However, this module is not able to render Markdown text into HTML.
Indeed, I cannot find an Nginx module that can do this. I tried to
write one by my own; however, had I digged into this work could I
realise what a pain it is to write an Nginx filter module! This made
me to resort to an alternative way, to write a separate HTTP server,
instead of an Nginx module. Thus comes Markdown Renderer.
With Markdown Renderer, a new `location` line can be added to above
example configuration:
location ~ \.md$ {
proxy_pass http://localhost:8002;
}
where `localhost:8002` is supposed to be the Markdown Renderer server
started with proper command line flags set. For example:
./markdown-renderer -addr=:8002 -data="http://svn-server:9006 -css="/markdown.css"
where `svn-server:9006` is just a replaceholder; you should change it
to your SVN or document server.
### Play with Markdown Renderer
The `nginx.conf` attached with this project configures two Nginx
virtual servers: the document-type-recognizer server as described in
above use case, and one that mimics the SVN/document server.
The recognizer server listens on `localhost:8001`, the Markdown
Renderer server listens on `localhost:8002`, and the fake SVN server
listens on `localhost:8003`. They work in a chain:
|browser|----|:8001|----(.md files)----|:8002|----|:8003|
\---(other docs)-------------/
If you want to setup this configuration on your computer and play with
it, these are the steps:
1. Checkout and build Markdown Renderer:
export ~/Projects/markdown-renderer
cd ~/Projects
go get github.com/wangkuiyi/markdown-renderer
1. Download, build and install Nginx.
1. Make Nginx use the configuration file provided with Markdown Renderer.
cd /usr/local/nginx/conf # suppose that Nginx was installed here.
mv nginx.conf nginx.conf.bak # backup the configuration file.
ln -s ~/Projects/markdown-renderer/src/github.com/wangkuiyi/markdown-renderer/nginx.conf
1. Start Nginx.
/usr/local/nginx/sbin/nginx
1. Build and start Markdown Renderer
cd ~/Projects/markdown-renderer/src/github.com/wangkuiyi/markdown-renderer
go install
~/Projects/markdown-renderer/bin/markdown-renderer

49
main.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"bufio"
"flag"
"fmt"
"github.com/knieriem/markdown"
"log"
"net/http"
"regexp"
)
var addr = flag.String("addr", ":8002", "address to listen on")
var data = flag.String("data", "http://localhost:8003/", "the data server")
var css = flag.String("css", "/markdown.css", "path to CSS")
var validPath = regexp.MustCompile("^/([_a-zA-Z0-9]+)\\.md$")
func renderMarkdownHandler(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.Error(w, r.URL.Path, http.StatusNotAcceptable)
return
}
md := *data + m[1] + ".md"
resp, err := http.Get(md)
if err != nil {
http.Error(w, err.Error() + md, http.StatusInternalServerError)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "text/html;charset=UTF-8")
bo := bufio.NewWriter(w)
fmt.Fprintf(bo, "<link href=\"%s\" rel=\"stylesheet\"> </link>\n", *css)
markdown.NewParser(nil).Markdown(resp.Body, markdown.ToHTML(bo))
bo.Flush()
}
func main() {
flag.Parse()
http.HandleFunc("/", renderMarkdownHandler)
e := http.ListenAndServe(*addr, nil)
if e != nil {
log.Fatal("ListenAndServe: ", e)
}
}

260
markdown.css Normal file
View File

@ -0,0 +1,260 @@
body{
margin: 0 auto;
font-family: Georgia, Palatino, serif;
color: #444444;
line-height: 1;
max-width: 960px;
padding: 30px;
}
h1, h2, h3, h4 {
color: #111111;
font-weight: 400;
}
h1, h2, h3, h4, h5, p {
margin-bottom: 24px;
padding: 0;
}
h1 {
font-size: 48px;
}
h2 {
font-size: 36px;
/* The bottom margin is small. It's designed to be used with gray meta text
* below a post title. */
margin: 24px 0 6px;
}
h3 {
font-size: 24px;
}
h4 {
font-size: 21px;
}
h5 {
font-size: 18px;
}
a {
color: #0099ff;
margin: 0;
padding: 0;
vertical-align: baseline;
}
a:hover {
text-decoration: none;
color: #ff6600;
}
a:visited {
color: purple;
}
ul, ol {
padding: 0;
margin: 0;
}
li {
line-height: 24px;
}
li ul, li ul {
margin-left: 24px;
}
p, ul, ol {
font-size: 16px;
line-height: 24px;
max-width: 540px;
}
pre {
padding: 0px 24px;
max-width: 800px;
white-space: pre-wrap;
}
code {
font-family: Consolas, Monaco, Andale Mono, monospace;
line-height: 1.5;
font-size: 13px;
}
aside {
display: block;
float: right;
width: 390px;
}
blockquote {
border-left:.5em solid #eee;
padding: 0 2em;
margin-left:0;
max-width: 476px;
}
blockquote cite {
font-size:14px;
line-height:20px;
color:#bfbfbf;
}
blockquote cite:before {
content: '\2014 \00A0';
}
blockquote p {
color: #666;
max-width: 460px;
}
hr {
width: 540px;
text-align: left;
margin: 0 auto 0 0;
color: #999;
}
/* Code below this line is copyright Twitter Inc. */
button,
input,
select,
textarea {
font-size: 100%;
margin: 0;
vertical-align: baseline;
*vertical-align: middle;
}
button, input {
line-height: normal;
*overflow: visible;
}
button::-moz-focus-inner, input::-moz-focus-inner {
border: 0;
padding: 0;
}
button,
input[type="button"],
input[type="reset"],
input[type="submit"] {
cursor: pointer;
-webkit-appearance: button;
}
input[type=checkbox], input[type=radio] {
cursor: pointer;
}
/* override default chrome & firefox settings */
input:not([type="image"]), textarea {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
input[type="search"] {
-webkit-appearance: textfield;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
label,
input,
select,
textarea {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
font-weight: normal;
line-height: normal;
margin-bottom: 18px;
}
input[type=checkbox], input[type=radio] {
cursor: pointer;
margin-bottom: 0;
}
input[type=text],
input[type=password],
textarea,
select {
display: inline-block;
width: 210px;
padding: 4px;
font-size: 13px;
font-weight: normal;
line-height: 18px;
height: 18px;
color: #808080;
border: 1px solid #ccc;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
select, input[type=file] {
height: 27px;
line-height: 27px;
}
textarea {
height: auto;
}
/* grey out placeholders */
:-moz-placeholder {
color: #bfbfbf;
}
::-webkit-input-placeholder {
color: #bfbfbf;
}
input[type=text],
input[type=password],
select,
textarea {
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear 0.2s, box-shadow linear 0.2s;
transition: border linear 0.2s, box-shadow linear 0.2s;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
-moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
input[type=text]:focus, input[type=password]:focus, textarea:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
-moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
}
/* buttons */
button {
display: inline-block;
padding: 4px 14px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
line-height: 18px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
background-color: #0064cd;
background-repeat: repeat-x;
background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
background-image: -moz-linear-gradient(top, #049cdb, #0064cd);
background-image: -ms-linear-gradient(top, #049cdb, #0064cd);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
background-image: -o-linear-gradient(top, #049cdb, #0064cd);
background-image: linear-gradient(top, #049cdb, #0064cd);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
border: 1px solid #004b9a;
border-bottom-color: #003f81;
-webkit-transition: 0.1s linear all;
-moz-transition: 0.1s linear all;
transition: 0.1s linear all;
border-color: #0064cd #0064cd #003f81;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
}
button:hover {
color: #fff;
background-position: 0 -15px;
text-decoration: none;
}
button:active {
-webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
}
button::-moz-focus-inner {
padding: 0;
border: 0;
}

29
nginx.conf Normal file
View File

@ -0,0 +1,29 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8001;
server_name localhost;
location / {
root /Users/wangyi/site;
index index.md;
}
location ~ \.md$ {
proxy_pass http://localhost:8002; # Markdown Renderer server.
}
}
server {
listen 8003;
server_name localhost; # Markdown source loader server.
location / {
root /Users/wangyi/site;
}
}
}