feat: support hidden code lines (#254)

This PR adds support for hidden code lines for the languages Rust, Python, Shell and Bash using prefixes at the start of the line, particular to each language. Support for more languages can be added in future.

The hidden code lines will not be visible in snippets in the presentation, but still be evaluated as normal if executed.
This commit is contained in:
dmackdev 2024-07-08 15:29:34 +01:00 committed by GitHub
parent 99bafe8aea
commit 480f9475cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 137 additions and 5 deletions

View File

@ -65,11 +65,11 @@ impl CodeExecutor {
match &code.language {
CodeLanguage::Shell(interpreter) => {
let args: &[&str] = &[];
Self::execute_shell(interpreter, code.contents.as_bytes(), args)
Self::execute_shell(interpreter, code.executable_contents().as_bytes(), args)
}
lang => {
let executor = self.executor(lang).ok_or(CodeExecuteError::UnsupportedExecution)?;
Self::execute_lang(executor, code.contents.as_bytes())
Self::execute_lang(executor, code.executable_contents().as_bytes())
}
}
}
@ -295,6 +295,32 @@ echo 'hello world'
assert_eq!(state.output, expected_lines);
}
#[test]
fn shell_code_execution_executes_hidden_lines() {
let contents = r"
/// echo 'this line was hidden'
/// echo 'this line was hidden and contains another prefix /// '
echo 'hello world'
"
.into();
let code = Code {
contents,
language: CodeLanguage::Shell("sh".into()),
attributes: CodeAttributes { execute: true, ..Default::default() },
};
let handle = CodeExecutor::default().execute(&code).expect("execution failed");
let state = loop {
let state = handle.state();
if state.status.is_finished() {
break state;
}
};
let expected_lines =
vec!["this line was hidden", "this line was hidden and contains another prefix /// ", "hello world"];
assert_eq!(state.output, expected_lines);
}
#[test]
fn custom_executor() {
let dir = tempdir().unwrap();

View File

@ -1,5 +1,5 @@
use crate::style::TextStyle;
use std::{convert::Infallible, iter, ops::Range, path::PathBuf, str::FromStr};
use std::{convert::Infallible, fmt::Write, iter, ops::Range, path::PathBuf, str::FromStr};
use strum::EnumIter;
use unicode_width::UnicodeWidthStr;
@ -185,6 +185,25 @@ pub(crate) struct Code {
pub(crate) attributes: CodeAttributes,
}
impl Code {
pub(crate) fn visible_lines(&self) -> impl Iterator<Item = &str> {
let prefix = self.language.hidden_line_prefix();
self.contents.lines().filter(move |line| !prefix.is_some_and(|prefix| line.starts_with(prefix)))
}
pub(crate) fn executable_contents(&self) -> String {
if let Some(prefix) = self.language.hidden_line_prefix() {
self.contents.lines().fold(String::new(), |mut output, line| {
let line = line.strip_prefix(prefix).unwrap_or(line);
let _ = writeln!(output, "{line}");
output
})
} else {
self.contents.to_owned()
}
}
}
/// The language of a piece of code.
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord)]
pub enum CodeLanguage {
@ -250,6 +269,14 @@ impl CodeLanguage {
pub(crate) fn supports_auto_render(&self) -> bool {
matches!(self, Self::Latex | Self::Typst | Self::Mermaid)
}
pub(crate) fn hidden_line_prefix(&self) -> Option<&'static str> {
match self {
CodeLanguage::Rust => Some("# "),
CodeLanguage::Python | CodeLanguage::Shell(_) | CodeLanguage::Bash => Some("/// "),
_ => None,
}
}
}
impl FromStr for CodeLanguage {
@ -395,3 +422,82 @@ impl Table {
/// A table row.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TableRow(pub(crate) Vec<TextBlock>);
#[cfg(test)]
mod test {
use super::*;
#[test]
fn code_visible_lines_bash() {
let contents = r"echo 'hello world'
/// echo 'this was hidden'
echo '/// is the prefix'
/// echo 'the prefix is /// '
echo 'hello again'
"
.to_string();
let expected = vec!["echo 'hello world'", "", "echo '/// is the prefix'", "echo 'hello again'"];
let code = Code { contents, language: CodeLanguage::Bash, attributes: Default::default() };
assert_eq!(expected, code.visible_lines().collect::<Vec<_>>());
}
#[test]
fn code_visible_lines_rust() {
let contents = r##"# fn main() {
println!("Hello world");
# // The prefix is # .
# }
"##
.to_string();
let expected = vec!["println!(\"Hello world\");"];
let code = Code { contents, language: CodeLanguage::Rust, attributes: Default::default() };
assert_eq!(expected, code.visible_lines().collect::<Vec<_>>());
}
#[test]
fn code_executable_contents_bash() {
let contents = r"echo 'hello world'
/// echo 'this was hidden'
echo '/// is the prefix'
/// echo 'the prefix is /// '
echo 'hello again'
"
.to_string();
let expected = r"echo 'hello world'
echo 'this was hidden'
echo '/// is the prefix'
echo 'the prefix is /// '
echo 'hello again'
"
.to_string();
let code = Code { contents, language: CodeLanguage::Bash, attributes: Default::default() };
assert_eq!(expected, code.executable_contents());
}
#[test]
fn code_executable_contents_rust() {
let contents = r##"# fn main() {
println!("Hello world");
# // The prefix is # .
# }
"##
.to_string();
let expected = r##"fn main() {
println!("Hello world");
// The prefix is # .
}
"##
.to_string();
let code = Code { contents, language: CodeLanguage::Rust, attributes: Default::default() };
assert_eq!(expected, code.executable_contents());
}
}

View File

@ -42,8 +42,8 @@ impl<'a> CodePreparer<'a> {
}
let padding = " ".repeat(horizontal_padding as usize);
let padder = NumberPadder::new(code.contents.lines().count());
for (index, line) in code.contents.lines().enumerate() {
let padder = NumberPadder::new(code.visible_lines().count());
for (index, line) in code.visible_lines().enumerate() {
let mut line = line.to_string();
let mut prefix = padding.clone();
if code.attributes.line_numbers {