// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.
-use enum_iterator::Sequence;
+use nodit::{Interval, NoditMap, interval::ie};
+use smallstr::SmallString;
use std::{
- borrow::Cow,
cmp::Ordering,
- fmt::{Debug, Display},
+ fmt::{Debug, Display, Write as _},
ops::Range,
};
use unicode_width::UnicodeWidthChar;
-use crate::output::pivot::look::FontStyle;
+use crate::output::pivot::look::{Color, FontStyle};
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub struct Attribute {
+ pub fg: Option<Color>,
+ pub bg: Option<Color>,
+ pub bold: bool,
+ pub italic: bool,
+ pub underline: bool,
+}
+
+impl Attribute {
+ pub fn is_empty(&self) -> bool {
+ self.fg.is_none() && self.bg.is_none() && !self.bold && !self.italic && !self.underline
+ }
+
+ pub fn affixes(&self) -> (&'static str, &'static str) {
+ match (self.bold, self.italic, self.underline) {
+ (false, false, false) => ("", ""),
+ (false, false, true) => ("_", "_"),
+ (false, true, false) => ("/", "/"),
+ (false, true, true) => ("_/", "/_"),
+ (true, false, false) => ("*", "*"),
+ (true, true, false) => ("*/", "/*"),
+ (true, false, true) => ("_*", "*_"),
+ (true, true, true) => ("_/*", "*/_"),
+ }
+ }
+
+ pub fn sgr<T>(&self, content: T) -> Sgr<'_, T> {
+ Sgr {
+ attribute: self,
+ content,
+ }
+ }
+
+ pub fn for_style(font_style: &FontStyle) -> Self {
+ Self {
+ fg: Some(font_style.fg),
+ bg: Some(font_style.bg),
+ bold: font_style.bold,
+ italic: font_style.italic,
+ underline: font_style.underline,
+ }
+ }
+}
+
+pub struct Sgr<'a, T> {
+ attribute: &'a Attribute,
+ content: T,
+}
+
+impl<'a, T> Display for Sgr<'a, T>
+where
+ T: Display,
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut s = SmallString::<[u8; 32]>::new();
+ if let Some(fg) = self.attribute.fg {
+ write!(&mut s, "38;2;{};{};{};", fg.r, fg.g, fg.b).unwrap();
+ }
+ if let Some(bg) = self.attribute.bg {
+ write!(&mut s, "48;2;{};{};{};", bg.r, bg.g, bg.b).unwrap();
+ }
+ if self.attribute.bold {
+ write!(&mut s, "1;").unwrap();
+ }
+ if self.attribute.italic {
+ write!(&mut s, "3;").unwrap();
+ }
+ if self.attribute.underline {
+ write!(&mut s, "4;").unwrap();
+ }
+ if !s.is_empty() {
+ s.pop();
+ write!(f, "\x1b[{s}m{}\x1b[0m", &self.content)?;
+ } else {
+ write!(f, "{}", &self.content)?;
+ }
+ Ok(())
+ }
+}
/// A line of text, encoded in UTF-8, with support functions that properly
/// handle double-width characters and backspaces.
///
/// Designed to make appending text fast, and access and modification of other
/// column positions possible.
-#[derive(Clone, Default)]
+#[derive(Clone, Default, Debug)]
pub struct TextLine {
/// Content.
string: String,
+ /// Attributes.
+ attributes: NoditMap<usize, Interval<usize>, Attribute>,
+
/// Display width, in character positions.
width: usize,
}
self.width = x;
}
- fn put_closure<F>(&mut self, x0: usize, w: usize, push_str: F)
+ fn put_closure<F>(&mut self, x0: usize, w: usize, push_str: F, attribute: Option<Attribute>)
where
F: FnOnce(&mut String),
{
self.string.extend((x1..span.columns.end).map(|_| '?'));
self.string.push_str(&tail);
}
+ if w > 0 {
+ let interval = ie(x0, x1);
+ let _ = self.attributes.cut(&interval);
+ if let Some(attribute) = attribute {
+ self.attributes
+ .insert_merge_touching_if_values_equal(interval, attribute)
+ .expect("interval was cut");
+ }
+ }
}
- pub fn put(&mut self, x0: usize, s: &str) {
+ pub fn put(&mut self, x0: usize, s: &str, attribute: Option<Attribute>) {
self.string.reserve(s.len());
- self.put_closure(x0, Widths::new(s).sum(), |dst| dst.push_str(s));
+ self.put_closure(x0, Widths::new(s).sum(), |dst| dst.push_str(s), attribute);
}
- pub fn put_multiple(&mut self, x0: usize, c: char, n: usize) {
+ pub fn put_multiple(&mut self, x0: usize, c: char, n: usize, attribute: Option<Attribute>) {
self.string.reserve(c.len_utf8() * n);
- self.put_closure(x0, c.width().unwrap() * n, |dst| {
- (0..n).for_each(|_| dst.push(c))
- });
+ self.put_closure(
+ x0,
+ c.width().unwrap() * n,
+ |dst| (0..n).for_each(|_| dst.push(c)),
+ attribute,
+ );
}
fn find_span(&self, x0: usize, x1: usize) -> Position {
pub fn str(&self) -> &str {
&self.string
}
+
+ pub fn display_sgr(&self) -> DisplaySgr<'_> {
+ DisplaySgr(self)
+ }
+
+ pub fn display_wiki(&self) -> DisplayWiki<'_> {
+ DisplayWiki(self)
+ }
+
+ fn iter(&self) -> impl Iterator<Item = (&'_ str, Option<Attribute>)> {
+ TextIter {
+ next: None,
+ line: self,
+ attr: self.attributes.iter(),
+ ofs: 0,
+ x: 0,
+ }
+ }
}
impl Display for TextLine {
}
}
+pub struct DisplayWiki<'a>(&'a TextLine);
+
+impl<'a> Display for DisplayWiki<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for (s, attribute) in self.0.iter() {
+ let (prefix, suffix) = if let Some(attribute) = attribute {
+ attribute.affixes()
+ } else {
+ ("", "")
+ };
+ write!(f, "{prefix}{s}{suffix}")?;
+ }
+ Ok(())
+ }
+}
+
+pub struct DisplaySgr<'a>(&'a TextLine);
+
+impl<'a> Display for DisplaySgr<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for (s, attribute) in self.0.iter() {
+ if let Some(attribute) = attribute {
+ write!(f, "{}", attribute.sgr(s))?;
+ } else {
+ f.write_str(s)?;
+ }
+ }
+ Ok(())
+ }
+}
+
+struct TextIter<'a, I>
+where
+ I: DoubleEndedIterator<Item = (&'a Interval<usize>, &'a Attribute)>,
+{
+ line: &'a TextLine,
+ next: Option<(&'a str, Option<Attribute>)>,
+ attr: I,
+ ofs: usize,
+ x: usize,
+}
+
+impl<'a, I> TextIter<'a, I>
+where
+ I: DoubleEndedIterator<Item = (&'a Interval<usize>, &'a Attribute)>,
+{
+ fn take_up_to(&mut self, column: usize) -> &'a str {
+ let mut x = self.x;
+ for (index, c) in self.line.string[self.ofs..].char_indices() {
+ let w = c.width().unwrap_or_default();
+ if x + w > column {
+ let result = &self.line.string[self.ofs..self.ofs + index];
+ self.ofs += index;
+ self.x = column;
+ return result;
+ }
+ x += w;
+ }
+ let result = &self.line.string[self.ofs..];
+ self.x = x;
+ self.ofs = self.line.string.len();
+ result
+ }
+}
+
+impl<'a, I> Iterator for TextIter<'a, I>
+where
+ I: DoubleEndedIterator<Item = (&'a Interval<usize>, &'a Attribute)>,
+{
+ type Item = (&'a str, Option<Attribute>);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(next) = self.next.take() {
+ return Some(next);
+ }
+
+ match self.attr.next() {
+ Some((interval, attribute)) => {
+ let start = *interval.start();
+ let end = *interval.end() + 1;
+ if start > self.x {
+ let this = self.take_up_to(start);
+ self.next = Some((self.take_up_to(end), Some(*attribute)));
+ Some((this, None))
+ } else {
+ Some((self.take_up_to(end), Some(*attribute)))
+ }
+ }
+ None => {
+ let rest = &self.line.string[self.ofs..];
+ self.ofs = self.line.string.len();
+ if rest.is_empty() {
+ None
+ } else {
+ Some((rest, None))
+ }
+ }
+ }
+ }
+}
+
/// Position of one or more characters within a [TextLine].
#[derive(Debug)]
struct Position {
}
}
-#[derive(Copy, Clone, PartialEq, Eq, Sequence)]
-pub struct Emphasis {
- pub bold: bool,
- pub underline: bool,
-}
-
-impl From<&FontStyle> for Emphasis {
- fn from(style: &FontStyle) -> Self {
- Self {
- bold: style.bold,
- underline: style.underline,
- }
- }
-}
-
-impl Emphasis {
- const fn plain() -> Self {
- Self {
- bold: false,
- underline: false,
- }
- }
- pub fn is_plain(&self) -> bool {
- *self == Self::plain()
- }
- pub fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> {
- if self.is_plain() {
- Cow::from(s)
- } else {
- let mut output = String::with_capacity(
- s.len() * (1 + self.bold as usize * 2 + self.underline as usize * 2),
- );
- for c in s.chars() {
- if self.bold {
- output.push(c);
- output.push('\x08');
- }
- if self.underline {
- output.push('_');
- output.push('\x08');
- }
- output.push(c);
- }
- Cow::from(output)
- }
- }
-}
-
-impl Debug for Emphasis {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Self {
- bold: false,
- underline: false,
- } => "plain",
- Self {
- bold: true,
- underline: false,
- } => "bold",
- Self {
- bold: false,
- underline: true,
- } => "underline",
- Self {
- bold: true,
- underline: true,
- } => "bold+underline",
- }
- )
- }
-}
-
pub fn clip_text<'a>(
text: &'a str,
bb: &Range<isize>,
#[cfg(test)]
mod tests {
- use super::{Emphasis, TextLine};
- use enum_iterator::all;
+ use std::fmt::Debug;
+
+ use crate::output::{drivers::text::text_line::Attribute, pivot::look::FontStyle};
+
+ use super::TextLine;
+ use enum_iterator::{Sequence, all};
+ use itertools::Itertools;
+
+ #[derive(Copy, Clone, Default, PartialEq, Eq, Sequence)]
+ pub struct Emphasis {
+ pub bold: bool,
+ pub italic: bool,
+ pub underline: bool,
+ }
+
+ impl From<&FontStyle> for Emphasis {
+ fn from(style: &FontStyle) -> Self {
+ Self {
+ bold: style.bold,
+ italic: style.italic,
+ underline: style.underline,
+ }
+ }
+ }
+
+ impl From<Emphasis> for Attribute {
+ fn from(value: Emphasis) -> Self {
+ Attribute {
+ bold: value.bold,
+ italic: value.italic,
+ underline: value.underline,
+ ..Attribute::default()
+ }
+ }
+ }
+
+ impl Emphasis {
+ fn plain() -> Self {
+ Self::default()
+ }
+ pub fn is_plain(&self) -> bool {
+ *self == Self::plain()
+ }
+ pub fn apply<'a>(&self, s: &'a str) -> String {
+ let (prefix, suffix) = Attribute::from(*self).affixes();
+ format!("{prefix}{s}{suffix}")
+ }
+ }
+
+ impl Debug for Emphasis {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut attributes = Vec::new();
+ if self.bold {
+ attributes.push("bold");
+ }
+ if self.italic {
+ attributes.push("italic");
+ }
+ if self.underline {
+ attributes.push("underline");
+ }
+ if attributes.is_empty() {
+ write!(f, "plain")
+ } else {
+ write!(f, "{}", attributes.into_iter().format("+"))
+ }
+ }
+ }
#[test]
fn overwrite_rest_of_line() {
- for lowercase in all::<Emphasis>() {
- for uppercase in all::<Emphasis>() {
- let mut line = TextLine::new();
- line.put(0, &lowercase.apply("abc"));
- line.put(1, &uppercase.apply("BCD"));
- assert_eq!(
- line.str(),
- &format!("{}{}", lowercase.apply("a"), uppercase.apply("BCD")),
- "uppercase={uppercase:?} lowercase={lowercase:?}"
- );
+ for (x0, prefix) in [(0, ""), (1, " "), (2, " ")] {
+ for lowercase in all::<Emphasis>() {
+ for uppercase in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ line.put(x0, "abc", Some(lowercase.into()));
+ line.put(x0 + 1, "BCD", Some(uppercase.into()));
+
+ println!("{}", line.display_sgr());
+ assert_eq!(
+ line.display_wiki().to_string(),
+ if lowercase == uppercase {
+ format!("{prefix}{}", lowercase.apply("aBCD"))
+ } else {
+ format!("{prefix}{}{}", lowercase.apply("a"), uppercase.apply("BCD"))
+ },
+ "prefix={prefix:?} uppercase={uppercase:?} lowercase={lowercase:?}"
+ );
+ }
}
}
}
for uppercase in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `AbCDEf`.
- line.put(0, &lowercase.apply("abcdef"));
- line.put(0, &uppercase.apply("A"));
- line.put(2, &uppercase.apply("CDE"));
+ line.put(0, "abcdef", Some(lowercase.into()));
+ line.put(0, "A", Some(uppercase.into()));
+ line.put(2, "CDE", Some(uppercase.into()));
assert_eq!(
- line.str().replace('\x08', "#"),
- format!(
- "{}{}{}{}",
- uppercase.apply("A"),
- lowercase.apply("b"),
- uppercase.apply("CDE"),
- lowercase.apply("f")
- )
- .replace('\x08', "#"),
+ line.display_wiki().to_string(),
+ if lowercase == uppercase {
+ lowercase.apply("AbCDEf")
+ } else {
+ format!(
+ "{}{}{}{}",
+ uppercase.apply("A"),
+ lowercase.apply("b"),
+ uppercase.apply("CDE"),
+ lowercase.apply("f")
+ )
+ },
"uppercase={uppercase:?} lowercase={lowercase:?}"
);
}
for lowercase in all::<Emphasis>() {
for hiragana in all::<Emphasis>() {
let mut line = TextLine::new();
- // Produces `kaきくけ"`.
- line.put(0, &lowercase.apply("kakiku"));
- line.put(2, &hiragana.apply("きくけ"));
+ // Produces `kaきくけ`.
+ line.put(0, "kakiku", Some(lowercase.into()));
+ line.put(2, "きくけ", Some(hiragana.into()));
+
assert_eq!(
- line.str(),
- &format!("{}{}", lowercase.apply("ka"), hiragana.apply("きくけ")),
+ line.display_wiki().to_string(),
+ if lowercase == hiragana {
+ lowercase.apply("kaきくけ")
+ } else {
+ lowercase.apply("ka") + &hiragana.apply("きくけ")
+ },
"lowercase={lowercase:?} hiragana={hiragana:?}"
);
}
for lowercase in all::<Emphasis>() {
for hiragana in all::<Emphasis>() {
let mut line = TextLine::new();
- // Produces `かkiくけko".
- line.put(0, &lowercase.apply("kakikukeko"));
- line.put(0, &hiragana.apply("か"));
- line.put(4, &hiragana.apply("くけ"));
+ // Produces `かkiくけko`.
+ line.put(0, "kakikukeko", Some(lowercase.into()));
+ line.put(0, "か", Some(hiragana.into()));
+ line.put(4, "くけ", Some(hiragana.into()));
assert_eq!(
- line.str(),
- &format!(
- "{}{}{}{}",
- hiragana.apply("か"),
- lowercase.apply("ki"),
- hiragana.apply("くけ"),
- lowercase.apply("ko")
- ),
+ line.display_wiki().to_string(),
+ if lowercase == hiragana {
+ lowercase.apply("かkiくけko")
+ } else {
+ hiragana.apply("か")
+ + &lowercase.apply("ki")
+ + &hiragana.apply("くけ")
+ + &lowercase.apply("ko")
+ },
"lowercase={lowercase:?} hiragana={hiragana:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `あきくけ`.
- line.put(0, &bottom.apply("あいう"));
- line.put(2, &top.apply("きくけ"));
+ line.put(0, "あいう", Some(bottom.into()));
+ line.put(2, "きくけ", Some(top.into()));
+
assert_eq!(
- line.str(),
- &format!("{}{}", bottom.apply("あ"), top.apply("きくけ")),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("あきくけ")
+ } else {
+ bottom.apply("あ") + &top.apply("きくけ")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `あきくけ`.
- line.put(0, &bottom.apply("あいう"));
- line.put(3, &top.apply("きくけ"));
+ line.put(0, "あいう", Some(bottom.into()));
+ line.put(3, "きくけ", Some(top.into()));
+
assert_eq!(
- line.str(),
- &format!("{}?{}", bottom.apply("あ"), top.apply("きくけ")),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("あ?きくけ")
+ } else {
+ bottom.apply("あ?") + &top.apply("きくけ")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `かいくけお`.
- line.put(0, &bottom.apply("あいうえお"));
- line.put(0, &top.apply("か"));
- line.put(4, &top.apply("くけ"));
+ line.put(0, "あいうえお", Some(bottom.into()));
+ line.put(0, "か", Some(top.into()));
+ line.put(4, "くけ", Some(top.into()));
assert_eq!(
- line.str(),
- &format!(
- "{}{}{}{}",
- top.apply("か"),
- bottom.apply("い"),
- top.apply("くけ"),
- bottom.apply("お")
- ),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("かいくけお")
+ } else {
+ top.apply("か")
+ + &bottom.apply("い")
+ + &top.apply("くけ")
+ + &bottom.apply("お")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `?か?うえおさ`.
- line.put(0, &bottom.apply("あいうえおさ"));
- line.put(1, &top.apply("か"));
+ line.put(0, "あいうえおさ", Some(bottom.into()));
+ line.put(1, "か", Some(top.into()));
assert_eq!(
- line.str(),
- &format!("?{}?{}", top.apply("か"), bottom.apply("うえおさ"),),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("?か?うえおさ")
+ } else {
+ bottom.apply("?") + &top.apply("か") + &bottom.apply("?うえおさ")
+ },
"bottom={bottom:?} top={top:?}"
);
// Produces `?か??くけ?さ`.
- line.put(5, &top.apply("くけ"));
+ line.put(5, "くけ", Some(top.into()));
+
assert_eq!(
- line.str(),
- &format!(
- "?{}??{}?{}",
- top.apply("か"),
- top.apply("くけ"),
- bottom.apply("さ")
- ),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("?か??くけ?さ")
+ } else {
+ bottom.apply("?")
+ + &top.apply("か")
+ + &bottom.apply("??")
+ + &top.apply("くけ")
+ + &bottom.apply("?さ")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `あkikuko`.
- line.put(0, &bottom.apply("あいう"));
- line.put(2, &top.apply("kikuko"));
+ line.put(0, "あいう", Some(bottom.into()));
+ line.put(2, "kikuko", Some(top.into()));
assert_eq!(
- line.str(),
- &format!("{}{}", bottom.apply("あ"), top.apply("kikuko"),),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("あkikuko")
+ } else {
+ bottom.apply("あ") + &top.apply("kikuko")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `あ?kikuko`.
- line.put(0, &bottom.apply("あいう"));
- line.put(3, &top.apply("kikuko"));
+ line.put(0, "あいう", Some(bottom.into()));
+ line.put(3, "kikuko", Some(top.into()));
assert_eq!(
- line.str(),
- &format!("{}?{}", bottom.apply("あ"), top.apply("kikuko"),),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("あ?kikuko")
+ } else {
+ bottom.apply("あ?") + &top.apply("kikuko")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `kaいうえお`.
- line.put(0, &bottom.apply("あいうえお"));
- line.put(0, &top.apply("ka"));
+ line.put(0, "あいうえお", Some(bottom.into()));
+ line.put(0, "ka", Some(top.into()));
+
assert_eq!(
- line.str(),
- &format!("{}{}", top.apply("ka"), bottom.apply("いうえお"),),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("kaいうえお")
+ } else {
+ top.apply("ka") + &bottom.apply("いうえお")
+ },
"bottom={bottom:?} top={top:?}"
);
-
// Produces `kaいkukeお`.
- line.put(4, &top.apply("kuke"));
+ line.put(4, "kuke", Some(top.into()));
assert_eq!(
- line.str(),
- &format!(
- "{}{}{}{}",
- top.apply("ka"),
- bottom.apply("い"),
- top.apply("kuke"),
- bottom.apply("お")
- ),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("kaいkukeお")
+ } else {
+ top.apply("ka")
+ + &bottom.apply("い")
+ + &top.apply("kuke")
+ + &bottom.apply("お")
+ },
"bottom={bottom:?} top={top:?}"
);
}
for top in all::<Emphasis>() {
let mut line = TextLine::new();
// Produces `?aいうえおさ`.
- line.put(0, &bottom.apply("あいうえおさ"));
- line.put(1, &top.apply("a"));
+ line.put(0, "あいうえおさ", Some(bottom.into()));
+ line.put(1, "a", Some(top.into()));
assert_eq!(
- line.str(),
- &format!("?{}{}", top.apply("a"), bottom.apply("いうえおさ"),),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("?aいうえおさ")
+ } else {
+ bottom.apply("?") + &top.apply("a") + &bottom.apply("いうえおさ")
+ },
"bottom={bottom:?} top={top:?}"
);
// Produces `?aい?kuke?さ`.
- line.put(5, &top.apply("kuke"));
+ line.put(5, "kuke", Some(top.into()));
assert_eq!(
- line.str(),
- &format!(
- "?{}{}?{}?{}",
- top.apply("a"),
- bottom.apply("い"),
- top.apply("kuke"),
- bottom.apply("さ")
- ),
+ line.display_wiki().to_string(),
+ if top == bottom {
+ top.apply("?aい?kuke?さ")
+ } else {
+ bottom.apply("?")
+ + &top.apply("a")
+ + &bottom.apply("い?")
+ + &top.apply("kuke")
+ + &bottom.apply("?さ")
+ },
"bottom={bottom:?} top={top:?}"
);
}
graph: &Graph,
series: &'a BTreeMap<&str, Series>,
footnotes: &pivot::Footnotes,
+ styles: &HashMap<&str, &Style>,
) -> (Vec<Dim<'a>>, Vec<usize>) {
let axes = graph
.facet_layout
.filter_map(|child| child.facet_level())
.map(|facet_level| (facet_level.level, &facet_level.axis))
.collect::<HashMap<_, _>>();
- let styles = self
- .children
- .iter()
- .filter_map(|child| child.style())
- .filter_map(|style| style.id.as_ref().map(|id| (id.as_str(), style)))
- .collect::<HashMap<_, _>>();
let mut dims = Vec::new();
let mut level_ofs = 1;
let mut current_layer = Vec::new();
let (title, caption, footnotes) = self.decode_labels(graph);
let series = self.decode_series(binary_data, warn);
- let (mut dims, current_layer) = self.decode_dimensions(graph, &series, &footnotes);
+ let styles = self
+ .children
+ .iter()
+ .filter_map(|child| child.style())
+ .filter_map(|style| style.id.as_ref().map(|id| (id.as_str(), style)))
+ .collect::<HashMap<_, _>>();
+ let (mut dims, current_layer) = self.decode_dimensions(graph, &series, &footnotes, &styles);
let mut data = graph.decode_data(&footnotes, &dims, &series, warn);
- graph.decode_styles(&look, &series, &mut dims, &mut data, &footnotes, warn);
+ graph.decode_styles(
+ &look, &series, &mut dims, &mut data, &footnotes, &styles, warn,
+ );
Ok(PivotTable::new(
dims.into_iter()
'outer: for (i, cell) in cell.values.iter().enumerate() {
coords.clear();
for dim in dims {
- if let Some(Some(coordinate)) = dim.coordinate.categories.get(i)
- && let Some(locator) =
- dim.coordinate.coordinate_to_index.borrow().get(&coordinate)
+ if let Some(Some(coordinate)) = dim.series.categories.get(i)
+ && let Some(locator) = dim.series.coordinate_to_index.borrow().get(&coordinate)
&& let Some(index) = locator.as_leaf()
{
coords.push(index);
dims: &mut [Dim],
data: &mut HashMap<Vec<usize>, Value>,
footnotes: &pivot::Footnotes,
+ styles: &HashMap<&str, &Style>,
warn: &mut dyn FnMut(LegacyXmlWarning),
) {
let has_cell_footnotes = series.contains_key("footnotes");
.iter()
.filter_map(|child| child.set_cell_properties())
{
- #[derive(Copy, Clone, Debug, PartialEq)]
- enum TargetType {
- Labeling,
- MajorTicks,
- }
-
- impl TargetType {
- fn from_id(target: &str) -> Option<Self> {
- if target == "labeling" {
- Some(Self::Labeling)
- } else if target.ends_with("majorTicks") {
- Some(Self::MajorTicks)
- } else {
- None
- }
- }
- }
-
- #[derive(Debug)]
- struct Target<'a> {
- sf: &'a SetFormat,
- target_type: TargetType,
- }
- impl<'a> Target<'a> {
- fn decode(
- &self,
- intersect: &Intersect,
- look: &Look,
- series: &BTreeMap<&str, Series>,
- dims: &mut [Dim],
- data: &mut HashMap<Vec<usize>, Value>,
- footnotes: &pivot::Footnotes,
- has_cell_footnotes: bool,
- ) {
- match self.target_type {
- TargetType::MajorTicks => {
- // Formatting for individual row or column labels.
- for w in intersect.wheres() {
- let Some(s) = series.get(w.variable.as_str()) else {
- continue;
- };
- let Some(dim_index) = s.dimension_index.get() else {
- continue;
- };
- let dimension = &mut dims[dim_index].dimension;
- let Ok(axis) = Axis2::try_from(dims[dim_index].axis) else {
- continue;
- };
- for index in
- w.include.split(';').filter_map(|s| s.parse::<usize>().ok())
- {
- if let Some(locator) =
- s.coordinate_to_index.borrow().get(&index).copied()
- && let Some(category) = dimension.root.category_mut(locator)
- {
- Style::apply_to_value(
- category.name_mut(),
- Some(&self.sf),
- None,
- None,
- &look.areas[Area::Labels(axis)],
- footnotes,
- has_cell_footnotes,
- );
- }
- }
- }
- }
- TargetType::Labeling => {
- // Formatting for individual cells or groups of them
- // with some dimensions in common.
- let mut include = vec![HashSet::new(); dims.len()];
- for w in intersect.wheres() {
- let Some(s) = series.get(w.variable.as_str()) else {
- continue;
- };
- let Some(dim_index) = s.dimension_index.get() else {
- // Group indexes may be included even though
- // they are redundant. Ignore them.
- continue;
- };
- for index in
- w.include.split(';').filter_map(|s| s.parse::<usize>().ok())
- {
- if let Some(locator) =
- s.coordinate_to_index.borrow().get(&index).copied()
- && let Some(leaf_index) = locator.as_leaf()
- {
- include[dim_index].insert(leaf_index);
- }
- }
- }
-
- // XXX This is inefficient in the common case where
- // all of the dimensions are matched. We should use
- // a heuristic where if all of the dimensions are
- // matched and the product of n[*] is less than the
- // number of cells then iterate through all the
- // possibilities rather than all the cells. Or even
- // only do it if there is just one possibility.
- for (indexes, value) in data {
- let mut skip = false;
- for (dimension, index) in indexes.iter().enumerate() {
- if !include[dimension].is_empty()
- && !include[dimension].contains(index)
- {
- skip = true;
- break;
- }
- }
- if !skip {
- Style::apply_to_value(
- value,
- Some(&self.sf),
- None,
- None,
- &look.areas[Area::Data(RowParity::Even)],
- footnotes,
- has_cell_footnotes,
- );
- }
- }
- }
- }
- }
- }
-
let targets = scp
.sets
.iter()
- .filter_map(|set| set.as_set_format())
- .filter_map(|sf| {
- TargetType::from_id(&sf.target).map(|target_type| Target { sf, target_type })
- })
+ .filter_map(|set| set.decode(styles))
.collect::<Vec<_>>();
if let Some(union_) = &scp.union_ {
for target in &targets {
if target.target_type == TargetType::Labeling {
for value in data.values_mut() {
- Style::apply_to_value(
+ target.apply(
value,
- Some(&target.sf),
- None,
- None,
&look.areas[Area::Data(RowParity::Even)],
- &footnotes,
+ footnotes,
has_cell_footnotes,
);
}
include: String,
}
+#[derive(Copy, Clone, Debug, PartialEq)]
+enum TargetType {
+ Labeling,
+ MajorTicks,
+ Interval,
+}
+
+impl TargetType {
+ fn from_id(target: &str) -> Option<Self> {
+ if target == "interval" {
+ Some(Self::Interval)
+ } else if target == "labeling" {
+ Some(Self::Labeling)
+ } else if target.ends_with("majorTicks") {
+ Some(Self::MajorTicks)
+ } else {
+ None
+ }
+ }
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
enum Set {
SetFormat(SetFormat),
+ SetStyle(SetStyle),
#[serde(other)]
Other,
}
impl Set {
- fn as_set_format(&self) -> Option<&SetFormat> {
+ fn decode<'a>(&'a self, styles: &HashMap<&str, &'a Style>) -> Option<DecodedSet<'a>> {
+ Some(DecodedSet {
+ target_type: self.target_type()?,
+ change: match self {
+ Set::SetFormat(set_format) => Change::Format(set_format),
+ Set::SetStyle(set_style) => Change::Style(set_style.style.get(styles)?),
+ Set::Other => return None,
+ },
+ })
+ }
+
+ fn target(&self) -> Option<&str> {
match self {
- Set::SetFormat(set_format) => Some(set_format),
+ Set::SetFormat(set_format) => Some(&set_format.target),
+ Set::SetStyle(set_style) => Some(&set_style.target),
Set::Other => None,
}
}
+
+ fn target_type(&self) -> Option<TargetType> {
+ self.target().and_then(TargetType::from_id)
+ }
+}
+
+struct DecodedSet<'a> {
+ target_type: TargetType,
+ change: Change<'a>,
+}
+
+impl<'a> DecodedSet<'a> {
+ fn decode(
+ &self,
+ intersect: &Intersect,
+ look: &Look,
+ series: &BTreeMap<&str, Series>,
+ dims: &mut [Dim],
+ data: &mut HashMap<Vec<usize>, Value>,
+ footnotes: &pivot::Footnotes,
+ has_cell_footnotes: bool,
+ ) {
+ match self.target_type {
+ TargetType::MajorTicks => {
+ // Formatting for individual row or column labels.
+ for w in intersect.wheres() {
+ let Some(s) = series.get(w.variable.as_str()) else {
+ continue;
+ };
+ let Some(dim_index) = s.dimension_index.get() else {
+ continue;
+ };
+ let dimension = &mut dims[dim_index].dimension;
+ let Ok(axis) = Axis2::try_from(dims[dim_index].axis) else {
+ continue;
+ };
+ for index in w.include.split(';').filter_map(|s| s.parse::<usize>().ok()) {
+ if let Some(locator) = s.coordinate_to_index.borrow().get(&index).copied()
+ && let Some(category) = dimension.root.category_mut(locator)
+ {
+ self.apply(
+ category.name_mut(),
+ &look.areas[Area::Labels(axis)],
+ footnotes,
+ has_cell_footnotes,
+ );
+ }
+ }
+ }
+ }
+ TargetType::Interval | TargetType::Labeling => {
+ // Formatting for individual cells or groups of them
+ // with some dimensions in common.
+ let mut include = vec![HashSet::new(); dims.len()];
+ for w in intersect.wheres() {
+ let Some(s) = series.get(w.variable.as_str()) else {
+ continue;
+ };
+ let Some(dim_index) = s.dimension_index.get() else {
+ // Group indexes may be included even though
+ // they are redundant. Ignore them.
+ continue;
+ };
+ for index in w.include.split(';').filter_map(|s| s.parse::<usize>().ok()) {
+ if let Some(locator) = s.coordinate_to_index.borrow().get(&index).copied()
+ && let Some(leaf_index) = locator.as_leaf()
+ {
+ include[dim_index].insert(leaf_index);
+ }
+ }
+ }
+
+ // XXX This is inefficient in the common case where
+ // all of the dimensions are matched. We should use
+ // a heuristic where if all of the dimensions are
+ // matched and the product of n[*] is less than the
+ // number of cells then iterate through all the
+ // possibilities rather than all the cells. Or even
+ // only do it if there is just one possibility.
+ for (indexes, value) in data {
+ let mut skip = false;
+ for (dimension, index) in indexes.iter().enumerate() {
+ if !include[dimension].is_empty() && !include[dimension].contains(index) {
+ skip = true;
+ break;
+ }
+ }
+ if !skip {
+ self.apply(
+ value,
+ &look.areas[Area::Data(RowParity::Even)],
+ footnotes,
+ has_cell_footnotes,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ fn apply(
+ &self,
+ value: &mut Value,
+ base_style: &AreaStyle,
+ footnotes: &pivot::Footnotes,
+ has_cell_footnotes: bool,
+ ) {
+ match self.change {
+ Change::Format(set_format) => Style::apply_to_value(
+ value,
+ Some(set_format),
+ None,
+ None,
+ base_style,
+ footnotes,
+ has_cell_footnotes,
+ ),
+ Change::Style(style) => {
+ let (fg, bg) = if self.target_type == TargetType::Interval {
+ (None, Some(style))
+ } else {
+ (Some(style), None)
+ };
+ Style::apply_to_value(
+ value,
+ None,
+ fg,
+ bg,
+ base_style,
+ footnotes,
+ has_cell_footnotes,
+ )
+ }
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+enum Change<'a> {
+ Format(&'a SetFormat),
+ Style(&'a Style),
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SetStyle {
+ #[serde(rename = "@target")]
+ target: String,
+
+ #[serde(rename = "@style")]
+ style: Ref<Style>,
}
#[derive(Deserialize, Debug)]
dims.push(Dim {
axis: a,
dimension,
- coordinate: variables[0],
+ series: variables[0],
});
}
struct Dim<'a> {
axis: Axis3,
dimension: pivot::Dimension,
- coordinate: &'a Series,
+ series: &'a Series,
}