Description lists, link syntax, and new enum syntax
This commit is contained in:
parent
2661f1a506
commit
704f2fbaf1
src
eval
library
model
parse
syntax
tests
ref
typ
code
math
structure
style
text
@ -195,16 +195,20 @@ impl Eval for MarkupNode {
|
||||
Ok(match self {
|
||||
Self::Space => Content::Space,
|
||||
Self::Parbreak => Content::Parbreak,
|
||||
&Self::Linebreak { justified } => Content::Linebreak { justified },
|
||||
&Self::Linebreak => Content::Linebreak { justify: false },
|
||||
Self::Text(text) => Content::Text(text.clone()),
|
||||
&Self::Quote { double } => Content::Quote { double },
|
||||
Self::Strong(strong) => strong.eval(vm)?,
|
||||
Self::Emph(emph) => emph.eval(vm)?,
|
||||
Self::Link(url) => {
|
||||
Content::show(library::text::LinkNode::from_url(url.clone()))
|
||||
}
|
||||
Self::Raw(raw) => raw.eval(vm)?,
|
||||
Self::Math(math) => math.eval(vm)?,
|
||||
Self::Heading(heading) => heading.eval(vm)?,
|
||||
Self::List(list) => list.eval(vm)?,
|
||||
Self::Enum(enum_) => enum_.eval(vm)?,
|
||||
Self::Desc(desc) => desc.eval(vm)?,
|
||||
Self::Label(_) => Content::Empty,
|
||||
Self::Ref(label) => Content::show(library::structure::RefNode(label.clone())),
|
||||
Self::Expr(expr) => expr.eval(vm)?.display(),
|
||||
@ -273,11 +277,8 @@ impl Eval for ListNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Content::Item(library::structure::ListItem {
|
||||
kind: library::structure::UNORDERED,
|
||||
number: None,
|
||||
body: Box::new(self.body().eval(vm)?),
|
||||
}))
|
||||
let body = Box::new(self.body().eval(vm)?);
|
||||
Ok(Content::Item(library::structure::ListItem::List(body)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,11 +286,23 @@ impl Eval for EnumNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Content::Item(library::structure::ListItem {
|
||||
kind: library::structure::ORDERED,
|
||||
number: self.number(),
|
||||
body: Box::new(self.body().eval(vm)?),
|
||||
}))
|
||||
let number = self.number();
|
||||
let body = Box::new(self.body().eval(vm)?);
|
||||
Ok(Content::Item(library::structure::ListItem::Enum(
|
||||
number, body,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for DescNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
let term = self.term().eval(vm)?;
|
||||
let body = self.body().eval(vm)?;
|
||||
Ok(Content::Item(library::structure::ListItem::Desc(Box::new(
|
||||
library::structure::DescItem { term, body },
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,7 @@ pub fn new() -> Scope {
|
||||
std.def_node::<structure::HeadingNode>("heading");
|
||||
std.def_node::<structure::ListNode>("list");
|
||||
std.def_node::<structure::EnumNode>("enum");
|
||||
std.def_node::<structure::DescNode>("desc");
|
||||
std.def_node::<structure::TableNode>("table");
|
||||
|
||||
// Layout.
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use unscanny::Scanner;
|
||||
|
||||
use crate::library::layout::{BlockSpacing, GridNode, TrackSizing};
|
||||
@ -9,9 +7,7 @@ use crate::library::utility::Numbering;
|
||||
|
||||
/// An unordered (bulleted) or ordered (numbered) list.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct ListNode<const L: ListKind = UNORDERED> {
|
||||
/// Where the list starts.
|
||||
pub start: usize,
|
||||
pub struct ListNode<const L: ListKind = LIST> {
|
||||
/// If true, the items are separated by leading instead of list spacing.
|
||||
pub tight: bool,
|
||||
/// If true, the spacing above the list is leading instead of above spacing.
|
||||
@ -20,19 +16,11 @@ pub struct ListNode<const L: ListKind = UNORDERED> {
|
||||
pub items: StyleVec<ListItem>,
|
||||
}
|
||||
|
||||
/// An item in a list.
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct ListItem {
|
||||
/// The kind of item.
|
||||
pub kind: ListKind,
|
||||
/// The number of the item.
|
||||
pub number: Option<usize>,
|
||||
/// The node that produces the item's body.
|
||||
pub body: Box<Content>,
|
||||
}
|
||||
|
||||
/// An ordered list.
|
||||
pub type EnumNode = ListNode<ORDERED>;
|
||||
pub type EnumNode = ListNode<ENUM>;
|
||||
|
||||
/// A description list.
|
||||
pub type DescNode = ListNode<DESC>;
|
||||
|
||||
#[node(showable)]
|
||||
impl<const L: ListKind> ListNode<L> {
|
||||
@ -44,7 +32,11 @@ impl<const L: ListKind> ListNode<L> {
|
||||
pub const INDENT: RawLength = RawLength::zero();
|
||||
/// The space between the label and the body of each item.
|
||||
#[property(resolve)]
|
||||
pub const BODY_INDENT: RawLength = Em::new(0.5).into();
|
||||
pub const BODY_INDENT: RawLength = Em::new(match L {
|
||||
LIST | ENUM => 0.5,
|
||||
DESC | _ => 1.0,
|
||||
})
|
||||
.into();
|
||||
|
||||
/// The spacing above the list.
|
||||
#[property(resolve, shorthand(around))]
|
||||
@ -57,19 +49,34 @@ impl<const L: ListKind> ListNode<L> {
|
||||
pub const SPACING: BlockSpacing = Ratio::one().into();
|
||||
|
||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
Ok(Content::show(Self {
|
||||
start: args.named("start")?.unwrap_or(1),
|
||||
tight: args.named("tight")?.unwrap_or(true),
|
||||
attached: args.named("attached")?.unwrap_or(false),
|
||||
items: args
|
||||
let items = match L {
|
||||
LIST => args
|
||||
.all()?
|
||||
.into_iter()
|
||||
.map(|body| ListItem {
|
||||
kind: L,
|
||||
number: None,
|
||||
body: Box::new(body),
|
||||
})
|
||||
.map(|body| ListItem::List(Box::new(body)))
|
||||
.collect(),
|
||||
ENUM => {
|
||||
let mut number: usize = args.named("start")?.unwrap_or(1);
|
||||
args.all()?
|
||||
.into_iter()
|
||||
.map(|body| {
|
||||
let item = ListItem::Enum(Some(number), Box::new(body));
|
||||
number += 1;
|
||||
item
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
DESC | _ => args
|
||||
.all()?
|
||||
.into_iter()
|
||||
.map(|item| ListItem::Desc(Box::new(item)))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
Ok(Content::show(Self {
|
||||
tight: args.named("tight")?.unwrap_or(true),
|
||||
attached: args.named("attached")?.unwrap_or(false),
|
||||
items,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -77,10 +84,7 @@ impl<const L: ListKind> ListNode<L> {
|
||||
impl<const L: ListKind> Show for ListNode<L> {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self {
|
||||
items: self.items.map(|item| ListItem {
|
||||
body: Box::new(item.body.unguard(sel).role(Role::ListItemBody)),
|
||||
..*item
|
||||
}),
|
||||
items: self.items.map(|item| item.unguard(sel)),
|
||||
..*self
|
||||
}
|
||||
.pack()
|
||||
@ -88,13 +92,12 @@ impl<const L: ListKind> Show for ListNode<L> {
|
||||
|
||||
fn encode(&self, _: StyleChain) -> Dict {
|
||||
dict! {
|
||||
"start" => Value::Int(self.start as i64),
|
||||
"tight" => Value::Bool(self.tight),
|
||||
"attached" => Value::Bool(self.attached),
|
||||
"items" => Value::Array(
|
||||
self.items
|
||||
.items()
|
||||
.map(|item| Value::Content(item.body.as_ref().clone()))
|
||||
.map(|item| item.encode())
|
||||
.collect()
|
||||
),
|
||||
}
|
||||
@ -106,34 +109,54 @@ impl<const L: ListKind> Show for ListNode<L> {
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Content> {
|
||||
let mut cells = vec![];
|
||||
let mut number = self.start;
|
||||
let mut number = 1;
|
||||
|
||||
let label = styles.get(Self::LABEL);
|
||||
|
||||
for (item, map) in self.items.iter() {
|
||||
number = item.number.unwrap_or(number);
|
||||
|
||||
cells.push(LayoutNode::default());
|
||||
cells.push(
|
||||
label
|
||||
.resolve(world, L, number)?
|
||||
.styled_with_map(map.clone())
|
||||
.role(Role::ListLabel)
|
||||
.pack(),
|
||||
);
|
||||
cells.push(LayoutNode::default());
|
||||
cells.push((*item.body).clone().styled_with_map(map.clone()).pack());
|
||||
number += 1;
|
||||
}
|
||||
|
||||
let indent = styles.get(Self::INDENT);
|
||||
let body_indent = styles.get(Self::BODY_INDENT);
|
||||
let gutter = if self.tight {
|
||||
styles.get(ParNode::LEADING)
|
||||
} else {
|
||||
styles.get(Self::SPACING)
|
||||
};
|
||||
|
||||
let indent = styles.get(Self::INDENT);
|
||||
let body_indent = styles.get(Self::BODY_INDENT);
|
||||
for (item, map) in self.items.iter() {
|
||||
if let &ListItem::Enum(Some(n), _) = item {
|
||||
number = n;
|
||||
}
|
||||
|
||||
cells.push(LayoutNode::default());
|
||||
|
||||
let label = if L == LIST || L == ENUM {
|
||||
label
|
||||
.resolve(world, L, number)?
|
||||
.styled_with_map(map.clone())
|
||||
.role(Role::ListLabel)
|
||||
.pack()
|
||||
} else {
|
||||
LayoutNode::default()
|
||||
};
|
||||
|
||||
cells.push(label);
|
||||
cells.push(LayoutNode::default());
|
||||
|
||||
let body = match &item {
|
||||
ListItem::List(body) => body.as_ref().clone(),
|
||||
ListItem::Enum(_, body) => body.as_ref().clone(),
|
||||
ListItem::Desc(item) => Content::sequence(vec![
|
||||
Content::Horizontal {
|
||||
amount: (-body_indent).into(),
|
||||
weak: false,
|
||||
},
|
||||
(item.term.clone() + Content::Text(':'.into())).strong(),
|
||||
Content::Space,
|
||||
item.body.clone(),
|
||||
]),
|
||||
};
|
||||
|
||||
cells.push(body.styled_with_map(map.clone()).pack());
|
||||
number += 1;
|
||||
}
|
||||
|
||||
Ok(Content::block(GridNode {
|
||||
tracks: Spec::with_x(vec![
|
||||
@ -165,35 +188,110 @@ impl<const L: ListKind> Show for ListNode<L> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(realized
|
||||
.role(Role::List { ordered: L == ORDERED })
|
||||
.spaced(above, below))
|
||||
Ok(realized.role(Role::List { ordered: L == ENUM }).spaced(above, below))
|
||||
}
|
||||
}
|
||||
|
||||
/// An item in a list.
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub enum ListItem {
|
||||
/// An item of an unordered list.
|
||||
List(Box<Content>),
|
||||
/// An item of an ordered list.
|
||||
Enum(Option<usize>, Box<Content>),
|
||||
/// An item of a description list.
|
||||
Desc(Box<DescItem>),
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
/// What kind of item this is.
|
||||
pub fn kind(&self) -> ListKind {
|
||||
match self {
|
||||
Self::List(_) => LIST,
|
||||
Self::Enum { .. } => ENUM,
|
||||
Self::Desc { .. } => DESC,
|
||||
}
|
||||
}
|
||||
|
||||
fn unguard(&self, sel: Selector) -> Self {
|
||||
match self {
|
||||
Self::List(body) => Self::List(Box::new(body.unguard(sel))),
|
||||
Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))),
|
||||
Self::Desc(item) => Self::Desc(Box::new(DescItem {
|
||||
term: item.term.unguard(sel),
|
||||
body: item.body.unguard(sel),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the item into a value.
|
||||
fn encode(&self) -> Value {
|
||||
match self {
|
||||
Self::List(body) => Value::Content(body.as_ref().clone()),
|
||||
Self::Enum(number, body) => Value::Dict(dict! {
|
||||
"number" => match *number {
|
||||
Some(n) => Value::Int(n as i64),
|
||||
None => Value::None,
|
||||
},
|
||||
"body" => Value::Content(body.as_ref().clone()),
|
||||
}),
|
||||
Self::Desc(item) => Value::Dict(dict! {
|
||||
"term" => Value::Content(item.term.clone()),
|
||||
"body" => Value::Content(item.body.clone()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ListItem {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.kind == UNORDERED {
|
||||
f.write_char('-')?;
|
||||
} else {
|
||||
if let Some(number) = self.number {
|
||||
write!(f, "{}", number)?;
|
||||
}
|
||||
f.write_char('.')?;
|
||||
match self {
|
||||
Self::List(body) => write!(f, "- {body:?}"),
|
||||
Self::Enum(number, body) => match number {
|
||||
Some(n) => write!(f, "{n}. {body:?}"),
|
||||
None => write!(f, "+ {body:?}"),
|
||||
},
|
||||
Self::Desc(item) => item.fmt(f),
|
||||
}
|
||||
f.write_char(' ')?;
|
||||
self.body.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A description list item.
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct DescItem {
|
||||
/// The term described by the list item.
|
||||
pub term: Content,
|
||||
/// The description of the term.
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
castable! {
|
||||
DescItem,
|
||||
Expected: "dictionary with `term` and `body` keys",
|
||||
Value::Dict(dict) => {
|
||||
let term: Content = dict.get("term")?.clone().cast()?;
|
||||
let body: Content = dict.get("body")?.clone().cast()?;
|
||||
Self { term, body }
|
||||
},
|
||||
}
|
||||
|
||||
impl Debug for DescItem {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "/ {:?}: {:?}", self.term, self.body)
|
||||
}
|
||||
}
|
||||
|
||||
/// How to label a list.
|
||||
pub type ListKind = usize;
|
||||
|
||||
/// Unordered list labelling style.
|
||||
pub const UNORDERED: ListKind = 0;
|
||||
/// An unordered list.
|
||||
pub const LIST: ListKind = 0;
|
||||
|
||||
/// Ordered list labelling style.
|
||||
pub const ORDERED: ListKind = 1;
|
||||
/// An ordered list.
|
||||
pub const ENUM: ListKind = 1;
|
||||
|
||||
/// A description list.
|
||||
pub const DESC: ListKind = 2;
|
||||
|
||||
/// How to label a list or enumeration.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
@ -218,8 +316,9 @@ impl Label {
|
||||
) -> SourceResult<Content> {
|
||||
Ok(match self {
|
||||
Self::Default => match kind {
|
||||
UNORDERED => Content::Text('•'.into()),
|
||||
ORDERED | _ => Content::Text(format_eco!("{}.", number)),
|
||||
LIST => Content::Text('•'.into()),
|
||||
ENUM => Content::Text(format_eco!("{}.", number)),
|
||||
DESC | _ => panic!("description lists don't have a label"),
|
||||
},
|
||||
Self::Pattern(prefix, numbering, upper, suffix) => {
|
||||
let fmt = numbering.apply(number);
|
||||
|
@ -10,6 +10,13 @@ pub struct LinkNode {
|
||||
pub body: Option<Content>,
|
||||
}
|
||||
|
||||
impl LinkNode {
|
||||
/// Create a link node from a URL with its bare text.
|
||||
pub fn from_url(url: EcoString) -> Self {
|
||||
Self { dest: Destination::Url(url), body: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[node(showable)]
|
||||
impl LinkNode {
|
||||
/// The fill color of text in the link. Just the surrounding text color
|
||||
|
@ -181,8 +181,8 @@ pub struct LinebreakNode;
|
||||
#[node]
|
||||
impl LinebreakNode {
|
||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||
let justified = args.named("justified")?.unwrap_or(false);
|
||||
Ok(Content::Linebreak { justified })
|
||||
let justify = args.named("justify")?.unwrap_or(false);
|
||||
Ok(Content::Linebreak { justify })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ impl Show for RawNode {
|
||||
let mut highlighter = HighlightLines::new(syntax, &THEME);
|
||||
for (i, line) in self.text.lines().enumerate() {
|
||||
if i != 0 {
|
||||
seq.push(Content::Linebreak { justified: false });
|
||||
seq.push(Content::Linebreak { justify: false });
|
||||
}
|
||||
|
||||
for (style, piece) in
|
||||
|
@ -14,7 +14,7 @@ use super::{
|
||||
use crate::diag::StrResult;
|
||||
use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing};
|
||||
use crate::library::prelude::*;
|
||||
use crate::library::structure::{DocNode, ListItem, ListNode, ORDERED, UNORDERED};
|
||||
use crate::library::structure::{DocNode, ListItem, ListNode, DESC, ENUM, LIST};
|
||||
use crate::library::text::{
|
||||
DecoNode, EmphNode, ParChild, ParNode, StrongNode, UNDERLINE,
|
||||
};
|
||||
@ -62,7 +62,7 @@ pub enum Content {
|
||||
/// A word space.
|
||||
Space,
|
||||
/// A forced line break.
|
||||
Linebreak { justified: bool },
|
||||
Linebreak { justify: bool },
|
||||
/// Horizontal spacing.
|
||||
Horizontal { amount: Spacing, weak: bool },
|
||||
/// Plain text.
|
||||
@ -264,7 +264,7 @@ impl Debug for Content {
|
||||
match self {
|
||||
Self::Empty => f.pad("Empty"),
|
||||
Self::Space => f.pad("Space"),
|
||||
Self::Linebreak { justified } => write!(f, "Linebreak({justified})"),
|
||||
Self::Linebreak { justify } => write!(f, "Linebreak({justify})"),
|
||||
Self::Horizontal { amount, weak } => {
|
||||
write!(f, "Horizontal({amount:?}, {weak})")
|
||||
}
|
||||
@ -633,8 +633,8 @@ impl<'a> ParBuilder<'a> {
|
||||
Content::Space => {
|
||||
self.0.weak(ParChild::Text(' '.into()), styles, 2);
|
||||
}
|
||||
&Content::Linebreak { justified } => {
|
||||
let c = if justified { '\u{2028}' } else { '\n' };
|
||||
&Content::Linebreak { justify } => {
|
||||
let c = if justify { '\u{2028}' } else { '\n' };
|
||||
self.0.destructive(ParChild::Text(c.into()), styles);
|
||||
}
|
||||
&Content::Horizontal { amount, weak } => {
|
||||
@ -734,7 +734,7 @@ impl<'a> ListBuilder<'a> {
|
||||
.items
|
||||
.items()
|
||||
.next()
|
||||
.map_or(true, |first| item.kind == first.kind) =>
|
||||
.map_or(true, |first| item.kind() == first.kind()) =>
|
||||
{
|
||||
self.items.push(item.clone(), styles);
|
||||
self.tight &= self.staged.drain(..).all(|(t, _)| *t != Content::Parbreak);
|
||||
@ -751,21 +751,16 @@ impl<'a> ListBuilder<'a> {
|
||||
fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> {
|
||||
let (items, shared) = self.items.finish();
|
||||
let kind = match items.items().next() {
|
||||
Some(item) => item.kind,
|
||||
Some(item) => item.kind(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let start = 1;
|
||||
let tight = self.tight;
|
||||
let attached = tight && self.attachable;
|
||||
|
||||
let content = match kind {
|
||||
UNORDERED => {
|
||||
Content::show(ListNode::<UNORDERED> { start, tight, attached, items })
|
||||
}
|
||||
ORDERED | _ => {
|
||||
Content::show(ListNode::<ORDERED> { start, tight, attached, items })
|
||||
}
|
||||
LIST => Content::show(ListNode::<LIST> { tight, attached, items }),
|
||||
ENUM => Content::show(ListNode::<ENUM> { tight, attached, items }),
|
||||
DESC | _ => Content::show(ListNode::<DESC> { tight, attached, items }),
|
||||
};
|
||||
|
||||
let stored = parent.scratch.templates.alloc(content);
|
||||
|
@ -9,7 +9,7 @@ use super::{Interruption, NodeId, StyleChain};
|
||||
use crate::eval::{RawLength, Smart};
|
||||
use crate::geom::{Corners, Length, Numeric, Relative, Sides, Spec};
|
||||
use crate::library::layout::PageNode;
|
||||
use crate::library::structure::{EnumNode, ListNode};
|
||||
use crate::library::structure::{DescNode, EnumNode, ListNode};
|
||||
use crate::library::text::ParNode;
|
||||
use crate::util::ReadableTypeId;
|
||||
|
||||
@ -68,7 +68,10 @@ impl Property {
|
||||
Some(Interruption::Page)
|
||||
} else if self.is_of::<ParNode>() {
|
||||
Some(Interruption::Par)
|
||||
} else if self.is_of::<ListNode>() || self.is_of::<EnumNode>() {
|
||||
} else if self.is_of::<ListNode>()
|
||||
|| self.is_of::<EnumNode>()
|
||||
|| self.is_of::<DescNode>()
|
||||
{
|
||||
Some(Interruption::List)
|
||||
} else {
|
||||
None
|
||||
|
@ -5,7 +5,7 @@ use comemo::Tracked;
|
||||
use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntry};
|
||||
use crate::diag::SourceResult;
|
||||
use crate::eval::{Args, Func, Regex, Value};
|
||||
use crate::library::structure::{EnumNode, ListNode};
|
||||
use crate::library::structure::{DescNode, EnumNode, ListNode};
|
||||
use crate::syntax::Spanned;
|
||||
use crate::World;
|
||||
|
||||
@ -93,7 +93,10 @@ impl Recipe {
|
||||
/// What kind of structure the property interrupts.
|
||||
pub fn interruption(&self) -> Option<Interruption> {
|
||||
if let Pattern::Node(id) = self.pattern {
|
||||
if id == NodeId::of::<ListNode>() || id == NodeId::of::<EnumNode>() {
|
||||
if id == NodeId::of::<ListNode>()
|
||||
|| id == NodeId::of::<EnumNode>()
|
||||
|| id == NodeId::of::<DescNode>()
|
||||
{
|
||||
return Some(Interruption::List);
|
||||
}
|
||||
}
|
||||
|
@ -407,18 +407,18 @@ mod tests {
|
||||
test("", 0..0, "do it", 0..5);
|
||||
test("a d e", 1 .. 3, " b c d", 0 .. 9);
|
||||
test("*~ *", 2..2, "*", 0..5);
|
||||
test("_1_\n2a\n3", 5..5, "4", 0..7);
|
||||
test("_1_\n2a\n3~", 8..8, "4", 5..10);
|
||||
test("_1_\n2a\n3", 5..5, "4", 4..7);
|
||||
test("_1_\n2a\n3~", 8..8, "4", 4..10);
|
||||
test("_1_ 2 3a\n4", 7..7, "5", 0..9);
|
||||
test("* {1+2} *", 5..6, "3", 2..7);
|
||||
test("a #f() e", 1 .. 6, " b c d", 0 .. 9);
|
||||
test("a\nb\nc\nd\ne\n", 5 .. 5, "c", 2 .. 7);
|
||||
test("a\n\nb\n\nc\n\nd\n\ne\n", 7 .. 7, "c", 3 .. 10);
|
||||
test("a\nb\nc *hel a b lo* d\nd\ne", 13..13, "c ", 6..20);
|
||||
test("a\nb\nc *hel a b lo* d\nd\ne", 13..13, "c ", 4..20);
|
||||
test("~~ {a} ~~", 4 .. 5, "b", 3 .. 6);
|
||||
test("{(0, 1, 2)}", 5 .. 6, "11pt", 0..14);
|
||||
test("\n= A heading", 4 .. 4, "n evocative", 0 .. 23);
|
||||
test("for~your~thing", 9 .. 9, "a", 4 .. 15);
|
||||
test("for~your~thing", 9 .. 9, "a", 0 .. 15);
|
||||
test("a your thing a", 6 .. 7, "a", 0 .. 14);
|
||||
test("{call(); abc}", 7 .. 7, "[]", 0 .. 15);
|
||||
test("#call() abc", 7 .. 7, "[]", 0 .. 10);
|
||||
@ -429,17 +429,17 @@ mod tests {
|
||||
test("#grid(columns: (auto, 1fr, 40%), [*plonk*], rect(width: 100%, height: 1pt, fill: conifer), [thing])", 34 .. 41, "_bar_", 33 .. 40);
|
||||
test("{let i=1; for x in range(5) {i}}", 6 .. 6, " ", 0 .. 33);
|
||||
test("{let i=1; for x in range(5) {i}}", 13 .. 14, " ", 0 .. 33);
|
||||
test("hello~~{x}", 7 .. 10, "#f()", 5 .. 11);
|
||||
test("this~is -- in my opinion -- spectacular", 8 .. 10, "---", 5 .. 25);
|
||||
test("understanding `code` is complicated", 15 .. 15, "C ", 14 .. 22);
|
||||
test("hello~~{x}", 7 .. 10, "#f()", 0 .. 11);
|
||||
test("this~is -- in my opinion -- spectacular", 8 .. 10, "---", 0 .. 25);
|
||||
test("understanding `code` is complicated", 15 .. 15, "C ", 0 .. 22);
|
||||
test("{ let x = g() }", 10 .. 12, "f(54", 0 .. 17);
|
||||
test(r#"a ```typst hello``` b"#, 16 .. 17, "", 2 .. 18);
|
||||
test(r#"a ```typst hello```"#, 16 .. 17, "", 2 .. 18);
|
||||
test(r#"a ```typst hello``` b"#, 16 .. 17, "", 0 .. 18);
|
||||
test(r#"a ```typst hello```"#, 16 .. 17, "", 0 .. 18);
|
||||
test("#for", 4 .. 4, "//", 0 .. 6);
|
||||
test("#show a: f as b..", 16..16, "c", 0..18);
|
||||
test("a\n#let \nb", 7 .. 7, "i", 2 .. 9);
|
||||
test("a\n#for i \nb", 9 .. 9, "in", 2 .. 12);
|
||||
test("a~https://fun/html", 13..14, "n", 2..18);
|
||||
test("a~https://fun/html", 13..14, "n", 0..18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -452,7 +452,7 @@ mod tests {
|
||||
test("abc\n= a heading\njoke", 3 .. 4, "\nnot ", 0 .. 19);
|
||||
test("#let x = (1, 2 + ;~ Five\r\n\r", 20 .. 23, "2.", 0 .. 23);
|
||||
test("hey #myfriend", 4 .. 4, "\\", 0 .. 14);
|
||||
test("hey #myfriend", 4 .. 4, "\\", 3 .. 6);
|
||||
test("hey #myfriend", 4 .. 4, "\\", 0 .. 6);
|
||||
test("= foo\nbar\n - a\n - b", 6 .. 9, "", 0 .. 11);
|
||||
test("= foo\n bar\n baz", 6 .. 8, "", 0 .. 9);
|
||||
test(" // hi", 1 .. 1, " ", 0 .. 7);
|
||||
@ -461,12 +461,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_incremental_type_invariants() {
|
||||
test("a #for x in array {x}", 18 .. 21, "[#x]", 2 .. 22);
|
||||
test("a #let x = 1 {5}", 3 .. 6, "if", 2 .. 11);
|
||||
test("a #for x in array {x}", 18 .. 21, "[#x]", 0 .. 22);
|
||||
test("a #let x = 1 {5}", 3 .. 6, "if", 0 .. 11);
|
||||
test("a {let x = 1 {5}} b", 3 .. 6, "if", 2 .. 16);
|
||||
test("#let x = 1 {5}", 4 .. 4, " if", 0 .. 13);
|
||||
test("{let x = 1 {5}}", 4 .. 4, " if", 0 .. 18);
|
||||
test("a // b c #f()", 3 .. 4, "", 2 .. 12);
|
||||
test("a // b c #f()", 3 .. 4, "", 0 .. 12);
|
||||
test("{\nf()\n//g(a)\n}", 6 .. 8, "", 0 .. 12);
|
||||
test("a{\nf()\n//g(a)\n}b", 7 .. 9, "", 1 .. 13);
|
||||
test("a #while x {\n g(x) \n} b", 11 .. 11, "//", 0 .. 26);
|
||||
@ -477,8 +477,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_incremental_wrongly_or_unclosed_things() {
|
||||
test(r#"{"hi"}"#, 4 .. 5, "c", 0 .. 6);
|
||||
test(r"this \u{abcd}", 8 .. 9, "", 5 .. 12);
|
||||
test(r"this \u{abcd} that", 12 .. 13, "", 5 .. 17);
|
||||
test(r"this \u{abcd}", 8 .. 9, "", 0 .. 12);
|
||||
test(r"this \u{abcd} that", 12 .. 13, "", 0 .. 17);
|
||||
test(r"{{let x = z}; a = 1} b", 6 .. 6, "//", 0 .. 24);
|
||||
test("a b c", 1 .. 1, " /* letters */", 0 .. 19);
|
||||
test("a b c", 1 .. 1, " /* letters", 0 .. 16);
|
||||
|
@ -162,11 +162,6 @@ fn markup(p: &mut Parser, mut at_start: bool) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Parse a single line of markup.
|
||||
fn markup_line(p: &mut Parser) {
|
||||
markup_indented(p, usize::MAX);
|
||||
}
|
||||
|
||||
/// Parse markup that stays right of the given `column`.
|
||||
fn markup_indented(p: &mut Parser, min_indent: usize) {
|
||||
p.eat_while(|t| match t {
|
||||
@ -185,7 +180,6 @@ fn markup_indented(p: &mut Parser, min_indent: usize) {
|
||||
{
|
||||
break;
|
||||
}
|
||||
Some(NodeKind::Label(_)) => break,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -195,6 +189,33 @@ fn markup_indented(p: &mut Parser, min_indent: usize) {
|
||||
marker.end(p, NodeKind::Markup { min_indent });
|
||||
}
|
||||
|
||||
/// Parse a line of markup that can prematurely end if `f` returns true.
|
||||
fn markup_line<F>(p: &mut Parser, mut f: F)
|
||||
where
|
||||
F: FnMut(&NodeKind) -> bool,
|
||||
{
|
||||
p.eat_while(|t| match t {
|
||||
NodeKind::Space { newlines } => *newlines == 0,
|
||||
NodeKind::LineComment | NodeKind::BlockComment => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
p.perform(NodeKind::Markup { min_indent: usize::MAX }, |p| {
|
||||
let mut at_start = false;
|
||||
while let Some(kind) = p.peek() {
|
||||
if let NodeKind::Space { newlines: (1 ..) } = kind {
|
||||
break;
|
||||
}
|
||||
|
||||
if f(kind) {
|
||||
break;
|
||||
}
|
||||
|
||||
markup_node(p, &mut at_start);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Parse a markup node.
|
||||
fn markup_node(p: &mut Parser, at_start: &mut bool) {
|
||||
let token = match p.peek() {
|
||||
@ -226,6 +247,7 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
|
||||
| NodeKind::Ellipsis
|
||||
| NodeKind::Quote { .. }
|
||||
| NodeKind::Escape(_)
|
||||
| NodeKind::Link(_)
|
||||
| NodeKind::Raw(_)
|
||||
| NodeKind::Math(_)
|
||||
| NodeKind::Label(_)
|
||||
@ -233,12 +255,22 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
|
||||
p.eat();
|
||||
}
|
||||
|
||||
// Grouping markup.
|
||||
// Strong, emph, heading.
|
||||
NodeKind::Star => strong(p),
|
||||
NodeKind::Underscore => emph(p),
|
||||
NodeKind::Eq => heading(p, *at_start),
|
||||
|
||||
// Lists.
|
||||
NodeKind::Minus => list_node(p, *at_start),
|
||||
NodeKind::EnumNumbering(_) => enum_node(p, *at_start),
|
||||
NodeKind::Plus | NodeKind::EnumNumbering(_) => enum_node(p, *at_start),
|
||||
NodeKind::Slash => {
|
||||
desc_node(p, *at_start).ok();
|
||||
}
|
||||
NodeKind::Colon => {
|
||||
let marker = p.marker();
|
||||
p.eat();
|
||||
marker.convert(p, NodeKind::Text(':'.into()));
|
||||
}
|
||||
|
||||
// Hashtag + keyword / identifier.
|
||||
NodeKind::Ident(_)
|
||||
@ -293,7 +325,7 @@ fn heading(p: &mut Parser, at_start: bool) {
|
||||
|
||||
if at_start && p.peek().map_or(true, |kind| kind.is_space()) {
|
||||
p.eat_while(|kind| *kind == NodeKind::Space { newlines: 0 });
|
||||
markup_line(p);
|
||||
markup_line(p, |kind| matches!(kind, NodeKind::Label(_)));
|
||||
marker.end(p, NodeKind::Heading);
|
||||
} else {
|
||||
let text = p.get(current_start .. p.prev_end()).into();
|
||||
@ -331,6 +363,25 @@ fn enum_node(p: &mut Parser, at_start: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a single description list item.
|
||||
fn desc_node(p: &mut Parser, at_start: bool) -> ParseResult {
|
||||
let marker = p.marker();
|
||||
let text: EcoString = p.peek_src().into();
|
||||
p.eat();
|
||||
|
||||
let min_indent = p.column(p.prev_end());
|
||||
if at_start && p.eat_if(NodeKind::Space { newlines: 0 }) && !p.eof() {
|
||||
markup_line(p, |node| matches!(node, NodeKind::Colon));
|
||||
p.expect(NodeKind::Colon)?;
|
||||
markup_indented(p, min_indent);
|
||||
marker.end(p, NodeKind::Desc);
|
||||
} else {
|
||||
marker.convert(p, NodeKind::Text(text));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse an expression within a markup mode.
|
||||
fn markup_expr(p: &mut Parser) {
|
||||
// Does the expression need termination or can content follow directly?
|
||||
|
@ -26,14 +26,12 @@ pub fn resolve_string(string: &str) -> EcoString {
|
||||
// TODO: Error if closing brace is missing.
|
||||
let sequence = s.eat_while(char::is_ascii_hexdigit);
|
||||
let _terminated = s.eat_if('}');
|
||||
|
||||
match resolve_hex(sequence) {
|
||||
Some(c) => out.push(c),
|
||||
None => out.push_str(s.from(start)),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Error for invalid escape sequence.
|
||||
_ => out.push_str(s.from(start)),
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +103,11 @@ impl<'s> Iterator for Tokens<'s> {
|
||||
let start = self.s.cursor();
|
||||
let c = self.s.eat()?;
|
||||
Some(match c {
|
||||
// Comments.
|
||||
'/' if self.s.eat_if('/') => self.line_comment(),
|
||||
'/' if self.s.eat_if('*') => self.block_comment(),
|
||||
'*' if self.s.eat_if('/') => NodeKind::Unknown("*/".into()),
|
||||
|
||||
// Blocks.
|
||||
'{' => NodeKind::LeftBrace,
|
||||
'}' => NodeKind::RightBrace,
|
||||
@ -110,15 +115,7 @@ impl<'s> Iterator for Tokens<'s> {
|
||||
']' => NodeKind::RightBracket,
|
||||
|
||||
// Whitespace.
|
||||
' ' if self.s.done() || !self.s.at(char::is_whitespace) => {
|
||||
NodeKind::Space { newlines: 0 }
|
||||
}
|
||||
c if c.is_whitespace() => self.whitespace(),
|
||||
|
||||
// Comments with special case for URLs.
|
||||
'/' if self.s.eat_if('*') => self.block_comment(),
|
||||
'/' if !self.maybe_in_url() && self.s.eat_if('/') => self.line_comment(),
|
||||
'*' if self.s.eat_if('/') => NodeKind::Unknown(self.s.from(start).into()),
|
||||
c if c.is_whitespace() => self.whitespace(c),
|
||||
|
||||
// Other things.
|
||||
_ => match self.mode {
|
||||
@ -130,122 +127,50 @@ impl<'s> Iterator for Tokens<'s> {
|
||||
}
|
||||
|
||||
impl<'s> Tokens<'s> {
|
||||
#[inline]
|
||||
fn markup(&mut self, start: usize, c: char) -> NodeKind {
|
||||
match c {
|
||||
// Escape sequences.
|
||||
'\\' => self.backslash(),
|
||||
|
||||
// Keywords and identifiers.
|
||||
'#' => self.hash(),
|
||||
|
||||
// Markup.
|
||||
'~' => NodeKind::NonBreakingSpace,
|
||||
'-' => self.hyph(),
|
||||
'.' if self.s.eat_if("..") => NodeKind::Ellipsis,
|
||||
'\'' => NodeKind::Quote { double: false },
|
||||
'"' => NodeKind::Quote { double: true },
|
||||
'*' if !self.in_word() => NodeKind::Star,
|
||||
'_' if !self.in_word() => NodeKind::Underscore,
|
||||
'`' => self.raw(),
|
||||
'=' => NodeKind::Eq,
|
||||
'$' => self.math(),
|
||||
'<' => self.label(),
|
||||
'@' => self.reference(),
|
||||
c if c == '.' || c.is_ascii_digit() => self.numbering(start, c),
|
||||
|
||||
// Plain text.
|
||||
_ => self.text(start),
|
||||
fn line_comment(&mut self) -> NodeKind {
|
||||
self.s.eat_until(is_newline);
|
||||
if self.s.peek().is_none() {
|
||||
self.terminated = false;
|
||||
}
|
||||
NodeKind::LineComment
|
||||
}
|
||||
|
||||
fn code(&mut self, start: usize, c: char) -> NodeKind {
|
||||
match c {
|
||||
// Parens.
|
||||
'(' => NodeKind::LeftParen,
|
||||
')' => NodeKind::RightParen,
|
||||
fn block_comment(&mut self) -> NodeKind {
|
||||
let mut state = '_';
|
||||
let mut depth = 1;
|
||||
self.terminated = false;
|
||||
|
||||
// Length two.
|
||||
'=' if self.s.eat_if('=') => NodeKind::EqEq,
|
||||
'!' if self.s.eat_if('=') => NodeKind::ExclEq,
|
||||
'<' if self.s.eat_if('=') => NodeKind::LtEq,
|
||||
'>' if self.s.eat_if('=') => NodeKind::GtEq,
|
||||
'+' if self.s.eat_if('=') => NodeKind::PlusEq,
|
||||
'-' if self.s.eat_if('=') => NodeKind::HyphEq,
|
||||
'*' if self.s.eat_if('=') => NodeKind::StarEq,
|
||||
'/' if self.s.eat_if('=') => NodeKind::SlashEq,
|
||||
'.' if self.s.eat_if('.') => NodeKind::Dots,
|
||||
'=' if self.s.eat_if('>') => NodeKind::Arrow,
|
||||
|
||||
// Length one.
|
||||
',' => NodeKind::Comma,
|
||||
';' => NodeKind::Semicolon,
|
||||
':' => NodeKind::Colon,
|
||||
'+' => NodeKind::Plus,
|
||||
'-' => NodeKind::Minus,
|
||||
'*' => NodeKind::Star,
|
||||
'/' => NodeKind::Slash,
|
||||
'=' => NodeKind::Eq,
|
||||
'<' => NodeKind::Lt,
|
||||
'>' => NodeKind::Gt,
|
||||
'.' if self.s.done() || !self.s.at(char::is_ascii_digit) => NodeKind::Dot,
|
||||
|
||||
// Identifiers.
|
||||
c if is_id_start(c) => self.ident(start),
|
||||
|
||||
// Numbers.
|
||||
c if c.is_ascii_digit() || (c == '.' && self.s.at(char::is_ascii_digit)) => {
|
||||
self.number(start, c)
|
||||
// Find the first `*/` that does not correspond to a nested `/*`.
|
||||
while let Some(c) = self.s.eat() {
|
||||
state = match (state, c) {
|
||||
('*', '/') => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
self.terminated = true;
|
||||
break;
|
||||
}
|
||||
'_'
|
||||
}
|
||||
('/', '*') => {
|
||||
depth += 1;
|
||||
'_'
|
||||
}
|
||||
('/', '/') => {
|
||||
self.line_comment();
|
||||
'_'
|
||||
}
|
||||
_ => c,
|
||||
}
|
||||
|
||||
// Strings.
|
||||
'"' => self.string(),
|
||||
|
||||
_ => NodeKind::Unknown(self.s.from(start).into()),
|
||||
}
|
||||
|
||||
NodeKind::BlockComment
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn text(&mut self, start: usize) -> NodeKind {
|
||||
macro_rules! table {
|
||||
($($c:literal)|*) => {{
|
||||
let mut t = [false; 128];
|
||||
$(t[$c as usize] = true;)*
|
||||
t
|
||||
}}
|
||||
fn whitespace(&mut self, c: char) -> NodeKind {
|
||||
if c == ' ' && !self.s.at(char::is_whitespace) {
|
||||
return NodeKind::Space { newlines: 0 };
|
||||
}
|
||||
|
||||
const TABLE: [bool; 128] = table! {
|
||||
// Ascii whitespace.
|
||||
' ' | '\t' | '\n' | '\x0b' | '\x0c' | '\r' |
|
||||
// Comments, parentheses, code.
|
||||
'/' | '[' | ']' | '{' | '}' | '#' |
|
||||
// Markup
|
||||
'~' | '-' | '.' | '\'' | '"' | '*' | '_' | '`' | '$' | '\\'
|
||||
};
|
||||
|
||||
loop {
|
||||
self.s.eat_until(|c: char| {
|
||||
TABLE.get(c as usize).copied().unwrap_or_else(|| c.is_whitespace())
|
||||
});
|
||||
|
||||
// Allow a single space, optionally preceded by . or - if something
|
||||
// alphanumeric follows directly. This leads to less text nodes,
|
||||
// which is good for performance.
|
||||
let mut s = self.s;
|
||||
s.eat_if(['.', '-']);
|
||||
s.eat_if(' ');
|
||||
if !s.at(char::is_alphanumeric) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.s = s;
|
||||
}
|
||||
|
||||
NodeKind::Text(self.s.from(start).into())
|
||||
}
|
||||
|
||||
fn whitespace(&mut self) -> NodeKind {
|
||||
self.s.uneat();
|
||||
|
||||
// Count the number of newlines.
|
||||
@ -267,25 +192,84 @@ impl<'s> Tokens<'s> {
|
||||
NodeKind::Space { newlines }
|
||||
}
|
||||
|
||||
fn backslash(&mut self) -> NodeKind {
|
||||
let c = match self.s.peek() {
|
||||
Some(c) => c,
|
||||
None => return NodeKind::Linebreak { justified: false },
|
||||
#[inline]
|
||||
fn markup(&mut self, start: usize, c: char) -> NodeKind {
|
||||
match c {
|
||||
// Escape sequences.
|
||||
'\\' => self.backslash(),
|
||||
|
||||
// Single-char things.
|
||||
'~' => NodeKind::NonBreakingSpace,
|
||||
'.' if self.s.eat_if("..") => NodeKind::Ellipsis,
|
||||
'\'' => NodeKind::Quote { double: false },
|
||||
'"' => NodeKind::Quote { double: true },
|
||||
'*' if !self.in_word() => NodeKind::Star,
|
||||
'_' if !self.in_word() => NodeKind::Underscore,
|
||||
'=' => NodeKind::Eq,
|
||||
'+' => NodeKind::Plus,
|
||||
'/' => NodeKind::Slash,
|
||||
':' => NodeKind::Colon,
|
||||
|
||||
// Multi-char things.
|
||||
'#' => self.hash(start),
|
||||
'-' => self.hyph(),
|
||||
'h' if self.s.eat_if("ttp://") || self.s.eat_if("ttps://") => {
|
||||
self.link(start)
|
||||
}
|
||||
'`' => self.raw(),
|
||||
'$' => self.math(),
|
||||
c if c.is_ascii_digit() => self.numbering(start),
|
||||
'<' => self.label(),
|
||||
'@' => self.reference(start),
|
||||
|
||||
// Plain text.
|
||||
_ => self.text(start),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn text(&mut self, start: usize) -> NodeKind {
|
||||
macro_rules! table {
|
||||
($(|$c:literal)*) => {{
|
||||
let mut t = [false; 128];
|
||||
$(t[$c as usize] = true;)*
|
||||
t
|
||||
}}
|
||||
}
|
||||
|
||||
const TABLE: [bool; 128] = table! {
|
||||
| ' ' | '\t' | '\n' | '\x0b' | '\x0c' | '\r' | '\\' | '/'
|
||||
| '[' | ']' | '{' | '}' | '~' | '-' | '.' | '\'' | '"'
|
||||
| '*' | '_' | ':' | 'h' | '`' | '$' | '<' | '>' | '@' | '#'
|
||||
};
|
||||
|
||||
match c {
|
||||
// Backslash and comments.
|
||||
'\\' | '/' |
|
||||
// Parenthesis and hashtag.
|
||||
'[' | ']' | '{' | '}' | '#' |
|
||||
// Markup.
|
||||
'~' | '-' | '.' | ':' |
|
||||
'\'' | '"' | '*' | '_' | '`' | '$' | '=' |
|
||||
'<' | '>' | '@' => {
|
||||
self.s.expect(c);
|
||||
NodeKind::Escape(c)
|
||||
loop {
|
||||
self.s.eat_until(|c: char| {
|
||||
TABLE.get(c as usize).copied().unwrap_or_else(|| c.is_whitespace())
|
||||
});
|
||||
|
||||
// Continue with the same text node if the thing would become text
|
||||
// anyway.
|
||||
let mut s = self.s;
|
||||
match s.eat() {
|
||||
Some('/') if !s.at(['/', '*']) => {}
|
||||
Some(' ') if s.at(char::is_alphanumeric) => {}
|
||||
Some('-') if !s.at(['-', '?']) => {}
|
||||
Some('.') if !s.at("..") => {}
|
||||
Some('h') if !s.at("ttp://") && !s.at("ttps://") => {}
|
||||
Some('@' | '#') if !s.at(is_id_start) => {}
|
||||
_ => break,
|
||||
}
|
||||
'u' if self.s.eat_if("u{") => {
|
||||
|
||||
self.s = s;
|
||||
}
|
||||
|
||||
NodeKind::Text(self.s.from(start).into())
|
||||
}
|
||||
|
||||
fn backslash(&mut self) -> NodeKind {
|
||||
match self.s.peek() {
|
||||
Some('u') if self.s.eat_if("u{") => {
|
||||
let sequence = self.s.eat_while(char::is_ascii_alphanumeric);
|
||||
if self.s.eat_if('}') {
|
||||
if let Some(c) = resolve_hex(sequence) {
|
||||
@ -298,26 +282,23 @@ impl<'s> Tokens<'s> {
|
||||
}
|
||||
} else {
|
||||
self.terminated = false;
|
||||
NodeKind::Error(
|
||||
SpanPos::End,
|
||||
"expected closing brace".into(),
|
||||
)
|
||||
NodeKind::Error(SpanPos::End, "expected closing brace".into())
|
||||
}
|
||||
}
|
||||
|
||||
// Linebreaks.
|
||||
c if c.is_whitespace() => NodeKind::Linebreak { justified: false },
|
||||
'+' => {
|
||||
self.s.expect(c);
|
||||
NodeKind::Linebreak { justified: true }
|
||||
}
|
||||
Some(c) if c.is_whitespace() => NodeKind::Linebreak,
|
||||
None => NodeKind::Linebreak,
|
||||
|
||||
// Just the backslash.
|
||||
_ => NodeKind::Text('\\'.into()),
|
||||
// Escapes.
|
||||
Some(c) => {
|
||||
self.s.expect(c);
|
||||
NodeKind::Escape(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(&mut self) -> NodeKind {
|
||||
fn hash(&mut self, start: usize) -> NodeKind {
|
||||
if self.s.at(is_id_start) {
|
||||
let read = self.s.eat_while(is_id_continue);
|
||||
match keyword(read) {
|
||||
@ -325,7 +306,7 @@ impl<'s> Tokens<'s> {
|
||||
None => NodeKind::Ident(read.into()),
|
||||
}
|
||||
} else {
|
||||
NodeKind::Text('#'.into())
|
||||
self.text(start)
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,19 +324,26 @@ impl<'s> Tokens<'s> {
|
||||
}
|
||||
}
|
||||
|
||||
fn numbering(&mut self, start: usize, c: char) -> NodeKind {
|
||||
let number = if c != '.' {
|
||||
self.s.eat_while(char::is_ascii_digit);
|
||||
let read = self.s.from(start);
|
||||
if !self.s.eat_if('.') {
|
||||
return NodeKind::Text(self.s.from(start).into());
|
||||
}
|
||||
read.parse().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
fn in_word(&self) -> bool {
|
||||
let alphanumeric = |c: Option<char>| c.map_or(false, |c| c.is_alphanumeric());
|
||||
let prev = self.s.scout(-2);
|
||||
let next = self.s.peek();
|
||||
alphanumeric(prev) && alphanumeric(next)
|
||||
}
|
||||
|
||||
NodeKind::EnumNumbering(number)
|
||||
fn link(&mut self, start: usize) -> NodeKind {
|
||||
#[rustfmt::skip]
|
||||
self.s.eat_while(|c: char| matches!(c,
|
||||
| '0' ..= '9'
|
||||
| 'a' ..= 'z'
|
||||
| 'A' ..= 'Z'
|
||||
| '~' | '/' | '%' | '?' | '#' | '&' | '+' | '='
|
||||
| '\'' | '.' | ',' | ';'
|
||||
));
|
||||
if self.s.scout(-1) == Some('.') {
|
||||
self.s.uneat();
|
||||
}
|
||||
NodeKind::Link(self.s.from(start).into())
|
||||
}
|
||||
|
||||
fn raw(&mut self) -> NodeKind {
|
||||
@ -376,7 +364,6 @@ impl<'s> Tokens<'s> {
|
||||
}
|
||||
|
||||
let start = self.s.cursor();
|
||||
|
||||
let mut found = 0;
|
||||
while found < backticks {
|
||||
match self.s.eat() {
|
||||
@ -394,10 +381,9 @@ impl<'s> Tokens<'s> {
|
||||
self.s.get(start .. end),
|
||||
)))
|
||||
} else {
|
||||
self.terminated = false;
|
||||
let remaining = backticks - found;
|
||||
let noun = if remaining == 1 { "backtick" } else { "backticks" };
|
||||
|
||||
self.terminated = false;
|
||||
NodeKind::Error(
|
||||
SpanPos::End,
|
||||
if found == 0 {
|
||||
@ -410,53 +396,40 @@ impl<'s> Tokens<'s> {
|
||||
}
|
||||
|
||||
fn math(&mut self) -> NodeKind {
|
||||
let mut display = false;
|
||||
if self.s.eat_if('[') {
|
||||
display = true;
|
||||
}
|
||||
|
||||
let start = self.s.cursor();
|
||||
|
||||
let mut escaped = false;
|
||||
let mut dollar = !display;
|
||||
|
||||
let terminated = loop {
|
||||
match self.s.eat() {
|
||||
Some('$') if !escaped && dollar => break true,
|
||||
Some(']') if !escaped => dollar = true,
|
||||
Some(c) => {
|
||||
dollar = !display;
|
||||
escaped = c == '\\' && !escaped;
|
||||
}
|
||||
None => break false,
|
||||
let formula = self.s.eat_until(|c| {
|
||||
if c == '$' && !escaped {
|
||||
true
|
||||
} else {
|
||||
escaped = c == '\\' && !escaped;
|
||||
false
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
let end = self.s.cursor()
|
||||
- match (terminated, display) {
|
||||
(false, _) => 0,
|
||||
(true, false) => 1,
|
||||
(true, true) => 2,
|
||||
};
|
||||
let display = formula.len() >= 2
|
||||
&& formula.starts_with(char::is_whitespace)
|
||||
&& formula.ends_with(char::is_whitespace);
|
||||
|
||||
if terminated {
|
||||
NodeKind::Math(Arc::new(MathNode {
|
||||
formula: self.s.get(start .. end).into(),
|
||||
display,
|
||||
}))
|
||||
if self.s.eat_if('$') {
|
||||
NodeKind::Math(Arc::new(MathNode { formula: formula.into(), display }))
|
||||
} else {
|
||||
self.terminated = false;
|
||||
NodeKind::Error(
|
||||
SpanPos::End,
|
||||
if !display || (!escaped && dollar) {
|
||||
"expected closing dollar sign".into()
|
||||
} else {
|
||||
"expected closing bracket and dollar sign".into()
|
||||
},
|
||||
)
|
||||
NodeKind::Error(SpanPos::End, "expected dollar sign".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn numbering(&mut self, start: usize) -> NodeKind {
|
||||
self.s.eat_while(char::is_ascii_digit);
|
||||
let read = self.s.from(start);
|
||||
if self.s.eat_if('.') {
|
||||
if let Ok(number) = read.parse() {
|
||||
return NodeKind::EnumNumbering(number);
|
||||
}
|
||||
}
|
||||
|
||||
self.text(start)
|
||||
}
|
||||
|
||||
fn label(&mut self) -> NodeKind {
|
||||
let label = self.s.eat_while(is_id_continue);
|
||||
if self.s.eat_if('>') {
|
||||
@ -471,12 +444,59 @@ impl<'s> Tokens<'s> {
|
||||
}
|
||||
}
|
||||
|
||||
fn reference(&mut self) -> NodeKind {
|
||||
fn reference(&mut self, start: usize) -> NodeKind {
|
||||
let label = self.s.eat_while(is_id_continue);
|
||||
if !label.is_empty() {
|
||||
NodeKind::Ref(label.into())
|
||||
} else {
|
||||
NodeKind::Error(SpanPos::Full, "label cannot be empty".into())
|
||||
self.text(start)
|
||||
}
|
||||
}
|
||||
|
||||
fn code(&mut self, start: usize, c: char) -> NodeKind {
|
||||
match c {
|
||||
// Parentheses.
|
||||
'(' => NodeKind::LeftParen,
|
||||
')' => NodeKind::RightParen,
|
||||
|
||||
// Two-char operators.
|
||||
'=' if self.s.eat_if('=') => NodeKind::EqEq,
|
||||
'!' if self.s.eat_if('=') => NodeKind::ExclEq,
|
||||
'<' if self.s.eat_if('=') => NodeKind::LtEq,
|
||||
'>' if self.s.eat_if('=') => NodeKind::GtEq,
|
||||
'+' if self.s.eat_if('=') => NodeKind::PlusEq,
|
||||
'-' if self.s.eat_if('=') => NodeKind::HyphEq,
|
||||
'*' if self.s.eat_if('=') => NodeKind::StarEq,
|
||||
'/' if self.s.eat_if('=') => NodeKind::SlashEq,
|
||||
'.' if self.s.eat_if('.') => NodeKind::Dots,
|
||||
'=' if self.s.eat_if('>') => NodeKind::Arrow,
|
||||
|
||||
// Single-char operators.
|
||||
',' => NodeKind::Comma,
|
||||
';' => NodeKind::Semicolon,
|
||||
':' => NodeKind::Colon,
|
||||
'+' => NodeKind::Plus,
|
||||
'-' => NodeKind::Minus,
|
||||
'*' => NodeKind::Star,
|
||||
'/' => NodeKind::Slash,
|
||||
'=' => NodeKind::Eq,
|
||||
'<' => NodeKind::Lt,
|
||||
'>' => NodeKind::Gt,
|
||||
'.' if !self.s.at(char::is_ascii_digit) => NodeKind::Dot,
|
||||
|
||||
// Identifiers.
|
||||
c if is_id_start(c) => self.ident(start),
|
||||
|
||||
// Numbers.
|
||||
c if c.is_ascii_digit() || (c == '.' && self.s.at(char::is_ascii_digit)) => {
|
||||
self.number(start, c)
|
||||
}
|
||||
|
||||
// Strings.
|
||||
'"' => self.string(),
|
||||
|
||||
// Invalid token.
|
||||
_ => NodeKind::Unknown(self.s.from(start).into()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,18 +563,18 @@ impl<'s> Tokens<'s> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn string(&mut self) -> NodeKind {
|
||||
let mut escaped = false;
|
||||
let string = resolve_string(self.s.eat_until(|c| {
|
||||
let verbatim = self.s.eat_until(|c| {
|
||||
if c == '"' && !escaped {
|
||||
true
|
||||
} else {
|
||||
escaped = c == '\\' && !escaped;
|
||||
false
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
let string = resolve_string(verbatim);
|
||||
if self.s.eat_if('"') {
|
||||
NodeKind::Str(string)
|
||||
} else {
|
||||
@ -562,56 +582,6 @@ impl<'s> Tokens<'s> {
|
||||
NodeKind::Error(SpanPos::End, "expected quote".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn line_comment(&mut self) -> NodeKind {
|
||||
self.s.eat_until(is_newline);
|
||||
if self.s.peek().is_none() {
|
||||
self.terminated = false;
|
||||
}
|
||||
NodeKind::LineComment
|
||||
}
|
||||
|
||||
fn block_comment(&mut self) -> NodeKind {
|
||||
let mut state = '_';
|
||||
let mut depth = 1;
|
||||
self.terminated = false;
|
||||
|
||||
// Find the first `*/` that does not correspond to a nested `/*`.
|
||||
while let Some(c) = self.s.eat() {
|
||||
state = match (state, c) {
|
||||
('*', '/') => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
self.terminated = true;
|
||||
break;
|
||||
}
|
||||
'_'
|
||||
}
|
||||
('/', '*') => {
|
||||
depth += 1;
|
||||
'_'
|
||||
}
|
||||
('/', '/') => {
|
||||
self.line_comment();
|
||||
'_'
|
||||
}
|
||||
_ => c,
|
||||
}
|
||||
}
|
||||
|
||||
NodeKind::BlockComment
|
||||
}
|
||||
|
||||
fn in_word(&self) -> bool {
|
||||
let alphanumeric = |c: Option<char>| c.map_or(false, |c| c.is_alphanumeric());
|
||||
let prev = self.s.scout(-2);
|
||||
let next = self.s.peek();
|
||||
alphanumeric(prev) && alphanumeric(next)
|
||||
}
|
||||
|
||||
fn maybe_in_url(&self) -> bool {
|
||||
self.mode == TokenMode::Markup && self.s.before().ends_with(":/")
|
||||
}
|
||||
}
|
||||
|
||||
fn keyword(ident: &str) -> Option<NodeKind> {
|
||||
@ -872,14 +842,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_tokenize_text() {
|
||||
// Test basic text.
|
||||
t!(Markup[" /"]: "hello" => Text("hello"));
|
||||
t!(Markup[" /"]: "hello-world" => Text("hello-world"));
|
||||
t!(Markup[" /"]: "hello" => Text("hello"));
|
||||
t!(Markup[" /"]: "reha-world" => Text("reha-world"));
|
||||
|
||||
// Test code symbols in text.
|
||||
t!(Markup[" /"]: "a():\"b" => Text("a():"), Quote { double: true }, Text("b"));
|
||||
t!(Markup[" /"]: ";:,|/+" => Text(";:,|"), Text("/+"));
|
||||
t!(Markup[" /"]: "a():\"b" => Text("a()"), Colon, Quote { double: true }, Text("b"));
|
||||
t!(Markup[" /"]: ";,|/+" => Text(";,|/+"));
|
||||
t!(Markup[" /"]: "=-a" => Eq, Minus, Text("a"));
|
||||
t!(Markup[" "]: "#123" => Text("#"), Text("123"));
|
||||
t!(Markup[" "]: "#123" => Text("#123"));
|
||||
|
||||
// Test text ends.
|
||||
t!(Markup[""]: "hello " => Text("hello"), Space(0));
|
||||
@ -904,11 +874,9 @@ mod tests {
|
||||
t!(Markup: r"\`" => Escape('`'));
|
||||
t!(Markup: r"\$" => Escape('$'));
|
||||
t!(Markup: r"\#" => Escape('#'));
|
||||
|
||||
// Test unescapable symbols.
|
||||
t!(Markup[" /"]: r"\a" => Text(r"\"), Text("a"));
|
||||
t!(Markup[" /"]: r"\u" => Text(r"\"), Text("u"));
|
||||
t!(Markup[" /"]: r"\1" => Text(r"\"), Text("1"));
|
||||
t!(Markup: r"\a" => Escape('a'));
|
||||
t!(Markup: r"\u" => Escape('u'));
|
||||
t!(Markup: r"\1" => Escape('1'));
|
||||
|
||||
// Test basic unicode escapes.
|
||||
t!(Markup: r"\u{}" => Error(Full, "invalid unicode escape sequence"));
|
||||
@ -930,16 +898,15 @@ mod tests {
|
||||
t!(Markup: "_" => Underscore);
|
||||
t!(Markup[""]: "===" => Eq, Eq, Eq);
|
||||
t!(Markup["a1/"]: "= " => Eq, Space(0));
|
||||
t!(Markup[" "]: r"\" => Linebreak { justified: false });
|
||||
t!(Markup[" "]: r"\+" => Linebreak { justified: true });
|
||||
t!(Markup[" "]: r"\" => Linebreak);
|
||||
t!(Markup: "~" => NonBreakingSpace);
|
||||
t!(Markup["a1/"]: "-?" => Shy);
|
||||
t!(Markup["a "]: r"a--" => Text("a"), EnDash);
|
||||
t!(Markup["a1/"]: "- " => Minus, Space(0));
|
||||
t!(Markup[" "]: "." => EnumNumbering(None));
|
||||
t!(Markup[" "]: "1." => EnumNumbering(Some(1)));
|
||||
t!(Markup[" "]: "1.a" => EnumNumbering(Some(1)), Text("a"));
|
||||
t!(Markup[" /"]: "a1." => Text("a1"), EnumNumbering(None));
|
||||
t!(Markup[" "]: "+" => Plus);
|
||||
t!(Markup[" "]: "1." => EnumNumbering(1));
|
||||
t!(Markup[" "]: "1.a" => EnumNumbering(1), Text("a"));
|
||||
t!(Markup[" /"]: "a1." => Text("a1."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -995,7 +962,7 @@ mod tests {
|
||||
for (s, t) in list.clone() {
|
||||
t!(Markup[" "]: format!("#{}", s) => t);
|
||||
t!(Markup[" "]: format!("#{0}#{0}", s) => t, t);
|
||||
t!(Markup[" /"]: format!("# {}", s) => Text("#"), Space(0), Text(s));
|
||||
t!(Markup[" /"]: format!("# {}", s) => Text(&format!("# {s}")));
|
||||
}
|
||||
|
||||
for (s, t) in list {
|
||||
@ -1037,18 +1004,16 @@ mod tests {
|
||||
t!(Markup: "$$" => Math("", false));
|
||||
t!(Markup: "$x$" => Math("x", false));
|
||||
t!(Markup: r"$\\$" => Math(r"\\", false));
|
||||
t!(Markup: "$[x + y]$" => Math("x + y", true));
|
||||
t!(Markup: r"$[\\]$" => Math(r"\\", true));
|
||||
t!(Markup: r"$[\\]$" => Math(r"[\\]", false));
|
||||
t!(Markup: "$ x + y $" => Math(" x + y ", true));
|
||||
|
||||
// Test unterminated.
|
||||
t!(Markup[""]: "$x" => Error(End, "expected closing dollar sign"));
|
||||
t!(Markup[""]: "$[x" => Error(End, "expected closing bracket and dollar sign"));
|
||||
t!(Markup[""]: "$[x]\n$" => Error(End, "expected closing bracket and dollar sign"));
|
||||
t!(Markup[""]: "$x" => Error(End, "expected dollar sign"));
|
||||
t!(Markup[""]: "$[x]\n" => Error(End, "expected dollar sign"));
|
||||
|
||||
// Test escape sequences.
|
||||
t!(Markup: r"$\$x$" => Math(r"\$x", false));
|
||||
t!(Markup: r"$[\\\]$]$" => Math(r"\\\]$", true));
|
||||
t!(Markup[""]: r"$[ ]\\$" => Error(End, "expected closing bracket and dollar sign"));
|
||||
t!(Markup: r"$\$x$" => Math(r"\$x", false));
|
||||
t!(Markup: r"$\ \$ $" => Math(r"\ \$ ", false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -63,9 +63,7 @@ impl Markup {
|
||||
self.0.children().filter_map(|node| match node.kind() {
|
||||
NodeKind::Space { newlines: (2 ..) } => Some(MarkupNode::Parbreak),
|
||||
NodeKind::Space { .. } => Some(MarkupNode::Space),
|
||||
&NodeKind::Linebreak { justified } => {
|
||||
Some(MarkupNode::Linebreak { justified })
|
||||
}
|
||||
NodeKind::Linebreak => Some(MarkupNode::Linebreak),
|
||||
NodeKind::Text(s) => Some(MarkupNode::Text(s.clone())),
|
||||
NodeKind::Escape(c) => Some(MarkupNode::Text((*c).into())),
|
||||
NodeKind::NonBreakingSpace => Some(MarkupNode::Text('\u{00A0}'.into())),
|
||||
@ -76,6 +74,7 @@ impl Markup {
|
||||
&NodeKind::Quote { double } => Some(MarkupNode::Quote { double }),
|
||||
NodeKind::Strong => node.cast().map(MarkupNode::Strong),
|
||||
NodeKind::Emph => node.cast().map(MarkupNode::Emph),
|
||||
NodeKind::Link(url) => Some(MarkupNode::Link(url.clone())),
|
||||
NodeKind::Raw(raw) => Some(MarkupNode::Raw(raw.as_ref().clone())),
|
||||
NodeKind::Math(math) => Some(MarkupNode::Math(Spanned::new(
|
||||
math.as_ref().clone(),
|
||||
@ -84,6 +83,7 @@ impl Markup {
|
||||
NodeKind::Heading => node.cast().map(MarkupNode::Heading),
|
||||
NodeKind::List => node.cast().map(MarkupNode::List),
|
||||
NodeKind::Enum => node.cast().map(MarkupNode::Enum),
|
||||
NodeKind::Desc => node.cast().map(MarkupNode::Desc),
|
||||
NodeKind::Label(v) => Some(MarkupNode::Label(v.clone())),
|
||||
NodeKind::Ref(v) => Some(MarkupNode::Ref(v.clone())),
|
||||
_ => node.cast().map(MarkupNode::Expr),
|
||||
@ -96,8 +96,8 @@ impl Markup {
|
||||
pub enum MarkupNode {
|
||||
/// Whitespace containing less than two newlines.
|
||||
Space,
|
||||
/// A forced line break: `\` or `\+` if justified.
|
||||
Linebreak { justified: bool },
|
||||
/// A forced line break.
|
||||
Linebreak,
|
||||
/// A paragraph break: Two or more newlines.
|
||||
Parbreak,
|
||||
/// Plain text.
|
||||
@ -108,6 +108,8 @@ pub enum MarkupNode {
|
||||
Strong(StrongNode),
|
||||
/// Emphasized content: `_Emphasized_`.
|
||||
Emph(EmphNode),
|
||||
/// A hyperlink.
|
||||
Link(EcoString),
|
||||
/// A raw block with optional syntax highlighting: `` `...` ``.
|
||||
Raw(RawNode),
|
||||
/// A math formula: `$a^2 = b^2 + c^2$`.
|
||||
@ -116,8 +118,10 @@ pub enum MarkupNode {
|
||||
Heading(HeadingNode),
|
||||
/// An item in an unordered list: `- ...`.
|
||||
List(ListNode),
|
||||
/// An item in an enumeration (ordered list): `1. ...`.
|
||||
/// An item in an enumeration (ordered list): `+ ...` or `1. ...`.
|
||||
Enum(EnumNode),
|
||||
/// An item in a description list: `/ Term: Details.
|
||||
Desc(DescNode),
|
||||
/// A label.
|
||||
Label(EcoString),
|
||||
/// A reference.
|
||||
@ -170,8 +174,8 @@ pub struct RawNode {
|
||||
pub struct MathNode {
|
||||
/// The formula between the dollars / brackets.
|
||||
pub formula: EcoString,
|
||||
/// Whether the formula is display-level, that is, it is surrounded by
|
||||
/// `$[..]$`.
|
||||
/// Whether the formula is display-level, that is, it contains whitespace
|
||||
/// after the starting dollar sign and before the ending dollar sign.
|
||||
pub display: bool,
|
||||
}
|
||||
|
||||
@ -205,7 +209,7 @@ node! {
|
||||
impl ListNode {
|
||||
/// The contents of the list item.
|
||||
pub fn body(&self) -> Markup {
|
||||
self.0.cast_first_child().expect("list node is missing body")
|
||||
self.0.cast_first_child().expect("list item is missing body")
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,18 +221,36 @@ node! {
|
||||
impl EnumNode {
|
||||
/// The contents of the list item.
|
||||
pub fn body(&self) -> Markup {
|
||||
self.0.cast_first_child().expect("enum node is missing body")
|
||||
self.0.cast_first_child().expect("enum item is missing body")
|
||||
}
|
||||
|
||||
/// The number, if any.
|
||||
pub fn number(&self) -> Option<usize> {
|
||||
self.0.children().find_map(|node| match node.kind() {
|
||||
NodeKind::EnumNumbering(num) => Some(*num),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
node! {
|
||||
/// An item in a description list: `/ Term: Details.
|
||||
DescNode: Desc
|
||||
}
|
||||
|
||||
impl DescNode {
|
||||
/// The term described by the list item.
|
||||
pub fn term(&self) -> Markup {
|
||||
self.0
|
||||
.children()
|
||||
.find_map(|node| match node.kind() {
|
||||
NodeKind::EnumNumbering(num) => Some(*num),
|
||||
_ => None,
|
||||
})
|
||||
.expect("enum node is missing number")
|
||||
.cast_first_child()
|
||||
.expect("description list item is missing term")
|
||||
}
|
||||
|
||||
/// The description of the term.
|
||||
pub fn body(&self) -> Markup {
|
||||
self.0
|
||||
.cast_last_child()
|
||||
.expect("description list item is missing body")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,12 +147,12 @@ pub fn highlight_pre(text: &str, mode: TokenMode, theme: &Theme) -> String {
|
||||
/// The syntax highlighting category of a node.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Category {
|
||||
/// A line or block comment.
|
||||
Comment,
|
||||
/// Any kind of bracket, parenthesis or brace.
|
||||
Bracket,
|
||||
/// Punctuation in code.
|
||||
Punctuation,
|
||||
/// A line or block comment.
|
||||
Comment,
|
||||
/// An easily typable shortcut to a unicode codepoint.
|
||||
Shortcut,
|
||||
/// An escape sequence.
|
||||
@ -161,14 +161,18 @@ pub enum Category {
|
||||
Strong,
|
||||
/// Emphasized text.
|
||||
Emph,
|
||||
/// A hyperlink.
|
||||
Link,
|
||||
/// Raw text or code.
|
||||
Raw,
|
||||
/// A math formula.
|
||||
Math,
|
||||
/// A section heading.
|
||||
Heading,
|
||||
/// A list or enumeration.
|
||||
/// A symbol of a list, enumeration, or description list.
|
||||
List,
|
||||
/// A term in a description list.
|
||||
Term,
|
||||
/// A label.
|
||||
Label,
|
||||
/// A reference.
|
||||
@ -204,66 +208,74 @@ impl Category {
|
||||
i: usize,
|
||||
) -> Option<Category> {
|
||||
match child.kind() {
|
||||
NodeKind::LineComment => Some(Category::Comment),
|
||||
NodeKind::BlockComment => Some(Category::Comment),
|
||||
NodeKind::LeftBrace => Some(Category::Bracket),
|
||||
NodeKind::RightBrace => Some(Category::Bracket),
|
||||
NodeKind::LeftBracket => Some(Category::Bracket),
|
||||
NodeKind::RightBracket => Some(Category::Bracket),
|
||||
NodeKind::LeftParen => Some(Category::Bracket),
|
||||
NodeKind::RightParen => Some(Category::Bracket),
|
||||
NodeKind::Comma => Some(Category::Punctuation),
|
||||
NodeKind::Semicolon => Some(Category::Punctuation),
|
||||
NodeKind::Colon => Some(Category::Punctuation),
|
||||
NodeKind::Dot => Some(Category::Punctuation),
|
||||
NodeKind::LineComment => Some(Category::Comment),
|
||||
NodeKind::BlockComment => Some(Category::Comment),
|
||||
|
||||
NodeKind::Markup { .. } => match parent.kind() {
|
||||
NodeKind::Desc
|
||||
if parent
|
||||
.children()
|
||||
.take_while(|child| child.kind() != &NodeKind::Colon)
|
||||
.find(|c| matches!(c.kind(), NodeKind::Markup { .. }))
|
||||
.map_or(false, |ident| std::ptr::eq(ident, child)) =>
|
||||
{
|
||||
Some(Category::Term)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
NodeKind::Space { .. } => None,
|
||||
NodeKind::Linebreak { .. } => Some(Category::Shortcut),
|
||||
NodeKind::Text(_) => None,
|
||||
NodeKind::Escape(_) => Some(Category::Escape),
|
||||
NodeKind::NonBreakingSpace => Some(Category::Shortcut),
|
||||
NodeKind::Shy => Some(Category::Shortcut),
|
||||
NodeKind::EnDash => Some(Category::Shortcut),
|
||||
NodeKind::EmDash => Some(Category::Shortcut),
|
||||
NodeKind::Ellipsis => Some(Category::Shortcut),
|
||||
NodeKind::Escape(_) => Some(Category::Escape),
|
||||
NodeKind::Strong => Some(Category::Strong),
|
||||
NodeKind::Emph => Some(Category::Emph),
|
||||
NodeKind::Raw(_) => Some(Category::Raw),
|
||||
NodeKind::Math(_) => Some(Category::Math),
|
||||
NodeKind::Heading => Some(Category::Heading),
|
||||
NodeKind::Minus => match parent.kind() {
|
||||
NodeKind::List => Some(Category::List),
|
||||
_ => Some(Category::Operator),
|
||||
},
|
||||
NodeKind::EnumNumbering(_) => Some(Category::List),
|
||||
NodeKind::Label(_) => Some(Category::Label),
|
||||
NodeKind::Ref(_) => Some(Category::Ref),
|
||||
NodeKind::Not => Some(Category::Keyword),
|
||||
NodeKind::And => Some(Category::Keyword),
|
||||
NodeKind::Or => Some(Category::Keyword),
|
||||
NodeKind::Let => Some(Category::Keyword),
|
||||
NodeKind::Set => Some(Category::Keyword),
|
||||
NodeKind::Show => Some(Category::Keyword),
|
||||
NodeKind::Wrap => Some(Category::Keyword),
|
||||
NodeKind::If => Some(Category::Keyword),
|
||||
NodeKind::Else => Some(Category::Keyword),
|
||||
NodeKind::While => Some(Category::Keyword),
|
||||
NodeKind::For => Some(Category::Keyword),
|
||||
NodeKind::In => Some(Category::Keyword),
|
||||
NodeKind::As => Some(Category::Keyword),
|
||||
NodeKind::Break => Some(Category::Keyword),
|
||||
NodeKind::Continue => Some(Category::Keyword),
|
||||
NodeKind::Return => Some(Category::Keyword),
|
||||
NodeKind::Import => Some(Category::Keyword),
|
||||
NodeKind::From => Some(Category::Keyword),
|
||||
NodeKind::Include => Some(Category::Keyword),
|
||||
NodeKind::Plus => Some(Category::Operator),
|
||||
NodeKind::Quote { .. } => None,
|
||||
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),
|
||||
NodeKind::StarEq => Some(Category::Operator),
|
||||
NodeKind::SlashEq => Some(Category::Operator),
|
||||
NodeKind::Underscore => None,
|
||||
NodeKind::Strong => Some(Category::Strong),
|
||||
NodeKind::Emph => Some(Category::Emph),
|
||||
NodeKind::Link(_) => Some(Category::Link),
|
||||
NodeKind::Raw(_) => Some(Category::Raw),
|
||||
NodeKind::Math(_) => Some(Category::Math),
|
||||
NodeKind::Heading => Some(Category::Heading),
|
||||
NodeKind::List => None,
|
||||
NodeKind::Enum => None,
|
||||
NodeKind::EnumNumbering(_) => Some(Category::List),
|
||||
NodeKind::Desc => None,
|
||||
NodeKind::Label(_) => Some(Category::Label),
|
||||
NodeKind::Ref(_) => Some(Category::Ref),
|
||||
|
||||
NodeKind::Comma => Some(Category::Punctuation),
|
||||
NodeKind::Semicolon => Some(Category::Punctuation),
|
||||
NodeKind::Colon => match parent.kind() {
|
||||
NodeKind::Desc => Some(Category::Term),
|
||||
_ => Some(Category::Punctuation),
|
||||
},
|
||||
NodeKind::Plus => match parent.kind() {
|
||||
NodeKind::Enum => Some(Category::List),
|
||||
_ => Some(Category::Operator),
|
||||
},
|
||||
NodeKind::Minus => match parent.kind() {
|
||||
NodeKind::List => Some(Category::List),
|
||||
_ => Some(Category::Operator),
|
||||
},
|
||||
NodeKind::Slash => match parent.kind() {
|
||||
NodeKind::Desc => Some(Category::List),
|
||||
_ => Some(Category::Operator),
|
||||
},
|
||||
NodeKind::Dot => Some(Category::Punctuation),
|
||||
NodeKind::Eq => match parent.kind() {
|
||||
NodeKind::Heading => None,
|
||||
_ => Some(Category::Operator),
|
||||
@ -274,10 +286,34 @@ impl Category {
|
||||
NodeKind::LtEq => Some(Category::Operator),
|
||||
NodeKind::Gt => Some(Category::Operator),
|
||||
NodeKind::GtEq => Some(Category::Operator),
|
||||
NodeKind::PlusEq => Some(Category::Operator),
|
||||
NodeKind::HyphEq => Some(Category::Operator),
|
||||
NodeKind::StarEq => Some(Category::Operator),
|
||||
NodeKind::SlashEq => Some(Category::Operator),
|
||||
NodeKind::Dots => Some(Category::Operator),
|
||||
NodeKind::Arrow => Some(Category::Operator),
|
||||
NodeKind::Not => Some(Category::Keyword),
|
||||
NodeKind::And => Some(Category::Keyword),
|
||||
NodeKind::Or => Some(Category::Keyword),
|
||||
NodeKind::None => Some(Category::None),
|
||||
NodeKind::Auto => Some(Category::Auto),
|
||||
NodeKind::Let => Some(Category::Keyword),
|
||||
NodeKind::Set => Some(Category::Keyword),
|
||||
NodeKind::Show => Some(Category::Keyword),
|
||||
NodeKind::Wrap => Some(Category::Keyword),
|
||||
NodeKind::If => Some(Category::Keyword),
|
||||
NodeKind::Else => Some(Category::Keyword),
|
||||
NodeKind::For => Some(Category::Keyword),
|
||||
NodeKind::In => Some(Category::Keyword),
|
||||
NodeKind::While => Some(Category::Keyword),
|
||||
NodeKind::Break => Some(Category::Keyword),
|
||||
NodeKind::Continue => Some(Category::Keyword),
|
||||
NodeKind::Return => Some(Category::Keyword),
|
||||
NodeKind::Import => Some(Category::Keyword),
|
||||
NodeKind::Include => Some(Category::Keyword),
|
||||
NodeKind::From => Some(Category::Keyword),
|
||||
NodeKind::As => Some(Category::Keyword),
|
||||
|
||||
NodeKind::Ident(_) => match parent.kind() {
|
||||
NodeKind::Markup { .. } => Some(Category::Interpolated),
|
||||
NodeKind::FuncCall => Some(Category::Function),
|
||||
@ -302,15 +338,6 @@ impl Category {
|
||||
NodeKind::Float(_) => Some(Category::Number),
|
||||
NodeKind::Numeric(_, _) => Some(Category::Number),
|
||||
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::Text(_) => None,
|
||||
NodeKind::Quote { .. } => None,
|
||||
NodeKind::List => None,
|
||||
NodeKind::Enum => None,
|
||||
NodeKind::CodeBlock => None,
|
||||
NodeKind::ContentBlock => None,
|
||||
NodeKind::GroupExpr => None,
|
||||
@ -341,6 +368,9 @@ impl Category {
|
||||
NodeKind::BreakExpr => None,
|
||||
NodeKind::ContinueExpr => None,
|
||||
NodeKind::ReturnExpr => None,
|
||||
|
||||
NodeKind::Error(_, _) => Some(Category::Invalid),
|
||||
NodeKind::Unknown(_) => Some(Category::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,10 +384,12 @@ impl Category {
|
||||
Self::Escape => "constant.character.escape.content.typst",
|
||||
Self::Strong => "markup.bold.typst",
|
||||
Self::Emph => "markup.italic.typst",
|
||||
Self::Link => "markup.underline.link.typst",
|
||||
Self::Raw => "markup.raw.typst",
|
||||
Self::Math => "string.other.math.typst",
|
||||
Self::Heading => "markup.heading.typst",
|
||||
Self::List => "markup.list.typst",
|
||||
Self::Term => "markup.bold.typst",
|
||||
Self::Label => "entity.name.label.typst",
|
||||
Self::Ref => "markup.other.reference.typst",
|
||||
Self::Keyword => "keyword.typst",
|
||||
|
@ -571,6 +571,14 @@ impl PartialEq for NodeData {
|
||||
/// the parser.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NodeKind {
|
||||
/// A line comment, two slashes followed by inner contents, terminated with
|
||||
/// a newline: `//<str>\n`.
|
||||
LineComment,
|
||||
/// A block comment, a slash and a star followed by inner contents,
|
||||
/// terminated with a star and a slash: `/*<str>*/`.
|
||||
///
|
||||
/// The comment can contain nested block comments.
|
||||
BlockComment,
|
||||
/// A left curly brace, starting a code block: `{`.
|
||||
LeftBrace,
|
||||
/// A right curly brace, terminating a code block: `}`.
|
||||
@ -585,23 +593,83 @@ pub enum NodeKind {
|
||||
/// A right round parenthesis, terminating a grouped expression, collection,
|
||||
/// argument or parameter list: `)`.
|
||||
RightParen,
|
||||
|
||||
/// Markup of which all lines must have a minimal indentation.
|
||||
///
|
||||
/// Notably, the number does not determine in which column the markup
|
||||
/// started, but to the right of which column all markup elements must be,
|
||||
/// so it is zero except for headings and lists.
|
||||
Markup { min_indent: usize },
|
||||
/// One or more whitespace characters. Single spaces are collapsed into text
|
||||
/// nodes if they would otherwise be surrounded by text nodes.
|
||||
///
|
||||
/// Also stores how many newlines are contained.
|
||||
Space { newlines: usize },
|
||||
/// A forced line break.
|
||||
Linebreak,
|
||||
/// Consecutive text without markup. While basic text with just single
|
||||
/// spaces is collapsed into a single node, certain symbols that could
|
||||
/// possibly be markup force text into multiple nodes.
|
||||
Text(EcoString),
|
||||
/// A slash and the letter "u" followed by a hexadecimal unicode entity
|
||||
/// enclosed in curly braces: `\u{1F5FA}`.
|
||||
Escape(char),
|
||||
/// A non-breaking space: `~`.
|
||||
NonBreakingSpace,
|
||||
/// A soft hyphen: `-?`.
|
||||
Shy,
|
||||
/// An en-dash: `--`.
|
||||
EnDash,
|
||||
/// An em-dash: `---`.
|
||||
EmDash,
|
||||
/// An ellipsis: `...`.
|
||||
Ellipsis,
|
||||
/// A smart quote: `'` or `"`.
|
||||
Quote { double: bool },
|
||||
/// The strong text toggle, multiplication operator, and wildcard import
|
||||
/// symbol: `*`.
|
||||
Star,
|
||||
/// Toggles emphasized text: `_`.
|
||||
Underscore,
|
||||
/// Strong content: `*Strong*`.
|
||||
Strong,
|
||||
/// Emphasized content: `_Emphasized_`.
|
||||
Emph,
|
||||
/// A hyperlink.
|
||||
Link(EcoString),
|
||||
/// A raw block with optional syntax highlighting: `` `...` ``.
|
||||
Raw(Arc<RawNode>),
|
||||
/// A math formula: `$x$`, `$[x^2]$`.
|
||||
Math(Arc<MathNode>),
|
||||
/// A section heading: `= Introduction`.
|
||||
Heading,
|
||||
/// An item in an unordered list: `- ...`.
|
||||
List,
|
||||
/// An item in an enumeration (ordered list): `+ ...` or `1. ...`.
|
||||
Enum,
|
||||
/// An explicit enumeration numbering: `23.`.
|
||||
EnumNumbering(usize),
|
||||
/// An item in a description list: `/ Term: Details.
|
||||
Desc,
|
||||
/// A label: `<label>`.
|
||||
Label(EcoString),
|
||||
/// A reference: `@label`.
|
||||
Ref(EcoString),
|
||||
|
||||
/// A comma separator in a sequence: `,`.
|
||||
Comma,
|
||||
/// A semicolon terminating an expression: `;`.
|
||||
Semicolon,
|
||||
/// A colon between name / key and value in a dictionary, argument or
|
||||
/// parameter list: `:`.
|
||||
/// parameter list, or between the term and body of a description list
|
||||
/// term: `:`.
|
||||
Colon,
|
||||
/// The unary plus and addition operator: `+`.
|
||||
/// The unary plus and addition operator, and start of enum items: `+`.
|
||||
Plus,
|
||||
/// The unary negation and subtraction operator: `-`.
|
||||
/// The unary negation and subtraction operator, and start of list
|
||||
/// items: `-`.
|
||||
Minus,
|
||||
/// The division operator: `/`.
|
||||
/// The division operator and start of description list items: `/`.
|
||||
Slash,
|
||||
/// A field access and method call operator: `.`.
|
||||
Dot,
|
||||
@ -627,16 +695,16 @@ pub enum NodeKind {
|
||||
StarEq,
|
||||
/// The divide-assign operator: `/=`.
|
||||
SlashEq,
|
||||
/// The spread operator: `..`.
|
||||
Dots,
|
||||
/// An arrow between a closure's parameters and body: `=>`.
|
||||
Arrow,
|
||||
/// The `not` operator.
|
||||
Not,
|
||||
/// The `and` operator.
|
||||
And,
|
||||
/// The `or` operator.
|
||||
Or,
|
||||
/// The spread operator: `..`.
|
||||
Dots,
|
||||
/// An arrow between a closure's parameters and body: `=>`.
|
||||
Arrow,
|
||||
/// The none literal: `none`.
|
||||
None,
|
||||
/// The auto literal: `auto`.
|
||||
@ -673,60 +741,7 @@ pub enum NodeKind {
|
||||
From,
|
||||
/// The `as` keyword.
|
||||
As,
|
||||
/// Markup of which all lines must have a minimal indentation.
|
||||
///
|
||||
/// Notably, the number does not determine in which column the markup
|
||||
/// started, but to the right of which column all markup elements must be,
|
||||
/// so it is zero except for headings and lists.
|
||||
Markup { min_indent: usize },
|
||||
/// One or more whitespace characters. Single spaces are collapsed into text
|
||||
/// nodes if they would otherwise be surrounded by text nodes.
|
||||
///
|
||||
/// Also stores how many newlines are contained.
|
||||
Space { newlines: usize },
|
||||
/// Consecutive text without markup. While basic text with just single
|
||||
/// spaces is collapsed into a single node, certain symbols that could
|
||||
/// possibly be markup force text into multiple nodes.
|
||||
Text(EcoString),
|
||||
/// A forced line break: `\` or `\+` if justified.
|
||||
Linebreak { justified: bool },
|
||||
/// A non-breaking space: `~`.
|
||||
NonBreakingSpace,
|
||||
/// A soft hyphen: `-?`.
|
||||
Shy,
|
||||
/// An en-dash: `--`.
|
||||
EnDash,
|
||||
/// An em-dash: `---`.
|
||||
EmDash,
|
||||
/// An ellipsis: `...`.
|
||||
Ellipsis,
|
||||
/// A smart quote: `'` or `"`.
|
||||
Quote { double: bool },
|
||||
/// A slash and the letter "u" followed by a hexadecimal unicode entity
|
||||
/// enclosed in curly braces: `\u{1F5FA}`.
|
||||
Escape(char),
|
||||
/// Strong content: `*Strong*`.
|
||||
Strong,
|
||||
/// Emphasized content: `_Emphasized_`.
|
||||
Emph,
|
||||
/// A raw block with optional syntax highlighting: `` `...` ``.
|
||||
Raw(Arc<RawNode>),
|
||||
/// A math formula: `$x$`, `$[x^2]$`.
|
||||
Math(Arc<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>),
|
||||
/// A label: `<label>`.
|
||||
Label(EcoString),
|
||||
/// A reference: `@label`.
|
||||
Ref(EcoString),
|
||||
|
||||
/// An identifier: `center`.
|
||||
Ident(EcoString),
|
||||
/// A boolean: `true`, `false`.
|
||||
@ -799,14 +814,7 @@ pub enum NodeKind {
|
||||
ContinueExpr,
|
||||
/// A return expression: `return x + 1`.
|
||||
ReturnExpr,
|
||||
/// A line comment, two slashes followed by inner contents, terminated with
|
||||
/// a newline: `//<str>\n`.
|
||||
LineComment,
|
||||
/// A block comment, a slash and a star followed by inner contents,
|
||||
/// terminated with a star and a slash: `/*<str>*/`.
|
||||
///
|
||||
/// The comment can contain nested block comments.
|
||||
BlockComment,
|
||||
|
||||
/// Tokens that appear in the wrong place.
|
||||
Error(SpanPos, EcoString),
|
||||
/// Unknown character sequences.
|
||||
@ -844,7 +852,7 @@ impl NodeKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether changes _inside_ this node are safely encapuslated, so that only
|
||||
/// Whether changes _inside_ this node are safely encapsulated, so that only
|
||||
/// this node must be reparsed.
|
||||
pub fn is_bounded(&self) -> bool {
|
||||
match self {
|
||||
@ -860,7 +868,6 @@ impl NodeKind {
|
||||
| Self::BlockComment
|
||||
| Self::Space { .. }
|
||||
| Self::Escape(_) => true,
|
||||
Self::Text(t) => t != "-" && !t.ends_with('.'),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -868,14 +875,43 @@ impl NodeKind {
|
||||
/// A human-readable name for the kind.
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::LineComment => "line comment",
|
||||
Self::BlockComment => "block comment",
|
||||
Self::LeftBrace => "opening brace",
|
||||
Self::RightBrace => "closing brace",
|
||||
Self::LeftBracket => "opening bracket",
|
||||
Self::RightBracket => "closing bracket",
|
||||
Self::LeftParen => "opening paren",
|
||||
Self::RightParen => "closing paren",
|
||||
|
||||
Self::Markup { .. } => "markup",
|
||||
Self::Space { newlines: (2 ..) } => "paragraph break",
|
||||
Self::Space { .. } => "space",
|
||||
Self::Linebreak => "linebreak",
|
||||
Self::Text(_) => "text",
|
||||
Self::Escape(_) => "escape sequence",
|
||||
Self::NonBreakingSpace => "non-breaking space",
|
||||
Self::Shy => "soft hyphen",
|
||||
Self::EnDash => "en dash",
|
||||
Self::EmDash => "em dash",
|
||||
Self::Ellipsis => "ellipsis",
|
||||
Self::Quote { double: false } => "single quote",
|
||||
Self::Quote { double: true } => "double quote",
|
||||
Self::Star => "star",
|
||||
Self::Underscore => "underscore",
|
||||
Self::Strong => "strong content",
|
||||
Self::Emph => "emphasized content",
|
||||
Self::Link(_) => "link",
|
||||
Self::Raw(_) => "raw block",
|
||||
Self::Math(_) => "math formula",
|
||||
Self::Heading => "heading",
|
||||
Self::List => "list item",
|
||||
Self::Enum => "enumeration item",
|
||||
Self::EnumNumbering(_) => "enumeration item numbering",
|
||||
Self::Desc => "description list item",
|
||||
Self::Label(_) => "label",
|
||||
Self::Ref(_) => "reference",
|
||||
|
||||
Self::Comma => "comma",
|
||||
Self::Semicolon => "semicolon",
|
||||
Self::Colon => "colon",
|
||||
@ -894,11 +930,11 @@ impl NodeKind {
|
||||
Self::HyphEq => "subtract-assign operator",
|
||||
Self::StarEq => "multiply-assign operator",
|
||||
Self::SlashEq => "divide-assign operator",
|
||||
Self::Dots => "dots",
|
||||
Self::Arrow => "arrow",
|
||||
Self::Not => "operator `not`",
|
||||
Self::And => "operator `and`",
|
||||
Self::Or => "operator `or`",
|
||||
Self::Dots => "dots",
|
||||
Self::Arrow => "arrow",
|
||||
Self::None => "`none`",
|
||||
Self::Auto => "`auto`",
|
||||
Self::Let => "keyword `let`",
|
||||
@ -909,7 +945,6 @@ impl NodeKind {
|
||||
Self::Else => "keyword `else`",
|
||||
Self::For => "keyword `for`",
|
||||
Self::In => "keyword `in`",
|
||||
Self::As => "keyword `as`",
|
||||
Self::While => "keyword `while`",
|
||||
Self::Break => "keyword `break`",
|
||||
Self::Continue => "keyword `continue`",
|
||||
@ -917,30 +952,8 @@ impl NodeKind {
|
||||
Self::Import => "keyword `import`",
|
||||
Self::Include => "keyword `include`",
|
||||
Self::From => "keyword `from`",
|
||||
Self::Markup { .. } => "markup",
|
||||
Self::Space { newlines: (2 ..) } => "paragraph break",
|
||||
Self::Space { .. } => "space",
|
||||
Self::Linebreak { justified: false } => "linebreak",
|
||||
Self::Linebreak { justified: true } => "justified linebreak",
|
||||
Self::Text(_) => "text",
|
||||
Self::NonBreakingSpace => "non-breaking space",
|
||||
Self::Shy => "soft hyphen",
|
||||
Self::EnDash => "en dash",
|
||||
Self::EmDash => "em dash",
|
||||
Self::Ellipsis => "ellipsis",
|
||||
Self::Quote { double: false } => "single quote",
|
||||
Self::Quote { double: true } => "double quote",
|
||||
Self::Escape(_) => "escape sequence",
|
||||
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::Label(_) => "label",
|
||||
Self::Ref(_) => "reference",
|
||||
Self::As => "keyword `as`",
|
||||
|
||||
Self::Ident(_) => "identifier",
|
||||
Self::Bool(_) => "boolean",
|
||||
Self::Int(_) => "integer",
|
||||
@ -977,8 +990,7 @@ impl NodeKind {
|
||||
Self::BreakExpr => "`break` expression",
|
||||
Self::ContinueExpr => "`continue` expression",
|
||||
Self::ReturnExpr => "`return` expression",
|
||||
Self::LineComment => "line comment",
|
||||
Self::BlockComment => "block comment",
|
||||
|
||||
Self::Error(_, _) => "parse error",
|
||||
Self::Unknown(text) => match text.as_str() {
|
||||
"*/" => "end of block comment",
|
||||
@ -998,14 +1010,41 @@ impl Hash for NodeKind {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
std::mem::discriminant(self).hash(state);
|
||||
match self {
|
||||
Self::LineComment => {}
|
||||
Self::BlockComment => {}
|
||||
Self::LeftBrace => {}
|
||||
Self::RightBrace => {}
|
||||
Self::LeftBracket => {}
|
||||
Self::RightBracket => {}
|
||||
Self::LeftParen => {}
|
||||
Self::RightParen => {}
|
||||
|
||||
Self::Markup { min_indent } => min_indent.hash(state),
|
||||
Self::Space { newlines } => newlines.hash(state),
|
||||
Self::Linebreak => {}
|
||||
Self::Text(s) => s.hash(state),
|
||||
Self::Escape(c) => c.hash(state),
|
||||
Self::NonBreakingSpace => {}
|
||||
Self::Shy => {}
|
||||
Self::EnDash => {}
|
||||
Self::EmDash => {}
|
||||
Self::Ellipsis => {}
|
||||
Self::Quote { double } => double.hash(state),
|
||||
Self::Star => {}
|
||||
Self::Underscore => {}
|
||||
Self::Strong => {}
|
||||
Self::Emph => {}
|
||||
Self::Link(link) => link.hash(state),
|
||||
Self::Raw(raw) => raw.hash(state),
|
||||
Self::Math(math) => math.hash(state),
|
||||
Self::Heading => {}
|
||||
Self::List => {}
|
||||
Self::Enum => {}
|
||||
Self::EnumNumbering(num) => num.hash(state),
|
||||
Self::Desc => {}
|
||||
Self::Label(c) => c.hash(state),
|
||||
Self::Ref(c) => c.hash(state),
|
||||
|
||||
Self::Comma => {}
|
||||
Self::Semicolon => {}
|
||||
Self::Colon => {}
|
||||
@ -1024,11 +1063,11 @@ impl Hash for NodeKind {
|
||||
Self::HyphEq => {}
|
||||
Self::StarEq => {}
|
||||
Self::SlashEq => {}
|
||||
Self::Dots => {}
|
||||
Self::Arrow => {}
|
||||
Self::Not => {}
|
||||
Self::And => {}
|
||||
Self::Or => {}
|
||||
Self::Dots => {}
|
||||
Self::Arrow => {}
|
||||
Self::None => {}
|
||||
Self::Auto => {}
|
||||
Self::Let => {}
|
||||
@ -1039,7 +1078,6 @@ impl Hash for NodeKind {
|
||||
Self::Else => {}
|
||||
Self::For => {}
|
||||
Self::In => {}
|
||||
Self::As => {}
|
||||
Self::While => {}
|
||||
Self::Break => {}
|
||||
Self::Continue => {}
|
||||
@ -1047,27 +1085,8 @@ impl Hash for NodeKind {
|
||||
Self::Import => {}
|
||||
Self::Include => {}
|
||||
Self::From => {}
|
||||
Self::Markup { min_indent } => min_indent.hash(state),
|
||||
Self::Space { newlines } => newlines.hash(state),
|
||||
Self::Linebreak { justified } => justified.hash(state),
|
||||
Self::Text(s) => s.hash(state),
|
||||
Self::NonBreakingSpace => {}
|
||||
Self::Shy => {}
|
||||
Self::EnDash => {}
|
||||
Self::EmDash => {}
|
||||
Self::Ellipsis => {}
|
||||
Self::Quote { double } => double.hash(state),
|
||||
Self::Escape(c) => c.hash(state),
|
||||
Self::Strong => {}
|
||||
Self::Emph => {}
|
||||
Self::Raw(raw) => raw.hash(state),
|
||||
Self::Math(math) => math.hash(state),
|
||||
Self::List => {}
|
||||
Self::Heading => {}
|
||||
Self::Enum => {}
|
||||
Self::EnumNumbering(num) => num.hash(state),
|
||||
Self::Label(c) => c.hash(state),
|
||||
Self::Ref(c) => c.hash(state),
|
||||
Self::As => {}
|
||||
|
||||
Self::Ident(v) => v.hash(state),
|
||||
Self::Bool(v) => v.hash(state),
|
||||
Self::Int(v) => v.hash(state),
|
||||
@ -1104,8 +1123,7 @@ impl Hash for NodeKind {
|
||||
Self::BreakExpr => {}
|
||||
Self::ContinueExpr => {}
|
||||
Self::ReturnExpr => {}
|
||||
Self::LineComment => {}
|
||||
Self::BlockComment => {}
|
||||
|
||||
Self::Error(pos, msg) => (pos, msg).hash(state),
|
||||
Self::Unknown(text) => text.hash(state),
|
||||
}
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 2.6 KiB After ![]() (image error) Size: 797 B ![]() ![]() |
BIN
tests/ref/structure/desc.png
Normal file
BIN
tests/ref/structure/desc.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 16 KiB |
Binary file not shown.
Before ![]() (image error) Size: 27 KiB After ![]() (image error) Size: 28 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 44 KiB After ![]() (image error) Size: 44 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 14 KiB After ![]() (image error) Size: 13 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 40 KiB After ![]() (image error) Size: 48 KiB ![]() ![]() |
@ -24,11 +24,6 @@ Still comment.
|
||||
|
||||
E
|
||||
|
||||
---
|
||||
// Line comments have a special case for URLs.
|
||||
https://example.com \
|
||||
https:/* block comments don't ... */
|
||||
|
||||
---
|
||||
// End should not appear without start.
|
||||
// Error: 7-9 unexpected end of block comment
|
||||
|
@ -5,15 +5,15 @@ The sum of $a$ and $b$ is $a + b$.
|
||||
|
||||
---
|
||||
We will show that:
|
||||
$[ a^2 + b^2 = c^2 ]$
|
||||
$ a^2 + b^2 = c^2 $
|
||||
|
||||
---
|
||||
Prove by induction:
|
||||
$[ \sum_{k=0}^n k = \frac{n(n+1)}{2} ]$
|
||||
$ \sum_{k=0}^n k = \frac{n(n+1)}{2} $
|
||||
|
||||
---
|
||||
// Test that blackboard style looks nice.
|
||||
$[ f: \mathbb{N} \rightarrow \mathbb{R} ]$
|
||||
$ f: \mathbb{N} \rightarrow \mathbb{R} $
|
||||
|
||||
---
|
||||
#set math(family: "IBM Plex Sans")
|
||||
@ -26,5 +26,5 @@ $a$
|
||||
$\sqrt{x$
|
||||
|
||||
---
|
||||
// Error: 2:1 expected closing bracket and dollar sign
|
||||
$[a
|
||||
// Error: 2:1 expected dollar sign
|
||||
$a
|
||||
|
48
tests/typ/structure/desc.typ
Normal file
48
tests/typ/structure/desc.typ
Normal file
@ -0,0 +1,48 @@
|
||||
// Test description lists.
|
||||
|
||||
---
|
||||
/
|
||||
No: list \
|
||||
/No: list
|
||||
|
||||
---
|
||||
// Test with constructor.
|
||||
#desc(
|
||||
(term: [One], body: [First]),
|
||||
(term: [Two], body: [Second]),
|
||||
)
|
||||
|
||||
---
|
||||
// Test joining.
|
||||
#for word in lorem(4).split().map(s => s.trim(".")) [
|
||||
/ #word: Latin stuff.
|
||||
]
|
||||
|
||||
---
|
||||
// Test multiline.
|
||||
#set text(8pt)
|
||||
|
||||
/ Fruit: A tasty, edible thing.
|
||||
/ Veggie:
|
||||
An important energy source
|
||||
for vegetarians.
|
||||
|
||||
---
|
||||
// Test style change.
|
||||
#set text(8pt)
|
||||
|
||||
/ First list: #lorem(4)
|
||||
#set desc(body-indent: 30pt)
|
||||
/ Second list: #lorem(4)
|
||||
|
||||
---
|
||||
// Test grid like show rule.
|
||||
#show it: desc as table(
|
||||
columns: 2,
|
||||
padding: 3pt,
|
||||
..it.items.map(item => (emph(item.term), item.body)).flatten(),
|
||||
)
|
||||
|
||||
/ A: One letter
|
||||
/ BB: Two letters
|
||||
/ CCC: Three letters
|
@ -1,4 +1,4 @@
|
||||
// Test enums.
|
||||
// Test enumerations.
|
||||
|
||||
---
|
||||
#enum[Embrace][Extend][Extinguish]
|
||||
@ -12,28 +12,28 @@
|
||||
---
|
||||
2. Second
|
||||
1. First
|
||||
. Indented
|
||||
+ Indented
|
||||
|
||||
---
|
||||
// Test automatic numbering in summed content.
|
||||
#for i in range(5) {
|
||||
[. #roman(1 + i)]
|
||||
[+ #roman(1 + i)]
|
||||
}
|
||||
|
||||
---
|
||||
// Test label pattern.
|
||||
#set enum(label: "~ A:")
|
||||
. First
|
||||
. Second
|
||||
1. First
|
||||
+ Second
|
||||
|
||||
#set enum(label: "(*)")
|
||||
. A
|
||||
. B
|
||||
. C
|
||||
+ A
|
||||
+ B
|
||||
+ C
|
||||
|
||||
#set enum(label: "i)")
|
||||
. A
|
||||
. B
|
||||
+ A
|
||||
+ B
|
||||
|
||||
---
|
||||
// Test label closure.
|
||||
@ -47,12 +47,13 @@
|
||||
|
||||
---
|
||||
#set enum(label: n => n > 1)
|
||||
. A
|
||||
. B
|
||||
+ A
|
||||
+ B
|
||||
|
||||
---
|
||||
// Lone dot is not a list.
|
||||
.
|
||||
// Lone plus is not an enum.
|
||||
+
|
||||
No enum
|
||||
|
||||
---
|
||||
// Error: 18-20 invalid pattern
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Test lists.
|
||||
// Test unordered lists.
|
||||
|
||||
---
|
||||
-
|
||||
|
@ -44,9 +44,9 @@ Hello *{x}*
|
||||
move(dy: -0.15em, image(path, width: 1em, height: 1em))
|
||||
})
|
||||
|
||||
. Monkey
|
||||
. Rhino
|
||||
. Tiger
|
||||
+ Monkey
|
||||
+ Rhino
|
||||
+ Tiger
|
||||
|
||||
---
|
||||
// Error: 11-25 set is only allowed directly in code and content blocks
|
||||
|
@ -2,15 +2,12 @@
|
||||
|
||||
---
|
||||
// Escapable symbols.
|
||||
\\ \/ \[ \] \{ \} \# \* \_ \
|
||||
\= \~ \` \$ \" \' \< \> \@
|
||||
\\ \/ \[ \] \{ \} \# \* \_ \+ \= \~ \
|
||||
\` \$ \" \' \< \> \@ \( \) \A
|
||||
|
||||
// No need to escape.
|
||||
( ) ;
|
||||
|
||||
// Unescapable.
|
||||
\a \: \; \( \)
|
||||
|
||||
// Escaped comments.
|
||||
\//
|
||||
\/\* \*\/
|
||||
|
@ -21,8 +21,8 @@ D
|
||||
|
||||
---
|
||||
// Test forced justification with justified break.
|
||||
A B C \+
|
||||
D E F \+
|
||||
A B C #linebreak(justify: true)
|
||||
D E F #linebreak(justify: true)
|
||||
|
||||
---
|
||||
// Test that justificating chinese text is at least a bit sensible.
|
||||
|
@ -30,7 +30,7 @@ Trailing break \ \
|
||||
---
|
||||
// Test justified breaks.
|
||||
#set par(justify: true)
|
||||
With a soft \+
|
||||
break you can force a break without #linebreak(justified: true)
|
||||
breaking justification. #linebreak(justified: false)
|
||||
With a soft #linebreak(justify: true)
|
||||
break you can force a break without #linebreak(justify: true)
|
||||
breaking justification. #linebreak(justify: false)
|
||||
Nice!
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Test hyperlinking.
|
||||
|
||||
---
|
||||
// Link without body.
|
||||
#link("https://example.com/")
|
||||
// Link syntax.
|
||||
https://example.com/
|
||||
|
||||
// Link with body.
|
||||
#link("https://typst.app/")[Some text text text]
|
||||
@ -10,10 +10,17 @@
|
||||
// With line break.
|
||||
This link appears #link("https://google.com/")[in the middle of] a paragraph.
|
||||
|
||||
// Prefix is trimmed.
|
||||
// Certain prefixes are trimmed when using the `link` function.
|
||||
Contact #link("mailto:hi@typst.app") or
|
||||
call #link("tel:123") for more information.
|
||||
|
||||
---
|
||||
// Test that the period is trimmed.
|
||||
https://a.b.?q=%10#. \
|
||||
Wahttp://link \
|
||||
Nohttps:\//link \
|
||||
Nohttp\://comment
|
||||
|
||||
---
|
||||
// Styled with underline and color.
|
||||
#set link(fill: rgb("283663"))
|
||||
|
@ -39,7 +39,7 @@ Hello
|
||||
fn main() {}
|
||||
```
|
||||
|
||||
$[ x + y = z ]$
|
||||
$ x + y = z $
|
||||
|
||||
- List
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user