1<?php
2/**
3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15require_once(__DIR__ . '/PluginUtility.php');
16
17
18/**
19 * Class IconUtility
20 * @package ComboStrap
21 * @see https://combostrap.com/icon
22 *
23 *
24 * Material design does not have a repository structure where we can extract the location
25 * from the name
26 * https://material.io/resources/icons https://google.github.io/material-design-icons/
27 *
28 * Injection via javascript to avoid problem with the php svgsimple library
29 * https://www.npmjs.com/package/svg-injector
30 */
31class Icon extends ImageSvg
32{
33    const CONF_ICONS_MEDIA_NAMESPACE = "icons_namespace";
34    const CONF_ICONS_MEDIA_NAMESPACE_DEFAULT = ":" . PluginUtility::COMBOSTRAP_NAMESPACE_NAME . ":icons";
35    // Canonical name
36    const NAME = "icon";
37
38
39    const ICON_LIBRARY_URLS = array(
40        self::BOOTSTRAP => "https://raw.githubusercontent.com/twbs/icons/main/icons",
41        self::MATERIAL_DESIGN => "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg",
42        self::FEATHER => "https://raw.githubusercontent.com/feathericons/feather/master/icons",
43        self::CODE_ICON => "https://raw.githubusercontent.com/microsoft/vscode-codicons/main/src/icons",
44        self::LOGOS => "https://raw.githubusercontent.com/gilbarbara/logos/master/logos",
45        self::CARBON => "https://raw.githubusercontent.com/carbon-design-system/carbon/main/packages/icons/src/svg/32",
46        self::TWEET_EMOJI => "https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg",
47        self::ANT_DESIGN => "https://raw.githubusercontent.com/ant-design/ant-design-icons/master/packages/icons-svg/svg",
48        self::FAD => "https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs",
49        self::CLARITY => "https://raw.githubusercontent.com/vmware/clarity-assets/master/icons/essential",
50        self::OCTICON => "https://raw.githubusercontent.com/primer/octicons/main/icons"
51    );
52
53    const ICON_LIBRARY_WEBSITE_URLS = array(
54        self::BOOTSTRAP => "https://icons.getbootstrap.com/",
55        self::MATERIAL_DESIGN => "https://materialdesignicons.com/",
56        self::FEATHER => "https://feathericons.com/",
57        self::CODE_ICON => "https://microsoft.github.io/vscode-codicons/",
58        self::LOGOS => "https://svgporn.com/",
59        self::CARBON => "https://www.carbondesignsystem.com/guidelines/icons/library/",
60        self::TWEET_EMOJI => "https://twemoji.twitter.com/",
61        self::ANT_DESIGN => "https://ant.design/components/icon/",
62        self::CLARITY => "https://clarity.design/foundation/icons/",
63        self::OCTICON => "https://primer.style/octicons/"
64    );
65
66    const CONF_DEFAULT_ICON_LIBRARY = "defaultIconLibrary";
67    const CONF_DEFAULT_ICON_LIBRARY_DEFAULT = self::MATERIAL_DESIGN_ACRONYM;
68
69    /**
70     * Deprecated library acronym / name
71     */
72    const DEPRECATED_LIBRARY_ACRONYM = array(
73        "bs" => self::BOOTSTRAP, // old one (deprecated) - the good acronym is bi (seen also in the class)
74        "md" => self::MATERIAL_DESIGN
75    );
76
77    /**
78     * Public known acronym / name (Used in the configuration)
79     */
80    const PUBLIC_LIBRARY_ACRONYM = array(
81        "bi" => self::BOOTSTRAP,
82        self::MATERIAL_DESIGN_ACRONYM => self::MATERIAL_DESIGN,
83        "fe" => self::FEATHER,
84        "codicon" => self::CODE_ICON,
85        "logos" => self::LOGOS,
86        "carbon" => self::CARBON,
87        "twemoji" => self::TWEET_EMOJI,
88        "ant-design" => self::ANT_DESIGN,
89        "fad" => self::FAD,
90        "clarity" => self::CLARITY,
91        "octicon" => self::OCTICON
92    );
93
94    const FEATHER = "feather";
95    const BOOTSTRAP = "bootstrap";
96    const MATERIAL_DESIGN = "material-design";
97    const CODE_ICON = "codicon";
98    const LOGOS = "logos";
99    const CARBON = "carbon";
100    const MATERIAL_DESIGN_ACRONYM = "mdi";
101    const TWEET_EMOJI = "twemoji";
102    const ANT_DESIGN = "ant-design";
103    const FAD = "fad";
104    const CLARITY = "clarity";
105    const OCTICON = "octicon";
106
107
108    /**
109     * The function used to render an icon
110     * @param TagAttributes $tagAttributes -  the icon attributes
111     * @return Icon
112     * @throws ExceptionCombo
113     */
114    static public function create(TagAttributes $tagAttributes): Icon
115    {
116
117
118        $name = "name";
119        if (!$tagAttributes->hasComponentAttribute($name)) {
120            throw new ExceptionCombo("The attributes should have a name. It's mandatory for an icon.", self::NAME);
121        }
122
123        /**
124         * The Name
125         */
126        $iconNameAttribute = $tagAttributes->getValue($name);
127
128        /**
129         * If the name have an extension, it's a file from the media directory
130         * Otherwise, it's an icon from a library
131         */
132        $mediaDokuPath = DokuPath::createMediaPathFromId($iconNameAttribute);
133        if (!empty($mediaDokuPath->getExtension())) {
134
135            // loop through candidates until a match was found:
136            // May be an icon from the templates
137            if (!FileSystems::exists($mediaDokuPath)) {
138
139                // Trying to see if it's not in the template images directory
140                $message = "The media file could not be found in the media library. If you want an icon from an icon library, indicate a name without extension.";
141                $message .= "<BR> Media File Library tested: $mediaDokuPath";
142                throw new ExceptionCombo($message, self::NAME);
143
144
145            }
146
147        } else {
148
149            // It may be a icon already downloaded
150            $iconNameSpace = PluginUtility::getConfValue(self::CONF_ICONS_MEDIA_NAMESPACE, self::CONF_ICONS_MEDIA_NAMESPACE_DEFAULT);
151            if (substr($iconNameSpace, 0, 1) != DokuPath::PATH_SEPARATOR) {
152                $iconNameSpace = DokuPath::PATH_SEPARATOR . $iconNameSpace;
153            }
154            if (substr($iconNameSpace, -1) != DokuPath::PATH_SEPARATOR) {
155                $iconNameSpace = $iconNameSpace . ":";
156            }
157            $mediaPathId = $iconNameSpace . $iconNameAttribute . ".svg";
158            $mediaDokuPath = DokuPath::createMediaPathFromAbsolutePath($mediaPathId);
159
160            // Bug: null file created when the stream could not get any byte
161            // We delete them
162            if (FileSystems::exists($mediaDokuPath)) {
163                if (FileSystems::getSize($mediaDokuPath) == 0) {
164                    FileSystems::delete($mediaDokuPath);
165                }
166            }
167
168            if (!FileSystems::exists($mediaDokuPath)) {
169
170                /**
171                 * Download the icon
172                 */
173
174                // Create the target directory if it does not exist
175                $iconDir = $mediaDokuPath->getParent();
176                if (!FileSystems::exists($iconDir)) {
177                    try {
178                        FileSystems::createDirectory($iconDir);
179                    } catch (ExceptionCombo $e) {
180                        throw new ExceptionCombo("The icon directory ($iconDir) could not be created.", self::NAME, 0, $e);
181                    }
182                }
183
184                // Name parsing to extract the library name and icon name
185                $sepPosition = strpos($iconNameAttribute, ":");
186                $library = PluginUtility::getConfValue(self::CONF_DEFAULT_ICON_LIBRARY, self::CONF_DEFAULT_ICON_LIBRARY_DEFAULT);
187                $iconName = $iconNameAttribute;
188                if ($sepPosition != false) {
189                    $library = substr($iconNameAttribute, 0, $sepPosition);
190                    $iconName = substr($iconNameAttribute, $sepPosition + 1);
191                }
192
193                // Get the qualified library name
194                $acronymLibraries = self::getLibraries();
195                if (isset($acronymLibraries[$library])) {
196                    $library = $acronymLibraries[$library];
197                }
198
199                // Get the url
200                $iconLibraries = self::ICON_LIBRARY_URLS;
201                if (!isset($iconLibraries[$library])) {
202                    throw new ExceptionCombo("The icon library ($library) is unknown. The icon could not be downloaded.", self::NAME);
203                } else {
204                    $iconBaseUrl = $iconLibraries[$library];
205                }
206
207                /**
208                 * Name processing
209                 */
210                switch ($library) {
211
212                    case self::TWEET_EMOJI:
213                        try {
214                            $iconName = self::getEmojiCodePoint($iconName);
215                        } catch (ExceptionCombo $e) {
216                            throw new ExceptionCombo("The emoji name $iconName is unknown. The emoji could not be downloaded.", self::NAME, 0, $e);
217                        }
218                        break;
219                    case self::ANT_DESIGN:
220                        // table-outlined where table is the svg, outlined the category
221                        // ordered-list-outlined where ordered-list is the svg, outlined the category
222                        $iconProcessed = $iconName;
223                        $index = strrpos($iconProcessed, "-");
224                        if ($index === false) {
225                            throw new ExceptionCombo ("We expect that a ant design icon name ($iconName) has two parts separated by a `-` (example: table-outlined). The icon could not be downloaded.", self::NAME);
226                        }
227                        $iconName = substr($iconProcessed, 0, $index);
228                        $iconType = substr($iconProcessed, $index + 1);
229                        $iconBaseUrl .= "/$iconType";
230                        break;
231                    case self::CARBON:
232                        $iconName = self::getCarbonPhysicalName($iconName);
233                        break;
234                    case self::FAD:
235                        $iconName = self::getFadPhysicalName($iconName);
236                }
237
238
239                // The url
240                $downloadUrl = "$iconBaseUrl/$iconName.svg";
241                $filePointer = @fopen($downloadUrl, 'r');
242                if ($filePointer != false) {
243
244                    $numberOfByte = @file_put_contents($mediaDokuPath->toLocalPath()->toAbsolutePath()->toString(), $filePointer);
245                    if ($numberOfByte != false) {
246                        LogUtility::msg("The icon ($iconName) from the library ($library) was downloaded to ($mediaPathId)", LogUtility::LVL_MSG_INFO, self::NAME);
247                    } else {
248                        LogUtility::msg("Internal error: The icon ($iconName) from the library ($library) could no be written to ($mediaPathId)", LogUtility::LVL_MSG_ERROR, self::NAME);
249                    }
250
251                } else {
252
253                    // (ie no icon file found at ($downloadUrl)
254                    $urlLibrary = self::ICON_LIBRARY_WEBSITE_URLS[$library];
255                    LogUtility::msg("The library (<a href=\"$urlLibrary\">$library</a>) does not have a icon (<a href=\"$downloadUrl\">$iconName</a>).", LogUtility::LVL_MSG_ERROR, self::NAME);
256
257                }
258
259            }
260
261        }
262
263        /**
264         * After optimization, the width and height of the svg are gone
265         * but the icon type set them again
266         *
267         * The icon type is used to set:
268         *   * the default dimension
269         *   * color styling
270         *   * disable the responsive properties
271         *
272         */
273        $tagAttributes->addComponentAttributeValue(TagAttributes::TYPE_KEY, SvgDocument::ICON_TYPE);
274
275        return new Icon($mediaDokuPath, $tagAttributes);
276
277    }
278
279    /**
280     * @param $iconName
281     * @param $mediaFilePath
282     * @deprecated Old code to download icon from the material design api
283     */
284    public
285    static function downloadIconFromMaterialDesignApi($iconName, $mediaFilePath)
286    {
287        // Try the official API
288        // Read the icon meta of
289        // Meta Json file got all icons
290        //
291        //   * Available at: https://raw.githubusercontent.com/Templarian/MaterialDesign/master/meta.json
292        //   * See doc: https://github.com/Templarian/MaterialDesign-Site/blob/master/src/content/api.md)
293        $arrayFormat = true;
294        $iconMetaJson = json_decode(file_get_contents(__DIR__ . '/../resources/dictionary/icon-meta.json'), $arrayFormat);
295        $iconId = null;
296        foreach ($iconMetaJson as $key => $value) {
297            if ($value['name'] == $iconName) {
298                $iconId = $value['id'];
299                break;
300            }
301        }
302        if ($iconId != null) {
303
304            // Download
305            // Call to the API
306            // https://dev.materialdesignicons.com/contribute/site/api
307            $downloadUrl = "https://materialdesignicons.com/api/download/icon/svg/$iconId";
308            $filePointer = file_put_contents($mediaFilePath, fopen($downloadUrl, 'r'));
309            if ($filePointer == false) {
310                LogUtility::msg("The file ($downloadUrl) could not be downloaded to ($mediaFilePath)", LogUtility::LVL_MSG_ERROR, self::NAME);
311            } else {
312                LogUtility::msg("The material design icon ($iconName) was downloaded to ($mediaFilePath)", LogUtility::LVL_MSG_INFO, self::NAME);
313            }
314
315        }
316
317    }
318
319    private static function getLibraries(): array
320    {
321        return array_merge(
322            self::PUBLIC_LIBRARY_ACRONYM,
323            self::DEPRECATED_LIBRARY_ACRONYM
324        );
325    }
326
327    /**
328     * @throws ExceptionCombo
329     */
330    public static function getEmojiCodePoint(string $emojiName)
331    {
332        $path = LocalPath::createFromPath(Resources::getDictionaryDirectory() . "/emojis.json");
333        $jsonContent = FileSystems::getContent($path);
334        $jsonArray = Json::createFromString($jsonContent)->toArray();
335        return $jsonArray[$emojiName];
336    }
337
338    /**
339     * Iconify normalized the name of the carbon library (making them lowercase)
340     *
341     * For instance, CSV is csv (https://icon-sets.iconify.design/carbon/csv/)
342     *
343     * This dictionary reproduce it.
344     *
345     * @param string $logicalName
346     * @return mixed
347     * @throws ExceptionCombo
348     */
349    private static function getCarbonPhysicalName(string $logicalName)
350    {
351        $path = LocalPath::createFromPath(Resources::getDictionaryDirectory() . "/carbon-icons.json");
352        $jsonContent = FileSystems::getContent($path);
353        $jsonArray = Json::createFromString($jsonContent)->toArray();
354        $physicalName = $jsonArray[$logicalName];
355        if ($physicalName === null) {
356            LogUtility::msg("The icon ($logicalName) is unknown as 32x32 carbon icon");
357            // by default, just lowercase
358            return lower($logicalName);
359        }
360        return $physicalName;
361    }
362
363    /**
364     * @throws ExceptionCombo
365     */
366    private static function getFadPhysicalName($logicalName)
367    {
368        $path = LocalPath::createFromPath(Resources::getDictionaryDirectory() . "/fad-icons.json");
369        $jsonContent = FileSystems::getContent($path);
370        $jsonArray = Json::createFromString($jsonContent)->toArray();
371        $physicalName = $jsonArray[$logicalName];
372        if ($physicalName === null) {
373            LogUtility::msg("The icon ($logicalName) is unknown as fad icon");
374            return $logicalName;
375        }
376        return $physicalName;
377    }
378
379
380    public function render(): string
381    {
382
383        if (FileSystems::exists($this->getPath())) {
384
385            $svgImageLink = SvgImageLink::createMediaLinkFromPath(
386                $this->getPath(),
387                $this->getAttributes()
388            );
389            return $svgImageLink->renderMediaTag();
390
391        } else {
392
393            LogUtility::msg("The icon ($this) does not exist and cannot be rendered.");
394            return "";
395
396        }
397
398    }
399
400
401}
402