use std::process::ExitStatus; use anyhow::Error; use anyhow::Context as AnyhowContext; use anyhow::Result; use anyhow::anyhow; use handlebars::{Handlebars, HelperDef, RenderContext, Helper, Context, JsonRender, HelperResult, Output, RenderError, PathAndJson}; use log::trace; use serde::Deserialize; use serde::Serialize; use syntect::easy::HighlightLines; use syntect::highlighting::{ThemeSet, Style}; use syntect::parsing::SyntaxSet; use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; use tokio::process::Command; use crate::package::Package; use crate::package::Phase; use crate::package::PhaseName; #[derive(parse_display::Display, Serialize, Deserialize, Clone, Debug)] #[serde(transparent)] #[display("{0}")] pub struct Script(String); impl From for Script { fn from(s: String) -> Script { Script(s) } } #[derive(Clone, Debug)] pub struct Shebang(String); impl Script { pub fn highlighted<'a>(&'a self, script_theme: &'a str) -> HighlightedScript<'a> { HighlightedScript::new(self, script_theme) } pub fn lines_numbered(&self) -> impl Iterator { self.0.lines().enumerate() } pub async fn lint(&self, mut cmd: Command) -> Result<(ExitStatus, String, String)> { use tokio::io::AsyncWriteExt; use tokio::io::BufWriter; let mut child = cmd .stderr(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped()) .spawn() .context("Spawning subprocess for linting package script")?; trace!("Child = {:?}", child); { let stdin = child.stdin.take().ok_or_else(|| anyhow!("No stdin"))?; let mut writer = BufWriter::new(stdin); let _ = writer .write_all(self.0.as_bytes()) .await .context("Writing package script to STDIN of subprocess")?; let _ = writer .flush() .await .context("Flushing STDIN of subprocess")?; trace!("Script written"); } trace!("Waiting for child..."); let out = child.wait_with_output() .await .context("Waiting for subprocess")?; Ok((out.status, String::from_utf8(out.stdout)?, String::from_utf8(out.stderr)?)) } } #[derive(Debug)] pub struct HighlightedScript<'a> { script: &'a Script, script_theme: &'a str, ps: SyntaxSet, ts: ThemeSet, } impl<'a> HighlightedScript<'a> { fn new(script: &'a Script, script_theme: &'a str) -> Self { HighlightedScript { script, script_theme, ps: SyntaxSet::load_defaults_newlines(), ts: ThemeSet::load_defaults(), } } pub fn lines(&'a self) -> Result + 'a> { let syntax = self.ps .find_syntax_by_first_line(&self.script.0) .ok_or_else(|| anyhow!("Failed to load syntax for highlighting script"))?; let theme = self.ts .themes .get(self.script_theme) .ok_or_else(|| anyhow!("Theme not available: {}", self.script_theme))?; let mut h = HighlightLines::new(syntax, &theme); Ok({ LinesWithEndings::from(&self.script.0) .map(move |line| { let ranges: Vec<(Style, &str)> = h.highlight(line, &self.ps); as_24_bit_terminal_escaped(&ranges[..], true) }) }) } pub fn lines_numbered(&'a self) -> Result + 'a> { self.lines().map(|iter| iter.enumerate()) } } impl From for Shebang { fn from(s: String) -> Self { Shebang(s) } } impl AsRef for Script { fn as_ref(&self) -> &str { self.0.as_ref() } } pub struct ScriptBuilder<'a> { shebang: &'a Shebang, } impl<'a> ScriptBuilder<'a> { pub fn new(shebang: &'a Shebang) -> Self { ScriptBuilder { shebang, } } pub fn build(self, package: &Package, phaseorder: &Vec, strict_mode: bool) -> Result