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:
@@ -395,6 +395,154 @@ function ucwords(input) {
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a string safe for use as a variable/function name
|
||||
* @param {string} string - Input string
|
||||
* @param {number} [max_length=64] - Maximum length
|
||||
* @returns {string} Safe string
|
||||
*/
|
||||
function safe_string(string, max_length = 64) {
|
||||
// Replace non-alphanumeric with underscores
|
||||
string = String(string).replace(/[^a-zA-Z0-9_]+/g, '_');
|
||||
|
||||
// Ensure first character is not a number
|
||||
if (string === '' || /^[0-9]/.test(string)) {
|
||||
string = '_' + string;
|
||||
}
|
||||
|
||||
// Trim to max length
|
||||
return string.substring(0, max_length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert snake_case to camelCase
|
||||
* @param {string} string - Snake case string
|
||||
* @param {boolean} [capitalize_first=false] - Whether to capitalize first letter (PascalCase)
|
||||
* @returns {string} Camel case string
|
||||
*/
|
||||
function snake_to_camel(string, capitalize_first = false) {
|
||||
let result = String(string).replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||
|
||||
if (capitalize_first && result.length > 0) {
|
||||
result = result.charAt(0).toUpperCase() + result.slice(1);
|
||||
} else if (!capitalize_first && result.length > 0) {
|
||||
result = result.charAt(0).toLowerCase() + result.slice(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert camelCase to snake_case
|
||||
* @param {string} string - Camel case string
|
||||
* @returns {string} Snake case string
|
||||
*/
|
||||
function camel_to_snake(string) {
|
||||
return String(string)
|
||||
.replace(/^[A-Z]/, (letter) => letter.toLowerCase())
|
||||
.replace(/[A-Z]/g, (letter) => '_' + letter.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Common TLDs for domain detection in linkify functions
|
||||
* @type {string}
|
||||
*/
|
||||
const 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 {boolean} [new_window=true] - Whether to add target="_blank" to links
|
||||
* @returns {string} HTML with clickable links
|
||||
*/
|
||||
function linkify_text(content, new_window = true) {
|
||||
if (content == null || content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// First escape HTML
|
||||
const escaped = html(String(content));
|
||||
|
||||
return _linkify_content(escaped, 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 {boolean} [new_window=true] - Whether to add target="_blank" to links
|
||||
* @returns {string} HTML with clickable links
|
||||
*/
|
||||
function linkify_html(content, new_window = true) {
|
||||
if (content == null || content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Split content into segments: inside <a> tags and outside
|
||||
const pattern = /(<a\s[^>]*>.*?<\/a>)/gi;
|
||||
const segments = String(content).split(pattern);
|
||||
|
||||
let result = '';
|
||||
for (const segment of segments) {
|
||||
// Check if this segment is an <a> tag
|
||||
if (/^<a\s/i.test(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
|
||||
* @param {boolean} new_window - Whether to add target="_blank"
|
||||
* @returns {string} Content with URLs converted to links
|
||||
* @private
|
||||
*/
|
||||
function _linkify_content(content, new_window) {
|
||||
const target = new_window ? ' target="_blank" rel="noopener noreferrer"' : '';
|
||||
|
||||
// Pattern for URLs with protocol
|
||||
const url_pattern = /(https?:\/\/[^\s<>\[\]()]+)/gi;
|
||||
|
||||
// Pattern for domain-like text
|
||||
const domain_pattern = new RegExp(
|
||||
'\\b((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+(' + LINKIFY_TLDS + ')(?:\\/[^\\s<>\\[\\]()]*)?)\\b',
|
||||
'gi'
|
||||
);
|
||||
|
||||
// First, replace URLs with protocol
|
||||
content = content.replace(url_pattern, (match) => {
|
||||
// Clean trailing punctuation that's likely not part of URL
|
||||
const url = match.replace(/[.,;:!?)'\"]+$/, '');
|
||||
const trailing = match.slice(url.length);
|
||||
return '<a href="' + url + '"' + target + '>' + url + '</a>' + trailing;
|
||||
});
|
||||
|
||||
// Then, replace domain-like text (but not if already inside an href)
|
||||
content = content.replace(domain_pattern, (match) => {
|
||||
// Clean trailing punctuation
|
||||
const domain = match.replace(/[.,;:!?)'\"]+$/, '');
|
||||
const trailing = match.slice(domain.length);
|
||||
return '<a href="https://' + domain + '"' + target + '>' + domain + '</a>' + trailing;
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OBJECT AND ARRAY UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
@@ -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