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 */ $value = static::replaceFileStoreTags( $value ); /* Return */ return $value; } /** * Parse * * @param string $value HTML to run through HTML purifier * @return string */ public function purify( $value ) { /* CKEditor sometimes includes these for markers. HTMLPurifier will remove the style attribute so we need to strip them first */ $value = str_replace( ' ', '', $value ); /* Clean HTML */ if ( $value and $this->htmlPurifier ) { $value = $this->htmlPurifier->purify( $value ); } return $value; } /** * Returns the blank image used as a placeholder to facilitate lazy loading * * @return string */ public static function blankImage() { return (string) \IPS\Http\Url::internal( "applications/core/interface/js/spacer.png", 'none', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE ); } /** * Returns a url to a blank page used as a placeholder to facilitate lazy loading in frames * * @return string */ public static function blankPage() { return (string) \IPS\Http\Url::internal( "applications/core/interface/index.html", 'none', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE ); } /** * Remove image proxy * * @param string $content HTML to parse * @param bool $useProxyUrl Use the proxied image URL (the locally stored image) instead of the original URL * @return stinrg */ public static function removeImageProxy( $content, $useProxyUrl = FALSE ) { $source = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' ); $source->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $content ) ); /* Get document images */ $contentImages = $source->getElementsByTagName( 'img' ); foreach( $contentImages as $element ) { static::_removeImageProxy( $element, $useProxyUrl ); } /* Get DOMDocument output */ $content = \IPS\Text\DOMParser::getDocumentBodyContents( $source ); /* Replace file storage tags */ $content = preg_replace( '/<fileStore\.([\d\w\_]+?)>/i', '', $content ); /* 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___>', $content ); } /** * Parse content to add or remove lazy loading * * @param string $content HTML to parse * @param bool $status Add or remove lazy loading * @return stinrg */ public static function parseLazyLoad( $content, $status=TRUE ) { /* Lazy loading applies to images, iframes and videos - return now if we don't detect any with basic string checks */ if( mb_strpos( $content, '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', '', $content ); /* 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___>', $content ); } /** * Remove lazy-loading from content * * @param string $content HTML to parse * @return stinrg */ public static function removeLazyLoad( $content ) { /* Load source */ $source = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' ); $source->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $content ) ); /* Swap data-src for src */ $contentImages = $source->getElementsByTagName( 'img' ); foreach( $contentImages as $element ) { if ( $element->hasAttribute('data-src') ) { $element->setAttribute( 'src', $element->getAttribute('data-src') ); $element->removeAttribute( 'data-src' ); } } $contentVideos = $source->getElementsByTagName( 'video' ); foreach( $contentVideos as $element ) { if ( $element->hasAttribute('data-video-embed') ) { $element->setAttribute( 'data-controller', 'core.global.core.embeddedvideo' ); $element->removeAttribute( 'data-video-embed' ); } } /* Swap data-video-src for src */ $contentVideos = $source->getElementsByTagName( 'source' ); foreach( $contentVideos as $element ) { if ( $element->parentNode->tagName === 'video' and $element->hasAttribute('data-video-src') ) { $element->setAttribute( 'src', $element->getAttribute('data-video-src') ); $element->removeAttribute( 'data-video-src' ); } } /* Swap data-embed-src for src */ $contentEmbeds = $source->getElementsByTagName( 'iframe' ); foreach( $contentEmbeds as $element ) { if ( $element->hasAttribute('data-embed-src') ) { $element->setAttribute( 'src', $element->getAttribute('data-embed-src') ); $element->removeAttribute( 'data-embed-src' ); } } /* Get DOMDocument output */ $content = \IPS\Text\DOMParser::getDocumentBodyContents( $source ); /* Replace file storage tags */ $content = preg_replace( '/<fileStore\.([\d\w\_]+?)>/i', '', $content ); /* 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___>', $content ); } /** * Replace {fileStore.xxx} with * * @param string $value HTML to parse * @return string */ public static function replaceFileStoreTags( $value ) { /* Some tags have multiple __base_url__ replacements, so we have to replace this in a safe way ensuring we only match inside A, IMG, IFRAME and VIDEO tags to prevent tampering */ preg_match_all( '#<(img|a|iframe|video|audio|source)([^>]+?)%7B___base_url___%7D([^>]+?)>#i', $value, $matches, PREG_SET_ORDER ); foreach( $matches as $val ) { $changed = $val[0]; /* srcset can have multiple urls in it */ preg_match( '#srcset=(\'|")([^\'"]+?)(\1)#i', $changed, $srcsetMatches ); if ( isset( $srcsetMatches[2] ) ) { if ( mb_stristr( $srcsetMatches[2], '%7B___base_url___%7D' ) ) { $changed = str_replace( $srcsetMatches[2], str_replace( '%7B___base_url___%7D', '<___base_url___>', $srcsetMatches[2] ), $changed ); } } $changed = preg_replace( '#(href|src|data\-src|data\-video\-src|data\-audio\-src|data\-fileid|data\-ipshover\-target)=(\'|")%7B___base_url___%7D/#i', '\1=\2<___base_url___>/', $changed ); if ( $changed != $val[0] ) { $value = str_replace( $val[0], $changed, $value ); } } /* Replace {fileStore.xxx} with */ $value = preg_replace( '#(srcset|href|data\-src|src|data\-video\-src|data\-video\-src)=(\'|")(%7B|\{)fileStore\.([\d\w\_]+?)(%7D|\})/#i', '\1=\2/', $value ); /* Return */ return $value; } /* !Parser: HTMLPurifier */ /** * Get HTML Purifier Configuration * * @param callback $callback A function which will be passed the HTMLPurifier_Config object to customise it * @return \HTMLPurifier_Config */ protected function _htmlPurifierConfiguration( $callback = NULL ) { /* Start with a base configruation */ $config = \HTMLPurifier_Config::createDefault(); /* HTMLPurifier by default caches data to disk which we cannot allow. Register our custom cache definiton to use \IPS\Data\Store instead */ $definitionCacheFactory = \HTMLPurifier_DefinitionCacheFactory::instance(); $definitionCacheFactory->register( 'IPSCache', "HtmlPurifierDefinitionCache" ); require_once( \IPS\ROOT_PATH . '/system/Text/HtmlPurifierDefinitionCache.php' ); $config->set( 'Cache.DefinitionImpl', 'IPSCache' ); /* Allow iFrames from services we allow. We limit this to a whitelist because to allow any iframe would open us to phishing and other such security issues */ $config->set( 'HTML.SafeIframe', true ); $config->set( 'URI.SafeIframeRegexp', static::safeIframeRegexp() ); $config->set( 'Output.Newline', "\n" ); /* If we cannot post remote images, make sure we don't try to using the background-image style property */ if( !\IPS\Settings::i()->allow_remote_images ) { $config->set( 'CSS.ForbiddenProperties', array( 'background-image' ) ); } /* Set allowed CSS classes. We limit this to a whitelist because to allow any iframe would open us to phishing (for example, someone posts something which, by using our CSS classes, looks like a login form), and general annoyances */ $config->set( 'Attr.AllowedClasses', static::getAllowedCssClasses() ); /* Increase default image width */ $config->set( 'CSS.MaxImgLength', "4800px" ); $config->set( 'HTML.MaxImgLength', "4800" ); /* Callback */ if ( $callback ) { $callback( $config ); } /* HTML Definition */ $htmlDefinition = $config->getHTMLDefinition( TRUE ); $this->_htmlPurifierModifyHtmlDefinition( $htmlDefinition ); /* CSS Definition */ $cssDefinition = $config->getCSSDefinition(); $this->_htmlPurifierModifyCssDefinition( $cssDefinition, $config ); if( \IPS\Settings::i()->allow_remote_images AND \IPS\Settings::i()->allow_only_https_remote_images ) { $uri = $config->getDefinition('URI'); $uri->addFilter( new \IPS\Text\HtmlPurifierHttpsImages(), $config ); } /* Return */ return $config; } /** * Customize HTML Purifier HTML Definition * * @param HTMLPurifier_HTMLDefinition $def The definition * @return void */ protected function _htmlPurifierModifyHtmlDefinition( \HTMLPurifier_HTMLDefinition $def ) { /* Links (set by _parseAElement) */ $def->addAttribute( 'a', 'rel', 'Text' ); /* srcset for emoticons (used by _parseImgElement) */ $def->addAttribute( 'img', 'srcset', new HtmlPurifierSrcsetDef( TRUE ) ); /* Quotes (used by BBCode and ipsquote editor plugin) */ $def->addAttribute( 'blockquote', 'data-ipsquote', 'Bool' ); $def->addAttribute( 'blockquote', 'data-ipsquote-timestamp', 'Number' ); $def->addAttribute( 'blockquote', 'data-ipsquote-username', 'Text' ); $def->addAttribute( 'blockquote', 'data-ipsquote-contentapp', 'Text' ); $def->addAttribute( 'blockquote', 'data-ipsquote-contentclass', 'Text' ); $def->addAttribute( 'blockquote', 'data-ipsquote-contenttype', 'Text' ); $def->addAttribute( 'blockquote', 'data-ipsquote-contentid', 'Number' ); $def->addAttribute( 'blockquote', 'data-ipsquote-contentcommentid', 'Number' ); $def->addAttribute( 'blockquote', 'data-ipsquote-userid', 'Number' ); $def->addAttribute( 'blockquote', 'data-cite', 'Text' ); /* Spoilers (used by BBCode and ipsspoiler editor plugin) */ $def->addAttribute( 'div', 'data-ipsspoiler', 'Bool' ); /* Mentions (used by BBCode and ipsmentions editor plugin) */ $def->addAttribute( 'a', 'data-ipshover', new HtmlPurifierSwitchAttrDef( 'a', array( 'data-ipshover-target' ), new \HTMLPurifier_AttrDef_HTML_Bool(''), new \HTMLPurifier_AttrDef_Enum( array() ) ) ); $def->addAttribute( 'a', 'data-ipshover-target', new HtmlPurifierInternalLinkDef( TRUE, array( array( 'app' => 'core', 'module' => 'members', 'controller' => 'profile', 'do' => 'hovercard' ) ) ) ); $def->addAttribute( 'a', 'data-mentionid', 'Number' ); $def->addAttribute( 'a', 'contenteditable', 'Enum#false' ); /* Emoticons (used by the ipsautolink plugin) */ $def->addAttribute( 'img', 'data-emoticon', 'Bool' ); // Identifies emoticons and stops lightbox running on them /* Attachments (set by _parseAElement, _parseImgElement and "insert existing attachment") - Gallery/Downloads use the full URL rather than an ID, hence Text */ $def->addAttribute( 'a', 'data-fileid', new HtmlPurifierIntOrInternalLink() ); $def->addAttribute( 'img', 'data-fileid', new HtmlPurifierIntOrInternalLink() ); $def->addAttribute( 'a', 'data-fileext', 'Text' ); /* Existing media (inserted with data-extension by the JS so that _getFile is able to locate) */ $def->addAttribute( 'img', 'data-extension', 'Text' ); $def->addAttribute( 'a', 'data-extension', 'Text' ); /* Lazy loading */ $def->addAttribute( 'img', 'data-ratio', 'Text' ); $def->addAttribute( 'img', 'data-src', new \HTMLPurifier_AttrDef_URI( TRUE ) ); $def->addAttribute( 'iframe', 'data-embed-src', new \HTMLPurifier_AttrDef_URI( TRUE ) ); /* iFrames (used by embeddableMedia) */ $def->addAttribute( 'iframe', 'data-controller', new \HTMLPurifier_AttrDef_Enum( array( 'core.front.core.autosizeiframe' ) ) ); // used in core/global/embed/iframe.phtml $def->addAttribute( 'iframe', 'data-embedid', 'Text' ); // used in core/global/embed/iframe.phtml $def->addAttribute( 'iframe', 'data-embedauthorid', 'Number' ); // used for embed notifications $def->addAttribute( 'iframe', 'data-embedcontent', 'Text' ); // used in embeddableMedia $def->addAttribute( 'iframe', 'allowfullscreen', 'Text' ); // Some services will specify this property /* data-controllers */ $allowedDivDataControllers = array( 'core.front.core.articlePages', // [page] (set by _parseContent) ); if( \IPS\Settings::i()->editor_allowed_datacontrollers ) { $allowedDivDataControllers = array_merge( $allowedDivDataControllers, explode( ',', \IPS\Settings::i()->editor_allowed_datacontrollers ) ); } $def->addAttribute( 'div', 'data-controller', new \HTMLPurifier_AttrDef_Enum( $allowedDivDataControllers, TRUE ) ); /* [page] (set by _parseContent) */ $def->addAttribute( 'div', 'data-role', new \HTMLPurifier_AttrDef_Enum( array( 'contentPage' ), TRUE ) ); $def->addAttribute( 'hr', 'data-role', new \HTMLPurifier_AttrDef_Enum( array( 'contentPageBreak' ), TRUE ) ); /* data-munge-src used by _removeMunge() */ $def->addAttribute( 'img', 'data-munge-src', 'Text' ); $def->addAttribute( 'iframe', 'data-munge-src', 'Text' ); /* Videos */ $def->addElement( 'video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array( 'controls' => 'Bool', 'data-controller' => new \HTMLPurifier_AttrDef_Enum( array( 'core.global.core.embeddedvideo' ) ), 'data-video-embed' => 'Text' ) ); $def->addElement( 'source', 'Block', 'Flow', 'Common', array( 'src' => new \HTMLPurifier_AttrDef_URI( TRUE ), 'srcset' => new HtmlPurifierSrcsetDef( TRUE ), 'media' => 'Text', 'type' => 'Text', 'data-video-src' => new \HTMLPurifier_AttrDef_URI( TRUE ) ) ); /* Audio */ $def->addElement('audio', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array( 'controls' => 'Bool', 'data-controller' => new \HTMLPurifier_AttrDef_Enum( array( 'core.global.core.embeddedaudio' ) ), 'src' => new \HTMLPurifier_AttrDef_URI( TRUE ), 'srcset' => new HtmlPurifierSrcsetDef( TRUE ), 'media' => 'Text', 'type' => 'Text', 'data-audio-embed' => 'Text', 'data-audio-src' => new \HTMLPurifier_AttrDef_URI( TRUE ) )); /* Picture tag - we don't use it, but RSS imports might */ $def->addElement( 'picture', 'Inline', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array() ); } /** * @brief Maximum allowed width/height px values (to prevent causing page layout oddities) */ protected $cssMaxWidthHeight = 1000; /** * @brief Maximum allowed border width px value (to prevent causing page layout oddities) */ protected $cssMaxBorderWidth = 50; /** * Customize HTML Purifier CSS Definition * * @param HTMLPurifier_HTMLDefinition $def The definition * @param HTMLPurifier_Config $config HTML Purifier configuration object * @return void */ protected function _htmlPurifierModifyCssDefinition( \HTMLPurifier_CSSDefinition $def, \HTMLPurifier_Config $config ) { /* Do not allow negative margins */ $margin = $def->info['margin-right'] = $def->info['margin-left'] = $def->info['margin-bottom'] = $def->info['margin-top'] = new \HTMLPurifier_AttrDef_CSS_Composite( array( new \HTMLPurifier_AttrDef_CSS_Length( 0 ), new \HTMLPurifier_AttrDef_CSS_Percentage( TRUE ), new \HTMLPurifier_AttrDef_Enum(array('auto')) ) ); $def->info['margin'] = new \HTMLPurifier_AttrDef_CSS_Multiple( $margin ); /* Don't allow white-space:nowrap */ $def->info['white-space'] = new \HTMLPurifier_AttrDef_Enum( array( 'normal', 'pre', 'pre-wrap', 'pre-line') ); /* Limit the maximum width and height allowed */ $def->info['width'] = $def->info['height'] = new \HTMLPurifier_AttrDef_CSS_Composite( array( new \HTMLPurifier_AttrDef_CSS_Length( '0px', $this->cssMaxWidthHeight . 'px' ), new \HTMLPurifier_AttrDef_CSS_Percentage( true ), new \HTMLPurifier_AttrDef_Enum( array( 'auto' ) ) ) ); /* Limit the maximum border width allowed */ $border_width = $def->info['border-top-width'] = $def->info['border-bottom-width'] = $def->info['border-left-width'] = $def->info['border-right-width'] = new \HTMLPurifier_AttrDef_CSS_Composite(array( new \HTMLPurifier_AttrDef_Enum( array( 'thin', 'medium', 'thick' ) ), new \HTMLPurifier_AttrDef_CSS_Length( '0px', $this->cssMaxBorderWidth . 'px' ) ) ); $def->info['border-width'] = new \HTMLPurifier_AttrDef_CSS_Multiple( $border_width ); /* We have to reset this so the constructor picks up the new values we just specified */ $def->info['border'] = $def->info['border-bottom'] = $def->info['border-top'] = $def->info['border-left'] = $def->info['border-right'] = new \HTMLPurifier_AttrDef_CSS_Border( $config ); } /** * Get URL bases (whout schema) that we'll allow iframes from * * @return array */ protected static function safeIframeRegexp() { $return = array(); /* 3rd party sites (YouTube, etc.) */ foreach ( static::allowedIFrameBases() as $base ) { $return[] = '(https?:)?//' . preg_quote( $base, '%' ); } /* Some, but not all local URLs Allowed: Any URLs which go through the front-end, e.g.: site.com/?app=core&module=system&controller=embed&url=whatever site.com/index.php?app=core&module=system&controller=embed&url=whatever site.com/topic/1-test/?do=embed site.com/index.php?/topic/1-test/?do=embed site.com/index.php?app=forums&module=forums&controller=topic&id=1&do=embed Not Allowed: Anything which goes to anything in an /interface directory - e.g.: site.com/applications/core/interface/file/attachment.php - this would automatically cause files to be downloaded Not Allowed: Any file traversal to expose FileSystem directories, e.g: site.com/applications/core/../uploads/monthly_xx_xx/file.js? Not Allowed: URLs to the open proxy: site.com/index.php?app=core&module=system&controller=redirect */ $notAllowed = array(); foreach( \IPS\Application::enabledApplications() as $app ) { $notAllowed[] = str_replace( '/', '(?:/{1,})', '(?:/{0,})' . preg_quote( 'applications/' . $app->directory . '/interface/', '%' ) ); } foreach( \IPS\File::getStore() as $configuration ) { if ( $configuration['method'] == 'FileSystem' and ! empty( $configuration['configuration'] ) and $config = json_decode( $configuration['configuration'], TRUE ) ) { if ( !empty( $config['dir'] ) and mb_strpos( $config['dir'], '{root}' ) !== false ) { if ( $path = trim( str_replace( '{root}', '', $config['dir'] ), '\/' ) ) { $notAllowed[] = '(?:.+?/{1,})\.{1,}(?:/{1,})' . $path; } } } } $return[] = '(https?:)?//' . preg_quote( str_replace( array( 'http://', 'https://' ), '', \IPS\Settings::i()->base_url ), '%' ) . '\/?(\?|index\.php\?|(?!' . implode( '|', $notAllowed ) . ').+?\?)((?!(controller|section)=redirect).)*$'; $return[] = preg_quote( '%7B___base_url___%7D', '%' ) . '\/?(\?|index\.php\?|(?!' . implode( '|', $notAllowed ) . ').+?\?)((?!(controller|section)=redirect).)*$'; /* Return */ return '%^(' . implode( '|', $return ) . ')%'; } /** * Get URL bases (whout schema) that we'll allow iframes from * * @return array * @note When updating this list, be sure to update the editor_embeds_desc lang string */ protected static function allowedIFrameBases() { $return = array(); /* Our default embed options */ $return = array_merge( $return, array( 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/', 'player.vimeo.com/video/', 'www.hulu.com/embed.html', 'www.collegehumor.com/e/', 'embed-ssl.ted.com/', 'embed.ted.com', 'vine.co/v/', 'embed.spotify.com/', 'www.dailymotion.com/embed/', 'www.funnyordie.com/', 'coub.com/', 'www.reverbnation.com/', 'www.ustream.tv/embed/', 'api.smugmug.com/services/embed/', 'www.google.com/maps/', 'www.screencast.com/users/', 'fast.wistia.net/embed/', 'www.screencast.com/users/', 'players.brightcove.net/', ) ); /* Extra admin-defined options */ if ( \IPS\Settings::i()->editor_allowed_iframe_bases ) { $return = array_merge( $return, explode( ',', \IPS\Settings::i()->editor_allowed_iframe_bases ) ); } return $return; } /** * Get allowed CSS classes * * @return array */ protected function getAllowedCssClasses() { /* Init */ $return = array(); /* Quotes (used by ipsquote editor plugin) */ $return[] = 'ipsQuote'; $return[] = 'ipsQuote_citation'; $return[] = 'ipsQuote_contents'; /* Code (used by ipscode editor plugin) */ $return[] = 'ipsCode'; $return[] = 'prettyprint'; $return[] = 'prettyprinted'; $return[] = 'lang-auto'; $return[] = 'lang-javascript'; $return[] = 'lang-php'; $return[] = 'lang-css'; $return[] = 'lang-html'; $return[] = 'lang-xml'; $return[] = 'lang-c'; $return[] = 'lang-sql'; $return[] = 'lang-lua'; $return[] = 'lang-swift'; $return[] = 'lang-perl'; $return[] = 'lang-python'; $return[] = 'lang-ruby'; $return[] = 'lang-latex'; $return[] = 'tag'; $return[] = 'pln'; $return[] = 'atn'; $return[] = 'atv'; $return[] = 'pun'; $return[] = 'com'; $return[] = 'kwd'; $return[] = 'str'; $return[] = 'lit'; $return[] = 'typ'; $return[] = 'dec'; $return[] = 'src'; $return[] = 'nocode'; /* Spoiler (used by ipsspoiler editor plugin) */ $return[] = 'ipsSpoiler'; $return[] = 'ipsSpoiler_header'; $return[] = 'ipsSpoiler_contents'; $return[] = 'ipsStyle_spoiler'; /* Images and attachments (used when attachments are inserted into the editor) */ $return[] = 'ipsImage'; $return[] = 'ipsImage_thumbnailed'; $return[] = 'ipsAttachLink'; $return[] = 'ipsAttachLink_image'; $return[] = 'ipsAttachLink_left'; $return[] = 'ipsAttachLink_right'; $return[] = 'ipsEmoji'; /* Embeds (used by various return values of embeddedMedia) */ $return[] = 'ipsEmbedded'; $return[] = 'ipsEmbeddedVideo'; $return[] = 'ipsEmbeddedVideo_limited'; $return[] = 'ipsEmbeddedOther'; $return[] = 'ipsEmbeddedOther_limited'; /* Links (Used to replace disallowed URLs */ $return[] = 'ipsType_noLinkStyling'; /* Brightcove */ $return[] = 'ipsEmbeddedBrightcove'; $return[] = 'ipsEmbeddedBrightcove_inner'; $return[] = 'ipsEmbeddedBrightcove_frame'; /* Custom */ if( \IPS\Settings::i()->editor_allowed_classes ) { $return = array_merge( $return, explode( ',', \IPS\Settings::i()->editor_allowed_classes ) ); } return $return; } /* !Parser: Main Parser */ /** * @brief The closing BBCode tags we are looking for and how many are open */ protected $closeTagsForOpenBBCode = array(); /** * @brief Open Inline BBCode tags */ protected $openInlineBBCode = array(); /** * @brief All open Block-Level BBCode tags */ protected $openBlockBBCodeByTag = array(); /** * @brief All open Block-Level BBCode tags in the order they were created */ protected $openBlockBBCodeInOrder = array(); /** * @brief Open Block-Level BBCode tags */ protected $openBlockDepth = NULL; /** * @brief Force BBCode enabled (used by 4.0 upgrader parsing and converters) * @see set_forceBbcodeEnabled() */ protected $forceBbcodeEnabled = FALSE; /** * @brief This is used to stop BBCode parsing temporarily (such as in [code] tags) */ protected $bbcodeParse = TRUE; /** * @brief If we have opened a BBCode tag which we don't parse other BBCode inside, the string at which we will resume parsing */ protected $resumeBBCodeParsingOn = NULL; /** * @brief Does the content contain [page] tags? */ protected $containsPageTags = FALSE; /** * @brief Open tags */ protected $openAbbrTags = array(); /** * Parse BBCode, Profanity, etc. by loading into a DOMDocument * * @param string $value HTML to parse * @return string */ protected function _parseContent( $value ) { /* This fix resolves an issue using
mode where BBCode tags are wrapped in P tags like so:

[tag]

Content

[/tag]

tags. The fix just removes the

tags inside block BBCode tags, so our example ends up parsing like so:

[tag]

Content

[/tag]

We will want to find a more elegant fix for this at some point */ if ( $this->bbcode !== NULL and ! \IPS\Settings::i()->editor_paragraph_padding ) { $blockTags = array(); foreach( $this->bbcode as $tag => $data ) { if ( ! empty( $data['block'] ) ) { $blockTags[] = $tag; } } if ( \count( $blockTags ) ) { /* If we are inside block tags, ensure that

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( '#

\s{0,}]+?)?'.'>#i', '

', $match[0] ), $value ); } } } /* The editor button just drops in a
, so we need to make sure that the structure is correct, as follows:

Page one


Page two

The editor will just have

Page one


Page two

*/ if ( static::canUse( $this->member, 'Page', $this->area ) ) { if ( ! preg_match( '##', $value ) ) { $value = preg_replace( '##', '' . "\n" . '
' . "\n" . '
', $value ); $value = '
' . $value . '
'; } } /* Parse */ $parser = new DOMParser( array( $this, '_parseDomElement' ), array( $this, '_parseDomText' ) ); $document = $parser->parseValueIntoDocument( $value ); /* [page] tags need to be handled specially */ if ( $this->bbcode !== NULL and static::canUse( $this->member, 'Page', $this->area ) and $this->containsPageTags ) { $body = DOMParser::getDocumentBody( $document ); $bodyWithPages = $this->_parseContentWithSeparationTag( $body, function ( \DOMDocument $document ) { $mainDiv = $document->createElement('div'); $mainDiv->setAttribute( 'data-controller', 'core.front.core.articlePages' ); return $mainDiv; }, function ( \DOMDocument $document ) { $subDiv = $document->createElement('div'); $subDiv->setAttribute( 'data-role', 'contentPage' ); $hr = $document->createElement('hr'); $hr->setAttribute( 'data-role', 'contentPageBreak' ); $subDiv->appendChild( $hr ); return $subDiv; }, '[page]' ); $newBody = new \DOMElement('body'); $body->parentNode->replaceChild( $newBody, $body ); $newBody->appendChild( $bodyWithPages ); } /* Return */ return DOMParser::getDocumentBodyContents( $document ); } /** * Parse HTML element (e.g. ,

, , 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 ); } } /*

 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 

) to go in the block-level element for any content left (e.g. "

[center]This needs to be centered

") and set the parent being used to it */ if ( $sectionId != $sectionCount ) { if ( !isset( $bbcode['noChildren'] ) or !$bbcode['noChildren'] ) { $contentElement = $parent->cloneNode( FALSE ); $blockElement->appendChild( $contentElement ); $parent = $contentElement; } else { $parent = $bbcodeElement; } } /* Add to $openBlockBBcode for closing later */ $id = mt_rand() . '-' . $tag; $this->openBlockBBCodeByTag[ $tag ][ $id ] = $bbcodeElement; /* Add to $penBlockBBCodeInOrder to that so _parseDomElement() will use that as the parent for subsequent elements */ $this->openBlockBBCodeInOrder[ $id ] = $blockElement; } /* Inline */ else { $this->openInlineBBCode[ $tag ][] = $bbcodeElement; } /* Add it to the array */ if ( !isset( $this->closeTagsForOpenBBCode[ "[/{$tag}]" ] ) ) { $this->closeTagsForOpenBBCode[ "[/{$tag}]" ] = 0; } $this->closeTagsForOpenBBCode[ "[/{$tag}]" ]++; } } /** * Close BBCode tag * * @param string $tag The tag (e.g. "b") * @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 _closeBBCode( $tag, \DOMNode &$parent, $sectionId, $sectionCount ) { /* Get definition */ $bbcode = $this->bbcode[ $tag ]; /* Block level? */ if ( isset( $bbcode['block'] ) and $bbcode['block'] ) { /* Find the block we're closing */ foreach ( $this->openBlockBBCodeByTag[ $tag ] as $key => $block ) { } // Just sets $key and $block for the last one /* Create a content element to go after the block-level element for any remaining text in this DOMText node (e.g. "

[/center]This should not be centered

") and set the parent being used to it */ if ( $block->previousSibling and $block->previousSibling instanceof \DOMText ) // Happens for noChildren tags - e.g. "[list]Foo[list]Bar[/list]Baz[/list]" { $parent = $block->parentNode; } else { if ( $block->previousSibling ) { $contentElement = $block->previousSibling->cloneNode( FALSE ); } else { $contentElement = $parent->ownerDocument->createElement('p'); } $block->parentNode->appendChild( $contentElement ); $parent = $contentElement; } /* Remove it from the list of open blocks */ unset( $this->openBlockBBCodeByTag[ $tag ][ $key ] ); unset( $this->openBlockBBCodeInOrder[ $key ] ); /* Finished callback? */ if ( isset( $bbcode['finishedCallback'] ) and $bbcode['finishedCallback'] ) { $callback = $bbcode['finishedCallback']; $newBlock = $callback( $block ); if ( $block->parentNode ) { $block->parentNode->replaceChild( $newBlock, $block ); } else { $parent->ownerDocument->getElementsByTagName('body')->item(0)->appendChild( $newBlock ); } } } /* Inline */ else { array_pop( $this->openInlineBBCode[ $tag ] ); if ( empty( $this->openInlineBBCode[ $tag ] ) ) { unset( $this->openInlineBBCode[ $tag ] ); } } /* Remove from array of open BBCodes */ $this->closeTagsForOpenBBCode["[/{$tag}]"]--; if ( !$this->closeTagsForOpenBBCode["[/{$tag}]"] ) { unset( $this->closeTagsForOpenBBCode["[/{$tag}]"] ); } } /** * Insert a node to a parent while applying inline BBCode * * @param \DOMNode $node Node to insert * @param \DOMNode $parent Parent to insert into * @return void */ protected function _insertNodeApplyingInlineBBcode( \DOMNode $node, \DOMNode $parent ) { /* Apply any open inline BBCode elements */ if ( $this->bbcodeParse ) { foreach ( $this->openInlineBBCode as $tag => $elements ) { foreach ( $elements as $bbcodeElement ) { $parent = $parent->appendChild( $bbcodeElement->cloneNode( TRUE ) ); } } } /* Insert the text */ $parent->appendChild( $node ); } /** * Adjust for Block-Level BBCode if necessary at start of the node * * @param \DOMNode $parent The node from the new document which will be the working node's parent. Passed by reference and will be modified if there is an open block-level BBCode * @return void */ protected function _adjustParentForBlockBBCodeAtStartOfNode( \DOMNode &$parent ) { /* If we have an open block-level BBCode element, and we're not already on a child of one we have already moved, insert this element into that instead of the defined parent */ if ( \count( $this->openBlockBBCodeInOrder ) ) { if ( !$this->openBlockDepth ) { $openBlocks = $this->openBlockBBCodeInOrder; $parent = array_pop( $openBlocks ); } $this->openBlockDepth++; } } /** * Adjust for Block-Level BBCode if necessary at end of the node * * @param \DOMNode $parent The node from the new document which will be the working node's parent. Passed by reference and will be modified if there is an open block-level BBCode * @return void */ protected function _adjustParentForBlockBBCodeAtEndOfNode( \DOMNode &$parent ) { /* If we have an open block-level BBCode element, decrease the depth we're at */ if ( $this->openBlockDepth ) { if ( $this->openBlockDepth == 1 ) { $parent = $parent->parentNode; } $this->openBlockDepth--; } } /* !Parser: Element-Specific Parsing */ /** * Element-Specific Parsing * * @param \DOMElement $element The element * @param \DOMElement|NULL $originalElement The original element. * @note _parseDomElement() creates a new element and imports it into the document. You can inspect $originalElement if you need to check context. * @return \DOMNode|NULL */ protected function _parseElement( \DOMElement $element, \DOMElement $originalElement = NULL ) { /* Element-Specific */ switch ( $element->tagName ) { case 'a': $element = $this->_parseAElement( $element ); break; case 'img': $element = $this->_parseImgElement( $element ); break; case 'iframe': $element = $this->_parseIframeElement( $element ); break; case 'video': $element = $this->_parseVideoElement( $element ); break; case 'audio': $element = $this->_parseAudioElement( $element, $originalElement ); break; case 'source': $element = $this->_parseSourceElement( $element, $originalElement ); break; } /* Anything which has a URL may need swapping out */ foreach ( array( 'href', 'src', 'data-src', 'data-video-src', 'data-embed-src', 'srcset', 'data-ipshover-target', 'data-fileid', 'cite', 'action', 'longdesc', 'usemap', 'poster' ) as $attribute ) { if ( $element->hasAttribute( $attribute ) ) { if ( preg_match( '#^(https?:)?//(' . preg_quote( rtrim( str_replace( array( 'http://', 'https://' ), '', \IPS\Settings::i()->base_url ), '/' ), '#' ) . ')/(.+?)$#', $element->getAttribute( $attribute ), $matches ) ) { $element->setAttribute( $attribute, '%7B___base_url___%7D/' . $matches[3] ); } } } foreach ( array( 'srcset', 'style' ) as $attribute ) { if ( $element->hasAttribute( $attribute ) ) { if ( mb_strpos( $element->getAttribute( $attribute ), \IPS\Settings::i()->base_url ) ) { $element->setAttribute( $attribute, str_replace( \IPS\Settings::i()->base_url, '%7B___base_url___%7D/', $element->getAttribute( $attribute ) ) ); } } } /* Return */ return $element; } /** * Parse
element * * @param \DOMElement $element The element * @return void */ protected function _parseAElement( \DOMElement $element ) { /* Punycode it if necessary */ if ( !preg_match( '/^[\x00-\x7F]*$/', $element->getAttribute('href') ) ) { try { $punycodeEncoded = (string) \IPS\Http\Url::createFromString( $element->getAttribute('href') ); $element->setAttribute( 'href', $punycodeEncoded ); } catch( \IPS\Http\Url\Exception $e ) { } } /* If it's not allowed, remove the href */ if ( !static::isAllowedUrl( $element->getAttribute('href') ) ) { $element->removeAttribute( 'href' ); $element->setAttribute( 'class', 'ipsType_noLinkStyling' ); return $element; } /* Attachment? */ if ( $attachment = static::_getAttachment( $element->getAttribute('href'), $element->hasAttribute('data-fileid') ? $element->getAttribute('data-fileid') : NULL ) ) { $element->setAttribute( 'data-fileid', $attachment['attach_id'] ); $element->setAttribute( 'href', str_replace( array( 'http:', 'https:' ), '', str_replace( static::$fileObjectClasses['core_Attachment']->baseUrl(), '{fileStore.core_Attachment}', $element->getAttribute('href') ) ) ); $element->setAttribute( 'data-fileext', $attachment['attach_ext'] ); if ( ! 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 ) ); } } $this->_logAttachment( $attachment ); } /* Some other media? */ elseif ( $element->getAttribute('data-extension') and $file = $this->_getFile( $element->getAttribute('data-extension'), $element->getAttribute('href') ) ) { $element->setAttribute( 'href', '{fileStore.' . $file->storageExtension . '}/' . (string) $file ); } try { $rels = $this->_getRelAttributes( \IPS\Http\Url::createFromString( $element->getAttribute('href') ) ); } catch( \IPS\Http\Url\Exception $e ) { $rels = array(); } /* Add rels */ $element->setAttribute( 'rel', implode( ' ', $rels ) ); return $element; } /** * @brief Emoticon Count */ protected $_emoticons = 0; /** * Parse 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