1<?php
2// intlmsg.php -- HotCRP helper functions for message i18n
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class IntlMsg {
6    public $context;
7    public $otext;
8    public $require;
9    public $priority = 0.0;
10    public $next;
11
12    private function arg(IntlMsgSet $ms, $args, $which) {
13        if (ctype_digit($which))
14            return get($args, +$which);
15        else
16            return $ms->get($which);
17    }
18    function check_require(IntlMsgSet $ms, $args) {
19        if (!$this->require)
20            return 0;
21        $nreq = 0;
22        foreach ($this->require as $req) {
23            if (preg_match('/\A\s*\$(\w+)\s*([=!<>]=?|≠|≤|≥)\s*([-+]?(?:\d+\.?\d*|\.\d+))\s*\z/', $req, $m)) {
24                $arg = $this->arg($ms, $args, $m[1]);
25                if ((string) $arg === ""
26                    || !CountMatcher::compare((float) $arg, $m[2], (float) $m[3]))
27                    return false;
28                ++$nreq;
29            } else if (preg_match('/\A\s*\$(\w+)\s*([=!]=?|≠|!?\^=)\s*(\S+)\s*\z/', $req, $m)) {
30                $arg = $this->arg($ms, $args, $m[1]);
31                if ((string) $arg === "")
32                    return false;
33                if ($m[2] === "^=" || $m[2] === "!^=") {
34                    $have = str_starts_with($arg, $m[3]);
35                    $weight = 0.9;
36                } else {
37                    $have = $arg === $m[3];
38                    $weight = 1;
39                }
40                $want = ($m[2] === "=" || $m[2] === "==" || $m[2] === "^=");
41                if ($have !== $want)
42                    return false;
43                $nreq += $weight;
44            } else if (preg_match('/\A\s*(|!)\s*\$(\w+)\s*\z/', $req, $m)) {
45                $arg = $this->arg($ms, $args, $m[2]);
46                $bool_arg = (string) $arg !== "" && $arg !== 0;
47                if ($bool_arg !== ($m[1] === ""))
48                    return false;
49                ++$nreq;
50            }
51        }
52        return $nreq;
53    }
54}
55
56class IntlMsgSet {
57    private $ims = [];
58    private $defs = [];
59    private $_ctx;
60    private $_default_priority;
61
62    function set_default_priority($p) {
63        $this->_default_priority = (float) $p;
64    }
65    function clear_default_priority() {
66        $this->_default_priority = null;
67    }
68
69    function add($m, $ctx = null) {
70        if (is_string($m))
71            $x = $this->addj(func_get_args());
72        else if (!$ctx)
73            $x = $this->addj($m);
74        else {
75            $octx = $this->_ctx;
76            $this->_ctx = $ctx;
77            $x = $this->addj($m);
78            $this->_ctx = $octx;
79        }
80        return $x;
81    }
82
83    function addj($m) {
84        if (is_associative_array($m))
85            $m = (object) $m;
86        if (is_object($m) && isset($m->members) && is_array($m->members)) {
87            $octx = $this->_ctx;
88            if (isset($m->context) && is_string($m->context))
89                $this->_ctx = ((string) $this->_ctx === "" ? "" : $this->_ctx . "/") . $m->context;
90            foreach ($m->members as $mm)
91                $this->addj($mm);
92            $this->_ctx = $octx;
93            return true;
94        }
95        $im = new IntlMsg;
96        if ($this->_default_priority !== null)
97            $im->priority = $this->_default_priority;
98        if (is_array($m)) {
99            $n = count($m);
100            $p = false;
101            while ($n > 0 && !is_string($m[$n - 1])) {
102                if ((is_int($m[$n - 1]) || is_float($m[$n - 1])) && $p === false)
103                    $p = $im->priority = (float) $m[$n - 1];
104                else if (is_array($m[$n - 1]) && $im->require === null)
105                    $im->require = $m[$n - 1];
106                else
107                    return false;
108                --$n;
109            }
110            if ($n < 2 || $n > 3 || !is_string($m[0]) || !is_string($m[1])
111                || ($n === 3 && !is_string($m[2])))
112                return false;
113            if ($n === 3) {
114                $im->context = $m[0];
115                $itext = $m[1];
116                $im->otext = $m[2];
117            } else {
118                $itext = $m[0];
119                $im->otext = $m[1];
120            }
121        } else if (is_object($m)) {
122            if (isset($m->context) && is_string($m->context))
123                $im->context = $m->context;
124            if (isset($m->id) && is_string($m->id))
125                $itext = $m->id;
126            else if (isset($m->itext) && is_string($m->itext))
127                $itext = $m->itext;
128            else
129                return false;
130            if (isset($m->otext) && is_string($m->otext))
131                $im->otext = $m->otext;
132            else if (isset($m->itext) && is_string($m->itext))
133                $im->otext = $m->itext;
134            else
135                return false;
136            if (isset($m->priority) && (is_float($m->priority) || is_int($m->priority)))
137                $im->priority = (float) $m->priority;
138            if (isset($m->require) && is_array($m->require))
139                $im->require = $m->require;
140        } else
141            return false;
142        if ($this->_ctx)
143            $im->context = $this->_ctx . ($im->context ? "/" . $im->context : "");
144        $im->next = get($this->ims, $itext);
145        $this->ims[$itext] = $im;
146        return true;
147    }
148
149    function set($name, $value) {
150        $this->defs[$name] = $value;
151    }
152
153    function get($name) {
154        return get($this->defs, $name);
155    }
156
157    private function find($context, $itext, $args) {
158        $match = null;
159        $matchnreq = $matchctxlen = 0;
160        for ($im = get($this->ims, $itext); $im; $im = $im->next) {
161            $ctxlen = $nreq = 0;
162            if ($context !== null && $im->context !== null) {
163                if ($context === $im->context)
164                    $ctxlen = 10000;
165                else {
166                    $ctxlen = (int) min(strlen($context), strlen($im->context));
167                    if (strncmp($context, $im->context, $ctxlen) !== 0
168                        || ($ctxlen < strlen($context) && $context[$ctxlen] !== "/")
169                        || ($ctxlen < strlen($im->context) && $im->context[$ctxlen] !== "/"))
170                        continue;
171                }
172            } else if ($context === null && $im->context !== null)
173                continue;
174            if ($im->require
175                && ($nreq = $im->check_require($this, $args)) === false)
176                continue;
177            if (!$match
178                || $im->priority > $match->priority
179                || ($im->priority == $match->priority
180                    && ($ctxlen > $matchctxlen
181                        || ($ctxlen == $matchctxlen
182                            && $nreq > $matchnreq)))) {
183                $match = $im;
184                $matchnreq = $nreq;
185                $matchctxlen = $ctxlen;
186            }
187        }
188        return $match;
189    }
190
191    private function expand($args) {
192        $pos = 0;
193        while (($pos = strpos($args[0], "%", $pos)) !== false) {
194            if (preg_match('/\A(?!\d+)\w+(?=[$%])/', substr($args[0], $pos + 1), $m)
195                && isset($this->defs[$m[0]])) {
196                $args[] = $this->defs[$m[0]];
197                $t = substr($args[0], 0, $pos + 1) . (count($args) - 1);
198                $pos += 1 + strlen($m[0]);
199                if ($args[0][$pos] == "%") {
200                    $t .= "\$s";
201                    ++$pos;
202                }
203                $args[0] = $t . substr($args[0], $pos);
204                $pos = strlen($t);
205            } else
206                $pos += 2;
207        }
208        return call_user_func_array("sprintf", $args);
209    }
210
211    function x($itext) {
212        $args = func_get_args();
213        if (($im = $this->find(null, $itext, $args)))
214            $args[0] = $im->otext;
215        return $this->expand($args);
216    }
217
218    function xc($context, $itext) {
219        $args = array_slice(func_get_args(), 1);
220        if (($im = $this->find($context, $itext, $args)))
221            $args[0] = $im->otext;
222        return $this->expand($args);
223    }
224
225    function xi($id, $itext) {
226        $args = array_slice(func_get_args(), 1);
227        if (($im = $this->find(null, $id, $args)))
228            $args[0] = $im->otext;
229        return $this->expand($args);
230    }
231
232    function xci($context, $id, $itext) {
233        $args = array_slice(func_get_args(), 2);
234        if (($im = $this->find($context, $id, $args)))
235            $args[0] = $im->otext;
236        return $this->expand($args);
237    }
238}
239