Make WordPress Core

Changeset 41376


Ignore:
Timestamp:
09/13/2017 06:07:48 AM (9 years ago)
Author:
westonruter
Message:

Editor: Add CodeMirror-powered code editor with syntax highlighting, linting, and auto-completion.

  • Code editor is integrated into the Theme/Plugin Editor, Additional CSS in Customizer, and Custom HTML widget. Code editor is not yet integrated into the post editor, and it may not be until accessibility concerns are addressed.
  • The CodeMirror component in the Custom HTML widget is integrated in a similar way to TinyMCE being integrated into the Text widget, adopting the same approach for integrating dynamic JavaScript-initialized fields.
  • Linting is performed for JS, CSS, HTML, and JSON via JSHint, CSSLint, HTMLHint, and JSONLint respectively. Linting is not yet supported for PHP.
  • When user lacks unfiltered_html the capability, the Custom HTML widget will report any Kses-invalid elements and attributes as errors via a custom Kses rule for HTMLHint.
  • When linting errors are detected, the user will be prevented from saving the code until the errors are fixed, reducing instances of broken websites.
  • The placeholder value is removed from Custom CSS in favor of a fleshed-out section description which now auto-expands when the CSS field is empty. See #39892.
  • The CodeMirror library is included as wp.CodeMirror to prevent conflicts with any existing CodeMirror global.
  • An wp.codeEditor.initialize() API in JS is provided to convert a textarea into CodeMirror, with a wp_enqueue_code_editor() function in PHP to manage enqueueing the assets and settings needed to edit a given type of code.
  • A user preference is added to manage whether or not "syntax highlighting" is enabled. The feature is opt-out, being enabled by default.
  • Allowed file extensions in the theme and plugin editors have been updated to include formats which CodeMirror has modes for: conf, css, diff, patch, html, htm, http, js, json, jsx, less, md, php, phtml, php3, php4, php5, php7, phps, scss, sass, sh, bash, sql, svg, xml, yml, yaml, txt.

Props westonruter, georgestephanis, obenland, melchoyce, pixolin, mizejewski, michelleweber, afercia, grahamarmfield, samikeijonen, rianrietveld, iseulde.
See #38707.
Fixes #12423, #39892.

Location:
trunk
Files:
12 added
21 edited

Legend:

Unmodified
Added
Removed
  • trunk/Gruntfile.js

    r41375 r41376  
    361361                                        'wp-includes/js/tinymce/plugins/wp*/plugin.js',
    362362                                        // Third party scripts
     363                                        '!wp-includes/js/codemirror/*.js',
    363364                                        '!wp-admin/js/farbtastic.js',
    364365                                        '!wp-includes/js/backbone*.js',
  • trunk/src/wp-admin/css/common.css

    r41356 r41376  
    22012201}
    22022202
    2203 #template div {
     2203#template > div {
    22042204        margin-right: 190px;
    22052205}
     
    30163016        font-family: Consolas, Monaco, monospace;
    30173017        font-size: 13px;
    3018         width: 97%;
    30193018        background: #f9f9f9;
    30203019        -moz-tab-size: 4;
     
    30233022}
    30243023
     3024#template textarea,
     3025#template .CodeMirror {
     3026        width: 97%;
     3027        height: calc( 100vh - 220px );
     3028}
     3029
    30253030/* rtl:ignore */
    30263031#template textarea,
     
    30333038}
    30343039
     3040#file-editor-linting-error {
     3041        margin-top: 1em;
     3042        margin-bottom: 1em;
     3043}
     3044#file-editor-linting-error > .notice {
     3045        margin: 0;
     3046        display: inline-block;
     3047}
     3048#file-editor-linting-error > .notice > p {
     3049        width: auto;
     3050}
     3051#template .submit {
     3052        margin-top: 1em;
     3053        padding: 0;
     3054}
     3055
     3056#template .submit input[type=submit][disabled] {
     3057        cursor: not-allowed;
     3058}
    30353059#templateside {
    30363060        float: right;
     
    35863610        }
    35873611
    3588         #template div {
     3612        #template > div {
    35893613                float: none;
    35903614                margin: 0;
     
    35923616        }
    35933617
     3618        #template .CodeMirror,
    35943619        #template textarea {
    35953620                width: 100%;
  • trunk/src/wp-admin/css/customize-controls.css

    r41374 r41376  
    551551        margin-bottom: 0;
    552552}
     553.customize-section-description ul {
     554        margin-left: 1em;
     555}
     556.customize-section-description ul > li {
     557        list-style: disc;
     558}
     559.section-description-buttons {
     560        text-align: right;
     561}
     562
     563.section-description-buttons button.button-link {
     564        color: #0073aa;
     565        text-decoration: underline;
     566}
    553567
    554568.customize-control {
     
    11531167        width: calc( 100% + 24px );
    11541168        margin-bottom: -12px;
     1169}
     1170
     1171.customize-section-description-container + #customize-control-custom_css:last-child .CodeMirror {
     1172        height: calc( 100vh - 185px );
     1173}
     1174.CodeMirror-lint-tooltip,
     1175.CodeMirror-hints {
     1176        z-index: 500000 !important;
     1177}
     1178
     1179.customize-section-description-container + #customize-control-custom_css:last-child .customize-control-notifications-container {
     1180        margin-left: 12px;
     1181        margin-right: 12px;
    11551182}
    11561183
  • trunk/src/wp-admin/css/widgets.css

    r41352 r41376  
    646646}
    647647
     648.custom-html-widget-fields > p > .CodeMirror {
     649        border: 1px solid #e5e5e5;
     650}
     651.custom-html-widget-fields code {
     652        padding-top: 1px;
     653        padding-bottom: 1px;
     654}
     655ul.CodeMirror-hints {
     656        z-index: 101; /* Due to z-index 100 set on .widget.open */
     657}
     658.widget-control-actions .custom-html-widget-save-button.button.validation-blocked {
     659        cursor: not-allowed;
     660}
     661
    648662/* =Media Queries
    649663-------------------------------------------------------------- */
  • trunk/src/wp-admin/includes/user.php

    r40940 r41376  
    9292
    9393        if ( $update ) {
    94                 $user->rich_editing = isset( $_POST['rich_editing'] ) && 'false' == $_POST['rich_editing'] ? 'false' : 'true';
     94                $user->rich_editing = isset( $_POST['rich_editing'] ) && 'false' === $_POST['rich_editing'] ? 'false' : 'true';
     95                $user->syntax_highlighting = isset( $_POST['syntax_highlighting'] ) && 'false' === $_POST['syntax_highlighting'] ? 'false' : 'true';
    9596                $user->admin_color = isset( $_POST['admin_color'] ) ? sanitize_text_field( $_POST['admin_color'] ) : 'fresh';
    9697                $user->show_admin_bar_front = isset( $_POST['admin_bar_front'] ) ? 'true' : 'false';
  • trunk/src/wp-admin/js/customize-controls.js

    r41374 r41376  
    56325632                });
    56335633
    5634                 // Allow tabs to be entered in Custom CSS textarea.
    5635                 api.control( 'custom_css', function setupCustomCssControl( control ) {
    5636                         control.deferred.embedded.done( function allowTabs() {
    5637                                 var $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
    5638 
    5639                                 $textarea.on( 'blur', function onBlur() {
    5640                                         $textarea.data( 'next-tab-blurs', false );
    5641                                 } );
    5642 
    5643                                 $textarea.on( 'keydown', function onKeydown( event ) {
    5644                                         var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
    5645 
    5646                                         if ( escKeyCode === event.keyCode ) {
    5647                                                 if ( ! $textarea.data( 'next-tab-blurs' ) ) {
    5648                                                         $textarea.data( 'next-tab-blurs', true );
    5649                                                         event.stopPropagation(); // Prevent collapsing the section.
     5634                // Add code editor for Custom CSS.
     5635                (function() {
     5636                        var ready, sectionReady = $.Deferred(), controlReady = $.Deferred();
     5637
     5638                        api.section( 'custom_css', function( section ) {
     5639                                section.deferred.embedded.done( function() {
     5640                                        if ( section.expanded() ) {
     5641                                                sectionReady.resolve( section );
     5642                                        } else {
     5643                                                section.expanded.bind( function( isExpanded ) {
     5644                                                        if ( isExpanded ) {
     5645                                                                sectionReady.resolve( section );
     5646                                                        }
     5647                                                } );
     5648                                        }
     5649                                });
     5650                        });
     5651                        api.control( 'custom_css', function( control ) {
     5652                                control.deferred.embedded.done( function() {
     5653                                        controlReady.resolve( control );
     5654                                });
     5655                        });
     5656
     5657                        ready = $.when( sectionReady, controlReady );
     5658
     5659                        // Set up the section desription behaviors.
     5660                        ready.done( function setupSectionDescription( section, control ) {
     5661
     5662                                // Close the section description when clicking the close button.
     5663                                section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
     5664                                        section.container.find( '.section-meta .customize-section-description:first' )
     5665                                                .removeClass( 'open' )
     5666                                                .slideUp()
     5667                                                .attr( 'aria-expanded', 'false' );
     5668                                });
     5669
     5670                                // Reveal help text if setting is empty.
     5671                                if ( ! control.setting.get() ) {
     5672                                        section.container.find( '.section-meta .customize-section-description:first' )
     5673                                                .addClass( 'open' )
     5674                                                .show()
     5675                                                .attr( 'aria-expanded', 'true' );
     5676                                }
     5677                        });
     5678
     5679                        // Set up the code editor itself.
     5680                        if ( api.settings.customCss && api.settings.customCss.codeEditor ) {
     5681
     5682                                // Set up the syntax highlighting editor.
     5683                                ready.done( function setupSyntaxHighlightingEditor( section, control ) {
     5684                                        var $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
     5685
     5686                                        // Make sure editor gets focused when control is focused.
     5687                                        control.focus = (function( originalFocus ) { // eslint-disable-line max-nested-callbacks
     5688                                                return function( params ) { // eslint-disable-line max-nested-callbacks
     5689                                                        var extendedParams = _.extend( {}, params ), originalCompleteCallback;
     5690                                                        originalCompleteCallback = extendedParams.completeCallback;
     5691                                                        extendedParams.completeCallback = function() {
     5692                                                                if ( originalCompleteCallback ) {
     5693                                                                        originalCompleteCallback();
     5694                                                                }
     5695                                                                if ( control.editor ) {
     5696                                                                        control.editor.codemirror.focus();
     5697                                                                }
     5698                                                        };
     5699                                                        originalFocus.call( this, extendedParams );
     5700                                                };
     5701                                        })( control.focus );
     5702
     5703                                        settings = _.extend( {}, api.settings.customCss.codeEditor, {
     5704
     5705                                                /**
     5706                                                 * Handle tabbing to the field after the editor.
     5707                                                 *
     5708                                                 * @returns {void}
     5709                                                 */
     5710                                                onTabNext: function onTabNext() {
     5711                                                        var controls, controlIndex;
     5712                                                        controls = section.controls();
     5713                                                        controlIndex = controls.indexOf( control );
     5714                                                        if ( controls.length === controlIndex + 1 ) {
     5715                                                                $( '#customize-footer-actions .collapse-sidebar' ).focus();
     5716                                                        } else {
     5717                                                                controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
     5718                                                        }
     5719                                                },
     5720
     5721                                                /**
     5722                                                 * Handle tabbing to the field before the editor.
     5723                                                 *
     5724                                                 * @returns {void}
     5725                                                 */
     5726                                                onTabPrevious: function onTabPrevious() {
     5727                                                        var controls, controlIndex;
     5728                                                        controls = section.controls();
     5729                                                        controlIndex = controls.indexOf( control );
     5730                                                        if ( 0 === controlIndex ) {
     5731                                                                section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
     5732                                                        } else {
     5733                                                                controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
     5734                                                        }
     5735                                                },
     5736
     5737                                                /**
     5738                                                 * Update error notice.
     5739                                                 *
     5740                                                 * @param {Array} errorAnnotations - Error annotations.
     5741                                                 * @returns {void}
     5742                                                 */
     5743                                                onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
     5744                                                        var message;
     5745                                                        control.setting.notifications.remove( 'csslint_error' );
     5746
     5747                                                        if ( 0 !== errorAnnotations.length ) {
     5748                                                                if ( 1 === errorAnnotations.length ) {
     5749                                                                        message = api.l10n.customCssError.singular.replace( '%d', '1' );
     5750                                                                } else {
     5751                                                                        message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
     5752                                                                }
     5753                                                                control.setting.notifications.add( 'csslint_error', new api.Notification( 'csslint_error', {
     5754                                                                        message: message,
     5755                                                                        type: 'error'
     5756                                                                } ) );
     5757                                                        }
    56505758                                                }
    5651                                                 return;
    5652                                         }
    5653 
    5654                                         // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
    5655                                         if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
    5656                                                 return;
    5657                                         }
    5658 
    5659                                         // Prevent capturing Tab characters if Esc was pressed.
    5660                                         if ( $textarea.data( 'next-tab-blurs' ) ) {
    5661                                                 return;
    5662                                         }
    5663 
    5664                                         selectionStart = textarea.selectionStart;
    5665                                         selectionEnd = textarea.selectionEnd;
    5666                                         value = textarea.value;
    5667 
    5668                                         if ( selectionStart >= 0 ) {
    5669                                                 textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
    5670                                                 $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
    5671                                         }
    5672 
    5673                                         event.stopPropagation();
    5674                                         event.preventDefault();
    5675                                 } );
    5676                         } );
    5677                 } );
     5759                                        });
     5760
     5761                                        control.editor = wp.codeEditor.initialize( $textarea, settings );
     5762
     5763                                        // Refresh when receiving focus.
     5764                                        control.editor.codemirror.on( 'focus', function( codemirror ) {
     5765                                                codemirror.refresh();
     5766                                        });
     5767
     5768                                        /*
     5769                                         * When the CodeMirror instance changes, mirror to the textarea,
     5770                                         * where we have our "true" change event handler bound.
     5771                                         */
     5772                                        control.editor.codemirror.on( 'change', function( codemirror ) {
     5773                                                suspendEditorUpdate = true;
     5774                                                $textarea.val( codemirror.getValue() ).trigger( 'change' );
     5775                                                suspendEditorUpdate = false;
     5776                                        });
     5777
     5778                                        // Update CodeMirror when the setting is changed by another plugin.
     5779                                        control.setting.bind( function( value ) {
     5780                                                if ( ! suspendEditorUpdate ) {
     5781                                                        control.editor.codemirror.setValue( value );
     5782                                                }
     5783                                        });
     5784
     5785                                        // Prevent collapsing section when hitting Esc to tab out of editor.
     5786                                        control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
     5787                                                var escKeyCode = 27;
     5788                                                if ( escKeyCode === event.keyCode ) {
     5789                                                        event.stopPropagation();
     5790                                                }
     5791                                        });
     5792                                });
     5793                        } else {
     5794
     5795                                // Allow tabs to be entered in Custom CSS textarea.
     5796                                ready.done( function allowTabs( section, control ) {
     5797
     5798                                        var $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
     5799
     5800                                        $textarea.on( 'blur', function onBlur() {
     5801                                                $textarea.data( 'next-tab-blurs', false );
     5802                                        } );
     5803
     5804                                        $textarea.on( 'keydown', function onKeydown( event ) {
     5805                                                var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
     5806
     5807                                                if ( escKeyCode === event.keyCode ) {
     5808                                                        if ( ! $textarea.data( 'next-tab-blurs' ) ) {
     5809                                                                $textarea.data( 'next-tab-blurs', true );
     5810                                                                event.stopPropagation(); // Prevent collapsing the section.
     5811                                                        }
     5812                                                        return;
     5813                                                }
     5814
     5815                                                // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
     5816                                                if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
     5817                                                        return;
     5818                                                }
     5819
     5820                                                // Prevent capturing Tab characters if Esc was pressed.
     5821                                                if ( $textarea.data( 'next-tab-blurs' ) ) {
     5822                                                        return;
     5823                                                }
     5824
     5825                                                selectionStart = textarea.selectionStart;
     5826                                                selectionEnd = textarea.selectionEnd;
     5827                                                value = textarea.value;
     5828
     5829                                                if ( selectionStart >= 0 ) {
     5830                                                        textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
     5831                                                        $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
     5832                                                }
     5833
     5834                                                event.stopPropagation();
     5835                                                event.preventDefault();
     5836                                        });
     5837                                });
     5838                        }
     5839                })();
    56785840
    56795841                // Toggle visibility of Header Video notice when active state change.
  • trunk/src/wp-admin/plugin-editor.php

    r38745 r41376  
    116116
    117117        // List of allowable extensions
    118         $editable_extensions = array('php', 'txt', 'text', 'js', 'css', 'html', 'htm', 'xml', 'inc', 'include');
     118        $editable_extensions = array(
     119                'bash',
     120                'conf',
     121                'css',
     122                'diff',
     123                'htm',
     124                'html',
     125                'http',
     126                'inc',
     127                'include',
     128                'js',
     129                'json',
     130                'jsx',
     131                'less',
     132                'md',
     133                'patch',
     134                'php',
     135                'php3',
     136                'php4',
     137                'php5',
     138                'php7',
     139                'phps',
     140                'phtml',
     141                'sass',
     142                'scss',
     143                'sh',
     144                'sql',
     145                'svg',
     146                'text',
     147                'txt',
     148                'xml',
     149                'yaml',
     150                'yml',
     151        );
    119152
    120153        /**
     
    157190                '<p>' . __('<a href="https://wordpress.org/support/">Support Forums</a>') . '</p>'
    158191        );
     192
     193        $settings = wp_enqueue_code_editor( array( 'file' => $real_file ) );
     194        if ( ! empty( $settings ) ) {
     195                wp_enqueue_script( 'wp-theme-plugin-editor' );
     196                wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
     197        }
    159198
    160199        require_once(ABSPATH . 'wp-admin/admin-header.php');
  • trunk/src/wp-admin/theme-editor.php

    r38722 r41376  
    6464$allowed_files = $style_files = array();
    6565$has_templates = false;
    66 $default_types = array( 'php', 'css' );
     66$default_types = array(
     67        'bash',
     68        'conf',
     69        'css',
     70        'diff',
     71        'htm',
     72        'html',
     73        'http',
     74        'inc',
     75        'include',
     76        'js',
     77        'json',
     78        'jsx',
     79        'less',
     80        'md',
     81        'patch',
     82        'php',
     83        'php3',
     84        'php4',
     85        'php5',
     86        'php7',
     87        'phps',
     88        'phtml',
     89        'sass',
     90        'scss',
     91        'sh',
     92        'sql',
     93        'svg',
     94        'text',
     95        'txt',
     96        'xml',
     97        'yaml',
     98        'yml',
     99);
    67100
    68101/**
     
    126159
    127160default:
     161
     162        $settings = wp_enqueue_code_editor( compact( 'file' ) );
     163        if ( ! empty( $settings ) ) {
     164                wp_enqueue_script( 'wp-theme-plugin-editor' );
     165                wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
     166        }
    128167
    129168        require_once( ABSPATH . 'wp-admin/admin-header.php' );
  • trunk/src/wp-admin/user-edit.php

    r41163 r41376  
    246246                <th scope="row"><?php _e( 'Visual Editor' ); ?></th>
    247247                <td><label for="rich_editing"><input name="rich_editing" type="checkbox" id="rich_editing" value="false" <?php if ( ! empty( $profileuser->rich_editing ) ) checked( 'false', $profileuser->rich_editing ); ?> /> <?php _e( 'Disable the visual editor when writing' ); ?></label></td>
     248        </tr>
     249<?php endif; ?>
     250<?php
     251$show_syntax_highlighting_preference = (
     252        // For Custom HTML widget and Additional CSS in Customizer.
     253        user_can( $profileuser, 'edit_theme_options' )
     254        ||
     255        // Edit plugins.
     256        user_can( $profileuser, 'edit_plugins' )
     257        ||
     258        // Edit themes.
     259        user_can( $profileuser, 'edit_themes' )
     260);
     261?>
     262<?php if ( $show_syntax_highlighting_preference ) : ?>
     263        <tr class="user-syntax-highlighting-wrap">
     264                <th scope="row"><?php _e( 'Syntax Highlighting' ); ?></th>
     265                <td>
     266                        <label for="syntax_highlighting"><input name="syntax_highlighting" type="checkbox" id="syntax_highlighting" value="false" <?php if ( ! empty( $profileuser->syntax_highlighting ) ) checked( 'false', $profileuser->syntax_highlighting ); ?> /> <?php _e( 'Disable syntax highlighting when editing code' ); ?></label>
     267                </td>
    248268        </tr>
    249269<?php endif; ?>
  • trunk/src/wp-includes/author-template.php

    r40952 r41376  
    129129 * - plugins_per_page
    130130 * - rich_editing
     131 * - syntax_highlighting
    131132 * - user_activation_key
    132133 * - user_description
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41374 r41376  
    212212         */
    213213        private $_changeset_data;
     214
     215        /**
     216         * Code Editor Settings for Custom CSS.
     217         *
     218         * This variable contains the settings returned by `wp_enqueue_code_editor()` which are then later output
     219         * to the client in `WP_Customize_Manager::customize_pane_settings()`. A value of false means that the
     220         * Custom CSS section or control was removed, or that the Syntax Highlighting user pref was turned off.
     221         *
     222         * @see wp_enqueue_code_editor()
     223         * @see WP_Customize_Manager::enqueue_control_scripts()
     224         * @see WP_Customize_Manager::customize_pane_settings()
     225         * @since 4.9.0
     226         * @var array|false
     227         */
     228        private $_custom_css_code_editor_settings = false;
    214229
    215230        /**
     
    33233338                        $control->enqueue();
    33243339                }
     3340
     3341                if ( $this->get_section( 'custom_css' ) && $this->get_control( 'custom_css' ) ) {
     3342                        $this->_custom_css_code_editor_settings = wp_enqueue_code_editor( array(
     3343                                'type' => 'text/css',
     3344                        ) );
     3345                }
    33253346        }
    33263347
     
    35793600                                'stylesheet' => $this->get_stylesheet(),
    35803601                                'active'     => $this->is_theme_active(),
     3602                        ),
     3603                        'customCss' => array(
     3604                                'codeEditor' => $this->_custom_css_code_editor_settings,
    35813605                        ),
    35823606                        'url'      => array(
     
    41784202
    41794203                /* Custom CSS */
     4204                $section_description = '<p>';
     4205                $section_description .= __( 'Add your own CSS code here to customize the appearance and layout of your site.', 'better-code-editing' );
     4206                $section_description .= sprintf(
     4207                        ' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text">%3$s</span></a>',
     4208                        esc_url( __( 'https://codex.wordpress.org/CSS', 'default' ) ),
     4209                        __( 'Learn more about CSS', 'default' ),
     4210                        /* translators: accessibility text */
     4211                        __( '(opens in a new window)', 'default' )
     4212                );
     4213                $section_description .= '</p>';
     4214
     4215                $section_description .= '<p>' . __( 'When using a keyboard to navigate:', 'better-code-editing' ) . '</p>';
     4216                $section_description .= '<ul>';
     4217                $section_description .= '<li>' . __( 'In the CSS edit field, Tab enters a tab character.', 'better-code-editing' ) . '</li>';
     4218                $section_description .= '<li>' . __( 'To move keyboard focus, press Esc then Tab for the next element, or Esc then Shift+Tab for the previous element.', 'better-code-editing' ) . '</li>';
     4219                $section_description .= '</ul>';
     4220
     4221                if ( 'false' !== wp_get_current_user()->syntax_highlighting ) {
     4222                        $section_description .= '<p>';
     4223                        $section_description .= sprintf(
     4224                                /* translators: placeholder is link to user profile */
     4225                                __( 'The edit field automatically highlights code syntax. You can disable this in your %s to work in plain text mode.', 'better-code-editing' ),
     4226                                sprintf(
     4227                                        ' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text">%3$s</span></a>',
     4228                                        esc_url( get_edit_profile_url() . '#syntax_highlighting' ),
     4229                                        __( 'user profile', 'better-code-editing' ),
     4230                                        /* translators: accessibility text */
     4231                                        __( '(opens in a new window)', 'default' )
     4232                                )
     4233                        );
     4234                        $section_description .= '</p>';
     4235                }
     4236
     4237                $section_description .= '<p class="section-description-buttons">';
     4238                $section_description .= '<button type="button" class="button-link section-description-close">' . __( 'Close', 'default' ) . '</button>';
     4239                $section_description .= '</p>';
     4240
    41804241                $this->add_section( 'custom_css', array(
    41814242                        'title'              => __( 'Additional CSS' ),
    41824243                        'priority'           => 200,
    41834244                        'description_hidden' => true,
    4184                         'description'        => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
    4185                                 __( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
    4186                                 esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
    4187                                 __( 'Learn more about CSS' ),
    4188                                 /* translators: accessibility text */
    4189                                 __( '(opens in a new window)' )
    4190                         ),
     4245                        'description'        => $section_description,
    41914246                ) );
    41924247
    41934248                $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
    41944249                        'capability' => 'edit_css',
    4195                         'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
     4250                        'default' => '',
    41964251                ) );
    41974252                $this->add_setting( $custom_css_setting );
  • trunk/src/wp-includes/class-wp-user.php

    r41366 r41376  
    3333 * @property string $deleted
    3434 * @property string $locale
     35 * @property string $rich_editing
     36 * @property string $syntax_highlighting
    3537 */
    3638class WP_User {
  • trunk/src/wp-includes/customize/class-wp-customize-custom-css-setting.php

    r41162 r41376  
    149149         * Notifications are rendered when the customizer state is saved.
    150150         *
    151          * @todo There are cases where valid CSS can be incorrectly marked as invalid when strings or comments include balancing characters. To fix, CSS tokenization needs to be used.
    152          *
    153151         * @since 4.7.0
     152         * @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor.
    154153         *
    155154         * @param string $css The input string.
     
    161160                if ( preg_match( '#</?\w+#', $css ) ) {
    162161                        $validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) );
    163                 }
    164 
    165                 $imbalanced = false;
    166 
    167                 // Make sure that there is a closing brace for each opening brace.
    168                 if ( ! $this->validate_balanced_characters( '{', '}', $css ) ) {
    169                         $validity->add( 'imbalanced_curly_brackets', sprintf(
    170                                 /* translators: 1: {}, 2: }, 3: { */
    171                                 __( 'Your curly brackets %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
    172                                 '<code>{}</code>',
    173                                 '<code>}</code>',
    174                                 '<code>{</code>'
    175                         ) );
    176                         $imbalanced = true;
    177                 }
    178 
    179                 // Ensure brackets are balanced.
    180                 if ( ! $this->validate_balanced_characters( '[', ']', $css ) ) {
    181                         $validity->add( 'imbalanced_braces', sprintf(
    182                                 /* translators: 1: [], 2: ], 3: [ */
    183                                 __( 'Your brackets %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
    184                                 '<code>[]</code>',
    185                                 '<code>]</code>',
    186                                 '<code>[</code>'
    187                         ) );
    188                         $imbalanced = true;
    189                 }
    190 
    191                 // Ensure parentheses are balanced.
    192                 if ( ! $this->validate_balanced_characters( '(', ')', $css ) ) {
    193                         $validity->add( 'imbalanced_parentheses', sprintf(
    194                                 /* translators: 1: (), 2: ), 3: ( */
    195                                 __( 'Your parentheses %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
    196                                 '<code>()</code>',
    197                                 '<code>)</code>',
    198                                 '<code>(</code>'
    199                         ) );
    200                         $imbalanced = true;
    201                 }
    202 
    203                 // Ensure double quotes are equal.
    204                 if ( ! $this->validate_equal_characters( '"', $css ) ) {
    205                         $validity->add( 'unequal_double_quotes', sprintf(
    206                                 /* translators: 1: " (double quote) */
    207                                 __( 'Your double quotes %1$s are uneven. Make sure there is a closing %1$s for every opening %1$s.' ),
    208                                 '<code>"</code>'
    209                         ) );
    210                         $imbalanced = true;
    211                 }
    212 
    213                 /*
    214                  * Make sure any code comments are closed properly.
    215                  *
    216                  * The first check could miss stray an unpaired comment closing figure, so if
    217                  * The number appears to be balanced, then check for equal numbers
    218                  * of opening/closing comment figures.
    219                  *
    220                  * Although it may initially appear redundant, we use the first method
    221                  * to give more specific feedback to the user.
    222                  */
    223                 $unclosed_comment_count = $this->validate_count_unclosed_comments( $css );
    224                 if ( 0 < $unclosed_comment_count ) {
    225                         $validity->add( 'unclosed_comment', sprintf(
    226                                 /* translators: 1: number of unclosed comments, 2: *​/ */
    227                                 _n(
    228                                         'There is %1$s unclosed code comment. Close each comment with %2$s.',
    229                                         'There are %1$s unclosed code comments. Close each comment with %2$s.',
    230                                         $unclosed_comment_count
    231                                 ),
    232                                 $unclosed_comment_count,
    233                                 '<code>*/</code>'
    234                         ) );
    235                         $imbalanced = true;
    236                 } elseif ( ! $this->validate_balanced_characters( '/*', '*/', $css ) ) {
    237                         $validity->add( 'imbalanced_comments', sprintf(
    238                                 /* translators: 1: *​/, 2: /​* */
    239                                 __( 'There is an extra %1$s, indicating an end to a comment. Be sure that there is an opening %2$s for every closing %1$s.' ),
    240                                 '<code>*/</code>',
    241                                 '<code>/*</code>'
    242                         ) );
    243                         $imbalanced = true;
    244                 }
    245                 if ( $imbalanced && $this->is_possible_content_error( $css ) ) {
    246                         $validity->add( 'possible_false_positive', sprintf(
    247                                 /* translators: %s: content: ""; */
    248                                 __( 'Imbalanced/unclosed character errors can be caused by %s declarations. You may need to remove this or add it to a custom CSS file.' ),
    249                                 '<code>content: "";</code>'
    250                         ) );
    251162                }
    252163
     
    286197                return $post_id;
    287198        }
    288 
    289         /**
    290          * Ensure there are a balanced number of paired characters.
    291          *
    292          * This is used to check that the number of opening and closing
    293          * characters is equal.
    294          *
    295          * For instance, there should be an equal number of braces ("{", "}")
    296          * in the CSS.
    297          *
    298          * @since 4.7.0
    299          *
    300          * @param string $opening_char The opening character.
    301          * @param string $closing_char The closing character.
    302          * @param string $css The CSS input string.
    303          *
    304          * @return bool
    305          */
    306         private function validate_balanced_characters( $opening_char, $closing_char, $css ) {
    307                 return substr_count( $css, $opening_char ) === substr_count( $css, $closing_char );
    308         }
    309 
    310         /**
    311          * Ensure there are an even number of paired characters.
    312          *
    313          * This is used to check that the number of a specific
    314          * character is even.
    315          *
    316          * For instance, there should be an even number of double quotes
    317          * in the CSS.
    318          *
    319          * @since 4.7.0
    320          *
    321          * @param string $char A character.
    322          * @param string $css The CSS input string.
    323          * @return bool Equality.
    324          */
    325         private function validate_equal_characters( $char, $css ) {
    326                 $char_count = substr_count( $css, $char );
    327                 return ( 0 === $char_count % 2 );
    328         }
    329 
    330         /**
    331          * Count unclosed CSS Comments.
    332          *
    333          * Used during validation.
    334          *
    335          * @see self::validate()
    336          *
    337          * @since 4.7.0
    338          *
    339          * @param string $css The CSS input string.
    340          * @return int Count.
    341          */
    342         private function validate_count_unclosed_comments( $css ) {
    343                 $count = 0;
    344                 $comments = explode( '/*', $css );
    345 
    346                 if ( ! is_array( $comments ) || ( 1 >= count( $comments ) ) ) {
    347                         return $count;
    348                 }
    349 
    350                 unset( $comments[0] ); // The first item is before the first comment.
    351                 foreach ( $comments as $comment ) {
    352                         if ( false === strpos( $comment, '*/' ) ) {
    353                                 $count++;
    354                         }
    355                 }
    356                 return $count;
    357         }
    358 
    359         /**
    360          * Find "content:" within a string.
    361          *
    362          * Imbalanced/Unclosed validation errors may be caused
    363          * when a character is used in a "content:" declaration.
    364          *
    365          * This function is used to detect if this is a possible
    366          * cause of the validation error, so that if it is,
    367          * a notification may be added to the Validation Errors.
    368          *
    369          * Example:
    370          * .element::before {
    371          *   content: "(\"";
    372          * }
    373          * .element::after {
    374          *   content: "\")";
    375          * }
    376          *
    377          * Using ! empty() because strpos() may return non-boolean values
    378          * that evaluate to false. This would be problematic when
    379          * using a strict "false === strpos()" comparison.
    380          *
    381          * @since 4.7.0
    382          *
    383          * @param string $css The CSS input string.
    384          * @return bool
    385          */
    386         private function is_possible_content_error( $css ) {
    387                 $found = preg_match( '/\bcontent\s*:/', $css );
    388                 if ( ! empty( $found ) ) {
    389                         return true;
    390                 }
    391                 return false;
    392         }
    393199}
  • trunk/src/wp-includes/general-template.php

    r41371 r41376  
    31173117
    31183118/**
     3119 * Enqueue assets needed by the code editor for the given settings.
     3120 *
     3121 * @since 4.9.0
     3122 *
     3123 * @see wp_enqueue_editor()
     3124 * @see _WP_Editors::parse_settings()
     3125 * @param array $args {
     3126 *     Args.
     3127 *
     3128 *     @type string   $type     The MIME type of the file to be edited.
     3129 *     @type string   $file     Filename to be edited. Extension is used to sniff the type. Can be supplied as alternative to `$type` param.
     3130 *     @type array    $settings Settings to merge on top of defaults which derive from `$type` or `$file` args.
     3131 *     @type WP_Theme $theme    Theme being edited when on theme editor.
     3132 *     @type string   $plugin   Plugin being edited when on plugin editor.
     3133 * }
     3134 * @returns array|false Settings for the enqueued code editor, or false if the editor was not enqueued .
     3135 */
     3136function wp_enqueue_code_editor( $args ) {
     3137        if ( is_user_logged_in() && 'false' === wp_get_current_user()->syntax_highlighting ) {
     3138                return false;
     3139        }
     3140
     3141        $settings = array(
     3142                'codemirror' => array(
     3143                        'indentUnit' => 4,
     3144                        'indentWithTabs' => true,
     3145                        'inputStyle' => 'contenteditable',
     3146                        'lineNumbers' => true,
     3147                        'lineWrapping' => true,
     3148                        'styleActiveLine' => true,
     3149                        'continueComments' => true,
     3150                        'extraKeys' => array(
     3151                                'Ctrl-Space' => 'autocomplete',
     3152                                'Ctrl-/' => 'toggleComment',
     3153                                'Cmd-/' => 'toggleComment',
     3154                                'Alt-F' => 'findPersistent',
     3155                        ),
     3156                        'direction' => 'ltr', // Code is shown in LTR even in RTL languages.
     3157                ),
     3158                'csslint' => array(
     3159                        'errors' => true, // Parsing errors.
     3160                        'box-model' => true,
     3161                        'display-property-grouping' => true,
     3162                        'duplicate-properties' => true,
     3163                        'known-properties' => true,
     3164                        'outline-none' => true,
     3165                ),
     3166                'jshint' => array(
     3167                        // The following are copied from <https://github.com/WordPress/wordpress-develop/blob/4.8.1/.jshintrc>.
     3168                        'boss' => true,
     3169                        'curly' => true,
     3170                        'eqeqeq' => true,
     3171                        'eqnull' => true,
     3172                        'es3' => true,
     3173                        'expr' => true,
     3174                        'immed' => true,
     3175                        'noarg' => true,
     3176                        'nonbsp' => true,
     3177                        'onevar' => true,
     3178                        'quotmark' => 'single',
     3179                        'trailing' => true,
     3180                        'undef' => true,
     3181                        'unused' => true,
     3182
     3183                        'browser' => true,
     3184
     3185                        'globals' => array(
     3186                                '_' => false,
     3187                                'Backbone' => false,
     3188                                'jQuery' => false,
     3189                                'JSON' => false,
     3190                                'wp' => false,
     3191                        ),
     3192                ),
     3193                'htmlhint' => array(
     3194                        'tagname-lowercase' => true,
     3195                        'attr-lowercase' => true,
     3196                        'attr-value-double-quotes' => true,
     3197                        'doctype-first' => false,
     3198                        'tag-pair' => true,
     3199                        'spec-char-escape' => true,
     3200                        'id-unique' => true,
     3201                        'src-not-empty' => true,
     3202                        'attr-no-duplication' => true,
     3203                        'alt-require' => true,
     3204                        'space-tab-mixed-disabled' => 'tab',
     3205                        'attr-unsafe-chars' => true,
     3206                ),
     3207        );
     3208
     3209        $type = '';
     3210        if ( isset( $args['type'] ) ) {
     3211                $type = $args['type'];
     3212
     3213                // Remap MIME types to ones that CodeMirror modes will recognize.
     3214                if ( 'application/x-patch' === $type || 'text/x-patch' === $type ) {
     3215                        $type = 'text/x-diff';
     3216                }
     3217        } elseif ( isset( $args['file'] ) && false !== strpos( basename( $args['file'] ), '.' ) ) {
     3218                $extension = strtolower( pathinfo( $args['file'], PATHINFO_EXTENSION ) );
     3219                foreach ( wp_get_mime_types() as $exts => $mime ) {
     3220                        if ( preg_match( '!^(' . $exts . ')$!i', $extension ) ) {
     3221                                $type = $mime;
     3222                                break;
     3223                        }
     3224                }
     3225
     3226                // Supply any types that are not matched by wp_get_mime_types().
     3227                if ( empty( $type ) ) {
     3228                        switch ( $extension ) {
     3229                                case 'conf':
     3230                                        $type = 'text/nginx';
     3231                                        break;
     3232                                case 'css':
     3233                                        $type = 'text/css';
     3234                                        break;
     3235                                case 'diff':
     3236                                case 'patch':
     3237                                        $type = 'text/x-diff';
     3238                                        break;
     3239                                case 'html':
     3240                                case 'htm':
     3241                                        $type = 'text/html';
     3242                                        break;
     3243                                case 'http':
     3244                                        $type = 'message/http';
     3245                                        break;
     3246                                case 'js':
     3247                                        $type = 'text/javascript';
     3248                                        break;
     3249                                case 'json':
     3250                                        $type = 'application/json';
     3251                                        break;
     3252                                case 'jsx':
     3253                                        $type = 'text/jsx';
     3254                                        break;
     3255                                case 'less':
     3256                                        $type = 'text/x-less';
     3257                                        break;
     3258                                case 'md':
     3259                                        $type = 'text/x-gfm';
     3260                                        break;
     3261                                case 'php':
     3262                                case 'phtml':
     3263                                case 'php3':
     3264                                case 'php4':
     3265                                case 'php5':
     3266                                case 'php7':
     3267                                case 'phps':
     3268                                        $type = 'application/x-httpd-php';
     3269                                        break;
     3270                                case 'scss':
     3271                                        $type = 'text/x-scss';
     3272                                        break;
     3273                                case 'sass':
     3274                                        $type = 'text/x-sass';
     3275                                        break;
     3276                                case 'sh':
     3277                                case 'bash':
     3278                                        $type = 'text/x-sh';
     3279                                        break;
     3280                                case 'sql':
     3281                                        $type = 'text/x-sql';
     3282                                        break;
     3283                                case 'svg':
     3284                                        $type = 'application/svg+xml';
     3285                                        break;
     3286                                case 'xml':
     3287                                        $type = 'text/xml';
     3288                                        break;
     3289                                case 'yml':
     3290                                case 'yaml':
     3291                                        $type = 'text/x-yaml';
     3292                                        break;
     3293                                case 'txt':
     3294                                default:
     3295                                        $type = 'text/plain';
     3296                                        break;
     3297                        }
     3298                }
     3299        }
     3300
     3301        if ( 'text/css' === $type ) {
     3302                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3303                        'mode' => 'css',
     3304                        'lint' => true,
     3305                        'autoCloseBrackets' => true,
     3306                        'matchBrackets' => true,
     3307                ) );
     3308        } elseif ( 'text/x-scss' === $type || 'text/x-less' === $type || 'text/x-sass' === $type ) {
     3309                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3310                        'mode' => $type,
     3311                        'autoCloseBrackets' => true,
     3312                        'matchBrackets' => true,
     3313                ) );
     3314        } elseif ( 'text/x-diff' === $type ) {
     3315                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3316                        'mode' => 'diff',
     3317                ) );
     3318        } elseif ( 'text/html' === $type ) {
     3319                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3320                        'mode' => 'htmlmixed',
     3321                        'lint' => true,
     3322                        'autoCloseBrackets' => true,
     3323                        'autoCloseTags' => true,
     3324                        'matchTags' => array(
     3325                                'bothTags' => true,
     3326                        ),
     3327                ) );
     3328
     3329                if ( ! current_user_can( 'unfiltered_html' ) ) {
     3330                        $settings['htmlhint']['kses'] = wp_kses_allowed_html( 'post' );
     3331                }
     3332        } elseif ( 'text/x-gfm' === $type ) {
     3333                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3334                        'mode' => 'gfm',
     3335                        'highlightFormatting' => true,
     3336                ) );
     3337        } elseif ( 'application/javascript' === $type || 'text/javascript' === $type ) {
     3338                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3339                        'mode' => 'javascript',
     3340                        'lint' => true,
     3341                        'autoCloseBrackets' => true,
     3342                        'matchBrackets' => true,
     3343                ) );
     3344        } elseif ( false !== strpos( $type, 'json' ) ) {
     3345                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3346                        'mode' => array(
     3347                                'name' => 'javascript',
     3348                        ),
     3349                        'lint' => true,
     3350                        'autoCloseBrackets' => true,
     3351                        'matchBrackets' => true,
     3352                ) );
     3353                if ( 'application/ld+json' === $type ) {
     3354                        $settings['codemirror']['mode']['jsonld'] = true;
     3355                } else {
     3356                        $settings['codemirror']['mode']['json'] = true;
     3357                }
     3358        } elseif ( false !== strpos( $type, 'jsx' ) ) {
     3359                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3360                        'mode' => 'jsx',
     3361                        'autoCloseBrackets' => true,
     3362                        'matchBrackets' => true,
     3363                ) );
     3364        } elseif ( 'text/x-markdown' === $type ) {
     3365                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3366                        'mode' => 'markdown',
     3367                        'highlightFormatting' => true,
     3368                ) );
     3369        } elseif ( 'text/nginx' === $type ) {
     3370                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3371                        'mode' => 'nginx',
     3372                ) );
     3373        } elseif ( 'application/x-httpd-php' === $type ) {
     3374                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3375                        'mode' => 'php',
     3376                        'autoCloseBrackets' => true,
     3377                        'autoCloseTags' => true,
     3378                        'matchBrackets' => true,
     3379                        'matchTags' => array(
     3380                                'bothTags' => true,
     3381                        ),
     3382                ) );
     3383        } elseif ( 'text/x-sql' === $type || 'text/x-mysql' === $type ) {
     3384                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3385                        'mode' => 'sql',
     3386                        'autoCloseBrackets' => true,
     3387                        'matchBrackets' => true,
     3388                ) );
     3389        } elseif ( false !== strpos( $type, 'xml' ) ) {
     3390                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3391                        'mode' => 'xml',
     3392                        'autoCloseBrackets' => true,
     3393                        'autoCloseTags' => true,
     3394                        'matchTags' => array(
     3395                                'bothTags' => true,
     3396                        ),
     3397                ) );
     3398        } elseif ( 'text/x-yaml' === $type ) {
     3399                $settings['codemirror'] = array_merge( $settings['codemirror'], array(
     3400                        'mode' => 'yaml',
     3401                ) );
     3402        } else {
     3403                $settings['codemirror']['mode'] = $type;
     3404        }
     3405
     3406        if ( ! empty( $settings['codemirror']['lint'] ) ) {
     3407                $settings['codemirror']['gutters'][] = 'CodeMirror-lint-markers';
     3408        }
     3409
     3410        // Let settings supplied via args override any defaults.
     3411        if ( isset( $args['settings'] ) ) {
     3412                foreach ( $args['settings'] as $key => $value ) {
     3413                        $settings[ $key ] = array_merge(
     3414                                $settings[ $key ],
     3415                                $value
     3416                        );
     3417                }
     3418        }
     3419
     3420        /**
     3421         * Filters settings that are passed into the code editor.
     3422         *
     3423         * Returning a falsey value will disable the syntax-highlighting code editor.
     3424         *
     3425         * @since 4.9.0
     3426         *
     3427         * @param array $settings The array of settings passed to the code editor. A falsey value disables the editor.
     3428         * @param array $args {
     3429         *     Args passed when calling `wp_enqueue_code_editor()`.
     3430         *
     3431         *     @type string   $type     The MIME type of the file to be edited.
     3432         *     @type string   $file     Filename being edited.
     3433         *     @type array    $settings Settings to merge on top of defaults which derive from `$type` or `$file` args.
     3434         *     @type WP_Theme $theme    Theme being edited when on theme editor.
     3435         *     @type string   $plugin   Plugin being edited when on plugin editor.
     3436         * }
     3437         */
     3438        $settings = apply_filters( 'wp_code_editor_settings', $settings, $args );
     3439
     3440        if ( empty( $settings ) || empty( $settings['codemirror'] ) ) {
     3441                return false;
     3442        }
     3443
     3444        wp_enqueue_script( 'code-editor' );
     3445        wp_enqueue_style( 'code-editor' );
     3446
     3447        wp_enqueue_script( 'codemirror' );
     3448        wp_enqueue_style( 'codemirror' );
     3449
     3450        if ( isset( $settings['codemirror']['mode'] ) ) {
     3451                $mode = $settings['codemirror']['mode'];
     3452                if ( is_string( $mode ) ) {
     3453                        $mode = array(
     3454                                'name' => $mode,
     3455                        );
     3456                }
     3457
     3458                if ( ! empty( $settings['codemirror']['lint'] ) ) {
     3459                        switch ( $mode['name'] ) {
     3460                                case 'css':
     3461                                case 'text/css':
     3462                                case 'text/x-scss':
     3463                                case 'text/x-less':
     3464                                        wp_enqueue_script( 'csslint' );
     3465                                        break;
     3466                                case 'htmlmixed':
     3467                                case 'text/html':
     3468                                case 'php':
     3469                                case 'application/x-httpd-php':
     3470                                case 'text/x-php':
     3471                                        wp_enqueue_script( 'htmlhint' );
     3472                                        wp_enqueue_script( 'csslint' );
     3473                                        wp_enqueue_script( 'jshint' );
     3474                                        if ( ! current_user_can( 'unfiltered_html' ) ) {
     3475                                                wp_enqueue_script( 'htmlhint-kses' );
     3476                                        }
     3477                                        break;
     3478                                case 'javascript':
     3479                                case 'application/ecmascript':
     3480                                case 'application/json':
     3481                                case 'application/javascript':
     3482                                case 'application/ld+json':
     3483                                case 'text/typescript':
     3484                                case 'application/typescript':
     3485                                        wp_enqueue_script( 'jshint' );
     3486                                        wp_enqueue_script( 'jsonlint' );
     3487                                        break;
     3488                        }
     3489                }
     3490        }
     3491
     3492        wp_add_inline_script( 'code-editor', sprintf( 'jQuery.extend( wp.codeEditor.defaultSettings, %s );', wp_json_encode( $settings ) ) );
     3493
     3494        /**
     3495         * Fires when scripts and styles are enqueued for the code editor.
     3496         *
     3497         * @since 4.9.0
     3498         *
     3499         * @param array $settings Settings for the enqueued code editor.
     3500         */
     3501        do_action( 'wp_enqueue_code_editor', $settings );
     3502
     3503        return $settings;
     3504}
     3505
     3506/**
    31193507 * Retrieves the contents of the search WordPress query variable.
    31203508 *
  • trunk/src/wp-includes/script-loader.php

    r41375 r41376  
    467467        );
    468468
     469        $scripts->add( 'codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.29.1-alpha-ee20357' );
     470        $scripts->add( 'csslint', '/wp-includes/js/codemirror/csslint.js', array(), '1.0.5' );
     471        $scripts->add( 'jshint', '/wp-includes/js/codemirror/jshint.js', array(), '2.9.5' );
     472        $scripts->add( 'jsonlint', '/wp-includes/js/codemirror/jsonlint.js', array(), '1.6.2' );
     473        $scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '0.9.14-xwp' );
     474        $scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) );
     475        $scripts->add( 'code-editor', "/wp-admin/js/code-editor$suffix.js", array( 'jquery', 'codemirror' ) );
     476        $scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'code-editor', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) );
     477        did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( wp_array_slice_assoc(
     478                /* translators: placeholder is error count */
     479                _n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
     480                array( 'singular', 'plural' )
     481        ) ) ) );
     482
    469483        $scripts->add( 'wp-playlist', "/wp-includes/js/mediaelement/wp-playlist$suffix.js", array( 'wp-util', 'backbone', 'mediaelement' ), false, 1 );
    470484
     
    552566                // Used for overriding the file types allowed in plupload.
    553567                'allowedFiles'       => __( 'Allowed Files' ),
     568                'customCssError'     => wp_array_slice_assoc(
     569                        /* translators: placeholder is error count */
     570                        _n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
     571                        array( 'singular', 'plural' )
     572                ),
    554573        ) );
    555574        $scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     
    689708                $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo', 'wp-api-request' ) );
    690709                $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) );
    691                 $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
     710                $scripts->add( 'custom-html-widgets', "/wp-admin/js/widgets/custom-html-widgets$suffix.js", array( 'code-editor', 'jquery', 'backbone', 'wp-util', 'jquery-ui-core', 'wp-a11y' ) );
    692711
    693712                $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
     
    930949        $styles->add( 'site-icon',           "/wp-admin/css/site-icon$suffix.css" );
    931950        $styles->add( 'l10n',                "/wp-admin/css/l10n$suffix.css" );
     951        $styles->add( 'code-editor',         "/wp-admin/css/code-editor$suffix.css", array( 'codemirror' ) );
    932952
    933953        $styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n' ) );
     
    964984        $styles->add( 'wp-mediaelement',     "/wp-includes/js/mediaelement/wp-mediaelement$suffix.css", array( 'mediaelement' ) );
    965985        $styles->add( 'thickbox',            '/wp-includes/js/thickbox/thickbox.css', array( 'dashicons' ) );
     986        $styles->add( 'codemirror',          '/wp-includes/js/codemirror/codemirror.min.css', array(), '5.29.1-alpha-ee20357' );
    966987
    967988        // Deprecated CSS
  • trunk/src/wp-includes/user.php

    r41289 r41376  
    13771377 *
    13781378 * Most of the `$userdata` array fields have filters associated with the values. Exceptions are
    1379  * 'ID', 'rich_editing', 'comment_shortcuts', 'admin_color', 'use_ssl',
     1379 * 'ID', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl',
    13801380 * 'user_registered', and 'role'. The filters have the prefix 'pre_user_' followed by the field
    13811381 * name. An example using 'description' would have the filter called, 'pre_user_description' that
     
    14111411 *     @type string|bool $rich_editing         Whether to enable the rich-editor for the user.
    14121412 *                                             False if not empty.
     1413 *     @type string|bool $syntax_highlighting  Whether to enable the rich code editor for the user.
     1414 *                                             False if not empty.
    14131415 *     @type string|bool $comment_shortcuts    Whether to enable comment moderation keyboard
    14141416 *                                             shortcuts for the user. Default false.
     
    16231625
    16241626        $meta['rich_editing'] = empty( $userdata['rich_editing'] ) ? 'true' : $userdata['rich_editing'];
     1627
     1628        $meta['syntax_highlighting'] = empty( $userdata['syntax_highlighting'] ) ? 'true' : $userdata['syntax_highlighting'];
    16251629
    16261630        $meta['comment_shortcuts'] = empty( $userdata['comment_shortcuts'] ) || 'false' === $userdata['comment_shortcuts'] ? 'false' : 'true';
     
    17101714         *     @type string   $description          The user's description.
    17111715         *     @type bool     $rich_editing         Whether to enable the rich-editor for the user. False if not empty.
     1716         *     @type bool     $syntax_highlighting  Whether to enable the rich code editor for the user. False if not empty.
    17121717         *     @type bool     $comment_shortcuts    Whether to enable keyboard shortcuts for the user. Default false.
    17131718         *     @type string   $admin_color          The color scheme for a user's admin screen. Default 'fresh'.
     
    20362041 */
    20372042function _get_additional_user_keys( $user ) {
    2038         $keys = array( 'first_name', 'last_name', 'nickname', 'description', 'rich_editing', 'comment_shortcuts', 'admin_color', 'use_ssl', 'show_admin_bar_front', 'locale' );
     2043        $keys = array( 'first_name', 'last_name', 'nickname', 'description', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl', 'show_admin_bar_front', 'locale' );
    20392044        return array_merge( $keys, array_keys( wp_get_user_contact_methods( $user ) ) );
    20402045}
  • trunk/src/wp-includes/widgets/class-wp-widget-custom-html.php

    r41132 r41376  
    1818
    1919        /**
     20         * Whether or not the widget has been registered yet.
     21         *
     22         * @since 4.9.0
     23         * @var bool
     24         */
     25        protected $registered = false;
     26
     27        /**
    2028         * Default instance.
    2129         *
     
    4452                );
    4553                parent::__construct( 'custom_html', __( 'Custom HTML' ), $widget_ops, $control_ops );
     54        }
     55
     56        /**
     57         * Add hooks for enqueueing assets when registering all widget instances of this widget class.
     58         *
     59         * @since 4.9.0
     60         *
     61         * @param integer $number Optional. The unique order number of this widget instance
     62         *                        compared to other instances of the same class. Default -1.
     63         */
     64        public function _register_one( $number = -1 ) {
     65                parent::_register_one( $number );
     66                if ( $this->registered ) {
     67                        return;
     68                }
     69                $this->registered = true;
     70
     71                wp_add_inline_script( 'custom-html-widgets', sprintf( 'wp.customHtmlWidgets.idBases.push( %s );', wp_json_encode( $this->id_base ) ) );
     72
     73                // Note that the widgets component in the customizer will also do the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts().
     74                add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) );
     75
     76                // Note that the widgets component in the customizer will also do the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts().
     77                add_action( 'admin_footer-widgets.php', array( 'WP_Widget_Custom_HTML', 'render_control_template_scripts' ) );
     78
     79                // Note this action is used to ensure the help text is added to the end.
     80                add_action( 'admin_head-widgets.php', array( 'WP_Widget_Custom_HTML', 'add_help_text' ) );
    4681        }
    4782
     
    119154
    120155        /**
     156         * Loads the required scripts and styles for the widget control.
     157         *
     158         * @since 4.9.0
     159         */
     160        public function enqueue_admin_scripts() {
     161                $settings = wp_enqueue_code_editor( array(
     162                        'type' => 'text/html',
     163                ) );
     164
     165                wp_enqueue_script( 'custom-html-widgets' );
     166                if ( empty( $settings ) ) {
     167                        $settings = array(
     168                                'disabled' => true,
     169                        );
     170                }
     171                wp_add_inline_script( 'custom-html-widgets', sprintf( 'wp.customHtmlWidgets.init( %s );', wp_json_encode( $settings ) ), 'after' );
     172
     173                $l10n = array(
     174                        'errorNotice' => wp_array_slice_assoc(
     175                                /* translators: placeholder is error count */
     176                                _n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
     177                                array( 'singular', 'plural' )
     178                        ),
     179                );
     180                wp_add_inline_script( 'custom-html-widgets', sprintf( 'jQuery.extend( wp.customHtmlWidgets.l10n, %s );', wp_json_encode( $l10n ) ), 'after' );
     181        }
     182
     183        /**
    121184         * Outputs the Custom HTML widget settings form.
    122185         *
    123186         * @since 4.8.1
    124          *
     187         * @since 4.9.0 The form contains only hidden sync inputs. For the control UI, see `WP_Widget_Custom_HTML::render_control_template_scripts()`.
     188         *
     189         * @see WP_Widget_Custom_HTML::render_control_template_scripts()
    125190         * @param array $instance Current instance.
    126191         * @returns void
     
    129194                $instance = wp_parse_args( (array) $instance, $this->default_instance );
    130195                ?>
    131                 <p>
    132                         <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
    133                         <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>"/>
    134                 </p>
    135 
    136                 <p>
    137                         <label for="<?php echo $this->get_field_id( 'content' ); ?>"><?php _e( 'Content:' ); ?></label>
    138                         <textarea class="widefat code" rows="16" cols="20" id="<?php echo $this->get_field_id( 'content' ); ?>" name="<?php echo $this->get_field_name( 'content' ); ?>"><?php echo esc_textarea( $instance['content'] ); ?></textarea>
    139                 </p>
    140 
    141                 <?php if ( ! current_user_can( 'unfiltered_html' ) ) : ?>
    142                         <?php
    143                         $probably_unsafe_html = array( 'script', 'iframe', 'form', 'input', 'style' );
    144                         $allowed_html = wp_kses_allowed_html( 'post' );
    145                         $disallowed_html = array_diff( $probably_unsafe_html, array_keys( $allowed_html ) );
    146                         ?>
    147                         <?php if ( ! empty( $disallowed_html ) ) : ?>
    148                                 <p>
    149                                         <?php _e( 'Some HTML tags are not permitted, including:' ); ?>
    150                                         <code><?php echo join( '</code>, <code>', $disallowed_html ); ?></code>
    151                                 </p>
     196                <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title sync-input" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>"/>
     197                <textarea id="<?php echo $this->get_field_id( 'content' ); ?>" name="<?php echo $this->get_field_name( 'content' ); ?>" class="content sync-input" hidden><?php echo esc_textarea( $instance['content'] ); ?></textarea>
     198                <?php
     199        }
     200
     201        /**
     202         * Render form template scripts.
     203         *
     204         * @since 4.9.0
     205         */
     206        public static function render_control_template_scripts() {
     207                ?>
     208                <script type="text/html" id="tmpl-widget-custom-html-control-fields">
     209                        <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
     210                        <p>
     211                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:', 'default' ); ?></label>
     212                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
     213                        </p>
     214
     215                        <p>
     216                                <label for="{{ elementIdPrefix }}content" class="screen-reader-text"><?php esc_html_e( 'Content:', 'default' ); ?></label>
     217                                <textarea id="{{ elementIdPrefix }}content" class="widefat code content" rows="16" cols="20"></textarea>
     218                        </p>
     219
     220                        <?php if ( ! current_user_can( 'unfiltered_html' ) ) : ?>
     221                                <?php
     222                                $probably_unsafe_html = array( 'script', 'iframe', 'form', 'input', 'style' );
     223                                $allowed_html = wp_kses_allowed_html( 'post' );
     224                                $disallowed_html = array_diff( $probably_unsafe_html, array_keys( $allowed_html ) );
     225                                ?>
     226                                <?php if ( ! empty( $disallowed_html ) ) : ?>
     227                                        <# if ( data.codeEditorDisabled ) { #>
     228                                                <p>
     229                                                        <?php _e( 'Some HTML tags are not permitted, including:', 'default' ); ?>
     230                                                        <code><?php echo join( '</code>, <code>', $disallowed_html ); ?></code>
     231                                                </p>
     232                                        <# } #>
     233                                <?php endif; ?>
    152234                        <?php endif; ?>
    153                 <?php endif; ?>
     235
     236                        <div class="code-editor-error-container"></div>
     237                </script>
    154238                <?php
    155239        }
     240
     241        /**
     242         * Add help text to widgets admin screen.
     243         *
     244         * @since 4.9.0
     245         */
     246        public static function add_help_text() {
     247                $screen = get_current_screen();
     248
     249                $content = '<p>';
     250                $content .= __( 'Use the Custom HTML widget to add arbitrary HTML code to your widget areas.' );
     251                $content .= '</p>';
     252
     253                $content .= '<p>' . __( 'When using a keyboard to navigate:' ) . '</p>';
     254                $content .= '<ul>';
     255                $content .= '<li>' . __( 'In the HTML edit field, Tab enters a tab character.' ) . '</li>';
     256                $content .= '<li>' . __( 'To move keyboard focus, press Esc then Tab for the next element, or Esc then Shift+Tab for the previous element.' ) . '</li>';
     257                $content .= '</ul>';
     258
     259                $content .= '<p>';
     260                $content .= sprintf(
     261                        /* translators: placeholder is link to user profile */
     262                        __( 'The edit field automatically highlights code syntax. You can disable this in your %s to work in plan text mode.' ),
     263                        sprintf(
     264                                ' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text">%3$s</span></a>',
     265                                esc_url( get_edit_profile_url() . '#syntax_highlighting' ),
     266                                __( 'user profile' ),
     267                                /* translators: accessibility text */
     268                                __( '(opens in a new window)', 'default' )
     269                        )
     270                );
     271                $content .= '</p>';
     272
     273                $screen->add_help_tab( array(
     274                        'id' => 'custom_html_widget',
     275                        'title' => __( 'Custom HTML Widget' ),
     276                        'content' => $content,
     277                ) );
     278        }
    156279}
  • trunk/src/wp-includes/widgets/class-wp-widget-text.php

    r41361 r41376  
    354354                wp_enqueue_editor();
    355355                wp_enqueue_script( 'text-widgets' );
     356                wp_add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
    356357        }
    357358
  • trunk/tests/phpunit/tests/customize/custom-css-setting.php

    r39919 r41376  
    356356                $this->assertTrue( $result );
    357357
    358                 // Check for Unclosed Comment.
    359                 $unclosed_comment = $basic_css . ' /* This is a comment. ';
     358                // Check for markup.
     359                $unclosed_comment = $basic_css . '</style>';
    360360                $result = $this->setting->validate( $unclosed_comment );
    361                 $this->assertTrue( array_key_exists( 'unclosed_comment', $result->errors ) );
    362 
    363                 // Check for Unopened Comment.
    364                 $unclosed_comment = $basic_css . ' This is a comment.*/';
    365                 $result = $this->setting->validate( $unclosed_comment );
    366                 $this->assertTrue( array_key_exists( 'imbalanced_comments', $result->errors ) );
    367 
    368                 // Check for Unclosed Curly Brackets.
    369                 $unclosed_curly_bracket = $basic_css . '  a.link { text-decoration: none;';
    370                 $result = $this->setting->validate( $unclosed_curly_bracket );
    371                 $this->assertTrue( array_key_exists( 'imbalanced_curly_brackets', $result->errors ) );
    372 
    373                 // Check for Unopened Curly Brackets.
    374                 $unopened_curly_bracket = $basic_css . '  a.link text-decoration: none; }';
    375                 $result = $this->setting->validate( $unopened_curly_bracket );
    376                 $this->assertTrue( array_key_exists( 'imbalanced_curly_brackets', $result->errors ) );
    377 
    378                 // Check for Unclosed Braces.
    379                 $unclosed_brace = $basic_css . '  input[type="text" { color: #f00; } ';
    380                 $result = $this->setting->validate( $unclosed_brace );
    381                 $this->assertTrue( array_key_exists( 'imbalanced_braces', $result->errors ) );
    382 
    383                 // Check for Unopened Braces.
    384                 $unopened_brace = $basic_css . ' inputtype="text"] { color: #f00; } ';
    385                 $result = $this->setting->validate( $unopened_brace );
    386                 $this->assertTrue( array_key_exists( 'imbalanced_braces', $result->errors ) );
    387 
    388                 // Check for Imbalanced Double Quotes.
    389                 $imbalanced_double_quotes = $basic_css . ' div.background-image { background-image: url( "image.jpg ); } ';
    390                 $result = $this->setting->validate( $imbalanced_double_quotes );
    391                 $this->assertTrue( array_key_exists( 'unequal_double_quotes', $result->errors ) );
    392 
    393                 // Check for Unclosed Parentheses.
    394                 $unclosed_parentheses = $basic_css . ' div.background-image { background-image: url( "image.jpg" ; } ';
    395                 $result = $this->setting->validate( $unclosed_parentheses );
    396                 $this->assertTrue( array_key_exists( 'imbalanced_parentheses', $result->errors ) );
    397 
    398                 // Check for Unopened Parentheses.
    399                 $unopened_parentheses = $basic_css . ' div.background-image { background-image: url "image.jpg" ); } ';
    400                 $result = $this->setting->validate( $unopened_parentheses );
    401                 $this->assertTrue( array_key_exists( 'imbalanced_parentheses', $result->errors ) );
    402 
    403                 // A basic Content declaration with no other errors should not throw an error.
    404                 $content_declaration = $basic_css . ' a:before { content: ""; display: block; }';
    405                 $result = $this->setting->validate( $content_declaration );
    406                 $this->assertTrue( $result );
    407 
    408                 // An error, along with a Content declaration will throw two errors.
    409                 // In this case, we're using an extra opening brace.
    410                 $content_declaration = $basic_css . ' a:before { content: "["; display: block; }';
    411                 $result = $this->setting->validate( $content_declaration );
    412                 $this->assertTrue( array_key_exists( 'imbalanced_braces', $result->errors ) );
    413                 $this->assertTrue( array_key_exists( 'possible_false_positive', $result->errors ) );
    414 
    415                 $css = 'body { background: #f00; } h1.site-title { font-size: 36px; } a:hover { text-decoration: none; } input[type="text"] { padding: 1em; } /* This is a comment */';
    416                 $this->assertTrue( $this->setting->validate( $css ) );
    417 
    418                 $validity = $this->setting->validate( $css . ' /* This is another comment.' );
    419                 $this->assertInstanceOf( 'WP_Error', $validity );
    420                 $this->assertContains( 'unclosed code comment', join( ' ', $validity->get_error_messages() ) );
    421 
    422                 $css = '/* This is comment one. */  /* This is comment two. */';
    423                 $this->assertTrue( $this->setting->validate( $css ) );
    424 
    425                 $basic_css = 'body { background: #f00; } h1.site-title { font-size: 36px; } a:hover { text-decoration: none; } input[type="text"] { padding: 1em; }';
    426                 $this->assertTrue( $this->setting->validate( $basic_css ) );
    427 
    428                 $css = $basic_css . ' .link:before { content: "*"; display: block; }';
    429                 $this->assertTrue( $this->setting->validate( $css ) );
    430 
    431                 $css .= ' ( trailing';
    432                 $validity = $this->setting->validate( $css );
    433                 $this->assertWPError( $validity );
    434                 $this->assertNotEmpty( $result->get_error_message( 'possible_false_positive' ) );
     361                $this->assertTrue( array_key_exists( 'illegal_markup', $result->errors ) );
    435362        }
    436363}
  • trunk/tests/phpunit/tests/customize/manager.php

    r41372 r41376  
    23522352                $this->assertNotEmpty( $data );
    23532353
    2354                 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts' ), array_keys( $data ) );
     2354                $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'customCss', 'changeset', 'timeouts' ), array_keys( $data ) );
    23552355                $this->assertEquals( $autofocus, $data['autofocus'] );
    23562356                $this->assertArrayHasKey( 'save', $data['nonce'] );
  • trunk/tests/phpunit/tests/user.php

    r41255 r41376  
    359359                $user_data = array(
    360360                        'ID' => self::$author_id, 'use_ssl' => 1, 'show_admin_bar_front' => 1,
    361                         'rich_editing' => 1, 'first_name' => 'first', 'last_name' => 'last',
     361                        'rich_editing' => 1, 'syntax_highlighting' => 1, 'first_name' => 'first', 'last_name' => 'last',
    362362                        'nickname' => 'nick', 'comment_shortcuts' => 'true', 'admin_color' => 'classic',
    363363                        'description' => 'describe'
Note: See TracChangeset for help on using the changeset viewer.