PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / trunk
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant vtrunk
2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / src / Parser / GoogleUrlParser.php

GoogleUrlParser.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant trunk, at src/Parser/GoogleUrlParser.php

194 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Google Fonts API URL parser.
4 *
5 * Understands both the legacy v1 syntax (?family=Roboto:400,700|Open+Sans:300)
6 * and the css2 syntax (?family=Roboto:ital,wght@0,400;1,700).
7 *
8 * @package EasyFonts
9 */
10
11 namespace EasyFonts\Parser;
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * Turns a Google/Bunny Fonts CSS URL into a structured family map.
17 */
18 class GoogleUrlParser {
19
20 /**
21 * Parse a CSS URL into families.
22 *
23 * @param string $url Stylesheet URL.
24 * @return array<string,array{weights:string[],styles:string[]}>
25 */
26 public function parse( string $url ): array {
27 $url = html_entity_decode( $url, ENT_QUOTES, 'UTF-8' );
28 $query = wp_parse_url( $url, PHP_URL_QUERY );
29
30 if ( ! $query ) {
31 return array();
32 }
33
34 parse_str( $query, $params );
35
36 $raw = array();
37
38 if ( isset( $params['family'] ) ) {
39 $raw = is_array( $params['family'] ) ? $params['family'] : array( $params['family'] );
40 }
41
42 $families = array();
43
44 foreach ( $raw as $family_string ) {
45 $this->parse_family_string( (string) $family_string, $families );
46 }
47
48 return $families;
49 }
50
51 /**
52 * Parse a single family= value (may contain v1 pipes or a css2 axis spec).
53 *
54 * @param string $value Family value.
55 * @param array<string,array{weights:string[],styles:string[]}> $families Accumulator (by ref).
56 */
57 private function parse_family_string( string $value, array &$families ): void {
58 if ( false !== strpos( $value, '@' ) ) {
59 $this->parse_css2( $value, $families );
60 return;
61 }
62
63 // v1: pipe-separated families, each "Name:specs".
64 foreach ( explode( '|', $value ) as $chunk ) {
65 $parts = explode( ':', $chunk, 2 );
66 $family = $this->clean_name( $parts[0] );
67
68 if ( '' === $family ) {
69 continue;
70 }
71
72 $weights = array();
73 $styles = array();
74
75 $specs = $parts[1] ?? '400';
76
77 foreach ( explode( ',', $specs ) as $spec ) {
78 $spec = trim( $spec );
79
80 if ( preg_match( '/^(\d+)(italic)?$/i', $spec, $m ) ) {
81 $weights[] = $m[1];
82 $styles[] = isset( $m[2] ) && '' !== $m[2] ? 'italic' : 'normal';
83 } elseif ( 'italic' === strtolower( $spec ) ) {
84 $weights[] = '400';
85 $styles[] = 'italic';
86 } elseif ( 'regular' === strtolower( $spec ) || '' === $spec ) {
87 $weights[] = '400';
88 $styles[] = 'normal';
89 }
90 }
91
92 $this->merge( $families, $family, $weights, $styles );
93 }
94 }
95
96 /**
97 * Parse css2 axis syntax: "Name:ital,wght@0,400;1,700" or "Name:wght@400;700".
98 *
99 * @param string $value Family value.
100 * @param array<string,array{weights:string[],styles:string[]}> $families Accumulator (by ref).
101 */
102 private function parse_css2( string $value, array &$families ): void {
103 [ $left, $axes ] = array_pad( explode( '@', $value, 2 ), 2, '' );
104
105 $family = $this->clean_name( explode( ':', $left )[0] );
106
107 if ( '' === $family ) {
108 return;
109 }
110
111 // Axis names appear before '@', e.g. "Roboto:ital,wght".
112 $axis_names = array();
113
114 if ( false !== strpos( $left, ':' ) ) {
115 $axis_names = explode( ',', explode( ':', $left, 2 )[1] );
116 }
117
118 $ital_index = array_search( 'ital', $axis_names, true );
119 $wght_index = array_search( 'wght', $axis_names, true );
120
121 $weights = array();
122 $styles = array();
123
124 foreach ( explode( ';', $axes ) as $tuple ) {
125 $cols = explode( ',', $tuple );
126
127 $weight = '400';
128 $style = 'normal';
129
130 if ( false !== $wght_index && isset( $cols[ $wght_index ] ) ) {
131 $weight = trim( $cols[ $wght_index ] );
132 } elseif ( 1 === count( $cols ) && is_numeric( trim( $cols[0] ) ) ) {
133 $weight = trim( $cols[0] );
134 }
135
136 if ( false !== $ital_index && isset( $cols[ $ital_index ] ) ) {
137 $style = '1' === trim( $cols[ $ital_index ] ) ? 'italic' : 'normal';
138 }
139
140 // Variable range like "400..700" — keep the base weight.
141 if ( false !== strpos( $weight, '..' ) ) {
142 $weight = explode( '..', $weight )[0];
143 }
144
145 if ( '' !== $weight ) {
146 $weights[] = $weight;
147 $styles[] = $style;
148 }
149 }
150
151 if ( empty( $weights ) ) {
152 $weights = array( '400' );
153 $styles = array( 'normal' );
154 }
155
156 $this->merge( $families, $family, $weights, $styles );
157 }
158
159 /**
160 * Merge parsed weights/styles into the accumulator.
161 *
162 * @param array<string,array{weights:string[],styles:string[]}> $families Accumulator (by ref).
163 * @param string $family Family name.
164 * @param string[] $weights Weights.
165 * @param string[] $styles Styles.
166 */
167 private function merge( array &$families, string $family, array $weights, array $styles ): void {
168 if ( empty( $weights ) ) {
169 $weights = array( '400' );
170 $styles = array( 'normal' );
171 }
172
173 if ( ! isset( $families[ $family ] ) ) {
174 $families[ $family ] = array(
175 'weights' => array(),
176 'styles' => array(),
177 );
178 }
179
180 $families[ $family ]['weights'] = array_values( array_unique( array_merge( $families[ $family ]['weights'], $weights ) ) );
181 $families[ $family ]['styles'] = array_values( array_unique( array_merge( $families[ $family ]['styles'], $styles ) ) );
182 }
183
184 /**
185 * Normalize a raw family token ("Open+Sans" => "Open Sans").
186 *
187 * @param string $name Raw name.
188 * @return string
189 */
190 private function clean_name( string $name ): string {
191 return trim( str_replace( '+', ' ', trim( $name ) ) );
192 }
193 }
194