Skip to content

Commit

Permalink
Addition of a script to facilitate the creation of the Material Desig…
Browse files Browse the repository at this point in the history
…n icons (either in React component or SVG form or both).

This commit also includes documentation added to the developer guide and separate documentation describing how to use the script, how to create icons manually if required and how to use the icons in either React or the templates.
  • Loading branch information
nick-next committed Nov 19, 2024
1 parent b64ffa8 commit 0d27b14
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 2 deletions.
8 changes: 8 additions & 0 deletions docs/developer_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,11 @@ the same region.
### Testing cloudbuild changes

To test .yaml cloudbuild files, you can use cloud-build-local to dry run the file before actually pushing. Find documentation for how to install and use cloud-build-local [here](https://github.com/GoogleCloudPlatform/cloud-build-local).

### Inline Icons

The Data Commons site makes use of Material Design icons. In certain cases, font-based Material Design icon usage can result in
flashes of unstyled content that can be avoided by using SVG icons.

We have provided tools to facilitate the creation and use of Material SVG icons in both the Jinja template and in React components.
For instructions on how to generate and use these SVGs and components, please see: [Icon Readme](../tools/resources/README.md):
2 changes: 1 addition & 1 deletion server/templates/resources/icons/progress_activity.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion static/js/components/elements/icons/keyboard_arrow_down.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
* limitations under the License.
*/

/*
* Autogenerated by generate_icon.py
*
*/

/*
* Material Icon: Keyboard Arrow Down
* Source: https://fonts.google.com/icons
* Source: https://github.com/google/material-design-icons
*/

import React, { ReactElement } from "react";
Expand Down
111 changes: 111 additions & 0 deletions tools/resources/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Data Commons Material UI Icon Generation Tool

## Produce via script

A python script is provided to quickly and easily generate a React component
that displays a Material UI Icon, an SVG of a Material UI icon for use in the
Jinja templates or both.

### Script Usage

To generate both a React component and an SVG of a Material UI icon, run the
following command:

```bash
python3 generate_icon.py {icon_name}
````

For example:

```bash
python3 generate_icon.py arrow_forward
````
This will download the SVG from the Material UI repository, process it and
generate two files:
- An SVG for use in Jinja templates in the `server/templates/resources/icons`
directory.
- A React component in `static/js/components/elements/icons`.
To generate only the SVG for the templates, run either of the following commands:
```bash
python3 generate_icon.py -f arrow_forward
python3 generate_icon.py --flask arrow_forward
```
To generate only the React component run either of the following commands:
```bash
python3 generate_icon.py -r arrow_forward
python3 generate_icon.py --react arrow_forward
```
It is recommended to run the prettier on the generated `.tsx` file.
## Produce manually
If for any reason you need to generate an icon manually (for example, if an icon
is not available from the repository via the script), you can do so with these
directions. Note that this should only rarely be required.
1. Download the SVG. If it is a Material UI font, it will likely come from
https://fonts.google.com/icons
2. Change the height to "1em". Change the fill to "currentColor". Remove the width.
3. For use in the Jinja templates, copy this SVG into `server/templates/resources/icons`.
4. For use as a React component
1. open an existing component in `static/js/components/elements/icons`, and save it
with the new icon's name.
2. Update the source to indicate the source of the SVG you are using. This might
be https://fonts.google.com/icons.
3. Update the name in the comments.
4. Paste the SVG over top of the old SVG.
5. Add {...props} as the final prop in the SVG before the `>`.
## Usage
### Jinja Templates
To use a generated icon in a Jinja template
```
{% from 'macros/icons.html' import inline_svg %}
{{ inline_svg('arrow_forward') }}
```
### React
You can use the React component directly inside the JSX:
```jsx
<ArrowForward />
```
If done so without explicitly providing color and size, the icon
will inherit its color and size from the enclosing CSS, just as a
font icon would. For example:
```css
span.big-red-icon {
font-size: 50px;
color: red;
}
```
```jsx
<span className="big-red-icon">
<ArrowForward />
</span>
```
This allows you to use the React component icons much as you would the Material
Icons provided through Google Fonts, with the added advantage that they are inline
and not subject to flashes of unstyled content.
You can also style the icons directly with its props. Each added icon
component can take any prop that you can send into an SVG.
```jsx
<ArrowForward
fill="red"
height="50px"
/>
```
32 changes: 32 additions & 0 deletions tools/resources/component_template.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright {{ year }} Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/*
* Autogenerated by generate_icon.py
*/

/*
* Material Icon: {{ icon name }}
* Source: https://github.com/google/material-design-icons
*/

import React, { ReactElement } from "react";

export const {{ icon component name }} = (
props: React.SVGProps<SVGSVGElement>
): ReactElement => (
{{ svg }}
);
181 changes: 181 additions & 0 deletions tools/resources/generate_icon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This script imports a Material Design icon by name in snake_case. (e.g. chevron_left).
# The name of the icon is provided as a parameter.
# With the -r or --react flag, only the React component will be created.
# With the -f or --flask flag, the version for use in the Jinja templates will be created
# With no flag given, both will be created.

import argparse
from datetime import datetime
import os
import sys
import xml.etree.ElementTree as ET

import requests


def parse_arguments():
parser = argparse.ArgumentParser(
description=
'Download and generate Material Design icons for use in Jinja templates and as React components.'
)
parser.add_argument(
'icon_name',
type=str,
help='Name of the icon in snake_case (e.g., chevron_left)')
parser.add_argument('-r',
'--react',
action='store_true',
help='Generate only the React component version')
parser.add_argument(
'-f',
'--flask',
action='store_true',
help='Generate only the SVG version for use in Jinja templates')
return parser.parse_args()


def convert_snake_to_camel(icon_name):
components = icon_name.split('_')
return ''.join(x.capitalize() for x in components)


def convert_snake_to_title(icon_name):
components = icon_name.split('_')
return ' '.join(x.capitalize() for x in components)


def download_svg(icon_name):
"""
Downloads the requested SVG from the Material Design icon repository.
"""
base_url = 'https://raw.githubusercontent.com/google/material-design-icons/refs/heads/master/symbols/web'
svg_url = f'{base_url}/{icon_name}/materialsymbolsoutlined/{icon_name}_24px.svg'

print(f'Downloading SVG from: {svg_url}')
response = requests.get(svg_url)
if response.status_code == 200:
print('SVG download complete.')
return response.text
else:
print(f'Error: Failed to download SVG. Status Code: {response.status_code}')
return None


def process_svg(svg_content):
"""
Processes the SVG content to prepare to allow it to be styled through CSS similar to how a font is
"""

ET.register_namespace('', "http://www.w3.org/2000/svg")

try:
root = ET.fromstring(svg_content)
except ET.ParseError as e:
print(f'Error parsing SVG: {e}')
return None

if 'width' in root.attrib:
del root.attrib['width']

root.set('height', '1em')

root.set('fill', 'currentColor')

for elem in root.iter():
if 'fill' in elem.attrib:
elem.set('fill', 'currentColor')

processed_svg = ET.tostring(root, encoding='unicode')
return processed_svg


def save_svg(svg_content, output_path):
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(svg_content)
print(f'Saved SVG to {output_path}')


def generate_react_component(icon_name, svg_content, react_dir, template_path):
"""
Generates a React .tsx component for the icon based on the template found in "component_template.txt"
"""
component_name = convert_snake_to_camel(icon_name)
icon_title = convert_snake_to_title(icon_name)
current_year = datetime.now().year
try:
with open(template_path, 'r', encoding='utf-8') as template_file:
template = template_file.read()
except FileNotFoundError:
print(f'Error: Template file not found at {template_path}.')
return

svg_tag_start = svg_content.find('<svg')
svg_tag_end = svg_content.find('>', svg_tag_start)

# We need to add {...props} so the props of the SVG can be overridden by the component.
svg_with_props = (svg_content[:svg_tag_end] + ' {...props}' +
svg_content[svg_tag_end:])

component = template.replace('{{ year }}', str(current_year))
component = component.replace('{{ icon name }}', icon_title)
component = component.replace('{{ icon component name }}', component_name)
component = component.replace('{{ svg }}', svg_with_props)

component_file_name = f'{icon_name}.tsx'
component_path = os.path.join(react_dir, component_file_name)

with open(component_path, 'w', encoding='utf-8') as f:
f.write(component)
print(f'Generated React component at {component_path}')


def main():
args = parse_arguments()
icon_name = args.icon_name.lower()
generate_react_svg = args.react or not (args.react or args.flask)
generate_flask_svg = args.flask or not (args.react or args.flask)

script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.abspath(os.path.join(script_dir, '..', '..'))

html_icons_dir = os.path.join(root_dir, 'server', 'templates', 'resources',
'icons')
react_icons_dir = os.path.join(root_dir, 'static', 'js', 'components',
'elements', 'icons')

template_path = os.path.join(script_dir, 'component_template.txt')

svg_content = download_svg(icon_name)
if not svg_content:
sys.exit(1)

processed_svg = process_svg(svg_content)
if not processed_svg:
sys.exit(1)

if generate_flask_svg:
html_svg_path = os.path.join(html_icons_dir, f'{icon_name}.svg')
save_svg(processed_svg, html_svg_path)

if generate_react_svg:
generate_react_component(icon_name, processed_svg, react_icons_dir,
template_path)


if __name__ == '__main__':
main()

0 comments on commit 0d27b14

Please sign in to comment.