Initial import of Markdown Renderer.
This commit is contained in:
commit
05cc531ee2
104
README.md
Normal file
104
README.md
Normal 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
49
main.go
Normal 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
260
markdown.css
Normal 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
29
nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user