[...], 'url' => '...', 'title' => '...']]
* @return string Modified content with links
*/
public function injectLinks(string $content, array $link_data): string
{
if (empty($link_data)) {
return $content;
}
usort($link_data, function ($a, $b) {
return $b['match']['offset'] <=> $a['match']['offset'];
});
foreach ($link_data as $data) {
$content = $this->injectSingleLink(
$content,
$data['match'],
$data['url'],
$data['title'] ?? ''
);
}
return $content;
}
/**
* Inject a single link into content
*
* @param string $content
* @param array $match Match data with offset and length
* @param string $url Target URL
* @param string $title Link title
* @return string
*/
private function injectSingleLink(string $content, array $match, string $url, string $title = ''): string
{
$offset = $match['offset'];
$length = $match['length'];
$matched_text = $match['text'];
$link_html = $this->buildLinkHtml($matched_text, $url, $title);
$before = substr($content, 0, $offset);
$after = substr($content, $offset + $length);
return $before . $link_html . $after;
}
/**
* Build HTML link element
*
* @param string $text Link text
* @param string $url Link URL
* @param string $title Link title attribute
* @return string
*/
private function buildLinkHtml(string $text, string $url, string $title = ''): string
{
$url = esc_url($url);
$text = $text;
if (!empty($title)) {
$title = esc_attr($title);
return '' . $text . '';
}
return '' . $text . '';
}
/**
* Inject multiple links for different keywords
*
* @param string $content
* @param array $links_by_keyword Associative array [keyword => [link_data...]]
* @return string
*/
public function injectMultipleKeywordLinks(string $content, array $links_by_keyword): string
{
$all_links = [];
foreach ($links_by_keyword as $keyword => $link_data_array) {
foreach ($link_data_array as $link_data) {
$all_links[] = $link_data;
}
}
return $this->injectLinks($content, $all_links);
}
/**
* Preview links without injecting (for testing/validation)
*
* @param string $content
* @param array $link_data
* @return array Preview data with snippets
*/
public function previewLinks(string $content, array $link_data): array
{
$previews = [];
foreach ($link_data as $data) {
$match = $data['match'];
$offset = $match['offset'];
$length = $match['length'];
$context_start = max(0, $offset - 50);
$context_length = min(strlen($content) - $context_start, $length + 100);
$context = substr($content, $context_start, $context_length);
$previews[] = [
'url' => $data['url'],
'title' => $data['title'] ?? '',
'matched_text' => $match['text'],
'context' => $context,
'position' => $offset,
'link_html' => $this->buildLinkHtml($match['text'], $data['url'], $data['title'] ?? ''),
];
}
return $previews;
}
/**
* Count total links that will be injected
*
* @param array $link_data
* @return int
*/
public function countLinks(array $link_data): int
{
return count($link_data);
}
/**
* Validate link data structure
*
* @param array $link_data
* @return bool
* @throws \InvalidArgumentException
*/
public function validateLinkData(array $link_data): bool
{
foreach ($link_data as $index => $data) {
if (!isset($data['match']) || !is_array($data['match'])) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \InvalidArgumentException("Link data at index {$index} missing 'match' array");
}
if (!isset($data['match']['offset']) || !is_int($data['match']['offset'])) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \InvalidArgumentException("Link data at index {$index} missing valid 'offset'");
}
if (!isset($data['match']['length']) || !is_int($data['match']['length'])) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \InvalidArgumentException("Link data at index {$index} missing valid 'length'");
}
if (!isset($data['match']['text']) || !is_string($data['match']['text'])) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \InvalidArgumentException("Link data at index {$index} missing valid 'text'");
}
if (!isset($data['url']) || !is_string($data['url'])) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \InvalidArgumentException("Link data at index {$index} missing valid 'url'");
}
}
return true;
}
/**
* Remove links from content (for testing/reversal)
*
* @param string $content
* @param string $url URL to remove links for
* @return string
*/
public function removeLinks(string $content, string $url): string
{
$escaped_url = preg_quote(esc_url($url), '/');
$pattern = "/]*href=[\"']{$escaped_url}[\"'][^>]*>(.*?)<\/a>/i";
return preg_replace($pattern, '$1', $content);
}
}