phug-php/phug

View on GitHub
src/Phug/Lexer/Lexer/Scanner/IndentationScanner.php

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
<?php

/**
 * @example space or tab to put contents inside parent element
 */

namespace Phug\Lexer\Scanner;

use Phug\Lexer;
use Phug\Lexer\ScannerInterface;
use Phug\Lexer\State;
use Phug\Lexer\Token\IndentToken;
use Phug\Lexer\Token\OutdentToken;
use Phug\Reader;

class IndentationScanner implements ScannerInterface
{
    protected function getLevelFromIndent(State $state, $indent)
    {
        return mb_strlen(str_replace(
            Lexer::INDENT_TAB,
            str_repeat(Lexer::INDENT_SPACE, Lexer::DEFAULT_TAB_WIDTH),
            $indent
        ));
    }

    protected function getIndentChar(Reader $reader)
    {
        $char = null;

        if ($reader->peekIndentation()) {
            $char = $reader->getLastPeekResult();
            $reader->consume();
        }

        return $char;
    }

    protected function formatIndentChar(State $state, $indentChar)
    {
        $isTab = $indentChar === Lexer::INDENT_TAB;
        $indentStyle = $isTab ? Lexer::INDENT_TAB : Lexer::INDENT_SPACE;
        //Update the indentation style
        if (!$state->getIndentStyle()) {
            $state->setIndentStyle($indentStyle);
        }
        if ($state->getIndentStyle() !== $indentStyle && !$state->getOption('allow_mixed_indent')) {
            $state->throwException(
                'Invalid indentation, you can use tabs or spaces but not both'
            );
        }

        return $indentChar;
    }

    public function getIndentLevel(State $state, $maxLevel = INF, callable $getIndentChar = null)
    {
        if ($maxLevel <= 0) {
            return 0;
        }

        $reader = $state->getReader();
        $indent = '';

        if (is_null($getIndentChar)) {
            $getIndentChar = [$this, 'getIndentChar'];
        }

        while ($indentChar = call_user_func($getIndentChar, $reader)) {
            $indent .= $this->formatIndentChar($state, $indentChar);
            if ($state->getIndentWidth() &&
                $this->getLevelFromIndent($state, $indent) >= $maxLevel
            ) {
                break;
            }
        }

        if (!$state->getIndentWidth() &&
            mb_strpos($indent, Lexer::INDENT_SPACE) !== false &&
            mb_strpos($indent, Lexer::INDENT_TAB) !== false
        ) {
            $state->setIndentWidth(Lexer::DEFAULT_TAB_WIDTH);
        }

        //Update the indentation width
        $length = $this->getLevelFromIndent($state, $indent);
        if ($length && !$state->getIndentWidth()) {
            //We will use the pretty first indentation as our indent width
            $state->setIndentWidth($length);
        }

        return $length;
    }

    protected function setStateLevel(State $state, $indent)
    {
        $oldLevel = $state->getLevel();
        $newLevel = $this->getIndentLevel($state, INF, function () use (&$indent) {
            $char = null;

            if ($indent !== null && mb_strlen($indent)) {
                $char = mb_substr($indent, 0, 1);
                $indent = mb_substr($indent, 1);
            }

            return $char;
        });

        $state->setLevel($newLevel);

        return $state->getLevel() - $oldLevel;
    }

    public function scan(State $state)
    {
        $reader = $state->getReader();

        //TODO: $state->endToken
        //There's no indentation if we're not at the start of a line
        if ($reader->getOffset() !== 1) {
            return;
        }

        $indent = $reader->readIndentation();

        //If this is an empty line, we ignore the indentation completely.
        foreach ($state->scan(NewLineScanner::class) as $token) {
            yield $token;

            return;
        }

        //We create a token for each indentation/outdentation
        if ($this->setStateLevel($state, $indent) > 0) {
            $state->indent();

            yield $state->createToken(IndentToken::class);

            return;
        }

        while ($state->nextOutdent() !== false) {
            yield $state->createToken(OutdentToken::class);
        }
    }
}