Support recursive show rules
This commit is contained in:
parent
f77f1f61bf
commit
d59109e8ff
@ -26,19 +26,22 @@ pub struct Arg {
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Create empty arguments from a span.
|
||||
pub fn new(span: Span) -> Self {
|
||||
Self { span, items: vec![] }
|
||||
}
|
||||
|
||||
/// Create positional arguments from a span and values.
|
||||
pub fn from_values(span: Span, values: impl IntoIterator<Item = Value>) -> Self {
|
||||
Self {
|
||||
span,
|
||||
items: values
|
||||
.into_iter()
|
||||
.map(|value| Arg {
|
||||
span,
|
||||
name: None,
|
||||
value: Spanned::new(value, span),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
let items = values
|
||||
.into_iter()
|
||||
.map(|value| Arg {
|
||||
span,
|
||||
name: None,
|
||||
value: Spanned::new(value, span),
|
||||
})
|
||||
.collect();
|
||||
Self { span, items }
|
||||
}
|
||||
|
||||
/// Consume and cast the first positional argument.
|
||||
|
@ -91,8 +91,10 @@ impl<'a> CapturesVisitor<'a> {
|
||||
// A show rule contains a binding, but that binding is only active
|
||||
// after the target has been evaluated.
|
||||
Some(Expr::Show(show)) => {
|
||||
self.visit(show.target().as_red());
|
||||
self.bind(show.binding());
|
||||
self.visit(show.pattern().as_red());
|
||||
if let Some(binding) = show.binding() {
|
||||
self.bind(binding);
|
||||
}
|
||||
self.visit(show.body().as_red());
|
||||
}
|
||||
|
||||
|
@ -46,8 +46,8 @@ impl Dict {
|
||||
}
|
||||
|
||||
/// Borrow the value the given `key` maps to.
|
||||
pub fn get(&self, key: EcoString) -> StrResult<&Value> {
|
||||
self.0.get(&key).ok_or_else(|| missing_key(&key))
|
||||
pub fn get(&self, key: &EcoString) -> StrResult<&Value> {
|
||||
self.0.get(key).ok_or_else(|| missing_key(key))
|
||||
}
|
||||
|
||||
/// Mutably borrow the value the given `key` maps to.
|
||||
@ -59,7 +59,7 @@ impl Dict {
|
||||
}
|
||||
|
||||
/// Whether the dictionary contains a specific key.
|
||||
pub fn contains(&self, key: &str) -> bool {
|
||||
pub fn contains(&self, key: &EcoString) -> bool {
|
||||
self.0.contains_key(key)
|
||||
}
|
||||
|
||||
@ -69,10 +69,10 @@ impl Dict {
|
||||
}
|
||||
|
||||
/// Remove a mapping by `key`.
|
||||
pub fn remove(&mut self, key: EcoString) -> StrResult<()> {
|
||||
match Arc::make_mut(&mut self.0).remove(&key) {
|
||||
pub fn remove(&mut self, key: &EcoString) -> StrResult<()> {
|
||||
match Arc::make_mut(&mut self.0).remove(key) {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(missing_key(&key)),
|
||||
None => Err(missing_key(key)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,8 @@ use std::sync::Arc;
|
||||
|
||||
use super::{Args, Control, Eval, Scope, Scopes, Value};
|
||||
use crate::diag::{StrResult, TypResult};
|
||||
use crate::model::{Content, StyleMap};
|
||||
use crate::model::{Content, NodeId, StyleMap};
|
||||
use crate::syntax::ast::Expr;
|
||||
use crate::syntax::Span;
|
||||
use crate::util::EcoString;
|
||||
use crate::Context;
|
||||
|
||||
@ -35,7 +34,7 @@ impl Func {
|
||||
name,
|
||||
func,
|
||||
set: None,
|
||||
show: None,
|
||||
node: None,
|
||||
})))
|
||||
}
|
||||
|
||||
@ -49,15 +48,7 @@ impl Func {
|
||||
Ok(Value::Content(content.styled_with_map(styles.scoped())))
|
||||
},
|
||||
set: Some(T::set),
|
||||
show: if T::SHOWABLE {
|
||||
Some(|recipe, span| {
|
||||
let mut styles = StyleMap::new();
|
||||
styles.set_recipe::<T>(recipe, span);
|
||||
styles
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
node: T::SHOWABLE.then(|| NodeId::of::<T>()),
|
||||
})))
|
||||
}
|
||||
|
||||
@ -80,7 +71,20 @@ impl Func {
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the function with a virtual machine and arguments.
|
||||
/// The number of positional arguments this function takes, if known.
|
||||
pub fn argc(&self) -> Option<usize> {
|
||||
match self.0.as_ref() {
|
||||
Repr::Closure(closure) => Some(
|
||||
closure.params.iter().filter(|(_, default)| default.is_none()).count(),
|
||||
),
|
||||
Repr::With(wrapped, applied) => Some(wrapped.argc()?.saturating_sub(
|
||||
applied.items.iter().filter(|arg| arg.name.is_none()).count(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the function with the given arguments.
|
||||
pub fn call(&self, ctx: &mut Context, mut args: Args) -> TypResult<Value> {
|
||||
let value = match self.0.as_ref() {
|
||||
Repr::Native(native) => (native.func)(ctx, &mut args)?,
|
||||
@ -104,10 +108,10 @@ impl Func {
|
||||
Ok(styles)
|
||||
}
|
||||
|
||||
/// Execute the function's show rule.
|
||||
pub fn show(&self, recipe: Func, span: Span) -> StrResult<StyleMap> {
|
||||
/// The id of the node to customize with this function's show rule.
|
||||
pub fn node(&self) -> StrResult<NodeId> {
|
||||
match self.0.as_ref() {
|
||||
Repr::Native(Native { show: Some(show), .. }) => Ok(show(recipe, span)),
|
||||
Repr::Native(Native { node: Some(id), .. }) => Ok(*id),
|
||||
_ => Err("this function cannot be customized with show")?,
|
||||
}
|
||||
}
|
||||
@ -138,8 +142,8 @@ struct Native {
|
||||
pub func: fn(&mut Context, &mut Args) -> TypResult<Value>,
|
||||
/// The set rule.
|
||||
pub set: Option<fn(&mut Args) -> TypResult<StyleMap>>,
|
||||
/// The show rule.
|
||||
pub show: Option<fn(Func, Span) -> StyleMap>,
|
||||
/// The id of the node to customize with this function's show rule.
|
||||
pub node: Option<NodeId>,
|
||||
}
|
||||
|
||||
impl Hash for Native {
|
||||
@ -147,7 +151,7 @@ impl Hash for Native {
|
||||
self.name.hash(state);
|
||||
(self.func as usize).hash(state);
|
||||
self.set.map(|set| set as usize).hash(state);
|
||||
self.show.map(|show| show as usize).hash(state);
|
||||
self.node.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ pub fn call_mut(
|
||||
},
|
||||
|
||||
Value::Dict(dict) => match method {
|
||||
"remove" => dict.remove(args.expect("key")?).at(span)?,
|
||||
"remove" => dict.remove(&args.expect("key")?).at(span)?,
|
||||
_ => missing()?,
|
||||
},
|
||||
|
||||
|
@ -38,7 +38,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult};
|
||||
use crate::geom::{Angle, Em, Fraction, Length, Ratio};
|
||||
use crate::library;
|
||||
use crate::model::{Content, StyleMap};
|
||||
use crate::model::{Content, Pattern, Recipe, StyleEntry, StyleMap};
|
||||
use crate::syntax::ast::*;
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::util::EcoString;
|
||||
@ -79,8 +79,9 @@ fn eval_markup(
|
||||
eval_markup(ctx, scp, nodes)?.styled_with_map(styles)
|
||||
}
|
||||
MarkupNode::Expr(Expr::Show(show)) => {
|
||||
let styles = show.eval(ctx, scp)?;
|
||||
eval_markup(ctx, scp, nodes)?.styled_with_map(styles)
|
||||
let recipe = show.eval(ctx, scp)?;
|
||||
eval_markup(ctx, scp, nodes)?
|
||||
.styled_with_entry(StyleEntry::Recipe(recipe).into())
|
||||
}
|
||||
MarkupNode::Expr(Expr::Wrap(wrap)) => {
|
||||
let tail = eval_markup(ctx, scp, nodes)?;
|
||||
@ -434,8 +435,17 @@ impl Eval for FieldAccess {
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
let object = self.object().eval(ctx, scp)?;
|
||||
let span = self.field().span();
|
||||
let field = self.field().take();
|
||||
|
||||
Ok(match object {
|
||||
Value::Dict(dict) => dict.get(self.field().take()).at(self.span())?.clone(),
|
||||
Value::Dict(dict) => dict.get(&field).at(span)?.clone(),
|
||||
|
||||
Value::Content(Content::Show(_, Some(dict))) => dict
|
||||
.get(&field)
|
||||
.map_err(|_| format!("unknown field {field:?}"))
|
||||
.at(span)?
|
||||
.clone(),
|
||||
|
||||
v => bail!(
|
||||
self.object().span(),
|
||||
@ -455,7 +465,7 @@ impl Eval for FuncCall {
|
||||
|
||||
Ok(match callee {
|
||||
Value::Array(array) => array.get(args.into_index()?).at(self.span())?.clone(),
|
||||
Value::Dict(dict) => dict.get(args.into_key()?).at(self.span())?.clone(),
|
||||
Value::Dict(dict) => dict.get(&args.into_key()?).at(self.span())?.clone(),
|
||||
Value::Func(func) => {
|
||||
let point = || Tracepoint::Call(func.name().map(ToString::to_string));
|
||||
func.call(ctx, args).trace(point, self.span())?
|
||||
@ -615,13 +625,12 @@ impl Eval for SetExpr {
|
||||
}
|
||||
|
||||
impl Eval for ShowExpr {
|
||||
type Output = StyleMap;
|
||||
type Output = Recipe;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
// Evaluate the target function.
|
||||
let target = self.target();
|
||||
let target_span = target.span();
|
||||
let target = target.eval(ctx, scp)?.cast::<Func>().at(target_span)?;
|
||||
let pattern = self.pattern();
|
||||
let pattern = pattern.eval(ctx, scp)?.cast::<Pattern>().at(pattern.span())?;
|
||||
|
||||
// Collect captured variables.
|
||||
let captured = {
|
||||
@ -630,18 +639,24 @@ impl Eval for ShowExpr {
|
||||
visitor.finish()
|
||||
};
|
||||
|
||||
// Define parameters.
|
||||
let mut params = vec![];
|
||||
if let Some(binding) = self.binding() {
|
||||
params.push((binding.take(), None));
|
||||
}
|
||||
|
||||
// Define the recipe function.
|
||||
let body = self.body();
|
||||
let body_span = body.span();
|
||||
let recipe = Func::from_closure(Closure {
|
||||
let span = body.span();
|
||||
let func = Func::from_closure(Closure {
|
||||
name: None,
|
||||
captured,
|
||||
params: vec![(self.binding().take(), None)],
|
||||
params,
|
||||
sink: None,
|
||||
body,
|
||||
});
|
||||
|
||||
Ok(target.show(recipe, body_span).at(target_span)?)
|
||||
Ok(Recipe { pattern, func, span })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ use crate::geom::{
|
||||
Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides,
|
||||
};
|
||||
use crate::library::text::RawNode;
|
||||
use crate::model::{Content, Layout, LayoutNode};
|
||||
use crate::model::{Content, Layout, LayoutNode, Pattern};
|
||||
use crate::syntax::Spanned;
|
||||
use crate::util::EcoString;
|
||||
|
||||
@ -617,13 +617,13 @@ where
|
||||
}
|
||||
|
||||
let sides = Sides {
|
||||
left: dict.get("left".into()).or_else(|_| dict.get("x".into())),
|
||||
top: dict.get("top".into()).or_else(|_| dict.get("y".into())),
|
||||
right: dict.get("right".into()).or_else(|_| dict.get("x".into())),
|
||||
bottom: dict.get("bottom".into()).or_else(|_| dict.get("y".into())),
|
||||
left: dict.get(&"left".into()).or_else(|_| dict.get(&"x".into())),
|
||||
top: dict.get(&"top".into()).or_else(|_| dict.get(&"y".into())),
|
||||
right: dict.get(&"right".into()).or_else(|_| dict.get(&"x".into())),
|
||||
bottom: dict.get(&"bottom".into()).or_else(|_| dict.get(&"y".into())),
|
||||
}
|
||||
.map(|side| {
|
||||
side.or_else(|_| dict.get("rest".into()))
|
||||
side.or_else(|_| dict.get(&"rest".into()))
|
||||
.and_then(|v| T::cast(v.clone()))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
@ -684,6 +684,12 @@ castable! {
|
||||
Value::Content(content) => content.pack(),
|
||||
}
|
||||
|
||||
castable! {
|
||||
Pattern,
|
||||
Expected: "function",
|
||||
Value::Func(func) => Pattern::Node(func.node()?),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -52,7 +52,11 @@ impl<T: Numeric> Relative<T> {
|
||||
|
||||
impl<T: Numeric> Debug for Relative<T> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{:?} + {:?}", self.rel, self.abs)
|
||||
match (self.rel.is_zero(), self.abs.is_zero()) {
|
||||
(false, false) => write!(f, "{:?} + {:?}", self.rel, self.abs),
|
||||
(false, true) => self.rel.fmt(f),
|
||||
(true, _) => self.abs.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,10 @@ impl MathNode {
|
||||
}
|
||||
|
||||
impl Show for MathNode {
|
||||
fn unguard(&self, _: Selector) -> ShowNode {
|
||||
Self { formula: self.formula.clone(), ..*self }.pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"formula" => Value::Str(self.formula.clone()),
|
||||
|
@ -15,8 +15,8 @@ pub use crate::eval::{
|
||||
pub use crate::frame::*;
|
||||
pub use crate::geom::*;
|
||||
pub use crate::model::{
|
||||
Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Show, ShowNode, StyleChain,
|
||||
StyleMap, StyleVec,
|
||||
Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Selector, Show, ShowNode,
|
||||
StyleChain, StyleMap, StyleVec,
|
||||
};
|
||||
pub use crate::syntax::{Span, Spanned};
|
||||
pub use crate::util::{EcoString, OptionExt};
|
||||
|
@ -64,6 +64,10 @@ impl HeadingNode {
|
||||
}
|
||||
|
||||
impl Show for HeadingNode {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self { body: self.body.unguard(sel), ..*self }.pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"level" => Value::Int(self.level.get() as i64),
|
||||
|
@ -75,6 +75,17 @@ impl<const L: ListKind> ListNode<L> {
|
||||
}
|
||||
|
||||
impl<const L: ListKind> Show for ListNode<L> {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self {
|
||||
items: self.items.map(|item| ListItem {
|
||||
body: Box::new(item.body.unguard(sel)),
|
||||
..*item
|
||||
}),
|
||||
..*self
|
||||
}
|
||||
.pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"start" => Value::Int(self.start as i64),
|
||||
@ -83,7 +94,7 @@ impl<const L: ListKind> Show for ListNode<L> {
|
||||
"items" => Value::Array(
|
||||
self.items
|
||||
.items()
|
||||
.map(|item| Value::Content((*item.body).clone()))
|
||||
.map(|item| Value::Content(item.body.as_ref().clone()))
|
||||
.collect()
|
||||
),
|
||||
}
|
||||
|
@ -51,6 +51,15 @@ impl TableNode {
|
||||
}
|
||||
|
||||
impl Show for TableNode {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self {
|
||||
tracks: self.tracks.clone(),
|
||||
gutter: self.gutter.clone(),
|
||||
cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(),
|
||||
}
|
||||
.pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"cells" => Value::Array(
|
||||
|
@ -36,11 +36,15 @@ impl<const L: DecoLine> DecoNode<L> {
|
||||
pub const EVADE: bool = true;
|
||||
|
||||
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
|
||||
Ok(Content::show(Self(args.expect::<Content>("body")?)))
|
||||
Ok(Content::show(Self(args.expect("body")?)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: DecoLine> Show for DecoNode<L> {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self(self.0.unguard(sel)).pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! { "body" => Value::Content(self.0.clone()) }
|
||||
}
|
||||
|
@ -28,6 +28,14 @@ impl LinkNode {
|
||||
}
|
||||
|
||||
impl Show for LinkNode {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self {
|
||||
url: self.url.clone(),
|
||||
body: self.body.as_ref().map(|body| body.unguard(sel)),
|
||||
}
|
||||
.pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"url" => Value::Str(self.url.clone()),
|
||||
|
@ -463,6 +463,10 @@ impl StrongNode {
|
||||
}
|
||||
|
||||
impl Show for StrongNode {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self(self.0.unguard(sel)).pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! { "body" => Value::Content(self.0.clone()) }
|
||||
}
|
||||
@ -484,6 +488,10 @@ impl EmphNode {
|
||||
}
|
||||
|
||||
impl Show for EmphNode {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
Self(self.0.unguard(sel)).pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! { "body" => Value::Content(self.0.clone()) }
|
||||
}
|
||||
|
@ -618,7 +618,10 @@ fn shared_get<'a, K: Key<'a>>(
|
||||
children: &StyleVec<ParChild>,
|
||||
key: K,
|
||||
) -> Option<K::Output> {
|
||||
children.maps().all(|map| !map.contains(key)).then(|| styles.get(key))
|
||||
children
|
||||
.styles()
|
||||
.all(|map| !map.contains(key))
|
||||
.then(|| styles.get(key))
|
||||
}
|
||||
|
||||
/// Find suitable linebreaks.
|
||||
|
@ -51,6 +51,10 @@ impl RawNode {
|
||||
}
|
||||
|
||||
impl Show for RawNode {
|
||||
fn unguard(&self, _: Selector) -> ShowNode {
|
||||
Self { text: self.text.clone(), ..*self }.pack()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
dict! {
|
||||
"text" => Value::Str(self.text.clone()),
|
||||
|
@ -7,8 +7,8 @@ use std::ops::{Add, AddAssign};
|
||||
use typed_arena::Arena;
|
||||
|
||||
use super::{
|
||||
CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Show, ShowNode, StyleMap,
|
||||
StyleVecBuilder,
|
||||
CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Property, Show, ShowNode,
|
||||
StyleEntry, StyleMap, StyleVecBuilder, Target,
|
||||
};
|
||||
use crate::diag::StrResult;
|
||||
use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing};
|
||||
@ -38,6 +38,8 @@ use crate::util::EcoString;
|
||||
/// sequences.
|
||||
#[derive(PartialEq, Clone, Hash)]
|
||||
pub enum Content {
|
||||
/// Empty content.
|
||||
Empty,
|
||||
/// A word space.
|
||||
Space,
|
||||
/// A forced line break.
|
||||
@ -68,8 +70,9 @@ pub enum Content {
|
||||
Pagebreak { weak: bool },
|
||||
/// A page node.
|
||||
Page(PageNode),
|
||||
/// A node that can be realized with styles.
|
||||
Show(ShowNode),
|
||||
/// A node that can be realized with styles, optionally with attached
|
||||
/// properties.
|
||||
Show(ShowNode, Option<Dict>),
|
||||
/// Content with attached styles.
|
||||
Styled(Arc<(Self, StyleMap)>),
|
||||
/// A sequence of multiple nodes.
|
||||
@ -79,7 +82,7 @@ pub enum Content {
|
||||
impl Content {
|
||||
/// Create empty content.
|
||||
pub fn new() -> Self {
|
||||
Self::sequence(vec![])
|
||||
Self::Empty
|
||||
}
|
||||
|
||||
/// Create content from an inline-level node.
|
||||
@ -103,15 +106,15 @@ impl Content {
|
||||
where
|
||||
T: Show + Debug + Hash + Sync + Send + 'static,
|
||||
{
|
||||
Self::Show(node.pack())
|
||||
Self::Show(node.pack(), None)
|
||||
}
|
||||
|
||||
/// Create a new sequence nodes from multiples nodes.
|
||||
pub fn sequence(seq: Vec<Self>) -> Self {
|
||||
if seq.len() == 1 {
|
||||
seq.into_iter().next().unwrap()
|
||||
} else {
|
||||
Self::Sequence(Arc::new(seq))
|
||||
match seq.as_slice() {
|
||||
[] => Self::Empty,
|
||||
[_] => seq.into_iter().next().unwrap(),
|
||||
_ => Self::Sequence(Arc::new(seq)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,15 +127,20 @@ impl Content {
|
||||
}
|
||||
|
||||
/// Style this content with a single style property.
|
||||
pub fn styled<'k, K: Key<'k>>(mut self, key: K, value: K::Value) -> Self {
|
||||
pub fn styled<'k, K: Key<'k>>(self, key: K, value: K::Value) -> Self {
|
||||
self.styled_with_entry(StyleEntry::Property(Property::new(key, value)))
|
||||
}
|
||||
|
||||
/// Style this content with a style entry.
|
||||
pub fn styled_with_entry(mut self, entry: StyleEntry) -> Self {
|
||||
if let Self::Styled(styled) = &mut self {
|
||||
if let Some((_, map)) = Arc::get_mut(styled) {
|
||||
map.apply(key, value);
|
||||
map.apply(entry);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
Self::Styled(Arc::new((self, StyleMap::with(key, value))))
|
||||
Self::Styled(Arc::new((self, entry.into())))
|
||||
}
|
||||
|
||||
/// Style this content with a full style map.
|
||||
@ -151,6 +159,11 @@ impl Content {
|
||||
Self::Styled(Arc::new((self, styles)))
|
||||
}
|
||||
|
||||
/// Reenable the show rule identified by the selector.
|
||||
pub fn unguard(&self, sel: Selector) -> Self {
|
||||
self.clone().styled_with_entry(StyleEntry::Unguard(sel))
|
||||
}
|
||||
|
||||
/// Underline this content.
|
||||
pub fn underlined(self) -> Self {
|
||||
Self::show(DecoNode::<UNDERLINE>(self))
|
||||
@ -228,6 +241,7 @@ impl Default for Content {
|
||||
impl Debug for Content {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Empty => f.pad("Empty"),
|
||||
Self::Space => f.pad("Space"),
|
||||
Self::Linebreak { justified } => write!(f, "Linebreak({justified})"),
|
||||
Self::Horizontal { amount, weak } => {
|
||||
@ -245,7 +259,7 @@ impl Debug for Content {
|
||||
Self::Item(item) => item.fmt(f),
|
||||
Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"),
|
||||
Self::Page(page) => page.fmt(f),
|
||||
Self::Show(node) => node.fmt(f),
|
||||
Self::Show(node, _) => node.fmt(f),
|
||||
Self::Styled(styled) => {
|
||||
let (sub, map) = styled.as_ref();
|
||||
map.fmt(f)?;
|
||||
@ -261,6 +275,8 @@ impl Add for Content {
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self::Sequence(match (self, rhs) {
|
||||
(Self::Empty, rhs) => return rhs,
|
||||
(lhs, Self::Empty) => return lhs,
|
||||
(Self::Sequence(mut lhs), Self::Sequence(rhs)) => {
|
||||
let mutable = Arc::make_mut(&mut lhs);
|
||||
match Arc::try_unwrap(rhs) {
|
||||
@ -352,7 +368,8 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
|
||||
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> TypResult<()> {
|
||||
// Handle special content kinds.
|
||||
match content {
|
||||
Content::Show(node) => return self.show(node, styles),
|
||||
Content::Empty => return Ok(()),
|
||||
Content::Show(node, _) => return self.show(node, styles),
|
||||
Content::Styled(styled) => return self.styled(styled, styles),
|
||||
Content::Sequence(seq) => return self.sequence(seq, styles),
|
||||
_ => {}
|
||||
@ -388,15 +405,11 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
|
||||
}
|
||||
|
||||
fn show(&mut self, node: &ShowNode, styles: StyleChain<'a>) -> TypResult<()> {
|
||||
let id = node.id();
|
||||
let realized = match styles.realize(self.ctx, node)? {
|
||||
Some(content) => content,
|
||||
None => node.realize(self.ctx, styles)?,
|
||||
};
|
||||
|
||||
let content = node.finalize(self.ctx, styles, realized)?;
|
||||
let stored = self.scratch.templates.alloc(content);
|
||||
self.accept(stored, styles.unscoped(id))
|
||||
if let Some(realized) = styles.apply(self.ctx, Target::Node(node))? {
|
||||
let stored = self.scratch.templates.alloc(realized);
|
||||
self.accept(stored, styles.unscoped(node.id()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn styled(
|
||||
|
@ -1,14 +1,17 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::NodeId;
|
||||
use crate::eval::{Func, Node};
|
||||
use super::{Content, Interruption, NodeId, Show, ShowNode, StyleEntry};
|
||||
use crate::diag::{At, TypResult};
|
||||
use crate::eval::{Args, Func, Value};
|
||||
use crate::library::structure::{EnumNode, ListNode};
|
||||
use crate::syntax::Span;
|
||||
use crate::Context;
|
||||
|
||||
/// A show rule recipe.
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct Recipe {
|
||||
/// The affected node.
|
||||
pub node: NodeId,
|
||||
/// The patterns to customize.
|
||||
pub pattern: Pattern,
|
||||
/// The function that defines the recipe.
|
||||
pub func: Func,
|
||||
/// The span to report all erros with.
|
||||
@ -16,14 +19,87 @@ pub struct Recipe {
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
/// Create a new recipe for the node `T`.
|
||||
pub fn new<T: Node>(func: Func, span: Span) -> Self {
|
||||
Self { node: NodeId::of::<T>(), func, span }
|
||||
/// Whether the recipe is applicable to the target.
|
||||
pub fn applicable(&self, target: Target) -> bool {
|
||||
match (&self.pattern, target) {
|
||||
(Pattern::Node(id), Target::Node(node)) => *id == node.id(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to apply the recipe to the target.
|
||||
pub fn apply(
|
||||
&self,
|
||||
ctx: &mut Context,
|
||||
sel: Selector,
|
||||
target: Target,
|
||||
) -> TypResult<Option<Content>> {
|
||||
let content = match (target, &self.pattern) {
|
||||
(Target::Node(node), &Pattern::Node(id)) if node.id() == id => {
|
||||
let node = node.unguard(sel);
|
||||
self.call(ctx, || {
|
||||
let dict = node.encode();
|
||||
Value::Content(Content::Show(node, Some(dict)))
|
||||
})?
|
||||
}
|
||||
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(content.styled_with_entry(StyleEntry::Guard(sel))))
|
||||
}
|
||||
|
||||
/// Call the recipe function, with the argument if desired.
|
||||
fn call<F>(&self, ctx: &mut Context, arg: F) -> TypResult<Content>
|
||||
where
|
||||
F: FnOnce() -> Value,
|
||||
{
|
||||
let args = if self.func.argc() == Some(0) {
|
||||
Args::new(self.span)
|
||||
} else {
|
||||
Args::from_values(self.span, [arg()])
|
||||
};
|
||||
|
||||
self.func.call(ctx, args)?.cast().at(self.span)
|
||||
}
|
||||
|
||||
/// What kind of structure the property interrupts.
|
||||
pub fn interruption(&self) -> Option<Interruption> {
|
||||
if let Pattern::Node(id) = self.pattern {
|
||||
if id == NodeId::of::<ListNode>() || id == NodeId::of::<EnumNode>() {
|
||||
return Some(Interruption::List);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Recipe {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Recipe for {:?} from {:?}", self.node, self.span)
|
||||
write!(f, "Recipe matching {:?} from {:?}", self.pattern, self.span)
|
||||
}
|
||||
}
|
||||
|
||||
/// A show rule pattern that may match a target.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum Pattern {
|
||||
/// Defines the appearence of some node.
|
||||
Node(NodeId),
|
||||
}
|
||||
|
||||
/// A target for a show rule recipe.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum Target<'a> {
|
||||
/// A showable node.
|
||||
Node(&'a ShowNode),
|
||||
}
|
||||
|
||||
/// Identifies a show rule recipe.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
|
||||
pub enum Selector {
|
||||
/// The nth recipe from the top of the chain.
|
||||
Nth(usize),
|
||||
/// The base recipe for a kind of node.
|
||||
Base(NodeId),
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter, Write};
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{Content, NodeId, StyleChain};
|
||||
use super::{Content, NodeId, Selector, StyleChain};
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::Dict;
|
||||
use crate::util::Prehashed;
|
||||
@ -10,6 +10,9 @@ use crate::Context;
|
||||
|
||||
/// A node that can be realized given some styles.
|
||||
pub trait Show: 'static {
|
||||
/// Unguard nested content against recursive show rules.
|
||||
fn unguard(&self, sel: Selector) -> ShowNode;
|
||||
|
||||
/// Encode this node into a dictionary.
|
||||
fn encode(&self) -> Dict;
|
||||
|
||||
@ -63,6 +66,10 @@ impl ShowNode {
|
||||
}
|
||||
|
||||
impl Show for ShowNode {
|
||||
fn unguard(&self, sel: Selector) -> ShowNode {
|
||||
self.0.unguard(sel)
|
||||
}
|
||||
|
||||
fn encode(&self) -> Dict {
|
||||
self.0.encode()
|
||||
}
|
||||
|
@ -3,11 +3,9 @@ use std::hash::Hash;
|
||||
use std::iter;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use super::{Barrier, Content, Key, Property, Recipe, Show, ShowNode};
|
||||
use crate::diag::{At, TypResult};
|
||||
use crate::eval::{Args, Func, Node, Value};
|
||||
use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target};
|
||||
use crate::diag::TypResult;
|
||||
use crate::library::text::{FontFamily, TextNode};
|
||||
use crate::syntax::Span;
|
||||
use crate::util::ReadableTypeId;
|
||||
use crate::Context;
|
||||
|
||||
@ -65,11 +63,6 @@ impl StyleMap {
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a show rule recipe for a node.
|
||||
pub fn set_recipe<T: Node>(&mut self, func: Func, span: Span) {
|
||||
self.push(StyleEntry::Recipe(Recipe::new::<T>(func, span)));
|
||||
}
|
||||
|
||||
/// Whether the map contains a style property for the given key.
|
||||
pub fn contains<'a, K: Key<'a>>(&self, _: K) -> bool {
|
||||
self.0
|
||||
@ -91,16 +84,12 @@ impl StyleMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an outer value for a style property.
|
||||
///
|
||||
/// If the property needs folding and the value is already contained in the
|
||||
/// style map, `self` contributes the inner values and `value` is the outer
|
||||
/// one.
|
||||
/// Set an outer style property.
|
||||
///
|
||||
/// Like [`chain`](Self::chain) or [`apply_map`](Self::apply_map), but with
|
||||
/// only a single property.
|
||||
pub fn apply<'a, K: Key<'a>>(&mut self, key: K, value: K::Value) {
|
||||
self.0.insert(0, StyleEntry::Property(Property::new(key, value)));
|
||||
/// only a entry.
|
||||
pub fn apply(&mut self, entry: StyleEntry) {
|
||||
self.0.insert(0, entry);
|
||||
}
|
||||
|
||||
/// Apply styles from `tail` in-place. The resulting style map is equivalent
|
||||
@ -126,11 +115,7 @@ impl StyleMap {
|
||||
|
||||
/// The highest-level kind of of structure the map interrupts.
|
||||
pub fn interruption(&self) -> Option<Interruption> {
|
||||
self.0
|
||||
.iter()
|
||||
.filter_map(|entry| entry.property())
|
||||
.filter_map(|property| property.interruption())
|
||||
.max()
|
||||
self.0.iter().filter_map(|entry| entry.interruption()).max()
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,29 +167,17 @@ pub enum Interruption {
|
||||
pub enum StyleEntry {
|
||||
/// A style property originating from a set rule or constructor.
|
||||
Property(Property),
|
||||
/// A barrier for scoped styles.
|
||||
Barrier(Barrier),
|
||||
/// A show rule recipe.
|
||||
Recipe(Recipe),
|
||||
/// A barrier for scoped styles.
|
||||
Barrier(Barrier),
|
||||
/// Guards against recursive show rules.
|
||||
Guard(Selector),
|
||||
/// Allows recursive show rules again.
|
||||
Unguard(Selector),
|
||||
}
|
||||
|
||||
impl StyleEntry {
|
||||
/// If this is a property, return it.
|
||||
pub fn property(&self) -> Option<&Property> {
|
||||
match self {
|
||||
Self::Property(property) => Some(property),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is a recipe, return it.
|
||||
pub fn recipe(&self) -> Option<&Recipe> {
|
||||
match self {
|
||||
Self::Recipe(recipe) => Some(recipe),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Make this style the first link of the `tail` chain.
|
||||
pub fn chain<'a>(&'a self, tail: &'a StyleChain) -> StyleChain<'a> {
|
||||
if let StyleEntry::Barrier(barrier) = self {
|
||||
@ -222,6 +195,31 @@ impl StyleEntry {
|
||||
tail: Some(tail),
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is a property, return it.
|
||||
pub fn property(&self) -> Option<&Property> {
|
||||
match self {
|
||||
Self::Property(property) => Some(property),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is a recipe, return it.
|
||||
pub fn recipe(&self) -> Option<&Recipe> {
|
||||
match self {
|
||||
Self::Recipe(recipe) => Some(recipe),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The highest-level kind of of structure the entry interrupts.
|
||||
pub fn interruption(&self) -> Option<Interruption> {
|
||||
match self {
|
||||
Self::Property(property) => property.interruption(),
|
||||
Self::Recipe(recipe) => recipe.interruption(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for StyleEntry {
|
||||
@ -231,6 +229,8 @@ impl Debug for StyleEntry {
|
||||
Self::Property(property) => property.fmt(f)?,
|
||||
Self::Recipe(recipe) => recipe.fmt(f)?,
|
||||
Self::Barrier(barrier) => barrier.fmt(f)?,
|
||||
Self::Guard(sel) => write!(f, "Guard against {sel:?}")?,
|
||||
Self::Unguard(sel) => write!(f, "Unguard against {sel:?}")?,
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
@ -262,35 +262,6 @@ impl<'a> StyleChain<'a> {
|
||||
Self { head: &root.0, tail: None }
|
||||
}
|
||||
|
||||
/// Get the output value of a style property.
|
||||
///
|
||||
/// Returns the property's default value if no map in the chain contains an
|
||||
/// entry for it. Also takes care of resolving and folding and returns
|
||||
/// references where applicable.
|
||||
pub fn get<K: Key<'a>>(self, key: K) -> K::Output {
|
||||
K::get(self, self.values(key))
|
||||
}
|
||||
|
||||
/// Realize a node with a user recipe.
|
||||
pub fn realize(
|
||||
self,
|
||||
ctx: &mut Context,
|
||||
node: &ShowNode,
|
||||
) -> TypResult<Option<Content>> {
|
||||
let id = node.id();
|
||||
if let Some(recipe) = self
|
||||
.entries()
|
||||
.filter_map(StyleEntry::recipe)
|
||||
.find(|recipe| recipe.node == id)
|
||||
{
|
||||
let dict = node.encode();
|
||||
let args = Args::from_values(recipe.span, [Value::Dict(dict)]);
|
||||
Ok(Some(recipe.func.call(ctx, args)?.cast().at(recipe.span)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the chain, but without trailing scoped properties for the given
|
||||
/// `node`.
|
||||
pub fn unscoped(mut self, node: NodeId) -> Self {
|
||||
@ -306,6 +277,80 @@ impl<'a> StyleChain<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the output value of a style property.
|
||||
///
|
||||
/// Returns the property's default value if no map in the chain contains an
|
||||
/// entry for it. Also takes care of resolving and folding and returns
|
||||
/// references where applicable.
|
||||
pub fn get<K: Key<'a>>(self, key: K) -> K::Output {
|
||||
K::get(self, self.values(key))
|
||||
}
|
||||
|
||||
/// Apply show recipes in this style chain to a target.
|
||||
pub fn apply(self, ctx: &mut Context, target: Target) -> TypResult<Option<Content>> {
|
||||
// Find out how many recipes there any and whether any of their patterns
|
||||
// match.
|
||||
let mut n = 0;
|
||||
let mut any = true;
|
||||
for recipe in self.entries().filter_map(StyleEntry::recipe) {
|
||||
n += 1;
|
||||
any |= recipe.applicable(target);
|
||||
}
|
||||
|
||||
// Find an applicable recipe.
|
||||
let mut realized = None;
|
||||
let mut guarded = false;
|
||||
if any {
|
||||
for recipe in self.entries().filter_map(StyleEntry::recipe) {
|
||||
if recipe.applicable(target) {
|
||||
let sel = Selector::Nth(n);
|
||||
if self.guarded(sel) {
|
||||
guarded = true;
|
||||
} else if let Some(content) = recipe.apply(ctx, sel, target)? {
|
||||
realized = Some(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
n -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let Target::Node(node) = target {
|
||||
// Realize if there was no matching recipe.
|
||||
if realized.is_none() {
|
||||
let sel = Selector::Base(node.id());
|
||||
if self.guarded(sel) {
|
||||
guarded = true;
|
||||
} else {
|
||||
let content = node.unguard(sel).realize(ctx, self)?;
|
||||
realized = Some(content.styled_with_entry(StyleEntry::Guard(sel)));
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize only if guarding didn't stop any recipe.
|
||||
if !guarded {
|
||||
if let Some(content) = realized {
|
||||
realized = Some(node.finalize(ctx, self, content)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(realized)
|
||||
}
|
||||
|
||||
/// Whether the recipe identified by the selector is guarded.
|
||||
fn guarded(&self, sel: Selector) -> bool {
|
||||
for entry in self.entries() {
|
||||
match *entry {
|
||||
StyleEntry::Guard(s) if s == sel => return true,
|
||||
StyleEntry::Unguard(s) if s == sel => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Remove the last link from the chain.
|
||||
fn pop(&mut self) {
|
||||
*self = self.tail.copied().unwrap_or_default();
|
||||
@ -386,7 +431,7 @@ impl<'a, K: Key<'a>> Iterator for Values<'a, K> {
|
||||
StyleEntry::Barrier(barrier) => {
|
||||
self.depth += barrier.is_for(K::node()) as usize;
|
||||
}
|
||||
StyleEntry::Recipe(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -459,13 +504,15 @@ impl<T> StyleVec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the contained maps. Note that zipping this with `items()`
|
||||
/// does not yield the same result as calling `iter()` because this method
|
||||
/// only returns maps once that are shared by consecutive items. This method
|
||||
/// is designed for use cases where you want to check, for example, whether
|
||||
/// any of the maps fulfills a specific property.
|
||||
pub fn maps(&self) -> impl Iterator<Item = &StyleMap> {
|
||||
self.maps.iter().map(|(map, _)| map)
|
||||
/// Map the contained items.
|
||||
pub fn map<F, U>(&self, f: F) -> StyleVec<U>
|
||||
where
|
||||
F: FnMut(&T) -> U,
|
||||
{
|
||||
StyleVec {
|
||||
items: self.items.iter().map(f).collect(),
|
||||
maps: self.maps.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the contained items.
|
||||
@ -473,6 +520,15 @@ impl<T> StyleVec<T> {
|
||||
self.items.iter()
|
||||
}
|
||||
|
||||
/// Iterate over the contained maps. Note that zipping this with `items()`
|
||||
/// does not yield the same result as calling `iter()` because this method
|
||||
/// only returns maps once that are shared by consecutive items. This method
|
||||
/// is designed for use cases where you want to check, for example, whether
|
||||
/// any of the maps fulfills a specific property.
|
||||
pub fn styles(&self) -> impl Iterator<Item = &StyleMap> {
|
||||
self.maps.iter().map(|(map, _)| map)
|
||||
}
|
||||
|
||||
/// Iterate over references to the contained items and associated style maps.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&T, &StyleMap)> + '_ {
|
||||
self.items().zip(
|
||||
|
@ -807,9 +807,15 @@ fn set_expr(p: &mut Parser) -> ParseResult {
|
||||
fn show_expr(p: &mut Parser) -> ParseResult {
|
||||
p.perform(NodeKind::ShowExpr, |p| {
|
||||
p.assert(NodeKind::Show);
|
||||
ident(p)?;
|
||||
p.expect(NodeKind::Colon)?;
|
||||
ident(p)?;
|
||||
let marker = p.marker();
|
||||
expr(p)?;
|
||||
if p.eat_if(NodeKind::Colon) {
|
||||
marker.filter_children(p, |child| match child.kind() {
|
||||
NodeKind::Ident(_) | NodeKind::Colon => Ok(()),
|
||||
_ => Err("expected identifier"),
|
||||
});
|
||||
expr(p)?;
|
||||
}
|
||||
p.expect(NodeKind::As)?;
|
||||
expr(p)
|
||||
})
|
||||
|
@ -1012,17 +1012,21 @@ node! {
|
||||
|
||||
impl ShowExpr {
|
||||
/// The binding to assign to.
|
||||
pub fn binding(&self) -> Ident {
|
||||
self.0.cast_first_child().expect("show rule is missing binding")
|
||||
pub fn binding(&self) -> Option<Ident> {
|
||||
let mut children = self.0.children();
|
||||
children
|
||||
.find_map(RedRef::cast)
|
||||
.filter(|_| children.any(|child| child.kind() == &NodeKind::Colon))
|
||||
}
|
||||
|
||||
/// The function to customize with this show rule.
|
||||
pub fn target(&self) -> Ident {
|
||||
/// The pattern that this rule matches.
|
||||
pub fn pattern(&self) -> Expr {
|
||||
self.0
|
||||
.children()
|
||||
.filter_map(RedRef::cast)
|
||||
.nth(1)
|
||||
.expect("show rule is missing target")
|
||||
.rev()
|
||||
.skip_while(|child| child.kind() != &NodeKind::As)
|
||||
.find_map(RedRef::cast)
|
||||
.expect("show rule is missing pattern")
|
||||
}
|
||||
|
||||
/// The expression that realizes the node.
|
||||
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
---
|
||||
// Error: 2-13 dictionary does not contain key: "invalid"
|
||||
// Error: 6-13 dictionary does not contain key: "invalid"
|
||||
{(:).invalid}
|
||||
|
||||
---
|
||||
|
Loading…
x
Reference in New Issue
Block a user