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