Add unified string utilities to PHP and JS
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1551,3 +1551,122 @@ function validate_short_url(?string $url): bool
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML and convert newlines to <br>
|
||||
*
|
||||
* Combines htmlspecialchars() and nl2br() for displaying user-generated
|
||||
* plain text as HTML with preserved line breaks.
|
||||
*
|
||||
* @param string|null $str String to process
|
||||
* @return string HTML-escaped string with line breaks
|
||||
*/
|
||||
function htmlbr(?string $str): string
|
||||
{
|
||||
if ($str === null || $str === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return nl2br(htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Common TLDs for domain detection in linkify functions
|
||||
*/
|
||||
define('LINKIFY_TLDS', 'com|org|net|edu|gov|io|co|me|info|biz|us|uk|ca|au|de|fr|es|it|nl|ru|jp|cn|in|br|mx|app|dev|xyz|online|site|tech|store|blog|shop');
|
||||
|
||||
/**
|
||||
* Convert plain text to HTML with URLs converted to hyperlinks
|
||||
*
|
||||
* First escapes the text to HTML, then converts URLs (with protocols) and
|
||||
* domain-like text (with known TLDs) into clickable hyperlinks.
|
||||
*
|
||||
* @param string|null $content Plain text content
|
||||
* @param bool $new_window Whether to add target="_blank" to links
|
||||
* @return string HTML with clickable links
|
||||
*/
|
||||
function linkify_text(?string $content, bool $new_window = true): string
|
||||
{
|
||||
if ($content === null || $content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// First escape HTML
|
||||
$html = htmlspecialchars($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
return _linkify_content($html, $new_window);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert URLs in HTML to hyperlinks, preserving existing links
|
||||
*
|
||||
* Converts URLs (with protocols) and domain-like text (with known TLDs)
|
||||
* into clickable hyperlinks, but only for text not already inside <a> tags.
|
||||
*
|
||||
* @param string|null $content HTML content
|
||||
* @param bool $new_window Whether to add target="_blank" to links
|
||||
* @return string HTML with clickable links
|
||||
*/
|
||||
function linkify_html(?string $content, bool $new_window = true): string
|
||||
{
|
||||
if ($content === null || $content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Split content into segments: inside <a> tags and outside
|
||||
// Pattern matches <a ...>...</a> including nested content
|
||||
$pattern = '/(<a\s[^>]*>.*?<\/a>)/is';
|
||||
$segments = preg_split($pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
$result = '';
|
||||
foreach ($segments as $segment) {
|
||||
// Check if this segment is an <a> tag (starts with <a and contains </a>)
|
||||
if (preg_match('/^<a\s/i', $segment)) {
|
||||
// Already a link, keep as-is
|
||||
$result .= $segment;
|
||||
} else {
|
||||
// Not inside a link, linkify it
|
||||
$result .= _linkify_content($segment, $new_window);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to convert URLs/domains to links in content
|
||||
*
|
||||
* @param string $content Content to process (should not contain <a> tags to linkify)
|
||||
* @param bool $new_window Whether to add target="_blank"
|
||||
* @return string Content with URLs converted to links
|
||||
*/
|
||||
function _linkify_content(string $content, bool $new_window): string
|
||||
{
|
||||
$target = $new_window ? ' target="_blank" rel="noopener noreferrer"' : '';
|
||||
$tlds = LINKIFY_TLDS;
|
||||
|
||||
// Pattern for URLs with protocol
|
||||
$url_pattern = '/(https?:\/\/[^\s<>\[\]()]+)/i';
|
||||
|
||||
// Pattern for domain-like text (domain.tld or subdomain.domain.tld with optional path)
|
||||
$domain_pattern = '/\b((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+(' . $tlds . ')(?:\/[^\s<>\[\]()]*)?)\b/i';
|
||||
|
||||
// First, replace URLs with protocol
|
||||
$content = preg_replace_callback($url_pattern, function ($matches) use ($target) {
|
||||
$url = $matches[1];
|
||||
// Clean trailing punctuation that's likely not part of URL
|
||||
$url = rtrim($url, '.,;:!?)\'\"');
|
||||
return '<a href="' . $url . '"' . $target . '>' . $url . '</a>';
|
||||
}, $content);
|
||||
|
||||
// Then, replace domain-like text (but not if already inside an href)
|
||||
$content = preg_replace_callback($domain_pattern, function ($matches) use ($target) {
|
||||
$domain = $matches[1];
|
||||
// Clean trailing punctuation
|
||||
$domain = rtrim($domain, '.,;:!?)\'\"');
|
||||
// Don't linkify if it looks like it's already in an href attribute
|
||||
return '<a href="https://' . $domain . '"' . $target . '>' . $domain . '</a>';
|
||||
}, $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user