From dac4c649f791e04966a465a7ac194a4fd48eef97 Mon Sep 17 00:00:00 2001 From: Piotr <114903054+elevenhsoft@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:02:23 +0200 Subject: [PATCH] markdown parser new way --- examples/markdown-widget/src/window.rs | 2 +- src/widget/markdown/mod.rs | 30 ++- src/widget/markdown/parser.rs | 339 +++++++++++++------------ src/widget/text.rs | 2 +- 4 files changed, 196 insertions(+), 177 deletions(-) diff --git a/examples/markdown-widget/src/window.rs b/examples/markdown-widget/src/window.rs index d85a28ac..21a0f76e 100644 --- a/examples/markdown-widget/src/window.rs +++ b/examples/markdown-widget/src/window.rs @@ -38,7 +38,7 @@ impl Application for Window { } fn view(&self) -> cosmic::prelude::Element { - let md = widget::text::markdown(self.markdown_text.clone()); + let md = widget::text::markdown(&self.markdown_text); widget::row().push(widget::scrollable(md)).into() } diff --git a/src/widget/markdown/mod.rs b/src/widget/markdown/mod.rs index d55568df..4addb99c 100644 --- a/src/widget/markdown/mod.rs +++ b/src/widget/markdown/mod.rs @@ -2,6 +2,7 @@ pub mod parser; use std::sync::{Mutex, OnceLock}; +use crate::{Element, Renderer}; use cosmic_text::{ Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Shaping, SwashCache, Weight, }; @@ -11,8 +12,7 @@ use iced_core::{ widget::{self, tree}, Widget, }; - -use crate::{Element, Renderer}; +use markdown::tokenize; static FONT_SYSTEM: OnceLock> = OnceLock::new(); @@ -23,15 +23,25 @@ pub struct Markdown { } impl Markdown { - pub fn new(content: String) -> Self { + pub fn new(content: &str) -> Self { FONT_SYSTEM.get_or_init(|| Mutex::new(FontSystem::new())); let metrics = metrics(14.0); let buffer = Buffer::new_empty(metrics); let mut editor = Editor::new(buffer); + let mut parser = parser::Parser::new(); + let blocks = tokenize(content); - editor.with_buffer_mut(|buffer| set_buffer_text(&content, buffer)); + parser.run(Box::leak(Box::new(blocks))); + + editor.with_buffer_mut(|buffer| { + set_buffer_text( + &mut FONT_SYSTEM.get().unwrap().lock().unwrap(), + &mut parser.get_spans(), + buffer, + ) + }); Self { syntax_editor: Mutex::new(editor), @@ -326,15 +336,17 @@ fn metrics(font_size: f32) -> Metrics { Metrics::new(font_size, line_height) } -fn set_buffer_text<'a>(text: &'a str, buffer: &mut Buffer) { +fn set_buffer_text( + font_system: &mut FontSystem, + collect_spans: &mut [(&'static str, Attrs)], + buffer: &mut Buffer, +) { let attrs = Attrs::new(); attrs.family(Family::SansSerif); - let spans: Vec<(&'a str, Attrs)> = parser::markdown_to_string(text, attrs); - buffer.set_rich_text( - &mut FONT_SYSTEM.get().unwrap().lock().unwrap(), - spans.iter().copied(), + font_system, + collect_spans.iter().copied(), attrs, Shaping::Advanced, ) diff --git a/src/widget/markdown/parser.rs b/src/widget/markdown/parser.rs index 1d954e31..d2a3483e 100644 --- a/src/widget/markdown/parser.rs +++ b/src/widget/markdown/parser.rs @@ -13,187 +13,194 @@ use syntect::{ use super::metrics; -pub fn markdown_to_string<'a, 'b>(content: &'a str, attrs: Attrs<'b>) -> Vec<(&'a str, Attrs<'b>)> -where - 'b: 'a, -{ - let mut result: Vec<(&'a str, Attrs)> = Vec::new(); - - for block in tokenize(content) { - result.extend(parse_block(block, attrs)); - result.push(("\n", attrs)); - } - - result +pub struct Parser<'a, 'b> { + spans: Vec<(&'a str, Attrs<'b>)>, } -fn parse_block<'a, 'b>(block: Block, attrs: Attrs<'b>) -> Vec<(&'a str, Attrs<'b>)> +impl<'a, 'b> Parser<'a, 'b> where 'b: 'a, { - let mut result: Vec<(&'a str, Attrs)> = Vec::new(); + pub fn new() -> Self { + Self { spans: Vec::new() } + } - match block { - Block::Header(span, level) => { - for item in span { - match level { - 1 => { - let attrs = attrs.metrics(metrics(24.0)); - attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); + pub fn get_spans(self) -> Vec<(&'a str, Attrs<'b>)> { + self.spans + } + + pub fn run<'block>(&mut self, block: &'block Vec) + where + 'block: 'b, + { + for block in block { + self.parse_block(block, Attrs::new()); + self.spans.push(("\n", Attrs::new())); + } + } + + fn parse_block<'block>(&mut self, block: &'block Block, attrs: Attrs<'block>) + where + 'block: 'b, + { + match block { + Block::Header(span, level) => { + for item in span { + match level { + 1 => { + let attrs = attrs.metrics(metrics(24.0)); + attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } + 2 => { + let attrs = attrs.metrics(metrics(22.0)); + attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } + 3 => { + let attrs = attrs.metrics(metrics(20.0)); + attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } + 4 => { + let attrs = attrs.metrics(metrics(18.0)); + attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } + 5 => { + let attrs = attrs.metrics(metrics(16.0)); + attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } + 6 => { + let attrs = attrs.metrics(metrics(14.0)); + attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } + _ => self.parse_span(item, Attrs::new()), } - 2 => { - let attrs = attrs.metrics(metrics(22.0)); - attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); - } - 3 => { - let attrs = attrs.metrics(metrics(20.0)); - attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); - } - 4 => { - let attrs = attrs.metrics(metrics(18.0)); - attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); - } - 5 => { - let attrs = attrs.metrics(metrics(16.0)); - attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); - } - 6 => { - let attrs = attrs.metrics(metrics(14.0)); - attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); - } - _ => result.extend(parse_span(item, attrs)), + } + self.spans.push(("\n", Attrs::new())); + } + Block::Paragraph(span) => { + for item in span { + self.parse_span(item, attrs); + } + self.spans.push(("\n", Attrs::new())); + } + Block::Blockquote(blockquote) => { + for item in blockquote { + let attrs = attrs.family(Family::Monospace); + self.parse_block(item, attrs); + } + self.spans.push(("\n", Attrs::new())); + } + Block::CodeBlock(lang, code) => { + let extension = if let Some(lang) = lang { + language_to_extension(lang) + } else { + "txt" + }; + + let attrs = attrs.family(Family::Monospace); + let code_block = highlight_code(code, extension, attrs); + + self.spans.extend(code_block); + self.spans.push(("\n", attrs)); + } + Block::OrderedList(listitem, _type) => { + for (num, item) in listitem.iter().enumerate() { + let attrs = attrs.family(Family::Serif); + self.spans + .push((Box::leak(Box::new(format!("{}. ", num + 1))), attrs)); + self.parse_listitem(item); + self.spans.push(("\n", attrs)); + } + self.spans.push(("\n", Attrs::new())); + } + Block::UnorderedList(listitem) => { + for item in listitem { + let attrs = attrs.family(Family::Serif); + self.spans.push((" - ", attrs)); + self.parse_listitem(item); + self.spans.push(("\n", attrs)); + } + self.spans.push(("\n", Attrs::new())); + } + Block::Raw(raw_text) => { + self.spans.push((raw_text, Attrs::new())); + self.spans.push(("\n", Attrs::new())); + } + Block::Hr => self.spans.push(("\n", Attrs::new())), + } + } + + fn parse_span<'c>(&mut self, span: &'c Span, attrs: Attrs<'c>) + where + 'c: 'b, + { + match span { + Span::Break => self.spans.push(("\n", attrs)), + Span::Text(text) => self.spans.push((text, attrs)), + Span::Code(code) => { + let attrs = attrs.family(Family::Monospace); + self.spans.push((code, attrs)); + } + Span::Link(name, url, _title) => { + let color = Attrs::new().color(link_color()); + let spans: &[(&str, Attrs)] = &[ + (Box::leak(format!("{}: ", name).into_boxed_str()), attrs), + (url, color), + ]; + self.spans.extend_from_slice(spans); + } + Span::Image(alt, url, title) => { + let color = Attrs::new().color(link_color()); + let spans: &[(&str, Attrs)] = if let Some(title) = title { + &[ + (Box::leak(format!("{}: ", title).into_boxed_str()), attrs), + (url, color), + ] + } else { + &[ + (Box::leak(format!("{}: ", alt).into_boxed_str()), attrs), + (url, color), + ] + }; + self.spans.extend_from_slice(spans); + } + Span::Emphasis(emphasis) => { + for item in emphasis { + let attrs = attrs.family(Family::Cursive); + self.parse_span(item, attrs); } } - result.push(("\n", attrs)); - } - Block::Paragraph(span) => { - for item in span { - result.extend(parse_span(item, attrs)); - } - result.push(("\n", attrs)); - } - Block::Blockquote(blockquote) => { - for item in blockquote { - let attrs = attrs.family(Family::Monospace); - result.extend(parse_block(item, attrs)); - } - result.push(("\n", attrs)); - } - Block::CodeBlock(lang, code) => { - if let Some(lang) = lang { - let code_block = highlight_code( - Box::leak(code.into_boxed_str()), - language_to_extension(lang), - attrs, - ); - - result.extend(code_block); - } else { - result.push((Box::leak(code.into_boxed_str()), attrs)); - }; - - result.push(("\n", attrs)); - } - Block::OrderedList(listitem, _type) => { - for (num, item) in listitem.iter().enumerate() { - let attrs = attrs.family(Family::Serif); - result.push((Box::leak(format!("{}. ", num + 1).into_boxed_str()), attrs)); - result.extend(parse_listitem(item.to_owned(), attrs)); - result.push(("\n", attrs)); - } - result.push(("\n", attrs)); - } - Block::UnorderedList(listitem) => { - for item in listitem { - let attrs = attrs.family(Family::Serif); - result.push((" - ", attrs)); - result.extend(parse_listitem(item.to_owned(), attrs)); - result.push(("\n", attrs)); - } - result.push(("\n", attrs)); - } - Block::Raw(raw_text) => { - result.push((Box::leak(raw_text.into_boxed_str()), attrs)); - - result.push(("\n", attrs)); - } - Block::Hr => result.push(("\n", attrs)), - } - - result -} - -fn parse_span<'a>(span: Span, attrs: Attrs<'a>) -> Vec<(&'a str, Attrs)> { - let mut result: Vec<(&'a str, Attrs)> = Vec::new(); - - match span { - Span::Break => result.push(("\n", attrs)), - Span::Text(text) => result.push((Box::leak(text.into_boxed_str()), attrs)), - Span::Code(code) => { - attrs.family(Family::Monospace); - result.push((Box::leak(code.into_boxed_str()), attrs)); - } - Span::Link(name, url, _title) => { - let spans: &[(&str, Attrs)] = &[ - (Box::leak(format!("{}: ", name).into_boxed_str()), attrs), - (Box::leak(url.into_boxed_str()), attrs.color(link_color())), - ]; - result.extend_from_slice(spans); - } - Span::Image(alt, url, title) => { - let spans: &[(&str, Attrs)] = if let Some(title) = title { - &[ - (Box::leak(format!("{}: ", title).into_boxed_str()), attrs), - (Box::leak(url.into_boxed_str()), attrs.color(link_color())), - ] - } else { - &[ - (Box::leak(format!("{}: ", alt).into_boxed_str()), attrs), - (Box::leak(url.into_boxed_str()), attrs.color(link_color())), - ] - }; - result.extend_from_slice(spans); - } - Span::Emphasis(emphasis) => { - for item in emphasis { - let attrs = attrs.family(Family::Cursive); - result.extend(parse_span(item, attrs)); - } - } - Span::Strong(strong) => { - for item in strong { - let attrs = attrs.weight(Weight::BOLD); - result.extend(parse_span(item, attrs)); + Span::Strong(strong) => { + for item in strong { + let attrs = attrs.weight(Weight::BOLD); + self.parse_span(item, attrs); + } } } } - result -} - -fn parse_listitem<'a>(item: ListItem, attrs: Attrs<'a>) -> Vec<(&'a str, Attrs)> { - let mut result: Vec<(&'a str, Attrs)> = Vec::new(); - - match item { - ListItem::Simple(simple) => { - for item in simple { - result.extend(parse_span(item, attrs)); + fn parse_listitem<'d>(&mut self, item: &'d ListItem) + where + 'd: 'b, + { + match item { + ListItem::Simple(simple) => { + for item in simple { + self.parse_span(item, Attrs::new()); + } } - } - ListItem::Paragraph(block) => { - for item in block { - result.extend(parse_block(item, attrs)); + ListItem::Paragraph(block) => { + for item in block { + self.parse_block(item, Attrs::new()); + } } } } - - result } fn highlight_code<'a>( @@ -276,8 +283,8 @@ fn cosmic_syntax() -> ThemeSet { theme_set } -fn language_to_extension(lang: String) -> &'static str { - match lang.as_str() { +fn language_to_extension(lang: &str) -> &'static str { + match lang { "python" => "py", "javascript" => "js", "java" => "java", diff --git a/src/widget/text.rs b/src/widget/text.rs index b08c7417..5b49d813 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -93,7 +93,7 @@ pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme #[cfg(feature = "markdown")] /// [`Markdown`] widget with md text parser -pub fn markdown(content: String) -> crate::widget::markdown::Markdown { +pub fn markdown(content: &str) -> crate::widget::markdown::Markdown { use crate::widget::markdown; markdown::Markdown::new(content)