mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 23:42:59 +00:00
chore: move snippet processing code to separate module
This commit is contained in:
parent
44f0787bb5
commit
6fb9df56a3
@ -2,10 +2,7 @@ use crate::{
|
|||||||
code::{
|
code::{
|
||||||
execute::SnippetExecutor,
|
execute::SnippetExecutor,
|
||||||
highlighting::{HighlightThemeSet, SnippetHighlighter},
|
highlighting::{HighlightThemeSet, SnippetHighlighter},
|
||||||
snippet::{
|
snippet::SnippetLanguage,
|
||||||
ExternalFile, Highlight, HighlightContext, HighlightGroup, HighlightMutator, HighlightedLine, Snippet,
|
|
||||||
SnippetExec, SnippetLanguage, SnippetLine, SnippetParser, SnippetRepr, SnippetSplitter,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
config::{KeyBindingsConfig, OptionsConfig},
|
config::{KeyBindingsConfig, OptionsConfig},
|
||||||
markdown::{
|
markdown::{
|
||||||
@ -21,27 +18,19 @@ use crate::{
|
|||||||
ChunkMutator, Modals, Presentation, PresentationMetadata, PresentationState, PresentationThemeMetadata,
|
ChunkMutator, Modals, Presentation, PresentationMetadata, PresentationState, PresentationThemeMetadata,
|
||||||
RenderOperation, SlideBuilder, SlideChunk,
|
RenderOperation, SlideBuilder, SlideChunk,
|
||||||
},
|
},
|
||||||
render::{
|
render::operation::{BlockLine, ImageRenderProperties, ImageSize, MarginProperties},
|
||||||
operation::{AsRenderOperations, BlockLine, ImageRenderProperties, ImageSize, MarginProperties, RenderAsync},
|
|
||||||
properties::WindowSize,
|
|
||||||
},
|
|
||||||
resource::Resources,
|
resource::Resources,
|
||||||
terminal::image::{
|
terminal::image::{
|
||||||
Image,
|
Image,
|
||||||
printer::{ImageRegistry, RegisterImageError},
|
printer::{ImageRegistry, RegisterImageError},
|
||||||
},
|
},
|
||||||
theme::{
|
theme::{
|
||||||
Alignment, AuthorPositioning, CodeBlockStyle, ElementType, Margin, PresentationTheme, ProcessingThemeError,
|
Alignment, AuthorPositioning, ElementType, Margin, PresentationTheme, ProcessingThemeError, ThemeOptions,
|
||||||
ThemeOptions,
|
|
||||||
raw::{self, RawColor},
|
raw::{self, RawColor},
|
||||||
registry::{LoadThemeError, PresentationThemeRegistry},
|
registry::{LoadThemeError, PresentationThemeRegistry},
|
||||||
},
|
},
|
||||||
third_party::{ThirdPartyRender, ThirdPartyRenderError, ThirdPartyRenderRequest},
|
third_party::{ThirdPartyRender, ThirdPartyRenderError},
|
||||||
ui::{
|
ui::{
|
||||||
execution::{
|
|
||||||
DisplaySeparator, RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation,
|
|
||||||
SnippetExecutionDisabledOperation,
|
|
||||||
},
|
|
||||||
footer::{FooterGenerator, FooterVariables, InvalidFooterTemplateError},
|
footer::{FooterGenerator, FooterVariables, InvalidFooterTemplateError},
|
||||||
modals::{IndexBuilder, KeyBindingsModalBuilder},
|
modals::{IndexBuilder, KeyBindingsModalBuilder},
|
||||||
separator::RenderSeparator,
|
separator::RenderSeparator,
|
||||||
@ -50,9 +39,12 @@ use crate::{
|
|||||||
use comrak::{Arena, nodes::AlertType};
|
use comrak::{Arena, nodes::AlertType};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{cell::RefCell, collections::HashSet, fmt::Display, iter, mem, path::PathBuf, rc::Rc, str::FromStr};
|
use snippet::{SnippetOperations, SnippetProcessor, SnippetProcessorState};
|
||||||
|
use std::{collections::HashSet, fmt::Display, iter, mem, path::PathBuf, rc::Rc, str::FromStr};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
mod snippet;
|
||||||
|
|
||||||
pub(crate) type BuildResult = Result<(), BuildError>;
|
pub(crate) type BuildResult = Result<(), BuildError>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -863,256 +855,23 @@ impl<'a> PresentationBuilder<'a> {
|
|||||||
self.chunk_operations.extend(iter::repeat(RenderOperation::RenderLineBreak).take(count));
|
self.chunk_operations.extend(iter::repeat(RenderOperation::RenderLineBreak).take(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_differ(&mut self, text: String) {
|
|
||||||
self.chunk_operations.push(RenderOperation::RenderDynamic(Rc::new(Differ(text))));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_code(&mut self, info: String, code: String, source_position: SourcePosition) -> BuildResult {
|
fn push_code(&mut self, info: String, code: String, source_position: SourcePosition) -> BuildResult {
|
||||||
let mut snippet = SnippetParser::parse(info, code)
|
let state = SnippetProcessorState {
|
||||||
.map_err(|e| BuildError::InvalidSnippet { source_position, error: e.to_string() })?;
|
resources: &self.resources,
|
||||||
if matches!(snippet.language, SnippetLanguage::File) {
|
image_registry: &self.image_registry,
|
||||||
snippet = self.load_external_snippet(snippet, source_position)?;
|
snippet_executor: self.code_executor.clone(),
|
||||||
}
|
theme: &self.theme,
|
||||||
if self.options.auto_render_languages.contains(&snippet.language) {
|
presentation_state: &self.presentation_state,
|
||||||
snippet.attributes.representation = SnippetRepr::Render;
|
third_party: self.third_party,
|
||||||
}
|
highlighter: &self.highlighter,
|
||||||
self.push_differ(snippet.contents.clone());
|
options: &self.options,
|
||||||
// Redraw slide if attributes change
|
slide_number: self.slide_builders.len() + 1,
|
||||||
self.push_differ(format!("{:?}", snippet.attributes));
|
font_size: self.slide_font_size(),
|
||||||
|
|
||||||
let execution_allowed = self.is_execution_allowed(&snippet);
|
|
||||||
match snippet.attributes.representation {
|
|
||||||
SnippetRepr::Render => return self.push_rendered_code(snippet, source_position),
|
|
||||||
SnippetRepr::Image => {
|
|
||||||
if execution_allowed {
|
|
||||||
return self.push_code_as_image(snippet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SnippetRepr::ExecReplace => {
|
|
||||||
if execution_allowed {
|
|
||||||
return self.push_code_execution(snippet, 0, ExecutionMode::ReplaceSnippet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SnippetRepr::Snippet => (),
|
|
||||||
};
|
};
|
||||||
|
let processor = SnippetProcessor::new(state);
|
||||||
let block_length = self.push_code_lines(&snippet);
|
let SnippetOperations { operations, mutators } = processor.process_code(info, code, source_position)?;
|
||||||
match snippet.attributes.execution {
|
self.chunk_operations.extend(operations);
|
||||||
SnippetExec::None => Ok(()),
|
self.chunk_mutators.extend(mutators);
|
||||||
SnippetExec::Exec | SnippetExec::AcquireTerminal if !execution_allowed => {
|
|
||||||
let auto_start = match snippet.attributes.representation {
|
|
||||||
SnippetRepr::Image | SnippetRepr::ExecReplace => true,
|
|
||||||
SnippetRepr::Render | SnippetRepr::Snippet => false,
|
|
||||||
};
|
|
||||||
self.push_execution_disabled_operation(auto_start);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
SnippetExec::Exec => self.push_code_execution(snippet, block_length, ExecutionMode::AlongSnippet),
|
|
||||||
SnippetExec::AcquireTerminal => self.push_acquire_terminal_execution(snippet, block_length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_execution_allowed(&self, snippet: &Snippet) -> bool {
|
|
||||||
match snippet.attributes.representation {
|
|
||||||
SnippetRepr::Snippet => self.options.enable_snippet_execution,
|
|
||||||
SnippetRepr::Image | SnippetRepr::ExecReplace => self.options.enable_snippet_execution_replace,
|
|
||||||
SnippetRepr::Render => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_code_lines(&mut self, snippet: &Snippet) -> usize {
|
|
||||||
let lines = SnippetSplitter::new(&self.theme.code, self.code_executor.hidden_line_prefix(&snippet.language))
|
|
||||||
.split(snippet);
|
|
||||||
let block_length = lines.iter().map(|line| line.width()).max().unwrap_or(0) * self.slide_font_size() as usize;
|
|
||||||
let (lines, context) = self.highlight_lines(snippet, lines, block_length);
|
|
||||||
for line in lines {
|
|
||||||
self.chunk_operations.push(RenderOperation::RenderDynamic(Rc::new(line)));
|
|
||||||
}
|
|
||||||
self.set_colors(self.theme.default_style.style.colors);
|
|
||||||
if self.options.allow_mutations && context.borrow().groups.len() > 1 {
|
|
||||||
self.chunk_mutators.push(Box::new(HighlightMutator::new(context)));
|
|
||||||
}
|
|
||||||
block_length
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_external_snippet(
|
|
||||||
&mut self,
|
|
||||||
mut code: Snippet,
|
|
||||||
source_position: SourcePosition,
|
|
||||||
) -> Result<Snippet, BuildError> {
|
|
||||||
let file: ExternalFile = serde_yaml::from_str(&code.contents)
|
|
||||||
.map_err(|e| BuildError::InvalidSnippet { source_position, error: e.to_string() })?;
|
|
||||||
let path = file.path;
|
|
||||||
let path_display = path.display();
|
|
||||||
let contents = self.resources.external_snippet(&path).map_err(|e| BuildError::InvalidSnippet {
|
|
||||||
source_position,
|
|
||||||
error: format!("failed to load {path_display}: {e}"),
|
|
||||||
})?;
|
|
||||||
code.language = file.language;
|
|
||||||
code.contents = contents;
|
|
||||||
Ok(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_rendered_code(&mut self, code: Snippet, source_position: SourcePosition) -> BuildResult {
|
|
||||||
let Snippet { contents, language, attributes } = code;
|
|
||||||
let error_holder = self.presentation_state.async_error_holder();
|
|
||||||
let request = match language {
|
|
||||||
SnippetLanguage::Typst => ThirdPartyRenderRequest::Typst(contents, self.theme.typst.clone()),
|
|
||||||
SnippetLanguage::Latex => ThirdPartyRenderRequest::Latex(contents, self.theme.typst.clone()),
|
|
||||||
SnippetLanguage::Mermaid => ThirdPartyRenderRequest::Mermaid(contents, self.theme.mermaid.clone()),
|
|
||||||
_ => {
|
|
||||||
return Err(BuildError::InvalidSnippet {
|
|
||||||
source_position,
|
|
||||||
error: format!("language {language:?} doesn't support rendering"),
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let operation = self.third_party.render(
|
|
||||||
request,
|
|
||||||
&self.theme,
|
|
||||||
error_holder,
|
|
||||||
self.slide_builders.len() + 1,
|
|
||||||
attributes.width,
|
|
||||||
)?;
|
|
||||||
self.chunk_operations.push(operation);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight_lines(
|
|
||||||
&self,
|
|
||||||
code: &Snippet,
|
|
||||||
lines: Vec<SnippetLine>,
|
|
||||||
block_length: usize,
|
|
||||||
) -> (Vec<HighlightedLine>, Rc<RefCell<HighlightContext>>) {
|
|
||||||
let mut code_highlighter = self.highlighter.language_highlighter(&code.language);
|
|
||||||
let style = self.code_style(code);
|
|
||||||
let block_length = match &self.theme.code.alignment {
|
|
||||||
Alignment::Left { .. } | Alignment::Right { .. } => block_length,
|
|
||||||
Alignment::Center { minimum_size, .. } => block_length.max(*minimum_size as usize),
|
|
||||||
};
|
|
||||||
let font_size = self.slide_font_size();
|
|
||||||
let dim_style = {
|
|
||||||
let mut highlighter = self.highlighter.language_highlighter(&SnippetLanguage::Rust);
|
|
||||||
highlighter.style_line("//", &style).0.first().expect("no styles").style.size(font_size)
|
|
||||||
};
|
|
||||||
let groups = match self.options.allow_mutations {
|
|
||||||
true => code.attributes.highlight_groups.clone(),
|
|
||||||
false => vec![HighlightGroup::new(vec![Highlight::All])],
|
|
||||||
};
|
|
||||||
let context =
|
|
||||||
Rc::new(RefCell::new(HighlightContext { groups, current: 0, block_length, alignment: style.alignment }));
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
for line in lines.into_iter() {
|
|
||||||
let prefix = line.dim_prefix(&dim_style);
|
|
||||||
let highlighted = line.highlight(&mut code_highlighter, &style, font_size);
|
|
||||||
let not_highlighted = line.dim(&dim_style);
|
|
||||||
let line_number = line.line_number;
|
|
||||||
let context = context.clone();
|
|
||||||
output.push(HighlightedLine {
|
|
||||||
prefix,
|
|
||||||
right_padding_length: line.right_padding_length * self.slide_font_size() as u16,
|
|
||||||
highlighted,
|
|
||||||
not_highlighted,
|
|
||||||
line_number,
|
|
||||||
context,
|
|
||||||
block_color: dim_style.colors.background,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
(output, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn code_style(&self, snippet: &Snippet) -> CodeBlockStyle {
|
|
||||||
let mut style = self.theme.code.clone();
|
|
||||||
if snippet.attributes.no_background {
|
|
||||||
style.alignment = match style.alignment {
|
|
||||||
Alignment::Center { .. } => Alignment::Center { minimum_size: 0, minimum_margin: Margin::default() },
|
|
||||||
Alignment::Left { .. } => Alignment::Left { margin: Margin::default() },
|
|
||||||
Alignment::Right { .. } => Alignment::Right { margin: Margin::default() },
|
|
||||||
};
|
|
||||||
style.background = false;
|
|
||||||
}
|
|
||||||
style
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_execution_disabled_operation(&mut self, auto_start: bool) {
|
|
||||||
let operation = SnippetExecutionDisabledOperation::new(
|
|
||||||
self.theme.execution_output.status.failure_style,
|
|
||||||
self.theme.code.alignment,
|
|
||||||
);
|
|
||||||
if auto_start {
|
|
||||||
operation.start_render();
|
|
||||||
}
|
|
||||||
self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_code_as_image(&mut self, snippet: Snippet) -> BuildResult {
|
|
||||||
if !self.code_executor.is_execution_supported(&snippet.language) {
|
|
||||||
return Err(BuildError::UnsupportedExecution(snippet.language));
|
|
||||||
}
|
|
||||||
let operation = RunImageSnippet::new(
|
|
||||||
snippet,
|
|
||||||
self.code_executor.clone(),
|
|
||||||
self.image_registry.clone(),
|
|
||||||
self.theme.execution_output.status.clone(),
|
|
||||||
);
|
|
||||||
operation.start_render();
|
|
||||||
|
|
||||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
|
||||||
self.chunk_operations.push(operation);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_acquire_terminal_execution(&mut self, snippet: Snippet, block_length: usize) -> BuildResult {
|
|
||||||
if !self.code_executor.is_execution_supported(&snippet.language) {
|
|
||||||
return Err(BuildError::UnsupportedExecution(snippet.language));
|
|
||||||
}
|
|
||||||
let block_length = block_length as u16;
|
|
||||||
let block_length = match &self.theme.code.alignment {
|
|
||||||
Alignment::Left { .. } | Alignment::Right { .. } => block_length,
|
|
||||||
Alignment::Center { minimum_size, .. } => block_length.max(*minimum_size),
|
|
||||||
};
|
|
||||||
let operation = RunAcquireTerminalSnippet::new(
|
|
||||||
snippet,
|
|
||||||
self.code_executor.clone(),
|
|
||||||
self.theme.execution_output.status.clone(),
|
|
||||||
block_length,
|
|
||||||
self.slide_font_size(),
|
|
||||||
);
|
|
||||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
|
||||||
self.chunk_operations.push(operation);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_code_execution(&mut self, snippet: Snippet, block_length: usize, mode: ExecutionMode) -> BuildResult {
|
|
||||||
if !self.code_executor.is_execution_supported(&snippet.language) {
|
|
||||||
return Err(BuildError::UnsupportedExecution(snippet.language));
|
|
||||||
}
|
|
||||||
let separator = match mode {
|
|
||||||
ExecutionMode::AlongSnippet => DisplaySeparator::On,
|
|
||||||
ExecutionMode::ReplaceSnippet => DisplaySeparator::Off,
|
|
||||||
};
|
|
||||||
let alignment = self.code_style(&snippet).alignment;
|
|
||||||
let default_colors = self.theme.default_style.style.colors;
|
|
||||||
let mut execution_output_style = self.theme.execution_output.clone();
|
|
||||||
if snippet.attributes.no_background {
|
|
||||||
execution_output_style.style.colors.background = None;
|
|
||||||
}
|
|
||||||
let operation = RunSnippetOperation::new(
|
|
||||||
snippet,
|
|
||||||
self.code_executor.clone(),
|
|
||||||
default_colors,
|
|
||||||
execution_output_style,
|
|
||||||
block_length as u16,
|
|
||||||
separator,
|
|
||||||
alignment,
|
|
||||||
self.slide_font_size(),
|
|
||||||
);
|
|
||||||
if matches!(mode, ExecutionMode::ReplaceSnippet) {
|
|
||||||
operation.start_render();
|
|
||||||
}
|
|
||||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
|
||||||
self.chunk_operations.push(operation);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1493,19 +1252,6 @@ struct ImageAttributes {
|
|||||||
width: Option<Percent>,
|
width: Option<Percent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Differ(String);
|
|
||||||
|
|
||||||
impl AsRenderOperations for Differ {
|
|
||||||
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diffable_content(&self) -> Option<&str> {
|
|
||||||
Some(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::presentation::Slide;
|
use crate::presentation::Slide;
|
363
src/presentation/builder/snippet.rs
Normal file
363
src/presentation/builder/snippet.rs
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
use super::{BuildError, BuildResult, ExecutionMode, PresentationBuilderOptions};
|
||||||
|
use crate::{
|
||||||
|
ImageRegistry,
|
||||||
|
code::{
|
||||||
|
execute::SnippetExecutor,
|
||||||
|
highlighting::SnippetHighlighter,
|
||||||
|
snippet::{
|
||||||
|
ExternalFile, Highlight, HighlightContext, HighlightGroup, HighlightMutator, HighlightedLine, Snippet,
|
||||||
|
SnippetExec, SnippetLanguage, SnippetLine, SnippetParser, SnippetRepr, SnippetSplitter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
markdown::elements::SourcePosition,
|
||||||
|
presentation::{ChunkMutator, PresentationState},
|
||||||
|
render::{
|
||||||
|
operation::{AsRenderOperations, RenderAsync, RenderOperation},
|
||||||
|
properties::WindowSize,
|
||||||
|
},
|
||||||
|
resource::Resources,
|
||||||
|
theme::{Alignment, CodeBlockStyle, PresentationTheme},
|
||||||
|
third_party::{ThirdPartyRender, ThirdPartyRenderRequest},
|
||||||
|
ui::execution::{
|
||||||
|
DisplaySeparator, RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation,
|
||||||
|
SnippetExecutionDisabledOperation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
pub(crate) struct SnippetProcessorState<'a> {
|
||||||
|
pub(crate) resources: &'a Resources,
|
||||||
|
pub(crate) image_registry: &'a ImageRegistry,
|
||||||
|
pub(crate) snippet_executor: Rc<SnippetExecutor>,
|
||||||
|
pub(crate) theme: &'a PresentationTheme,
|
||||||
|
pub(crate) presentation_state: &'a PresentationState,
|
||||||
|
pub(crate) third_party: &'a ThirdPartyRender,
|
||||||
|
pub(crate) highlighter: &'a SnippetHighlighter,
|
||||||
|
pub(crate) options: &'a PresentationBuilderOptions,
|
||||||
|
pub(crate) slide_number: usize,
|
||||||
|
pub(crate) font_size: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SnippetProcessor<'a> {
|
||||||
|
operations: Vec<RenderOperation>,
|
||||||
|
mutators: Vec<Box<dyn ChunkMutator>>,
|
||||||
|
resources: &'a Resources,
|
||||||
|
image_registry: &'a ImageRegistry,
|
||||||
|
snippet_executor: Rc<SnippetExecutor>,
|
||||||
|
theme: &'a PresentationTheme,
|
||||||
|
presentation_state: &'a PresentationState,
|
||||||
|
third_party: &'a ThirdPartyRender,
|
||||||
|
highlighter: &'a SnippetHighlighter,
|
||||||
|
options: &'a PresentationBuilderOptions,
|
||||||
|
slide_number: usize,
|
||||||
|
font_size: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SnippetProcessor<'a> {
|
||||||
|
pub(crate) fn new(state: SnippetProcessorState<'a>) -> Self {
|
||||||
|
let SnippetProcessorState {
|
||||||
|
resources,
|
||||||
|
image_registry,
|
||||||
|
snippet_executor,
|
||||||
|
theme,
|
||||||
|
presentation_state,
|
||||||
|
third_party,
|
||||||
|
highlighter,
|
||||||
|
options,
|
||||||
|
slide_number,
|
||||||
|
font_size,
|
||||||
|
} = state;
|
||||||
|
Self {
|
||||||
|
operations: Vec::new(),
|
||||||
|
mutators: Vec::new(),
|
||||||
|
resources,
|
||||||
|
image_registry,
|
||||||
|
snippet_executor,
|
||||||
|
theme,
|
||||||
|
presentation_state,
|
||||||
|
third_party,
|
||||||
|
highlighter,
|
||||||
|
options,
|
||||||
|
slide_number,
|
||||||
|
font_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process_code(
|
||||||
|
mut self,
|
||||||
|
info: String,
|
||||||
|
code: String,
|
||||||
|
source_position: SourcePosition,
|
||||||
|
) -> Result<SnippetOperations, BuildError> {
|
||||||
|
self.do_process_code(info, code, source_position)?;
|
||||||
|
|
||||||
|
let Self { operations, mutators, .. } = self;
|
||||||
|
Ok(SnippetOperations { operations, mutators })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_process_code(&mut self, info: String, code: String, source_position: SourcePosition) -> BuildResult {
|
||||||
|
let mut snippet = SnippetParser::parse(info, code)
|
||||||
|
.map_err(|e| BuildError::InvalidSnippet { source_position, error: e.to_string() })?;
|
||||||
|
if matches!(snippet.language, SnippetLanguage::File) {
|
||||||
|
snippet = self.load_external_snippet(snippet, source_position)?;
|
||||||
|
}
|
||||||
|
if self.options.auto_render_languages.contains(&snippet.language) {
|
||||||
|
snippet.attributes.representation = SnippetRepr::Render;
|
||||||
|
}
|
||||||
|
self.push_differ(snippet.contents.clone());
|
||||||
|
// Redraw slide if attributes change
|
||||||
|
self.push_differ(format!("{:?}", snippet.attributes));
|
||||||
|
|
||||||
|
let execution_allowed = self.is_execution_allowed(&snippet);
|
||||||
|
match snippet.attributes.representation {
|
||||||
|
SnippetRepr::Render => return self.push_rendered_code(snippet, source_position),
|
||||||
|
SnippetRepr::Image => {
|
||||||
|
if execution_allowed {
|
||||||
|
return self.push_code_as_image(snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SnippetRepr::ExecReplace => {
|
||||||
|
if execution_allowed {
|
||||||
|
return self.push_code_execution(snippet, 0, ExecutionMode::ReplaceSnippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SnippetRepr::Snippet => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
let block_length = self.push_code_lines(&snippet);
|
||||||
|
match snippet.attributes.execution {
|
||||||
|
SnippetExec::None => Ok(()),
|
||||||
|
SnippetExec::Exec | SnippetExec::AcquireTerminal if !execution_allowed => {
|
||||||
|
let auto_start = match snippet.attributes.representation {
|
||||||
|
SnippetRepr::Image | SnippetRepr::ExecReplace => true,
|
||||||
|
SnippetRepr::Render | SnippetRepr::Snippet => false,
|
||||||
|
};
|
||||||
|
self.push_execution_disabled_operation(auto_start);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SnippetExec::Exec => self.push_code_execution(snippet, block_length, ExecutionMode::AlongSnippet),
|
||||||
|
SnippetExec::AcquireTerminal => self.push_acquire_terminal_execution(snippet, block_length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_execution_allowed(&self, snippet: &Snippet) -> bool {
|
||||||
|
match snippet.attributes.representation {
|
||||||
|
SnippetRepr::Snippet => self.options.enable_snippet_execution,
|
||||||
|
SnippetRepr::Image | SnippetRepr::ExecReplace => self.options.enable_snippet_execution_replace,
|
||||||
|
SnippetRepr::Render => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_code_lines(&mut self, snippet: &Snippet) -> usize {
|
||||||
|
let lines = SnippetSplitter::new(&self.theme.code, self.snippet_executor.hidden_line_prefix(&snippet.language))
|
||||||
|
.split(snippet);
|
||||||
|
let block_length = lines.iter().map(|line| line.width()).max().unwrap_or(0) * self.font_size as usize;
|
||||||
|
let (lines, context) = self.highlight_lines(snippet, lines, block_length);
|
||||||
|
for line in lines {
|
||||||
|
self.operations.push(RenderOperation::RenderDynamic(Rc::new(line)));
|
||||||
|
}
|
||||||
|
self.operations.push(RenderOperation::SetColors(self.theme.default_style.style.colors));
|
||||||
|
if self.options.allow_mutations && context.borrow().groups.len() > 1 {
|
||||||
|
self.mutators.push(Box::new(HighlightMutator::new(context)));
|
||||||
|
}
|
||||||
|
block_length
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_external_snippet(
|
||||||
|
&mut self,
|
||||||
|
mut code: Snippet,
|
||||||
|
source_position: SourcePosition,
|
||||||
|
) -> Result<Snippet, BuildError> {
|
||||||
|
let file: ExternalFile = serde_yaml::from_str(&code.contents)
|
||||||
|
.map_err(|e| BuildError::InvalidSnippet { source_position, error: e.to_string() })?;
|
||||||
|
let path = file.path;
|
||||||
|
let path_display = path.display();
|
||||||
|
let contents = self.resources.external_snippet(&path).map_err(|e| BuildError::InvalidSnippet {
|
||||||
|
source_position,
|
||||||
|
error: format!("failed to load {path_display}: {e}"),
|
||||||
|
})?;
|
||||||
|
code.language = file.language;
|
||||||
|
code.contents = contents;
|
||||||
|
Ok(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_rendered_code(&mut self, code: Snippet, source_position: SourcePosition) -> BuildResult {
|
||||||
|
let Snippet { contents, language, attributes } = code;
|
||||||
|
let error_holder = self.presentation_state.async_error_holder();
|
||||||
|
let request = match language {
|
||||||
|
SnippetLanguage::Typst => ThirdPartyRenderRequest::Typst(contents, self.theme.typst.clone()),
|
||||||
|
SnippetLanguage::Latex => ThirdPartyRenderRequest::Latex(contents, self.theme.typst.clone()),
|
||||||
|
SnippetLanguage::Mermaid => ThirdPartyRenderRequest::Mermaid(contents, self.theme.mermaid.clone()),
|
||||||
|
_ => {
|
||||||
|
return Err(BuildError::InvalidSnippet {
|
||||||
|
source_position,
|
||||||
|
error: format!("language {language:?} doesn't support rendering"),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let operation =
|
||||||
|
self.third_party.render(request, self.theme, error_holder, self.slide_number, attributes.width)?;
|
||||||
|
self.operations.push(operation);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_lines(
|
||||||
|
&self,
|
||||||
|
code: &Snippet,
|
||||||
|
lines: Vec<SnippetLine>,
|
||||||
|
block_length: usize,
|
||||||
|
) -> (Vec<HighlightedLine>, Rc<RefCell<HighlightContext>>) {
|
||||||
|
let mut code_highlighter = self.highlighter.language_highlighter(&code.language);
|
||||||
|
let style = self.code_style(code);
|
||||||
|
let block_length = match &self.theme.code.alignment {
|
||||||
|
Alignment::Left { .. } | Alignment::Right { .. } => block_length,
|
||||||
|
Alignment::Center { minimum_size, .. } => block_length.max(*minimum_size as usize),
|
||||||
|
};
|
||||||
|
let font_size = self.font_size;
|
||||||
|
let dim_style = {
|
||||||
|
let mut highlighter = self.highlighter.language_highlighter(&SnippetLanguage::Rust);
|
||||||
|
highlighter.style_line("//", &style).0.first().expect("no styles").style.size(font_size)
|
||||||
|
};
|
||||||
|
let groups = match self.options.allow_mutations {
|
||||||
|
true => code.attributes.highlight_groups.clone(),
|
||||||
|
false => vec![HighlightGroup::new(vec![Highlight::All])],
|
||||||
|
};
|
||||||
|
let context =
|
||||||
|
Rc::new(RefCell::new(HighlightContext { groups, current: 0, block_length, alignment: style.alignment }));
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
for line in lines.into_iter() {
|
||||||
|
let prefix = line.dim_prefix(&dim_style);
|
||||||
|
let highlighted = line.highlight(&mut code_highlighter, &style, font_size);
|
||||||
|
let not_highlighted = line.dim(&dim_style);
|
||||||
|
let line_number = line.line_number;
|
||||||
|
let context = context.clone();
|
||||||
|
output.push(HighlightedLine {
|
||||||
|
prefix,
|
||||||
|
right_padding_length: line.right_padding_length * self.font_size as u16,
|
||||||
|
highlighted,
|
||||||
|
not_highlighted,
|
||||||
|
line_number,
|
||||||
|
context,
|
||||||
|
block_color: dim_style.colors.background,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(output, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_style(&self, snippet: &Snippet) -> CodeBlockStyle {
|
||||||
|
let mut style = self.theme.code.clone();
|
||||||
|
if snippet.attributes.no_background {
|
||||||
|
style.alignment = match style.alignment {
|
||||||
|
Alignment::Center { .. } => Alignment::Center { minimum_size: 0, minimum_margin: Default::default() },
|
||||||
|
Alignment::Left { .. } => Alignment::Left { margin: Default::default() },
|
||||||
|
Alignment::Right { .. } => Alignment::Right { margin: Default::default() },
|
||||||
|
};
|
||||||
|
style.background = false;
|
||||||
|
}
|
||||||
|
style
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_execution_disabled_operation(&mut self, auto_start: bool) {
|
||||||
|
let operation = SnippetExecutionDisabledOperation::new(
|
||||||
|
self.theme.execution_output.status.failure_style,
|
||||||
|
self.theme.code.alignment,
|
||||||
|
);
|
||||||
|
if auto_start {
|
||||||
|
operation.start_render();
|
||||||
|
}
|
||||||
|
self.operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_code_as_image(&mut self, snippet: Snippet) -> BuildResult {
|
||||||
|
if !self.snippet_executor.is_execution_supported(&snippet.language) {
|
||||||
|
return Err(BuildError::UnsupportedExecution(snippet.language));
|
||||||
|
}
|
||||||
|
let operation = RunImageSnippet::new(
|
||||||
|
snippet,
|
||||||
|
self.snippet_executor.clone(),
|
||||||
|
self.image_registry.clone(),
|
||||||
|
self.theme.execution_output.status.clone(),
|
||||||
|
);
|
||||||
|
operation.start_render();
|
||||||
|
|
||||||
|
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||||
|
self.operations.push(operation);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_acquire_terminal_execution(&mut self, snippet: Snippet, block_length: usize) -> BuildResult {
|
||||||
|
if !self.snippet_executor.is_execution_supported(&snippet.language) {
|
||||||
|
return Err(BuildError::UnsupportedExecution(snippet.language));
|
||||||
|
}
|
||||||
|
let block_length = block_length as u16;
|
||||||
|
let block_length = match &self.theme.code.alignment {
|
||||||
|
Alignment::Left { .. } | Alignment::Right { .. } => block_length,
|
||||||
|
Alignment::Center { minimum_size, .. } => block_length.max(*minimum_size),
|
||||||
|
};
|
||||||
|
let operation = RunAcquireTerminalSnippet::new(
|
||||||
|
snippet,
|
||||||
|
self.snippet_executor.clone(),
|
||||||
|
self.theme.execution_output.status.clone(),
|
||||||
|
block_length,
|
||||||
|
self.font_size,
|
||||||
|
);
|
||||||
|
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||||
|
self.operations.push(operation);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_code_execution(&mut self, snippet: Snippet, block_length: usize, mode: ExecutionMode) -> BuildResult {
|
||||||
|
if !self.snippet_executor.is_execution_supported(&snippet.language) {
|
||||||
|
return Err(BuildError::UnsupportedExecution(snippet.language));
|
||||||
|
}
|
||||||
|
let separator = match mode {
|
||||||
|
ExecutionMode::AlongSnippet => DisplaySeparator::On,
|
||||||
|
ExecutionMode::ReplaceSnippet => DisplaySeparator::Off,
|
||||||
|
};
|
||||||
|
let alignment = self.code_style(&snippet).alignment;
|
||||||
|
let default_colors = self.theme.default_style.style.colors;
|
||||||
|
let mut execution_output_style = self.theme.execution_output.clone();
|
||||||
|
if snippet.attributes.no_background {
|
||||||
|
execution_output_style.style.colors.background = None;
|
||||||
|
}
|
||||||
|
let operation = RunSnippetOperation::new(
|
||||||
|
snippet,
|
||||||
|
self.snippet_executor.clone(),
|
||||||
|
default_colors,
|
||||||
|
execution_output_style,
|
||||||
|
block_length as u16,
|
||||||
|
separator,
|
||||||
|
alignment,
|
||||||
|
self.font_size,
|
||||||
|
);
|
||||||
|
if matches!(mode, ExecutionMode::ReplaceSnippet) {
|
||||||
|
operation.start_render();
|
||||||
|
}
|
||||||
|
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||||
|
self.operations.push(operation);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_differ(&mut self, text: String) {
|
||||||
|
self.operations.push(RenderOperation::RenderDynamic(Rc::new(Differ(text))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SnippetOperations {
|
||||||
|
pub(crate) operations: Vec<RenderOperation>,
|
||||||
|
pub(crate) mutators: Vec<Box<dyn ChunkMutator>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Differ(String);
|
||||||
|
|
||||||
|
impl AsRenderOperations for Differ {
|
||||||
|
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diffable_content(&self) -> Option<&str> {
|
||||||
|
Some(&self.0)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user