1<?php
2
3
4namespace ComboStrap;
5
6
7use ComboStrap\TagAttribute\StyleAttribute;
8use dokuwiki\Extension\SyntaxPlugin;
9use syntax_plugin_combo_xmlinlinetag;
10use syntax_plugin_combo_link;
11
12class Dimension
13{
14    /**
15     * The element that have an width and height
16     */
17    const NATURAL_SIZING_ELEMENT = [SvgImageLink::CANONICAL, RasterImageLink::CANONICAL];
18
19    const DESIGN_LAYOUT_CONSTRAINED = "constrained"; // fix value
20    const DESIGN_LAYOUT_FLUID = "fluid"; // adapt
21
22    /**
23     * On the width, if set, the design is fluid and will adapt to all screen
24     * with a min-width
25     */
26    const WIDTH_LAYOUT_DEFAULT = self::DESIGN_LAYOUT_FLUID;
27    /**
28     * On height, if set, the design is constrained and overflow
29     */
30    const HEIGHT_LAYOUT_DEFAULT = self::DESIGN_LAYOUT_CONSTRAINED;
31    const SCROLL = "scroll";
32    const SCROLL_TOGGLE_MECHANISM = "toggle";
33    const SCROLL_LIFT_MECHANISM = "lift";
34
35    /**
36     * Logical height and width
37     * used by default to define the width and height of an image or a css box
38     */
39    const HEIGHT_KEY = 'height';
40    const WIDTH_KEY = 'width';
41
42    /**
43     * The ratio (16:9, ...) permits to change:
44     *   * the viewBox in svg
45     *   * the intrinsic dimension in raster
46     *
47     * It's then part of the request
48     * because in svg it is the definition of the viewBox
49     *
50     * The rendering function takes care of it
51     * and it's also passed in the fetch url
52     */
53    public const RATIO_ATTRIBUTE = "ratio";
54    const ZOOM_ATTRIBUTE = "zoom";
55
56    const CANONICAL = "dimension";
57    public const WIDTH_KEY_SHORT = "w";
58    public const HEIGHT_KEY_SHORT = "h";
59
60
61    /**
62     * @param TagAttributes $attributes
63     */
64    public static function processWidthAndHeight(TagAttributes &$attributes)
65    {
66        self::processWidth($attributes);
67
68        $heightName = self::HEIGHT_KEY;
69        if ($attributes->hasComponentAttribute($heightName)) {
70            $heightValue = trim($attributes->getValueAndRemove($heightName));
71            if ($heightValue !== "") {
72                $heightValue = TagAttributes::toQualifiedCssValue($heightValue);
73
74                if (in_array($attributes->getLogicalTag(), self::NATURAL_SIZING_ELEMENT)) {
75
76                    /**
77                     * A element with a natural height is responsive, we set only the max-height
78                     *
79                     * By default, the image has a `height: auto` due to the img-fluid class
80                     * Making its height responsive
81                     */
82                    $attributes->addStyleDeclarationIfNotSet("max-height", $heightValue);
83
84                } else {
85
86                    /**
87                     * HTML Block
88                     *
89                     * Without the height value, a block display will collapse
90                     */
91                    if (self::HEIGHT_LAYOUT_DEFAULT == self::DESIGN_LAYOUT_CONSTRAINED) {
92
93                        /**
94                         * The box is constrained in height
95                         * By default, a box is not constrained
96                         */
97                        $attributes->addStyleDeclarationIfNotSet("height", $heightValue);
98
99                        $scrollMechanism = $attributes->getValueAndRemoveIfPresent(Dimension::SCROLL);
100                        if ($scrollMechanism !== null) {
101                            $scrollMechanism = trim(strtolower($scrollMechanism));
102                        }
103                        switch ($scrollMechanism) {
104                            case self::SCROLL_TOGGLE_MECHANISM:
105                                // https://jsfiddle.net/gerardnico/h0g6xw58/
106                                $attributes->addStyleDeclarationIfNotSet("overflow-y", "hidden");
107                                $attributes->addStyleDeclarationIfNotSet("position", "relative");
108                                $attributes->addStyleDeclarationIfNotSet("display", "block");
109                                // The block should collapse to this height
110                                $attributes->addStyleDeclarationIfNotSet("min-height", $heightValue);
111                                if ($attributes->hasComponentAttribute("id")) {
112                                    $id = $attributes->getValue("id");
113                                } else {
114                                    $id = $attributes->generateAndSetId();
115                                }
116                                /**
117                                 * Css of the button and other standard attribute
118                                 */
119                                PluginUtility::getSnippetManager()->attachCssInternalStyleSheet("height-toggle");
120                                /**
121                                 * Set the color dynamically to the color of the parent
122                                 */
123                                PluginUtility::getSnippetManager()->attachJavascriptFromComponentId("height-toggle");
124
125                                $toggleOnClickId = "height-toggle-onclick";
126                                $attributes->addClassName(StyleAttribute::addComboStrapSuffix($toggleOnClickId));
127                                $attributes->addStyleDeclarationIfNotSet("cursor", "pointer");
128                                PluginUtility::getSnippetManager()->attachJavascriptFromComponentId($toggleOnClickId);
129
130                                /**
131                                 * The height when there is not the show class
132                                 * is the original height
133                                 */
134                                $css = <<<EOF
135#$id:not(.show){
136  height: $heightValue;
137  transition: height .35s ease;
138}
139EOF;
140                                PluginUtility::getSnippetManager()->attachCssInternalStyleSheet("height-toggle-show", $css);
141                                $bootstrapDataNameSpace = Bootstrap::getDataNamespace();
142                                $buttonClass = StyleAttribute::addComboStrapSuffix("height-toggle");
143                                /** @noinspection HtmlUnknownAttribute */
144                                $button = <<<EOF
145<button class="$buttonClass" data$bootstrapDataNameSpace-toggle="collapse" data$bootstrapDataNameSpace-target="#$id" aria-expanded="false"></button>
146EOF;
147
148                                $attributes->addHtmlAfterEnterTag($button);
149
150                                break;
151                            case self::SCROLL_LIFT_MECHANISM;
152                            default:
153                                $attributes->addStyleDeclarationIfNotSet("overflow", "auto");
154                                break;
155
156                        }
157
158
159                    } else {
160
161                        /**
162                         * if fluid
163                         * min-height and not height to not constraint the box
164                         */
165                        $attributes->addStyleDeclarationIfNotSet("min-height", $heightValue);
166
167                    }
168                }
169            }
170
171        }
172    }
173
174    /**
175     *
176     * Toggle with a click on the collapsed element
177     * if there is no control element such as button or link inside
178     *
179     * This function is used at the {@link DOKU_LEXER_EXIT} state of a {@link SyntaxPlugin::handle()}
180     *
181     * @param CallStack $callStack
182     */
183    public static function addScrollToggleOnClickIfNoControl(CallStack $callStack)
184    {
185        $callStack->moveToEnd();
186        $openingCall = $callStack->moveToPreviousCorrespondingOpeningCall();
187        $scrollAttribute = $openingCall->getAttribute(Dimension::SCROLL);
188        if ($scrollAttribute !== self::SCROLL_TOGGLE_MECHANISM) {
189            return;
190        }
191        while ($actualCall = $callStack->next()) {
192            if (in_array($actualCall->getTagName(),
193                [ButtonTag::MARKUP_LONG, syntax_plugin_combo_link::TAG, "internallink", "externallink"])) {
194                $openingCall->setAttribute(Dimension::SCROLL, Dimension::SCROLL_LIFT_MECHANISM);
195                return;
196            }
197        }
198
199    }
200
201    /**
202     * @param $value - a css value to a pixel
203     * @throws ExceptionCompile
204     * @deprecated for {@link ConditionalLength::toPixelNumber()}
205     */
206    public static function toPixelValue($value): int
207    {
208
209        return ConditionalLength::createFromString($value)->toPixelNumber();
210
211    }
212
213    /**
214     * Convert 16:9, ... to a float
215     * @param string $stringRatio
216     * @return float
217     * @throws ExceptionBadSyntax
218     */
219    public static function convertTextualRatioToNumber(string $stringRatio): float
220    {
221        list($width, $height) = explode(":", $stringRatio, 2);
222        try {
223            $width = DataType::toInteger($width);
224        } catch (ExceptionCompile $e) {
225            throw new ExceptionBadSyntax("The width value ($width) of the ratio `$stringRatio` is not numeric", PageImageTag::CANONICAL);
226        }
227        try {
228            $height = DataType::toInteger($height);
229        } catch (ExceptionCompile $e) {
230            throw new ExceptionBadSyntax("The width value ($height) of the ratio `$stringRatio` is not numeric", PageImageTag::CANONICAL);
231        }
232        if ($height === 0) {
233            throw new ExceptionBadSyntax("The height value of the ratio `$stringRatio` should not be zero", PageImageTag::CANONICAL);
234        }
235        return floatval($width / $height);
236
237    }
238
239    private static function processWidth(TagAttributes $attributes)
240    {
241        $widthValueAsString = $attributes->getComponentAttributeValueAndRemoveIfPresent(self::WIDTH_KEY);
242        if ($widthValueAsString === null) {
243            return;
244        }
245
246        $widthValueAsString = trim($widthValueAsString);
247        $logicalTag = $attributes->getLogicalTag();
248        if ($widthValueAsString === "") {
249            LogUtility::error("The width value is empty for the tag ({$logicalTag})");
250            return;
251        }
252        $widthValues = explode(" ", $widthValueAsString);
253        foreach ($widthValues as $widthValue) {
254
255            try {
256                $conditionalWidthLength = ConditionalLength::createFromString($widthValue);
257            } catch (ExceptionBadArgument $e) {
258                LogUtility::error("The width value ($widthValue) is not a valid length. Error: {$e->getMessage()}");
259                continue;
260            }
261
262
263            /**
264             * For an image (png, svg)
265             * They have width and height **element** attribute
266             */
267            if (in_array($logicalTag, self::NATURAL_SIZING_ELEMENT)) {
268
269                /**
270                 * If the image is not asked as static resource (ie HTTP request)
271                 * but added in HTML
272                 * (ie {@link \action_plugin_combo_svg})
273                 */
274                $requestedMime = $attributes->getMime();
275                if ($requestedMime == TagAttributes::TEXT_HTML_MIME) {
276
277                    $length = $conditionalWidthLength->getLength();
278                    if ($length === "0") {
279
280                        /**
281                         * For an image, the dimension are restricted by height
282                         */
283                        if ($attributes->hasComponentAttribute(self::HEIGHT_KEY)) {
284                            $attributes->addStyleDeclarationIfNotSet("width", "auto");
285                        }
286                        return;
287
288                    }
289
290                    /**
291                     * For an image, the dimension are restricted by width
292                     * (max-width or 100% of the container )
293                     */
294                    try {
295                        $attributes->addStyleDeclarationIfNotSet('max-width', $conditionalWidthLength->toCssLength());
296                    } catch (ExceptionBadArgument $e) {
297                        LogUtility::error("The conditional length ($conditionalWidthLength) could not be transformed as CSS value. Error", self::CANONICAL);
298                        $attributes->addStyleDeclarationIfNotSet('max-width', $conditionalWidthLength->getLength());
299                    }
300                    $attributes->addStyleDeclarationIfNotSet('width', "100%");
301                }
302                return;
303            }
304
305            /**
306             * For a element without natural sizing
307             */
308            $unit = $conditionalWidthLength->getLengthUnit();
309            switch ($unit) {
310                case ConditionalLength::PERCENTAGE:
311                    try {
312                        $attributes->addClassName($conditionalWidthLength->toColClass());
313                    } catch (ExceptionBadArgument $e) {
314                        LogUtility::error("The conditional length ($conditionalWidthLength) could not be converted to a col class. Error: {$e->getMessage()}");
315                    }
316                    break;
317                default:
318                    try {
319                        $attributes->addStyleDeclarationIfNotSet('max-width', $conditionalWidthLength->toCssLength());
320                        // to overcome the setting 'fit-content' set by auto ...
321                        $attributes->setStyleDeclaration('width', 'auto');
322                    } catch (ExceptionBadArgument $e) {
323                        LogUtility::error("The conditional length ($conditionalWidthLength) could not be transformed as CSS value. Error", self::CANONICAL);
324                        $attributes->addStyleDeclarationIfNotSet('max-width', $conditionalWidthLength->getLength());
325                    }
326                    break;
327            }
328
329
330        }
331
332    }
333
334
335}
336