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:
parent
d7072f378f
commit
8d1ce390e2
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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");
|
||||
|
||||
|
BIN
tests/ref/markup/emph-strong.png
Normal file
BIN
tests/ref/markup/emph-strong.png
Normal file
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 |
@ -4,7 +4,7 @@
|
||||
---
|
||||
// Test template addition.
|
||||
// Ref: true
|
||||
{[*Hello ] + [world!]}
|
||||
{[*Hello* ] + [world!]}
|
||||
|
||||
---
|
||||
// Test math operators.
|
||||
|
33
tests/typ/markup/emph-strong.typ
Normal file
33
tests/typ/markup/emph-strong.typ
Normal 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*
|
@ -1,11 +0,0 @@
|
||||
// Test emphasis toggle.
|
||||
|
||||
---
|
||||
// Basic.
|
||||
_Emphasized!_
|
||||
|
||||
// Inside of words.
|
||||
Partly em_phas_ized.
|
||||
|
||||
// Scoped to body.
|
||||
#box[_Scoped] to body.
|
@ -32,4 +32,4 @@ let f() , ; : | + - /= == 12 "string"
|
||||
---
|
||||
// Unterminated.
|
||||
// Error: 6 expected closing brace
|
||||
\u{41*Bold*
|
||||
\u{41[*Bold*]
|
||||
|
@ -1,11 +0,0 @@
|
||||
// Test strong toggle.
|
||||
|
||||
---
|
||||
// Basic.
|
||||
*Strong!*
|
||||
|
||||
// Inside of words.
|
||||
Partly str*ength*ened.
|
||||
|
||||
// Scoped to body.
|
||||
#box[*Scoped] to body.
|
@ -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
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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) => {
|
||||
|
@ -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" }]
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user