Replace markdown editor with new 'EasyMark' markup language
This commit is contained in:
parent
b647592a5a
commit
16d66bd22d
|
|
@ -0,0 +1,337 @@
|
||||||
|
//! A parser for `EasyMark`: a very simple markup language.
|
||||||
|
//!
|
||||||
|
//! WARNING: `EasyMark` is subject to change.
|
||||||
|
//!
|
||||||
|
//! This module does not depend on anything else in egui
|
||||||
|
//! and should perhaps be its own crate.
|
||||||
|
//
|
||||||
|
//! # `EasyMark` design goals:
|
||||||
|
//! 1. easy to parse
|
||||||
|
//! 2. easy to learn
|
||||||
|
//! 3. similar to markdown
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Item<'a> {
|
||||||
|
/// `\n`
|
||||||
|
Newline,
|
||||||
|
///
|
||||||
|
Text(Style, &'a str),
|
||||||
|
/// title, url
|
||||||
|
Hyperlink(Style, &'a str, &'a str),
|
||||||
|
/// leading space before e.g. a [`Self::BulletPoint`].
|
||||||
|
Indentation(usize),
|
||||||
|
/// >
|
||||||
|
QuoteIndent,
|
||||||
|
/// - a point well made.
|
||||||
|
BulletPoint,
|
||||||
|
/// 1. numbered list. The string is the number(s).
|
||||||
|
NumberedPoint(&'a str),
|
||||||
|
/// ---
|
||||||
|
Separator,
|
||||||
|
/// language, code
|
||||||
|
CodeBlock(&'a str, &'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
/// # heading (large text)
|
||||||
|
pub heading: bool,
|
||||||
|
/// > quoted (slightly dimmer color or other font style)
|
||||||
|
pub quoted: bool,
|
||||||
|
/// `code` (monospace, some other color)
|
||||||
|
pub code: bool,
|
||||||
|
/// self.strong* (emphasized, e.g. bold)
|
||||||
|
pub strong: bool,
|
||||||
|
/// _underline_
|
||||||
|
pub underline: bool,
|
||||||
|
/// -strikethrough-
|
||||||
|
pub strikethrough: bool,
|
||||||
|
/// /italics/
|
||||||
|
pub italics: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parser for the `EasyMark` markup language.
|
||||||
|
///
|
||||||
|
/// See the module-level documentation for details.
|
||||||
|
///
|
||||||
|
/// # Example:
|
||||||
|
/// ```
|
||||||
|
/// # use egui::experimental::easy_mark_parser::Parser;
|
||||||
|
/// for item in Parser::new("Hello *world*!") {
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
pub struct Parser<'a> {
|
||||||
|
/// The remainer of the input text
|
||||||
|
s: &'a str,
|
||||||
|
/// Are we at the start of a line?
|
||||||
|
start_of_line: bool,
|
||||||
|
/// Current self.style. Reset after a newline.
|
||||||
|
style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parser<'a> {
|
||||||
|
pub fn new(s: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
s,
|
||||||
|
start_of_line: true,
|
||||||
|
style: Style::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `1. `, `42. ` etc.
|
||||||
|
fn numbered_list(&mut self) -> Option<Item<'a>> {
|
||||||
|
let bytes = self.s.as_bytes();
|
||||||
|
// 1. numbered bullet
|
||||||
|
if bytes.len() >= 3 && bytes[0].is_ascii_digit() && bytes[1] == b'.' && bytes[2] == b' ' {
|
||||||
|
let number = &self.s[0..1];
|
||||||
|
self.s = &self.s[3..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::NumberedPoint(number));
|
||||||
|
}
|
||||||
|
// 42. double-digit numbered bullet
|
||||||
|
if bytes.len() >= 4
|
||||||
|
&& bytes[0].is_ascii_digit()
|
||||||
|
&& bytes[1].is_ascii_digit()
|
||||||
|
&& bytes[2] == b'.'
|
||||||
|
&& bytes[3] == b' '
|
||||||
|
{
|
||||||
|
let number = &self.s[0..2];
|
||||||
|
self.s = &self.s[4..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::NumberedPoint(number));
|
||||||
|
}
|
||||||
|
// There is no triple-digit numbered bullet. Please don't make numbered lists that long.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```{language}\n{code}```
|
||||||
|
fn code_block(&mut self) -> Option<Item<'a>> {
|
||||||
|
if let Some(language_start) = self.s.strip_prefix("```") {
|
||||||
|
if let Some(newline) = language_start.find('\n') {
|
||||||
|
let language = &language_start[..newline];
|
||||||
|
let code_start = &language_start[newline + 1..];
|
||||||
|
if let Some(end) = code_start.find("\n```") {
|
||||||
|
let code = &code_start[..end].trim();
|
||||||
|
self.s = &code_start[end + 4..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::CodeBlock(language, code));
|
||||||
|
} else {
|
||||||
|
self.s = "";
|
||||||
|
return Some(Item::CodeBlock(language, code_start));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// `code`
|
||||||
|
fn inline_code(&mut self) -> Option<Item<'a>> {
|
||||||
|
if let Some(rest) = self.s.strip_prefix('`') {
|
||||||
|
self.s = rest;
|
||||||
|
self.start_of_line = false;
|
||||||
|
self.style.code = true;
|
||||||
|
let rest_of_line = &self.s[..self.s.find('\n').unwrap_or_else(|| self.s.len())];
|
||||||
|
if let Some(end) = rest_of_line.find('`') {
|
||||||
|
let item = Item::Text(self.style, &self.s[..end]);
|
||||||
|
self.s = &self.s[end + 1..];
|
||||||
|
self.style.code = false;
|
||||||
|
return Some(item);
|
||||||
|
} else {
|
||||||
|
let end = rest_of_line.len();
|
||||||
|
let item = Item::Text(self.style, rest_of_line);
|
||||||
|
self.s = &self.s[end..];
|
||||||
|
self.style.code = false;
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `<url>` or `[link](url)`
|
||||||
|
fn url(&mut self) -> Option<Item<'a>> {
|
||||||
|
if self.s.starts_with('<') {
|
||||||
|
let this_line = &self.s[..self.s.find('\n').unwrap_or_else(|| self.s.len())];
|
||||||
|
if let Some(url_end) = this_line.find('>') {
|
||||||
|
let url = &self.s[1..url_end];
|
||||||
|
self.s = &self.s[url_end + 1..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::Hyperlink(self.style, url, url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [text](url)
|
||||||
|
if self.s.starts_with('[') {
|
||||||
|
let this_line = &self.s[..self.s.find('\n').unwrap_or_else(|| self.s.len())];
|
||||||
|
if let Some(bracket_end) = this_line.find(']') {
|
||||||
|
let text = &this_line[1..bracket_end];
|
||||||
|
if this_line[bracket_end + 1..].starts_with('(') {
|
||||||
|
if let Some(parens_end) = this_line[bracket_end + 2..].find(')') {
|
||||||
|
let parens_end = bracket_end + 2 + parens_end;
|
||||||
|
let url = &self.s[bracket_end + 2..parens_end];
|
||||||
|
self.s = &self.s[parens_end + 1..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::Hyperlink(self.style, text, url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Parser<'a> {
|
||||||
|
type Item = Item<'a>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
if self.s.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// \n
|
||||||
|
if self.s.starts_with('\n') {
|
||||||
|
self.s = &self.s[1..];
|
||||||
|
self.start_of_line = true;
|
||||||
|
self.style = Style::default();
|
||||||
|
return Some(Item::Newline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore line break (continue on the same line)
|
||||||
|
if self.s.starts_with("\\\n") && self.s.len() >= 2 {
|
||||||
|
self.s = &self.s[2..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// \ escape (to show e.g. a backtick)
|
||||||
|
if self.s.starts_with('\\') && self.s.len() >= 2 {
|
||||||
|
let text = &self.s[1..2];
|
||||||
|
self.s = &self.s[2..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::Text(self.style, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.start_of_line {
|
||||||
|
// leading space (indentation)
|
||||||
|
if self.s.starts_with(' ') {
|
||||||
|
let length = self.s.find(|c| c != ' ').unwrap_or_else(|| self.s.len());
|
||||||
|
self.s = &self.s[length..];
|
||||||
|
self.start_of_line = true; // indentation doesn't count
|
||||||
|
return Some(Item::Indentation(length));
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Heading
|
||||||
|
if let Some(after) = self.s.strip_prefix("# ") {
|
||||||
|
self.s = after;
|
||||||
|
self.start_of_line = false;
|
||||||
|
self.style.heading = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// > quote
|
||||||
|
if let Some(after) = self.s.strip_prefix("> ") {
|
||||||
|
self.s = after;
|
||||||
|
self.start_of_line = true; // quote indentation doesn't count
|
||||||
|
self.style.quoted = true;
|
||||||
|
return Some(Item::QuoteIndent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// - bullet point
|
||||||
|
if self.s.starts_with("- ") {
|
||||||
|
self.s = &self.s[2..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::BulletPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `1. `, `42. ` etc.
|
||||||
|
if let Some(item) = self.numbered_list() {
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- separator
|
||||||
|
if let Some(after) = self.s.strip_prefix("---") {
|
||||||
|
self.s = after.trim_start_matches('-'); // remove extra dashes
|
||||||
|
self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(Item::Separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ```{language}\n{code}```
|
||||||
|
if let Some(item) = self.code_block() {
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `code`
|
||||||
|
if let Some(item) = self.inline_code() {
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rest) = self.s.strip_prefix('*') {
|
||||||
|
self.s = rest;
|
||||||
|
self.start_of_line = false;
|
||||||
|
self.style.strong = !self.style.strong;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(rest) = self.s.strip_prefix('_') {
|
||||||
|
self.s = rest;
|
||||||
|
self.start_of_line = false;
|
||||||
|
self.style.underline = !self.style.underline;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(rest) = self.s.strip_prefix('~') {
|
||||||
|
self.s = rest;
|
||||||
|
self.start_of_line = false;
|
||||||
|
self.style.strikethrough = !self.style.strikethrough;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(rest) = self.s.strip_prefix('/') {
|
||||||
|
self.s = rest;
|
||||||
|
self.start_of_line = false;
|
||||||
|
self.style.italics = !self.style.italics;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `<url>` or `[link](url)`
|
||||||
|
if let Some(item) = self.url() {
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swallow everything up to the next special character:
|
||||||
|
let special = self.s[1..]
|
||||||
|
.find(&['*', '`', '~', '_', '/', '\\', '<', '[', '\n'][..])
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or_else(|| self.s.len());
|
||||||
|
let item = Item::Text(self.style, &self.s[..special]);
|
||||||
|
self.s = &self.s[special..];
|
||||||
|
self.start_of_line = false;
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_easy_mark_parser() {
|
||||||
|
let items: Vec<_> = Parser::new("~strikethrough `code`~").collect();
|
||||||
|
assert_eq!(
|
||||||
|
items,
|
||||||
|
vec![
|
||||||
|
Item::Text(
|
||||||
|
Style {
|
||||||
|
strikethrough: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
"strikethrough "
|
||||||
|
),
|
||||||
|
Item::Text(
|
||||||
|
Style {
|
||||||
|
code: true,
|
||||||
|
strikethrough: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
"code"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
use super::easy_mark_parser as easy_mark;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Parse and display a VERY simple and small subset of Markdown.
|
||||||
|
pub fn easy_mark(ui: &mut Ui, easy_mark: &str) {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
let row_height = ui.fonts()[TextStyle::Body].row_height();
|
||||||
|
let one_indent = row_height / 2.0;
|
||||||
|
let spacing = vec2(0.0, 2.0);
|
||||||
|
let style = ui.style_mut();
|
||||||
|
style.spacing.interact_size.y = row_height;
|
||||||
|
style.spacing.item_spacing = spacing;
|
||||||
|
|
||||||
|
for item in easy_mark::Parser::new(easy_mark) {
|
||||||
|
match item {
|
||||||
|
easy_mark::Item::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();
|
||||||
|
}
|
||||||
|
|
||||||
|
easy_mark::Item::Text(style, text) => {
|
||||||
|
ui.add(label_from_style(text, style));
|
||||||
|
}
|
||||||
|
easy_mark::Item::Hyperlink(style, text, url) => {
|
||||||
|
let label = label_from_style(text, style);
|
||||||
|
ui.add(Hyperlink::from_label_and_url(label, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
easy_mark::Item::Separator => {
|
||||||
|
ui.add(Separator::new().horizontal());
|
||||||
|
}
|
||||||
|
easy_mark::Item::Indentation(indent) => {
|
||||||
|
let indent = indent as f32 * one_indent;
|
||||||
|
ui.allocate_exact_size(vec2(indent, row_height), Sense::hover());
|
||||||
|
}
|
||||||
|
easy_mark::Item::QuoteIndent => {
|
||||||
|
let rect = ui
|
||||||
|
.allocate_exact_size(vec2(row_height, row_height), Sense::hover())
|
||||||
|
.0;
|
||||||
|
let rect = rect.expand2(spacing * 0.5);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[rect.center_top(), rect.center_bottom()],
|
||||||
|
(1.0, ui.style().visuals.weak_text_color()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
easy_mark::Item::BulletPoint => {
|
||||||
|
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||||
|
bullet_point(ui, one_indent);
|
||||||
|
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||||
|
}
|
||||||
|
easy_mark::Item::NumberedPoint(number) => {
|
||||||
|
let width = 3.0 * one_indent;
|
||||||
|
numbered_point(ui, width, number);
|
||||||
|
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||||
|
}
|
||||||
|
easy_mark::Item::CodeBlock(_language, code) => {
|
||||||
|
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||||
|
let mut rect = ui.monospace(code).rect;
|
||||||
|
rect = rect.expand(1.0); // looks better
|
||||||
|
rect.max.x = ui.max_rect_finite().max.x;
|
||||||
|
let code_bg_color = ui.style().visuals.code_bg_color;
|
||||||
|
ui.painter().set(
|
||||||
|
where_to_put_background,
|
||||||
|
Shape::rect_filled(rect, 1.0, code_bg_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label_from_style(text: &str, style: easy_mark::Style) -> Label {
|
||||||
|
let easy_mark::Style {
|
||||||
|
heading,
|
||||||
|
quoted,
|
||||||
|
code,
|
||||||
|
strong,
|
||||||
|
underline,
|
||||||
|
strikethrough,
|
||||||
|
italics,
|
||||||
|
} = style;
|
||||||
|
|
||||||
|
let mut label = Label::new(text);
|
||||||
|
if heading {
|
||||||
|
label = label.heading().strong();
|
||||||
|
}
|
||||||
|
if code {
|
||||||
|
label = label.code();
|
||||||
|
}
|
||||||
|
if strong {
|
||||||
|
label = label.strong();
|
||||||
|
} else if quoted {
|
||||||
|
label = label.weak();
|
||||||
|
}
|
||||||
|
if underline {
|
||||||
|
label = label.underline();
|
||||||
|
}
|
||||||
|
if strikethrough {
|
||||||
|
label = label.strikethrough();
|
||||||
|
}
|
||||||
|
if italics {
|
||||||
|
label = label.italics();
|
||||||
|
}
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bullet_point(ui: &mut Ui, width: f32) -> Response {
|
||||||
|
let row_height = ui.fonts()[TextStyle::Body].row_height();
|
||||||
|
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||||
|
ui.painter().circle_filled(
|
||||||
|
rect.center(),
|
||||||
|
rect.height() / 8.0,
|
||||||
|
ui.style().visuals.strong_text_color(),
|
||||||
|
);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
|
||||||
|
let row_height = ui.fonts()[TextStyle::Body].row_height();
|
||||||
|
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||||
|
let text = format!("{}.", number);
|
||||||
|
let text_color = ui.style().visuals.strong_text_color();
|
||||||
|
ui.painter().text(
|
||||||
|
rect.right_center(),
|
||||||
|
Align2::RIGHT_CENTER,
|
||||||
|
text,
|
||||||
|
TextStyle::Body,
|
||||||
|
text_color,
|
||||||
|
);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
//! 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")
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Be very careful about depending on the experimental parts of egui!
|
//! Be very careful about depending on the experimental parts of egui!
|
||||||
|
|
||||||
mod markdown;
|
pub mod easy_mark_parser;
|
||||||
pub mod markdown_parser;
|
mod easy_mark_viewer;
|
||||||
|
|
||||||
pub use markdown::markdown;
|
pub use easy_mark_viewer::easy_mark;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ impl Default for Demos {
|
||||||
Box::new(super::dancing_strings::DancingStrings::default()),
|
Box::new(super::dancing_strings::DancingStrings::default()),
|
||||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||||
Box::new(super::font_book::FontBook::default()),
|
Box::new(super::font_book::FontBook::default()),
|
||||||
Box::new(super::markdown_editor::MarkdownEditor::default()),
|
|
||||||
Box::new(super::painting::Painting::default()),
|
Box::new(super::painting::Painting::default()),
|
||||||
Box::new(super::scrolling::Scrolling::default()),
|
Box::new(super::scrolling::Scrolling::default()),
|
||||||
Box::new(super::sliders::Sliders::default()),
|
Box::new(super::sliders::Sliders::default()),
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
use egui::*;
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub struct MarkdownEditor {
|
|
||||||
markdown: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MarkdownEditor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
markdown: r#"
|
|
||||||
# Markdown editor
|
|
||||||
Markdown support in egui is experimental, and *very* limited. There are:
|
|
||||||
|
|
||||||
* bullet points
|
|
||||||
* with sub-points
|
|
||||||
* `inline code`
|
|
||||||
* *emphasis*
|
|
||||||
* [hyperlinks](https://github.com/emilk/egui)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Also the separator
|
|
||||||
|
|
||||||
"#
|
|
||||||
.trim_start()
|
|
||||||
.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Demo for MarkdownEditor {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"🖹 Markdown Editor"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
|
||||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
|
||||||
use super::View;
|
|
||||||
self.ui(ui);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::View for MarkdownEditor {
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
egui::reset_button(ui, self);
|
|
||||||
ui.add(crate::__egui_github_link_file!());
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
ScrollArea::auto_sized()
|
|
||||||
.id_source("source")
|
|
||||||
.show(&mut columns[0], |ui| {
|
|
||||||
ui.text_edit_multiline(&mut self.markdown);
|
|
||||||
});
|
|
||||||
ScrollArea::auto_sized()
|
|
||||||
.id_source("rendered")
|
|
||||||
.show(&mut columns[1], |ui| {
|
|
||||||
egui::experimental::markdown(ui, &self.markdown);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,7 +14,6 @@ pub mod font_contents_emoji;
|
||||||
pub mod font_contents_ubuntu;
|
pub mod font_contents_ubuntu;
|
||||||
pub mod input_test;
|
pub mod input_test;
|
||||||
pub mod layout_test;
|
pub mod layout_test;
|
||||||
pub mod markdown_editor;
|
|
||||||
pub mod painting;
|
pub mod painting;
|
||||||
pub mod scrolling;
|
pub mod scrolling;
|
||||||
pub mod sliders;
|
pub mod sliders;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct EasyMarkEditor {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EasyMarkEditor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
code: DEFAULT_CODE.trim().to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl epi::App for EasyMarkEditor {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"🖹 EasyMark editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
self.ui(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EasyMarkEditor {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
egui::reset_button(ui, self);
|
||||||
|
ui.add(crate::__egui_github_link_file!());
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
ScrollArea::auto_sized()
|
||||||
|
.id_source("source")
|
||||||
|
.show(&mut columns[0], |ui| {
|
||||||
|
// ui.text_edit_multiline(&mut self.code);
|
||||||
|
ui.add(TextEdit::multiline(&mut self.code).text_style(TextStyle::Monospace));
|
||||||
|
});
|
||||||
|
ScrollArea::auto_sized()
|
||||||
|
.id_source("rendered")
|
||||||
|
.show(&mut columns[1], |ui| {
|
||||||
|
egui::experimental::easy_mark(ui, &self.code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CODE: &str = r#"
|
||||||
|
# EasyMark
|
||||||
|
EasyMark is a markup language, designed for extreme simplicity.
|
||||||
|
|
||||||
|
```
|
||||||
|
WARNING: EasyMark is still an evolving specification,
|
||||||
|
and is also missing some features.
|
||||||
|
```
|
||||||
|
|
||||||
|
----------------
|
||||||
|
|
||||||
|
# At a glance
|
||||||
|
- inline text:
|
||||||
|
- normal, `code`, *strong*, ~strikethrough~, _underline_, /italics/
|
||||||
|
- `\` escapes the next character
|
||||||
|
- [hyperlink](https://github.com/emilk/egui)
|
||||||
|
- Embedded URL: <https://github.com/emilk/egui>
|
||||||
|
- `# ` header
|
||||||
|
- `---` separator (horizontal line)
|
||||||
|
- `> ` quote
|
||||||
|
- `- ` bullet list
|
||||||
|
- `1. ` numbered list
|
||||||
|
- \`\`\` code fence
|
||||||
|
|
||||||
|
# Design
|
||||||
|
> /"Why do what everyone else is doing, when everyone else is already doing it?"
|
||||||
|
> \- Emil
|
||||||
|
|
||||||
|
Goals:
|
||||||
|
1. easy to parse
|
||||||
|
2. easy to learn
|
||||||
|
3. similar to markdown
|
||||||
|
|
||||||
|
[The reference parser](https://github.com/emilk/egui/blob/master/egui/src/experimental/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion.
|
||||||
|
|
||||||
|
There is never more than one way to accomplish the same thing, and each special character is only used for one thing. For instance `*` is used for *strong* and `-` is used for bullet lists. There is no alternative way to specify the *strong* style or getting a bullet list.
|
||||||
|
|
||||||
|
Similarity to markdown is kept when possible, but with much less ambiguity and some improvements (like _underlining_).
|
||||||
|
|
||||||
|
# Details
|
||||||
|
All style changes are single characters, so it is `*strong*`, NOT `**strong**`. Style is reset by a matching character, or at the end of the line.
|
||||||
|
|
||||||
|
Style change characters and escapes (`\`) work everywhere except for in inline code, code blocks and in URLs.
|
||||||
|
|
||||||
|
You can mix styles. For instance: /italics _underline_/ and *strong `code`*.
|
||||||
|
|
||||||
|
You can use styles on URLs: ~my webpage is at <http://www.example.com>~.
|
||||||
|
|
||||||
|
Newlines are preserved. If you want to continue text on the same line, just do so. Alternatively, escape the newline by ending the line with a backslash (`\`). \
|
||||||
|
Escaping the newline effectively ignores it.
|
||||||
|
|
||||||
|
The style characters are chosen to be similar to what they are representing:
|
||||||
|
`_` = _underline_
|
||||||
|
`~` = ~strikethrough~ (`-` is too common in normal text)
|
||||||
|
`/` = /italics/
|
||||||
|
`*` = *strong*
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
- Sub-headers (`## h2`, `### h3` etc)
|
||||||
|
- Images
|
||||||
|
- we want to be able to optionally specify size (width and\/or height)
|
||||||
|
- centering of images is very desirable
|
||||||
|
- captioning (image with a text underneath it)
|
||||||
|
- `![caption=My image][width=200][center](url)` ?
|
||||||
|
- Tables
|
||||||
|
"#;
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
mod color_test;
|
mod color_test;
|
||||||
mod demo;
|
mod demo;
|
||||||
|
mod easy_mark_editor;
|
||||||
mod fractal_clock;
|
mod fractal_clock;
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
mod http_app;
|
mod http_app;
|
||||||
|
|
||||||
pub use color_test::ColorTest;
|
pub use color_test::ColorTest;
|
||||||
pub use demo::DemoApp;
|
pub use demo::DemoApp;
|
||||||
|
pub use easy_mark_editor::EasyMarkEditor;
|
||||||
pub use fractal_clock::FractalClock;
|
pub use fractal_clock::FractalClock;
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
pub use http_app::HttpApp;
|
pub use http_app::HttpApp;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#[cfg_attr(feature = "persistence", serde(default))]
|
#[cfg_attr(feature = "persistence", serde(default))]
|
||||||
pub struct Apps {
|
pub struct Apps {
|
||||||
demo: crate::apps::DemoApp,
|
demo: crate::apps::DemoApp,
|
||||||
|
easy_mark_editor: crate::apps::EasyMarkEditor,
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
http: crate::apps::HttpApp,
|
http: crate::apps::HttpApp,
|
||||||
clock: crate::apps::FractalClock,
|
clock: crate::apps::FractalClock,
|
||||||
|
|
@ -14,6 +15,7 @@ impl Apps {
|
||||||
fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut dyn epi::App)> {
|
fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut dyn epi::App)> {
|
||||||
vec![
|
vec![
|
||||||
("demo", &mut self.demo as &mut dyn epi::App),
|
("demo", &mut self.demo as &mut dyn epi::App),
|
||||||
|
("easymark", &mut self.easy_mark_editor as &mut dyn epi::App),
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
("http", &mut self.http as &mut dyn epi::App),
|
("http", &mut self.http as &mut dyn epi::App),
|
||||||
("clock", &mut self.clock as &mut dyn epi::App),
|
("clock", &mut self.clock as &mut dyn epi::App),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue