Make WordPress Core


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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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 *
Note: See TracChangeset for help on using the changeset viewer.