@@ -40,6 +40,85 @@ private static function maybe_add_quotes( $item ) {
4040 return $ item ;
4141 }
4242
43+ /**
44+ * Normalize @font-face font-family CSS text.
45+ *
46+ * This function attempts to be generous in the allowed values:
47+ * - Valid @font-face font-family values must return a semantically equivalent result.
48+ * - Normalization must be idempotent.
49+ * - Common mistakes such as providing multiple comma-separated font-family values return a
50+ * normalization of the first item: `a, b` becomes `"a"`.
51+ * - Invalid trailing content is ignored: `"string" garbage` becomes `"string"`.
52+ *
53+ * > Syntax of <family-name>
54+ * > <family-name> = <string> | <custom-ident>+
55+ *
56+ * > To avoid mistakes in escaping, it is recommended to quote font family names that contain
57+ * > white space, digits, or punctuation characters other than hyphens
58+ *
59+ * @see https://drafts.csswg.org/css-fonts/#family-name-syntax
60+ *
61+ * @param string $font_family CSS text @font-face font-family value.
62+ * @return string Normalized value.
63+ */
64+ public static function normalize_css_font_face_font_family ( string $ font_family ): ?string {
65+ $ processor = WP_CSS_Token_Processor::create ( $ font_family );
66+ if ( null === $ processor ) {
67+ return null ;
68+ }
69+
70+ // Ignore leading whitespace tokens.
71+ while ( $ processor ->next_token () && WP_CSS_Token_Processor::TOKEN_WHITESPACE === $ processor ->get_token_type () ) {
72+ continue ;
73+ }
74+
75+ $ token_type = $ processor ->get_token_type ();
76+ if ( WP_CSS_Token_Processor::TOKEN_STRING === $ token_type ) {
77+ $ plaintext_font_family = $ processor ->get_token_value ();
78+ return WP_CSS_Builder::string ( $ plaintext_font_family );
79+ }
80+
81+ /**
82+ * Idents can be composed to form a <family-name>, otherwise consider the
83+ * font-family invalid.
84+ */
85+ if ( WP_CSS_Token_Processor::TOKEN_IDENT !== $ token_type ) {
86+ return null ;
87+ }
88+
89+ /**
90+ * > If a sequence of identifiers is given as a <family-name>, the computed value is
91+ * > the name converted to a string by joining all the identifiers in the sequence
92+ * > by single spaces.
93+ *
94+ * @see https://drafts.csswg.org/css-fonts/#family-name-syntax
95+ */
96+ $ plaintext_font_ident_parts = array ( $ processor ->get_token_value () );
97+ while ( $ processor ->next_token () ) {
98+ switch ( $ processor ->get_token_type () ) {
99+ case WP_CSS_Token_Processor::TOKEN_IDENT :
100+ $ plaintext_font_family [] = $ processor ->get_token_value ();
101+ break ;
102+
103+ case WP_CSS_Token_Processor::TOKEN_WHITESPACE :
104+ continue ;
105+
106+ /**
107+ * Comma tokens suggest this was a multi-value font-family (for qualified rules, not
108+ * @font-face rules). Stop processing to handle only the first value.
109+ */
110+ case WP_CSS_Token_Processor::TOKEN_COMMA :
111+ break 2 ;
112+
113+ // Anything else is an error.
114+ default :
115+ return null ;
116+ }
117+ }
118+
119+ return WP_CSS_Builder::string ( implode ( ' ' , $ plaintext_font_ident_parts ) );
120+ }
121+
43122 /**
44123 * Sanitizes and formats font family names.
45124 *
0 commit comments