// // Copyright (c) 2020-2022 science+computing ag and other contributors // // This program and the accompanying materials are made // available under the terms of the Eclipse Public License 2.0 // which is available at https://www.eclipse.org/legal/epl-2.0/ // // SPDX-License-Identifier: EPL-2.0 // use std::process::ExitStatus; use anyhow::anyhow; use anyhow::Context as AnyhowContext; use anyhow::Error; use anyhow::Result; use handlebars::{ Context, Handlebars, Helper, HelperDef, HelperResult, JsonRender, Output, PathAndJson, RenderContext, RenderError, }; use log::trace; use serde::Deserialize; use serde::Serialize; use syntect::easy::HighlightLines; use syntect::highlighting::{Style, ThemeSet}; 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().map(|(n, l)| (n + 1, l)) } 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().map(|(n, l)| (n + 1, l))) } } 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: &[PhaseName], strict_mode: bool, ) -> Result