PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.3
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.3
2.14.3 2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 All 58 releases
← All changes | includes/core/class-seo-text.php +140 -0 2.9.0 → 2.14.3 View file →
@@ -81,8 +81,59 @@
81 81 return 'words' === _x('words', 'Word count type. Do not translate!', 'default');
82 82 }
83 83
84 84 /**
85 + * The text a reader sees for a resolved title or description.
86 + *
87 + * Pattern_Resolver hands back what the page will print, and that is still
88 + * HTML: get_the_title() runs wptexturize, so "Foo & Bar" arrives as
89 + * `Foo & Bar`, while the same words typed into the SEO title field
90 + * arrive as `Foo &amp; Bar` or a bare `&`. All three render as one `<title>`.
91 + * Anything that compares, measures or displays the value as text has to
92 + * look at it after the browser would have decoded it, or it groups two
93 + * identical titles apart, counts `&#038;` as six characters, and shows the
94 + * entity to the user.
95 + *
96 + * @since 2.10.0
97 + *
98 + * @param string $value Resolved title or description.
99 + * @return string The same value with HTML entities decoded.
100 + */
101 + public static function as_displayed(string $value): string {
102 + return html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
103 + }
104 +
105 + /**
106 + * Strip leading and trailing whitespace and title separators.
107 + *
108 + * The separator is removed as a whole string, never byte by byte.
109 + * `trim($title, " \t..." . $separator)` reads its second argument as a
110 + * list of single bytes, so with the Bullet separator (`•`, E2 80 A2) it
111 + * also stripped any E2, 80 or A2 byte at either end of the title: the
112 + * middle of €, ✅, curly quotes and many Cyrillic letters. The broken
113 + * UTF-8 left behind made `<title>` invalid and `esc_attr()` blank out
114 + * og:title and twitter:title (#972).
115 + *
116 + * @since 2.15.0
117 + *
118 + * @param string $value Title or description.
119 + * @param string $separator The active separator, possibly multibyte.
120 + * @return string
121 + */
122 + public static function trim_separator(string $value, string $separator): string {
123 + $alternatives = '[\s\x00]';
124 + if ('' !== $separator) {
125 + $alternatives .= '|' . preg_quote($separator, '/');
126 + }
127 +
128 + $trimmed = preg_replace('/^(?:' . $alternatives . ')+|(?:' . $alternatives . ')+$/u', '', $value);
129 +
130 + // Invalid UTF-8 makes a /u pattern fail. Fall back to whitespace only,
131 + // which cannot cut a character in half.
132 + return null === $trimmed ? trim($value) : $trimmed;
133 + }
134 +
135 + /**
85 136 * Trim a description to a character budget, multibyte-safe.
86 137 *
87 138 * Cuts on a word boundary when one is available inside the budget, so the
88 139 * result does not end mid-word; falls back to a hard character cut for
@@ -118,8 +169,97 @@
118 169 $cut = mb_substr($cut, 0, $last_gap);
119 170 }
120 171
121 172 return rtrim($cut) . '…';
173 + }
174 +
175 + /**
176 + * Make text fit to appear in JSON-LD.
177 + *
178 + * JSON-LD is not HTML, so an HTML entity in it is not decoded by anything
179 + * downstream: `&amp;` was published to answer engines literally. And the
180 + * excerpt path appends core's trimming marker, so descriptions arrived
181 + * ending in `[…]`, a truncation artefact presented as the page's own
182 + * summary.
183 + *
184 + * Lives here rather than in the schema class that introduced it (#766)
185 + * because two producers build description nodes: the automatic
186 + * Global_SEO_Schema_Output and the Schema Manager's Schema_Builder. Only
187 + * the first normalised, so a deployed node, which outranks the automatic
188 + * one, published the raw entity again.
189 + *
190 + * @since 2.10.0
191 + *
192 + * @param string $text Raw text.
193 + * @return string
194 + */
195 + public static function normalize_schema_text(string $text): string {
196 + if ('' === trim($text)) {
197 + return '';
198 + }
199 +
200 + $text = self::decode_schema_entities(wp_strip_all_tags($text));
201 +
202 + // Core's excerpt marker, in both its entity and literal forms, with or
203 + // without the surrounding brackets it is normally wrapped in.
204 + $text = (string) preg_replace(
205 + '/\s*(\[\s*(\x{2026}|\.\.\.)\s*\]|\x{2026})\s*$/u',
206 + '',
207 + $text
208 + );
209 +
210 + return trim((string) preg_replace('/\s+/u', ' ', $text));
211 + }
212 +
213 + /**
214 + * Make a title or name fit to appear in JSON-LD.
215 + *
216 + * get_the_title() runs wptexturize and keeps any markup in the title, and
217 + * WordPress stores term names and the site name esc_html()'d, so a page
218 + * called "R&D at Tom's" reached the graph as `R&#038;D at Tom&#8217;s` and
219 + * a category "Cats & Dogs" as `Cats &amp; Dogs`. Nothing downstream
220 + * decodes JSON-LD, so those were published literally (#976).
221 + *
222 + * normalize_schema_text() without its excerpt-marker strip: that marker
223 + * is an artefact of a derived description, while a title that ends in an
224 + * ellipsis was written that way and keeps it. A decoded `<` stays safe
225 + * because the graph is encoded with JSON_HEX_TAG (#118).
226 + *
227 + * @since 2.15.0
228 + *
229 + * @param string $text Raw title or name.
230 + * @return string Plain text with tags removed and entities decoded.
231 + */
232 + public static function normalize_schema_name(string $text): string {
233 + if ('' === trim($text)) {
234 + return '';
235 + }
236 +
237 + $text = self::decode_schema_entities(wp_strip_all_tags($text));
238 +
239 + return trim((string) preg_replace('/\s+/u', ' ', $text));
240 + }
241 +
242 + /**
243 + * Decode HTML entities in text bound for JSON-LD, and nothing else.
244 + *
245 + * The narrow half of normalize_schema_text(), for values that must keep
246 + * their exact shape otherwise: a stored snapshot already truncated with an
247 + * ellipsis would lose it to the excerpt-marker strip.
248 + *
249 + * @since 2.10.0
250 + *
251 + * @param string $text Text that may carry HTML entities.
252 + * @return string
253 + */
254 + public static function decode_schema_entities(string $text): string {
255 + // Twice: a description that has been through an escaping pass already
256 + // (core stores `&amp;amp;` for a literal `&amp;` in some paths) would
257 + // otherwise still carry an entity after one decode. Decoding an
258 + // already-plain string is a no-op, so this is safe to repeat.
259 + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
260 +
261 + return html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
122 262 }
123 263
124 264 /**
125 265 * Apply a WORD cap that stays a word cap.