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