Make WordPress Core


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