diff --git a/.github/scripts/build_assets/PathResolverAction.py b/.github/scripts/build_assets/PathResolverAction.py
new file mode 100644
index 00000000..1876440a
--- /dev/null
+++ b/.github/scripts/build_assets/PathResolverAction.py
@@ -0,0 +1,19 @@
+import argparse
+from pathlib import Path
+
+
+class PathResolverAction(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ path = Path(values).resolve()
+ if not path.exists():
+ raise ValueError(f"{path} doesn't exist.")
+
+ if self.dest == "icons_folder_path":
+ if not path.is_dir():
+ raise ValueError("icons_folder_path must be a directory")
+
+ elif self.dest == "download_path":
+ if not path.is_dir():
+ raise ValueError("download_path must be a directory")
+
+ setattr(namespace, self.dest, str(path))
diff --git a/.github/scripts/build_assets/SeleniumRunner.py b/.github/scripts/build_assets/SeleniumRunner.py
new file mode 100644
index 00000000..643effd7
--- /dev/null
+++ b/.github/scripts/build_assets/SeleniumRunner.py
@@ -0,0 +1,232 @@
+from selenium.webdriver.firefox.webdriver import WebDriver
+from selenium.webdriver.firefox.options import Options
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as ec
+from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
+from typing import List
+
+
+class SeleniumRunner:
+ """
+ A runner that upload and download Icomoon resources using Selenium.
+ The WebDriver will use Firefox.
+ """
+
+ """
+ The long wait time for the driver in seconds.
+ """
+ LONG_WAIT_IN_SEC = 20
+
+ """
+ The medium wait time for the driver in seconds.
+ """
+ MED_WAIT_IN_SEC = 5
+
+ """
+ The short wait time for the driver in seconds.
+ """
+ SHORT_WAIT_IN_SEC = 0.6
+
+ """
+ The Icomoon Url.
+ """
+ ICOMOON_URL = "https://icomoon.io/app/#/select"
+
+ def __init__(self, icomoon_json_path: str, download_path: str,
+ headless=True):
+ """
+ Create a SeleniumRunner object.
+ :param icomoon_json_path: a path to the iconmoon.json.
+ :param download_path: the location where you want to download
+ the icomoon.zip to.
+ svg folders.
+ :param headless: whether to run browser in headless (no UI) mode.
+ """
+ self.icomoon_json_path = icomoon_json_path
+ self.download_path = download_path
+ self.headless = headless
+ self.driver = None
+ self.set_options()
+
+ def set_options(self):
+ """
+ Build the WebDriver with Firefox Options allowing downloads and
+ set download to download_path.
+
+ :raises AssertionError: if the page title does not contain
+ "IcoMoon App".
+ """
+ options = Options()
+ allowed_mime_types = "application/zip, application/gzip, application/octet-stream"
+ # disable prompt to download from Firefox
+ options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types)
+ options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types)
+
+ # set the default download path to downloadPath
+ options.set_preference("browser.download.folderList", 2)
+ options.set_preference("browser.download.dir", self.download_path)
+ options.headless = self.headless
+
+ self.driver = WebDriver(options=options)
+ self.driver.get(self.ICOMOON_URL)
+ assert "IcoMoon App" in self.driver.title
+
+ def upload_icomoon(self):
+ """
+ Upload the icomoon_test.json to icomoon.io.
+ :raises TimeoutException: happens when elements are not found.
+ """
+ print("Uploading JSON file...")
+ try:
+ # find the file input and enter the file path
+ import_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
+ ec.presence_of_element_located((By.CSS_SELECTOR, "div#file input"))
+ )
+ import_btn.send_keys(self.icomoon_json_path)
+ except Exception as e:
+ print("hi")
+ self.close()
+ raise e
+
+ try:
+ confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until(
+ ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']"))
+ )
+ confirm_btn.click()
+ except SeleniumTimeoutException as e:
+ print(e.stacktrace)
+ print("Cannot find the confirm button when uploading the icomoon_test.json",
+ "Ensure that the icomoon_test.json is in the correct format for Icomoon.io",
+ sep='\n')
+ self.close()
+
+ print("JSON file uploaded.")
+
+ def upload_svgs(self, svgs: List[str]):
+ """
+ Upload the SVGs provided in folder_info
+ :param svgs: a list of svg Paths that we'll upload to icomoon.
+ """
+ try:
+ print("Uploading SVGs...")
+
+ edit_mode_btn = self.driver.find_element_by_css_selector(
+ "div.btnBar button i.icon-edit"
+ )
+ edit_mode_btn.click()
+
+ self.click_hamburger_input()
+
+ for svg in svgs:
+ import_btn = self.driver.find_element_by_css_selector(
+ "li.file input[type=file]"
+ )
+ import_btn.send_keys(svg)
+ print(f"Uploaded {svg}")
+ self.test_for_possible_alert()
+ self.remove_color_from_icon()
+
+ self.click_hamburger_input()
+ select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
+ ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']"))
+ )
+ select_all_button.click()
+ except Exception as e:
+ self.close()
+ raise e
+
+ def click_hamburger_input(self):
+ """
+ Click the hamburger input until the pop up menu appears. This
+ method is needed because sometimes, we need to click the hamburger
+ input two times before the menu appears.
+ :return: None.
+ """
+ try:
+ hamburger_input = self.driver.find_element_by_css_selector(
+ "button.btn5.lh-def.transparent i.icon-menu"
+ )
+
+ menu_appear_callback = ec.element_to_be_clickable(
+ (By.CSS_SELECTOR, "h1#setH2 ul")
+ )
+
+ while not menu_appear_callback(self.driver):
+ hamburger_input.click()
+ except Exception as e:
+ self.close()
+ raise e
+
+ def test_for_possible_alert(self):
+ """
+ Test for the possible alert when we upload the svgs.
+ :return: None.
+ """
+ try:
+ dismiss_btn = WebDriverWait(self.driver, self.SHORT_WAIT_IN_SEC, 0.15).until(
+ ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Dismiss']"))
+ )
+ dismiss_btn.click()
+ except SeleniumTimeoutException:
+ pass
+
+ def remove_color_from_icon(self):
+ """
+ Remove the color from the most recent uploaded icon.
+ :return: None.
+ """
+ try:
+ recently_uploaded_icon = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
+ ec.element_to_be_clickable((By.XPATH, "//div[@id='set0']//mi-box[1]//div"))
+ )
+ recently_uploaded_icon.click()
+
+ color_tab = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
+ ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
+ )
+ color_tab.click()
+
+ color_tab = self.driver\
+ .find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
+ color_tab.click()
+
+ close_btn = self.driver\
+ .find_element_by_css_selector("div.overlayWindow i.icon-close")
+ close_btn.click()
+ except Exception as e:
+ self.close()
+ raise e
+
+ def download_icomoon_fonts(self):
+ """
+ Download the icomoon.zip from icomoon.io.
+ """
+ try:
+ print("Downloading Font files...")
+ self.driver.find_element_by_css_selector(
+ "a[href='#/select/font']"
+ ).click()
+
+ button = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
+ ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
+ )
+ button.click()
+ print("Font files downloaded.")
+ except Exception as e:
+ self.close()
+ raise e
+
+ def close(self, err=True):
+ """
+ Close the SeleniumRunner instance.
+ :param err, whether this was called due to an error or not.
+ """
+ print("Closing down SeleniumRunner...")
+ self.driver.quit()
+
+ if err:
+ code = 1
+ else:
+ code = 0
+ exit(code)
diff --git a/.github/scripts/build_assets/filehandler.py b/.github/scripts/build_assets/filehandler.py
new file mode 100644
index 00000000..99f87921
--- /dev/null
+++ b/.github/scripts/build_assets/filehandler.py
@@ -0,0 +1,143 @@
+import json
+from zipfile import ZipFile
+from pathlib import Path
+from typing import List
+import os
+import re
+
+
+def find_new_icons(devicon_json_path: str, icomoon_json_path: str) -> List[dict]:
+ """
+ Find the newly added icons by finding the difference between
+ the devicon_test.json and the icomoon_test.json.
+ :param devicon_json_path, the path to the devicon_test.json.
+ :param icomoon_json_path: a path to the iconmoon.json.
+ :return: a list of the new icons as JSON objects.
+ """
+ with open(devicon_json_path) as json_file:
+ devicon_json = json.load(json_file)
+
+ with open(icomoon_json_path) as json_file:
+ icomoon_json = json.load(json_file)
+
+ new_icons = []
+ for icon in devicon_json:
+ if is_not_in_icomoon_json(icon, icomoon_json):
+ new_icons.append(icon)
+
+ return new_icons
+
+
+def is_not_in_icomoon_json(icon, icomoon_json) -> bool:
+ """
+ Checks whether the icon's name is not in the icomoon_json.
+ :param icon: the icon object we are searching for.
+ :param icomoon_json: the icomoon json object parsed from
+ icomoon_test.json.
+ :return: True if icon's name is not in the icomoon_test.json, else False.
+ """
+ pattern = re.compile(f"^{icon['name']}-")
+
+ for font in icomoon_json["icons"]:
+ if pattern.search(font["properties"]["name"]):
+ return False
+ return True
+
+
+def get_svgs_paths(new_icons: List[dict], icons_folder_path: str) -> List[str]:
+ """
+ Get all the suitable svgs file path listed in the devicon_test.json.
+ :param new_icons, a list containing the info on the new icons.
+ :param icons_folder_path, the path where the function can find the
+ listed folders.
+ :return: a list of svg file paths that can be uploaded to Icomoon.
+ """
+ file_paths = []
+ for icon_info in new_icons:
+ folder_path = Path(icons_folder_path, icon_info['name'])
+
+ if not (folder_path.exists() and folder_path.is_dir()):
+ print(f"Invalid path. This is not a directory: {folder_path}\nSkipping entry...")
+ continue
+
+ try:
+ aliases = icon_info["aliases"]
+ except KeyError:
+ continue
+
+ for font_version in icon_info["versions"]["font"]:
+ if is_alias(font_version, aliases):
+ continue
+
+ file_name = f"{icon_info['name']}-{font_version}.svg"
+ path = Path(folder_path, file_name)
+
+ if path.exists():
+ file_paths.append(str(path))
+
+ return file_paths
+
+
+def is_alias(font_version: str, aliases: List[dict]):
+ """
+ Check whether the font version is an alias of another version.
+ :return: True if it is, else False.
+ """
+ for alias in aliases:
+ if font_version == alias["alias"]:
+ return True
+ return False
+
+
+def extract_files(zip_path: str, extract_path: str, delete=True):
+ """
+ Extract the style.css and font files from the devicon.zip
+ folder. Must call the gulp task "get-icomoon-files"
+ before calling this.
+ :param zip_path, path where the zip file returned
+ from the icomoon.io is located.
+ :param extract_path, the location where the function
+ will put the extracted files.
+ :param delete, whether the function should delete the zip file
+ when it's done.
+ """
+ print("Extracting zipped files...")
+
+ icomoon_zip = ZipFile(zip_path)
+ target_files = ('selection.json', 'fonts/', 'fonts/devicon.ttf',
+ 'fonts/devicon.woff', 'fonts/devicon.eot',
+ 'fonts/devicon.svg', "style.css")
+ for file in target_files:
+ icomoon_zip.extract(file, extract_path)
+
+ print("Files extracted")
+
+ if delete:
+ print("Deleting devicon zip file...")
+ icomoon_zip.close()
+ os.remove(zip_path)
+
+
+def rename_extracted_files(extract_path: str):
+ """
+ Rename the extracted files selection.json and style.css.
+ :param extract_path, the location where the function
+ can find the extracted files.
+ :return: None.
+ """
+ print("Renaming files")
+ old_to_new_list = [
+ {
+ "old": Path(extract_path, "selection.json"),
+ "new": Path(extract_path, "icomoon.json")
+ },
+ {
+ "old": Path(extract_path, "style.css"),
+ "new": Path(extract_path, "devicon.css")
+ }
+ ]
+
+ for dict_ in old_to_new_list:
+ os.replace(dict_["old"], dict_["new"])
+
+ print("Files renamed")
diff --git a/.github/scripts/build_assets/geckodriver.exe b/.github/scripts/build_assets/geckodriver.exe
new file mode 100644
index 00000000..9fae8e0e
Binary files /dev/null and b/.github/scripts/build_assets/geckodriver.exe differ
diff --git a/.github/scripts/icomoon_upload.py b/.github/scripts/icomoon_upload.py
new file mode 100644
index 00000000..44be49d9
--- /dev/null
+++ b/.github/scripts/icomoon_upload.py
@@ -0,0 +1,51 @@
+from build_assets.SeleniumRunner import SeleniumRunner
+import build_assets.filehandler as filehandler
+from pathlib import Path
+from argparse import ArgumentParser
+from build_assets.PathResolverAction import PathResolverAction
+
+
+def main():
+ parser = ArgumentParser(description="Upload svgs to Icomoon to create icon files.")
+
+ parser.add_argument("--headless",
+ help="Whether to run the browser in headless/no UI mode",
+ action="store_true")
+
+ parser.add_argument("icomoon_json_path",
+ help="The path to the icomoon_test.json aka the selection.json created by Icomoon",
+ action=PathResolverAction)
+
+ parser.add_argument("devicon_json_path",
+ help="The path to the devicon_test.json",
+ action=PathResolverAction)
+
+ parser.add_argument("icons_folder_path",
+ help="The path to the icons folder",
+ action=PathResolverAction)
+
+ parser.add_argument("download_path",
+ help="The path where you'd like to download the Icomoon files to",
+ action=PathResolverAction)
+
+ args = parser.parse_args()
+
+ runner = SeleniumRunner(args.icomoon_json_path, args.download_path,
+ args.headless)
+ runner.upload_icomoon()
+
+ new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path)
+ svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path)
+ runner.upload_svgs(svgs)
+ runner.download_icomoon_fonts()
+
+ zip_name = "devicon-v1.0.zip"
+ zip_path = str(Path(args.download_path, zip_name))
+ # filehandler.extract_files(zip_path, args.download_path)
+ # filehandler.rename_extracted_files(args.download_path)
+ runner.close(err=False)
+ print("Task completed.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
new file mode 100644
index 00000000..954f0db0
--- /dev/null
+++ b/.github/scripts/requirements.txt
@@ -0,0 +1 @@
+selenium
\ No newline at end of file
diff --git a/.github/workflows/build_icons.yml b/.github/workflows/build_icons.yml
index 5afab59f..9ab71c8d 100644
--- a/.github/workflows/build_icons.yml
+++ b/.github/workflows/build_icons.yml
@@ -6,7 +6,16 @@ jobs:
name: Get Fonts From Icomoon
runs-on: windows-2019
steps:
- - name: Hello World
- uses: actions/hello-world-javascript-action@v1
- with:
- who-to-greet: 'Mona the Octocat'
\ No newline at end of file
+ - uses: actions/checkout@v2
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r ./.github/scripts/requirements.txt
+ - name: Add geckodriver to PATH
+ run: set PATH=${{ format('{0}/.github/scripts/build_assets/geckodriver.exe', github.workspace) }};%PATH%
+ - name: Run icomoon_upload.py
+ run: python ./.github/scripts/icomoon_upload.py ./icomoon.json ./devicon.json ./icons ./ --headless
diff --git a/devicon.json b/devicon.json
index 73f08d72..b8b6dfb1 100644
--- a/devicon.json
+++ b/devicon.json
@@ -1,12 +1,15 @@
[
{
- "name": "amazonwebservices",
- "tags": ["cloud", "hosting", "server"],
+ "name": "haskell",
+ "color": "#5d5086",
+ "tags": ["language", "functional"],
"versions": {
- "svg": ["original", "original-wordmark", "plain-wordmark"],
- "font": ["original", "plain-wordmark"]
- }
+ "svg": ["original", "original-wordmark", "plain", "plain-wordmark"],
+ "font": ["plain", "plain-wordmark"]
+ },
+ "aliases": []
},
+
{
"name": "android",
"tags": ["os", "mobile"],
diff --git a/haskell/haskell-original-wordmark.svg b/icons/haskell/haskell-original-wordmark.svg
similarity index 100%
rename from haskell/haskell-original-wordmark.svg
rename to icons/haskell/haskell-original-wordmark.svg
diff --git a/haskell/haskell-original-workmark.svg b/icons/haskell/haskell-original-workmark.svg
similarity index 98%
rename from haskell/haskell-original-workmark.svg
rename to icons/haskell/haskell-original-workmark.svg
index ee85d187..6b73e178 100644
--- a/haskell/haskell-original-workmark.svg
+++ b/icons/haskell/haskell-original-workmark.svg
@@ -1,47 +1,47 @@
-
-
-
+
+
+
diff --git a/haskell/haskell-original.svg b/icons/haskell/haskell-original.svg
similarity index 97%
rename from haskell/haskell-original.svg
rename to icons/haskell/haskell-original.svg
index 999609f2..ea13aa20 100644
--- a/haskell/haskell-original.svg
+++ b/icons/haskell/haskell-original.svg
@@ -1,17 +1,17 @@
-
-
-
+
+
+
diff --git a/haskell/haskell-plain-wordmark.svg b/icons/haskell/haskell-plain-wordmark.svg
similarity index 100%
rename from haskell/haskell-plain-wordmark.svg
rename to icons/haskell/haskell-plain-wordmark.svg
diff --git a/haskell/haskell-plain-workmark.svg b/icons/haskell/haskell-plain-workmark.svg
similarity index 98%
rename from haskell/haskell-plain-workmark.svg
rename to icons/haskell/haskell-plain-workmark.svg
index 4d8b8483..bc33c852 100644
--- a/haskell/haskell-plain-workmark.svg
+++ b/icons/haskell/haskell-plain-workmark.svg
@@ -1,44 +1,44 @@
-
-
-
+
+
+
diff --git a/haskell/haskell-plain.svg b/icons/haskell/haskell-plain.svg
similarity index 98%
rename from haskell/haskell-plain.svg
rename to icons/haskell/haskell-plain.svg
index bdb8089c..de1a26ab 100644
--- a/haskell/haskell-plain.svg
+++ b/icons/haskell/haskell-plain.svg
@@ -1,12 +1,12 @@
-
-
-
+
+
+
diff --git a/haskell/haskell.eps b/icons/haskell/haskell.eps
similarity index 100%
rename from haskell/haskell.eps
rename to icons/haskell/haskell.eps
diff --git a/haskell/link.txt b/icons/haskell/link.txt
similarity index 100%
rename from haskell/link.txt
rename to icons/haskell/link.txt