use std::borrow::Cow; use cosmic_text::{self, Align}; use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Style, Weight}; #[cfg(feature = "databake")] use databake::Bake; use emath::{Pos2, Rect, Vec2}; use jotdown::{Container, Event, ListKind, OrderedListNumbering, OrderedListStyle}; use nominals::{LetterLower, LetterUpper, Nominal, RomanLower, RomanUpper}; #[cfg(any(feature = "serde", feature = "databake"))] pub mod serde_suck; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(any(feature = "serde", feature = "databake"))] pub use serde_suck::*; use rangemap::RangeMap; pub struct JotdownBufferIter<'a, T: Iterator>> { pub djot: T, pub indent: Vec, } struct JotdownIntoBuffer<'a, 'b, T: Iterator>> { pub djot: &'b mut T, pub attrs: Attrs<'static>, pub metrics: Metrics, pub indent: &'b mut Vec, pub image_url: Option>, pub link_start: usize, pub urls: Vec<(std::ops::Range, Cow<'a, str>)>, pub location: usize, pub added: bool, pub top_level_container: Option>, } #[derive(Default, Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct Indent { #[cfg_attr(feature = "serde", serde(with = "ListKindOption"))] pub modifier: Option, pub indent: f32, } #[cfg(feature = "databake")] impl databake::Bake for Indent { fn bake(&self, ctx: &databake::CrateEnv) -> databake::TokenStream { let indent = self.indent; let modifier = self.modifier.bake(ctx); databake::quote! { cosmic_jotdown::Indent { indent: #indent, modifier: #modifier, } } } } pub const INDENT_AMOUNT: f32 = 16.0; impl<'a, 'b, T: Iterator>> Iterator for JotdownIntoBuffer<'a, 'b, T> { type Item = (&'a str, Attrs<'static>); fn next(&mut self) -> Option { while let Some(event) = self.djot.next() { match event { Event::LeftSingleQuote => { self.added = true; self.location += "‘".len(); return Some(("‘", self.attrs.clone())); } Event::LeftDoubleQuote => { self.added = true; self.location += "“".len(); return Some(("“", self.attrs.clone())); } Event::RightSingleQuote => { self.added = true; self.location += "’".len(); return Some(("’", self.attrs.clone())); } Event::RightDoubleQuote => { self.added = true; self.location += "”".len(); return Some(("”", self.attrs.clone())); } Event::Ellipsis => { self.added = true; self.location += "…".len(); return Some(("…", self.attrs.clone())); } Event::EmDash => { self.added = true; self.location += "—".len(); return Some(("—", self.attrs.clone())); } Event::EnDash => { self.added = true; self.location += "–".len(); return Some(("–", self.attrs.clone())); } Event::Softbreak | Event::NonBreakingSpace => { self.added = true; self.location += " ".len(); return Some((" ", self.attrs.clone())); } Event::Str(Cow::Borrowed(s)) | Event::Symbol(Cow::Borrowed(s)) => { self.added = true; self.location += s.len(); return Some((s, self.attrs.clone())); } Event::Start(container, _) => match container { Container::Heading { level, .. } => { let l = match level { 1 => 2.0, 2 => 1.5, 3 => 1.17, 4 => 1.0, 5 => 0.83, _ => 0.67, }; self.metrics = Metrics::new(l, l * 1.1); self.top_level_container.get_or_insert(container); } Container::Emphasis => self.attrs = self.attrs.style(Style::Italic), Container::Strong => self.attrs = self.attrs.weight(Weight::BOLD), Container::Verbatim => self.attrs = self.attrs.family(Family::Monospace), Container::ListItem { .. } | Container::Paragraph => { self.top_level_container.get_or_insert(container); } Container::List { kind, .. } => self.indent.push(Indent { indent: self.indent.last().copied().unwrap_or_default().indent + INDENT_AMOUNT, modifier: Some(kind), }), Container::Image(ref url, _) => { self.added = true; self.image_url = Some(url.clone()); self.top_level_container.get_or_insert(container); } Container::Link(_, _) => { self.link_start = self.location + 1; self.attrs = self.attrs.color(Color::rgb(96, 198, 233)); } _ => {} }, Event::End(container) => match container { Container::Emphasis => self.attrs = self.attrs.style(Style::Normal), Container::Strong => self.attrs = self.attrs.weight(Weight::NORMAL), Container::Verbatim => self.attrs = self.attrs.family(Family::SansSerif), Container::List { .. } => { self.indent.pop(); } Container::Heading { .. } | Container::Paragraph | Container::Image(_, _) => { if self.added { return None; } } Container::Link(Cow::Borrowed(url), _) => { self.urls .push((self.link_start..self.location, Cow::Borrowed(url))); self.attrs = self.attrs.color(Color::rgb(255, 255, 255)); } _ => {} }, Event::Hardbreak | Event::ThematicBreak(_) => {} Event::Blankline | Event::Escape | Event::FootnoteReference(_) => {} Event::Str(Cow::Owned(_)) | Event::Symbol(Cow::Owned(_)) => panic!(), } } return None; } } #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct RichText<'a>( #[cfg_attr(feature = "serde", serde(borrow))] pub Cow<'a, str>, #[cfg_attr(feature = "serde", serde(with = "AttrsSerde"))] pub Attrs<'a>, ); #[cfg(feature = "databake")] impl<'a> Bake for RichText<'a> { fn bake(&self, ctx: &databake::CrateEnv) -> databake::TokenStream { let field0 = self.0.bake(ctx); let field1: AttrsSerde = self.1.into(); let field1 = field1.bake(ctx); databake::quote! { cosmic_jotdown::RichText(#field0, #field1) } } } #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[cfg_attr(feature = "databake", derive(databake::Bake))] #[cfg_attr(feature = "databake", databake(path = cosmic_jotdown))] pub struct JotdownItem<'a> { pub indent: Indent, #[cfg_attr(feature = "serde", serde(borrow))] pub buffer: Cow<'a, [RichText<'a>]>, pub metrics: (f32, f32), pub image_url: Option>, pub margin: f32, pub url_map: Option)]>>, } #[derive(Clone)] pub struct ResolvedJotdownItem<'a> { pub indent: Indent, pub buffer: Buffer, pub metrics: Metrics, pub relative_bounds: Rect, pub url_map: Option>>, pub image_urls: Option<(String, String)>, } pub fn resolve_paragraphs<'a>( paragaphs: &[JotdownItem<'a>], viewbox: Vec2, font_system: &mut FontSystem, metrics: Metrics, align: Option, factor: f32, base_uri: &'static str, ) -> (Vec2, Vec>) { let mut size = Vec2::new(0.0, 0.0); // Cannot be f32::MAX let mut last_margin = 99999.0; let mut last_image_size: Option = None; let mut first = true; let mut in_list = false; let mut list_number = 0; let mut paragraphs = paragaphs .iter() .flat_map(|jbuffer| { let mut buffer = jbuffer .clone() .resolve(font_system, viewbox.x, metrics, align, factor); let margin = jbuffer.margin * buffer.metrics.line_height; let margin_top = (margin - last_margin).max(0.0); buffer.relative_bounds = buffer .relative_bounds .translate(Vec2::new(buffer.indent.indent, size.y + margin_top)); let buffer_size = buffer.relative_bounds.size(); let buffer_indent = buffer.indent.indent; let result_buffers = if let Some(url) = &jbuffer.image_url { let url = url.split_once('#').unwrap(); let image_size = url.1.split_once('x').unwrap(); let image_size = Vec2::new(image_size.0.parse().unwrap(), image_size.1.parse().unwrap()); let image = format!("{}/{}", base_uri, url.0); let split = url.0.rsplit_once('.').unwrap(); let hi_image = format!("{}/{}_hi.{}", base_uri, split.0, split.1); buffer.image_urls = Some((image, hi_image)); log::info!("{:?}", buffer.image_urls); buffer.relative_bounds = Rect::from_min_size(Pos2::new(buffer.indent.indent, size.y), image_size); const IMAGE_PADDING: f32 = 8.0; if let Some(last_size) = last_image_size.as_mut() { let ls = *last_size; last_size.x += image_size.x + IMAGE_PADDING; if last_size.x > viewbox.x { size.y += last_size.y; last_size.x = image_size.x + IMAGE_PADDING; last_size.y = image_size.y + margin; buffer.relative_bounds = Rect::from_min_size( Pos2::new(buffer.indent.indent, size.y), image_size, ); } else { last_size.y = last_size.y.max(image_size.y + margin); buffer.relative_bounds = buffer.relative_bounds.translate(Vec2::new(ls.x, 0.0)); } } else { if image_size.x <= viewbox.x { last_image_size = Some(image_size + Vec2::new(IMAGE_PADDING, margin)); } } if image_size.x > viewbox.x { let max_size = Vec2::new(viewbox.x, image_size.y); let new_size = scale_to_fit(image_size, max_size, true); buffer.relative_bounds = Rect::from_min_size(Pos2::new(buffer.indent.indent, size.y), new_size); if let Some(last_image_size) = &mut last_image_size { last_image_size.y -= image_size.y - new_size.y; } else { size.y += new_size.y + margin } } [Some(buffer), None] } else if let Some(mut list_kind) = buffer.indent.modifier { if let Some(image_size) = last_image_size { size.y += image_size.y; buffer.relative_bounds = buffer .relative_bounds .translate(Vec2::new(0.0, image_size.y)); size.x = size.x.max(image_size.x); last_image_size = None; } if let ListKind::Ordered { start, .. } = &mut list_kind { if !in_list { list_number = *start; } else { list_number += 1; } *start = list_number; } in_list = true; let mut list_buffer = Buffer::new(font_system, buffer.metrics); list_buffer.set_text( font_system, make_list_number(list_kind).as_ref(), Attrs::new().family(Family::SansSerif), Shaping::Advanced, ); list_buffer.set_wrap(font_system, cosmic_text::Wrap::WordOrGlyph); list_buffer.set_size(font_system, f32::MAX, f32::MAX); list_buffer.shape_until_scroll(font_system, false); let list_buffer_metrics = buffer.metrics; let indent = (buffer_indent) - (INDENT_AMOUNT * factor); let res = [ Some(buffer), Some(ResolvedJotdownItem { indent: Indent { modifier: None, indent, }, relative_bounds: measure_buffer( &list_buffer, Vec2::new(f32::MAX, f32::MAX), ) .translate(Vec2::new(indent, size.y + margin_top)), buffer: list_buffer, metrics: list_buffer_metrics, url_map: None, image_urls: None, }), ]; size.y += buffer_size.y + (margin_top + margin); size.x = size.x.max(buffer_size.x + buffer_indent); res } else { if let Some(image_size) = last_image_size { size.y += image_size.y; buffer.relative_bounds = buffer .relative_bounds .translate(Vec2::new(0.0, image_size.y)); size.x = size.x.max(image_size.x); last_image_size = None; } in_list = false; size.y += buffer_size.y + (margin_top + margin); size.x = size.x.max(buffer_size.x + buffer_indent); [Some(buffer), None] }; last_margin = margin; first = false; result_buffers }) .filter_map(|p| p) .collect::>(); size.y -= last_margin; if let Some(image_size) = last_image_size { size.y += image_size.y; } if align.is_some() { paragraphs.iter_mut().for_each(|paragraph| { if paragraph.image_urls.is_none() { paragraph .relative_bounds .set_width(size.x - paragraph.indent.indent); paragraph .buffer .set_size(font_system, size.x - paragraph.indent.indent, f32::MAX); paragraph.buffer.shape_until_scroll(font_system, false); } }); } (size, paragraphs) } impl<'a> JotdownItem<'a> { pub fn resolve( mut self, font_system: &mut FontSystem, width: f32, metrics: Metrics, align: Option, factor: f32, ) -> ResolvedJotdownItem<'a> { self.indent.indent *= factor; let buffer = self.make_buffer(font_system, width, metrics, align); ResolvedJotdownItem { indent: self.indent, relative_bounds: measure_buffer(&buffer, Vec2::new(width, f32::MAX)), buffer, metrics: Metrics::new( self.metrics.0 * metrics.font_size, self.metrics.1 * metrics.line_height, ), url_map: { self.url_map.map(|self_url_map| { let mut url_map = RangeMap::new(); url_map.extend(self_url_map.iter().map(|m| (m.0..m.1, m.2.clone()))); url_map }) }, image_urls: None, } } pub fn new_default(text: Vec>) -> Self { Self { indent: Indent { modifier: None, indent: 0.0, }, buffer: Cow::Owned(text), metrics: (1.0, 1.1), url_map: None, margin: 0.0, image_url: None, } } } impl<'a> JotdownItem<'a> { pub fn make_buffer( &self, font_system: &mut FontSystem, width: f32, metrics: Metrics, align: Option, ) -> Buffer { let mut buffer = Buffer::new( font_system, Metrics::new( metrics.font_size * self.metrics.0, metrics.line_height * self.metrics.1, ), ); buffer.set_rich_text( font_system, self.buffer.iter().map(|r| (r.0.as_ref(), r.1)), Attrs::new().family(Family::SansSerif), Shaping::Advanced, ); buffer.set_wrap(font_system, cosmic_text::Wrap::WordOrGlyph); buffer.set_size(font_system, width - self.indent.indent, f32::MAX); for line in &mut buffer.lines { line.set_align(align); } buffer.shape_until_scroll(font_system, false); buffer } } impl<'a, T: Iterator>> Iterator for JotdownBufferIter<'a, T> { type Item = JotdownItem<'a>; fn next(&mut self) -> Option { let mut jot = JotdownIntoBuffer { djot: &mut self.djot, attrs: Attrs::new().family(Family::SansSerif), indent: &mut self.indent, image_url: None, metrics: Metrics::new(1.0, 1.1), added: false, link_start: 0, location: 0, urls: Vec::new(), top_level_container: None, }; let buffer = (&mut jot) .map(|r| RichText(r.0.into(), r.1)) .collect::>(); let image_url = jot.image_url; let urls = jot.urls; let top_level_containers = jot.top_level_container; let added = jot.added; let metrics = jot.metrics; let indent = self.indent.last().copied().unwrap_or_default(); if !added { return None; } else { return Some(JotdownItem { indent, url_map: if urls.is_empty() { None } else { let mut map = Vec::new(); map.extend( urls.into_iter() .map(|(range, url)| (range.start, range.end + 1, url)), ); Some(Cow::Owned(map)) }, buffer: Cow::Owned(buffer), image_url, metrics: (metrics.font_size, metrics.line_height), margin: match top_level_containers { Some(Container::Heading { level, .. }) => match level { 1 => 0.34, 2 => 0.42, 3 => 0.5, 4 => 0.65, 5 => 0.85, 6 => 1.25, _ => unreachable!(), }, Some(Container::ListItem) => 0.5, Some(Container::Image(_, _)) => 1.0, _ => 1.0, }, }); } } } pub fn jotdown_into_buffers<'a, T: Iterator>>( djot: T, ) -> JotdownBufferIter<'a, T> { JotdownBufferIter { djot, indent: Vec::new(), } } pub fn make_list_number(list_kind: ListKind) -> Cow<'static, str> { match list_kind { ListKind::Unordered | ListKind::Task => Cow::Borrowed("•"), ListKind::Ordered { numbering, style, start: number, } => { use std::fmt::Write; let mut result = String::new(); if matches!(style, OrderedListStyle::ParenParen) { result.push('('); } match numbering { OrderedListNumbering::Decimal => { result.write_fmt(format_args!("{}", number)).unwrap() } OrderedListNumbering::AlphaLower => { result .write_fmt(format_args!("{}", (number - 1).to_nominal(&LetterLower))) .unwrap(); } OrderedListNumbering::AlphaUpper => { result .write_fmt(format_args!("{}", (number - 1).to_nominal(&LetterUpper))) .unwrap(); } OrderedListNumbering::RomanLower => { result .write_fmt(format_args!("{}", number.to_nominal(&RomanLower))) .unwrap(); } OrderedListNumbering::RomanUpper => { result .write_fmt(format_args!("{}", number.to_nominal(&RomanUpper))) .unwrap(); } } match style { OrderedListStyle::Period => result.push('.'), OrderedListStyle::Paren | OrderedListStyle::ParenParen => result.push(')'), } Cow::Owned(result) } } } pub fn measure_buffer(buffer: &Buffer, vb: Vec2) -> Rect { let mut rtl = false; let (width, total_lines) = buffer .layout_runs() .fold((0.0, 0usize), |(width, total_lines), run| { if run.rtl { rtl = true; } (run.line_w.max(width), total_lines + 1) }); let (max_width, max_height) = buffer.size(); let size = Vec2::new( if rtl { vb.x } else { width.min(max_width) }, (total_lines as f32 * buffer.metrics().line_height).min(max_height), ); Rect::from_min_size(Pos2::ZERO, size) } fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 { if maintain_aspect_ratio { let ratio_x = available_size.x / image_size.x; let ratio_y = available_size.y / image_size.y; let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y }; let ratio = if ratio.is_finite() { ratio } else { 1.0 }; image_size * ratio } else { available_size } }