' . esc_html($title) . ''; if ($subtitle !== '') { $html .= '
' . esc_html($subtitle) . '
'; } return $html; } /** * A rounded pill. $tone is 'up', 'down' or 'flat'. */ public static function pill(string $text, string $tone = 'flat', string $margin = ''): string { [$color, $bg] = self::tone_colors($tone); return '' . esc_html($text) . ''; } /** * Change pill under a stat. $delta is the signed change already formatted * ("12.4%", "1.8"), $direction 'up'/'down'/'flat', $good whether that * direction is good news (clicks up = good; position number up = bad). */ public static function change(?string $delta, string $direction, bool $good, string $suffix = ''): string { if ($delta === null) { return ''; } if ($direction === 'flat') { return self::pill(__('No change', 'thinkrank'), 'flat', 'margin-top:6px;'); } $arrow = $direction === 'up' ? '▲' : '▼'; $text = $arrow . ' ' . $delta . ($suffix !== '' ? ' ' . $suffix : ''); return self::pill($text, $good ? 'up' : 'down', 'margin-top:6px;'); } /** * One stat tile: icon circle, label, big number, change pill. Returns a * — pair them with stat_grid(). * * @param array{label:string,value:string,change?:string,icon?:string} $stat */ public static function stat(array $stat): string { $icon = ''; if (!empty($stat['icon'])) { $icon = ''; } return '' . '' . $icon . '
' . '
' . esc_html($stat['label']) . '
' . '
' . esc_html($stat['value']) . '
' . ($stat['change'] ?? '') . '
'; } /** * Lay stat tiles out two per row. * * @param string[] $tiles Output of stat(). */ public static function stat_grid(array $tiles): string { if ($tiles === []) { return ''; } $html = ''; foreach (array_chunk($tiles, 2) as $pair) { $html .= '' . implode('', $pair) . (count($pair) === 1 ? '' : '') . ''; } return $html . '
'; } /** * Ranked list: a title, a muted line under it, and a pill on the right. * * @param array $rows * @param string $tone 'up' or 'down' for the pills. */ public static function list_rows(array $rows, string $tone): string { if ($rows === []) { return ''; } $html = ''; foreach (array_values($rows) as $i => $row) { $border = $i > 0 ? 'border-top:1px solid ' . self::LINE . ';' : ''; $title = esc_html((string) $row['title']); if (!empty($row['href'])) { $title = '' . $title . ''; } $sub = ''; if (!empty($row['subtitle'])) { $sub = '
' . esc_html((string) $row['subtitle']) . '
'; } $html .= '' . '' . ''; } return $html . '
' . '
' . $title . '
' . $sub . '
' . self::pill((string) $row['pill'], $tone) . '
'; } /** * One horizontal bar row: label, bar, count (share). */ public static function bar_row(string $label, int $count, int $total, string $color): string { $pct = $total > 0 ? (int) round($count / $total * 100) : 0; return '' . '' . esc_html($label) . '' . '' . '' . '
 
' . '' . esc_html(number_format_i18n($count)) . ' (' . $pct . '%)' . ''; } /** * Brand-coloured "See all ›" link. */ public static function link(string $text, string $href): string { if ($href === '') { return ''; } return '
' . esc_html($text) . ' ›
'; } /** * Muted note under a card's content. */ public static function note(string $text, string $margin_top = '10px'): string { return '
' . esc_html($text) . '
'; } /** * Percentage change between two totals, or null when there is no previous * value to compare with (a division by zero is not "+100%"). * * @return array{text:string,direction:string}|null */ public static function pct_change(float $current, float $previous): ?array { if ($previous <= 0.0) { return null; } $pct = ($current - $previous) / $previous * 100; if (abs($pct) < 0.05) { return ['text' => '0%', 'direction' => 'flat']; } return [ 'text' => number_format_i18n(abs($pct), 1) . '%', 'direction' => $pct > 0 ? 'up' : 'down', ]; } /** * Signed percentage as a subject-line token: "+12.4%", "−3.1%", "". */ public static function signed_pct(?array $change): string { if ($change === null || $change['direction'] === 'flat') { return ''; } return ($change['direction'] === 'up' ? '+' : '−') . $change['text']; } /** * Path of a URL for a list subtitle: "/blog/post/" rather than the full * address, which is the site's own and already known to the reader. */ public static function display_path(string $url): string { $path = (string) wp_parse_url($url, PHP_URL_PATH); $query = (string) wp_parse_url($url, PHP_URL_QUERY); if ($path === '') { return $url; } return $path . ($query !== '' ? '?' . $query : ''); } /** * Deep link into the Essential SEO screen. The recipient must be logged * in, but the link still lands them on the right panel. */ public static function admin_link(string $section, string $item): string { return (string) admin_url( 'admin.php?page=thinkrank-essential-seo&nav_section=' . rawurlencode($section) . '&nav_item=' . rawurlencode($item) ); } /** * A page's title for a list row, when the URL belongs to this site and * resolves to a post; the URL path otherwise. A row that reads "How to * add a table of contents" is worth more than one that reads * "/blog/gutenberg-table-of-contents/". */ public static function page_title(string $url): string { if (function_exists('url_to_postid') && function_exists('get_the_title')) { $post_id = (int) url_to_postid($url); if ($post_id > 0) { $title = trim((string) get_the_title($post_id)); if ($title !== '') { return $title; } } } return self::display_path($url); } /** * @return array{0:string,1:string} [text colour, background] */ private static function tone_colors(string $tone): array { switch ($tone) { case 'up': return [self::UP, self::UP_BG]; case 'down': return [self::DOWN, self::DOWN_BG]; default: return [self::FLAT, self::FLAT_BG]; } } }