mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Add section headings 👨🦲
Co-authored-by: Laurenz Mädje <laurmaedje@gmail.com>
This commit is contained in:
parent
798c8a10c8
commit
d986bc4b0a
@ -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();
|
||||
|
@ -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<Option<Spanned<Token<'s>>>>,
|
||||
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,45 +46,73 @@ 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 {
|
||||
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<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(newlines) => self.with_span(if newlines < 2 {
|
||||
SyntaxNode::Spacing
|
||||
} else {
|
||||
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();
|
||||
continue;
|
||||
return None;
|
||||
}
|
||||
|
||||
Token::LeftBracket => {
|
||||
self.parse_bracket_call(false).map(SyntaxNode::Call)
|
||||
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, Span::at(token.span.end),
|
||||
"expected backtick",
|
||||
);
|
||||
error!(@self.feedback, 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",
|
||||
);
|
||||
error!(@self.feedback, end, "expected backticks");
|
||||
}
|
||||
|
||||
let lang = lang.and_then(|lang| {
|
||||
@ -105,39 +135,63 @@ impl Parser<'_> {
|
||||
}
|
||||
|
||||
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, Span::at(token.span.end),
|
||||
"expected closing brace",
|
||||
);
|
||||
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();
|
||||
error!(
|
||||
@self.feedback, token.span,
|
||||
"invalid unicode escape sequence",
|
||||
);
|
||||
continue;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
unexpected => {
|
||||
error!(@self.feedback, token.span, "unexpected {}", unexpected.name());
|
||||
self.eat();
|
||||
error!(
|
||||
@self.feedback, token.span,
|
||||
"unexpected {}", unexpected.name(),
|
||||
);
|
||||
continue;
|
||||
return None;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
tree
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
@ -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));
|
||||
|
@ -31,6 +31,8 @@ pub enum SyntaxNode {
|
||||
ToggleBolder,
|
||||
/// Plain text.
|
||||
Text(String),
|
||||
/// Section headings.
|
||||
Heading(Heading),
|
||||
/// Lines of raw text.
|
||||
Raw(Vec<String>),
|
||||
/// 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<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.
|
||||
#[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<Spanned<Ident>>,
|
||||
pub lines: Vec<String>,
|
||||
pub block: bool,
|
||||
}
|
||||
|
@ -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.*
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user