use super::{
highlighting::{LanguageHighlighter, StyledTokens},
padding::NumberPadder,
};
use crate::{
markdown::{
elements::{Percent, PercentParseError},
text::{WeightedLine, WeightedText},
text_style::{Color, TextStyle},
},
presentation::ChunkMutator,
render::{
operation::{AsRenderOperations, BlockLine, RenderOperation},
properties::WindowSize,
},
theme::{Alignment, CodeBlockStyle},
};
use serde::Deserialize;
use std::{cell::RefCell, convert::Infallible, fmt::Write, ops::Range, path::PathBuf, rc::Rc, str::FromStr};
use strum::{EnumDiscriminants, EnumIter};
use unicode_width::UnicodeWidthStr;
pub(crate) struct SnippetSplitter<'a> {
style: &'a CodeBlockStyle,
hidden_line_prefix: Option<&'a str>,
}
impl<'a> SnippetSplitter<'a> {
pub(crate) fn new(style: &'a CodeBlockStyle, hidden_line_prefix: Option<&'a str>) -> Self {
Self { style, hidden_line_prefix }
}
pub(crate) fn split(&self, code: &Snippet) -> Vec<SnippetLine> {
let mut lines = Vec::new();
let horizontal_padding = self.style.padding.horizontal;
let vertical_padding = self.style.padding.vertical;
if vertical_padding > 0 {
lines.push(SnippetLine::empty());
}
self.push_lines(code, horizontal_padding, &mut lines);
if vertical_padding > 0 {
lines.push(SnippetLine::empty());
}
lines
}
fn push_lines(&self, code: &Snippet, horizontal_padding: u8, lines: &mut Vec<SnippetLine>) {
if code.contents.is_empty() {
return;
}
let padding = " ".repeat(horizontal_padding as usize);
let padder = NumberPadder::new(code.visible_lines(self.hidden_line_prefix).count());
for (index, line) in code.visible_lines(self.hidden_line_prefix).enumerate() {
let mut line = line.replace('\t', " ");
let mut prefix = padding.clone();
if code.attributes.line_numbers {
let line_number = index + 1;
prefix.push_str(&padder.pad_right(line_number));
prefix.push(' ');
}
line.push('\n');
let line_number = Some(index as u16 + 1);
lines.push(SnippetLine { prefix, code: line, right_padding_length: padding.len() as u16, line_number });
}
}
}
pub(crate) struct SnippetLine {
pub(crate) prefix: String,
pub(crate) code: String,
pub(crate) right_padding_length: u16,
pub(crate) line_number: Option<u16>,
}
impl SnippetLine {
pub(crate) fn empty() -> Self {
Self { prefix: String::new(), code: "\n".into(), right_padding_length: 0, line_number: None }
}
pub(crate) fn width(&self) -> usize {
self.prefix.width() + self.code.width() + self.right_padding_length as usize
}
pub(crate) fn highlight(
&self,
code_highlighter: &mut LanguageHighlighter,
block_style: &CodeBlockStyle,
font_size: u8,
) -> WeightedLine {
let mut line = code_highlighter.style_line(&self.code, block_style);
line.apply_style(&TextStyle::default().size(font_size));
line.into()
}
pub(crate) fn dim(&self, dim_style: &TextStyle) -> WeightedLine {
let output = vec![StyledTokens { style: *dim_style, tokens: &self.code }.apply_style()];
output.into()
}
pub(crate) fn dim_prefix(&self, dim_style: &TextStyle) -> WeightedText {
let text = StyledTokens { style: *dim_style, tokens: &self.prefix }.apply_style();
text.into()
}
}
#[derive(Debug)]
pub(crate) struct HighlightContext {
pub(crate) groups: Vec<HighlightGroup>,
pub(crate) current: usize,
pub(crate) block_length: u16,
pub(crate) alignment: Alignment,
}
#[derive(Debug)]
pub(crate) struct HighlightedLine {
pub(crate) prefix: WeightedText,
pub(crate) right_padding_length: u16,
pub(crate) highlighted: WeightedLine,
pub(crate) not_highlighted: WeightedLine,
pub(crate) line_number: Option<u16>,
pub(crate) context: Rc<RefCell<HighlightContext>>,
pub(crate) block_color: Option<Color>,
}
impl AsRenderOperations for HighlightedLine {
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
let context = self.context.borrow();
let group = &context.groups[context.current];
let needs_highlight = self.line_number.map(|number| group.contains(number)).unwrap_or_default();
let text = match needs_highlight {
true => self.highlighted.clone(),
false => self.not_highlighted.clone(),
};
vec![
RenderOperation::RenderBlockLine(BlockLine {
prefix: self.prefix.clone(),
right_padding_length: self.right_padding_length,
repeat_prefix_on_wrap: false,
text,
block_length: context.block_length,
alignment: context.alignment,
block_color: self.block_color,
}),
RenderOperation::RenderLineBreak,
]
}
}
#[derive(Debug)]
pub(crate) struct HighlightMutator {
context: Rc<RefCell<HighlightContext>>,
}
impl HighlightMutator {
pub(crate) fn new(context: Rc<RefCell<HighlightContext>>) -> Self {
Self { context }
}
}
impl ChunkMutator for HighlightMutator {
fn mutate_next(&self) -> bool {
let mut context = self.context.borrow_mut();
if context.current == context.groups.len() - 1 {
false
} else {
context.current += 1;
true
}
}
fn mutate_previous(&self) -> bool {
let mut context = self.context.borrow_mut();
if context.current == 0 {
false
} else {
context.current -= 1;
true
}
}
fn reset_mutations(&self) {
self.context.borrow_mut().current = 0;
}
fn apply_all_mutations(&self) {
let mut context = self.context.borrow_mut();
context.current = context.groups.len() - 1;
}
fn mutations(&self) -> (usize, usize) {
let context = self.context.borrow();
(context.current, context.groups.len())
}
}
pub(crate) type ParseResult<T> = Result<T, SnippetBlockParseError>;
pub(crate) struct SnippetParser;
impl SnippetParser {
pub(crate) fn parse(info: String, code: String) -> ParseResult<Snippet> {
let (language, attributes) = Self::parse_block_info(&info)?;
let code = Snippet { contents: code, language, attributes };
Ok(code)
}
fn parse_block_info(input: &str) -> ParseResult<(SnippetLanguage, SnippetAttributes)> {
let (language, input) = Self::parse_language(input);
let attributes = Self::parse_attributes(input)?;
if attributes.width.is_some() && !matches!(attributes.execution, SnippetExecution::Render) {
return Err(SnippetBlockParseError::NotRenderSnippet("width"));
}
Ok((language, attributes))
}
fn parse_language(input: &str) -> (SnippetLanguage, &str) {
let token = Self::next_identifier(input);
let language = token.parse().expect("language parsing");
let rest = &input[token.len()..];
(language, rest)
}
fn parse_attributes(mut input: &str) -> ParseResult<SnippetAttributes> {
let mut attributes = SnippetAttributes::default();
let mut processed_attributes = Vec::new();
while let (Some(attribute), rest) = Self::parse_attribute(input)? {
let discriminant = SnippetAttributeDiscriminants::from(&attribute);
if processed_attributes.contains(&discriminant) {
return Err(SnippetBlockParseError::DuplicateAttribute("duplicate attribute"));
}
use SnippetAttribute::*;
match attribute {
LineNumbers => attributes.line_numbers = true,
Exec(spec) => {
attributes.execution = attributes
.execution
.try_merge(SnippetExecution::Exec(SnippetExecArgs { spec, ..Default::default() }))?;
}
AutoExec(spec) => {
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
spec,
auto: true,
..Default::default()
}))?;
}
ExecPty(spec, args) => {
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
spec,
pty: Some(args),
..Default::default()
}))?;
}
ExecReplace(spec) => {
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
spec,
repr: SnippetRepr::ExecReplace,
..Default::default()
}))?;
}
Id(id) => {
attributes.id = Some(id);
}
Validate(spec) => {
if attributes.validate.is_some() {
return Err(SnippetBlockParseError::DuplicateAttribute("+validate"));
}
attributes.validate = Some(spec);
}
Image => {
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
repr: SnippetRepr::Image,
..Default::default()
}))?;
}
Render => {
attributes.execution = attributes.execution.try_merge(SnippetExecution::Render)?;
}
AcquireTerminal(spec) => {
attributes.execution = attributes.execution.try_merge(SnippetExecution::Exec(SnippetExecArgs {
spec,
repr: SnippetRepr::AcquireTerminal,
..Default::default()
}))?;
}
NoBackground => attributes.no_background = true,
HighlightedLines(lines) => attributes.highlight_groups = lines,
Width(width) => attributes.width = Some(width),
ExpectedExecutionResult(result) => attributes.expected_execution_result = result,
};
processed_attributes.push(discriminant);
input = rest;
}
if attributes.highlight_groups.is_empty() {
attributes.highlight_groups.push(HighlightGroup::new(vec![Highlight::All]));
}
Ok(attributes)
}
fn parse_attribute(input: &str) -> ParseResult<(Option<SnippetAttribute>, &str)> {
let input = Self::skip_whitespace(input);
let (attribute, input) = match input.chars().next() {
Some('+') => {
let token = Self::next_identifier(&input[1..]);
let attribute = match token {
"line_numbers" => SnippetAttribute::LineNumbers,
"exec" => SnippetAttribute::Exec(SnippetExecutorSpec::default()),
"auto_exec" => SnippetAttribute::AutoExec(SnippetExecutorSpec::default()),
"exec_replace" => SnippetAttribute::ExecReplace(SnippetExecutorSpec::default()),
"validate" => SnippetAttribute::Validate(SnippetExecutorSpec::default()),
"image" => SnippetAttribute::Image,
"render" => SnippetAttribute::Render,
"no_background" => SnippetAttribute::NoBackground,
"acquire_terminal" => SnippetAttribute::AcquireTerminal(SnippetExecutorSpec::default()),
"pty" => SnippetAttribute::ExecPty(SnippetExecutorSpec::default(), Default::default()),
other => {
let (attribute, parameter) = other
.split_once(':')
.ok_or_else(|| SnippetBlockParseError::InvalidToken(Self::next_identifier(input).into()))?;
match attribute {
"exec" => SnippetAttribute::Exec(SnippetExecutorSpec::Alternative(parameter.to_string())),
"auto_exec" => {
SnippetAttribute::AutoExec(SnippetExecutorSpec::Alternative(parameter.to_string()))
}
"exec_replace" => {
SnippetAttribute::ExecReplace(SnippetExecutorSpec::Alternative(parameter.to_string()))
}
"id" => SnippetAttribute::Id(parameter.to_string()),
"validate" => {
SnippetAttribute::Validate(SnippetExecutorSpec::Alternative(parameter.to_string()))
}
"acquire_terminal" => SnippetAttribute::AcquireTerminal(SnippetExecutorSpec::Alternative(
parameter.to_string(),
)),
"width" => {
let width = parameter.parse().map_err(SnippetBlockParseError::InvalidWidth)?;
SnippetAttribute::Width(width)
}
"expect" => match parameter {
"success" => {
SnippetAttribute::ExpectedExecutionResult(ExpectedSnippetExecutionResult::Success)
}
"failure" | "fail" => {
SnippetAttribute::ExpectedExecutionResult(ExpectedSnippetExecutionResult::Failure)
}
_ => {
return Err(SnippetBlockParseError::InvalidToken(
Self::next_identifier(input).into(),
));
}
},
"pty" => SnippetAttribute::ExecPty(SnippetExecutorSpec::default(), parameter.parse()?),
_ => return Err(SnippetBlockParseError::InvalidToken(Self::next_identifier(input).into())),
}
}
};
(Some(attribute), &input[token.len() + 1..])
}
Some('{') => {
let (lines, input) = Self::parse_highlight_groups(&input[1..])?;
(Some(SnippetAttribute::HighlightedLines(lines)), input)
}
Some(_) => return Err(SnippetBlockParseError::InvalidToken(Self::next_identifier(input).into())),
None => (None, input),
};
Ok((attribute, input))
}
fn parse_highlight_groups(input: &str) -> ParseResult<(Vec<HighlightGroup>, &str)> {
use SnippetBlockParseError::InvalidHighlightedLines;
let Some((head, tail)) = input.split_once('}') else {
return Err(InvalidHighlightedLines("no enclosing '}'".into()));
};
let head = head.trim();
if head.is_empty() {
return Ok((Vec::new(), tail));
}
let mut highlight_groups = Vec::new();
for group in head.split('|') {
let group = Self::parse_highlight_group(group)?;
highlight_groups.push(group);
}
Ok((highlight_groups, tail))
}
fn parse_highlight_group(input: &str) -> ParseResult<HighlightGroup> {
let mut highlights = Vec::new();
for piece in input.split(',') {
let piece = piece.trim();
if piece == "all" {
highlights.push(Highlight::All);
continue;
}
match piece.split_once('-') {
Some((left, right)) => {
let left = Self::parse_number(left)?;
let right = Self::parse_number(right)?;
let right = right.checked_add(1).ok_or_else(|| {
SnippetBlockParseError::InvalidHighlightedLines(format!("{right} is too large"))
})?;
highlights.push(Highlight::Range(left..right));
}
None => {
let number = Self::parse_number(piece)?;
highlights.push(Highlight::Single(number));
}
}
}
Ok(HighlightGroup::new(highlights))
}
fn parse_number(input: &str) -> ParseResult<u16> {
input
.trim()
.parse()
.map_err(|_| SnippetBlockParseError::InvalidHighlightedLines(format!("not a number: '{input}'")))
}
fn skip_whitespace(input: &str) -> &str {
input.trim_start_matches(' ')
}
fn next_identifier(input: &str) -> &str {
match input.split_once(' ') {
Some((token, _)) => token,
None => input,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum SnippetBlockParseError {
#[error("invalid code attribute: {0}")]
InvalidToken(String),
#[error("invalid highlighted lines: {0}")]
InvalidHighlightedLines(String),
#[error("invalid width: {0}")]
InvalidWidth(PercentParseError),
#[error("invalid pty args, expected '[standby:]<columns>:<rows>'")]
InvalidPtyArgs,
#[error("duplicate attribute: {0}")]
DuplicateAttribute(&'static str),
#[error("+exec_replace +image and +render can't be used together ")]
MultipleRepresentation,
#[error("attribute {0} can only be set in +render blocks")]
NotRenderSnippet(&'static str),
}
#[derive(EnumDiscriminants)]
enum SnippetAttribute {
LineNumbers,
Exec(SnippetExecutorSpec),
AutoExec(SnippetExecutorSpec),
ExecReplace(SnippetExecutorSpec),
ExecPty(SnippetExecutorSpec, PtyArgs),
Validate(SnippetExecutorSpec),
Image,
Render,
HighlightedLines(Vec<HighlightGroup>),
Width(Percent),
NoBackground,
AcquireTerminal(SnippetExecutorSpec),
ExpectedExecutionResult(ExpectedSnippetExecutionResult),
Id(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum SnippetExecutorSpec {
#[default]
Default,
Alternative(String),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum ExpectedSnippetExecutionResult {
#[default]
Success,
Failure,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Snippet {
pub(crate) contents: String,
pub(crate) language: SnippetLanguage,
pub(crate) attributes: SnippetAttributes,
}
impl Snippet {
pub(crate) fn visible_lines<'a, 'b>(
&'a self,
hidden_line_prefix: Option<&'b str>,
) -> impl Iterator<Item = &'a str> + 'b
where
'a: 'b,
{
self.contents.lines().filter(move |line| !hidden_line_prefix.is_some_and(|prefix| line.starts_with(prefix)))
}
pub(crate) fn executable_contents(&self, hidden_line_prefix: Option<&str>) -> String {
if let Some(prefix) = 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()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub enum SnippetLanguage {
Ada,
Asp,
Awk,
Bash,
BatchFile,
C,
CMake,
Crontab,
CSharp,
Clojure,
Cpp,
Css,
Dart,
D2,
DLang,
Diff,
Docker,
Dotenv,
Elixir,
Elm,
Erlang,
File,
Fish,
FSharp,
GdScript,
Go,
GraphQL,
Haskell,
Html,
Java,
JavaScript,
Json,
Jsonnet,
Julia,
Kotlin,
Latex,
Lua,
Makefile,
Mermaid,
Markdown,
Nix,
Nushell,
OCaml,
Perl,
Php,
PowerShell,
Protobuf,
Puppet,
Python,
R,
Racket,
Ruby,
Rust,
RustScript,
Scala,
Shell,
Sql,
Swift,
Svelte,
Tcl,
Terraform,
Toml,
TypeScript,
TypeScriptReact,
Typst,
Unknown(String),
Xml,
Yaml,
Verilog,
Vue,
Wsl,
Zig,
Zsh,
}
crate::utils::impl_deserialize_from_str!(SnippetLanguage);
impl FromStr for SnippetLanguage {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use SnippetLanguage::*;
let language = match s.to_lowercase().as_str() {
"ada" => Ada,
"asp" => Asp,
"awk" => Awk,
"bash" => Bash,
"bat" | "cmd" => BatchFile,
"c" => C,
"cmake" => CMake,
"crontab" => Crontab,
"csharp" => CSharp,
"clojure" => Clojure,
"cpp" | "c++" => Cpp,
"css" => Css,
"dart" => Dart,
"d2" => D2,
"d" => DLang,
"diff" => Diff,
"docker" => Docker,
"dotenv" => Dotenv,
"elixir" => Elixir,
"elm" => Elm,
"erlang" => Erlang,
"file" => File,
"fish" => Fish,
"fsharp" => FSharp,
"go" => Go,
"graphql" => GraphQL,
"gdscript" => GdScript,
"haskell" => Haskell,
"html" => Html,
"java" => Java,
"javascript" | "js" => JavaScript,
"json" => Json,
"jsonnet" => Jsonnet,
"julia" => Julia,
"kotlin" => Kotlin,
"latex" => Latex,
"lua" => Lua,
"make" => Makefile,
"markdown" => Markdown,
"mermaid" => Mermaid,
"nix" => Nix,
"nushell" | "nu" => Nushell,
"ocaml" => OCaml,
"perl" => Perl,
"php" => Php,
"powershell" | "pwsh" => PowerShell,
"protobuf" => Protobuf,
"puppet" => Puppet,
"python" => Python,
"r" => R,
"racket" => Racket,
"ruby" => Ruby,
"rust" => Rust,
"rust-script" => RustScript,
"scala" => Scala,
"shell" | "sh" => Shell,
"sql" => Sql,
"svelte" => Svelte,
"swift" => Swift,
"tcl" => Tcl,
"terraform" => Terraform,
"toml" => Toml,
"typescript" | "ts" => TypeScript,
"tsx" => TypeScriptReact,
"typst" => Typst,
"xml" => Xml,
"yaml" => Yaml,
"verilog" => Verilog,
"vue" => Vue,
"wsl" => Wsl,
"zig" => Zig,
"zsh" => Zsh,
other => Unknown(other.to_string()),
};
Ok(language)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct SnippetAttributes {
pub(crate) execution: SnippetExecution,
pub(crate) line_numbers: bool,
pub(crate) highlight_groups: Vec<HighlightGroup>,
pub(crate) width: Option<Percent>,
pub(crate) no_background: bool,
pub(crate) validate: Option<SnippetExecutorSpec>,
pub(crate) expected_execution_result: ExpectedSnippetExecutionResult,
pub(crate) id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum SnippetRepr {
#[default]
SnippetOutput,
Image,
ExecReplace,
AcquireTerminal,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct PtyArgs {
pub(crate) columns: Option<u16>,
pub(crate) rows: Option<u16>,
pub(crate) standby: bool,
}
impl FromStr for PtyArgs {
type Err = SnippetBlockParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut standby = false;
let s = match s.strip_prefix("standby") {
Some(rest) => {
let rest = match rest.get(..1) {
Some(":") => &rest[1..],
Some(_) => return Err(SnippetBlockParseError::InvalidPtyArgs),
None => rest,
};
standby = true;
rest
}
None => s,
};
if s.is_empty() {
return Ok(Self { standby, ..Default::default() });
}
let Some((columns, rows)) = s.split_once(':') else {
return Err(SnippetBlockParseError::InvalidPtyArgs);
};
let columns = columns.parse().map_err(|_| SnippetBlockParseError::InvalidPtyArgs)?;
let rows = rows.parse().map_err(|_| SnippetBlockParseError::InvalidPtyArgs)?;
Ok(Self { columns: Some(columns), rows: Some(rows), standby })
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct SnippetExecArgs {
pub(crate) spec: SnippetExecutorSpec,
pub(crate) auto: bool,
pub(crate) pty: Option<PtyArgs>,
pub(crate) repr: SnippetRepr,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum SnippetExecution {
#[default]
None,
Render,
Exec(SnippetExecArgs),
}
impl SnippetExecution {
fn try_merge(self, other: SnippetExecution) -> ParseResult<Self> {
match (self, other) {
(Self::None, other) => Ok(other),
(Self::Render, Self::None) => Ok(Self::Render),
(Self::Render, Self::Render) => Err(SnippetBlockParseError::DuplicateAttribute("+render")),
(Self::Render, Self::Exec(_)) | (Self::Exec(_), Self::Render) => {
Err(SnippetBlockParseError::MultipleRepresentation)
}
(Self::Exec(mut ours), Self::Exec(theirs)) => {
let SnippetExecArgs { spec, auto, pty, repr } = theirs;
ours.auto = ours.auto || auto;
ours.pty = pty.or(ours.pty);
ours.spec = match ours.spec {
SnippetExecutorSpec::Default => spec,
SnippetExecutorSpec::Alternative(spec) => SnippetExecutorSpec::Alternative(spec),
};
ours.repr = match (ours.repr, repr) {
(SnippetRepr::SnippetOutput, other) => other,
(ours, SnippetRepr::SnippetOutput) => ours,
_ => return Err(SnippetBlockParseError::MultipleRepresentation),
};
Ok(Self::Exec(ours))
}
(Self::Exec(args), Self::None) => Ok(Self::Exec(args)),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct HighlightGroup(Vec<Highlight>);
impl HighlightGroup {
pub(crate) fn new(highlights: Vec<Highlight>) -> Self {
Self(highlights)
}
pub(crate) fn contains(&self, line_number: u16) -> bool {
for higlight in &self.0 {
match higlight {
Highlight::All => return true,
Highlight::Single(number) if number == &line_number => return true,
Highlight::Range(range) if range.contains(&line_number) => return true,
_ => continue,
};
}
false
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Highlight {
All,
Single(u16),
Range(Range<u16>),
}
#[derive(Debug, Deserialize)]
pub(crate) struct ExternalFile {
pub(crate) path: PathBuf,
pub(crate) language: SnippetLanguage,
pub(crate) start_line: Option<usize>,
pub(crate) end_line: Option<usize>,
}
#[cfg(test)]
mod test {
use super::*;
use Highlight::*;
use rstest::rstest;
fn parse_language(input: &str) -> SnippetLanguage {
let (language, _) = SnippetParser::parse_block_info(input).expect("parse failed");
language
}
fn try_parse_attributes(input: &str) -> Result<SnippetAttributes, SnippetBlockParseError> {
let (_, attributes) = SnippetParser::parse_block_info(input)?;
Ok(attributes)
}
fn parse_attributes(input: &str) -> SnippetAttributes {
try_parse_attributes(input).expect("parse failed")
}
#[test]
fn code_with_line_numbers() {
let total_lines = 11;
let input_lines = "hi\n".repeat(total_lines);
let code = Snippet {
contents: input_lines,
language: SnippetLanguage::Unknown("".to_string()),
attributes: SnippetAttributes { line_numbers: true, ..Default::default() },
};
let lines = SnippetSplitter::new(&Default::default(), None).split(&code);
assert_eq!(lines.len(), total_lines);
let mut lines = lines.into_iter().enumerate();
for (index, line) in lines.by_ref().take(9) {
let line_number = index + 1;
assert_eq!(&line.prefix, &format!(" {line_number} "));
}
for (index, line) in lines {
let line_number = index + 1;
assert_eq!(&line.prefix, &format!("{line_number} "));
}
}
#[test]
fn unknown_language() {
assert_eq!(parse_language("potato"), SnippetLanguage::Unknown("potato".to_string()));
}
#[test]
fn no_attributes() {
assert_eq!(parse_language("rust"), SnippetLanguage::Rust);
}
#[test]
fn one_attribute() {
let attributes = parse_attributes("bash +exec");
assert_eq!(attributes.execution, SnippetExecution::Exec(Default::default()));
assert!(!attributes.line_numbers);
}
#[test]
fn two_attributes() {
let attributes = parse_attributes("bash +exec +line_numbers");
assert_eq!(attributes.execution, SnippetExecution::Exec(Default::default()));
assert!(attributes.line_numbers);
}
#[test]
fn acquire_terminal() {
let attributes = parse_attributes("bash +acquire_terminal +exec");
assert_eq!(
attributes.execution,
SnippetExecution::Exec(SnippetExecArgs { repr: SnippetRepr::AcquireTerminal, ..Default::default() })
);
assert!(!attributes.line_numbers);
}
#[test]
fn image() {
let attributes = parse_attributes("bash +image +exec");
assert_eq!(
attributes.execution,
SnippetExecution::Exec(SnippetExecArgs { repr: SnippetRepr::Image, ..Default::default() })
);
assert!(!attributes.line_numbers);
}
#[test]
fn invalid_attributes() {
SnippetParser::parse_block_info("bash +potato").unwrap_err();
SnippetParser::parse_block_info("bash potato").unwrap_err();
}
#[rstest]
#[case::no_end("{")]
#[case::number_no_end("{42")]
#[case::comma_nothing("{42,")]
#[case::brace_comma("{,}")]
#[case::range_no_end("{42-")]
#[case::range_end("{42-}")]
#[case::too_many_ranges("{42-3-5}")]
#[case::range_comma("{42-,")]
#[case::too_large("{65536}")]
#[case::too_large_end("{1-65536}")]
fn invalid_line_highlights(#[case] input: &str) {
let input = format!("bash {input}");
SnippetParser::parse_block_info(&input).expect_err("parsed successfully");
}
#[test]
fn highlight_none() {
let attributes = parse_attributes("bash {}");
assert_eq!(attributes.highlight_groups, &[HighlightGroup::new(vec![Highlight::All])]);
}
#[test]
fn highlight_specific_lines() {
let attributes = parse_attributes("bash { 1, 2 , 3 }");
assert_eq!(attributes.highlight_groups, &[HighlightGroup::new(vec![Single(1), Single(2), Single(3)])]);
}
#[test]
fn highlight_line_range() {
let attributes = parse_attributes("bash { 1, 2-4,6 , all , 10 - 12 }");
assert_eq!(
attributes.highlight_groups,
&[HighlightGroup::new(vec![Single(1), Range(2..5), Single(6), All, Range(10..13)])]
);
}
#[test]
fn multiple_groups() {
let attributes = parse_attributes("bash {1-3,5 |6-9}");
assert_eq!(attributes.highlight_groups.len(), 2);
assert_eq!(attributes.highlight_groups[0], HighlightGroup::new(vec![Range(1..4), Single(5)]));
assert_eq!(attributes.highlight_groups[1], HighlightGroup::new(vec![Range(6..10)]));
}
#[test]
fn parse_width() {
let attributes = parse_attributes("mermaid +width:50% +render");
assert_eq!(attributes.execution, SnippetExecution::Render);
assert_eq!(attributes.width, Some(Percent(50)));
}
#[test]
fn invalid_width() {
try_parse_attributes("mermaid +width:50%% +render").expect_err("parse succeeded");
try_parse_attributes("mermaid +width: +render").expect_err("parse succeeded");
try_parse_attributes("mermaid +width:50%").expect_err("parse succeeded");
}
#[test]
fn code_visible_lines() {
let contents = r##"# fn main() {
println!("Hello world");
# // The prefix is # .
# }
"##
.to_string();
let expected = vec!["println!(\"Hello world\");"];
let code = Snippet { contents, language: SnippetLanguage::Rust, attributes: Default::default() };
assert_eq!(expected, code.visible_lines(Some("# ")).collect::<Vec<_>>());
}
#[test]
fn code_executable_contents() {
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 = Snippet { contents, language: SnippetLanguage::Rust, attributes: Default::default() };
assert_eq!(expected, code.executable_contents(Some("# ")));
}
#[test]
fn tabs_in_snippet() {
let snippet = Snippet { contents: "\thi".into(), language: SnippetLanguage::C, attributes: Default::default() };
let lines = SnippetSplitter::new(&Default::default(), None).split(&snippet);
assert_eq!(lines[0].code, " hi\n");
}
#[rstest]
#[case::exec("bash +exec:foo", SnippetExecutorSpec::Alternative("foo".to_string()))]
#[case::exec_and_more("bash +exec:foo +line_numbers", SnippetExecutorSpec::Alternative("foo".to_string()))]
#[case::exec_replace("bash +exec_replace:foo", SnippetExecutorSpec::Alternative("foo".to_string()))]
#[case::exec_replace_and_more("bash +exec_replace:foo +line_numbers", SnippetExecutorSpec::Alternative("foo".into()))]
fn alternative_executor(#[case] input: &str, #[case] spec: SnippetExecutorSpec) {
let attributes = parse_attributes(input);
let SnippetExecution::Exec(args) = attributes.execution else { panic!("not an exec snippet") };
assert_eq!(args.spec, spec);
}
#[test]
fn acquire_terminal_alternative() {
let attributes = parse_attributes("bash +acquire_terminal:foo");
assert_eq!(
attributes.execution,
SnippetExecution::Exec(SnippetExecArgs {
spec: SnippetExecutorSpec::Alternative("foo".into()),
repr: SnippetRepr::AcquireTerminal,
..Default::default()
})
);
}
#[rstest]
#[case::success("expect:success", ExpectedSnippetExecutionResult::Success)]
#[case::failure("expect:failure", ExpectedSnippetExecutionResult::Failure)]
#[case::fail("expect:fail", ExpectedSnippetExecutionResult::Failure)]
fn parse_expect(#[case] input: &str, #[case] expected: ExpectedSnippetExecutionResult) {
let attributes = parse_attributes(&format!("bash +{input}"));
assert_eq!(attributes.expected_execution_result, expected);
}
}