Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix code snippets coloring macOS and Linux #7495

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

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

4 changes: 3 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 @@ -86,6 +86,7 @@ open = "5.1.0"
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_ansi_color = "0.6.9"
r3bl_rs_utils_core = "0.9.12"
r3bl_tui = "0.5.2"
r3bl_tuify = "0.1.25"
Expand All @@ -103,6 +104,7 @@ serde_yaml = "0.9"
shellexpand = { version = "3.1.0", default-features = false, features = ["base-0"] }
strip-ansi-escapes = "0.2.0"
syntect = { version = "5.2.0", default-features = false, features = ["default-syntaxes", "regex-onig"] }
termbg = "0"
thiserror = "1"
time = { version = "0.3", default-features = false, features = ["std", "local-offset"] }
tiny_http = "0.12.0"
Expand Down
195 changes: 135 additions & 60 deletions implementations/rust/ockam/ockam_command/src/docs.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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 std::time::Duration;
use syntect::{
easy::HighlightLines,
highlighting::{Style, Theme, ThemeSet},
highlighting::{Style, Theme as SyntectTheme, ThemeSet},
parsing::Regex,
parsing::SyntaxSet,
util::{as_24_bit_terminal_escaped, LinesWithEndings},
util::LinesWithEndings,
};

const FOOTER: &str = "
Expand All @@ -26,15 +27,25 @@ discord channel https://discord.ockam.io

static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
static HEADER_RE: Lazy<Regex> = Lazy::new(|| Regex::new("^[A-Za-z][A-Za-z0-9 ]+:$".into()));
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,
};
static THEME: Lazy<Option<SyntectTheme>> = Lazy::new(|| {
let mut theme_set = ThemeSet::load_defaults();
let theme = theme_set.themes.remove(theme_name).unwrap();
Some(theme)
match termbg::theme(Duration::from_millis(100)) {
Ok(termbg::Theme::Light) => theme_set.themes.remove("base16-ocean.light"),
Ok(termbg::Theme::Dark) => theme_set.themes.remove("base16-ocean.dark"),
Err(_) => None,
}
});
static DEFAULT_THEME: Lazy<SyntectTheme> = Lazy::new(|| {
let mut theme_set = ThemeSet::load_defaults();
theme_set.themes.remove("base16-ocean.dark").unwrap_or(
theme_set.themes.remove("base16-ocean.light").unwrap_or(
theme_set
.themes
.pop_first()
.map(|(_, theme)| theme)
.unwrap_or_default(),
),
)
});

fn is_markdown() -> bool {
Expand Down Expand Up @@ -74,7 +85,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,76 +97,88 @@ 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.as_str()) {
// TODO: fix the fenced code block highlighter (currently disabled) - then use _code_highlighter here
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);

// 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("")
}

struct FencedCodeBlockHighlighter<'a> {
inner: Option<HighlightLines<'a>>,
inner: HighlightLines<'a>,
in_fenced_block: bool,
}

impl FencedCodeBlockHighlighter<'_> {
fn new() -> Self {
let inner = match &*THEME {
Some(theme) => {
let syntax = SYNTAX_SET.find_syntax_by_extension("sh").unwrap();
Some(HighlightLines::new(syntax, theme))
}
None => None,
};
let syntax = SYNTAX_SET.find_syntax_by_extension("sh").unwrap();
let theme = THEME.as_ref().unwrap_or(&DEFAULT_THEME);
Self {
inner,
inner: HighlightLines::new(syntax, theme),
in_fenced_block: false,
}
}

// 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" {
self.in_fenced_block = true;
return true;
}
if line == "```sh\n" {
self.in_fenced_block = true;
return true;
}

if !self.in_fenced_block {
return false;
}
if !self.in_fenced_block {
return false;
}

if line == "```\n" {
// Push a reset to clear the coloring.
output.push("\x1b[0m".to_string());
self.in_fenced_block = false;
return true;
}
if line == "```\n" {
// Push a reset to clear the coloring.
output.push("\x1b[0m".to_string());
self.in_fenced_block = false;
return true;
}

// Highlight the code line
let ranges: Vec<(Style, &str)> = highlighter
.highlight_line(line, &SYNTAX_SET)
.unwrap_or_default();
output.push(as_24_bit_terminal_escaped(&ranges[..], false));
true
} else {
false
// Highlight the code line
let ranges: Vec<(Style, &str)> = self
.inner
.highlight_line(line, &SYNTAX_SET)
.unwrap_or_default();

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

true
}

/// 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());
}
}
}
Expand All @@ -174,3 +197,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."
);
}
}
Loading