Making WordPress.org


Ignore:
Timestamp:
11/25/2021 01:11:00 AM (3 years ago)
Author:
dd32
Message:

Handbooks, ToC: Update the TOC generation to support Gutenberg usage.

In gutenberg, the headings automatically have IDs present, and can have class and style attributes which also didn't work with the plugin.

This change alters how markup is generated significantly by generating IDs once per the_content call, and makes use of named matching groups for readability.

This change means that:

  • It works with Gutenberg header blocks including id=..
  • Custom IDs can be specified for headings (In the Advanced panel in Gutenberg) to avoid changing the ID when changing the title
  • Headings can be styled inside Gutenberg and it'll still be presented correctly in the ToC markup (both style and class attributes)

Fixes #5963.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/handbook/inc/table-of-contents.php

    r11118 r11341  
    4646    }
    4747
    48     function load_filters() {
     48    public function load_filters() {
    4949        if ( is_singular( $this->post_types ) && ! is_embed() ) {
    5050            add_filter( 'the_content', array( $this, 'add_toc' ) );
     
    103103        }
    104104
    105         $toc = '';
    106 
    107         $items = $this->get_tags( 'h([1-4])', $content );
     105        $toc   = '';
     106        $items = $this->get_tags( 'h(?P<level>[1-4])', $content );
    108107
    109108        if ( count( $items ) < 2 ) {
     
    111110        }
    112111
     112        // Generate a list of the IDs in the document (generating them as needed).
    113113        $this->used_ids = $this->get_reserved_ids();
    114         for ( $i = 1; $i <= 4; $i++ )
    115             $content = $this->add_ids_and_jumpto_links( "h$i", $content );
     114        foreach ( $items as $i => $item ) {
     115            $items[ $i ]['id'] = $this->get_id_for_item( $item );
     116        }
     117
     118        // Replace each level of the headings.
     119        $content = $this->add_ids_and_jumpto_links( $items, $content );
    116120
    117121        if ( ! apply_filters( 'handbook_display_toc', true ) ) {
     
    119123        }
    120124
    121         if ( $items ) {
    122             $contents_header = 'h' . $items[0][2]; // Duplicate the first <h#> tag in the document.
    123             $toc .= $this->styles;
    124             $toc .= '<div class="table-of-contents">';
    125             $toc .= "<$contents_header>" . esc_html( $this->args->header_text ) . "</$contents_header><ul class=\"items\">";
    126             $last_item = false;
    127             $used_ids = $this->get_reserved_ids();
    128 
    129             foreach ( $items as $item ) {
    130                 if ( $last_item ) {
    131                     if ( $last_item < $item[2] )
    132                         $toc .= "\n<ul>\n";
    133                     elseif ( $last_item > $item[2] )
    134                         $toc .= "\n</ul></li>\n";
    135                     else
    136                         $toc .= "</li>\n";
     125        $contents_header = 'h' . $items[0]['level']; // Duplicate the first <h#> tag in the document for the TOC header
     126        $toc            .= $this->styles;
     127        $toc            .= '<div class="table-of-contents">';
     128        $toc            .= "<$contents_header>" . esc_html( $this->args->header_text ) . "</$contents_header><ul class=\"items\">";
     129        $last_item       = false;
     130
     131        foreach ( $items as $item ) {
     132            if ( $last_item ) {
     133                if ( $last_item < $item['level'] ) {
     134                    $toc .= "\n<ul>\n";
     135                } elseif ( $last_item > $item['level'] ) {
     136                    $toc .= "\n</ul></li>\n";
     137                } else {
     138                    $toc .= "</li>\n";
    137139                }
    138 
    139                 $last_item = $item[2];
    140 
    141                 $id = sanitize_title( $item[3] );
    142                 // Append unique suffix if anchor ID isn't unique.
    143                 $count = 2;
    144                 $orig_id = $id;
    145                 while ( in_array( $id, $used_ids ) && $count < 50 ) {
    146                     $id = $orig_id . '-' . $count;
    147                     $count++;
    148                 }
    149                 $used_ids[] = $id;
    150 
    151                 $toc .= '<li><a href="#' . esc_attr( $id  ) . '">' . $item[3]  . '</a>';
    152             }
    153             $toc .= "</ul>\n</div>\n";
    154         }
     140            }
     141
     142            $last_item = $item['level'];
     143
     144            $toc .= '<li><a href="#' . esc_attr( $item['id']  ) . '">' . $item['title']  . '</a>';
     145        }
     146
     147        $toc .= "</ul>\n</div>\n";
    155148
    156149        return $toc . $content;
    157150    }
    158151
    159     protected function add_ids_and_jumpto_links( $tag, $content ) {
    160         $items = $this->get_tags( $tag, $content );
     152    /**
     153     * Add the HTML markup for the in-content header elements.
     154     */
     155    protected function add_ids_and_jumpto_links( $items, $content ) {
    161156        $first = true;
    162157        $matches = array();
     
    165160        foreach ( $items as $item ) {
    166161            $replacement = '';
    167             $matches[] = $item[0];
    168             $id = sanitize_title( $item[2] );
    169 
    170             // Append unique suffix if anchor ID isn't unique.
    171             $count = 2;
    172             $orig_id = $id;
    173             while ( in_array( $id, $this->used_ids ) && $count < 50 ) {
    174                 $id = $orig_id . '-' . $count;
    175                 $count++;
    176             }
    177             $this->used_ids[] = $id;
    178        
     162            $matches[]   = $item[0];
     163            $tag         = 'h' . $item['level']; // 'h2'
     164            $id          = $item['id'];
     165            $title       = $item['title'];
     166            $extra_attrs = $item['attrs']; // 'class="" style=""'
     167            $class       = 'toc-heading';
     168
     169            if ( $extra_attrs ) {
     170                // Strip all IDs from the heading attributes (including empty), we'll replace it with one below.
     171                $extra_attrs = trim( preg_replace( '/id=(["\'])[^"\']*\\1/i', '', $extra_attrs ) );
     172
     173                // Extract any classes present, we're adding our own attribute.
     174                if ( preg_match( '/class=(["\'])(?P<class>[^"\']+)\\1/i', $extra_attrs, $m ) ) {
     175                    $extra_attrs = str_replace( $m[0], '', $extra_attrs );
     176                    $class      .= ' ' . $m['class'];
     177                }
     178            }
     179
    179180            if ( ! $first ) {
    180181                $replacement .= '<p class="toc-jump"><a href="#top">' . __( 'Top &uarr;', 'wporg' ) . '</a></p>';
     
    182183                $first = false;
    183184            }
    184             $a11y_text      = sprintf( '<span class="screen-reader-text">%s</span>', $item[2] );
     185
     186            $a11y_text      = sprintf( '<span class="screen-reader-text">%s</span>', $title );
    185187            $anchor         = sprintf( '<a href="#%1$s" class="anchor"><span aria-hidden="true">#</span>%2$s</a>', $id, $a11y_text );
    186             $replacement   .= sprintf( '<%1$s class="toc-heading" id="%2$s" tabindex="-1">%3$s %4$s</%1$s>', $tag, $id, $item[2], $anchor );
     188            $replacement   .= sprintf( '<%1$s id="%2$s" class="%3$s" tabindex="-1" %4$s>%5$s %6$s</%1$s>', $tag, $id, $class, $extra_attrs, $title, $anchor );
    187189            $replacements[] = $replacement;
    188190        }
     
    201203    }
    202204
    203     private function get_tags( $tag, $content = '' ) {
    204         if ( empty( $content ) )
     205    /**
     206     * Generate an ID for a given HTML element, use the tags `id` attribute if set.
     207     */
     208    protected function get_id_for_item( $item ) {
     209        if ( ! empty( $item['id'] ) ) {
     210            return $item['id'];
     211        }
     212
     213        // Check to see if the item already had a non-empty ID, else generate one from the title.
     214        if ( preg_match( '/id=(["\'])(?P<id>[^"\']+)\\1/', $item['attrs'], $m ) ) {
     215            $id = $m['id'];
     216        } else {
     217            $id = sanitize_title( $item['title'] );
     218        }
     219
     220        // Append unique suffix if anchor ID isn't unique in the document.
     221        $count   = 2;
     222        $orig_id = $id;
     223        while ( in_array( $id, $this->used_ids ) && $count < 50 ) {
     224            $id = $orig_id . '-' . $count;
     225            $count++;
     226        }
     227
     228        $this->used_ids[] = $id;
     229
     230        return $id;
     231    }
     232
     233    protected function get_tags( $tag, $content = '' ) {
     234        if ( empty( $content ) ) {
    205235            $content = get_the_content();
    206         preg_match_all( "/(<{$tag}>)(.*)(<\/{$tag}>)/", $content, $matches, PREG_SET_ORDER );
     236        }
     237
     238        preg_match_all( "/(?P<tag><{$tag}(?P<attrs>[^>]*)>)(?P<title>.*)(<\/{$tag}>)/iJ", $content, $matches, PREG_SET_ORDER );
     239
    207240        return $matches;
    208241    }
Note: See TracChangeset for help on using the changeset viewer.