From b6202b646a0d5ecced301d9bac8bfcaf977d7ee4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 15 Dec 2022 22:51:55 +0100 Subject: [PATCH] Reflection for castables --- library/src/basics/heading.rs | 2 + library/src/basics/list.rs | 36 +- library/src/basics/table.rs | 11 +- library/src/compute/calc.rs | 12 + library/src/compute/create.rs | 26 +- library/src/compute/data.rs | 6 + library/src/compute/foundations.rs | 8 + library/src/compute/utility.rs | 7 +- library/src/layout/align.rs | 2 + library/src/layout/columns.rs | 4 + library/src/layout/container.rs | 8 +- library/src/layout/grid.rs | 25 +- library/src/layout/hide.rs | 2 + library/src/layout/pad.rs | 2 + library/src/layout/page.rs | 55 +- library/src/layout/par.rs | 19 +- library/src/layout/place.rs | 2 + library/src/layout/repeat.rs | 2 + library/src/layout/spacing.rs | 16 +- library/src/layout/stack.rs | 10 +- library/src/layout/transform.rs | 4 + library/src/math/matrix.rs | 20 +- library/src/math/mod.rs | 20 + library/src/math/style.rs | 16 + library/src/meta/document.rs | 2 + library/src/meta/link.rs | 2 + library/src/meta/outline.rs | 2 + library/src/meta/reference.rs | 2 + library/src/prelude.rs | 10 +- library/src/text/deco.rs | 2 + library/src/text/misc.rs | 17 +- library/src/text/mod.rs | 97 ++-- library/src/text/quotes.rs | 2 + library/src/text/raw.rs | 2 + library/src/text/shift.rs | 2 + library/src/text/symbol.rs | 2 + library/src/visualize/image.rs | 15 +- library/src/visualize/line.rs | 2 + library/src/visualize/shape.rs | 6 +- macros/src/{capability.rs => capable.rs} | 4 +- macros/src/castable.rs | 229 ++++++++ macros/src/func.rs | 63 +- macros/src/lib.rs | 34 +- macros/src/node.rs | 46 +- src/diag.rs | 34 +- src/geom/mod.rs | 2 + src/geom/smart.rs | 64 +++ src/geom/stroke.rs | 1 - src/ide/complete.rs | 701 ++++++++++++++++------- src/ide/highlight.rs | 8 +- src/ide/tooltip.rs | 159 ++++- src/lib.rs | 2 + src/model/cast.rs | 529 ++++++++--------- src/model/content.rs | 8 +- src/model/dict.rs | 23 +- src/model/func.rs | 59 +- src/model/mod.rs | 2 +- src/model/ops.rs | 4 +- src/model/str.rs | 8 +- src/model/styles.rs | 3 +- src/model/value.rs | 8 +- src/syntax/ast.rs | 7 +- src/syntax/linked.rs | 92 +-- tests/src/tests.rs | 4 +- tests/typ/basics/table.typ | 2 +- tests/typ/compiler/show-node.typ | 2 +- tests/typ/compute/utility.typ | 4 +- tests/typ/layout/columns.typ | 2 +- tests/typ/text/edge.typ | 4 +- tests/typ/text/features.typ | 8 +- tests/typ/text/font.typ | 2 +- tests/typ/visualize/shape-rect.typ | 8 +- 72 files changed, 1843 insertions(+), 763 deletions(-) rename macros/src/{capability.rs => capable.rs} (100%) create mode 100644 macros/src/castable.rs create mode 100644 src/geom/smart.rs diff --git a/library/src/basics/heading.rs b/library/src/basics/heading.rs index 80f93e8d1..60415697c 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 0fa8f125b..4c016128b 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; impl ListNode { /// 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> for Label { +impl Cast> for ListLabel { fn is(value: &Spanned) -> bool { - matches!(&value.v, Value::Content(_) | Value::Func(_)) + matches!( + &value.v, + Value::None | Value::Str(_) | Value::Content(_) | Value::Func(_) + ) } fn cast(value: Spanned) -> StrResult { @@ -267,10 +272,15 @@ impl Cast> 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 5a4e8e818..10a9143f8 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 Cast> for Celled { fn cast(value: Spanned) -> StrResult { 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 71e43e215..62d0a4191 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 { let Spanned { v, span } = args.expect("numeric value")?; @@ -20,12 +22,16 @@ pub fn abs(args: &mut Args) -> SourceResult { } /// The minimum of a sequence of values. +/// +/// Tags: calculate. #[func] pub fn min(args: &mut Args) -> SourceResult { minmax(args, Ordering::Less) } /// The maximum of a sequence of values. +/// +/// Tags: calculate. #[func] pub fn max(args: &mut Args) -> SourceResult { minmax(args, Ordering::Greater) @@ -53,18 +59,24 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { } /// Whether an integer is even. +/// +/// Tags: calculate. #[func] pub fn even(args: &mut Args) -> SourceResult { Ok(Value::Bool(args.expect::("integer")? % 2 == 0)) } /// Whether an integer is odd. +/// +/// Tags: calculate. #[func] pub fn odd(args: &mut Args) -> SourceResult { Ok(Value::Bool(args.expect::("integer")? % 2 != 0)) } /// The modulo of two numbers. +/// +/// Tags: calculate. #[func] pub fn mod_(args: &mut Args) -> SourceResult { 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 acd2e31f8..a0eecfb8e 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 { let Spanned { v, span } = args.expect("value")?; @@ -21,6 +23,8 @@ pub fn int(args: &mut Args) -> SourceResult { } /// Convert a value to a float. +/// +/// Tags: create. #[func] pub fn float(args: &mut Args) -> SourceResult { let Spanned { v, span } = args.expect("value")?; @@ -36,6 +40,8 @@ pub fn float(args: &mut Args) -> SourceResult { } /// Create a grayscale color. +/// +/// Tags: create. #[func] pub fn luma(args: &mut Args) -> SourceResult { let Component(luma) = args.expect("gray component")?; @@ -43,6 +49,8 @@ pub fn luma(args: &mut Args) -> SourceResult { } /// Create an RGB(A) color. +/// +/// Tags: create. #[func] pub fn rgb(args: &mut Args) -> SourceResult { Ok(Value::Color(if let Some(string) = args.find::>()? { @@ -60,6 +68,8 @@ pub fn rgb(args: &mut Args) -> SourceResult { } /// Create a CMYK color. +/// +/// Tags: create. #[func] pub fn cmyk(args: &mut Args) -> SourceResult { 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 { let Spanned { v, span } = args.expect("value")?; @@ -113,12 +123,16 @@ pub fn str(args: &mut Args) -> SourceResult { } /// Create a label from a string. +/// +/// Tags: create. #[func] pub fn label(args: &mut Args) -> SourceResult { Ok(Value::Label(Label(args.expect("string")?))) } /// Create a regular expression from a string. +/// +/// Tags: create. #[func] pub fn regex(args: &mut Args) -> SourceResult { let Spanned { v, span } = args.expect::>("regular expression")?; @@ -126,6 +140,8 @@ pub fn regex(args: &mut Args) -> SourceResult { } /// Create an array consisting of a sequence of numbers. +/// +/// Tags: create. #[func] pub fn range(args: &mut Args) -> SourceResult { let first = args.expect::("end")?; diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs index 57dce5c19..af545304a 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 { 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 { 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 { let Spanned { v: path, span } = diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs index abe797dc3..22d26553e 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 { Ok(args.expect::("value")?.type_name().into()) } /// The string representation of a value. +/// +/// Tags: foundations. #[func] pub fn repr(args: &mut Args) -> SourceResult { Ok(args.expect::("value")?.repr().into()) } /// Ensure that a condition is fulfilled. +/// +/// Tags: foundations. #[func] pub fn assert(args: &mut Args) -> SourceResult { let Spanned { v, span } = args.expect::>("condition")?; @@ -27,6 +33,8 @@ pub fn assert(args: &mut Args) -> SourceResult { } /// Evaluate a string as Typst markup. +/// +/// Tags: foundations. #[func] pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult { let Spanned { v: text, span } = args.expect::>("source")?; diff --git a/library/src/compute/utility.rs b/library/src/compute/utility.rs index d48f794ea..5a6534f47 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 { let words: usize = args.expect("number of words")?; @@ -11,6 +13,8 @@ pub fn lorem(args: &mut Args) -> SourceResult { } /// Apply a numbering pattern to a number. +/// +/// Tags: utility. #[func] pub fn numbering(args: &mut Args) -> SourceResult { let pattern = args.expect::("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 4fae3c3cd..f00aeaf27 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 0e29bc00e..3bbf56e4e 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 852577156..d451bccfd 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 2a6bd4ff3..e70210c0d 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); 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::()?.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 => 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 1318b7ed8..4e70dca91 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 9c44919d1..9d882ba49 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 5a23b27b6..fe83137e8 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 = 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> for Marginal { fn is(value: &Spanned) -> bool { - matches!(&value.v, Value::Content(_) | Value::Func(_)) + matches!( + &value.v, + Value::None | Value::Str(_) | Value::Content(_) | Value::Func(_) + ) } fn cast(value: Spanned) -> StrResult { @@ -196,43 +206,51 @@ impl Cast> 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 { 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 925eea542..eed0dbb19 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 4c9c0a46f..c3fcd0d5a 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 196f19de7..a47dbb3e4 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 91e45b032..e961c0cf9 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 => Self::Relative(v), + v: Fr => Self::Fractional(v), } diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index 1e956669c..111e34331 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 35b6709a0..f1a89d4c0 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 2d32f4b58..21294b71d 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 59c621e8f..41cdb9f55 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 0fdff740b..444b1fb44 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 1dae4a2a3..8c664df37 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 94328b00a..6f5d8af1f 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 d0fbc3f71..27ca59440 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 657e5ef7c..378d19d2d 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 9b461389a..36d49dbff 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 fceb4cfdf..868667157 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 1c5a32b45..fc4f7d73d 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 { case(Case::Lower, args) } /// Convert a string or content to uppercase. +/// +/// Tags: text. #[func] pub fn upper(args: &mut Args) -> SourceResult { 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 { let body: Content = args.expect("content")?; diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index d09d8f28e..7340e5def 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); 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); 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); 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 - .into_iter() - .filter_map(|(k, v)| { - let tag = Tag::from_bytes_lossy(k.as_bytes()); - let num = v.cast::().ok()?.try_into().ok()?; - Some((tag, num)) + .map(|v| { + let tag = v.cast::()?; + Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1)) }) - .collect()), + .collect::>()?), + values: Dict => Self(values + .into_iter() + .map(|(k, v)| { + let num = v.cast::()?; + let tag = Tag::from_bytes_lossy(k.as_bytes()); + Ok((tag, num)) + }) + .collect::>()?), } impl Fold for FontFeatures { diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs index 0f678de32..ab6f166c8 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 4ad70654d..125a5da15 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 65adc0279..ad4a6cd96 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 fc746eb23..eece81abf 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 b8b05aec7..936ec3bfd 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 ed6a3d92d..9c9b8b008 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 702fc6f8b..a34431899 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 ShapeNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. - #[property(skip, resolve, fold)] + #[property(reflect, skip, resolve, fold)] pub const STROKE: Smart>> = Smart::Auto; /// How much to pad the shape's content. @@ -36,7 +38,7 @@ impl ShapeNode { pub const OUTSET: Sides>> = 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>> = Corners::splat(Rel::zero()); fn construct(_: &Vm, args: &mut Args) -> SourceResult { diff --git a/macros/src/capability.rs b/macros/src/capable.rs similarity index 100% rename from macros/src/capability.rs rename to macros/src/capable.rs index aa98f5843..dcfdfc829 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 { let ident = &item.ident; diff --git a/macros/src/castable.rs b/macros/src/castable.rs new file mode 100644 index 000000000..48cdf9e13 --- /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 { + 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::() { + 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::() { + 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 { + #dynamic_check + #str_check + #(#cast_checks)* + ::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, + casts: Punctuated, +} + +impl Parse for Castable { + fn parse(input: ParseStream) -> Result { + 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, + pattern: Pattern, + expr: syn::Expr, +} + +impl Parse for Cast { + fn parse(input: ParseStream) -> Result { + 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 { + 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 af558f966..4523d48a2 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 { - 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 { 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 { impl #params ::typst::model::FuncType for #ident #args #clause { fn create_func(name: &'static str) -> ::typst::model::Func { - ::typst::model::Func::from_node::(name, #doc) + ::typst::model::Func::from_node::(name, #info) } } }) } } - -/// Extract the item's documentation. -fn documentation(item: &syn::Item) -> Result { - 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 7f6a4a6cf..15dc3ee7f 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 192ca6674..ad079c0e5 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -36,6 +36,7 @@ struct Property { shorthand: Option, resolve: bool, fold: bool, + reflect: bool, } /// The shorthand form of a style property. @@ -117,6 +118,7 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result { 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 { "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 { referenced, resolve, fold, + reflect, }) } @@ -205,6 +205,7 @@ fn create(node: &Node) -> Result { 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 { #name_method #construct_func #set_func + #properties_func #field_method } }; @@ -221,7 +223,7 @@ fn create(node: &Node) -> Result { let mut items: Vec = 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 e244ba7ce..55f16b5fc 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -156,16 +156,6 @@ impl Trace for SourceResult { /// A result type with a string error message. pub type StrResult = Result; -/// 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 { /// 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(buf: &mut String, parts: &[S], last: &str) +where + S: AsRef, +{ + 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 = Result; diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 3c7c2fc96..6161774b9 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 000000000..d20bcdfee --- /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 { + /// The value should be determined smartly based on the circumstances. + Auto, + /// A specific value. + Custom(T), +} + +impl Smart { + /// Map the contained custom value with `f`. + pub fn map(self, f: F) -> Smart + 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) -> 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(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 Default for Smart { + fn default() -> Self { + Self::Auto + } +} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index eae43c243..86191d337 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 7f312727a..d4e72b3d1 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, - /// Details about the completed item. - pub detail: Option, -} - -/// 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, + /// An optional short description, at most one sentence. + pub detail: Option, +} + +/// 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::(); + if let Some(grand) = parent.parent(); + if let Some(expr) = grand.cast::(); + 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, ¶m); + 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 { + 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.", - ); - - self.snippet( - "regex selector", - "regex(\"${regex}\"): ${}", - "Replace matches of a regular expression.", - ); - - self.scope_completions_where( - from, - |value| matches!(value, Value::Func(func) if func.select(None).is_ok()), - ": ${}", - ); - } - - /// Add completions for selectors. - fn show_rule_recipe_completions(&mut self, from: usize) { - self.snippet( - "replacement", - "[${content}]", - "Replace the selected element with content.", - ); - - self.snippet( - "replacement (string)", - "\"${text}\"", - "Replace the selected element with a string of text.", - ); - - self.snippet( - "transformation", - "element => [${content}]", - "Transform the element with a function.", - ); - - self.scope_completions_where(from, |value| matches!(value, Value::Func(_)), ""); + /// 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()), + }); } /// Add completions for the global scope. - fn scope_completions(&mut self, from: usize) { - self.scope_completions_where(from, |_| true, ""); + fn scope_completions(&mut self) { + self.scope_completions_where(|_| true); } /// Add completions for a subset of the global scope. - fn scope_completions_where( - &mut self, - from: usize, - filter: fn(&Value) -> bool, - extra: &str, - ) { - self.from = from; - for (name, value) in self.world.library().scope.iter() { + fn scope_completions_where(&mut self, filter: impl Fn(&Value) -> bool) { + for (name, value) in self.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() - )), - }, - }); + self.value_completion(Some(name.clone()), value, None); } } } + /// 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(); + } + + for param in &info.params { + if exclude.iter().any(|ident| ident.as_str() == param.name) { + continue; + } + + 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(¶m.cast); + } + } + + if self.before.ends_with(',') { + self.enrich(" ", ""); + } + } + + /// 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(¶m.cast); + + if self.before.ends_with(':') { + self.enrich(" ", ""); + } + } + + /// 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, + label: Option, + value: &Value, + docs: Option<&'static str>, + ) { + 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.snippet( + self.scope_completions_where(|value| { + matches!( + value, + Value::Func(func) if func.info().map_or(false, |info| { + info.tags.contains(&"math") + }), + ) + }); + + 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 75539a3b7..ff9b8450c 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 { 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 { } 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 df193cb26..7f6ca692d 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 { 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 { + 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 { + 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::(); + 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::(); + 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(¶m.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 { + 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::(); + 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, +) -> 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 631f10423..4045c02de 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 bfde1bdd3..833b9e9ea 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: Sized { @@ -18,94 +22,97 @@ pub trait Cast: Sized { /// Try to cast the value into an instance of `Self`. fn cast(value: V) -> StrResult; + + /// Describe the acceptable values. + fn describe() -> CastInfo; + + /// Produce an error for an inacceptable value. + fn error(value: Value) -> StrResult { + 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; - } - - castable! { - $type, - Expected: ::TYPE_NAME, - $($tts)* - @this: Self => this.clone(), - } - - impl From<$type> for $crate::model::Value { - fn from(v: $type) -> Self { - $crate::model::Value::Dyn($crate::model::Dynamic::new(v)) - } - } - }; +/// 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), } -#[doc(inline)] -pub use crate::__dynamic as dynamic; - -/// 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 cast(value: $crate::model::Value) -> $crate::diag::StrResult { - <$inner>::cast(value).map(Self) - } - } - }; - - ( - $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>())* +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, + 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); } - _ => false, } } - - fn cast(value: $crate::model::Value) -> $crate::diag::StrResult { - 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, - )) - } } - }; + + 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"); + } + + 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::__castable as castable; +impl Add for CastInfo { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + lhs.extend(rhs); + lhs + } + (Self::Union(mut lhs), rhs) => { + lhs.push(rhs); + lhs + } + (lhs, Self::Union(mut rhs)) => { + rhs.insert(0, lhs); + rhs + } + (lhs, rhs) => vec![lhs, rhs], + }) + } +} impl Cast for Value { fn is(_: &Value) -> bool { @@ -115,6 +122,10 @@ impl Cast for Value { fn cast(value: Value) -> StrResult { Ok(value) } + + fn describe() -> CastInfo { + CastInfo::Any + } } impl Cast> for T { @@ -125,6 +136,10 @@ impl Cast> for T { fn cast(value: Spanned) -> StrResult { T::cast(value.v) } + + fn describe() -> CastInfo { + T::describe() + } } impl Cast> for Spanned { @@ -136,14 +151,64 @@ impl Cast> for Spanned { 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: "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: "2d alignment", -} - castable! { Axes>, - 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 => 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 => aligns.map(Some), } castable! { Axes>, - 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,67 +276,87 @@ 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)?, +} + +/// Castable from [`Value::None`]. +pub struct NoneValue; + +impl Cast for NoneValue { + fn is(value: &Value) -> bool { + matches!(value, Value::None) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::None => Ok(Self), + _ => ::error(value), + } + } + + fn describe() -> CastInfo { + CastInfo::Type("none") + } } impl Cast for Option { @@ -327,71 +367,33 @@ impl Cast for Option { fn cast(value: Value) -> StrResult { match value { Value::None => Ok(None), - v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")), + v if T::is(&v) => Ok(Some(T::cast(v)?)), + _ => ::error(value), } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("none") + } } -/// A value that can be automatically determined. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum Smart { - /// The value should be determined smartly based on the circumstances. - Auto, - /// A specific value. - Custom(T), -} +/// Castable from [`Value::Auto`]. +pub struct AutoValue; -impl Smart { - /// Map the contained custom value with `f`. - pub fn map(self, f: F) -> Smart - where - F: FnOnce(T) -> U, - { - match self { - Self::Auto => Smart::Auto, - Self::Custom(x) => Smart::Custom(f(x)), +impl Cast for AutoValue { + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::Auto => Ok(Self), + _ => ::error(value), } } - /// Keeps `self` if it contains a custom value, otherwise returns `other`. - pub fn or(self, other: Smart) -> 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(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 Default for Smart { - fn default() -> Self { - Self::Auto + fn describe() -> CastInfo { + CastInfo::Type("auto") } } @@ -403,11 +405,14 @@ impl Cast for Smart { fn cast(value: Value) -> StrResult { 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)?)), + _ => ::error(value), } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("auto") + } } impl Cast for Sides @@ -420,7 +425,7 @@ where fn cast(mut value: Value) -> StrResult { 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", - ) - }) + ::error(value) } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } } impl Cast for Corners @@ -460,7 +462,7 @@ where fn cast(mut value: Value) -> StrResult { 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", - ) - }) + ::error(value) } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } } diff --git a/src/model/content.rs b/src/model/content.rs index e73fa4a8b..df910a581 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 + where + Self: Sized; + /// Access a field on this node. fn field(&self, name: &str) -> Option; } diff --git a/src/model/dict.rs b/src/model/dict.rs index d54a0e824..6e014d7e9 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 { + 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 { - 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 { 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 0261b5e2d..5b38b7008 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, - 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(name: &'static str, doc: &'static str) -> Self { + pub fn from_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::()), - 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, /// 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, +} + +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 015df9b35..6ba8014c6 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 1a8dcb6b1..9da9b0cca 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 0c288d9b7..d1bf9d232 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 b2c328fa7..1eaf51283 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 98d11e155..1c687d8dc 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 77c788d37..56d4415e0 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 0d9d0c788..2826835e4 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> - + ExactSizeIterator> - + '_ { - 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 { 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>, + iter: std::iter::Enumerate>, + front: usize, + back: usize, +} + +impl<'a> Iterator for LinkedChildren<'a> { + type Item = LinkedNode<'a>; + + fn next(&mut self) -> Option { + 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) { + self.iter.size_hint() + } +} + +impl DoubleEndedIterator for LinkedChildren<'_> { + fn next_back(&mut self) -> Option { + 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 06162df0f..7c1e39b72 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 527141c57..e806b1aa8 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 98f36f13e..f14fb002f 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 cfc2e8af1..c99c0858a 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 315393a0f..8702688c9 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 c3c60b280..26be4aeb7 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 070fdcdf6..cae240f39 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 e57fedbd4..170703c78 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 94686da27..9a115e543 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)