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