phug/reader

Pug (前身为Jade) 的 PHP 字符串读取器,基于缩进来构建的 HTML 模板引擎


README

什么是 Phug Reader?

Reader 类是一个小型实用工具,可以解析和扫描字符串以查找特定实体。

它主要基于正则表达式,但也引入了扫描任何类型字符串和表达式的工具(例如字符串转义、括号计数等)

传递给 Reader 的字符串将通过 consume 机制逐字节吞咽。当字符串为空时,通常完成解析。

这个类专门用于词法分析和表达式验证。

安装

通过 Composer 安装

composer require phug/reader

用法

基础

使用 Phug\Reader 读取的过程涉及 查看消费。你 查看,检查它是否是你想要的,如果是,你就 消费

Reader 上的 read-方法将自动查看和消费,直到找到你搜索的内容。 match-方法类似于 peek,但使用正则表达式。

让我们创建一个小的示例代码来解析

$code = 'someString = "some string"';

现在我们为这段代码创建一个 Reader

$reader = new Reader($code);

如果你需要一个固定的编码,请使用第二个 $encoding 参数。

现在我们可以开始读取过程。首先,我们想要读取我们的标识符。我们可以通过使用 readIdentifier() 来轻松完成,如果未遇到标识符,则返回 null,否则返回找到的标识符。它会在遇到任何不是标识符字符的任何内容时停止(在这种情况下标识符后的空格)

$identifier = $reader->readIdentifier();
if ($identifier === null) {
    throw new Exception("Failed to read: Identifier expected");
}
    
var_dump($identifier); //`someString`

要直接到达我们的 = 字符,我们只需跳过我们遇到的任何空格。这也允许你使用任何你想要的间距(例如,如果你喜欢,你可以用制表符缩进上面的代码)

$reader->readSpaces();

如果我们需要空格,我们总是可以捕获返回的结果。如果没有遇到空格,它将只返回 null

现在我们想要解析赋值运算符(=)(或者更确切地说,验证它是否存在)

if (!$reader->peekChar('=')) {
    throw new Exception("Failed to read: Assignment expected");
}
    
//Consume the result, since we're `peek`ing, not `read`ing.
$reader->consume();

再次跳过空格

$reader->readSpaces();

然后读取字符串。如果没有遇到引号字符("'),则返回 null。否则,它将返回(已解析的)字符串,不带引号。请注意,你必须明确检查 null,因为我们也可能有一个空字符串(""),它在 PHP 中求值为 true

$string = $reader->readString();

if ($string === null) {
    throw new Exception("Failed to read: Expected string");
}

var_dump($string); //`some string`

默认情况下,遇到的引号样式将被转义,这样你就可以正确地扫描 "some \" string"。如果你想添加其他转义,请使用 readString 的第一个参数。

现在你已经有了解析的所有部分,可以组成你的实际动作

echo "Set `$identifier` to `$string`"; //Set `someString` to `some string`

并且已经以这种方式进行了验证。

这只是一个小的示例,Phug Reader 是为了循环解析而设计的。

构建一个小型的分词器

use Phug\Reader;

//Some C-style example code
$code = 'someVar = {a, "this is a string (really, it \"is\")", func(b, c), d}';

$reader = new Reader($code);

$tokens = [];
$blockLevel = 0;
$expressionLevel = 0;
while ($reader->hasLength()) {
    
    //Skip spaces of any kind.
    $reader->readSpaces();
    
    //Scan for identifiers
    if ($identifier = $reader->readIdentifier()) {
        
        $tokens[] = ['type' => 'identifier', 'name' => $identifier];
        continue;
    }
    
    //Scan for Assignments
    if ($reader->peekChar('=')) {
        
        $reader->consume();
        $tokens[] = ['type' => 'assignment'];
        continue;
    }
    
    //Scan for strings
    if (($string = $reader->readString()) !== null) {
        
        $tokens[] = ['type' => 'string', 'value' => $string];
        continue;
    }
    
    //Scan block start
    if ($reader->peekChar('{')) {
        
        $reader->consume();
        $blockLevel++;
        $tokens[] = ['type' => 'blockStart'];
        continue;
    }
    
    //Scan block end
    if ($reader->peekChar('}')) {
    
        $reader->consume();
        $blockLevel--;
        $tokens[] = ['type' => 'blockEnd'];
        continue;
    }
    
    //Scan parenthesis start
    if ($reader->peekChar('(')) {
        
        $reader->consume();
        $expressionLevel++;
        $tokens[] = ['type' => 'listStart'];
        continue;
    }
    
    //Scan parenthesis end
    if ($reader->peekChar(')')) {
        
        $reader->consume();
        $expressionLevel--;
        $tokens[] = ['type' => 'listEnd'];
        continue;
    }
    
    //Scan comma
    if ($reader->peekChar(',')) {
        
        $reader->consume();
        $tokens[] = ['type' => 'next'];
        continue;
    }

    throw new \Exception(
        "Unexpected ".$reader->peek(10)
    );
}

if ($blockLevel || $expressionLevel)
    throw new \Exception("Unclosed bracket encountered");

var_dump($tokens);
/* Output:
[
    ['type' => 'identifier', 'name' => 'someVar'],
    ['type' => 'assignment'],
    ['type' => 'blockStart'],
    ['type' => 'identifier', 'name' => 'a'],
    ['type' => 'next'],
    ['type' => 'string', 'value' => 'this is a string (really, it "is")'],
    ['type' => 'next'],
    ['type' => 'identifier', 'name' => 'func'],
    ['type' => 'listStart'],
    ['type' => 'identifier', 'name' => 'b'],
    ['type' => 'next'],
    ['type' => 'identifier', 'name' => 'c'],
    ['type' => 'listEnd'],
    ['type' => 'next'],
    ['type' => 'identifier', 'name' => 'd'],
    ['type' => 'blockEnd']
]
*/

保持表达式完整

有时你想要保持表达式完整,例如,当你允许包含需要单独解析的第三方代码时。

Reader 提供了一个括号计数实用工具,可以做到这一点。让我们以 Jade 为例

a(href=getUri('/abc', true), title=(title ? title : 'Sorry, no title.'))

为了解析这个,我们来做以下操作

//Scan Identifier ("a")
$identifier = $reader->readIdentifier();

$attributes = [];
//Enter an attribute block if available
if ($reader->peekChar('(')) {

    $reader->consume();
    while ($reader->hasLength()) {
    
    
        //Ignore spaces
        $reader->readSpaces();
    
    
        //Scan the attribute name
        if (!($name = $this->readIdentifier())) {
            throw new \Exception("Attributes need a name!");
        }
        
        
        //Ignore spaces
        $reader->readSpaces();
        
        
        //Make sure there's a =-character
        if (!$reader->peekChar('=')) {
            throw new \Exception("Failed to read: Expected attribute value");
        }
            
        $reader->consume();
        
        
        //Ignore spaces
        $reader->readSpaces();
        
        
        //Read the expression until , or ) is encountered
        //It will ignore , and ) inside any kind of brackets and count brackets correctly until we actually
        //reached the end-bracket
        $value = $reader->readExpression([',', ')']);
        
        
        //Add the attribute to our attribute array
        $attributes[$name] = $value;
        
        
        //If we don't encounter a , to go on, we break the loop
        if (!$reader->peekChar(',')) {
            break;
        }
            
            
        //Else we consume the , and continue our attribute parsing
        $reader->consume();       
    }
    
    //Now make sure we actually closed our attribute block correctly.
    if (!$reader->peekChar(')')) {
        throw new \Exception("Failed to read: Expected closing bracket");
    }
}


$element = ['identifier' => $identifier, 'attributes' => $attributes];

var_dump($element);
/* Output:
[
    'identifier' => 'a',
    'attributes' => [
        'href' => 'getUri(\'/abc\', true)',
        'title' => '(title ? title : \'Sorry, no title.\')'
    ]
]
*/

现在你已经有了一个用于(非常基础的)Jade元素的解析器!它可以处理你喜欢的任意数量的属性,以及所有你能想到的可能值,而不会打断列表,无论包含多少逗号和括号。

深入研究,Phug Reader实际上能够解析任何类型的源代码和文本。