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

View File

@ -14,7 +14,7 @@ fn show_popup(siv: &mut Cursive) {
// Look for a view tagged "text".
// We _know_ it's there, so unwrap it.
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 File

@ -136,10 +136,13 @@ impl<'a> Printer<'a> {
/// Call the given closure with a styled printer,
/// 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
F: FnOnce(&Printer),
T: Into<Style>,
{
let style = style.into();
let color = style.color;
let effects = style.effects;

View File

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

View File

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

View File

@ -1,32 +1,34 @@
use super::Span;
use super::chunk::Chunk;
use super::segment::{Segment, SegmentWithText};
use unicode_width::UnicodeWidthStr;
use utils::span::SpannedString;
use xi_unicode::LineBreakLeafIter;
/// Iterator that returns non-breakable chunks of text.
///
/// Works accross spans of text.
pub struct ChunkIterator<'a, 'b>
pub struct ChunkIterator<'a, T>
where
'a: 'b,
T: 'a,
{
/// Input that we want to split
spans: &'b [Span<'a>],
/// Input that we want to chunk.
source: &'a SpannedString<T>,
/// ID of the span we are processing.
current_span: usize,
/// How much of the current span has been processed already.
offset: usize,
}
impl<'a, 'b> ChunkIterator<'a, 'b>
impl<'a, T> ChunkIterator<'a, T>
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 {
spans,
source,
current_span: 0,
offset: 0,
}
@ -37,30 +39,31 @@ where
///
/// 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).
impl<'a, 'b> Iterator for ChunkIterator<'a, 'b>
impl<'a, T> Iterator for ChunkIterator<'a, T>
where
'a: 'b,
T: 'a,
{
type Item = Chunk<'b>;
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
// Protect agains empty spans
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() {
if self.current_span >= self.source.spans_raw().len() {
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;
// 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.
let mut segments = Vec::new();
@ -74,7 +77,7 @@ where
// Look at next possible break
// `hard_stop = true` means that the break is non-optional,
// 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"
// 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 {
// If pos = 0, we had a span before.
let prev_span = &self.spans[self.current_span - 1];
(0, prev_span.text.ends_with(' '))
let prev_span =
&self.source.spans_raw()[self.current_span - 1];
let prev_text =
prev_span.content.resolve(self.source.source());
(0, prev_text.ends_with(' '))
} else {
// We actually got something.
// 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
// near the end of a row, so this information will be useful
// later.)
let text = &span.text[self.offset..pos];
let text = &span_text[self.offset..pos];
(text.width(), text.ends_with(' '))
};
@ -109,26 +115,30 @@ where
end: pos,
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,
// we need to look at the next span first.
self.current_span += 1;
// Skip empty spans
while self.current_span < self.spans.len()
&& self.spans[self.current_span].text.is_empty()
{
while let Some(true) = self.source
.spans_raw()
.get(self.current_span)
.map(|span| {
span.content.resolve(self.source.source()).is_empty()
}) {
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!
// 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 {
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;
continue;
}

View File

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

View File

@ -15,24 +15,6 @@ mod segment;
#[cfg(test)]
mod tests;
pub use self::lines_iterator::SpanLinesIterator;
pub use self::lines_iterator::LinesIterator;
pub use self::row::Row;
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 std::borrow::Cow;
use super::Segment;
use utils::span::{Span, SpannedString};
/// A list of segments representing a row of text
#[derive(Debug, Clone, PartialEq, Eq)]
@ -11,20 +11,13 @@ pub struct Row {
}
impl Row {
/// Resolve the row indices into styled spans.
pub fn resolve<'a: 'b, 'b>(&self, spans: &'b [Span<'a>]) -> Vec<Span<'b>> {
/// Resolve the row indices into string slices and attributes.
pub fn resolve<'a, T>(
&self, source: &'a SpannedString<T>
) -> Vec<Span<'a, T>> {
self.segments
.iter()
.map(|seg| {
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,
}
})
.map(|seg| seg.resolve(source))
.collect()
}
}

View File

@ -1,3 +1,5 @@
use utils::span::{Span, SpannedString};
/// Refers to a part of a span
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Segment {
@ -18,6 +20,19 @@ impl Segment {
pub fn with_text<'a>(self, text: &'a str) -> SegmentWithText<'a> {
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)]

View File

@ -1,348 +1,67 @@
use super::Span;
use super::SpanLinesIterator;
use super::chunk::Chunk;
use super::chunk_iterator::ChunkIterator;
use super::row::Row;
use super::segment::Segment;
use std::borrow::Cow;
use theme::Style;
use super::LinesIterator;
use theme::{Effect, Style};
use utils::markup::StyledString;
use utils::span::Span;
fn input() -> Vec<Span<'static>> {
vec![
Span {
text: Cow::Borrowed("A beautiful "),
style: Style::none(),
},
Span {
text: Cow::Borrowed("boat"),
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(),
},
]
}
fn input() -> StyledString {
let mut text = StyledString::plain("I ");
text.append(StyledString::styled("didn't", Effect::Bold));
text.append(StyledString::plain(" say "));
text.append(StyledString::styled("half", Effect::Italic));
text.append(StyledString::plain(" the things people say I did."));
text.append(StyledString::plain("\n"));
text.append(StyledString::plain(" - A. Einstein"));
#[cfg(feature = "markdown")]
#[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(),
},
]
]
);
text
}
#[test]
fn test_lines_iter() {
fn test_line_breaks() {
let input = input();
let iter = SpanLinesIterator::new(&input, 16);
let rows: Vec<Row> = iter.collect();
let spans: Vec<_> = rows.iter().map(|row| row.resolve(&input)).collect();
let iter = LinesIterator::new(&input, 17);
assert_eq!(
&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(),
},
]
]
);
let rows: Vec<_> = iter.map(|row| row.resolve(&input)).collect();
assert_eq!(
&rows[..],
&[
Row {
segments: vec![
Segment {
span_id: 0,
start: 0,
end: 12,
width: 12,
},
Segment {
span_id: 1,
start: 0,
end: 4,
width: 4,
},
],
width: 16,
},
Row {
segments: vec![
Segment {
span_id: 2,
start: 1,
end: 10,
width: 9,
},
],
width: 9,
},
Row {
segments: vec![
Segment {
span_id: 2,
start: 11,
end: 26,
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,
}
vec![
Span {
content: "I ",
attr: &Style::none(),
},
Span {
content: "didn't",
attr: &Style::from(Effect::Bold),
},
Span {
content: " say ",
attr: &Style::none(),
},
Span {
content: "half",
attr: &Style::from(Effect::Italic),
},
],
vec![
Span {
content: "the things people",
attr: &Style::none(),
},
],
vec![
Span {
content: "say I did.",
attr: &Style::none(),
},
],
vec![
Span {
content: " - A. Einstein",
attr: &Style::none(),
},
],
]
);
}

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;
use self::pulldown_cmark::{Event, Tag};
use std::borrow::Cow;
use theme::{Effect, Style};
use utils::lines::spans::Span;
use utils::markup::{StyledString, StyledIndexedSpan};
use utils::span::IndexedCow;
/// `Markup` trait implementation for markdown text.
///
/// 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>
/// Parses the given string as markdown text.
pub fn parse<S>(input: S) -> StyledString
where
S: Into<String>,
{
type M = Markdown;
let input = input.into();
fn to_string(self) -> String {
self.0.into()
}
let spans = parse_spans(&input);
StyledString::new(input, spans)
}
/// Iterator that parse a markdown text and outputs styled spans.
pub struct Parser<'a> {
first: bool,
stack: Vec<Style>,
input: &'a str,
parser: pulldown_cmark::Parser<'a>,
}
@ -61,23 +33,21 @@ impl<'a> Parser<'a> {
/// Creates a new parser with the given input text.
pub fn new(input: &'a str) -> Self {
Parser {
input,
first: true,
parser: pulldown_cmark::Parser::new(input),
stack: Vec::new(),
}
}
fn literal_string<'b>(&self, text: String) -> Span<'b> {
Span {
text: Cow::Owned(text),
style: Style::merge(&self.stack),
}
}
fn literal<'b>(&self, text: &'b str) -> Span<'b> {
Span {
text: Cow::Borrowed(text),
style: Style::merge(&self.stack),
/// Creates a new span with the given value
fn literal<S>(&self, text: S) -> StyledIndexedSpan
where
S: Into<String>,
{
StyledIndexedSpan {
content: IndexedCow::Owned(text.into()),
attr: Style::merge(&self.stack),
}
}
}
@ -87,7 +57,7 @@ fn header(level: usize) -> &'static str {
}
impl<'a> Iterator for Parser<'a> {
type Item = Span<'a>;
type Item = StyledIndexedSpan;
fn next(&mut self) -> Option<Self::Item> {
loop {
@ -103,7 +73,7 @@ impl<'a> Iterator for Parser<'a> {
self.stack.push(Style::from(Effect::Italic))
}
Tag::Header(level) => {
return Some(self.literal_string(format!(
return Some(self.literal(format!(
"{} ",
header(level as usize)
)))
@ -123,10 +93,7 @@ impl<'a> Iterator for Parser<'a> {
Tag::Paragraph if self.first => self.first = false,
Tag::Header(_) => return Some(self.literal("\n\n")),
Tag::Link(link, _) => {
return Some(self.literal_string(format!(
"]({})",
link
)))
return Some(self.literal(format!("]({})", link)))
}
Tag::Code => return Some(self.literal("```")),
Tag::Emphasis | Tag::Strong => {
@ -142,9 +109,9 @@ impl<'a> Iterator for Parser<'a> {
| Event::Html(text)
| Event::Text(text) => {
// Return something!
return Some(Span {
text,
style: Style::merge(&self.stack),
return Some(StyledIndexedSpan {
content: IndexedCow::from_cow(text, self.input),
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.
///
/// 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).inspect(|span| eprintln!("{:?}", span)).collect()
}
@ -163,6 +130,7 @@ pub fn parse<'a>(input: &'a str) -> Vec<Span<'a>> {
#[cfg(test)]
mod tests {
use super::*;
use utils::span::Span;
#[test]
fn test_parse() {
@ -170,43 +138,44 @@ mod tests {
Attention
====
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);
assert_eq!(
&spans[..],
&[
Span {
text: Cow::Borrowed("# "),
style: Style::none(),
content: "# ",
attr: &Style::none(),
},
Span {
text: Cow::Borrowed("Attention"),
style: Style::none(),
content: "Attention",
attr: &Style::none(),
},
Span {
text: Cow::Borrowed("\n\n"),
style: Style::none(),
content: "\n\n",
attr: &Style::none(),
},
Span {
text: Cow::Borrowed("I "),
style: Style::none(),
content: "I ",
attr: &Style::none(),
},
Span {
text: Cow::Borrowed("really"),
style: Style::from(Effect::Italic),
content: "really",
attr: &Style::from(Effect::Italic),
},
Span {
text: Cow::Borrowed(" love "),
style: Style::none(),
content: " love ",
attr: &Style::none(),
},
Span {
text: Cow::Borrowed("Cursive"),
style: Style::from(Effect::Bold),
content: "Cursive",
attr: &Style::from(Effect::Bold),
},
Span {
text: Cow::Borrowed("!"),
style: Style::none(),
content: "!",
attr: &Style::none(),
}
]
);

View File

@ -5,109 +5,28 @@
#[cfg(feature = "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 utils::lines::spans::Span;
/// Trait for parsing text into styled spans.
pub trait Markup {
/// Possible error happening when parsing.
type Error;
use utils::span::{SpannedString, IndexedSpan, Span};
/// Parses text and return the styled spans.
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.
/// A parsed string with markup style.
///
/// This only wraps the text and indicates how it should be parsed;
/// it does not parse the text itself.
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.
/// Contains both the source string, and parsed information indicating the
/// style to apply.
///
/// This is used to pass around a parsed string.
pub type StyledHandle = OwningHandle<StringRef, Vec<Span<'static>>>;
/// **Note**: due to limitations in rustdoc, you will need to read the
/// documentation from the [`SpannedString`] page.
///
/// [`SpannedString`]: ../span/struct.SpannedString.html
pub type StyledString = SpannedString<Style>;
/// A String that parses a markup language.
pub struct StyledString {
content: Option<StyledHandle>,
}
/// Indexes a span into a source string.
pub type StyledIndexedSpan = IndexedSpan<Style>;
impl StyledString {
/// Creates a new styled string, parsing the given content.
///
/// # 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 })
}
/// A resolved styled span borrowing its source string.
pub type StyledSpan<'a> = Span<'a, Style>;
impl SpannedString<Style> {
/// Returns a plain StyledString without any style.
///
/// > You got no style, Dutch. You know that.
@ -115,81 +34,35 @@ impl StyledString {
where
S: Into<String>,
{
Self::new(content).unwrap()
Self::styled(content, Style::none())
}
/// Sets the content of this string.
///
/// # 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>
/// Creates a new `StyledString` using a single style for the entire text.
pub fn styled<S, T>(content: S, style: T) -> Self
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)?);
Ok(())
Self::single_span(content, style)
}
/// Sets the content of this string to plain text.
pub fn set_plain<S>(&mut self, content: S)
/// Appends the given plain text to `self`.
pub fn append_plain<S>(&mut self, text: S)
where
S: Into<String>,
{
self.set_content(content).unwrap();
self.append(Self::plain(text));
}
/// Append `content` to the end.
///
/// Re-parse everything after.
pub fn append_content<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
/// Appends `text` to `self`, using `style`.
pub fn append_styled<S, T>(&mut self, text: S, style: T)
where
T: MarkupText,
S: Into<String>,
T: Into<Style>,
{
self.with_content::<T::M, _, _>(|c| c.push_str(&content.to_string()))
}
/// 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()
self.append(Self::styled(text, style));
}
}

View File

@ -1,6 +1,7 @@
//! Toolbox to make text layout easier.
mod reader;
pub mod span;
pub mod lines;
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::Arc;
use theme::Effect;
use utils::lines::spans::{Row, SpanLinesIterator};
use utils::markup::{Markup, MarkupText, StyledString};
use unicode_width::UnicodeWidthStr;
use utils::lines::spans::{LinesIterator, Row};
use utils::markup::StyledString;
use vec::Vec2;
use view::{ScrollBase, ScrollStrategy, SizeCache, View};
@ -29,7 +30,7 @@ use view::{ScrollBase, ScrollStrategy, SizeCache, View};
///
/// // Later, possibly in a different thread
/// content.set_content("new content");
/// assert!(content.get_content().contains("new"));
/// assert!(content.get_content().source().contains("new"));
/// ```
#[derive(Clone)]
pub struct TextContent {
@ -37,35 +38,29 @@ pub struct 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.
///
/// 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
T: MarkupText,
S: Into<StyledString>,
{
let content = StyledString::new(content)?;
let content = content.into();
Ok(TextContent {
TextContent {
content: Arc::new(Mutex::new(TextContentInner {
content,
size_cache: None,
})),
})
}
}
}
/// 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!
pub struct TextContentRef {
@ -76,42 +71,29 @@ pub struct TextContentRef {
}
impl Deref for TextContentRef {
type Target = str;
type Target = StyledString;
fn deref(&self) -> &str {
fn deref(&self) -> &StyledString {
&self.handle.content
}
}
impl TextContent {
/// Replaces the content with the given value.
pub fn set_content<S>(&mut self, content: S)
where
S: Into<String>,
S: Into<StyledString>,
{
self.with_content(|c| c.set_plain(content));
}
/// 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))
self.with_content(|c| *c = content.into());
}
/// Append `content` to the end of a `TextView`.
pub fn append_content<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
pub fn append<S>(&mut self, content: S)
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.
@ -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 {
// content: String,
content: StyledString,
@ -145,6 +132,7 @@ struct TextContentInner {
}
impl TextContentInner {
/// From a shareable content (Arc + Mutex), return a
fn get_content(content: &Arc<Mutex<TextContentInner>>) -> TextContentRef {
let arc_ref: ArcRef<Mutex<TextContentInner>> =
ArcRef::new(Arc::clone(content));
@ -197,26 +185,9 @@ impl TextView {
/// Creates a new TextView with the given content.
pub fn new<S>(content: S) -> Self
where
S: Into<String>,
S: Into<StyledString>,
{
Self::styled(content).unwrap()
}
/// 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)
Self::new_with_content(TextContent::new(content))
}
/// Creates a new TextView using the given `Arc<Mutex<String>>`.
@ -233,7 +204,7 @@ impl TextView {
///
/// // Later, possibly in a different thread
/// 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 {
TextView {
@ -318,58 +289,25 @@ impl TextView {
where
S: Into<String>,
{
self.markup(content).unwrap()
}
/// 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))
self.with(|s| s.set_content(content))
}
/// Replace the text in this view.
pub fn set_content<S>(&mut self, content: S)
where
S: Into<String>,
S: Into<StyledString>,
{
self.set_markup(content).unwrap();
}
/// 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.content.lock().unwrap().content = content.into();
self.invalidate();
Ok(())
}
/// Append `content` to the end of a `TextView`.
pub fn append_content<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
pub fn append<S>(&mut self, content: S)
where
T: MarkupText,
S: Into<StyledString>,
{
self.content
.lock()
.unwrap()
.content
.append_content(content)?;
self.content.lock().unwrap().content.append(content.into());
self.invalidate();
Ok(())
}
/// 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
// (This means we try to use the entire available width for text).
self.rows =
SpanLinesIterator::new(content.content.spans(), size.x).collect();
self.rows = LinesIterator::new(&content.content, size.x).collect();
// Width taken by the scrollbar. Without a scrollbar, it's 0.
let mut scrollbar_width = 0;
@ -473,8 +410,7 @@ impl TextView {
};
self.rows =
SpanLinesIterator::new(content.content.spans(), available)
.collect();
LinesIterator::new(&content.content, available).collect();
if self.rows.is_empty() && !content.content.is_empty() {
// 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 mut x = self.align.h.get_offset(l, printer.size.x);
for span in row.resolve(content.content.spans()) {
printer.with_style(span.style, |printer| {
printer.print((x, 0), &span.text);
x += span.text.len();
for span in row.resolve(&content.content) {
printer.with_style(*span.attr, |printer| {
printer.print((x, 0), &span.content);
x += span.content.width();
});
}
// let text = &content.content[row.start..row.end];