Refactor spans and markup

We now use mostly indexed spans into a source string.
Indexed Spans can still be resolved to a string slice when needed.
This commit is contained in:
Alexandre Bury 2018-01-13 10:36:56 -08:00
parent a9d9239fac
commit 39405ba1ec
17 changed files with 569 additions and 758 deletions

View File

@ -2,7 +2,7 @@ extern crate cursive;
use cursive::Cursive; use cursive::Cursive;
#[cfg(feature = "markdown")] #[cfg(feature = "markdown")]
use cursive::utils::markup::MarkdownText; use cursive::utils::markup::markdown;
use cursive::views::{Dialog, TextView}; use cursive::views::{Dialog, TextView};
// Make sure you compile with the `markdown` feature! // Make sure you compile with the `markdown` feature!
@ -13,14 +13,13 @@ fn main() {
let mut siv = Cursive::new(); let mut siv = Cursive::new();
#[cfg(feature = "markdown")] #[cfg(feature = "markdown")]
let text = MarkdownText("Isn't *that* **cool**?"); let text = markdown::parse("Isn't *that* **cool**?");
#[cfg(not(feature = "markdown"))] #[cfg(not(feature = "markdown"))]
let text = "Rebuild with --features markdown ;)"; let text = "Rebuild with --features markdown ;)";
siv.add_layer( siv.add_layer(
Dialog::around(TextView::styled(text).unwrap()) Dialog::around(TextView::new(text)).button("Hell yeah!", |s| s.quit()),
.button("Hell yeah!", |s| s.quit()),
); );
siv.run(); siv.run();

View File

@ -14,7 +14,7 @@ fn show_popup(siv: &mut Cursive) {
// Look for a view tagged "text". // Look for a view tagged "text".
// We _know_ it's there, so unwrap it. // We _know_ it's there, so unwrap it.
s.call_on_id("text", |view: &mut TextView| { s.call_on_id("text", |view: &mut TextView| {
let content = reverse(&view.get_content()); let content = reverse(view.get_content().source());
view.set_content(content); view.set_content(content);
}); });
}) })

View File

@ -136,10 +136,13 @@ impl<'a> Printer<'a> {
/// Call the given closure with a styled printer, /// Call the given closure with a styled printer,
/// that will apply the given style on prints. /// that will apply the given style on prints.
pub fn with_style<F>(&self, style: Style, f: F) pub fn with_style<F, T>(&self, style: T, f: F)
where where
F: FnOnce(&Printer), F: FnOnce(&Printer),
T: Into<Style>,
{ {
let style = style.into();
let color = style.color; let color = style.color;
let effects = style.effects; let effects = style.effects;

View File

@ -17,6 +17,12 @@ pub struct Style {
pub color: Option<ColorStyle>, pub color: Option<ColorStyle>,
} }
impl Default for Style {
fn default() -> Self {
Self::none()
}
}
impl Style { impl Style {
/// Returns a new `Style` that doesn't apply anything. /// Returns a new `Style` that doesn't apply anything.
pub fn none() -> Self { pub fn none() -> Self {

View File

@ -22,11 +22,13 @@ impl<'a> Chunk<'a> {
segment.seg.start += to_remove.length; segment.seg.start += to_remove.length;
segment.seg.width -= to_remove.width; segment.seg.width -= to_remove.width;
segment.text = &segment.text[to_remove.length..]; segment.text = &segment.text[to_remove.length..];
self.width -= to_remove.width;
break; break;
} else { } else {
// This segment is too small, so it'll disapear entirely. // This segment is too small, so it'll disapear entirely.
to_remove.length -= segment.seg.end - segment.seg.start; to_remove.length -= segment.seg.end - segment.seg.start;
to_remove.width -= segment.seg.width; to_remove.width -= segment.seg.width;
self.width -= segment.seg.width;
// Empty this segment // Empty this segment
segment.seg.start = segment.seg.end; segment.seg.start = segment.seg.end;

View File

@ -1,32 +1,34 @@
use super::Span;
use super::chunk::Chunk; use super::chunk::Chunk;
use super::segment::{Segment, SegmentWithText}; use super::segment::{Segment, SegmentWithText};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use utils::span::SpannedString;
use xi_unicode::LineBreakLeafIter; use xi_unicode::LineBreakLeafIter;
/// Iterator that returns non-breakable chunks of text. /// Iterator that returns non-breakable chunks of text.
/// ///
/// Works accross spans of text. /// Works accross spans of text.
pub struct ChunkIterator<'a, 'b> pub struct ChunkIterator<'a, T>
where where
'a: 'b, T: 'a,
{ {
/// Input that we want to split /// Input that we want to chunk.
spans: &'b [Span<'a>], source: &'a SpannedString<T>,
/// ID of the span we are processing.
current_span: usize, current_span: usize,
/// How much of the current span has been processed already. /// How much of the current span has been processed already.
offset: usize, offset: usize,
} }
impl<'a, 'b> ChunkIterator<'a, 'b> impl<'a, T> ChunkIterator<'a, T>
where where
'a: 'b, T: 'a,
{ {
pub fn new(spans: &'b [Span<'a>]) -> Self { /// Creates a new ChunkIterator on the given styled string.
pub fn new(source: &'a SpannedString<T>) -> Self {
ChunkIterator { ChunkIterator {
spans, source,
current_span: 0, current_span: 0,
offset: 0, offset: 0,
} }
@ -37,30 +39,31 @@ where
/// ///
/// These chunks may go accross spans (a single word may be broken into more /// These chunks may go accross spans (a single word may be broken into more
/// than one span, for instance if parts of it are marked up differently). /// than one span, for instance if parts of it are marked up differently).
impl<'a, 'b> Iterator for ChunkIterator<'a, 'b> impl<'a, T> Iterator for ChunkIterator<'a, T>
where where
'a: 'b, T: 'a,
{ {
type Item = Chunk<'b>; type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
// Protect agains empty spans if self.current_span >= self.source.spans_raw().len() {
while self.current_span < self.spans.len()
&& self.spans[self.current_span].text.is_empty()
{
self.current_span += 1;
}
if self.current_span >= self.spans.len() {
return None; return None;
} }
let mut span: &Span<'a> = &self.spans[self.current_span]; // Protect agains empty spans
if self.source.spans_raw()[self.current_span].is_empty() {
self.current_span += 1;
return self.next();
}
let mut span = &self.source.spans_raw()[self.current_span];
let mut span_text = span.content.resolve(self.source.source());
let mut total_width = 0; let mut total_width = 0;
// We'll use an iterator from xi-unicode to detect possible breaks. // We'll use an iterator from xi-unicode to detect possible breaks.
let mut iter = LineBreakLeafIter::new(&span.text, self.offset); let text = span.content.resolve(self.source.source());
let mut iter = LineBreakLeafIter::new(text, self.offset);
// We'll accumulate segments from spans. // We'll accumulate segments from spans.
let mut segments = Vec::new(); let mut segments = Vec::new();
@ -74,7 +77,7 @@ where
// Look at next possible break // Look at next possible break
// `hard_stop = true` means that the break is non-optional, // `hard_stop = true` means that the break is non-optional,
// like after a `\n`. // like after a `\n`.
let (pos, hard_stop) = iter.next(&span.text); let (pos, hard_stop) = iter.next(span_text);
// When xi-unicode reaches the end of a span, it returns a "fake" // When xi-unicode reaches the end of a span, it returns a "fake"
// break. To know if it's actually a true break, we need to give // break. To know if it's actually a true break, we need to give
@ -85,8 +88,11 @@ where
let (width, ends_with_space) = if pos == 0 { let (width, ends_with_space) = if pos == 0 {
// If pos = 0, we had a span before. // If pos = 0, we had a span before.
let prev_span = &self.spans[self.current_span - 1]; let prev_span =
(0, prev_span.text.ends_with(' ')) &self.source.spans_raw()[self.current_span - 1];
let prev_text =
prev_span.content.resolve(self.source.source());
(0, prev_text.ends_with(' '))
} else { } else {
// We actually got something. // We actually got something.
// Remember its width, and whether it ends with a space. // Remember its width, and whether it ends with a space.
@ -94,7 +100,7 @@ where
// (When a chunk ends with a space, we may compress it a bit // (When a chunk ends with a space, we may compress it a bit
// near the end of a row, so this information will be useful // near the end of a row, so this information will be useful
// later.) // later.)
let text = &span.text[self.offset..pos]; let text = &span_text[self.offset..pos];
(text.width(), text.ends_with(' ')) (text.width(), text.ends_with(' '))
}; };
@ -109,26 +115,30 @@ where
end: pos, end: pos,
width, width,
}, },
text: &span.text[self.offset..pos], text: &span_text[self.offset..pos],
}); });
} }
if pos == span.text.len() { if pos == span_text.len() {
// If we reached the end of the slice, // If we reached the end of the slice,
// we need to look at the next span first. // we need to look at the next span first.
self.current_span += 1; self.current_span += 1;
// Skip empty spans // Skip empty spans
while self.current_span < self.spans.len() while let Some(true) = self.source
&& self.spans[self.current_span].text.is_empty() .spans_raw()
{ .get(self.current_span)
.map(|span| {
span.content.resolve(self.source.source()).is_empty()
}) {
self.current_span += 1; self.current_span += 1;
} }
if self.current_span >= self.spans.len() { if self.current_span >= self.source.spans_raw().len() {
// If this was the last chunk, return as is! // If this was the last chunk, return as is!
// Well, make sure we don't end with a newline... // Well, make sure we don't end with a newline...
let hard_stop = hard_stop || span.text.ends_with('\n'); let text = span.content.resolve(self.source.source());
let hard_stop = hard_stop || text.ends_with('\n');
return Some(Chunk { return Some(Chunk {
width: total_width, width: total_width,
@ -138,7 +148,8 @@ where
}); });
} }
span = &self.spans[self.current_span]; span = &self.source.spans_raw()[self.current_span];
span_text = span.content.resolve(self.source.source());
self.offset = 0; self.offset = 0;
continue; continue;
} }

View File

@ -1,4 +1,3 @@
use super::Span;
use super::chunk::{Chunk, ChunkPart}; use super::chunk::{Chunk, ChunkPart};
use super::chunk_iterator::ChunkIterator; use super::chunk_iterator::ChunkIterator;
use super::prefix::prefix; use super::prefix::prefix;
@ -8,15 +7,16 @@ use super::segment_merge_iterator::SegmentMergeIterator;
use std::iter::Peekable; use std::iter::Peekable;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use utils::span::SpannedString;
/// Generates rows of text in constrainted width. /// Generates rows of text in constrainted width.
/// ///
/// Works on spans of text. /// Works on spans of text.
pub struct SpanLinesIterator<'a, 'b> pub struct LinesIterator<'a, T>
where where
'a: 'b, T: 'a,
{ {
iter: Peekable<ChunkIterator<'a, 'b>>, iter: Peekable<ChunkIterator<'a, T>>,
/// Available width /// Available width
width: usize, width: usize,
@ -26,24 +26,18 @@ where
chunk_offset: ChunkPart, chunk_offset: ChunkPart,
} }
impl<'a, 'b> SpanLinesIterator<'a, 'b> impl<'a, T> LinesIterator<'a, T> {
where
'a: 'b,
{
/// Creates a new iterator with the given content and width. /// Creates a new iterator with the given content and width.
pub fn new(spans: &'b [Span<'a>], width: usize) -> Self { pub fn new(source: &'a SpannedString<T>, width: usize) -> Self {
SpanLinesIterator { LinesIterator {
iter: ChunkIterator::new(spans).peekable(), iter: ChunkIterator::new(source).peekable(),
width, width,
chunk_offset: ChunkPart::default(), chunk_offset: ChunkPart::default(),
} }
} }
} }
impl<'a, 'b> Iterator for SpanLinesIterator<'a, 'b> impl<'a, T> Iterator for LinesIterator<'a, T> {
where
'a: 'b,
{
type Item = Row; type Item = Row;
fn next(&mut self) -> Option<Row> { fn next(&mut self) -> Option<Row> {

View File

@ -15,24 +15,6 @@ mod segment;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use self::lines_iterator::SpanLinesIterator; pub use self::lines_iterator::LinesIterator;
pub use self::row::Row; pub use self::row::Row;
pub use self::segment::Segment; pub use self::segment::Segment;
use std::borrow::Cow;
use theme::Style;
/// Input to the algorithm
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span<'a> {
/// Text for this span.
///
/// It can be either a reference to some input text,
/// or an owned string.
///
/// The owned string is mostly useful when parsing marked-up text that
/// contains escape codes.
pub text: Cow<'a, str>,
/// Style to apply to this span of text.
pub style: Style,
}

View File

@ -1,5 +1,5 @@
use super::{Segment, Span}; use super::Segment;
use std::borrow::Cow; use utils::span::{Span, SpannedString};
/// A list of segments representing a row of text /// A list of segments representing a row of text
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -11,20 +11,13 @@ pub struct Row {
} }
impl Row { impl Row {
/// Resolve the row indices into styled spans. /// Resolve the row indices into string slices and attributes.
pub fn resolve<'a: 'b, 'b>(&self, spans: &'b [Span<'a>]) -> Vec<Span<'b>> { pub fn resolve<'a, T>(
&self, source: &'a SpannedString<T>
) -> Vec<Span<'a, T>> {
self.segments self.segments
.iter() .iter()
.map(|seg| { .map(|seg| seg.resolve(source))
let span: &'b Span<'a> = &spans[seg.span_id];
let text: &'b str = &span.text;
let text: &'b str = &text[seg.start..seg.end];
Span {
text: Cow::Borrowed(text),
style: span.style,
}
})
.collect() .collect()
} }
} }

View File

@ -1,3 +1,5 @@
use utils::span::{Span, SpannedString};
/// Refers to a part of a span /// Refers to a part of a span
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Segment { pub struct Segment {
@ -18,6 +20,19 @@ impl Segment {
pub fn with_text<'a>(self, text: &'a str) -> SegmentWithText<'a> { pub fn with_text<'a>(self, text: &'a str) -> SegmentWithText<'a> {
SegmentWithText { text, seg: self } SegmentWithText { text, seg: self }
} }
/// Resolve this segment to a string slice and an attribute.
pub fn resolve<'a, T>(&self, source: &'a SpannedString<T>) -> Span<'a, T> {
let span = &source.spans_raw()[self.span_id];
let content = span.content.resolve(source.source());
let content = &content[self.start..self.end];
Span {
content,
attr: &span.attr,
}
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -1,348 +1,67 @@
use super::Span; use super::LinesIterator;
use super::SpanLinesIterator; use theme::{Effect, Style};
use super::chunk::Chunk; use utils::markup::StyledString;
use super::chunk_iterator::ChunkIterator; use utils::span::Span;
use super::row::Row;
use super::segment::Segment;
use std::borrow::Cow;
use theme::Style;
fn input() -> Vec<Span<'static>> { fn input() -> StyledString {
vec![ let mut text = StyledString::plain("I ");
Span { text.append(StyledString::styled("didn't", Effect::Bold));
text: Cow::Borrowed("A beautiful "), text.append(StyledString::plain(" say "));
style: Style::none(), text.append(StyledString::styled("half", Effect::Italic));
}, text.append(StyledString::plain(" the things people say I did."));
Span { text.append(StyledString::plain("\n"));
text: Cow::Borrowed("boat"), text.append(StyledString::plain(" - A. Einstein"));
style: Style::none(),
},
Span {
text: Cow::Borrowed(" isn't it?\nYes indeed, my "),
style: Style::none(),
},
Span {
text: Cow::Borrowed("Super"),
style: Style::none(),
},
Span {
text: Cow::Borrowed("Captain !"),
style: Style::none(),
},
]
}
#[cfg(feature = "markdown")] text
#[test]
fn test_lines_markdown() {
let input = r"
A beautiful *boat* isn't it?
Yes indeed, my **Super**Captain !";
let input_spans = ::utils::markup::markdown::parse(input);
let iter = SpanLinesIterator::new(&input_spans, 16);
let rows: Vec<Row> = iter.collect();
let output_spans: Vec<_> =
rows.iter().map(|row| row.resolve(&input_spans)).collect();
assert_eq!(
&output_spans[..],
&[
vec![
Span {
text: Cow::Borrowed("A beautiful "),
style: Style::none(),
},
Span {
text: Cow::Borrowed("boat"),
style: Style::from(Effect::Italic),
},
],
vec![
Span {
text: Cow::Borrowed("isn\'t it?"),
style: Style::none(),
},
],
vec![
Span {
text: Cow::Borrowed("Yes indeed, my "),
style: Style::none(),
},
],
vec![
Span {
text: Cow::Borrowed("Super"),
style: Style::from(Effect::Bold),
},
Span {
text: Cow::Borrowed("Captain "),
style: Style::none(),
},
Span {
text: Cow::Borrowed("!"),
style: Style::none(),
},
]
]
);
} }
#[test] #[test]
fn test_lines_iter() { fn test_line_breaks() {
let input = input(); let input = input();
let iter = SpanLinesIterator::new(&input, 16); let iter = LinesIterator::new(&input, 17);
let rows: Vec<Row> = iter.collect();
let spans: Vec<_> = rows.iter().map(|row| row.resolve(&input)).collect();
assert_eq!( let rows: Vec<_> = iter.map(|row| row.resolve(&input)).collect();
&spans[..],
&[
vec![
Span {
text: Cow::Borrowed("A beautiful "),
style: Style::none(),
},
Span {
text: Cow::Borrowed("boat"),
style: Style::none(),
},
],
vec![
Span {
text: Cow::Borrowed("isn\'t it?"),
style: Style::none(),
},
],
vec![
Span {
text: Cow::Borrowed("Yes indeed, my "),
style: Style::none(),
},
],
vec![
Span {
text: Cow::Borrowed("Super"),
style: Style::none(),
},
Span {
text: Cow::Borrowed("Captain !"),
style: Style::none(),
},
]
]
);
assert_eq!( assert_eq!(
&rows[..], &rows[..],
&[ &[
Row { vec![
segments: vec![ Span {
Segment { content: "I ",
span_id: 0, attr: &Style::none(),
start: 0, },
end: 12, Span {
width: 12, content: "didn't",
}, attr: &Style::from(Effect::Bold),
Segment { },
span_id: 1, Span {
start: 0, content: " say ",
end: 4, attr: &Style::none(),
width: 4, },
}, Span {
], content: "half",
width: 16, attr: &Style::from(Effect::Italic),
}, },
Row { ],
segments: vec![ vec![
Segment { Span {
span_id: 2, content: "the things people",
start: 1, attr: &Style::none(),
end: 10, },
width: 9, ],
}, vec![
], Span {
width: 9, content: "say I did.",
}, attr: &Style::none(),
Row { },
segments: vec![ ],
Segment { vec![
span_id: 2, Span {
start: 11, content: " - A. Einstein",
end: 26, attr: &Style::none(),
width: 15, },
}, ],
],
width: 15,
},
Row {
segments: vec![
Segment {
span_id: 3,
start: 0,
end: 5,
width: 5,
},
Segment {
span_id: 4,
start: 0,
end: 9,
width: 9,
},
],
width: 14,
}
]
);
}
#[test]
fn test_chunk_iter() {
let input = input();
let iter = ChunkIterator::new(&input);
let chunks: Vec<Chunk> = iter.collect();
assert_eq!(
&chunks[..],
&[
Chunk {
width: 2,
segments: vec![
Segment {
span_id: 0,
start: 0,
end: 2,
width: 2,
}.with_text("A "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 10,
segments: vec![
Segment {
span_id: 0,
start: 2,
end: 12,
width: 10,
}.with_text("beautiful "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 5,
segments: vec![
Segment {
span_id: 1,
start: 0,
end: 4,
width: 4,
}.with_text("boat"),
Segment {
span_id: 2,
start: 0,
end: 1,
width: 1,
}.with_text(" "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 6,
segments: vec![
// "isn't "
Segment {
span_id: 2,
start: 1,
end: 7,
width: 6,
}.with_text("isn't "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 3,
segments: vec![
// "it?\n"
Segment {
span_id: 2,
start: 7,
end: 11,
width: 3,
}.with_text("it?\n"),
],
hard_stop: true,
ends_with_space: false,
},
Chunk {
width: 4,
segments: vec![
// "Yes "
Segment {
span_id: 2,
start: 11,
end: 15,
width: 4,
}.with_text("Yes "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 8,
segments: vec![
// "indeed, "
Segment {
span_id: 2,
start: 15,
end: 23,
width: 8,
}.with_text("indeed, "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 3,
segments: vec![
// "my "
Segment {
span_id: 2,
start: 23,
end: 26,
width: 3,
}.with_text("my "),
],
hard_stop: false,
ends_with_space: true,
},
Chunk {
width: 14,
segments: vec![
// "Super"
Segment {
span_id: 3,
start: 0,
end: 5,
width: 5,
}.with_text("Super"),
// "Captain !"
Segment {
span_id: 4,
start: 0,
end: 9,
width: 9,
}.with_text("Captain !"),
],
hard_stop: false,
ends_with_space: false,
}
] ]
); );
} }

View File

@ -0,0 +1,89 @@
//! Parse various text markup formats.
//!
//! Each module is optional and relies on a feature.
#[cfg(feature = "markdown")]
pub mod markdown;
#[cfg(feature = "markdown")]
pub use self::markdown::MarkdownText;
use owning_ref::OwningHandle;
use owning_ref::StringRef;
use std::ops::Deref;
use theme::Style;
use utils::span::{StSpannedString;
/// A parsed string with markup style.
///
/// Contains both the source string, and parsed information indicating the
/// style to apply.
pub type StyledString = SpannedString<Style>;
pub type StyledIndexedSpan = IndexedSpan<Style>;
impl SpannedString<Style> {
/// Returns a plain StyledString without any style.
///
/// > You got no style, Dutch. You know that.
pub fn plain<S>(content: S) -> Self
where
S: Into<String>,
{
Self::styled(content, Style::none())
}
/// Creates a new `StyledString` using a single style for the entire text.
pub fn styled<S, T>(content: S, style: T) -> Self
where
S: Into<String>,
T: Into<Style>,
{
let content = content.into();
let spans = vec![
StyledIndexedSpan {
content: IndexedCow::Borrowed {
start: 0,
end: content.len(),
},
style: style.into(),
},
];
Self::new(content, spans)
}
/// Appends the given plain text to `self`.
pub fn append_plain<S>(&mut self, text: S)
where
S: Into<String>,
{
self.append(Self::plain(text));
}
/// Appends `text` to `self`, using `style`.
pub fn append_styled<S, T>(&mut self, text: S, style: T)
where
S: Into<String>,
T: Into<Style>,
{
self.append(Self::styled(text, style));
}
/// Appends the given plain text to `self`.
pub fn append_plain<S>(&mut self, text: S)
where
S: Into<String>,
{
self.append(Self::plain(text));
}
/// Appends `text` to `self`, using `style`.
pub fn append_styled<S, T>(&mut self, text: S, style: T)
where
S: Into<String>,
T: Into<Style>,
{
self.append(Self::styled(text, style));
}
}

View File

@ -5,55 +5,27 @@
extern crate pulldown_cmark; extern crate pulldown_cmark;
use self::pulldown_cmark::{Event, Tag}; use self::pulldown_cmark::{Event, Tag};
use std::borrow::Cow;
use theme::{Effect, Style}; use theme::{Effect, Style};
use utils::lines::spans::Span; use utils::markup::{StyledString, StyledIndexedSpan};
use utils::span::IndexedCow;
/// `Markup` trait implementation for markdown text. /// Parses the given string as markdown text.
/// pub fn parse<S>(input: S) -> StyledString
/// Requires the `markdown` feature.
pub struct Markdown;
impl super::Markup for Markdown {
type Error = ();
fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error> {
Ok(parse(input))
}
}
/// Thin wrapper around text that should be parsed as Markdown.
///
/// This does not parse the text here, but indicates how it should be parsed.
///
/// # Examples
///
/// ```rust
/// // Can use `&str`
/// let text = MarkdownText("*Markdown* text!");
///
/// // Or `String`
/// let text = MarkdownText(String::from("*Right __here__!"));
/// ```
pub struct MarkdownText<S>(pub S)
where
S: Into<String>;
impl<S> super::MarkupText for MarkdownText<S>
where where
S: Into<String>, S: Into<String>,
{ {
type M = Markdown; let input = input.into();
fn to_string(self) -> String { let spans = parse_spans(&input);
self.0.into()
} StyledString::new(input, spans)
} }
/// Iterator that parse a markdown text and outputs styled spans. /// Iterator that parse a markdown text and outputs styled spans.
pub struct Parser<'a> { pub struct Parser<'a> {
first: bool, first: bool,
stack: Vec<Style>, stack: Vec<Style>,
input: &'a str,
parser: pulldown_cmark::Parser<'a>, parser: pulldown_cmark::Parser<'a>,
} }
@ -61,23 +33,21 @@ impl<'a> Parser<'a> {
/// Creates a new parser with the given input text. /// Creates a new parser with the given input text.
pub fn new(input: &'a str) -> Self { pub fn new(input: &'a str) -> Self {
Parser { Parser {
input,
first: true, first: true,
parser: pulldown_cmark::Parser::new(input), parser: pulldown_cmark::Parser::new(input),
stack: Vec::new(), stack: Vec::new(),
} }
} }
fn literal_string<'b>(&self, text: String) -> Span<'b> { /// Creates a new span with the given value
Span { fn literal<S>(&self, text: S) -> StyledIndexedSpan
text: Cow::Owned(text), where
style: Style::merge(&self.stack), S: Into<String>,
} {
} StyledIndexedSpan {
content: IndexedCow::Owned(text.into()),
fn literal<'b>(&self, text: &'b str) -> Span<'b> { attr: Style::merge(&self.stack),
Span {
text: Cow::Borrowed(text),
style: Style::merge(&self.stack),
} }
} }
} }
@ -87,7 +57,7 @@ fn header(level: usize) -> &'static str {
} }
impl<'a> Iterator for Parser<'a> { impl<'a> Iterator for Parser<'a> {
type Item = Span<'a>; type Item = StyledIndexedSpan;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
loop { loop {
@ -103,7 +73,7 @@ impl<'a> Iterator for Parser<'a> {
self.stack.push(Style::from(Effect::Italic)) self.stack.push(Style::from(Effect::Italic))
} }
Tag::Header(level) => { Tag::Header(level) => {
return Some(self.literal_string(format!( return Some(self.literal(format!(
"{} ", "{} ",
header(level as usize) header(level as usize)
))) )))
@ -123,10 +93,7 @@ impl<'a> Iterator for Parser<'a> {
Tag::Paragraph if self.first => self.first = false, Tag::Paragraph if self.first => self.first = false,
Tag::Header(_) => return Some(self.literal("\n\n")), Tag::Header(_) => return Some(self.literal("\n\n")),
Tag::Link(link, _) => { Tag::Link(link, _) => {
return Some(self.literal_string(format!( return Some(self.literal(format!("]({})", link)))
"]({})",
link
)))
} }
Tag::Code => return Some(self.literal("```")), Tag::Code => return Some(self.literal("```")),
Tag::Emphasis | Tag::Strong => { Tag::Emphasis | Tag::Strong => {
@ -142,9 +109,9 @@ impl<'a> Iterator for Parser<'a> {
| Event::Html(text) | Event::Html(text)
| Event::Text(text) => { | Event::Text(text) => {
// Return something! // Return something!
return Some(Span { return Some(StyledIndexedSpan {
text, content: IndexedCow::from_cow(text, self.input),
style: Style::merge(&self.stack), attr: Style::merge(&self.stack),
}); });
} }
} }
@ -155,7 +122,7 @@ impl<'a> Iterator for Parser<'a> {
/// Parse the given markdown text into a list of spans. /// Parse the given markdown text into a list of spans.
/// ///
/// This is a shortcut for `Parser::new(input).collect()`. /// This is a shortcut for `Parser::new(input).collect()`.
pub fn parse<'a>(input: &'a str) -> Vec<Span<'a>> { pub fn parse_spans<'a>(input: &'a str) -> Vec<StyledIndexedSpan> {
Parser::new(input).collect() Parser::new(input).collect()
// Parser::new(input).inspect(|span| eprintln!("{:?}", span)).collect() // Parser::new(input).inspect(|span| eprintln!("{:?}", span)).collect()
} }
@ -163,6 +130,7 @@ pub fn parse<'a>(input: &'a str) -> Vec<Span<'a>> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use utils::span::Span;
#[test] #[test]
fn test_parse() { fn test_parse() {
@ -170,43 +138,44 @@ mod tests {
Attention Attention
==== ====
I *really* love __Cursive__!"; I *really* love __Cursive__!";
let spans = parse(input); let spans = parse_spans(input);
let spans: Vec<_> = spans.iter().map(|span| span.resolve(input)).collect();
// println!("{:?}", spans); // println!("{:?}", spans);
assert_eq!( assert_eq!(
&spans[..], &spans[..],
&[ &[
Span { Span {
text: Cow::Borrowed("# "), content: "# ",
style: Style::none(), attr: &Style::none(),
}, },
Span { Span {
text: Cow::Borrowed("Attention"), content: "Attention",
style: Style::none(), attr: &Style::none(),
}, },
Span { Span {
text: Cow::Borrowed("\n\n"), content: "\n\n",
style: Style::none(), attr: &Style::none(),
}, },
Span { Span {
text: Cow::Borrowed("I "), content: "I ",
style: Style::none(), attr: &Style::none(),
}, },
Span { Span {
text: Cow::Borrowed("really"), content: "really",
style: Style::from(Effect::Italic), attr: &Style::from(Effect::Italic),
}, },
Span { Span {
text: Cow::Borrowed(" love "), content: " love ",
style: Style::none(), attr: &Style::none(),
}, },
Span { Span {
text: Cow::Borrowed("Cursive"), content: "Cursive",
style: Style::from(Effect::Bold), attr: &Style::from(Effect::Bold),
}, },
Span { Span {
text: Cow::Borrowed("!"), content: "!",
style: Style::none(), attr: &Style::none(),
} }
] ]
); );

View File

@ -5,109 +5,28 @@
#[cfg(feature = "markdown")] #[cfg(feature = "markdown")]
pub mod markdown; pub mod markdown;
#[cfg(feature = "markdown")]
pub use self::markdown::MarkdownText;
use owning_ref::OwningHandle;
use owning_ref::StringRef;
use std::borrow::Cow;
use std::ops::Deref;
use theme::Style; use theme::Style;
use utils::lines::spans::Span;
/// Trait for parsing text into styled spans. use utils::span::{SpannedString, IndexedSpan, Span};
pub trait Markup {
/// Possible error happening when parsing.
type Error;
/// Parses text and return the styled spans. /// A parsed string with markup style.
fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error>;
/// Returns a string and its parsed spans.
///
/// Generates a self-borrowing struct containing the source string, as well
/// as the styled spans borrowing this string.
fn make_handle<S>(input: S) -> Result<StyledHandle, Self::Error>
where
S: Into<String>,
{
let input = input.into();
OwningHandle::try_new(StringRef::new(input), |input| {
Self::parse(unsafe { &*input })
})
}
}
/// Thin wrapper around a string, with a markup format.
/// ///
/// This only wraps the text and indicates how it should be parsed; /// Contains both the source string, and parsed information indicating the
/// it does not parse the text itself. /// style to apply.
pub trait MarkupText {
/// Markup format to use to parse the string.
type M: Markup;
/// Access the inner string.
fn to_string(self) -> String;
}
/// Unwrapped text gets the "Plain" markup for free.
impl<S: Into<String>> MarkupText for S {
type M = Plain;
fn to_string(self) -> String {
self.into()
}
}
/// Dummy `Markup` implementation that returns the text as-is.
pub struct Plain;
impl Markup for Plain {
type Error = ();
fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error> {
Ok(if input.is_empty() {
Vec::new()
} else {
vec![
Span {
text: Cow::Borrowed(input),
style: Style::none(),
},
]
})
}
}
/// Holds both parsed spans, and the input string they borrow.
/// ///
/// This is used to pass around a parsed string. /// **Note**: due to limitations in rustdoc, you will need to read the
pub type StyledHandle = OwningHandle<StringRef, Vec<Span<'static>>>; /// documentation from the [`SpannedString`] page.
///
/// [`SpannedString`]: ../span/struct.SpannedString.html
pub type StyledString = SpannedString<Style>;
/// A String that parses a markup language. /// Indexes a span into a source string.
pub struct StyledString { pub type StyledIndexedSpan = IndexedSpan<Style>;
content: Option<StyledHandle>,
}
impl StyledString { /// A resolved styled span borrowing its source string.
/// Creates a new styled string, parsing the given content. pub type StyledSpan<'a> = Span<'a, Style>;
///
/// # Examples
///
/// ```rust
/// # use cursive::utils::markup::StyledString;
/// let styled_string = StyledString::new("*plain* text");
/// ```
pub fn new<T>(content: T) -> Result<Self, <T::M as Markup>::Error>
where
T: MarkupText,
{
let content = content.to_string();
let content = Some(T::M::make_handle(content)?);
Ok(StyledString { content })
}
impl SpannedString<Style> {
/// Returns a plain StyledString without any style. /// Returns a plain StyledString without any style.
/// ///
/// > You got no style, Dutch. You know that. /// > You got no style, Dutch. You know that.
@ -115,81 +34,35 @@ impl StyledString {
where where
S: Into<String>, S: Into<String>,
{ {
Self::new(content).unwrap() Self::styled(content, Style::none())
} }
/// Sets the content of this string. /// Creates a new `StyledString` using a single style for the entire text.
/// pub fn styled<S, T>(content: S, style: T) -> Self
/// # Examples
///
/// ```rust
/// # use cursive::utils::markup::StyledString;
/// # let mut styled_string = StyledString::new("").unwrap();
/// styled_string.set_content("*plain* text").unwrap();
/// ```
pub fn set_content<T>(
&mut self, content: T
) -> Result<(), <<T as MarkupText>::M as Markup>::Error>
where where
T: MarkupText, S: Into<String>,
T: Into<Style>,
{ {
let content = content.to_string(); let content = content.into();
let style = style.into();
self.content = Some(T::M::make_handle(content)?); Self::single_span(content, style)
Ok(())
} }
/// Sets the content of this string to plain text. /// Appends the given plain text to `self`.
pub fn set_plain<S>(&mut self, content: S) pub fn append_plain<S>(&mut self, text: S)
where where
S: Into<String>, S: Into<String>,
{ {
self.set_content(content).unwrap(); self.append(Self::plain(text));
} }
/// Append `content` to the end. /// Appends `text` to `self`, using `style`.
/// pub fn append_styled<S, T>(&mut self, text: S, style: T)
/// Re-parse everything after.
pub fn append_content<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where where
T: MarkupText, S: Into<String>,
T: Into<Style>,
{ {
self.with_content::<T::M, _, _>(|c| c.push_str(&content.to_string())) self.append(Self::styled(text, style));
}
/// Run a closure on the text content.
///
/// And re-parse everything after.
pub fn with_content<M, F, O>(&mut self, f: F) -> Result<O, M::Error>
where
M: Markup,
F: FnOnce(&mut String) -> O,
{
// Get hold of the StyledHandle
let content = self.content.take().unwrap();
// Get the inner String
let mut content = content.into_inner().into_inner();
// Do what we have to do
let out = f(&mut content);
// And re-parse everything
self.content = Some(M::make_handle(content)?);
Ok(out)
}
/// Gives access to the parsed styled spans.
pub fn spans<'a>(&'a self) -> &'a [Span<'a>] {
&self.content.as_ref().unwrap()
}
}
impl Deref for StyledString {
type Target = str;
fn deref(&self) -> &str {
&self.content.as_ref().unwrap().owner()
} }
} }

View File

@ -1,6 +1,7 @@
//! Toolbox to make text layout easier. //! Toolbox to make text layout easier.
mod reader; mod reader;
pub mod span;
pub mod lines; pub mod lines;
pub mod markup; pub mod markup;

219
src/utils/span.rs Normal file
View File

@ -0,0 +1,219 @@
//! Work with spans of text.
//!
//! This module defines various structs describing a span of text from a
//! larger string.
use std::borrow::Cow;
/// A string with associated spans.
///
/// Each span has an associated attribute `T`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpannedString<T> {
source: String,
spans: Vec<IndexedSpan<T>>,
}
impl<S, T> From<S> for SpannedString<T>
where
S: Into<String>,
T: Default,
{
fn from(value: S) -> Self {
Self::single_span(value.into(), T::default())
}
}
impl<T> SpannedString<T> {
/// Creates a new `SpannedString` manually.
///
/// It is not recommended to use this directly.
/// Instead, look for methods like `Markdown::parse`.
pub fn new<S>(source: S, spans: Vec<IndexedSpan<T>>) -> Self
where
S: Into<String>,
{
let source = source.into();
// Make sure the spans are within bounds.
// This should disapear when compiled in release mode.
for span in &spans {
if let IndexedCow::Borrowed { start: _, end } = span.content {
assert!(end <= source.len());
}
}
SpannedString { source, spans }
}
/// Returns a new SpannedString with a single span.
pub fn single_span(source: String, attr: T) -> Self {
let spans = vec![
IndexedSpan {
content: IndexedCow::Borrowed {
start: 0,
end: source.len(),
},
attr,
},
];
Self::new(source, spans)
}
/// Appends the given `StyledString` to `self`.
pub fn append<S>(&mut self, other: S)
where
S: Into<Self>,
{
let other = other.into();
self.append_raw(other.source, other.spans);
}
/// Appends `content` and its corresponding spans to the end.
///
/// It is not recommended to use this directly;
/// instead, look at the `append` method.
pub fn append_raw(&mut self, source: String, spans: Vec<IndexedSpan<T>>) {
let offset = self.source.len();
let mut spans = spans;
for span in &mut spans {
span.content.offset(offset);
}
self.source.push_str(&source);
self.spans.append(&mut spans);
}
/// Gives access to the parsed styled spans.
pub fn spans<'a>(&'a self) -> Vec<Span<'a, T>> {
self.spans
.iter()
.map(|span| span.resolve(&self.source))
.collect()
}
/// Returns a reference to the indexed spans.
pub fn spans_raw(&self) -> &[IndexedSpan<T>] {
&self.spans
}
/// Returns a reference to the source string.
///
/// This is the non-parsed string.
pub fn source(&self) -> &str {
&self.source
}
/// Returns `true` if self is empty.
pub fn is_empty(&self) -> bool {
self.source.is_empty() || self.spans.is_empty()
}
}
/// An indexed span with an associated attribute.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedSpan<T> {
/// Content of the span.
pub content: IndexedCow,
/// Attribute applied to the span.
pub attr: T,
}
/// A resolved span borrowing its source string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span<'a, T: 'a> {
/// Content of this span.
pub content: &'a str,
/// Attribute associated to this span.
pub attr: &'a T,
}
impl<T> IndexedSpan<T> {
/// Resolve the span to a string slice and an attribute.
pub fn resolve<'a>(&'a self, source: &'a str) -> Span<'a, T>
where
T: 'a,
{
Span {
content: self.content.resolve(source),
attr: &self.attr,
}
}
/// Returns `true` if `self` is an empty span.
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
}
/// A span of text that can be either owned, or indexed in another String.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IndexedCow {
/// Indexes content in a separate string.
Borrowed {
/// Byte offset of the beginning of the span (inclusive)
start: usize,
/// Byte offset of the end of the span (exclusive)
end: usize,
},
/// Owns its content.
Owned(String),
}
impl IndexedCow {
/// Resolve the span to a string slice.
pub fn resolve<'a>(&'a self, source: &'a str) -> &'a str {
match *self {
IndexedCow::Borrowed { start, end } => &source[start..end],
IndexedCow::Owned(ref content) => &content,
}
}
/// Returns an indexed view of the given item.
///
/// **Note**: it is assumed `cow`, if borrowed, is a substring of `source`.
pub fn from_cow(cow: Cow<str>, source: &str) -> Self {
match cow {
Cow::Owned(value) => IndexedCow::Owned(value),
Cow::Borrowed(value) => {
let source_pos = source.as_ptr() as usize;
let value_pos = value.as_ptr() as usize;
// Make sure `value` is indeed a substring of `source`
assert!(value_pos >= source_pos);
assert!(value_pos + value.len() <= source_pos + source.len());
let start = value_pos - source_pos;
let end = start + value.len();
IndexedCow::Borrowed { start, end }
}
}
}
/// Returns `ŧrue` if this represents an empty span.
pub fn is_empty(&self) -> bool {
match *self {
IndexedCow::Borrowed { start, end } => start == end,
IndexedCow::Owned(ref content) => content.is_empty(),
}
}
/// If `self` is borrowed, offset its indices by the given value.
///
/// Useful to update spans when concatenating sources.
pub fn offset(&mut self, offset: usize) {
if let IndexedCow::Borrowed {
ref mut start,
ref mut end,
} = *self
{
*start += offset;
*end += offset;
}
}
}

View File

@ -9,8 +9,9 @@ use std::ops::Deref;
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
use std::sync::Arc; use std::sync::Arc;
use theme::Effect; use theme::Effect;
use utils::lines::spans::{Row, SpanLinesIterator}; use unicode_width::UnicodeWidthStr;
use utils::markup::{Markup, MarkupText, StyledString}; use utils::lines::spans::{LinesIterator, Row};
use utils::markup::StyledString;
use vec::Vec2; use vec::Vec2;
use view::{ScrollBase, ScrollStrategy, SizeCache, View}; use view::{ScrollBase, ScrollStrategy, SizeCache, View};
@ -29,7 +30,7 @@ use view::{ScrollBase, ScrollStrategy, SizeCache, View};
/// ///
/// // Later, possibly in a different thread /// // Later, possibly in a different thread
/// content.set_content("new content"); /// content.set_content("new content");
/// assert!(content.get_content().contains("new")); /// assert!(content.get_content().source().contains("new"));
/// ``` /// ```
#[derive(Clone)] #[derive(Clone)]
pub struct TextContent { pub struct TextContent {
@ -37,35 +38,29 @@ pub struct TextContent {
} }
impl TextContent { impl TextContent {
/// Creates a new text content around the given value.
pub fn new<S>(content: S) -> Self
where
S: Into<String>,
{
Self::styled(content).unwrap()
}
/// Creates a new text content around the given value. /// Creates a new text content around the given value.
/// ///
/// Parses the given value. /// Parses the given value.
pub fn styled<T>(content: T) -> Result<Self, <T::M as Markup>::Error> pub fn new<S>(content: S) -> Self
where where
T: MarkupText, S: Into<StyledString>,
{ {
let content = StyledString::new(content)?; let content = content.into();
Ok(TextContent { TextContent {
content: Arc::new(Mutex::new(TextContentInner { content: Arc::new(Mutex::new(TextContentInner {
content, content,
size_cache: None, size_cache: None,
})), })),
}) }
} }
} }
/// A reference to the text content. /// A reference to the text content.
/// ///
/// It implements `Deref<Target=str>`. /// This can be deref'ed into a [`StyledString`].
///
/// [`StyledString`]: ../utils/markup/type.StyledString.html
/// ///
/// This keeps the content locked. Do not store this! /// This keeps the content locked. Do not store this!
pub struct TextContentRef { pub struct TextContentRef {
@ -76,42 +71,29 @@ pub struct TextContentRef {
} }
impl Deref for TextContentRef { impl Deref for TextContentRef {
type Target = str; type Target = StyledString;
fn deref(&self) -> &str { fn deref(&self) -> &StyledString {
&self.handle.content &self.handle.content
} }
} }
impl TextContent { impl TextContent {
/// Replaces the content with the given value. /// Replaces the content with the given value.
pub fn set_content<S>(&mut self, content: S) pub fn set_content<S>(&mut self, content: S)
where where
S: Into<String>, S: Into<StyledString>,
{ {
self.with_content(|c| c.set_plain(content)); self.with_content(|c| *c = content.into());
}
/// Replaces the content with the given value.
///
/// The given markup text will be parsed.
pub fn set_markup<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText,
{
self.with_content(|c| c.set_content(content))
} }
/// Append `content` to the end of a `TextView`. /// Append `content` to the end of a `TextView`.
pub fn append_content<T>( pub fn append<S>(&mut self, content: S)
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where where
T: MarkupText, S: Into<StyledString>,
{ {
self.with_content(|c| c.append_content(content)) self.with_content(|c| c.append(content))
} }
/// Returns a reference to the content. /// Returns a reference to the content.
@ -136,6 +118,11 @@ impl TextContent {
} }
} }
/// Internel representation of the content for a TextView.
///
/// This is mostly just a StyledString.
///
/// Can be shared (through a `Arc<Mutex>`).
struct TextContentInner { struct TextContentInner {
// content: String, // content: String,
content: StyledString, content: StyledString,
@ -145,6 +132,7 @@ struct TextContentInner {
} }
impl TextContentInner { impl TextContentInner {
/// From a shareable content (Arc + Mutex), return a
fn get_content(content: &Arc<Mutex<TextContentInner>>) -> TextContentRef { fn get_content(content: &Arc<Mutex<TextContentInner>>) -> TextContentRef {
let arc_ref: ArcRef<Mutex<TextContentInner>> = let arc_ref: ArcRef<Mutex<TextContentInner>> =
ArcRef::new(Arc::clone(content)); ArcRef::new(Arc::clone(content));
@ -197,26 +185,9 @@ impl TextView {
/// Creates a new TextView with the given content. /// Creates a new TextView with the given content.
pub fn new<S>(content: S) -> Self pub fn new<S>(content: S) -> Self
where where
S: Into<String>, S: Into<StyledString>,
{ {
Self::styled(content).unwrap() Self::new_with_content(TextContent::new(content))
}
/// Creates a new TextView by parsing the given content.
///
/// # Examples
///
/// ```rust,ignore
/// # use cursive::views::TextView;
/// use cursive::utils::markup::MarkdownText;
/// // This will require the `markdown` feature!
/// let view = TextView::styled(MarkdownText("**Bold** text"));
/// ```
pub fn styled<T>(content: T) -> Result<Self, <T::M as Markup>::Error>
where
T: MarkupText,
{
TextContent::styled(content).map(TextView::new_with_content)
} }
/// Creates a new TextView using the given `Arc<Mutex<String>>`. /// Creates a new TextView using the given `Arc<Mutex<String>>`.
@ -233,7 +204,7 @@ impl TextView {
/// ///
/// // Later, possibly in a different thread /// // Later, possibly in a different thread
/// content.set_content("new content"); /// content.set_content("new content");
/// assert!(content.get_content().contains("new")); /// assert!(content.get_content().source().contains("new"));
/// ``` /// ```
pub fn new_with_content(content: TextContent) -> Self { pub fn new_with_content(content: TextContent) -> Self {
TextView { TextView {
@ -318,58 +289,25 @@ impl TextView {
where where
S: Into<String>, S: Into<String>,
{ {
self.markup(content).unwrap() self.with(|s| s.set_content(content))
}
/// Replace the text in this view.
///
/// Parse the given markup text.
///
/// Chainable variant.
pub fn markup<T>(self, content: T) -> Result<Self, <T::M as Markup>::Error>
where
T: MarkupText,
{
self.try_with(|s| s.set_markup(content))
} }
/// Replace the text in this view. /// Replace the text in this view.
pub fn set_content<S>(&mut self, content: S) pub fn set_content<S>(&mut self, content: S)
where where
S: Into<String>, S: Into<StyledString>,
{ {
self.set_markup(content).unwrap(); self.content.lock().unwrap().content = content.into();
}
/// Replace the text in this view.
///
/// Parses the given markup text.
pub fn set_markup<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText,
{
self.content.lock().unwrap().content.set_content(content)?;
self.invalidate(); self.invalidate();
Ok(())
} }
/// Append `content` to the end of a `TextView`. /// Append `content` to the end of a `TextView`.
pub fn append_content<T>( pub fn append<S>(&mut self, content: S)
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where where
T: MarkupText, S: Into<StyledString>,
{ {
self.content self.content.lock().unwrap().content.append(content.into());
.lock()
.unwrap()
.content
.append_content(content)?;
self.invalidate(); self.invalidate();
Ok(())
} }
/// Returns the current text in this view. /// Returns the current text in this view.
@ -456,8 +394,7 @@ impl TextView {
// First attempt: naively hope that we won't need a scrollbar_width // First attempt: naively hope that we won't need a scrollbar_width
// (This means we try to use the entire available width for text). // (This means we try to use the entire available width for text).
self.rows = self.rows = LinesIterator::new(&content.content, size.x).collect();
SpanLinesIterator::new(content.content.spans(), size.x).collect();
// Width taken by the scrollbar. Without a scrollbar, it's 0. // Width taken by the scrollbar. Without a scrollbar, it's 0.
let mut scrollbar_width = 0; let mut scrollbar_width = 0;
@ -473,8 +410,7 @@ impl TextView {
}; };
self.rows = self.rows =
SpanLinesIterator::new(content.content.spans(), available) LinesIterator::new(&content.content, available).collect();
.collect();
if self.rows.is_empty() && !content.content.is_empty() { if self.rows.is_empty() && !content.content.is_empty() {
// We have some content, we we didn't find any row for it? // We have some content, we we didn't find any row for it?
@ -526,10 +462,10 @@ impl View for TextView {
let l = row.width; let l = row.width;
let mut x = self.align.h.get_offset(l, printer.size.x); let mut x = self.align.h.get_offset(l, printer.size.x);
for span in row.resolve(content.content.spans()) { for span in row.resolve(&content.content) {
printer.with_style(span.style, |printer| { printer.with_style(*span.attr, |printer| {
printer.print((x, 0), &span.text); printer.print((x, 0), &span.content);
x += span.text.len(); x += span.content.width();
}); });
} }
// let text = &content.content[row.start..row.end]; // let text = &content.content[row.start..row.end];