-
-
Save marcioAlmada/b0621c4798468ae37720 to your computer and use it in GitHub Desktop.
PHPP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--TEST-- | |
Simple ignore test | |
--FILE-- | |
HTML here, this should be preserved: @ "debug" public | |
<?php | |
%{ | |
ignore @; | |
ignore public; | |
ignore "debug"; | |
}% // this comment should be preserved | |
@test("debug"); | |
class X { | |
public function test(){} | |
} | |
?> | |
--EXPECTF-- | |
HTML here, this should be preserved: @ "debug" public | |
<?php | |
// this comment should be preserved | |
test(); | |
class X { | |
function test(){} | |
} | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php declare(strict_types=1); | |
namespace phpp; | |
use Exception; | |
function parse($source) : string { | |
$tstream = tstream::fromSource($source); | |
$directives = new directives(); | |
in_php: while ($tstream->valid()) { | |
$tstream->skip(); | |
if ($tstream->try('%', '{')) { | |
$tstream->mark()->consume('%', '{'); | |
goto in_preprocessor; | |
} | |
$directives->applyOn($tstream); | |
$tstream->next(); | |
} | |
in_preprocessor: while ($tstream->valid()) { | |
$tstream->skip(); | |
if ($tstream->try('}', '%')) { | |
$tstream->consume('}', '%'); | |
$tstream->deleteFromMark(); | |
goto in_php; | |
} | |
$directive_name = $tstream->consume('T_STRING')->value; | |
$directive = (directives::lookup($directive_name))::parse($tstream); | |
$directives->push($directive); | |
$tstream->next(); | |
} | |
return (string) $tstream; | |
} | |
class tstream { | |
const SKIPPABLE = ['T_WHITESPACE', 'T_COMMENT', 'T_DOC_COMMENT', 'T_INLINE_HTML']; | |
protected $index = 0, $mark = [-1], $tokens; | |
private function __construct(array $tokens) { | |
if (! $tokens) | |
throw new Exception("Empty token stream."); | |
$this->tokens = $tokens; | |
if (reset($this->tokens)->type !== 'START') | |
array_unshift($this->tokens, new token(['START', null, null])); | |
if (end($this->tokens)->type !== 'END') | |
array_push($this->tokens, new token(['END', null, null])); | |
} | |
function __toString() : string { | |
return implode('', $this->tokens); | |
} | |
function current() : token { | |
return $this->tokens[$this->index]; | |
} | |
function valid() { | |
return $this->current()->type !== 'END'; | |
} | |
function step($step = 1) : token { | |
if ($this->valid()) $this->index += $step; | |
return $this->current(); | |
} | |
function skip(array $skippable = self::SKIPPABLE) : token { | |
while (in_array($this->current()->type, $skippable)) $this->step(); | |
return $this->current(); | |
} | |
function next() : token { | |
$this->step(); | |
$this->skip(); | |
return $this->current(); | |
} | |
function try(string ...$types) : bool { | |
$delta = $this->shift(...$types); | |
$this->index -= $delta; // rewind so try has no side effects | |
return $delta === count($types); | |
} | |
function consume(string ...$types) : token { | |
$delta = $this->shift(...$types); | |
if ($delta !== count($types)) { | |
$found = $this->current()->toError(); | |
$expected = $types[$delta]; | |
$this->index -= $delta; // rewind so bad consume has no side effects | |
throw new Exception( | |
"Unexpected {$found}, expected '{$expected}'."); | |
} | |
return $this->tokens[$this->index - ($delta ? 1 : 0)]; | |
} | |
protected function shift(string ...$types) : int { | |
$index = $this->index; | |
foreach ($types as $type) | |
if ($this->current()->type === $type && $this->step()) continue; | |
else break; | |
return $this->index - $index; | |
} | |
function mark() : self { | |
$this->mark[] = $this->index; | |
return $this; | |
} | |
function deleteFromMark() : tstream { | |
return self::fromSlice(array_splice($this->tokens, ...$this->delta())); | |
} | |
protected function delta() : /* [int, int] */ array { | |
$mark = end($this->mark); | |
if (! isset($this->tokens[$mark])) | |
throw new Exception( | |
"Invalid token interval {$mark}...{$this->index}"); | |
array_pop($this->mark); | |
$delta = ($this->index - $mark) ?: 1; | |
$this->index = $mark; // rewind to current mark | |
return [$this->index, $delta]; | |
} | |
static function fromSource(string $source) { | |
$tokens = token_get_all($source) ?? []; | |
foreach ($tokens as $index => $token) $tokens[$index] = new token($token); | |
return self::fromSlice($tokens); | |
} | |
static function fromSlice(/** token[] */ array $tokens) { | |
return new self($tokens); | |
} | |
} | |
class token { | |
public $type, $value, $line; | |
protected $literal = false; | |
function __construct($token) { | |
if (is_array($token)) { | |
$token[0] = is_int($token[0]) ? token_name($token[0]) : $token[0]; | |
list($this->type, $this->value, $this->line) = $token; | |
return; | |
} | |
$this->value = $this->type = $token; | |
$this->literal = true; | |
} | |
function toError() { | |
return $this->literal ? "'{$this->value}'" | |
: "{$this->type}({$this->value})"; | |
} | |
function __toString() { | |
return (string) $this->value; | |
} | |
function __get($property) { | |
return $this->$property; | |
} | |
} | |
interface directive { | |
function __construct(token ...$token); | |
function __invoke(tstream $tstream) : bool; | |
} | |
class directives { | |
const LOOKUP = [ | |
'rule' => rule::class, | |
'ignore' => ignore::class, | |
]; | |
protected $directives = []; | |
function push(directive $directive) { | |
$this->directives[] = $directive; | |
} | |
function applyOn(tstream $tstream) { | |
foreach ($this->directives as $directive) $directive($tstream); | |
} | |
static function lookup(string $directive) { | |
if (! isset(static::LOOKUP[$directive])) | |
throw new Exception("Undefined directive {$directive}."); | |
return static::LOOKUP[$directive]; | |
} | |
} | |
class ignore implements directive { | |
protected $ignorable; | |
function __construct(token ...$token) { | |
list($this->ignorable) = $token; | |
} | |
function __invoke(tstream $tstream) : bool { | |
return $tstream->current()->type === $this->ignorable->type | |
&& $tstream->current()->value === $this->ignorable->value | |
&& $tstream->mark()->deleteFromMark() | |
&& $tstream->step(-1); | |
} | |
static function parse(tstream $tstream) : directive { | |
$tstream->skip(); | |
$ignorable = clone($tstream->current()); | |
$tstream->next(); | |
$tstream->consume(';'); | |
return new self($ignorable); | |
} | |
} | |
class rule implements directive { | |
protected $name, $pattern, $expansion; | |
function __construct(token ...$token) { | |
list($this->name, $this->pattern, $this->expansion) = $tokens; | |
} | |
function __invoke(tstream $tstream) : bool { | |
// TODO | |
return false; | |
} | |
static function parse(tstream $tstream) : directive { | |
$tstream->skip(); | |
$tstream->consume('{'); | |
return new self(0, 0, 0);//($name, $pattern, $expansion); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment