diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 639c1a875..8b088f85a 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -57,10 +57,11 @@ impl ExecWithMap for syntax::Node { Self::Parbreak(_) => ctx.parbreak(), Self::Strong(_) => ctx.state.font_mut().strong ^= true, Self::Emph(_) => ctx.state.font_mut().emph ^= true, - Self::Raw(raw) => raw.exec(ctx), - Self::Heading(heading) => heading.exec_with_map(ctx, map), - Self::List(list) => list.exec_with_map(ctx, map), - Self::Expr(expr) => map[&(expr as *const _)].exec(ctx), + Self::Raw(n) => n.exec(ctx), + Self::Heading(n) => n.exec_with_map(ctx, map), + Self::List(n) => n.exec_with_map(ctx, map), + Self::Enum(n) => n.exec_with_map(ctx, map), + Self::Expr(n) => map[&(n as *const _)].exec(ctx), } } } @@ -98,33 +99,43 @@ impl ExecWithMap for syntax::HeadingNode { } } -impl ExecWithMap for syntax::ListNode { +impl ExecWithMap for syntax::ListItem { fn exec_with_map(&self, ctx: &mut ExecContext, map: &ExprMap) { - ctx.parbreak(); - - let bullet = ctx.exec_stack(|ctx| ctx.push_text("•")); - let body = ctx.exec_tree_stack(&self.body, map); - - let stack = StackNode { - dirs: Gen::new(Dir::TTB, ctx.state.lang.dir), - aspect: None, - children: vec![ - StackChild::Any(bullet.into(), Gen::default()), - StackChild::Spacing(ctx.state.font.size / 2.0), - StackChild::Any(body.into(), Gen::default()), - ], - }; - - ctx.push(FixedNode { - width: None, - height: None, - child: stack.into(), - }); - - ctx.parbreak(); + exec_item(ctx, "•".to_string(), &self.body, map); } } +impl ExecWithMap for syntax::EnumItem { + fn exec_with_map(&self, ctx: &mut ExecContext, map: &ExprMap) { + let label = self.number.unwrap_or(1).to_string() + "."; + exec_item(ctx, label, &self.body, map); + } +} + +fn exec_item(ctx: &mut ExecContext, label: String, body: &syntax::Tree, map: &ExprMap) { + ctx.parbreak(); + + let label = ctx.exec_stack(|ctx| ctx.push_text(label)); + let body = ctx.exec_tree_stack(body, map); + let stack = StackNode { + dirs: Gen::new(Dir::TTB, ctx.state.lang.dir), + aspect: None, + children: vec![ + StackChild::Any(label.into(), Gen::default()), + StackChild::Spacing(ctx.state.font.size / 2.0), + StackChild::Any(body.into(), Gen::default()), + ], + }; + + ctx.push(FixedNode { + width: None, + height: None, + child: stack.into(), + }); + + ctx.parbreak(); +} + impl Exec for Value { fn exec(&self, ctx: &mut ExecContext) { match self { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 5ab5b2d8a..412576686 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -25,25 +25,33 @@ pub fn parse(src: &str) -> Pass { /// Parse a syntax tree. fn tree(p: &mut Parser) -> Tree { - tree_while(p, |_| true) + tree_while(p, true, |_| true) } /// Parse a syntax tree that stays right of the column at the start of the next /// non-whitespace token. fn tree_indented(p: &mut Parser) -> Tree { - p.skip_white(); + p.eat_while(|t| match t { + Token::Space(n) => n == 0, + Token::LineComment(_) | Token::BlockComment(_) => true, + _ => false, + }); + let column = p.column(p.next_start()); - tree_while(p, |p| match p.peek() { + tree_while(p, false, |p| match p.peek() { Some(Token::Space(n)) if n >= 1 => p.column(p.next_end()) >= column, _ => true, }) } /// Parse a syntax tree. -fn tree_while(p: &mut Parser, mut f: impl FnMut(&mut Parser) -> bool) -> Tree { - // We keep track of whether we are at the start of a block or paragraph - // to know whether things like headings are allowed. - let mut at_start = true; +fn tree_while( + p: &mut Parser, + mut at_start: bool, + mut f: impl FnMut(&mut Parser) -> bool, +) -> Tree { + // We use `at_start` to keep track of whether we are at the start of a line + // or template to know whether things like headings are allowed. let mut tree = vec![]; while !p.eof() && f(p) { if let Some(node) = node(p, &mut at_start) { @@ -85,19 +93,13 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { Token::Star => Node::Strong(span), Token::Underscore => Node::Emph(span), Token::Raw(t) => raw(p, t), - Token::Hashtag => { - if *at_start { - return Some(heading(p)); - } else { - Node::Text(p.peek_src().into()) - } - } - Token::Hyph => { - if *at_start { - return Some(list(p)); - } else { - Node::Text(p.peek_src().into()) - } + Token::Hashtag if *at_start => return Some(heading(p)), + Token::Hyph if *at_start => return Some(list_item(p)), + Token::Numbering(number) if *at_start => return Some(enum_item(p, number)), + + // Line-based markup that is not currently at the start of the line. + Token::Hashtag | Token::Hyph | Token::Numbering(_) => { + Node::Text(p.peek_src().into()) } // Hashtag + keyword / identifier. @@ -118,19 +120,12 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { } p.end_group(); - // Uneat spaces we might have eaten eagerly. return expr.map(Node::Expr); } - // Block. - Token::LeftBrace => { - return Some(Node::Expr(block(p, false))); - } - - // Template. - Token::LeftBracket => { - return Some(Node::Expr(template(p))); - } + // Block and template. + Token::LeftBrace => return Some(Node::Expr(block(p, false))), + Token::LeftBracket => return Some(Node::Expr(template(p))), // Comments. Token::LineComment(_) | Token::BlockComment(_) => { @@ -202,11 +197,19 @@ fn heading(p: &mut Parser) -> Node { } /// Parse a single list item. -fn list(p: &mut Parser) -> Node { +fn list_item(p: &mut Parser) -> Node { let start = p.next_start(); p.assert(Token::Hyph); let body = tree_indented(p); - Node::List(ListNode { span: p.span(start), body }) + Node::List(ListItem { span: p.span(start), body }) +} + +/// Parse a single enum item. +fn enum_item(p: &mut Parser, number: Option) -> Node { + let start = p.next_start(); + p.assert(Token::Numbering(number)); + let body = tree_indented(p); + Node::Enum(EnumItem { span: p.span(start), number, body }) } /// Parse an expression. @@ -500,7 +503,9 @@ fn block(p: &mut Parser, scoping: bool) -> Expr { } } p.end_group(); - p.skip_white(); + + // Forcefully skip over newlines since the group's contents can't. + p.eat_while(|t| matches!(t, Token::Space(_))); } let span = p.end_group(); Expr::Block(BlockExpr { span, exprs, scoping }) diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 273465872..8ea80d687 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -242,6 +242,16 @@ impl<'s> Parser<'s> { } } + /// Consume tokens while the condition is true. + pub fn eat_while(&mut self, mut f: F) + where + F: FnMut(Token<'s>) -> bool, + { + while self.peek().map_or(false, |t| f(t)) { + self.eat(); + } + } + /// Consume the next token if the closure maps it a to `Some`-variant. pub fn eat_map(&mut self, f: F) -> Option where @@ -278,18 +288,6 @@ impl<'s> Parser<'s> { debug_assert_eq!(next, Some(t)); } - /// Skip whitespace and comment tokens. - pub fn skip_white(&mut self) { - while matches!( - self.peek(), - Some(Token::Space(_)) | - Some(Token::LineComment(_)) | - Some(Token::BlockComment(_)) - ) { - self.eat(); - } - } - /// The index at which the last token ended. /// /// Refers to the end of the last _non-whitespace_ token in code mode. diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index f3ca25d95..a496010e3 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -102,6 +102,7 @@ impl<'s> Tokens<'s> { '`' => self.raw(), '$' => self.math(), '-' => self.hyph(start), + c if c == '.' || c.is_ascii_digit() => self.numbering(start, c), // Plain text. _ => self.text(start), @@ -185,11 +186,11 @@ impl<'s> Tokens<'s> { // Whitespace. c if c.is_whitespace() => true, // Comments. - '/' if self.s.check(|c| c == '/' || c == '*') => true, + '/' => true, // Parentheses. '[' | ']' | '{' | '}' => true, // Markup. - '#' | '~' | '*' | '_' | '-' | '`' | '$' => true, + '#' | '~' | '*' | '_' | '`' | '$' | '-' => true, // Escaping. '\\' => true, // Just text. @@ -274,6 +275,25 @@ impl<'s> Tokens<'s> { } } + fn numbering(&mut self, start: usize, c: char) -> Token<'s> { + let number = if c != '.' { + self.s.eat_while(|c| c.is_ascii_digit()); + let read = self.s.eaten_from(start); + if !self.s.eat_if('.') { + return Token::Text(read); + } + read.parse().ok() + } else { + None + }; + + if self.s.check(|c| !c.is_whitespace()) { + return Token::Text(self.s.eaten_from(start)); + } + + Token::Numbering(number) + } + fn raw(&mut self) -> Token<'s> { let mut backticks = 1; while self.s.eat_if('`') { @@ -357,12 +377,12 @@ impl<'s> Tokens<'s> { } } - fn number(&mut self, start: usize, first: char) -> Token<'s> { + fn number(&mut self, start: usize, c: char) -> Token<'s> { // Read the first part (integer or fractional depending on `first`). self.s.eat_while(|c| c.is_ascii_digit()); - // Read the fractional part if not already done and present. - if first != '.' && self.s.eat_if('.') { + // Read the fractional part if not already done. + if c != '.' && self.s.eat_if('.') { self.s.eat_while(|c| c.is_ascii_digit()); } @@ -654,7 +674,7 @@ mod tests { // Test code symbols in text. t!(Markup[" /"]: "a():\"b" => Text("a():\"b")); - t!(Markup[" /"]: ";:,|/+" => Text(";:,|/+")); + t!(Markup[" /"]: ";:,|/+" => Text(";:,|"), Text("/+")); t!(Markup[" /"]: "#-a" => Text("#"), Text("-"), Text("a")); t!(Markup[" "]: "#123" => Text("#"), Text("123")); @@ -707,10 +727,14 @@ mod tests { t!(Markup: "_" => Underscore); t!(Markup[""]: "###" => Hashtag, Hashtag, Hashtag); t!(Markup["a1/"]: "# " => Hashtag, Space(0)); - t!(Markup["a1/"]: "- " => Hyph, Space(0)); t!(Markup: "~" => Tilde); t!(Markup[" "]: r"\" => Backslash); t!(Markup["a "]: r"a--" => Text("a"), HyphHyph); + t!(Markup["a1/"]: "- " => Hyph, Space(0)); + t!(Markup[" "]: "." => Numbering(None)); + t!(Markup[" "]: "1." => Numbering(Some(1))); + t!(Markup[" "]: "1.a" => Text("1."), Text("a")); + t!(Markup[" /"]: "a1." => Text("a1.")); } #[test] diff --git a/src/pretty.rs b/src/pretty.rs index 1281e27b9..e2942f6fd 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -97,13 +97,14 @@ impl Pretty for Node { Self::Strong(_) => p.push('*'), Self::Emph(_) => p.push('_'), Self::Raw(raw) => raw.pretty(p), - Self::Heading(heading) => heading.pretty(p), - Self::List(list) => list.pretty(p), - Self::Expr(expr) => { - if expr.has_short_form() { + Self::Heading(n) => n.pretty(p), + Self::List(n) => n.pretty(p), + Self::Enum(n) => n.pretty(p), + Self::Expr(n) => { + if n.has_short_form() { p.push('#'); } - expr.pretty(p); + n.pretty(p); } } } @@ -175,13 +176,23 @@ impl Pretty for HeadingNode { } } -impl Pretty for ListNode { +impl Pretty for ListItem { fn pretty(&self, p: &mut Printer) { p.push_str("- "); self.body.pretty(p); } } +impl Pretty for EnumItem { + fn pretty(&self, p: &mut Printer) { + if let Some(number) = self.number { + write!(p, "{}", number).unwrap(); + } + p.push_str(". "); + self.body.pretty(p); + } +} + impl Pretty for Expr { fn pretty(&self, p: &mut Printer) { match self { diff --git a/src/syntax/node.rs b/src/syntax/node.rs index b4684d0b3..a97430b64 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -21,8 +21,10 @@ pub enum Node { Raw(RawNode), /// A section heading: `= Introduction`. Heading(HeadingNode), - /// A single list item: `- ...`. - List(ListNode), + /// An item in an unordered list: `- ...`. + List(ListItem), + /// An item in an enumeration (ordered list): `1. ...`. + Enum(EnumItem), /// An expression. Expr(Expr), } @@ -115,11 +117,22 @@ pub struct HeadingNode { pub body: Rc, } -/// A single list item: `- ...`. +/// An item in an unordered list: `- ...`. #[derive(Debug, Clone, PartialEq)] -pub struct ListNode { +pub struct ListItem { /// The source code location. pub span: Span, /// The contents of the list item. pub body: Tree, } + +/// An item in an enumeration (ordered list): `1. ...`. +#[derive(Debug, Clone, PartialEq)] +pub struct EnumItem { + /// The source code location. + pub span: Span, + /// The number, if any. + pub number: Option, + /// The contents of the list item. + pub body: Tree, +} diff --git a/src/syntax/token.rs b/src/syntax/token.rs index 2263f806f..254a56a24 100644 --- a/src/syntax/token.rs +++ b/src/syntax/token.rs @@ -118,6 +118,10 @@ pub enum Token<'s> { /// One or two dollar signs followed by inner contents, terminated with the /// same number of dollar signs. Math(MathToken<'s>), + /// A numbering: `23.`. + /// + /// Can also exist without the number: `.`. + Numbering(Option), /// An identifier: `center`. Ident(&'s str), /// A boolean: `true`, `false`. @@ -256,6 +260,7 @@ impl<'s> Token<'s> { Self::UnicodeEscape(_) => "unicode escape sequence", Self::Raw(_) => "raw block", Self::Math(_) => "math formula", + Self::Numbering(_) => "numbering", Self::Ident(_) => "identifier", Self::Bool(_) => "boolean", Self::Int(_) => "integer", diff --git a/src/syntax/visit.rs b/src/syntax/visit.rs index 97e8d4edf..a1a848ef9 100644 --- a/src/syntax/visit.rs +++ b/src/syntax/visit.rs @@ -59,6 +59,7 @@ visit! { Node::Raw(_) => {} Node::Heading(n) => v.visit_heading(n), Node::List(n) => v.visit_list(n), + Node::Enum(n) => v.visit_enum(n), Node::Expr(n) => v.visit_expr(n), } } @@ -67,7 +68,11 @@ visit! { v.visit_tree(&node.body); } - fn visit_list(v, node: &ListNode) { + fn visit_list(v, node: &ListItem) { + v.visit_tree(&node.body); + } + + fn visit_enum(v, node: &EnumItem) { v.visit_tree(&node.body); } diff --git a/tests/ref/markup/enums.png b/tests/ref/markup/enums.png new file mode 100644 index 000000000..f9bc552bd Binary files /dev/null and b/tests/ref/markup/enums.png differ diff --git a/tests/typ/markup/enums.typ b/tests/typ/markup/enums.typ new file mode 100644 index 000000000..516fd0c1c --- /dev/null +++ b/tests/typ/markup/enums.typ @@ -0,0 +1,11 @@ +// Test enums. + +--- +1. Embrace +2. Extend +3. Extinguish + +--- +1. First. + 2. Second. + 1. Back to first.