mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Dynamic labels
This commit is contained in:
parent
3cdd8bfa40
commit
7af46fc025
@ -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")?;
|
||||
|
@ -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);
|
||||
|
@ -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)]
|
||||
|
@ -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()),
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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! {
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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 |
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user