Feat: UI / UX
This commit is contained in:
@@ -160,6 +160,12 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
ops.push(Op::Branch(else_ops.len()));
|
||||
ops.extend(else_ops);
|
||||
}
|
||||
} else if word == "case" {
|
||||
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
ops.extend(case_ops);
|
||||
} else if word == "of" || word == "endof" || word == "endcase" {
|
||||
return Err(format!("unexpected '{word}'"));
|
||||
} else if !compile_word(word, Some(*span), &mut ops, dict) {
|
||||
return Err(format!("unknown word: {word}"));
|
||||
}
|
||||
@@ -300,3 +306,69 @@ fn compile_if(
|
||||
|
||||
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
|
||||
}
|
||||
|
||||
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
|
||||
let mut depth = 1;
|
||||
let mut endcase_pos = None;
|
||||
let mut clauses: Vec<(usize, usize)> = Vec::new();
|
||||
let mut last_of = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"case" => depth += 1,
|
||||
"endcase" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
endcase_pos = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
"of" if depth == 1 => last_of = Some(i),
|
||||
"endof" if depth == 1 => {
|
||||
let of_pos = last_of.ok_or("'endof' without matching 'of'")?;
|
||||
clauses.push((of_pos, i));
|
||||
last_of = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let endcase_pos = endcase_pos.ok_or("missing 'endcase'")?;
|
||||
|
||||
let mut ops = Vec::new();
|
||||
let mut branch_fixups: Vec<usize> = Vec::new();
|
||||
let mut clause_start = 0;
|
||||
|
||||
for &(of_pos, endof_pos) in &clauses {
|
||||
let test_ops = compile(&tokens[clause_start..of_pos], dict)?;
|
||||
let body_ops = compile(&tokens[of_pos + 1..endof_pos], dict)?;
|
||||
|
||||
ops.extend(test_ops);
|
||||
ops.push(Op::Over);
|
||||
ops.push(Op::Eq);
|
||||
ops.push(Op::BranchIfZero(body_ops.len() + 2, None, None));
|
||||
ops.push(Op::Drop);
|
||||
ops.extend(body_ops);
|
||||
branch_fixups.push(ops.len());
|
||||
ops.push(Op::Branch(0));
|
||||
|
||||
clause_start = endof_pos + 1;
|
||||
}
|
||||
|
||||
let default_tokens = &tokens[clause_start..endcase_pos];
|
||||
if !default_tokens.is_empty() {
|
||||
let default_ops = compile(default_tokens, dict)?;
|
||||
ops.extend(default_ops);
|
||||
}
|
||||
|
||||
ops.push(Op::Drop);
|
||||
|
||||
let end = ops.len();
|
||||
for pos in branch_fixups {
|
||||
ops[pos] = Op::Branch(end - pos - 1);
|
||||
}
|
||||
|
||||
Ok((ops, endcase_pos + 1))
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ pub enum Op {
|
||||
Emit,
|
||||
Get,
|
||||
Set,
|
||||
SetKeep,
|
||||
GetContext(&'static str),
|
||||
Rand(Option<SourceSpan>),
|
||||
ExpRand(Option<SourceSpan>),
|
||||
|
||||
@@ -637,6 +637,16 @@ impl Forth {
|
||||
.expect("var_writes taken")
|
||||
.insert(name, val);
|
||||
}
|
||||
Op::SetKeep => {
|
||||
let name = pop(stack)?;
|
||||
let name = name.as_str()?.to_string();
|
||||
let val = stack.last().ok_or("Stack underflow")?.clone();
|
||||
var_writes_cell
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.expect("var_writes taken")
|
||||
.insert(name, val);
|
||||
}
|
||||
|
||||
Op::GetContext(name) => {
|
||||
let val = match *name {
|
||||
|
||||
@@ -272,6 +272,14 @@ pub(crate) fn compile_word(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(var_name) = name.strip_prefix(',') {
|
||||
if !var_name.is_empty() {
|
||||
ops.push(Op::PushStr(Arc::from(var_name), span));
|
||||
ops.push(Op::SetKeep);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(midi) = parse_note_name(name) {
|
||||
ops.push(Op::PushInt(midi, span));
|
||||
return true;
|
||||
|
||||
@@ -558,6 +558,16 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ",<var>",
|
||||
aliases: &[],
|
||||
category: "Variables",
|
||||
stack: "(val -- val)",
|
||||
desc: "Store value in variable, keep on stack",
|
||||
example: "440 ,freq => 440",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Definitions
|
||||
Word {
|
||||
name: ":",
|
||||
|
||||
@@ -3,5 +3,5 @@ mod parser;
|
||||
mod theme;
|
||||
|
||||
pub use highlighter::{CodeHighlighter, NoHighlight};
|
||||
pub use parser::parse;
|
||||
pub use parser::{parse, CodeBlock, ParsedMarkdown};
|
||||
pub use theme::{DefaultTheme, MarkdownTheme};
|
||||
|
||||
@@ -5,17 +5,31 @@ use ratatui::text::{Line as RLine, Span};
|
||||
use crate::highlighter::CodeHighlighter;
|
||||
use crate::theme::MarkdownTheme;
|
||||
|
||||
pub struct CodeBlock {
|
||||
pub start_line: usize,
|
||||
pub end_line: usize,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
pub struct ParsedMarkdown {
|
||||
pub lines: Vec<RLine<'static>>,
|
||||
pub code_blocks: Vec<CodeBlock>,
|
||||
}
|
||||
|
||||
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
md: &str,
|
||||
theme: &T,
|
||||
highlighter: &H,
|
||||
) -> Vec<RLine<'static>> {
|
||||
) -> ParsedMarkdown {
|
||||
let processed = preprocess_markdown(md);
|
||||
let text = minimad::Text::from(processed.as_str());
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let mut code_line_nr: usize = 0;
|
||||
let mut table_buffer: Vec<TableRow> = Vec::new();
|
||||
let mut code_blocks: Vec<CodeBlock> = Vec::new();
|
||||
let mut current_block_start: Option<usize> = None;
|
||||
let mut current_block_source: Vec<String> = Vec::new();
|
||||
|
||||
let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>, theme: &T| {
|
||||
if buf.is_empty() {
|
||||
@@ -27,16 +41,43 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
}
|
||||
};
|
||||
|
||||
let close_block = |start: Option<usize>,
|
||||
source: &mut Vec<String>,
|
||||
blocks: &mut Vec<CodeBlock>,
|
||||
lines: &Vec<RLine<'static>>| {
|
||||
if let Some(start) = start {
|
||||
blocks.push(CodeBlock {
|
||||
start_line: start,
|
||||
end_line: lines.len(),
|
||||
source: std::mem::take(source).join("\n"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for line in text.lines {
|
||||
let is_code = matches!(&line, Line::Normal(c) if c.style == CompositeStyle::Code);
|
||||
if !is_code {
|
||||
close_block(
|
||||
current_block_start.take(),
|
||||
&mut current_block_source,
|
||||
&mut code_blocks,
|
||||
&lines,
|
||||
);
|
||||
}
|
||||
|
||||
match line {
|
||||
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
code_line_nr += 1;
|
||||
if current_block_start.is_none() {
|
||||
current_block_start = Some(lines.len());
|
||||
}
|
||||
let raw: String = composite
|
||||
.compounds
|
||||
.iter()
|
||||
.map(|c: &minimad::Compound| c.src)
|
||||
.collect();
|
||||
current_block_source.push(raw.clone());
|
||||
let mut spans = vec![
|
||||
Span::styled(format!(" {code_line_nr:>2} "), theme.code_border()),
|
||||
Span::styled("│ ", theme.code_border()),
|
||||
@@ -66,9 +107,15 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
}
|
||||
}
|
||||
}
|
||||
close_block(
|
||||
current_block_start.take(),
|
||||
&mut current_block_source,
|
||||
&mut code_blocks,
|
||||
&lines,
|
||||
);
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
|
||||
lines
|
||||
ParsedMarkdown { lines, code_blocks }
|
||||
}
|
||||
|
||||
pub fn preprocess_markdown(md: &str) -> String {
|
||||
@@ -300,28 +347,39 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_headings() {
|
||||
let md = "# H1\n## H2\n### H3";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(lines.len(), 3);
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_code_block() {
|
||||
let md = "```\ncode line\n```";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert!(!lines.is_empty());
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert!(!parsed.lines.is_empty());
|
||||
assert_eq!(parsed.code_blocks.len(), 1);
|
||||
assert_eq!(parsed.code_blocks[0].source, "code line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_table() {
|
||||
let md = "| A | B |\n|---|---|\n| 1 | 2 |";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(lines.len(), 2);
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_theme_works() {
|
||||
let md = "Hello **world**";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(lines.len(), 1);
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_code_blocks() {
|
||||
let md = "text\n```\nfirst\n```\nmore text\n```\nsecond line 1\nsecond line 2\n```";
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.code_blocks.len(), 2);
|
||||
assert_eq!(parsed.code_blocks[0].source, "first");
|
||||
assert_eq!(parsed.code_blocks[1].source, "second line 1\nsecond line 2");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user