]*>|#i', $content, $tags, PREG_OFFSET_CAPTURE ) ) { return $content; } $result = ''; $last = 0; $depth = 0; $start = null; $found = false; foreach ( $tags[0] as $tag ) { $is_open = '/' !== $tag[0][1]; if ( $is_open && 0 === $depth ) { $start = self::is_ours( $tag[0] ) && ! self::framed( $content, $tag[1] ) ? $tag[1] : null; } $depth += $is_open ? 1 : -1; if ( 0 === $depth && null !== $start ) { $end = $tag[1] + strlen( $tag[0] ); $table = substr( $content, $start, $end - $start ); $result .= substr( $content, $last, $start - $last ) . self::wrap( $table ); $last = $end; $start = null; $found = true; } $depth = max( 0, $depth ); } if ( ! $found ) { return $content; } wp_enqueue_style( 'threece-elements' ); return $result . substr( $content, $last ); } /** * The table tag carries our class as a whole word. * * @param string $tag Opening table tag. */ private static function is_ours( $tag ): bool { return (bool) preg_match( '#\sclass\s*=\s*(["\'])(?:[^"\']*\s)?' . self::CLASS_NAME . '(?:\s[^"\']*)?\1#i', $tag ); } /** * The opening tag of the table carries one of our modifier classes. * * @param string $table Table HTML. * @param string $modifier Modifier without prefix, e.g. stack. */ private static function has_class( $table, $modifier ): bool { return (bool) preg_match( '#^]*\sclass\s*=\s*(["\'])(?:[^"\']*\s)?' . self::CLASS_NAME . '--' . preg_quote( $modifier, '#' ) . '(?:\s[^"\']*)?\1#i', $table ); } /** * Card layout on phones: every body cell gets the text of its column header as data-label. Header from thead or * from a first row of th cells, column spans counted. Tables with row spans get no labels, they would be wrong. * * @param string $table Table HTML. */ private static function cell_labels( $table ): string { if ( preg_match( '#\srowspan\s*=#i', $table ) || ! class_exists( 'DOMDocument' ) ) { return $table; } $doc = new DOMDocument(); $previous = libxml_use_internal_errors( true ); $loaded = $doc->loadHTML( '
' . $table . '
', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); libxml_clear_errors(); libxml_use_internal_errors( $previous ); $xp = new DOMXPath( $doc ); $box = $doc->getElementsByTagName( 'div' )->item( 0 ); $tbl = $doc->getElementsByTagName( 'table' )->item( 0 ); if ( ! $loaded || ! $box || ! $tbl ) { return $table; } // Rows of this table only, not of tables nested in its cells. $rows = $xp->query( './tr | ./thead/tr | ./tbody/tr | ./tfoot/tr', $tbl ); $head = $xp->query( './thead/tr[1]', $tbl )->item( 0 ); if ( ! $head && $rows && $rows->length ) { $first = $rows->item( 0 ); $head = $first && 0.0 === $xp->evaluate( 'count(./td)', $first ) && $xp->evaluate( 'count(./th)', $first ) > 0 ? $first : null; } if ( ! $head || ! $rows ) { return $table; } $labels = array(); foreach ( $xp->query( './th | ./td', $head ) as $cell ) { if ( ! $cell instanceof DOMElement ) { continue; } $text = (string) $xp->evaluate( 'normalize-space(.)', $cell ); $labels = array_merge( $labels, array_fill( 0, max( 1, (int) $cell->getAttribute( 'colspan' ) ), $text ) ); } foreach ( $rows as $tr ) { if ( $tr === $head ) { continue; } $column = 0; foreach ( $xp->query( './th | ./td', $tr ) as $cell ) { if ( ! $cell instanceof DOMElement ) { continue; } if ( isset( $labels[ $column ] ) && '' !== $labels[ $column ] ) { $cell->setAttribute( 'data-label', $labels[ $column ] ); } $column += max( 1, (int) $cell->getAttribute( 'colspan' ) ); } } $html = ''; foreach ( $xp->query( './node()', $box ) as $node ) { $html .= $doc->saveHTML( $node ); } return $html; } /** * The table already sits right inside our frame (content filtered twice). * * @param string $content Content. * @param int $offset Position of the table tag. */ private static function framed( $content, $offset ): bool { return (bool) preg_match( '#
]*>\s*$#', substr( $content, max( 0, $offset - 300 ), min( 300, $offset ) ) ); } /** * Frame around one table, labelled with its caption or "Table". * * @param string $table Table HTML. */ private static function wrap( $table ): string { if ( self::has_class( $table, 'stack' ) ) { $table = self::cell_labels( $table ); } if ( self::has_class( $table, 'sortable' ) ) { wp_enqueue_script_module( 'threece-table-sort' ); } $label = __( 'Table', '3task-toolbar-for-classic-editor' ); if ( preg_match( '#]*>(.*?)#is', $table, $caption ) ) { $text = trim( (string) preg_replace( '#\s+#', ' ', wp_strip_all_tags( $caption[1] ) ) ); $label = '' !== $text ? $text : $label; } return '
' . $table . '
'; } }