cosmic-jotdown/src/lib.rs
2024-04-19 22:57:43 -04:00

645 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Item = Event<'a>>> {
pub djot: T,
pub indent: Vec<Indent>,
}
struct JotdownIntoBuffer<'a, 'b, T: Iterator<Item = Event<'a>>> {
pub djot: &'b mut T,
pub attrs: Attrs<'static>,
pub metrics: Metrics,
pub indent: &'b mut Vec<Indent>,
pub image_url: Option<Cow<'a, str>>,
pub link_start: usize,
pub urls: Vec<(std::ops::Range<usize>, Cow<'a, str>)>,
pub location: usize,
pub added: bool,
pub top_level_container: Option<Container<'a>>,
}
#[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<ListKind>,
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<Item = Event<'a>>> Iterator for JotdownIntoBuffer<'a, 'b, T> {
type Item = (&'a str, Attrs<'static>);
fn next(&mut self) -> Option<Self::Item> {
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<Cow<'a, str>>,
pub margin: f32,
pub url_map: Option<Cow<'a, [(usize, usize, Cow<'a, str>)]>>,
}
#[derive(Clone)]
pub struct ResolvedJotdownItem<'a> {
pub indent: Indent,
pub buffer: Buffer,
pub metrics: Metrics,
pub relative_bounds: Rect,
pub url_map: Option<RangeMap<usize, Cow<'a, str>>>,
pub image_urls: Option<(String, String)>,
}
pub fn resolve_paragraphs<'a>(
paragaphs: &[JotdownItem<'a>],
viewbox: Vec2,
font_system: &mut FontSystem,
metrics: Metrics,
align: Option<Align>,
factor: f32,
base_uri: &'static str,
) -> (Vec2, Vec<ResolvedJotdownItem<'a>>) {
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<Vec2> = 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::<Vec<_>>();
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<Align>,
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<RichText<'a>>) -> 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<Align>,
) -> 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<Item = Event<'a>>> Iterator for JotdownBufferIter<'a, T> {
type Item = JotdownItem<'a>;
fn next(&mut self) -> Option<Self::Item> {
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::<Vec<_>>();
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<Item = Event<'a>>>(
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
}
}