Invision Power Services, Inc.
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 12 Jun 2013
*/
namespace IPS\Text;
/* To prevent PHP errors (extending class does not exist) revealing path */
use function preg_replace;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Text Parser
*/
class _Parser
{
/**
* @brief Regex for detecting an Emoji character
* @note This string is automatically generated by the Emoji Data Builder. Do not modify it manually.
*/
const EMOJI_REGEX = '\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F469}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F469}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F91D}\x{200D}\x{1F9D1}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0065}\x{E006E}\x{E0067}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0073}\x{E0063}\x{E0074}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0077}\x{E006C}\x{E0073}\x{E007F}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F46E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F46E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F470}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F470}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F471}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F471}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F9D4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9DB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9DC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9DD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{26F9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{26F9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F33E}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F373}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F37C}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F393}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3A4}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3A8}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3EB}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3ED}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F4BB}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F4BC}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F527}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F52C}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F680}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F692}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9AF}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B0}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B1}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B2}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B3}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9BC}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9BD}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F33E}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F373}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F37C}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F393}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3A4}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3A8}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3EB}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3ED}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F4BB}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F4BC}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F527}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F52C}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F680}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F692}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9AF}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B0}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B1}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B2}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B3}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9BC}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9BD}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F33E}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F373}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F37C}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F384}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F393}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3A4}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3A8}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3EB}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F3ED}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F4BB}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F4BC}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F527}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F52C}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F680}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F692}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9AF}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B0}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B1}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B2}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9B3}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9BC}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F9BD}|\x{1F468}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}|\x{1F441}\x{FE0F}\x{200D}\x{1F5E8}\x{FE0F}|\x{1F3F3}\x{FE0F}\x{200D}\x{26A7}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{1F574}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{1F590}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{261D}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{26F9}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{270C}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{270D}[\x{1F3FB}-\x{1F3FF}]?\x{FE0F}|\x{1F3F3}\x{FE0F}\x{200D}\x{1F308}|\x{1F636}\x{200D}\x{1F32B}\x{FE0F}|\x{1F3F4}\x{200D}\x{2620}\x{FE0F}|\x{1F43B}\x{200D}\x{2744}\x{FE0F}|\x{1F46F}\x{200D}\x{2640}\x{FE0F}|\x{1F46F}\x{200D}\x{2642}\x{FE0F}|\x{1F93C}\x{200D}\x{2640}\x{FE0F}|\x{1F93C}\x{200D}\x{2642}\x{FE0F}|\x{1F9DE}\x{200D}\x{2640}\x{FE0F}|\x{1F9DE}\x{200D}\x{2642}\x{FE0F}|\x{1F9DF}\x{200D}\x{2640}\x{FE0F}|\x{1F9DF}\x{200D}\x{2642}\x{FE0F}|\x{2764}\x{FE0F}\x{200D}\x{1F525}|\x{2764}\x{FE0F}\x{200D}\x{1FA79}|\x{1F385}[\x{1F3FB}-\x{1F3FF}]?|\x{1F3C2}[\x{1F3FB}-\x{1F3FF}]?|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?|\x{1F3C7}[\x{1F3FB}-\x{1F3FF}]?|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?|\x{1F442}[\x{1F3FB}-\x{1F3FF}]?|\x{1F443}[\x{1F3FB}-\x{1F3FF}]?|\x{1F446}[\x{1F3FB}-\x{1F3FF}]?|\x{1F447}[\x{1F3FB}-\x{1F3FF}]?|\x{1F448}[\x{1F3FB}-\x{1F3FF}]?|\x{1F449}[\x{1F3FB}-\x{1F3FF}]?|\x{1F44A}[\x{1F3FB}-\x{1F3FF}]?|\x{1F44B}[\x{1F3FB}-\x{1F3FF}]?|\x{1F44C}[\x{1F3FB}-\x{1F3FF}]?|\x{1F44D}[\x{1F3FB}-\x{1F3FF}]?|\x{1F44E}[\x{1F3FB}-\x{1F3FF}]?|\x{1F44F}[\x{1F3FB}-\x{1F3FF}]?|\x{1F450}[\x{1F3FB}-\x{1F3FF}]?|\x{1F466}[\x{1F3FB}-\x{1F3FF}]?|\x{1F467}[\x{1F3FB}-\x{1F3FF}]?|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?|\x{1F46B}[\x{1F3FB}-\x{1F3FF}]?|\x{1F46C}[\x{1F3FB}-\x{1F3FF}]?|\x{1F46D}[\x{1F3FB}-\x{1F3FF}]?|\x{1F46E}[\x{1F3FB}-\x{1F3FF}]?|\x{1F470}[\x{1F3FB}-\x{1F3FF}]?|\x{1F471}[\x{1F3FB}-\x{1F3FF}]?|\x{1F472}[\x{1F3FB}-\x{1F3FF}]?|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?|\x{1F474}[\x{1F3FB}-\x{1F3FF}]?|\x{1F475}[\x{1F3FB}-\x{1F3FF}]?|\x{1F476}[\x{1F3FB}-\x{1F3FF}]?|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?|\x{1F478}[\x{1F3FB}-\x{1F3FF}]?|\x{1F47C}[\x{1F3FB}-\x{1F3FF}]?|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?|\x{1F483}[\x{1F3FB}-\x{1F3FF}]?|\x{1F485}[\x{1F3FB}-\x{1F3FF}]?|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?|\x{1F48F}[\x{1F3FB}-\x{1F3FF}]?|\x{1F491}[\x{1F3FB}-\x{1F3FF}]?|\x{1F4AA}[\x{1F3FB}-\x{1F3FF}]?|\x{1F57A}[\x{1F3FB}-\x{1F3FF}]?|\x{1F595}[\x{1F3FB}-\x{1F3FF}]?|\x{1F596}[\x{1F3FB}-\x{1F3FF}]?|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?|\x{1F64C}[\x{1F3FB}-\x{1F3FF}]?|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?|\x{1F64F}[\x{1F3FB}-\x{1F3FF}]?|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?|\x{1F6C0}[\x{1F3FB}-\x{1F3FF}]?|\x{1F6CC}[\x{1F3FB}-\x{1F3FF}]?|\x{1F90C}[\x{1F3FB}-\x{1F3FF}]?|\x{1F90F}[\x{1F3FB}-\x{1F3FF}]?|\x{1F918}[\x{1F3FB}-\x{1F3FF}]?|\x{1F919}[\x{1F3FB}-\x{1F3FF}]?|\x{1F91A}[\x{1F3FB}-\x{1F3FF}]?|\x{1F91B}[\x{1F3FB}-\x{1F3FF}]?|\x{1F91C}[\x{1F3FB}-\x{1F3FF}]?|\x{1F91D}[\x{1F3FB}-\x{1F3FF}]?|\x{1F91E}[\x{1F3FB}-\x{1F3FF}]?|\x{1F91F}[\x{1F3FB}-\x{1F3FF}]?|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?|\x{1F930}[\x{1F3FB}-\x{1F3FF}]?|\x{1F931}[\x{1F3FB}-\x{1F3FF}]?|\x{1F932}[\x{1F3FB}-\x{1F3FF}]?|\x{1F933}[\x{1F3FB}-\x{1F3FF}]?|\x{1F934}[\x{1F3FB}-\x{1F3FF}]?|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?|\x{1F936}[\x{1F3FB}-\x{1F3FF}]?|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?|\x{1F977}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9B5}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9B6}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9BB}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D2}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D3}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D4}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D5}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9D9}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9DB}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9DC}[\x{1F3FB}-\x{1F3FF}]?|\x{1F9DD}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAC3}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAC4}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAC5}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF0}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF1}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF2}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF3}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF4}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF5}[\x{1F3FB}-\x{1F3FF}]?|\x{1FAF6}[\x{1F3FB}-\x{1F3FF}]?|\x{270A}[\x{1F3FB}-\x{1F3FF}]?|\x{270B}[\x{1F3FB}-\x{1F3FF}]?|\x{1F415}\x{200D}\x{1F9BA}|\x{1F468}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F467}|\x{1F62E}\x{200D}\x{1F4A8}|\x{1F635}\x{200D}\x{1F4AB}|\x{1F408}\x{200D}\x{2B1B}|\x{0023}\x{FE0F}\x{20E3}|\x{002A}\x{FE0F}\x{20E3}|\x{0030}\x{FE0F}\x{20E3}|\x{0031}\x{FE0F}\x{20E3}|\x{0032}\x{FE0F}\x{20E3}|\x{0033}\x{FE0F}\x{20E3}|\x{0034}\x{FE0F}\x{20E3}|\x{0035}\x{FE0F}\x{20E3}|\x{0036}\x{FE0F}\x{20E3}|\x{0037}\x{FE0F}\x{20E3}|\x{0038}\x{FE0F}\x{20E3}|\x{0039}\x{FE0F}\x{20E3}|\x{1F1E6}\x{1F1E8}|\x{1F1E6}\x{1F1E9}|\x{1F1E6}\x{1F1EA}|\x{1F1E6}\x{1F1EB}|\x{1F1E6}\x{1F1EC}|\x{1F1E6}\x{1F1EE}|\x{1F1E6}\x{1F1F1}|\x{1F1E6}\x{1F1F2}|\x{1F1E6}\x{1F1F4}|\x{1F1E6}\x{1F1F6}|\x{1F1E6}\x{1F1F7}|\x{1F1E6}\x{1F1F8}|\x{1F1E6}\x{1F1F9}|\x{1F1E6}\x{1F1FA}|\x{1F1E6}\x{1F1FC}|\x{1F1E6}\x{1F1FD}|\x{1F1E6}\x{1F1FF}|\x{1F1E7}\x{1F1E6}|\x{1F1E7}\x{1F1E7}|\x{1F1E7}\x{1F1E9}|\x{1F1E7}\x{1F1EA}|\x{1F1E7}\x{1F1EB}|\x{1F1E7}\x{1F1EC}|\x{1F1E7}\x{1F1ED}|\x{1F1E7}\x{1F1EE}|\x{1F1E7}\x{1F1EF}|\x{1F1E7}\x{1F1F1}|\x{1F1E7}\x{1F1F2}|\x{1F1E7}\x{1F1F3}|\x{1F1E7}\x{1F1F4}|\x{1F1E7}\x{1F1F6}|\x{1F1E7}\x{1F1F7}|\x{1F1E7}\x{1F1F8}|\x{1F1E7}\x{1F1F9}|\x{1F1E7}\x{1F1FB}|\x{1F1E7}\x{1F1FC}|\x{1F1E7}\x{1F1FE}|\x{1F1E7}\x{1F1FF}|\x{1F1E8}\x{1F1E6}|\x{1F1E8}\x{1F1E8}|\x{1F1E8}\x{1F1E9}|\x{1F1E8}\x{1F1EB}|\x{1F1E8}\x{1F1EC}|\x{1F1E8}\x{1F1ED}|\x{1F1E8}\x{1F1EE}|\x{1F1E8}\x{1F1F0}|\x{1F1E8}\x{1F1F1}|\x{1F1E8}\x{1F1F2}|\x{1F1E8}\x{1F1F3}|\x{1F1E8}\x{1F1F4}|\x{1F1E8}\x{1F1F5}|\x{1F1E8}\x{1F1F7}|\x{1F1E8}\x{1F1FA}|\x{1F1E8}\x{1F1FB}|\x{1F1E8}\x{1F1FC}|\x{1F1E8}\x{1F1FD}|\x{1F1E8}\x{1F1FE}|\x{1F1E8}\x{1F1FF}|\x{1F1E9}\x{1F1EA}|\x{1F1E9}\x{1F1EC}|\x{1F1E9}\x{1F1EF}|\x{1F1E9}\x{1F1F0}|\x{1F1E9}\x{1F1F2}|\x{1F1E9}\x{1F1F4}|\x{1F1E9}\x{1F1FF}|\x{1F1EA}\x{1F1E6}|\x{1F1EA}\x{1F1E8}|\x{1F1EA}\x{1F1EA}|\x{1F1EA}\x{1F1EC}|\x{1F1EA}\x{1F1ED}|\x{1F1EA}\x{1F1F7}|\x{1F1EA}\x{1F1F8}|\x{1F1EA}\x{1F1F9}|\x{1F1EA}\x{1F1FA}|\x{1F1EB}\x{1F1EE}|\x{1F1EB}\x{1F1EF}|\x{1F1EB}\x{1F1F0}|\x{1F1EB}\x{1F1F2}|\x{1F1EB}\x{1F1F4}|\x{1F1EB}\x{1F1F7}|\x{1F1EC}\x{1F1E6}|\x{1F1EC}\x{1F1E7}|\x{1F1EC}\x{1F1E9}|\x{1F1EC}\x{1F1EA}|\x{1F1EC}\x{1F1EB}|\x{1F1EC}\x{1F1EC}|\x{1F1EC}\x{1F1ED}|\x{1F1EC}\x{1F1EE}|\x{1F1EC}\x{1F1F1}|\x{1F1EC}\x{1F1F2}|\x{1F1EC}\x{1F1F3}|\x{1F1EC}\x{1F1F5}|\x{1F1EC}\x{1F1F6}|\x{1F1EC}\x{1F1F7}|\x{1F1EC}\x{1F1F8}|\x{1F1EC}\x{1F1F9}|\x{1F1EC}\x{1F1FA}|\x{1F1EC}\x{1F1FC}|\x{1F1EC}\x{1F1FE}|\x{1F1ED}\x{1F1F0}|\x{1F1ED}\x{1F1F2}|\x{1F1ED}\x{1F1F3}|\x{1F1ED}\x{1F1F7}|\x{1F1ED}\x{1F1F9}|\x{1F1ED}\x{1F1FA}|\x{1F1EE}\x{1F1E8}|\x{1F1EE}\x{1F1E9}|\x{1F1EE}\x{1F1EA}|\x{1F1EE}\x{1F1F1}|\x{1F1EE}\x{1F1F2}|\x{1F1EE}\x{1F1F3}|\x{1F1EE}\x{1F1F4}|\x{1F1EE}\x{1F1F6}|\x{1F1EE}\x{1F1F7}|\x{1F1EE}\x{1F1F8}|\x{1F1EE}\x{1F1F9}|\x{1F1EF}\x{1F1EA}|\x{1F1EF}\x{1F1F2}|\x{1F1EF}\x{1F1F4}|\x{1F1EF}\x{1F1F5}|\x{1F1F0}\x{1F1EA}|\x{1F1F0}\x{1F1EC}|\x{1F1F0}\x{1F1ED}|\x{1F1F0}\x{1F1EE}|\x{1F1F0}\x{1F1F2}|\x{1F1F0}\x{1F1F3}|\x{1F1F0}\x{1F1F5}|\x{1F1F0}\x{1F1F7}|\x{1F1F0}\x{1F1FC}|\x{1F1F0}\x{1F1FE}|\x{1F1F0}\x{1F1FF}|\x{1F1F1}\x{1F1E6}|\x{1F1F1}\x{1F1E7}|\x{1F1F1}\x{1F1E8}|\x{1F1F1}\x{1F1EE}|\x{1F1F1}\x{1F1F0}|\x{1F1F1}\x{1F1F7}|\x{1F1F1}\x{1F1F8}|\x{1F1F1}\x{1F1F9}|\x{1F1F1}\x{1F1FA}|\x{1F1F1}\x{1F1FB}|\x{1F1F1}\x{1F1FE}|\x{1F1F2}\x{1F1E6}|\x{1F1F2}\x{1F1E8}|\x{1F1F2}\x{1F1E9}|\x{1F1F2}\x{1F1EA}|\x{1F1F2}\x{1F1EB}|\x{1F1F2}\x{1F1EC}|\x{1F1F2}\x{1F1ED}|\x{1F1F2}\x{1F1F0}|\x{1F1F2}\x{1F1F1}|\x{1F1F2}\x{1F1F2}|\x{1F1F2}\x{1F1F3}|\x{1F1F2}\x{1F1F4}|\x{1F1F2}\x{1F1F5}|\x{1F1F2}\x{1F1F6}|\x{1F1F2}\x{1F1F7}|\x{1F1F2}\x{1F1F8}|\x{1F1F2}\x{1F1F9}|\x{1F1F2}\x{1F1FA}|\x{1F1F2}\x{1F1FB}|\x{1F1F2}\x{1F1FC}|\x{1F1F2}\x{1F1FD}|\x{1F1F2}\x{1F1FE}|\x{1F1F2}\x{1F1FF}|\x{1F1F3}\x{1F1E6}|\x{1F1F3}\x{1F1E8}|\x{1F1F3}\x{1F1EA}|\x{1F1F3}\x{1F1EB}|\x{1F1F3}\x{1F1EC}|\x{1F1F3}\x{1F1EE}|\x{1F1F3}\x{1F1F1}|\x{1F1F3}\x{1F1F4}|\x{1F1F3}\x{1F1F5}|\x{1F1F3}\x{1F1F7}|\x{1F1F3}\x{1F1FA}|\x{1F1F3}\x{1F1FF}|\x{1F1F4}\x{1F1F2}|\x{1F1F5}\x{1F1E6}|\x{1F1F5}\x{1F1EA}|\x{1F1F5}\x{1F1EB}|\x{1F1F5}\x{1F1EC}|\x{1F1F5}\x{1F1ED}|\x{1F1F5}\x{1F1F0}|\x{1F1F5}\x{1F1F1}|\x{1F1F5}\x{1F1F2}|\x{1F1F5}\x{1F1F3}|\x{1F1F5}\x{1F1F7}|\x{1F1F5}\x{1F1F8}|\x{1F1F5}\x{1F1F9}|\x{1F1F5}\x{1F1FC}|\x{1F1F5}\x{1F1FE}|\x{1F1F6}\x{1F1E6}|\x{1F1F7}\x{1F1EA}|\x{1F1F7}\x{1F1F4}|\x{1F1F7}\x{1F1F8}|\x{1F1F7}\x{1F1FA}|\x{1F1F7}\x{1F1FC}|\x{1F1F8}\x{1F1E6}|\x{1F1F8}\x{1F1E7}|\x{1F1F8}\x{1F1E8}|\x{1F1F8}\x{1F1E9}|\x{1F1F8}\x{1F1EA}|\x{1F1F8}\x{1F1EC}|\x{1F1F8}\x{1F1ED}|\x{1F1F8}\x{1F1EE}|\x{1F1F8}\x{1F1EF}|\x{1F1F8}\x{1F1F0}|\x{1F1F8}\x{1F1F1}|\x{1F1F8}\x{1F1F2}|\x{1F1F8}\x{1F1F3}|\x{1F1F8}\x{1F1F4}|\x{1F1F8}\x{1F1F7}|\x{1F1F8}\x{1F1F8}|\x{1F1F8}\x{1F1F9}|\x{1F1F8}\x{1F1FB}|\x{1F1F8}\x{1F1FD}|\x{1F1F8}\x{1F1FE}|\x{1F1F8}\x{1F1FF}|\x{1F1F9}\x{1F1E6}|\x{1F1F9}\x{1F1E8}|\x{1F1F9}\x{1F1E9}|\x{1F1F9}\x{1F1EB}|\x{1F1F9}\x{1F1EC}|\x{1F1F9}\x{1F1ED}|\x{1F1F9}\x{1F1EF}|\x{1F1F9}\x{1F1F0}|\x{1F1F9}\x{1F1F1}|\x{1F1F9}\x{1F1F2}|\x{1F1F9}\x{1F1F3}|\x{1F1F9}\x{1F1F4}|\x{1F1F9}\x{1F1F7}|\x{1F1F9}\x{1F1F9}|\x{1F1F9}\x{1F1FB}|\x{1F1F9}\x{1F1FC}|\x{1F1F9}\x{1F1FF}|\x{1F1FA}\x{1F1E6}|\x{1F1FA}\x{1F1EC}|\x{1F1FA}\x{1F1F2}|\x{1F1FA}\x{1F1F3}|\x{1F1FA}\x{1F1F8}|\x{1F1FA}\x{1F1FE}|\x{1F1FA}\x{1F1FF}|\x{1F1FB}\x{1F1E6}|\x{1F1FB}\x{1F1E8}|\x{1F1FB}\x{1F1EA}|\x{1F1FB}\x{1F1EC}|\x{1F1FB}\x{1F1EE}|\x{1F1FB}\x{1F1F3}|\x{1F1FB}\x{1F1FA}|\x{1F1FC}\x{1F1EB}|\x{1F1FC}\x{1F1F8}|\x{1F1FD}\x{1F1F0}|\x{1F1FE}\x{1F1EA}|\x{1F1FE}\x{1F1F9}|\x{1F1FF}\x{1F1E6}|\x{1F1FF}\x{1F1F2}|\x{1F1FF}\x{1F1FC}|\x{1F170}\x{FE0F}|\x{1F171}\x{FE0F}|\x{1F17E}\x{FE0F}|\x{1F17F}\x{FE0F}|\x{1F202}\x{FE0F}|\x{1F237}\x{FE0F}|\x{1F321}\x{FE0F}|\x{1F324}\x{FE0F}|\x{1F325}\x{FE0F}|\x{1F326}\x{FE0F}|\x{1F327}\x{FE0F}|\x{1F328}\x{FE0F}|\x{1F329}\x{FE0F}|\x{1F32A}\x{FE0F}|\x{1F32B}\x{FE0F}|\x{1F32C}\x{FE0F}|\x{1F336}\x{FE0F}|\x{1F37D}\x{FE0F}|\x{1F396}\x{FE0F}|\x{1F397}\x{FE0F}|\x{1F399}\x{FE0F}|\x{1F39A}\x{FE0F}|\x{1F39B}\x{FE0F}|\x{1F39E}\x{FE0F}|\x{1F39F}\x{FE0F}|\x{1F3CD}\x{FE0F}|\x{1F3CE}\x{FE0F}|\x{1F3D4}\x{FE0F}|\x{1F3D5}\x{FE0F}|\x{1F3D6}\x{FE0F}|\x{1F3D7}\x{FE0F}|\x{1F3D8}\x{FE0F}|\x{1F3D9}\x{FE0F}|\x{1F3DA}\x{FE0F}|\x{1F3DB}\x{FE0F}|\x{1F3DC}\x{FE0F}|\x{1F3DD}\x{FE0F}|\x{1F3DE}\x{FE0F}|\x{1F3DF}\x{FE0F}|\x{1F3F3}\x{FE0F}|\x{1F3F5}\x{FE0F}|\x{1F3F7}\x{FE0F}|\x{1F43F}\x{FE0F}|\x{1F441}\x{FE0F}|\x{1F4FD}\x{FE0F}|\x{1F549}\x{FE0F}|\x{1F54A}\x{FE0F}|\x{1F56F}\x{FE0F}|\x{1F570}\x{FE0F}|\x{1F573}\x{FE0F}|\x{1F576}\x{FE0F}|\x{1F577}\x{FE0F}|\x{1F578}\x{FE0F}|\x{1F579}\x{FE0F}|\x{1F587}\x{FE0F}|\x{1F58A}\x{FE0F}|\x{1F58B}\x{FE0F}|\x{1F58C}\x{FE0F}|\x{1F58D}\x{FE0F}|\x{1F5A5}\x{FE0F}|\x{1F5A8}\x{FE0F}|\x{1F5B1}\x{FE0F}|\x{1F5B2}\x{FE0F}|\x{1F5BC}\x{FE0F}|\x{1F5C2}\x{FE0F}|\x{1F5C3}\x{FE0F}|\x{1F5C4}\x{FE0F}|\x{1F5D1}\x{FE0F}|\x{1F5D2}\x{FE0F}|\x{1F5D3}\x{FE0F}|\x{1F5DC}\x{FE0F}|\x{1F5DD}\x{FE0F}|\x{1F5DE}\x{FE0F}|\x{1F5E1}\x{FE0F}|\x{1F5E3}\x{FE0F}|\x{1F5E8}\x{FE0F}|\x{1F5EF}\x{FE0F}|\x{1F5F3}\x{FE0F}|\x{1F5FA}\x{FE0F}|\x{1F6CB}\x{FE0F}|\x{1F6CD}\x{FE0F}|\x{1F6CE}\x{FE0F}|\x{1F6CF}\x{FE0F}|\x{1F6E0}\x{FE0F}|\x{1F6E1}\x{FE0F}|\x{1F6E2}\x{FE0F}|\x{1F6E3}\x{FE0F}|\x{1F6E4}\x{FE0F}|\x{1F6E5}\x{FE0F}|\x{1F6E9}\x{FE0F}|\x{1F6F0}\x{FE0F}|\x{1F6F3}\x{FE0F}|\x{00A9}\x{FE0F}|\x{00AE}\x{FE0F}|\x{203C}\x{FE0F}|\x{2049}\x{FE0F}|\x{2122}\x{FE0F}|\x{2139}\x{FE0F}|\x{2194}\x{FE0F}|\x{2195}\x{FE0F}|\x{2196}\x{FE0F}|\x{2197}\x{FE0F}|\x{2198}\x{FE0F}|\x{2199}\x{FE0F}|\x{21A9}\x{FE0F}|\x{21AA}\x{FE0F}|\x{2328}\x{FE0F}|\x{23CF}\x{FE0F}|\x{23ED}\x{FE0F}|\x{23EE}\x{FE0F}|\x{23EF}\x{FE0F}|\x{23F1}\x{FE0F}|\x{23F2}\x{FE0F}|\x{23F8}\x{FE0F}|\x{23F9}\x{FE0F}|\x{23FA}\x{FE0F}|\x{24C2}\x{FE0F}|\x{25AA}\x{FE0F}|\x{25AB}\x{FE0F}|\x{25B6}\x{FE0F}|\x{25C0}\x{FE0F}|\x{25FB}\x{FE0F}|\x{25FC}\x{FE0F}|\x{2600}\x{FE0F}|\x{2601}\x{FE0F}|\x{2602}\x{FE0F}|\x{2603}\x{FE0F}|\x{2604}\x{FE0F}|\x{260E}\x{FE0F}|\x{2611}\x{FE0F}|\x{2618}\x{FE0F}|\x{2620}\x{FE0F}|\x{2622}\x{FE0F}|\x{2623}\x{FE0F}|\x{2626}\x{FE0F}|\x{262A}\x{FE0F}|\x{262E}\x{FE0F}|\x{262F}\x{FE0F}|\x{2638}\x{FE0F}|\x{2639}\x{FE0F}|\x{263A}\x{FE0F}|\x{2640}\x{FE0F}|\x{2642}\x{FE0F}|\x{265F}\x{FE0F}|\x{2660}\x{FE0F}|\x{2663}\x{FE0F}|\x{2665}\x{FE0F}|\x{2666}\x{FE0F}|\x{2668}\x{FE0F}|\x{267B}\x{FE0F}|\x{267E}\x{FE0F}|\x{2692}\x{FE0F}|\x{2694}\x{FE0F}|\x{2695}\x{FE0F}|\x{2696}\x{FE0F}|\x{2697}\x{FE0F}|\x{2699}\x{FE0F}|\x{269B}\x{FE0F}|\x{269C}\x{FE0F}|\x{26A0}\x{FE0F}|\x{26A7}\x{FE0F}|\x{26B0}\x{FE0F}|\x{26B1}\x{FE0F}|\x{26C8}\x{FE0F}|\x{26CF}\x{FE0F}|\x{26D1}\x{FE0F}|\x{26D3}\x{FE0F}|\x{26E9}\x{FE0F}|\x{26F0}\x{FE0F}|\x{26F1}\x{FE0F}|\x{26F4}\x{FE0F}|\x{26F7}\x{FE0F}|\x{26F8}\x{FE0F}|\x{2702}\x{FE0F}|\x{2708}\x{FE0F}|\x{2709}\x{FE0F}|\x{270F}\x{FE0F}|\x{2712}\x{FE0F}|\x{2714}\x{FE0F}|\x{2716}\x{FE0F}|\x{271D}\x{FE0F}|\x{2721}\x{FE0F}|\x{2733}\x{FE0F}|\x{2734}\x{FE0F}|\x{2744}\x{FE0F}|\x{2747}\x{FE0F}|\x{2763}\x{FE0F}|\x{2764}\x{FE0F}|\x{27A1}\x{FE0F}|\x{2934}\x{FE0F}|\x{2935}\x{FE0F}|\x{2B05}\x{FE0F}|\x{2B06}\x{FE0F}|\x{2B07}\x{FE0F}|\x{3030}\x{FE0F}|\x{303D}\x{FE0F}|\x{3297}\x{FE0F}|\x{3299}\x{FE0F}|\x{1F004}|\x{1F0CF}|\x{1F18E}|\x{1F191}|\x{1F192}|\x{1F193}|\x{1F194}|\x{1F195}|\x{1F196}|\x{1F197}|\x{1F198}|\x{1F199}|\x{1F19A}|\x{1F201}|\x{1F21A}|\x{1F22F}|\x{1F232}|\x{1F233}|\x{1F234}|\x{1F235}|\x{1F236}|\x{1F238}|\x{1F239}|\x{1F23A}|\x{1F250}|\x{1F251}|\x{1F300}|\x{1F301}|\x{1F302}|\x{1F303}|\x{1F304}|\x{1F305}|\x{1F306}|\x{1F307}|\x{1F308}|\x{1F309}|\x{1F30A}|\x{1F30B}|\x{1F30C}|\x{1F30D}|\x{1F30E}|\x{1F30F}|\x{1F310}|\x{1F311}|\x{1F312}|\x{1F313}|\x{1F314}|\x{1F315}|\x{1F316}|\x{1F317}|\x{1F318}|\x{1F319}|\x{1F31A}|\x{1F31B}|\x{1F31C}|\x{1F31D}|\x{1F31E}|\x{1F31F}|\x{1F320}|\x{1F32D}|\x{1F32E}|\x{1F32F}|\x{1F330}|\x{1F331}|\x{1F332}|\x{1F333}|\x{1F334}|\x{1F335}|\x{1F337}|\x{1F338}|\x{1F339}|\x{1F33A}|\x{1F33B}|\x{1F33C}|\x{1F33D}|\x{1F33E}|\x{1F33F}|\x{1F340}|\x{1F341}|\x{1F342}|\x{1F343}|\x{1F344}|\x{1F345}|\x{1F346}|\x{1F347}|\x{1F348}|\x{1F349}|\x{1F34A}|\x{1F34B}|\x{1F34C}|\x{1F34D}|\x{1F34E}|\x{1F34F}|\x{1F350}|\x{1F351}|\x{1F352}|\x{1F353}|\x{1F354}|\x{1F355}|\x{1F356}|\x{1F357}|\x{1F358}|\x{1F359}|\x{1F35A}|\x{1F35B}|\x{1F35C}|\x{1F35D}|\x{1F35E}|\x{1F35F}|\x{1F360}|\x{1F361}|\x{1F362}|\x{1F363}|\x{1F364}|\x{1F365}|\x{1F366}|\x{1F367}|\x{1F368}|\x{1F369}|\x{1F36A}|\x{1F36B}|\x{1F36C}|\x{1F36D}|\x{1F36E}|\x{1F36F}|\x{1F370}|\x{1F371}|\x{1F372}|\x{1F373}|\x{1F374}|\x{1F375}|\x{1F376}|\x{1F377}|\x{1F378}|\x{1F379}|\x{1F37A}|\x{1F37B}|\x{1F37C}|\x{1F37E}|\x{1F37F}|\x{1F380}|\x{1F381}|\x{1F382}|\x{1F383}|\x{1F384}|\x{1F386}|\x{1F387}|\x{1F388}|\x{1F389}|\x{1F38A}|\x{1F38B}|\x{1F38C}|\x{1F38D}|\x{1F38E}|\x{1F38F}|\x{1F390}|\x{1F391}|\x{1F392}|\x{1F393}|\x{1F3A0}|\x{1F3A1}|\x{1F3A2}|\x{1F3A3}|\x{1F3A4}|\x{1F3A5}|\x{1F3A6}|\x{1F3A7}|\x{1F3A8}|\x{1F3A9}|\x{1F3AA}|\x{1F3AB}|\x{1F3AC}|\x{1F3AD}|\x{1F3AE}|\x{1F3AF}|\x{1F3B0}|\x{1F3B1}|\x{1F3B2}|\x{1F3B3}|\x{1F3B4}|\x{1F3B5}|\x{1F3B6}|\x{1F3B7}|\x{1F3B8}|\x{1F3B9}|\x{1F3BA}|\x{1F3BB}|\x{1F3BC}|\x{1F3BD}|\x{1F3BE}|\x{1F3BF}|\x{1F3C0}|\x{1F3C1}|\x{1F3C5}|\x{1F3C6}|\x{1F3C8}|\x{1F3C9}|\x{1F3CF}|\x{1F3D0}|\x{1F3D1}|\x{1F3D2}|\x{1F3D3}|\x{1F3E0}|\x{1F3E1}|\x{1F3E2}|\x{1F3E3}|\x{1F3E4}|\x{1F3E5}|\x{1F3E6}|\x{1F3E7}|\x{1F3E8}|\x{1F3E9}|\x{1F3EA}|\x{1F3EB}|\x{1F3EC}|\x{1F3ED}|\x{1F3EE}|\x{1F3EF}|\x{1F3F0}|\x{1F3F4}|\x{1F3F8}|\x{1F3F9}|\x{1F3FA}|\x{1F3FB}|\x{1F3FC}|\x{1F3FD}|\x{1F3FE}|\x{1F3FF}|\x{1F400}|\x{1F401}|\x{1F402}|\x{1F403}|\x{1F404}|\x{1F405}|\x{1F406}|\x{1F407}|\x{1F408}|\x{1F409}|\x{1F40A}|\x{1F40B}|\x{1F40C}|\x{1F40D}|\x{1F40E}|\x{1F40F}|\x{1F410}|\x{1F411}|\x{1F412}|\x{1F413}|\x{1F414}|\x{1F415}|\x{1F416}|\x{1F417}|\x{1F418}|\x{1F419}|\x{1F41A}|\x{1F41B}|\x{1F41C}|\x{1F41D}|\x{1F41E}|\x{1F41F}|\x{1F420}|\x{1F421}|\x{1F422}|\x{1F423}|\x{1F424}|\x{1F425}|\x{1F426}|\x{1F427}|\x{1F428}|\x{1F429}|\x{1F42A}|\x{1F42B}|\x{1F42C}|\x{1F42D}|\x{1F42E}|\x{1F42F}|\x{1F430}|\x{1F431}|\x{1F432}|\x{1F433}|\x{1F434}|\x{1F435}|\x{1F436}|\x{1F437}|\x{1F438}|\x{1F439}|\x{1F43A}|\x{1F43B}|\x{1F43C}|\x{1F43D}|\x{1F43E}|\x{1F440}|\x{1F444}|\x{1F445}|\x{1F451}|\x{1F452}|\x{1F453}|\x{1F454}|\x{1F455}|\x{1F456}|\x{1F457}|\x{1F458}|\x{1F459}|\x{1F45A}|\x{1F45B}|\x{1F45C}|\x{1F45D}|\x{1F45E}|\x{1F45F}|\x{1F460}|\x{1F461}|\x{1F462}|\x{1F463}|\x{1F464}|\x{1F465}|\x{1F46A}|\x{1F46F}|\x{1F479}|\x{1F47A}|\x{1F47B}|\x{1F47D}|\x{1F47E}|\x{1F47F}|\x{1F480}|\x{1F484}|\x{1F488}|\x{1F489}|\x{1F48A}|\x{1F48B}|\x{1F48C}|\x{1F48D}|\x{1F48E}|\x{1F490}|\x{1F492}|\x{1F493}|\x{1F494}|\x{1F495}|\x{1F496}|\x{1F497}|\x{1F498}|\x{1F499}|\x{1F49A}|\x{1F49B}|\x{1F49C}|\x{1F49D}|\x{1F49E}|\x{1F49F}|\x{1F4A0}|\x{1F4A1}|\x{1F4A2}|\x{1F4A3}|\x{1F4A4}|\x{1F4A5}|\x{1F4A6}|\x{1F4A7}|\x{1F4A8}|\x{1F4A9}|\x{1F4AB}|\x{1F4AC}|\x{1F4AD}|\x{1F4AE}|\x{1F4AF}|\x{1F4B0}|\x{1F4B1}|\x{1F4B2}|\x{1F4B3}|\x{1F4B4}|\x{1F4B5}|\x{1F4B6}|\x{1F4B7}|\x{1F4B8}|\x{1F4B9}|\x{1F4BA}|\x{1F4BB}|\x{1F4BC}|\x{1F4BD}|\x{1F4BE}|\x{1F4BF}|\x{1F4C0}|\x{1F4C1}|\x{1F4C2}|\x{1F4C3}|\x{1F4C4}|\x{1F4C5}|\x{1F4C6}|\x{1F4C7}|\x{1F4C8}|\x{1F4C9}|\x{1F4CA}|\x{1F4CB}|\x{1F4CC}|\x{1F4CD}|\x{1F4CE}|\x{1F4CF}|\x{1F4D0}|\x{1F4D1}|\x{1F4D2}|\x{1F4D3}|\x{1F4D4}|\x{1F4D5}|\x{1F4D6}|\x{1F4D7}|\x{1F4D8}|\x{1F4D9}|\x{1F4DA}|\x{1F4DB}|\x{1F4DC}|\x{1F4DD}|\x{1F4DE}|\x{1F4DF}|\x{1F4E0}|\x{1F4E1}|\x{1F4E2}|\x{1F4E3}|\x{1F4E4}|\x{1F4E5}|\x{1F4E6}|\x{1F4E7}|\x{1F4E8}|\x{1F4E9}|\x{1F4EA}|\x{1F4EB}|\x{1F4EC}|\x{1F4ED}|\x{1F4EE}|\x{1F4EF}|\x{1F4F0}|\x{1F4F1}|\x{1F4F2}|\x{1F4F3}|\x{1F4F4}|\x{1F4F5}|\x{1F4F6}|\x{1F4F7}|\x{1F4F8}|\x{1F4F9}|\x{1F4FA}|\x{1F4FB}|\x{1F4FC}|\x{1F4FF}|\x{1F500}|\x{1F501}|\x{1F502}|\x{1F503}|\x{1F504}|\x{1F505}|\x{1F506}|\x{1F507}|\x{1F508}|\x{1F509}|\x{1F50A}|\x{1F50B}|\x{1F50C}|\x{1F50D}|\x{1F50E}|\x{1F50F}|\x{1F510}|\x{1F511}|\x{1F512}|\x{1F513}|\x{1F514}|\x{1F515}|\x{1F516}|\x{1F517}|\x{1F518}|\x{1F519}|\x{1F51A}|\x{1F51B}|\x{1F51C}|\x{1F51D}|\x{1F51E}|\x{1F51F}|\x{1F520}|\x{1F521}|\x{1F522}|\x{1F523}|\x{1F524}|\x{1F525}|\x{1F526}|\x{1F527}|\x{1F528}|\x{1F529}|\x{1F52A}|\x{1F52B}|\x{1F52C}|\x{1F52D}|\x{1F52E}|\x{1F52F}|\x{1F530}|\x{1F531}|\x{1F532}|\x{1F533}|\x{1F534}|\x{1F535}|\x{1F536}|\x{1F537}|\x{1F538}|\x{1F539}|\x{1F53A}|\x{1F53B}|\x{1F53C}|\x{1F53D}|\x{1F54B}|\x{1F54C}|\x{1F54D}|\x{1F54E}|\x{1F550}|\x{1F551}|\x{1F552}|\x{1F553}|\x{1F554}|\x{1F555}|\x{1F556}|\x{1F557}|\x{1F558}|\x{1F559}|\x{1F55A}|\x{1F55B}|\x{1F55C}|\x{1F55D}|\x{1F55E}|\x{1F55F}|\x{1F560}|\x{1F561}|\x{1F562}|\x{1F563}|\x{1F564}|\x{1F565}|\x{1F566}|\x{1F567}|\x{1F5A4}|\x{1F5FB}|\x{1F5FC}|\x{1F5FD}|\x{1F5FE}|\x{1F5FF}|\x{1F600}|\x{1F601}|\x{1F602}|\x{1F603}|\x{1F604}|\x{1F605}|\x{1F606}|\x{1F607}|\x{1F608}|\x{1F609}|\x{1F60A}|\x{1F60B}|\x{1F60C}|\x{1F60D}|\x{1F60E}|\x{1F60F}|\x{1F610}|\x{1F611}|\x{1F612}|\x{1F613}|\x{1F614}|\x{1F615}|\x{1F616}|\x{1F617}|\x{1F618}|\x{1F619}|\x{1F61A}|\x{1F61B}|\x{1F61C}|\x{1F61D}|\x{1F61E}|\x{1F61F}|\x{1F620}|\x{1F621}|\x{1F622}|\x{1F623}|\x{1F624}|\x{1F625}|\x{1F626}|\x{1F627}|\x{1F628}|\x{1F629}|\x{1F62A}|\x{1F62B}|\x{1F62C}|\x{1F62D}|\x{1F62E}|\x{1F62F}|\x{1F630}|\x{1F631}|\x{1F632}|\x{1F633}|\x{1F634}|\x{1F635}|\x{1F636}|\x{1F637}|\x{1F638}|\x{1F639}|\x{1F63A}|\x{1F63B}|\x{1F63C}|\x{1F63D}|\x{1F63E}|\x{1F63F}|\x{1F640}|\x{1F641}|\x{1F642}|\x{1F643}|\x{1F644}|\x{1F648}|\x{1F649}|\x{1F64A}|\x{1F680}|\x{1F681}|\x{1F682}|\x{1F683}|\x{1F684}|\x{1F685}|\x{1F686}|\x{1F687}|\x{1F688}|\x{1F689}|\x{1F68A}|\x{1F68B}|\x{1F68C}|\x{1F68D}|\x{1F68E}|\x{1F68F}|\x{1F690}|\x{1F691}|\x{1F692}|\x{1F693}|\x{1F694}|\x{1F695}|\x{1F696}|\x{1F697}|\x{1F698}|\x{1F699}|\x{1F69A}|\x{1F69B}|\x{1F69C}|\x{1F69D}|\x{1F69E}|\x{1F69F}|\x{1F6A0}|\x{1F6A1}|\x{1F6A2}|\x{1F6A4}|\x{1F6A5}|\x{1F6A6}|\x{1F6A7}|\x{1F6A8}|\x{1F6A9}|\x{1F6AA}|\x{1F6AB}|\x{1F6AC}|\x{1F6AD}|\x{1F6AE}|\x{1F6AF}|\x{1F6B0}|\x{1F6B1}|\x{1F6B2}|\x{1F6B3}|\x{1F6B7}|\x{1F6B8}|\x{1F6B9}|\x{1F6BA}|\x{1F6BB}|\x{1F6BC}|\x{1F6BD}|\x{1F6BE}|\x{1F6BF}|\x{1F6C1}|\x{1F6C2}|\x{1F6C3}|\x{1F6C4}|\x{1F6C5}|\x{1F6D0}|\x{1F6D1}|\x{1F6D2}|\x{1F6D5}|\x{1F6D6}|\x{1F6D7}|\x{1F6DD}|\x{1F6DE}|\x{1F6DF}|\x{1F6EB}|\x{1F6EC}|\x{1F6F4}|\x{1F6F5}|\x{1F6F6}|\x{1F6F7}|\x{1F6F8}|\x{1F6F9}|\x{1F6FA}|\x{1F6FB}|\x{1F6FC}|\x{1F7E0}|\x{1F7E1}|\x{1F7E2}|\x{1F7E3}|\x{1F7E4}|\x{1F7E5}|\x{1F7E6}|\x{1F7E7}|\x{1F7E8}|\x{1F7E9}|\x{1F7EA}|\x{1F7EB}|\x{1F7F0}|\x{1F90D}|\x{1F90E}|\x{1F910}|\x{1F911}|\x{1F912}|\x{1F913}|\x{1F914}|\x{1F915}|\x{1F916}|\x{1F917}|\x{1F920}|\x{1F921}|\x{1F922}|\x{1F923}|\x{1F924}|\x{1F925}|\x{1F927}|\x{1F928}|\x{1F929}|\x{1F92A}|\x{1F92B}|\x{1F92C}|\x{1F92D}|\x{1F92E}|\x{1F92F}|\x{1F93A}|\x{1F93C}|\x{1F93F}|\x{1F940}|\x{1F941}|\x{1F942}|\x{1F943}|\x{1F944}|\x{1F945}|\x{1F947}|\x{1F948}|\x{1F949}|\x{1F94A}|\x{1F94B}|\x{1F94C}|\x{1F94D}|\x{1F94E}|\x{1F94F}|\x{1F950}|\x{1F951}|\x{1F952}|\x{1F953}|\x{1F954}|\x{1F955}|\x{1F956}|\x{1F957}|\x{1F958}|\x{1F959}|\x{1F95A}|\x{1F95B}|\x{1F95C}|\x{1F95D}|\x{1F95E}|\x{1F95F}|\x{1F960}|\x{1F961}|\x{1F962}|\x{1F963}|\x{1F964}|\x{1F965}|\x{1F966}|\x{1F967}|\x{1F968}|\x{1F969}|\x{1F96A}|\x{1F96B}|\x{1F96C}|\x{1F96D}|\x{1F96E}|\x{1F96F}|\x{1F970}|\x{1F971}|\x{1F972}|\x{1F973}|\x{1F974}|\x{1F975}|\x{1F976}|\x{1F978}|\x{1F979}|\x{1F97A}|\x{1F97B}|\x{1F97C}|\x{1F97D}|\x{1F97E}|\x{1F97F}|\x{1F980}|\x{1F981}|\x{1F982}|\x{1F983}|\x{1F984}|\x{1F985}|\x{1F986}|\x{1F987}|\x{1F988}|\x{1F989}|\x{1F98A}|\x{1F98B}|\x{1F98C}|\x{1F98D}|\x{1F98E}|\x{1F98F}|\x{1F990}|\x{1F991}|\x{1F992}|\x{1F993}|\x{1F994}|\x{1F995}|\x{1F996}|\x{1F997}|\x{1F998}|\x{1F999}|\x{1F99A}|\x{1F99B}|\x{1F99C}|\x{1F99D}|\x{1F99E}|\x{1F99F}|\x{1F9A0}|\x{1F9A1}|\x{1F9A2}|\x{1F9A3}|\x{1F9A4}|\x{1F9A5}|\x{1F9A6}|\x{1F9A7}|\x{1F9A8}|\x{1F9A9}|\x{1F9AA}|\x{1F9AB}|\x{1F9AC}|\x{1F9AD}|\x{1F9AE}|\x{1F9AF}|\x{1F9B4}|\x{1F9B7}|\x{1F9BA}|\x{1F9BC}|\x{1F9BD}|\x{1F9BE}|\x{1F9BF}|\x{1F9C0}|\x{1F9C1}|\x{1F9C2}|\x{1F9C3}|\x{1F9C4}|\x{1F9C5}|\x{1F9C6}|\x{1F9C7}|\x{1F9C8}|\x{1F9C9}|\x{1F9CA}|\x{1F9CB}|\x{1F9CC}|\x{1F9D0}|\x{1F9DE}|\x{1F9DF}|\x{1F9E0}|\x{1F9E1}|\x{1F9E2}|\x{1F9E3}|\x{1F9E4}|\x{1F9E5}|\x{1F9E6}|\x{1F9E7}|\x{1F9E8}|\x{1F9E9}|\x{1F9EA}|\x{1F9EB}|\x{1F9EC}|\x{1F9ED}|\x{1F9EE}|\x{1F9EF}|\x{1F9F0}|\x{1F9F1}|\x{1F9F2}|\x{1F9F3}|\x{1F9F4}|\x{1F9F5}|\x{1F9F6}|\x{1F9F7}|\x{1F9F8}|\x{1F9F9}|\x{1F9FA}|\x{1F9FB}|\x{1F9FC}|\x{1F9FD}|\x{1F9FE}|\x{1F9FF}|\x{1FA70}|\x{1FA71}|\x{1FA72}|\x{1FA73}|\x{1FA74}|\x{1FA78}|\x{1FA79}|\x{1FA7A}|\x{1FA7B}|\x{1FA7C}|\x{1FA80}|\x{1FA81}|\x{1FA82}|\x{1FA83}|\x{1FA84}|\x{1FA85}|\x{1FA86}|\x{1FA90}|\x{1FA91}|\x{1FA92}|\x{1FA93}|\x{1FA94}|\x{1FA95}|\x{1FA96}|\x{1FA97}|\x{1FA98}|\x{1FA99}|\x{1FA9A}|\x{1FA9B}|\x{1FA9C}|\x{1FA9D}|\x{1FA9E}|\x{1FA9F}|\x{1FAA0}|\x{1FAA1}|\x{1FAA2}|\x{1FAA3}|\x{1FAA4}|\x{1FAA5}|\x{1FAA6}|\x{1FAA7}|\x{1FAA8}|\x{1FAA9}|\x{1FAAA}|\x{1FAAB}|\x{1FAAC}|\x{1FAB0}|\x{1FAB1}|\x{1FAB2}|\x{1FAB3}|\x{1FAB4}|\x{1FAB5}|\x{1FAB6}|\x{1FAB7}|\x{1FAB8}|\x{1FAB9}|\x{1FABA}|\x{1FAC0}|\x{1FAC1}|\x{1FAC2}|\x{1FAD0}|\x{1FAD1}|\x{1FAD2}|\x{1FAD3}|\x{1FAD4}|\x{1FAD5}|\x{1FAD6}|\x{1FAD7}|\x{1FAD8}|\x{1FAD9}|\x{1FAE0}|\x{1FAE1}|\x{1FAE2}|\x{1FAE3}|\x{1FAE4}|\x{1FAE5}|\x{1FAE6}|\x{1FAE7}|\x{231A}|\x{231B}|\x{23E9}|\x{23EA}|\x{23EB}|\x{23EC}|\x{23F0}|\x{23F3}|\x{25FD}|\x{25FE}|\x{2614}|\x{2615}|\x{2648}|\x{2649}|\x{264A}|\x{264B}|\x{264C}|\x{264D}|\x{264E}|\x{264F}|\x{2650}|\x{2651}|\x{2652}|\x{2653}|\x{267F}|\x{2693}|\x{26A1}|\x{26AA}|\x{26AB}|\x{26BD}|\x{26BE}|\x{26C4}|\x{26C5}|\x{26CE}|\x{26D4}|\x{26EA}|\x{26F2}|\x{26F3}|\x{26F5}|\x{26FA}|\x{26FD}|\x{2705}|\x{2728}|\x{274C}|\x{274E}|\x{2753}|\x{2754}|\x{2755}|\x{2757}|\x{2795}|\x{2796}|\x{2797}|\x{27B0}|\x{27BF}|\x{2B1B}|\x{2B1C}|\x{2B50}|\x{2B55}';
/**
* @brief Regex for detecting email addresses
*/
const EMAIL_REGEX = '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,9}';
/* !Parser: Bootstrap */
/**
* @brief If parsing BBCode, the supported BBCode tags
*/
protected $bbcode = NULL;
/**
* @brief Attachment IDs
*/
protected $attachIds = NULL;
/**
* @brief Attachment Lang
*/
protected $attachIdsLang = NULL;
/**
* @brief Rows from core_attachments_map containing attachments which belong to the content being edited - as they are found by the parser, they will be removed so we are left with attachments that have been removed
*/
public $existingAttachments = array();
/**
* @brief Attachment IDs
*/
public $mappedAttachments = array();
/**
* @brief If parsing BBCode or attachments, the member posting
*/
protected $member = NULL;
/**
* @brief If parsing BBCode or attachments, the Editor area we're parsing in. e.g. "core_Signatures". A boolean value will allow or disallow all BBCodes that are dependant on area.
*/
protected $area = NULL;
/**
* @brief Loose Profanity Filters
*/
protected $looseProfanity = array();
/**
* @brief Exact Profanity Filters
*/
protected $exactProfanity = array();
/**
* @brief Case-sensitive Acronyms
*/
public $caseSensitiveAcronyms = array();
/**
* @brief Case-insensitive Acronyms
*/
public $caseInsensitiveAcronyms = array();
/**
* @brief If cleaning HTML, the HTMLPurifier object
*/
protected $htmlPurifier = NULL;
/**
* @brief Save on queries and fetch the alt label would just the once
*/
protected $_altLabelWord = NULL;
/**
* Constructor
*
* @param bool $bbcode Parse BBCode?
* @param array|null $attachIds array of ID numbers to idenfity content for attachments if the content has been saved - the first two must be int or null, the third must be string or null. If content has not been saved yet, an MD5 hash used to claim attachments after saving.
* @param \IPS\Member|null $member The member posting, NULL will use currently logged in member.
* @param string|bool $area If parsing BBCode or attachments, the Editor area we're parsing in. e.g. "core_Signatures". A boolean value will allow or disallow all BBCodes that are dependant on area.
* @param bool $filterProfanity Remove profanity?
* @param bool $cleanHtml If TRUE, HTML will be cleaned through HTMLPurifier
* @param callback $htmlPurifierConfig A function which will be passed the HTMLPurifier_Config object to customise it - see example
* @param bool $parseAcronyms Parse acronyms?
* @param ?int $attachIdsLang Language ID number if this Editor is part of a Translatable field.
* @return void
*/
public function __construct( $bbcode=FALSE, $attachIds=NULL, \IPS\Member $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL, $parseAcronyms=TRUE, $attachIdsLang=NULL )
{
/* Set the Member */
$this->member = $member ?: \IPS\Member::loggedIn();
/* Set the member and area */
if ( $bbcode or $attachIds )
{
$this->area = $area;
}
/* Get available BBCodes */
if ( $bbcode )
{
$this->bbcode = static::bbcodeTags( $this->member, $this->area );
}
/* Get attachments */
$this->attachIds = $attachIds;
$this->attachIdsLang = $attachIdsLang;
if( $attachIds !== NULL )
{
$where = array( array( 'location_key=?', $area ) );
if ( \is_array( $attachIds ) )
{
$i = 1;
foreach ( $attachIds as $id )
{
$where[] = array("id{$i}=?", $id);
$i++;
}
}
elseif ( \is_string( $attachIds ) )
{
$where[] = array( 'temp=?', $attachIds );
}
$this->existingAttachments = iterator_to_array( \IPS\Db::i()->select( '*', 'core_attachments_map', $where )->setKeyField( 'attachment_id' ) );
$this->mappedAttachments = array_keys( $this->existingAttachments );
}
/* Get profanity filters */
if ( $filterProfanity )
{
foreach( \IPS\core\Profanity::getProfanity() AS $profanity )
{
if ( $profanity->action == 'swap' )
{
if ( $profanity->m_exact )
{
$this->exactProfanity[ $profanity->type ] = $profanity->swop;
}
else
{
$this->looseProfanity[ $profanity->type ] = $profanity->swop;
}
}
}
}
/* Get HTMLPurifier Configuration */
if ( $cleanHtml )
{
if ( !\function_exists('idn_to_ascii') )
{
\IPS\IPS::$PSR0Namespaces['TrueBV'] = \IPS\ROOT_PATH . "/system/3rd_party/php-punycode";
require_once \IPS\ROOT_PATH . "/system/3rd_party/php-punycode/polyfill.php";
}
require_once \IPS\ROOT_PATH . "/system/3rd_party/HTMLPurifier/HTMLPurifier.auto.php";
$this->htmlPurifier = new \HTMLPurifier( $this->_htmlPurifierConfiguration( $htmlPurifierConfig ) );
}
/* Get acronyms */
if ( $parseAcronyms )
{
$this->caseSensitiveAcronyms = iterator_to_array( \IPS\Db::i()->select( array( 'a_short', 'a_long', 'a_type' ), 'core_acronyms', array( 'a_casesensitive=1' ) )->setKeyField( 'a_short' ) );
$this->caseInsensitiveAcronyms = array();
foreach ( \IPS\Db::i()->select( array( 'a_short', 'a_long', 'a_type' ), 'core_acronyms', array( 'a_casesensitive=0' ) )->setKeyField( 'a_short' ) as $k => $v )
{
$this->caseInsensitiveAcronyms[ mb_strtolower( $k ) ] = $v;
}
}
}
/**
* Force bbcode parsing enabled
*
* @return void
*/
public function __set( $name, $enabled = TRUE )
{
if( $name == 'forceBbcodeEnabled' )
{
$this->forceBbcodeEnabled = $enabled;
if( $enabled )
{
$this->bbcode = static::bbcodeTags( $this->member, $this->area );
}
else
{
$this->bbcode = NULL;
}
}
}
/**
* Parse
*
* @param string $value HTML to parse
* @return string
*/
public function parse( $value )
{
/* Clean HTML */
$value = $this->purify( $value );
/* BBCode, Profanity, etc. */
if ( $value )
{
$value = $this->_parseContent( $value );
}
/* Clean HTML */
$value = $this->purify( $value );
/* Replace any {fileStore.whatever} tags with [tag] Content [/tag]loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $content ) );
/* Swap src for data-src */
$contentImages = $source->getElementsByTagName( 'img' );
foreach( $contentImages as $element )
{
if ( $element->hasAttribute('src') )
{
$imgSrc = $element->hasAttribute('data-src') ? $element->getAttribute( 'data-src' ) : $element->getAttribute( 'src' );
$element->removeAttribute( 'data-loaded' );
$element->setAttribute( 'data-src', $imgSrc );
$element->setAttribute( 'src', static::blankImage() ); // A blank pixel
if ( $element->hasAttribute('data-fileid') AND $attachment = static::_getAttachment( $element->getAttribute('src'), $element->getAttribute('data-fileid') ) )
{
$width = $attachment['attach_thumb_width'] ?: $attachment['attach_img_width'];
$height = ( $attachment['attach_thumb_height'] AND $attachment['attach_thumb_width'] ) ? $attachment['attach_thumb_height'] : $attachment['attach_img_height'];
if( !$element->hasAttribute('width') )
{
$element->setAttribute( 'width', $width );
}
$element->setAttribute( 'data-ratio', number_format( (float) round( ( $height / $width ) * 100, 2 ), 2, '.', '' ) );
}
}
}
/* Set flags on videos */
$contentVideos = $source->getElementsByTagName( 'video' );
foreach( $contentVideos as $element )
{
if ( $element->hasAttribute('data-controller') )
{
$element->setAttribute( 'data-video-embed', '' );
$element->removeAttribute( 'data-controller' );
}
}
/* Swap src for data-video-src */
$contentVideos = $source->getElementsByTagName( 'source' );
foreach( $contentVideos as $element )
{
if ( $element->parentNode->tagName === 'video' and $element->hasAttribute('src') )
{
$videoSrc = $element->hasAttribute('data-video-src') ? $element->getAttribute( 'data-video-src' ) : $element->getAttribute( 'src' );
$element->setAttribute( 'data-video-src', $videoSrc );
$element->setAttribute( 'src', static::blankImage() ); // A blank pixel
}
}
/* Swap src for data-embed-src */
$contentEmbeds = $source->getElementsByTagName( 'iframe' );
foreach( $contentEmbeds as $element )
{
if ( $element->hasAttribute('src') )
{
$embedSrc = $element->hasAttribute('data-embed-src') ? $element->getAttribute( 'data-embed-src' ) : $element->getAttribute( 'src' );
$element->setAttribute( 'data-embed-src', $embedSrc );
$element->removeAttribute( 'data-controller' );
$element->setAttribute( 'src', static::blankPage() ); // A blank pixel
}
}
/* Get DOMDocument output */
$content = \IPS\Text\DOMParser::getDocumentBodyContents( $source );
/* Replace file storage tags */
$content = preg_replace( '/<fileStore\.([\d\w\_]+?)>/i', '
mode where BBCode tags are wrapped in P tags like so:
tags inside block BBCode tags, so our example ends up parsing like so:
[tag]
Content
[/tag]
tags are converted to
to prevent parser confusion */
preg_match_all( '#\[(' . implode( '|', $blockTags ) . ')\](.+?)\[/\1\]#si', $value, $matches, PREG_SET_ORDER );
foreach( $matches as $id => $match )
{
$value = str_replace( $match[0], preg_replace( '#
]+?)?'.'>#i', '
', $match[0] ), $value );
}
}
}
/* The editor button just drops in a
Page one
Page two
Page one
Page two
*/ if ( static::canUse( $this->member, 'Page', $this->area ) ) { if ( ! preg_match( '#, , etc.)
*
* @param \DOMElement $element The element from the source document to parse
* @param \DOMNode $parent The node from the new document which will be this node's parent
* @param \IPS\Text\DOMParser $parser DOMParser Object
* @return void
*/
public function _parseDomElement( \DOMElement $element, \DOMNode $parent, \IPS\Text\DOMParser $parser )
{
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtStartOfNode( $parent );
/* Start of an ? */
$okayToParse = TRUE;
if ( $element->tagName === 'abbr' and $element->hasAttribute('title') )
{
$title = $element->getAttribute('title');
if ( !\in_array( $title, $this->openAbbrTags ) )
{
$this->openAbbrTags[] = $title;
}
else
{
$okayToParse = FALSE;
}
}
/* Import */
if ( $okayToParse )
{
/* Import the element as it is */
$ownerDocument = $parent->ownerDocument ?: $parent;
$newElement = $ownerDocument->importNode( $element );
/* Element-specific parsing */
if ( $okayToParse )
{
$newElement = $this->_parseElement( $newElement, $element );
}
/* Append */
$parent->appendChild( $newElement );
/* Swap out emoticons that should be plaintext (meaning we hit the maximum limit of emoticons per editor) */
foreach( $parent->getElementsByTagName( 'img' ) AS $img )
{
if ( $img->hasAttribute( 'data-ipsEmoticon-plain' ) )
{
$replace = $parent->appendChild( new \DOMText( $img->getAttribute( 'data-ipsEmoticon-plain' ) ) );
$parent->replaceChild( $replace, $img );
}
}
/* ) to go in the block-level element for any content left (e.g. " [center]This needs to be centered [/center]This should not be centered tags don't parse BBCode inside */
$resumeBBCodeAfterPre = FALSE;
if ( $newElement->tagName == 'pre' and $this->bbcodeParse )
{
$this->bbcodeParse = FALSE;
$resumeBBCodeAfterPre = TRUE;
}
}
else
{
$newElement = $parent;
}
/* Loop children */
$parser->_parseDomNodeList( $element->childNodes, $newElement );
/* Finish */
if ( $okayToParse )
{
/* tags don't parse BBCode inside */
if ( $newElement->tagName == 'pre' and $resumeBBCodeAfterPre )
{
$this->bbcodeParse = TRUE;
}
/* End of an ? */
if ( $okayToParse and $element->tagName === 'abbr' and $element->hasAttribute('title') )
{
$k = array_search( $element->getAttribute('title'), $this->openAbbrTags );
if ( $k !== FALSE )
{
unset( $this->openAbbrTags[ $k ] );
}
}
/* If we did have children, but now we don't (for example, the entire content is a block-level BBCode), drop this element to avoid unintentional whitespace */
if ( $newElement->parentNode and $element->childNodes->length and !$newElement->childNodes->length )
{
$parent->removeChild( $newElement );
}
}
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtEndOfNode( $parent );
}
/**
* Parse Text
*
* @param \DOMText $textNode The text from the source document to parse
* @param \DOMNode $parent The node from the new document which will be this node's parent - passed by reference and may be modified for siblings
* @return void
*/
public function _parseDomText( \DOMText $textNode, \DOMNode &$parent, \IPS\Text\DOMParser $parser )
{
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtStartOfNode( $parent );
/* Init */
$text = $textNode->wholeText;
$breakPoints = array( '(' . static::EMOJI_REGEX . ')' );
/* Contains [page] tags? */
if ( mb_strpos( $text, '[page]' ) !== FALSE )
{
$this->containsPageTags = TRUE;
}
/* If we are parsing BBCode, we will look for opening (e.g. "[foo=bar]") and closing (e.g. "[/foo]") tags */
if ( $this->bbcode !== NULL and \count( $this->bbcode ) )
{
/* First, if we have any single-tag BBCodes (e.g. "[img=URL]") expressed as normal BBCodes (e.g. "[img]URL[/img]") - fix that */
foreach ( $this->bbcode as $tag => $bbcode )
{
if ( isset( $bbcode['single'] ) and $bbcode['single'] )
{
if ( isset( $bbcode['attributes'] ) and \in_array( '{option}', $bbcode['attributes'] ) )
{
$text = preg_replace( '/\[(' . preg_quote( $tag, '/' ) . ')\](.+?)\[\/' . preg_quote( $tag, '/' ) . '\]/i', '[$1=$2]', $text );
}
else
{
$text = preg_replace( '/\[(' . preg_quote( $tag, '/' ) . ')\]\s*\[\/' . preg_quote( $tag, '/' ) . '\]/i', '[$1]', $text );
}
}
}
/* And add our regex to the breakpoints */
$breakPoints[] = '(\[\/?(?:' . implode( '|', array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->bbcode ) ) ) . ')(?:[=\s].+?)?\])';
}
/* If we have any acronyms, they also need to be breakpoints */
if ( \count( $this->caseSensitiveAcronyms ) or \count( $this->caseInsensitiveAcronyms ) )
{
$breakPoints[] = '((?=<^|\b|\W)(?:' . implode( '|', array_merge( array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->caseSensitiveAcronyms ) ), array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->caseInsensitiveAcronyms ) ) ) ) . ')(?=\b|\W|$))';
}
/* Loop through each section */
if ( \count( $breakPoints ) )
{
$sections = array_values( array_filter( preg_split( '/' . implode( '|', $breakPoints ) . '/iu', $text, null, PREG_SPLIT_DELIM_CAPTURE ), function( $val ) { return $val !== ''; } ) );
foreach( $sections as $sectionId => $section )
{
$this->_parseTextSection( $section, $parent, ++$sectionId, \count( $sections ) );
}
}
else
{
$this->_parseTextSection( $textNode->wholeText, $parent, 1, 1 );
}
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtEndOfNode( $parent );
}
/**
* Parse a section of text after it has been split into relevant sections
*
* @param string $section The text from the source document to parse
* @param \DOMNode $parent The node from the new document which will be this node's parent - passed by reference and may be modified for siblings
* @param int $sectionId The position of this section out of all the sections in the node - used to indicate if there's text before/after this section
* @param int $sectionCount The total number of sections in the node - used to indicate if there's text before/after this section
* @return void
*/
protected function _parseTextSection( $section, \DOMNode &$parent, $sectionId, $sectionCount )
{
/* If it's empty, skip it */
if ( $section === '' )
{
return;
}
/* If this restarts parsing, do that */
if ( $section == $this->resumeBBCodeParsingOn )
{
$this->bbcodeParse = TRUE;
$this->resumeBBCodeParsingOn = NULL;
}
/* Start of BBCode tag? */
if (
$this->bbcode !== NULL and $this->bbcodeParse and // BBCode is enabled
preg_match( '/^\[([a-z\*]+?)(?:([=\s])(.+?))?\]$/i', $section, $matches ) and // It looks like a BBCode tag
array_key_exists( mb_strtolower( $matches[1] ), $this->bbcode ) and // The tag is in the list
( !isset( $this->bbcode[ mb_strtolower( $matches[1] ) ]['allowOption'] ) or $this->bbcode[ mb_strtolower( $matches[1] ) ]['allowOption'] === TRUE or !isset( $matches[3] ) or !$matches[3] ) // If options aren't allowed for this tag, there isn't one
)
{
/* What was the option? */
$option = NULL;
if ( isset( $matches[3] ) )
{
$option = $matches[3];
/* If it's [foo="bar"] then we strip the quotes, (if it's [foo bar="baz"] then we don't) */
if ( !preg_match( '/^\s*$/', $matches[2] ) )
{
$option = trim( $option, '"\'' );
}
}
/* Send to _openBBcode */
$this->_openBBCode( mb_strtolower( $matches[1] ), $option, $parent, $sectionId, $sectionCount );
}
/* End of BBCode tag? */
elseif ( $this->bbcodeParse and array_key_exists( mb_strtolower( $section ), $this->closeTagsForOpenBBCode ) )
{
$this->_closeBBCode( mb_substr( mb_strtolower( $section ), 2, -1 ), $parent, $sectionId, $sectionCount );
}
/* Normal text */
else
{
/* HTMLPurifier will strip carrage returns, but if HTML posting is enabled this doesn't happen which
leaves blank spaces - so we need to strip here */
if ( !$this->htmlPurifier )
{
$section = str_replace( "\r", '', $section );
}
/* Profanity */
foreach ( $this->exactProfanity as $bad => $good )
{
$section = preg_replace( '/(^|\b|\s)' . preg_quote( $bad, '/' ) . '(\b|\s|!|\?|\.|,|$)/iu', "\\1" . $good . "\\2", $section );
}
$section = str_ireplace( array_keys( $this->looseProfanity ), array_values( $this->looseProfanity ), $section );
/* Note what $parent is */
$originalParent = $parent;
/* Acronym? */
if ( $this->bbcodeParse and array_key_exists( $section, $this->caseSensitiveAcronyms ) and !\in_array( $this->caseSensitiveAcronyms[ $section ], $this->openAbbrTags ) )
{
switch( $this->caseSensitiveAcronyms[ $section ]['a_type'] )
{
case 'acronym':
$parent = $parent->appendChild( new \DOMElement( 'abbr' ) );
$parent->setAttribute( 'title', $this->caseSensitiveAcronyms[ $section ]['a_long'] );
break;
case 'link':
$replace = ( $parent->tagName != 'a' );
$parentNode = $parent;
while( ( $parentNode = $parentNode->parentNode ) !== NULL )
{
if( $parentNode instanceof \DOMElement AND $parentNode->tagName == 'a' )
{
$replace = FALSE;
break;
}
}
if( $replace )
{
$parent = $parent->appendChild( new \DOMElement( 'a' ) );
$parent->setAttribute( 'href', $this->caseSensitiveAcronyms[ $section ]['a_long'] );
try
{
$rels = $this->_getRelAttributes( \IPS\Http\Url::createFromString( $this->caseSensitiveAcronyms[ $section ]['a_long'] ) );
}
catch( \IPS\Http\Url\Exception $e )
{
$rels = array();
}
/* Add rels */
$parent->setAttribute( 'rel', implode( ' ', $rels ) );
}
break;
}
}
elseif ( $this->bbcodeParse and array_key_exists( mb_strtolower( $section ), $this->caseInsensitiveAcronyms ) and !\in_array( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ], $this->openAbbrTags ) )
{
switch( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_type'] )
{
case 'acronym':
$parent = $parent->appendChild( new \DOMElement( 'abbr' ) );
$parent->setAttribute( 'title', $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_long'] );
break;
case 'link':
$replace = ( $parent->tagName != 'a' );
$parentNode = $parent;
while( ( $parentNode = $parentNode->parentNode ) !== NULL )
{
if( $parentNode instanceof \DOMElement AND $parentNode->tagName == 'a' )
{
$replace = FALSE;
break;
}
}
if( $replace )
{
$parent = $parent->appendChild( new \DOMElement( 'a' ) );
$parent->setAttribute( 'href', $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_long'] );
try
{
$rels = $this->_getRelAttributes( \IPS\Http\Url::createFromString( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_long'] ) );
}
catch( \IPS\Http\Url\Exception $e )
{
$rels = array();
}
/* Add rels */
$parent->setAttribute( 'rel', implode( ' ', $rels ) );
}
break;
}
}
/* Emoji? */
if ( ( !$originalParent->getAttribute('class') or !\in_array( 'ipsEmoji', explode( ' ', $originalParent->getAttribute('class') ) ) ) and preg_match( '/' . static::EMOJI_REGEX . '/u', $section ) )
{
$parent = $parent->appendChild( new \DOMElement( 'span' ) );
$parent->setAttribute( 'class', 'ipsEmoji' );
}
/* Check for emails */
if ( \IPS\Settings::i()->email_filter_action == 'replace' and preg_match( '/' . static::EMAIL_REGEX . '/u', $section ) )
{
$section = preg_replace( '/' . static::EMAIL_REGEX . '/u', \IPS\Settings::i()->email_filter_replace_text, $section );
}
/* Insert the text */
$this->_insertNodeApplyingInlineBBcode( new \DOMText( $section ), $parent );
/* Restore the parent */
$parent = $originalParent;
}
}
/**
* Open BBCode tag
*
* @param string $tag The tag (e.g. "b")
* @param string|NULL $option If an option was provided (e.g. "[foo=bar]"), it's value
* @param \DOMNode $parent The node from the new document which will be this node's parent - passed by reference and may be modified for siblings
* @param int $sectionId The position of this section out of all the sections in the node - used to indicate if there's text before/after this section
* @param int $sectionCount The total number of sections in the node - used to indicate if there's text before/after this section
* @return void
*/
protected function _openBBCode( $tag, $option, \DOMNode &$parent, $sectionId, $sectionCount )
{
/* Get definiton */
$bbcode = $this->bbcode[ $tag ];
/* Get the document */
$document = $parent->ownerDocument ?: $parent;
/* Create the element */
$bbcodeElement = $document->createElement( $bbcode['tag'] );
/* Add any attributes */
if ( isset( $bbcode['attributes'] ) )
{
foreach ( $bbcode['attributes'] as $k => $v )
{
$bbcodeElement->setAttribute( $k, str_replace( '{option}', ( $option ?: ( isset( $bbcode['defaultOption'] ) ? $bbcode['defaultOption'] : '' ) ), $v ) );
}
}
/* Callback */
if ( isset( $bbcode['callback'] ) )
{
$callback = $bbcode['callback'];
$bbcodeElement = $callback( $bbcodeElement, array( 2 => $option ), $document );
}
/* Stop parsing? ([code] blocks make it so BBCode isn't parsed inside them) */
if ( isset( $bbcode['noParse'] ) and $bbcode['noParse'] )
{
$this->bbcodeParse = FALSE;
$this->resumeBBCodeParsingOn = "[/{$tag}]";
}
/* Parse it */
$bbcodeElement = $this->_parseElement( $bbcodeElement );
/* Single only? */
if ( isset( $bbcode['single'] ) and $bbcode['single'] )
{
$this->_insertNodeApplyingInlineBBcode( $bbcodeElement, $parent );
}
/* Or with content? */
else
{
/* Block level? */
if ( isset( $bbcode['block'] ) and $bbcode['block'] )
{
/* Insert the block level element */
$lastOpennedBlockId = NULL;
if ( !empty( $this->openBlockBBCodeInOrder ) )
{
$openBBCodeBlocks = array_keys( $this->openBlockBBCodeInOrder );
$lastOpennedBlockId = array_pop( $openBBCodeBlocks );
}
if ( $lastOpennedBlockId and list( $id, $tagName ) = explode( '-', $lastOpennedBlockId ) and isset( $this->bbcode[ $tagName ]['noChildren'] ) and $this->bbcode[ $tagName ]['noChildren'] )
{
$parent->appendChild( $bbcodeElement );
}
else
{
$parent->parentNode->appendChild( $bbcodeElement );
}
/* Callback */
$blockElement = $bbcodeElement;
if ( isset( $bbcode['getBlockContentElement'] ) )
{
$callback = $bbcode['getBlockContentElement'];
$blockElement = $callback( $bbcodeElement );
}
/* Create an element of the same type (normally element
*
* @param \DOMElement $element The element
* @return bool
*/
protected function _parseImgElement( \DOMElement $element )
{
/* When editing content in the AdminCP, images and iframes get the src munged. When we save, we need to put that back */
$this->_removeMunge( $element );
/* Lazy-loaded? */
$srcAttribute = ( $element->hasAttribute( 'src' ) AND $element->hasAttribute( 'data-src' ) ) ? 'data-src' : 'src';
/* Is it an emoji? */
if ( $element->getAttribute('class') and \in_array( 'ipsEmoji', explode( ' ', $element->getAttribute('class') ) ) and $element->getAttribute('alt') )
{
$newElement = $element->ownerDocument->importNode( new \DOMElement( 'span' ) );
$newElement->setAttribute( 'class', 'ipsEmoji' );
$newElement->appendChild( new \DOMText( $element->getAttribute('alt') ) );
return $newElement;
}
/* If it's not allowed, remove the src */
try
{
static::isAllowedImageUrl( $element->getAttribute( $srcAttribute ) );
}
catch( \UnexpectedValueException $e )
{
$newElement = $element->ownerDocument->importNode( new \DOMElement( 'span' ) );
$newElement->appendChild( new \DOMText( $element->getAttribute( $srcAttribute ) ) );
return $newElement;
}
/* Is it an emoticon? */
if ( $element->hasAttribute('data-emoticon') )
{
if ( $this->_emoticons < 75 )
{
if ( !isset( static::$fileObjectClasses['core_Emoticons'] ) )
{
static::$fileObjectClasses['core_Emoticons'] = \IPS\File::getClass('core_Emoticons' );
}
$element->setAttribute( 'src', str_replace( array( 'http:', 'https:' ), '', str_replace( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '{fileStore.core_Emoticons}', $element->getAttribute('src') ) ) );
if ( $srcSet = $element->getAttribute('srcset') )
{
$element->setAttribute( 'srcset', str_replace( array( 'http:', 'https:' ), '', str_replace( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '%7BfileStore.core_Emoticons%7D', $srcSet ) ) );
}
$this->_emoticons++;
}
else
{
/* Set an attribute on the element - we'll need to know this later */
$element->setAttribute( 'data-ipsEmoticon-plain', $element->getAttribute('title') );
}
}
/* Or an attachment? */
elseif ( $attachment = static::_getAttachment( $element->getAttribute($srcAttribute ), $element->hasAttribute('data-fileid') ? $element->getAttribute('data-fileid') : NULL ) )
{
$file = $this->_getFile( 'core_Attachment', $element->getAttribute( $srcAttribute ) );
$element->setAttribute( 'data-fileid', $attachment['attach_id'] );
$element->setAttribute( $srcAttribute, str_replace( array( 'http:', 'https:' ), '', str_replace( static::$fileObjectClasses['core_Attachment']->baseUrl(), '{fileStore.core_Attachment}', $element->getAttribute( $srcAttribute ) ) ) );
if ( ( !$element->hasAttribute('alt') or $element->getAttribute('alt') == $file->filename ) and ! empty( $attachment['attach_labels'] ) and $labels = static::getAttachmentLabels( $attachment ) )
{
if ( \count( $labels ) )
{
if ( $this->_altLabelWord === NULL )
{
$this->_altLabelWord = \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'alt_label_could_be' );
}
$element->setAttribute( 'alt', $this->_altLabelWord . ' ' . implode( ', ', $labels ) );
}
}
if ( !$element->getAttribute('alt') )
{
$element->setAttribute( 'alt', $attachment['attach_file'] );
}
$this->_logAttachment( $attachment );
}
/* Or some other media? */
elseif ( $element->getAttribute('data-extension') and $file = $this->_getFile( $element->getAttribute('data-extension'), $element->getAttribute( $srcAttribute ) ) )
{
$element->setAttribute( $srcAttribute, '{fileStore.' . $file->storageExtension . '}/' . (string) $file );
if ( !$element->getAttribute('alt') )
{
$element->setAttribute( 'alt', $file->originalFilename );
}
}
/* Nope, regular image */
else
{
/* We need an alt (HTMLPurifier handles this normally, but it may not always run) */
if ( !$element->getAttribute('alt') )
{
$element->setAttribute( 'alt', mb_substr( basename( $element->getAttribute('src') ), 0, 40 ) );
}
}
/* If this isn't an emoticon, replace the src and add attributes to enable lazy loading */
if ( !$element->hasAttribute('data-emoticon') )
{
/* If this image already has a data-src, we need to use that because it's the
original image url. `src` will likely contain our base64 blank image instead.
We need to do this regardless of whether lazy-loading is enabled now, for
situations where content has data-src, but lazy loading has been disabled since,
without a post rebuild. In that case, we'll want to swap the data-src for a real src. */
$imgSrc = $element->hasAttribute('data-src') ? $element->getAttribute( 'data-src' ) : $element->getAttribute( 'src' );
$element->removeAttribute( 'data-src' );
$element->removeAttribute( 'src' );
if ( \IPS\Settings::i()->lazy_load_enabled )
{
$element->setAttribute( 'data-src', $imgSrc );
$element->setAttribute( 'src', static::blankImage() ); // A blank pixel
}
else
{
$element->setAttribute( 'src', $imgSrc );
}
// We might still have this from the editor, but it'll stop lazy-load from running
// Ensure this is removed regardless of whether lazy loading is enabled right now
$element->removeAttribute( 'data-loaded' );
}
return $element;
}
/**
* Parse
\t" so also strip any whitespace after a break CKEditor doesn't actually have a way to indent individual lines */ if ( ( $previousSibling = $textNode->previousSibling and $previousSibling instanceof \DOMElement and $previousSibling->tagName == 'br' ) or ( $textNode->parentNode instanceof \DOMElement and $textNode->parentNode->tagName == 'p' and $textNode->parentNode->lastChild and $textNode->parentNode->lastChild instanceof \DOMElement and $textNode->parentNode->lastChild->tagName == 'br' ) ) { $text = preg_replace( '/^\s/', '', $text ); } /* Insert */ $parent->appendChild( new \DOMText( $text ) ); } ) ), 21, -6 ); /* Create a new
with those contents */
$return = $originalNode->ownerDocument->createElement( 'pre' );
$return->appendChild( new \DOMText( html_entity_decode( $contents ) ) ); // We have to decode HTML entities otherwise they'll be double-encoded. Test with "[code]Test[/code]"
$return->setAttribute( 'class', 'ipsCode' );
return $return;
} );
$return['code'] = $code;
$return['codebox'] = $code;
$return['html'] = $code;
$return['php'] = $code;
$return['sql'] = $code;
$return['xml'] = $code;
}
/* Color */
if ( static::canUse( $member, 'TextColor', $area ) )
{
$return['color'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'color:{option}' ), 'allowOption' => TRUE );
}
/* Font */
if ( static::canUse( $member, 'Font', $area ) )
{
$return['font'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'font-family:{option}' ), 'allowOption' => TRUE );
}
/* HR */
$return['hr'] = array( 'tag' => 'hr', 'single' => TRUE, 'allowOption' => FALSE );
/* Image */
if ( static::canUse( $member, 'ipsImage', $area ) )
{
$return['img'] = array( 'tag' => 'img', 'attributes' => array( 'src' => '{option}', 'class' => 'ipsImage' ), 'single' => TRUE, 'allowOption' => TRUE );
}
/* Indent */
if ( static::canUse( $member, 'Indent', $area ) )
{
$return['indent'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'margin-left:{option}px' ), 'block' => FALSE, 'defaultOption' => 25, 'allowOption' => TRUE );
}
/* Italics */
if ( static::canUse( $member, 'Italic', $area ) )
{
$return['i'] = array( 'tag' => 'em', 'allowOption' => FALSE );
}
/* Justify */
if ( static::canUse( $member, 'JustifyLeft', $area ) )
{
$return['left'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'text-align:left' ), 'block' => TRUE, 'allowOption' => FALSE );
}
if ( static::canUse( $member, 'JustifyCenter', $area ) )
{
$return['center'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'text-align:center' ), 'block' => TRUE, 'allowOption' => FALSE );
}
if ( static::canUse( $member, 'JustifyRight', $area ) )
{
$return['right'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'text-align:right' ), 'block' => TRUE, 'allowOption' => FALSE );
}
/* Links */
if ( static::canUse( $member, 'ipsLink', $area ) )
{
/* Email */
$return['email'] = array( 'tag' => 'a', 'attributes' => array( 'href' => 'mailto:{option}' ), 'allowOption' => TRUE );
/* Member */
$return['member'] = array(
'tag' => 'a',
'attributes'=> array( 'contenteditable' => 'false', 'data-ipsHover' => '' ),
'callback' => function( \DOMElement $node, $matches, \DOMDocument $document )
{
try
{
$member = \IPS\Member::load( $matches[2], 'name' );
if ( $member->member_id != 0 )
{
$node->setAttribute( 'href', $member->url() );
$node->setAttribute( 'data-ipsHover-target', $member->url()->setQueryString( 'do', 'hovercard' ) );
$node->setAttribute( 'data-mentionid', $member->member_id );
$node->appendChild( $document->createTextNode( '@' . $member->name ) );
}
}
catch ( \Exception $e ) {}
return $node;
},
'single' => TRUE,
'allowOption' => TRUE
);
/* Links */
$return['url'] = array( 'tag' => 'a', 'attributes' => array( 'href' => '{option}' ), 'allowOption' => TRUE );
}
/* List */
if ( static::canUse( $member, 'BulletedList', $area ) or static::canUse( $member, 'NumberedList', $area ) )
{
$return['list'] = array(
'tag' => 'ul',
'callback' => function( $node, $matches, $document )
{
/* Set the main attributes for our element */ $node->setAttribute( 'class', 'ipsQuote' ); $node->setAttribute( 'data-ipsQuote', '' ); if ( isset( $options['name'] ) and $options['name'] ) { $node->setAttribute( 'data-ipsQuote-username', $options['name'] ); } if ( isset( $options['date'] ) and $options['date'] ) { $node->setAttribute( 'data-ipsQuote-timestamp', strtotime( $options['date'] ) ); } if ( \IPS\Application::appIsEnabled('forums') and isset( $options['post'] ) and $options['post'] ) { try { $post = \IPS\forums\Topic\Post::load( $options['post'] ); $node->setAttribute( 'data-ipsQuote-contentapp', 'forums' ); $node->setAttribute( 'data-ipsQuote-contenttype', 'forums' ); $node->setAttribute( 'data-ipsQuote-contentclass', 'forums_Topic' ); $node->setAttribute( 'data-ipsQuote-contentid', $post->item()->tid ); $node->setAttribute( 'data-ipsQuote-contentcommentid', $post->pid ); } catch ( \OutOfRangeException $e ) {} } /* Create the citation element */ $citation = $document->createElement('div'); $citation->setAttribute( 'class', 'ipsQuote_citation' ); $node->appendChild( $citation ); /* Create the content element */ $contents = $document->createElement('div'); $contents->setAttribute( 'class', 'ipsQuote_contents ipsClearfix' ); $node->appendChild( $contents ); return $node; }, 'getBlockContentElement' => function( \DOMElement $node ) { foreach ( $node->childNodes as $child ) { if ( $child instanceof \DOMElement and mb_strpos( $child->getAttribute('class'), 'ipsQuote_contents' ) !== FALSE ) { return $child; } } return $node; }, 'block' => TRUE, 'allowOption' => TRUE ); } /* Size */ if ( static::canUse( $member, 'FontSize', $area ) ) { $return['size'] = array( 'tag' => 'span', 'callback' => function( $node, $matches ) { switch ( $matches[2] ) { case 1: $node->setAttribute( 'style', 'font-size:8px' ); break; case 2: $node->setAttribute( 'style', 'font-size:10px' ); break; case 3: $node->setAttribute( 'style', 'font-size:12px' ); break; case 4: $node->setAttribute( 'style', 'font-size:14px' ); break; case 5: $node->setAttribute( 'style', 'font-size:18px' ); break; case 6: $node->setAttribute( 'style', 'font-size:24px' ); break; case 7: $node->setAttribute( 'style', 'font-size:36px' ); break; case 8: $node->setAttribute( 'style', 'font-size:48px' ); break; } return $node; }, 'allowOption' => TRUE ); } /* Spoiler */ if ( static::canUse( $member, 'ipsSpoiler', $area ) ) { $return['spoiler'] = array( 'tag' => 'div', 'callback' => function( \DOMElement $node, $matches, \DOMDocument $document ) { /* Set the main attributes for ourelement */ $node->setAttribute( 'class', 'ipsSpoiler' ); $node->setAttribute( 'data-ipsSpoiler', '' ); /* Create the citation element */ $header = $document->createElement('div'); $header->setAttribute( 'class', 'ipsSpoiler_header' ); $node->appendChild( $header ); $headerSpan = $document->createElement('span'); $header->appendChild( $headerSpan ); /* Create the content element */ $contents = $document->createElement('div'); $contents->setAttribute( 'class', 'ipsSpoiler_contents' ); $node->appendChild( $contents ); return $node; }, 'getBlockContentElement' => function( \DOMElement $node ) { foreach ( $node->childNodes as $child ) { if ( $child instanceof \DOMElement and $child->getAttribute('class') == 'ipsSpoiler_contents' ) { return $child; } } return $node; }, 'block' => TRUE, 'allowOption' => FALSE ); } /* Strike */ if ( static::canUse( $member, 'Strike', $area ) ) { $return['s'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'text-decoration:line-through' ), 'allowOption' => FALSE ); $return['strike'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'text-decoration:line-through' ), 'allowOption' => FALSE ); } /* Subscript */ if ( static::canUse( $member, 'Subscript', $area ) ) { $return['sub'] = array( 'tag' => 'sub', 'allowOption' => FALSE ); } /* Superscript */ if ( static::canUse( $member, 'Superscript', $area ) ) { $return['sup'] = array( 'tag' => 'sup', 'allowOption' => FALSE ); } /* Underline */ if ( static::canUse( $member, 'Underline', $area ) ) { $return['u'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'text-decoration:underline' ), 'allowOption' => FALSE ); } /* App-Specific */ foreach ( \IPS\Application::allExtensions( 'core', 'BBCode', $member ) as $key => $bbcode ) { if ( $bbcode->permissionCheck( $member, $area ) ) { [ $app, $tag ] = explode( '_', $key ); $return[ $tag ] = $bbcode->getConfiguration(); } } return $return; } /** * @brief Cached permissions */ protected static $permissions = array(); /** * Can use plugin? * * @param \IPS\Member $member The member * @param string $key Plugin key * @param string $area The Editor area * @return bool */ public static function canUse( \IPS\Member $member, $key, $area ) { $permissionSettings = json_decode( \IPS\Settings::i()->ckeditor_permissions, TRUE ); if ( !isset( static::$permissions[ $member->member_id ][ $key ] ) ) { if ( !isset( $permissionSettings[ $key ] ) ) { static::$permissions[ $member->member_id ][ $key ] = TRUE; } else { $val = TRUE; if ( $permissionSettings[ $key ]['groups'] !== '*' ) { if ( !$member->inGroup( $permissionSettings[ $key ]['groups'] ) ) { $val = FALSE; } } if ( $permissionSettings[ $key ]['areas'] !== '*' ) { if ( !\in_array( $area, $permissionSettings[ $key ]['areas'] ) ) { $val = FALSE; } } static::$permissions[ $member->member_id ][ $key ] = $val; } } return static::$permissions[ $member->member_id ][ $key ]; } /** * Parse content looking for a separation tag * Used for lists where [*] breaks and whole content where [page] breaks * * @param \DOMNode $originalNode The node containing the content we want to examine * @param callback $mainElementCreator A callback which returns a \DOMElement to be the main element. Is passed \DOMDocument as a parameter * @param callback $subElementCreator A callback which returns a \DOMElement to be a new sub element. Is passed \DOMDocument as a parameter * @return \DOMElement */ protected function _parseContentWithSeparationTag( \DOMNode $originalNode, $mainElementCreator, $subElementCreator, $separator ) { /* Create a copy of the node */ $workingDocument = new \DOMDocument; $workingNode = $workingDocument->importNode( $originalNode, TRUE ); /* Create a freshwith a single
- inside */ $mainElement = $mainElementCreator( $originalNode->ownerDocument ); $currentSubElement = $mainElement->appendChild( $subElementCreator( $originalNode->ownerDocument ) ); /* Parse */ $ignoreNextSeparator = TRUE; foreach ( $workingNode->childNodes as $node ) { $this->_parseContentWithSeparationTagLoop( $mainElement, $currentSubElement, $separator, $subElementCreator, $node, $mainElement, $ignoreNextSeparator ); } /* Return */ return $mainElement; } /** * Loop for _parseContentWithSeparationTag * * @param \DOMElement $mainElement The main element all of our content is going into * @param \DOMElement $currentSubElement The current sub element that nodes go into. When a $separator is detected, a new one is created * @param string $separator The separator, e.g. "[*]" or "[page]" * @param callback $subElementCreator A callback which returns a \DOMElement to be a new sub element * @param \DOMNode $node The current node we're examining * @param \DOMNode $parent The parent of the current node we're examining * @param bool $ignoreNextSeparator If we have [list][*]Foo[/list] We want to ignore the first [*] so we don't end up with
', ' ', $text ); /* Remove all HTML apart from- this keeps track of that * @return void */ protected function _parseContentWithSeparationTagLoop( \DOMElement $mainElement, \DOMElement &$currentSubElement, $separator, $subElementCreator, \DOMNode $node, \DOMNode &$parent, &$ignoreNextSeparator ) { /* If the node is an element... */ if ( $node->nodeType === XML_ELEMENT_NODE ) { /* Ignore any preceeding
- Foo
s */ if ( $ignoreNextSeparator and $node->tagName == 'br' ) { return; } /* Import and insert */ $newElement = $mainElement->ownerDocument->importNode( $node ); if ( $parent->isSameNode( $mainElement ) ) { $currentSubElement->appendChild( $newElement ); } else { $parent->appendChild( $newElement ); } /* Loop children */ foreach ( $node->childNodes as $child ) { $this->_parseContentWithSeparationTagLoop( $mainElement, $currentSubElement, $separator, $subElementCreator, $child, $newElement, $ignoreNextSeparator ); } } /* Or if it's text... */ elseif ( $node->nodeType === XML_TEXT_NODE ) { /* Ignore any closing tag */ $text = $node->wholeText; $text = str_replace( preg_replace( '/\[(.+?)\]/', '[/$1]', $separator ), '', $text ); /* Break it up where we find the separator... */ foreach ( array_filter( preg_split( '/(' . preg_quote( $separator, '/' ) . ')/', $text, null, PREG_SPLIT_DELIM_CAPTURE ), 'trim' ) as $textSection ) { /* If this section the separator... */ if ( $textSection === $separator ) { /* Unless we're ignoring it, create a new element... */ if ( !$ignoreNextSeparator ) { /* Strip any extrenous
from the last one */ if ( $currentSubElement->lastChild and $currentSubElement->lastChild->nodeType === XML_ELEMENT_NODE and $currentSubElement->lastChild->tagName == 'br' ) { $currentSubElement->removeChild( $currentSubElement->lastChild ); } /* Create a new one */ $currentSubElement = $mainElement->appendChild( $subElementCreator( $mainElement->ownerDocument ) ); if ( !$parent->isSameNode( $mainElement ) ) { $parent = $parent->cloneNode( FALSE ); $currentSubElement->appendChild( $parent ); } } /* Or if we are, then don't ignore the next one */ else { $ignoreNextSeparator = FALSE; } } else { /* Insert */ if ( $parent->isSameNode( $mainElement ) ) { $currentSubElement->appendChild( new \DOMText( $textSection ) ); } else { $parent->appendChild( new \DOMText( $textSection ) ); } /* If we're meant to be ignoring the next separator, but we have found content before it, remove that flag */ if ( $ignoreNextSeparator ) { $ignoreNextSeparator = FALSE; } } } } } /* !Embeddable Media */ /** * Get OEmbed Services * Implemented in this way so it's easy for hook authors to override if they wanted to * * @see oEmbed * @return array */ protected static function oembedServices() { $services = array( 'youtube.com' => array( 'https://www.youtube.com/oembed', static::EMBED_VIDEO ), 'm.youtube.com' => array( 'https://www.youtube.com/oembed', static::EMBED_VIDEO ), 'youtu.be' => array( 'https://www.youtube.com/oembed', static::EMBED_VIDEO ), 'flickr.com' => array( 'https://www.flickr.com/services/oembed/', static::EMBED_IMAGE ), 'flic.kr' => array( 'https://www.flickr.com/services/oembed/', static::EMBED_IMAGE ), 'hulu.com' => array( 'http://www.hulu.com/api/oembed.json', static::EMBED_VIDEO ), 'vimeo.com' => array( 'https://vimeo.com/api/oembed.json', static::EMBED_VIDEO ), 'collegehumor.com' => array( 'http://www.collegehumor.com/oembed.json', static::EMBED_VIDEO ), 'twitter.com' => array( 'https://publish.twitter.com/oembed', static::EMBED_TWEET ), 'mobile.twitter.com' => array( 'https://publish.twitter.com/oembed', static::EMBED_TWEET ), 'x.com' => array( 'https://publish.x.com/oembed', static::EMBED_TWEET ), 'x.twitter.com' => array( 'https://publish.x.com/oembed', static::EMBED_TWEET ), 'soundcloud.com' => array( 'https://soundcloud.com/oembed', static::EMBED_VIDEO ), 'open.spotify.com' => array( 'https://embed.spotify.com/oembed', static::EMBED_VIDEO ), 'play.spotify.com' => array( 'https://embed.spotify.com/oembed', static::EMBED_VIDEO ), 'ted.com' => array( 'https://www.ted.com/services/v1/oembed', static::EMBED_VIDEO ), 'vine.co' => array( 'https://vine.co/oembed.json', static::EMBED_VIDEO ), 'dailymotion.com' => array( 'https://www.dailymotion.com/services/oembed', static::EMBED_VIDEO ), 'dai.ly' => array( 'https://www.dailymotion.com/services/oembed', static::EMBED_VIDEO ), 'coub.com' => array( 'http://coub.com/api/oembed.json' . ( \IPS\Settings::i()->max_video_width ? ( '?maxwidth=' . \IPS\Settings::i()->max_video_width ) : '' ), static::EMBED_VIDEO ), '*.deviantart.com' => array( 'https://backend.deviantart.com/oembed', static::EMBED_IMAGE ), 'docs.com' => array( 'http://docs.com/api/oembed', static::EMBED_LINK ), 'funnyordie.com' => array( 'http://www.funnyordie.com/oembed.json', static::EMBED_VIDEO ), 'gettyimages.com' => array( 'http://embed.gettyimages.com/oembed', static::EMBED_IMAGE ), 'ifixit.com' => array( 'http://www.ifixit.com/Embed', static::EMBED_LINK ), 'kickstarter.com' => array( 'http://www.kickstarter.com/services/oembed', static::EMBED_LINK ), 'meetup.com' => array( 'https://api.meetup.com/oembed', static::EMBED_LINK ), 'mixcloud.com' => array( 'https://www.mixcloud.com/oembed/', static::EMBED_VIDEO ), 'mix.office.com' => array( 'https://mix.office.com/oembed', static::EMBED_VIDEO ), 'reddit.com' => array( 'http://www.reddit.com/oembed', static::EMBED_LINK ), 'reverbnation.com' => array( 'https://www.reverbnation.com/oembed', static::EMBED_VIDEO ), 'screencast.com' => array( 'https://api.screencast.com/external/oembed', static::EMBED_IMAGE ), 'slideshare.net' => array( 'http://www.slideshare.net/api/oembed/2', static::EMBED_VIDEO ), '*.smugmug.com' => array( 'http://api.smugmug.com/services/oembed', static::EMBED_IMAGE ), 'ustream.tv' => array( 'https://www.ustream.tv/oembed', static::EMBED_VIDEO ), '*.wistia.com' => array( 'http://fast.wistia.com/oembed', static::EMBED_VIDEO ), '*.wi.st' => array( 'http://fast.wistia.com/oembed', static::EMBED_VIDEO ), 'tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ), 'm.tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ), 'vm.tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ), 'bsky.app' => [ 'https://embed.bsky.app/oembed', static::EMBED_TWEET ] ); /* Can we support Facebook and Instagram oembeds? */ if( $token = static::getFacebookToken() ) { $services['instagram.com'] = array( array( 'https://graph.facebook.com/v9.0/instagram_oembed?access_token=' . $token, static::EMBED_IMAGE ) ); $services['instagr.am'] = $services['instagram.com']; $services['facebook.com'] = array( '(\/.+?\/videos\/|video.php)' => array( 'https://graph.facebook.com/v9.0/oembed_video?access_token=' . $token, static::EMBED_VIDEO ), '(\/.+?\/posts\/|\/.+?\/activity\/|\/.+?\/photos?\/|photo.php|\/.+?\/media\/|permalink.php|\/.+?\/questions\/|\/.+?\/notes\/|\/.+?\/media\/)' => array( 'https://graph.facebook.com/v9.0/oembed_post?access_token=' . $token, static::EMBED_STATUS ), 0 => array( 'https://graph.facebook.com/v9.0/oembed_page?access_token=' . $token, static::EMBED_STATUS ), ); } return $services; } /** * Get an application token from Facebook for oembed support * * @return NULL|string */ protected static function getFacebookToken() { try { if( !\IPS\Settings::i()->fb_ig_oembed_token AND \IPS\Settings::i()->fb_ig_oembed_appid AND \IPS\Settings::i()->fb_ig_oembed_appsecret ) { $value = \IPS\Http\Url::external( "https://graph.facebook.com/oauth/access_token?client_id=" . \IPS\Settings::i()->fb_ig_oembed_appid . "&client_secret=" . \IPS\Settings::i()->fb_ig_oembed_appsecret . "&grant_type=client_credentials" )->request()->get()->decodeJson(); if( !empty( $value['access_token'] ) ) { \IPS\Settings::i()->changeValues( array( 'fb_ig_oembed_token' => $value['access_token'] ) ); } else { \IPS\Log::debug( "Facebook access token could not be retrieved for oembed:\n" . var_export( $value, true ), 'facebook_oembed' ); } } } catch( \Exception $e ) { \IPS\Log::debug( $e, 'facebook_oembed' ); } return \IPS\Settings::i()->fb_ig_oembed_token ? \IPS\Settings::i()->fb_ig_oembed_token : NULL; } /** * @brief External link request timeout */ public static $requestTimeout = \IPS\DEFAULT_REQUEST_TIMEOUT; /** * Convert URL to embed HTML * * @param \IPS\Http\Url $url URL * @param bool $iframe Some services need to be in iFrames so they cannot be edited in the editor. If TRUE, will return contents for iframe, if FALSE, return the iframe. * @param \IPS\Member|NULL $member Member to check permissions against or NULL for currently logged in member * @return string|null HTML embded code, or NULL if URL is not embeddable */ public static function embeddableMedia( $url, $iframe=FALSE, $member=NULL ) { /* Internal */ if ( $url instanceof \IPS\Http\Url\Internal ) { /* Internal Embed */ if ( $embedCode = static::_internalEmbed( $url, $iframe, $member ) ) { return $embedCode; } } /* External */ else { /* oEmbed? */ if ( $embedCode = static::_oembedEmbed( $url, $iframe ) ) { return $embedCode; } /* Other services */ if ( $embedCode = static::_customEmbed( $url, $iframe ) ) { return $embedCode; } } /* Still here? It's not embeddable */ return NULL; } /** * @brief Embed type constants */ const EMBED_IMAGE = 1; const EMBED_VIDEO = 2; const EMBED_STATUS = 3; const EMBED_TWEET = 4; const EMBED_LINK = 5; /** * oEmbed Embed Code * * @param \IPS\Http\Url $url URL * @param bool $iframe Some services need to be in iFrames so they cannot be edited in the editor. If TRUE, will return contents for iframe, if FALSE, return the iframe. * @param int $attemptNumber The attempt number, useful for automatically retrying a service * @return string|null */ protected static function _oembedEmbed( \IPS\Http\Url $url, $iframe=FALSE, $attemptNumber=1 ) { /* Strip the "www." from the domain */ $domain = $url->data['host']; if ( mb_substr( $domain, 0, 4 ) === 'www.' ) { $domain = mb_substr( $domain, 4 ); } if( !$domain ) { return null; } /* TikTok does not support oembed for their short links, but will redirect to the full URL which does */ if( $domain == 'vm.tiktok.com' ) { try { $response = $url->request( null, null, 0 )->get(); if( $response->httpResponseCode == 301 ) { $url = \IPS\Http\Url::external( $response->httpHeaders['Location'] )->stripQueryString(); $domain = $url->data['host']; } else { throw new \IPS\Http\Request\Exception; } } catch( \IPS\Http\Request\Exception $e ) { throw new \UnexpectedValueException( 'embed__fail_500', (float) static::EMBED_VIDEO ); } } /* TikTok doesn't return logical response codes for invalid URLs */ if( $domain == 'tiktok.com' AND mb_strpos( $url->data['path'], '/video/' ) === FALSE ) { return NULL; } elseif( $domain == 'm.tiktok.com' AND mb_strpos( $url->data['path'], '/v/' ) === FALSE ) { return NULL; } /* Get oEmbed Services */ $oembedServices = static::oembedServices(); /* If the URL's domain is in the list... */ $entry = NULL; $entryKey = NULL; if ( array_key_exists( $domain, $oembedServices ) ) { $entry = $oembedServices[ $domain ]; $entryKey = $domain; } else { foreach( $oembedServices as $k => $v ) { if( mb_strpos( $k, '*' ) !== FALSE ) { if( preg_match( "/^" . str_replace( array( '.', '*' ), array( '\\.', '.*?' ), $k ) . "$/i", $domain ) ) { $entry = $v; $entryKey = $k; } } } } if( $entry ) { $endtype = static::EMBED_LINK; $endpoint = NULL; /* If we have multiple possible ones, find the best */ if ( \is_array( $entry[0] ) ) { foreach ( $entry as $regex => $endpoints ) { if ( \is_string( $regex ) and preg_match( $regex, $url ) ) { $endpoint = $endpoints[0]; $endtype = $endpoints[1]; break; } } if( $endpoint === NULL ) { $endpoint = $entry[0][0]; $endtype = $entry[0][1]; } } else { $endpoint = $entry[0]; $endtype = $entry[1]; } /* Youtube shorts feature doesn't return a valid response in its own oembed API so we need to tweak it */ if ( ( $domain == 'youtube.com' or $domain == 'm.youtube.com' or $domain == 'youtu.be' ) and mb_strpos( $url->data['path'], '/shorts/' ) !== FALSE) { $url->queryString['v'] = preg_replace( '#/shorts/(\w+?)#', '$1', $url->data['path'] ); $url->data['path'] = '/watch'; } /* Call oEmbed Service */ try { $language = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->bcp47(); $response = \IPS\Http\Url::external( $endpoint ) ->setQueryString( array( 'format' => 'json', 'url' => (string) $url->stripQueryString( 'autoplay' ), 'scheme' => ( $url->data[ \IPS\Http\Url::COMPONENT_SCHEME ] === 'https' or \IPS\Request::i()->isSecure() ) ? 'https' : null ) ) ->request( static::$requestTimeout ) ->setHeaders( array( 'Accept-Language' => $language ) ) ->get(); if( $response->httpResponseCode != '200' ) { /* If this is FB/IG we can reattempt one more time in case the stored access token was incorrect or expired */ if( \in_array( $entryKey, array( 'instagram.com', 'instagr.am', 'facebook.com' ) ) AND $attemptNumber < 2 ) { \IPS\Settings::i()->changeValues( array( 'fb_ig_oembed_token' => '' ) ); return static::_oembedEmbed( $url, $iframe, 2 ); } switch( $response->httpResponseCode ) { case '404': throw new \UnexpectedValueException( 'embed__fail_404', (float) $endtype ); break; case '401': case '403': throw new \UnexpectedValueException( 'embed__fail_403', (float) $endtype ); break; default: throw new \UnexpectedValueException( 'embed__fail_500', (float) $endtype ); break; } } $response = $response->decodeJson(); } /* If it error'd (connection error or unexpected response), we'll not embed this */ catch ( \IPS\Http\Request\Exception $e ) { throw new \UnexpectedValueException( 'embed__fail_500', (float) $endtype ); } /* Flickr when used in the video template is a bit quirky. Requires rich. */ if ( $domain == 'flickr.com' OR $domain == 'flic.kr' ) { $response['type'] = ( $response['type'] == 'video' ) ? 'rich' : 'photo'; /* If we embed an album, no 'url' is returned but a thumbnail URL is so use that but stick with "photo" as the template. */ if( $response['type'] == 'photo' AND !isset( $response['url'] ) AND isset( $response['thumbnail_url'] ) ) { $response['url'] = $response['thumbnail_url']; } } /* Coub returns 'video' as the type, but points to an iframe with the video embedded, so switch to rich */ if( $domain == 'coub.com' ) { $response['type'] = ( $response['type'] == 'video' ) ? 'rich' : $response['type']; } /* Coub returns 'video' as the type, but points to an iframe with the video embedded, so switch to rich */ if( $domain == 'coub.com' ) { $response['type'] = ( $response['type'] == 'video' ) ? 'rich' : $response['type']; } /* For Youtube, we need to be a bit hacky to pass rel=0 */ if ( $domain == 'youtube.com' or $domain == 'm.youtube.com' or $domain == 'youtu.be' ) { /* YouTube has no way to make it return youtube-nocookie from the oEmbed endpoint, so we need to swap it manually. */ if ( mb_strpos( $response['html'], 'youtube-nocookie.com/embed' ) === FALSE ) { $response['html'] = str_replace( 'youtube.com', 'youtube-nocookie.com', $response['html'] ); } if ( isset( $url->queryString['rel'] ) ) { $response['html'] = str_replace( '?feature=oembed', '?feature=oembed&rel=0', $response['html'] ); } if ( isset( $url->queryString['start'] ) ) { $response['html'] = str_replace( '?feature=oembed', '?feature=oembed&start=' . \intval( $url->queryString['start'] ), $response['html'] ); } } /* Tiktok tries to embed a tag which isn't triggered by CKEditor and doesn't survive HTMLPurifier, so force it to load in an iframe */ if ( $domain == 'tiktok.com' OR $domain == 'm.tiktok.com' ) { $response['type'] = 'rich'; } /* We need a type otherwise we can't embed */ if( !isset( $response['type'] ) ) { throw new \UnexpectedValueException( 'embed_no_oembed_type' ); } /* The "type" parameter is a way for services to indicate the type of content they are retruning. It is not strict, but we use it to identify the best styles to apply. */ switch ( $response['type'] ) { /* Static photo - show antag, linked if necessary, using .ipsImage to be responsive. Similar outcome to if a user had used the "insert image from URL" button */ case 'photo': return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->photo( $response['url'], $url, $response['title'] ); /* Video - insert the provided HTML directly (it will be a video so there's nothing we need to prevent from being edited), using .ipsEmbeddedVideo to make it responsive */ case 'video': $response['html'] = str_replace( 'allowfullscreen', 'allowfullscreen=""', $response['html'] ); return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->video( $response['html'] ); /* Other - show an
s*/ $text = strip_tags( $text, ( $oneLine === TRUE ) ? NULL : '
' ); /* Remove any
s from the start so there isn't just blank space at the top, but maintaining
s elsewhere */ $text = preg_replace( '/^(\s|
|' . \chr(0xC2) . \chr(0xA0) . ')+/', '', $text ); /* Truncate to length, if appropriate */ if( $oneLine === TRUE AND $length > 0 ) { $text = mb_substr( $text, 0, $length ); } /* Return */ return $text; } /** * Munge resources in ACP * * @deprecated No longer needed as of 4.5, ACP URLs no longer have the session ID in the URL * @param string $value The value to parse * @param bool $makeLinksAcpSafe If TRUE, will also make links safe for ACP * @return string */ public static function mungeResources( $value, $makeLinksAcpSafe=FALSE ) { return $value; } /** * @brief Emoticons */ protected static $emoticons = NULL; /** * Rebuild attachment urls * * @param string $textContent Content * @return mixed False, or rebuilt content */ public static function rebuildAttachmentUrls( $textContent ) { $rebuilt = FALSE; $textContent = preg_replace( '#<([^>]+?)(href|src)=(\'|")/#i', '<\1\2=\3%7BfileStore.\4%7D/', $textContent ); $textContent = preg_replace( '#<([^>]+?)(href|src)=(\'|")<___base_url___>/#i', '<\1\2=\3%7B___base_url___%7D/', $textContent ); $textContent = preg_replace( '#<([^>]+?)(data-(fileid|ipshover\-target))=(\'|")<___base_url___>/#i', '<\1\2=\3%7B___base_url___%7D/', $textContent ); /* srcset can have multiple urls in it */ preg_match_all( '#<(?:[^>]+?)srcset=(\'|")([^\'"]+?)(\1)#i', $textContent, $srcsetMatches, PREG_SET_ORDER ); foreach( $srcsetMatches as $val ) { if ( mb_stristr( $val[2], '<___base_url___>' ) ) { $textContent = str_replace( $val[2], str_replace( '<___base_url___>', '%7B___base_url___%7D', $val[2] ), $textContent ); } } /* Create DOMDocument */ $content = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' ); @$content->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $textContent ) ); $xpath = new \DOMXpath( $content ); foreach ( $xpath->query('//img') as $image ) { if( $image->getAttribute( 'data-fileid' ) ) { try { $attachment = \IPS\Db::i()->select( '*', 'core_attachments', array( 'attach_id=?', $image->getAttribute( 'data-fileid' ) ) )->first(); $image->setAttribute( 'src', '{fileStore.core_Attachment}/' . ( $attachment['attach_thumb_location'] ? $attachment['attach_thumb_location'] : $attachment['attach_location'] ) ); $anchor = $image->parentNode; /* Make sure it's actually an anchor */ if ( $anchor->tagName !== 'a' ) { /* It's not, so create one and add the image to it */ $parent = $image->parentNode; $clonedImage = clone $image; $anchor = $content->createElement( 'a' ); $anchor->setAttribute( 'href', '{fileStore.core_Attachment}/' . $attachment['attach_location'] ); $anchor->appendChild( $clonedImage ); $parent->replaceChild( $anchor, $image ); } else { $anchor->setAttribute( 'href', '{fileStore.core_Attachment}/' . $attachment['attach_location'] ); } $rebuilt = TRUE; } catch ( \Exception $e ) { } } else { if ( ! isset( static::$fileObjectClasses['core_Emoticons'] ) ) { static::$fileObjectClasses['core_Emoticons'] = \IPS\File::getClass('core_Emoticons' ); } if ( static::$emoticons === NULL ) { static::$emoticons = array(); try { foreach ( \IPS\Db::i()->select( 'image, image_2x, width, height', 'core_emoticons' ) as $row ) { static::$emoticons[] = $row; } } catch( \IPS\Db\Exception $ex ) { /* The image_2x column was added in 4.1 so may not exist if Parser is used in previous upgrade modules */ foreach ( \IPS\Db::i()->select( 'image, NULL as image_2x, 0 as width, 0 as height', 'core_emoticons' ) as $row ) { static::$emoticons[] = $row; } } } if ( ( $image->tagName === 'img' and preg_match( '#^(' . preg_quote( rtrim( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '/' ), '#' ) . ')/(.+?)$#', $image->getAttribute('src'), $matches ) ) ) { foreach( static::$emoticons as $emo ) { if ( $emo['image'] == $matches[2] ) { $image->setAttribute( 'src', '{fileStore.core_Emoticons}/' . $matches[2] ); if( $emo['image_2x'] && $emo['width'] && $emo['height'] ) { /* Retina emoticons require a width and height for proper scaling */ $image->setAttribute( 'srcset', '%7BfileStore.core_Emoticons%7D/' . $emo['image_2x'] . ' 2x' ); $image->setAttribute( 'width', $emo['width'] ); $image->setAttribute( 'height', $emo['height'] ); } $rebuilt = TRUE; } } } } } if( $rebuilt ) { $value = $content->saveHTML(); $value = preg_replace( '/]+?)>/i', '', preg_replace( '/^/', '', str_replace( array( '', '', '', '', '', '' ), '', $value ) ) ); /* Replace any {fileStore.whatever} tags with */ return static::replaceFileStoreTags( $value ); } return FALSE; } /** * Rebuild rel tag contents for posts * * @param string $textContent The content of the text, and they say comments aren't useful * @param \IPS\Member $member The author of the content * @return mixed FALSE or changed content */ public static function rebuildUrlRels( $textContent, $member ) { $obj = new static( TRUE, NULL, $member ); $rebuilt = FALSE; /* Create DOMDocument */ $content = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' ); @$content->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $textContent ) ); $xpath = new \DOMXpath( $content ); foreach( $xpath->query('//a') as $link ) { if ( $link->getAttribute('href') ) { try { $url = \IPS\Http\Url::createFromString( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $link->getAttribute('href') ) ); $rels = $obj->_getRelAttributes( $url ); $link->setAttribute( 'rel', implode( ' ', $rels ) ); $rebuilt = TRUE; } catch( \Exception $e ) { } } } if ( $rebuilt ) { $value = $content->saveHTML(); $value = preg_replace( '/]+?)>/i', '', preg_replace( '/^/', '', str_replace( array( '', '', '', '', '', '' ), '', $value ) ) ); /* Replace file storage tags */ $value = preg_replace( '/<fileStore\.([\d\w\_]+?)>/i', ' ', $value ); /* DOMDocument::saveHTML will encode the base_url brackets, so we need to make sure it's in the expected format. */ return str_replace( '<___base_url___>', '<___base_url___>', $value ); } return FALSE; } /** * Perform a safe html_entity_decode if you are not using UTF-8 MB4 * * @param string $value Value to html entity decode * @return string */ public static function utf8mb4SafeDecode( $value ) { $value = html_entity_decode( $value, ENT_QUOTES, 'UTF-8' ); if ( \IPS\Settings::i()->getFromConfGlobal('sql_utf8mb4') !== TRUE ) { $value = preg_replace_callback( '/[\x{10000}-\x{10FFFF}]/u', function( $mb4Character ) { return mb_convert_encoding( $mb4Character[0], 'HTML-ENTITIES', 'UTF-8' ); }, $value ); } return $value; } /** * Does this post contain a quote by member? * * @param int $memberId I dunno, take a wild guess. We don't pass a member object as they may have been deleted. * @param string $content Content, that's "content", not "content". I'm data, not relaxed. * @return boolean */ public static function containsQuoteBy( $memberId, $content ) { if ( $memberId and $content ) { return preg_match( '#data-ipsquote-userid=[\'"]' . $memberId . '[\'"]#i', $content ); } return FALSE; } /** * Remove quote attribution (sets it to guest ) * * @param int $memberId Member ID. We don't pass a member object as they may have been deleted. * @param string $content Still Content * @return boolean */ public static function removeQuoteAttributionBy( $memberId, $content ) { /* Do it */ return DOMParser::parse( $content, function( \DOMElement $element, \DOMNode $parent, \IPS\Text\DOMParser $parser ) use( $memberId ) { /* If the element tag name matches the first bit... */ if( $element->tagName == 'blockquote' ) { if ( $element->getAttribute('data-ipsquote-userid') == $memberId ) { $element->setAttribute('data-ipsquote-userid', 0 ); if ( $element->hasAttribute('data-ipsquote-username') ) { $element->setAttribute('data-ipsquote-username', '' ); } foreach( $element->childNodes AS $child ) { if ( $child instanceof \DOMElement and mb_strpos( $child->getAttribute('class'), 'ipsQuote_citation' ) !== FALSE ) { $replace = new \DOMElement( 'div' ); $element->replaceChild( $replace, $child ); $replace->setAttribute('class', 'ipsQuote_citation' ); } } } } /* If we're still here, it's fine and we can import it */ $ownerDocument = $parent->ownerDocument ?: $parent; $newElement = $ownerDocument->importNode( $element ); $parent->appendChild( $newElement ); /* And continue to children */ $parser->_parseDomNodeList( $element->childNodes, $newElement ); } ); } /** * Checks if there is actual content or if this was just an empty editor submission * * @param string $text Text that came from an editor * @return bool */ public static function hasContent( $text ) { if( !trim( $text ) ) { return FALSE; } if( preg_match( "/^\s* \s*(
]*>| |\u00A0| )?\s*<\/p>\s*$/i", $text ) ) { return FALSE; } return TRUE; } /** * Parse attachment labels and pull the required data * * @param array $attachment Row from core_attachments * @return array */ public static function getAttachmentLabels( $attachment ): array { return []; } }