diff --git a/.github/scripts/build_assets/arg_getters.py b/.github/scripts/build_assets/arg_getters.py index fe2893a23..4e1d59c06 100644 --- a/.github/scripts/build_assets/arg_getters.py +++ b/.github/scripts/build_assets/arg_getters.py @@ -3,6 +3,10 @@ def get_selenium_runner_args(peek_mode=False): + """ + Get the commandline arguments for the icomoon_peek.py and + icomoon_build.py. + """ parser = ArgumentParser(description="Upload svgs to Icomoon to create icon files.") parser.add_argument("--headless", @@ -33,4 +37,23 @@ def get_selenium_runner_args(peek_mode=False): parser.add_argument("--pr_title", help="The title of the PR that we are peeking at") + return parser.parse_args() + + +def get_check_svgs_args(): + """ + Get the commandline arguments for the chec_svgs.py. + """ + parser = ArgumentParser(description="Check the SVGs to ensure their attributes are correct.") + parser.add_argument("icomoon_json_path", + help="The path to the icomoon.json aka the selection.json created by Icomoon", + action=PathResolverAction) + + parser.add_argument("devicon_json_path", + help="The path to the devicon.json", + action=PathResolverAction) + + parser.add_argument("icons_folder_path", + help="The path to the icons folder", + action=PathResolverAction) return parser.parse_args() \ No newline at end of file diff --git a/.github/scripts/build_assets/drafts/check_devicon_object.py b/.github/scripts/build_assets/drafts/check_devicon_object.py new file mode 100644 index 000000000..b610d88ed --- /dev/null +++ b/.github/scripts/build_assets/drafts/check_devicon_object.py @@ -0,0 +1,37 @@ +from typing import List + +# abandoned since it's not too hard to check devicon objects using our eyes +# however, still keep in case we need it in the future + +def check_devicon_objects(icons: List[dict]): + """ + Check that the devicon objects added is up to standard. + """ + err_msgs = [] + for icon in icons: + if type(icon["name"]) != str: + err_msgs.append("'name' must be a string, not: " + str(icon["name"])) + + try: + for tag in icon["tags"]: + if type(tag) != str: + raise TypeError() + except TypeError: + err_msgs.append("'tags' must be an array of strings, not: " + str(icon["tags"])) + break + + + if type(icon["versions"]["svg"]) != list or len(icon["versions"]["svg"]) == 0: + err_msgs.append("Icon name must be a string") + + if type(icon["versions"]["font"]) != list or len(icon["versions"]["svg"]) == 0: + err_msgs.append("Icon name must be a string") + + if type(icon["color"]) != str or "#" not in icon["color"]: + err_msgs.append("'color' must be a string in the format '#abcdef'") + + if type(icon["aliases"]) != list: + err_msgs.append("'aliases' must be an array of dicts") + + if len(err_msgs) > 0: + raise Exception("Error found in devicon.json: \n" + "\n".join(err_msgs)) diff --git a/.github/scripts/build_assets/filehandler.py b/.github/scripts/build_assets/filehandler.py index 65a1234cd..496a5e0b5 100644 --- a/.github/scripts/build_assets/filehandler.py +++ b/.github/scripts/build_assets/filehandler.py @@ -44,12 +44,16 @@ def is_not_in_icomoon_json(icon, icomoon_json) -> bool: return True -def get_svgs_paths(new_icons: List[dict], icons_folder_path: str) -> List[str]: +def get_svgs_paths(new_icons: List[dict], icons_folder_path: str, + icon_versions_only: bool=False, as_str: bool=True) -> List[str] or List[Path]: """ Get all the suitable svgs file path listed in the devicon.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. + :param icon_versions_only, whether to only get the svgs that can be + made into an icon. + :param: as_str, whether to add the path as a string or as a Path. :return: a list of svg file paths that can be uploaded to Icomoon. """ file_paths = [] @@ -59,27 +63,56 @@ def get_svgs_paths(new_icons: List[dict], icons_folder_path: str) -> List[str]: if not folder_path.is_dir(): raise ValueError(f"Invalid path. This is not a directory: {folder_path}.") - # TODO: remove the try-except when the devicon.json is upgraded - try: - aliases = icon_info["aliases"] - except KeyError: - aliases = [] # create empty list of aliases if not provided in devicon.json + if icon_versions_only: + get_icon_svgs_paths(folder_path, icon_info, file_paths, as_str) + else: + get_all_svgs_paths(folder_path, icon_info, file_paths, as_str) + return file_paths - for font_version in icon_info["versions"]["font"]: - # if it's an alias, we don't want to make it into an icon - if is_alias(font_version, aliases): - print(f"Not exist {icon_info['name']}-{font_version}.svg") - continue - file_name = f"{icon_info['name']}-{font_version}.svg" - path = Path(folder_path, file_name) +def get_icon_svgs_paths(folder_path: Path, icon_info: dict, + file_paths: List[str], as_str: bool): + """ + Get only the svg file paths that can be made into an icon from the icon_info. + :param: folder_path, the folder where we can find the icons. + :param: icon_info, an icon object in the devicon.json. + :param: file_paths, an array containing all the file paths found. + :param: as_str, whether to add the path as a string or as a Path. + """ + aliases = icon_info["aliases"] - if path.exists(): - file_paths.append(str(path)) - else: - raise ValueError(f"This path doesn't exist: {path}") + for font_version in icon_info["versions"]["font"]: + # if it's an alias, we don't want to make it into an icon + if is_alias(font_version, aliases): + print(f"Skipping this font since it's an alias: {icon_info['name']}-{font_version}.svg") + continue - return file_paths + file_name = f"{icon_info['name']}-{font_version}.svg" + path = Path(folder_path, file_name) + + if path.exists(): + file_paths.append(str(path) if as_str else path) + else: + raise ValueError(f"This path doesn't exist: {path}") + + +def get_all_svgs_paths(folder_path: Path, icon_info: dict, + file_paths: List[str], as_str: bool): + """ + Get all the svg file paths of an icon. + :param: folder_path, the folder where we can find the icons. + :param: icon_info, an icon object in the devicon.json. + :param: file_paths, an array containing all the file paths found. + :param: as_str, whether to add the path as a string or as a Path. + """ + for font_version in icon_info["versions"]["svg"]: + file_name = f"{icon_info['name']}-{font_version}.svg" + path = Path(folder_path, file_name) + + if path.exists(): + file_paths.append(str(path) if as_str else path) + else: + raise ValueError(f"This path doesn't exist: {path}") def is_alias(font_version: str, aliases: List[dict]): diff --git a/.github/scripts/build_assets/github_env.py b/.github/scripts/build_assets/github_env.py new file mode 100644 index 000000000..0afc6fff7 --- /dev/null +++ b/.github/scripts/build_assets/github_env.py @@ -0,0 +1,33 @@ +import os +import platform + + +def set_env_var(key: str, value: str, delimiter: str='~'): + """ + Set the GitHub env variable of 'key' to 'value' using + the method specified here: + https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable + Support both Windows and Ubuntu machines provided by GitHub Actions. + + :param: key, the name of the env variable. + :param: value, the value of the env variable. + :param: delimiter, the delimiter that you want to use + to write to the file. Only applicable if the 'value' contains + '\n' character aka a multiline string. + """ + if platform.system() == "Windows": + if "\n" in value: + os.system(f'echo "{key}<<{delimiter}" >> %GITHUB_ENV%') + os.system(f'echo "{value}" >> %GITHUB_ENV%') + os.system(f'echo "{delimiter}" >> %GITHUB_ENV%') + else: + os.system(f'echo "{key}={value}" >> %GITHUB_ENV%') + elif platform.system() == "Linux": + if "\n" in value: + os.system(f'echo "{key}<<{delimiter}" >> $GITHUB_ENV') + os.system(f'echo "{value}" >> $GITHUB_ENV') + os.system(f'echo "{delimiter}" >> $GITHUB_ENV') + else: + os.system(f'echo "{key}={value}" >> $GITHUB_ENV') + else: + raise Exception("This function doesn't support this platform: " + platform.system()) \ No newline at end of file diff --git a/.github/scripts/check_all_icons.py b/.github/scripts/check_all_icons.py new file mode 100644 index 000000000..914e7c49e --- /dev/null +++ b/.github/scripts/check_all_icons.py @@ -0,0 +1,18 @@ +from pathlib import Path +import json + + +# pycharm complains that build_assets is an unresolved ref +# don't worry about it, the script still runs +from build_assets import filehandler + + +if __name__ == "__main__": + """ + Use as a cmd line script to check all the icons of the devicon.json. + """ + devicon_json_path = str(Path("./devicon.json").resolve()) + icons_folder_path = str(Path("./icons").resolve()) + with open(devicon_json_path) as json_file: + devicon_json = json.load(json_file) + svgs = filehandler.get_svgs_paths(devicon_json, icons_folder_path) \ No newline at end of file diff --git a/.github/scripts/check_svgs.py b/.github/scripts/check_svgs.py new file mode 100644 index 000000000..460ee3535 --- /dev/null +++ b/.github/scripts/check_svgs.py @@ -0,0 +1,91 @@ +from typing import List +import sys +import xml.etree.ElementTree as et +import time +from pathlib import Path + + +# pycharm complains that build_assets is an unresolved ref +# don't worry about it, the script still runs +from build_assets import filehandler, arg_getters +from build_assets import github_env + + +def main(): + """ + Check the quality of the svgs. + If any error is found, set an environmental variable called ERR_MSGS + that will contains the error messages. + """ + args = arg_getters.get_check_svgs_args() + new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path) + + if len(new_icons) == 0: + sys.exit("No files need to be uploaded. Ending script...") + + # print list of new icons + print("SVGs being checked:", *new_icons, sep = "\n", end='\n\n') + + time.sleep(1) # do this so the logs stay clean + try: + # check the svgs + svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path, as_str=False) + check_svgs(svgs) + print("All SVGs found were good.\nTask completed.") + except Exception as e: + github_env.set_env_var("ERR_MSGS", str(e)) + sys.exit(str(e)) + + +def check_svgs(svg_file_paths: List[Path]): + """ + Check the width, height, viewBox and style of each svgs passed in. + The viewBox must be '0 0 128 128'. + If the svg has a width and height attr, ensure it's '128px'. + The style must not contain any 'fill' declarations. + If any error is found, they will be thrown. + :param: svg_file_paths, the file paths to the svg to check for. + """ + # batch err messages together so user can fix everything at once + err_msgs = [] + for svg_path in svg_file_paths: + tree = et.parse(svg_path) + root = tree.getroot() + namespace = "{http://www.w3.org/2000/svg}" + err_msg = [f"{svg_path.name}:"] + + if root.tag != f"{namespace}svg": + err_msg.append(f"-root is '{root.tag}'. Root must be an 'svg' element") + + if root.get("viewBox") != "0 0 128 128": + err_msg.append("-'viewBox' is not '0 0 128 128' -> Set it or scale the file using https://www.iloveimg.com/resize-image/resize-svg") + + acceptable_size = [None, "128px", "128"] + if root.get("height") not in acceptable_size: + err_msg.append("-'height' is present in svg element but is not '128' or '128px' -> Remove it or set it to '128' or '128px'") + + if root.get("width") not in acceptable_size: + err_msg.append("-'width' is present in svg element but is not '128' or '128px' -> Remove it or set it to '128' or '128px'") + + if root.get("style") is not None and "enable-background" in root.get("style"): + err_msg.append("-deprecated 'enable-background' in style attribute -> Remove it") + + if root.get("x") is not None: + err_msg.append("-unneccessary 'x' attribute in svg element -> Remove it") + + if root.get("y") is not None: + err_msg.append("-unneccessary 'y' attribute in svg element -> Remove it") + + style = root.findtext(f".//{namespace}style") + if style != None and "fill" in style: + err_msg.append("-contains style declaration using 'fill' -> Replace classes with the 'fill' attribute instead") + + if len(err_msg) > 1: + err_msgs.append("\n".join(err_msg)) + + if len(err_msgs) > 0: + raise Exception("Errors found in these files:\n" + "\n\n".join(err_msgs)) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/icomoon_build.py b/.github/scripts/icomoon_build.py index 303a7ffc6..96d49e737 100644 --- a/.github/scripts/icomoon_build.py +++ b/.github/scripts/icomoon_build.py @@ -22,7 +22,7 @@ def main(): runner = SeleniumRunner(args.download_path, args.geckodriver_path, args.headless) runner.upload_icomoon(args.icomoon_json_path) - svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path) + svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path, True) runner.upload_svgs(svgs) zip_name = "devicon-v1.0.zip" @@ -34,7 +34,7 @@ def main(): except TimeoutException as e: sys.exit("Selenium Time Out Error: \n" + str(e)) except Exception as e: - sys.exit(e) + sys.exit(str(e)) finally: runner.close() diff --git a/.github/scripts/icomoon_peek.py b/.github/scripts/icomoon_peek.py index cccfce5d7..551b58482 100644 --- a/.github/scripts/icomoon_peek.py +++ b/.github/scripts/icomoon_peek.py @@ -2,6 +2,7 @@ import re import sys from selenium.common.exceptions import TimeoutException +import xml.etree.ElementTree as et # pycharm complains that build_assets is an unresolved ref # don't worry about it, the script still runs @@ -21,7 +22,7 @@ def main(): if len(filtered_icons) == 0: message = "No icons found matching the icon name in the PR's title.\n" \ - "Ensure that the PR title matches the convention here: \n" \ + "Ensure that a new icon entry is added in the devicon.json and the PR title matches the convention here: \n" \ "https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#overview.\n" \ "Ending script...\n" sys.exit(message) @@ -33,14 +34,14 @@ def main(): runner = None try: runner = SeleniumRunner(args.download_path, args.geckodriver_path, args.headless) - svgs = filehandler.get_svgs_paths(filtered_icons, args.icons_folder_path) + svgs = filehandler.get_svgs_paths(filtered_icons, args.icons_folder_path, True) screenshot_folder = filehandler.create_screenshot_folder("./") runner.upload_svgs(svgs, screenshot_folder) print("Task completed.") except TimeoutException as e: sys.exit("Selenium Time Out Error: \n" + str(e)) except Exception as e: - sys.exit(e) + sys.exit(str(e)) finally: runner.close() @@ -62,5 +63,7 @@ def find_object_added_in_this_pr(icons: List[dict], pr_title: str): return [] + + if __name__ == "__main__": main() diff --git a/.github/workflows/build_icons.yml b/.github/workflows/build_icons.yml index 29f6e14ec..61abca1fe 100644 --- a/.github/workflows/build_icons.yml +++ b/.github/workflows/build_icons.yml @@ -8,7 +8,6 @@ jobs: - uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} - repository: ${{ github.event.pull_request.head.repo.full_name}} - name: Setup Python v3.8 uses: actions/setup-python@v2 with: @@ -44,13 +43,24 @@ jobs: uses: peter-evans/create-pull-request@v3 env: MESSAGE: | - Automated font-building task ran by GitHub Actions bot. This PR built new font files and devicon.css file. + What's up! - Here are all the files that were built: + I'm Devicon's Build Bot and I just built some new font files and devicon.min.css file. + + Here are all the files that were built into icons: ![Files Built]({0}) + The devicon.min.css file contains: + -The icon content + -The aliases + -The colored classes + -The default fallback font + More information can be found in the GitHub Action logs for this workflow. + + Adios, + Build Bot :sunglasses: with: branch: 'master-build-result' base: 'master' diff --git a/.github/workflows/check_svgs.yml b/.github/workflows/check_svgs.yml new file mode 100644 index 000000000..949159860 --- /dev/null +++ b/.github/workflows/check_svgs.yml @@ -0,0 +1,60 @@ +name: Check SVGs +on: pull_request +jobs: + check: + name: Check the SVGs' quality + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Setup Python v3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: python -m pip install --upgrade pip + - name: Run the check_svg script + run: > + python ./.github/scripts/check_svgs.py ./icomoon.json ./devicon.json ./icons + - name: Comment on the PR about the result - Success + if: success() + uses: github-actions-up-and-running/pr-comment@v1.0.1 + env: + MESSAGE: | + Hi! + I'm Devicons' SVG-Checker Bot and I just checked all the SVGs in this branch. + + Everything looks great. Good job! + + Have a nice day, + SVG-Checker Bot :grin: + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: ${{ env.MESSAGE }} + + - name: Comment on the PR about the result - Failed + if: failure() + uses: github-actions-up-and-running/pr-comment@v1.0.1 + env: + MESSAGE: | + Hi! + + I'm Devicons' SVG-Checker Bot and it seems we've ran into a problem. I'm supposed to check your svgs but I couldn't do my task due to an issue. + + Here is what went wrong: + ``` + {0} + ``` + + For more reference, check out our [CONTRIBUTING guide](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#svgStandards) + + Please address these issues. When you update this PR, I will check your SVGs again. + + Thanks for your help, + SVG-Checker Bot :smile: + + PS. One day, I will be smart enough to fix these errors for you :persevere:. Until then, I can only point them out. + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: ${{ format(env.MESSAGE, env.ERR_MSGS)}} diff --git a/.github/workflows/peek_icons.yml b/.github/workflows/peek_icons.yml index 3d3fedee0..7a028f476 100644 --- a/.github/workflows/peek_icons.yml +++ b/.github/workflows/peek_icons.yml @@ -4,11 +4,13 @@ on: types: [labeled] jobs: build: - name: Get Fonts From Icomoon + name: Peek Icons if: contains(github.event.pull_request.labels.*.name, 'bot:peek') runs-on: windows-2019 steps: - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} - name: Setup Python v3.8 uses: actions/setup-python@v2 with: @@ -42,23 +44,20 @@ jobs: uses: github-actions-up-and-running/pr-comment@v1.0.1 env: MESSAGE: | - Hi! + ~Hi - I'm Devicons' Peek Bot and it seems we've ran into a problem. I'm supposed to check your svgs but I couldn't do my task due to an issue. + I'm Devicons' Peek Bot and it seems we've ran into a problem (sorry!). - Can you please double check and fix the possible issues below: + Please double check and fix the possible issues below: - Your svgs are named and added correctly to the /icons folder as seen [here](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#orgGuidelines). - Your icon information has been added to the `devicon.json` as seen [here](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#updateDevicon) - Your PR title follows the format seen [here](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#overview) - Once everything is fixed, the maintainers will try again. If I still fail, the maintainers will investigate what cause this problem. - - Thank you for your help :smile: + Once everything is fixed, I will try. If I still fail (sorry!), the maintainers will investigate further. - Cheers :), - - Peek Bot + Best of luck, + Peek Bot :relaxed: with: repo-token: ${{ secrets.GITHUB_TOKEN }} message: ${{env.MESSAGE}} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1aca5df9..01f6ace70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,13 +73,14 @@ First of all, thanks for taking the time to contribute! This project can only gr
Before you submit your logos/svgs, please ensure that they meet the following standard:
(Icon name)-(original|plain|line)(-wordmark?).
.svg
file contains one version of an icon in a 0 0 128 128
viewbox..svg
file contains one version of an icon in a 0 0 128 128
viewbox. You can use a service like resize-image for scaling the svg.svg
element does not need the height
and width
attributes. However, if you do use it, ensure their values are either "128"
or "128px"
. Ex: height="128"
.svg
must use the fill
attribute instead of using classes
for colors. See here for more details.(Icon name)-(original|plain|line)(-wordmark?).