Making WordPress.org

Changeset 11341


Ignore:
Timestamp:
11/25/2021 01:11:00 AM (5 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.