OO-tokenizer implementatie
Crosspost van PHPFreakz.nl: Ik zit redelijk vaak te werken met de tokenizer van PHP (token_get_all, token_name), en ik erger me steeds mateloos aan dat ik constant moet kijken of het een array is, of het dit of dat is, enzovoorts. Ik heb dus een simpele uitwerking hiervan gemaakt, compleet Objectgeoriënteerd, zodat ik het ook makkelijk kan gebruiken in mijn eigen kleine projectjes (denk aan een shortifier of beautifier). Ik heb een (gigantische) uitwerking van de highlighting in PHP erbij gemaakt als voorbeeld, waaruit dus blijkt wat de kracht hiervan is. De uitvoer daarvan is overigens XHTML Strict-valid, dus mocht je het willen gebruiken (ik raad het af i.v.m. performance, maar het werkt) zal dat dus geen problemen opleveren. Anyway, genoeg informatie, uitleg staat grotendeels in de code, vragen/complimenten/opmerkingen (graag)/verbeterpunten (nog liever)/hatemail mag natuurlijk altijd.
[code]<?php
// deze gebruiken we om dingen als !, (, ), ;, etc. aan te geven
if(!defined('T_SYMBOL')) {
/**
* @ignore
*/
define('T_SYMBOL', 0);
}
error_reporting(E_ALL);
/**
* Standaardexceptie voor Tokenizer
*
* Het is zo dus makkelijker om excepties van deze module af te vangen
*
* @package Tokenizer
*/
class Tokenizer_Exception extends Exception { }
/**
* De tokenizer klasse
*
* Gebruik:
* <pre>$tokenizer = new Tokenizer('<?php echo $variabele; ?>', Tokenizer::INPUT_STRING);
* // of
* $tokenizer = new Tokenizer('/pad/naar/bestand.php', Tokenizer::INPUT_FILE);
*
* while($token = $tokenizer->getToken()) {
* echo $token->getName . ' - "' . $token->getContents() . '"<br />';
* }</pre>
*
* @package Tokenizer
*/
class Tokenizer {
/**
* Input is een bestandsnaam
*/
const INPUT_FILE = 1;
/**
* Input is een string met PHP-code
*/
const INPUT_STRING = 2;
/**
* Bevat een array van {@link Token}s
*
* @var array
*/
private $_tokens = array();
/**
* De PHP-code, ofwel direct ingegeven, ofwel uit een bestand gelezen
*
* @var string
*/
private $_input = '';
/**
* Het aantal tokens dat $this->_tokens bevat
*
* @var integer
*/
private $_count = 0;
/**
* De pointer naar de huidige index in $this->_tokens
*
* @var integer
*/
private $_pointer = 0;
/**
* De klasse die ieder token representeert
*
* @var string
*/
private $_tokenClass = 'Token';
/**
* Initialiseer de tokenizer.
*
* Als het een bestand is, lees het bestand in.
* Daarna wordt de string geparsed met token_get_all(),
* en is het klaar voor gebruik
*
* @param string $input Bestandsnaam of inputstring
* @param integer $type {@link Tokenizer::INPUT_FILE} of {@link Tokenizer::INPUT_STRING}
*
* @throws Tokenizer_Exception
*/
public function __construct($input, $type = null, $tokenClass = null) {
$this->_input = $input;
if($tokenClass === null) {
$tokenClass = 'Token';
}
// check met behulp van reflection of gegeven tokenklasse wel
// een instantie is die afgeleid is van Token_Abstract
$reflection = new ReflectionClass($tokenClass);
if(!$reflection->isSubclassOf(new ReflectionClass('Token_Abstract'))) {
throw new Tokenizer_Exception('Tokenklasse is ongeldig');
}
unset($reflection);
$this->_tokenClass = $tokenClass;
if($type === null) {
$type = self::INPUT_STRING;
}
switch($type) {
case self::INPUT_FILE:
// als het een bestand is, lees het in
$this->_readFile();
break;
case self::INPUT_STRING:
break;
default:
throw new Tokenizer_Exception('Onbekend type gegeven');
}
$this->_parseTokens();
}
/**
* Gemaksmethode om een bestand te tokenizen
*
* @param string $fileName
* @param string $tokenClass Eventuele tokenklasse
* @return Tokenizer
*/
static public function fromFile($fileName, $tokenClass = null) {
return new Tokenizer($fileName, self::INPUT_FILE, $tokenClass);
}
/**
* Gemaksmethode om een string te tokenizen
*
* @param string $input
* @param string $tokenClass Eventuele tokenklasse
* @return Tokenizer
*/
static public function fromString($input, $tokenClass = null) {
return new Tokenizer($fileName, self::INPUT_STRING, $tokenClass);
}
/**
* Lees het bestand in $this->_input in
*
* @throws Tokenizer_Exception
*/
private function _readFile() {
// check of het bestand te lezen is
if(!is_readable($this->_input)) {
throw new Tokenizer_Exception('Gegeven bestand is niet leesbaar');
}
$this->_input = file_get_contents($this->_input);
}
/**
* Parse $this->_input, en plaats alles in $this->_tokens
*/
private function _parseTokens() {
$tokens = token_get_all(str_replace(array("\r\n", "\r"), "\n", $this->_input));
for($i = 0; isset($tokens[$i]); ++$i) {
// voeg ieder token toe als Token-object.
$this->_tokens[] = call_user_func(array($this->_tokenClass, 'fromArray'), $tokens[$i]);
}
$this->_count = count($this->_tokens);
}
/**
* Haal een token op
*
* Gebruik (gebruik liever {@link Tokenizer::getNextToken()}:
* <pre>$tokens = new Tokenizer_File('/pad/naar/bestand.php');
* $i = 0;
* while($token = $tokens->getToken($i++)) {
* echo $token;
* }</pre>
*
* <b>Let op:</b> als je met {@link Tokenizer::getNextToken()} al een
* token hebt opgehaald, staat de pointer al op het volgende element,
* dus moet je voor naar achter een meer doen, en voor naar voren een
* minder!
*
* @param integer $step
* @return Token_Abstract|false
*/
public function getToken($step = 0) {
$pointer = $this->_pointer;
if($step <> 0) {
$pointer += $step;
}
if($pointer < 0 || $pointer >= $this->_count) {
return false;
}
return $this->_tokens[$pointer];
}
/**
* Haal het volgende token op
*
* Gebruik:
* <pre>$tokens = new Tokenizer_File('/pad/naar/bestand.php');
* while($token = $tokens->getNextToken()) {
* echo $token;
* }</pre>
*
* @return Token_Abstract|false
*/
public function getNextToken() {
if($this->_pointer >= $this->_count) {
return false;
}
return $this->_tokens[$this->_pointer++];
}
/**
* Verschuif de pointer van de tokensarray
*
* @param integer $step
*/
public function step($step) {
$this->_pointer += $step;
$this->_pointer = max(0, min($this->_count, $this->_pointer));
}
}
/**
* Gemaksklasse voor het tokenizen van bestanden
*
* @package Tokenizer
*/
class Tokenizer_File extends Tokenizer {
/**
* Enkel bestandsnaam hier
*
* @param string $fileName De bestandsnaam
* @param string $tokenClass Eventuele tokenklasse
*/
public function __construct($fileName, $tokenClass = null) {
parent::__construct($fileName, Tokenizer::INPUT_FILE, $tokenClass);
}
}
/**
* Gemaksklasse voor het tokenizer van strings
*
* @package Tokenizer
*/
class Tokenizer_String extends Tokenizer {
/**
* Enkel een string hier
*
* @param string $input De inputstring met PHP code
* @param string $tokenClass Eventuele tokenklasse
*/
public function __construct($input, $tokenClass = null) {
parent::__construct($input, Tokenizer::INPUT_STRING, $tokenClass);
}
}
/**
* Abstracte implementatie van een token
*
* Deze implementatie is al voorgebouwd op PHP-tokens,
* je kunt dit natuurlijk aanpassen door het te extenden
* naar je eigen ontwerp.
*
* @package Tokenizer
*/
abstract class Token_Abstract {
/**
* Type token, afhankelijk van implementatie
*
* @var mixed
*/
private $_type = null;
/**
* Inhoud van het token
*
* @var string
*/
private $_contents = '';
/**
* Factoryimplementatie voor ieder token.
*
* @param mixed $type
* @param string $contents
*/
protected function __construct($type, $contents) {
$this->_type = $type;
$this->_contents = $contents;
}
/**
* Implementatie van __toString, zie {@link Token_Abstract::getHtml()}
*
* @return unknown
*/
public function __toString() {
return $this->getHtml();
}
/**
* Haal de naam op van het type token
*
* Hier geimplementeerd met {@link token_name}
*
* @return string
*/
public function getName() {
return $this->_type == T_SYMBOL ? 'T_SYMBOL' : token_name($this->_type);
}
/**
* Haal het type token op
*
* @return mixed
*/
public function getType() {
return $this->_type;
}
/**
* Haal de inhoud van dit token op
*
* @return string
*/
public function getContents() {
return $this->_contents;
}
/**
* Haal een HTML-representatie van dit token op.
*
* Zie {@link Token_Abstract::getContents()}
*
* @return string
*/
public function getHtml() {
return str_replace("\t", ' ', htmlspecialchars($this->getContents()));
}
/**
* Kijk of dit token van het aangegeven type is
*
* @param mixed $type
* @return boolean
*/
public function is($type) {
if(is_array($type)) {
return in_array($this->_type, $type);
} else {
return $this->_type == $type;
}
}
/**
* Te implementeren methode die zorgt voor een nieuw object op basis van een array
*
* @param array|string $array
* @return Token_Abstract
*/
abstract static public function fromArray($array);
}
/**
* Representatie van een token.
*
* Alleen te gebruiken in combinatie met {@link Tokenizer}, niet op zichzelf
*
* @package Tokenizer
*/
class Token extends Token_Abstract {
/**
* Implementatie van {@link Token_Abstract::fromArray()}
*
* @param array|string $array
* @return Token
*/
static public function fromArray($array) {
if(!is_array($array)) {
$array = array(T_SYMBOL, $array);
} elseif(!isset($array[0], $array[1]) || !is_int($array[0])) {
throw new Tokenizer_Exception('Ongeldig token gegeven');
}
return new self($array[0], $array[1]);
}
}
//---------------------------------------------------------------------
/**
* VOORBEELD:
*
* Syntax highlighting met de Tokenizer
*/
//---------------------------------------------------------------------
/**
* Implementatie van een token die in de {@link Highlighter} wordt gebruikt
*
* @package Tokenizer
* @subpackage Highlighter
*/
class Token_Highlight extends Token_Abstract {
/**
* Lijst met types die de tokenizer van PHP herkent als "syntax"
*
* De waarde van iedere entry, betekent dat het type te linken is naar de manual
*
* @var array
*/
static protected $syntaxList = array(
T_REQUIRE_ONCE => true, T_REQUIRE => true, T_EVAL => true, T_INCLUDE_ONCE => true, T_INCLUDE => true,
T_LOGICAL_OR => false, T_LOGICAL_XOR => false, T_LOGICAL_AND => false, T_PRINT => true, T_SR_EQUAL => false,
T_SL_EQUAL => false, T_XOR_EQUAL => false, T_OR_EQUAL => false, T_AND_EQUAL => false, T_MOD_EQUAL => false,
T_CONCAT_EQUAL => false, T_DIV_EQUAL => false, T_MUL_EQUAL => false, T_MINUS_EQUAL => false,
T_PLUS_EQUAL => false, T_BOOLEAN_OR => false, T_BOOLEAN_AND => false, T_IS_NOT_IDENTICAL => false,
T_IS_IDENTICAL => false, T_IS_NOT_EQUAL => false, T_IS_EQUAL => false, T_IS_GREATER_OR_EQUAL => false,
T_IS_SMALLER_OR_EQUAL => false, T_SR => false, T_SL => false, T_INSTANCEOF => true, T_UNSET_CAST => false,
T_BOOL_CAST => false, T_OBJECT_CAST => false, T_ARRAY_CAST => false, T_STRING_CAST => false,
T_DOUBLE_CAST => false, T_INT_CAST => false, T_DEC => false, T_INC => false, T_CLONE => true, T_NEW => true,
T_EXIT => true, T_IF => true, T_ELSEIF => true, T_ELSE => true, T_ENDIF => true, T_ECHO => true,
T_DO => true, T_WHILE => true, T_ENDWHILE => false, T_FOR => true, T_ENDFOR => false, T_FOREACH => true,
T_ENDFOREACH => false, T_DECLARE => true, T_ENDDECLARE => false, T_AS => false, T_SWITCH => true,
T_ENDSWITCH => false, T_CASE => true, T_DEFAULT => false, T_BREAK => true, T_CONTINUE => true,
T_FUNCTION => true, T_CONST => false, T_RETURN => true, T_TRY => true, T_CATCH => true, T_THROW => true,
T_USE => true, T_GLOBAL => true, T_PUBLIC => true, T_PROTECTED => true, T_PRIVATE => true, T_FINAL => false,
T_ABSTRACT => true, T_STATIC => true, T_VAR => false, T_UNSET => true, T_ISSET => true, T_EMPTY => true,
T_HALT_COMPILER => true, T_CLASS => true, T_INTERFACE => true, T_EXTENDS => true, T_IMPLEMENTS => true,
T_OBJECT_OPERATOR => false, T_DOUBLE_ARROW => false, T_LIST => true, T_ARRAY => true, T_CLASS_C => false,
T_METHOD_C => false, T_FUNC_C => false, T_START_HEREDOC => false, T_END_HEREDOC => false,
T_PAAMAYIM_NEKUDOTAYIM => false
);
/**
* Is dit token een keyword?
*
* @var boolean
*/
private $_isSyntax = false;
/**
* Heeft dit token een entry in de PHP manual?
*
* @var boolean
*/
private $_isLinkable = false;
/**
* Setup
*
* @param mixed $type
* @param string $contents
*/
protected function __construct($type, $contents) {
parent::__construct($type, $contents);
// is het token een keyword?
if(isset(self::$syntaxList[$type])) {
$this->_isSyntax = true;
$this->_isLinkable = self::$syntaxList[$type];
}
}
/**
* Implementatie van {@link Token_Abstract::fromArray()}
*
* @param array|string $array
* @return Token_Highlight
*/
static public function fromArray($array) {
if(!is_array($array)) {
$array = array(T_SYMBOL, $array);
} elseif(!isset($array[0], $array[1]) || !is_int($array[0])) {
throw new Tokenizer_Exception('Ongeldig token gegeven');
}
return new self($array[0], $array[1]);
}
/**
* Kijk of het een keyword is
*
* @return boolean
*/
public function isSyntax() {
return $this->_isSyntax;
}
/**
* Kijk of het linkable is
*
* @return boolean
*/
public function isLinkable() {
return $this->_isLinkable;
}
/**
* Kijk of het een string-quote is
*
* In PHP wordt bij string met dubbele quotes waarin
* variabelen staan, de quotes apart gegeven.
*
* @return boolean
*/
public function isStringToken() {
return $this->getContents() == '"';
}
}
/**
* Voorbeeldimplementatie van de tokenizer, in combinatie
* met een eigen {@link Token}-implementatie
*
* @package Tokenizer
* @subpackage Highlighter
*/
class Highlighter {
/**
* De tokenizer
*
* @var Tokenizer
*/
private $_tokenizer = null;
/**
* Kleuren voor syntax highlighting
*
* @var array
*/
private $_typeToClass = array();
/**
* Stel wat variabelen leeg in, en klaar.
*/
public function __construct() {
$this->_tokenizer = null;
$this->_typeToClass = array();
}
/**
* Highlight een bestand
*
* @param string $fileName Bestandsnaam die gehighlight dient te worden
* @return string Uiteindelijke output
*/
public function highlightFile($fileName) {
// gebruik de gemaksklasse Tokenizer_File voor bestands-tokenizing
$this->_tokenizer = new Tokenizer_File($fileName, 'Token_Highlight');
$output = $this->_highlight();
$this->_tokenizer = null;
return $output;
}
/**
* Highlight een string
*
* @param string $string PHP-code die gehighlight dient te worden
* @return string
*/
public function highlightString($string) {
// gebruik de gemaksklasse Tokenizer_String voor string-tokenizing
$this->_tokenizer = new Tokenizer_String($string, 'Token_Highlight');
$output = $this->_highlight();
$this->_tokenizer = null;
return $output;
}
/**
* Stel een classnaam in per type token
*
* @param mixed $type 'default', 'keyword', een type of een array van dezen
* @param string $class
*/
public function setClass($type, $class = null) {
if($class === null && is_array($type)) {
// kan worden aangeroepen met een array
foreach($type as $key => $value) {
$this->setClass($key, $value);
}
} else {
$this->_typeToClass[$type] = $class;
}
return $this;
}
/**
* Haal de classnaam op voor dit token
*
* @param mixed $token {@link Token_Abstract} of een generiek type
* @return string|false
*/
public function getClass($token) {
if(!$token instanceof Token_Abstract) {
// in dit geval is het een string, een type dus
if(isset($this->_typeToClass[$token])) {
return $this->_typeToClass[$token];
} elseif(isset($this->_typeToClass['default'])) {
return $this->_typeToClass['default'];
}
} else {
// in dit geval is het een token
$class = $token->getType();
if(isset($this->_typeToClass[$class])) {
return $this->_typeToClass[$class];
} elseif(isset($this->_typeToClass['keyword']) && $token->isSyntax()) {
return $this->_typeToClass['keyword'];
} elseif(isset($this->_typeToClass['default'])) {
return $this->_typeToClass['default'];
}
}
return false;
}
/**
* Opschoning van de output.
*
* Haalt momenteel dubbele spans achter elkaar weg
*
* @param string $output
* @return string
*/
protected function _cleanupOutput($output) {
do {
// vervang 2 aansluitende span's met dezelfde class
// door 1 span die beide omsluit
$newOutput = preg_replace(
'~(<span(?:[^<>"\']|"[^"]*"|\'[^\']*\')*>)((?:(?!</span>).)+)</span>\1~is',
'$1$2',
$output
);
// zolang de output verandert
} while($output <> $newOutput && $output = $newOutput);
return $newOutput;
}
/**
* De almachtige highlighter
*
* Deze zorgt voor de correcte highlighting, en voor function linking
*
* @return string
*/
protected function _highlight() {
$output = '';
// zitten we in een ""-string?
$stringFlag = false;
// of is die net gesloten?
$lastStringFlag = false;
while($token = $this->_tokenizer->getNextToken()) {
// E_NOTICE voorkomen, hier definieren
$functionFlag = false;
if($token->isStringToken()) {
$stringFlag = !$stringFlag;
if(!$stringFlag) {
$lastStringFlag = true;
}
}
// als we niet in een string zitten, normale volgorde
if(!$stringFlag && !$lastStringFlag) {
// probeer de class op te halen voor dit token
if($class = $this->getClass($token)) {
$class = ' class="' . $class . '"';
}
// check of het een functie _kan_ zijn
if($token->is(T_STRING)) {
// het 3e token dat geweest is (we zijn al een stap vooruit!)
$prevToken = $this->_tokenizer->getToken(-3);
// kijk of het token bestond, en of het niet een functiedeclaratie aangeeft
if(!$prevToken instanceof Token_Abstract || !$prevToken->is(T_FUNCTION)) {
// check het volgende token, en het token daarna op een (
for($i = 0; $i <= 1; ++$i) {
$nextToken = $this->_tokenizer->getToken($i);
if($nextToken instanceof Token_Abstract && $nextToken->getContents() == '(') {
$functionFlag = $this->getManualLink($token->getContents());
break;
}
}
unset($nextToken);
}
unset($prevToken);
// kijken of het misschien syntax is, en of het te linken is
} elseif($token->isSyntax() && $token->isLinkable()) {
$functionFlag = $this->getManualLink($token->getContents(), false);
}
} else {
$lastStringFlag = false;
// probeer de classnaam op te halen voor dubbel-quotes-strings
if($class = $this->getClass(T_ENCAPSED_AND_WHITESPACE)) {
$class = ' class="' . $class . '"';
}
}
// append aan de output
// is het een functie, voeg dan de a-tag in
if($functionFlag !== false) {
$output .= '<a href="' . htmlspecialchars($functionFlag) . '"' . $class . '>' . $token->getHtml() . '</a>';
} else {
$output .= '<span' . $class . '>' . $token->getHtml() . '</span>';
}
}
// retourneer de code met code-tags
return '<pre>' . $this->_cleanupOutput($output) . '</pre>';
}
/**
* Als de opgegeven functie of keyword bestaat, geef een URL naar de manual
*
* @param string $functionName
* @return string|false
*/
public function getManualLink($functionName, $isFunction = true) {
// OF het is geen functie, OF de functie moet bestaan
if(!$isFunction XOR function_exists($functionName)) {
return 'http://www.php.net/' . str_replace('_', '-', strtolower($functionName));
}
return false;
}
}
header('Content-Type: text/html;charset=iso-8859-1');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl">
<head>
<title>Voorbeeld van OO-implementatie van de PHP-tokenizer</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1;" />
<style type="text/css">
.highlight_blue {
color: #00b;
}
.highlight_html {
color: #000;
}
.highlight_orange {
color: #ff8000;
}
.highlight_green {
color: #070;
}
.highlight_red {
color: #d00;
}
</style>
</head>
<body>
<?php
// parsetijd bijhouden
$time = microtime(true);
$highlighter = new Highlighter;
// stel de default classes in die worden gebruikt
$highlighter->setClass(array(
// standaardkleur
'default' => 'highlight_blue',
// keywords (syntax)
'keyword' => 'highlight_green',
// symbolen ('(', ')', '!', ';', etc.)
T_SYMBOL => 'highlight_green',
// strings
T_ENCAPSED_AND_WHITESPACE => 'highlight_red',
T_CONSTANT_ENCAPSED_STRING => 'highlight_red',
// html
T_INLINE_HTML => 'highlight_black',
// commentaar
T_COMMENT => 'highlight_orange',
T_DOC_COMMENT => 'highlight_orange'
));
// hopla, daar krijgen we al geldige content
echo $highlighter->highlightFile(__FILE__);
$time = microtime(true) - $time;
?>
<pre><b>Parsetijd:</b> <?php echo number_format($time, 5, '.', '')?></pre>
</body>
</html>[/code]
Reacties
0