use crate::branding::{BrandingCompileEnvVars, OUTPUT_BRANDING};
use crate::Result;
use colorful::Colorful;
use ockam_api::terminal::TextHighlighter;
use ockam_core::env::get_env_with_default;
use once_cell::sync::Lazy;
use syntect::{parsing::Regex, util::LinesWithEndings};
const PREVIEW_TOOLTIP_TEXT: &str = include_str!("./static/preview_tooltip.txt");
const PREVIEW_TAG: &str = include_str!("./static/preview_tag.txt");
const UNSAFE_TOOLTIP_TEXT: &str = include_str!("./static/unsafe_tooltip.txt");
const UNSAFE_TAG: &str = include_str!("./static/unsafe_tag.txt");
static IS_MARKDOWN: Lazy<bool> =
Lazy::new(|| get_env_with_default("OCKAM_HELP_RENDER_MARKDOWN", false).unwrap_or(false));
static HIDE: Lazy<bool> =
Lazy::new(|| get_env_with_default("OCKAM_HELP_SHOW_HIDDEN", true).unwrap_or(true));
static HEADER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(Examples:|Learn More:|Feedback:).*$".into()));
static FOOTER: Lazy<String> = Lazy::new(|| {
if BrandingCompileEnvVars::bin_name() == "ockam" {
"
Learn More:
Use 'ockam <SUBCOMMAND> --help' for more information about a subcommand.
Where <SUBCOMMAND> might be: 'node', 'status', 'enroll', etc.
Learn more about Command: https://command.ockam.io/manual/
Learn more about Ockam: https://docs.ockam.io/reference/command
Feedback:
If you have questions, as you explore, join us on the contributors
discord channel https://discord.ockam.io"
.to_string()
} else {
format!(
"
Learn More:
Use 'ockam <SUBCOMMAND> --help' for more information about a subcommand.
Where <SUBCOMMAND> might be: 'node', 'status', 'enroll', etc.
Feedback:
If you have questions, please email us on {}",
BrandingCompileEnvVars::support_email()
)
}
});
pub(crate) fn hide() -> bool {
*HIDE
}
pub(crate) fn about(text: &str) -> &'static str {
render(text)
}
pub(crate) fn before_help(text: &str) -> &'static str {
let mut processed = String::new();
if *IS_MARKDOWN {
if let Some(s) = enrich_preview_tag(text) {
processed.push_str(&s);
}
if let Some(s) = enrich_unsafe_tag(text) {
processed.push_str(&s);
}
} else {
processed.push_str(text);
}
render(processed.as_str())
}
pub(crate) fn after_help(text: &str) -> &'static str {
let mut processed = String::new();
if *IS_MARKDOWN {
processed.push_str("### Examples\n\n");
processed.push_str(text);
} else {
processed.push_str("Examples:\n\n");
processed.push_str(text);
processed.push_str(&FOOTER);
}
render(processed.as_str())
}
fn render(body: &str) -> &'static str {
let body = OUTPUT_BRANDING.replace(body);
if *IS_MARKDOWN {
Box::leak(body.into_boxed_str())
} else {
let syntax_highlighted = process_terminal_docs(body);
Box::leak(syntax_highlighted.into_boxed_str())
}
}
fn process_terminal_docs(input: String) -> String {
let mut output: Vec<String> = Vec::new();
for line in LinesWithEndings::from(&input) {
if HEADER_RE.is_match(line) {
output.push(line.bold().underlined().to_string());
}
else if line.starts_with("#### ") {
output.push(line.replace("#### ", "").underlined().to_string());
}
else if line.starts_with("##### ") {
output.push(line.replace("##### ", "").to_string());
}
else {
output.push(line.to_string());
}
}
output.join("")
}
struct FencedCodeBlockHighlighter<'a> {
inner: TextHighlighter<'a>,
in_fenced_block: bool,
}
#[allow(dead_code)]
impl FencedCodeBlockHighlighter<'_> {
fn new() -> Result<Self> {
Ok(Self {
inner: TextHighlighter::new("sh")?,
in_fenced_block: false,
})
}
fn in_fenced_block(&mut self, line: &str) -> bool {
if line.contains("```") {
self.in_fenced_block = !self.in_fenced_block;
}
self.in_fenced_block
}
fn highlight(&mut self, line: &str) -> Result<String> {
Ok(self.inner.process(line)?)
}
}
fn enrich_tag(text: &str, tag: &str, tooltip_text: &str, display_text: &str) -> Option<String> {
if !text.contains(tag) {
return None;
}
let mut tooltip = String::new();
for line in tooltip_text.trim_end().lines() {
tooltip.push_str(&format!("<p>{}</p>", line));
}
tooltip = format!("<div class=\"tt\">{tooltip}</div>");
let container = format!(
"<div class=\"chip t\"><b>{}</b>{}</div>\n",
display_text, tooltip
);
Some(text.replace(tag, &container))
}
fn enrich_preview_tag(text: &str) -> Option<String> {
enrich_tag(text, PREVIEW_TAG, PREVIEW_TOOLTIP_TEXT, "Preview")
}
fn enrich_unsafe_tag(text: &str) -> Option<String> {
enrich_tag(text, UNSAFE_TAG, UNSAFE_TOOLTIP_TEXT, "Unsafe")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syntax_highlighting() {
let mut highlighter = FencedCodeBlockHighlighter::new().unwrap();
assert!(highlighter.in_fenced_block("```sh\n"));
let line = highlighter.highlight("echo \"Hello, world!\"\n").unwrap();
assert!(line.contains("\x1b[38;2;150;181;180m")); assert!(line.contains("\x1b[38;2;192;197;206m"));
assert!(!highlighter.in_fenced_block("```\n"));
}
#[ignore = "The highlighting is disabled"]
#[test]
fn test_process_terminal_docs_with_code_blocks() {
let input = "```sh
# To enroll a known identity
$ ockam project-member add identifier
# To generate an enrollment ticket that can be used to enroll a device
$ ockam project ticket --attribute component=control
```";
let result = render(input);
assert!(
result.contains("\x1b["),
"The output should contain ANSI escape codes."
);
assert!(
result.contains("\x1b[0m"),
"The output should reset ANSI coloring at the end."
);
}
}