diff --git a/Cargo.lock b/Cargo.lock index a76eb77..2fd99e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + [[package]] name = "bitflags" version = "2.5.0" @@ -28,13 +34,28 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "cosmic-jotdown" version = "0.1.0" dependencies = [ "cosmic-text", + "emath", + "image", "jotdown", "log", + "nominals", "rangemap", "serde", ] @@ -85,6 +106,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "emath" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" + [[package]] name = "font-types" version = "0.5.2" @@ -117,6 +144,18 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", +] + [[package]] name = "jotdown" version = "0.3.2" @@ -154,19 +193,34 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.79" +name = "nominals" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "bd4b6e50a0a7f2214e99ecf7f4a2c9cb9572e5817d96e37a6d31387961c23994" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -227,18 +281,18 @@ checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -273,9 +327,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 408bce8..f092cc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,9 @@ jotdown = { git = "https://git.nations.lol/fnmain/jotdown" } log = "0.4.21" serde = { version = "1.0.197", features = ["derive"], optional = true } rangemap = "1.5.1" +nominals = "0.3.0" +emath = "0.27.2" +image = { version = "0.24.9", default-features = false } [features] -default = ["serde"] serde = ["dep:serde", "rangemap/serde1"] diff --git a/src/lib.rs b/src/lib.rs index 8733619..7bdbdfb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,21 @@ use std::borrow::Cow; -use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Style, Weight}; -use jotdown::{Container, Event, ListKind}; +use cosmic_text::{self, Align}; + +use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Style, Weight}; +use emath::{Pos2, Rect, Vec2}; +use jotdown::{Container, Event, ListKind, OrderedListNumbering, OrderedListStyle}; +use nominals::{LetterLower, LetterUpper, Nominal, RomanLower, RomanUpper}; -pub use jotdown; -use rangemap::RangeMap; #[cfg(feature = "serde")] -mod serde_suck; +pub mod serde_suck; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "serde")] pub use serde_suck::*; +use rangemap::RangeMap; + pub struct JotdownBufferIter<'a, T: Iterator>> { pub djot: T, pub indent: Vec, @@ -27,9 +31,10 @@ struct JotdownIntoBuffer<'a, 'b, T: Iterator>> { pub urls: Vec<(std::ops::Range, Cow<'a, str>)>, pub location: usize, pub added: bool, + pub top_level_container: Option>, } -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct Indent { #[cfg_attr(feature = "serde", serde(with = "ListKindOption"))] @@ -37,7 +42,7 @@ pub struct Indent { pub indent: f32, } -pub const INDENT_AMOUNT: f32 = 18.0; +pub const INDENT_AMOUNT: f32 = 64.0; impl<'a, 'b, T: Iterator>> Iterator for JotdownIntoBuffer<'a, 'b, T> { type Item = (&'a str, Attrs<'static>); @@ -92,17 +97,32 @@ impl<'a, 'b, T: Iterator>> Iterator for JotdownIntoBuffer<'a, ' } Event::Start(container, _) => match container { Container::Heading { level, .. } => { - self.metrics = Metrics::new(4.0 - level as f32, 4.0 - level as f32); + 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(url, _) => self.image_url = Some(url), + Container::Image(ref url, _) => { + 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)); @@ -116,7 +136,7 @@ impl<'a, 'b, T: Iterator>> Iterator for JotdownIntoBuffer<'a, ' Container::List { .. } => { self.indent.pop(); } - Container::Heading { .. } | Container::Paragraph | Container::Image(_, _) => { + Container::Heading { .. } | Container::Paragraph => { if self.added { return None; } @@ -138,20 +158,14 @@ impl<'a, 'b, T: Iterator>> Iterator for JotdownIntoBuffer<'a, ' } } +#[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct RichText<'a>( - Cow<'a, str>, - #[cfg_attr(feature = "serde", serde(with = "AttrsSerde"))] - #[cfg_attr(feature = "serde", serde(borrow))] - Attrs<'a>, + #[cfg_attr(feature = "serde", serde(borrow))] pub Cow<'a, str>, + #[cfg_attr(feature = "serde", serde(with = "AttrsSerde"))] pub Attrs<'a>, ); -impl<'a> From<(Cow<'a, str>, Attrs<'a>)> for RichText<'a> { - fn from(value: (Cow<'a, str>, Attrs<'a>)) -> Self { - Self(value.0, value.1) - } -} - +#[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct JotdownItem<'a> { pub indent: Indent, @@ -160,15 +174,235 @@ pub struct JotdownItem<'a> { #[cfg_attr(feature = "serde", serde(with = "MetricsSerde"))] pub metrics: Metrics, 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)); + buffer.relative_bounds = Rect::from_min_size( + Pos2::new(buffer.indent.indent, size.y + margin_top), + 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 + margin_top; + last_size.x = image_size.x + IMAGE_PADDING; + last_size.y = image_size.y + margin_top; + buffer.relative_bounds = Rect::from_min_size( + Pos2::new(buffer.indent.indent, size.y + margin_top), + image_size, + ); + } else { + last_size.y = last_size.y.max(image_size.y + margin_top); + buffer.relative_bounds = + buffer.relative_bounds.translate(Vec2::new(ls.x, 0.0)); + } + } else { + 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 + margin_top), + new_size, + ); + size.y += new_size.y + margin_top; + } else { + last_image_size = Some(image_size + Vec2::new(IMAGE_PADDING, margin_top)); + } + } + [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; + } + + 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); + [ + 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, + }), + ] + } else { + if let Some(image_size) = last_image_size { + size.y += image_size.y; + } + + in_list = false; + [Some(buffer), None] + }; + + size.y += buffer_size.y + (margin_top + margin); + last_margin = margin; + first = false; + size.x = size.x.max(buffer_size.x + buffer_indent); + + result_buffers + }) + .filter_map(|p| p) + .collect::>(); + + size.y -= last_margin; + + if let Some(image_size) = last_image_size { + size.y += image_size.y; + } + + 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.font_size * metrics.font_size, + self.metrics.line_height * metrics.line_height, + ), + url_map: self.url_map, + image_urls: None, + } + } + + pub fn new_default(text: Vec>) -> Self { + Self { + indent: Indent { + modifier: None, + indent: 0.0, + }, + buffer: text, + metrics: Metrics::new(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, @@ -186,6 +420,11 @@ impl<'a> JotdownItem<'a> { 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 } @@ -200,19 +439,21 @@ impl<'a, T: Iterator>> Iterator for JotdownBufferIter<'a, T> { attrs: Attrs::new().family(Family::SansSerif), indent: &mut self.indent, image_url: None, - metrics: Metrics::new(1.0, 1.0), + 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(Cow::Borrowed(r.0), r.1)) + .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(); @@ -225,12 +466,29 @@ impl<'a, T: Iterator>> Iterator for JotdownBufferIter<'a, T> { None } else { let mut map = RangeMap::new(); - map.extend(urls.into_iter()); + map.extend( + urls.into_iter() + .map(|(range, url)| (range.start..range.end + 1, url)), + ); Some(map) }, buffer, image_url, metrics, + 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, + }, }); } } @@ -244,3 +502,88 @@ pub fn jotdown_into_buffers<'a, T: Iterator>>( 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 + } +}