1<?php
2
3
4namespace ComboStrap;
5
6
7use ComboStrap\Web\Url;
8use ComboStrap\Xml\XmlDocument;
9
10class Html
11{
12
13
14    /**
15     * @param string $name
16     * @throws ExceptionRuntime
17     * Garbage In / Garbage Out design
18     */
19    public static function validNameGuard(string $name)
20    {
21        /**
22         * If the name is not in lowercase,
23         * the shorthand css selector does not work
24         */
25        $validName = strtolower($name);
26        if ($validName != $name) {
27            throw new ExceptionRuntime("The name ($name) is not a valid name");
28        }
29    }
30
31    /**
32     * Transform a text into a valid HTML id
33     * @param $string
34     * @return string
35     *
36     * See also: https://github.com/Flet/github-slugger
37     */
38    public static function toHtmlId($string): string
39    {
40        /**
41         * sectionId calls cleanID
42         * cleanID delete all things before a ':'
43         * we do then the replace before to not
44         * lost a minus '-' separator
45         */
46        $string = str_replace(array(':', '.'), '', $string);
47        return sectionID($string, $check);
48    }
49
50    /**
51     * Encode special HTML characters to entity (ie escaping)
52     *
53     * This is used to transform text that may be interpreted as HTML
54     * into a text
55     *   * that will not be interpreted as HTML
56     *   * that may be added in html attribute
57     *
58     * For instance:
59     *  * text that should go in attribute with special HTML characters (such as title)
60     *  * text that we don't create (to prevent HTML injection)
61     *
62     * Example:
63     *
64     * <script>...</script>
65     * to
66     * "&lt;script&gt;...&lt;/hello&gt;"
67     *
68     *
69     * @param $text
70     * @return string
71     *
72     * Note that if the `meta[charset]` matches the text encoding   , it should not be encoded
73     *
74     * True ? Beware that this still allows users to insert unsafe scripting vectors, such as markdown links like [xss](javascript:alert%281%29).
75     */
76    public static function encode($text): string
77    {
78        /**
79         * See https://stackoverflow.com/questions/46483/htmlentities-vs-htmlspecialchars/3614344
80         *
81         * Not {@link htmlentities } htmlentities($text, ENT_QUOTES);
82         * Otherwise we get `Error while loading HTMLError: Entity 'hellip' not defined`
83         * when loading HTML with {@link XmlDocument}
84         *
85         * See also {@link self::htmlDecode()}
86         *
87         * Without ENT_QUOTES
88         * <h4 class="heading-combo">
89         * is encoded as
90         * &gt;h4 class="heading-combo"&lt;
91         * and cannot be added in a attribute because of the quote
92         * This is used for {@link Tooltip}
93         */
94        return htmlspecialchars($text, ENT_XHTML | ENT_QUOTES | ENT_DISALLOWED);
95
96    }
97
98    public static function decode($int): string
99    {
100        return htmlspecialchars_decode($int, ENT_XHTML | ENT_QUOTES);
101    }
102
103    public static function getDiffBetweenValuesSeparatedByBlank(string $expected, string $actual, string $expectedName = "expected class", string $actualName = "actual class"): string
104    {
105        $leftClasses = preg_split("/\s/", $expected);
106        $rightClasses = preg_split("/\s/", $actual);
107        $error = "";
108        foreach ($leftClasses as $leftClass) {
109            if (!in_array($leftClass, $rightClasses)) {
110                $error .= "The $expectedName has the value (" . $leftClass . ") that is not present in the $actualName)\n";
111            } else {
112                // Delete the value
113                $key = array_search($leftClass, $rightClasses);
114                unset($rightClasses[$key]);
115            }
116        }
117        foreach ($rightClasses as $rightClass) {
118            $error .= "The $actualName has the value (" . $rightClass . ") that is not present in the $expectedName)\n";
119        }
120        return $error;
121    }
122
123    /**
124     * @throws ExceptionBadSyntax - bad url
125     * @throws ExceptionNotEquals - not equals
126     */
127    public static function getDiffBetweenSrcSet(string $expected, string $actual)
128    {
129        $expectedSrcSets = explode(",", $expected);
130        $actualSrcSets = explode(",", $actual);
131        $countExpected = count($expectedSrcSets);
132        $countActual = count($actualSrcSets);
133        if ($countExpected !== $countActual) {
134            throw new ExceptionNotEquals("The expected srcSet count ($countExpected) is not the same than the actual ($countActual).");
135        }
136        for ($i = 0; $i < $countExpected; $i++) {
137            $expectedSrcSet = trim($expectedSrcSets[$i]);
138            $expectedSrcSetExploded = explode(" ", $expectedSrcSet, 2);
139            $expectedSrc = $expectedSrcSetExploded[0];
140            if (count($expectedSrcSetExploded) == 2) {
141                $expectedWidth = $expectedSrcSetExploded[1];
142            } else {
143                $expectedWidth = null;
144            }
145            $actualSrcSet = trim($actualSrcSets[$i]);
146            $actualSrcSetExploded = explode(" ", $actualSrcSet, 2);
147            $actualSrc = $actualSrcSetExploded[0];
148            if (count($actualSrcSetExploded) == 2) {
149                $actualWidth = $actualSrcSetExploded[1];
150            } else {
151                $actualWidth = null;
152            }
153            if ($expectedWidth !== $actualWidth) {
154                throw new ExceptionNotEquals("The expected width ($expectedWidth) of the srcSet ($i) is not the same than the actual ($actualWidth).");
155            }
156            try {
157                Html::getDiffBetweenUrlStrings($expectedSrc, $actualSrc);
158            } catch (ExceptionBadSyntax $e) {
159                throw new ExceptionBadSyntax("Bad Syntax on Src Set ($i). Error: {$e->getMessage()}. Expected: $expectedSrc vs Actual: $actualSrc. ");
160            } catch (ExceptionNotEquals $e) {
161                throw ExceptionNotEquals::create("Not Equals on Src Set ($i). Error: {$e->getMessage()}.", $expectedSrc, $actualSrc);
162            }
163        }
164    }
165
166
167    /**
168     * @throws ExceptionBadSyntax
169     * @throws ExceptionNotEquals
170     */
171    public static function getDiffBetweenUrlStrings(string $expected, string $actual)
172    {
173        try {
174            $url = Url::createFromString($expected);
175        } catch (ExceptionBadSyntax $e) {
176            throw new ExceptionBadSyntax("The expected URL string ($expected) is not valid. Error: {$e->getMessage()}");
177        }
178        try {
179            $urlActual = Url::createFromString($actual);
180        } catch (ExceptionBadSyntax $e) {
181            throw new ExceptionBadSyntax("The $actual URL string ($actual) is not valid. Error: {$e->getMessage()}");
182        }
183        $url->equals($urlActual);
184
185    }
186
187    /**
188     * Merge class name
189     * @param string $newNames - the name that we want to add
190     * @param ?string $actualNames - the actual names
191     * @return string - the class name list
192     *
193     * for instance:
194     *   * newNames = foo blue
195     *   * actual Name = foo bar
196     * return
197     *   * foo bar blue
198     */
199    public static function mergeClassNames(string $newNames, ?string $actualNames): string
200    {
201        /**
202         * It may be in the form "value1 value2"
203         */
204        $newValues = StringUtility::explodeAndTrim($newNames, " ");
205        if (!empty($actualNames)) {
206            $actualValues = StringUtility::explodeAndTrim(trim($actualNames), " ");
207        } else {
208            $actualValues = [];
209        }
210        $newValues = array_merge($actualValues, $newValues);
211        $newValues = array_unique($newValues);
212        return implode(" ", $newValues);
213    }
214
215    /**
216     * @param array $styleProperties - an array of CSS properties with key, value
217     * @return string - the value for the style attribute (ie all rules where joined with the comma)
218     */
219    public static function array2InlineStyle(array $styleProperties)
220    {
221        $inlineCss = "";
222        foreach ($styleProperties as $key => $value) {
223            $inlineCss .= "$key:$value;";
224        }
225        // Suppress the last ;
226        if ($inlineCss[strlen($inlineCss) - 1] == ";") {
227            $inlineCss = substr($inlineCss, 0, -1);
228        }
229        return $inlineCss;
230    }
231}
232