xref: /plugin/combo/ComboStrap/GridTag.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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