Demo entry 5354378

Simple web server to render markdown

   

Submitted by Lee Jenkins on Jun 17, 2016 at 23:04
Language: Rust. Code size: 8.0 kB.

extern crate hyper;
extern crate preferences;
extern crate pulldown_cmark;

use hyper::server::{
    Request,
    Response,
    Server,
};
use hyper::status::StatusCode;
use hyper::uri::RequestUri;
use preferences::{
    Preferences,
    PreferencesMap,
};
use pulldown_cmark::{
    html,
    Event,
    Parser,
    Tag,
};
use std::borrow::Cow;
use std::fs::File;
use std::io::{
    self,
    Read,
    Write,
};
use std::path::{
    Path,
    PathBuf,
};
use std::process::{
    Command,
    Stdio,
};
use std::str;

const PKG_NAME: &'static str = env!("CARGO_PKG_NAME");

struct Config {
    base_dir: PathBuf,
    global_css: PathBuf,
}

impl Config {
    fn new() -> Config {
        let mut relative_save_path = PathBuf::from(PKG_NAME);
        relative_save_path.push("config.json");
        let mut prefs: PreferencesMap<String> = PreferencesMap::new();
        prefs.load(relative_save_path.to_str().unwrap()).unwrap();

        // TODO stop and prompt for these if not set
        let base_dir_string = prefs.get("base_dir").unwrap();
        // Expand home directory
        let base_dir = if base_dir_string.starts_with("~/") {
            std::env::home_dir().unwrap().join(&base_dir_string[2..])
        } else {
            PathBuf::from(base_dir_string)
        }.canonicalize().unwrap();
        Config {
            base_dir: base_dir,
            global_css: PathBuf::from(prefs.get("global_css").unwrap()),
        }
    }

    fn format_body(&self, body: String) -> String {
        // TODO HTML5 boilerplate
        // TODO platform agnostic path to string
        format!("\
            <!doctype html><html><head><meta charset='utf-8'>\
            <link rel='stylesheet' type='text/css' href='/{}'>\
            </head><body>{}</body></html>\
        ", self.global_css.to_str().unwrap(), body)
    }
}

fn main() {
    let config = Config::new();
    let handler = move |request: Request, mut response: Response| {
        match (request.method, request.uri) {
            (hyper::Get, RequestUri::AbsolutePath(path)) => {
                respond(&config, response, &path);
            }
            (hyper::Get, _) => *response.status_mut() = StatusCode::BadRequest,
            _ => *response.status_mut() = StatusCode::MethodNotAllowed,
        }
    };
    println!("Listening at http://localhost:4792");
    Server::http("::FFFF:127.0.0.1:4792").unwrap().handle(handler).unwrap();
}

fn respond(config: &Config, mut response: Response, path: &str) {
    let file_path = config.base_dir.join(&path[1..]);
    let contents = if file_path.is_dir() {
        let mut html = String::new();
        html.push_str("<ul>");
        for entry in file_path.read_dir().unwrap().map(Result::unwrap) {
            let name = entry.file_name().into_string().unwrap();
            html.push_str("<li><a href=\"");
            html.push_str(path);
            // Add the separator if it's missing
            if !path.ends_with("/") {
                html.push('/');
            }
            html.push_str(&name);
            html.push_str("\">");
            html.push_str(&name);
            // Add a / to indicate this is a directory if relevant
            // Go through PathBuf so that we traverse symlinks
            if entry.path().is_dir() {
                html.push('/');
            }
            html.push_str("</a></li>");
        }
        html.push_str("</ul>");
        config.format_body(html)
    } else {
        let content_result = if path.ends_with(".md") {
            render_file(file_path).map(|body| config.format_body(body))
        } else {
            read_file(file_path)
        };
        match content_result {
            Ok(contents) => contents,
            Err(error) => {
                *response.status_mut() =
                if io::ErrorKind::NotFound == error.kind() {
                    StatusCode::NotFound
                } else {
                    StatusCode::InternalServerError
                };
                return;
            }
        }
    };
    response.send(contents.as_bytes()).unwrap();
}

struct HightlightingParser<'a>(Parser<'a>);

impl<'a> Iterator for HightlightingParser<'a> {
    type Item = Event<'a>;

    // TODO automatically add links
    fn next(&mut self) -> Option<Event<'a>> {
        match self.0.next() {
            Some(Event::Start(Tag::CodeBlock(ref language))) => {
                // Collect the contents of the code block
                let mut code = String::new();
                let mut current_event;
                loop {
                    current_event = self.0.next();
                    if let Some(Event::Text(code_line)) = current_event {
                        // TODO Pass lines on as they are received instead of buffering.
                        code.push_str(&code_line);
                    } else {
                        break;
                    }
                }
                match current_event {
                    Some(Event::End(Tag::CodeBlock(_))) => {
                        // TODO Starting a process is expensive. Write a simple Python daemon to
                        // wrap pygments and keep it alive.
                        let mut pygmentize_command = Command::new("pygmentize");
                        pygmentize_command.stdin(Stdio::piped())
                                          .stdout(Stdio::piped())
                                          .stderr(Stdio::piped())
                                          .arg("-f")
                                          .arg("html")  // Output format is html
                                          .arg("-O")
                                          .arg("nowrap");  // Don't wrap the result in div/pre
                        let mut pygmentize = if language.is_empty() {
                            // Guess the language from contents
                            pygmentize_command.arg("-g")
                        } else {
                            // Lex assuming the specified language
                            pygmentize_command.arg("-l")
                                              .arg(language as &str)
                        }.spawn().unwrap();

                        pygmentize.stdin.as_mut().unwrap().write_all(code.as_bytes()).unwrap();
                        let mut html = String::from("<pre><code>");
                        let output = pygmentize.wait_with_output().unwrap();
                        let stderr = str::from_utf8(&output.stderr).unwrap();
                        if stderr.contains("Error: no lexer for alias") {
                            println!("Unrecognized language '{}'. \
                                      Check http://pygments.org/docs/lexers/",
                                     language);
                            // TODO escape this shit
                            html.push_str(&code);
                        } else if !stderr.is_empty() {
                            panic!("pygmentize: {}", stderr);
                        } else {
                            html.push_str(str::from_utf8(&output.stdout).unwrap());
                        }
                        html.push_str("</code></pre>");

                        Some(Event::Html(Cow::from(html)))
                    }
                    _ => panic!("Found unexpected event in code block: {:?}",
                                current_event),
                }
            }
            event => event,
        }
    }
}

fn render_file<P>(path: P) -> Result<String, io::Error> where P: AsRef<Path> {
    let markdown = try!(read_file(path));
    let mut html = String::with_capacity(markdown.len() * 3 / 2);
    let parser = HightlightingParser(Parser::new(&markdown));
    html::push_html(&mut html, parser);
    Ok(html)
}

fn read_file<P>(path: P) -> Result<String, io::Error> where P: AsRef<Path> {
    let mut text = String::new();
    try!(File::open(path)).read_to_string(&mut text).unwrap();
    Ok(text)
}

This snippet took 0.01 seconds to highlight.

Back to the Entry List or Home.

Delete this entry (admin only).