WordPress.org

Making WordPress.org

Changeset 2814


Ignore:
Timestamp:
03/25/2016 06:22:12 PM (2 years ago)
Author:
coffee2code
Message:

developer.wordpress.org: Auto-link plaintext references to classes, functions, and methods.

  • Ignores references made in <code> blocks.
  • Does not attempt to recognize references to hooks. Such references must use {@see 'hook_name'} syntax.

Fixes #1483.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-developer/inc/formatting.php

    r2452 r2814  
    2727        add_filter( 'the_excerpt', array( __CLASS__, 'remove_inline_internal' ) );
    2828        add_filter( 'the_content', array( __CLASS__, 'remove_inline_internal' ) );
     29
     30        add_filter( 'the_excerpt', array( __CLASS__, 'autolink_references' ), 11 );
     31        add_filter( 'the_content', array( __CLASS__, 'autolink_references' ), 11 );
    2932
    3033        add_action( 'the_content', array( __CLASS__, 'fix_unintended_markdown' ) );
     
    237240    }
    238241
     242    /**
     243     * Automatically detects inline references to parsed resources and links to them.
     244     *
     245     * Examples:
     246     * - Functions: get_the_ID()
     247     * - Classes:   WP_Query
     248     * - Methods:   WP_Query::is_single()
     249     *
     250     * Note: currently there is not a reliable way to infer references to hooks. Recommend
     251     * using the {@}see 'hook_name'} notation as used in the inline docs.
     252     *
     253     * @param  string $text The text.
     254     * @return string
     255     */
     256    public function autolink_references( $text ) {
     257        $r = '';
     258        $textarr = preg_split( '/(<[^<>]+>)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); // split out HTML tags
     259        $nested_code_pre = 0; // Keep track of how many levels link is nested inside <pre> or <code>
     260        foreach ( $textarr as $piece ) {
     261
     262            if ( preg_match( '|^<code[\s>]|i', $piece ) || preg_match( '|^<pre[\s>]|i', $piece ) || preg_match( '|^<script[\s>]|i', $piece ) || preg_match( '|^<style[\s>]|i', $piece ) )
     263                $nested_code_pre++;
     264            elseif ( $nested_code_pre && ( '</code>' === strtolower( $piece ) || '</pre>' === strtolower( $piece ) || '</script>' === strtolower( $piece ) || '</style>' === strtolower( $piece ) ) )
     265                $nested_code_pre--;
     266
     267            if ( $nested_code_pre || empty( $piece ) || ( $piece[0] === '<' && ! preg_match( '|^<\s*[\w]{1,20}+://|', $piece ) ) ) {
     268                $r .= $piece;
     269                continue;
     270            }
     271
     272            // Long strings might contain expensive edge cases ...
     273            if ( 10000 < strlen( $piece ) ) {
     274                // ... break it up
     275                foreach ( _split_str_by_whitespace( $piece, 2100 ) as $chunk ) { // 2100: Extra room for scheme and leading and trailing paretheses
     276                    if ( 2101 < strlen( $chunk ) ) {
     277                        $r .= $chunk; // Too big, no whitespace: bail.
     278                    } else {
     279                        $r .= make_clickable( $chunk );
     280                    }
     281                }
     282            } else {
     283                /*
     284                 * Everthing outside of this conditional block was copied from core's
     285                 *`make_clickable()`.
     286                 */
     287
     288                $content = " $piece "; // Pad with whitespace to simplify the regexes
     289
     290                // Only if the text contains something that might be a function.
     291                if ( false !== strpos( $content, '()' ) ) {
     292
     293                    // Detect references to class methods, e.g. WP_Query::query()
     294                    // or functions, e.g. register_post_type().
     295                    $content = preg_replace_callback(
     296                        '~
     297                            (?!<.*?)       # Non-capturing check to ensure not matching what looks like the inside of an HTML tag.
     298                            (              # 1: The full method or function name.
     299                                ((\w+)::)? # 2: The class prefix, if a method reference.
     300                                (\w+)      # 3: The method or function name.
     301                            )
     302                            \(\)           # The () that signifies either a method or function.
     303                            (?![^<>]*?>)   # Non-capturing check to ensure not matching what looks like the inside of an HTML tag.
     304                        ~x',
     305                        function ( $matches ) {
     306                            // Reference to a class method.
     307                            if ( $matches[2] ) {
     308                                // Only link actually parsed methods.
     309                                if ( $post = get_page_by_title( $matches[1], OBJECT, 'wp-parser-method' ) ) {
     310                                    return sprintf(
     311                                        '<a href="%s">%s</a>',
     312                                        get_permalink( $post->ID ),
     313                                        $matches[0]
     314                                    );
     315                                }
     316
     317                            // Reference to a function.
     318                            } else {
     319                                // Only link actually parsed functions.
     320                                if ( $post = get_page_by_title( $matches[1], OBJECT, 'wp-parser-function' ) ) {
     321                                    return sprintf(
     322                                        '<a href="%s">%s</a>',
     323                                        get_permalink( $post->ID ),
     324                                        $matches[0]
     325                                    );
     326                                }
     327                            }
     328
     329                            // It's not a reference to an actual thing, so restore original text.
     330                            return $matches[0];
     331                        },
     332                        $content
     333                    );
     334
     335                }
     336
     337                // Detect references to classes, e.g. WP_Query
     338                $content = preg_replace_callback(
     339                    // Most class names start with an uppercase letter and have an underscore.
     340                    // The exceptions are explicitly listed since future classes likely won't violate previous statement.
     341                    '~'
     342                        . '(?<!/)'
     343                        . '\b'                // Word boundary
     344                        . '('                 // Primary match grouping
     345                            . 'wpdb|wp_atom_server|wp_xmlrpc_server extends IXR_Server'               // Exceptions that start with lowercase letter
     346                            . '|AtomFeed|AtomEntry|AtomParser|MagpieRSS|RSSCache|Translations|Walker' // Exceptions that lack an underscore
     347                            . '|[A-Z][a-zA-Z]+_\w+'                                                   // Most start with uppercase, has underscore
     348                        . ')'                 // End primary match grouping
     349                        . '\b'                // Word boundary
     350                        . '(?!([<:]|"|\'>))'  // Does not appear within a tag
     351                    . '~',
     352                    function ( $matches ) {
     353                        // If match is all caps, it's not a possible class name.
     354                        // We'll chalk the sole exception, WP, as merely being an abbreviation (the regex won't match it anyhow).
     355                        if ( strtoupper( $matches[0] ) === $matches[0] ) {
     356                            return $matches[0];
     357                        }
     358
     359                        // Only link actually parsed classes.
     360                        if ( $post = get_page_by_title( $matches[0], OBJECT, 'wp-parser-class' ) ) {
     361                            return sprintf(
     362                                '<a href="%s">%s</a>',
     363                                get_permalink( $post->ID ),
     364                                $matches[0]
     365                            );
     366                        }
     367
     368                        // Not a class reference, so put the original reference back in.
     369                        return $matches[0];
     370                    },
     371                    $content
     372                );
     373
     374                // Maybelater: Detect references to hooks, Currently not deemed reliably possible.
     375
     376                $content = substr( $content, 1, -1 ); // Remove our whitespace padding.
     377                $r .= $content;
     378
     379            } // end else
     380
     381        } // end foreach
     382
     383        // Cleanup of accidental links within links
     384        return preg_replace( '#(<a([ \r\n\t]+[^>]+?>|>))<a [^>]+?>([^>]+?)</a></a>#i', "$1$3</a>", $r );
     385    }
     386
    239387} // DevHub_Formatting
    240388
Note: See TracChangeset for help on using the changeset viewer.