Skip to content
Snippets Groups Projects
BodytextParser.php 10.3 KiB
Newer Older
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 3/18/16
 * Time: 5:43 PM
 */

Marc Egger's avatar
Marc Egger committed
namespace IMATHUZH\Qfq\Core;

 
use IMATHUZH\Qfq\Core\Helper\Support;
const NESTING_TOKEN_OPEN = '#&nesting-open-&#';
const NESTING_TOKEN_CLOSE = '#&nesting-close&#';
const NESTING_TOKEN_LENGTH = 17;
/**
 * Class BodytextParser
 * @package qfq
 */
Carsten  Rose's avatar
Carsten Rose committed
     * @param string $bodyText
Marc Egger's avatar
Marc Egger committed
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
    public function process($bodyText) {
Carsten  Rose's avatar
Carsten Rose committed
        $bodyText = $this->trimAndRemoveCommentAndEmptyLine($bodyText, $nestingOpen, $nestingClose);
        // Encrypt double curly braces to prevent false positives with nesting: form = {{form}}\n
Carsten  Rose's avatar
Carsten Rose committed
        $bodyText = Support::encryptDoubleCurlyBraces($bodyText);
        $bodyText = $this->joinLine($bodyText, $nestingOpen, $nestingClose);
Carsten  Rose's avatar
Carsten Rose committed
        $bodyText = $this->encryptNestingDelimeter($bodyText, $nestingOpen, $nestingClose);
        $bodyText = $this->unNest($bodyText, $nestingOpen, $nestingClose);
Carsten  Rose's avatar
Carsten Rose committed
        $bodyText = $this->trimAndRemoveCommentAndEmptyLine($bodyText, $nestingOpen, $nestingClose);
        $bodyText = Support::decryptDoubleCurlyBraces($bodyText);
Carsten  Rose's avatar
Carsten Rose committed
        if (strpos($bodyText, NESTING_TOKEN_OPEN) !== false) {
Marc Egger's avatar
Marc Egger committed
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
                json_encode([ERROR_MESSAGE_TO_USER => 'Report: Missing close delimiter', ERROR_MESSAGE_TO_DEVELOPER => $bodyText]), ERROR_MISSING_CLOSE_DELIMITER);
Carsten  Rose's avatar
Carsten Rose committed
        return $bodyText;
    }

    /**
     * Trim all lines, remove all empty lines and  all lines which start with '#'
     * @param $nestingOpen
     * @param $nestingClose
    private function trimAndRemoveCommentAndEmptyLine($bodytext, &$nestingOpen, &$nestingClose) {
        if ($src === false) {
            return '';
        }

        $firstLine = trim($src[0]);

        $this->setNestingToken($firstLine, $nestingOpen, $nestingClose);

     * Set the 'nesting token for this tt-conten record. Valid tokens are {}, <>, [], ().
     * If the first line of bodytext is a comment line and the last char of that line is a valid token: set that one.
     * If not: set {} as nesting token.
     * Example:
     *   # Some nice text       - no token found, take {}
     *   # ]                    - []
     *   # Powefull QFQ: <      - <>
     *
     * @param $firstLine
     * @param $nestingOpen
     * @param $nestingClose
    private function setNestingToken($firstLine, &$nestingOpen, &$nestingClose) {

        if ($nestingOpen !== '') {
            return;  // tokens already set or not bodytext: do not change.
        // Nothing defined: set default {}.
        if ($firstLine === false || $firstLine === '' || $firstLine[0] !== '#') {
            $nestingOpen = '{';
            $nestingClose = '}';
            return;
        }

        // Definition: first line of bodytext, has to be a comment line. If the last char is one of the valid token: set that one.
        // Nothing found: set {}.

        if ($firstLine[0] === '#') {
            $token = substr($firstLine, -1);
            switch($token) {
                case '<':
                    $nestingOpen = '<';
                    $nestingClose = '>';
                    break;
                case '[':
                    $nestingOpen = '[';
                    $nestingClose = ']';
                    break;
                case '(':
                    $nestingOpen = '(';
                    $nestingClose = ')';
                    break;
                default:
                    break;
     * Join lines. Nesting isn't changed.
     *
     * Iterates over all lines.
     *   Is a line a 'new line'?
     *    no: concat it to the last one.
     *    yes: flush the buffer, start a new 'new line'
     *
     * New Line Trigger:
     * a: {
     * b: }
     * c: 20
     * d: 20.30
     *
     * e: 5 {
     * f: 5.10 {
     *
     * g: head =
     * h: 10.20.head =
     *
     *  c,d,e,f: ^\d+(\.\d+)*(\s*{)?$
     *  g,h:     ^(\d+\.)*(sql|head)\s*=
     * @param $bodyText
     * @param $nestingOpen
     * @param $nestingClose
    private function joinLine($bodyText, $nestingOpen, $nestingClose) {
        $bodytextArray = explode(PHP_EOL, $bodyText);
        $nestingOpenRegexp = $nestingOpen;
        if ($nestingOpen === '(' || $nestingOpen === '[') {
            $nestingOpenRegexp = '\\' . $nestingOpen;
        }

            // Line end with '\'?
            if (substr($row, -1) == '\\') {
                $row = trim(substr($row, 0, -1)); // remove last char and trim
                $joinDelimiterNext = '';
            } else {
                $joinDelimiterNext = ' ';
            }

            if (($row == $nestingOpen || $row == $nestingClose)
                || (1 === preg_match('/^\d+(\.\d+)*(\s*' . $nestingOpenRegexp . ')?$/', $row))
                || (1 === preg_match('/^(\d+\.)*(' . TOKEN_VALID_LIST . ')\s*=/', $row))
                // if there is already something: save this.
                if ($full !== '') {
Carsten  Rose's avatar
Carsten Rose committed
                // continue row: concat - the space is necessary to join SQL statements correctly: 'SELECT ... FROM ... WHERE ... AND\np.id=...'  - here a 'AND' and 'p.id' need a space.
    /**
     * Encrypt $nestingOpen and $nestingClose by a more complex token. This makes it easy to search later for '}' or '{'
     *
     * Valid open (complete line): {, 10 {, 10.20 {
     * Valid close (complete line): }
     *
     * @param $bodytext
     * @param $nestingOpen
     * @param $nestingClose
     * @return mixed
     */
    private function encryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) {

        if ($nestingOpen === '(' || $nestingOpen === '[') {
            $nestingOpen = '\\' . $nestingOpen;
            $nestingClose = '\\' . $nestingClose;
        }

        $bodytext = preg_replace('/^((\d+)(\.\d+)*\s*)?(' . $nestingOpen . ')$/m', '$1' . NESTING_TOKEN_OPEN, $bodytext);
        $bodytext = preg_replace('/^' . $nestingClose . '$/m', '$1' . NESTING_TOKEN_CLOSE, $bodytext);

        return $bodytext;
    }
     * Unnest all level.
     *
     * Input:
     * 10 {
     *   sql = SELECT
     *   20.sql = INSERT ..
     *   30 {
     *      sql = DELETE
     *   }
     * }
     *
     * Output:
     * 10.sql = SELECT
     * 10.20.sql = INSERT
     * 10.20.30.sql = DELETE
     *
     * @param $bodytext
     * @param $nestingOpen
     * @param $nestingClose
     * @return mixed|string
Marc Egger's avatar
Marc Egger committed
     * @throws \UserFormException
    private function unNest($bodytext, $nestingOpen, $nestingClose) {
        // Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested'
//        $bodytext = str_replace('\{', '#&[_#', $bodytext);
//        $bodytext = str_replace('\}', '#&]_#', $bodytext);
//        $bodytext = Support::encryptDoubleCurlyBraces($bodytext);
        $posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);
            $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN);
                $result = $this->decryptNestingDelimeter($result, $nestingOpen, $nestingClose);
Marc Egger's avatar
Marc Egger committed
                throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
                    json_encode([ERROR_MESSAGE_TO_USER => 'Missing open delimiter', ERROR_MESSAGE_TO_DEVELOPER => "Missing open delimiter: $result"]),
            }

            $pre = substr($result, 0, $posMatchOpen);
            if ($pre === false)
                $pre = '';

            $post = substr($result, $posFirstClose + NESTING_TOKEN_LENGTH);
            $match = trim(substr($result, $posMatchOpen + NESTING_TOKEN_LENGTH, $posFirstClose - $posMatchOpen - NESTING_TOKEN_LENGTH));

            // "10.sql = SELECT...\n20 {\n
            $levelStartPos = strrpos(trim($pre), PHP_EOL);
            $levelStartPos = ($levelStartPos === false) ? 0 : $levelStartPos + 1;  // Skip PHP_EOL

            $level = trim(substr($pre, $levelStartPos));
//            if($level==='') {
//                $pre=
//            }
            // remove 'level' from last line
            $pre = substr($pre, 0, $levelStartPos);

            // Split nested content in single rows
            $lines = explode(PHP_EOL, $match);
            foreach ($lines as $line) {
                if ($line !== '') {
                    $pre .= $level . '.' . $line . PHP_EOL;
                }
            $posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);
//        $result = str_replace('#&[_#', '{', $result);
//        $result = str_replace('#&]_#', '}', $result);
//        $result = Support::decryptDoubleCurlyBraces($result);
    /**
     * Decrypt complex token by '{\n' and '}\n'
     *
     * @param $bodytext
     * @param $nestingOpen
     * @param $nestingClose
    private function decryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) {

        $bodytext = str_replace(NESTING_TOKEN_OPEN, "$nestingOpen\n", $bodytext);
        $bodytext = str_replace(NESTING_TOKEN_CLOSE, "$nestingClose\n", $bodytext);