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

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