Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 151 |
ImageMap | |
0.00% |
0 / 1 |
|
0.00% |
0 / 3 |
2352 | |
0.00% |
0 / 151 |
getConfig | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
sourceToDom | |
0.00% |
0 / 1 |
1640 | |
0.00% |
0 / 140 |
|||
tokenizeCoords | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 10 |
<?php | |
declare( strict_types = 1 ); | |
namespace Wikimedia\Parsoid\Ext\ImageMap; | |
use Wikimedia\Parsoid\DOM\DocumentFragment; | |
use Wikimedia\Parsoid\Ext\DOMDataUtils; | |
use Wikimedia\Parsoid\Ext\DOMUtils; | |
use Wikimedia\Parsoid\Ext\ExtensionError; | |
use Wikimedia\Parsoid\Ext\ExtensionModule; | |
use Wikimedia\Parsoid\Ext\ExtensionTagHandler; | |
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; | |
use Wikimedia\Parsoid\Ext\WTUtils; | |
use Wikimedia\Parsoid\Utils\DOMCompat; | |
/** | |
* This is an adaptation of the existing ImageMap extension of the legacy | |
* parser. | |
* | |
* Syntax: | |
* <imagemap> | |
* Image:Foo.jpg | 100px | picture of a foo | |
* | |
* rect 0 0 50 50 [[Foo type A]] | |
* circle 50 50 20 [[Foo type B]] | |
* | |
* desc bottom-left | |
* </imagemap> | |
* | |
* Coordinates are relative to the source image, not the thumbnail. | |
*/ | |
class ImageMap extends ExtensionTagHandler implements ExtensionModule { | |
private const TOP_RIGHT = 0; | |
private const BOTTOM_RIGHT = 1; | |
private const BOTTOM_LEFT = 2; | |
private const TOP_LEFT = 3; | |
private const NONE = 4; | |
/** @inheritDoc */ | |
public function getConfig(): array { | |
return [ | |
'name' => 'ImageMap', | |
'tags' => [ | |
[ | |
'name' => 'imagemap', | |
'handler' => self::class, | |
] | |
] | |
]; | |
} | |
/** @inheritDoc */ | |
public function sourceToDom( | |
ParsoidExtensionAPI $extApi, string $src, array $extArgs | |
): DocumentFragment { | |
$domFragment = $extApi->getTopLevelDoc()->createDocumentFragment(); | |
$thumb = null; | |
$anchor = null; | |
$imageNode = null; | |
$mapHTML = null; | |
// Define canonical desc types to allow i18n of 'imagemap_desc_types' | |
$descTypesCanonical = 'top-right, bottom-right, bottom-left, top-left, none'; | |
$descType = self::BOTTOM_RIGHT; | |
$scale = 1; | |
$lineNum = 0; | |
$first = true; | |
$defaultLinkAttribs = null; | |
$nextOffset = $extApi->extTag->getOffsets()->innerStart(); | |
$lines = explode( "\n", $src ); | |
foreach ( $lines as $line ) { | |
++$lineNum; | |
$offset = $nextOffset; | |
$nextOffset = $offset + strlen( $line ) + 1; // For the nl | |
$offset += strlen( $line ) - strlen( ltrim( $line ) ); | |
$line = trim( $line ); | |
if ( $line == '' || $line[0] == '#' ) { | |
continue; | |
} | |
if ( $first ) { | |
$first = false; | |
// The first line should have an image specification on it | |
// Extract it and render the HTML | |
$bits = explode( '|', $line, 2 ); | |
if ( count( $bits ) == 1 ) { | |
$image = $bits[0]; | |
$options = ''; | |
} else { | |
list( $image, $options ) = $bits; | |
$options = '|' . $options; | |
} | |
$imageOpts = [ | |
[ $options, $offset + strlen( $image ) ], | |
]; | |
$thumb = $extApi->renderMedia( | |
$image, $imageOpts, $error, | |
// NOTE(T290044): Imagemaps are always rendered as blocks | |
true | |
); | |
if ( !$thumb ) { | |
throw new ExtensionError( $error ); | |
} | |
$anchor = $thumb->firstChild; | |
$imageNode = $anchor->firstChild; | |
// Could be a span | |
if ( DOMCompat::nodeName( $imageNode ) !== 'img' ) { | |
throw new ExtensionError( 'imagemap_invalid_image' ); | |
} | |
DOMUtils::assertElt( $imageNode ); | |
// Add the linear dimensions to avoid inaccuracy in the scale | |
// factor when one is much larger than the other | |
// (sx+sy)/(x+y) = s | |
$thumbWidth = (int)( $imageNode->getAttribute( 'width' ) ?? '' ); | |
$thumbHeight = (int)( $imageNode->getAttribute( 'height' ) ?? '' ); | |
$imageWidth = (int)( $imageNode->getAttribute( 'data-file-width' ) ?? '' ); | |
$imageHeight = (int)( $imageNode->getAttribute( 'data-file-height' ) ?? '' ); | |
$denominator = $imageWidth + $imageHeight; | |
$numerator = $thumbWidth + $thumbHeight; | |
if ( $denominator <= 0 || $numerator <= 0 ) { | |
throw new ExtensionError( 'imagemap_invalid_image' ); | |
} | |
$scale = $numerator / $denominator; | |
continue; | |
} | |
// Handle desc spec | |
$cmd = strtok( $line, " \t" ); | |
if ( $cmd == 'desc' ) { | |
$typesText = $descTypesCanonical; | |
// FIXME: Support this ... | |
// $typesText = wfMessage( 'imagemap_desc_types' )->inContentLanguage()->text(); | |
// if ( $descTypesCanonical != $typesText ) { | |
// // i18n desc types exists | |
// $typesText = $descTypesCanonical . ', ' . $typesText; | |
// } | |
$types = array_map( 'trim', explode( ',', $typesText ) ); | |
$type = trim( strtok( '' ) ?: '' ); | |
$descType = array_search( $type, $types, true ); | |
if ( $descType > 4 ) { | |
// A localized descType is used. Subtract 5 to reach the canonical desc type. | |
$descType -= 5; | |
} | |
// <0? In theory never, but paranoia... | |
if ( $descType === false || $descType < 0 ) { | |
throw new ExtensionError( 'imagemap_invalid_desc', $typesText ); | |
} | |
continue; | |
} | |
// Find the link | |
$link = trim( strstr( $line, '[' ) ?: '' ); | |
if ( !$link ) { | |
throw new ExtensionError( 'imagemap_no_link', $lineNum ); | |
} | |
// FIXME: Omits DSR offsets, which will be more relevant when VE | |
// supports HTML editing of maps. | |
$linkFragment = $extApi->wikitextToDOM( | |
$link, | |
[ | |
'parseOpts' => [ | |
'extTag' => 'imagemap', | |
'context' => 'inline', | |
], | |
// Create new frame, because $link doesn't literally | |
// appear on the page, it has been hand-crafted here | |
'processInNewFrame' => true | |
], | |
true // sol | |
); | |
$a = DOMCompat::querySelector( $linkFragment, 'a' ); | |
if ( $a == null ) { | |
// Meh, might be for other reasons | |
throw new ExtensionError( 'imagemap_invalid_title', $lineNum ); | |
} | |
DOMUtils::assertElt( $a ); | |
$href = $a->getAttribute( 'href' ) ?? ''; | |
$externLink = str_starts_with( $a->getAttribute( 'rel' ) ?? '', "mw:ExtLink/" ); | |
$alt = ''; | |
$hasContent = $externLink || ( DOMDataUtils::getDataParsoid( $a )->stx ?? null ) === 'piped'; | |
if ( $hasContent ) { | |
// FIXME: The legacy extension does ad hoc link parsing, which | |
// results in link content not interpreting wikitext syntax. | |
// Here we produce a known difference by just taking the text | |
// content of the resulting dom. | |
// See the test, "Link with wikitext syntax in content" | |
$alt = trim( $a->textContent ); | |
} | |
$shapeSpec = substr( $line, 0, -strlen( $link ) ); | |
// Tokenize shape spec | |
$shape = strtok( $shapeSpec, " \t" ); | |
switch ( $shape ) { | |
case 'default': | |
$coords = []; | |
break; | |
case 'rect': | |
$coords = self::tokenizeCoords( $lineNum, 4 ); | |
break; | |
case 'circle': | |
$coords = self::tokenizeCoords( $lineNum, 3 ); | |
break; | |
case 'poly': | |
$coords = self::tokenizeCoords( $lineNum, 1, true ); | |
if ( count( $coords ) % 2 !== 0 ) { | |
throw new ExtensionError( 'imagemap_poly_odd', $lineNum ); | |
} | |
break; | |
default: | |
$coords = []; | |
throw new ExtensionError( 'imagemap_unrecognised_shape', $lineNum ); | |
} | |
// Scale the coords using the size of the source image | |
foreach ( $coords as $i => $c ) { | |
$coords[$i] = (int)round( $c * $scale ); | |
} | |
// Construct the area tag | |
$attribs = [ 'href' => $href ]; | |
if ( $externLink ) { | |
$attribs['class'] = 'plainlinks'; | |
// FIXME: T186241 | |
// if ( $wgNoFollowLinks ) { | |
// $attribs['rel'] = 'nofollow'; | |
// } | |
} | |
if ( $shape != 'default' ) { | |
$attribs['shape'] = $shape; | |
} | |
if ( $coords ) { | |
$attribs['coords'] = implode( ',', $coords ); | |
} | |
if ( $alt != '' ) { | |
if ( $shape != 'default' ) { | |
$attribs['alt'] = $alt; | |
} | |
$attribs['title'] = $alt; | |
} | |
if ( $shape == 'default' ) { | |
$defaultLinkAttribs = $attribs; | |
} else { | |
if ( $mapHTML == null ) { | |
$mapHTML = $domFragment->ownerDocument->createElement( 'map' ); | |
} | |
$area = $domFragment->ownerDocument->createElement( 'area' ); | |
foreach ( $attribs as $key => $val ) { | |
$area->setAttribute( $key, $val ); | |
} | |
$mapHTML->appendChild( $area ); | |
} | |
} | |
if ( $first ) { | |
throw new ExtensionError( 'imagemap_no_image' ); | |
} | |
if ( $mapHTML != null ) { | |
// Construct the map | |
// Add a hash of the map HTML to avoid breaking cached HTML fragments that are | |
// later joined together on the one page (T18471). | |
// The only way these hashes can clash is if the map is identical, in which | |
// case it wouldn't matter that the "wrong" map was used. | |
$mapName = 'ImageMap_' . substr( md5( DOMCompat::getInnerHTML( $mapHTML ) ), 0, 16 ); | |
$mapHTML->setAttribute( 'name', $mapName ); | |
// Alter the image tag | |
$imageNode->setAttribute( 'usemap', "#$mapName" ); | |
$thumb->insertBefore( $mapHTML, $imageNode->parentNode->nextSibling ); | |
} | |
// Determine whether a "magnify" link is present | |
// FIXME: Find a css way to achieving this | |
if ( $defaultLinkAttribs ) { | |
$defaultAnchor = $domFragment->ownerDocument->createElement( 'a' ); | |
foreach ( $defaultLinkAttribs as $name => $value ) { | |
$defaultAnchor->setAttribute( $name, $value ); | |
} | |
} else { | |
$defaultAnchor = $domFragment->ownerDocument->createElement( 'span' ); | |
} | |
$defaultAnchor->appendChild( $imageNode ); | |
$thumb->replaceChild( $defaultAnchor, $anchor ); | |
if ( !WTUtils::hasVisibleCaption( $thumb ) ) { | |
$caption = DOMCompat::querySelector( $thumb, 'figcaption' ); | |
$captionText = trim( $caption->textContent ); | |
if ( $captionText ) { | |
$defaultAnchor->setAttribute( 'title', $captionText ); | |
} | |
} | |
// For T22030 | |
DOMCompat::getClassList( $thumb )->add( 'noresize' ); | |
$domFragment->appendChild( $thumb ); | |
return $domFragment; | |
} | |
/** | |
* @param int $lineNum Line number, for error reporting | |
* @param int $minCount Minimum token count | |
* @param bool $allowNegative | |
* @return array Array of coordinates | |
* @throws ExtensionError | |
*/ | |
private static function tokenizeCoords( | |
int $lineNum, int $minCount = 0, $allowNegative = false | |
) { | |
$coords = []; | |
$coord = strtok( " \t" ); | |
while ( $coord !== false ) { | |
if ( !is_numeric( $coord ) || $coord > 1e9 || ( !$allowNegative && $coord < 0 ) ) { | |
throw new ExtensionError( 'imagemap_invalid_coord', $lineNum ); | |
} | |
$coords[] = $coord; | |
$coord = strtok( " \t" ); | |
} | |
if ( count( $coords ) < $minCount ) { | |
// TODO: Should this also check there aren't too many coords? | |
throw new ExtensionError( 'imagemap_missing_coord', $lineNum ); | |
} | |
return $coords; | |
} | |
} |