VvebOIDC/system/sqlp/lexer.php

268 lines
6.2 KiB
PHP

<?php
/**
* Vvveb
*
* Copyright (C) 2022 Ziadin Givan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Vvveb\System\Sqlp;
class Lexer {
protected $regex;
protected $offsetToToken;
protected $macroMap;
public function __construct($tokenMap, $macroMap) {
$this->regex = '/(' . implode(')|(', array_keys($tokenMap)) . ')/As';
$this->offsetToToken = array_values($tokenMap);
$this->macroMap = $macroMap;
}
public function lex($string) {
$tokens = [];
$offset = 0;
$sql = '';
while (isset($string[$offset])) {
if (! preg_match($this->regex, $string, $matches, 0, $offset)) {
throw new \Exception(sprintf('Unexpected character "%s" at offset %d', $string[$offset], $offset));
}
// find the first non-empty element (but skipping $matches[0]) using a quick for loop
for ($i = 1; '' === $matches[$i]; ++$i);
$token = $this->offsetToToken[$i - 1];
//gather all sql tokens into one continuous string
if ($token == 'T_SQL') {
$sql .= $matches[0];
} else {
if ($sql) {
$tokens[] = [$sql, 'T_SQL', $offset];
$sql = '';
}
$tokens[] = [$matches[0], $token, $offset];
}
$offset += strlen($matches[0]);
}
//add remaining sql if any left
if ($sql) {
$tokens[] = [$sql, 'T_SQL', $offset];
}
return $tokens;
}
// a recursive function to build the ast structure
function tree(&$structure, $k=0) {
$output = [];
$count = count($structure);
for ($i= $k; $i < $count; $i++) {
list($element, $type, $offset) = $structure[$i];
$node = $structure[$i];
if ($type == 'T_IF_START') {
$ret = $this->tree($structure, $i + 1);
$node['children'] = $ret[0];
$i = $ret[1];
} else {
if ($type == 'T_IF_END' || $type == 'T_EACH_END') {
$output[] = $node;
return [$output, $i];
} else {
if ($type == 'T_SQL') {
}
}
}
$output[] = $node;
}
return $output;
}
function parseError($structure, $node_idx) {
$node = $structure[$node_idx];
throw(
new \Exception(
sprintf('sqlP parse error, expecting close tag for `%s` at offset %d at `%s`',
$node[0] , $node[2], $this->treeToSql($structure))
));
}
// a recursive function to build the ast structure
function treeMacro(&$structure, $k=0, $level = 0, $startTag = false) {
$output = [];
$count = count($structure);
for ($i = $k; $i < $count; $i++) {
list($element, $type, $offset) = $structure[$i];
$node = $structure[$i];
if ($type == 'T_EACH') {
$ret = $this->treeMacro($structure, $i + 1, $level + 1, $type);
if (is_array($ret) && count($ret) == 2) {
$node['children'] = $ret[0];
$i = $ret[1];
} else {
$this->parseError($structure, $i);
}
} else {
if ($type == 'T_ELSE') {
$ret = $this->treeMacro($structure, $i + 1, $level + 1, $type);
if (is_array($ret) && count($ret) == 2) {
$node['children'] = $ret[0];
$i = $ret[1];
} else {
$this->parseError($structure, $i);
}
}
}
if ($type == 'T_IF_START') {
$ret = $this->treeMacro($structure, $i + 1, $level + 1, $type);
if (is_array($ret) && count($ret) == 2) {
$node['children'] = $ret[0];
$i = $ret[1];
} else {
$this->parseError($structure, $i);
}
} else {
if ($level > 0 && (
($type == 'T_ELSE' && $startTag == 'T_IF_START') ||
($type == 'T_IF_END' && $startTag == 'T_ELSE') ||
($type == 'T_IF_END' && $startTag == 'T_IF_START') ||
($type == 'T_EACH_END' && $startTag == 'T_EACH')
)) {
$output[] = $node;
return [$output, $i];
} else {
if ($type == 'T_SQL') {
}
}
}
$output[] = $node;
}
return $output;
}
function treeToSql(&$tree) {
$sql = '';
foreach ($tree as $token) {
$sql .= $token[0];
if (isset($token['children'])) {
$sql .= $this->treeToSql($token['children']);
}
}
return $sql;
}
function parseMacro($statement, $regex, $template) {
$macro = $template;
if (preg_match_all($regex, $statement, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$macro = $template;
//replace macro template variables %$variable
$macro = preg_replace_callback(
'@\$%(\w+)@',
function ($varMatch) use ($match) {
return
preg_replace_callback(
'/:([\w\.]+)/ms',
function ($matches) {
return '$' . \Vvveb\dotToArrayKey('params.' . $matches[1]);
//return '$params[\'' . $matches[1] . '\']';
},
$match[$varMatch[1]]);
},
$macro);
//@result
//replace result variables
$macro =
preg_replace_callback(
'/@result\.([\w\.]+)/',
function ($matches) {
return \Vvveb\dotToArrayKey('$results.' . $matches[1]);
},
$macro);
//replace macro template placeholders %placeholder
$macro = preg_replace_callback(
'@\%(\w+)@',
function ($varMatch) use ($match) {
return $match[$varMatch[1]];
},
$macro);
$statement = str_replace($match[0], $macro, $statement);
}
}
return $statement;
}
function treeToPhp(&$tree, &$macroMap) {
$sql = '';
foreach ($tree as $token) {
$name = $token[1];
$code = $token[0];
if ($name == 'T_SQL') {
$sql .= $code;
} elseif (isset($macroMap[$name])) {
$macro = $macroMap[$name];
if (is_array($macro)) {
$regex = $macro[0];
$string = $macro[1];
$sql .= $this->parseMacro($code, $regex, $string);
} else {
$sql .= $macro;
}
}
if (isset($token['children'])) {
$sql .= $this->treeToPhp($token['children'], $macroMap);
}
}
return $sql;
}
}