A bare bones front-end for knockout designed for maximum compatibility with "obsolete" browsers
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

334 lines
10KB

  1. <?php
  2. namespace App\Vendor\BBCode;
  3. use App\Vendor\BBCode\Tags\BoldTag;
  4. use App\Vendor\BBCode\Tags\NoParseTag;
  5. use App\Vendor\BBCode\Tokenizer\Token;
  6. use App\Vendor\BBCode\Tokenizer\Tokenizer;
  7. use Closure;
  8. /*
  9. * BBCode to HTML converter
  10. *
  11. * Inspired by Kai Mallea (kmallea@gmail.com)
  12. *
  13. * Licensed under the MIT license:
  14. * http://www.opensource.org/licenses/mit-license.php
  15. */
  16. class Bbcode {
  17. protected $tagTypes = [];
  18. /**
  19. * The text with BBCodes
  20. *
  21. * @var string|null
  22. */
  23. protected $text = null;
  24. /**
  25. * BBCode constructor.
  26. *
  27. * @param string|null $text The text - might include BBCode tags
  28. */
  29. public function __construct($text = null)
  30. {
  31. $this->setText($text);
  32. }
  33. /**
  34. * Set the raw text - might include BBCode tags
  35. *
  36. * @param string $text The text
  37. * @retun void
  38. */
  39. public function setText($text)
  40. {
  41. $this->text = $text;
  42. }
  43. /**
  44. * Renders only the text without any BBCode tags.
  45. *
  46. * @param string $text Optional: Render the passed BBCode string instead of the internally stored one
  47. * @return string
  48. */
  49. public function renderPlain($text = null)
  50. {
  51. if ($this->text !== null and $text === null) {
  52. $text = $this->text;
  53. }
  54. return preg_replace("/\[(.*?)\]/is", '', $text);
  55. }
  56. /**
  57. * Renders only the text without any BBCode tags.
  58. * Alias for renderRaw().
  59. *
  60. * @deprecated Deprecated since 1.1.0
  61. *
  62. * @param string $text Optional: Render the passed BBCode string instead of the internally stored one
  63. * @return string
  64. */
  65. public function renderRaw($text = null)
  66. {
  67. return $this->renderPlain($text);
  68. }
  69. /**
  70. * Renders BBCode to HTML
  71. *
  72. * @param string $text Optional: Render the passed BBCode string instead of the internally stored one
  73. * @param bool $escape Escape HTML entities? (Only "<" and ">"!)
  74. * @param bool $keepLines Keep line breaks by replacing them with <br>?
  75. * @return string
  76. */
  77. public function render($text = null, $escape = true, $keepLines = true)
  78. {
  79. if ($this->text !== null and $text === null) {
  80. $text = $this->text;
  81. }
  82. $tokenizer = new Tokenizer($this->tagTypes);
  83. $tokens = $tokenizer->tokenize($text, $escape, $keepLines);
  84. $html = '';
  85. $level = 0;
  86. foreach ($tokens as $index => $token) {
  87. switch ($token->getType()) {
  88. case Token::TYPE_LINEBREAK:
  89. $html .= '<br/>';
  90. break;
  91. case Token::TYPE_PLAIN_TEXT:
  92. $html .= $token->getValue();
  93. break;
  94. case Token::TYPE_TAG_OPENING:
  95. case Token::TYPE_TAG_CLOSING:
  96. $tagOpening = $token->getType() === Token::TYPE_TAG_OPENING;
  97. $tagName = $token->getValue();
  98. // If the tag is not known, just do not render it. We do not want to throw any exceptions.
  99. if (isset($this->tagTypes[$tagName])) {
  100. $tagType = $this->tagTypes[$tagName];
  101. /** @var AbstractTagType $tag */
  102. $tag = new $tagType;
  103. $tag->render($html, $tagOpening);
  104. }
  105. if ($tagOpening) {
  106. $level++;
  107. } else {
  108. $level--;
  109. }
  110. }
  111. }
  112. return $html;
  113. $html = '';
  114. $len = mb_strlen($text);
  115. $inTag = false; // True if current position is inside a tag
  116. $inName = false; // True if current pos is inside a tag name
  117. $inStr = false; // True if current pos is inside a string
  118. /** @var Tag|null $tag */
  119. $tag = null;
  120. $openTags = array();
  121. /*
  122. * Loop over each character of the text
  123. */
  124. for ($i = 0; $i < $len; $i++) {
  125. $char = mb_substr($text, $i, 1);
  126. if ($keepLines) {
  127. if ($char == "\n") {
  128. $html .= '<br/>';
  129. }
  130. if ($char == "\r") {
  131. continue;
  132. }
  133. }
  134. if (! $escape or ($char != '<' and $char != '>')) {
  135. /*
  136. * $inTag == true means the current position is inside a tag definition
  137. * (= inside the brackets of a tag)
  138. */
  139. if ($inTag) {
  140. if ($char == '"') {
  141. if ($inStr) {
  142. $inStr = false;
  143. } else {
  144. if ($inName) {
  145. $tag->valid = false;
  146. } else {
  147. $inStr = true;
  148. }
  149. }
  150. } else {
  151. /*
  152. * This closes a tag
  153. */
  154. if ($char == ']' and ! $inStr) {
  155. $inTag = false;
  156. $inName = false;
  157. if ($tag->valid) {
  158. $code = null;
  159. if ($tag->opening) {
  160. $code = $this->generateTag($tag, $html, null, $openTags);
  161. } else {
  162. $openingTag = $this->popTag($openTags, $tag);
  163. if ($openingTag) {
  164. $code = $this->generateTag($tag, $html, $openingTag, $openTags);
  165. }
  166. }
  167. if ($code !== null and $tag->opening) {
  168. $openTags[$tag->name][] = $tag;
  169. }
  170. $html .= $code;
  171. }
  172. continue;
  173. }
  174. if ($inName and ! $inStr) {
  175. /*
  176. * This makes the current tag a closing tag
  177. */
  178. if ($char == '/') {
  179. if ($tag->name) {
  180. $tag->valid = false;
  181. } else {
  182. $tag->opening = false;
  183. }
  184. } else {
  185. /*
  186. * This means a property starts
  187. */
  188. if ($char == '=') {
  189. if ($tag->name) {
  190. $inName = false;
  191. } else {
  192. $tag->valid = false;
  193. }
  194. } else {
  195. $tag->name .= mb_strtolower($char);
  196. }
  197. }
  198. } else { // If we are not inside the name we are inside a property
  199. $tag->property .= $char;
  200. }
  201. }
  202. } else {
  203. /*
  204. * This opens a tag
  205. */
  206. if ($char == '[') {
  207. $inTag = true;
  208. $inName = true;
  209. $tag = new Tag();
  210. $tag->position = mb_strlen($html);
  211. } else {
  212. $html .= $char;
  213. }
  214. }
  215. } else {
  216. $html .= htmlspecialchars($char);
  217. }
  218. }
  219. /*
  220. * Check for tags that are not closed and close them.
  221. */
  222. foreach ($openTags as $name => $openTagsByType) {
  223. $closingTag = new Tag($name, false);
  224. foreach ($openTagsByType as $openTag) {
  225. $html .= $this->generateTag($closingTag, $html, $openTag);
  226. }
  227. }
  228. return $html;
  229. }
  230. /**
  231. * Generates and returns the HTML code of the current tag
  232. *
  233. * @param Tag $tag The current tag
  234. * @param string $html The current HTML code passed by reference - might be altered!
  235. * @param Tag|null $openingTag The opening tag that is linked to the tag (or null)
  236. * @param Tag[] $openTags Array with tags that are opned but not closed
  237. * @return string
  238. */
  239. protected function generateTag(Tag $tag, &$html, Tag $openingTag = null, array $openTags = [])
  240. {
  241. $code = null;
  242. if (in_array($tag->name, $this->ignoredTags)) {
  243. return $code;
  244. }
  245. // Custom tags:
  246. foreach ($this->customTagClosures as $name => $closure) {
  247. if ($tag->name === $name) {
  248. exit($tag->name);
  249. $code .= $closure($tag, $html, $openingTag);
  250. }
  251. }
  252. return $code;
  253. }
  254. /**
  255. * Magic method __toString()
  256. *
  257. * @return string
  258. */
  259. public function __toString()
  260. {
  261. return $this->render();
  262. }
  263. /**
  264. * Returns the last tag of a given type and removes it from the array.
  265. *
  266. * @param Tag[] $tags Array of tags
  267. * @param Tag $tag Return the last tag of the type of this tag
  268. * @return Tag|null
  269. */
  270. protected function popTag(array &$tags, $tag)
  271. {
  272. if (! isset($tags[$tag->name])) {
  273. return null;
  274. }
  275. $size = sizeof($tags[$tag->name]);
  276. if ($size === 0) {
  277. return null;
  278. } else {
  279. return array_pop($tags[$tag->name]);
  280. }
  281. }
  282. /**
  283. * Returns true if $haystack ends with $needle
  284. *
  285. * @param string $haystack
  286. * @param string $needle
  287. * @return bool
  288. */
  289. protected function endsWith($haystack, $needle)
  290. {
  291. return ($needle === '' or mb_substr($haystack, -mb_strlen($needle)) === $needle);
  292. }
  293. }