Page MenuHomeWickedGov Phorge

Token.php
No OneTemporary

Size
10 KB
Referenced Files
None
Subscribers
None

Token.php

<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Tokens;
use stdClass;
use Wikimedia\Assert\Assert;
use Wikimedia\JsonCodec\JsonCodec;
use Wikimedia\Parsoid\NodeData\DataMw;
use Wikimedia\Parsoid\NodeData\DataParsoid;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Wt2Html\Frame;
/**
* Catch-all class for all token types.
*/
abstract class Token implements \JsonSerializable {
public DataParsoid $dataParsoid;
public ?DataMw $dataMw = null;
/** @var KV[] */
public $attribs;
protected function __construct(
?DataParsoid $dataParsoid, ?DataMw $dataMw
) {
$this->dataParsoid = $dataParsoid ?? new DataParsoid;
$this->dataMw = $dataMw;
}
/**
* @inheritDoc
*/
#[\ReturnTypeWillChange]
abstract public function jsonSerialize();
/**
* Get a name for the token.
* Derived classes can override this.
* @return string
*/
public function getName(): string {
return $this->getType();
}
/**
* Returns a string key for this token
* @return string
*/
public function getType(): string {
$classParts = explode( '\\', get_class( $this ) );
return end( $classParts );
}
/**
* Generic set attribute method.
*
* @param string $name
* Always a string when used this way.
* The more complex form (where the key is a non-string) are found when
* KV objects are constructed in the tokenizer.
* @param string|Token|array<Token|string> $value
* @param ?KVSourceRange $srcOffsets
*/
public function addAttribute(
string $name, $value, ?KVSourceRange $srcOffsets = null
): void {
$this->attribs[] = new KV( $name, $value, $srcOffsets );
}
/**
* Generic set attribute method with support for change detection.
* Set a value and preserve the original wikitext that produced it.
*
* @param string $name
* @param string|Token|array<Token|string> $value
* @param mixed $origValue
*/
public function addNormalizedAttribute( string $name, $value, $origValue ): void {
$this->addAttribute( $name, $value );
$this->setShadowInfo( $name, $value, $origValue );
}
/**
* Generic attribute accessor.
*
* @param string $name
* @return string|Token|array<Token|string>|KV[]|null
*/
public function getAttributeV( string $name ) {
return KV::lookup( $this->attribs, $name );
}
/**
* Generic attribute accessor.
*
* @param string $name
* @return KV|null
*/
public function getAttributeKV( string $name ) {
return KV::lookupKV( $this->attribs, $name );
}
/**
* Generic attribute accessor.
*
* @param string $name
* @return bool
*/
public function hasAttribute( string $name ): bool {
return $this->getAttributeKV( $name ) !== null;
}
/**
* Set an unshadowed attribute.
*
* @param string $name
* @param string|Token|array<Token|string> $value
*/
public function setAttribute( string $name, $value ): void {
// First look for the attribute and change the last match if found.
for ( $i = count( $this->attribs ) - 1; $i >= 0; $i-- ) {
$kv = $this->attribs[$i];
$k = $kv->k;
if ( is_string( $k ) && mb_strtolower( $k ) === $name ) {
$kv->v = $value;
$this->attribs[$i] = $kv;
return;
}
}
// Nothing found, just add the attribute
$this->addAttribute( $name, $value );
}
/**
* Store the original value of an attribute in a token's dataParsoid.
*
* @param string $name
* @param mixed $value
* @param mixed $origValue
*/
public function setShadowInfo( string $name, $value, $origValue ): void {
// Don't shadow if value is the same or the orig is null
if ( $value !== $origValue && $origValue !== null ) {
$this->dataParsoid->a ??= [];
$this->dataParsoid->a[$name] = $value;
$this->dataParsoid->sa ??= [];
$this->dataParsoid->sa[$name] = $origValue;
}
}
/**
* Attribute info accessor for the wikitext serializer. Performs change
* detection and uses unnormalized attribute values if set. Expects the
* context to be set to a token.
*
* @param string $name
* @return array Information about the shadow info attached to this attribute:
* - value: (string|Token|array<Token|string>)
* When modified is false and fromsrc is true, this is always a string.
* - modified: (bool)
* - fromsrc: (bool)
*/
public function getAttributeShadowInfo( string $name ): array {
$curVal = $this->getAttributeV( $name );
// Not the case, continue regular round-trip information.
if ( !property_exists( $this->dataParsoid, 'a' ) ||
!array_key_exists( $name, $this->dataParsoid->a )
) {
return [
"value" => $curVal,
// Mark as modified if a new element
"modified" => $this->dataParsoid->isModified(),
"fromsrc" => false
];
} elseif ( $this->dataParsoid->a[$name] !== $curVal ) {
return [
"value" => $curVal,
"modified" => true,
"fromsrc" => false
];
} elseif ( !property_exists( $this->dataParsoid, 'sa' ) ||
!array_key_exists( $name, $this->dataParsoid->sa )
) {
return [
"value" => $curVal,
"modified" => false,
"fromsrc" => false
];
} else {
return [
"value" => $this->dataParsoid->sa[$name],
"modified" => false,
"fromsrc" => true
];
}
}
/**
* Completely remove all attributes with this name.
*
* @param string $name
*/
public function removeAttribute( string $name ): void {
foreach ( $this->attribs as $i => $kv ) {
if ( mb_strtolower( $kv->k ) === $name ) {
unset( $this->attribs[$i] );
}
}
$this->attribs = array_values( $this->attribs );
}
/**
* Add a space-separated property value.
* These are Parsoid-added attributes, not something present in source.
* So, only a regular ASCII space characters will be used here.
*
* @param string $name The attribute name
* @param string $value The value to add to the attribute
*/
public function addSpaceSeparatedAttribute( string $name, string $value ): void {
$curVal = $this->getAttributeKV( $name );
if ( $curVal !== null ) {
if ( in_array( $value, explode( ' ', $curVal->v ), true ) ) {
// value is already included, nothing to do.
return;
}
// Value was not yet included in the existing attribute, just add
// it separated with a space
$this->setAttribute( $curVal->k, $curVal->v . ' ' . $value );
} else {
// the attribute did not exist at all, just add it
$this->addAttribute( $name, $value );
}
}
/**
* Get the wikitext source of a token.
*
* @param Frame $frame
* @return string
*/
public function getWTSource( Frame $frame ): string {
$tsr = $this->dataParsoid->tsr ?? null;
if ( !( $tsr instanceof SourceRange ) ) {
throw new InvalidTokenException( 'Expected token to have tsr info.' );
}
$srcText = $frame->getSrcText();
Assert::invariant( $tsr->end >= $tsr->start, 'Bad TSR' );
return $tsr->substr( $srcText );
}
/**
* Create key value set from an array
*
* @param array $a
* @return array
*/
private static function kvsFromArray( array $a ): array {
$kvs = [];
foreach ( $a as $e ) {
if ( is_array( $e["k"] ?? null ) ) {
self::rebuildNestedTokens( $e["k"] );
}
$v = $e['v'] ?? null;
if ( is_array( $v ) ) {
// $v is either an array of Tokens or an array of KVs
if ( count( $v ) > 0 ) {
if ( is_array( $v[0] ) && array_key_exists( 'k', $v[0] ) ) {
$v = self::kvsFromArray( $v );
} else {
self::rebuildNestedTokens( $v );
}
}
}
$so = $e["srcOffsets"] ?? null;
if ( $so ) {
$so = KVSourceRange::newFromJsonArray( $so );
}
$kvs[] = new KV(
$e["k"] ?? null,
$v,
$so,
$e["ksrc"] ?? null,
$e["vsrc"] ?? null
);
}
return $kvs;
}
/**
* @param iterable|stdClass|DataParsoid &$a
*/
private static function rebuildNestedTokens( &$a ): void {
// objects do not count as iterables in PHP but can be iterated nevertheless
// @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
foreach ( $a as &$v ) {
$v = self::getToken( $v );
}
unset( $v ); // Future-proof protection
}
/**
* Get a token from some PHP structure. Used by the PHPUnit tests.
*
* @param KV|Token|array|string|int|float|bool|null $input
* @return Token|string|int|float|bool|null|array<Token|string|int|float|bool|null>
*/
public static function getToken( $input ) {
if ( !$input ) {
return $input;
}
if ( is_array( $input ) && isset( $input['type'] ) ) {
$codec = new JsonCodec();
if ( isset( $input['dataParsoid'] ) ) {
$da = $codec->newFromJsonArray(
$input['dataParsoid'],
DOMDataUtils::getCodecHints()['data-parsoid']
);
} else {
$da = null;
}
if ( isset( $input['dataMw'] ) ) {
$dmw = $codec->newFromJsonArray(
$input['dataMw'],
DOMDataUtils::getCodecHints()['data-mw']
);
} else {
$dmw = null;
}
// In theory this should be refactored to use JsonCodecable
// and remove the ad-hoc deserialization code here.
switch ( $input['type'] ) {
case "SelfclosingTagTk":
$token = new SelfclosingTagTk( $input['name'], self::kvsFromArray( $input['attribs'] ), $da, $dmw );
break;
case "TagTk":
$token = new TagTk( $input['name'], self::kvsFromArray( $input['attribs'] ), $da, $dmw );
break;
case "EndTagTk":
$token = new EndTagTk( $input['name'], self::kvsFromArray( $input['attribs'] ), $da, $dmw );
break;
case "NlTk":
$token = new NlTk( $da->tsr ?? null, $da, $dmw );
break;
case "EOFTk":
$token = new EOFTk();
break;
case "CommentTk":
$token = new CommentTk( $input["value"], $da, $dmw );
break;
default:
// Looks like data-parsoid can have a 'type' property in some cases
// We can change that usage and then throw an exception here
$token = &$input;
}
} elseif ( is_array( $input ) ) {
$token = &$input;
} else {
$token = $input;
}
if ( is_array( $token ) ) {
self::rebuildNestedTokens( $token );
} elseif ( $token instanceof Token ) {
if ( !empty( $token->attribs ) ) {
self::rebuildNestedTokens( $token->attribs );
}
self::rebuildNestedTokens( $token->dataParsoid );
}
return $token;
}
public function fetchExpandedAttrValue( string $key ): ?string {
if ( preg_match(
'/mw:ExpandedAttrs/', $this->getAttributeV( 'typeof' ) ?? ''
) ) {
$dmw = $this->dataMw;
if ( !isset( $dmw->attribs ) ) {
return null;
}
foreach ( $dmw->attribs as $attr ) {
if ( ( $attr->key['txt'] ?? null ) === $key ) {
return $attr->value['html'] ?? null;
}
}
}
return null;
}
}

File Metadata

Mime Type
text/x-php
Expires
Sat, Oct 3, 04:21 (3 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
74/25/b5797d2d4f8f8a6e791c0fa66028
Default Alt Text
Token.php (10 KB)

Event Timeline