Skip to content

Instantly share code, notes, and snippets.

@SeanPesce
Last active June 16, 2025 12:54
Show Gist options
  • Save SeanPesce/83ea143e6dffd1cfd9d4f990d3d8408b to your computer and use it in GitHub Desktop.
Save SeanPesce/83ea143e6dffd1cfd9d4f990d3d8408b to your computer and use it in GitHub Desktop.
Python code to convert Android vector drawable XML files to SVG format
#!/usr/bin/env python3
# Author: Sean Pesce
#
# Converted from original JavaScript code by Seanghay Yath:
# https://github.com/seanghay/vector-drawable-svg
import math
import sys
from xml.etree.ElementTree import Element, fromstring, tostring
from xml.dom import minidom
path_transformers = [
lambda vdAttrs: {'d': vdAttrs.get('android:pathData')},
lambda vdAttrs: {
'fill': convert_hex_color(vdAttrs.get('android:fillColor'), vdAttrs.get('android:fillAlpha'))[0],
'fill-opacity': convert_hex_color(vdAttrs.get('android:fillColor'), vdAttrs.get('android:fillAlpha'))[1] if convert_hex_color(vdAttrs.get('android:fillColor'), vdAttrs.get('android:fillAlpha'))[1] != 1 else None
},
lambda vdAttrs: {'stroke-linejoin': vdAttrs.get('android:strokeLineJoin')},
lambda vdAttrs: {'stroke-linecap': vdAttrs.get('android:strokeLineCap')},
lambda vdAttrs: {'stroke-miterlimit': vdAttrs.get('android:strokeMiterLimit')},
lambda vdAttrs: convert_vector_drawable_stroke_attributes(vdAttrs),
lambda vdAttrs: {'stroke-width': vdAttrs.get('android:strokeWidth')},
lambda vdAttrs: {
'fill-rule': vdAttrs['android:fillType'].lower() if (('android:fillType' in vdAttrs) and vdAttrs['android:fillType']) else None
},
]
group_transformers = [
lambda vdAttrs: {'id': vdAttrs.get('android:name')},
lambda vdAttrs: {
'transform': ' '.join([
f'translate({vdAttrs.get("android:translateX", 0)}, {vdAttrs.get("android:translateY", 0)})' if (vdAttrs.get("android:translateX", 0) != 0 or vdAttrs.get("android:translateY", 0) != 0) else '',
f'rotate({vdAttrs.get("android:rotation", 0)})' if vdAttrs.get("android:rotation", 0) != 0 else '',
f'scale({vdAttrs.get("android:scaleX", 1)}, {vdAttrs.get("android:scaleY", 1)})' if (vdAttrs.get("android:scaleX", 1) != 1 or vdAttrs.get("android:scaleY", 1) != 1) else ''
# @TODO: Handle 'android:pivotX' and 'android:pivotY'?
]).strip() or None
},
]
gradient_transformers = [
lambda vdAttrs: {'x1': vdAttrs.get('android:startX')},
lambda vdAttrs: {'y1': vdAttrs.get('android:startY')},
lambda vdAttrs: {'x2': vdAttrs.get('android:endX')},
lambda vdAttrs: {'y2': vdAttrs.get('android:endY')},
lambda vdAttrs: {'cx': vdAttrs.get('android:centerX')},
lambda vdAttrs: {'cy': vdAttrs.get('android:centerY')},
lambda vdAttrs: {'r': vdAttrs.get('android:gradientRadius')},
]
gradient_item_transformers = [
lambda vdAttrs: {
'stop-color': convert_hex_color(vdAttrs.get('android:color'))[0],
'stop-opacity': convert_hex_color(vdAttrs.get('android:color'))[1] if convert_hex_color(vdAttrs['android:color'])[1] != 1 else None
},
lambda vdAttrs: {'offset': vdAttrs.get('android:offset')},
]
def parse_android_resource(value):
"""
Parse Android XML Resources and returns an object.
:param {string | undefined} value
:returns {Object.<string, string>}
"""
if not isinstance(value, str):
return
doc = minidom.parseString(value)
resources_node = doc.getElementsByTagName('resources')[0]
if not resources_node:
return
resource_map = {}
for node in resources_node.childNodes:
if node.nodeType != node.ELEMENT_NODE:
continue
if node.firstChild.nodeType != node.TEXT_NODE:
continue
key = f"@{node.tagName}/{node.getAttribute('name')}"
value = node.firstChild.nodeValue
resource_map[key] = value
for key, value in resource_map.items():
if re.match(r'@\w+/\w+', value):
if value in resource_map:
resource_map[key] = resource_map[value]
return resource_map
def transform_attributes(vd_node, svg_node, transformers):
if not vd_node.attributes:
return
vd_attrs = {attr: vd_node.attributes.values().mapping[attr].value for attr in vd_node.attributes.values().mapping}
for transformer in transformers:
svg_attrs = transformer(vd_attrs)
for name, value in svg_attrs.items():
if value is not None:
svg_node.setAttribute(name, value)
return
def parse_path(root, path_node):
svg_path = root.createElement('path')
svg_path.setAttribute('fill', 'none')
transform_attributes(path_node, svg_path, path_transformers)
return svg_path
def parse_gradient(root, gradient_node):
gradient_type = gradient_node.getAttribute('android:type')
def svg_gradient(gradient_type):
if gradient_type == 'linear':
return root.createElement('linearGradient')
elif gradient_type == 'radial':
return root.createElement('radialGradient')
elif gradient_type == 'sweep':
raise ValueError('Sweep gradient is not compatible by SVG')
else:
raise ValueError('invalid gradient type')
svg_gradient = svg_gradient(gradient_type)
svg_gradient.setAttribute('gradientUnits', 'userSpaceOnUse')
transform_attributes(gradient_node, svg_gradient, gradient_transformers)
for it in gradient_node.childNodes:
if it.tagName == 'item':
svg_gradient_stop = root.createElement('stop')
transform_attributes(it, svg_gradient_stop, gradient_item_transformers)
svg_gradient.appendChild(svg_gradient_stop)
return svg_gradient
def transform_node(node, parent, root, defs):
if type(node) == minidom.Text:
return None
if node.tagName == 'path':
svg_path = parse_path(root, node)
for it in node.childNodes:
if it.tagName == 'aapt:attr':
attr_name = it.getAttribute('name')
if attr_name in ['android:fillColor', 'android:strokeColor']:
for child_node in it.childNodes:
if child_node.tagName == 'gradient':
svg_gradient = parse_gradient(root, child_node)
if svg_gradient:
size = len(defs.childNodes)
gradient_id = f'gradient_{size}'
svg_gradient.setAttribute('id', gradient_id)
defs.appendChild(svg_gradient)
svg_attr_name = 'fill' if attr_name == 'android:fillColor' else 'stroke'
svg_path.setAttribute(svg_attr_name, f'url(#{gradient_id})')
else:
continue
return svg_path
if node.tagName == 'group':
group_node = root.createElement('g')
transform_attributes(node, group_node, group_transformers)
prev_clip_path_id = None
for it in node.childNodes:
child_path = transform_node(it, node, root, defs)
if child_path:
clip_path_node = None
if child_path.tagName == 'clipPath':
clip_path_node = child_path
if clip_path_node:
if defs is not None:
size = len(list(defs))
prev_clip_path_id = f'clip_path_{size}'
clip_path_node.setAttribute('id', prev_clip_path_id)
defs.append(minidom_to_elementtree(clip_path_node))
continue
if prev_clip_path_id:
child_path.setAttribute('clip-path', f'url(#{prev_clip_path_id})')
prev_clip_path_id = None
group_node.appendChild(child_path)
return group_node
if node.tagName == 'clip-path':
path_data = node.getAttribute('android:pathData')
svg_clip_path_node = root.createElement('clipPath')
path = root.createElement('path')
path.setAttribute('d', path_data)
svg_clip_path_node.appendChild(path)
return svg_clip_path_node
return None
def remove_dimen_suffix(dimen):
dimen = dimen.strip()
if not dimen:
return dimen
if dimen.isnumeric():
return dimen
if isinstance(dimen, str):
is_valid_float = False
while dimen and (not is_valid_float):
try:
float(dimen)
is_valid_float = True
except Exception as err:
#print(f'[WARNING] Invalid float value: {dimen}', file=sys.stderr)
dimen = dimen[:-1]
return dimen
def convert_hex_color(argb, opacity_str='1'):
if argb is None:
return [None, None,]
if opacity_str is None:
opacity_str = '1'
if type(opacity_str) not in (str,):
opacity_str = f'{int(opacity_str)}'
digits = argb and argb.lstrip('#')
opacity = float(opacity_str)
if not digits or (len(digits) not in [4, 8]):
return [argb, opacity]
if len(digits) == 4:
alpha = int(digits[0] * 2, 16) / 255
red = digits[1]
green = digits[2]
blue = digits[3]
else:
alpha = int(digits[:2], 16) / 255
red = digits[2:4]
green = digits[4:6]
blue = digits[6:8]
return [
'#' + red + green + blue,
(alpha if isinstance(alpha, float) and math.isfinite(alpha) else 1) * opacity,
]
def convert_vector_drawable_stroke_attributes(vdAttrs):
color = vdAttrs.get('android:strokeColor')
alpha = vdAttrs.get('android:strokeAlpha')
stroke_color = None
stroke_opacity = None
if (color is not None) and (alpha is not None):
stroke_color, stroke_opacity = convert_hex_color(color, alpha)
elif color is not None:
stroke_color, stroke_opacity = convert_hex_color(color)
return {
'stroke': stroke_color,
'stroke-opacity': stroke_opacity if ((stroke_opacity is not None) and stroke_opacity != 1) else None
}
def minidom_to_elementtree(minidom_element):
"""Converts a minidom Element to an ElementTree Element."""
xml_string = minidom_element.toxml()
return fromstring(xml_string)
def transform(content, options=None, encoding='utf8'):
if type(content) in (bytes, bytearray, memoryview,):
content = content.decode(encoding)
if options is None:
options = {}
override = options.get('override')
doc = minidom.parseString(content)
if override and isinstance(override, dict):
def traverse(node, callback):
callback(node)
for child in node.childNodes:
traverse(child, callback)
traverse(doc, lambda node: None if not node.attributes else [
node.setAttribute(attr.name, override[node.getAttribute(attr.name)])
for attr in node.attributes.values() if node.getAttribute(attr.name) in override
])
vector_drawables = doc.getElementsByTagName('vector')
if len(vector_drawables) != 1:
raise ValueError('VectorDrawable is invalid')
vector_drawable = vector_drawables[0]
viewport_width = vector_drawable.getAttribute('android:viewportWidth')
viewport_height = vector_drawable.getAttribute('android:viewportHeight')
output_width = remove_dimen_suffix(vector_drawable.getAttribute('android:width'))
output_height = remove_dimen_suffix(vector_drawable.getAttribute('android:height'))
svg_node = Element('svg')
svg_node.set('id', 'vector')
svg_node.set('xmlns', 'http://www.w3.org/2000/svg')
svg_node.set('width', output_width or viewport_width)
svg_node.set('height', output_height or viewport_height)
svg_node.set('viewBox', f'0 0 {viewport_width} {viewport_height}')
for child in doc.documentElement.childNodes:
if type(child) == minidom.Text:
assert not (child.wholeText.strip()), f'[ERROR] Found Unexpected textual data: "{child.wholeText}"'
children_nodes = [child for child in doc.documentElement.childNodes if type(child) != minidom.Text and child.tagName]
defs_node = Element('defs')
nodes = [transform_node(it, doc.documentElement, doc, defs_node) for it in children_nodes]
if list(defs_node):
svg_node.append(defs_node)
node_indices = {
'g': 0,
'path': 0,
}
for node in nodes:
id_ = node.getAttribute('id')
current_id = node_indices.get(node.tagName)
if isinstance(current_id, int):
node_indices[node.tagName] += 1
node.setAttribute('id', id_ or f'{node.tagName}_{current_id}')
svg_node.append(minidom_to_elementtree(node))
svg_string = tostring(svg_node).decode()
if options:
if options.get('pretty'):
import xml.dom.minidom
return xml.dom.minidom.parseString(svg_string).toprettyxml()
return svg_string
if __name__ == '__main__':
vd_data = None
if len(sys.argv) < 2:
print(f'Usage:\n\tpython3 {sys.argv[0]} <Android vector drawable XML file>', file=sys.stderr)
sys.exit(1)
with open(sys.argv[1], 'r') as f:
vd_data = f.read()
# Test data
#vd_data = '<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48.000000dip" android:width="48.000000dip" android:viewportWidth="48.000000" android:viewportHeight="48.000000">\n <group>\n <clip-path android:pathData="M0,0h48v48h-48z M 0,0"/>\n <path android:fillColor="#FFFFFFFF" android:pathData="M24.53,22.621m-17.396,0a17.396,17.396 0,1 1,34.792 0a17.396,17.396 0,1 1,-34.792 0"/>\n <group>\n <clip-path android:pathData="M7.593,33.276l10.709,6.975 15.558,-0.223L33.86,11.092L7.593,11.092Z M 0,0"/>\n <path android:fillColor="#FF0A0909" android:pathData="M28.906,15.808a30.48,30.48 0,0 1,-10.081 3.284c-5.721,0.783 -9.159,2.366 -9.085,8.811 0.025,2.2 1.159,5.092 0.872,7.23 -0.2,1.5 -2.146,3.24 -3.061,5.114 -1.735,3.554 -1.046,8.134 -0.672,11.97 3.335,-0.4 2.627,-0.456 3.024,-3.823a32.659,32.659 0,0 1,4.166 -12.265c1.185,5.421 0.542,10.621 -0.682,15.975 3.6,-1.58 4.256,-3.637 5.206,-7.631 1.213,-5.1 -2.339,-7.965 -2.774,-12.631 -0.18,-1.928 2.086,-7.2 3.276,-8.733 1.21,-1.557 2.061,-1.211 4.247,-1.709a15.516,15.516 0,0 0,7.439 -4.154,13.7 13.7,0 0,0 -1.877,-1.436"/>\n <path android:fillColor="#FF0A0909" android:pathData="M28.906,15.018s0.355,-0.739 0.618,-0.312 0.183,0.629 0.183,0.629a10.6,10.6 0,0 1,2.229 -0.454c0.624,0.033 0.424,0.493 0.424,0.493s0.533,-0.483 0.562,0.118c0.01,0.208 0.312,-0.394 0.542,-0.1s0.04,0.178 0.138,0.473 0.259,0.645 -1.056,0.317 -3.641,-1.167 -3.641,-1.167"/>\n <path android:fillColor="#FF979898" android:pathData="M12.711,23.804c-0.24,-0.12 -7.21,-0.9 -7.991,-1.142l-0.781,-0.24 -0.9,7.991 9.794,-0.12Z"/>\n <path android:fillColor="#FFB8B8B9" android:pathData="M13.386,25.177c-0.226,-0.145 -7.07,-1.675 -7.821,-2l-0.75,-0.323 -1.76,7.847 9.749,0.939Z"/>\n <path android:fillColor="#FFE1E1E2" android:pathData="M13.626,26.078c-0.226,-0.146 -7.07,-1.675 -7.821,-2l-0.75,-0.324 -1.76,7.847 9.749,0.939Z"/>\n <path android:fillColor="#FF0A0909" android:pathData="M13.695,52.559c0.3,0.422 1.384,2.51 2.686,3.312 1.121,0.69 2.269,0.791 1.631,-1.055a24.3,24.3 0,0 0,-1.569 -3.5Z"/>\n <path android:fillColor="#FF0A0909" android:pathData="M9.71,52.378c-0.665,0.1 -3.828,0.248 -5.339,1.053 -1.3,0.693 -1.82,1.623 0.868,1.761a48.514,48.514 0,0 0,5.216 -0.041Z"/>\n <path android:fillColor="#FF0A0909" android:pathData="M19.128,14.844c0,2.072 -1.031,3.752 -2.3,3.752s-2.3,-1.68 -2.3,-3.752 1.031,-3.752 2.3,-3.752 2.3,1.68 2.3,3.752"/>\n </group>\n <path android:fillColor="#FFFFFFFF" android:pathData="M22.149,25.813a0.2,0.2 0,0 1,-0.1 -0.2l1.576,-10.171a0.2,0.2 0,0 1,0.091 -0.138,0.2 0.2,0 0,1 0.163,-0.02l13.827,4.5a0.192,0.192 0,0 1,0.131 0.215L36.261,30.174a0.2,0.2 0,0 1,-0.091 0.138,0.194 0.194,0 0,1 -0.163,0.02l-13.827,-4.5a0.2,0.2 0,0 1,-0.035 -0.016"/>\n <path android:fillColor="#FF232322" android:pathData="M37.843,19.632a0.4,0.4 0,0 0,-0.071 -0.031l-13.827,-4.5a0.389,0.389 0,0 0,-0.326 0.04,0.4 0.4,0 0,0 -0.182,0.276L21.861,25.582a0.389,0.389 0,0 0,0.192 0.4,0.38 0.38,0 0,0 0.071,0.031l13.827,4.5a0.389,0.389 0,0 0,0.326 -0.04,0.4 0.4,0 0,0 0.182,-0.276l1.576,-10.172a0.389,0.389 0,0 0,-0.192 -0.4m-0.194,0.342L36.078,30.146l-13.827,-4.5 1.576,-10.171Z"/>\n <path android:fillColor="#FFFFFFFF" android:pathData="M22.149,25.813a0.2,0.2 0,0 1,-0.09 -0.227l3.159,-10.4a0.2,0.2 0,0 1,0.1 -0.118,0.2 0.2,0 0,1 0.151,-0.013l13.833,4.5a0.2,0.2 0,0 1,0.035 0.016,0.2 0.2,0 0,1 0.09,0.227l-3.159,10.4a0.2,0.2 0,0 1,-0.1 0.118,0.2 0.2,0 0,1 -0.151,0.013l-13.833,-4.5a0.2,0.2 0,0 1,-0.035 -0.016"/>\n <path android:fillColor="#FF232322" android:pathData="M39.432,19.403a0.41,0.41 0,0 0,-0.071 -0.031l-13.833,-4.5a0.389,0.389 0,0 0,-0.3 0.026,0.4 0.4,0 0,0 -0.194,0.235l-3.159,10.4a0.39,0.39 0,0 0,0.179 0.454,0.409 0.409,0 0,0 0.071,0.031l13.833,4.5a0.391,0.391 0,0 0,0.3 -0.026,0.4 0.4,0 0,0 0.194,-0.235l3.159,-10.4a0.391,0.391 0,0 0,-0.179 -0.454m-0.194,0.342L36.076,30.148l-13.833,-4.5 3.159,-10.4Z"/>\n <path android:fillColor="#FFFFFFFF" android:pathData="M22.15,25.812a0.193,0.193 0,0 1,-0.081 -0.09,0.2 0.2,0 0,1 0,-0.158l4.74,-10.917a0.2,0.2 0,0 1,0.24 -0.109l6.074,1.978a9.6,9.6 0,0 1,1.718 0.768c2.689,1.521 3.058,3.338 2.87,4.628a3.237,3.237 0,0 1,0.74 2.6,5.537 5.537,0 0,1 -0.355,1.473l-1.84,4.24a0.2,0.2 0,0 1,-0.24 0.11l-13.829,-4.5a0.209,0.209 0,0 1,-0.035 -0.016"/>\n <path android:fillColor="#FF232322" android:pathData="M34.937,17.112l-0.194,0.342 0.194,-0.342a9.888,9.888 0,0 0,-1.756 -0.784l-6.071,-1.977a0.393,0.393 0,0 0,-0.48 0.219l-4.74,10.917a0.394,0.394 0,0 0,0 0.317,0.385 0.385,0 0,0 0.163,0.18 0.376,0.376 0,0 0,0.07 0.031l13.829,4.5a0.392,0.392 0,0 0,0.48 -0.219l1.84,-4.238c0.047,-0.11 1.067,-2.578 -0.358,-4.215a3.917,3.917 0,0 0,-0.806 -2.915,6.944 6.944,0 0,0 -2.172,-1.818m-0.194,0.342c1.413,0.8 3.144,2.263 2.759,4.524 1.457,1.464 0.413,3.929 0.413,3.929l-1.84,4.238 -13.829,-4.5 4.74,-10.917 6.074,1.978a9.5,9.5 0,0 1,1.683 0.752"/>\n <path android:fillColor="#FF1A00C4" android:pathData="M37.957,22.871l-11.186,-3.623 0.991,-2.326 9.665,3.2Z"/>\n <path android:fillColor="#FF1A00C4" android:pathData="M28.805,23.856l-3.4,-1.178 1.036,-2.308 3.4,1.178Z"/>\n <path android:fillColor="#FF0A0909" android:pathData="M35.585,29.154a0.2,0.2 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,1 1,0.125 -0.376l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386ZM35.93,28.323a0.2,0.2 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,0 1,0.125 -0.376l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386ZM36.275,27.492a0.2,0.2 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,0 1,0.125 -0.376l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386ZM36.62,26.661a0.2,0.2 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,1 1,0.125 -0.376l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386ZM27.588,26.539a0.2,0.2 0,0 1,-0.064 -0.011l-3.307,-1.136a0.2,0.2 0,1 1,0.128 -0.374l3.307,1.136a0.2,0.2 0,0 1,-0.064 0.385ZM36.965,25.831a0.194,0.194 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,1 1,0.125 -0.375l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386ZM28.018,25.779a0.2,0.2 0,0 1,-0.064 -0.011l-3.307,-1.136a0.2,0.2 0,0 1,0.129 -0.374l3.307,1.136a0.2,0.2 0,0 1,-0.064 0.385ZM37.31,25.001a0.194,0.194 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,1 1,0.125 -0.376l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386ZM28.325,24.948a0.2,0.2 0,0 1,-0.063 -0.01l-3.184,-1.066a0.2,0.2 0,1 1,0.126 -0.375l3.184,1.066a0.2,0.2 0,0 1,-0.063 0.385ZM37.654,24.17a0.2,0.2 0,0 1,-0.062 -0.01l-6.84,-2.27a0.2,0.2 0,0 1,0.125 -0.375l6.84,2.27a0.2,0.2 0,0 1,-0.062 0.386Z"/>\n <path android:fillColor="#FFFFFFFF" android:pathData="M37.952,26.154a41.711,41.711 0,0 0,-3.553 -4.4,6.59 6.59,0 0,0 -0.969,-5.024 9.218,9.218 0,0 1,1.994 0.816,5.812 5.812,0 0,1 3.062,5.343 10.383,10.383 0,0 1,-0.533 3.264"/>\n <path android:fillColor="#FF232322" android:pathData="M37.993,26.613l-0.01,0.024c0.011,0 0.014,-0.007 0.01,-0.024m-2.472,-9.237a9.908,9.908 0,0 0,-2.646 -0.992s1.941,1.74 1.316,5.439a34.984,34.984 0,0 1,3.8 4.791c0.173,-0.427 2.486,-6.432 -2.472,-9.237m-0.193,0.342a5.629,5.629 0,0 1,2.963 5.175,10.025 10.025,0 0,1 -0.4,2.818c-0.29,-0.41 -0.665,-0.9 -1,-1.334 -0.413,-0.529 -1.429,-1.808 -2.277,-2.691a7.022,7.022 0,0 0,-0.722 -4.607,8.561 8.561,0 0,1 1.442,0.639"/>\n <path android:fillColor="#FF1A00C4" android:pathData="M24.242,46.537l10.725,-6.07 -21.686,-0.5Z"/>\n <path android:fillColor="#FF1A00C4" android:pathData="M24.58,43.307a20.643,20.643 0,0 1,-14.6 -35.241A20.643,20.643 0,1 1,39.178 37.26,20.509 20.509,0 0,1 24.58,43.307ZM24.58,5.97A16.694,16.694 0,1 0,41.274 22.663,16.713 16.713,0 0,0 24.58,5.97Z"/>\n </group>\n</vector>\n'
svg_data = transform(vd_data)
print(svg_data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment