Просмотр файла app/Classes/BBCode.php

Размер файла: 12.34Kb
  1. <?php
  2.  
  3. declare(strict_types=1);
  4.  
  5. namespace App\Classes;
  6.  
  7. use App\Models\Sticker;
  8. use App\Models\User;
  9. use Illuminate\Support\Facades\Cache;
  10. use Illuminate\Support\Facades\DB;
  11.  
  12. /**
  13. * Класс обработки BB-кодов
  14. *
  15. * @license Code and contributions have MIT License
  16. * @link https://visavi.net
  17. * @author Alexander Grigorev <admin@visavi.net>
  18. */
  19. class BBCode
  20. {
  21. /**
  22. * @var array
  23. */
  24. protected static $parsers = [
  25. 'code' => [
  26. 'pattern' => '/\[code\](.+?)\[\/code\]/s',
  27. 'callback' => 'highlightCode'
  28. ],
  29. 'http' => [
  30. 'pattern' => '%\b(((?<=^|\s)\w+://)[^\s()<>\[\]]+)%s',
  31. 'callback' => 'urlReplace',
  32. ],
  33. 'link' => [
  34. 'pattern' => '%\[url\]((\w+://|//|/)[^\s()<>\[\]]+)\[/url\]%s',
  35. 'callback' => 'urlReplace',
  36. ],
  37. 'namedLink' => [
  38. 'pattern' => '%\[url\=((\w+://|//|/)[^\s()<>\[\]]+)\](.+?)\[/url\]%s',
  39. 'callback' => 'urlReplace',
  40. ],
  41. 'image' => [
  42. 'pattern' => '%\[img\]((\w+://|//|/)[^\s()<>\[\]]+\.(jpg|png|gif|jpeg))\[/img\]%s',
  43. 'replace' => '<div class="media-file"><a href="$1" class="gallery"><img src="$1" class="img-fluid" alt="image"></a></div>',
  44. ],
  45. 'bold' => [
  46. 'pattern' => '/\[b\](.+?)\[\/b\]/s',
  47. 'replace' => '<strong>$1</strong>',
  48. ],
  49. 'italic' => [
  50. 'pattern' => '/\[i\](.+?)\[\/i\]/s',
  51. 'replace' => '<em>$1</em>',
  52. ],
  53. 'underLine' => [
  54. 'pattern' => '/\[u\](.+?)\[\/u\]/s',
  55. 'replace' => '<u>$1</u>',
  56. ],
  57. 'lineThrough' => [
  58. 'pattern' => '/\[s\](.+?)\[\/s\]/s',
  59. 'replace' => '<del>$1</del>',
  60. ],
  61. 'fontSize' => [
  62. 'pattern' => '/\[size\=([1-5])\](.+?)\[\/size\]/s',
  63. 'callback' => 'fontSize',
  64. ],
  65. 'fontColor' => [
  66. 'pattern' => '/\[color\=(#[A-f0-9]{6}|#[A-f0-9]{3})\](.+?)\[\/color\]/s',
  67. 'replace' => '<span style="color:$1">$2</span>',
  68. 'iterate' => 5,
  69. ],
  70. 'center' => [
  71. 'pattern' => '/\[center\](.+?)\[\/center\]/s',
  72. 'replace' => '<div style="text-align:center;">$1</div>',
  73. ],
  74. 'quote' => [
  75. 'pattern' => '/\[quote\](.+?)\[\/quote\]/s',
  76. 'replace' => '<blockquote class="blockquote">$1</blockquote>',
  77. 'iterate' => 3,
  78. ],
  79. 'namedQuote' => [
  80. 'pattern' => '/\[quote\=(.+?)\](.+?)\[\/quote\]/s',
  81. 'replace' => '<blockquote class="blockquote">$2<footer class="blockquote-footer">$1</footer></blockquote>',
  82. 'iterate' => 3,
  83. ],
  84. 'orderedList' => [
  85. 'pattern' => '/\[list=1\](.+?)\[\/list\]/s',
  86. 'callback' => 'listReplace',
  87. ],
  88. 'unorderedList' => [
  89. 'pattern' => '/\[list\](.+?)\[\/list\]/s',
  90. 'callback' => 'listReplace',
  91. ],
  92. 'spoiler' => [
  93. 'pattern' => '/\[spoiler\](.+?)\[\/spoiler\]/s',
  94. 'callback' => 'spoilerText',
  95. 'iterate' => 1,
  96. ],
  97. 'shortSpoiler' => [
  98. 'pattern' => '/\[spoiler\=(.+?)\](.+?)\[\/spoiler\]/s',
  99. 'callback' => 'spoilerText',
  100. 'iterate' => 1,
  101. ],
  102. 'hide' => [
  103. 'pattern' => '/\[hide\](.+?)\[\/hide\]/s',
  104. 'callback' => 'hiddenText',
  105. ],
  106. 'youtube' => [
  107. 'pattern' => '/\[youtube\](.*youtu(?:\.be\/|be\.com\/.*(?:vi?\/?=?|embed\/)))([\w-]{11}).*\[\/youtube\]/U',
  108. 'replace' => '<div class="media-file ratio ratio-16x9"><iframe src="//www.youtube.com/embed/$2" allowfullscreen></iframe></div>',
  109. ],
  110. 'username' => [
  111. 'pattern' => '/(?<=^|\s)@([\w\-]{3,20}+)(?=(\s|,))/',
  112. 'callback' => 'userReplace',
  113. ],
  114. ];
  115.  
  116. /**
  117. * Обрабатывает текст
  118. *
  119. * @param string $source текст содержаший BBCode
  120. *
  121. * @return string распарсенный текст
  122. */
  123. public function parse(string $source): string
  124. {
  125. $source = nl2br($source, false);
  126. $source = str_replace('[cut]', '', $source);
  127.  
  128. foreach (self::$parsers as $parser) {
  129. $iterate = $parser['iterate'] ?? 1;
  130.  
  131. for ($i = 0; $i < $iterate; $i++) {
  132. if (isset($parser['callback'])) {
  133. $source = preg_replace_callback($parser['pattern'], [$this, $parser['callback']], $source);
  134. } else {
  135. $source = preg_replace($parser['pattern'], $parser['replace'], $source);
  136. }
  137. }
  138. }
  139. return $this->clearBreakLines($source);
  140. }
  141.  
  142. /**
  143. * Clear break lines
  144. *
  145. * @param string $source
  146. *
  147. * @return string
  148. */
  149. private function clearBreakLines(string $source): string
  150. {
  151. $tags = [
  152. '</div><br>' => '</div>',
  153. '</pre><br>' => '</pre>',
  154. '</blockquote><br>' => '</blockquote>',
  155. ];
  156.  
  157. return strtr($source, $tags);
  158. }
  159.  
  160. /**
  161. * Очищает текст от BB-кодов
  162. *
  163. * @param string $source неочищенный текст
  164. *
  165. * @return string очищенный текст
  166. */
  167. public function clear(string $source): string
  168. {
  169. return preg_replace('/\[(.*?)\]/', '', $source);
  170. }
  171.  
  172. /**
  173. * Обрабатывает ссылки
  174. *
  175. * @param array $match ссылка
  176. *
  177. * @return string обработанная ссылка
  178. */
  179. public function urlReplace(array $match): string
  180. {
  181. $name = $match[3] ?? $match[1];
  182.  
  183. $target = '';
  184. if ($match[2] !== '/') {
  185. if (str_contains($match[1], siteDomain(config('app.url')))) {
  186. $match[1] = '//' . str_replace($match[2], '', $match[1]);
  187. } else {
  188. $target = ' target="_blank" rel="nofollow"';
  189. }
  190. }
  191.  
  192. return '<a href="' . $match[1] . '"' . $target . '>' . rawurldecode($name) . '</a>';
  193. }
  194.  
  195. /**
  196. * Обрабатывет списки
  197. *
  198. * @param array $match список
  199. *
  200. * @return string обработанный список
  201. */
  202. public function listReplace(array $match): string
  203. {
  204. $li = preg_split('/<br>\R/', $match[1], -1, PREG_SPLIT_NO_EMPTY);
  205.  
  206. if (empty($li)) {
  207. return $match[0];
  208. }
  209.  
  210. $list = [];
  211. foreach ($li as $l) {
  212. $list[] = '<li>' . $l . '</li>';
  213. }
  214.  
  215. $tag = str_contains($match[0], '[list]') ? 'ul' : 'ol';
  216.  
  217. return '<' . $tag . '>' . implode($list) . '</' . $tag . '>';
  218. }
  219.  
  220. /**
  221. * Обрабатывает размер текста
  222. *
  223. * @param array $match массив элементов
  224. *
  225. * @return string обработанный текст
  226. */
  227. public function fontSize(array $match): string
  228. {
  229. $sizes = [1 => 'x-small', 2 => 'small', 3 => 'medium', 4 => 'large', 5 => 'x-large'];
  230.  
  231. return '<span style="font-size:' . $sizes[$match[1]] . '">' . $match[2] . '</span>';
  232. }
  233.  
  234. /**
  235. * Подсвечивает код
  236. *
  237. * @param array $match массив элементов
  238. *
  239. * @return string текст с подсветкой
  240. */
  241. public function highlightCode(array $match): string
  242. {
  243. //Чтобы bb-код, стикеры и логины не работали внутри тега [code]
  244. $match[1] = strtr($match[1], [':' => '&#58;', '[' => '&#91;', '@' => '&#64;', '<br>' => '']);
  245.  
  246. return '<pre class="prettyprint linenums pre-scrollable">' . $match[1] . '</pre>';
  247. }
  248.  
  249. /**
  250. * Скрывает текст под спойлер
  251. *
  252. * @param array $match массив элементов
  253. *
  254. * @return string код спойлера
  255. */
  256. public function spoilerText(array $match): string
  257. {
  258. $title = empty($match[2]) ? __('main.expand_view') : $match[1];
  259. $text = empty($match[2]) ? $match[1] : $match[2];
  260.  
  261. return '<div class="spoiler">
  262. <b class="spoiler-title">' . $title . '</b>
  263. <div class="spoiler-text" style="display: none;">' . $text . '</div>
  264. </div>';
  265. }
  266.  
  267. /**
  268. * Скрывает текст от неавторизованных пользователей
  269. *
  270. * @param array $match массив элементов
  271. *
  272. * @return string скрытый код
  273. */
  274. public function hiddenText(array $match): string
  275. {
  276. return '<div class="hidden-text">
  277. <span class="fw-bold">' . __('main.hidden_content') . ':</span> ' .
  278. (getUser() ? $match[1] : __('main.not_authorized')) .
  279. '</div>';
  280. }
  281.  
  282. /**
  283. * Обрабатывает логины пользователей
  284. *
  285. * @param array $match
  286. *
  287. * @return string
  288. */
  289. public function userReplace(array $match): string
  290. {
  291. static $listUsers;
  292.  
  293. if (empty($listUsers)) {
  294. $listUsers = Cache::remember('users', 3600, static function () {
  295. return User::query()
  296. ->select('login', 'name')
  297. ->where('point', '>', 0)
  298. ->get()
  299. ->pluck('name', 'login')
  300. ->toArray();
  301. });
  302. }
  303.  
  304. if (! array_key_exists($match[1], $listUsers)) {
  305. return $match[0];
  306. }
  307.  
  308. $name = $listUsers[$match[1]] ?: $match[1];
  309.  
  310. return '<a href="/users/' . $match[1] . '">' . check($name) . '</a>';
  311. }
  312.  
  313. /**
  314. * Обрабатывет стикеры
  315. *
  316. * @param string $source
  317. *
  318. * @return string Обработанный текст
  319. */
  320. public function parseStickers(string $source): string
  321. {
  322. static $listStickers;
  323.  
  324. if (empty($listStickers)) {
  325. $listStickers = Cache::rememberForever('stickers', static function () {
  326. return Sticker::query()
  327. ->select('code', 'name')
  328. ->orderByDesc(DB::raw('CHAR_LENGTH(code)'))
  329. ->get()
  330. ->pluck('name', 'code')
  331. ->map(function ($item, $key) {
  332. return '<img src="' . $item . '" alt="' . getBodyName($item) . '">';
  333. })
  334. ->toArray();
  335. });
  336. }
  337.  
  338. return strtr($source, $listStickers);
  339. }
  340.  
  341. /**
  342. * Добавляет или переопределяет парсер
  343. *
  344. * @param string $name Parser name
  345. * @param string $pattern Pattern
  346. * @param string $replace Replace pattern
  347. *
  348. * @return void
  349. */
  350. public function setParser(string $name, string $pattern, string $replace): void
  351. {
  352. self::$parsers[$name] = [
  353. 'pattern' => $pattern,
  354. 'replace' => $replace
  355. ];
  356. }
  357.  
  358. /**
  359. * Устанавливает список доступных парсеров
  360. *
  361. * @param mixed $only parsers
  362. *
  363. * @return BBCode object
  364. */
  365. public function only($only = null): self
  366. {
  367. $only = is_array($only) ? $only : func_get_args();
  368. self::$parsers = $this->arrayOnly($only);
  369.  
  370. return $this;
  371. }
  372.  
  373. /**
  374. * Исключает парсеры из набора
  375. *
  376. * @param mixed $except parsers
  377. *
  378. * @return BBCode object
  379. */
  380. public function except($except = null): self
  381. {
  382. $except = is_array($except) ? $except : func_get_args();
  383. self::$parsers = $this->arrayExcept($except);
  384.  
  385. return $this;
  386. }
  387.  
  388. /**
  389. * Возвращает список всех парсеров
  390. *
  391. * @return array array of parsers
  392. */
  393. public function getParsers(): array
  394. {
  395. return self::$parsers;
  396. }
  397.  
  398. /**
  399. * Filters all parsers that you don´t want
  400. *
  401. * @param array $only chosen parsers
  402. *
  403. * @return array parsers
  404. */
  405. private function arrayOnly(array $only): array
  406. {
  407. return array_intersect_key(self::$parsers, array_flip($only));
  408. }
  409.  
  410. /**
  411. * Removes the parsers that you don´t want
  412. *
  413. * @param array $excepts
  414. *
  415. * @return array parsers
  416. */
  417. private function arrayExcept(array $excepts): array
  418. {
  419. return array_diff_key(self::$parsers, array_flip($excepts));
  420. }
  421. }