Dynamic labels

This commit is contained in:
Laurenz 2022-11-26 13:39:18 +01:00
parent 3cdd8bfa40
commit 7af46fc025
16 changed files with 91 additions and 59 deletions

View File

@ -14,11 +14,17 @@ pub fn str(_: &Vm, args: &mut Args) -> SourceResult<Value> {
Ok(Value::Str(match v {
Value::Int(v) => format_str!("{}", v),
Value::Float(v) => format_str!("{}", v),
Value::Label(label) => label.0.into(),
Value::Str(v) => v,
v => bail!(span, "cannot convert {} to string", v.type_name()),
}))
}
/// Create a label from a string.
pub fn label(_: &Vm, args: &mut Args) -> SourceResult<Value> {
Ok(Value::Label(Label(args.expect("string")?)))
}
/// Create blind text.
pub fn lorem(_: &Vm, args: &mut Args) -> SourceResult<Value> {
let words: usize = args.expect("number of words")?;

View File

@ -105,6 +105,7 @@ fn scope() -> Scope {
std.def_fn("cmyk", base::cmyk);
std.def_fn("repr", base::repr);
std.def_fn("str", base::str);
std.def_fn("label", base::label);
std.def_fn("regex", base::regex);
std.def_fn("letter", base::letter);
std.def_fn("roman", base::roman);

View File

@ -16,7 +16,7 @@ pub use typst::geom::*;
#[doc(no_inline)]
pub use typst::model::{
array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast,
Content, Dict, Finalize, Fold, Func, Node, NodeId, Resolve, Show, Smart, Str,
Content, Dict, Finalize, Fold, Func, Label, Node, NodeId, Resolve, Show, Smart, Str,
StyleChain, StyleMap, StyleVec, Unlabellable, Value, Vm,
};
#[doc(no_inline)]

View File

@ -183,8 +183,9 @@ dynamic! {
dynamic! {
Selector: "selector",
Value::Func(func) => Self::Node(func.node()?, None),
Value::Str(text) => Self::text(&text),
Value::Label(label) => Self::Label(label),
Value::Func(func) => Self::Node(func.node()?, None),
@regex: Regex => Self::Regex(regex.clone()),
}

View File

@ -21,7 +21,7 @@ pub struct Content {
obj: Arc<dyn Bounds>,
guards: Vec<Guard>,
span: Option<Span>,
label: Option<EcoString>,
label: Option<Label>,
}
impl Content {
@ -54,7 +54,7 @@ impl Content {
}
/// Attach a label to the content.
pub fn labelled(mut self, label: EcoString) -> Self {
pub fn labelled(mut self, label: Label) -> Self {
self.label = Some(label);
self
}
@ -131,7 +131,7 @@ impl Content {
}
/// The content's label.
pub fn label(&self) -> Option<&EcoString> {
pub fn label(&self) -> Option<&Label> {
self.label.as_ref()
}
@ -139,7 +139,7 @@ impl Content {
pub fn field(&self, name: &str) -> Option<Value> {
if name == "label" {
return Some(match &self.label {
Some(label) => Value::Str(label.clone().into()),
Some(label) => Value::Label(label.clone()),
None => Value::None,
});
}
@ -335,6 +335,16 @@ impl Debug for SequenceNode {
}
}
/// A label for a node.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Label(pub EcoString);
impl Debug for Label {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "<{}>", self.0)
}
}
/// A constructable, stylable content node.
pub trait Node: 'static + Capable {
/// Pack a node into type-erased content.

View File

@ -8,7 +8,7 @@ use comemo::{Track, Tracked};
use unicode_segmentation::UnicodeSegmentation;
use super::{
methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func,
methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label,
LangItems, Recipe, Scope, Scopes, Selector, StyleMap, Transform, Value,
};
use crate::diag::{
@ -231,11 +231,16 @@ fn eval_markup(
let tail = eval_markup(vm, nodes)?;
seq.push(tail.styled_with_recipe(vm.world, recipe)?)
}
ast::MarkupNode::Label(label) => {
if let Some(node) = seq.iter_mut().rev().find(|node| node.labellable()) {
*node = mem::take(node).labelled(label.get().clone());
ast::MarkupNode::Expr(expr) => match expr.eval(vm)? {
Value::Label(label) => {
if let Some(node) =
seq.iter_mut().rev().find(|node| node.labellable())
{
*node = mem::take(node).labelled(label);
}
}
}
value => seq.push(value.display().spanned(expr.span())),
},
_ => seq.push(node.eval(vm)?),
}
@ -274,9 +279,8 @@ impl Eval for ast::MarkupNode {
Self::List(v) => v.eval(vm)?,
Self::Enum(v) => v.eval(vm)?,
Self::Desc(v) => v.eval(vm)?,
Self::Label(_) => unimplemented!("handled above"),
Self::Ref(v) => v.eval(vm)?,
Self::Expr(v) => v.eval(vm)?.display(),
Self::Expr(_) => unimplemented!("handled above"),
}
.spanned(self.span()))
}
@ -527,6 +531,7 @@ impl Eval for ast::Lit {
Unit::Percent => Ratio::new(v / 100.0).into(),
},
ast::LitKind::Str(v) => Value::Str(v.into()),
ast::LitKind::Label(v) => Value::Label(Label(v)),
})
}
}

View File

@ -307,6 +307,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Fraction(a), Fraction(b)) => a == b,
(Color(a), Color(b)) => a == b,
(Str(a), Str(b)) => a == b,
(Label(a), Label(b)) => a == b,
(Content(a), Content(b)) => a == b,
(Array(a), Array(b)) => a == b,
(Dict(a), Dict(b)) => a == b,

View File

@ -78,6 +78,14 @@ fn try_apply(
recipe.apply(world, target.clone().guarded(guard)).map(Some)
}
Some(Selector::Label(label)) => {
if target.label() != Some(label) {
return Ok(None);
}
recipe.apply(world, target.clone().guarded(guard)).map(Some)
}
Some(Selector::Regex(regex)) => {
let Some(text) = item!(text_str)(&target) else {
return Ok(None);

View File

@ -7,7 +7,7 @@ use std::sync::Arc;
use comemo::{Prehashed, Tracked};
use super::{Args, Content, Dict, Func, NodeId, Regex, Smart, Value};
use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Smart, Value};
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::geom::{
Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides,
@ -354,7 +354,9 @@ pub enum Selector {
/// If there is a dictionary, only nodes with the fields from the
/// dictionary match.
Node(NodeId, Option<Dict>),
/// Matches text through a regular expression.
/// Matches nodes with a specific label.
Label(Label),
/// Matches text nodes through a regular expression.
Regex(Regex),
}
@ -368,13 +370,17 @@ impl Selector {
pub fn matches(&self, target: &Content) -> bool {
match self {
Self::Node(id, dict) => {
*id == target.id()
target.id() == *id
&& dict
.iter()
.flat_map(|dict| dict.iter())
.all(|(name, value)| target.field(name).as_ref() == Some(value))
}
Self::Regex(_) => target.id() == item!(text_id),
Self::Label(label) => target.label() == Some(label),
Self::Regex(regex) => {
target.id() == item!(text_id)
&& item!(text_str)(target).map_or(false, |text| regex.is_match(text))
}
}
}
}

View File

@ -6,7 +6,7 @@ use std::sync::Arc;
use siphasher::sip128::{Hasher128, SipHasher};
use super::{format_str, ops, Args, Array, Cast, Content, Dict, Func, Str};
use super::{format_str, ops, Args, Array, Cast, 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};
@ -38,6 +38,8 @@ pub enum Value {
Color(Color),
/// A string: `"string"`.
Str(Str),
/// A label: `<intro>`.
Label(Label),
/// A content value: `[*Hi* there]`.
Content(Content),
/// An array of values: `(1, "hi", 12cm)`.
@ -76,6 +78,7 @@ impl Value {
Self::Fraction(_) => Fr::TYPE_NAME,
Self::Color(_) => Color::TYPE_NAME,
Self::Str(_) => Str::TYPE_NAME,
Self::Label(_) => Label::TYPE_NAME,
Self::Content(_) => Content::TYPE_NAME,
Self::Array(_) => Array::TYPE_NAME,
Self::Dict(_) => Dict::TYPE_NAME,
@ -130,6 +133,7 @@ impl Debug for Value {
Self::Fraction(v) => Debug::fmt(v, f),
Self::Color(v) => Debug::fmt(v, f),
Self::Str(v) => Debug::fmt(v, f),
Self::Label(v) => Debug::fmt(v, f),
Self::Content(_) => f.pad("[...]"),
Self::Array(v) => Debug::fmt(v, f),
Self::Dict(v) => Debug::fmt(v, f),
@ -168,6 +172,7 @@ impl Hash for Value {
Self::Fraction(v) => v.hash(state),
Self::Color(v) => v.hash(state),
Self::Str(v) => v.hash(state),
Self::Label(v) => v.hash(state),
Self::Content(v) => v.hash(state),
Self::Array(v) => v.hash(state),
Self::Dict(v) => v.hash(state),
@ -373,6 +378,7 @@ primitive! { Rel<Length>: "relative length",
primitive! { Fr: "fraction", Fraction }
primitive! { Color: "color", Color }
primitive! { Str: "string", Str }
primitive! { Label: "label", Label }
primitive! { Content: "content",
Content,
None => Content::empty(),

View File

@ -95,8 +95,6 @@ pub enum MarkupNode {
Raw(Raw),
/// A hyperlink: `https://typst.org`.
Link(Link),
/// A label: `<label>`.
Label(Label),
/// A reference: `@target`.
Ref(Ref),
/// A section heading: `= Introduction`.
@ -126,7 +124,6 @@ impl AstNode for MarkupNode {
SyntaxKind::Emph => node.cast().map(Self::Emph),
SyntaxKind::Raw(_) => node.cast().map(Self::Raw),
SyntaxKind::Link(_) => node.cast().map(Self::Link),
SyntaxKind::Label(_) => node.cast().map(Self::Label),
SyntaxKind::Ref(_) => node.cast().map(Self::Ref),
SyntaxKind::Heading => node.cast().map(Self::Heading),
SyntaxKind::ListItem => node.cast().map(Self::List),
@ -149,7 +146,6 @@ impl AstNode for MarkupNode {
Self::Emph(v) => v.as_untyped(),
Self::Raw(v) => v.as_untyped(),
Self::Link(v) => v.as_untyped(),
Self::Label(v) => v.as_untyped(),
Self::Ref(v) => v.as_untyped(),
Self::Heading(v) => v.as_untyped(),
Self::List(v) => v.as_untyped(),
@ -313,21 +309,6 @@ impl Link {
}
}
node! {
/// A label: `<label>`.
Label
}
impl Label {
/// Get the label.
pub fn get(&self) -> &EcoString {
match self.0.kind() {
SyntaxKind::Label(v) => v,
_ => panic!("label is of wrong kind"),
}
}
}
node! {
/// A reference: `@target`.
Ref
@ -704,6 +685,7 @@ node! {
| SyntaxKind::Float(_)
| SyntaxKind::Numeric(_, _)
| SyntaxKind::Str(_)
| SyntaxKind::Label(_)
}
impl Lit {
@ -717,6 +699,7 @@ impl Lit {
SyntaxKind::Float(v) => LitKind::Float(v),
SyntaxKind::Numeric(v, unit) => LitKind::Numeric(v, unit),
SyntaxKind::Str(ref v) => LitKind::Str(v.clone()),
SyntaxKind::Label(ref v) => LitKind::Label(v.clone()),
_ => panic!("literal is of wrong kind"),
}
}
@ -739,6 +722,8 @@ pub enum LitKind {
Numeric(f64, Unit),
/// A quoted string: `"..."`.
Str(EcoString),
/// A label: `<intro>`.
Label(EcoString),
}
node! {

View File

@ -153,7 +153,7 @@ pub enum SyntaxKind {
Raw(Arc<RawFields>),
/// A hyperlink: `https://typst.org`.
Link(EcoString),
/// A label: `<label>`.
/// A label: `<intro>`.
Label(EcoString),
/// A reference: `@target`.
Ref(EcoString),

View File

@ -232,7 +232,6 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
| SyntaxKind::Shorthand(_)
| SyntaxKind::Link(_)
| SyntaxKind::Raw(_)
| SyntaxKind::Label(_)
| SyntaxKind::Ref(_) => p.eat(),
// Math.
@ -257,6 +256,7 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
// Hashtag + keyword / identifier.
SyntaxKind::Ident(_)
| SyntaxKind::Label(_)
| SyntaxKind::Let
| SyntaxKind::Set
| SyntaxKind::Show
@ -617,7 +617,8 @@ fn literal(p: &mut Parser) -> bool {
| SyntaxKind::Float(_)
| SyntaxKind::Bool(_)
| SyntaxKind::Numeric(_, _)
| SyntaxKind::Str(_),
| SyntaxKind::Str(_)
| SyntaxKind::Label(_),
) => {
p.eat();
true

View File

@ -207,8 +207,8 @@ impl<'s> Tokens<'s> {
}
'`' => self.raw(),
c if c.is_ascii_digit() => self.numbering(start),
'<' => self.label(),
'@' => self.reference(start),
'<' if self.s.at(is_id_continue) => self.label(),
'@' if self.s.at(is_id_continue) => self.reference(),
// Escape sequences.
'\\' => self.backslash(),
@ -417,13 +417,8 @@ impl<'s> Tokens<'s> {
}
}
fn reference(&mut self, start: usize) -> SyntaxKind {
let label = self.s.eat_while(is_id_continue);
if !label.is_empty() {
SyntaxKind::Ref(label.into())
} else {
self.text(start)
}
fn reference(&mut self) -> SyntaxKind {
SyntaxKind::Ref(self.s.eat_while(is_id_continue).into())
}
fn math(&mut self, start: usize, c: char) -> SyntaxKind {
@ -475,6 +470,9 @@ impl<'s> Tokens<'s> {
'(' => SyntaxKind::LeftParen,
')' => SyntaxKind::RightParen,
// Labels.
'<' if self.s.at(is_id_continue) => self.label(),
// Two-char operators.
'=' if self.s.eat_if('=') => SyntaxKind::EqEq,
'!' if self.s.eat_if('=') => SyntaxKind::ExclEq,
@ -954,7 +952,7 @@ mod tests {
t!(Code: "=" => Eq);
t!(Code: "==" => EqEq);
t!(Code: "!=" => ExclEq);
t!(Code: "<" => Lt);
t!(Code[" /"]: "<" => Lt);
t!(Code: "<=" => LtEq);
t!(Code: ">" => Gt);
t!(Code: ">=" => GtEq);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -3,7 +3,7 @@
---
// Test labelled headings.
#show heading: set text(10pt)
#show heading.where(label: "intro"): underline
#show heading.where(label: <intro>): underline
= Introduction <intro>
The beginning.
@ -13,7 +13,7 @@ The end.
---
// Test label after expression.
#show strong.where(label: "v"): set text(red)
#show strong.where(label: <v>): set text(red)
#let a = [*A*]
#let b = [*B*]
@ -22,22 +22,22 @@ The end.
---
// Test labelled text.
#show "t": it => {
set text(blue) if it.label == "last"
set text(blue) if it.label == <last>
it
}
This is a thing [that <last>] happened.
---
// Test abusing labels for styling.
#show strong.where(label: "red"): set text(red)
#show strong.where(label: "blue"): set text(blue)
// Test abusing dynamic labels for styling.
#show <red>: set text(red)
#show <blue>: set text(blue)
*A* *B* <red> *C* <blue> *D*
*A* *B* <red> *C* #label("bl" + "ue") *D*
---
// Test that label ignores parbreak.
#show emph.where(label: "hide"): none
#show <hide>: none
_Hidden_
<hide>
@ -49,6 +49,10 @@ _Visible_
---
// Test that label only works within one content block.
#show strong.where(label: "strike"): strike
#show <strike>: strike
*This is* [<strike>] *protected.*
*This is not.* <strike>
---
// Test that incomplete label is text.
1 < 2 is #if 1 < 2 [not] a label.