Created
August 24, 2012 14:13
-
-
Save dkvdm/3451063 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Redirects search results from /?s=query to /search/query/, converts %20 to + | |
* | |
* @link http://txfx.net/wordpress-plugins/nice-search/ | |
*/ | |
function roots_nice_search_redirect() { | |
if (is_search() && strpos($_SERVER['REQUEST_URI'], '/wp-admin/') === false && strpos($_SERVER['REQUEST_URI'], '/search/') === false) { | |
wp_redirect(home_url('/search/' . str_replace(array(' ', '%20'), array('+', '+'), urlencode(get_query_var('s')))), 301); | |
exit(); | |
} | |
} | |
add_action('template_redirect', 'roots_nice_search_redirect'); | |
/** | |
* Fix for get_search_query() returning +'s between search terms | |
*/ | |
function roots_search_query($escaped = true) { | |
$query = apply_filters('roots_search_query', get_query_var('s')); | |
if ($escaped) { | |
$query = esc_attr($query); | |
} | |
return urldecode($query); | |
} | |
add_filter('get_search_query', 'roots_search_query'); | |
/** | |
* Fix for empty search queries redirecting to home page | |
* | |
* @link http://wordpress.org/support/topic/blank-search-sends-you-to-the-homepage#post-1772565 | |
* @link http://core.trac.wordpress.org/ticket/11330 | |
*/ | |
function roots_request_filter($query_vars) { | |
if (isset($_GET['s']) && empty($_GET['s'])) { | |
$query_vars['s'] = ' '; | |
} | |
return $query_vars; | |
} | |
add_filter('request', 'roots_request_filter'); | |
/** | |
* Root relative URLs | |
* | |
* WordPress likes to use absolute URLs on everything - let's clean that up. | |
* Inspired by http://www.456bereastreet.com/archive/201010/how_to_make_wordpress_urls_root_relative/ | |
* | |
* You can enable/disable this feature in config.php: | |
* current_theme_supports('root-relative-urls'); | |
* | |
* @author Scott Walkinshaw <scott.walkinshaw@gmail.com> | |
*/ | |
function roots_root_relative_url($input) { | |
$output = preg_replace_callback( | |
'!(https?://[^/|"]+)([^"]+)?!', | |
create_function( | |
'$matches', | |
// If full URL is home_url("/") and this isn't a subdir install, return a slash for relative root | |
'if (isset($matches[0]) && $matches[0] === home_url("/") && str_replace("http://", "", home_url("/", "http"))==$_SERVER["HTTP_HOST"]) { return "/";' . | |
// If domain is equal to home_url("/"), then make URL relative | |
'} elseif (isset($matches[0]) && strpos($matches[0], home_url("/")) !== false) { return $matches[2];' . | |
// If domain is not equal to home_url("/"), do not make external link relative | |
'} else { return $matches[0]; };' | |
), | |
$input | |
); | |
return $output; | |
} | |
/** | |
* Terrible workaround to remove the duplicate subfolder in the src of <script> and <link> tags | |
* Example: /subfolder/subfolder/css/style.css | |
*/ | |
function roots_fix_duplicate_subfolder_urls($input) { | |
$output = roots_root_relative_url($input); | |
preg_match_all('!([^/]+)/([^/]+)!', $output, $matches); | |
if (isset($matches[1]) && isset($matches[2])) { | |
if ($matches[1][0] === $matches[2][0]) { | |
$output = substr($output, strlen($matches[1][0]) + 1); | |
} | |
} | |
return $output; | |
} | |
function enable_root_relative_urls() { | |
return !(is_admin() && in_array($GLOBALS['pagenow'], array('wp-login.php', 'wp-register.php'))) && current_theme_supports('root-relative-urls'); | |
} | |
if (enable_root_relative_urls()) { | |
$root_rel_filters = array( | |
'bloginfo_url', | |
'theme_root_uri', | |
'stylesheet_directory_uri', | |
'template_directory_uri', | |
'plugins_url', | |
'the_permalink', | |
'wp_list_pages', | |
'wp_list_categories', | |
'wp_nav_menu', | |
'the_content_more_link', | |
'the_tags', | |
'get_pagenum_link', | |
'get_comment_link', | |
'month_link', | |
'day_link', | |
'year_link', | |
'tag_link', | |
'the_author_posts_link' | |
); | |
add_filters($root_rel_filters, 'roots_root_relative_url'); | |
add_filter('script_loader_src', 'roots_fix_duplicate_subfolder_urls'); | |
add_filter('style_loader_src', 'roots_fix_duplicate_subfolder_urls'); | |
} | |
/** | |
* Cleanup language_attributes() used in <html> tag | |
* | |
* Change lang="en-US" to lang="en" | |
* Remove dir="ltr" | |
*/ | |
function roots_language_attributes() { | |
$attributes = array(); | |
$output = ''; | |
if (function_exists('is_rtl')) { | |
if (is_rtl() == 'rtl') { | |
$attributes[] = 'dir="rtl"'; | |
} | |
} | |
$lang = get_bloginfo('language'); | |
if ($lang && $lang !== 'en-US') { | |
$attributes[] = "lang=\"$lang\""; | |
} else { | |
$attributes[] = 'lang="en"'; | |
} | |
$output = implode(' ', $attributes); | |
$output = apply_filters('roots_language_attributes', $output); | |
return $output; | |
} | |
add_filter('language_attributes', 'roots_language_attributes'); | |
/** | |
* Remove the WordPress version from RSS feeds | |
*/ | |
function roots_remove_generator() { return; } | |
add_filter('the_generator', 'roots_remove_generator'); | |
/** | |
* Cleanup wp_head() | |
* | |
* Remove unnecessary <link>'s | |
* Remove inline CSS used by Recent Comments widget | |
* Remove inline CSS used by posts with galleries | |
* Remove self-closing tag and change ''s to "'s on rel_canonical() | |
*/ | |
function roots_head_cleanup() { | |
// http://wpengineer.com/1438/wordpress-header/ | |
remove_action('wp_head', 'feed_links', 2); | |
remove_action('wp_head', 'feed_links_extra', 3); | |
remove_action('wp_head', 'rsd_link'); | |
remove_action('wp_head', 'wlwmanifest_link'); | |
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0); | |
remove_action('wp_head', 'wp_generator'); | |
remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0); | |
add_action('wp_head', 'roots_remove_recent_comments_style', 1); | |
add_filter('use_default_gallery_style', '__return_null'); | |
if (!class_exists('WPSEO_Frontend')) { | |
remove_action('wp_head', 'rel_canonical'); | |
add_action('wp_head', 'roots_rel_canonical'); | |
} | |
} | |
function roots_rel_canonical() { | |
global $wp_the_query; | |
if (!is_singular()) { | |
return; | |
} | |
if (!$id = $wp_the_query->get_queried_object_id()) { | |
return; | |
} | |
$link = get_permalink($id); | |
echo "\t<link rel=\"canonical\" href=\"$link\">\n"; | |
} | |
function roots_remove_recent_comments_style() { | |
global $wp_widget_factory; | |
if (isset($wp_widget_factory->widgets['WP_Widget_Recent_Comments'])) { | |
remove_action('wp_head', array($wp_widget_factory->widgets['WP_Widget_Recent_Comments'], 'recent_comments_style')); | |
} | |
} | |
add_action('init', 'roots_head_cleanup'); | |
/** | |
* Cleanup gallery_shortcode() | |
* | |
* Re-create the [gallery] shortcode and use thumbnails styling from Bootstrap | |
* | |
* @link http://twitter.github.com/bootstrap/components.html#thumbnails | |
*/ | |
function roots_gallery($attr) { | |
global $post, $wp_locale; | |
static $instance = 0; | |
$instance++; | |
$output = apply_filters('post_gallery', '', $attr); | |
if ($output != '') { | |
return $output; | |
} | |
if (isset($attr['orderby'])) { | |
$attr['orderby'] = sanitize_sql_orderby($attr['orderby']); | |
if (!$attr['orderby']) { | |
unset($attr['orderby']); | |
} | |
} | |
extract(shortcode_atts(array( | |
'order' => 'ASC', | |
'orderby' => 'menu_order ID', | |
'id' => $post->ID, | |
'icontag' => 'li', | |
'captiontag' => 'p', | |
'columns' => 3, | |
'size' => 'thumbnail', | |
'include' => '', | |
'exclude' => '' | |
), $attr)); | |
$id = intval($id); | |
if ($order === 'RAND') { | |
$orderby = 'none'; | |
} | |
if (!empty($include)) { | |
$include = preg_replace( '/[^0-9,]+/', '', $include ); | |
$_attachments = get_posts( array('include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby) ); | |
$attachments = array(); | |
foreach ($_attachments as $key => $val) { | |
$attachments[$val->ID] = $_attachments[$key]; | |
} | |
} elseif (!empty($exclude)) { | |
$exclude = preg_replace('/[^0-9,]+/', '', $exclude); | |
$attachments = get_children(array('post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby)); | |
} else { | |
$attachments = get_children(array('post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby)); | |
} | |
if (empty($attachments)) { | |
return ''; | |
} | |
if (is_feed()) { | |
$output = "\n"; | |
foreach ($attachments as $att_id => $attachment) | |
$output .= wp_get_attachment_link($att_id, $size, true) . "\n"; | |
return $output; | |
} | |
$captiontag = tag_escape($captiontag); | |
$columns = intval($columns); | |
$itemwidth = $columns > 0 ? floor(100/$columns) : 100; | |
$float = is_rtl() ? 'right' : 'left'; | |
$selector = "gallery-{$instance}"; | |
$gallery_style = $gallery_div = ''; | |
if (apply_filters('use_default_gallery_style', true)) { | |
$gallery_style = ''; | |
} | |
$size_class = sanitize_html_class($size); | |
$gallery_div = "<ul id='$selector' class='thumbnails gallery galleryid-{$id} gallery-columns-{$columns} gallery-size-{$size_class}'>"; | |
$output = apply_filters('gallery_style', $gallery_style . "\n\t\t" . $gallery_div); | |
$i = 0; | |
foreach ($attachments as $id => $attachment) { | |
$link = isset($attr['link']) && 'file' == $attr['link'] ? wp_get_attachment_link($id, $size, false, false) : wp_get_attachment_link($id, $size, true, false); | |
$output .= " | |
<{$icontag} class=\"gallery-item\"> | |
$link | |
"; | |
if ($captiontag && trim($attachment->post_excerpt)) { | |
$output .= " | |
<{$captiontag} class=\"gallery-caption hidden\"> | |
" . wptexturize($attachment->post_excerpt) . " | |
</{$captiontag}>"; | |
} | |
$output .= "</{$icontag}>"; | |
if ($columns > 0 && ++$i % $columns == 0) { | |
$output .= ''; | |
} | |
} | |
$output .= "</ul>\n"; | |
return $output; | |
} | |
remove_shortcode('gallery'); | |
add_shortcode('gallery', 'roots_gallery'); | |
/** | |
* Add class="thumbnail" to attachment items | |
*/ | |
function roots_attachment_link_class($html) { | |
$postid = get_the_ID(); | |
$html = str_replace('<a', '<a class="thumbnail"', $html); | |
return $html; | |
} | |
add_filter('wp_get_attachment_link', 'roots_attachment_link_class', 10, 1); | |
/** | |
* Add Bootstrap thumbnail styling to images with captions | |
* Use <figure> and <figcaption> | |
* | |
* @link http://justintadlock.com/archives/2011/07/01/captions-in-wordpress | |
*/ | |
function roots_caption($output, $attr, $content) { | |
if (is_feed()) { | |
return $output; | |
} | |
$defaults = array( | |
'id' => '', | |
'align' => 'alignnone', | |
'width' => '', | |
'caption' => '' | |
); | |
$attr = shortcode_atts($defaults, $attr); | |
// If the width is less than 1 or there is no caption, return the content wrapped between the [caption] tags | |
if (1 > $attr['width'] || empty($attr['caption'])) { | |
return $content; | |
} | |
// Set up the attributes for the caption <div> | |
$attributes = (!empty($attr['id']) ? ' id="' . esc_attr($attr['id']) . '"' : '' ); | |
$attributes .= ' class="thumbnail wp-caption ' . esc_attr($attr['align']) . '"'; | |
$attributes .= ' style="width: ' . esc_attr($attr['width']) . 'px"'; | |
$output = '<figure' . $attributes .'>'; | |
$output .= do_shortcode($content); | |
$output .= '<figcaption class="caption wp-caption-text">' . $attr['caption'] . '</figcaption>'; | |
$output .= '</figure>'; | |
return $output; | |
} | |
add_filter('img_caption_shortcode', 'roots_caption', 10, 3); | |
/** | |
* Remove unnecessary dashboard widgets | |
* | |
* @link http://www.deluxeblogtips.com/2011/01/remove-dashboard-widgets-in-wordpress.html | |
*/ | |
function roots_remove_dashboard_widgets() { | |
remove_meta_box('dashboard_incoming_links', 'dashboard', 'normal'); | |
remove_meta_box('dashboard_plugins', 'dashboard', 'normal'); | |
remove_meta_box('dashboard_primary', 'dashboard', 'normal'); | |
remove_meta_box('dashboard_secondary', 'dashboard', 'normal'); | |
} | |
add_action('admin_init', 'roots_remove_dashboard_widgets'); | |
/** | |
* Cleanup the_excerpt() | |
*/ | |
function roots_excerpt_length($length) { | |
return POST_EXCERPT_LENGTH; | |
} | |
function roots_excerpt_more($more) { | |
return ' … <a href="' . get_permalink() . '">' . __('Continued', 'roots') . '</a>'; | |
} | |
add_filter('excerpt_length', 'roots_excerpt_length'); | |
add_filter('excerpt_more', 'roots_excerpt_more'); | |
/** | |
* Replace various active menu class names with "active" | |
*/ | |
function roots_wp_nav_menu($text) { | |
$text = preg_replace('/(current(-menu-|[-_]page[-_])(item|parent|ancestor))/', 'active', $text); | |
$text = preg_replace('/( active){2,}/', ' active', $text); | |
return $text; | |
} | |
add_filter('wp_nav_menu', 'roots_wp_nav_menu'); | |
/** | |
* Cleaner walker for wp_nav_menu() | |
* | |
* Walker_Nav_Menu (WordPress default) example output: | |
* <li id="menu-item-8" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8"><a href="/">Home</a></li> | |
* <li id="menu-item-9" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-9"><a href="/sample-page/">Sample Page</a></l | |
* | |
* Roots_Nav_Walker example output: | |
* <li class="menu-home"><a href="/">Home</a></li> | |
* <li class="menu-sample-page"><a href="/sample-page/">Sample Page</a></li> | |
*/ | |
class Roots_Nav_Walker extends Walker_Nav_Menu { | |
function check_current($classes) { | |
return preg_match('/(current[-_])|active|dropdown/', $classes); | |
} | |
function start_lvl(&$output, $depth = 0, $args = array()) { | |
$output .= "\n<ul class=\"dropdown-menu\">\n"; | |
} | |
function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) { | |
global $wp_query; | |
$indent = ($depth) ? str_repeat("\t", $depth) : ''; | |
$slug = sanitize_title($item->title); | |
$id = 'menu-' . $slug; | |
$class_names = $value = ''; | |
$li_attributes = ''; | |
$classes = empty($item->classes) ? array() : (array) $item->classes; | |
$classes = array_filter($classes, array(&$this, 'check_current')); | |
if ($args->has_children) { | |
$classes[] = 'dropdown'; | |
$li_attributes .= ' data-dropdown="dropdown"'; | |
} | |
if ($custom_classes = get_post_meta($item->ID, '_menu_item_classes', true)) { | |
foreach ($custom_classes as $custom_class) { | |
$classes[] = $custom_class; | |
} | |
} | |
$class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args)); | |
$class_names = $class_names ? ' class="' . $id . ' ' . esc_attr($class_names) . '"' : ' class="' . $id . '"'; | |
$output .= $indent . '<li' . $class_names . '>'; | |
$attributes = ! empty($item->attr_title) ? ' title="' . esc_attr($item->attr_title) .'"' : ''; | |
$attributes .= ! empty($item->target) ? ' target="' . esc_attr($item->target ) .'"' : ''; | |
$attributes .= ! empty($item->xfn) ? ' rel="' . esc_attr($item->xfn ) .'"' : ''; | |
$attributes .= ! empty($item->url) ? ' href="' . esc_attr($item->url ) .'"' : ''; | |
$attributes .= ($args->has_children) ? ' class="dropdown-toggle" data-toggle="dropdown"' : ''; | |
$item_output = $args->before; | |
$item_output .= '<a'. $attributes .'>'; | |
$item_output .= $args->link_before . apply_filters('the_title', $item->title, $item->ID) . $args->link_after; | |
$item_output .= ($args->has_children) ? ' <b class="caret"></b>' : ''; | |
$item_output .= '</a>'; | |
$item_output .= $args->after; | |
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args); | |
} | |
function display_element($element, &$children_elements, $max_depth, $depth = 0, $args, &$output) { | |
if (!$element) { return; } | |
$id_field = $this->db_fields['id']; | |
if (is_array($args[0])) { | |
$args[0]['has_children'] = !empty($children_elements[$element->$id_field]); | |
} elseif (is_object($args[0])) { | |
$args[0]->has_children = !empty($children_elements[$element->$id_field]); | |
} | |
$cb_args = array_merge(array(&$output, $element, $depth), $args); | |
call_user_func_array(array(&$this, 'start_el'), $cb_args); | |
$id = $element->$id_field; | |
if (($max_depth == 0 || $max_depth > $depth+1) && isset($children_elements[$id])) { | |
foreach ($children_elements[$id] as $child) { | |
if (!isset($newlevel)) { | |
$newlevel = true; | |
$cb_args = array_merge(array(&$output, $depth), $args); | |
call_user_func_array(array(&$this, 'start_lvl'), $cb_args); | |
} | |
$this->display_element($child, $children_elements, $max_depth, $depth + 1, $args, $output); | |
} | |
unset($children_elements[$id]); | |
} | |
if (isset($newlevel) && $newlevel) { | |
$cb_args = array_merge(array(&$output, $depth), $args); | |
call_user_func_array(array(&$this, 'end_lvl'), $cb_args); | |
} | |
$cb_args = array_merge(array(&$output, $element, $depth), $args); | |
call_user_func_array(array(&$this, 'end_el'), $cb_args); | |
} | |
} | |
/** | |
* Cleanup wp_nav_menu_args | |
* | |
* Remove the container | |
* Use Roots_Nav_Walker() by default | |
*/ | |
function roots_nav_menu_args($args = '') { | |
$roots_nav_menu_args['container'] = false; | |
if (!$args['items_wrap']) { | |
$roots_nav_menu_args['items_wrap'] = '<ul class="%2$s">%3$s</ul>'; | |
} | |
if (current_theme_supports('bootstrap-top-navbar')) { | |
$roots_nav_menu_args['depth'] = 2; | |
} | |
if (!$args['walker']) { | |
$roots_nav_menu_args['walker'] = new Roots_Nav_Walker(); | |
} | |
return array_merge($args, $roots_nav_menu_args); | |
} | |
add_filter('wp_nav_menu_args', 'roots_nav_menu_args'); | |
/** | |
* Remove unnecessary self-closing tags | |
*/ | |
function roots_remove_self_closing_tags($input) { | |
return str_replace(' />', '>', $input); | |
} | |
add_filter('get_avatar', 'roots_remove_self_closing_tags'); // <img /> | |
add_filter('comment_id_fields', 'roots_remove_self_closing_tags'); // <input /> | |
add_filter('post_thumbnail_html', 'roots_remove_self_closing_tags'); // <img /> | |
/** | |
* Don't return the default description in the RSS feed if it hasn't been changed | |
*/ | |
function roots_remove_default_description($bloginfo) { | |
$default_tagline = 'Just another WordPress site'; | |
return ($bloginfo === $default_tagline) ? '' : $bloginfo; | |
} | |
add_filter('get_bloginfo_rss', 'roots_remove_default_description'); | |
/** | |
* Allow more tags in TinyMCE including <iframe> and <script> | |
*/ | |
function roots_change_mce_options($options) { | |
$ext = 'pre[id|name|class|style],iframe[align|longdesc|name|width|height|frameborder|scrolling|marginheight|marginwidth|src],script[charset|defer|language|src|type]'; | |
if (isset($initArray['extended_valid_elements'])) { | |
$options['extended_valid_elements'] .= ',' . $ext; | |
} else { | |
$options['extended_valid_elements'] = $ext; | |
} | |
return $options; | |
} | |
add_filter('tiny_mce_before_init', 'roots_change_mce_options'); | |
/** | |
* Cleanup output of stylesheet <link> tags | |
*/ | |
add_filter('style_loader_tag', 'roots_clean_style_tag'); | |
function roots_clean_style_tag($input) { | |
preg_match_all("!<link rel='stylesheet'\s?(id='[^']+')?\s+href='(.*)' type='text/css' media='(.*)' />!", $input, $matches); | |
// Only display media if it's print | |
$media = $matches[3][0] === 'print' ? ' media="print"' : ''; | |
return '<link rel="stylesheet" href="' . $matches[2][0] . '"' . $media . '>' . "\n"; | |
} | |
/** | |
* Apply filters to body_class() | |
*/ | |
function roots_body_class_filter($classes) { | |
// Add 'top-navbar' class if using Bootstrap's Navbar | |
if (current_theme_supports('bootstrap-top-navbar')) { | |
$classes[] = 'top-navbar'; | |
} | |
return $classes; | |
} | |
add_filter('body_class', 'roots_body_class_filter'); | |
/** | |
* Add additional classes onto widgets | |
* | |
* @link http://wordpress.org/support/topic/how-to-first-and-last-css-classes-for-sidebar-widgets | |
*/ | |
function roots_widget_first_last_classes($params) { | |
global $my_widget_num; | |
$this_id = $params[0]['id']; | |
$arr_registered_widgets = wp_get_sidebars_widgets(); | |
if (!$my_widget_num) { | |
$my_widget_num = array(); | |
} | |
if (!isset($arr_registered_widgets[$this_id]) || !is_array($arr_registered_widgets[$this_id])) { | |
return $params; | |
} | |
if (isset($my_widget_num[$this_id])) { | |
$my_widget_num[$this_id] ++; | |
} else { | |
$my_widget_num[$this_id] = 1; | |
} | |
$class = 'class="widget-' . $my_widget_num[$this_id] . ' '; | |
if ($my_widget_num[$this_id] == 1) { | |
$class .= 'widget-first '; | |
} elseif ($my_widget_num[$this_id] == count($arr_registered_widgets[$this_id])) { | |
$class .= 'widget-last '; | |
} | |
$params[0]['before_widget'] = preg_replace('/class=\"/', "$class", $params[0]['before_widget'], 1); | |
return $params; | |
} | |
add_filter('dynamic_sidebar_params', 'roots_widget_first_last_classes'); | |
/** | |
* Wrap embedded media as suggested by Readability | |
* | |
* @link https://gist.github.com/965956 | |
* @link http://www.readability.com/publishers/guidelines#publisher | |
*/ | |
function roots_embed_wrap($cache, $url, $attr = '', $post_ID = '') { | |
return '<div class="entry-content-asset">' . $cache . '</div>'; | |
} | |
add_filter('embed_oembed_html', 'roots_embed_wrap', 10, 4); | |
add_filter('embed_googlevideo', 'roots_embed_wrap', 10, 2); | |
/** | |
* Tell WordPress to use searchform.php from the templates/ directory | |
*/ | |
function roots_get_search_form() { | |
locate_template('/templates/searchform.php', true, true); | |
} | |
add_filter('get_search_form', 'roots_get_search_form'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment