aboutsummaryrefslogtreecommitdiffstats
path: root/core/namespace/Parsers/FunctionParser.php
blob: 5b2656e2ff3808a2b7fe223f79ab74ba967585f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php
namespace Parsers;
use ReflectionFunction;

class FunctionParser implements ParserInterface {
	private static $functions = [];

	#===============================================================================
	# Main regex for matching the whole function call
	#===============================================================================
	private const FUNCTION_PATTERN = '/
	\{\s?                         # Opening curly brace `{`
		(?<func>%s)               # The function name in uppercase letters
		(?:\:\s                   # A colon `:` followed by a required blank
			(?<arg_list>          # Capture group for the whole argument list
				(?:%s)+           # One or more arguments (ARGUMENT_PATTERN_PARTIAL)
			)
		)?
	\s?\}                         # Closing curly brace `}`
	/x';

	#===============================================================================
	# Partial regex for matching/splitting the argument list
	# - Thanks to @OnlineCop from the #regex community of the Libera IRC network! <3
	#===============================================================================
	private const ARGUMENT_PATTERN_PARTIAL =
	'(?<arg>                      # Either a quoted string or a plain number
		(?<qmark>["\'])           # Either a single or double quote
		(?>[^"\'\\\]++            # String between the quotes
		|    [\\\].               # A `\` followed by anything but literal newline
		|    (?!\k<qmark>)["\']   # A quote, but not our opening quote
		)*+
		\k<qmark>                 # Closing quote (same as opening quote)
		|
		[0-9]+                    # ... or just a plain number
	)
	(?:,\s*)?';

	#===============================================================================
	# Register function
	#===============================================================================
	public static function register(string $name, callable $callback): void {
		$Function = new ReflectionFunction($callback);
		self::$functions[$name] = [
			'callback' => $callback,
			'required' => $Function->getNumberOfRequiredParameters()
		];
	}

	#===============================================================================
	# Parse functions
	#===============================================================================
	public function parse(string $text): array {
		$functionNames = array_keys(self::$functions);
		$functionNames = implode('|', $functionNames);

		$pattern = self::FUNCTION_PATTERN;
		$options = self::ARGUMENT_PATTERN_PARTIAL;

		preg_match_all(sprintf($pattern, $functionNames, $options), $text, $matches);

		foreach(array_map(function($name, $parameters) {
			return [$name , $this->parseParameterString($parameters)];
		}, $matches['func'], $matches['arg_list']) as $match) {
			$functions[$match[0]][] = $match[1];
		}

		return $functions ?? [];
	}

	#===============================================================================
	# Transform functions
	#===============================================================================
	public function transform(string $text): string {
		$functionData = self::$functions;
		$functionNames = array_keys($functionData);
		$functionNames = implode('|', $functionNames);

		$pattern = self::FUNCTION_PATTERN;
		$options = self::ARGUMENT_PATTERN_PARTIAL;

		return preg_replace_callback(sprintf($pattern, $functionNames, $options),
		function($matches) use($functionData) {
			$function = $matches['func'];
			$callback = $functionData[$function]['callback'];
			$required = $functionData[$function]['required'];

			$arguments = $this->parseParameterString($matches['arg_list'] ?? '');

			if(count($arguments) < $required) {
				return sprintf('`{%s: *Missing arguments*}`', $function);
			}

			return $callback(...$arguments);
		}, $text);
	}

	#===============================================================================
	# Parse the parameter string found within the function shell
	#===============================================================================
	private function parseParameterString(string $parameters): array {
		$pattern = sprintf('/%s/x', self::ARGUMENT_PATTERN_PARTIAL);
		preg_match_all($pattern, $parameters, $matches);

		return array_map(function($arg, $qmark) {
			if(!$qmark) {
				return $arg;
			}

			# If a quotation mark is matched, the argument has been enclosed
			# between quotation marks when passed to the content function in
			# the editor. Therefore, the quotation marks must be removed and
			# we also need to take care of the backslash-escaped occurrences
			# of the quotation marks inside the argument string.

			$arg = substr($arg, 1);
			$arg = substr($arg, 0, strlen($arg)-1);
			$arg = str_replace('\\'.$qmark, $qmark, $arg);

			return $arg;
		}, $matches['arg'], $matches['qmark']);
	}
}