'
. 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 .= ''
. '| '
. ' ' . $title . ' ' . $sub
. ' | '
. ''
. self::pill((string) $row['pill'], $tone)
. ' |
';
}
return $html . '
';
}
/**
* 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 '';
}
/**
* 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];
}
}
}