<?php
/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2021 The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser;
class Tag
{
/**
* Tag type: start tag
*/
const START_TAG = 1;
/**
* Tag type: end tag
*/
const END_TAG = 2;
/**
* Tag type: self-closing tag
*/
const SELF_CLOSING_TAG = self::START_TAG | self::END_TAG;
/**
* @var array Dictionary of attributes
*/
protected $attributes = [];
/**
* @var array List of tags that are invalidated when this tag is invalidated
*/
protected $cascade = [];
/**
* @var Tag End tag that unconditionally ends this start tag
*/
protected $endTag = null;
/**
* @var integer Bitfield of boolean rules that apply to this tag
*/
protected $flags = 0;
/**
* @var bool Whether this tag is be invalid
*/
protected $invalid = false;
/**
* @var integer Length of text consumed by this tag
*/
protected $len;
/**
* @var string Name of this tag
*/
protected $name;
/**
* @var integer Position of this tag in the text
*/
protected $pos;
/**
* @var integer Tiebreaker used when sorting identical tags
*/
protected $sortPriority;
/**
* @var Tag Start tag that is unconditionally closed this end tag
*/
protected $startTag = null;
/**
* @var integer Tag type
*/
protected $type;
/**
* Constructor
*
* @param integer $type Tag's type
* @param string $name Name of the tag
* @param integer $pos Position of the tag in the text
* @param integer $len Length of text consumed by the tag
* @param integer $priority This tag's sorting tiebreaker
*/
public function __construct($type, $name, $pos, $len, $priority = 0)
{
$this->type = (int) $type;
$this->name = $name;
$this->pos = (int) $pos;
$this->len = (int) $len;
$this->sortPriority = (int) $priority;
}
//==========================================================================
// Actions
//==========================================================================
/**
* Add a set of flags to this tag's
*
* @param integer $flags
* @return void
*/
public function addFlags($flags)
{
$this->flags |= $flags;
}
/**
* Set given tag to be invalidated if this tag is invalidated
*
* @param Tag $tag
* @return void
*/
public function cascadeInvalidationTo(Tag $tag)
{
$this->cascade[] = $tag;
// If this tag is already invalid, cascade it now
if ($this->invalid)
{
$tag->invalidate();
}
}
/**
* Invalidate this tag, as well as tags bound to this tag
*
* @return void
*/
public function invalidate()
{
// Only invalidate if this tag is valid to prevent infinite loops
if (!$this->invalid)
{
$this->invalid = true;
foreach ($this->cascade as $tag)
{
$tag->invalidate();
}
}
}
/**
* Pair this tag with given tag
*
* @param Tag $tag
* @return void
*/
public function pairWith(Tag $tag)
{
if ($this->canBePaired($this, $tag))
{
$this->endTag = $tag;
$tag->startTag = $this;
$this->cascadeInvalidationTo($tag);
}
elseif ($this->canBePaired($tag, $this))
{
$this->startTag = $tag;
$tag->endTag = $this;
}
}
/**
* Test whether two tags can be paired
*/
protected function canBePaired(Tag $startTag, Tag $endTag): bool
{
return $startTag->name === $endTag->name && $startTag->type === self::START_TAG && $endTag->type === self::END_TAG && $startTag->pos <= $endTag->pos;
}
/**
* Remove a set of flags from this tag's
*
* @param integer $flags
* @return void
*/
public function removeFlags($flags)
{
$this->flags &= ~$flags;
}
/**
* Set the bitfield of boolean rules that apply to this tag
*
* @param integer $flags Bitfield of boolean rules that apply to this tag
* @return void
*/
public function setFlags($flags)
{
$this->flags = $flags;
}
//==========================================================================
// Getters
//==========================================================================
/**
* Return this tag's attributes
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Return this tag's end tag
*
* @return Tag|null This tag's end tag, or NULL if none is set
*/
public function getEndTag()
{
return $this->endTag;
}
/**
* Return the bitfield of boolean rules that apply to this tag
*
* @return integer
*/
public function getFlags()
{
return $this->flags;
}
/**
* Return the length of text consumed by this tag
*
* @return integer
*/
public function getLen()
{
return $this->len;
}
/**
* Return this tag's name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Return this tag's position
*
* @return integer
*/
public function getPos()
{
return $this->pos;
}
/**
* Return this tag's tiebreaker
*
* @return integer
*/
public function getSortPriority()
{
return $this->sortPriority;
}
/**
* Return this tag's start tag
*
* @return Tag|null This tag's start tag, or NULL if none is set
*/
public function getStartTag()
{
return $this->startTag;
}
/**
* Return this tag's type
*
* @return integer
*/
public function getType()
{
return $this->type;
}
//==========================================================================
// Tag's status
//==========================================================================
/**
* Test whether this tag can close given start tag
*
* @param Tag $startTag A start tag
* @return bool
*/
public function canClose(Tag $startTag)
{
if ($this->invalid
|| !$this->canBePaired($startTag, $this)
|| ($this->startTag && $this->startTag !== $startTag)
|| ($startTag->endTag && $startTag->endTag !== $this))
{
return false;
}
return true;
}
/**
* Test whether this tag is a br tag
*
* @return bool
*/
public function isBrTag()
{
return ($this->name === 'br');
}
/**
* Test whether this tag is an end tag (self-closing tags inclusive)
*
* @return bool
*/
public function isEndTag()
{
return (bool) ($this->type & self::END_TAG);
}
/**
* Test whether this tag is an ignore tag
*
* @return bool
*/
public function isIgnoreTag()
{
return ($this->name === 'i');
}
/**
* Test whether this tag is invalid
*
* @return bool
*/
public function isInvalid()
{
return $this->invalid;
}
/**
* Test whether this tag represents a paragraph break
*
* @return bool
*/
public function isParagraphBreak()
{
return ($this->name === 'pb');
}
/**
* Test whether this tag is a self-closing tag
*
* @return bool
*/
public function isSelfClosingTag()
{
return ($this->type === self::SELF_CLOSING_TAG);
}
/**
* Test whether this tag is a special tag: "br", "i", "pb" or "v"
*
* @return bool
*/
public function isSystemTag()
{
return (strpos('br i pb v', $this->name) !== false);
}
/**
* Test whether this tag is a start tag (self-closing tags inclusive)
*
* @return bool
*/
public function isStartTag()
{
return (bool) ($this->type & self::START_TAG);
}
/**
* Test whether this tag represents verbatim text
*
* @return bool
*/
public function isVerbatim()
{
return ($this->name === 'v');
}
//==========================================================================
// Attributes handling
//==========================================================================
/**
* Return the value of given attribute
*
* @param string $attrName
* @return mixed
*/
public function getAttribute($attrName)
{
return $this->attributes[$attrName];
}
/**
* Return whether given attribute is set
*
* @param string $attrName
* @return bool
*/
public function hasAttribute($attrName)
{
return isset($this->attributes[$attrName]);
}
/**
* Remove given attribute
*
* @param string $attrName
* @return void
*/
public function removeAttribute($attrName)
{
unset($this->attributes[$attrName]);
}
/**
* Set the value of an attribute
*
* @param string $attrName Attribute's name
* @param string $attrValue Attribute's value
* @return void
*/
public function setAttribute($attrName, $attrValue)
{
$this->attributes[$attrName] = $attrValue;
}
/**
* Set all of this tag's attributes at once
*
* @param array $attributes
* @return void
*/
public function setAttributes(array $attributes)
{
$this->attributes = $attributes;
}
}