about summary refs log tree commit diff
path: root/tools/cheddar/src/bin/cheddar.rs
//! This file defines the binary for cheddar, which can be interacted
//! with in two different ways:
//!
//! 1. As a CLI tool that acts as a cgit filter.
//! 2. As a long-running HTTP server that handles rendering requests
//!    (matching the SourceGraph protocol).
use clap::{App, Arg};
use rouille::{router, try_or_400, Response};
use serde::Deserialize;
use serde_json::json;
use std::collections::HashMap;
use std::io;

use cheddar::{format_code, format_markdown, THEMES};

// Server endpoint for rendering the syntax of source code. This
// replaces the 'syntect_server' component of Sourcegraph.
fn code_endpoint(request: &rouille::Request) -> rouille::Response {
    #[derive(Deserialize)]
    struct SourcegraphQuery {
        filepath: String,
        theme: String,
        code: String,
    }

    let query: SourcegraphQuery = try_or_400!(rouille::input::json_input(request));
    let mut buf: Vec<u8> = Vec::new();

    // We don't use syntect with the sourcegraph themes bundled
    // currently, so let's fall back to something that is kind of
    // similar (tm).
    let theme = &THEMES.themes[match query.theme.as_str() {
        "Sourcegraph (light)" => "Solarized (light)",
        _ => "Solarized (dark)",
    }];

    format_code(theme, &mut query.code.as_bytes(), &mut buf, &query.filepath);

    Response::json(&json!({
        "is_plaintext": false,
        "data": String::from_utf8_lossy(&buf)
    }))
}

// Server endpoint for rendering a Markdown file.
fn markdown_endpoint(request: &rouille::Request) -> rouille::Response {
    let mut texts: HashMap<String, String> = try_or_400!(rouille::input::json_input(request));

    for text in texts.values_mut() {
        let mut buf: Vec<u8> = Vec::new();
        format_markdown(&mut text.as_bytes(), &mut buf, true);
        *text = String::from_utf8_lossy(&buf).to_string();
    }

    Response::json(&texts)
}

fn highlighting_server(listen: &str) {
    println!("Starting syntax highlighting server on '{}'", listen);

    rouille::start_server(listen, move |request| {
        router!(request,
                // Markdown rendering route
                (POST) (/markdown) => {
                    markdown_endpoint(request)
                },

                // Code rendering route
                (POST) (/) => {
                    code_endpoint(request)
                },

                _ => {
                    rouille::Response::empty_404()
                },
        )
    });
}

fn main() {
    // Parse the command-line flags passed to cheddar to determine
    // whether it is running in about-filter mode (`--about-filter`)
    // and what file extension has been supplied.
    let matches = App::new("cheddar")
        .about("TVL's syntax highlighter")
        .arg(
            Arg::with_name("about-filter")
                .help("Run as a cgit about-filter (renders Markdown)")
                .long("about-filter")
                .takes_value(false),
        )
        .arg(
            Arg::with_name("no-tagfilter")
                .help("Disable HTML tag filter")
                .long("no-tagfilter")
                .takes_value(false),
        )
        .arg(
            Arg::with_name("sourcegraph-server")
                .help("Run as a Sourcegraph compatible web-server")
                .long("sourcegraph-server")
                .takes_value(false),
        )
        .arg(
            Arg::with_name("listen")
                .help("Address to listen on")
                .long("listen")
                .takes_value(true),
        )
        .arg(Arg::with_name("filename").help("File to render").index(1))
        .get_matches();

    if matches.is_present("sourcegraph-server") {
        highlighting_server(
            matches
                .value_of("listen")
                .expect("Listening address is required for server mode"),
        );
        return;
    }

    let filename = matches.value_of("filename").expect("filename is required");

    let stdin = io::stdin();
    let mut in_handle = stdin.lock();

    let stdout = io::stdout();
    let mut out_handle = stdout.lock();

    if matches.is_present("about-filter") && filename.ends_with(".md") {
        format_markdown(
            &mut in_handle,
            &mut out_handle,
            !matches.is_present("no-tagfilter"),
        );
    } else {
        format_code(
            &THEMES.themes["InspiredGitHub"],
            &mut in_handle,
            &mut out_handle,
            filename,
        );
    }
}