Rework strong and emph

- Star and underscore not parsed as strong/emph inside of words
- Stars/underscores must be balanced and they cannot go over paragraph break
- New `strong` and `emph` classes
This commit is contained in:
Laurenz 2022-01-30 12:50:58 +01:00
parent d7072f378f
commit 8d1ce390e2
27 changed files with 268 additions and 148 deletions

View File

@ -213,15 +213,9 @@ impl Eval for MarkupNode {
Self::Space => Node::Space,
Self::Linebreak => Node::Linebreak,
Self::Parbreak => Node::Parbreak,
Self::Strong => {
ctx.styles.toggle(TextNode::STRONG);
Node::new()
}
Self::Emph => {
ctx.styles.toggle(TextNode::EMPH);
Node::new()
}
Self::Text(text) => Node::Text(text.clone()),
Self::Strong(strong) => strong.eval(ctx)?,
Self::Emph(emph) => emph.eval(ctx)?,
Self::Raw(raw) => raw.eval(ctx)?,
Self::Math(math) => math.eval(ctx)?,
Self::Heading(heading) => heading.eval(ctx)?,
@ -232,6 +226,22 @@ impl Eval for MarkupNode {
}
}
impl Eval for StrongNode {
type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
Ok(self.body().eval(ctx)?.styled(TextNode::STRONG, true))
}
}
impl Eval for EmphNode {
type Output = Node;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
Ok(self.body().eval(ctx)?.styled(TextNode::EMPH, true))
}
}
impl Eval for RawNode {
type Output = Node;

View File

@ -87,19 +87,6 @@ impl StyleMap {
}
}
/// Toggle a boolean style property, removing it if it exists and inserting
/// it with `true` if it doesn't.
pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) {
for (i, entry) in self.0.iter_mut().enumerate() {
if entry.is::<P>() {
self.0.swap_remove(i);
return;
}
}
self.0.push(Entry::new(key, true));
}
/// Mark all contained properties as _scoped_. This means that they only
/// apply to the first descendant node (of their type) in the hierarchy and
/// not its children, too. This is used by class constructors.

View File

@ -93,6 +93,8 @@ pub fn new() -> Scope {
std.def_class::<ParbreakNode>("parbreak");
std.def_class::<LinebreakNode>("linebreak");
std.def_class::<TextNode>("text");
std.def_class::<StrongNode>("strong");
std.def_class::<EmphNode>("emph");
std.def_class::<DecoNode<Underline>>("underline");
std.def_class::<DecoNode<Strikethrough>>("strike");
std.def_class::<DecoNode<Overline>>("overline");

View File

@ -150,6 +150,26 @@ impl Debug for TextNode {
}
}
/// Strong text, rendered in boldface.
pub struct StrongNode;
#[class]
impl StrongNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(args.expect::<Node>("body")?.styled(TextNode::STRONG, true))
}
}
/// Emphasized text, rendered with an italic face.
pub struct EmphNode;
#[class]
impl EmphNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(args.expect::<Node>("body")?.styled(TextNode::EMPH, true))
}
}
/// A generic or named font family.
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum FontFamily {

View File

@ -435,10 +435,12 @@ impl NodeKind {
| Self::LeftParen
| Self::RightParen => SuccessionRule::Unsafe,
// These work similar to parentheses.
Self::Star | Self::Underscore => SuccessionRule::Unsafe,
// Replacing an operator can change whether the parent is an
// operation which makes it unsafe. The star can appear in markup.
Self::Star
| Self::Comma
// operation which makes it unsafe.
Self::Comma
| Self::Semicolon
| Self::Colon
| Self::Plus

View File

@ -21,7 +21,7 @@ use crate::util::EcoString;
/// Parse a source file.
pub fn parse(src: &str) -> Rc<GreenNode> {
let mut p = Parser::new(src, TokenMode::Markup);
markup(&mut p);
markup(&mut p, true);
match p.finish().into_iter().next() {
Some(Green::Node(node)) => node,
_ => unreachable!(),
@ -61,7 +61,7 @@ pub fn parse_markup(
) -> Option<(Vec<Green>, bool)> {
let mut p = Parser::with_prefix(prefix, src, TokenMode::Markup);
if min_column == 0 {
markup(&mut p);
markup(&mut p, true);
} else {
markup_indented(&mut p, min_column);
}
@ -128,8 +128,8 @@ pub fn parse_comment(
}
/// Parse markup.
fn markup(p: &mut Parser) {
markup_while(p, true, 0, &mut |_| true)
fn markup(p: &mut Parser, at_start: bool) {
markup_while(p, at_start, 0, &mut |_| true)
}
/// Parse markup that stays right of the given column.
@ -191,8 +191,6 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
| NodeKind::EnDash
| NodeKind::EmDash
| NodeKind::NonBreakingSpace
| NodeKind::Emph
| NodeKind::Strong
| NodeKind::Linebreak
| NodeKind::Raw(_)
| NodeKind::Math(_)
@ -200,6 +198,9 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
p.eat();
}
// Grouping markup.
NodeKind::Star => strong(p),
NodeKind::Underscore => emph(p),
NodeKind::Eq => heading(p, *at_start),
NodeKind::Minus => list_node(p, *at_start),
NodeKind::EnumNumbering(_) => enum_node(p, *at_start),
@ -227,6 +228,24 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
*at_start = false;
}
/// Parse strong content.
fn strong(p: &mut Parser) {
p.perform(NodeKind::Strong, |p| {
p.start_group(Group::Strong);
markup(p, false);
p.end_group();
})
}
/// Parse emphasized content.
fn emph(p: &mut Parser) {
p.perform(NodeKind::Emph, |p| {
p.start_group(Group::Emph);
markup(p, false);
p.end_group();
})
}
/// Parse a heading.
fn heading(p: &mut Parser, at_start: bool) {
let marker = p.marker();
@ -234,7 +253,7 @@ fn heading(p: &mut Parser, at_start: bool) {
p.eat_assert(&NodeKind::Eq);
while p.eat_if(&NodeKind::Eq) {}
if at_start && p.peek().map_or(true, |kind| kind.is_whitespace()) {
if at_start && p.peek().map_or(true, |kind| kind.is_space()) {
let column = p.column(p.prev_end());
markup_indented(p, column);
marker.end(p, NodeKind::Heading);
@ -250,7 +269,7 @@ fn list_node(p: &mut Parser, at_start: bool) {
let text: EcoString = p.peek_src().into();
p.eat_assert(&NodeKind::Minus);
if at_start && p.peek().map_or(true, |kind| kind.is_whitespace()) {
if at_start && p.peek().map_or(true, |kind| kind.is_space()) {
let column = p.column(p.prev_end());
markup_indented(p, column);
marker.end(p, NodeKind::List);
@ -265,7 +284,7 @@ fn enum_node(p: &mut Parser, at_start: bool) {
let text: EcoString = p.peek_src().into();
p.eat();
if at_start && p.peek().map_or(true, |kind| kind.is_whitespace()) {
if at_start && p.peek().map_or(true, |kind| kind.is_space()) {
let column = p.column(p.prev_end());
markup_indented(p, column);
marker.end(p, NodeKind::Enum);
@ -620,7 +639,7 @@ fn params(p: &mut Parser, marker: Marker) {
fn template(p: &mut Parser) {
p.perform(NodeKind::Template, |p| {
p.start_group(Group::Bracket);
markup(p);
markup(p, true);
p.end_group();
});
}

View File

@ -239,17 +239,18 @@ impl<'s> Parser<'s> {
pub fn start_group(&mut self, kind: Group) {
self.groups.push(GroupEntry { kind, prev_mode: self.tokens.mode() });
self.tokens.set_mode(match kind {
Group::Bracket => TokenMode::Markup,
_ => TokenMode::Code,
Group::Bracket | Group::Strong | Group::Emph => TokenMode::Markup,
Group::Paren | Group::Brace | Group::Expr | Group::Imports => TokenMode::Code,
});
self.repeek();
match kind {
Group::Paren => self.eat_assert(&NodeKind::LeftParen),
Group::Bracket => self.eat_assert(&NodeKind::LeftBracket),
Group::Brace => self.eat_assert(&NodeKind::LeftBrace),
Group::Expr => {}
Group::Imports => {}
Group::Strong => self.eat_assert(&NodeKind::Star),
Group::Emph => self.eat_assert(&NodeKind::Underscore),
Group::Expr => self.repeek(),
Group::Imports => self.repeek(),
}
}
@ -273,6 +274,8 @@ impl<'s> Parser<'s> {
Group::Paren => Some((NodeKind::RightParen, true)),
Group::Bracket => Some((NodeKind::RightBracket, true)),
Group::Brace => Some((NodeKind::RightBrace, true)),
Group::Strong => Some((NodeKind::Star, true)),
Group::Emph => Some((NodeKind::Underscore, true)),
Group::Expr => Some((NodeKind::Semicolon, false)),
Group::Imports => None,
} {
@ -322,9 +325,11 @@ impl<'s> Parser<'s> {
Some(NodeKind::RightParen) => self.inside(Group::Paren),
Some(NodeKind::RightBracket) => self.inside(Group::Bracket),
Some(NodeKind::RightBrace) => self.inside(Group::Brace),
Some(NodeKind::Star) => self.inside(Group::Strong),
Some(NodeKind::Underscore) => self.inside(Group::Emph),
Some(NodeKind::Semicolon) => self.inside(Group::Expr),
Some(NodeKind::From) => self.inside(Group::Imports),
Some(NodeKind::Space(n)) => *n >= 1 && self.stop_at_newline(),
Some(NodeKind::Space(n)) => self.space_ends_group(*n),
Some(_) => false,
None => true,
};
@ -332,31 +337,34 @@ impl<'s> Parser<'s> {
/// Returns whether the given type can be skipped over.
fn is_trivia(&self, token: &NodeKind) -> bool {
Self::is_trivia_ext(token, self.stop_at_newline())
}
/// Returns whether the given type can be skipped over given the current
/// newline mode.
fn is_trivia_ext(token: &NodeKind, stop_at_newline: bool) -> bool {
match token {
NodeKind::Space(n) => *n == 0 || !stop_at_newline,
NodeKind::Space(n) => !self.space_ends_group(*n),
NodeKind::LineComment => true,
NodeKind::BlockComment => true,
_ => false,
}
}
/// Whether the active group must end at a newline.
fn stop_at_newline(&self) -> bool {
matches!(
self.groups.last().map(|group| group.kind),
Some(Group::Expr | Group::Imports)
)
/// Whether a space with the given number of newlines ends the current group.
fn space_ends_group(&self, n: usize) -> bool {
if n == 0 {
return false;
}
match self.groups.last().map(|group| group.kind) {
Some(Group::Strong | Group::Emph) => n >= 2,
Some(Group::Expr | Group::Imports) => n >= 1,
_ => false,
}
}
/// Whether we are inside the given group.
/// Whether we are inside the given group (can be nested).
fn inside(&self, kind: Group) -> bool {
self.groups.iter().any(|g| g.kind == kind)
self.groups
.iter()
.rev()
.take_while(|g| !kind.is_weak() || g.kind.is_weak())
.any(|g| g.kind == kind)
}
}
@ -431,15 +439,20 @@ impl Marker {
F: Fn(&Green) -> Result<(), &'static str>,
{
for child in &mut p.children[self.0 ..] {
if (p.tokens.mode() == TokenMode::Markup
|| !Parser::is_trivia_ext(child.kind(), false))
&& !child.kind().is_error()
{
if let Err(msg) = f(child) {
let error = NodeKind::Error(ErrorPos::Full, msg.into());
let inner = mem::take(child);
*child = GreenNode::with_child(error, inner).into();
}
// Don't expose errors.
if child.kind().is_error() {
continue;
}
// Don't expose trivia in code.
if p.tokens.mode() == TokenMode::Code && child.kind().is_trivia() {
continue;
}
if let Err(msg) = f(child) {
let error = NodeKind::Error(ErrorPos::Full, msg.into());
let inner = mem::take(child);
*child = GreenNode::with_child(error, inner).into();
}
}
}
@ -485,12 +498,23 @@ pub enum Group {
Brace,
/// A parenthesized group: `(...)`.
Paren,
/// A group surrounded with stars: `*...*`.
Strong,
/// A group surrounded with underscore: `_..._`.
Emph,
/// A group ended by a semicolon or a line break: `;`, `\n`.
Expr,
/// A group for import items, ended by a semicolon, line break or `from`.
Imports,
}
impl Group {
/// Whether the group can only force other weak groups to end.
fn is_weak(self) -> bool {
matches!(self, Group::Strong | Group::Emph)
}
}
/// Allows parser methods to use the try operator. Never returned top-level
/// because the parser recovers from all errors.
pub type ParseResult<T = ()> = Result<T, ParseError>;

View File

@ -123,8 +123,8 @@ impl<'s> Tokens<'s> {
// Markup.
'~' => NodeKind::NonBreakingSpace,
'*' => NodeKind::Strong,
'_' => NodeKind::Emph,
'*' if !self.in_word() => NodeKind::Star,
'_' if !self.in_word() => NodeKind::Underscore,
'`' => self.raw(),
'$' => self.math(),
'-' => self.hyph(),
@ -527,6 +527,13 @@ impl<'s> Tokens<'s> {
NodeKind::BlockComment
}
fn in_word(&self) -> bool {
let alphanumeric = |c: Option<char>| c.map_or(false, |c| c.is_alphanumeric());
let prev = self.s.get(.. self.s.last_index()).chars().next_back();
let next = self.s.peek();
alphanumeric(prev) && alphanumeric(next)
}
fn maybe_in_url(&self) -> bool {
self.mode == TokenMode::Markup && self.s.eaten().ends_with(":/")
}
@ -651,7 +658,7 @@ mod tests {
('/', None, "[", LeftBracket),
('/', None, "//", LineComment),
('/', None, "/**/", BlockComment),
('/', Some(Markup), "*", Strong),
('/', Some(Markup), "*", Star),
('/', Some(Markup), "$ $", Math(" ", false)),
('/', Some(Markup), r"\\", Escape('\\')),
('/', Some(Markup), "#let", Let),
@ -790,8 +797,8 @@ mod tests {
#[test]
fn test_tokenize_markup_symbols() {
// Test markup tokens.
t!(Markup[" a1"]: "*" => Strong);
t!(Markup: "_" => Emph);
t!(Markup[" a1"]: "*" => Star);
t!(Markup: "_" => Underscore);
t!(Markup[""]: "===" => Eq, Eq, Eq);
t!(Markup["a1/"]: "= " => Eq, Space(0));
t!(Markup: "~" => NonBreakingSpace);

View File

@ -63,8 +63,6 @@ impl Markup {
NodeKind::Space(_) => Some(MarkupNode::Space),
NodeKind::Linebreak => Some(MarkupNode::Linebreak),
NodeKind::Parbreak => Some(MarkupNode::Parbreak),
NodeKind::Strong => Some(MarkupNode::Strong),
NodeKind::Emph => Some(MarkupNode::Emph),
NodeKind::Text(s) | NodeKind::TextInLine(s) => {
Some(MarkupNode::Text(s.clone()))
}
@ -72,8 +70,10 @@ impl Markup {
NodeKind::EnDash => Some(MarkupNode::Text('\u{2013}'.into())),
NodeKind::EmDash => Some(MarkupNode::Text('\u{2014}'.into())),
NodeKind::NonBreakingSpace => Some(MarkupNode::Text('\u{00A0}'.into())),
NodeKind::Math(math) => Some(MarkupNode::Math(math.as_ref().clone())),
NodeKind::Strong => node.cast().map(MarkupNode::Strong),
NodeKind::Emph => node.cast().map(MarkupNode::Emph),
NodeKind::Raw(raw) => Some(MarkupNode::Raw(raw.as_ref().clone())),
NodeKind::Math(math) => Some(MarkupNode::Math(math.as_ref().clone())),
NodeKind::Heading => node.cast().map(MarkupNode::Heading),
NodeKind::List => node.cast().map(MarkupNode::List),
NodeKind::Enum => node.cast().map(MarkupNode::Enum),
@ -91,12 +91,12 @@ pub enum MarkupNode {
Linebreak,
/// A paragraph break: Two or more newlines.
Parbreak,
/// Strong text was enabled / disabled: `*`.
Strong,
/// Emphasized text was enabled / disabled: `_`.
Emph,
/// Plain text.
Text(EcoString),
/// Strong content: `*Strong*`.
Strong(StrongNode),
/// Emphasized content: `_Emphasized_`.
Emph(EmphNode),
/// A raw block with optional syntax highlighting: `` `...` ``.
Raw(RawNode),
/// A math formula: `$a^2 = b^2 + c^2$`.
@ -111,6 +111,32 @@ pub enum MarkupNode {
Expr(Expr),
}
node! {
/// Strong content: `*Strong*`.
StrongNode: Strong
}
impl StrongNode {
/// The contents of the strong node.
pub fn body(&self) -> Markup {
self.0.cast_first_child().expect("strong node is missing markup body")
}
}
node! {
/// Emphasized content: `_Emphasized_`.
EmphNode: Emph
}
impl EmphNode {
/// The contents of the emphasis node.
pub fn body(&self) -> Markup {
self.0
.cast_first_child()
.expect("emphasis node is missing markup body")
}
}
/// A raw block with optional syntax highlighting: `` `...` ``.
#[derive(Debug, Clone, PartialEq)]
pub struct RawNode {

View File

@ -151,7 +151,10 @@ impl Category {
NodeKind::From => Some(Category::Keyword),
NodeKind::Include => Some(Category::Keyword),
NodeKind::Plus => Some(Category::Operator),
NodeKind::Star => Some(Category::Operator),
NodeKind::Star => match parent.kind() {
NodeKind::Strong => None,
_ => Some(Category::Operator),
},
NodeKind::Slash => Some(Category::Operator),
NodeKind::PlusEq => Some(Category::Operator),
NodeKind::HyphEq => Some(Category::Operator),
@ -191,6 +194,7 @@ impl Category {
NodeKind::Str(_) => Some(Category::String),
NodeKind::Error(_, _) => Some(Category::Invalid),
NodeKind::Unknown(_) => Some(Category::Invalid),
NodeKind::Underscore => None,
NodeKind::Markup(_) => None,
NodeKind::Space(_) => None,
NodeKind::Parbreak => None,
@ -276,11 +280,7 @@ mod tests {
assert_eq!(vec, goal);
}
test("= *AB*", &[
(0 .. 6, Heading),
(2 .. 3, Strong),
(5 .. 6, Strong),
]);
test("= *AB*", &[(0 .. 6, Heading), (2 .. 6, Strong)]);
test("#f(x + 1)", &[
(0 .. 2, Function),

View File

@ -496,6 +496,8 @@ pub enum NodeKind {
RightParen,
/// An asterisk: `*`.
Star,
/// An underscore: `_`.
Underscore,
/// A comma: `,`.
Comma,
/// A semicolon: `;`.
@ -599,25 +601,25 @@ pub enum NodeKind {
/// A slash and the letter "u" followed by a hexadecimal unicode entity
/// enclosed in curly braces: `\u{1F5FA}`.
Escape(char),
/// Strong text was enabled / disabled: `*`.
/// Strong content: `*Strong*`.
Strong,
/// Emphasized text was enabled / disabled: `_`.
/// Emphasized content: `_Emphasized_`.
Emph,
/// An arbitrary number of backticks followed by inner contents, terminated
/// with the same number of backticks: `` `...` ``.
Raw(Rc<RawNode>),
/// Dollar signs surrounding inner contents.
Math(Rc<MathNode>),
/// A section heading: `= Introduction`.
Heading,
/// An item in an unordered list: `- ...`.
List,
/// An item in an enumeration (ordered list): `1. ...`.
Enum,
/// A numbering: `23.`.
///
/// Can also exist without the number: `.`.
EnumNumbering(Option<usize>),
/// An item in an unordered list: `- ...`.
List,
/// An arbitrary number of backticks followed by inner contents, terminated
/// with the same number of backticks: `` `...` ``.
Raw(Rc<RawNode>),
/// Dollar signs surrounding inner contents.
Math(Rc<MathNode>),
/// An identifier: `center`.
Ident(EcoString),
/// A boolean: `true`, `false`.
@ -736,14 +738,14 @@ impl NodeKind {
matches!(self, Self::LeftParen | Self::RightParen)
}
/// Whether this is whitespace.
pub fn is_whitespace(&self) -> bool {
matches!(self, Self::Space(_) | Self::Parbreak)
/// Whether this is a space.
pub fn is_space(&self) -> bool {
matches!(self, Self::Space(_))
}
/// Whether this is trivia.
pub fn is_trivia(&self) -> bool {
self.is_whitespace() || matches!(self, Self::LineComment | Self::BlockComment)
self.is_space() || matches!(self, Self::LineComment | Self::BlockComment)
}
/// Whether this is some kind of error.
@ -761,7 +763,7 @@ impl NodeKind {
}
}
/// Which mode this token can appear in, in both if `None`.
/// Which mode this node can appear in, in both if `None`.
pub fn mode(&self) -> Option<TokenMode> {
match self {
Self::Markup(_)
@ -814,6 +816,7 @@ impl NodeKind {
Self::LeftParen => "opening paren",
Self::RightParen => "closing paren",
Self::Star => "star",
Self::Underscore => "underscore",
Self::Comma => "comma",
Self::Semicolon => "semicolon",
Self::Colon => "colon",
@ -864,14 +867,14 @@ impl NodeKind {
Self::EnDash => "en dash",
Self::EmDash => "em dash",
Self::Escape(_) => "escape sequence",
Self::Strong => "strong",
Self::Emph => "emphasis",
Self::Strong => "strong content",
Self::Emph => "emphasized content",
Self::Raw(_) => "raw block",
Self::Math(_) => "math formula",
Self::List => "list item",
Self::Heading => "heading",
Self::Enum => "enumeration item",
Self::EnumNumbering(_) => "enumeration item numbering",
Self::List => "list item",
Self::Raw(_) => "raw block",
Self::Math(_) => "math formula",
Self::Ident(_) => "identifier",
Self::Bool(_) => "boolean",
Self::Int(_) => "integer",

View File

@ -95,8 +95,8 @@ impl Pretty for MarkupNode {
Self::Space => p.push(' '),
Self::Linebreak => p.push_str(r"\"),
Self::Parbreak => p.push_str("\n\n"),
Self::Strong => p.push('*'),
Self::Emph => p.push('_'),
Self::Strong(strong) => strong.pretty(p),
Self::Emph(emph) => emph.pretty(p),
Self::Text(text) => p.push_str(text),
Self::Raw(raw) => raw.pretty(p),
Self::Math(math) => math.pretty(p),
@ -113,6 +113,22 @@ impl Pretty for MarkupNode {
}
}
impl Pretty for StrongNode {
fn pretty(&self, p: &mut Printer) {
p.push('*');
self.body().pretty(p);
p.push('*');
}
}
impl Pretty for EmphNode {
fn pretty(&self, p: &mut Printer) {
p.push('_');
self.body().pretty(p);
p.push('_');
}
}
impl Pretty for RawNode {
fn pretty(&self, p: &mut Printer) {
// Find out how many backticks we need.
@ -604,12 +620,12 @@ mod tests {
#[test]
fn test_pretty_print_markup() {
// Basic stuff.
roundtrip("*");
roundtrip("_");
roundtrip(" ");
roundtrip("*ab*");
roundtrip("\\ ");
roundtrip("\n\n");
roundtrip("hi");
roundtrip("_ab_");
roundtrip("= *Ok*");
roundtrip("- Ok");

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -4,7 +4,7 @@
---
// Test template addition.
// Ref: true
{[*Hello ] + [world!]}
{[*Hello* ] + [world!]}
---
// Test math operators.

View File

@ -0,0 +1,33 @@
// Test emph and strong.
---
// Basic.
_Emphasized and *strong* words!_
// Inside of a word it's a normal underscore or star.
hello_world Nutzer*innen
// Can contain paragraph in child template.
_Still [
] emphasized._
---
// Inside of words can still use the functions.
P#strong[art]ly em#emph[phas]ized.
---
// Error: 13 expected underscore
#box[_Scoped] to body.
---
// Ends at paragraph break.
// Error: 7 expected underscore
_Hello
World
---
// Error: 1:12 expected star
// Error: 2:1 expected star
_Cannot *be_ interleaved*

View File

@ -1,11 +0,0 @@
// Test emphasis toggle.
---
// Basic.
_Emphasized!_
// Inside of words.
Partly em_phas_ized.
// Scoped to body.
#box[_Scoped] to body.

View File

@ -32,4 +32,4 @@ let f() , ; : | + - /= == 12 "string"
---
// Unterminated.
// Error: 6 expected closing brace
\u{41*Bold*
\u{41[*Bold*]

View File

@ -1,11 +0,0 @@
// Test strong toggle.
---
// Basic.
*Strong!*
// Inside of words.
Partly str*ength*ened.
// Scoped to body.
#box[*Scoped] to body.

View File

@ -1,10 +0,0 @@
// Test set rules for toggleable booleans.
---
// Test toggling and untoggling.
*AB_C*DE
*_*
---
// Test toggling and nested templates.
*A[B*[_C]]D*E

View File

@ -16,7 +16,7 @@ in booklovers and the great fulfiller of human need.
---
// Test wrap in template.
A [_B #wrap c in [*#c*]; C] D
A [_B #wrap c in [*#c*]; C_] D
---
// Test wrap style precedence.

View File

@ -10,7 +10,7 @@
---
// Test that consecutive, embedded LTR runs stay LTR.
// Here, we have two runs: "A" and italic "B".
#let content = [أنت A_B_مطرC]
#let content = [أنت A#emph[B]مطرC]
#set text(serif, "Noto Sans Arabic")
#par(lang: "ar", content)
#par(lang: "de", content)
@ -18,7 +18,7 @@
---
// Test that consecutive, embedded RTL runs stay RTL.
// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
#let content = [Aגֶ*שֶׁ*םB]
#let content = [Aגֶ#strong[שֶׁ]םB]
#set text(serif, "Noto Serif Hebrew")
#par(lang: "he", content)
#par(lang: "de", content)

View File

@ -10,7 +10,7 @@ Supercalifragilisticexpialidocious Expialigoricmetrioxidation.
---
// Test that there are no unwanted line break opportunities on run change.
This is partly emp_has_ized.
This is partly emp#emph[has]ized.
---
Hard \ break.

View File

@ -259,6 +259,8 @@ fn test_part(
debug: bool,
rng: &mut LinearShift,
) -> (bool, bool, Vec<Rc<Frame>>) {
let mut ok = true;
let id = ctx.sources.provide(src_path, src);
let source = ctx.sources.get(id);
if debug {
@ -267,7 +269,8 @@ fn test_part(
let (local_compare_ref, mut ref_errors) = parse_metadata(&source);
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
let mut ok = test_reparse(ctx.sources.get(id).src(), i, rng);
ok &= test_reparse(ctx.sources.get(id).src(), i, rng);
let (frames, mut errors) = match ctx.evaluate(id) {
Ok(module) => {

View File

@ -44,15 +44,15 @@
},
{
"name": "markup.bold.typst",
"begin": "\\*",
"end": "\\*|(?=\\])",
"begin": "(^\\*|\\*$|((?<=\\W|_)\\*)|(\\*(?=\\W|_)))",
"end": "(^\\*|\\*$|((?<=\\W|_)\\*)|(\\*(?=\\W|_)))|\n|(?=\\])",
"captures": { "0": { "name": "punctuation.definition.bold.typst" } },
"patterns": [{ "include": "#markup" }]
},
{
"name": "markup.italic.typst",
"begin": "_",
"end": "_|(?=\\])",
"begin": "(^_|_$|((?<=\\W|_)_)|(_(?=\\W|_)))",
"end": "(^_|_$|((?<=\\W|_)_)|(_(?=\\W|_)))|\n|(?=\\])",
"captures": { "0": { "name": "punctuation.definition.italic.typst" } },
"patterns": [{ "include": "#markup" }]
},