+ {!! $signs[$post['poster_id']] !!} +
{{ $topicName }} - #{!! $post['post_number'] !!}
+{{ $topic['subject'] }} - #{!! $post['post_number'] !!}
#{!! $post['post_number'] !!} -+ {!! $signs[$post['poster_id']] !!} +
From 495166e21c276024fe64557335f4fea28623955d Mon Sep 17 00:00:00 2001
From: Visman ' . $body . '
){1,2}%', '$1', $body);
+ $body = preg_replace('%(?:
\s*?){1,2}(?p>)%', '$1', $body);
+
+ // Remove any empty paragraph tags (inserted via quotes/lists/code/etc) which should be stripped
+ $body = str_replace('', '', $body);
+
+ $body = preg_replace('%
\s*?
%', '
', $body); + + $body = str_replace('
', '
', $body);
+ $body = str_replace('
' . $body . '
';
+ },
+ ],
+ ['tag' => 'b',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'i',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'em',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'u',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 's',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'del',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'ins',
+ 'handler' => function($body) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'h',
+ 'type' => 'h',
+ 'handler' => function($body) {
+ return '
'; + }, + ], + ['tag' => 'hr', + 'type' => 'block', + 'single' => true, + 'handler' => function() { + return '
'; + }, + ], + ['tag' => 'color', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'colour', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'background', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'size', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^[1-9]\d*(?:em|ex|pt|px|\%)?$%', + ], + ], + 'handler' => function($body, $attrs) { + if (is_numeric($attrs['Def'])) { + $attrs['Def'] .= 'px'; + } + return '' . $body . ''; + }, + ], + ['tag' => 'right', + 'type' => 'block', + 'handler' => function($body) { + return '
' . $body . '
'; + }, + ], + ['tag' => 'center', + 'type' => 'block', + 'handler' => function($body) { + return '
' . $body . '
'; + }, + ], + ['tag' => 'justify', + 'type' => 'block', + 'handler' => function($body) { + return '
' . $body . '
';
+ },
+ ],
+ ['tag' => 'mono',
+ 'handler' => function($body) {
+ return '' . $body . '
';
+ },
+ ],
+ ['tag' => 'font',
+ 'self nesting' => true,
+ 'attrs' => [
+ 'Def' => [
+ 'format' => '%^[a-z\d, -]+$%i',
+ ],
+ ],
+ 'handler' => function($body, $attrs) {
+ return '' . $body . '';
+ },
+ ],
+ ['tag' => 'email',
+ 'type' => 'email',
+ 'attrs' => [
+ 'Def' => [
+ 'format' => '%^[^\x00-\x1f\s]+?@[^\x00-\x1f\s]+$%',
+ ],
+ 'no attr' => [
+ 'body format' => '%^[^\x00-\x1f\s]+?@[^\x00-\x1f\s]+$%D',
+ 'text only' => true,
+ ],
+ ],
+ 'handler' => function($body, $attrs) {
+ if (empty($attrs['Def'])) {
+ return '' . $body . '';
+ } else {
+ return '' . $body . '';
+ }
+ },
+ ],
+ ['tag' => '*',
+ 'type' => 'block',
+ 'self nesting' => true,
+ 'parents' => ['list'],
+ 'auto' => true,
+ 'handler' => function($body) {
+ return '
' . $body . '
'; + case '1': + return '
'; + default: + return '
';
+ }
+ },
+ ],
+ ['tag' => 'after',
+ 'type' => 'block',
+ 'single' => true,
+ 'attrs' => [
+ 'Def' => [
+ 'format' => '%^\d+$%',
+ ],
+ ],
+ 'handler' => function($body, $attrs) {
+ $arr = array();
+ $sec = $attrs['Def'] % 60;
+ $min = ($attrs['Def'] / 60) % 60;
+ $hours = ($attrs['Def'] / 3600) % 24;
+ $days = (int) ($attrs['Def'] / 86400);
+ if ($days > 0) {
+ $arr[] = $days . __('After time d');
+ }
+ if ($hours > 0) {
+ $arr[] = $hours . __('After time H');
+ }
+ if ($min > 0) {
+ $arr[] = (($min < 10) ? '0' . $min : $min) . __('After time i');
+ }
+ if ($sec > 0) {
+ $arr[] = (($sec < 10) ? '0' . $sec : $sec) . __('After time s');
+ }
+
+ $attr = __('After time') . ' ' . implode(' ', $arr);
+
+ return '' . $attr . ':
';
+ },
+ ],
+ ['tag' => 'quote',
+ 'type' => 'block',
+ 'self nesting' => true,
+ 'attrs' => [
+ 'Def' => true,
+ 'no attr' => true,
+ ],
+ 'handler' => function($body, $attrs) {
+ if (isset($attrs['Def'])) {
+ $st = '
+@if($online) +@include('layouts/stats') +@endif \ No newline at end of file diff --git a/composer.json b/composer.json index eecb01e5..a1e1cb56 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ }, "require": { "php": ">=5.6.0", - "artoodetoo/dirk": "dev-master" + "artoodetoo/dirk": "dev-master", + "MioVisman/Parserus": "^0.9.1" } } diff --git a/composer.lock b/composer.lock index 5f7dc7f3..ce407c03 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ad22a23d7b225fa300553d8d0dcdaeb9", - "content-hash": "2eea8744cdbc34c8408e6d137176a8df", + "hash": "f222f403c8d48f00cb5e3e37f680ebcb", + "content-hash": "b8c1ffae094a89502ad8a3c60385d4b8", "packages": [ { "name": "artoodetoo/dirk", @@ -52,6 +52,48 @@ "views" ], "time": "2017-01-10 21:38:22" + }, + { + "name": "miovisman/parserus", + "version": "0.9.1", + "source": { + "type": "git", + "url": "https://github.com/MioVisman/Parserus.git", + "reference": "d039178d46c989ebb8eb89970b52eb6ce927c817" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MioVisman/Parserus/zipball/d039178d46c989ebb8eb89970b52eb6ce927c817", + "reference": "d039178d46c989ebb8eb89970b52eb6ce927c817", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parserus": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Visman", + "email": "mio.visman@yandex.ru", + "homepage": "https://github.com/MioVisman" + } + ], + "description": "BBCode parser.", + "homepage": "https://github.com/MioVisman/Parserus", + "keywords": [ + "bbcode", + "parser" + ], + "time": "2016-11-26 06:56:51" } ], "packages-dev": [], diff --git a/public/.htaccess b/public/.htaccess index 5639360b..ad1d367a 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -4,7 +4,7 @@ AddDefaultCharset UTF-8 RewriteEngine On #RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d + #RewriteCond %{REQUEST_FILENAME} !-d #RewriteCond %{REQUEST_URI} !/public/.*\.\w+$ RewriteCond %{REQUEST_URI} !favicon\.ico RewriteRule . index.php [L] diff --git a/public/img/sm/big_smile.png b/public/img/sm/big_smile.png new file mode 100644 index 0000000000000000000000000000000000000000..1c4ea81f3a54f6ac7961f9bbbf96db62dd7018cd GIT binary patch literal 373 zcmV-*0gC>KP)'; + } else { + $st = '
'; + } + + return $st . $body . '
'; + }, + ], + ['tag' => 'spoiler', + 'type' => 'block', + 'self nesting' => true, + 'attrs' => [ + 'Def' => true, + 'no attr' => true, + ], + 'handler' => function($body, $attrs) { + if (isset($attrs['Def'])) { + $st = '
+ @endforeach @@ -112,3 +120,6 @@ @endif @yield('crumbs')▼' . $attrs['Def'] . '- {{ $topicName }}
+{{ $topic['subject'] }}
@foreach($posts as $post)- -+{{ $topicName }} - #{!! $post['post_number'] !!}
++ {{ $topic['subject'] }} - #{!! $post['post_number'] !!}
#{!! $post['post_number'] !!} -- @endif - + @endif -+ {!! $post['message'] !!} ++@if(isset($signs[$post['poster_id']])) +++@endif
+ {!! $signs[$post['poster_id']] !!} +z!o&f*YV&%;z|Jp|X U;m;KL6EL|Jy3no-Y3DJO9Nh^|?jpoB%K;5cshw=H_tm z00001bW%=J06^y0W&i*Hhe1RCwB5&sP$IFbo7xYz#`U2{okm-rN1}7HPvw26pt} zxB9T{Atb>GTAz=HOIp-TyxCi8PrGCqW4nkVV|eDtjo8KA_l@CMrq4I82YIHo&hpju z@fi`j`;T}?LFbo%LbxiyvV!}~y{Dw8spS62m kfw*L18=f(*( TqA@7w00000NkvXXu0mjf&M~#W literal 0 HcmV?d00001 diff --git a/public/img/sm/cool.png b/public/img/sm/cool.png new file mode 100644 index 0000000000000000000000000000000000000000..54ec46f6e9381190f6b3fdbcb47d2e72760d538a GIT binary patch literal 380 zcmV-?0fYXDP) i`iE5&iK1;+p_sVq!){MgRZ+DJd!c-vIvQ0RPwk|FZxj8UX*g0P~;#_@e;*`2hdz zd;iP;|J4AusT0PB0RF51=$!!Kr3ZI%TnPyYiG*@BE*SI6JpbZc{qzyFv=RTm0RH3- zJ3BirE-t>dD&C_pr+OJCAP};95&zL=|Ltf0;d}q#K0`er|J7FPyLkKn0004WQchC< zK<3zH0001&Nkl Q?!(EDS&i4n>h)|FZ-rOqL*z~E3tQMB7q-dl9dMV{w^ z>(Y5io^T-qPjr!H r86@-JNb1tnj(g0ZRvqoCd a`F}t4p9zOyzihq$0000 Hz=O00993;+p{d@c{p`0ROrG_@e+SDk?@sM*r?(|LuGI`2hdp z692&f{QMFB%mDw?0P~;#{q-t0E*1XrM*i_){_GLPhXDQb5#pr>{;UA$odEyUW&h?q z|Iu9k<758qD*xLm<;^Gm$wt *L_t(|+Fj2_5`!=d1yGtB z1{``z=%n|a`(G@w!zzC?<9X7;lBPmr;GOj9-{bJf$;5}4L7v^6D6U0WmO_l(u$7Ix zFAC3tI&AhijDuyCxh~Xdce@hsyo#czng_?rxdt1E`M%GPk#L8CEgdn&kZ#5HNg7)^ zAwV29Em$+LWf+9u&`zl-|F7c&Cvj*ybn#n=a?b50Ra6branhEWlK=O80jw1aPv>*_ Q^#A|>07*qoM6N<$g1`981^@s6 literal 0 HcmV?d00001 diff --git a/public/img/sm/lol.png b/public/img/sm/lol.png new file mode 100644 index 0000000000000000000000000000000000000000..bad071878c6540ba172c144a9e0907212ddb0080 GIT binary patch literal 364 zcmV-y0h9iTP) `f|Nq8^0RPwk L@OxkWH05dQ2t z|HwxFxd8v#MngRz|HVH4=0^YFKL6lj|Ltf0)mH!GR{!C9=$ruZpa3T7T8RJv00DGT zPE!Ct=GbNc004wZL_t(|+Fj4r4udcZh2b>ow532Po9?~W`@bAE5sJij_bZVtiw>CR z;rjSI-?t1d)8aKE-!e}*QB*Zh0d<*!Iy*|Gl$7_&X@8*S`Pf^4I Lf66s{=4D*%M~n7!)yvcpwA#K8BEjsf7);k?`r{_)(GWo&Xz;~0000< KMNUMnLSTZ8ex&ID literal 0 HcmV?d00001 diff --git a/public/img/sm/mad.png b/public/img/sm/mad.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ffcc61530be5dab6679949500dd815d6d5bc4e GIT binary patch literal 409 zcmV;K0cQS*P) <00000;hF#u5fLUC06H%i|GNPF^D6(>0R8k4{^ (wVq*W^LjT-i|LkPqqzC@x07gbe#)km^%mC<|0ROZA z{;UAA0088q8TGkE_^~SG%_slKM*i(8=g>L%suBOiD*xbn{_GJ$Jt6<&R{zC5|J7Fi z+baLS5&zl|{{R3OD9%Cv0004WQchC 00000NkvXXu0mjf De?-0w literal 0 HcmV?d00001 diff --git a/public/img/sm/neutral.png b/public/img/sm/neutral.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e4cb8c9caf3de9dac165a49c28b2b3beb036f6 GIT binary patch literal 415 zcmV;Q0bu@#P) >|{_kUAVq*X8 zd;aAB|G@zN%mDw?0RQ6>{rCX<{1G=U75()p#)km@tN`hq0R8L{{qzyqp)dc{W&hqn z;Hd!lsuAR*8RMW1|G*LWu`2%UD*xIM|HUfxxkdlUM*rI?<;^Gl%wp%zIsf5%|KMZ) z<5vI0K0`er|J7Cr$BFa+0004WQchC 6M6E^oynZtha?jdt^?<&_wamtUL_xS3#O2JKj(&*AP7Tfe>H>BbzR?w z4yR)Y>ugt5o(DZ2wkr;9yP_z%HrsEOYp^6V&vQy-ij@Fc5uucl$VzNSo5_f9ow#nm z6T~bjrNv@cBYpoONm-i2wi2`7g9OIdt{p=&KuS4{K!$(+`vQzW3;M_U#x(!{002ov JPDHLkV1f(zzjOcq literal 0 HcmV?d00001 diff --git a/public/img/sm/roll.png b/public/img/sm/roll.png new file mode 100644 index 0000000000000000000000000000000000000000..1c3bc7e50b3a99a1714ee9c13a73fd1718a91720 GIT binary patch literal 386 zcmV-|0e$|7P) %w9RKcR2?+_>p)mjB6E!Oo{;UAtsQ~}X0RPwk z|KUFW)mH!GR{!8*|HVG#%_ry3IsegT|KWSt&qmCU0NBVo-?n1XqDADS8RMW1PPOgE z00001bW%=J06^y0W&i*Hlu1NERCwB5kJl1|AP@kRc4#cb5@L&{_x}I?mOJ8;JIpf6 zJpi5q2;9f2cZcKt)Q>0arI{hOYuCEzxs2noLA^S_uAfPoCU?@i$MGuA0WPNxU@tWC z*B6@?k=9W~$^^E|3xG e?R);^0R8v?{^S$<{1N}m z0RPnh|G@zNvjFm-0R5~6{q-t0E*1XsM*r?){_$e{>=FI+5ypoA>74-Dp)dc{W&h?q z|KnresQ~`%D&@^5|H(%G+$#0CMgQ7H|KAY*#VY>HV)(Ht taJaA^+7@|HVH4<5p+-tK0wp00DGTPE!Ct=GbNc005*(L_t(|+Fj2_62dSH1yB-q zC#3gANJ8%&?td|4lNEn7<9YHz(nN=;z&q;Me}>mPXCrUr6msw8OmkIMRaHKQ?x-7i zUlg_twOem<`1Y1rW?4|1!|BYxb>sVfQ+xYMdIb|KT-OztgpzWw>0luQcFZ77lnvrw zrV^zsc!CU*5n>t Yk^-}eQwa11W?Z7jI} O0000 Hs7f0QjQ-|J?xp-~j*F0{{R3;+p{d@&Nt#0RHhqDk>^QMn+;{V*l-X|FZ!9x&Z#= z0RPkg^Pm9# eKc8yG>x^Bb{Jg%^e0_(
OV literal 0 HcmV?d00001 diff --git a/public/img/sm/tongue.png b/public/img/sm/tongue.png new file mode 100644 index 0000000000000000000000000000000000000000..397b98ae4f0ded90ef2e07a8130195fe05642e32 GIT binary patch literal 416 zcmV;R0bl-!P) gZ>=FOt5C80C|HnfA<751*3IEIh z>74+^hXA!2E#=K8{^J?{+D88ED(BES|Jy3{xkdlQD)_M~ yV)Z0h^~sX;z8AK&0iu9D**$yJkIE&J?#nrFwU=bj^+HkB_87RCW$8N9 zwmP~n-s}uxHiJ5x&KIJQB2P`zO!MM-YZ7pViQ_l{W=KR7-1jgE0_^!M36&!T!9df2 zYaIEEF&W1)q+IP!T7*#li6vc)2T>u!x0mlxA>dIIc9bjq|L+IgI}8<>5n^}%0000< KMNUMnLSTYhjl)L( literal 0 HcmV?d00001 diff --git a/public/img/sm/wink.png b/public/img/sm/wink.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc40af40303418a4ff7bcf26af55f6af52a64d8 GIT binary patch literal 428 zcmV;d0aN~oP) T0ROrG|I`5d{1N}} zbvG^*{^|h#$^gcP0RHO$+MzG~^eF%15B}>B{qzz4tN{M;M*i_)|J7w;Vq*Wl2LIMq z|K>gEodEcx0RO}# {x@B1}FCKf|?ZYjl8KtZKJqHP*IB^^&iKBD%;Z~-huIrkw*nNkX zWdn3g^9@-A%&jU02t(R3OGK3w;F%Z^{{JGdRXeB{C%=J^Wm#q_C<4-9+# Jf(k0000 Y00000;+g k43{HqE5^by8~0Myjf>74+XPfz0}Cc0i;)}Ai)xkcs8C*Gqm z{>) pTC+M&zU!{^ L-#VY^dd;j4+|HVH4)mGrC0RP%X|G*Lb z@&NzaD*wy?;GF>f+YvqkgS!9#00DGTPE!Ct=GbNc005RrL_t(|+Fj4r5`!QVMNy5$ z3)s8Hmfm~M|NmGHlBevs!?1=2K )CmhGl&hu;=^E_-;1m~si`(^HY?!^|4)(=uy0fXa+s)4McP&$bTH4T;;) array($vendorDir . '/miovisman/parserus'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index e108cb5f..a271f3ab 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -28,11 +28,22 @@ class ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7 ), ); + public static $prefixesPsr0 = array ( + 'P' => + array ( + 'Parserus' => + array ( + 0 => __DIR__ . '/..' . '/miovisman/parserus', + ), + ), + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit90ad93c7251d4f60daa9e545879c49e7::$prefixesPsr0; }, null, ClassLoader::class); } diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 87567c11..b70afd12 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -46,5 +46,49 @@ "templating", "views" ] + }, + { + "name": "miovisman/parserus", + "version": "0.9.1", + "version_normalized": "0.9.1.0", + "source": { + "type": "git", + "url": "https://github.com/MioVisman/Parserus.git", + "reference": "d039178d46c989ebb8eb89970b52eb6ce927c817" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MioVisman/Parserus/zipball/d039178d46c989ebb8eb89970b52eb6ce927c817", + "reference": "d039178d46c989ebb8eb89970b52eb6ce927c817", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "time": "2016-11-26 06:56:51", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Parserus": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Visman", + "email": "mio.visman@yandex.ru", + "homepage": "https://github.com/MioVisman" + } + ], + "description": "BBCode parser.", + "homepage": "https://github.com/MioVisman/Parserus", + "keywords": [ + "bbcode", + "parser" + ] } ] diff --git a/vendor/miovisman/parserus/LICENSE b/vendor/miovisman/parserus/LICENSE new file mode 100644 index 00000000..2896d351 --- /dev/null +++ b/vendor/miovisman/parserus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Visman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/miovisman/parserus/Parserus.php b/vendor/miovisman/parserus/Parserus.php new file mode 100644 index 00000000..5dba4d80 --- /dev/null +++ b/vendor/miovisman/parserus/Parserus.php @@ -0,0 +1,1222 @@ + + * @link https://github.com/MioVisman/Parserus + * @license https://opensource.org/licenses/MIT The MIT License (MIT) + */ + +class Parserus +{ + /** + * Массив дерева тегов построенный методом parse() + * @var array + */ + protected $data; + + /** + * Индекс последнего элемента из массива data + * @var int + */ + protected $dataId; + + /** + * Индекс текущего элемента дерева из массива data + * @var int + */ + protected $curId; + + /** + * Битовая маска флагов для функции htmlspecialchars() + * @var int + */ + protected $eFlags; + + /** + * Массив искомых значений для замены при преобразовании текста в HTML + * @var array + */ + protected $tSearch = ["\n", "\t", ' ', ' ']; + + /** + * Массив значений замены при преобразовании текста в HTML + * @var array + */ + protected $tRepl; + + /** + * Массив разрешенных тегов. Если null, то все теги из bbcodes разрешены + * @var array|null + */ + protected $whiteList = null; + + /** + * Массив запрещенных тегов. Если null, то все теги из bbcodes разрешены + * @var array|null + */ + protected $blackList = null; + + /** + * Ассоциативный массив bb-кодов + * @var array + */ + protected $bbcodes = []; + + /** + * Ассоциативный массив переменных, которые можно использовать в bb-кодах + * @var array + */ + protected $attrs = []; + + /** + * Ассоциативный массив смайлов + * @var array + */ + protected $smilies = []; + + /** + * Паттерн для поиска смайлов в тексте при получении HTML + * @var string|null + */ + protected $smPattern = null; + + /** + * Флаг необходимости обработки смайлов при получении HTML + * @var bool + */ + protected $smOn = false; + + /** + * Шаблон подстановки при обработке смайлов + * Например: + * @var string + */ + protected $smTpl = ''; + + /** + * Имя тега под которым идет отображение смайлов + * @var string + */ + protected $smTag = ''; + + /** + * Список тегов в которых не нужно отображать смайлы + * @var array + */ + protected $smBL = []; + + /** + * Массив ошибок полученных при отработке метода parse() + * @var array + */ + protected $errors = []; + + /** + * Флаг строгого режима поиска ошибок + * Нужен, например, для проверки атрибутов тегов при получении текста от пользователя + * @var bool + */ + protected $strict = false; + + /** + * Конструктор + * + * @param int $flag Один из флагов ENT_HTML401, ENT_XML1, ENT_XHTML, ENT_HTML5 + */ + public function __construct($flag = ENT_HTML5) + { + if (! in_array($flag, [ENT_HTML401, ENT_XML1, ENT_XHTML, ENT_HTML5])) { + $flag = ENT_HTML5; + } + $this->eFlags = $flag | ENT_QUOTES | ENT_SUBSTITUTE; + $this->tRepl = in_array($flag, [ENT_HTML5, ENT_HTML401]) + ? ['
', ' ', ' ', ' '] + : ['
', ' ', ' ', ' ']; + } + + /** + * Метод добавляет один bb-код + * + * @param array $bb Массив описания bb-кода + * @return Parserus $this + */ + public function addBBCode(array $bb) + { + $res = [ + 'type' => 'inline', + 'parents' => ['inline' => 1, 'block' => 2], + 'auto' => true, + 'self nesting' => false, + ]; + + if ($bb['tag'] === 'ROOT') { + $tag = 'ROOT'; + } else { + $tag = strtolower($bb['tag']); + } + + if (isset($bb['type'])) { + $res['type'] = $bb['type']; + + if ($bb['type'] !== 'inline') { + $res['parents'] = ['block' => 1]; + $res['auto'] = false; + } + } + + if (isset($bb['parents'])) { + $res['parents'] = array_flip($bb['parents']); + } + + if (isset($bb['auto'])) { + $res['auto'] = (bool) $bb['auto']; + } + + if (isset($bb['self nesting'])) { + $res['self nesting'] = (bool) $bb['self nesting']; + } + + if (isset($bb['recursive'])) { + $res['recursive'] = true; + } + + if (isset($bb['text only'])) { + $res['text only'] = true; + } + + if (isset($bb['tags only'])) { + $res['tags only'] = true; + } + + if (isset($bb['single'])) { + $res['single'] = true; + } + + if (isset($bb['pre'])) { + $res['pre'] = true; + } + + $res['handler'] = isset($bb['handler']) ? $bb['handler'] : null; + + $required = []; + $attrs = []; + $other = false; + + if (! isset($bb['attrs'])) { + $cur = []; + + if (isset($bb['body format'])) { + $cur['body format'] = $bb['body format']; + } + if (isset($bb['text only'])) { + $cur['text only'] = true; + } + + $attrs['no attr'] = $cur; + } else { + foreach ($bb['attrs'] as $attr => $cur) { + if (! is_array($cur)) { + $cur = []; + } + + if (isset($bb['text only'])) { + $cur['text only'] = true; + } + + $attrs[$attr] = $cur; + + if (isset($cur['required'])) { + $required[] = $attr; + } + + if ($attr !== 'Def' && $attr !== 'no attr') { + $other = true; + } + } + } + + $res['attrs'] = $attrs; + $res['required'] = $required; + $res['other'] = $other; + + $this->bbcodes[$tag] = $res; + return $this; + } + + /** + * Метод задает массив bb-кодов + * + * @param array $bbcodes Массив описаний bb-кодов + * @return Parserus $this + */ + public function setBBCodes(array $bbcodes) + { + $this->bbcodes = []; + + foreach ($bbcodes as $bb) { + $this->addBBCode($bb); + } + + $this->defaultROOT(); + return $this; + } + + /** + * Метод устанавливает тег ROOT при его отсутствии + */ + protected function defaultROOT() + { + if (! isset($this->bbcodes['ROOT'])) { + $this->addBBCode(['tag' => 'ROOT', 'type' => 'block']); + } + } + + /** + * Метод задает массив смайлов + * + * @param array $smilies Ассоциативный массив смайлов + * @return Parserus $this + */ + public function setSmilies(array $smilies) + { + $this->smilies = $smilies; + $this->createSmPattern(); + return $this; + } + + /** + * Метод генерирует паттерн для поиска смайлов в тексте + */ + protected function createSmPattern() + { + if (empty($this->smilies)) { + $this->smPattern = null; + return; + } + + $arr = array_keys($this->smilies); + sort($arr); + $arr[] = ' '; + + $symbol = ''; + $pattern = ''; + $quote = ''; + $sub = []; + + foreach ($arr as $val) { + if (preg_match('%^(.)(.+)%u', $val, $match)) { + if ($symbol === $match[1]) { + $sub[] = preg_quote($match[2], '%'); + } else { + if (count($sub) > 1) { + $pattern .= $quote . preg_quote($symbol, '%') . '(?:' . implode('|', $sub) . ')'; + $quote = '|'; + } else if (count($sub) == 1) { + $pattern .= $quote . preg_quote($symbol, '%') . $sub[0]; + $quote = '|'; + } + $symbol = $match[1]; + $sub = [preg_quote($match[2], '%')]; + } + } + } + + $this->smPattern = '%(?<=\s|^)(?:' . $pattern . ')(?![\p{L}\p{N}])%u'; + } + + /** + * Метод устанавливает шаблон для отображения смайлов + * + * @param string $tpl Строка шаблона, например: + * @param string $tag Имя тега под которым идет отображение смайлов + * @param array $bl Список тегов в которых не нужно отображать смайлы + * @return Parserus $this + */ + public function setSmTpl($tpl, $tag = 'img', array $bl = ['url']) + { + $this->smTpl = $tpl; + $this->smTag = $tag; + $this->smBL = array_flip($bl); + return $this; + } + + /** + * Метод включает (если есть возможность) отображение смайлов на текущем дереве тегов + * + * @return Parserus $this + */ + public function detectSmilies() + { + $this->smOn = null !== $this->smPattern && isset($this->bbcodes[$this->smTag]); + return $this; + } + + /** + * Метод устанавливает список разрешенных bb-кодов + * + * @param mixed $list Массив bb-кодов, null и т.д. + * @return Parserus $this + */ + public function setWhiteList($list = null) + { + $this->whiteList = is_array($list) ? $list : null; + return $this; + } + + /** + * Метод устанавливает список запрещенных bb-кодов + * + * @param mixed $list Массив bb-кодов, null и т.д. + * @return Parserus $this + */ + public function setBlackList($list = null) + { + $this->blackList = ! empty($list) && is_array($list) ? $list : null; + return $this; + } + + /** + * Метод задает значение переменной для возможного использования в bb-кодах + * + * @param string $name Имя переменной + * @param mixed $val Значение переменной + * @return Parserus $this + */ + public function setAttr($name, $val) + { + $this->attrs[$name] = $val; + return $this; + } + + /** + * Метод для получения значения переменной + * + * @param string $name Имя переменной + * @return mixed|null Значение переменной или null, если переменная не была задана ранее + */ + public function attr($name) + { + return isset($this->attrs[$name]) ? $this->attrs[$name] : null; + } + + /** + * Метод добавляет новый тег в дерево тегов + * + * @param string $tag Имя тега + * @param int $parentId Указатель на родителя + * @param array $attrs Массив атрибутов тега + * @param bool $textOnly Флаг. Если true, то в теле только текст + * @return int Указатель на данный тег + */ + protected function addTagNode($tag, $parentId = null, $attrs = [], $textOnly = false) + { + $this->data[++$this->dataId] = [ + 'tag' => $tag, + 'parent' => $parentId, + 'children' => [], + 'attrs' => $attrs, + ]; + + if ($textOnly) { + $this->data[$this->dataId]['text only'] = true; + } + + if (null !== $parentId) { + $this->data[$parentId]['children'][] = $this->dataId; + } + + return $this->dataId; + } + + /** + * Метод добавляет текстовый узел в дерево тегов + * + * @param string $text Текст + * @param int $parentId Указатель на родителя + * @return string Пустая строка + */ + protected function addTextNode($text, $parentId) + { + if (isset($text[0])) { + + $this->data[++$this->dataId] = [ + 'text' => $text, + 'parent' => $parentId, + ]; + + $this->data[$parentId]['children'][] = $this->dataId; + } + + return ''; + } + + /** + * Метод нормализует содержимое атрибута + * + * @param string $attr Содержимое атрибута полученное из регулярного выражения + * @return string + */ + protected function getNormAttr($attr) + { + // удаление крайних кавычек + if (isset($attr[1]) + && $attr[0] === $attr[strlen($attr) - 1] + && ($attr[0] === '"' || $attr[0] === '\'') + ) { + return substr($attr, 1, -1); + } + + return $attr; + } + + /** + * Метод выделяет все атрибуты с их содержимым для обрабатываемого тега + * + * @param string $tag Имя обрабатываемого тега + * @param string $type "Тип атрибутов" = ' ', '=' или ']' + * @param string $text Текст из которого выделяются атрибуты + * @return null|array + */ + protected function parseAttrs($tag, $type, $text) + { + $attrs = []; + $tagText = ''; + + if ($type === '=') { + $pattern = '%^(?!\x20) + ("[^\x00-\x1f"]*(?:"+(?!\x20*+\]|\x20++[a-z-]{2,15}=)[^\x00-\x1f"]*)*" + |\'[^\x00-\x1f\']*(?:\'+(?!\x20*+\]|\x20++[a-z-]{2,15}=)[^\x00-\x1f\']*)*\' + |[^\x00-\x20\]]+(?:\x20++(?!\]|[a-z-]{2,15}=)[^\x00-\x20\]]+)*) + \x20* + (\]|\x20(?=[a-z-]{2,15}=))%x'; + + $match = preg_split($pattern, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (! isset($match[1])) { + return null; + } + + $type = $match[2]; + $tagText .= $match[1] . $match[2]; + $text = $match[3]; + + $tmp = $this->getNormAttr($match[1]); + if (isset($tmp[0])) { + $attrs['Def'] = $tmp; + + // в теге не может быть первичного атрибута + if ($this->strict + && ! isset($this->bbcodes[$tag]['attrs']['Def']) + ) { + $this->errors[] = [7, $tag]; + return null; + } + } + } + + if ($type !== ']') { + $pattern = '%^\x20*+([a-z-]{2,15}) + =(?!\x20) + ("[^\x00-\x1f"]*(?:"+(?!\x20*+\]|\x20++[a-z-]{2,15}=)[^\x00-\x1f"]*)*" + |\'[^\x00-\x1f\']*(?:\'+(?!\x20*+\]|\x20++[a-z-]{2,15}=)[^\x00-\x1f\']*)*\' + |[^\x00-\x20\]]+(?:\x20++(?!\]|[a-z-]{2,15}=)[^\x00-\x20\]]+)*) + \x20* + (\]|\x20(?=[a-z-]{2,15}=))%x'; + + do { + $match = preg_split($pattern, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (! isset($match[1])) { + return null; + } + + $tagText .= $match[1] . '=' . $match[2] . $match[3]; + $text = $match[4]; + + $tmp = $this->getNormAttr($match[2]); + if (isset($tmp[0])) { + $attrs[$match[1]] = $tmp; + + if ($this->strict) { + // в теге не может быть вторичных атрибутов + if (! $this->bbcodes[$tag]['other']) { + $this->errors[] = [8, $tag]; + return null; + } + // этот атрибут отсутвтует в описании тега + if (! isset($this->bbcodes[$tag]['attrs'][$match[1]])) { + $this->errors[] = [10, $tag, $match[1]]; + return null; + } + } + } + + } while ($match[3] !== ']'); + } + + if (empty($attrs)) { + // в теге должны быть атрибуты + if (! empty($this->bbcodes[$tag]['required']) + || ! isset($this->bbcodes[$tag]['attrs']['no attr']) + ) { + $this->errors[] = [6, $tag]; + return null; + } + } else { + foreach ($this->bbcodes[$tag]['required'] as $key) { + // нет обязательного атрибута + if (! isset($attrs[$key])) { + $this->errors[] = [13, $tag, $key]; + return null; + } + } + } + + return [ + 'attrs' => $attrs, + 'tag' => $tagText, + 'text' => $text, + ]; + } + + /** + * Метод определяет указатель на родительский тег для текущего + * + * @param string $tag Имя тега + * @return int|false false, если невозможно подобрать родителя + */ + protected function findParent($tag) + { + if (false === $this->bbcodes[$tag]['self nesting']) { + $curId = $this->curId; + + while (null !== $curId) { + // этот тег нельзя открыть внутри аналогичного + if ($this->data[$curId]['tag'] === $tag) { + $this->errors[] = [12, $tag]; + return false; + } + $curId = $this->data[$curId]['parent']; + } + } + + $curId = $this->curId; + $curTag = $this->data[$curId]['tag']; + + while (null !== $curId) { + if (isset($this->bbcodes[$tag]['parents'][$this->bbcodes[$curTag]['type']])) { + return $curId; + } else if ($this->bbcodes[$tag]['type'] === 'inline' + || false === $this->bbcodes[$curTag]['auto'] + ) { + // тег не может быть открыт на этой позиции + $this->errors[] = [3, $tag, $this->data[$this->curId]['tag']]; + return false; + } + + $curId = $this->data[$curId]['parent']; + $curTag = $this->data[$curId]['tag']; + } + + $this->errors[] = [3, $tag, $this->data[$this->curId]['tag']]; + return false; + } + + /** + * Метод проводит проверку значений атрибутов и(или) тела тега на соответствие правилам + * + * @param string $tag Имя тега + * @param array $attrs Массив атрибутов + * @param string $text Текст из которого выделяется тело тега + * @return array|false false в случае ошибки + */ + protected function validationTag($tag, $attrs, $text) + { + if (empty($attrs)) { + $attrs['no attr'] = null; + } + + $body = null; + $end = null; + $tested = []; + $flag = false; + $bb = $this->bbcodes[$tag]; + + foreach ($attrs as $key => $val) { + // проверка формата атрибута + if (isset($bb['attrs'][$key]['format']) + && ! preg_match($bb['attrs'][$key]['format'], $val) + ) { + $this->errors[] = [9, $tag, $key]; + return false; + } + + // для рекурсивного тега тело не проверяется даже если есть правила + if (isset($bb['recursive'])) { + continue; + } + + // тело тега + if (null === $body + && (isset($bb['attrs'][$key]['body format']) + || isset($bb['attrs'][$key]['text only'])) + ) { + $ptag = preg_quote($tag, '%'); + $match = preg_split('%^([^\[]*(?:\[(?!/' . $ptag . '\])[^\[]*)*)(?:\[/' . $ptag . '\])?%i', $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + $body = $match[1]; + $end = $match[2]; + } + + // для тега с 'text only' устанавливается флаг для возврата тела + if (isset($bb['attrs'][$key]['text only'])) { + $flag = true; + } + + // проверка формата тела тега + if (isset($bb['attrs'][$key]['body format'])) { + if (isset($tested[$bb['attrs'][$key]['body format']])) { + continue; + } else if (! preg_match($bb['attrs'][$key]['body format'], $body)) { + $this->errors[] = [11, $tag]; + return false; + } + + $tested[$bb['attrs'][$key]['body format']] = true; + } + } + + unset($attrs['no attr']); + + return [ + 'attrs' => $attrs, + 'body' => $flag ? $body : null, + 'end' => $end, + ]; + } + + /** + * Метод закрывает текущий тег + * + * @param string $tag Имя обрабатываемого тега + * @param string $curText Текст до тега, который еще не был учтен + * @param string $tagText Текст самого тега - [/tag] + * @return string Пустая строка, если тег удалось закрыть + */ + protected function closeTag($tag, $curText, $tagText) { + // ошибка одиночного тега + if (isset($this->bbcodes[$tag]['single'])) { + $this->errors[] = [5, $tag]; + return $curText . $tagText; + } + + $curId = $this->curId; + $curTag = $this->data[$curId]['tag']; + + while ($curTag !== $tag && $curId > 0) { + if (false === $this->bbcodes[$curTag]['auto']) { + break; + } + + $curId = $this->data[$curId]['parent']; + $curTag = $this->data[$curId]['tag']; + } + + // ошибка закрытия тега + if ($curTag !== $tag) { + $this->errors[] = [4, $tag]; + return $curText . $tagText; + } + + $this->addTextNode($curText, $this->curId); + + $this->curId = $this->data[$curId]['parent']; + return ''; + } + + /** + * Сброс состояния + * + * @param array $opts Ассоциативный массив опций + */ + protected function reset(array $opts) + { + $this->defaultROOT(); + $this->data = []; + $this->dataId = -1; + $this->curId = $this->addTagNode( + isset($opts['root']) && isset($this->bbcodes[$opts['root']]) + ? $opts['root'] + : 'ROOT' + ); + $this->smOn = false; + $this->errors = []; + $this->strict = isset($opts['strict']) ? (bool) $opts['strict'] : false; + } + + /** + * Метод строит дерево тегов из текста содержащего bb-коды + * + * @param string $text Обрабатываемый текст + * @param array $opts Ассоциативный массив опций + * @return Parserus $this + */ + public function parse($text, $opts = []) + { + $this->reset($opts); + $curText = ''; + $recCount = 0; + + $text = str_replace("\r\n", "\n", $text); + $text = str_replace("\r", "\n", $text); + + while (($match = preg_split('%(\[(/)?(' . ($recCount ? $recTag : '[a-z\*][a-z\d-]{0,10}') . ')((?(1)\]|[=\]\x20])))%i', $text, 2, PREG_SPLIT_DELIM_CAPTURE)) + && isset($match[1]) + ) { + /* $match[0] - текст до тега + * $match[1] - [ + (|/) + имя тега + (]| |=) + * $match[2] - (|/) + * $match[3] - имя тега + * $match[4] - тип атрибутов --> (]| |=) + * $match[5] - остаток текста до конца + */ + $tagText = $match[1]; + $curText .= $match[0]; + $text = $match[5]; + $tag = strtolower($match[3]); + + if (! isset($this->bbcodes[$tag])) { + $curText .= $tagText; + continue; + } + + if (! empty($match[2])) { + if ($recCount && --$recCount) { + $curText .= $tagText; + } else { + $curText = $this->closeTag($tag, $curText, $tagText); + } + continue; + } + + $attrs = $this->parseAttrs($tag, $match[4], $text); + + if (null === $attrs) { + $curText .= $tagText; + continue; + } + + if (isset($attrs['tag'][0])) { + $tagText .= $attrs['tag']; + $text = $attrs['text']; + } + + if ($recCount) { + ++$recCount; + $curText .= $tagText; + continue; + } + + if (null !== $this->blackList && in_array($tag, $this->blackList)) { + $curText .= $tagText; + $this->errors[] = [1, $tag]; + continue; + } + + if (null !== $this->whiteList && ! in_array($tag, $this->whiteList)) { + $curText .= $tagText; + $this->errors[] = [2, $tag]; + continue; + } + + if (($parentId = $this->findParent($tag)) === false) { + $curText .= $tagText; + continue; + } + + if (($attrs = $this->validationTag($tag, $attrs['attrs'], $text)) === false) { + $curText .= $tagText; + continue; + } + + $curText = $this->addTextNode($curText, $this->curId); + + $id = $this->addTagNode( + $tag, + $parentId, + $attrs['attrs'], + isset($attrs['body']) || isset($this->bbcodes[$tag]['text only']) + ); + + if (isset($attrs['body'])) { + $this->addTextNode($attrs['body'], $id); + + $text = $attrs['end']; + $this->curId = $parentId; + + } else if (isset($this->bbcodes[$tag]['single'])) { + $this->curId = $parentId; + + } else { + $this->curId = $id; + + if (isset($this->bbcodes[$tag]['recursive'])) { + $recCount = 1; + $recTag = preg_quote($tag, '%'); + } + } + } + + $this->addTextNode($curText . $text, $this->curId); + return $this; + } + + /** + * Метод возвращает HTML построенный на основании дерева тегов + * + * @param int $id Указатель на текущий тег + * @return string + */ + public function getHtml($id = 0) + { + if (isset($this->data[$id]['tag'])) { + + $body = ''; + foreach ($this->data[$id]['children'] as $cid) { + $body .= $this->getHtml($cid); + } + + $bb = $this->bbcodes[$this->data[$id]['tag']]; + + if (null === $bb['handler']) { + return $body; + } + + $attrs = []; + foreach ($this->data[$id]['attrs'] as $key => $val) { + if (isset($bb['attrs'][$key])) { + $attrs[$key] = $this->e($val); + } + } + + return $bb['handler']($body, $attrs, $this); + } + + $pid = $this->data[$id]['parent']; + $bb = $this->bbcodes[$this->data[$pid]['tag']]; + + if (isset($bb['tags only'])) { + return ''; + } + + switch (2 * (end($this->data[$pid]['children']) === $id) + + ($this->data[$pid]['children'][0] === $id) + ) { + case 1: + $text = $this->e(preg_replace('%^\x20*\n%', '', $this->data[$id]['text'])); + break; + case 2: + $text = $this->e(preg_replace('%\n\x20*$%D', '', $this->data[$id]['text'])); + break; + case 3: + $text = $this->e(preg_replace('%^\x20*\n|\n\x20*$%D', '', $this->data[$id]['text'])); + break; + default: + $text = $this->e($this->data[$id]['text']); + break; + } + + if (empty($this->data[$pid]['text only']) + && $this->smOn + && isset($this->bbcodes[$this->smTag]['parents'][$bb['type']]) + && ! isset($this->smBL[$this->data[$pid]['tag']]) + ) { + $text = preg_replace_callback($this->smPattern, function($m) { + return str_replace( + ['{url}', '{alt}'], + [$this->e($this->smilies[$m[0]]), $this->e($m[0])], + $this->smTpl + ); + }, $text); + } + + if (! isset($bb['pre'])) { + $text = str_replace($this->tSearch, $this->tRepl, $text); + } + + return $text; + } + + /** + * Метод возвращает текст с bb-кодами построенный на основании дерева тегов + * + * @param int $id Указатель на текущий тег + * @return string + */ + public function getCode($id = 0) + { + if (isset($this->data[$id]['text'])) { + return $this->data[$id]['text']; + } + + $body = ''; + foreach ($this->data[$id]['children'] as $cid) { + $body .= $this->getCode($cid); + } + + if ($id === 0) { + return $body; + } + + $tag = $this->data[$id]['tag']; + $attrs = $this->data[$id]['attrs']; + + $def = ''; + $other = ''; + $count = count($attrs); + foreach ($attrs as $attr => $val) { + $quote = ''; + if ($count > 1 || strpbrk($val, ' \'"]')) { + $quote = '"'; + if (false !== strpos($val, '"') && false === strpos($val, '\'')) { + $quote = '\''; + } + } + if ($attr === 'Def') { + $def = '=' . $quote . $val . $quote; + } else { + $other .= ' ' . $attr . '=' . $quote . $val . $quote; + } + } + + return '[' . $tag . $def . $other . ']' . (isset($this->bbcodes[$tag]['single']) ? '' : $body . '[/' . $tag .']'); + } + + /** + * Метод ищет в текстовых узлах ссылки и создает на их месте узлы с bb-кодами url + * Для уменьшения нагрузки использовать при сохранении, а не при выводе + * + * @return Parserus $this + */ + public function detectUrls() + { + $pattern = '%\b(?<=\s|^) + (?>(?:ht|f)tps?://|www\.|ftp\.) + (?:[\p{L}\p{N}]+(?:[\p{L}\p{N}\-]*[\p{L}\p{N}])?\.)+ + \p{L}[\p{L}\p{N}\-]*[\p{L}\p{N}] + (?::\d{1,5})? + (?:/ + (?:[\p{L}\p{N};:@&=$_.+!*\'"(),\%/-]+)? + (?:\?[\p{L}\p{N};:@&=$_.+!*\'"(),\%-]+)? + (?:\#[\p{L}\p{N}-]+)? + )?%xu'; + + return $this->detect('url', $pattern, true); + } + + /** + * Метод ищет в текстовых узлах совпадения с $pattern и создает на их месте узлы с bb-кодами $tag + * + * @param string $tag Имя для создания bb-кода + * @param string $pattern Регулярное выражение для поиска + * @param bool $textOnly Флаг. true, если содержимое созданного тега текстовое + * @return Parserus $this + */ + protected function detect($tag, $pattern, $textOnly) + { + if (! isset($this->bbcodes[$tag])) { + return $this; + } + + $error = null; + if (null !== $this->blackList && in_array($tag, $this->blackList)) { + $error = 1; + } else if (null !== $this->whiteList && ! in_array($tag, $this->whiteList)) { + $error = 2; + } + + for ($id = $this->dataId; $id > 0; --$id) { + // не текстовый узел + if (! isset($this->data[$id]['text'])) { + continue; + } + + $pid = $this->data[$id]['parent']; + + // родитель может содержать только текст или не подходит по типу + if (isset($this->data[$pid]['text only']) || + ! isset($this->bbcodes[$tag]['parents'][$this->bbcodes[$this->data[$pid]['tag']]['type']]) + ) { + continue; + } + + if (! preg_match_all($pattern, $this->data[$id]['text'], $matches, PREG_OFFSET_CAPTURE)) { + continue; + } else if ($error) { + $this->errors[] = [$error, $tag]; + return $this; + } + + $idx = array_search($id, $this->data[$pid]['children']); + $arrEnd = array_slice($this->data[$pid]['children'], $idx + 1); + $this->data[$pid]['children'] = array_slice($this->data[$pid]['children'], 0, $idx); + + $pos = 0; + + foreach ($matches[0] as $match) { + $this->addTextNode(substr($this->data[$id]['text'], $pos, $match[1] - $pos), $pid); + + $new = $this->addTagNode($tag, $pid, [], $textOnly); + $this->addTextNode($match[0], $new); + + $pos = $match[1] + strlen($match[0]); + } + + $this->addTextNode($this->endStr($this->data[$id]['text'], $pos), $pid); + unset($this->data[$id]); + + $this->data[$pid]['children'] = array_merge($this->data[$pid]['children'], $arrEnd); + } + + return $this; + } + + /** + * Метод удаляет пустые теги из дерева + * + * @param string $mask Маска символов, которые не учитываются при определении пустоты текстовых узлов + * @param bool $flag Если true, то при пустом дереве оно не будет очищено, а останется без изменений, + * но будет оставлена ошибка, которая отобразится в getErrors() + * @return bool Если true, то дерево тегов пусто + */ + public function stripEmptyTags($mask = '', $flag = false) + { + if ($flag) { + $data = $this->data; + + if ($this->stripEmptyTags_($mask, 0)) { + $this->errors[] = [14]; + $this->data = $data; + return true; + } + return false; + + } else { + return $this->stripEmptyTags_($mask, 0); + } + } + + /** + * Метод рекурсивно удаляет пустые теги из дерева + * + * @param string $mask Маска символов, которые не учитываются при определении пустоты текстовых узлов + * @param int $id Указатель на текущий тег + * @return bool Если true, то тег/узел пустой + */ + protected function stripEmptyTags_($mask, $id) + { + // текстовый узел + if (isset($this->data[$id]['text'])) { + if (isset($mask[0])) { + return trim($this->data[$id]['text'], $mask) === ''; + } + return false; + } + + // одиночный тег + if (isset($this->bbcodes[$this->data[$id]['tag']]['single'])) { + return false; + } + + $res = true; + // перебор детей с удалением тегов + foreach ($this->data[$id]['children'] as $key => $cid) { + if ($this->stripEmptyTags_($mask, $cid)) { + if (isset($this->data[$cid]['tag'])) { + unset($this->data[$id]['children'][$key]); + unset($this->data[$cid]); + } + } else { + $res = false; + } + } + + if ($res) { + foreach ($this->data[$id]['children'] as $cid) { + unset($this->data[$cid]); + } + $this->data[$id]['children'] = []; + } + + return $res; + } + + /** + * Метод возвращает массив ошибок + * + * @param array $lang Массив строк шаблонов описания ошибок + * @return array + */ + public function getErrors(array $lang = []) + { + $defLang = [ + 1 => 'Тег [%1$s] находится в черном списке', + 2 => 'Тег [%1$s] отсутствует в белом списке', + 3 => 'Тег [%1$s] нельзя открыть внутри тега [%2$s]', + 4 => 'Не найден начальный тег для парного тега [/%1$s]', + 5 => 'Найден парный тег [/%1$s] для одиночного тега [%1$s]', + 6 => 'В теге [%1$s] отсутствуют атрибуты', + 7 => 'Тег [%1$s=...] не может содержать первичный атрибут', + 8 => 'Тег [%1$s ...] не может содержать вторичные атрибуты', + 9 => 'Атрибут \'%2$s\' тега [%1$s] не соответствует шаблону', + 10 => 'Тег [%1$s ...] содержит неизвестный вторичный атрибут \'%2$s\'', + 11 => 'Тело тега [%1$s] не соответствует шаблону', + 12 => 'Тег [%1$s] нельзя открыть внутри аналогичного тега', + 13 => 'В теге [%1$s] отсутствует обязательный атрибут \'%2$s\'', + 14 => 'Все теги пустые' + ]; + + $errors = []; + + foreach ($this->errors as $args) { + $err = array_shift($args); + + if (isset($lang[$err])) { + $text = $lang[$err]; + } else if (isset($defLang[$err])) { + $text = $defLang[$err]; + } else { + $text = 'Unknown error'; + } + + $errors[] = vsprintf($text, array_map([$this, 'e'], $args)); + } + + return $errors; + } + + /** + * Метод преобразует специальные символы в HTML-сущности + * + * @param string $text + * @return string + */ + public function e($text) + { + return htmlspecialchars($text, $this->eFlags, 'UTF-8'); + } + + /** + * Метод возвращает окончание строки + * + * @param string $str Текст + * @param int $pos Начальная позиция в байтах с которой идет возврат текста + * @return string + */ + protected function endStr($str, $pos) + { + $s = substr($str, $pos); + return false === $s ? '' : $s; + } +} diff --git a/vendor/miovisman/parserus/README.md b/vendor/miovisman/parserus/README.md new file mode 100644 index 00000000..eb5ab5ba --- /dev/null +++ b/vendor/miovisman/parserus/README.md @@ -0,0 +1,40 @@ +# Parserus + +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +BBCode parser. + +## Requirements + +* PHP 5.4.0 + +## Installation + +Include `Parserus.php` or install [the composer package](https://packagist.org/packages/MioVisman/Parserus). + +## Example + +``` php +$parser = new Parserus(); + +echo $parser->addBBCode([ + 'tag' => 'b', + 'handler' => function($body) { + return '' . $body . ''; + } +])->addBBcode([ + 'tag' => 'i', + 'handler' => function($body) { + return '' . $body . ''; + }, +])->parse("[i]Hello\n[b]World[/b]![/i]") +->getHTML(); + +#output: Hello
World! +``` + +More examples in [the wiki](https://github.com/MioVisman/Parserus/wiki). + +## License + +This project is under MIT license. Please see the [license file](LICENSE) for details. diff --git a/vendor/miovisman/parserus/composer.json b/vendor/miovisman/parserus/composer.json new file mode 100644 index 00000000..bbfb2f7e --- /dev/null +++ b/vendor/miovisman/parserus/composer.json @@ -0,0 +1,21 @@ +{ + "name": "miovisman/parserus", + "description": "BBCode parser.", + "keywords": ["bbcode", "parser"], + "homepage": "https://github.com/MioVisman/Parserus", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Visman", + "email": "mio.visman@yandex.ru", + "homepage": "https://github.com/MioVisman" + } + ], + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "psr-0": {"Parserus": ""} + } +} diff --git a/vendor/miovisman/parserus/examples/_first.php b/vendor/miovisman/parserus/examples/_first.php new file mode 100644 index 00000000..cd1b8981 --- /dev/null +++ b/vendor/miovisman/parserus/examples/_first.php @@ -0,0 +1,20 @@ +addBBCode([ + 'tag' => 'b', + 'handler' => function($body) { + return '' . $body . ''; + } +])->addBBcode([ + 'tag' => 'i', + 'handler' => function($body) { + return '' . $body . ''; + }, +])->parse("[i]Hello\n[b]World[/b]![/i]") +->getHTML(); + +#output: Hello
World! diff --git a/vendor/miovisman/parserus/examples/_second.php b/vendor/miovisman/parserus/examples/_second.php new file mode 100644 index 00000000..f7583d3d --- /dev/null +++ b/vendor/miovisman/parserus/examples/_second.php @@ -0,0 +1,20 @@ +addBBCode([ + 'tag' => 'b', + 'handler' => function($body) { + return '' . $body . ''; + } +])->addBBcode([ + 'tag' => 'i', + 'handler' => function($body) { + return '' . $body . ''; + }, +])->parse("[i]Hello\n[b]World[/b]![/i]") +->getHTML(); + +#output: Hello
World! diff --git a/vendor/miovisman/parserus/examples/_third.php b/vendor/miovisman/parserus/examples/_third.php new file mode 100644 index 00000000..bb6902f7 --- /dev/null +++ b/vendor/miovisman/parserus/examples/_third.php @@ -0,0 +1,20 @@ +addBBCode([ + 'tag' => 'b', + 'handler' => function($body) { + return '' . $body . ''; + } +])->addBBcode([ + 'tag' => 'i', + 'handler' => function($body) { + return '' . $body . ''; + }, +])->parse("[i]\nHello\n[b]\nWorld!") +->getHTML(); + +#output: Hello
World! diff --git a/vendor/miovisman/parserus/examples/attr.php b/vendor/miovisman/parserus/examples/attr.php new file mode 100644 index 00000000..f49f828c --- /dev/null +++ b/vendor/miovisman/parserus/examples/attr.php @@ -0,0 +1,50 @@ +addBBCode([ + 'tag' => 'after', + 'type' => 'block', + 'single' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^\d+$%', + ], + ], + 'handler' => function($body, $attrs, $parser) { + $lang = $parser->attr('lang'); + $arr = array(); + $sec = $attrs['Def'] % 60; + $min = ($attrs['Def'] / 60) % 60; + $hours = ($attrs['Def'] / 3600) % 24; + $days = (int) ($attrs['Def'] / 86400); + if ($days > 0) { + $arr[] = $days . $lang['After time d']; + } + if ($hours > 0) { + $arr[] = $hours . $lang['After time H']; + } + if ($min > 0) { + $arr[] = (($min < 10) ? '0' . $min : $min) . $lang['After time i']; + } + if ($sec > 0) { + $arr[] = (($sec < 10) ? '0' . $sec : $sec) . $lang['After time s']; + } + + $attr = $lang['After time'] . ' ' . implode(' ', $arr); + + return '' . $attr . ':
'; + }, +])->setAttr('lang', [ + 'After time' => 'Added later', + 'After time s' => ' s', + 'After time i' => ' min', + 'After time H' => ' h', + 'After time d' => ' d', +])->parse('[after=10123]') + ->getHTML(); + + +#output: Added later 2 h 48 min 43 s:
diff --git a/vendor/miovisman/parserus/examples/bbcodes_test.php b/vendor/miovisman/parserus/examples/bbcodes_test.php new file mode 100644 index 00000000..c86a43b7 --- /dev/null +++ b/vendor/miovisman/parserus/examples/bbcodes_test.php @@ -0,0 +1,588 @@ + 'ROOT', + 'type' => 'block', + 'handler' => function($body) { + $body = '' . $body . '
'; + + // Replace any breaks next to paragraphs so our replace below catches them + $body = preg_replace('%(?p>)(?:\s*?
){1,2}%', '$1', $body); + $body = preg_replace('%(?:
\s*?){1,2}(?p>)%', '$1', $body); + + // Remove any empty paragraph tags (inserted via quotes/lists/code/etc) which should be stripped + $body = str_replace('', '', $body); + + $body = preg_replace('%
\s*?
%', '', $body); + + $body = str_replace('
', '', $body); + $body = str_replace('
', '
', $body); + $body = str_replace('', '
', $body); + + return $body; + }, + ], + ['tag' => 'code', + 'type' => 'block', + 'recursive' => true, + 'text only' => true, + 'pre' => true, + 'attrs' => [ + 'Def' => true, + 'no attr' => true, + ], + 'handler' => function($body, $attrs) { + $body = trim($body, "\n\r"); + $class = substr_count($body, "\n") > 28 ? ' class="vscroll"' : ''; + return '' . $body . '
'; + }, + ], + ['tag' => 'b', + 'handler' => function($body) { + return '' . $body . ''; + }, + ], + ['tag' => 'i', + 'handler' => function($body) { + return '' . $body . ''; + }, + ], + ['tag' => 'em', + 'handler' => function($body) { + return '' . $body . ''; + }, + ], + ['tag' => 'u', + 'handler' => function($body) { + return '' . $body . ''; + }, + ], + ['tag' => 's', + 'handler' => function($body) { + return '' . $body . ''; + }, + ], + ['tag' => 'del', + 'handler' => function($body) { + return '
' . $body . ''; + }, + ], + ['tag' => 'ins', + 'handler' => function($body) { + return '' . $body . ''; + }, + ], + ['tag' => 'h', + 'type' => 'h', + 'handler' => function($body) { + return '' . $body . '
'; + }, + ], + ['tag' => 'hr', + 'type' => 'block', + 'single' => true, + 'handler' => function() { + return '
'; + }, + ], + ['tag' => 'color', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'colour', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'background', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'size', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^[1-9]\d*(?:em|ex|pt|px|\%)?$%', + ], + ], + 'handler' => function($body, $attrs) { + if (is_numeric($attrs['Def'])) { + $attrs['Def'] .= 'px'; + } + return '' . $body . ''; + }, + ], + ['tag' => 'right', + 'type' => 'block', + 'handler' => function($body) { + return '
' . $body . '
'; + }, + ], + ['tag' => 'center', + 'type' => 'block', + 'handler' => function($body) { + return '
' . $body . '
'; + }, + ], + ['tag' => 'justify', + 'type' => 'block', + 'handler' => function($body) { + return '
' . $body . '
'; + }, + ], + ['tag' => 'mono', + 'handler' => function($body) { + return '
' . $body . '
'; + }, + ], + ['tag' => 'font', + 'self nesting' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^[a-z\d, -]+$%i', + ], + ], + 'handler' => function($body, $attrs) { + return '' . $body . ''; + }, + ], + ['tag' => 'email', + 'type' => 'email', + 'attrs' => [ + 'Def' => [ + 'format' => '%^[^\x00-\x1f\s]+?@[^\x00-\x1f\s]+$%', + ], + 'no attr' => [ + 'body format' => '%^[^\x00-\x1f\s]+?@[^\x00-\x1f\s]+$%D', + 'text only' => true, + ], + ], + 'handler' => function($body, $attrs) { + if (empty($attrs['Def'])) { + return '' . $body . ''; + } else { + return '' . $body . ''; + } + }, + ], + ['tag' => '*', + 'type' => 'block', + 'self nesting' => true, + 'parents' => ['list'], + 'auto' => true, + 'handler' => function($body) { + return ''; + }, + ], + ['tag' => 'list', + 'type' => 'list', + 'self nesting' => true, + 'tags only' => true, + 'attrs' => [ + 'Def' => true, + 'no attr' => true, + ], + 'handler' => function($body, $attrs) { + if (!isset($attrs['Def'])) { + $attrs['Def'] = '*'; + } + + switch ($attrs['Def'][0]) { + case 'a': + return ' ' . $body . '
' . $body . '
'; + case '1': + return '
' . $body . '
'; + default: + return '
' . $body . '
'; + } + }, + ], + ['tag' => 'after', + 'type' => 'block', + 'single' => true, + 'attrs' => [ + 'Def' => [ + 'format' => '%^\d+$%', + ], + ], + 'handler' => function($body, $attrs, $parser) { + $lang = $parser->attr('lang'); + $arr = array(); + $sec = $attrs['Def'] % 60; + $min = ($attrs['Def'] / 60) % 60; + $hours = ($attrs['Def'] / 3600) % 24; + $days = (int) ($attrs['Def'] / 86400); + if ($days > 0) { + $arr[] = $days . $lang['After time d']; + } + if ($hours > 0) { + $arr[] = $hours . $lang['After time H']; + } + if ($min > 0) { + $arr[] = (($min < 10) ? '0' . $min : $min) . $lang['After time i']; + } + if ($sec > 0) { + $arr[] = (($sec < 10) ? '0' . $sec : $sec) . $lang['After time s']; + } + + $attr = $lang['After time'] . ' ' . implode(' ', $arr); + + return '' . $attr . ':
'; + }, + ], + ['tag' => 'quote', + 'type' => 'block', + 'self nesting' => true, + 'attrs' => [ + 'Def' => true, + 'no attr' => true, + ], + 'handler' => function($body, $attrs, $parser) { + if (isset($attrs['Def'])) { + $lang = $parser->attr('lang'); + $st = '' . $attrs['Def'] . ' ' . $lang['wrote'] . ''; + } else { + $st = '
'; + } + + return $st . $body . '
'; + }, + ], + ['tag' => 'spoiler', + 'type' => 'block', + 'self nesting' => true, + 'attrs' => [ + 'Def' => true, + 'no attr' => true, + ], + 'handler' => function($body, $attrs, $parser) { + if (isset($attrs['Def'])) { + $st = '
▼' . $attrs['Def'] . '