WordPress.org

Make WordPress Core

Changeset 41376


Ignore:
Timestamp:
09/13/2017 06:07:48 AM (10 months 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.