4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
7 namespace Wikimedia\CSS\Sanitizer;
9 use Wikimedia\CSS\Grammar\Alternative;
10 use Wikimedia\CSS\Grammar\FunctionMatcher;
11 use Wikimedia\CSS\Grammar\Juxtaposition;
12 use Wikimedia\CSS\Grammar\KeywordMatcher;
13 use Wikimedia\CSS\Grammar\MatcherFactory;
14 use Wikimedia\CSS\Grammar\Quantifier;
15 use Wikimedia\CSS\Grammar\TokenMatcher;
16 use Wikimedia\CSS\Grammar\UnorderedGroup;
17 use Wikimedia\CSS\Objects\AtRule;
18 use Wikimedia\CSS\Objects\CSSObject;
19 use Wikimedia\CSS\Objects\Rule;
20 use Wikimedia\CSS\Objects\Token;
21 use Wikimedia\CSS\Util;
24 * Sanitizes a CSS \@font-face rule
25 * @see https://www.w3.org/TR/2013/CR-css-fonts-3-20131003/#font-resources
27 class FontFaceAtRuleSanitizer extends RuleSanitizer {
29 /** @var PropertySanitizer */
30 protected $propertySanitizer;
33 * @param MatcherFactory $matcherFactory
35 public function __construct( MatcherFactory $matcherFactory ) {
36 $matchData = self::fontMatchData( $matcherFactory );
38 $this->propertySanitizer = new PropertySanitizer();
39 $this->propertySanitizer->setKnownProperties( [
40 'font-family' => $matchData['familyName'],
41 'src' => Quantifier::hash( new Alternative( [
43 $matcherFactory->url( 'font' ),
45 new FunctionMatcher( 'format', Quantifier::hash( $matcherFactory->string() ) )
48 new FunctionMatcher( 'local', $matchData['familyName'] ),
50 'font-style' => $matchData['font-style'],
51 'font-weight' => new Alternative( [
52 new KeywordMatcher( [ 'normal', 'bold' ] ), $matchData['numWeight']
54 'font-stretch' => $matchData['font-stretch'],
55 'unicode-range' => Quantifier::hash(
56 new TokenMatcher( Token::T_UNICODE_RANGE, function ( Token $t ) {
57 list( $start, $end ) = $t->range();
58 return $start <= $end && $end <= 0x10ffff;
61 'font-variant' => $matchData['font-variant'],
62 'font-feature-settings' => $matchData['font-feature-settings'],
67 * Get some shared data for font declaration matchers
68 * @param MatcherFactory $matcherFactory
71 public static function fontMatchData( MatcherFactory $matcherFactory ) {
72 $featureValueName = $matcherFactory->ident();
73 $featureValueNameHash = Quantifier::hash( $featureValueName );
75 'familyName' => new Alternative( [
76 $matcherFactory->string(),
77 Quantifier::plus( $matcherFactory->ident() ),
79 'numWeight' => new TokenMatcher( Token::T_NUMBER, function ( Token $t ) {
80 return $t->typeFlag() === 'integer' && preg_match( '/^[1-9]00$/', $t->representation() );
82 'font-style' => new KeywordMatcher( [ 'normal', 'italic', 'oblique' ] ),
83 'font-stretch' => new KeywordMatcher( [
84 'normal', 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded',
85 'expanded', 'extra-expanded', 'ultra-expanded'
87 'font-feature-settings' => new Alternative( [
88 new KeywordMatcher( 'normal' ),
89 Quantifier::hash( new Juxtaposition( [
90 new TokenMatcher( Token::T_STRING, function ( Token $t ) {
91 return preg_match( '/^[\x20-\x7e]{4}$/', $t->value() );
93 Quantifier::optional( new Alternative( [
94 $matcherFactory->integer(),
95 new KeywordMatcher( [ 'on', 'off' ] ),
100 new KeywordMatcher( [ 'common-ligatures', 'no-common-ligatures' ] ),
101 new KeywordMatcher( [ 'discretionary-ligatures', 'no-discretionary-ligatures' ] ),
102 new KeywordMatcher( [ 'historical-ligatures', 'no-historical-ligatures' ] ),
103 new KeywordMatcher( [ 'contextual', 'no-contextual' ] )
106 new FunctionMatcher( 'stylistic', $featureValueName ),
107 new KeywordMatcher( 'historical-forms' ),
108 new FunctionMatcher( 'styleset', $featureValueNameHash ),
109 new FunctionMatcher( 'character-variant', $featureValueNameHash ),
110 new FunctionMatcher( 'swash', $featureValueName ),
111 new FunctionMatcher( 'ornaments', $featureValueName ),
112 new FunctionMatcher( 'annotation', $featureValueName ),
115 'small-caps', 'all-small-caps', 'petite-caps', 'all-petite-caps', 'unicase', 'titling-caps'
118 new KeywordMatcher( [ 'lining-nums', 'oldstyle-nums' ] ),
119 new KeywordMatcher( [ 'proportional-nums', 'tabular-nums' ] ),
120 new KeywordMatcher( [ 'diagonal-fractions', 'stacked-fractions' ] ),
121 new KeywordMatcher( 'ordinal' ),
122 new KeywordMatcher( 'slashed-zero' ),
125 new KeywordMatcher( [ 'jis78', 'jis83', 'jis90', 'jis04', 'simplified', 'traditional' ] ),
126 new KeywordMatcher( [ 'full-width', 'proportional-width' ] ),
127 new KeywordMatcher( 'ruby' ),
130 $ret['font-variant'] = new Alternative( [
131 new KeywordMatcher( [ 'normal', 'none' ] ),
132 UnorderedGroup::someOf( array_merge(
135 [ new KeywordMatcher( $ret['capsKeywords'] ) ],
143 public function handlesRule( Rule $rule ) {
144 return $rule instanceof AtRule && !strcasecmp( $rule->getName(), 'font-face' );
147 protected function doSanitize( CSSObject $object ) {
148 if ( !$object instanceof Rule || !$this->handlesRule( $object ) ) {
149 $this->sanitizationError( 'expected-at-rule', $object, [ 'font-face' ] );
153 if ( $object->getBlock() === null ) {
154 $this->sanitizationError( 'at-rule-block-required', $object, [ 'font-face' ] );
158 // No non-whitespace prelude allowed
159 if ( Util::findFirstNonWhitespace( $object->getPrelude() ) ) {
160 $this->sanitizationError( 'invalid-font-face-at-rule', $object );
164 $ret = clone( $object );
165 $this->fixPreludeWhitespace( $ret, false );
166 $this->sanitizeDeclarationBlock( $ret->getBlock(), $this->propertySanitizer );