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 if ($firstChildTag === false) { 262 LogUtility::warning("The grid seems to not have any closed children"); 263 return array(PluginUtility::ATTRIBUTES => $openingCall->getAttributes()); 264 } 265 $childrenOpeningTags = []; 266 267 $fragmentEndTag = null; // the template end tag that has the instructions 268 $callStackTemplate = null; // the instructions in callstack form to modify the children 269 if ($firstChildTag !== false && $firstChildTag->getTagName() === FragmentTag::FRAGMENT_TAG && $firstChildTag->getState() === DOKU_LEXER_ENTER) { 270 $fragmentEndTag = $callStack->next(); 271 if ($fragmentEndTag->getTagName() !== FragmentTag::FRAGMENT_TAG || $fragmentEndTag->getState() !== DOKU_LEXER_EXIT) { 272 LogUtility::error("Error internal: We were unable to find the closing template tag.", GridTag::CANONICAL); 273 return $returnArray; 274 } 275 $templateInstructions = $fragmentEndTag->getPluginData(FragmentTag::CALLSTACK); 276 $callStackTemplate = CallStack::createFromInstructions($templateInstructions); 277 $callStackTemplate->moveToStart(); 278 $firstChildTag = $callStackTemplate->moveToFirstChildTag(); 279 if ($firstChildTag !== false) { 280 $childrenOpeningTags[] = $firstChildTag; 281 while ($actualCall = $callStackTemplate->moveToNextSiblingTag()) { 282 $childrenOpeningTags[] = $actualCall; 283 } 284 } 285 286 } else { 287 288 $childrenOpeningTags[] = $firstChildTag; 289 while ($actualCall = $callStack->moveToNextSiblingTag()) { 290 $childrenOpeningTags[] = $actualCall; 291 } 292 293 } 294 295 296 if ($type !== GridTag::TYPE_ROW_TAG) { 297 298 /** 299 * Scan and process the children for a grid tag 300 * - Add the col class 301 * - Do the cells have a width set ... 302 */ 303 foreach ($childrenOpeningTags as $actualCall) { 304 305 $actualCall->addClassName("col"); 306 307 $widthAttributeValue = $actualCall->getAttribute(Dimension::WIDTH_KEY); 308 if ($widthAttributeValue !== null) { 309 $type = GridTag::TYPE_WIDTH_SPECIFIED; 310 $conditionalWidthsLengths = explode(" ", $widthAttributeValue); 311 foreach ($conditionalWidthsLengths as $conditionalWidthsLength) { 312 try { 313 $conditionalLengthObject = ConditionalLength::createFromString($conditionalWidthsLength); 314 } catch (ExceptionBadArgument $e) { 315 $type = null; 316 LogUtility::error("The width length $conditionalWidthsLength is not a valid length value. Error: {$e->getMessage()}"); 317 break; 318 } 319 try { 320 if ($conditionalLengthObject->isRatio()) { 321 $ratio = $conditionalLengthObject->getRatio(); 322 if ($ratio > 1) { 323 LogUtility::warning("The ratio ($ratio) of the width ($conditionalLengthObject) should not be greater than 1 on the children of the row", GridTag::CANONICAL); 324 break; 325 } 326 } 327 } catch (ExceptionBadArgument $e) { 328 $type = null; 329 LogUtility::error("The ratio of the width ($conditionalLengthObject) is not a valid. Error: {$e->getMessage()}"); 330 break; 331 } 332 } 333 } 334 } 335 } 336 337 if ($type === null) { 338 $type = GridTag::TYPE_MAX_CHILDREN; 339 } 340 /** 341 * Setting the type on the opening tag to see the chosen type in the html attribute 342 */ 343 $openingCall->setType($type); 344 345 346 /** 347 * Type is now known 348 * Do the Distribution calculation 349 */ 350 switch ($type) { 351 case GridTag::TYPE_MAX_CHILDREN: 352 $maxLineDefaults = []; 353 try { 354 $maxLineDefaults["xs"] = ConditionalLength::createFromString("1-xs"); 355 $maxLineDefaults["sm"] = ConditionalLength::createFromString("2-sm"); 356 $maxLineDefaults["md"] = ConditionalLength::createFromString("3-md"); 357 $maxLineDefaults["lg"] = ConditionalLength::createFromString("4-lg"); 358 } catch (ExceptionBadArgument $e) { 359 LogUtility::error("Bad default value initialization. Error:{$e->getMessage()}", GridTag::CANONICAL); 360 } 361 /** 362 * Delete the default that are bigger than: 363 * * the asked max-line number 364 * * or the number of children (ie if there is two children, they split the space in two) 365 */ 366 $maxLineDefaultsFiltered = []; 367 $maxLineUsedToFilter = sizeof($childrenOpeningTags); 368 if ($fragmentEndTag !== null) { 369 // there is only one child in a iterator 370 $maxLineUsedToFilter = self::ITERATOR_DEFAULT_MAX_LINE; 371 } 372 if ($maxLineAttributeValue !== null && $maxLineUsedToFilter > $maxLineAttributeValue) { 373 $maxLineUsedToFilter = $maxLineAttributeValue; 374 } 375 foreach ($maxLineDefaults as $breakpoint => $maxLineDefault) { 376 if ($maxLineDefault->getNumerator() <= $maxLineUsedToFilter) { 377 $maxLineDefaultsFiltered[$breakpoint] = $maxLineDefault; 378 } 379 } 380 $maxLineArray = array_merge($maxLineDefaultsFiltered, $maxLineArray); 381 foreach ($maxLineArray as $maxCell) { 382 /** 383 * @var ConditionalLength $maxCell 384 */ 385 try { 386 $openingCall->addClassName($maxCell->toRowColsClass()); 387 } catch (ExceptionBadArgument $e) { 388 LogUtility::error("Error while adding the row-col class. Error: {$e->getMessage()}"); 389 } 390 } 391 break; 392 case GridTag::TYPE_WIDTH_SPECIFIED: 393 394 foreach ($childrenOpeningTags as $childOpeningTag) { 395 $widthAttributeValue = $childOpeningTag->getAttribute(Dimension::WIDTH_KEY); 396 if ($widthAttributeValue === null) { 397 continue; 398 } 399 $widthValues = explode(" ", $widthAttributeValue); 400 $widthColClasses = null; 401 foreach ($widthValues as $width) { 402 try { 403 $conditionalLengthObject = ConditionalLength::createFromString($width); 404 } catch (ExceptionBadArgument $e) { 405 LogUtility::error("The width value ($width) is not valid length. Error: {$e->getMessage()}"); 406 continue; 407 } 408 if (!$conditionalLengthObject->isRatio()) { 409 continue; 410 } 411 $breakpoint = $conditionalLengthObject->getBreakpointOrDefault(); 412 try { 413 $widthColClasses[$breakpoint] = $conditionalLengthObject->toColClass(); 414 $childOpeningTag->removeAttribute(Dimension::WIDTH_KEY); 415 } catch (ExceptionBadArgument $e) { 416 LogUtility::error("The conditional length $conditionalLengthObject could not be transformed as col class. Error: {$e->getMessage()}"); 417 } 418 } 419 if ($widthColClasses !== null) { 420 if (!isset($widthColClasses["xs"])) { 421 $widthColClasses["xs"] = "col-12"; 422 } 423 foreach ($widthColClasses as $widthClass) { 424 $childOpeningTag->addClassName($widthClass); 425 } 426 } 427 } 428 break; 429 case GridTag::TYPE_ROW_TAG: 430 /** 431 * For all box children that is not the last 432 * one, add a padding right 433 */ 434 $length = sizeof($childrenOpeningTags) - 1; 435 for ($i = 0; $i < $length; $i++) { 436 $childOpeningTag = $childrenOpeningTags[$i]; 437 if ($childOpeningTag->getDisplay() === Call::BlOCK_DISPLAY) { 438 $spacing = $childOpeningTag->getAttribute(Spacing::SPACING_ATTRIBUTE); 439 if ($spacing === null) { 440 $childOpeningTag->setAttribute(Spacing::SPACING_ATTRIBUTE, "me-3"); 441 } 442 } 443 } 444 break; 445 default: 446 LogUtility::error("The grid type ($type) is unknown.", GridTag::CANONICAL); 447 } 448 449 /** 450 * Template child callstack ? 451 */ 452 if ($fragmentEndTag !== null && $callStackTemplate !== null) { 453 $fragmentEndTag->setPluginData(FragmentTag::CALLSTACK, $callStackTemplate->getStack()); 454 } 455 456 return array( 457 PluginUtility::ATTRIBUTES => $openingCall->getAttributes() 458 ); 459 } 460 461 public static function renderEnterXhtml(TagAttributes $attributes): string 462 { 463 464 /** 465 * Type 466 */ 467 $type = $attributes->getType(); 468 if ($type === GridTag::TYPE_ROW_TAG) { 469 470 $attributes->addClassName("d-flex"); 471 472 } else { 473 474 $attributes->addClassName("row"); 475 476 /** 477 * Gutter 478 */ 479 $gutterAttributeValue = $attributes->getValueAndRemoveIfPresent(GridTag::GUTTER); 480 $gutters = explode(" ", $gutterAttributeValue); 481 foreach ($gutters as $gutter) { 482 $attributes->addClassName("g$gutter"); 483 } 484 485 } 486 487 /** 488 * Render 489 */ 490 $htmlElement = $attributes->getValueAndRemove(GridTag::HTML_TAG_ATT, "div"); 491 return $attributes->toHtmlEnterTag($htmlElement); 492 493 } 494 495 496 public static function renderExitXhtml(TagAttributes $tagAttributes): string 497 { 498 $htmlElement = $tagAttributes->getValue(GridTag::HTML_TAG_ATT); 499 return "</$htmlElement>"; 500 } 501 502 503} 504