mirror of
https://github.com/servo/servo.git
synced 2025-08-04 13:10:20 +01:00
style: Implement parsing/serialization for @font-feature-values rule
This commit is contained in:
parent
c365a39104
commit
43cf493832
11 changed files with 496 additions and 6 deletions
|
@ -78,6 +78,7 @@ impl CSSRule {
|
||||||
StyleCssRule::Import(s) => Root::upcast(CSSImportRule::new(window, parent_stylesheet, s)),
|
StyleCssRule::Import(s) => Root::upcast(CSSImportRule::new(window, parent_stylesheet, s)),
|
||||||
StyleCssRule::Style(s) => Root::upcast(CSSStyleRule::new(window, parent_stylesheet, s)),
|
StyleCssRule::Style(s) => Root::upcast(CSSStyleRule::new(window, parent_stylesheet, s)),
|
||||||
StyleCssRule::FontFace(s) => Root::upcast(CSSFontFaceRule::new(window, parent_stylesheet, s)),
|
StyleCssRule::FontFace(s) => Root::upcast(CSSFontFaceRule::new(window, parent_stylesheet, s)),
|
||||||
|
StyleCssRule::FontFeatureValues(_) => unimplemented!(),
|
||||||
StyleCssRule::CounterStyle(_) => unimplemented!(),
|
StyleCssRule::CounterStyle(_) => unimplemented!(),
|
||||||
StyleCssRule::Keyframes(s) => Root::upcast(CSSKeyframesRule::new(window, parent_stylesheet, s)),
|
StyleCssRule::Keyframes(s) => Root::upcast(CSSKeyframesRule::new(window, parent_stylesheet, s)),
|
||||||
StyleCssRule::Media(s) => Root::upcast(CSSMediaRule::new(window, parent_stylesheet, s)),
|
StyleCssRule::Media(s) => Root::upcast(CSSMediaRule::new(window, parent_stylesheet, s)),
|
||||||
|
|
|
@ -18,8 +18,12 @@ pub enum ContextualParseError<'a> {
|
||||||
UnsupportedPropertyDeclaration(&'a str, ParseError<'a>),
|
UnsupportedPropertyDeclaration(&'a str, ParseError<'a>),
|
||||||
/// A font face descriptor was not recognized.
|
/// A font face descriptor was not recognized.
|
||||||
UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>),
|
UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>),
|
||||||
|
/// A font feature values descroptor was not recognized.
|
||||||
|
UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>),
|
||||||
/// A keyframe rule was not valid.
|
/// A keyframe rule was not valid.
|
||||||
InvalidKeyframeRule(&'a str, ParseError<'a>),
|
InvalidKeyframeRule(&'a str, ParseError<'a>),
|
||||||
|
/// A font feature values rule was not valid.
|
||||||
|
InvalidFontFeatureValuesRule(&'a str, ParseError<'a>),
|
||||||
/// A keyframe property declaration was not recognized.
|
/// A keyframe property declaration was not recognized.
|
||||||
UnsupportedKeyframePropertyDeclaration(&'a str, ParseError<'a>),
|
UnsupportedKeyframePropertyDeclaration(&'a str, ParseError<'a>),
|
||||||
/// A rule was invalid for some reason.
|
/// A rule was invalid for some reason.
|
||||||
|
@ -108,9 +112,15 @@ impl<'a> ContextualParseError<'a> {
|
||||||
ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) =>
|
ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) =>
|
||||||
format!("Unsupported @font-face descriptor declaration: '{}', {}", decl,
|
format!("Unsupported @font-face descriptor declaration: '{}', {}", decl,
|
||||||
parse_error_to_str(err)),
|
parse_error_to_str(err)),
|
||||||
|
ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) =>
|
||||||
|
format!("Unsupported @font-feature-values descriptor declaration: '{}', {}", decl,
|
||||||
|
parse_error_to_str(err)),
|
||||||
ContextualParseError::InvalidKeyframeRule(rule, ref err) =>
|
ContextualParseError::InvalidKeyframeRule(rule, ref err) =>
|
||||||
format!("Invalid keyframe rule: '{}', {}", rule,
|
format!("Invalid keyframe rule: '{}', {}", rule,
|
||||||
parse_error_to_str(err)),
|
parse_error_to_str(err)),
|
||||||
|
ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) =>
|
||||||
|
format!("Invalid font feature value rule: '{}', {}", rule,
|
||||||
|
parse_error_to_str(err)),
|
||||||
ContextualParseError::UnsupportedKeyframePropertyDeclaration(decl, ref err) =>
|
ContextualParseError::UnsupportedKeyframePropertyDeclaration(decl, ref err) =>
|
||||||
format!("Unsupported keyframe property declaration: '{}', {}", decl,
|
format!("Unsupported keyframe property declaration: '{}', {}", decl,
|
||||||
parse_error_to_str(err)),
|
parse_error_to_str(err)),
|
||||||
|
|
|
@ -301,7 +301,8 @@ impl StylesheetInvalidationSet {
|
||||||
CounterStyle(..) |
|
CounterStyle(..) |
|
||||||
Keyframes(..) |
|
Keyframes(..) |
|
||||||
Page(..) |
|
Page(..) |
|
||||||
Viewport(..) => {
|
Viewport(..) |
|
||||||
|
FontFeatureValues(..) => {
|
||||||
debug!(" > Found unsupported rule, marking the whole subtree \
|
debug!(" > Found unsupported rule, marking the whole subtree \
|
||||||
invalid.");
|
invalid.");
|
||||||
|
|
||||||
|
|
389
components/style/stylesheets/font_feature_values_rule.rs
Normal file
389
components/style/stylesheets/font_feature_values_rule.rs
Normal file
|
@ -0,0 +1,389 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
//! The [`@font-feature-values`][font-feature-values] at-rule.
|
||||||
|
//!
|
||||||
|
//! [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule
|
||||||
|
|
||||||
|
use Atom;
|
||||||
|
use computed_values::font_family::FamilyName;
|
||||||
|
use cssparser::{AtRuleParser, AtRuleType, BasicParseError, DeclarationListParser, DeclarationParser, Parser};
|
||||||
|
use cssparser::{CowRcStr, RuleListParser, SourceLocation, QualifiedRuleParser, Token, serialize_identifier};
|
||||||
|
use error_reporting::ContextualParseError;
|
||||||
|
use parser::{ParserContext, log_css_error, Parse};
|
||||||
|
use selectors::parser::SelectorParseError;
|
||||||
|
use shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
|
||||||
|
use std::fmt;
|
||||||
|
use style_traits::{ParseError, StyleParseError, ToCss};
|
||||||
|
use stylesheets::CssRuleType;
|
||||||
|
|
||||||
|
/// A @font-feature-values block declaration.
|
||||||
|
/// It is `<ident>: <integer>+`.
|
||||||
|
/// This struct can take 3 value types.
|
||||||
|
/// - `SingleValue` is to keep just one unsigned integer value.
|
||||||
|
/// - `PairValues` is to keep one or two unsigned integer values.
|
||||||
|
/// - `VectorValues` is to keep a list of unsigned integer values.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct FFVDeclaration<T> {
|
||||||
|
/// An `<ident>` for declaration name.
|
||||||
|
pub name: Atom,
|
||||||
|
/// An `<integer>+` for declaration value.
|
||||||
|
pub value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToCss> ToCss for FFVDeclaration<T> {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
serialize_identifier(&self.name.to_string(), dest)?;
|
||||||
|
dest.write_str(": ")?;
|
||||||
|
self.value.to_css(dest)?;
|
||||||
|
dest.write_str(";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A @font-feature-values block declaration value that keeps one value.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SingleValue(pub u32);
|
||||||
|
|
||||||
|
impl Parse for SingleValue {
|
||||||
|
fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<SingleValue, ParseError<'i>> {
|
||||||
|
match *input.next()? {
|
||||||
|
Token::Number { int_value: Some(v), .. } if v >= 0 => Ok(SingleValue(v as u32)),
|
||||||
|
ref t => Err(BasicParseError::UnexpectedToken(t.clone()).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCss for SingleValue {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
write!(dest, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A @font-feature-values block declaration value that keeps one or two values.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PairValues(pub u32, pub Option<u32>);
|
||||||
|
|
||||||
|
impl Parse for PairValues {
|
||||||
|
fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<PairValues, ParseError<'i>> {
|
||||||
|
let first = match *input.next()? {
|
||||||
|
Token::Number { int_value: Some(a), .. } if a >= 0 => a as u32,
|
||||||
|
ref t => return Err(BasicParseError::UnexpectedToken(t.clone()).into()),
|
||||||
|
};
|
||||||
|
match input.next() {
|
||||||
|
Ok(&Token::Number { int_value: Some(b), .. }) if b >= 0 => {
|
||||||
|
Ok(PairValues(first, Some(b as u32)))
|
||||||
|
}
|
||||||
|
// It can't be anything other than number.
|
||||||
|
Ok(t) => Err(BasicParseError::UnexpectedToken(t.clone()).into()),
|
||||||
|
// It can be just one value.
|
||||||
|
Err(_) => Ok(PairValues(first, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCss for PairValues {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
write!(dest, "{}", self.0)?;
|
||||||
|
if let Some(second) = self.1 {
|
||||||
|
write!(dest, " {}", second)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A @font-feature-values block declaration value that keeps a list of values.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct VectorValues(pub Vec<u32>);
|
||||||
|
|
||||||
|
impl Parse for VectorValues {
|
||||||
|
fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<VectorValues, ParseError<'i>> {
|
||||||
|
let mut vec = vec![];
|
||||||
|
loop {
|
||||||
|
match input.next() {
|
||||||
|
Ok(&Token::Number { int_value: Some(a), .. }) if a >= 0 => {
|
||||||
|
vec.push(a as u32);
|
||||||
|
},
|
||||||
|
// It can't be anything other than number.
|
||||||
|
Ok(t) => return Err(BasicParseError::UnexpectedToken(t.clone()).into()),
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if vec.len() == 0 {
|
||||||
|
return Err(BasicParseError::EndOfInput.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(VectorValues(vec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCss for VectorValues {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
let mut iter = self.0.iter();
|
||||||
|
let first = iter.next();
|
||||||
|
if let Some(first) = first {
|
||||||
|
write!(dest, "{}", first)?;
|
||||||
|
for value in iter {
|
||||||
|
dest.write_str(" ")?;
|
||||||
|
write!(dest, "{}", value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a list of `FamilyName`s.
|
||||||
|
pub fn parse_family_name_list<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<Vec<FamilyName>, ParseError<'i>> {
|
||||||
|
input.parse_comma_separated(|i| FamilyName::parse(context, i)).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @font-feature-values inside block parser. Parses a list of `FFVDeclaration`.
|
||||||
|
/// (`<ident>: <integer>+`)
|
||||||
|
struct FFVDeclarationsParser<'a, 'b: 'a, T: 'a> {
|
||||||
|
context: &'a ParserContext<'b>,
|
||||||
|
declarations: &'a mut Vec<FFVDeclaration<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default methods reject all at rules.
|
||||||
|
impl<'a, 'b, 'i, T> AtRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> {
|
||||||
|
type Prelude = ();
|
||||||
|
type AtRule = ();
|
||||||
|
type Error = SelectorParseError<'i, StyleParseError<'i>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'i, T> DeclarationParser<'i> for FFVDeclarationsParser<'a, 'b, T>
|
||||||
|
where T: Parse
|
||||||
|
{
|
||||||
|
type Declaration = ();
|
||||||
|
type Error = SelectorParseError<'i, StyleParseError<'i>>;
|
||||||
|
|
||||||
|
fn parse_value<'t>(&mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<(), ParseError<'i>> {
|
||||||
|
let value = input.parse_entirely(|i| T::parse(self.context, i))?;
|
||||||
|
let new = FFVDeclaration {
|
||||||
|
name: Atom::from(&*name),
|
||||||
|
value: value,
|
||||||
|
};
|
||||||
|
update_or_push(&mut self.declarations, new);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! font_feature_values_blocks {
|
||||||
|
(
|
||||||
|
blocks = [
|
||||||
|
$( #[$doc: meta] $name: tt $ident: ident / $ident_camel: ident: $ty: ty, )*
|
||||||
|
]
|
||||||
|
) => {
|
||||||
|
/// The [`@font-feature-values`][font-feature-values] at-rule.
|
||||||
|
///
|
||||||
|
/// [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct FontFeatureValuesRule {
|
||||||
|
/// Font family list for @font-feature-values rule.
|
||||||
|
/// Family names cannot contain generic families. FamilyName
|
||||||
|
/// also accepts only non-generic names.
|
||||||
|
pub family_names: Vec<FamilyName>,
|
||||||
|
$(
|
||||||
|
#[$doc]
|
||||||
|
pub $ident: Vec<FFVDeclaration<$ty>>,
|
||||||
|
)*
|
||||||
|
/// The line and column of the rule's source code.
|
||||||
|
pub source_location: SourceLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontFeatureValuesRule {
|
||||||
|
/// Creates an empty FontFeatureValuesRule with given location and family name list.
|
||||||
|
fn new(family_names: Vec<FamilyName>, location: SourceLocation) -> Self {
|
||||||
|
FontFeatureValuesRule {
|
||||||
|
family_names: family_names,
|
||||||
|
$(
|
||||||
|
$ident: vec![],
|
||||||
|
)*
|
||||||
|
source_location: location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a `FontFeatureValuesRule`.
|
||||||
|
pub fn parse(context: &ParserContext, input: &mut Parser,
|
||||||
|
family_names: Vec<FamilyName>, location: SourceLocation)
|
||||||
|
-> FontFeatureValuesRule {
|
||||||
|
let mut rule = FontFeatureValuesRule::new(family_names, location);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut iter = RuleListParser::new_for_nested_rule(input, FontFeatureValuesRuleParser {
|
||||||
|
context: context,
|
||||||
|
rule: &mut rule,
|
||||||
|
});
|
||||||
|
while let Some(result) = iter.next() {
|
||||||
|
if let Err(err) = result {
|
||||||
|
let pos = err.span.start;
|
||||||
|
let error = ContextualParseError::UnsupportedRule(
|
||||||
|
iter.input.slice(err.span), err.error);
|
||||||
|
log_css_error(iter.input, pos, error, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints font family names.
|
||||||
|
pub fn font_family_to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
let mut iter = self.family_names.iter();
|
||||||
|
iter.next().unwrap().to_css(dest)?;
|
||||||
|
for val in iter {
|
||||||
|
dest.write_str(", ")?;
|
||||||
|
val.to_css(dest)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints inside of `@font-feature-values` block.
|
||||||
|
pub fn value_to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
$(
|
||||||
|
if self.$ident.len() > 0 {
|
||||||
|
dest.write_str(concat!("@", $name, " {\n"))?;
|
||||||
|
let iter = self.$ident.iter();
|
||||||
|
for val in iter {
|
||||||
|
val.to_css(dest)?;
|
||||||
|
dest.write_str("\n")?
|
||||||
|
}
|
||||||
|
dest.write_str("}\n")?
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCssWithGuard for FontFeatureValuesRule {
|
||||||
|
fn to_css<W>(&self, _guard: &SharedRwLockReadGuard, dest: &mut W) -> fmt::Result
|
||||||
|
where W: fmt::Write
|
||||||
|
{
|
||||||
|
dest.write_str("@font-feature-values ")?;
|
||||||
|
self.font_family_to_css(dest)?;
|
||||||
|
dest.write_str(" {\n")?;
|
||||||
|
self.value_to_css(dest)?;
|
||||||
|
dest.write_str("}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates with new value if same `ident` exists, otherwise pushes to the vector.
|
||||||
|
fn update_or_push<T>(vec: &mut Vec<FFVDeclaration<T>>, element: FFVDeclaration<T>) {
|
||||||
|
let position = vec.iter().position(|ref val| val.name == element.name);
|
||||||
|
if let Some(index) = position {
|
||||||
|
vec[index].value = element.value;
|
||||||
|
} else {
|
||||||
|
vec.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps the information about block type like @swash, @styleset etc.
|
||||||
|
enum BlockType {
|
||||||
|
$(
|
||||||
|
$ident_camel,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parser for `FontFeatureValuesRule`. Parses all blocks
|
||||||
|
/// <feature-type> {
|
||||||
|
/// <feature-value-declaration-list>
|
||||||
|
/// }
|
||||||
|
/// <feature-type> = @stylistic | @historical-forms | @styleset |
|
||||||
|
/// @character-variant | @swash | @ornaments | @annotation
|
||||||
|
struct FontFeatureValuesRuleParser<'a> {
|
||||||
|
context: &'a ParserContext<'a>,
|
||||||
|
rule: &'a mut FontFeatureValuesRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default methods reject all qualified rules.
|
||||||
|
impl<'a, 'i> QualifiedRuleParser<'i> for FontFeatureValuesRuleParser<'a> {
|
||||||
|
type Prelude = ();
|
||||||
|
type QualifiedRule = ();
|
||||||
|
type Error = SelectorParseError<'i, StyleParseError<'i>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'i> AtRuleParser<'i> for FontFeatureValuesRuleParser<'a> {
|
||||||
|
type Prelude = BlockType;
|
||||||
|
type AtRule = ();
|
||||||
|
type Error = SelectorParseError<'i, StyleParseError<'i>>;
|
||||||
|
|
||||||
|
fn parse_prelude<'t>(&mut self,
|
||||||
|
name: CowRcStr<'i>,
|
||||||
|
_input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<AtRuleType<Self::Prelude, Self::AtRule>, ParseError<'i>> {
|
||||||
|
match_ignore_ascii_case! { &*name,
|
||||||
|
$(
|
||||||
|
$name => Ok(AtRuleType::WithBlock(BlockType::$ident_camel)),
|
||||||
|
)*
|
||||||
|
_ => Err(BasicParseError::AtRuleBodyInvalid.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_block<'t>(&mut self, prelude: Self::Prelude, input: &mut Parser<'i, 't>)
|
||||||
|
-> Result<Self::AtRule, ParseError<'i>> {
|
||||||
|
let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::FontFeatureValues));
|
||||||
|
match prelude {
|
||||||
|
$(
|
||||||
|
BlockType::$ident_camel => {
|
||||||
|
let parser = FFVDeclarationsParser {
|
||||||
|
context: &context,
|
||||||
|
declarations: &mut self.rule.$ident,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut iter = DeclarationListParser::new(input, parser);
|
||||||
|
while let Some(declaration) = iter.next() {
|
||||||
|
if let Err(err) = declaration {
|
||||||
|
let pos = err.span.start;
|
||||||
|
let error = ContextualParseError::UnsupportedKeyframePropertyDeclaration(
|
||||||
|
iter.input.slice(err.span), err.error);
|
||||||
|
log_css_error(iter.input, pos, error, &context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
font_feature_values_blocks! {
|
||||||
|
blocks = [
|
||||||
|
#[doc = "A @swash blocksck. \
|
||||||
|
Specifies a feature name that will work with the swash() \
|
||||||
|
functional notation of font-variant-alternates."]
|
||||||
|
"swash" swash / Swash: SingleValue,
|
||||||
|
|
||||||
|
#[doc = "A @stylistic block. \
|
||||||
|
Specifies a feature name that will work with the annotation() \
|
||||||
|
functional notation of font-variant-alternates."]
|
||||||
|
"stylistic" stylistic / Stylistic: SingleValue,
|
||||||
|
|
||||||
|
#[doc = "A @ornaments block. \
|
||||||
|
Specifies a feature name that will work with the ornaments() ] \
|
||||||
|
functional notation of font-variant-alternates."]
|
||||||
|
"ornaments" ornaments / Ornaments: SingleValue,
|
||||||
|
|
||||||
|
#[doc = "A @annotation block. \
|
||||||
|
Specifies a feature name that will work with the stylistic() \
|
||||||
|
functional notation of font-variant-alternates."]
|
||||||
|
"annotation" annotation / Annotation: SingleValue,
|
||||||
|
|
||||||
|
#[doc = "A @character-variant block. \
|
||||||
|
Specifies a feature name that will work with the styleset() \
|
||||||
|
functional notation of font-variant-alternates. The value can be a pair."]
|
||||||
|
"character-variant" character_variant / CharacterVariant: PairValues,
|
||||||
|
|
||||||
|
#[doc = "A @styleset block. \
|
||||||
|
Specifies a feature name that will work with the character-variant() \
|
||||||
|
functional notation of font-variant-alternates. The value can be a list."]
|
||||||
|
"styleset" styleset / Styleset: VectorValues,
|
||||||
|
]
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
mod counter_style_rule;
|
mod counter_style_rule;
|
||||||
mod document_rule;
|
mod document_rule;
|
||||||
mod font_face_rule;
|
mod font_face_rule;
|
||||||
|
pub mod font_feature_values_rule;
|
||||||
pub mod import_rule;
|
pub mod import_rule;
|
||||||
pub mod keyframes_rule;
|
pub mod keyframes_rule;
|
||||||
mod loader;
|
mod loader;
|
||||||
|
@ -33,6 +34,7 @@ use style_traits::PARSING_MODE_DEFAULT;
|
||||||
pub use self::counter_style_rule::CounterStyleRule;
|
pub use self::counter_style_rule::CounterStyleRule;
|
||||||
pub use self::document_rule::DocumentRule;
|
pub use self::document_rule::DocumentRule;
|
||||||
pub use self::font_face_rule::FontFaceRule;
|
pub use self::font_face_rule::FontFaceRule;
|
||||||
|
pub use self::font_feature_values_rule::FontFeatureValuesRule;
|
||||||
pub use self::import_rule::ImportRule;
|
pub use self::import_rule::ImportRule;
|
||||||
pub use self::keyframes_rule::KeyframesRule;
|
pub use self::keyframes_rule::KeyframesRule;
|
||||||
pub use self::loader::StylesheetLoader;
|
pub use self::loader::StylesheetLoader;
|
||||||
|
@ -103,6 +105,7 @@ pub enum CssRule {
|
||||||
Style(Arc<Locked<StyleRule>>),
|
Style(Arc<Locked<StyleRule>>),
|
||||||
Media(Arc<Locked<MediaRule>>),
|
Media(Arc<Locked<MediaRule>>),
|
||||||
FontFace(Arc<Locked<FontFaceRule>>),
|
FontFace(Arc<Locked<FontFaceRule>>),
|
||||||
|
FontFeatureValues(Arc<Locked<FontFeatureValuesRule>>),
|
||||||
CounterStyle(Arc<Locked<CounterStyleRule>>),
|
CounterStyle(Arc<Locked<CounterStyleRule>>),
|
||||||
Viewport(Arc<Locked<ViewportRule>>),
|
Viewport(Arc<Locked<ViewportRule>>),
|
||||||
Keyframes(Arc<Locked<KeyframesRule>>),
|
Keyframes(Arc<Locked<KeyframesRule>>),
|
||||||
|
@ -125,6 +128,7 @@ impl MallocSizeOfWithGuard for CssRule {
|
||||||
CssRule::Import(_) => 0,
|
CssRule::Import(_) => 0,
|
||||||
CssRule::Media(_) => 0,
|
CssRule::Media(_) => 0,
|
||||||
CssRule::FontFace(_) => 0,
|
CssRule::FontFace(_) => 0,
|
||||||
|
CssRule::FontFeatureValues(_) => 0,
|
||||||
CssRule::CounterStyle(_) => 0,
|
CssRule::CounterStyle(_) => 0,
|
||||||
CssRule::Keyframes(_) => 0,
|
CssRule::Keyframes(_) => 0,
|
||||||
CssRule::Namespace(_) => 0,
|
CssRule::Namespace(_) => 0,
|
||||||
|
@ -195,6 +199,7 @@ impl CssRule {
|
||||||
CssRule::Import(_) => CssRuleType::Import,
|
CssRule::Import(_) => CssRuleType::Import,
|
||||||
CssRule::Media(_) => CssRuleType::Media,
|
CssRule::Media(_) => CssRuleType::Media,
|
||||||
CssRule::FontFace(_) => CssRuleType::FontFace,
|
CssRule::FontFace(_) => CssRuleType::FontFace,
|
||||||
|
CssRule::FontFeatureValues(_) => CssRuleType::FontFeatureValues,
|
||||||
CssRule::CounterStyle(_) => CssRuleType::CounterStyle,
|
CssRule::CounterStyle(_) => CssRuleType::CounterStyle,
|
||||||
CssRule::Keyframes(_) => CssRuleType::Keyframes,
|
CssRule::Keyframes(_) => CssRuleType::Keyframes,
|
||||||
CssRule::Namespace(_) => CssRuleType::Namespace,
|
CssRule::Namespace(_) => CssRuleType::Namespace,
|
||||||
|
@ -298,6 +303,10 @@ impl DeepCloneWithLock for CssRule {
|
||||||
CssRule::FontFace(Arc::new(lock.wrap(
|
CssRule::FontFace(Arc::new(lock.wrap(
|
||||||
rule.clone_conditionally_gecko_or_servo())))
|
rule.clone_conditionally_gecko_or_servo())))
|
||||||
},
|
},
|
||||||
|
CssRule::FontFeatureValues(ref arc) => {
|
||||||
|
let rule = arc.read_with(guard);
|
||||||
|
CssRule::FontFeatureValues(Arc::new(lock.wrap(rule.clone())))
|
||||||
|
},
|
||||||
CssRule::CounterStyle(ref arc) => {
|
CssRule::CounterStyle(ref arc) => {
|
||||||
let rule = arc.read_with(guard);
|
let rule = arc.read_with(guard);
|
||||||
CssRule::CounterStyle(Arc::new(lock.wrap(
|
CssRule::CounterStyle(Arc::new(lock.wrap(
|
||||||
|
@ -340,6 +349,7 @@ impl ToCssWithGuard for CssRule {
|
||||||
CssRule::Import(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
CssRule::Import(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
CssRule::Style(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
CssRule::Style(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
CssRule::FontFace(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
CssRule::FontFace(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
|
CssRule::FontFeatureValues(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
CssRule::CounterStyle(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
CssRule::CounterStyle(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
CssRule::Viewport(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
CssRule::Viewport(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
CssRule::Keyframes(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
CssRule::Keyframes(ref lock) => lock.read_with(guard).to_css(guard, dest),
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
//! Parsing of the stylesheet contents.
|
//! Parsing of the stylesheet contents.
|
||||||
|
|
||||||
use {Namespace, Prefix};
|
use {Namespace, Prefix};
|
||||||
|
use computed_values::font_family::FamilyName;
|
||||||
use counter_style::{parse_counter_style_body, parse_counter_style_name};
|
use counter_style::{parse_counter_style_body, parse_counter_style_name};
|
||||||
use cssparser::{AtRuleParser, AtRuleType, Parser, QualifiedRuleParser, RuleListParser};
|
use cssparser::{AtRuleParser, AtRuleType, Parser, QualifiedRuleParser, RuleListParser};
|
||||||
use cssparser::{CowRcStr, SourceLocation, BasicParseError};
|
use cssparser::{CowRcStr, SourceLocation, BasicParseError};
|
||||||
|
@ -21,9 +22,10 @@ use shared_lock::{Locked, SharedRwLock};
|
||||||
use str::starts_with_ignore_ascii_case;
|
use str::starts_with_ignore_ascii_case;
|
||||||
use style_traits::{StyleParseError, ParseError};
|
use style_traits::{StyleParseError, ParseError};
|
||||||
use stylesheets::{CssRule, CssRules, CssRuleType, Origin, StylesheetLoader};
|
use stylesheets::{CssRule, CssRules, CssRuleType, Origin, StylesheetLoader};
|
||||||
use stylesheets::{DocumentRule, KeyframesRule, MediaRule, NamespaceRule, PageRule};
|
use stylesheets::{DocumentRule, FontFeatureValuesRule, KeyframesRule, MediaRule};
|
||||||
use stylesheets::{StyleRule, SupportsRule, ViewportRule};
|
use stylesheets::{NamespaceRule, PageRule, StyleRule, SupportsRule, ViewportRule};
|
||||||
use stylesheets::document_rule::DocumentCondition;
|
use stylesheets::document_rule::DocumentCondition;
|
||||||
|
use stylesheets::font_feature_values_rule::parse_family_name_list;
|
||||||
use stylesheets::keyframes_rule::parse_keyframe_list;
|
use stylesheets::keyframes_rule::parse_keyframe_list;
|
||||||
use stylesheets::stylesheet::Namespaces;
|
use stylesheets::stylesheet::Namespaces;
|
||||||
use stylesheets::supports_rule::SupportsCondition;
|
use stylesheets::supports_rule::SupportsCondition;
|
||||||
|
@ -101,6 +103,8 @@ pub enum VendorPrefix {
|
||||||
pub enum AtRulePrelude {
|
pub enum AtRulePrelude {
|
||||||
/// A @font-face rule prelude.
|
/// A @font-face rule prelude.
|
||||||
FontFace(SourceLocation),
|
FontFace(SourceLocation),
|
||||||
|
/// A @font-feature-values rule prelude, with its FamilyName list.
|
||||||
|
FontFeatureValues(Vec<FamilyName>, SourceLocation),
|
||||||
/// A @counter-style rule prelude, with its counter style name.
|
/// A @counter-style rule prelude, with its counter style name.
|
||||||
CounterStyle(CustomIdent),
|
CounterStyle(CustomIdent),
|
||||||
/// A @media rule prelude, with its media queries.
|
/// A @media rule prelude, with its media queries.
|
||||||
|
@ -345,6 +349,14 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'b> {
|
||||||
"font-face" => {
|
"font-face" => {
|
||||||
Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace(location)))
|
Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace(location)))
|
||||||
},
|
},
|
||||||
|
"font-feature-values" => {
|
||||||
|
if !cfg!(feature = "gecko") && !cfg!(feature = "testing") {
|
||||||
|
// Support for this rule is not fully implemented in Servo yet.
|
||||||
|
return Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
|
||||||
|
}
|
||||||
|
let family_names = parse_family_name_list(self.context, input)?;
|
||||||
|
Ok(AtRuleType::WithBlock(AtRulePrelude::FontFeatureValues(family_names, location)))
|
||||||
|
},
|
||||||
"counter-style" => {
|
"counter-style" => {
|
||||||
if !cfg!(feature = "gecko") {
|
if !cfg!(feature = "gecko") {
|
||||||
// Support for this rule is not fully implemented in Servo yet.
|
// Support for this rule is not fully implemented in Servo yet.
|
||||||
|
@ -413,6 +425,11 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'b> {
|
||||||
Ok(CssRule::FontFace(Arc::new(self.shared_lock.wrap(
|
Ok(CssRule::FontFace(Arc::new(self.shared_lock.wrap(
|
||||||
parse_font_face_block(&context, input, location).into()))))
|
parse_font_face_block(&context, input, location).into()))))
|
||||||
}
|
}
|
||||||
|
AtRulePrelude::FontFeatureValues(family_names, location) => {
|
||||||
|
let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::FontFeatureValues));
|
||||||
|
Ok(CssRule::FontFeatureValues(Arc::new(self.shared_lock.wrap(
|
||||||
|
FontFeatureValuesRule::parse(&context, input, family_names, location)))))
|
||||||
|
}
|
||||||
AtRulePrelude::CounterStyle(name) => {
|
AtRulePrelude::CounterStyle(name) => {
|
||||||
let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::CounterStyle));
|
let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::CounterStyle));
|
||||||
Ok(CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(
|
Ok(CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(
|
||||||
|
|
|
@ -86,7 +86,8 @@ impl<'a, 'b, C> Iterator for RulesIterator<'a, 'b, C>
|
||||||
CssRule::CounterStyle(_) |
|
CssRule::CounterStyle(_) |
|
||||||
CssRule::Viewport(_) |
|
CssRule::Viewport(_) |
|
||||||
CssRule::Keyframes(_) |
|
CssRule::Keyframes(_) |
|
||||||
CssRule::Page(_) => {
|
CssRule::Page(_) |
|
||||||
|
CssRule::FontFeatureValues(_) => {
|
||||||
return Some(rule)
|
return Some(rule)
|
||||||
},
|
},
|
||||||
CssRule::Import(ref import_rule) => {
|
CssRule::Import(ref import_rule) => {
|
||||||
|
|
|
@ -247,6 +247,7 @@ pub trait StylesheetInDocument {
|
||||||
effective_style_rules(Style => StyleRule),
|
effective_style_rules(Style => StyleRule),
|
||||||
effective_media_rules(Media => MediaRule),
|
effective_media_rules(Media => MediaRule),
|
||||||
effective_font_face_rules(FontFace => FontFaceRule),
|
effective_font_face_rules(FontFace => FontFaceRule),
|
||||||
|
effective_font_face_feature_values_rules(FontFeatureValues => FontFeatureValuesRule),
|
||||||
effective_counter_style_rules(CounterStyle => CounterStyleRule),
|
effective_counter_style_rules(CounterStyle => CounterStyleRule),
|
||||||
effective_viewport_rules(Viewport => ViewportRule),
|
effective_viewport_rules(Viewport => ViewportRule),
|
||||||
effective_keyframes_rules(Keyframes => KeyframesRule),
|
effective_keyframes_rules(Keyframes => KeyframesRule),
|
||||||
|
|
|
@ -995,7 +995,8 @@ impl Stylist {
|
||||||
CssRule::Keyframes(..) |
|
CssRule::Keyframes(..) |
|
||||||
CssRule::Page(..) |
|
CssRule::Page(..) |
|
||||||
CssRule::Viewport(..) |
|
CssRule::Viewport(..) |
|
||||||
CssRule::Document(..) => {
|
CssRule::Document(..) |
|
||||||
|
CssRule::FontFeatureValues(..) => {
|
||||||
// Not affected by device changes.
|
// Not affected by device changes.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,7 +203,9 @@ impl<'a> ErrorHelpers<'a> for ContextualParseError<'a> {
|
||||||
match self {
|
match self {
|
||||||
ContextualParseError::UnsupportedPropertyDeclaration(s, err) |
|
ContextualParseError::UnsupportedPropertyDeclaration(s, err) |
|
||||||
ContextualParseError::UnsupportedFontFaceDescriptor(s, err) |
|
ContextualParseError::UnsupportedFontFaceDescriptor(s, err) |
|
||||||
|
ContextualParseError::UnsupportedFontFeatureValuesDescriptor(s, err) |
|
||||||
ContextualParseError::InvalidKeyframeRule(s, err) |
|
ContextualParseError::InvalidKeyframeRule(s, err) |
|
||||||
|
ContextualParseError::InvalidFontFeatureValuesRule(s, err) |
|
||||||
ContextualParseError::UnsupportedKeyframePropertyDeclaration(s, err) |
|
ContextualParseError::UnsupportedKeyframePropertyDeclaration(s, err) |
|
||||||
ContextualParseError::InvalidRule(s, err) |
|
ContextualParseError::InvalidRule(s, err) |
|
||||||
ContextualParseError::UnsupportedRule(s, err) |
|
ContextualParseError::UnsupportedRule(s, err) |
|
||||||
|
@ -289,7 +291,9 @@ impl<'a> ErrorHelpers<'a> for ContextualParseError<'a> {
|
||||||
ContextualParseError::InvalidCounterStyleNotEnoughSymbols(..) |
|
ContextualParseError::InvalidCounterStyleNotEnoughSymbols(..) |
|
||||||
ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols |
|
ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols |
|
||||||
ContextualParseError::InvalidCounterStyleExtendsWithSymbols |
|
ContextualParseError::InvalidCounterStyleExtendsWithSymbols |
|
||||||
ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols =>
|
ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols |
|
||||||
|
ContextualParseError::UnsupportedFontFeatureValuesDescriptor(..) |
|
||||||
|
ContextualParseError::InvalidFontFeatureValuesRule(..) =>
|
||||||
(b"PEUnknownAtRule\0", Action::Skip),
|
(b"PEUnknownAtRule\0", Action::Skip),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ use servo_url::ServoUrl;
|
||||||
use std::borrow::ToOwned;
|
use std::borrow::ToOwned;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use style::computed_values::font_family::FamilyName;
|
||||||
use style::context::QuirksMode;
|
use style::context::QuirksMode;
|
||||||
use style::error_reporting::{ParseErrorReporter, ContextualParseError};
|
use style::error_reporting::{ParseErrorReporter, ContextualParseError};
|
||||||
use style::media_queries::MediaList;
|
use style::media_queries::MediaList;
|
||||||
|
@ -24,6 +25,8 @@ use style::properties::longhands::animation_timing_function;
|
||||||
use style::shared_lock::SharedRwLock;
|
use style::shared_lock::SharedRwLock;
|
||||||
use style::stylesheets::{Origin, Namespaces};
|
use style::stylesheets::{Origin, Namespaces};
|
||||||
use style::stylesheets::{Stylesheet, StylesheetContents, NamespaceRule, CssRule, CssRules, StyleRule, KeyframesRule};
|
use style::stylesheets::{Stylesheet, StylesheetContents, NamespaceRule, CssRule, CssRules, StyleRule, KeyframesRule};
|
||||||
|
use style::stylesheets::font_feature_values_rule::{FFVDeclaration, FontFeatureValuesRule};
|
||||||
|
use style::stylesheets::font_feature_values_rule::{SingleValue, PairValues, VectorValues};
|
||||||
use style::stylesheets::keyframes_rule::{Keyframe, KeyframeSelector, KeyframePercentage};
|
use style::stylesheets::keyframes_rule::{Keyframe, KeyframeSelector, KeyframePercentage};
|
||||||
use style::values::{KeyframesName, CustomIdent};
|
use style::values::{KeyframesName, CustomIdent};
|
||||||
use style::values::computed::Percentage;
|
use style::values::computed::Percentage;
|
||||||
|
@ -65,6 +68,14 @@ fn test_parse_stylesheet() {
|
||||||
animation-name: 'foo'; /* animation properties not allowed here */
|
animation-name: 'foo'; /* animation properties not allowed here */
|
||||||
animation-timing-function: ease; /* … except animation-timing-function */
|
animation-timing-function: ease; /* … except animation-timing-function */
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@font-feature-values test {
|
||||||
|
@swash { foo: 12; bar: 24; }
|
||||||
|
@swash { bar: 36; baz: 48; }
|
||||||
|
@stylistic { fooo: 14; }
|
||||||
|
@rubbish { shouldnt-parse: 1; }
|
||||||
|
@styleset { hello: 10 11 12; }
|
||||||
|
@character-variant { ok: 78 2; }
|
||||||
}";
|
}";
|
||||||
let url = ServoUrl::parse("about::test").unwrap();
|
let url = ServoUrl::parse("about::test").unwrap();
|
||||||
let lock = SharedRwLock::new();
|
let lock = SharedRwLock::new();
|
||||||
|
@ -239,6 +250,50 @@ fn test_parse_stylesheet() {
|
||||||
line: 16,
|
line: 16,
|
||||||
column: 19,
|
column: 19,
|
||||||
},
|
},
|
||||||
|
}))),
|
||||||
|
CssRule::FontFeatureValues(Arc::new(stylesheet.shared_lock.wrap(FontFeatureValuesRule {
|
||||||
|
family_names: vec![FamilyName {
|
||||||
|
name: Atom::from("test"),
|
||||||
|
quoted: false,
|
||||||
|
}],
|
||||||
|
swash: vec![
|
||||||
|
FFVDeclaration {
|
||||||
|
name: "foo".into(),
|
||||||
|
value: SingleValue(12 as u32),
|
||||||
|
},
|
||||||
|
FFVDeclaration {
|
||||||
|
name: "bar".into(),
|
||||||
|
value: SingleValue(36 as u32),
|
||||||
|
},
|
||||||
|
FFVDeclaration {
|
||||||
|
name: "baz".into(),
|
||||||
|
value: SingleValue(48 as u32),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stylistic: vec![
|
||||||
|
FFVDeclaration {
|
||||||
|
name: "fooo".into(),
|
||||||
|
value: SingleValue(14 as u32),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
ornaments: vec![],
|
||||||
|
annotation: vec![],
|
||||||
|
character_variant: vec![
|
||||||
|
FFVDeclaration {
|
||||||
|
name: "ok".into(),
|
||||||
|
value: PairValues(78 as u32, Some(2 as u32)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
styleset: vec![
|
||||||
|
FFVDeclaration {
|
||||||
|
name: "hello".into(),
|
||||||
|
value: VectorValues(vec![10 as u32, 11 as u32, 12 as u32]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source_location: SourceLocation {
|
||||||
|
line: 25,
|
||||||
|
column: 29,
|
||||||
|
},
|
||||||
})))
|
})))
|
||||||
|
|
||||||
], &stylesheet.shared_lock),
|
], &stylesheet.shared_lock),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue