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::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();
|
||||||
|
@ -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]
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
|
@ -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.*
|
||||||
]
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user