Implement moz-transform property

This commit is contained in:
Nazım Can Altınova 2017-04-07 23:49:18 +03:00
parent 67799f9445
commit f9225d84aa
No known key found for this signature in database
GPG key ID: AF9BCD7CE6449954
7 changed files with 320 additions and 46 deletions

View file

@ -2902,6 +2902,10 @@ impl Fragment {
transform::ComputedOperation::Matrix(m) => {
m.to_gfx_matrix()
}
transform::ComputedOperation::MatrixWithPercents(_) => {
// `-moz-transform` is not implemented in Servo yet.
unreachable!()
}
transform::ComputedOperation::Skew(theta_x, theta_y) => {
Matrix4D::create_skew(Radians::new(theta_x.radians()),
Radians::new(theta_y.radians()))

View file

@ -479,12 +479,23 @@ impl ToCss for PropertyDeclarationBlock {
};
// Substeps 7 and 8
// We need to check the shorthand whether it's an alias property or not.
// If it's an alias property, it should be serialized like its longhand.
if shorthand.flags().contains(ALIAS_PROPERTY) {
append_serialization::<_, Cloned<slice::Iter< _>>, _>(
dest,
&property,
value,
importance,
&mut is_first_serialization)?;
} else {
append_serialization::<_, Cloned<slice::Iter< _>>, _>(
dest,
&shorthand,
value,
importance,
&mut is_first_serialization)?;
}
for current_longhand in &current_longhands {
// Substep 9

View file

@ -1834,12 +1834,14 @@ fn static_assert() {
<%def name="transform_function_arm(name, keyword, items)">
<%
pattern = None
if name == "matrix":
# m11, m12, m13, ..
indices = [str(i) + str(j) for i in range(1, 5) for j in range(1, 5)]
if keyword == "matrix3d":
# m11: number1, m12: number2, ..
single_patterns = ["m%s: number%s" % (index, i + 1) for (i, index) in enumerate(indices)]
single_patterns = ["m%s: %s" % (str(a / 4 + 1) + str(a % 4 + 1), b + str(a + 1)) for (a, b)
in enumerate(items)]
if name == "Matrix":
pattern = "ComputedMatrix { %s }" % ", ".join(single_patterns)
else:
pattern = "ComputedMatrixWithPercents { %s }" % ", ".join(single_patterns)
else:
# Generate contents of pattern from items
pattern = ", ".join([b + str(a+1) for (a,b) in enumerate(items)])
@ -1854,7 +1856,7 @@ fn static_assert() {
"number" : "bindings::Gecko_CSSValue_SetNumber(%s, %s)",
}
%>
longhands::transform::computed_value::ComputedOperation::${name.title()}(${pattern}) => {
longhands::transform::computed_value::ComputedOperation::${name}(${pattern}) => {
bindings::Gecko_CSSValue_SetFunction(gecko_value, ${len(items) + 1});
bindings::Gecko_CSSValue_SetKeyword(
bindings::Gecko_CSSValue_GetArrayItem(gecko_value, 0),
@ -1873,6 +1875,7 @@ fn static_assert() {
use gecko_bindings::structs::nsCSSKeyword::*;
use gecko_bindings::sugar::refptr::RefPtr;
use properties::longhands::transform::computed_value::ComputedMatrix;
use properties::longhands::transform::computed_value::ComputedMatrixWithPercents;
unsafe { output.clear() };
@ -1888,12 +1891,14 @@ fn static_assert() {
value list of the same length as the transform vector");
unsafe {
match servo {
${transform_function_arm("matrix", "matrix3d", ["number"] * 16)}
${transform_function_arm("skew", "skew", ["angle"] * 2)}
${transform_function_arm("translate", "translate3d", ["lop", "lop", "length"])}
${transform_function_arm("scale", "scale3d", ["number"] * 3)}
${transform_function_arm("rotate", "rotate3d", ["number"] * 3 + ["angle"])}
${transform_function_arm("perspective", "perspective", ["length"])}
${transform_function_arm("Matrix", "matrix3d", ["number"] * 16)}
${transform_function_arm("MatrixWithPercents", "matrix3d", ["number"] * 12 + ["lop"] * 2
+ ["length"] + ["number"])}
${transform_function_arm("Skew", "skew", ["angle"] * 2)}
${transform_function_arm("Translate", "translate3d", ["lop", "lop", "length"])}
${transform_function_arm("Scale", "scale3d", ["number"] * 3)}
${transform_function_arm("Rotate", "rotate3d", ["number"] * 3 + ["angle"])}
${transform_function_arm("Perspective", "perspective", ["length"])}
}
cur = (*cur).mNext;
}
@ -1929,19 +1934,19 @@ fn static_assert() {
}
%>
eCSSKeyword_${keyword} => {
ComputedOperation::${name.title()}(
% if name == "matrix":
ComputedOperation::${name}(
% if keyword == "matrix3d":
ComputedMatrix {
% endif
% for index, item in enumerate(items):
% if name == "matrix":
% if keyword == "matrix3d":
m${index / 4 + 1}${index % 4 + 1}:
% endif
${css_value_getters[item] % (
"bindings::Gecko_CSSValue_GetArrayItemConst(gecko_value, %d)" % (index + 1)
)},
% endfor
% if name == "matrix":
% if keyword == "matrix3d":
}
% endif
)
@ -1968,12 +1973,12 @@ fn static_assert() {
};
let servo = unsafe {
match transform_function {
${computed_operation_arm("matrix", "matrix3d", ["number"] * 16)}
${computed_operation_arm("skew", "skew", ["angle"] * 2)}
${computed_operation_arm("translate", "translate3d", ["lop", "lop", "length"])}
${computed_operation_arm("scale", "scale3d", ["number"] * 3)}
${computed_operation_arm("rotate", "rotate3d", ["number"] * 3 + ["angle"])}
${computed_operation_arm("perspective", "perspective", ["length"])}
${computed_operation_arm("Matrix", "matrix3d", ["number"] * 16)}
${computed_operation_arm("Skew", "skew", ["angle"] * 2)}
${computed_operation_arm("Translate", "translate3d", ["lop", "lop", "length"])}
${computed_operation_arm("Scale", "scale3d", ["number"] * 3)}
${computed_operation_arm("Rotate", "rotate3d", ["number"] * 3 + ["angle"])}
${computed_operation_arm("Perspective", "perspective", ["length"])}
_ => panic!("We shouldn't set any other transform function types"),
}
};

View file

@ -1127,6 +1127,7 @@ fn build_identity_transform_list(list: &[TransformOperation]) -> Vec<TransformOp
let identity = ComputedMatrix::identity();
result.push(TransformOperation::Matrix(identity));
}
TransformOperation::MatrixWithPercents(..) => {}
TransformOperation::Skew(..) => {
result.push(TransformOperation::Skew(Angle::zero(), Angle::zero()))
}
@ -1167,6 +1168,12 @@ fn interpolate_transform_list(from_list: &[TransformOperation],
let interpolated = from.interpolate(&_to, progress).unwrap();
result.push(TransformOperation::Matrix(interpolated));
}
(&TransformOperation::MatrixWithPercents(_),
&TransformOperation::MatrixWithPercents(_)) => {
// We don't interpolate `-moz-transform` matrices yet.
// They contain percentage values.
{}
}
(&TransformOperation::Skew(fx, fy),
&TransformOperation::Skew(tx, ty)) => {
let ix = fx.interpolate(&tx, progress).unwrap();

View file

@ -1108,22 +1108,26 @@ ${helpers.predefined_type("scroll-snap-coordinate",
<%helpers:longhand name="transform" products="gecko servo" extra_prefixes="webkit"
<%helpers:longhand name="transform" extra_prefixes="webkit"
animation_type="normal"
flags="CREATES_STACKING_CONTEXT FIXPOS_CB"
spec="https://drafts.csswg.org/css-transforms/#propdef-transform">
use app_units::Au;
use values::specified::{Angle, Length, LengthOrPercentage, Number};
use values::computed::{LengthOrPercentageOrNumber as ComputedLoPoNumber, LengthOrNumber as ComputedLoN};
use values::computed::{LengthOrPercentage as ComputedLoP, Length as ComputedLength};
use values::specified::{Angle, Length, LengthOrPercentage};
use values::specified::{LengthOrNumber, LengthOrPercentageOrNumber as LoPoNumber, Number};
use style_traits::ToCss;
use style_traits::values::Css;
use values::CSSFloat;
use values::HasViewportPercentage;
use std::fmt::{self, Display};
pub mod computed_value {
use app_units::Au;
use values::CSSFloat;
use values::computed;
use values::computed::{Length, LengthOrPercentage};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
@ -1134,6 +1138,16 @@ ${helpers.predefined_type("scroll-snap-coordinate",
pub m41: CSSFloat, pub m42: CSSFloat, pub m43: CSSFloat, pub m44: CSSFloat,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct ComputedMatrixWithPercents {
pub m11: CSSFloat, pub m12: CSSFloat, pub m13: CSSFloat, pub m14: CSSFloat,
pub m21: CSSFloat, pub m22: CSSFloat, pub m23: CSSFloat, pub m24: CSSFloat,
pub m31: CSSFloat, pub m32: CSSFloat, pub m33: CSSFloat, pub m34: CSSFloat,
pub m41: LengthOrPercentage, pub m42: LengthOrPercentage,
pub m43: Length, pub m44: CSSFloat,
}
impl ComputedMatrix {
pub fn identity() -> ComputedMatrix {
ComputedMatrix {
@ -1145,10 +1159,24 @@ ${helpers.predefined_type("scroll-snap-coordinate",
}
}
impl ComputedMatrixWithPercents {
pub fn identity() -> ComputedMatrixWithPercents {
ComputedMatrixWithPercents {
m11: 1.0, m12: 0.0, m13: 0.0, m14: 0.0,
m21: 0.0, m22: 1.0, m23: 0.0, m24: 0.0,
m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0,
m41: LengthOrPercentage::zero(), m42: LengthOrPercentage::zero(),
m43: Au(0), m44: 1.0
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub enum ComputedOperation {
Matrix(ComputedMatrix),
// For `-moz-transform` matrix and matrix3d.
MatrixWithPercents(ComputedMatrixWithPercents),
Skew(computed::Angle, computed::Angle),
Translate(computed::LengthOrPercentage,
computed::LengthOrPercentage,
@ -1174,6 +1202,9 @@ ${helpers.predefined_type("scroll-snap-coordinate",
pub enum SpecifiedOperation {
/// Represents a 2D 2x3 matrix.
Matrix { a: Number, b: Number, c: Number, d: Number, e: Number, f: Number },
/// Represents a 3D 4x4 matrix with percentage and length values.
/// For `moz-transform`.
PrefixedMatrix { a: Number, b: Number, c: Number, d: Number, e: LoPoNumber, f: LoPoNumber },
/// Represents a 3D 4x4 matrix.
Matrix3D {
m11: Number, m12: Number, m13: Number, m14: Number,
@ -1181,6 +1212,14 @@ ${helpers.predefined_type("scroll-snap-coordinate",
m31: Number, m32: Number, m33: Number, m34: Number,
m41: Number, m42: Number, m43: Number, m44: Number,
},
/// Represents a 3D 4x4 matrix with percentage and length values.
/// For `moz-transform`.
PrefixedMatrix3D {
m11: Number, m12: Number, m13: Number, m14: Number,
m21: Number, m22: Number, m23: Number, m24: Number,
m31: Number, m32: Number, m33: Number, m34: Number,
m41: LoPoNumber, m42: LoPoNumber, m43: LengthOrNumber, m44: Number,
},
/// A 2D skew.
///
/// If the second angle is not provided it is assumed zero.
@ -1254,6 +1293,15 @@ ${helpers.predefined_type("scroll-snap-coordinate",
l3.has_viewport_percentage()
},
SpecifiedOperation::Perspective(ref length) => length.has_viewport_percentage(),
SpecifiedOperation::PrefixedMatrix{ ref e, ref f, .. } => {
e.has_viewport_percentage() ||
f.has_viewport_percentage()
},
SpecifiedOperation::PrefixedMatrix3D{ ref m41, ref m42, ref m43, .. } => {
m41.has_viewport_percentage() ||
m42.has_viewport_percentage() ||
m43.has_viewport_percentage()
},
_ => false
}
}
@ -1266,6 +1314,9 @@ ${helpers.predefined_type("scroll-snap-coordinate",
Matrix { a, b, c, d, e, f} => write!(
dest, "matrix({}, {}, {}, {}, {}, {})",
Css(a), Css(b), Css(c), Css(d), Css(e), Css(f)),
PrefixedMatrix { a, b, c, d, ref e, ref f} => write!(
dest, "matrix({}, {}, {}, {}, {}, {})",
Css(a), Css(b), Css(c), Css(d), Css(e), Css(f)),
Matrix3D {
m11, m12, m13, m14,
m21, m22, m23, m24,
@ -1276,6 +1327,16 @@ ${helpers.predefined_type("scroll-snap-coordinate",
Css(m21), Css(m22), Css(m23), Css(m24),
Css(m31), Css(m32), Css(m33), Css(m34),
Css(m41), Css(m42), Css(m43), Css(m44)),
PrefixedMatrix3D {
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
ref m41, ref m42, ref m43, m44 } => write!(
dest, "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
Css(m11), Css(m12), Css(m13), Css(m14),
Css(m21), Css(m22), Css(m23), Css(m24),
Css(m31), Css(m32), Css(m33), Css(m34),
Css(m41), Css(m42), Css(m43), Css(m44)),
Skew(ax, None) => write!(dest, "skew({})", Css(ax)),
Skew(ax, Some(ay)) => write!(dest, "skew({}, {})", Css(ax), Css(ay)),
SkewX(angle) => write!(dest, "skewX({})", Css(angle)),
@ -1341,7 +1402,8 @@ ${helpers.predefined_type("scroll-snap-coordinate",
}
// Allow unitless zero angle for rotate() and skew() to align with gecko
pub fn parse(context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue,()> {
fn parse_internal(context: &ParserContext, input: &mut Parser, prefixed: bool)
-> Result<SpecifiedValue,()> {
if input.try(|input| input.expect_ident_matching("none")).is_ok() {
return Ok(SpecifiedValue(Vec::new()))
}
@ -1356,35 +1418,111 @@ ${helpers.predefined_type("scroll-snap-coordinate",
&name,
"matrix" => {
try!(input.parse_nested_block(|input| {
// Standard matrix parsing.
if !prefixed {
let values = try!(input.parse_comma_separated(|input| {
specified::parse_number(context, input)
}));
if values.len() != 6 {
return Err(())
}
result.push(SpecifiedOperation::Matrix {
a: values[0],
b: values[1],
c: values[2],
d: values[3],
e: values[4],
f: values[5]
f: values[5],
});
return Ok(());
}
// Non-standard prefixed matrix parsing.
//
// -moz-transform accepts LengthOrPercentageOrNumber in the
// nondiagonal homogeneous components. transform accepts only number.
let mut values = Vec::with_capacity(4);
let mut lengths = Vec::with_capacity(2);
// Consume first number
values.push(specified::parse_number(context, input)?);
// Parse other 5 number/LengthOrPercentageOrNumber
for i in 0..5 {
input.expect_comma()?;
if i < 3 {
values.push(specified::parse_number(context, input)?);
} else {
// -moz-transform accepts LengthOrPercentageOrNumber in the nondiagonal
// homogeneous components. transform accepts only number.
lengths.push(LoPoNumber::parse(context, input)?)
}
}
result.push(SpecifiedOperation::PrefixedMatrix {
a: values[0],
b: values[1],
c: values[2],
d: values[3],
e: lengths[0].clone(),
f: lengths[1].clone(),
});
Ok(())
}))
},
"matrix3d" => {
try!(input.parse_nested_block(|input| {
// Standard matrix3d parsing.
if !prefixed {
let values = try!(input.parse_comma_separated(|i| specified::parse_number(context, i)));
if values.len() != 16 {
return Err(())
}
result.push(SpecifiedOperation::Matrix3D {
m11: values[ 0], m12: values[ 1], m13: values[ 2], m14: values[ 3],
m21: values[ 4], m22: values[ 5], m23: values[ 6], m24: values[ 7],
m31: values[ 8], m32: values[ 9], m33: values[10], m34: values[11],
m41: values[12], m42: values[13], m43: values[14], m44: values[15]
});
return Ok(());
}
// Non-standard prefixed matrix3d parsing.
//
// -moz-transform accepts LengthOrPercentageOrNumber in the
// nondiagonal homogeneous components. transform accepts only number.
let mut values = Vec::with_capacity(13);
let mut lops = Vec::with_capacity(2);
let mut length_or_number = None;
// Parse first number
values.push(specified::parse_number(context, input)?);
// Parse other 15 number/LengthOrPercentageOrNumber
for i in 0..15 {
input.expect_comma()?;
// -moz-transform accepts LengthOrPercentageOrNumber in the nondiagonal
// homogeneous components. transform accepts only number.
if i < 11 || i > 13 {
values.push(specified::parse_number(context, input)?);
} else if i == 13 {
// m43
length_or_number = Some(LengthOrNumber::parse(context, input)?);
} else {
// m41 and m42
lops.push(LoPoNumber::parse(context, input)?);
}
}
result.push(SpecifiedOperation::PrefixedMatrix3D {
m11: values[ 0], m12: values[ 1], m13: values[ 2], m14: values[ 3],
m21: values[ 4], m22: values[ 5], m23: values[ 6], m24: values[ 7],
m31: values[ 8], m32: values[ 9], m33: values[10], m34: values[11],
m41: lops[0].clone(), m42: lops[1].clone(), m43: length_or_number.unwrap(),
m44: values[12]
});
Ok(())
}))
},
@ -1562,12 +1700,26 @@ ${helpers.predefined_type("scroll-snap-coordinate",
}
}
/// Parses `transform` property.
#[inline]
pub fn parse(context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue,()> {
parse_internal(context, input, false)
}
/// Parses `-moz-transform` property. This prefixed property also accepts LengthOrPercentage
/// in the nondiagonal homogeneous components of matrix and matrix3d.
#[inline]
pub fn parse_prefixed(context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue,()> {
parse_internal(context, input, true)
}
impl ToComputedValue for SpecifiedValue {
type ComputedValue = computed_value::T;
#[inline]
fn to_computed_value(&self, context: &Context) -> computed_value::T {
use self::SpecifiedOperation::*;
if self.0.is_empty() {
return computed_value::T(None)
}
@ -1585,11 +1737,21 @@ ${helpers.predefined_type("scroll-snap-coordinate",
comp.m42 = f.to_computed_value(context);
result.push(computed_value::ComputedOperation::Matrix(comp));
}
PrefixedMatrix { a, b, c, d, ref e, ref f } => {
let mut comp = computed_value::ComputedMatrixWithPercents::identity();
comp.m11 = a.to_computed_value(context);
comp.m12 = b.to_computed_value(context);
comp.m21 = c.to_computed_value(context);
comp.m22 = d.to_computed_value(context);
comp.m41 = lopon_to_lop(&e.to_computed_value(context));
comp.m42 = lopon_to_lop(&f.to_computed_value(context));
result.push(computed_value::ComputedOperation::MatrixWithPercents(comp));
}
Matrix3D {
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
m41, m42, m43, m44 } => {
ref m41, ref m42, ref m43, m44 } => {
let comp = computed_value::ComputedMatrix {
m11: m11.to_computed_value(context),
m12: m12.to_computed_value(context),
@ -1610,6 +1772,31 @@ ${helpers.predefined_type("scroll-snap-coordinate",
};
result.push(computed_value::ComputedOperation::Matrix(comp));
}
PrefixedMatrix3D {
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
ref m41, ref m42, ref m43, m44 } => {
let comp = computed_value::ComputedMatrixWithPercents {
m11: m11.to_computed_value(context),
m12: m12.to_computed_value(context),
m13: m13.to_computed_value(context),
m14: m14.to_computed_value(context),
m21: m21.to_computed_value(context),
m22: m22.to_computed_value(context),
m23: m23.to_computed_value(context),
m24: m24.to_computed_value(context),
m31: m31.to_computed_value(context),
m32: m32.to_computed_value(context),
m33: m33.to_computed_value(context),
m34: m34.to_computed_value(context),
m41: lopon_to_lop(&m41.to_computed_value(context)),
m42: lopon_to_lop(&m42.to_computed_value(context)),
m43: lon_to_length(&m43.to_computed_value(context)),
m44: m44.to_computed_value(context),
};
result.push(computed_value::ComputedOperation::MatrixWithPercents(comp));
}
Translate(ref tx, None) => {
let tx = tx.to_computed_value(context);
result.push(computed_value::ComputedOperation::Translate(
@ -1755,6 +1942,26 @@ ${helpers.predefined_type("scroll-snap-coordinate",
m44: Number::from_computed_value(&computed.m44),
});
}
computed_value::ComputedOperation::MatrixWithPercents(ref computed) => {
result.push(SpecifiedOperation::PrefixedMatrix3D {
m11: Number::from_computed_value(&computed.m11),
m12: Number::from_computed_value(&computed.m12),
m13: Number::from_computed_value(&computed.m13),
m14: Number::from_computed_value(&computed.m14),
m21: Number::from_computed_value(&computed.m21),
m22: Number::from_computed_value(&computed.m22),
m23: Number::from_computed_value(&computed.m23),
m24: Number::from_computed_value(&computed.m24),
m31: Number::from_computed_value(&computed.m31),
m32: Number::from_computed_value(&computed.m32),
m33: Number::from_computed_value(&computed.m33),
m34: Number::from_computed_value(&computed.m34),
m41: Either::First(LengthOrPercentage::from_computed_value(&computed.m41)),
m42: Either::First(LengthOrPercentage::from_computed_value(&computed.m42)),
m43: LengthOrNumber::from_computed_value(&Either::First(computed.m43)),
m44: Number::from_computed_value(&computed.m44),
});
}
computed_value::ComputedOperation::Translate(ref tx, ref ty, ref tz) => {
// XXXManishearth we lose information here; perhaps we should try to
// recover the original function? Not sure if this can be observed.
@ -1792,6 +1999,24 @@ ${helpers.predefined_type("scroll-snap-coordinate",
}).unwrap_or(Vec::new()))
}
}
// Converts computed LengthOrPercentageOrNumber into computed
// LengthOrPercentage. Number maps into Length
fn lopon_to_lop(value: &ComputedLoPoNumber) -> ComputedLoP {
match *value {
Either::First(length_or_percentage) => length_or_percentage,
Either::Second(number) => ComputedLoP::Length(Au::from_f32_px(number)),
}
}
// Converts computed LengthOrNumber into computed Length.
// Number maps into Length.
fn lon_to_length(value: &ComputedLoN) -> ComputedLength {
match *value {
Either::First(length) => length,
Either::Second(number) => Au::from_f32_px(number),
}
}
</%helpers:longhand>
// CSSOM View Module

View file

@ -476,6 +476,8 @@ bitflags! {
/// This property has values that can establish a containing block for
/// absolutely positioned elements.
const ABSPOS_CB = 1 << 2,
/// This property(shorthand) is an alias of another property.
const ALIAS_PROPERTY = 1 << 3,
}
}

View file

@ -288,3 +288,23 @@ macro_rules! try_parse_one {
}
}
</%helpers:shorthand>
<%helpers:shorthand name="-moz-transform" products="gecko"
sub_properties="transform"
flags="ALIAS_PROPERTY"
spec="Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/transform">
use properties::longhands::transform;
pub fn parse_value(context: &ParserContext, input: &mut Parser) -> Result<Longhands, ()> {
Ok(Longhands {
transform: transform::parse_prefixed(context, input)?,
})
}
impl<'a> ToCss for LonghandsToSerialize<'a> {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
self.transform.to_css(dest)
}
}
</%helpers:shorthand>