1<?php 2 3namespace ComboStrap; 4 5 6use ComboStrap\Tag\BarTag; 7use ComboStrap\TagAttribute\Align; 8use syntax_plugin_combo_cell; 9use syntax_plugin_combo_iterator; 10 11/** 12 * The implementation of row/col system of Boostrap is called a grid because: 13 * * the children may create may be layout on more than one line 14 * * you can define gutter between the children 15 * * even if this is a layout component that works only on one axis and not two. There is little chance that a user will use the css grid layout system 16 * to layout content 17 * 18 */ 19class GridTag 20{ 21 22 23 public const GUTTER = "gutter"; 24 /** 25 * Used when the grid is not contained 26 * and is just below the root 27 * We set a value 28 * @deprecated (30/04/2022) 29 */ 30 public const TYPE_AUTO_VALUE_DEPRECATED = "auto"; 31 /** 32 * By default, div but used in a ul, it could be a li 33 * This is modified in the callstack by the other component 34 * @deprecated with the new {@link Align} (30/04/2022) 35 */ 36 public const HTML_TAG_ATT = "html-tag"; 37 public const KNOWN_TYPES = [self::TYPE_MAX_CHILDREN, GridTag::TYPE_WIDTH_SPECIFIED, GridTag::TYPE_AUTO_VALUE_DEPRECATED, GridTag::TYPE_FIT_VALUE, GridTag::TYPE_FIT_OLD_VALUE]; 38 public const GRID_TAG = "grid"; 39 public const ROW_TAG = "row"; 40 /** 41 * 42 * @deprecated - contained/fit type was the same and has been deprecated for the {@link Align} attribute and the width value (grow/shrink) 43 * (30/04/2022) 44 */ 45 public const TYPE_FIT_VALUE = "fit"; 46 /** 47 * The type row is a hack to be able 48 * to support a row tag (ie flex div) 49 * 50 * Ie we migrate row to grid smoothly without loosing 51 * the possibility to use row as component 52 * @deprecated 53 */ 54 public const TYPE_ROW_TAG = "row"; 55 /** 56 * @deprecated (30/04/2022) 57 */ 58 public const TYPE_FIT_OLD_VALUE = "natural"; 59 public const MAX_CHILDREN_ATTRIBUTE = "max-line"; 60 /** 61 * The value is not `width` as this is also an 62 * attribute {@link Dimension::WIDTH_KEY} 63 * and it will fail the type check at {@link TagAttributes::hasComponentAttribute()} 64 */ 65 public const TYPE_WIDTH_SPECIFIED = "width-specified"; 66 public const TAG = GridTag::GRID_TAG; 67 /** 68 * The strap template permits to 69 * change this value 70 * but because of the new grid system, it has been deprecated 71 * We therefore don't get the grid total columns value from strap 72 * @see {@link https://combostrap.com/dynamic_grid Dynamic Grid } 73 */ 74 public const GRID_TOTAL_COLUMNS = 12; 75 public const TAGS = [GridTag::TAG, GridTag::ROW_TAG]; 76 public const TYPE_MAX_CHILDREN = "max"; 77 public const CANONICAL = GridTag::TAG; 78 const LOGICAL_TAG = self::GRID_TAG; 79 const ITERATOR_DEFAULT_MAX_LINE = 3; 80 81 82 public static function processEnter(TagAttributes $attributes, $handler, $match) 83 { 84 85 $callStack = CallStack::createFromHandler($handler); 86 $parent = $callStack->moveToParent(); 87 88 /** 89 * We have split row in two: 90 * * grid for a bootstrap grid 91 * 92 * We check 93 */ 94 $rowMatchPrefix = "<row"; 95 $isRowTag = substr($match, 0, strlen($rowMatchPrefix)) == $rowMatchPrefix; 96 if ($parent !== false 97 && !in_array($parent->getTagName(), [ 98 BarTag::BAR_TAG, 99 ContainerTag::TAG, 100 syntax_plugin_combo_cell::TAG, 101 syntax_plugin_combo_iterator::TAG, 102 ]) 103 && $isRowTag 104 ) { 105 // contained not in one 106 $scannedType = self::ROW_TAG; 107 } else { 108 $scannedType = self::GRID_TAG; 109 if ($isRowTag) { 110 LogUtility::warning("A non-contained row has been deprecated for grid. You should rename the row tag to grid"); 111 } 112 } 113 114 $defaultAttributes = []; 115 if ($scannedType === self::GRID_TAG) { 116 117 /** 118 * Vertical gutter 119 * On a two cell grid, the content will not 120 * touch on a mobile 121 * 122 * https://getbootstrap.com/docs/4.3/layout/grid/#no-gutters 123 * $attributes->addClassName("no-gutters"); 124 */ 125 $defaultGutter = "y-5"; 126 /** 127 * This is a block 128 * as we give it the same spacing than 129 * a paragraph 130 */ 131 $spacing = "mb-3"; 132 $defaultAttributes = [ 133 self::GUTTER => $defaultGutter, 134 Spacing::SPACING_ATTRIBUTE => $spacing 135 ]; 136 /** 137 * All element are centered 138 * If their is 5 cells and the last one 139 * is going at the line, it will be centered 140 * Y = top (the default of css) 141 */ 142 $defaultAlign[Align::X_AXIS] = Align::X_CENTER_CHILDREN; 143 } else { 144 /** 145 * Row is for now mainly use in a content-list and the content 146 * should be centered on y 147 * Why ? Because by default, a flex place text at the top and if a badge is added 148 * for instance, it will shift the text towards the top 149 */ 150 $defaultAlign[Align::Y_AXIS] = "y-center-children"; 151 } 152 153 154 if ($scannedType === self::ROW_TAG) { 155 $attributes->setType(self::TYPE_ROW_TAG); 156 } 157 158 /** 159 * Default 160 */ 161 foreach ($defaultAttributes as $key => $value) { 162 if (!$attributes->hasComponentAttribute($key)) { 163 $attributes->addComponentAttributeValue($key, $value); 164 } 165 } 166 167 /** 168 * Align default 169 */ 170 try { 171 $aligns = $attributes->getValues(Align::ALIGN_ATTRIBUTE, []); 172 $alignsByAxis = []; 173 foreach ($aligns as $align) { 174 $alignObject = ConditionalLength::createFromString($align); 175 $alignsByAxis[$alignObject->getAxisOrDefault()] = $align; 176 } 177 foreach ($defaultAlign as $axis => $value) { 178 if (!isset($alignsByAxis[$axis])) { 179 $attributes->addComponentAttributeValue(Align::ALIGN_ATTRIBUTE, $value); 180 } 181 } 182 } catch (ExceptionBadArgument $e) { 183 LogUtility::error("The align attribute default values could not be processed. Error: {$e->getMessage()}"); 184 } 185 186 187 /** 188 * The deprecations 189 */ 190 $type = $attributes->getType(); 191 if (($type === self::TYPE_AUTO_VALUE_DEPRECATED)) { 192 LogUtility::warning("The auto rows type has been deprecated.", self::CANONICAL); 193 $attributes->removeType(); 194 } 195 if ($type === self::TYPE_FIT_OLD_VALUE || $type === self::TYPE_FIT_VALUE) { 196 // in case it's the old value 197 $attributes->setType(self::TYPE_ROW_TAG); 198 LogUtility::warning("Deprecation: The type value (" . self::TYPE_FIT_VALUE . " and " . self::TYPE_FIT_OLD_VALUE . ") for a contained row tag.", self::CANONICAL); 199 } 200 201 202 $attributes->addComponentAttributeValue(self::HTML_TAG_ATT, "div"); 203 204 } 205 206 public static function handleExit(\Doku_Handler $handler): array 207 { 208 209 $callStack = CallStack::createFromHandler($handler); 210 211 /** 212 * The returned array 213 * (filed while processing) 214 */ 215 $returnArray = array(); 216 217 /** 218 * Sizing Type mode determination 219 */ 220 $openingCall = $callStack->moveToPreviousCorrespondingOpeningCall(); 221 $type = $openingCall->getType(); 222 223 /** 224 * Max-Cells Type ? 225 */ 226 $maxLineAttributeValue = null; // variable declaration to not have a linter warning 227 /** 228 * @var ConditionalLength[] $maxLineArray 229 */ 230 $maxLineArray = []; // variable declaration to not have a linter warning 231 if ($type == null) { 232 233 $maxLineAttributeValue = $openingCall->getAttribute(GridTag::MAX_CHILDREN_ATTRIBUTE); 234 if ($maxLineAttributeValue !== null) { 235 236 $maxCellsValues = explode(" ", $maxLineAttributeValue); 237 foreach ($maxCellsValues as $maxCellsValue) { 238 try { 239 $maxCellLength = ConditionalLength::createFromString($maxCellsValue); 240 } catch (ExceptionBadArgument $e) { 241 LogUtility::error("The max-cells attribute value ($maxCellsValue) is not a valid length value. Error: {$e->getMessage()}", GridTag::CANONICAL); 242 continue; 243 } 244 $number = $maxCellLength->getNumerator(); 245 if ($number > 12) { 246 LogUtility::error("The max-cells attribute value ($maxCellsValue) should be less than 12.", GridTag::CANONICAL); 247 } 248 $maxLineArray[$maxCellLength->getBreakpointOrDefault()] = $maxCellLength; 249 } 250 $openingCall->removeAttribute(GridTag::MAX_CHILDREN_ATTRIBUTE); 251 $type = GridTag::TYPE_MAX_CHILDREN; 252 } 253 } 254 255 256 /** 257 * Gather the cells children 258 * Is there a template callstack 259 */ 260 $firstChildTag = $callStack->moveToFirstChildTag(); 261 $childrenOpeningTags = []; 262 263 $fragmentEndTag = null; // the template end tag that has the instructions 264 $callStackTemplate = null; // the instructions in callstack form to modify the children 265 if ($firstChildTag !== false && $firstChildTag->getTagName() === FragmentTag::FRAGMENT_TAG && $firstChildTag->getState() === DOKU_LEXER_ENTER) { 266 $fragmentEndTag = $callStack->next(); 267 if ($fragmentEndTag->getTagName() !== FragmentTag::FRAGMENT_TAG || $fragmentEndTag->getState() !== DOKU_LEXER_EXIT) { 268 LogUtility::error("Error internal: We were unable to find the closing template tag.", GridTag::CANONICAL); 269 return $returnArray; 270 } 271 $templateInstructions = $fragmentEndTag->getPluginData(FragmentTag::CALLSTACK); 272 $callStackTemplate = CallStack::createFromInstructions($templateInstructions); 273 $callStackTemplate->moveToStart(); 274 $firstChildTag = $callStackTemplate->moveToFirstChildTag(); 275 if ($firstChildTag !== false) { 276 $childrenOpeningTags[] = $firstChildTag; 277 while ($actualCall = $callStackTemplate->moveToNextSiblingTag()) { 278 $childrenOpeningTags[] = $actualCall; 279 } 280 } 281 282 } else { 283 284 $childrenOpeningTags[] = $firstChildTag; 285 while ($actualCall = $callStack->moveToNextSiblingTag()) { 286 $childrenOpeningTags[] = $actualCall; 287 } 288 289 } 290 291 292 if ($type !== GridTag::TYPE_ROW_TAG) { 293 294 /** 295 * Scan and process the children for a grid tag 296 * - Add the col class 297 * - Do the cells have a width set ... 298 */ 299 foreach ($childrenOpeningTags as $actualCall) { 300 301 $actualCall->addClassName("col"); 302 303 $widthAttributeValue = $actualCall->getAttribute(Dimension::WIDTH_KEY); 304 if ($widthAttributeValue !== null) { 305 $type = GridTag::TYPE_WIDTH_SPECIFIED; 306 $conditionalWidthsLengths = explode(" ", $widthAttributeValue); 307 foreach ($conditionalWidthsLengths as $conditionalWidthsLength) { 308 try { 309 $conditionalLengthObject = ConditionalLength::createFromString($conditionalWidthsLength); 310 } catch (ExceptionBadArgument $e) { 311 $type = null; 312 LogUtility::error("The width length $conditionalWidthsLength is not a valid length value. Error: {$e->getMessage()}"); 313 break; 314 } 315 try { 316 if ($conditionalLengthObject->isRatio()) { 317 $ratio = $conditionalLengthObject->getRatio(); 318 if ($ratio > 1) { 319 LogUtility::warning("The ratio ($ratio) of the width ($conditionalLengthObject) should not be greater than 1 on the children of the row", GridTag::CANONICAL); 320 break; 321 } 322 } 323 } catch (ExceptionBadArgument $e) { 324 $type = null; 325 LogUtility::error("The ratio of the width ($conditionalLengthObject) is not a valid. Error: {$e->getMessage()}"); 326 break; 327 } 328 } 329 } 330 } 331 } 332 333 if ($type === null) { 334 $type = GridTag::TYPE_MAX_CHILDREN; 335 } 336 /** 337 * Setting the type on the opening tag to see the chosen type in the html attribute 338 */ 339 $openingCall->setType($type); 340 341 342 /** 343 * Type is now known 344 * Do the Distribution calculation 345 */ 346 switch ($type) { 347 case GridTag::TYPE_MAX_CHILDREN: 348 $maxLineDefaults = []; 349 try { 350 $maxLineDefaults["xs"] = ConditionalLength::createFromString("1-xs"); 351 $maxLineDefaults["sm"] = ConditionalLength::createFromString("2-sm"); 352 $maxLineDefaults["md"] = ConditionalLength::createFromString("3-md"); 353 $maxLineDefaults["lg"] = ConditionalLength::createFromString("4-lg"); 354 } catch (ExceptionBadArgument $e) { 355 LogUtility::error("Bad default value initialization. Error:{$e->getMessage()}", GridTag::CANONICAL); 356 } 357 /** 358 * Delete the default that are bigger than: 359 * * the asked max-line number 360 * * or the number of children (ie if there is two children, they split the space in two) 361 */ 362 $maxLineDefaultsFiltered = []; 363 $maxLineUsedToFilter = sizeof($childrenOpeningTags); 364 if ($fragmentEndTag !== null) { 365 // there is only one child in a iterator 366 $maxLineUsedToFilter = self::ITERATOR_DEFAULT_MAX_LINE; 367 } 368 if ($maxLineAttributeValue !== null && $maxLineUsedToFilter > $maxLineAttributeValue) { 369 $maxLineUsedToFilter = $maxLineAttributeValue; 370 } 371 foreach ($maxLineDefaults as $breakpoint => $maxLineDefault) { 372 if ($maxLineDefault->getNumerator() <= $maxLineUsedToFilter) { 373 $maxLineDefaultsFiltered[$breakpoint] = $maxLineDefault; 374 } 375 } 376 $maxLineArray = array_merge($maxLineDefaultsFiltered, $maxLineArray); 377 foreach ($maxLineArray as $maxCell) { 378 /** 379 * @var ConditionalLength $maxCell 380 */ 381 try { 382 $openingCall->addClassName($maxCell->toRowColsClass()); 383 } catch (ExceptionBadArgument $e) { 384 LogUtility::error("Error while adding the row-col class. Error: {$e->getMessage()}"); 385 } 386 } 387 break; 388 case GridTag::TYPE_WIDTH_SPECIFIED: 389 390 foreach ($childrenOpeningTags as $childOpeningTag) { 391 $widthAttributeValue = $childOpeningTag->getAttribute(Dimension::WIDTH_KEY); 392 if ($widthAttributeValue === null) { 393 continue; 394 } 395 $widthValues = explode(" ", $widthAttributeValue); 396 $widthColClasses = null; 397 foreach ($widthValues as $width) { 398 try { 399 $conditionalLengthObject = ConditionalLength::createFromString($width); 400 } catch (ExceptionBadArgument $e) { 401 LogUtility::error("The width value ($width) is not valid length. Error: {$e->getMessage()}"); 402 continue; 403 } 404 if (!$conditionalLengthObject->isRatio()) { 405 continue; 406 } 407 $breakpoint = $conditionalLengthObject->getBreakpointOrDefault(); 408 try { 409 $widthColClasses[$breakpoint] = $conditionalLengthObject->toColClass(); 410 $childOpeningTag->removeAttribute(Dimension::WIDTH_KEY); 411 } catch (ExceptionBadArgument $e) { 412 LogUtility::error("The conditional length $conditionalLengthObject could not be transformed as col class. Error: {$e->getMessage()}"); 413 } 414 } 415 if ($widthColClasses !== null) { 416 if (!isset($widthColClasses["xs"])) { 417 $widthColClasses["xs"] = "col-12"; 418 } 419 foreach ($widthColClasses as $widthClass) { 420 $childOpeningTag->addClassName($widthClass); 421 } 422 } 423 } 424 break; 425 case GridTag::TYPE_ROW_TAG: 426 /** 427 * For all box children that is not the last 428 * one, add a padding right 429 */ 430 $length = sizeof($childrenOpeningTags) - 1; 431 for ($i = 0; $i < $length; $i++) { 432 $childOpeningTag = $childrenOpeningTags[$i]; 433 if ($childOpeningTag->getDisplay() === Call::BlOCK_DISPLAY) { 434 $spacing = $childOpeningTag->getAttribute(Spacing::SPACING_ATTRIBUTE); 435 if ($spacing === null) { 436 $childOpeningTag->setAttribute(Spacing::SPACING_ATTRIBUTE, "me-3"); 437 } 438 } 439 } 440 break; 441 default: 442 LogUtility::error("The grid type ($type) is unknown.", GridTag::CANONICAL); 443 } 444 445 /** 446 * Template child callstack ? 447 */ 448 if ($fragmentEndTag !== null && $callStackTemplate !== null) { 449 $fragmentEndTag->setPluginData(FragmentTag::CALLSTACK, $callStackTemplate->getStack()); 450 } 451 452 return array( 453 PluginUtility::ATTRIBUTES => $openingCall->getAttributes() 454 ); 455 } 456 457 public static function renderEnterXhtml(TagAttributes $attributes): string 458 { 459 460 /** 461 * Type 462 */ 463 $type = $attributes->getType(); 464 if ($type === GridTag::TYPE_ROW_TAG) { 465 466 $attributes->addClassName("d-flex"); 467 468 } else { 469 470 $attributes->addClassName("row"); 471 472 /** 473 * Gutter 474 */ 475 $gutterAttributeValue = $attributes->getValueAndRemoveIfPresent(GridTag::GUTTER); 476 $gutters = explode(" ", $gutterAttributeValue); 477 foreach ($gutters as $gutter) { 478 $attributes->addClassName("g$gutter"); 479 } 480 481 } 482 483 /** 484 * Render 485 */ 486 $htmlElement = $attributes->getValueAndRemove(GridTag::HTML_TAG_ATT, "div"); 487 return $attributes->toHtmlEnterTag($htmlElement); 488 489 } 490 491 492 public static function renderExitXhtml(TagAttributes $tagAttributes): string 493 { 494 $htmlElement = $tagAttributes->getValue(GridTag::HTML_TAG_ATT); 495 return "</$htmlElement>"; 496 } 497 498 499} 500