summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/basics/heading.rs2
-rw-r--r--library/src/basics/list.rs36
-rw-r--r--library/src/basics/table.rs11
-rw-r--r--library/src/compute/calc.rs12
-rw-r--r--library/src/compute/create.rs26
-rw-r--r--library/src/compute/data.rs6
-rw-r--r--library/src/compute/foundations.rs8
-rw-r--r--library/src/compute/utility.rs7
-rw-r--r--library/src/layout/align.rs2
-rw-r--r--library/src/layout/columns.rs4
-rw-r--r--library/src/layout/container.rs8
-rw-r--r--library/src/layout/grid.rs25
-rw-r--r--library/src/layout/hide.rs2
-rw-r--r--library/src/layout/pad.rs2
-rw-r--r--library/src/layout/page.rs55
-rw-r--r--library/src/layout/par.rs19
-rw-r--r--library/src/layout/place.rs2
-rw-r--r--library/src/layout/repeat.rs2
-rw-r--r--library/src/layout/spacing.rs16
-rw-r--r--library/src/layout/stack.rs10
-rw-r--r--library/src/layout/transform.rs4
-rw-r--r--library/src/math/matrix.rs20
-rw-r--r--library/src/math/mod.rs20
-rw-r--r--library/src/math/style.rs16
-rw-r--r--library/src/meta/document.rs2
-rw-r--r--library/src/meta/link.rs2
-rw-r--r--library/src/meta/outline.rs2
-rw-r--r--library/src/meta/reference.rs2
-rw-r--r--library/src/prelude.rs10
-rw-r--r--library/src/text/deco.rs2
-rw-r--r--library/src/text/misc.rs17
-rw-r--r--library/src/text/mod.rs93
-rw-r--r--library/src/text/quotes.rs2
-rw-r--r--library/src/text/raw.rs2
-rw-r--r--library/src/text/shift.rs2
-rw-r--r--library/src/text/symbol.rs2
-rw-r--r--library/src/visualize/image.rs15
-rw-r--r--library/src/visualize/line.rs2
-rw-r--r--library/src/visualize/shape.rs6
-rw-r--r--macros/src/capable.rs (renamed from macros/src/capability.rs)4
-rw-r--r--macros/src/castable.rs229
-rw-r--r--macros/src/func.rs63
-rw-r--r--macros/src/lib.rs34
-rw-r--r--macros/src/node.rs46
-rw-r--r--src/diag.rs34
-rw-r--r--src/geom/mod.rs2
-rw-r--r--src/geom/smart.rs64
-rw-r--r--src/geom/stroke.rs1
-rw-r--r--src/ide/complete.rs689
-rw-r--r--src/ide/highlight.rs8
-rw-r--r--src/ide/tooltip.rs159
-rw-r--r--src/lib.rs2
-rw-r--r--src/model/cast.rs513
-rw-r--r--src/model/content.rs8
-rw-r--r--src/model/dict.rs23
-rw-r--r--src/model/func.rs59
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/ops.rs4
-rw-r--r--src/model/str.rs8
-rw-r--r--src/model/styles.rs3
-rw-r--r--src/model/value.rs8
-rw-r--r--src/syntax/ast.rs7
-rw-r--r--src/syntax/linked.rs92
-rw-r--r--tests/src/tests.rs4
-rw-r--r--tests/typ/basics/table.typ2
-rw-r--r--tests/typ/compiler/show-node.typ2
-rw-r--r--tests/typ/compute/utility.typ4
-rw-r--r--tests/typ/layout/columns.typ2
-rw-r--r--tests/typ/text/edge.typ4
-rw-r--r--tests/typ/text/features.typ8
-rw-r--r--tests/typ/text/font.typ2
-rw-r--r--tests/typ/visualize/shape-rect.typ8
72 files changed, 1827 insertions, 747 deletions
diff --git a/library/src/basics/heading.rs b/library/src/basics/heading.rs
index 80f93e8d..60415697 100644
--- a/library/src/basics/heading.rs
+++ b/library/src/basics/heading.rs
@@ -6,6 +6,8 @@ use crate::prelude::*;
use crate::text::{SpaceNode, TextNode, TextSize};
/// A section heading.
+///
+/// Tags: basics.
#[func]
#[capable(Prepare, Show, Finalize)]
#[derive(Debug, Hash)]
diff --git a/library/src/basics/list.rs b/library/src/basics/list.rs
index 0fa8f125..4c016128 100644
--- a/library/src/basics/list.rs
+++ b/library/src/basics/list.rs
@@ -4,6 +4,8 @@ use crate::prelude::*;
use crate::text::{SpaceNode, TextNode};
/// An unordered (bulleted) or ordered (numbered) list.
+///
+/// Tags: basics.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
@@ -24,7 +26,7 @@ pub type DescNode = ListNode<DESC>;
impl<const L: ListKind> ListNode<L> {
/// How the list is labelled.
#[property(referenced)]
- pub const LABEL: Label = Label::Default;
+ pub const LABEL: ListLabel = ListLabel::Default;
/// The indentation of each item's label.
#[property(resolve)]
pub const INDENT: Length = Length::zero();
@@ -199,10 +201,10 @@ pub struct DescItem {
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()?;
+ mut dict: Dict => {
+ let term: Content = dict.take("term")?.cast()?;
+ let body: Content = dict.take("body")?.cast()?;
+ dict.finish(&["term", "body"])?;
Self { term, body }
},
}
@@ -221,7 +223,7 @@ pub const DESC: ListKind = 2;
/// How to label a list or enumeration.
#[derive(Debug, Clone, Hash)]
-pub enum Label {
+pub enum ListLabel {
/// The default labelling.
Default,
/// A pattern with prefix, numbering, lower / upper case and suffix.
@@ -232,7 +234,7 @@ pub enum Label {
Func(Func, Span),
}
-impl Label {
+impl ListLabel {
/// Resolve the label based on the level.
pub fn resolve(
&self,
@@ -256,9 +258,12 @@ impl Label {
}
}
-impl Cast<Spanned<Value>> for Label {
+impl Cast<Spanned<Value>> for ListLabel {
fn is(value: &Spanned<Value>) -> bool {
- matches!(&value.v, Value::Content(_) | Value::Func(_))
+ matches!(
+ &value.v,
+ Value::None | Value::Str(_) | Value::Content(_) | Value::Func(_)
+ )
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
@@ -267,10 +272,15 @@ impl Cast<Spanned<Value>> for Label {
Value::Str(v) => Ok(Self::Pattern(v.parse()?)),
Value::Content(v) => Ok(Self::Content(v)),
Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => Err(format_eco!(
- "expected string, content or function, found {}",
- v.type_name(),
- )),
+ v => Self::error(v),
}
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Union(vec![
+ CastInfo::Type("string"),
+ CastInfo::Type("content"),
+ CastInfo::Type("function"),
+ ])
+ }
}
diff --git a/library/src/basics/table.rs b/library/src/basics/table.rs
index 5a4e8e81..10a9143f 100644
--- a/library/src/basics/table.rs
+++ b/library/src/basics/table.rs
@@ -2,6 +2,8 @@ use crate::layout::{GridNode, TrackSizing, TrackSizings};
use crate::prelude::*;
/// A table of items.
+///
+/// Tags: basics.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
@@ -125,9 +127,12 @@ impl<T: Cast> Cast<Spanned<Value>> for Celled<T> {
fn cast(value: Spanned<Value>) -> StrResult<Self> {
match value.v {
Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => T::cast(v)
- .map(Self::Value)
- .map_err(|msg| with_alternative(msg, "function")),
+ v if T::is(&v) => Ok(Self::Value(T::cast(v)?)),
+ v => Self::error(v),
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("function")
+ }
}
diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs
index 71e43e21..62d0a419 100644
--- a/library/src/compute/calc.rs
+++ b/library/src/compute/calc.rs
@@ -3,6 +3,8 @@ use std::cmp::Ordering;
use crate::prelude::*;
/// The absolute value of a numeric value.
+///
+/// Tags: calculate.
#[func]
pub fn abs(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect("numeric value")?;
@@ -20,12 +22,16 @@ pub fn abs(args: &mut Args) -> SourceResult<Value> {
}
/// The minimum of a sequence of values.
+///
+/// Tags: calculate.
#[func]
pub fn min(args: &mut Args) -> SourceResult<Value> {
minmax(args, Ordering::Less)
}
/// The maximum of a sequence of values.
+///
+/// Tags: calculate.
#[func]
pub fn max(args: &mut Args) -> SourceResult<Value> {
minmax(args, Ordering::Greater)
@@ -53,18 +59,24 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
}
/// Whether an integer is even.
+///
+/// Tags: calculate.
#[func]
pub fn even(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0))
}
/// Whether an integer is odd.
+///
+/// Tags: calculate.
#[func]
pub fn odd(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0))
}
/// The modulo of two numbers.
+///
+/// Tags: calculate.
#[func]
pub fn mod_(args: &mut Args) -> SourceResult<Value> {
let Spanned { v: v1, span: span1 } = args.expect("integer or float")?;
diff --git a/library/src/compute/create.rs b/library/src/compute/create.rs
index acd2e31f..a0eecfb8 100644
--- a/library/src/compute/create.rs
+++ b/library/src/compute/create.rs
@@ -5,6 +5,8 @@ use typst::model::Regex;
use crate::prelude::*;
/// Convert a value to an integer.
+///
+/// Tags: create.
#[func]
pub fn int(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect("value")?;
@@ -21,6 +23,8 @@ pub fn int(args: &mut Args) -> SourceResult<Value> {
}
/// Convert a value to a float.
+///
+/// Tags: create.
#[func]
pub fn float(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect("value")?;
@@ -36,6 +40,8 @@ pub fn float(args: &mut Args) -> SourceResult<Value> {
}
/// Create a grayscale color.
+///
+/// Tags: create.
#[func]
pub fn luma(args: &mut Args) -> SourceResult<Value> {
let Component(luma) = args.expect("gray component")?;
@@ -43,6 +49,8 @@ pub fn luma(args: &mut Args) -> SourceResult<Value> {
}
/// Create an RGB(A) color.
+///
+/// Tags: create.
#[func]
pub fn rgb(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Color(if let Some(string) = args.find::<Spanned<EcoString>>()? {
@@ -60,6 +68,8 @@ pub fn rgb(args: &mut Args) -> SourceResult<Value> {
}
/// Create a CMYK color.
+///
+/// Tags: create.
#[func]
pub fn cmyk(args: &mut Args) -> SourceResult<Value> {
let RatioComponent(c) = args.expect("cyan component")?;
@@ -74,12 +84,11 @@ struct Component(u8);
castable! {
Component,
- Expected: "integer or ratio",
- Value::Int(v) => match v {
+ v: i64 => match v {
0 ..= 255 => Self(v as u8),
_ => Err("must be between 0 and 255")?,
},
- Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) {
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
Self((v.get() * 255.0).round() as u8)
} else {
Err("must be between 0% and 100%")?
@@ -91,8 +100,7 @@ struct RatioComponent(u8);
castable! {
RatioComponent,
- Expected: "ratio",
- Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) {
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
Self((v.get() * 255.0).round() as u8)
} else {
Err("must be between 0% and 100%")?
@@ -100,6 +108,8 @@ castable! {
}
/// Convert a value to a string.
+///
+/// Tags: create.
#[func]
pub fn str(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect("value")?;
@@ -113,12 +123,16 @@ pub fn str(args: &mut Args) -> SourceResult<Value> {
}
/// Create a label from a string.
+///
+/// Tags: create.
#[func]
pub fn label(args: &mut Args) -> SourceResult<Value> {
Ok(Value::Label(Label(args.expect("string")?)))
}
/// Create a regular expression from a string.
+///
+/// Tags: create.
#[func]
pub fn regex(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?;
@@ -126,6 +140,8 @@ pub fn regex(args: &mut Args) -> SourceResult<Value> {
}
/// Create an array consisting of a sequence of numbers.
+///
+/// Tags: create.
#[func]
pub fn range(args: &mut Args) -> SourceResult<Value> {
let first = args.expect::<i64>("end")?;
diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs
index 57dce5c1..af545304 100644
--- a/library/src/compute/data.rs
+++ b/library/src/compute/data.rs
@@ -5,6 +5,8 @@ use typst::diag::{format_xml_like_error, FileError};
use crate::prelude::*;
/// Read structured data from a CSV file.
+///
+/// Tags: data-loading.
#[func]
pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } =
@@ -46,6 +48,8 @@ fn format_csv_error(error: csv::Error) -> String {
}
/// Read structured data from a JSON file.
+///
+/// Tags: data-loading.
#[func]
pub fn json(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } =
@@ -87,6 +91,8 @@ fn format_json_error(error: serde_json::Error) -> String {
}
/// Read structured data from an XML file.
+///
+/// Tags: data-loading.
#[func]
pub fn xml(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: path, span } =
diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs
index abe797dc..22d26553 100644
--- a/library/src/compute/foundations.rs
+++ b/library/src/compute/foundations.rs
@@ -5,18 +5,24 @@ use typst::model;
use typst::syntax::Source;
/// The name of a value's type.
+///
+/// Tags: foundations.
#[func]
pub fn type_(args: &mut Args) -> SourceResult<Value> {
Ok(args.expect::<Value>("value")?.type_name().into())
}
/// The string representation of a value.
+///
+/// Tags: foundations.
#[func]
pub fn repr(args: &mut Args) -> SourceResult<Value> {
Ok(args.expect::<Value>("value")?.repr().into())
}
/// Ensure that a condition is fulfilled.
+///
+/// Tags: foundations.
#[func]
pub fn assert(args: &mut Args) -> SourceResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<bool>>("condition")?;
@@ -27,6 +33,8 @@ pub fn assert(args: &mut Args) -> SourceResult<Value> {
}
/// Evaluate a string as Typst markup.
+///
+/// Tags: foundations.
#[func]
pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult<Value> {
let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?;
diff --git a/library/src/compute/utility.rs b/library/src/compute/utility.rs
index d48f794e..5a6534f4 100644
--- a/library/src/compute/utility.rs
+++ b/library/src/compute/utility.rs
@@ -4,6 +4,8 @@ use crate::prelude::*;
use crate::text::Case;
/// Create a blind text string.
+///
+/// Tags: utility.
#[func]
pub fn lorem(args: &mut Args) -> SourceResult<Value> {
let words: usize = args.expect("number of words")?;
@@ -11,6 +13,8 @@ pub fn lorem(args: &mut Args) -> SourceResult<Value> {
}
/// Apply a numbering pattern to a number.
+///
+/// Tags: utility.
#[func]
pub fn numbering(args: &mut Args) -> SourceResult<Value> {
let pattern = args.expect::<NumberingPattern>("pattern")?;
@@ -93,8 +97,7 @@ impl FromStr for NumberingPattern {
castable! {
NumberingPattern,
- Expected: "numbering pattern",
- Value::Str(s) => s.parse()?,
+ string: EcoString => string.parse()?,
}
/// Different kinds of numberings.
diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs
index 4fae3c3c..f00aeaf2 100644
--- a/library/src/layout/align.rs
+++ b/library/src/layout/align.rs
@@ -1,6 +1,8 @@
use crate::prelude::*;
/// Align content horizontally and vertically.
+///
+/// Tags: layout.
#[func]
#[capable]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs
index 0e29bc00..3bbf56e4 100644
--- a/library/src/layout/columns.rs
+++ b/library/src/layout/columns.rs
@@ -2,6 +2,8 @@ use crate::prelude::*;
use crate::text::TextNode;
/// Separate a region into multiple equally sized columns.
+///
+/// Tags: layout.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
@@ -103,6 +105,8 @@ impl Layout for ColumnsNode {
}
/// A column break.
+///
+/// Tags: layout.
#[func]
#[capable(Behave)]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs
index 85257715..d451bccf 100644
--- a/library/src/layout/container.rs
+++ b/library/src/layout/container.rs
@@ -2,6 +2,8 @@ use super::VNode;
use crate::prelude::*;
/// An inline-level container that sizes content.
+///
+/// Tags: layout.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
@@ -62,6 +64,8 @@ impl Layout for BoxNode {
impl Inline for BoxNode {}
/// A block-level container that places content into a separate flow.
+///
+/// Tags: layout.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
@@ -70,10 +74,10 @@ pub struct BlockNode(pub Content);
#[node]
impl BlockNode {
/// The spacing between the previous and this block.
- #[property(skip)]
+ #[property(reflect, skip)]
pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into());
/// The spacing between this and the following block.
- #[property(skip)]
+ #[property(reflect, skip)]
pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
/// Whether this block must stick to the following one.
#[property(skip)]
diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs
index 2a6bd4ff..e70210c0 100644
--- a/library/src/layout/grid.rs
+++ b/library/src/layout/grid.rs
@@ -3,6 +3,8 @@ use crate::prelude::*;
use super::Spacing;
/// Arrange content in a grid.
+///
+/// Tags: layout.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
@@ -85,17 +87,9 @@ pub struct TrackSizings(pub Vec<TrackSizing>);
castable! {
TrackSizings,
- Expected: "integer, auto, relative length, fraction, or array of the latter three",
- Value::Auto => Self(vec![TrackSizing::Auto]),
- Value::Length(v) => Self(vec![TrackSizing::Relative(v.into())]),
- Value::Ratio(v) => Self(vec![TrackSizing::Relative(v.into())]),
- Value::Relative(v) => Self(vec![TrackSizing::Relative(v)]),
- Value::Fraction(v) => Self(vec![TrackSizing::Fractional(v)]),
- Value::Int(v) => Self(vec![
- TrackSizing::Auto;
- Value::Int(v).cast::<NonZeroUsize>()?.get()
- ]),
- Value::Array(values) => Self(values
+ sizing: TrackSizing => Self(vec![sizing]),
+ count: NonZeroUsize => Self(vec![TrackSizing::Auto; count.get()]),
+ values: Array => Self(values
.into_iter()
.filter_map(|v| v.cast().ok())
.collect()),
@@ -103,12 +97,9 @@ castable! {
castable! {
TrackSizing,
- Expected: "auto, relative length, or fraction",
- Value::Auto => Self::Auto,
- Value::Length(v) => Self::Relative(v.into()),
- Value::Ratio(v) => Self::Relative(v.into()),
- Value::Relative(v) => Self::Relative(v),
- Value::Fraction(v) => Self::Fractional(v),
+ _: AutoValue => Self::Auto,
+ v: Rel<Length> => Self::Relative(v),
+ v: Fr => Self::Fractional(v),
}
/// Performs grid layout.
diff --git a/library/src/layout/hide.rs b/library/src/layout/hide.rs
index 1318b7ed..4e70dca9 100644
--- a/library/src/layout/hide.rs
+++ b/library/src/layout/hide.rs
@@ -1,6 +1,8 @@
use crate::prelude::*;
/// Hide content without affecting layout.
+///
+/// Tags: layout.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs
index 9c44919d..9d882ba4 100644
--- a/library/src/layout/pad.rs
+++ b/library/src/layout/pad.rs
@@ -1,6 +1,8 @@
use crate::prelude::*;
/// Pad content at the sides.
+///
+/// Tags: layout.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs
index 5a23b27b..fe83137e 100644
--- a/library/src/layout/page.rs
+++ b/library/src/layout/page.rs
@@ -5,6 +5,8 @@ use crate::prelude::*;
use crate::text::TextNode;
/// Layouts its child onto one or multiple pages.
+///
+/// Tags: layout.
#[func]
#[capable]
#[derive(Clone, Hash)]
@@ -12,6 +14,9 @@ pub struct PageNode(pub Content);
#[node]
impl PageNode {
+ /// The paper size.
+ #[property(reflect, skip, shorthand)]
+ pub const PAPER: Paper = Paper::A4;
/// The unflipped width of the page.
#[property(resolve)]
pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width().into());
@@ -145,6 +150,8 @@ impl Debug for PageNode {
}
/// A page break.
+///
+/// Tags: layout.
#[func]
#[capable]
#[derive(Debug, Copy, Clone, Hash)]
@@ -187,7 +194,10 @@ impl Marginal {
impl Cast<Spanned<Value>> for Marginal {
fn is(value: &Spanned<Value>) -> bool {
- matches!(&value.v, Value::Content(_) | Value::Func(_))
+ matches!(
+ &value.v,
+ Value::None | Value::Str(_) | Value::Content(_) | Value::Func(_)
+ )
}
fn cast(value: Spanned<Value>) -> StrResult<Self> {
@@ -196,43 +206,51 @@ impl Cast<Spanned<Value>> for Marginal {
Value::Str(v) => Ok(Self::Content(TextNode::packed(v))),
Value::Content(v) => Ok(Self::Content(v)),
Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => Err(format_eco!(
- "expected none, content or function, found {}",
- v.type_name(),
- )),
+ v => Self::error(v),
}
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Union(vec![
+ CastInfo::Type("none"),
+ CastInfo::Type("content"),
+ CastInfo::Type("function"),
+ ])
+ }
}
/// Specification of a paper.
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Copy, Clone, Hash)]
pub struct Paper {
/// The width of the paper in millimeters.
- width: f64,
+ width: Scalar,
/// The height of the paper in millimeters.
- height: f64,
+ height: Scalar,
}
impl Paper {
/// The width of the paper.
pub fn width(self) -> Abs {
- Abs::mm(self.width)
+ Abs::mm(self.width.0)
}
/// The height of the paper.
pub fn height(self) -> Abs {
- Abs::mm(self.height)
+ Abs::mm(self.height.0)
}
}
/// Defines paper constants and a paper parsing implementation.
macro_rules! papers {
- ($(($var:ident: $width:expr, $height: expr, $($pats:tt)*))*) => {
+ ($(($var:ident: $width:expr, $height: expr, $pat:literal))*) => {
/// Predefined papers.
///
/// Each paper is parsable from its name in kebab-case.
impl Paper {
- $(pub const $var: Self = Self { width: $width, height: $height };)*
+ $(pub const $var: Self = Self {
+ width: Scalar($width),
+ height: Scalar($height),
+ };)*
}
impl FromStr for Paper {
@@ -240,18 +258,17 @@ macro_rules! papers {
fn from_str(name: &str) -> Result<Self, Self::Err> {
match name.to_lowercase().as_str() {
- $($($pats)* => Ok(Self::$var),)*
+ $($pat => Ok(Self::$var),)*
_ => Err("invalid paper name"),
}
}
}
- };
-}
-castable! {
- Paper,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)?,
+ castable! {
+ Paper,
+ $($pat => Self::$var,)*
+ }
+ };
}
// All paper sizes in mm.
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs
index 925eea54..eed0dbb1 100644
--- a/library/src/layout/par.rs
+++ b/library/src/layout/par.rs
@@ -12,6 +12,8 @@ use crate::text::{
};
/// Arrange text, spacing and inline-level nodes into a paragraph.
+///
+/// Tags: layout.
#[func]
#[capable]
#[derive(Hash)]
@@ -109,9 +111,8 @@ pub struct HorizontalAlign(pub GenAlign);
castable! {
HorizontalAlign,
- Expected: "alignment",
- @align: GenAlign => match align.axis() {
- Axis::X => Self(*align),
+ align: GenAlign => match align.axis() {
+ Axis::X => Self(align),
Axis::Y => Err("must be horizontal")?,
},
}
@@ -135,15 +136,15 @@ pub enum Linebreaks {
castable! {
Linebreaks,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "simple" => Self::Simple,
- "optimized" => Self::Optimized,
- _ => Err(r#"expected "simple" or "optimized""#)?,
- },
+ /// Determine the linebreaks in a simple first-fit style.
+ "simple" => Self::Simple,
+ /// Optimize the linebreaks for the whole paragraph.
+ "optimized" => Self::Optimized,
}
/// A paragraph break.
+///
+/// Tags: layout.
#[func]
#[capable(Unlabellable)]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs
index 4c9c0a46..c3fcd0d5 100644
--- a/library/src/layout/place.rs
+++ b/library/src/layout/place.rs
@@ -1,6 +1,8 @@
use crate::prelude::*;
/// Place content at an absolute position.
+///
+/// Tags: layout.
#[func]
#[capable(Layout, Behave)]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/repeat.rs b/library/src/layout/repeat.rs
index 196f19de..a47dbb3e 100644
--- a/library/src/layout/repeat.rs
+++ b/library/src/layout/repeat.rs
@@ -1,6 +1,8 @@
use crate::prelude::*;
/// Repeats content to fill a line.
+///
+/// Tags: layout.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs
index 91e45b03..e961c0cf 100644
--- a/library/src/layout/spacing.rs
+++ b/library/src/layout/spacing.rs
@@ -3,6 +3,8 @@ use std::cmp::Ordering;
use crate::prelude::*;
/// Horizontal spacing.
+///
+/// Tags: layout.
#[func]
#[capable(Behave)]
#[derive(Debug, Copy, Clone, Hash)]
@@ -52,6 +54,8 @@ impl Behave for HNode {
}
/// Vertical spacing.
+///
+/// Tags: layout.
#[func]
#[capable(Behave)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
@@ -119,6 +123,11 @@ impl Behave for VNode {
}
}
+castable! {
+ VNode,
+ spacing: Spacing => VNode::block_around(spacing),
+}
+
/// Kinds of spacing.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Spacing {
@@ -160,9 +169,6 @@ impl PartialOrd for Spacing {
castable! {
Spacing,
- Expected: "relative length or fraction",
- Value::Length(v) => Self::Relative(v.into()),
- Value::Ratio(v) => Self::Relative(v.into()),
- Value::Relative(v) => Self::Relative(v),
- Value::Fraction(v) => Self::Fractional(v),
+ v: Rel<Length> => Self::Relative(v),
+ v: Fr => Self::Fractional(v),
}
diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs
index 1e956669..111e3433 100644
--- a/library/src/layout/stack.rs
+++ b/library/src/layout/stack.rs
@@ -4,6 +4,8 @@ use super::{AlignNode, Spacing};
use crate::prelude::*;
/// Arrange content and spacing along an axis.
+///
+/// Tags: layout.
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
@@ -81,12 +83,8 @@ impl Debug for StackChild {
castable! {
StackChild,
- Expected: "relative length, fraction, or content",
- Value::Length(v) => Self::Spacing(Spacing::Relative(v.into())),
- Value::Ratio(v) => Self::Spacing(Spacing::Relative(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Relative(v)),
- Value::Fraction(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Content(v) => Self::Block(v),
+ spacing: Spacing => Self::Spacing(spacing),
+ content: Content => Self::Block(content),
}
/// Performs stack layout.
diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs
index 35b6709a..f1a89d4c 100644
--- a/library/src/layout/transform.rs
+++ b/library/src/layout/transform.rs
@@ -3,6 +3,8 @@ use typst::geom::Transform;
use crate::prelude::*;
/// Move content without affecting layout.
+///
+/// Tags: layout.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
@@ -46,6 +48,8 @@ impl Layout for MoveNode {
impl Inline for MoveNode {}
/// Transform content without affecting layout.
+///
+/// Tags: layout.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
diff --git a/library/src/math/matrix.rs b/library/src/math/matrix.rs
index 2d32f4b5..21294b71 100644
--- a/library/src/math/matrix.rs
+++ b/library/src/math/matrix.rs
@@ -1,6 +1,8 @@
use super::*;
/// A column vector.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -52,17 +54,19 @@ pub enum Delimiter {
castable! {
Delimiter,
- Expected: "type of bracket or bar",
- Value::Str(s) => match s.as_str() {
- "(" => Self::Paren,
- "[" => Self::Bracket,
- "{" => Self::Brace,
- "|" => Self::Bar,
- _ => Err("expected \"(\", \"[\", \"{\", or \"|\"")?,
- },
+ /// Delimit matrices with parentheses.
+ "(" => Self::Paren,
+ /// Delimit matrices with brackets.
+ "[" => Self::Bracket,
+ /// Delimit matrices with curly braces.
+ "{" => Self::Brace,
+ /// Delimit matrices with vertical bars.
+ "|" => Self::Bar,
}
/// A case distinction.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs
index 59c621e8..41cdb9f5 100644
--- a/library/src/math/mod.rs
+++ b/library/src/math/mod.rs
@@ -15,6 +15,8 @@ use crate::prelude::*;
use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode};
/// A piece of a mathematical formula.
+///
+/// Tags: math.
#[func]
#[capable(Show, Layout, Inline, Texify)]
#[derive(Debug, Clone, Hash)]
@@ -244,6 +246,8 @@ impl Texify for Content {
}
/// An atom in a math formula: `x`, `+`, `12`.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -283,6 +287,8 @@ impl Texify for AtomNode {
}
/// An accented node.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -358,6 +364,8 @@ impl Texify for AccNode {
}
/// A fraction.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -389,6 +397,8 @@ impl Texify for FracNode {
}
/// A binomial.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -420,6 +430,8 @@ impl Texify for BinomNode {
}
/// A sub- and/or superscript.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -456,6 +468,8 @@ impl Texify for ScriptNode {
}
/// A math alignment point: `&`, `&&`.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -471,6 +485,8 @@ impl Texify for AlignPointNode {
}
/// A square root.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -493,6 +509,8 @@ impl Texify for SqrtNode {
}
/// A floored expression.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -515,6 +533,8 @@ impl Texify for FloorNode {
}
/// A ceiled expression.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
diff --git a/library/src/math/style.rs b/library/src/math/style.rs
index 0fdff740..444b1fb4 100644
--- a/library/src/math/style.rs
+++ b/library/src/math/style.rs
@@ -1,6 +1,8 @@
use super::*;
/// Serif (roman) font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -23,6 +25,8 @@ impl Texify for SerifNode {
}
/// Sans-serif font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -45,6 +49,8 @@ impl Texify for SansNode {
}
/// Bold font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -67,6 +73,8 @@ impl Texify for BoldNode {
}
/// Italic font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -89,6 +97,8 @@ impl Texify for ItalNode {
}
/// Calligraphic font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -111,6 +121,8 @@ impl Texify for CalNode {
}
/// Fraktur font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -133,6 +145,8 @@ impl Texify for FrakNode {
}
/// Monospace font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
@@ -155,6 +169,8 @@ impl Texify for MonoNode {
}
/// Blackboard bold (double-struck) font style.
+///
+/// Tags: math.
#[func]
#[capable(Texify)]
#[derive(Debug, Hash)]
diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs
index 1dae4a2a..8c664df3 100644
--- a/library/src/meta/document.rs
+++ b/library/src/meta/document.rs
@@ -2,6 +2,8 @@ use crate::layout::{LayoutRoot, PageNode};
use crate::prelude::*;
/// The root node that represents a full document.
+///
+/// Tags: meta.
#[func]
#[capable(LayoutRoot)]
#[derive(Hash)]
diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs
index 94328b00..6f5d8af1 100644
--- a/library/src/meta/link.rs
+++ b/library/src/meta/link.rs
@@ -2,6 +2,8 @@ use crate::prelude::*;
use crate::text::TextNode;
/// Link text and other elements to a destination.
+///
+/// Tags: meta.
#[func]
#[capable(Show, Finalize)]
#[derive(Debug, Hash)]
diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs
index d0fbc3f7..27ca5944 100644
--- a/library/src/meta/outline.rs
+++ b/library/src/meta/outline.rs
@@ -4,6 +4,8 @@ use crate::prelude::*;
use crate::text::{LinebreakNode, SpaceNode, TextNode};
/// A section outline (table of contents).
+///
+/// Tags: meta.
#[func]
#[capable(Prepare, Show)]
#[derive(Debug, Hash)]
diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs
index 657e5ef7..378d19d2 100644
--- a/library/src/meta/reference.rs
+++ b/library/src/meta/reference.rs
@@ -2,6 +2,8 @@ use crate::prelude::*;
use crate::text::TextNode;
/// A reference to a label.
+///
+/// Tags: meta.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
diff --git a/library/src/prelude.rs b/library/src/prelude.rs
index 9b461389..36d49dbf 100644
--- a/library/src/prelude.rs
+++ b/library/src/prelude.rs
@@ -8,17 +8,17 @@ pub use std::num::NonZeroUsize;
#[doc(no_inline)]
pub use comemo::{Track, Tracked, TrackedMut};
#[doc(no_inline)]
-pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult};
+pub use typst::diag::{bail, error, At, SourceResult, StrResult};
#[doc(no_inline)]
pub use typst::doc::*;
#[doc(no_inline)]
pub use typst::geom::*;
#[doc(no_inline)]
pub use typst::model::{
- array, capability, capable, castable, dict, dynamic, format_str, func, node, Args,
- Array, Cast, Content, Dict, Finalize, Fold, Func, Introspector, Label, Node, NodeId,
- Prepare, Resolve, Selector, Show, Smart, StabilityProvider, Str, StyleChain,
- StyleMap, StyleVec, Unlabellable, Value, Vm, Vt,
+ array, capability, capable, castable, dict, format_str, func, node, Args, Array,
+ AutoValue, Cast, CastInfo, Content, Dict, Finalize, Fold, Func, Introspector, Label,
+ Node, NodeId, NoneValue, Prepare, Resolve, Selector, Show, StabilityProvider, Str,
+ StyleChain, StyleMap, StyleVec, Unlabellable, Value, Vm, Vt,
};
#[doc(no_inline)]
pub use typst::syntax::{Span, Spanned};
diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs
index fceb4cfd..86866715 100644
--- a/library/src/text/deco.rs
+++ b/library/src/text/deco.rs
@@ -5,6 +5,8 @@ use super::TextNode;
use crate::prelude::*;
/// Typeset underline, stricken-through or overlined text.
+///
+/// Tags: text.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs
index 1c5a32b4..fc4f7d73 100644
--- a/library/src/text/misc.rs
+++ b/library/src/text/misc.rs
@@ -2,6 +2,8 @@ use super::TextNode;
use crate::prelude::*;
/// A text space.
+///
+/// Tags: text.
#[func]
#[capable(Unlabellable, Behave)]
#[derive(Debug, Hash)]
@@ -23,6 +25,8 @@ impl Behave for SpaceNode {
}
/// A line break.
+///
+/// Tags: text.
#[func]
#[capable(Behave)]
#[derive(Debug, Hash)]
@@ -45,6 +49,8 @@ impl Behave for LinebreakNode {
}
/// Strongly emphasizes content by increasing the font weight.
+///
+/// Tags: text.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
@@ -79,8 +85,7 @@ pub struct Delta(pub i64);
castable! {
Delta,
- Expected: "integer",
- Value::Int(delta) => Self(delta),
+ v: i64 => Self(v),
}
impl Fold for Delta {
@@ -92,6 +97,8 @@ impl Fold for Delta {
}
/// Emphasizes content by flipping the italicness.
+///
+/// Tags: text.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
@@ -130,12 +137,16 @@ impl Fold for Toggle {
}
/// Convert a string or content to lowercase.
+///
+/// Tags: text.
#[func]
pub fn lower(args: &mut Args) -> SourceResult<Value> {
case(Case::Lower, args)
}
/// Convert a string or content to uppercase.
+///
+/// Tags: text.
#[func]
pub fn upper(args: &mut Args) -> SourceResult<Value> {
case(Case::Upper, args)
@@ -171,6 +182,8 @@ impl Case {
}
/// Display text in small capitals.
+///
+/// Tags: text.
#[func]
pub fn smallcaps(args: &mut Args) -> SourceResult<Value> {
let body: Content = args.expect("content")?;
diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs
index d09d8f28..7340e5de 100644
--- a/library/src/text/mod.rs
+++ b/library/src/text/mod.rs
@@ -26,6 +26,8 @@ use crate::layout::ParNode;
use crate::prelude::*;
/// A single run of text with the same style.
+///
+/// Tags: text.
#[func]
#[capable]
#[derive(Clone, Hash)]
@@ -206,8 +208,7 @@ impl Debug for FontFamily {
castable! {
FontFamily,
- Expected: "string",
- Value::Str(string) => Self::new(&string),
+ string: EcoString => Self::new(&string),
}
/// Font family fallback list.
@@ -216,12 +217,10 @@ pub struct FallbackList(pub Vec<FontFamily>);
castable! {
FallbackList,
- Expected: "string or array of strings",
- Value::Str(string) => Self(vec![FontFamily::new(&string)]),
- Value::Array(values) => Self(values
+ family: FontFamily => Self(vec![family]),
+ values: Array => Self(values
.into_iter()
.filter_map(|v| v.cast().ok())
- .map(|string: EcoString| FontFamily::new(&string))
.collect()),
}
@@ -237,7 +236,10 @@ impl Fold for TextSize {
}
}
-castable!(TextSize: Length);
+castable! {
+ TextSize,
+ v: Length => Self(v),
+}
/// Specifies the bottom or top edge of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@@ -260,16 +262,17 @@ impl TextEdge {
castable! {
TextEdge,
- Expected: "string or length",
- Value::Length(v) => Self::Length(v),
- Value::Str(string) => Self::Metric(match string.as_str() {
- "ascender" => VerticalFontMetric::Ascender,
- "cap-height" => VerticalFontMetric::CapHeight,
- "x-height" => VerticalFontMetric::XHeight,
- "baseline" => VerticalFontMetric::Baseline,
- "descender" => VerticalFontMetric::Descender,
- _ => Err("unknown font metric")?,
- }),
+ v: Length => Self::Length(v),
+ /// The distance from the baseline to the ascender.
+ "ascender" => Self::Metric(VerticalFontMetric::Ascender),
+ /// The approximate height of uppercase letters.
+ "cap-height" => Self::Metric(VerticalFontMetric::CapHeight),
+ /// The approximate height of non-ascending lowercase letters.
+ "x-height" => Self::Metric(VerticalFontMetric::XHeight),
+ /// The baseline on which the letters rest.
+ "baseline" => Self::Metric(VerticalFontMetric::Baseline),
+ /// The distance from the baseline to the descender.
+ "descender" => Self::Metric(VerticalFontMetric::Descender),
}
/// The direction of text and inline objects in their line.
@@ -278,10 +281,9 @@ pub struct HorizontalDir(pub Smart<Dir>);
castable! {
HorizontalDir,
- Expected: "direction or auto",
- Value::Auto => Self(Smart::Auto),
- @dir: Dir => match dir.axis() {
- Axis::X => Self(Smart::Custom(*dir)),
+ _: AutoValue => Self(Smart::Auto),
+ dir: Dir => match dir.axis() {
+ Axis::X => Self(Smart::Custom(dir)),
Axis::Y => Err("must be horizontal")?,
},
}
@@ -303,9 +305,8 @@ pub struct Hyphenate(pub Smart<bool>);
castable! {
Hyphenate,
- Expected: "boolean or auto",
- Value::Auto => Self(Smart::Auto),
- Value::Bool(v) => Self(Smart::Custom(v)),
+ _: AutoValue => Self(Smart::Auto),
+ v: bool => Self(Smart::Custom(v)),
}
impl Resolve for Hyphenate {
@@ -337,8 +338,7 @@ impl StylisticSet {
castable! {
StylisticSet,
- Expected: "integer",
- Value::Int(v) => match v {
+ v: i64 => match v {
1 ..= 20 => Self::new(v as u8),
_ => Err("must be between 1 and 20")?,
},
@@ -355,12 +355,10 @@ pub enum NumberType {
castable! {
NumberType,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "lining" => Self::Lining,
- "old-style" => Self::OldStyle,
- _ => Err(r#"expected "lining" or "old-style""#)?,
- },
+ /// Numbers that fit well with capital text.
+ "lining" => Self::Lining,
+ /// Numbers that fit well into a flow of upper- and lowercase text.
+ "old-style" => Self::OldStyle,
}
/// The width of numbers / figures.
@@ -374,12 +372,10 @@ pub enum NumberWidth {
castable! {
NumberWidth,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "proportional" => Self::Proportional,
- "tabular" => Self::Tabular,
- _ => Err(r#"expected "proportional" or "tabular""#)?,
- },
+ /// Number widths are glyph specific.
+ "proportional" => Self::Proportional,
+ /// All numbers are of equal width / monospaced.
+ "tabular" => Self::Tabular,
}
/// OpenType font features settings.
@@ -388,20 +384,21 @@ pub struct FontFeatures(pub Vec<(Tag, u32)>);
castable! {
FontFeatures,
- Expected: "array of strings or dictionary mapping tags to integers",
- Value::Array(values) => Self(values
+ values: Array => Self(values
.into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
- .collect()),
- Value::Dict(values) => Self(values
+ .map(|v| {
+ let tag = v.cast::<EcoString>()?;
+ Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1))
+ })
+ .collect::<StrResult<_>>()?),
+ values: Dict => Self(values
.into_iter()
- .filter_map(|(k, v)| {
+ .map(|(k, v)| {
+ let num = v.cast::<u32>()?;
let tag = Tag::from_bytes_lossy(k.as_bytes());
- let num = v.cast::<i64>().ok()?.try_into().ok()?;
- Some((tag, num))
+ Ok((tag, num))
})
- .collect()),
+ .collect::<StrResult<_>>()?),
}
impl Fold for FontFeatures {
diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs
index 0f678de3..ab6f166c 100644
--- a/library/src/text/quotes.rs
+++ b/library/src/text/quotes.rs
@@ -3,6 +3,8 @@ use typst::syntax::is_newline;
use crate::prelude::*;
/// A smart quote.
+///
+/// Tags: text.
#[func]
#[capable]
#[derive(Debug, Hash)]
diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs
index 4ad70654..125a5da1 100644
--- a/library/src/text/raw.rs
+++ b/library/src/text/raw.rs
@@ -7,6 +7,8 @@ use crate::layout::BlockNode;
use crate::prelude::*;
/// Raw text with optional syntax highlighting.
+///
+/// Tags: text.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs
index 65adc027..ad4a6cd9 100644
--- a/library/src/text/shift.rs
+++ b/library/src/text/shift.rs
@@ -10,6 +10,8 @@ use crate::prelude::*;
/// typography possible, we first try to transform the text to superscript
/// codepoints. If that fails, we fall back to rendering shrunk normal letters
/// in a raised way.
+///
+/// Tags: text.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
diff --git a/library/src/text/symbol.rs b/library/src/text/symbol.rs
index fc746eb2..eece81ab 100644
--- a/library/src/text/symbol.rs
+++ b/library/src/text/symbol.rs
@@ -2,6 +2,8 @@ use crate::prelude::*;
use crate::text::TextNode;
/// A symbol identified by symmie notation.
+///
+/// Tags: text.
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs
index b8b05aec..936ec3bf 100644
--- a/library/src/visualize/image.rs
+++ b/library/src/visualize/image.rs
@@ -5,6 +5,8 @@ use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::prelude::*;
/// Show a raster or vector graphic.
+///
+/// Tags: visualize.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
@@ -112,11 +114,10 @@ pub enum ImageFit {
castable! {
ImageFit,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "cover" => Self::Cover,
- "contain" => Self::Contain,
- "stretch" => Self::Stretch,
- _ => Err(r#"expected "cover", "contain" or "stretch""#)?,
- },
+ /// The image should completely cover the area.
+ "cover" => Self::Cover,
+ /// The image should be fully contained in the area.
+ "contain" => Self::Contain,
+ /// The image should be stretched so that it exactly fills the area.
+ "stretch" => Self::Stretch,
}
diff --git a/library/src/visualize/line.rs b/library/src/visualize/line.rs
index ed6a3d92..9c9b8b00 100644
--- a/library/src/visualize/line.rs
+++ b/library/src/visualize/line.rs
@@ -1,6 +1,8 @@
use crate::prelude::*;
/// Display a line without affecting the layout.
+///
+/// Tags: visualize.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs
index 702fc6f8..a3443189 100644
--- a/library/src/visualize/shape.rs
+++ b/library/src/visualize/shape.rs
@@ -3,6 +3,8 @@ use std::f64::consts::SQRT_2;
use crate::prelude::*;
/// A sizable and fillable shape with optional content.
+///
+/// Tags: visualize.
#[func]
#[capable(Layout, Inline)]
#[derive(Debug, Hash)]
@@ -25,7 +27,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to fill the shape.
pub const FILL: Option<Paint> = None;
/// How to stroke the shape.
- #[property(skip, resolve, fold)]
+ #[property(reflect, skip, resolve, fold)]
pub const STROKE: Smart<Sides<Option<PartialStroke>>> = Smart::Auto;
/// How much to pad the shape's content.
@@ -36,7 +38,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
/// How much to round the shape's corners.
- #[property(skip, resolve, fold)]
+ #[property(reflect, skip, resolve, fold)]
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
diff --git a/macros/src/capability.rs b/macros/src/capable.rs
index aa98f584..dcfdfc82 100644
--- a/macros/src/capability.rs
+++ b/macros/src/capable.rs
@@ -1,9 +1,9 @@
-use super::*;
-
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::Token;
+use super::*;
+
/// Expand the `#[capability]` macro.
pub fn capability(item: syn::ItemTrait) -> Result<TokenStream> {
let ident = &item.ident;
diff --git a/macros/src/castable.rs b/macros/src/castable.rs
new file mode 100644
index 00000000..48cdf9e1
--- /dev/null
+++ b/macros/src/castable.rs
@@ -0,0 +1,229 @@
+use syn::parse::{Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::Token;
+
+use super::*;
+
+/// Expand the `castable!` macro.
+pub fn castable(stream: TokenStream) -> Result<TokenStream> {
+ let castable: Castable = syn::parse2(stream)?;
+ let ty = &castable.ty;
+
+ if castable.casts.is_empty() && castable.name.is_none() {
+ bail!(castable.ty, "expected at least one pattern");
+ }
+
+ let is_func = create_is_func(&castable);
+ let cast_func = create_cast_func(&castable);
+ let describe_func = create_describe_func(&castable);
+ let dynamic_impls = castable.name.as_ref().map(|name| {
+ quote! {
+ impl ::typst::model::Type for #ty {
+ const TYPE_NAME: &'static str = #name;
+ }
+
+ impl From<#ty> for ::typst::model::Value {
+ fn from(v: #ty) -> Self {
+ ::typst::model::Value::Dyn(::typst::model::Dynamic::new(v))
+ }
+ }
+ }
+ });
+
+ Ok(quote! {
+ impl ::typst::model::Cast for #ty {
+ #is_func
+ #cast_func
+ #describe_func
+ }
+
+ #dynamic_impls
+ })
+}
+
+/// Create the castable's `is` function.
+fn create_is_func(castable: &Castable) -> TokenStream {
+ let mut string_arms = vec![];
+ let mut cast_checks = vec![];
+
+ for cast in &castable.casts {
+ match &cast.pattern {
+ Pattern::Str(lit) => {
+ string_arms.push(quote! { #lit => return true });
+ }
+ Pattern::Ty(_, ty) => {
+ cast_checks.push(quote! {
+ if <#ty as ::typst::model::Cast>::is(value) {
+ return true;
+ }
+ });
+ }
+ }
+ }
+
+ let dynamic_check = castable.name.is_some().then(|| {
+ quote! {
+ if let ::typst::model::Value::Dyn(dynamic) = &value {
+ if dynamic.is::<Self>() {
+ return true;
+ }
+ }
+ }
+ });
+
+ let str_check = (!string_arms.is_empty()).then(|| {
+ quote! {
+ if let ::typst::model::Value::Str(string) = &value {
+ match string.as_str() {
+ #(#string_arms,)*
+ _ => {}
+ }
+ }
+ }
+ });
+
+ quote! {
+ fn is(value: &typst::model::Value) -> bool {
+ #dynamic_check
+ #str_check
+ #(#cast_checks)*
+ false
+ }
+ }
+}
+
+/// Create the castable's `cast` function.
+fn create_cast_func(castable: &Castable) -> TokenStream {
+ let mut string_arms = vec![];
+ let mut cast_checks = vec![];
+
+ for cast in &castable.casts {
+ let expr = &cast.expr;
+ match &cast.pattern {
+ Pattern::Str(lit) => {
+ string_arms.push(quote! { #lit => return Ok(#expr) });
+ }
+ Pattern::Ty(binding, ty) => {
+ cast_checks.push(quote! {
+ if <#ty as ::typst::model::Cast>::is(&value) {
+ let #binding = <#ty as ::typst::model::Cast>::cast(value)?;
+ return Ok(#expr);
+ }
+ });
+ }
+ }
+ }
+
+ let dynamic_check = castable.name.is_some().then(|| {
+ quote! {
+ if let ::typst::model::Value::Dyn(dynamic) = &value {
+ if let Some(concrete) = dynamic.downcast::<Self>() {
+ return Ok(concrete.clone());
+ }
+ }
+ }
+ });
+
+ let str_check = (!string_arms.is_empty()).then(|| {
+ quote! {
+ if let ::typst::model::Value::Str(string) = &value {
+ match string.as_str() {
+ #(#string_arms,)*
+ _ => {}
+ }
+ }
+ }
+ });
+
+ quote! {
+ fn cast(value: ::typst::model::Value) -> ::typst::diag::StrResult<Self> {
+ #dynamic_check
+ #str_check
+ #(#cast_checks)*
+ <Self as ::typst::model::Cast>::error(value)
+ }
+ }
+}
+
+/// Create the castable's `describe` function.
+fn create_describe_func(castable: &Castable) -> TokenStream {
+ let mut infos = vec![];
+
+ for cast in &castable.casts {
+ let docs = doc_comment(&cast.attrs);
+ infos.push(match &cast.pattern {
+ Pattern::Str(lit) => {
+ quote! { ::typst::model::CastInfo::Value(#lit.into(), #docs) }
+ }
+ Pattern::Ty(_, ty) => {
+ quote! { <#ty as ::typst::model::Cast>::describe() }
+ }
+ });
+ }
+
+ if let Some(name) = &castable.name {
+ infos.push(quote! {
+ CastInfo::Type(#name)
+ });
+ }
+
+ quote! {
+ fn describe() -> ::typst::model::CastInfo {
+ #(#infos)+*
+ }
+ }
+}
+
+struct Castable {
+ ty: syn::Type,
+ name: Option<syn::LitStr>,
+ casts: Punctuated<Cast, Token![,]>,
+}
+
+impl Parse for Castable {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let ty = input.parse()?;
+ let mut name = None;
+ if input.peek(Token![:]) {
+ let _: syn::Token![:] = input.parse()?;
+ name = Some(input.parse()?);
+ }
+ let _: syn::Token![,] = input.parse()?;
+ let casts = Punctuated::parse_terminated(input)?;
+ Ok(Self { ty, name, casts })
+ }
+}
+
+struct Cast {
+ attrs: Vec<syn::Attribute>,
+ pattern: Pattern,
+ expr: syn::Expr,
+}
+
+impl Parse for Cast {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let attrs = input.call(syn::Attribute::parse_outer)?;
+ let pattern = input.parse()?;
+ let _: syn::Token![=>] = input.parse()?;
+ let expr = input.parse()?;
+ Ok(Self { attrs, pattern, expr })
+ }
+}
+
+enum Pattern {
+ Str(syn::LitStr),
+ Ty(syn::Pat, syn::Type),
+}
+
+impl Parse for Pattern {
+ fn parse(input: ParseStream) -> Result<Self> {
+ if input.peek(syn::LitStr) {
+ Ok(Pattern::Str(input.parse()?))
+ } else {
+ let pat = input.parse()?;
+ let _: syn::Token![:] = input.parse()?;
+ let ty = input.parse()?;
+ Ok(Pattern::Ty(pat, ty))
+ }
+ }
+}
diff --git a/macros/src/func.rs b/macros/src/func.rs
index af558f96..4523d48a 100644
--- a/macros/src/func.rs
+++ b/macros/src/func.rs
@@ -2,7 +2,37 @@ use super::*;
/// Expand the `#[func]` macro.
pub fn func(item: syn::Item) -> Result<TokenStream> {
- let doc = documentation(&item)?;
+ let doc_comment = match &item {
+ syn::Item::Struct(item) => doc_comment(&item.attrs),
+ syn::Item::Enum(item) => doc_comment(&item.attrs),
+ syn::Item::Fn(item) => doc_comment(&item.attrs),
+ _ => String::new(),
+ };
+
+ let mut tags = vec![];
+ let mut kept = vec![];
+ for line in doc_comment.lines() {
+ let line = line.trim();
+ if let Some(suffix) = line.trim_end_matches(".").strip_prefix("Tags: ") {
+ tags.extend(suffix.split(", "));
+ } else {
+ kept.push(line);
+ }
+ }
+
+ while kept.last().map_or(false, |line| line.is_empty()) {
+ kept.pop();
+ }
+
+ let docs = kept.join("\n");
+ let info = quote! {
+ ::typst::model::FuncInfo {
+ name,
+ docs: #docs,
+ tags: &[#(#tags),*],
+ params: ::std::vec![],
+ }
+ };
if let syn::Item::Fn(item) = &item {
let vis = &item.vis;
@@ -29,7 +59,7 @@ pub fn func(item: syn::Item) -> Result<TokenStream> {
impl::typst::model::FuncType for #ty {
fn create_func(name: &'static str) -> ::typst::model::Func {
- ::typst::model::Func::from_fn(name, #full, #doc)
+ ::typst::model::Func::from_fn(name, #full, #info)
}
}
})
@@ -47,36 +77,9 @@ pub fn func(item: syn::Item) -> Result<TokenStream> {
impl #params ::typst::model::FuncType for #ident #args #clause {
fn create_func(name: &'static str) -> ::typst::model::Func {
- ::typst::model::Func::from_node::<Self>(name, #doc)
+ ::typst::model::Func::from_node::<Self>(name, #info)
}
}
})
}
}
-
-/// Extract the item's documentation.
-fn documentation(item: &syn::Item) -> Result<String> {
- let mut doc = String::new();
-
- // Extract attributes.
- let attrs = match item {
- syn::Item::Struct(item) => &item.attrs,
- syn::Item::Enum(item) => &item.attrs,
- syn::Item::Fn(item) => &item.attrs,
- _ => return Ok(doc),
- };
-
- // Parse doc comments.
- for attr in attrs {
- if let syn::Meta::NameValue(meta) = attr.parse_meta()? {
- if meta.path.is_ident("doc") {
- if let syn::Lit::Str(string) = &meta.lit {
- doc.push_str(&string.value());
- doc.push('\n');
- }
- }
- }
- }
-
- Ok(doc.trim().into())
-}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 7f6a4a6c..15dc3ee7 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -12,7 +12,8 @@ macro_rules! bail {
}
}
-mod capability;
+mod capable;
+mod castable;
mod func;
mod node;
@@ -40,7 +41,7 @@ pub fn node(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
#[proc_macro_attribute]
pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemTrait);
- capability::capability(item)
+ capable::capability(item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
@@ -49,7 +50,34 @@ pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
#[proc_macro_attribute]
pub fn capable(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::Item);
- capability::capable(stream.into(), item)
+ capable::capable(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
+
+/// Implement `Cast` and optionally `Type` for a type.
+#[proc_macro]
+pub fn castable(stream: BoundaryStream) -> BoundaryStream {
+ castable::castable(stream.into())
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Extract documentation comments from an attribute list.
+fn doc_comment(attrs: &[syn::Attribute]) -> String {
+ let mut doc = String::new();
+
+ // Parse doc comments.
+ for attr in attrs {
+ if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() {
+ if meta.path.is_ident("doc") {
+ if let syn::Lit::Str(string) = &meta.lit {
+ doc.push_str(&string.value());
+ doc.push('\n');
+ }
+ }
+ }
+ }
+
+ doc.trim().into()
+}
diff --git a/macros/src/node.rs b/macros/src/node.rs
index 192ca667..ad079c0e 100644
--- a/macros/src/node.rs
+++ b/macros/src/node.rs
@@ -36,6 +36,7 @@ struct Property {
shorthand: Option<Shorthand>,
resolve: bool,
fold: bool,
+ reflect: bool,
}
/// The shorthand form of a style property.
@@ -117,6 +118,7 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> {
let mut referenced = false;
let mut resolve = false;
let mut fold = false;
+ let mut reflect = false;
// Parse the `#[property(..)]` attribute.
let mut stream = tokens.into_iter().peekable();
@@ -150,14 +152,11 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> {
"referenced" => referenced = true,
"resolve" => resolve = true,
"fold" => fold = true,
+ "reflect" => reflect = true,
_ => bail!(ident, "invalid attribute"),
}
}
- if skip && shorthand.is_some() {
- bail!(item.ident, "skip and shorthand are mutually exclusive");
- }
-
if referenced && (fold || resolve) {
bail!(item.ident, "referenced is mutually exclusive with fold and resolve");
}
@@ -193,6 +192,7 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> {
referenced,
resolve,
fold,
+ reflect,
})
}
@@ -205,6 +205,7 @@ fn create(node: &Node) -> Result<TokenStream> {
let name_method = create_node_name_method(node);
let construct_func = create_node_construct_func(node);
let set_func = create_node_set_func(node);
+ let properties_func = create_node_properties_func(node);
let field_method = create_node_field_method(node);
let node_impl = quote! {
@@ -213,6 +214,7 @@ fn create(node: &Node) -> Result<TokenStream> {
#name_method
#construct_func
#set_func
+ #properties_func
#field_method
}
};
@@ -221,7 +223,7 @@ fn create(node: &Node) -> Result<TokenStream> {
let mut items: Vec<syn::ImplItem> = vec![];
let scope = quote::format_ident!("__{}_keys", node.self_name);
- for property in &node.properties {
+ for property in node.properties.iter() {
let (key, module) = create_property_module(node, &property);
modules.push(module);
@@ -331,6 +333,40 @@ fn create_node_set_func(node: &Node) -> syn::ImplItemMethod {
}
}
+/// Create the node's `properties` function.
+fn create_node_properties_func(node: &Node) -> syn::ImplItemMethod {
+ let infos = node
+ .properties
+ .iter()
+ .filter(|p| !p.skip || p.reflect)
+ .map(|property| {
+ let name = property.name.to_string().replace('_', "-").to_lowercase();
+ let docs = doc_comment(&property.attrs);
+ let value_ty = &property.value_ty;
+ let shorthand = matches!(property.shorthand, Some(Shorthand::Positional));
+ quote! {
+ ::typst::model::ParamInfo {
+ name: #name,
+ docs: #docs,
+ settable: true,
+ shorthand: #shorthand,
+ cast: <#value_ty as ::typst::model::Cast<
+ ::typst::syntax::Spanned<::typst::model::Value>
+ >>::describe(),
+ }
+ }
+ });
+
+ parse_quote! {
+ fn properties() -> ::std::vec::Vec<::typst::model::ParamInfo>
+ where
+ Self: Sized
+ {
+ ::std::vec![#(#infos),*]
+ }
+ }
+}
+
/// Create the node's `field` method.
fn create_node_field_method(node: &Node) -> syn::ImplItemMethod {
node.field.clone().unwrap_or_else(|| {
diff --git a/src/diag.rs b/src/diag.rs
index e244ba7c..55f16b5f 100644
--- a/src/diag.rs
+++ b/src/diag.rs
@@ -156,16 +156,6 @@ impl<T> Trace<T> for SourceResult<T> {
/// A result type with a string error message.
pub type StrResult<T> = Result<T, EcoString>;
-/// Transform `expected X, found Y` into `expected X or A, found Y`.
-pub fn with_alternative(msg: EcoString, alt: &str) -> EcoString {
- let mut parts = msg.split(", found ");
- if let (Some(a), Some(b)) = (parts.next(), parts.next()) {
- format_eco!("{} or {}, found {}", a, alt, b)
- } else {
- msg
- }
-}
-
/// Convert a [`StrResult`] to a [`SourceResult`] by adding span information.
pub trait At<T> {
/// Add the span information.
@@ -181,6 +171,30 @@ where
}
}
+/// Format the parts separated with commas and a final "and" or "or".
+pub(crate) fn comma_list<S>(buf: &mut String, parts: &[S], last: &str)
+where
+ S: AsRef<str>,
+{
+ for (i, part) in parts.iter().enumerate() {
+ match i {
+ 0 => {}
+ 1 if parts.len() == 2 => {
+ buf.push(' ');
+ buf.push_str(last);
+ buf.push(' ');
+ }
+ i if i + 1 == parts.len() => {
+ buf.push_str(", ");
+ buf.push_str(last);
+ buf.push(' ');
+ }
+ _ => buf.push_str(", "),
+ }
+ buf.push_str(part.as_ref());
+ }
+}
+
/// A result type with a file-related error.
pub type FileResult<T> = Result<T, FileError>;
diff --git a/src/geom/mod.rs b/src/geom/mod.rs
index 3c7c2fc9..6161774b 100644
--- a/src/geom/mod.rs
+++ b/src/geom/mod.rs
@@ -21,6 +21,7 @@ mod rounded;
mod scalar;
mod sides;
mod size;
+mod smart;
mod stroke;
mod transform;
@@ -43,6 +44,7 @@ pub use self::rounded::*;
pub use self::scalar::*;
pub use self::sides::*;
pub use self::size::*;
+pub use self::smart::*;
pub use self::stroke::*;
pub use self::transform::*;
diff --git a/src/geom/smart.rs b/src/geom/smart.rs
new file mode 100644
index 00000000..d20bcdfe
--- /dev/null
+++ b/src/geom/smart.rs
@@ -0,0 +1,64 @@
+use super::*;
+
+/// A value that can be automatically determined.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum Smart<T> {
+ /// The value should be determined smartly based on the circumstances.
+ Auto,
+ /// A specific value.
+ Custom(T),
+}
+
+impl<T> Smart<T> {
+ /// Map the contained custom value with `f`.
+ pub fn map<F, U>(self, f: F) -> Smart<U>
+ where
+ F: FnOnce(T) -> U,
+ {
+ match self {
+ Self::Auto => Smart::Auto,
+ Self::Custom(x) => Smart::Custom(f(x)),
+ }
+ }
+
+ /// Keeps `self` if it contains a custom value, otherwise returns `other`.
+ pub fn or(self, other: Smart<T>) -> Self {
+ match self {
+ Self::Custom(x) => Self::Custom(x),
+ Self::Auto => other,
+ }
+ }
+
+ /// Returns the contained custom value or a provided default value.
+ pub fn unwrap_or(self, default: T) -> T {
+ match self {
+ Self::Auto => default,
+ Self::Custom(x) => x,
+ }
+ }
+
+ /// Returns the contained custom value or computes a default value.
+ pub fn unwrap_or_else<F>(self, f: F) -> T
+ where
+ F: FnOnce() -> T,
+ {
+ match self {
+ Self::Auto => f(),
+ Self::Custom(x) => x,
+ }
+ }
+
+ /// Returns the contained custom value or the default value.
+ pub fn unwrap_or_default(self) -> T
+ where
+ T: Default,
+ {
+ self.unwrap_or_else(T::default)
+ }
+}
+
+impl<T> Default for Smart<T> {
+ fn default() -> Self {
+ Self::Auto
+ }
+}
diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs
index eae43c24..86191d33 100644
--- a/src/geom/stroke.rs
+++ b/src/geom/stroke.rs
@@ -1,5 +1,4 @@
use super::*;
-use crate::model::Smart;
/// A stroke of a geometric shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
diff --git a/src/ide/complete.rs b/src/ide/complete.rs
index 7f312727..d4e72b3d 100644
--- a/src/ide/complete.rs
+++ b/src/ide/complete.rs
@@ -1,38 +1,12 @@
use if_chain::if_chain;
-use crate::model::Value;
-use crate::syntax::{LinkedNode, Source, SyntaxKind};
+use super::summarize_font_family;
+use crate::model::{CastInfo, Scope, Value};
+use crate::syntax::ast::AstNode;
+use crate::syntax::{ast, LinkedNode, Source, SyntaxKind};
use crate::util::{format_eco, EcoString};
use crate::World;
-/// An autocompletion option.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct Completion {
- /// The kind of item this completes to.
- pub kind: CompletionKind,
- /// The label the completion is shown with.
- pub label: EcoString,
- /// The completed version of the input, defaults to the label.
- ///
- /// May use snippet syntax like `${lhs} + ${rhs}`.
- pub apply: Option<EcoString>,
- /// Details about the completed item.
- pub detail: Option<EcoString>,
-}
-
-/// A kind of item that can be completed.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
-pub enum CompletionKind {
- /// A syntactical structure.
- Syntax,
- /// A function name.
- Function,
- /// A constant of the given type.
- Constant,
- /// A symbol.
- Symbol,
-}
-
/// Autocomplete a cursor position in a source file.
///
/// Returns the position from which the completions apply and a list of
@@ -49,6 +23,7 @@ pub fn autocomplete(
let mut ctx = CompletionContext::new(world, source, cursor, explicit)?;
let _ = complete_rules(&mut ctx)
+ || complete_params(&mut ctx)
|| complete_symbols(&mut ctx)
|| complete_markup(&mut ctx)
|| complete_math(&mut ctx)
@@ -57,6 +32,39 @@ pub fn autocomplete(
Some((ctx.from, ctx.completions))
}
+/// An autocompletion option.
+#[derive(Debug, Clone)]
+pub struct Completion {
+ /// The kind of item this completes to.
+ pub kind: CompletionKind,
+ /// The label the completion is shown with.
+ pub label: EcoString,
+ /// The completed version of the input, possibly described with snippet
+ /// syntax like `${lhs} + ${rhs}`.
+ ///
+ /// Should default to the `label` if `None`.
+ pub apply: Option<EcoString>,
+ /// An optional short description, at most one sentence.
+ pub detail: Option<EcoString>,
+}
+
+/// A kind of item that can be completed.
+#[derive(Debug, Clone)]
+pub enum CompletionKind {
+ /// A syntactical structure.
+ Syntax,
+ /// A function.
+ Func,
+ /// A function parameter.
+ Param,
+ /// A constant.
+ Constant,
+ /// A font family.
+ Font,
+ /// A symmie symbol.
+ Symbol(char),
+}
+
/// Complete set and show rules.
fn complete_rules(ctx: &mut CompletionContext) -> bool {
// We don't want to complete directly behind the keyword.
@@ -68,13 +76,15 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool {
// Behind the set keyword: "set |".
if matches!(prev.kind(), SyntaxKind::Set) {
- ctx.set_rule_completions(ctx.cursor);
+ ctx.from = ctx.cursor;
+ ctx.set_rule_completions();
return true;
}
// Behind the show keyword: "show |".
if matches!(prev.kind(), SyntaxKind::Show) {
- ctx.show_rule_selector_completions(ctx.cursor);
+ ctx.from = ctx.cursor;
+ ctx.show_rule_selector_completions();
return true;
}
@@ -84,7 +94,84 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool {
if matches!(prev.kind(), SyntaxKind::Colon);
if matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule));
then {
- ctx.show_rule_recipe_completions(ctx.cursor);
+ ctx.from = ctx.cursor;
+ ctx.show_rule_recipe_completions();
+ return true;
+ }
+ }
+
+ false
+}
+
+/// Complete call and set rule parameters.
+fn complete_params(ctx: &mut CompletionContext) -> bool {
+ // Ensure that we are in a function call or set rule's argument list.
+ let (callee, args) = if_chain! {
+ if let Some(parent) = ctx.leaf.parent();
+ if let Some(parent) = match parent.kind() {
+ SyntaxKind::Named => parent.parent(),
+ _ => Some(parent),
+ };
+ if let Some(args) = parent.cast::<ast::Args>();
+ if let Some(grand) = parent.parent();
+ if let Some(expr) = grand.cast::<ast::Expr>();
+ if let Some(callee) = match expr {
+ ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(),
+ ast::Expr::Set(set) => Some(set.target()),
+ _ => None,
+ };
+ then {
+ (callee, args)
+ } else {
+ return false;
+ }
+ };
+
+ // Parameter values: "func(param:|)", "func(param: |)".
+ if_chain! {
+ if let Some(prev) = ctx.leaf.prev_leaf();
+ if let Some(before_colon) = match (prev.kind(), ctx.leaf.kind()) {
+ (_, SyntaxKind::Colon) => Some(prev),
+ (SyntaxKind::Colon, _) => prev.prev_leaf(),
+ _ => None,
+ };
+ if let SyntaxKind::Ident(param) = before_colon.kind();
+ then {
+ ctx.from = match ctx.leaf.kind() {
+ SyntaxKind::Colon | SyntaxKind::Space { .. } => ctx.cursor,
+ _ => ctx.leaf.offset(),
+ };
+ ctx.param_value_completions(&callee, &param);
+ return true;
+ }
+ }
+
+ // Parameters: "func(|)", "func(hi|)", "func(12,|)".
+ if_chain! {
+ if let Some(deciding) = if ctx.leaf.kind().is_trivia() {
+ ctx.leaf.prev_leaf()
+ } else {
+ Some(ctx.leaf.clone())
+ };
+ if matches!(
+ deciding.kind(),
+ SyntaxKind::LeftParen
+ | SyntaxKind::Comma
+ | SyntaxKind::Ident(_)
+ );
+ then {
+ ctx.from = match deciding.kind() {
+ SyntaxKind::Ident(_) => deciding.offset(),
+ _ => ctx.cursor,
+ };
+
+ // Exclude arguments which are already present.
+ let exclude: Vec<_> = args.items().filter_map(|arg| match arg {
+ ast::Arg::Named(named) => Some(named.name()),
+ _ => None,
+ }).collect();
+
+ ctx.param_completions(&callee, &exclude);
return true;
}
}
@@ -98,7 +185,7 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool {
/// in `math_completions`.
fn complete_symbols(ctx: &mut CompletionContext) -> bool {
// Whether a colon is necessary.
- let needs_colon = !ctx.text[ctx.cursor..].starts_with(':');
+ let needs_colon = !ctx.after.starts_with(':');
// Behind half-completed symbol: "$arrow:|$".
if_chain! {
@@ -106,26 +193,28 @@ fn complete_symbols(ctx: &mut CompletionContext) -> bool {
if let Some(prev) = ctx.leaf.prev_leaf();
if matches!(prev.kind(), SyntaxKind::Ident(_));
then {
- ctx.symbol_completions(prev.offset(), false);
+ ctx.from = prev.offset();
+ ctx.symbol_completions(false);
return true;
}
}
// Start of a symbol: ":|".
// Checking for a text node ensures that "\:" isn't completed.
- if ctx.text[..ctx.cursor].ends_with(':')
+ if ctx.before.ends_with(':')
&& matches!(ctx.leaf.kind(), SyntaxKind::Text(_) | SyntaxKind::Atom(_))
{
- ctx.symbol_completions(ctx.cursor, needs_colon);
+ ctx.from = ctx.cursor;
+ ctx.symbol_completions(needs_colon);
return true;
}
// An existing symbol: ":arrow:".
if matches!(ctx.leaf.kind(), SyntaxKind::Symbol(_)) {
// We want to complete behind the colon, therefore plus 1.
- let has_colon = ctx.text[ctx.leaf.offset()..].starts_with(':');
- let from = ctx.leaf.offset() + (has_colon as usize);
- ctx.symbol_completions(from, has_colon && needs_colon);
+ let has_colon = ctx.after.starts_with(':');
+ ctx.from = ctx.leaf.offset() + (has_colon as usize);
+ ctx.symbol_completions(has_colon && needs_colon);
return true;
}
@@ -142,8 +231,8 @@ fn complete_symbols(ctx: &mut CompletionContext) -> bool {
);
then {
// We want to complete behind the colon, therefore plus 1.
- let from = prev.offset() + 1;
- ctx.symbol_completions(from, needs_colon);
+ ctx.from = prev.offset() + 1;
+ ctx.symbol_completions(needs_colon);
return true;
}
}
@@ -160,18 +249,17 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
// Start of an interpolated identifier: "#|".
// Checking for a text node ensures that "\#" isn't completed.
- if ctx.text[..ctx.cursor].ends_with('#')
- && matches!(ctx.leaf.kind(), SyntaxKind::Text(_))
- {
- ctx.expr_completions(ctx.cursor, true);
+ if ctx.before.ends_with('#') && matches!(ctx.leaf.kind(), SyntaxKind::Text(_)) {
+ ctx.from = ctx.cursor;
+ ctx.expr_completions(true);
return true;
}
// An existing identifier: "#pa|".
if matches!(ctx.leaf.kind(), SyntaxKind::Ident(_)) {
// We want to complete behind the hashtag, therefore plus 1.
- let from = ctx.leaf.offset() + 1;
- ctx.expr_completions(from, true);
+ ctx.from = ctx.leaf.offset() + 1;
+ ctx.expr_completions(true);
return true;
}
@@ -181,14 +269,16 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
if matches!(prev.kind(), SyntaxKind::Eq);
if matches!(prev.parent_kind(), Some(SyntaxKind::LetBinding));
then {
- ctx.expr_completions(ctx.cursor, false);
+ ctx.from = ctx.cursor;
+ ctx.expr_completions(false);
return true;
}
}
// Anywhere: "|".
if ctx.explicit {
- ctx.markup_completions(ctx.cursor);
+ ctx.from = ctx.cursor;
+ ctx.markup_completions();
return true;
}
@@ -206,21 +296,23 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
// Start of an interpolated identifier: "#|".
if matches!(ctx.leaf.kind(), SyntaxKind::Atom(s) if s == "#") {
- ctx.expr_completions(ctx.cursor, true);
+ ctx.from = ctx.cursor;
+ ctx.expr_completions(true);
return true;
}
// Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(ctx.leaf.kind(), SyntaxKind::Atom(_) | SyntaxKind::Ident(_)) {
- let from = ctx.leaf.offset();
- ctx.symbol_completions(from, false);
- ctx.scope_completions(from);
+ ctx.from = ctx.leaf.offset();
+ ctx.symbol_completions(false);
+ ctx.scope_completions();
return true;
}
// Anywhere: "$|$".
if ctx.explicit {
- ctx.math_completions(ctx.cursor);
+ ctx.from = ctx.cursor;
+ ctx.math_completions();
return true;
}
@@ -238,8 +330,8 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
// An existing identifier: "{ pa| }".
if matches!(ctx.leaf.kind(), SyntaxKind::Ident(_)) {
- let from = ctx.leaf.offset();
- ctx.expr_completions(from, true);
+ ctx.from = ctx.leaf.offset();
+ ctx.expr_completions(true);
return true;
}
@@ -249,7 +341,8 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
&& (ctx.leaf.kind().is_trivia()
|| matches!(ctx.leaf.kind(), SyntaxKind::LeftParen | SyntaxKind::LeftBrace))
{
- ctx.expr_completions(ctx.cursor, false);
+ ctx.from = ctx.cursor;
+ ctx.expr_completions(false);
return true;
}
@@ -259,7 +352,9 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
/// Context for autocompletion.
struct CompletionContext<'a> {
world: &'a dyn World,
- text: &'a str,
+ scope: &'a Scope,
+ before: &'a str,
+ after: &'a str,
leaf: LinkedNode<'a>,
cursor: usize,
explicit: bool,
@@ -275,10 +370,13 @@ impl<'a> CompletionContext<'a> {
cursor: usize,
explicit: bool,
) -> Option<Self> {
+ let text = source.text();
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
Some(Self {
world,
- text: source.text(),
+ scope: &world.library().scope,
+ before: &text[..cursor],
+ after: &text[cursor..],
leaf,
cursor,
explicit,
@@ -287,224 +385,345 @@ impl<'a> CompletionContext<'a> {
})
}
- /// Add completions for all functions from the global scope.
- fn set_rule_completions(&mut self, from: usize) {
- self.scope_completions_where(
- from,
- |value| matches!(value, Value::Func(_)),
- "(${})",
- );
+ /// Add a prefix and suffix to all applications.
+ fn enrich(&mut self, prefix: &str, suffix: &str) {
+ for Completion { label, apply, .. } in &mut self.completions {
+ let current = apply.as_ref().unwrap_or(label);
+ *apply = Some(format_eco!("{prefix}{current}{suffix}"));
+ }
}
- /// Add completions for selectors.
- fn show_rule_selector_completions(&mut self, from: usize) {
- self.snippet(
- "text selector",
- "\"${text}\": ${}",
- "Replace occurances of specific text.",
- );
+ /// Add a snippet completion.
+ fn snippet_completion(
+ &mut self,
+ label: &'static str,
+ snippet: &'static str,
+ docs: &'static str,
+ ) {
+ self.completions.push(Completion {
+ kind: CompletionKind::Syntax,
+ label: label.into(),
+ apply: Some(snippet.into()),
+ detail: Some(docs.into()),
+ });
+ }
- self.snippet(
- "regex selector",
- "regex(\"${regex}\"): ${}",
- "Replace matches of a regular expression.",
- );
+ /// Add completions for the global scope.
+ fn scope_completions(&mut self) {
+ self.scope_completions_where(|_| true);
+ }
- self.scope_completions_where(
- from,
- |value| matches!(value, Value::Func(func) if func.select(None).is_ok()),
- ": ${}",
- );
+ /// Add completions for a subset of the global scope.
+ fn scope_completions_where(&mut self, filter: impl Fn(&Value) -> bool) {
+ for (name, value) in self.scope.iter() {
+ if filter(value) {
+ self.value_completion(Some(name.clone()), value, None);
+ }
+ }
}
- /// Add completions for selectors.
- fn show_rule_recipe_completions(&mut self, from: usize) {
- self.snippet(
- "replacement",
- "[${content}]",
- "Replace the selected element with content.",
- );
+ /// Add completions for the parameters of a function.
+ fn param_completions(&mut self, callee: &ast::Ident, exclude: &[ast::Ident]) {
+ let info = if_chain! {
+ if let Some(Value::Func(func)) = self.scope.get(callee);
+ if let Some(info) = func.info();
+ then { info }
+ else { return; }
+ };
+
+ if callee.as_str() == "text" {
+ self.font_completions();
+ }
- self.snippet(
- "replacement (string)",
- "\"${text}\"",
- "Replace the selected element with a string of text.",
- );
+ for param in &info.params {
+ if exclude.iter().any(|ident| ident.as_str() == param.name) {
+ continue;
+ }
- self.snippet(
- "transformation",
- "element => [${content}]",
- "Transform the element with a function.",
- );
+ self.completions.push(Completion {
+ kind: CompletionKind::Param,
+ label: param.name.into(),
+ apply: Some(format_eco!("{}: ${{}}", param.name)),
+ detail: Some(param.docs.into()),
+ });
+
+ if param.shorthand {
+ self.cast_completions(&param.cast);
+ }
+ }
- self.scope_completions_where(from, |value| matches!(value, Value::Func(_)), "");
+ if self.before.ends_with(',') {
+ self.enrich(" ", "");
+ }
}
- /// Add completions for the global scope.
- fn scope_completions(&mut self, from: usize) {
- self.scope_completions_where(from, |_| true, "");
+ /// Add completions for the values of a function parameter.
+ fn param_value_completions(&mut self, callee: &ast::Ident, name: &str) {
+ let param = if_chain! {
+ if let Some(Value::Func(func)) = self.scope.get(callee);
+ if let Some(info) = func.info();
+ if let Some(param) = info.param(name);
+ then { param }
+ else { return; }
+ };
+
+ self.cast_completions(&param.cast);
+
+ if self.before.ends_with(':') {
+ self.enrich(" ", "");
+ }
}
- /// Add completions for a subset of the global scope.
- fn scope_completions_where(
+ /// Add completions for a castable.
+ fn cast_completions(&mut self, cast: &CastInfo) {
+ match cast {
+ CastInfo::Any => {}
+ CastInfo::Value(value, docs) => {
+ self.value_completion(None, value, Some(docs));
+ }
+ CastInfo::Type("none") => {
+ self.snippet_completion("none", "none", "Nonexistent.")
+ }
+ CastInfo::Type("auto") => {
+ self.snippet_completion("auto", "auto", "A smart default");
+ }
+ CastInfo::Type("boolean") => {
+ self.snippet_completion("false", "false", "Yes / Enabled.");
+ self.snippet_completion("true", "true", "No / Disabled.");
+ }
+ CastInfo::Type("color") => {
+ self.snippet_completion(
+ "luma()",
+ "luma(${v})",
+ "A custom grayscale color.",
+ );
+ self.snippet_completion(
+ "rgb()",
+ "rgb(${r}, ${g}, ${b}, ${a})",
+ "A custom RGBA color.",
+ );
+ self.snippet_completion(
+ "cmyk()",
+ "cmyk(${c}, ${m}, ${y}, ${k})",
+ "A custom CMYK color.",
+ );
+ self.scope_completions_where(|value| value.type_name() == "color");
+ }
+ CastInfo::Type("function") => {
+ self.snippet_completion(
+ "function",
+ "(${params}) => ${output}",
+ "A custom function.",
+ );
+ }
+ CastInfo::Type(ty) => {
+ self.completions.push(Completion {
+ kind: CompletionKind::Syntax,
+ label: (*ty).into(),
+ apply: Some(format_eco!("${{{ty}}}")),
+ detail: Some(format_eco!("A value of type {ty}.")),
+ });
+ self.scope_completions_where(|value| value.type_name() == *ty);
+ }
+ CastInfo::Union(union) => {
+ for info in union {
+ self.cast_completions(info);
+ }
+ }
+ }
+ }
+
+ /// Add a completion for a specific value.
+ fn value_completion(
&mut self,
- from: usize,
- filter: fn(&Value) -> bool,
- extra: &str,
+ label: Option<EcoString>,
+ value: &Value,
+ docs: Option<&'static str>,
) {
- self.from = from;
- for (name, value) in self.world.library().scope.iter() {
- if filter(value) {
- let apply = (!extra.is_empty()).then(|| format_eco!("{name}{extra}"));
- self.completions.push(match value {
- Value::Func(func) => Completion {
- kind: CompletionKind::Function,
- label: name.clone(),
- apply,
- detail: func.doc().map(Into::into),
- },
- v => Completion {
- kind: CompletionKind::Constant,
- label: name.clone(),
- apply,
- detail: Some(format_eco!(
- "Constant of type `{}`.",
- v.type_name()
- )),
- },
- });
+ let mut label = label.unwrap_or_else(|| value.repr().into());
+ let mut apply = None;
+
+ if matches!(value, Value::Func(_)) {
+ apply = Some(format_eco!("{label}(${{}})"));
+ label.push_str("()");
+ } else {
+ if label.starts_with('"') {
+ let trimmed = label.trim_matches('"').into();
+ apply = Some(label);
+ label = trimmed;
}
}
+
+ let detail = docs.map(Into::into).or_else(|| match value {
+ Value::Func(func) => func.info().map(|info| info.docs.into()),
+ Value::Color(color) => Some(format_eco!("The color {color:?}.")),
+ Value::Auto => Some("A smart default.".into()),
+ _ => None,
+ });
+
+ self.completions.push(Completion {
+ kind: match value {
+ Value::Func(_) => CompletionKind::Func,
+ _ => CompletionKind::Constant,
+ },
+ label,
+ apply,
+ detail,
+ });
+ }
+
+ /// Add completions for all font families.
+ fn font_completions(&mut self) {
+ for (family, iter) in self.world.book().families() {
+ let detail = summarize_font_family(iter);
+ self.completions.push(Completion {
+ kind: CompletionKind::Font,
+ label: family.into(),
+ apply: Some(format_eco!("\"{family}\"")),
+ detail: Some(detail.into()),
+ })
+ }
}
/// Add completions for all symbols.
- fn symbol_completions(&mut self, from: usize, colon: bool) {
- self.from = from;
+ fn symbol_completions(&mut self, needs_colon: bool) {
+ self.symbol_completions_where(needs_colon, |_| true);
+ }
+
+ /// Add completions for a subset of all symbols.
+ fn symbol_completions_where(
+ &mut self,
+ needs_colon: bool,
+ filter: impl Fn(char) -> bool,
+ ) {
self.completions.reserve(symmie::list().len());
for &(name, c) in symmie::list() {
- self.completions.push(Completion {
- kind: CompletionKind::Symbol,
- label: name.into(),
- apply: colon.then(|| format_eco!("{name}:")),
- detail: Some(c.into()),
- });
+ if filter(c) {
+ self.completions.push(Completion {
+ kind: CompletionKind::Symbol(c),
+ label: name.into(),
+ apply: None,
+ detail: None,
+ });
+ }
+ }
+ if needs_colon {
+ self.enrich("", ":");
}
}
/// Add completions for markup snippets.
#[rustfmt::skip]
- fn markup_completions(&mut self, from: usize) {
- self.from = from;
-
- self.snippet(
+ fn markup_completions(&mut self) {
+ self.snippet_completion(
"linebreak",
"\\\n${}",
"Inserts a forced linebreak.",
);
- self.snippet(
+ self.snippet_completion(
"symbol",
":${}:",
"Inserts a symbol.",
);
- self.snippet(
+ self.snippet_completion(
"strong text",
"*${strong}*",
"Strongly emphasizes content by increasing the font weight.",
);
- self.snippet(
+ self.snippet_completion(
"emphasized text",
"_${emphasized}_",
"Emphasizes content by setting it in italic font style.",
);
- self.snippet(
+ self.snippet_completion(
"raw text",
"`${text}`",
"Displays text verbatim, in monospace.",
);
- self.snippet(
+ self.snippet_completion(
"code listing",
"```${lang}\n${code}\n```",
"Inserts computer code with syntax highlighting.",
);
- self.snippet(
+ self.snippet_completion(
"hyperlink",
"https://${example.com}",
"Links to a URL.",
);
- self.snippet(
+ self.snippet_completion(
"math (inline)",
"$${x}$",
"Inserts an inline-level mathematical formula.",
);
- self.snippet(
+ self.snippet_completion(
"math (block)",
"$ ${sum_x^2} $",
"Inserts a block-level mathematical formula.",
);
- self.snippet(
+ self.snippet_completion(
"label",
"<${name}>",
"Makes the preceding element referencable.",
);
- self.snippet(
+ self.snippet_completion(
"reference",
"@${name}",
"Inserts a reference to a label.",
);
- self.snippet(
+ self.snippet_completion(
"heading",
"= ${title}",
"Inserts a section heading.",
);
- self.snippet(
+ self.snippet_completion(
"list item",
"- ${item}",
"Inserts an item of an unordered list.",
);
- self.snippet(
+ self.snippet_completion(
"enumeration item",
"+ ${item}",
"Inserts an item of an ordered list.",
);
- self.snippet(
+ self.snippet_completion(
"enumeration item (numbered)",
"${number}. ${item}",
"Inserts an explicitly numbered item of an ordered list.",
);
- self.snippet(
+ self.snippet_completion(
"description list item",
"/ ${term}: ${description}",
"Inserts an item of a description list.",
);
- self.snippet(
+ self.snippet_completion(
"expression",
"#${}",
"Variables, function calls, and more.",
);
- self.snippet(
+ self.snippet_completion(
"code block",
"{ ${} }",
"Switches into code mode.",
);
- self.snippet(
+ self.snippet_completion(
"content block",
"[${content}]",
"Inserts a nested content block that isolates styles.",
@@ -513,23 +732,44 @@ impl<'a> CompletionContext<'a> {
/// Add completions for math snippets.
#[rustfmt::skip]
- fn math_completions(&mut self, from: usize) {
- self.symbol_completions(from, false);
- self.scope_completions(from);
+ fn math_completions(&mut self) {
+ // Exclude non-technical symbols.
+ self.symbol_completions_where(false, |c| match c as u32 {
+ 9728..=9983 => false,
+ 9984..=10175 => false,
+ 127744..=128511 => false,
+ 128512..=128591 => false,
+ 128640..=128767 => false,
+ 129280..=129535 => false,
+ 129648..=129791 => false,
+ 127136..=127231 => false,
+ 127024..=127135 => false,
+ 126976..=127023 => false,
+ _ => true,
+ });
+
+ self.scope_completions_where(|value| {
+ matches!(
+ value,
+ Value::Func(func) if func.info().map_or(false, |info| {
+ info.tags.contains(&"math")
+ }),
+ )
+ });
- self.snippet(
+ self.snippet_completion(
"subscript",
"${x}_${2:2}",
"Sets something in subscript.",
);
- self.snippet(
+ self.snippet_completion(
"superscript",
"${x}^${2:2}",
"Sets something in superscript.",
);
- self.snippet(
+ self.snippet_completion(
"fraction",
"${x}/${y}",
"Inserts a fraction.",
@@ -538,100 +778,107 @@ impl<'a> CompletionContext<'a> {
/// Add completions for expression snippets.
#[rustfmt::skip]
- fn expr_completions(&mut self, from: usize, short_form: bool) {
- self.scope_completions(from);
+ fn expr_completions(&mut self, short_form: bool) {
+ self.scope_completions_where(|value| {
+ !short_form || matches!(
+ value,
+ Value::Func(func) if func.info().map_or(true, |info| {
+ !info.tags.contains(&"math")
+ }),
+ )
+ });
- self.snippet(
+ self.snippet_completion(
"variable",
"${variable}",
"Accesses a variable.",
);
- self.snippet(
+ self.snippet_completion(
"function call",
"${function}(${arguments})[${body}]",
"Evaluates a function.",
);
- self.snippet(
+ self.snippet_completion(
"set rule",
"set ${}",
"Sets style properties on an element.",
);
- self.snippet(
+ self.snippet_completion(
"show rule",
"show ${}",
"Redefines the look of an element.",
);
- self.snippet(
+ self.snippet_completion(
"let binding",
"let ${name} = ${value}",
"Saves a value in a variable.",
);
- self.snippet(
+ self.snippet_completion(
"let binding (function)",
"let ${name}(${params}) = ${output}",
"Defines a function.",
);
- self.snippet(
+ self.snippet_completion(
"if conditional",
"if ${1 < 2} {\n\t${}\n}",
"Computes or inserts something conditionally.",
);
- self.snippet(
+ self.snippet_completion(
"if-else conditional",
"if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}",
"Computes or inserts different things based on a condition.",
);
- self.snippet(
+ self.snippet_completion(
"while loop",
"while ${1 < 2} {\n\t${}\n}",
"Computes or inserts somthing while a condition is met.",
);
- self.snippet(
+ self.snippet_completion(
"for loop",
"for ${value} in ${(1, 2, 3)} {\n\t${}\n}",
"Computes or inserts somthing for each value in a collection.",
);
- self.snippet(
+ self.snippet_completion(
"for loop (with key)",
"for ${key}, ${value} in ${(a: 1, b: 2)} {\n\t${}\n}",
"Computes or inserts somthing for each key and value in a collection.",
);
- self.snippet(
+ self.snippet_completion(
"break",
"break",
"Exits early from a loop.",
);
- self.snippet(
+ self.snippet_completion(
"continue",
"continue",
"Continues with the next iteration of a loop.",
);
- self.snippet(
+ self.snippet_completion(
"return",
"return ${output}",
"Returns early from a function.",
);
- self.snippet(
+ self.snippet_completion(
"import",
"import ${items} from \"${file.typ}\"",
"Imports variables from another file.",
);
- self.snippet(
+ self.snippet_completion(
"include",
"include \"${file.typ}\"",
"Includes content from another file.",
@@ -641,44 +888,90 @@ impl<'a> CompletionContext<'a> {
return;
}
- self.snippet(
+ self.snippet_completion(
"code block",
"{ ${} }",
"Inserts a nested code block.",
);
- self.snippet(
+ self.snippet_completion(
"content block",
"[${content}]",
"Switches into markup mode.",
);
- self.snippet(
+ self.snippet_completion(
"array",
"(${1, 2, 3})",
"Creates a sequence of values.",
);
- self.snippet(
+ self.snippet_completion(
"dictionary",
"(${a: 1, b: 2})",
"Creates a mapping from names to value.",
);
- self.snippet(
- "anonymous function",
+ self.snippet_completion(
+ "function",
"(${params}) => ${output}",
"Creates an unnamed function.",
);
}
- /// Add a snippet completion.
- fn snippet(&mut self, label: &str, snippet: &str, detail: &str) {
- self.completions.push(Completion {
- kind: CompletionKind::Syntax,
- label: label.into(),
- apply: Some(snippet.into()),
- detail: Some(detail.into()),
+ /// Add completions for all functions from the global scope.
+ fn set_rule_completions(&mut self) {
+ self.scope_completions_where(|value| {
+ matches!(
+ value,
+ Value::Func(func) if func.info().map_or(false, |info| {
+ info.params.iter().any(|param| param.settable)
+ }),
+ )
});
}
+
+ /// Add completions for selectors.
+ fn show_rule_selector_completions(&mut self) {
+ self.scope_completions_where(
+ |value| matches!(value, Value::Func(func) if func.select(None).is_ok()),
+ );
+
+ self.enrich("", ": ");
+
+ self.snippet_completion(
+ "text selector",
+ "\"${text}\": ${}",
+ "Replace occurances of specific text.",
+ );
+
+ self.snippet_completion(
+ "regex selector",
+ "regex(\"${regex}\"): ${}",
+ "Replace matches of a regular expression.",
+ );
+ }
+
+ /// Add completions for selectors.
+ fn show_rule_recipe_completions(&mut self) {
+ self.snippet_completion(
+ "replacement",
+ "[${content}]",
+ "Replace the selected element with content.",
+ );
+
+ self.snippet_completion(
+ "replacement (string)",
+ "\"${text}\"",
+ "Replace the selected element with a string of text.",
+ );
+
+ self.snippet_completion(
+ "transformation",
+ "element => [${content}]",
+ "Transform the element with a function.",
+ );
+
+ self.scope_completions_where(|value| matches!(value, Value::Func(_)));
+ }
}
diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs
index 75539a3b..ff9b8450 100644
--- a/src/ide/highlight.rs
+++ b/src/ide/highlight.rs
@@ -1,5 +1,3 @@
-//! Syntax highlighting for Typst source code.
-
use crate::syntax::{LinkedNode, SyntaxKind};
/// Syntax highlighting categories.
@@ -162,7 +160,8 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Markup { .. }
if node.parent_kind() == Some(&SyntaxKind::DescItem)
- && node.next_sibling_kind() == Some(&SyntaxKind::Colon) =>
+ && node.next_sibling().as_ref().map(|v| v.kind())
+ == Some(&SyntaxKind::Colon) =>
{
Some(Category::ListTerm)
}
@@ -207,7 +206,8 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
}
Some(SyntaxKind::SetRule) => Some(Category::Function),
Some(SyntaxKind::ShowRule)
- if node.prev_sibling_kind() == Some(&SyntaxKind::Show) =>
+ if node.prev_sibling().as_ref().map(|v| v.kind())
+ == Some(&SyntaxKind::Show) =>
{
Some(Category::Function)
}
diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs
index df193cb2..7f6ca692 100644
--- a/src/ide/tooltip.rs
+++ b/src/ide/tooltip.rs
@@ -1,19 +1,162 @@
-use crate::model::Value;
+use std::fmt::Write;
+
+use if_chain::if_chain;
+
+use crate::font::{FontInfo, FontStyle};
+use crate::model::{CastInfo, Value};
+use crate::syntax::ast::{self, AstNode};
use crate::syntax::{LinkedNode, Source, SyntaxKind};
use crate::World;
-/// Produce a tooltip which can be shown when a cursor position is hovered.
+/// Describe the item under the cursor.
pub fn tooltip(world: &dyn World, source: &Source, cursor: usize) -> Option<String> {
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
- // If a known identifier is under the cursor, provide its documentation.
- if let SyntaxKind::Ident(ident) = leaf.kind() {
- if let Some(value) = world.library().scope.get(ident) {
- if let Value::Func(func) = value {
- return func.doc().map(Into::into);
- }
+ function_tooltip(world, &leaf)
+ .or_else(|| named_param_tooltip(world, &leaf))
+ .or_else(|| font_family_tooltip(world, &leaf))
+}
+
+/// Tooltip for a function or set rule name.
+fn function_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<String> {
+ if_chain! {
+ if let SyntaxKind::Ident(ident) = leaf.kind();
+ if matches!(
+ leaf.parent_kind(),
+ Some(SyntaxKind::FuncCall | SyntaxKind::SetRule),
+ );
+ if let Some(Value::Func(func)) = world.library().scope.get(ident);
+ if let Some(info) = func.info();
+ then {
+ return Some(info.docs.into());
}
}
None
}
+
+/// Tooltips for components of a named parameter.
+fn named_param_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<String> {
+ let (info, named) = if_chain! {
+ // Ensure that we are in a named pair in the arguments to a function
+ // call or set rule.
+ if let Some(parent) = leaf.parent();
+ if let Some(named) = parent.cast::<ast::Named>();
+ if let Some(grand) = parent.parent();
+ if matches!(grand.kind(), SyntaxKind::Args);
+ if let Some(grand_grand) = grand.parent();
+ if let Some(expr) = grand_grand.cast::<ast::Expr>();
+ if let Some(callee) = match expr {
+ ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(),
+ ast::Expr::Set(set) => Some(set.target()),
+ _ => None,
+ };
+
+ // Find metadata about the function.
+ if let Some(Value::Func(func)) = world.library().scope.get(&callee);
+ if let Some(info) = func.info();
+ then { (info, named) }
+ else { return None; }
+ };
+
+ // Hovering over the parameter name.
+ if_chain! {
+ if leaf.index() == 0;
+ if let SyntaxKind::Ident(ident) = leaf.kind();
+ if let Some(param) = info.param(ident);
+ then {
+ return Some(param.docs.into());
+ }
+ }
+
+ // Hovering over a string parameter value.
+ if_chain! {
+ if let SyntaxKind::Str(string) = leaf.kind();
+ if let Some(param) = info.param(&named.name());
+ if let Some(docs) = find_string_doc(&param.cast, string);
+ then {
+ return Some(docs.into());
+ }
+ }
+
+ None
+}
+
+/// Find documentation for a castable string.
+fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> {
+ match info {
+ CastInfo::Value(Value::Str(s), docs) if s.as_str() == string => Some(docs),
+ CastInfo::Union(options) => {
+ options.iter().find_map(|option| find_string_doc(option, string))
+ }
+ _ => None,
+ }
+}
+
+/// Tooltip for font family.
+fn font_family_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<String> {
+ if_chain! {
+ // Ensure that we are on top of a string.
+ if let SyntaxKind::Str(string) = leaf.kind();
+ let lower = string.to_lowercase();
+
+ // Ensure that we are in the arguments to the text function.
+ if let Some(parent) = leaf.parent();
+ if matches!(parent.kind(), SyntaxKind::Args);
+ if let Some(grand) = parent.parent();
+ if let Some(expr) = grand.cast::<ast::Expr>();
+ if let Some(callee) = match expr {
+ ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(),
+ ast::Expr::Set(set) => Some(set.target()),
+ _ => None,
+ };
+
+ // Find the font family.
+ if callee.as_str() == "text";
+ if let Some((_, iter)) = world
+ .book()
+ .families()
+ .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str());
+
+ then {
+ let detail = summarize_font_family(iter);
+ return Some(detail);
+ }
+ };
+
+ None
+}
+
+/// Create a short description of a font family.
+pub(super) fn summarize_font_family<'a>(
+ variants: impl Iterator<Item = &'a FontInfo>,
+) -> String {
+ let mut infos: Vec<_> = variants.collect();
+ infos.sort_by_key(|info| info.variant);
+
+ let mut has_italic = false;
+ let mut min_weight = u16::MAX;
+ let mut max_weight = 0;
+ for info in &infos {
+ let weight = info.variant.weight.to_number();
+ has_italic |= info.variant.style == FontStyle::Italic;
+ min_weight = min_weight.min(weight);
+ max_weight = min_weight.max(weight);
+ }
+
+ let count = infos.len();
+ let s = if count == 1 { "" } else { "s" };
+ let mut detail = format!("{count} variant{s}.");
+
+ if min_weight == max_weight {
+ write!(detail, " Weight {min_weight}.").unwrap();
+ } else {
+ write!(detail, " Weights {min_weight}–{max_weight}.").unwrap();
+ }
+
+ if has_italic {
+ detail.push_str(" Has italics.");
+ }
+
+ detail
+}
diff --git a/src/lib.rs b/src/lib.rs
index 631f1042..4045c02d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,6 +29,8 @@
//! [PDF]: export::pdf
//! [raster images]: export::render
+#![recursion_limit = "1000"]
+
extern crate self as typst;
#[macro_use]
diff --git a/src/model/cast.rs b/src/model/cast.rs
index bfde1bdd..833b9e9e 100644
--- a/src/model/cast.rs
+++ b/src/model/cast.rs
@@ -1,15 +1,19 @@
use std::num::NonZeroUsize;
+use std::ops::Add;
use std::str::FromStr;
-use super::{Content, Regex, Selector, Transform, Value};
-use crate::diag::{with_alternative, StrResult};
+use super::{
+ castable, Array, Content, Dict, Func, Label, Regex, Selector, Str, Transform, Value,
+};
+use crate::diag::StrResult;
use crate::doc::{Destination, Lang, Location, Region};
use crate::font::{FontStretch, FontStyle, FontWeight};
use crate::geom::{
- Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides,
+ Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio,
+ Rel, Sides, Smart,
};
use crate::syntax::Spanned;
-use crate::util::{format_eco, EcoString};
+use crate::util::EcoString;
/// Cast from a value to a specific type.
pub trait Cast<V = Value>: Sized {
@@ -18,95 +22,98 @@ pub trait Cast<V = Value>: Sized {
/// Try to cast the value into an instance of `Self`.
fn cast(value: V) -> StrResult<Self>;
+
+ /// Describe the acceptable values.
+ fn describe() -> CastInfo;
+
+ /// Produce an error for an inacceptable value.
+ fn error(value: Value) -> StrResult<Self> {
+ Err(Self::describe().error(&value))
+ }
}
-/// Implement traits for dynamic types.
-#[macro_export]
-#[doc(hidden)]
-macro_rules! __dynamic {
- ($type:ty: $name:literal, $($tts:tt)*) => {
- impl $crate::model::Type for $type {
- const TYPE_NAME: &'static str = $name;
+/// Describes a possible value for a cast.
+#[derive(Debug, Clone)]
+pub enum CastInfo {
+ /// Any value is okay.
+ Any,
+ /// A specific value, plus short documentation for that value.
+ Value(Value, &'static str),
+ /// Any value of a type.
+ Type(&'static str),
+ /// Multiple alternatives.
+ Union(Vec<Self>),
+}
+
+impl CastInfo {
+ /// Produce an error message describing what was expected and what was
+ /// found.
+ pub fn error(&self, found: &Value) -> EcoString {
+ fn accumulate(
+ info: &CastInfo,
+ found: &Value,
+ parts: &mut Vec<EcoString>,
+ matching_type: &mut bool,
+ ) {
+ match info {
+ CastInfo::Any => parts.push("anything".into()),
+ CastInfo::Value(value, _) => {
+ parts.push(value.repr().into());
+ if value.type_name() == found.type_name() {
+ *matching_type = true;
+ }
+ }
+ CastInfo::Type(ty) => parts.push((*ty).into()),
+ CastInfo::Union(options) => {
+ for option in options {
+ accumulate(option, found, parts, matching_type);
+ }
+ }
+ }
}
- castable! {
- $type,
- Expected: <Self as $crate::model::Type>::TYPE_NAME,
- $($tts)*
- @this: Self => this.clone(),
+ let mut matching_type = false;
+ let mut parts = vec![];
+ accumulate(self, found, &mut parts, &mut matching_type);
+
+ let mut msg = String::from("expected ");
+ if parts.is_empty() {
+ msg.push_str(" nothing");
}
- impl From<$type> for $crate::model::Value {
- fn from(v: $type) -> Self {
- $crate::model::Value::Dyn($crate::model::Dynamic::new(v))
- }
+ crate::diag::comma_list(&mut msg, &parts, "or");
+
+ if !matching_type {
+ msg.push_str(", found ");
+ msg.push_str(found.type_name());
}
- };
+
+ msg.into()
+ }
}
-#[doc(inline)]
-pub use crate::__dynamic as dynamic;
+impl Add for CastInfo {
+ type Output = Self;
-/// Make a type castable from a value.
-#[macro_export]
-#[doc(hidden)]
-macro_rules! __castable {
- ($type:ty: $inner:ty) => {
- impl $crate::model::Cast<$crate::model::Value> for $type {
- fn is(value: &$crate::model::Value) -> bool {
- <$inner>::is(value)
+ fn add(self, rhs: Self) -> Self {
+ Self::Union(match (self, rhs) {
+ (Self::Union(mut lhs), Self::Union(rhs)) => {
+ lhs.extend(rhs);
+ lhs
}
-
- fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> {
- <$inner>::cast(value).map(Self)
+ (Self::Union(mut lhs), rhs) => {
+ lhs.push(rhs);
+ lhs
}
- }
- };
-
- (
- $type:ty,
- Expected: $expected:expr,
- $($pattern:pat => $out:expr,)*
- $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)*
- ) => {
- #[allow(unreachable_patterns)]
- impl $crate::model::Cast<$crate::model::Value> for $type {
- fn is(value: &$crate::model::Value) -> bool {
- #[allow(unused_variables)]
- match value {
- $($pattern => true,)*
- $crate::model::Value::Dyn(dynamic) => {
- false $(|| dynamic.is::<$dyn_type>())*
- }
- _ => false,
- }
- }
-
- fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> {
- let found = match value {
- $($pattern => return Ok($out),)*
- $crate::model::Value::Dyn(dynamic) => {
- $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() {
- return Ok($dyn_out);
- })*
- dynamic.type_name()
- }
- v => v.type_name(),
- };
-
- Err($crate::util::format_eco!(
- "expected {}, found {}",
- $expected,
- found,
- ))
+ (lhs, Self::Union(mut rhs)) => {
+ rhs.insert(0, lhs);
+ rhs
}
- }
- };
+ (lhs, rhs) => vec![lhs, rhs],
+ })
+ }
}
-#[doc(inline)]
-pub use crate::__castable as castable;
-
impl Cast for Value {
fn is(_: &Value) -> bool {
true
@@ -115,6 +122,10 @@ impl Cast for Value {
fn cast(value: Value) -> StrResult<Self> {
Ok(value)
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Any
+ }
}
impl<T: Cast> Cast<Spanned<Value>> for T {
@@ -125,6 +136,10 @@ impl<T: Cast> Cast<Spanned<Value>> for T {
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
+
+ fn describe() -> CastInfo {
+ T::describe()
+ }
}
impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
@@ -136,14 +151,64 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span))
}
+
+ fn describe() -> CastInfo {
+ T::describe()
+ }
+}
+
+castable! {
+ Dir: "direction",
+}
+
+castable! {
+ GenAlign: "alignment",
+}
+
+castable! {
+ Regex: "regular expression",
+}
+
+castable! {
+ Selector: "selector",
+ text: EcoString => Self::text(&text),
+ label: Label => Self::Label(label),
+ func: Func => func.select(None)?,
+ regex: Regex => Self::Regex(regex),
+}
+
+castable! {
+ Axes<GenAlign>: "2d alignment",
+}
+
+castable! {
+ PartialStroke: "stroke",
+ thickness: Length => Self {
+ paint: Smart::Auto,
+ thickness: Smart::Custom(thickness),
+ },
+ color: Color => Self {
+ paint: Smart::Custom(color.into()),
+ thickness: Smart::Auto,
+ },
+}
+
+castable! {
+ u32,
+ int: i64 => int.try_into().map_err(|_| {
+ if int < 0 {
+ "number must be at least zero"
+ } else {
+ "number too large"
+ }
+ })?,
}
castable! {
usize,
- Expected: "non-negative integer",
- Value::Int(int) => int.try_into().map_err(|_| {
+ int: i64 => int.try_into().map_err(|_| {
if int < 0 {
- "must be at least zero"
+ "number must be at least zero"
} else {
"number too large"
}
@@ -152,12 +217,11 @@ castable! {
castable! {
NonZeroUsize,
- Expected: "positive integer",
- Value::Int(int) => int
+ int: i64 => int
.try_into()
.and_then(|int: usize| int.try_into())
.map_err(|_| if int <= 0 {
- "must be positive"
+ "number must be positive"
} else {
"number too large"
})?,
@@ -165,41 +229,23 @@ castable! {
castable! {
Paint,
- Expected: "color",
- Value::Color(color) => Paint::Solid(color),
+ color: Color => Self::Solid(color),
}
castable! {
EcoString,
- Expected: "string",
- Value::Str(str) => str.into(),
+ string: Str => string.into(),
}
castable! {
String,
- Expected: "string",
- Value::Str(string) => string.into(),
-}
-
-dynamic! {
- Regex: "regular expression",
-}
-
-dynamic! {
- Selector: "selector",
- Value::Str(text) => Self::text(&text),
- Value::Label(label) => Self::Label(label),
- Value::Func(func) => func.select(None)?,
- @regex: Regex => Self::Regex(regex.clone()),
+ string: Str => string.into(),
}
castable! {
Transform,
- Expected: "content or function",
- Value::None => Self::Content(Content::empty()),
- Value::Str(text) => Self::Content(item!(text)(text.into())),
- Value::Content(content) => Self::Content(content),
- Value::Func(func) => {
+ content: Content => Self::Content(content),
+ func: Func => {
if func.argc().map_or(false, |count| count != 1) {
Err("function must have exactly one parameter")?
}
@@ -207,45 +253,19 @@ castable! {
},
}
-dynamic! {
- Dir: "direction",
-}
-
-dynamic! {
- GenAlign: "alignment",
-}
-
-dynamic! {
- Axes<GenAlign>: "2d alignment",
-}
-
castable! {
Axes<Option<GenAlign>>,
- Expected: "1d or 2d alignment",
- @align: GenAlign => {
+ align: GenAlign => {
let mut aligns = Axes::default();
- aligns.set(align.axis(), Some(*align));
+ aligns.set(align.axis(), Some(align));
aligns
},
- @aligns: Axes<GenAlign> => aligns.map(Some),
-}
-
-dynamic! {
- PartialStroke: "stroke",
- Value::Length(thickness) => Self {
- paint: Smart::Auto,
- thickness: Smart::Custom(thickness),
- },
- Value::Color(color) => Self {
- paint: Smart::Custom(color.into()),
- thickness: Smart::Auto,
- },
+ aligns: Axes<GenAlign> => aligns.map(Some),
}
castable! {
Axes<Rel<Length>>,
- Expected: "array of two relative lengths",
- Value::Array(array) => {
+ array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
@@ -256,142 +276,124 @@ castable! {
castable! {
Location,
- Expected: "dictionary with `page`, `x`, and `y` keys",
- Value::Dict(dict) => {
- let page = dict.get("page")?.clone().cast()?;
- let x: Length = dict.get("x")?.clone().cast()?;
- let y: Length = dict.get("y")?.clone().cast()?;
+ mut dict: Dict => {
+ let page = dict.take("page")?.cast()?;
+ let x: Length = dict.take("x")?.cast()?;
+ let y: Length = dict.take("y")?.cast()?;
+ dict.finish(&["page", "x", "y"])?;
Self { page, pos: Point::new(x.abs, y.abs) }
},
}
castable! {
Destination,
- Expected: "string or dictionary with `page`, `x`, and `y` keys",
- Value::Str(string) => Self::Url(string.into()),
- v @ Value::Dict(_) => Self::Internal(v.cast()?),
+ loc: Location => Self::Internal(loc),
+ string: EcoString => Self::Url(string),
}
castable! {
FontStyle,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "italic" => Self::Italic,
- "oblique" => Self::Oblique,
- _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
- },
+ /// The default style.
+ "normal" => Self::Normal,
+ /// A cursive style.
+ "italic" => Self::Italic,
+ /// A slanted style.
+ "oblique" => Self::Oblique,
}
castable! {
FontWeight,
- Expected: "integer or string",
- Value::Int(v) => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
- Value::Str(string) => match string.as_str() {
- "thin" => Self::THIN,
- "extralight" => Self::EXTRALIGHT,
- "light" => Self::LIGHT,
- "regular" => Self::REGULAR,
- "medium" => Self::MEDIUM,
- "semibold" => Self::SEMIBOLD,
- "bold" => Self::BOLD,
- "extrabold" => Self::EXTRABOLD,
- "black" => Self::BLACK,
- _ => Err("unknown font weight")?,
- },
+ v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
+ /// Thin weight (100).
+ "thin" => Self::THIN,
+ /// Extra light weight (200).
+ "extralight" => Self::EXTRALIGHT,
+ /// Light weight (300).
+ "light" => Self::LIGHT,
+ /// Regular weight (400).
+ "regular" => Self::REGULAR,
+ /// Medium weight (500).
+ "medium" => Self::MEDIUM,
+ /// Semibold weight (600).
+ "semibold" => Self::SEMIBOLD,
+ /// Bold weight (700).
+ "bold" => Self::BOLD,
+ /// Extrabold weight (800).
+ "extrabold" => Self::EXTRABOLD,
+ /// Black weight (900).
+ "black" => Self::BLACK,
}
castable! {
FontStretch,
- Expected: "ratio",
- Value::Ratio(v) => Self::from_ratio(v.get() as f32),
+ v: Ratio => Self::from_ratio(v.get() as f32),
}
castable! {
Lang,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)?,
+ string: EcoString => Self::from_str(&string)?,
}
castable! {
Region,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)?,
+ string: EcoString => Self::from_str(&string)?,
}
-impl<T: Cast> Cast for Option<T> {
+/// Castable from [`Value::None`].
+pub struct NoneValue;
+
+impl Cast for NoneValue {
fn is(value: &Value) -> bool {
- matches!(value, Value::None) || T::is(value)
+ matches!(value, Value::None)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
- Value::None => Ok(None),
- v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")),
+ Value::None => Ok(Self),
+ _ => <Self as Cast>::error(value),
}
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Type("none")
+ }
}
-/// A value that can be automatically determined.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub enum Smart<T> {
- /// The value should be determined smartly based on the circumstances.
- Auto,
- /// A specific value.
- Custom(T),
-}
-
-impl<T> Smart<T> {
- /// Map the contained custom value with `f`.
- pub fn map<F, U>(self, f: F) -> Smart<U>
- where
- F: FnOnce(T) -> U,
- {
- match self {
- Self::Auto => Smart::Auto,
- Self::Custom(x) => Smart::Custom(f(x)),
- }
+impl<T: Cast> Cast for Option<T> {
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::None) || T::is(value)
}
- /// Keeps `self` if it contains a custom value, otherwise returns `other`.
- pub fn or(self, other: Smart<T>) -> Self {
- match self {
- Self::Custom(x) => Self::Custom(x),
- Self::Auto => other,
+ fn cast(value: Value) -> StrResult<Self> {
+ match value {
+ Value::None => Ok(None),
+ v if T::is(&v) => Ok(Some(T::cast(v)?)),
+ _ => <Self as Cast>::error(value),
}
}
- /// Returns the contained custom value or a provided default value.
- pub fn unwrap_or(self, default: T) -> T {
- match self {
- Self::Auto => default,
- Self::Custom(x) => x,
- }
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("none")
}
+}
- /// Returns the contained custom value or computes a default value.
- pub fn unwrap_or_else<F>(self, f: F) -> T
- where
- F: FnOnce() -> T,
- {
- match self {
- Self::Auto => f(),
- Self::Custom(x) => x,
- }
+/// Castable from [`Value::Auto`].
+pub struct AutoValue;
+
+impl Cast for AutoValue {
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Auto)
}
- /// Returns the contained custom value or the default value.
- pub fn unwrap_or_default(self) -> T
- where
- T: Default,
- {
- self.unwrap_or_else(T::default)
+ fn cast(value: Value) -> StrResult<Self> {
+ match value {
+ Value::Auto => Ok(Self),
+ _ => <Self as Cast>::error(value),
+ }
}
-}
-impl<T> Default for Smart<T> {
- fn default() -> Self {
- Self::Auto
+ fn describe() -> CastInfo {
+ CastInfo::Type("auto")
}
}
@@ -403,11 +405,14 @@ impl<T: Cast> Cast for Smart<T> {
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Auto => Ok(Self::Auto),
- v => T::cast(v)
- .map(Self::Custom)
- .map_err(|msg| with_alternative(msg, "auto")),
+ v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)),
+ _ => <Self as Cast>::error(value),
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("auto")
+ }
}
impl<T> Cast for Sides<T>
@@ -420,7 +425,7 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
- let mut take = |key| dict.take(key).map(T::cast).transpose();
+ let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let x = take("x")?.or(rest);
@@ -432,22 +437,19 @@ where
bottom: take("bottom")?.or(y),
};
- if let Some((key, _)) = dict.iter().next() {
- return Err(format_eco!("unexpected key {key:?}"));
- }
+ dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
Ok(sides.map(Option::unwrap_or_default))
+ } else if T::is(&value) {
+ Ok(Self::splat(T::cast(value)?))
} else {
- T::cast(value).map(Self::splat).map_err(|msg| {
- with_alternative(
- msg,
- "dictionary with any of \
- `left`, `top`, `right`, `bottom`, \
- `x`, `y`, or `rest` as keys",
- )
- })
+ <Self as Cast>::error(value)
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
}
impl<T> Cast for Corners<T>
@@ -460,7 +462,7 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
- let mut take = |key| dict.take(key).map(T::cast).transpose();
+ let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
let rest = take("rest")?;
let left = take("left")?.or(rest);
@@ -474,20 +476,27 @@ where
bottom_left: take("bottom-left")?.or(bottom).or(left),
};
- if let Some((key, _)) = dict.iter().next() {
- return Err(format_eco!("unexpected key {key:?}"));
- }
+ dict.finish(&[
+ "top-left",
+ "top-right",
+ "bottom-right",
+ "bottom-left",
+ "left",
+ "top",
+ "right",
+ "bottom",
+ "rest",
+ ])?;
Ok(corners.map(Option::unwrap_or_default))
+ } else if T::is(&value) {
+ Ok(Self::splat(T::cast(value)?))
} else {
- T::cast(value).map(Self::splat).map_err(|msg| {
- with_alternative(
- msg,
- "dictionary with any of \
- `top-left`, `top-right`, `bottom-right`, `bottom-left`, \
- `left`, `top`, `right`, `bottom`, or `rest` as keys",
- )
- })
+ <Self as Cast>::error(value)
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
}
diff --git a/src/model/content.rs b/src/model/content.rs
index e73fa4a8..df910a58 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -11,7 +11,8 @@ use thin_vec::ThinVec;
use typst_macros::node;
use super::{
- capability, capable, Args, Guard, Key, Property, Recipe, Style, StyleMap, Value, Vm,
+ capability, capable, Args, Guard, Key, ParamInfo, Property, Recipe, Style, StyleMap,
+ Value, Vm,
};
use crate::diag::{SourceResult, StrResult};
use crate::syntax::Span;
@@ -426,6 +427,11 @@ pub trait Node: 'static + Capable {
where
Self: Sized;
+ /// List the settable properties.
+ fn properties() -> Vec<ParamInfo>
+ where
+ Self: Sized;
+
/// Access a field on this node.
fn field(&self, name: &str) -> Option<Value>;
}
diff --git a/src/model/dict.rs b/src/model/dict.rs
index d54a0e82..6e014d7e 100644
--- a/src/model/dict.rs
+++ b/src/model/dict.rs
@@ -62,6 +62,13 @@ impl Dict {
Arc::make_mut(&mut self.0).entry(key).or_default()
}
+ /// Remove the value if the dictionary contains the given key.
+ pub fn take(&mut self, key: &str) -> StrResult<Value> {
+ Arc::make_mut(&mut self.0)
+ .remove(key)
+ .ok_or_else(|| format_eco!("missing key: {:?}", Str::from(key)))
+ }
+
/// Whether the dictionary contains a specific key.
pub fn contains(&self, key: &str) -> bool {
self.0.contains_key(key)
@@ -80,11 +87,6 @@ impl Dict {
}
}
- /// Remove the value if the dictionary contains the given key.
- pub fn take(&mut self, key: &str) -> Option<Value> {
- Arc::make_mut(&mut self.0).remove(key)
- }
-
/// Clear the dictionary.
pub fn clear(&mut self) {
if Arc::strong_count(&self.0) == 1 {
@@ -118,6 +120,17 @@ impl Dict {
pub fn iter(&self) -> std::collections::btree_map::Iter<Str, Value> {
self.0.iter()
}
+
+ /// Return an "unexpected key" error if there is any remaining pair.
+ pub fn finish(&self, expected: &[&str]) -> StrResult<()> {
+ if let Some((key, _)) = self.iter().next() {
+ let parts: Vec<_> = expected.iter().map(|s| format_eco!("\"{s}\"")).collect();
+ let mut msg = format!("unexpected key {key:?}, valid keys are ");
+ crate::diag::comma_list(&mut msg, &parts, "and");
+ return Err(msg.into());
+ }
+ Ok(())
+ }
}
/// The missing key access error message.
diff --git a/src/model/func.rs b/src/model/func.rs
index 0261b5e2..5b38b700 100644
--- a/src/model/func.rs
+++ b/src/model/func.rs
@@ -5,8 +5,8 @@ use std::sync::Arc;
use comemo::{Track, Tracked};
use super::{
- Args, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, StyleMap,
- Value, Vm,
+ Args, CastInfo, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector,
+ StyleMap, Value, Vm,
};
use crate::diag::{bail, SourceResult, StrResult};
use crate::syntax::ast::{self, AstNode, Expr};
@@ -39,13 +39,14 @@ impl Func {
pub fn from_fn(
name: &'static str,
func: fn(&Vm, &mut Args) -> SourceResult<Value>,
- doc: &'static str,
+ info: FuncInfo,
) -> Self {
- Self(Arc::new(Repr::Native(Native { name, func, set: None, node: None, doc })))
+ Self(Arc::new(Repr::Native(Native { name, func, set: None, node: None, info })))
}
/// Create a new function from a native rust node.
- pub fn from_node<T: Node>(name: &'static str, doc: &'static str) -> Self {
+ pub fn from_node<T: Node>(name: &'static str, mut info: FuncInfo) -> Self {
+ info.params.extend(T::properties());
Self(Arc::new(Repr::Native(Native {
name,
func: |ctx, args| {
@@ -55,7 +56,7 @@ impl Func {
},
set: Some(|args| T::set(args, false)),
node: Some(NodeId::of::<T>()),
- doc,
+ info,
})))
}
@@ -73,11 +74,11 @@ impl Func {
}
}
- /// Documentation for the function.
- pub fn doc(&self) -> Option<&str> {
+ /// Extract details the function.
+ pub fn info(&self) -> Option<&FuncInfo> {
match self.0.as_ref() {
- Repr::Native(native) => Some(native.doc),
- Repr::With(func, _) => func.doc(),
+ Repr::Native(native) => Some(&native.info),
+ Repr::With(func, _) => func.info(),
_ => None,
}
}
@@ -192,7 +193,7 @@ struct Native {
/// The id of the node to customize with this function's show rule.
node: Option<NodeId>,
/// Documentation of the function.
- doc: &'static str,
+ info: FuncInfo,
}
impl Hash for Native {
@@ -201,10 +202,44 @@ impl Hash for Native {
(self.func as usize).hash(state);
self.set.map(|set| set as usize).hash(state);
self.node.hash(state);
- self.doc.hash(state);
}
}
+/// Details about a function.
+#[derive(Debug, Clone)]
+pub struct FuncInfo {
+ /// The function's name.
+ pub name: &'static str,
+ /// Tags that categorize the function.
+ pub tags: &'static [&'static str],
+ /// Documentation for the function.
+ pub docs: &'static str,
+ /// Details about the function's parameters.
+ pub params: Vec<ParamInfo>,
+}
+
+impl FuncInfo {
+ /// Get the parameter info for a parameter with the given name
+ pub fn param(&self, name: &str) -> Option<&ParamInfo> {
+ self.params.iter().find(|param| param.name == name)
+ }
+}
+
+/// Describes a named parameter.
+#[derive(Debug, Clone)]
+pub struct ParamInfo {
+ /// The parameter's name.
+ pub name: &'static str,
+ /// Documentation for the parameter.
+ pub docs: &'static str,
+ /// Is the parameter settable with a set rule?
+ pub settable: bool,
+ /// Can the name be omitted?
+ pub shorthand: bool,
+ /// Valid values for the parameter.
+ pub cast: CastInfo,
+}
+
/// A user-defined closure.
#[derive(Hash)]
pub(super) struct Closure {
diff --git a/src/model/mod.rs b/src/model/mod.rs
index 015df9b3..6ba8014c 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -26,7 +26,7 @@ mod typeset;
#[doc(hidden)]
pub use once_cell;
-pub use typst_macros::{capability, capable, func, node};
+pub use typst_macros::{capability, capable, castable, func, node};
pub use self::args::*;
pub use self::array::*;
diff --git a/src/model/ops.rs b/src/model/ops.rs
index 1a8dcb6b..9da9b0cc 100644
--- a/src/model/ops.rs
+++ b/src/model/ops.rs
@@ -1,8 +1,8 @@
//! Operations on values.
-use super::{Regex, Smart, Value};
+use super::{Regex, Value};
use crate::diag::StrResult;
-use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel};
+use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart};
use crate::util::format_eco;
use std::cmp::Ordering;
use Value::*;
diff --git a/src/model/str.rs b/src/model/str.rs
index 0c288d9b..d1bf9d23 100644
--- a/src/model/str.rs
+++ b/src/model/str.rs
@@ -442,9 +442,8 @@ pub enum StrPattern {
castable! {
StrPattern,
- Expected: "string or regular expression",
- Value::Str(text) => Self::Str(text),
- @regex: Regex => Self::Regex(regex.clone()),
+ text: Str => Self::Str(text),
+ regex: Regex => Self::Regex(regex),
}
/// A side of a string.
@@ -459,8 +458,7 @@ pub enum StrSide {
castable! {
StrSide,
- Expected: "start or end",
- @align: GenAlign => match align {
+ align: GenAlign => match align {
GenAlign::Start => Self::Start,
GenAlign::End => Self::End,
_ => Err("expected either `start` or `end`")?,
diff --git a/src/model/styles.rs b/src/model/styles.rs
index b2c328fa..1eaf5128 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -7,10 +7,11 @@ use std::sync::Arc;
use comemo::{Prehashed, Tracked};
-use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Smart, Value};
+use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Value};
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::geom::{
Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides,
+ Smart,
};
use crate::syntax::Span;
use crate::util::ReadableTypeId;
diff --git a/src/model/value.rs b/src/model/value.rs
index 98d11e15..1c687d8d 100644
--- a/src/model/value.rs
+++ b/src/model/value.rs
@@ -6,7 +6,9 @@ use std::sync::Arc;
use siphasher::sip128::{Hasher128, SipHasher};
-use super::{format_str, ops, Args, Array, Cast, Content, Dict, Func, Label, Str};
+use super::{
+ format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Str,
+};
use crate::diag::StrResult;
use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor};
use crate::util::{format_eco, EcoString};
@@ -351,6 +353,10 @@ macro_rules! primitive {
)),
}
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Type(Self::TYPE_NAME)
+ }
}
impl From<$type> for Value {
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
index 77c788d3..56d4415e 100644
--- a/src/syntax/ast.rs
+++ b/src/syntax/ast.rs
@@ -1577,12 +1577,17 @@ impl Ident {
_ => panic!("identifier is of wrong kind"),
}
}
+
+ /// Get the identifier as a string slice.
+ pub fn as_str(&self) -> &str {
+ self.get()
+ }
}
impl Deref for Ident {
type Target = str;
fn deref(&self) -> &Self::Target {
- self.get()
+ self.as_str()
}
}
diff --git a/src/syntax/linked.rs b/src/syntax/linked.rs
index 0d9d0c78..2826835e 100644
--- a/src/syntax/linked.rs
+++ b/src/syntax/linked.rs
@@ -29,6 +29,11 @@ impl<'a> LinkedNode<'a> {
self.node
}
+ /// The index of this node in its parent's children list.
+ pub fn index(&self) -> usize {
+ self.index
+ }
+
/// The absolute byte offset of the this node in the source file.
pub fn offset(&self) -> usize {
self.offset
@@ -40,18 +45,13 @@ impl<'a> LinkedNode<'a> {
}
/// Get this node's children.
- pub fn children(
- &self,
- ) -> impl DoubleEndedIterator<Item = LinkedNode<'a>>
- + ExactSizeIterator<Item = LinkedNode<'a>>
- + '_ {
- let parent = Rc::new(self.clone());
- let mut offset = self.offset;
- self.node.children().enumerate().map(move |(index, node)| {
- let child = Self { node, parent: Some(parent.clone()), index, offset };
- offset += node.len();
- child
- })
+ pub fn children(&self) -> LinkedChildren<'a> {
+ LinkedChildren {
+ parent: Rc::new(self.clone()),
+ iter: self.node.children().enumerate(),
+ front: self.offset,
+ back: self.offset + self.len(),
+ }
}
}
@@ -64,7 +64,7 @@ impl<'a> LinkedNode<'a> {
/// Get the kind of this node's parent.
pub fn parent_kind(&self) -> Option<&'a SyntaxKind> {
- self.parent().map(|parent| parent.node.kind())
+ Some(self.parent()?.node.kind())
}
/// Get the first previous non-trivia sibling node.
@@ -81,11 +81,6 @@ impl<'a> LinkedNode<'a> {
}
}
- /// Get the kind of this node's first previous non-trivia sibling.
- pub fn prev_sibling_kind(&self) -> Option<&'a SyntaxKind> {
- self.prev_sibling().map(|parent| parent.node.kind())
- }
-
/// Get the next non-trivia sibling node.
pub fn next_sibling(&self) -> Option<Self> {
let parent = self.parent()?;
@@ -99,11 +94,6 @@ impl<'a> LinkedNode<'a> {
Some(next)
}
}
-
- /// Get the kind of this node's next non-trivia sibling.
- pub fn next_sibling_kind(&self) -> Option<&'a SyntaxKind> {
- self.next_sibling().map(|parent| parent.node.kind())
- }
}
/// Access to leafs.
@@ -198,6 +188,51 @@ impl Debug for LinkedNode<'_> {
}
}
+/// An iterator over the children of a linked node.
+pub struct LinkedChildren<'a> {
+ parent: Rc<LinkedNode<'a>>,
+ iter: std::iter::Enumerate<std::slice::Iter<'a, SyntaxNode>>,
+ front: usize,
+ back: usize,
+}
+
+impl<'a> Iterator for LinkedChildren<'a> {
+ type Item = LinkedNode<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.iter.next().map(|(index, node)| {
+ let offset = self.front;
+ self.front += node.len();
+ LinkedNode {
+ node,
+ parent: Some(self.parent.clone()),
+ index,
+ offset,
+ }
+ })
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.iter.size_hint()
+ }
+}
+
+impl DoubleEndedIterator for LinkedChildren<'_> {
+ fn next_back(&mut self) -> Option<Self::Item> {
+ self.iter.next_back().map(|(index, node)| {
+ self.back -= node.len();
+ LinkedNode {
+ node,
+ parent: Some(self.parent.clone()),
+ index,
+ offset: self.back,
+ }
+ })
+ }
+}
+
+impl ExactSizeIterator for LinkedChildren<'_> {}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -236,15 +271,4 @@ mod tests {
assert_eq!(leaf.kind(), &SyntaxKind::Space { newlines: 0 });
assert_eq!(next.kind(), &SyntaxKind::Int(10));
}
-
- #[test]
- fn test_linked_node_leaf_at() {
- let source = Source::detached("");
- let leaf = LinkedNode::new(source.root()).leaf_at(0).unwrap();
- assert_eq!(leaf.kind(), &SyntaxKind::Markup { min_indent: 0 });
-
- let source = Source::detached("Hello\n");
- let leaf = LinkedNode::new(source.root()).leaf_at(6).unwrap();
- assert_eq!(leaf.kind(), &SyntaxKind::Space { newlines: 1 });
- }
}
diff --git a/tests/src/tests.rs b/tests/src/tests.rs
index 06162df0..7c1e39b7 100644
--- a/tests/src/tests.rs
+++ b/tests/src/tests.rs
@@ -14,8 +14,8 @@ use tiny_skia as sk;
use typst::diag::{bail, FileError, FileResult, SourceResult};
use typst::doc::{Document, Element, Frame, Meta};
use typst::font::{Font, FontBook};
-use typst::geom::{Abs, RgbaColor, Sides};
-use typst::model::{func, Library, Smart, Value};
+use typst::geom::{Abs, RgbaColor, Sides, Smart};
+use typst::model::{func, Library, Value};
use typst::syntax::{Source, SourceId, SyntaxNode};
use typst::util::{Buffer, PathExt};
use typst::World;
diff --git a/tests/typ/basics/table.typ b/tests/typ/basics/table.typ
index 527141c5..e806b1aa 100644
--- a/tests/typ/basics/table.typ
+++ b/tests/typ/basics/table.typ
@@ -18,5 +18,5 @@
#table()
---
-// Error: 14-19 expected color or none or function, found string
+// Error: 14-19 expected color, none, or function, found string
#table(fill: "hey")
diff --git a/tests/typ/compiler/show-node.typ b/tests/typ/compiler/show-node.typ
index 98f36f13..f14fb002 100644
--- a/tests/typ/compiler/show-node.typ
+++ b/tests/typ/compiler/show-node.typ
@@ -96,7 +96,7 @@ Another text.
= Heading
---
-// Error: 7-10 expected selector, found color
+// Error: 7-10 expected string, label, function, regular expression, or selector, found color
#show red: []
---
diff --git a/tests/typ/compute/utility.typ b/tests/typ/compute/utility.typ
index cfc2e8af..c99c0858 100644
--- a/tests/typ/compute/utility.typ
+++ b/tests/typ/compute/utility.typ
@@ -41,9 +41,9 @@
}
---
-// Error: 17-18 must be positive
+// Error: 17-18 number must be positive
#numbering("1", 0)
---
-// Error: 17-19 must be positive
+// Error: 17-19 number must be positive
#numbering("1", -1)
diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ
index 315393a0..8702688c 100644
--- a/tests/typ/layout/columns.typ
+++ b/tests/typ/layout/columns.typ
@@ -101,5 +101,5 @@ This is a normal page. Very normal.
---
// Test a page with zero columns.
-// Error: 49-50 must be positive
+// Error: 49-50 number must be positive
#set page(height: auto, width: 7.05cm, columns: 0)
diff --git a/tests/typ/text/edge.typ b/tests/typ/text/edge.typ
index c3c60b28..26be4aeb 100644
--- a/tests/typ/text/edge.typ
+++ b/tests/typ/text/edge.typ
@@ -17,9 +17,9 @@
#try(1pt + 0.3em, -0.15em)
---
-// Error: 21-23 expected string or length, found array
+// Error: 21-23 expected length, "ascender", "cap-height", "x-height", "baseline", or "descender", found array
#set text(top-edge: ())
---
-// Error: 24-26 unknown font metric
+// Error: 24-26 expected length, "ascender", "cap-height", "x-height", "baseline", or "descender"
#set text(bottom-edge: "")
diff --git a/tests/typ/text/features.typ b/tests/typ/text/features.typ
index 070fdcdf..cae240f3 100644
--- a/tests/typ/text/features.typ
+++ b/tests/typ/text/features.typ
@@ -55,9 +55,13 @@ fi vs. #text(features: (liga: 0))[No fi]
#set text(stylistic-set: 25)
---
-// Error: 24-25 expected string or auto, found integer
+// Error: 24-25 expected "lining", "old-style", or auto, found integer
#set text(number-type: 2)
---
-// Error: 21-26 expected array of strings or dictionary mapping tags to integers, found boolean
+// Error: 21-26 expected array or dictionary, found boolean
#set text(features: false)
+
+---
+// Error: 21-35 expected string, found boolean
+#set text(features: ("tag", false))
diff --git a/tests/typ/text/font.typ b/tests/typ/text/font.typ
index e57fedbd..170703c7 100644
--- a/tests/typ/text/font.typ
+++ b/tests/typ/text/font.typ
@@ -48,7 +48,7 @@ Emoji: 🐪, 🌋, 🏞
#set text(false)
---
-// Error: 18-24 expected "normal", "italic" or "oblique"
+// Error: 18-24 expected "normal", "italic", or "oblique"
#set text(style: "bold", weight: "thin")
---
diff --git a/tests/typ/visualize/shape-rect.typ b/tests/typ/visualize/shape-rect.typ
index 94686da2..9a115e54 100644
--- a/tests/typ/visualize/shape-rect.typ
+++ b/tests/typ/visualize/shape-rect.typ
@@ -57,13 +57,9 @@
Use the `*const T` pointer or the `&mut T` reference.
---
-// Error: 15-38 unexpected key "cake"
+// Error: 15-38 unexpected key "cake", valid keys are "top-left", "top-right", "bottom-right", "bottom-left", "left", "top", "right", "bottom", and "rest"
#rect(radius: (left: 10pt, cake: 5pt))
---
-// Error: 15-21 expected stroke or none or dictionary with any of `left`, `top`, `right`, `bottom`, `x`, `y`, or `rest` as keys or auto, found array
+// Error: 15-21 expected length, color, stroke, none, dictionary, or auto, found array
#rect(stroke: (1, 2))
-
----
-// Error: 15-19 expected relative length or none or dictionary with any of `top-left`, `top-right`, `bottom-right`, `bottom-left`, `left`, `top`, `right`, `bottom`, or `rest` as keys, found color
-#rect(radius: blue)