Skip to content

Commit

Permalink
feat: handle code overflow (#320)
Browse files Browse the repository at this point in the history
This makes it so that code blocks don't overflow to the right of the
code block but instead how you'd expect them to: by creating a new line
within the code block rect, properly colored and stuff.


![image](https://github.com/user-attachments/assets/529b71e0-a267-4b20-b745-cbb190f310cb)

Fixes #289
  • Loading branch information
mfontanini authored Aug 2, 2024
2 parents d61375c + e45f21a commit fb53aa0
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 288 deletions.
10 changes: 5 additions & 5 deletions src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl AnsiSplitter {
for p in line.ansi_parse() {
match p {
Output::TextBlock(text) => {
self.current_line.0.push(Text::new(text, self.current_style.clone()));
self.current_line.0.push(Text::new(text, self.current_style));
}
Output::Escape(s) => self.handle_escape(&s),
}
Expand Down Expand Up @@ -70,10 +70,10 @@ impl<'a> GraphicsCode<'a> {
for value in codes {
match value {
0 => *style = TextStyle::default(),
1 => *style = style.clone().bold(),
3 => *style = style.clone().italics(),
4 => *style = style.clone().underlined(),
9 => *style = style.clone().strikethrough(),
1 => *style = (*style).bold(),
3 => *style = (*style).italics(),
4 => *style = (*style).underlined(),
9 => *style = (*style).strikethrough(),
30 => style.colors.foreground = Some(Color::Black),
40 => style.colors.background = Some(Color::Black),
31 => style.colors.foreground = Some(Color::Red),
Expand Down
10 changes: 5 additions & 5 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,8 @@ where
mod test {
use super::*;
use crate::{
presentation::{
AsRenderOperations, BlockLine, BlockLineText, RenderAsync, RenderAsyncState, Slide, SlideBuilder,
},
markdown::text::WeightedTextBlock,
presentation::{AsRenderOperations, BlockLine, RenderAsync, RenderAsyncState, Slide, SlideBuilder},
render::properties::WindowSize,
style::{Color, Colors},
theme::{Alignment, Margin},
Expand Down Expand Up @@ -156,10 +155,11 @@ mod test {
#[case(RenderOperation::RenderText{line: String::from("asd").into(), alignment: Default::default()})]
#[case(RenderOperation::RenderBlockLine(
BlockLine{
text: BlockLineText::Preformatted("".into()),
prefix: "".into(),
text: WeightedTextBlock::from("".to_string()),
alignment: Default::default(),
block_length: 42,
unformatted_length: 1337
block_color: None,
}
))]
#[case(RenderOperation::RenderDynamic(Rc::new(Dynamic)))]
Expand Down
10 changes: 5 additions & 5 deletions src/markdown/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,14 +339,14 @@ impl<'a> InlinesParser<'a> {
let data = node.data.borrow();
match &data.value {
NodeValue::Text(text) => {
self.pending_text.push(Text::new(text.clone(), style.clone()));
self.pending_text.push(Text::new(text.clone(), style));
}
NodeValue::Code(code) => {
self.pending_text.push(Text::new(code.literal.clone(), TextStyle::default().code()));
}
NodeValue::Strong => self.process_children(node, style.clone().bold())?,
NodeValue::Emph => self.process_children(node, style.clone().italics())?,
NodeValue::Strikethrough => self.process_children(node, style.clone().strikethrough())?,
NodeValue::Strong => self.process_children(node, style.bold())?,
NodeValue::Emph => self.process_children(node, style.italics())?,
NodeValue::Strikethrough => self.process_children(node, style.strikethrough())?,
NodeValue::SoftBreak => self.pending_text.push(Text::from(" ")),
NodeValue::Link(link) => self.pending_text.push(Text::new(link.url.clone(), TextStyle::default().link())),
NodeValue::LineBreak => {
Expand Down Expand Up @@ -381,7 +381,7 @@ impl<'a> InlinesParser<'a> {

fn process_children(&mut self, node: &'a AstNode<'a>, style: TextStyle) -> ParseResult<()> {
for node in node.children() {
self.process_node(node, style.clone())?;
self.process_node(node, style)?;
}
Ok(())
}
Expand Down
81 changes: 49 additions & 32 deletions src/markdown/text.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
use super::elements::{Text, TextBlock};
use crate::style::TextStyle;
use std::mem;
use unicode_width::UnicodeWidthChar;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

/// A weighted block of text.
///
/// The weight of a character is its given by its width in unicode.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct WeightedTextBlock(Vec<WeightedText>);
pub(crate) struct WeightedTextBlock {
text: Vec<WeightedText>,
width: usize,
}

impl WeightedTextBlock {
/// Split this line into chunks of at most `max_length` width.
pub(crate) fn split(&self, max_length: usize) -> SplitTextIter {
SplitTextIter::new(&self.0, max_length)
SplitTextIter::new(&self.text, max_length)
}

/// The total width of this line.
pub(crate) fn width(&self) -> usize {
self.0.iter().map(|text| text.width()).sum()
self.width
}

/// Get an iterator to the underlying text chunks.
#[cfg(test)]
pub(crate) fn iter_texts(&self) -> impl Iterator<Item = &WeightedText> {
self.0.iter()
self.text.iter()
}
}

Expand All @@ -37,6 +40,7 @@ impl From<Vec<Text>> for WeightedTextBlock {
fn from(mut texts: Vec<Text>) -> Self {
let mut output = Vec::new();
let mut index = 0;
let mut width = 0;
// Compact chunks so any consecutive chunk with the same style is merged into the same block.
while index < texts.len() {
let mut target = mem::replace(&mut texts[index], Text::from(""));
Expand All @@ -46,17 +50,25 @@ impl From<Vec<Text>> for WeightedTextBlock {
target.content.push_str(&current_content);
current += 1;
}
width += target.content.width();
output.push(target.into());
index = current;
}
Self(output)
Self { text: output, width }
}
}

impl From<String> for WeightedTextBlock {
fn from(text: String) -> Self {
let texts = vec![WeightedText::from(text)];
Self(texts)
let width = text.width();
let text = vec![WeightedText::from(text)];
Self { text, width }
}
}

impl From<&str> for WeightedTextBlock {
fn from(text: &str) -> Self {
Self::from(text.to_string())
}
}

Expand All @@ -75,14 +87,13 @@ pub(crate) struct WeightedText {

impl WeightedText {
fn to_ref(&self) -> WeightedTextRef {
WeightedTextRef { text: &self.text.content, accumulators: &self.accumulators, style: self.text.style.clone() }
WeightedTextRef { text: &self.text.content, accumulators: &self.accumulators, style: self.text.style }
}

pub(crate) fn width(&self) -> usize {
self.accumulators.last().map(|a| a.width).unwrap_or(0)
self.to_ref().width()
}

#[cfg(test)]
pub(crate) fn text(&self) -> &Text {
&self.text
}
Expand Down Expand Up @@ -197,7 +208,7 @@ impl<'a> WeightedTextRef<'a> {
let leading_char_count = self.text[0..from].chars().count();
let output_char_count = text.chars().count();
let character_lengths = &self.accumulators[leading_char_count..leading_char_count + output_char_count + 1];
WeightedTextRef { text, accumulators: character_lengths, style: self.style.clone() }
WeightedTextRef { text, accumulators: character_lengths, style: self.style }
}

fn trim_start(self) -> Self {
Expand Down Expand Up @@ -304,63 +315,69 @@ mod test {

#[test]
fn split_at_full_length() {
let text = WeightedTextBlock(vec![WeightedText::from("hello world")]);
let text = WeightedTextBlock::from("hello world");
let lines = join_lines(text.split(11));
let expected = vec!["hello world"];
assert_eq!(lines, expected);
}

#[test]
fn no_split_necessary() {
let text = WeightedTextBlock(vec![WeightedText::from("short"), WeightedText::from("text")]);
let text = WeightedTextBlock { text: vec![WeightedText::from("short"), WeightedText::from("text")], width: 0 };
let lines = join_lines(text.split(50));
let expected = vec!["short text"];
assert_eq!(lines, expected);
}

#[test]
fn split_lines_single() {
let text = WeightedTextBlock(vec![WeightedText::from("this is a slightly long line")]);
let text = WeightedTextBlock { text: vec![WeightedText::from("this is a slightly long line")], width: 0 };
let lines = join_lines(text.split(6));
let expected = vec!["this", "is a", "slight", "ly", "long", "line"];
assert_eq!(lines, expected);
}

#[test]
fn split_lines_multi() {
let text = WeightedTextBlock(vec![
WeightedText::from("this is a slightly long line"),
WeightedText::from("another chunk"),
WeightedText::from("yet some other piece"),
]);
let text = WeightedTextBlock {
text: vec![
WeightedText::from("this is a slightly long line"),
WeightedText::from("another chunk"),
WeightedText::from("yet some other piece"),
],
width: 0,
};
let lines = join_lines(text.split(10));
let expected = vec!["this is a", "slightly", "long line", "another", "chunk yet", "some other", "piece"];
assert_eq!(lines, expected);
}

#[test]
fn long_splits() {
let text = WeightedTextBlock(vec![
WeightedText::from("this is a slightly long line"),
WeightedText::from("another chunk"),
WeightedText::from("yet some other piece"),
]);
let text = WeightedTextBlock {
text: vec![
WeightedText::from("this is a slightly long line"),
WeightedText::from("another chunk"),
WeightedText::from("yet some other piece"),
],
width: 0,
};
let lines = join_lines(text.split(50));
let expected = vec!["this is a slightly long line another chunk yet some", "other piece"];
assert_eq!(lines, expected);
}

#[test]
fn prefixed_by_whitespace() {
let text = WeightedTextBlock(vec![WeightedText::from(" * bullet")]);
let text = WeightedTextBlock::from(" * bullet");
let lines = join_lines(text.split(50));
let expected = vec![" * bullet"];
assert_eq!(lines, expected);
}

#[test]
fn utf8_character() {
let text = WeightedTextBlock(vec![WeightedText::from("• A")]);
let text = WeightedTextBlock::from("• A");
let lines = join_lines(text.split(50));
let expected = vec!["• A"];
assert_eq!(lines, expected);
Expand All @@ -369,7 +386,7 @@ mod test {
#[test]
fn many_utf8_characters() {
let content = "█████ ██";
let text = WeightedTextBlock(vec![WeightedText::from(content)]);
let text = WeightedTextBlock::from(content);
let lines = join_lines(text.split(3));
let expected = vec!["███", "██", "██"];
assert_eq!(lines, expected);
Expand All @@ -378,7 +395,7 @@ mod test {
#[test]
fn no_whitespaces_ascii() {
let content = "X".repeat(10);
let text = WeightedTextBlock(vec![WeightedText::from(content)]);
let text = WeightedTextBlock::from(content);
let lines = join_lines(text.split(3));
let expected = vec!["XXX", "XXX", "XXX", "X"];
assert_eq!(lines, expected);
Expand All @@ -387,7 +404,7 @@ mod test {
#[test]
fn no_whitespaces_utf8() {
let content = "─".repeat(10);
let text = WeightedTextBlock(vec![WeightedText::from(content)]);
let text = WeightedTextBlock::from(content);
let lines = join_lines(text.split(3));
let expected = vec!["───", "───", "───", "─"];
assert_eq!(lines, expected);
Expand All @@ -396,7 +413,7 @@ mod test {
#[test]
fn wide_characters() {
let content = "Hello world";
let text = WeightedTextBlock(vec![WeightedText::from(content)]);
let text = WeightedTextBlock::from(content);
let lines = join_lines(text.split(10));
// Each word is 10 characters long
let expected = vec!["Hello", "world"];
Expand All @@ -411,6 +428,6 @@ mod test {
#[case::split_merged(&["hello".into(), Text::new(" ", TextStyle::default().bold()), Text::new("w", TextStyle::default().bold()), "orld".into()], 3)]
fn compaction(#[case] texts: &[Text], #[case] expected: usize) {
let block = WeightedTextBlock::from(texts.to_vec());
assert_eq!(block.0.len(), expected);
assert_eq!(block.text.len(), expected);
}
}
13 changes: 4 additions & 9 deletions src/presentation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
custom::OptionsConfig,
markdown::text::WeightedTextBlock,
markdown::text::{WeightedText, WeightedTextBlock},
media::image::Image,
render::properties::WindowSize,
style::{Color, Colors},
Expand Down Expand Up @@ -554,18 +554,13 @@ pub(crate) struct PresentationThemeMetadata {
/// A line of preformatted text to be rendered.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct BlockLine {
pub(crate) text: BlockLineText,
pub(crate) unformatted_length: u16,
pub(crate) prefix: WeightedText,
pub(crate) text: WeightedTextBlock,
pub(crate) block_length: u16,
pub(crate) block_color: Option<Color>,
pub(crate) alignment: Alignment,
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum BlockLineText {
Preformatted(String),
Weighted(WeightedTextBlock),
}

/// A render operation.
///
/// Render operations are primitives that allow the input markdown file to be decoupled with what
Expand Down
Loading

0 comments on commit fb53aa0

Please sign in to comment.