Add section headings 👨‍🦲

Co-authored-by: Laurenz Mädje <laurmaedje@gmail.com>
This commit is contained in:
Martin Haug 2020-09-01 13:10:48 +02:00
parent 798c8a10c8
commit d986bc4b0a
5 changed files with 234 additions and 109 deletions

View File

@ -6,7 +6,7 @@ use super::*;
use crate::style::LayoutStyle; use crate::style::LayoutStyle;
use crate::syntax::decoration::Decoration; use crate::syntax::decoration::Decoration;
use crate::syntax::span::{Span, Spanned}; use crate::syntax::span::{Span, Spanned};
use crate::syntax::tree::{CallExpr, Code, SyntaxNode, SyntaxTree}; use crate::syntax::tree::{CallExpr, Code, Heading, SyntaxNode, SyntaxTree};
use crate::{DynFuture, Feedback, Pass}; use crate::{DynFuture, Feedback, Pass};
/// Layout a syntax tree into a collection of boxes. /// Layout a syntax tree into a collection of boxes.
@ -81,6 +81,8 @@ impl<'a> TreeLayouter<'a> {
self.layout_text(text).await; self.layout_text(text).await;
} }
SyntaxNode::Heading(heading) => self.layout_heading(heading).await,
SyntaxNode::Raw(lines) => self.layout_raw(lines).await, SyntaxNode::Raw(lines) => self.layout_raw(lines).await,
SyntaxNode::Code(block) => self.layout_code(block).await, SyntaxNode::Code(block) => self.layout_code(block).await,
@ -114,6 +116,18 @@ impl<'a> TreeLayouter<'a> {
); );
} }
async fn layout_heading(&mut self, heading: &Heading) {
let style = self.style.text.clone();
self.style.text.font_scale *= 1.5 - 0.1 * heading.level.v.min(5) as f64;
self.style.text.bolder = true;
self.layout_parbreak();
self.layout_tree(&heading.tree).await;
self.layout_parbreak();
self.style.text = style;
}
async fn layout_raw(&mut self, lines: &[String]) { async fn layout_raw(&mut self, lines: &[String]) {
// TODO: Make this more efficient. // TODO: Make this more efficient.
let fallback = self.style.text.fallback.clone(); let fallback = self.style.text.fallback.clone();

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
use super::decoration::Decoration; use super::decoration::Decoration;
use super::span::{Pos, Span, Spanned}; use super::span::{Pos, Span, Spanned};
use super::tokens::{is_newline_char, Token, TokenMode, Tokens}; use super::tokens::{is_newline_char, Token, TokenMode, Tokens};
use super::tree::{CallExpr, Code, Expr, SyntaxNode, SyntaxTree, TableExpr}; use super::tree::{CallExpr, Code, Expr, Heading, SyntaxNode, SyntaxTree, TableExpr};
use super::Ident; use super::Ident;
use crate::color::RgbaColor; use crate::color::RgbaColor;
use crate::compute::table::SpannedEntry; use crate::compute::table::SpannedEntry;
@ -20,6 +20,7 @@ struct Parser<'s> {
tokens: Tokens<'s>, tokens: Tokens<'s>,
peeked: Option<Option<Spanned<Token<'s>>>>, peeked: Option<Option<Spanned<Token<'s>>>>,
delimiters: Vec<(Pos, Token<'static>)>, delimiters: Vec<(Pos, Token<'static>)>,
at_block_or_line_start: bool,
feedback: Feedback, feedback: Feedback,
} }
@ -29,6 +30,7 @@ impl<'s> Parser<'s> {
tokens: Tokens::new(src, TokenMode::Body), tokens: Tokens::new(src, TokenMode::Body),
peeked: None, peeked: None,
delimiters: vec![], delimiters: vec![],
at_block_or_line_start: true,
feedback: Feedback::new(), feedback: Feedback::new(),
} }
} }
@ -44,101 +46,153 @@ impl Parser<'_> {
fn parse_body_contents(&mut self) -> SyntaxTree { fn parse_body_contents(&mut self) -> SyntaxTree {
let mut tree = SyntaxTree::new(); let mut tree = SyntaxTree::new();
while let Some(token) = self.peek() { self.at_block_or_line_start = true;
tree.push(match token.v { while !self.eof() {
// Starting from two newlines counts as a paragraph break, a single if let Some(node) = self.parse_node() {
// newline does not. tree.push(node);
Token::Space(newlines) => self.with_span(if newlines < 2 { }
SyntaxNode::Spacing
} else {
SyntaxNode::Parbreak
}),
Token::LineComment(_) | Token::BlockComment(_) => {
self.eat();
continue;
}
Token::LeftBracket => {
self.parse_bracket_call(false).map(SyntaxNode::Call)
}
Token::Star => self.with_span(SyntaxNode::ToggleBolder),
Token::Underscore => self.with_span(SyntaxNode::ToggleItalic),
Token::Backslash => self.with_span(SyntaxNode::Linebreak),
Token::Raw { raw, terminated } => {
if !terminated {
error!(
@self.feedback, Span::at(token.span.end),
"expected backtick",
);
}
self.with_span(SyntaxNode::Raw(unescape_raw(raw)))
}
Token::Code { lang, raw, terminated } => {
if !terminated {
error!(
@self.feedback, Span::at(token.span.end),
"expected backticks",
);
}
let lang = lang.and_then(|lang| {
if let Some(ident) = Ident::new(lang.v) {
Some(Spanned::new(ident, lang.span))
} else {
error!(@self.feedback, lang.span, "invalid identifier");
None
}
});
let mut lines = unescape_code(raw);
let block = lines.len() > 1;
if lines.last().map(|s| s.is_empty()).unwrap_or(false) {
lines.pop();
}
self.with_span(SyntaxNode::Code(Code { lang, lines, block }))
}
Token::Text(text) => self.with_span(SyntaxNode::Text(text.to_string())),
Token::UnicodeEscape { sequence, terminated } => {
if !terminated {
error!(
@self.feedback, Span::at(token.span.end),
"expected closing brace",
);
}
if let Some(c) = unescape_char(sequence) {
self.with_span(SyntaxNode::Text(c.to_string()))
} else {
self.eat();
error!(
@self.feedback, token.span,
"invalid unicode escape sequence",
);
continue;
}
}
unexpected => {
self.eat();
error!(
@self.feedback, token.span,
"unexpected {}", unexpected.name(),
);
continue;
}
});
} }
tree tree
} }
fn parse_node(&mut self) -> Option<Spanned<SyntaxNode>> {
let token = self.peek()?;
let end = Span::at(token.span.end);
// Set block or line start to false because most nodes have that effect, but
// remember the old value to actually check it for hashtags and because comments
// and spaces want to retain it.
let was_at_block_or_line_start = self.at_block_or_line_start;
self.at_block_or_line_start = false;
Some(match token.v {
// Starting from two newlines counts as a paragraph break, a single
// newline does not.
Token::Space(n) => {
if n == 0 {
self.at_block_or_line_start = was_at_block_or_line_start;
} else if n >= 1 {
self.at_block_or_line_start = true;
}
self.with_span(if n >= 2 {
SyntaxNode::Parbreak
} else {
SyntaxNode::Spacing
})
},
Token::LineComment(_) | Token::BlockComment(_) => {
self.at_block_or_line_start = was_at_block_or_line_start;
self.eat();
return None;
}
Token::LeftBracket => {
let call = self.parse_bracket_call(false);
self.at_block_or_line_start = false;
call.map(SyntaxNode::Call)
}
Token::Star => self.with_span(SyntaxNode::ToggleBolder),
Token::Underscore => self.with_span(SyntaxNode::ToggleItalic),
Token::Backslash => self.with_span(SyntaxNode::Linebreak),
Token::Hashtag if was_at_block_or_line_start => {
self.parse_heading().map(SyntaxNode::Heading)
}
Token::Raw { raw, terminated } => {
if !terminated {
error!(@self.feedback, end, "expected backtick");
}
self.with_span(SyntaxNode::Raw(unescape_raw(raw)))
}
Token::Code { lang, raw, terminated } => {
if !terminated {
error!(@self.feedback, end, "expected backticks");
}
let lang = lang.and_then(|lang| {
if let Some(ident) = Ident::new(lang.v) {
Some(Spanned::new(ident, lang.span))
} else {
error!(@self.feedback, lang.span, "invalid identifier");
None
}
});
let mut lines = unescape_code(raw);
let block = lines.len() > 1;
if lines.last().map(|s| s.is_empty()).unwrap_or(false) {
lines.pop();
}
self.with_span(SyntaxNode::Code(Code { lang, lines, block }))
}
Token::Text(text) => self.with_span(SyntaxNode::Text(text.to_string())),
Token::Hashtag => self.with_span(SyntaxNode::Text("#".to_string())),
Token::UnicodeEscape { sequence, terminated } => {
if !terminated {
error!(@self.feedback, end, "expected closing brace");
}
if let Some(c) = unescape_char(sequence) {
self.with_span(SyntaxNode::Text(c.to_string()))
} else {
error!(@self.feedback, token.span, "invalid unicode escape sequence");
self.eat();
return None;
}
}
unexpected => {
error!(@self.feedback, token.span, "unexpected {}", unexpected.name());
self.eat();
return None;
}
})
}
fn parse_heading(&mut self) -> Spanned<Heading> {
let start = self.pos();
self.assert(Token::Hashtag);
let mut level = 0;
while self.peekv() == Some(Token::Hashtag) {
level += 1;
self.eat();
}
let span = Span::new(start, self.pos());
let level = Spanned::new(level, span);
if level.v > 5 {
warning!(
@self.feedback, level.span,
"section depth larger than 6 has no effect",
);
}
self.skip_white();
let mut tree = SyntaxTree::new();
while !self.eof()
&& !matches!(self.peekv(), Some(Token::Space(n)) if n >= 1)
{
if let Some(node) = self.parse_node() {
tree.push(node);
}
}
let span = Span::new(start, self.pos());
Spanned::new(Heading { level, tree }, span)
}
} }
// Function calls. // Function calls.
@ -798,6 +852,15 @@ mod tests {
SyntaxNode::Text(text.to_string()) SyntaxNode::Text(text.to_string())
} }
macro_rules! H {
($level:expr, $($tts:tt)*) => {
SyntaxNode::Heading(Heading {
level: Spanned::zero($level),
tree: Tree![@$($tts)*],
})
};
}
macro_rules! R { macro_rules! R {
($($line:expr),* $(,)?) => { ($($line:expr),* $(,)?) => {
SyntaxNode::Raw(vec![$($line.to_string()),*]) SyntaxNode::Raw(vec![$($line.to_string()),*])
@ -999,6 +1062,15 @@ mod tests {
test("code\\", vec!["code\\"]); test("code\\", vec!["code\\"]);
} }
#[test]
fn test_parse_groups() {
e!("[)" => s(0,1, 0,2, "expected function name, found closing paren"),
s(0,2, 0,2, "expected closing bracket"));
e!("[v:{]}" => s(0,4, 0,4, "expected closing brace"),
s(0,5, 0,6, "unexpected closing brace"));
}
#[test] #[test]
fn test_parse_simple_nodes() { fn test_parse_simple_nodes() {
t!("" => ); t!("" => );
@ -1050,12 +1122,32 @@ mod tests {
} }
#[test] #[test]
fn test_parse_groups() { fn test_parse_headings() {
e!("[)" => s(0,1, 0,2, "expected function name, found closing paren"), t!("## Hello world!" => H![1, T("Hello"), S, T("world!")]);
s(0,2, 0,2, "expected closing bracket"));
e!("[v:{]}" => s(0,4, 0,4, "expected closing brace"), // Handle various whitespace usages.
s(0,5, 0,6, "unexpected closing brace")); t!("####Simple" => H![3, T("Simple")]);
t!(" # Whitespace!" => S, H![0, T("Whitespace!")]);
t!(" /* TODO: Improve */ ## Analysis" => S, S, H!(1, T("Analysis")));
// Complex heading contents.
t!("Some text [box][### Valuable facts]" => T("Some"), S, T("text"), S,
F!("box"; Tree![H!(2, T("Valuable"), S, T("facts"))])
);
t!("### Grandiose stuff [box][Get it \n\n straight]" => H![2,
T("Grandiose"), S, T("stuff"), S,
F!("box"; Tree![T("Get"), S, T("it"), P, T("straight")])
]);
t!("###### Multiline \\ headings" => H![5, T("Multiline"), S, L, S, T("headings")]);
// Things that should not become headings.
t!("\\## Text" => T("#"), T("#"), S, T("Text"));
t!(" ###### # Text" => S, H!(5, T("#"), S, T("Text")));
t!("I am #1" => T("I"), S, T("am"), S, T("#"), T("1"));
t!("[box][\n] # hi" => F!("box"; Tree![S]), S, T("#"), S, T("hi"));
// Depth warnings.
e!("########" => s(0,0, 0,8, "section depth larger than 6 has no effect"));
} }
#[test] #[test]

View File

@ -78,10 +78,12 @@ pub enum Token<'s> {
Star, Star,
/// An underscore in body-text. /// An underscore in body-text.
Underscore, Underscore,
/// A backslash followed by whitespace in text. /// A backslash followed by whitespace in text.
Backslash, Backslash,
/// A hashtag token in the body can indicate compute mode or headings.
Hashtag,
/// A unicode escape sequence. /// A unicode escape sequence.
UnicodeEscape { UnicodeEscape {
/// The escape sequence between two braces. /// The escape sequence between two braces.
@ -144,6 +146,7 @@ impl<'s> Token<'s> {
Star => "star", Star => "star",
Underscore => "underscore", Underscore => "underscore",
Backslash => "backslash", Backslash => "backslash",
Hashtag => "hashtag",
UnicodeEscape { .. } => "unicode escape sequence", UnicodeEscape { .. } => "unicode escape sequence",
Raw { .. } => "raw text", Raw { .. } => "raw text",
Code { .. } => "code block", Code { .. } => "code block",
@ -265,6 +268,9 @@ impl<'s> Iterator for Tokens<'s> {
'_' if self.mode == Body => Underscore, '_' if self.mode == Body => Underscore,
'`' if self.mode == Body => self.read_raw_or_code(), '`' if self.mode == Body => self.read_raw_or_code(),
// Sections.
'#' if self.mode == Body => Hashtag,
// Non-breaking spaces. // Non-breaking spaces.
'~' if self.mode == Body => Text("\u{00A0}"), '~' if self.mode == Body => Text("\u{00A0}"),
@ -282,7 +288,7 @@ impl<'s> Iterator for Tokens<'s> {
let val = match n { let val = match n {
c if c.is_whitespace() => true, c if c.is_whitespace() => true,
'[' | ']' | '{' | '}' | '/' | '*' => true, '[' | ']' | '{' | '}' | '/' | '*' => true,
'\\' | '_' | '`' | '~' if body => true, '\\' | '_' | '`' | '#' | '~' if body => true,
':' | '=' | ',' | '"' | '(' | ')' if !body => true, ':' | '=' | ',' | '"' | '(' | ')' if !body => true,
'+' | '-' if !body && !last_was_e => true, '+' | '-' if !body && !last_was_e => true,
_ => false, _ => false,
@ -442,7 +448,7 @@ impl<'s> Tokens<'s> {
fn read_escaped(&mut self) -> Token<'s> { fn read_escaped(&mut self) -> Token<'s> {
fn is_escapable(c: char) -> bool { fn is_escapable(c: char) -> bool {
match c { match c {
'[' | ']' | '\\' | '/' | '*' | '_' | '`' | '"' | '~' => true, '[' | ']' | '\\' | '/' | '*' | '_' | '`' | '"' | '#' | '~' => true,
_ => false, _ => false,
} }
} }
@ -674,6 +680,8 @@ mod tests {
t!(Body, "[func]*bold*" => L, T("func"), R, Star, T("bold"), Star); t!(Body, "[func]*bold*" => L, T("func"), R, Star, T("bold"), Star);
t!(Body, "hi_you_ there" => T("hi"), Underscore, T("you"), Underscore, S(0), T("there")); t!(Body, "hi_you_ there" => T("hi"), Underscore, T("you"), Underscore, S(0), T("there"));
t!(Body, "`raw`" => Raw("raw", true)); t!(Body, "`raw`" => Raw("raw", true));
t!(Body, "# hi" => Hashtag, S(0), T("hi"));
t!(Body, "#()" => Hashtag, T("()"));
t!(Body, "`[func]`" => Raw("[func]", true)); t!(Body, "`[func]`" => Raw("[func]", true));
t!(Body, "`]" => Raw("]", false)); t!(Body, "`]" => Raw("]", false));
t!(Body, "`\\``" => Raw("\\`", true)); t!(Body, "`\\``" => Raw("\\`", true));

View File

@ -31,6 +31,8 @@ pub enum SyntaxNode {
ToggleBolder, ToggleBolder,
/// Plain text. /// Plain text.
Text(String), Text(String),
/// Section headings.
Heading(Heading),
/// Lines of raw text. /// Lines of raw text.
Raw(Vec<String>), Raw(Vec<String>),
/// An optionally highlighted (multi-line) code block. /// An optionally highlighted (multi-line) code block.
@ -39,6 +41,22 @@ pub enum SyntaxNode {
Call(CallExpr), Call(CallExpr),
} }
/// A section heading.
#[derive(Debug, Clone, PartialEq)]
pub struct Heading {
/// The section depth (how many hashtags minus 1).
pub level: Spanned<u8>,
pub tree: SyntaxTree,
}
/// A code block.
#[derive(Debug, Clone, PartialEq)]
pub struct Code {
pub lang: Option<Spanned<Ident>>,
pub lines: Vec<String>,
pub block: bool,
}
/// An expression. /// An expression.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum Expr { pub enum Expr {
@ -197,10 +215,3 @@ impl CallExpr {
} }
} }
} }
/// A code block.
#[derive(Debug, Clone, PartialEq)]
pub struct Code {
pub lang: Option<Spanned<Ident>>,
pub lines: Vec<String>,
pub block: bool,
}

View File

@ -15,7 +15,7 @@
[v: 6mm] [v: 6mm]
[align: center][ [align: center][
*3. Übungsblatt Computerorientierte Mathematik II* [v: 2mm] #### 3. Übungsblatt Computerorientierte Mathematik II* [v: 2mm]
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) [v: 2mm] *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) [v: 2mm]
*Alle Antworten sind zu beweisen.* *Alle Antworten sind zu beweisen.*
] ]