Newer
Older

Carsten Rose
committed
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 3/18/16
* Time: 5:43 PM
*/
namespace IMATHUZH\Qfq\Core;
use IMATHUZH\Qfq\Core\Helper\Support;

Carsten Rose
committed
const NESTING_TOKEN_OPEN = '#&nesting-open-&#';
const NESTING_TOKEN_CLOSE = '#&nesting-close&#';
const NESTING_TOKEN_LENGTH = 17;

Carsten Rose
committed
/**
* Class BodytextParser
* @package qfq
*/
class BodytextParser {

Carsten Rose
committed
/**
* @return mixed|string

Carsten Rose
committed
*/
$nestingOpen = '';
$nestingClose = '';
$bodyText = $this->trimAndRemoveCommentAndEmptyLine($bodyText, $nestingOpen, $nestingClose);
// Encrypt double curly braces to prevent false positives with nesting: form = {{form}}\n
$bodyText = Support::encryptDoubleCurlyBraces($bodyText);
$bodyText = $this->joinLine($bodyText, $nestingOpen, $nestingClose);
$bodyText = $this->encryptNestingDelimeter($bodyText, $nestingOpen, $nestingClose);
$bodyText = $this->unNest($bodyText, $nestingOpen, $nestingClose);
$bodyText = $this->trimAndRemoveCommentAndEmptyLine($bodyText, $nestingOpen, $nestingClose);
$bodyText = Support::decryptDoubleCurlyBraces($bodyText);

Carsten Rose
committed
json_encode([ERROR_MESSAGE_TO_USER => 'Report: Missing close delimiter', ERROR_MESSAGE_TO_DEVELOPER => $bodyText]), ERROR_MISSING_CLOSE_DELIMITER);

Carsten Rose
committed
}
/**
* Trim all lines, remove all empty lines and all lines which start with '#'

Carsten Rose
committed
* @param $bodytext
* @param $nestingOpen
* @param $nestingClose

Carsten Rose
committed
* @return string
*/
private function trimAndRemoveCommentAndEmptyLine($bodytext, &$nestingOpen, &$nestingClose) {

Carsten Rose
committed
$data = array();
$src = explode(PHP_EOL, $bodytext);
if ($src === false) {
return '';
}
$firstLine = trim($src[0]);

Carsten Rose
committed
foreach ($src as $row) {
$row = trim($row);

Carsten Rose
committed
if ($row === '' || $row[0] === '#') {
continue;
}
$data[] = $row;
}
$this->setNestingToken($firstLine, $nestingOpen, $nestingClose);

Carsten Rose
committed
return implode(PHP_EOL, $data);
}
* 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 {}.

Carsten Rose
committed
$nestingOpen = '{';
$nestingClose = '}';
if ($firstLine[0] === '#') {
$token = substr($firstLine, -1);

Carsten Rose
committed
switch($token) {
case '<':
$nestingOpen = '<';
$nestingClose = '>';
break;
case '[':
$nestingOpen = '[';
$nestingClose = ']';
break;
case '(':
$nestingOpen = '(';
$nestingClose = ')';
break;
default:
break;
}
}

Carsten Rose
committed
/**
* 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*=

Carsten Rose
committed
*
* @param $bodyText
* @param $nestingOpen
* @param $nestingClose

Carsten Rose
committed
* @return string
*/
private function joinLine($bodyText, $nestingOpen, $nestingClose) {

Carsten Rose
committed
$data = array();
$bodytextArray = explode(PHP_EOL, $bodyText);

Carsten Rose
committed
$nestingOpenRegexp = $nestingOpen;
if ($nestingOpen === '(' || $nestingOpen === '[') {
$nestingOpenRegexp = '\\' . $nestingOpen;
}

Carsten Rose
committed
$full = '';
$joinDelimiter = ' ';

Carsten Rose
committed
foreach ($bodytextArray as $row) {
// 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))

Carsten Rose
committed
) {
// if there is already something: save this.
if ($full !== '') {

Carsten Rose
committed
$data[] = $full;
}

Carsten Rose
committed
// start new line
$full = $row;
} else {
// 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.
$full .= $joinDelimiter . $row;

Carsten Rose
committed
}
$joinDelimiter = $joinDelimiterNext;

Carsten Rose
committed
}
// Save last line
if ($full !== '') {

Carsten Rose
committed
$data[] = $full;
}

Carsten Rose
committed
return implode(PHP_EOL, $data);
}
/**
* 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 $nestingOpen
* @param $nestingClose
private function unNest($bodytext, $nestingOpen, $nestingClose) {

Carsten Rose
committed
// Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested'
// $bodytext = str_replace('\{', '#&[_#', $bodytext);
// $bodytext = str_replace('\}', '#&]_#', $bodytext);
// $bodytext = Support::encryptDoubleCurlyBraces($bodytext);

Carsten Rose
committed
$result = $bodytext;
$posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);

Carsten Rose
committed
while ($posFirstClose !== false) {
$posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN);

Carsten Rose
committed
if ($posMatchOpen === false) {
$result = $this->decryptNestingDelimeter($result, $nestingOpen, $nestingClose);
json_encode([ERROR_MESSAGE_TO_USER => 'Missing open delimiter', ERROR_MESSAGE_TO_DEVELOPER => "Missing open delimiter: $result"]),
ERROR_MISSING_OPEN_DELIMITER);

Carsten Rose
committed
}
$pre = substr($result, 0, $posMatchOpen);
if ($pre === false)
$pre = '';
$post = substr($result, $posFirstClose + NESTING_TOKEN_LENGTH);

Carsten Rose
committed
if ($post === false)
$post = '';
// trim also removes '\n'
$match = trim(substr($result, $posMatchOpen + NESTING_TOKEN_LENGTH, $posFirstClose - $posMatchOpen - NESTING_TOKEN_LENGTH));

Carsten Rose
committed
// "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;
}

Carsten Rose
committed
}
$result = $pre . $post;
$posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);

Carsten Rose
committed
}
// $result = str_replace('#&[_#', '{', $result);
// $result = str_replace('#&]_#', '}', $result);
// $result = Support::decryptDoubleCurlyBraces($result);

Carsten Rose
committed
return $result;
}
/**
* Decrypt complex token by '{\n' and '}\n'
*
* @param $bodytext
* @param $nestingOpen
* @param $nestingClose
* @return mixed
*/
private function decryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) {
$bodytext = str_replace(NESTING_TOKEN_OPEN, "$nestingOpen\n", $bodytext);
$bodytext = str_replace(NESTING_TOKEN_CLOSE, "$nestingClose\n", $bodytext);
return $bodytext;
}