1<?php
2
3
4namespace ComboStrap;
5
6
7/**
8 * Process background attribute
9 */
10class Background
11{
12    /**
13     * Background Logical attribute / Public
14     */
15    const BACKGROUND_COLOR = 'background-color';
16    const BACKGROUND_FILL = "fill";
17
18
19    /**
20     * CSS attribute / not public
21     */
22    const BACKGROUND_IMAGE = 'background-image';
23    const BACKGROUND_SIZE = "background-size";
24    const BACKGROUND_REPEAT = "background-repeat";
25    const BACKGROUND_POSITION = "background-position";
26
27
28    /**
29     * The page canonical of the documentation
30     */
31    const CANONICAL = "background";
32    /**
33     * A component attributes to store backgrounds
34     */
35    const BACKGROUNDS = "backgrounds";
36
37    /**
38     * Pattern css
39     * https://bansal.io/pattern-css
40     */
41    const PATTERN_ATTRIBUTE = "pattern";
42    const PATTERN_CSS_SNIPPET_ID = "pattern.css";
43    const PATTERN_CSS_SIZE = ["sm", "md", "lg", "xl"];
44    const PATTERN_NAMES = ['checks', 'grid', 'dots', 'cross-dots', 'diagonal-lines', 'horizontal-lines', 'vertical-lines', 'diagonal-stripes', 'horizontal-stripes', 'vertical-stripes', 'triangles', 'zigzag'];
45    const PATTERN_CSS_CLASS_PREFIX = "pattern";
46    const PATTERN_COLOR_ATTRIBUTE = "pattern-color";
47
48
49    public static function processBackgroundAttributes(TagAttributes &$tagAttributes)
50    {
51
52        /**
53         * Backgrounds set with the {@link \syntax_plugin_combo_background} component
54         */
55        if ($tagAttributes->hasComponentAttribute(self::BACKGROUNDS)) {
56            PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot(self::CANONICAL);
57            $backgrounds = $tagAttributes->getValueAndRemove(self::BACKGROUNDS);
58            switch (sizeof($backgrounds)) {
59                case 1:
60                    // Only one background was specified
61                    $background = $backgrounds[0];
62                    if (
63                        /**
64                         * We need to create a background node
65                         * if we transform or
66                         * use a CSS pattern (because it use the text color as one of painting color)
67                         */
68                        !isset($background[TagAttributes::TRANSFORM]) &&
69                        !isset($background[self::PATTERN_ATTRIBUTE])
70                    ) {
71                        /**
72                         * For readability,
73                         * we put the background on the parent node
74                         * because there is only one background
75                         */
76                        $tagAttributes->addComponentAttributeValueIfNotEmpty(self::BACKGROUND_IMAGE, $background[self::BACKGROUND_IMAGE]);
77                        $tagAttributes->addComponentAttributeValueIfNotEmpty(self::BACKGROUND_COLOR, $background[self::BACKGROUND_COLOR]);
78                        $tagAttributes->addComponentAttributeValueIfNotEmpty(Opacity::OPACITY_ATTRIBUTE, $background[Opacity::OPACITY_ATTRIBUTE]);
79                        $tagAttributes->addComponentAttributeValueIfNotEmpty(self::BACKGROUND_POSITION, $background[self::BACKGROUND_POSITION]);
80                        $tagAttributes->addComponentAttributeValueIfNotEmpty(self::BACKGROUND_FILL, $background[self::BACKGROUND_FILL]);
81                    } else {
82                        $backgroundTagAttribute = TagAttributes::createFromCallStackArray($background);
83                        $backgroundTagAttribute->addClassName(self::CANONICAL);
84                        $backgroundHTML = "<div class=\"backgrounds\">" . DOKU_LF .
85                            $backgroundTagAttribute->toHtmlEnterTag("div") .
86                            "</div>" . DOKU_LF .
87                            "</div>";
88                        $tagAttributes->addHtmlAfterEnterTag($backgroundHTML);
89                    }
90                    break;
91                default:
92                    /**
93                     * More than one background
94                     * This backgrounds should have been set on the backgrounds
95                     */
96                    $backgroundHTML = "";
97                    foreach ($backgrounds as $background) {
98                        $backgroundTagAttribute = TagAttributes::createFromCallStackArray($background);
99                        $backgroundTagAttribute->addClassName(self::CANONICAL);
100                        $backgroundHTMLEnter = $backgroundTagAttribute->toHtmlEnterTag("div");
101                        $backgroundHTML .= $backgroundHTMLEnter . "</div>";
102                    }
103                    $tagAttributes->addHtmlAfterEnterTag($backgroundHTML);
104                    break;
105            }
106        }
107
108        /**
109         * Background-image attribute
110         */
111        $backgroundImageStyleValue = "";
112        if ($tagAttributes->hasComponentAttribute(self::BACKGROUND_IMAGE)) {
113            $backgroundImageValue = $tagAttributes->getValueAndRemove(self::BACKGROUND_IMAGE);
114            if (is_string($backgroundImageValue)) {
115                /**
116                 * Image background is set by the user
117                 */
118                $backgroundImageStyleValue = $tagAttributes->getValueAndRemove(self::BACKGROUND_IMAGE);
119
120            } else {
121
122                if (is_array($backgroundImageValue)) {
123
124                    /**
125                     * Background-fill for background image
126                     */
127                    $backgroundFill = $tagAttributes->getValueAndRemove(self::BACKGROUND_FILL, "cover");
128                    switch ($backgroundFill) {
129                        case "cover":
130                            // it makes the background responsive
131                            $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_SIZE, $backgroundFill);
132                            $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_REPEAT, "no-repeat");
133                            $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_POSITION, "center center");
134
135                            /**
136                             * The type of image is important for the processing of SVG
137                             */
138                            $backgroundImageValue[TagAttributes::TYPE_KEY] = SvgDocument::ILLUSTRATION_TYPE;
139                            break;
140                        case "tile":
141                            // background size is then "auto" (ie repeat), the default
142                            // background position is not needed (the tile start on the left top corner)
143                            $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_REPEAT, "repeat");
144
145                            /**
146                             * The type of image is important for the processing of SVG
147                             * A tile needs to have a width and a height
148                             */
149                            $backgroundImageValue[TagAttributes::TYPE_KEY] = SvgDocument::TILE_TYPE;
150                            break;
151                        case "css":
152                            // custom, set by the user in own css stylesheet, nothing to do
153                            break;
154                        default:
155                            LogUtility::msg("The background `fill` attribute ($backgroundFill) is unknown. If you want to take over the filling via css, set the `fill` value to `css`.", self::CANONICAL);
156                            break;
157                    }
158
159
160                    $media = MediaLink::createFromCallStackArray($backgroundImageValue);
161                    if ($media instanceof ThirdMediaLink) {
162                        LogUtility::msg("The background image ($media) is not supported", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
163                        return;
164                    }
165                    /**
166                     * @var Image $image
167                     */
168                    $image = $media->getDefaultImage();
169                    if (!FileSystems::exists($image->getPath())) {
170                        LogUtility::msg("The image ($media) does not exist", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
171                        return;
172                    }
173                    $url = $image->getUrl(DokuwikiUrl::AMPERSAND_CHARACTER);
174                    $backgroundImageStyleValue = "url(" . $url . ")";
175
176                } else {
177                    LogUtility::msg("Internal Error: The background image value ($backgroundImageValue) is not a string nor an array", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
178                }
179
180            }
181        }
182        if (!empty($backgroundImageStyleValue)) {
183            if ($tagAttributes->hasComponentAttribute(Opacity::OPACITY_ATTRIBUTE)) {
184                $opacity = $tagAttributes->getValueAndRemove(Opacity::OPACITY_ATTRIBUTE);
185                $finalOpacity = 1 - $opacity;
186                /**
187                 * In the argument of linear-gradient, we don't use `0 100%` to apply the
188                 * same image everywhere
189                 *
190                 * because the validator https://validator.w3.org/ would complain with
191                 * `
192                 * CSS: background-image: too few values for the property linear-gradient.
193                 * `
194                 */
195                $backgroundImageStyleValue = "linear-gradient(to right, rgba(255,255,255, $finalOpacity) 0 50%, rgba(255,255,255, $finalOpacity) 50% 100%)," . $backgroundImageStyleValue;
196            }
197            $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_IMAGE, $backgroundImageStyleValue);
198        }
199
200
201        /**
202         * Process the pattern css
203         * https://bansal.io/pattern-css
204         * This call should be before the processing of the background color
205         * because it will set one if it's not available
206         */
207        self::processPatternAttribute($tagAttributes);
208
209        /**
210         * Background color
211         */
212        if ($tagAttributes->hasComponentAttribute(self::BACKGROUND_COLOR)) {
213
214            $colorValue = $tagAttributes->getValueAndRemove(self::BACKGROUND_COLOR);
215
216            $gradientPrefix = 'gradient-';
217            if (strpos($colorValue, $gradientPrefix) === 0) {
218                /**
219                 * A gradient is an image
220                 * Check that there is no image
221                 */
222                if (!empty($backgroundImageStyleValue)) {
223                    LogUtility::msg("An image and a linear gradient color are exclusive because a linear gradient color creates an image. You can't use the linear color (" . $colorValue . ") and the image (" . $backgroundImageStyleValue . ")", LogUtility::LVL_MSG_WARNING, self::CANONICAL);
224                } else {
225                    $mainColorValue = substr($colorValue, strlen($gradientPrefix));
226                    $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_IMAGE, 'linear-gradient(to top,#fff 0,' . ColorRgb::createFromString($mainColorValue)->toCssValue() . ' 100%)');
227                    $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_COLOR, 'unset!important');
228                }
229            } else {
230
231                $colorValue = ColorRgb::createFromString($colorValue)->toCssValue();
232                $tagAttributes->addStyleDeclarationIfNotSet(self::BACKGROUND_COLOR, $colorValue);
233
234            }
235        }
236
237
238    }
239
240    /**
241     * Return a background array with background properties
242     * from a media {@link MediaLink::toCallStackArray()}
243     * @param array $mediaCallStackArray
244     * @return array
245     */
246    public static function fromMediaToBackgroundImageStackArray(array $mediaCallStackArray): array
247    {
248        $backgroundProperties = [];
249        foreach ($mediaCallStackArray as $key => $property) {
250            switch ($key) {
251                case MediaLink::LINKING_KEY:
252                case TagAttributes::TITLE_KEY:
253                case MediaLink::ALIGN_KEY:
254                case FloatAttribute::FLOAT_KEY: // Float is when the image is at the right
255                    /**
256                     * Attributes not taken
257                     */
258                    break;
259                case TagAttributes::TYPE_KEY: // needed for the metadata registration
260                default:
261                    /**
262                     * Attributes taken
263                     */
264                    $backgroundProperties[$key] = $property;
265                    break;
266            }
267        }
268        return $backgroundProperties;
269    }
270
271    /**
272     * @param TagAttributes $tagAttributes
273     * Process the `pattern` attribute
274     * that implements
275     * https://bansal.io/pattern-css
276     */
277    private static function processPatternAttribute(TagAttributes &$tagAttributes)
278    {
279        /**
280         * Css Pattern
281         */
282        if ($tagAttributes->hasComponentAttribute(self::PATTERN_ATTRIBUTE)) {
283
284            /**
285             * Attach the stylesheet
286             */
287            PluginUtility::getSnippetManager()->attachCssExternalStyleSheetForSlot(
288                self::PATTERN_CSS_SNIPPET_ID,
289                "https://cdn.jsdelivr.net/npm/pattern.css@1.0.0/dist/pattern.min.css",
290                "sha256-Vwich3JPJa27TO9g6q+TxJGE7DNEigBaHNPm+KkMR6o=")
291                ->setCritical(false); // not blocking for rendering
292
293
294            $patternValue = strtolower($tagAttributes->getValueAndRemove(self::PATTERN_ATTRIBUTE));
295
296            $lastIndexOfMinus = StringUtility::lastIndexOf($patternValue, "-");
297            $lastMinusPart = substr($patternValue, $lastIndexOfMinus);
298            /**
299             * Do we have the size as last part
300             */
301            if (!in_array($lastMinusPart, self::PATTERN_CSS_SIZE)) {
302                /**
303                 * We don't have a size
304                 */
305                $pattern = $patternValue;
306                $size = "md";
307            } else {
308                $pattern = substr($patternValue, 0, $lastIndexOfMinus);
309                $size = $lastMinusPart;
310            }
311            /**
312             * Does this pattern is a known pattern
313             */
314            if (!in_array($pattern, self::PATTERN_NAMES)) {
315                LogUtility::msg("The pattern (" . $pattern . ") is not a known CSS pattern and was ignored.", LogUtility::LVL_MSG_WARNING, self::CANONICAL);
316                return;
317            } else {
318                $tagAttributes->addClassName(self::PATTERN_CSS_CLASS_PREFIX . "-" . $pattern . "-" . $size);
319            }
320
321            if (!$tagAttributes->hasComponentAttribute(self::BACKGROUND_COLOR)) {
322                LogUtility::msg("The background color was not set for the background with the (" . $pattern . "). It was set to the default color.", LogUtility::LVL_MSG_INFO, self::CANONICAL);
323                $tagAttributes->addComponentAttributeValue(self::BACKGROUND_COLOR, "steelblue");
324            }
325            /**
326             * Color
327             */
328            if ($tagAttributes->hasComponentAttribute(self::PATTERN_COLOR_ATTRIBUTE)) {
329                $patternColor = $tagAttributes->getValueAndRemove(self::PATTERN_COLOR_ATTRIBUTE);
330            } else {
331                LogUtility::msg("The pattern color was not set for the background with the (" . $pattern . "). It was set to the default color.", LogUtility::LVL_MSG_INFO, self::CANONICAL);
332                $patternColor = "#FDE482";
333            }
334            $tagAttributes->addStyleDeclarationIfNotSet(ColorRgb::COLOR, $patternColor);
335
336        }
337    }
338
339
340}
341