Make WordPress Core


Ignore:
Timestamp:
09/10/2022 12:37:00 PM (4 years ago)
Author:
SergeyBiryukov
Message:

Editor: Backport Elements API updates.

This commit backports the original PRs from Gutenberg repository:

Props onemaggie, bernhard-reiter, cbravobernal, mmaattiiaass, scruffian, andraganescu, dpcalhoun, get_dave, Mamaduka, SergeyBiryukov.
See #56467.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-theme-json.php

    r54105 r54118  
    345345
    346346        /**
     347         * Defines which pseudo selectors are enabled for which elements.
     348         *
     349         * Note: this will affect both top-level and block-level elements.
     350         *
     351         * @since 6.1.0
     352         */
     353        const VALID_ELEMENT_PSEUDO_SELECTORS = array(
     354                'link'   => array( ':hover', ':focus', ':active', ':visited' ),
     355                'button' => array( ':hover', ':focus', ':active', ':visited' ),
     356        );
     357
     358        /**
    347359         * The valid elements that can be found under styles.
    348360         *
    349361         * @since 5.8.0
    350          * @since 6.1.0 Added `heading`, `button`, and `caption` to the elements.
     362         * @since 6.1.0 Added `heading`, `button`. and `caption` elements.
    351363         * @var string[]
    352364         */
    353365        const ELEMENTS = array(
    354                 'link'    => 'a',
     366                'link'    => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
    355367                'heading' => 'h1, h2, h3, h4, h5, h6',
    356368                'h1'      => 'h1',
    … …  
    366378        );
    367379
     380        const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
     381                'button'  => 'wp-element-button',
     382                'caption' => 'wp-element-caption',
     383        );
     384
     385        /**
     386         * Returns a class name by an element name.
     387         *
     388         * @since 6.1.0
     389         *
     390         * @param string $element The name of the element.
     391         * @return string The name of the class.
     392         */
     393        public static function get_element_class_name( $element ) {
     394                $class_name = '';
     395
     396                if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) {
     397                        $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
     398                }
     399
     400                return $class_name;
     401        }
     402
    368403        /**
    369404         * Options that settings.appearanceTools enables.
    … …  
    489524         */
    490525        protected static function sanitize( $input, $valid_block_names, $valid_element_names ) {
     526
    491527                $output = array();
    492528
    … …  
    495531                }
    496532
     533                // Preserve only the top most level keys.
    497534                $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
    498535
    499                 // Some styles are only meant to be available at the top-level (e.g.: blockGap),
    500                 // hence, the schema for blocks & elements should not have them.
     536                /*
     537                 * Remove any rules that are annotated as "top" in VALID_STYLES constant.
     538                 * Some styles are only meant to be available at the top-level (e.g.: blockGap),
     539                 * hence, the schema for blocks & elements should not have them.
     540                 */
    501541                $styles_non_top_level = static::VALID_STYLES;
    502542                foreach ( array_keys( $styles_non_top_level ) as $section ) {
    … …  
    511551                $schema                 = array();
    512552                $schema_styles_elements = array();
     553
     554                /*
     555                 * Set allowed element pseudo selectors based on per element allow list.
     556                 * Target data structure in schema:
     557                 * e.g.
     558                 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
     559                 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
     560                 */
    513561                foreach ( $valid_element_names as $element ) {
    514562                        $schema_styles_elements[ $element ] = $styles_non_top_level;
    515                 }
     563
     564                        if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     565                                foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
     566                                        $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
     567                                }
     568                        }
     569                }
     570
    516571                $schema_styles_blocks   = array();
    517572                $schema_settings_blocks = array();
    … …  
    521576                        $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
    522577                }
     578
    523579                $schema['styles']             = static::VALID_STYLES;
    524580                $schema['styles']['blocks']   = $schema_styles_blocks;
    … …  
    548604
    549605                return $output;
     606        }
     607
     608        /**
     609         * Appends a sub-selector to an existing one.
     610         *
     611         * Given the compounded $selector "h1, h2, h3"
     612         * and the $to_append selector ".some-class" the result will be
     613         * "h1.some-class, h2.some-class, h3.some-class".
     614         *
     615         * @since 5.8.0
     616         * @since 6.1.0 Added append position.
     617         *
     618         * @param string $selector  Original selector.
     619         * @param string $to_append Selector to append.
     620         * @param string $position  A position sub-selector should be appended. Default 'right'.
     621         * @return string
     622         */
     623        protected static function append_to_selector( $selector, $to_append, $position = 'right' ) {
     624                $new_selectors = array();
     625                $selectors     = explode( ',', $selector );
     626                foreach ( $selectors as $sel ) {
     627                        $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel;
     628                }
     629                return implode( ',', $new_selectors );
    550630        }
    551631
    … …  
    612692                                $element_selector = array();
    613693                                foreach ( $block_selectors as $selector ) {
    614                                         $element_selector[] = $selector . ' ' . $el_selector;
     694                                        if ( $selector === $el_selector ) {
     695                                                $element_selector = array( $el_selector );
     696                                                break;
     697                                        }
     698                                        $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' );
    615699                                }
    616700                                static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
    … …  
    811895                                continue;
    812896                        }
    813 
    814                         $node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    815                         $selector     = $metadata['selector'];
    816                         $settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
    817                         $declarations = static::compute_style_properties( $node, $settings );
    818 
    819                         // 1. Separate the ones who use the general selector
    820                         // and the ones who use the duotone selector.
    821                         $declarations_duotone = array();
    822                         foreach ( $declarations as $index => $declaration ) {
    823                                 if ( 'filter' === $declaration['name'] ) {
    824                                         unset( $declarations[ $index ] );
    825                                         $declarations_duotone[] = $declaration;
    826                                 }
    827                         }
    828 
    829                         /*
    830                          * Reset default browser margin on the root body element.
    831                          * This is set on the root selector **before** generating the ruleset
    832                          * from the `theme.json`. This is to ensure that if the `theme.json` declares
    833                          * `margin` in its `spacing` declaration for the `body` element then these
    834                          * user-generated values take precedence in the CSS cascade.
    835                          * @link https://github.com/WordPress/gutenberg/issues/36147.
    836                          */
    837                         if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
    838                                 $block_rules .= 'body { margin: 0; }';
    839                         }
    840 
    841                         // 2. Generate the rules that use the general selector.
    842                         $block_rules .= static::to_ruleset( $selector, $declarations );
    843 
    844                         // 3. Generate the rules that use the duotone selector.
    845                         if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
    846                                 $selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] );
    847                                 $block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
    848                         }
    849 
    850                         if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
    851                                 $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
    852                                 $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
    853                                 $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
    854 
    855                                 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
    856                                 if ( $has_block_gap_support ) {
    857                                         $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
    858                                         $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
    859                                 }
    860                         }
     897                        $block_rules .= static::get_styles_for_block( $metadata );
    861898                }
    862899
    … …  
    9711008
    9721009                return $selector . '{' . $declaration_block . '}';
    973         }
    974 
    975         /**
    976          * Function that appends a sub-selector to a existing one.
    977          *
    978          * Given the compounded $selector "h1, h2, h3"
    979          * and the $to_append selector ".some-class" the result will be
    980          * "h1.some-class, h2.some-class, h3.some-class".
    981          *
    982          * @since 5.8.0
    983          *
    984          * @param string $selector  Original selector.
    985          * @param string $to_append Selector to append.
    986          * @return string
    987          */
    988         protected static function append_to_selector( $selector, $to_append ) {
    989                 $new_selectors = array();
    990                 $selectors     = explode( ',', $selector );
    991                 foreach ( $selectors as $sel ) {
    992                         $new_selectors[] = $sel . $to_append;
    993                 }
    994 
    995                 return implode( ',', $new_selectors );
    9961010        }
    9971011
    … …  
    13131327         * @since 5.8.0
    13141328         * @since 5.9.0 Added the `$settings` and `$properties` parameters.
     1329         * @since 6.1.0 Added the `$theme_json` parameter.
    13151330         *
    13161331         * @param array $styles    Styles to process.
    13171332         * @param array $settings  Theme settings.
    13181333         * @param array $properties Properties metadata.
     1334         * @param array $theme_json Theme JSON array.
    13191335         * @return array Returns the modified $declarations.
    13201336         */
    1321         protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) {
     1337        protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null ) {
    13221338                if ( null === $properties ) {
    13231339                        $properties = static::PROPERTIES_METADATA;
    … …  
    13301346
    13311347                foreach ( $properties as $css_property => $value_path ) {
    1332                         $value = static::get_property_value( $styles, $value_path );
     1348                        $value = static::get_property_value( $styles, $value_path, $theme_json );
    13331349
    13341350                        // Look up protected properties, keyed by value path.
    … …  
    13661382         * "--wp--preset--color--secondary".
    13671383         *
     1384         * It also converts references to a path to the value
     1385         * stored at that location, e.g.
     1386         * { "ref": "style.color.background" } => "#fff".
     1387         *
    13681388         * @since 5.8.0
    13691389         * @since 5.9.0 Added support for values of array type, which are returned as is.
     1390         * @since 6.1.0 Added the `$theme_json` parameter.
    13701391         *
    13711392         * @param array $styles Styles subtree.
    13721393         * @param array $path   Which property to process.
     1394         * @param array $theme_json Theme JSON array.
    13731395         * @return string|array Style property value.
    13741396         */
    1375         protected static function get_property_value( $styles, $path ) {
     1397        protected static function get_property_value( $styles, $path, $theme_json = null ) {
    13761398                $value = _wp_array_get( $styles, $path, '' );
     1399
     1400                /*
     1401                 * This converts references to a path to the value at that path
     1402                 * where the values is an array with a "ref" key, pointing to a path.
     1403                 * For example: { "ref": "style.color.background" } => "#fff".
     1404                 */
     1405                if ( is_array( $value ) && array_key_exists( 'ref', $value ) ) {
     1406                        $value_path = explode( '.', $value['ref'] );
     1407                        $ref_value  = _wp_array_get( $theme_json, $value_path );
     1408                        // Only use the ref value if we find anything.
     1409                        if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
     1410                                $value = $ref_value;
     1411                        }
     1412
     1413                        if ( is_array( $ref_value ) && array_key_exists( 'ref', $ref_value ) ) {
     1414                                $path_string      = json_encode( $path );
     1415                                $ref_value_string = json_encode( $ref_value );
     1416                                _doing_it_wrong(
     1417                                        'get_property_value',
     1418                                        sprintf(
     1419                                                /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
     1420                                                __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ),
     1421                                                'theme.json',
     1422                                                $ref_value_string,
     1423                                                $path_string,
     1424                                                $ref_value['ref']
     1425                                        ),
     1426                                        '6.1.0'
     1427                                );
     1428                        }
     1429                }
    13771430
    13781431                if ( '' === $value || is_array( $value ) ) {
    … …  
    13801433                }
    13811434
     1435                // Convert custom CSS properties.
    13821436                $prefix     = 'var:';
    13831437                $prefix_len = strlen( $prefix );
    … …  
    14911545                                        'selector' => static::ELEMENTS[ $element ],
    14921546                                );
    1493                         }
     1547
     1548                                // Handle any pseudo selectors for the element.
     1549                                if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     1550                                        foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
     1551
     1552                                                if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
     1553                                                        $nodes[] = array(
     1554                                                                'path'     => array( 'styles', 'elements', $element ),
     1555                                                                'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
     1556                                                        );
     1557                                                }
     1558                                        }
     1559                                }
     1560                        }
     1561                }
     1562
     1563                // Blocks.
     1564                if ( ! isset( $theme_json['styles']['blocks'] ) ) {
     1565                        return $nodes;
     1566                }
     1567
     1568                $nodes = array_merge( $nodes, static::get_block_nodes( $theme_json ) );
     1569
     1570                /**
     1571                 * Filters the list of style nodes with metadata.
     1572                 *
     1573                 * This allows for things like loading block CSS independently.
     1574                 *
     1575                 * @since 6.1.0
     1576                 *
     1577                 * @param array $nodes Style nodes with metadata.
     1578                 */
     1579                return apply_filters( 'get_style_nodes', $nodes );
     1580        }
     1581
     1582        /**
     1583         * A public helper to get the block nodes from a theme.json file.
     1584         *
     1585         * @since 6.1.0
     1586         *
     1587         * @return array The block nodes in theme.json.
     1588         */
     1589        public function get_styles_block_nodes() {
     1590                return static::get_block_nodes( $this->theme_json );
     1591        }
     1592
     1593        /**
     1594         * An internal method to get the block nodes from a theme.json file.
     1595         *
     1596         * @since 6.1.0
     1597         *
     1598         * @param array $theme_json The theme.json converted to an array.
     1599         * @return array The block nodes in theme.json.
     1600         */
     1601        private static function get_block_nodes( $theme_json ) {
     1602                $selectors = static::get_blocks_metadata();
     1603                $nodes     = array();
     1604                if ( ! isset( $theme_json['styles'] ) ) {
     1605                        return $nodes;
    14941606                }
    14951607
    … …  
    15111623
    15121624                        $nodes[] = array(
     1625                                'name'     => $name,
    15131626                                'path'     => array( 'styles', 'blocks', $name ),
    15141627                                'selector' => $selector,
    … …  
    15221635                                                'selector' => $selectors[ $name ]['elements'][ $element ],
    15231636                                        );
     1637
     1638                                        // Handle any pseudo selectors for the element.
     1639                                        if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     1640                                                foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
     1641                                                        if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
     1642                                                                $nodes[] = array(
     1643                                                                        'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
     1644                                                                        'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
     1645                                                                );
     1646                                                        }
     1647                                                }
     1648                                        }
    15241649                                }
    15251650                        }
    … …  
    15271652
    15281653                return $nodes;
     1654        }
     1655
     1656        /**
     1657         * Gets the CSS rules for a particular block from theme.json.
     1658         *
     1659         * @since 6.1.0
     1660         *
     1661         * @param array $block_metadata Meta data about the block to get styles for.
     1662         * @return array Styles for the block.
     1663         */
     1664        public function get_styles_for_block( $block_metadata ) {
     1665
     1666                $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
     1667
     1668                $selector = $block_metadata['selector'];
     1669                $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
     1670
     1671                /*
     1672                 * Get a reference to element name from path.
     1673                 * $block_metadata['path'] = array( 'styles','elements','link' );
     1674                 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
     1675                 * Skip non-element paths like just ['styles'].
     1676                 */
     1677                $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
     1678
     1679                $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
     1680
     1681                $element_pseudo_allowed = array();
     1682
     1683                if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     1684                        $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
     1685                }
     1686
     1687                /*
     1688                 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
     1689                 * This also resets the array keys.
     1690                 */
     1691                $pseudo_matches = array_values(
     1692                        array_filter(
     1693                                $element_pseudo_allowed,
     1694                                function( $pseudo_selector ) use ( $selector ) {
     1695                                        return str_contains( $selector, $pseudo_selector );
     1696                                }
     1697                        )
     1698                );
     1699
     1700                $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
     1701
     1702                /*
     1703                 * If the current selector is a pseudo selector that's defined in the allow list for the current
     1704                 * element then compute the style properties for it.
     1705                 * Otherwise just compute the styles for the default selector as normal.
     1706                 */
     1707                if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
     1708                        array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS )
     1709                        && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
     1710                ) {
     1711                        $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json );
     1712                } else {
     1713                        $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json );
     1714                }
     1715
     1716                $block_rules = '';
     1717
     1718                /*
     1719                 * 1. Separate the declarations that use the general selector
     1720                 * from the ones using the duotone selector.
     1721                 */
     1722                $declarations_duotone = array();
     1723                foreach ( $declarations as $index => $declaration ) {
     1724                        if ( 'filter' === $declaration['name'] ) {
     1725                                unset( $declarations[ $index ] );
     1726                                $declarations_duotone[] = $declaration;
     1727                        }
     1728                }
     1729
     1730                /*
     1731                 * Reset default browser margin on the root body element.
     1732                 * This is set on the root selector **before** generating the ruleset
     1733                 * from the `theme.json`. This is to ensure that if the `theme.json` declares
     1734                 * `margin` in its `spacing` declaration for the `body` element then these
     1735                 * user-generated values take precedence in the CSS cascade.
     1736                 * @link https://github.com/WordPress/gutenberg/issues/36147.
     1737                 */
     1738                if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
     1739                        $block_rules .= 'body { margin: 0; }';
     1740                }
     1741
     1742                // 2. Generate and append the rules that use the general selector.
     1743                $block_rules .= static::to_ruleset( $selector, $declarations );
     1744
     1745                // 3. Generate and append the rules that use the duotone selector.
     1746                if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
     1747                        $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] );
     1748                        $block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
     1749                }
     1750
     1751                if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
     1752                        $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
     1753                        $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
     1754                        $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
     1755
     1756                        $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
     1757                        if ( $has_block_gap_support ) {
     1758                                $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
     1759                                $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
     1760                        }
     1761                }
     1762
     1763                return $block_rules;
    15291764        }
    15301765
    … …  
    18382073                $valid_block_names   = array_keys( static::get_blocks_metadata() );
    18392074                $valid_element_names = array_keys( static::ELEMENTS );
    1840                 $theme_json          = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
     2075
     2076                $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
    18412077
    18422078                $blocks_metadata = static::get_blocks_metadata();
    18432079                $style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata );
     2080
    18442081                foreach ( $style_nodes as $metadata ) {
    18452082                        $input = _wp_array_get( $theme_json, $metadata['path'], array() );
    … …  
    18492086
    18502087                        $output = static::remove_insecure_styles( $input );
     2088
     2089                        /*
     2090                         * Get a reference to element name from path.
     2091                         * $metadata['path'] = array( 'styles', 'elements', 'link' );
     2092                         */
     2093                        $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
     2094
     2095                        /*
     2096                         * $output is stripped of pseudo selectors. Re-add and process them
     2097                         * or insecure styles here.
     2098                         */
     2099                        if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     2100                                foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
     2101                                        if ( isset( $input[ $pseudo_selector ] ) ) {
     2102                                                $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
     2103                                        }
     2104                                }
     2105                        }
     2106
    18512107                        if ( ! empty( $output ) ) {
    18522108                                _wp_array_set( $sanitized, $metadata['path'], $output );
Note: See TracChangeset for help on using the changeset viewer.