#![allow(clippy::not_unsafe_ptr_arg_deref)]
pub use std::io;
use anyhow::{anyhow, Result};use deno_bindgen::deno_bindgen;use image::{ImageOutputFormat, Rgba};use silicon::formatter::ImageFormatterBuilder;use silicon::utils::{init_syntect, Background, ShadowAdder, ToRgba};use syntect::easy::HighlightLines;use syntect::util::LinesWithEndings;
#[deno_bindgen]pub enum SiliconResult { FontList { data: Vec<String> }, ThemeList { data: Vec<String> }, Image { data: String }, Error { error: String },}
#[deno_bindgen]pub fn font_list() -> SiliconResult { let source = font_kit::source::SystemSource::new(); match source.all_families() { Ok(fonts) => SiliconResult::FontList { data: fonts }, Err(err) => SiliconResult::Error { error: err.to_string(), }, }}
#[deno_bindgen]pub fn theme_list() -> SiliconResult { let (_, ts) = init_syntect(); let thems: Vec<String> = ts.themes.keys().cloned().collect(); SiliconResult::ThemeList { data: thems }}
#[deno_bindgen]pub struct Options { code: String, language: String, no_line_number: bool, no_round_corner: bool, no_window_controls: bool, background_color: String, font: String, highlight_lines: String, line_offset: u32, line_pad: u32, pad_horiz: u32, pad_vert: u32, shadow_blur_radius: f32, shadow_color: String, shadow_offset_x: i32, shadow_offset_y: i32, tab_width: u8, theme: String,}
fn parse_font(s: String) -> Vec<(String, f32)> { let mut result = vec![]; for font in s.split(';') { let tmp = font.split('=').collect::<Vec<_>>(); let font_name = tmp[0].to_owned(); let font_size = tmp .get(1) .map(|s| s.parse::<f32>().unwrap()) .unwrap_or(26.0); result.push((font_name, font_size)); } result}
fn parse_line_range(s: String) -> Result<Vec<u32>> { let mut result = vec![]; for range in s.split(';') { let range: Vec<u32> = range .split('-') .map(|s| s.parse::<u32>()) .collect::<Result<Vec<_>, _>>()?; if range.len() == 1 { result.push(range[0]) } else { for i in range[0]..=range[1] { result.push(i); } } } Ok(result)}
fn parse_color(s: String) -> Result<Rgba<u8>> { let color = s.to_rgba().map_err(|x| anyhow!("invalid color: {}", x))?; Ok(color)}
fn run(opts: Options) -> Result<Vec<u8>> { let (ps, ts) = init_syntect(); let code = opts.code; let lang = opts.language.as_str();
let syntax = if let Some(syntax) = ps.find_syntax_by_token(lang) { syntax } else { ps.find_syntax_by_token("txt").unwrap() };
let theme = &ts .themes .get(&opts.theme) .ok_or_else(|| anyhow!("unsupported theme"))?;
let source = font_kit::source::SystemSource::new();
let all_fonts = source.all_families().map_err(|x| anyhow!("{}", x))?; let font = if opts.font.is_empty() || !all_fonts.contains(&opts.font) { vec![] } else { parse_font(opts.font) };
let mut h = HighlightLines::new(syntax, theme); let highlight = LinesWithEndings::from(&code) .map(|line| h.highlight(line, &ps)) .collect::<Vec<_>>();
let mut highlight_lines = Vec::<u32>::new(); if !opts.highlight_lines.is_empty() { highlight_lines = parse_line_range(opts.highlight_lines)?; }
let background = parse_color(opts.background_color)?; let shadow = parse_color(opts.shadow_color)?;
let shadow_adder = ShadowAdder::new() .background(Background::Solid(background)) .shadow_color(shadow) .blur_radius(opts.shadow_blur_radius) .pad_horiz(opts.pad_horiz) .pad_vert(opts.pad_vert) .offset_x(opts.shadow_offset_x) .offset_y(opts.shadow_offset_y);
let mut formatter = ImageFormatterBuilder::new() .line_pad(opts.line_pad) .window_controls(!opts.no_window_controls) .line_number(!opts.no_line_number) .font(font) .round_corner(!opts.no_round_corner) .shadow_adder(shadow_adder) .tab_width(opts.tab_width) .highlight_lines(highlight_lines) .line_offset(opts.line_offset) .build()?;
let image = formatter.format(&highlight, theme);
let mut out = io::Cursor::new(Vec::new()); image.write_to(&mut out, ImageOutputFormat::Png)?;
let result = out.get_ref().to_vec(); Ok(result)}
#[deno_bindgen]pub fn generate(opts: Options) -> SiliconResult { match run(opts) { Ok(raw_data) => { let b64 = base64::encode(raw_data.as_slice()); SiliconResult::Image { data: b64 } } Err(err) => SiliconResult::Error { error: err.to_string(), }, }}
#[cfg(test)]mod tests { use std::{fs::File, io::Read, path::Path};
use super::*;
fn assert_image(raw: Vec<u8>) { let png_header: Vec<u8> = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; let raw_header = &raw[..8]; assert_eq!(raw_header, png_header); }
fn default_opts() -> Options { Options { code: "".into(), language: "rs".into(), no_line_number: true, no_round_corner: false, no_window_controls: true, background_color: "#CCCCCC".into(), font: "".into(), highlight_lines: "5-7".into(), line_offset: 1, line_pad: 2, pad_horiz: 50, pad_vert: 50, shadow_blur_radius: 10.5, shadow_offset_x: 10, shadow_offset_y: 10, shadow_color: "#003399".into(), tab_width: 10, theme: "Solarized (dark)".into(), } }
#[test] fn test_run() { let mut source = File::open(Path::new("testdata/main.rs")).unwrap(); let mut contents = String::new(); source.read_to_string(&mut contents).unwrap();
let mut opts = default_opts(); opts.code = contents;
let got = run(opts).unwrap(); assert_image(got); }
#[test] fn test_run_without_lang() { let mut opts = default_opts(); opts.code = "hoge".into(); opts.language = "".into();
let got = run(opts).unwrap(); assert_image(got); }
#[test] fn test_run_not_found_font() { let mut opts = default_opts(); opts.code = "hoge".into(); opts.font = "not_found".into();
let got = run(opts).unwrap(); assert_image(got); }}