1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 06:10:06 -04:00

Add a VERY experimental markdown viewer

This commit is contained in:
Emil Ernerfeldt
2021-01-27 20:14:53 +01:00
parent 7d8ebb4c8f
commit 6029a438a2
9 changed files with 300 additions and 12 deletions

View File

@@ -0,0 +1,57 @@
use super::markdown_parser::*;
use crate::*;
/// Parse and display a VERY simple and small subset of Markdown.
pub fn markdown(ui: &mut Ui, markdown: &str) {
ui.horizontal_wrapped(|ui| {
let row_height = ui.fonts()[TextStyle::Body].row_height();
let style = ui.style_mut();
style.spacing.interact_size.y = row_height;
style.spacing.item_spacing = vec2(0.0, 2.0);
for item in MarkdownParser::new(markdown) {
match item {
MarkdownItem::Newline => {
// ui.label("\n"); // too much spacing (paragraph spacing)
ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height
ui.end_row();
}
MarkdownItem::Separator => {
ui.add(Separator::new().horizontal());
}
MarkdownItem::BulletPoint(indent) => {
let indent = indent as f32 * row_height / 3.0;
ui.allocate_exact_size(vec2(indent, row_height), Sense::hover());
bullet_point(ui);
}
MarkdownItem::Body(range) => {
ui.label(range);
}
MarkdownItem::Heading(range) => {
ui.heading(range);
}
MarkdownItem::Emphasis(range) => {
ui.colored_label(Color32::WHITE, range);
}
MarkdownItem::InlineCode(range) => {
ui.code(range);
}
MarkdownItem::Hyperlink(text, url) => {
ui.add(Hyperlink::new(url).text(text));
}
};
}
});
}
fn bullet_point(ui: &mut Ui) -> Response {
let row_height = ui.fonts()[TextStyle::Body].row_height();
let (rect, response) = ui.allocate_exact_size(vec2(row_height, row_height), Sense::hover());
ui.painter().circle_filled(
rect.center(),
rect.height() / 5.0,
ui.style().visuals.text_color(),
);
response
}

View File

@@ -0,0 +1,153 @@
//! A parser for a VERY (and intentionally so) strict and limited sub-set of Markdown.
//!
//! WARNING: the parsed dialect is subject to change.
//!
//! Does not depend on anything else in egui (could perhaps be its own crate if it grows).
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MarkdownItem<'a> {
Newline,
Separator,
BulletPoint(usize),
Body(&'a str),
Heading(&'a str),
Emphasis(&'a str),
InlineCode(&'a str),
Hyperlink(&'a str, &'a str),
}
pub struct MarkdownParser<'a> {
s: &'a str,
start_of_line: bool,
}
impl<'a> MarkdownParser<'a> {
pub fn new(s: &'a str) -> Self {
Self {
s,
start_of_line: true,
}
}
}
impl<'a> Iterator for MarkdownParser<'a> {
type Item = MarkdownItem<'a>;
fn next(&mut self) -> Option<Self::Item> {
let Self { s, start_of_line } = self;
if s.is_empty() {
return None;
}
//
if s.starts_with("---\n") {
*s = &s[4..];
*start_of_line = true;
return Some(MarkdownItem::Separator);
}
//
if s.starts_with('\n') {
*s = &s[1..];
*start_of_line = true;
return Some(MarkdownItem::Newline);
}
// # Heading
if *start_of_line && s.starts_with("# ") {
*s = &s[2..];
*start_of_line = false;
let end = s.find('\n').unwrap_or_else(|| s.len());
let item = MarkdownItem::Heading(&s[..end]);
*s = &s[end..];
return Some(item);
}
// Ugly way to parse bullet points with indentation.
// TODO: parse leading spaces separately as `MarkdownItem::Indentation`.
for bullet in &["* ", " * ", " * ", " * ", " * "] {
// * bullet point
if *start_of_line && s.starts_with(bullet) {
*s = &s[bullet.len()..];
*start_of_line = false;
return Some(MarkdownItem::BulletPoint(bullet.len() - 2));
}
}
// `code`
if s.starts_with('`') {
*s = &s[1..];
*start_of_line = false;
if let Some(end) = s.find('`') {
let item = MarkdownItem::InlineCode(&s[..end]);
*s = &s[end + 1..];
return Some(item);
} else {
let end = s.len();
let item = MarkdownItem::InlineCode(&s[..end]);
*s = &s[end..];
return Some(item);
}
}
// *emphasis*
if s.starts_with('*') {
*s = &s[1..];
*start_of_line = false;
if let Some(end) = s.find('*') {
let item = MarkdownItem::Emphasis(&s[..end]);
*s = &s[end + 1..];
return Some(item);
} else {
let end = s.len();
let item = MarkdownItem::Emphasis(&s[..end]);
*s = &s[end..];
return Some(item);
}
}
// [text](url)
if s.starts_with('[') {
if let Some(bracket_end) = s.find(']') {
let text = &s[1..bracket_end];
if s[bracket_end + 1..].starts_with('(') {
if let Some(parens_end) = s[bracket_end + 2..].find(')') {
let parens_end = bracket_end + 2 + parens_end;
let url = &s[bracket_end + 2..parens_end];
*s = &s[parens_end + 1..];
*start_of_line = false;
return Some(MarkdownItem::Hyperlink(text, url));
}
}
}
}
let end = s[1..]
.find(&['#', '*', '`', '[', '\n'][..])
.map(|i| i + 1)
.unwrap_or_else(|| s.len());
let item = MarkdownItem::Body(&s[..end]);
*s = &s[end..];
*start_of_line = false;
Some(item)
}
}
#[test]
fn test_markdown() {
let parts: Vec<_> = MarkdownParser::new("# Hello\nworld `of` *fun* [link](url)").collect();
assert_eq!(
parts,
vec![
MarkdownItem::Heading("Hello"),
MarkdownItem::Newline,
MarkdownItem::Body("world "),
MarkdownItem::InlineCode("of"),
MarkdownItem::Body(" "),
MarkdownItem::Emphasis("fun"),
MarkdownItem::Body(" "),
MarkdownItem::Hyperlink("link", "url")
]
);
}

View File

@@ -0,0 +1,8 @@
//! Experimental parts of egui, that may change suddenly or get removed.
//!
//! Be very careful about depending on the experimental parts of egui!
mod markdown;
pub mod markdown_parser;
pub use markdown::markdown;

View File

@@ -82,6 +82,7 @@ mod animation_manager;
pub mod containers;
mod context;
mod data;
pub mod experimental;
pub(crate) mod grid;
mod id;
mod input_state;