PageRenderTime 63ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Texy/Modifier.php

https://github.com/dg/texy
PHP | 248 lines | 180 code | 46 blank | 22 comment | 28 complexity | 3a39d3b89c894e92f64c435c71fcfe0d MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * This file is part of the Texy! (https://texy.info)
  4. * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  5. */
  6. declare(strict_types=1);
  7. namespace Texy;
  8. /**
  9. * Modifier processor.
  10. *
  11. * Modifiers are texts like .(title)[class1 class2 #id]{color: red}>^
  12. * . starts with dot
  13. * (...) title or alt modifier
  14. * [...] classes or ID modifier
  15. * {...} inner style modifier
  16. * < > <> = horizontal align modifier
  17. * ^ - _ vertical align modifier
  18. */
  19. final class Modifier
  20. {
  21. use Strict;
  22. public ?string $id = null;
  23. /** @var array<string, bool> of classes (as keys) */
  24. public $classes = [];
  25. /** @var array<string, string> of CSS styles */
  26. public $styles = [];
  27. /** @var array<string, string|string[]> of HTML element attributes */
  28. public $attrs = [];
  29. public ?string $hAlign = null;
  30. public ?string $vAlign = null;
  31. public ?string $title = null;
  32. public ?string $cite = null;
  33. /** @var array<string, int> list of properties which are regarded as HTML element attributes */
  34. public static $elAttrs = [
  35. 'abbr' => 1, 'accesskey' => 1, 'alt' => 1, 'cite' => 1, 'colspan' => 1, 'contenteditable' => 1, 'crossorigin' => 1,
  36. 'datetime' => 1, 'decoding' => 1, 'download' => 1, 'draggable' => 1, 'for' => 1, 'headers' => 1, 'hidden' => 1,
  37. 'href' => 1, 'hreflang' => 1, 'id' => 1, 'itemid' => 1, 'itemprop' => 1, 'itemref' => 1, 'itemscope' => 1, 'itemtype' => 1,
  38. 'lang' => 1, 'name' => 1, 'ping' => 1, 'referrerpolicy' => 1, 'rel' => 1, 'reversed' => 1, 'rowspan' => 1, 'scope' => 1,
  39. 'slot' => 1, 'src' => 1, 'srcset' => 1, 'start' => 1, 'target' => 1, 'title' => 1, 'translate' => 1, 'type' => 1, 'value' => 1,
  40. ];
  41. public function __construct(string $s = null)
  42. {
  43. $this->setProperties($s);
  44. }
  45. public function setProperties(?string $s): void
  46. {
  47. $p = 0;
  48. $len = $s ? strlen($s) : 0;
  49. while ($p < $len) {
  50. $ch = $s[$p];
  51. if ($ch === '(') { // title
  52. preg_match('#(?:\\\\\)|[^)\n])++\)#', $s, $m, 0, $p);
  53. $this->title = Helpers::unescapeHtml(str_replace('\)', ')', trim(substr($m[0], 1, -1))));
  54. $p += strlen($m[0]);
  55. } elseif ($ch === '{') { // style & attributes
  56. $a = strpos($s, '}', $p) + 1;
  57. $this->parseStyle(substr($s, $p + 1, $a - $p - 2));
  58. $p = $a;
  59. } elseif ($ch === '[') { // classes & ID
  60. $a = strpos($s, ']', $p) + 1;
  61. $this->parseClasses(str_replace('#', ' #', substr($s, $p + 1, $a - $p - 2)));
  62. $p = $a;
  63. } elseif ($val = ['^' => 'top', '-' => 'middle', '_' => 'bottom'][$ch] ?? null) { // alignment
  64. $this->vAlign = $val;
  65. $p++;
  66. } elseif (substr($s, $p, 2) === '<>') {
  67. $this->hAlign = 'center';
  68. $p += 2;
  69. } elseif ($val = ['=' => 'justify', '>' => 'right', '<' => 'left'][$ch] ?? null) {
  70. $this->hAlign = $val;
  71. $p++;
  72. } else {
  73. break;
  74. }
  75. }
  76. }
  77. /**
  78. * Decorates HtmlElement element.
  79. */
  80. public function decorate(Texy $texy, HtmlElement $el): HtmlElement
  81. {
  82. $this->decorateAttrs($texy, $el->attrs, $el->getName());
  83. $el->validateAttrs($texy->getDTD());
  84. $this->decorateClasses($texy, $el->attrs);
  85. $this->decorateStyles($texy, $el->attrs);
  86. $this->decorateAligns($texy, $el->attrs);
  87. return $el;
  88. }
  89. private function decorateAttrs(Texy $texy, array &$attrs, string $name): void
  90. {
  91. if (!$this->attrs) {
  92. } elseif ($texy->allowedTags === $texy::ALL) {
  93. $attrs = $this->attrs;
  94. } elseif (is_array($texy->allowedTags)) {
  95. $attrs = $texy->allowedTags[$name] ?? null;
  96. if ($attrs === $texy::ALL) {
  97. $attrs = $this->attrs;
  98. } elseif (is_array($attrs) && count($attrs)) {
  99. $attrs = array_flip($attrs);
  100. foreach ($this->attrs as $key => $value) {
  101. if (isset($attrs[$key])) {
  102. $attrs[$key] = $value;
  103. }
  104. }
  105. }
  106. }
  107. if ($this->title !== null) {
  108. $attrs['title'] = $texy->typographyModule->postLine($this->title);
  109. }
  110. }
  111. private function decorateClasses(Texy $texy, array &$attrs): void
  112. {
  113. if ($this->classes || $this->id !== null) {
  114. [$allowedClasses] = $texy->getAllowedProps();
  115. settype($attrs['class'], 'array');
  116. if ($allowedClasses === $texy::ALL) {
  117. foreach ($this->classes as $value => $foo) {
  118. $attrs['class'][] = $value;
  119. }
  120. $attrs['id'] = $this->id;
  121. } elseif (is_array($allowedClasses)) {
  122. foreach ($this->classes as $value => $foo) {
  123. if (isset($allowedClasses[$value])) {
  124. $attrs['class'][] = $value;
  125. }
  126. }
  127. if (isset($allowedClasses['#' . $this->id])) {
  128. $attrs['id'] = $this->id;
  129. }
  130. }
  131. }
  132. }
  133. private function decorateStyles(Texy $texy, array &$attrs): void
  134. {
  135. if ($this->styles) {
  136. [, $allowedStyles] = $texy->getAllowedProps();
  137. settype($attrs['style'], 'array');
  138. if ($allowedStyles === $texy::ALL) {
  139. foreach ($this->styles as $prop => $value) {
  140. $attrs['style'][$prop] = $value;
  141. }
  142. } elseif (is_array($allowedStyles)) {
  143. foreach ($this->styles as $prop => $value) {
  144. if (isset($allowedStyles[$prop])) {
  145. $attrs['style'][$prop] = $value;
  146. }
  147. }
  148. }
  149. }
  150. }
  151. private function decorateAligns(Texy $texy, array &$attrs): void
  152. {
  153. if ($this->hAlign) {
  154. $class = $texy->alignClasses[$this->hAlign] ?? null;
  155. if ($class) {
  156. settype($attrs['class'], 'array');
  157. $attrs['class'][] = $class;
  158. } else {
  159. settype($attrs['style'], 'array');
  160. $attrs['style']['text-align'] = $this->hAlign;
  161. }
  162. }
  163. if ($this->vAlign) {
  164. $class = $texy->alignClasses[$this->vAlign] ?? null;
  165. if ($class) {
  166. settype($attrs['class'], 'array');
  167. $attrs['class'][] = $class;
  168. } else {
  169. settype($attrs['style'], 'array');
  170. $attrs['style']['vertical-align'] = $this->vAlign;
  171. }
  172. }
  173. }
  174. private function parseStyle(string $s): void
  175. {
  176. foreach (explode(';', $s) as $value) {
  177. $pair = explode(':', $value, 2);
  178. $prop = strtolower(trim($pair[0]));
  179. if ($prop === '' || !isset($pair[1])) {
  180. continue;
  181. }
  182. $value = trim($pair[1]);
  183. if (isset(self::$elAttrs[$prop]) || str_starts_with($prop, 'data-')) { // attribute
  184. $this->attrs[$prop] = $value;
  185. } elseif ($value !== '') { // style
  186. $this->styles[$prop] = $value;
  187. }
  188. }
  189. }
  190. private function parseClasses(string $s): void
  191. {
  192. foreach (explode(' ', $s) as $value) {
  193. if ($value === '') {
  194. continue;
  195. } elseif ($value[0] === '#') {
  196. $this->id = substr($value, 1);
  197. } else {
  198. $this->classes[$value] = true;
  199. }
  200. }
  201. }
  202. }