public static function parse(
        $template,
        array $tokens,
        $pre = '%%',
        $post = '%%',
        $flags = self::FLAG_RECURSIVE | self::FLAG_STRICT
    ) {
        // nothing to do with empty templates or when no tokens given
        if (empty($template) || empty($tokens)) {
            return $template;
        }

        // FLAG_EMPTY_MISSING and FLAG_STRICT are mutually exclusive
        if (($flags & self::FLAG_EMPTY_MISSING) &&  ($flags & self::FLAG_STRICT)) {
            throw new InvalidArgumentException("Can't use FLAG_EMPTY_MISSING and FLAG_STRICT together");
        }

        // replace the tokens with their values
        $result = $template;
        foreach ($tokens as $token => $replacement) {
            $token = "$pre$token$post";
            $result = str_replace($token, $replacement, $result);
        }

        if ($flags & self::FLAG_RECURSIVE) {
            $recursiveResult = self::parse($result, $tokens, $pre, $post, $flags xor self::FLAG_RECURSIVE);
            if ($result <> $recursiveResult) {
                $result = self::parse($result, $pre, $post, $flags);
            }
        }

        // check for any tokens left in the result
        $remainingTokens = self::getTokens($result, $pre, $post);

        // ok if no tokens left
        if (empty($remainingTokens)) {
            return $result;
        }

        // throw exception if in strict mode
        if ($flags & self::FLAG_STRICT) {
            throw new RuntimeException("Missing values for [" . implode(", ", $remainingTokens) . "] tokens");
        }

        // replace unknown tokens with empty string if FLAG_EMPTY_MISSING
        if ($flags & self::FLAG_EMPTY_MISSING) {
            $tokens = [];
            foreach ($remainingTokens as $token) {
                $tokens[$token] = "";
            }

            return self::parse($result, $tokens, $pre, $post);
        }


        return $result;
    }