Skip to content

Commit

Permalink
feat(rust): add syntax highlighting for command's fenced code blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
YorickdeJong authored and adrianbenavides committed Feb 13, 2024
1 parent d71ae9f commit 72ea4bf
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion implementations/rust/ockam/ockam_command/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dockerfile = "../../../../tools/cross/Cross.Dockerfile.armv7"
# `../ockam`) and the `ockam` binary (in `./src/bin/ockam.rs`). I won't
# enumerate them here, but an example: `rustdoc` will try to place the docs for
# both of these in the same path, without realizing it, which may result in one
# overwriting the other)
# overwriting the other
#
# Anyway a result, we disable them for the binary crate, which is just a single
# file (`src/bin/ockam.rs`) which contains a single function call into
Expand Down Expand Up @@ -87,6 +87,7 @@ opentelemetry = { version = "0.21.0", features = ["metrics", "trace"] }
opentelemetry-otlp = { version = "0.14.0", features = ["metrics", "tls", "logs", "trace"], default-features = false }
pem-rfc7468 = { version = "0.7.0", features = ["std"] }
r3bl_rs_utils_core = "0.9.12"
r3bl_ansi_color = "0.6.9"
r3bl_tui = "0.5.2"
r3bl_tuify = "0.1.25"
rand = "0.8"
Expand Down
115 changes: 94 additions & 21 deletions implementations/rust/ockam/ockam_command/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use crate::terminal::TerminalBackground;
use colorful::Colorful;
use ockam_core::env::get_env_with_default;
use once_cell::sync::Lazy;
use r3bl_ansi_color::{AnsiStyledText, Color, Style as StyleAnsi};
use syntect::{
easy::HighlightLines,
highlighting::{Style, Theme, ThemeSet},
parsing::Regex,
parsing::SyntaxSet,
util::{as_24_bit_terminal_escaped, LinesWithEndings},
util::LinesWithEndings,
};

const FOOTER: &str = "
Expand All @@ -30,7 +31,7 @@ static THEME: Lazy<Option<Theme>> = Lazy::new(|| {
let theme_name = match TerminalBackground::detect_background_color() {
TerminalBackground::Light => "base16-ocean.light",
TerminalBackground::Dark => "base16-ocean.dark",
TerminalBackground::Unknown => return None,
TerminalBackground::Unknown => "base16-ocean.dark", //return None,
};
let mut theme_set = ThemeSet::load_defaults();
let theme = theme_set.themes.remove(theme_name).unwrap();
Expand Down Expand Up @@ -74,7 +75,7 @@ pub(crate) fn after_help(text: &str) -> &'static str {

/// Render the string if the document should be displayed in a terminal
/// Otherwise, if it is a Markdown document just return a static string
pub(crate) fn render(body: &str) -> &'static str {
pub fn render(body: &str) -> &'static str {
if is_markdown() {
Box::leak(body.to_string().into_boxed_str())
} else {
Expand All @@ -86,23 +87,26 @@ pub(crate) fn render(body: &str) -> &'static str {
/// Use a shell syntax highlighter to render the fenced code blocks in terminals
fn process_terminal_docs(input: String) -> String {
let mut output: Vec<String> = Vec::new();
let mut code_highlighter = FencedCodeBlockHighlighter::new();

let mut _code_highlighter = FencedCodeBlockHighlighter::new();
for line in LinesWithEndings::from(&input) {
// Check if the current line is a code block start/end or content.
let is_code_line = code_highlighter.process_line(line, &mut output);

for line in LinesWithEndings::from(input.as_str()) {
// TODO: fix the fenced code block highlighter (currently disabled) - then use _code_highlighter here

// Replace headers with bold and underline text
if HEADER_RE.is_match(line) {
output.push(line.to_string().bold().underlined().to_string());
}
// Replace subheaders with underlined text
else if line.starts_with("#### ") {
output.push(line.replace("#### ", "").underlined().to_string());
}
// Catch all
else {
output.push(line.to_string());
// The line is not part of a code block, so process normally.
if !is_code_line {
// Replace headers with bold and underline text
if HEADER_RE.is_match(line) {
output.push(line.to_string().bold().underlined().to_string());
}
// Replace subheaders with underlined text
else if line.starts_with("#### ") {
output.push(line.replace("#### ", "").underlined().to_string());
}
// No processing
else {
output.push(line.to_string());
}
}
}
output.join("")
Expand All @@ -128,8 +132,6 @@ impl FencedCodeBlockHighlighter<'_> {
}
}

// TODO: fix the fenced code block highlighter, as it does not work on macOS or Linux
#[allow(dead_code)]
fn process_line(&mut self, line: &str, output: &mut Vec<String>) -> bool {
if let Some(highlighter) = &mut self.inner {
if line == "```sh\n" {
Expand All @@ -152,12 +154,31 @@ impl FencedCodeBlockHighlighter<'_> {
let ranges: Vec<(Style, &str)> = highlighter
.highlight_line(line, &SYNTAX_SET)
.unwrap_or_default();
output.push(as_24_bit_terminal_escaped(&ranges[..], false));

// Convert each syntect range to an ANSI styled string
Self::convert_syntect_style_to_ansi(output, &ranges);

true
} else {
false
}
}

/// Convert a vector of syntect ranges to ANSI styled strings
fn convert_syntect_style_to_ansi(output: &mut Vec<String>, ranges: &Vec<(Style, &str)>) {
for (style, text) in ranges {
let ansi_styled_text = AnsiStyledText {
text,
style: &[StyleAnsi::Foreground(Color::Rgb(
style.foreground.r,
style.foreground.g,
style.foreground.b,
))],
};

output.push(ansi_styled_text.to_string());
}
}
}

const PREVIEW_TOOLTIP_TEXT: &str = include_str!("./static/preview_tooltip.txt");
Expand All @@ -174,3 +195,55 @@ fn enrich_preview_tag(text: &str) -> String {
let container = format!("<div class=\"chip t\">{}{}</div>", preview, tooltip);
text.replace("[Preview]", &container)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_syntax_highlighting() {
let mut highlighter = FencedCodeBlockHighlighter::new();
let mut output = Vec::new();

// Simulate the start of a code block
assert!(highlighter.process_line("```sh\n", &mut output));

// Simulate processing a line of code within the code block
let code_line = "echo \"Hello, world!\"\n";
let highlighted = highlighter.process_line(code_line, &mut output);

// We expect this line to be processed (highlighted)
assert!(highlighted);

// The output should contain the syntax highlighted version of the code line
// This is a simplistic check for ANSI escape codes - your actual check might be more complex
assert!(output.last().unwrap().contains("\x1b["));

// Simulate the end of a code block
assert!(highlighter.process_line("```\n", &mut output));

// Check that the highlighting is reset at the end
assert!(output.last().unwrap().contains("\x1b[0m"));
}

#[test]
fn test_process_terminal_docs_with_code_blocks() {
let input = "```sh
# To enroll a known identity
$ ockam project ticket --member id_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."
);
}
}
2 changes: 1 addition & 1 deletion implementations/rust/ockam/ockam_command/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ mod authority;
mod completion;
mod configuration;
mod credential;
mod docs;
pub mod docs;
pub mod enroll;
mod environment;
pub mod error;
Expand Down

0 comments on commit 72ea4bf

Please sign in to comment.