Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 124
Gallery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 8
1892
0.00% covered (danger)
0.00%
0 / 124
 getConfig
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 pCaption
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 pLine
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 38
 sourceToDom
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 18
 contentHandler
0.00% covered (danger)
0.00%
0 / 1
462
0.00% covered (danger)
0.00%
0 / 47
 domToWikitext
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 14
 modifyArgDict
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 diffHandler
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Ext\Gallery;
use stdClass;
use Wikimedia\Parsoid\Core\MediaStructure;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Ext\DOMDataUtils;
use Wikimedia\Parsoid\Ext\DOMUtils;
use Wikimedia\Parsoid\Ext\ExtensionModule;
use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Utils\WTUtils;
/**
 * Implements the php parser's `renderImageGallery` natively.
 *
 * Params to support (on the extension tag):
 * - showfilename
 * - caption
 * - mode
 * - widths
 * - heights
 * - perrow
 *
 * A proposed spec is at: https://phabricator.wikimedia.org/P2506
 */
class Gallery extends ExtensionTagHandler implements ExtensionModule {
    /** @inheritDoc */
    public function getConfig(): array {
        return [
            'name' => 'Gallery',
            'tags' => [
                [
                    'name' => 'gallery',
                    'handler' => self::class,
                ]
            ],
        ];
    }
    /**
     * Parse the gallery caption.
     * @param ParsoidExtensionAPI $extApi
     * @param array $extArgs
     * @return ?DocumentFragment
     */
    private function pCaption(
        ParsoidExtensionAPI $extApi, array $extArgs
    ): ?DocumentFragment {
        return $extApi->extArgToDOM( $extArgs, 'caption' );
    }
    /**
     * Parse a single line of the gallery.
     * @param ParsoidExtensionAPI $extApi
     * @param string $line
     * @param int $lineStartOffset
     * @param Opts $opts
     * @return ParsedLine|null
     */
    private static function pLine(
        ParsoidExtensionAPI $extApi, string $line, int $lineStartOffset,
        Opts $opts
    ): ?ParsedLine {
        // Regexp from php's `renderImageGallery`
        if ( !preg_match( '/^([^|]+)(\|(?:.*))?$/D', $line, $matches ) ) {
            return null;
        }
        $titleStr = $matches[1];
        $imageOptStr = $matches[2] ?? '';
        // TODO: % indicates rawurldecode.
        $mode = Mode::byName( $opts->mode );
        $imageOpts = [
            "|{$mode->dimensions( $opts )}",
            [ $imageOptStr, $lineStartOffset + strlen( $titleStr ) ],
        ];
        $thumb = $extApi->renderMedia(
            $titleStr, $imageOpts, $error,
            // Force block for an easier structure to manipulate, otherwise
            // we have to pull the caption out of the data-mw
            true
        );
        if ( !$thumb || DOMCompat::nodeName( $thumb ) !== 'figure' ) {
            return null;
        }
        $doc = $thumb->ownerDocument;
        $rdfaType = $thumb->getAttribute( 'typeof' ) ?? '';
        // T214601: Account for a format being set in $imageOptStr
        $rdfaType = preg_replace( '#mw:File(/\w+)?\b#', 'mw:File', $rdfaType, 1 );
        // Detach figcaption as well
        $figcaption = DOMCompat::querySelector( $thumb, 'figcaption' );
        DOMCompat::remove( $figcaption );
        if ( $opts->showfilename ) {
            // No need for error checking on this call since it was already
            // done in $extApi->renderMedia() above
            $title = $extApi->makeTitle(
                $titleStr,
                $extApi->getSiteConfig()->canonicalNamespaceId( 'file' )
            );
            $file = $title->getPrefixedDBKey();
            $galleryfilename = $doc->createElement( 'a' );
            $galleryfilename->setAttribute( 'href', $extApi->getTitleUri( $title ) );
            $galleryfilename->setAttribute( 'class', 'galleryfilename galleryfilename-truncate' );
            $galleryfilename->setAttribute( 'title', $file );
            $galleryfilename->appendChild( $doc->createTextNode( $file ) );
            $figcaption->insertBefore( $galleryfilename, $figcaption->firstChild );
        }
        $gallerytext = null;
        for ( $capChild = $figcaption->firstChild;
             $capChild !== null;
             $capChild = $capChild->nextSibling ) {
            if (
                $capChild instanceof Text &&
                preg_match( '/^\s*$/D', $capChild->nodeValue )
            ) {
                // skip blank text nodes
                continue;
            }
            // Found a non-blank node!
            $gallerytext = $figcaption;
            break;
        }
        return new ParsedLine( $thumb, $gallerytext, $rdfaType );
    }
    /** @inheritDoc */
    public function sourceToDom(
        ParsoidExtensionAPI $extApi, string $content, array $args
    ): DocumentFragment {
        $attrs = $extApi->extArgsToArray( $args );
        $opts = new Opts( $extApi, $attrs );
        $offset = $extApi->extTag->getOffsets()->innerStart();
        // Prepare the lines for processing
        $lines = explode( "\n", $content );
        $lines = array_map( static function ( $line ) use ( &$offset ) {
                $lineObj = [ 'line' => $line, 'offset' => $offset ];
                $offset += strlen( $line ) + 1; // For the nl
                return $lineObj;
        }, $lines );
        $caption = $opts->caption ? $this->pCaption( $extApi, $args ) : null;
        $lines = array_map( function ( $lineObj ) use ( $extApi, $opts ) {
            return $this->pLine(
                $extApi, $lineObj['line'], $lineObj['offset'], $opts
            );
        }, $lines );
        // Drop invalid lines like "References: 5."
        $lines = array_filter( $lines, static function ( $lineObj ) {
            return $lineObj !== null;
        } );
        $mode = Mode::byName( $opts->mode );
        $extApi->addModules( $mode->getModules() );
        $extApi->addModuleStyles( $mode->getModuleStyles() );
        return $mode->render( $extApi, $opts, $caption, $lines );
    }
    /**
     * @param ParsoidExtensionAPI $extApi
     * @param Element $node
     * @return string
     */
    private function contentHandler(
        ParsoidExtensionAPI $extApi, Element $node
    ): string {
        $content = "\n";
        for ( $child = $node->firstChild; $child; $child = $child->nextSibling ) {
            switch ( $child->nodeType ) {
            case XML_ELEMENT_NODE:
                DOMUtils::assertElt( $child );
                // Ignore if it isn't a "gallerybox"
                if (
                    DOMCompat::nodeName( $child ) !== 'li' ||
                    $child->getAttribute( 'class' ) !== 'gallerybox'
                ) {
                    break;
                }
                $thumb = DOMCompat::querySelector( $child, '.thumb' );
                if ( !$thumb ) {
                    break;
                }
                $gallerytext = DOMCompat::querySelector( $child, '.gallerytext' );
                if ( $gallerytext ) {
                    $showfilename = DOMCompat::querySelector( $gallerytext, '.galleryfilename' );
                    if ( $showfilename ) {
                        DOMCompat::remove( $showfilename ); // Destructive to the DOM!
                    }
                }
                $ms = MediaStructure::parse( DOMUtils::firstNonSepChild( $thumb ) );
                if ( $ms ) {
                    // FIXME: Dry all this out with T252246 / T262833
                    if ( $ms->hasResource() ) {
                        $resource = $ms->getResource();
                        $content .= PHPUtils::stripPrefix( $resource, './' );
                        // FIXME: Serializing of these attributes should
                        // match the link handler so that values stashed in
                        // data-mw aren't ignored.
                        if ( $ms->hasAlt() ) {
                            $altOnElt = trim( $ms->getAlt() );
                            $altFromCaption = $gallerytext ?
                                trim( WTUtils::textContentFromCaption( $gallerytext ) ) : '';
                            // The first condition is to support an empty \alt=\ option
                            // when no caption is present
                            if ( !$altOnElt || ( $altOnElt !== $altFromCaption ) ) {
                                $content .= '|alt=' .
                                    $extApi->escapeWikitext( $altOnElt, $child, $extApi::IN_MEDIA );
                            }
                        }
                        // FIXME: Handle missing media
                        if ( $ms->hasMediaUrl() && !$ms->isRedLink() ) {
                            $href = $ms->getMediaUrl();
                            if ( $href !== $resource ) {
                                $href = PHPUtils::stripPrefix( $href, './' );
                                $content .= '|link=' .
                                    $extApi->escapeWikitext( $href, $child, $extApi::IN_MEDIA );
                            }
                        }
                    }
                } else {
                    // TODO: Previously (<=1.5.0), we rendered valid titles
                    // returning mw:Error (apierror-filedoesnotexist) as
                    // plaintext.  Continue to serialize this content until
                    // that version is no longer supported.
                    $content .= $thumb->textContent;
                }
                if ( $gallerytext ) {
                    $caption = $extApi->domChildrenToWikitext(
                        $gallerytext, $extApi::IN_IMG_CAPTION
                    );
                    // Drop empty captions
                    if ( !preg_match( '/^\s*$/D', $caption ) ) {
                        // Ensure that this only takes one line since gallery
                        // tag content is split by line
                        $caption = str_replace( "\n", ' ', $caption );
                        $content .= '|' . $caption;
                    }
                }
                $content .= "\n";
                break;
            case XML_TEXT_NODE:
            case XML_COMMENT_NODE:
                // Ignore it
                break;
            default:
                PHPUtils::unreachable( 'should not be here!' );
                break;
            }
        }
        return $content;
    }
    /** @inheritDoc */
    public function domToWikitext(
        ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified
    ) {
        $dataMw = DOMDataUtils::getDataMw( $node );
        $dataMw->attrs = $dataMw->attrs ?? new stdClass;
        // Handle the "gallerycaption" first
        $galcaption = DOMCompat::querySelector( $node, 'li.gallerycaption' );
        if (
            $galcaption &&
            // FIXME: VE should signal to use the HTML by removing the
            // `caption` from data-mw.
            !is_string( $dataMw->attrs->caption ?? null )
        ) {
            $dataMw->attrs->caption = $extApi->domChildrenToWikitext(
                $galcaption, $extApi::IN_IMG_CAPTION | $extApi::IN_OPTION
            );
        }
        $startTagSrc = $extApi->extStartTagToWikitext( $node );
        if ( !isset( $dataMw->body ) ) {
            return $startTagSrc; // We self-closed this already.
        } else {
            // FIXME: VE should signal to use the HTML by removing the
            // `extsrc` from the data-mw.
            if ( is_string( $dataMw->body->extsrc ?? null ) ) {
                $content = $dataMw->body->extsrc;
            } else {
                $content = $this->contentHandler( $extApi, $node );
            }
            return $startTagSrc . $content . '</' . $dataMw->name . '>';
        }
    }
    /** @inheritDoc */
    public function modifyArgDict(
        ParsoidExtensionAPI $extApi, object $argDict
    ): void {
        // FIXME: Only remove after VE switches to editing HTML.
        if ( $extApi->getSiteConfig()->nativeGalleryEnabled() ) {
            // Remove extsrc from native extensions
            unset( $argDict->body->extsrc );
            // Remove the caption since it's redundant with the HTML
            // and we prefer editing it there.
            unset( $argDict->attrs->caption );
        }
    }
    /** @inheritDoc */
    public function diffHandler(
        ParsoidExtensionAPI $extApi, callable $domDiff, Element $origNode,
        Element $editedNode
    ): bool {
        return call_user_func( $domDiff, $origNode, $editedNode );
    }
}