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