diff --git a/src/layout/tree.rs b/src/layout/tree.rs index aa0e0df1b..faecc42b7 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -6,7 +6,7 @@ use super::*; use crate::style::LayoutStyle; use crate::syntax::decoration::Decoration; 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}; /// Layout a syntax tree into a collection of boxes. @@ -81,6 +81,8 @@ impl<'a> TreeLayouter<'a> { self.layout_text(text).await; } + SyntaxNode::Heading(heading) => self.layout_heading(heading).await, + SyntaxNode::Raw(lines) => self.layout_raw(lines).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]) { // TODO: Make this more efficient. let fallback = self.style.text.fallback.clone(); diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 6a8b8103a..19434bab0 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use super::decoration::Decoration; use super::span::{Pos, Span, Spanned}; 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 crate::color::RgbaColor; use crate::compute::table::SpannedEntry; @@ -20,6 +20,7 @@ struct Parser<'s> { tokens: Tokens<'s>, peeked: Option>>>, delimiters: Vec<(Pos, Token<'static>)>, + at_block_or_line_start: bool, feedback: Feedback, } @@ -29,6 +30,7 @@ impl<'s> Parser<'s> { tokens: Tokens::new(src, TokenMode::Body), peeked: None, delimiters: vec![], + at_block_or_line_start: true, feedback: Feedback::new(), } } @@ -44,101 +46,153 @@ impl Parser<'_> { fn parse_body_contents(&mut self) -> SyntaxTree { let mut tree = SyntaxTree::new(); - while let Some(token) = self.peek() { - tree.push(match token.v { - // Starting from two newlines counts as a paragraph break, a single - // newline does not. - 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; - } - }); + self.at_block_or_line_start = true; + while !self.eof() { + if let Some(node) = self.parse_node() { + tree.push(node); + } } tree } + + fn parse_node(&mut self) -> Option> { + 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 { + 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. @@ -798,6 +852,15 @@ mod tests { 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 { ($($line:expr),* $(,)?) => { SyntaxNode::Raw(vec![$($line.to_string()),*]) @@ -999,6 +1062,15 @@ mod tests { 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] fn test_parse_simple_nodes() { t!("" => ); @@ -1050,12 +1122,32 @@ mod tests { } #[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")); + fn test_parse_headings() { + t!("## Hello world!" => H![1, T("Hello"), S, T("world!")]); - e!("[v:{]}" => s(0,4, 0,4, "expected closing brace"), - s(0,5, 0,6, "unexpected closing brace")); + // Handle various whitespace usages. + 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] diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index e333968b4..f6ae834e3 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -78,10 +78,12 @@ pub enum Token<'s> { Star, /// An underscore in body-text. Underscore, - /// A backslash followed by whitespace in text. Backslash, + /// A hashtag token in the body can indicate compute mode or headings. + Hashtag, + /// A unicode escape sequence. UnicodeEscape { /// The escape sequence between two braces. @@ -144,6 +146,7 @@ impl<'s> Token<'s> { Star => "star", Underscore => "underscore", Backslash => "backslash", + Hashtag => "hashtag", UnicodeEscape { .. } => "unicode escape sequence", Raw { .. } => "raw text", Code { .. } => "code block", @@ -265,6 +268,9 @@ impl<'s> Iterator for Tokens<'s> { '_' if self.mode == Body => Underscore, '`' if self.mode == Body => self.read_raw_or_code(), + // Sections. + '#' if self.mode == Body => Hashtag, + // Non-breaking spaces. '~' if self.mode == Body => Text("\u{00A0}"), @@ -282,7 +288,7 @@ impl<'s> Iterator for Tokens<'s> { let val = match n { c if c.is_whitespace() => true, '[' | ']' | '{' | '}' | '/' | '*' => true, - '\\' | '_' | '`' | '~' if body => true, + '\\' | '_' | '`' | '#' | '~' if body => true, ':' | '=' | ',' | '"' | '(' | ')' if !body => true, '+' | '-' if !body && !last_was_e => true, _ => false, @@ -442,7 +448,7 @@ impl<'s> Tokens<'s> { fn read_escaped(&mut self) -> Token<'s> { fn is_escapable(c: char) -> bool { match c { - '[' | ']' | '\\' | '/' | '*' | '_' | '`' | '"' | '~' => true, + '[' | ']' | '\\' | '/' | '*' | '_' | '`' | '"' | '#' | '~' => true, _ => false, } } @@ -674,6 +680,8 @@ mod tests { 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, "`raw`" => Raw("raw", true)); + t!(Body, "# hi" => Hashtag, S(0), T("hi")); + t!(Body, "#()" => Hashtag, T("()")); t!(Body, "`[func]`" => Raw("[func]", true)); t!(Body, "`]" => Raw("]", false)); t!(Body, "`\\``" => Raw("\\`", true)); diff --git a/src/syntax/tree.rs b/src/syntax/tree.rs index 7295ec04e..94dfc1243 100644 --- a/src/syntax/tree.rs +++ b/src/syntax/tree.rs @@ -31,6 +31,8 @@ pub enum SyntaxNode { ToggleBolder, /// Plain text. Text(String), + /// Section headings. + Heading(Heading), /// Lines of raw text. Raw(Vec), /// An optionally highlighted (multi-line) code block. @@ -39,6 +41,22 @@ pub enum SyntaxNode { Call(CallExpr), } +/// A section heading. +#[derive(Debug, Clone, PartialEq)] +pub struct Heading { + /// The section depth (how many hashtags minus 1). + pub level: Spanned, + pub tree: SyntaxTree, +} + +/// A code block. +#[derive(Debug, Clone, PartialEq)] +pub struct Code { + pub lang: Option>, + pub lines: Vec, + pub block: bool, +} + /// An expression. #[derive(Clone, PartialEq)] pub enum Expr { @@ -197,10 +215,3 @@ impl CallExpr { } } } -/// A code block. -#[derive(Debug, Clone, PartialEq)] -pub struct Code { - pub lang: Option>, - pub lines: Vec, - pub block: bool, -} diff --git a/tests/coma.typ b/tests/coma.typ index 3a886699c..1271cd861 100644 --- a/tests/coma.typ +++ b/tests/coma.typ @@ -15,7 +15,7 @@ [v: 6mm] [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] *Alle Antworten sind zu beweisen.* ]