]> git.mxchange.org Git - friendica.git/commitdiff
Add more sub consoles
authorHypolite Petovan <mrpetovan@gmail.com>
Sun, 18 Mar 2018 17:26:36 +0000 (13:26 -0400)
committerHypolite Petovan <mrpetovan@gmail.com>
Sun, 18 Mar 2018 17:26:36 +0000 (13:26 -0400)
src/Core/Console.php
src/Core/Console/Extract.php [new file with mode: 0644]
src/Core/Console/GlobalCommunitySilence.php [new file with mode: 0644]
src/Core/Console/Maintenance.php [new file with mode: 0644]
src/Core/Console/PhpToPo.php [new file with mode: 0644]

index f9c37dde1ec9d64bbb7ebebb7eb45bf0ba9ca335..39e2941a32950edcc8977f2b4d0a2e272ff4805e 100644 (file)
@@ -95,8 +95,16 @@ HELP;
                                break;\r
                        case 'docbloxerrorchecker' : $subconsole = new Console\DocBloxErrorChecker($subargs);\r
                                break;\r
+                       case 'extract' : $subconsole = new Console\Extract($subargs);\r
+                               break;\r
                        case 'globalcommunityblock': $subconsole = new Console\GlobalCommunityBlock($subargs);\r
                                break;\r
+                       case 'globalcommunitysilence': $subconsole = new Console\GlobalCommunitySilence($subargs);\r
+                               break;\r
+                       case 'maintenance': $subconsole = new Console\Maintenance($subargs);\r
+                               break;\r
+                       case 'php2po': $subconsole = new Console\PhpToPo($subargs);\r
+                               break;\r
                        default:\r
                                throw new \Asika\SimpleConsole\CommandArgsException('Command ' . $command . ' doesn\'t exist');\r
                }\r
diff --git a/src/Core/Console/Extract.php b/src/Core/Console/Extract.php
new file mode 100644 (file)
index 0000000..e6cab06
--- /dev/null
@@ -0,0 +1,140 @@
+<?php\r
+\r
+namespace Friendica\Core\Console;\r
+\r
+/**\r
+ * Extracts translation strings from the Friendica project's files to be exported\r
+ * to Transifex for translation.\r
+ *\r
+ * Outputs a PHP file with language strings used by Friendica\r
+ *\r
+ * @author Hypolite Petovan <mrpetovan@gmail.com>\r
+ */\r
+class Extract extends \Asika\SimpleConsole\Console\r
+{\r
+       protected $helpOptions = ['h', 'help', '?'];\r
+\r
+       protected function getHelp()\r
+       {\r
+               $help = <<<HELP\r
+console extract - Generate translation string file for the Friendica project (deprecated)\r
+Usage\r
+       bin/console extract [-h|--help|-?] [-v]\r
+\r
+Description\r
+       This script was used to generate the translation string file to be exported to Transifex,\r
+       please use bin/run_xgettext.sh instead\r
+\r
+Options\r
+    -h|--help|-? Show help information\r
+    -v           Show more debug information.\r
+HELP;\r
+               return $help;\r
+       }\r
+\r
+       protected function doExecute()\r
+       {\r
+               if ($this->getOption('v')) {\r
+                       $this->out('Class: ' . __CLASS__);\r
+                       $this->out('Arguments: ' . var_export($this->args, true));\r
+                       $this->out('Options: ' . var_export($this->options, true));\r
+               }\r
+\r
+               if (count($this->args) > 0) {\r
+                       throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');\r
+               }\r
+\r
+               $s = '<?php' . PHP_EOL;\r
+               $s .= '\r
+               function string_plural_select($n){\r
+                       return ($n != 1);\r
+               }\r
+\r
+               ';\r
+\r
+               $arr = [];\r
+\r
+               $files = array_merge(\r
+                       ['index.php', 'boot.php'],\r
+                       glob('mod/*'),\r
+                       glob('include/*'),\r
+                       glob('addon/*/*'),\r
+                       $this->globRecursive('src')\r
+               );\r
+\r
+               foreach ($files as $file) {\r
+                       $str = file_get_contents($file);\r
+\r
+                       $pat = '|L10n::t\(([^\)]*+)[\)]|';\r
+                       $patt = '|L10n::tt\(([^\)]*+)[\)]|';\r
+\r
+                       $matches = [];\r
+                       $matchestt = [];\r
+\r
+                       preg_match_all($pat, $str, $matches);\r
+                       preg_match_all($patt, $str, $matchestt);\r
+\r
+                       if (count($matches) || count($matchestt)) {\r
+                               $s .= '// ' . $file . PHP_EOL;\r
+                       }\r
+\r
+                       if (!empty($matches[1])) {\r
+                               foreach ($matches[1] as $long_match) {\r
+                                       $match_arr = preg_split('/(?<=[\'"])\s*,/', $long_match);\r
+                                       $match = $match_arr[0];\r
+                                       if (!in_array($match, $arr)) {\r
+                                               if (substr($match, 0, 1) == '$') {\r
+                                                       continue;\r
+                                               }\r
+\r
+                                               $arr[] = $match;\r
+\r
+                                               $s .= '$a->strings[' . $match . '] = ' . $match . ';' . "\n";\r
+                                       }\r
+                               }\r
+                       }\r
+                       if (!empty($matchestt[1])) {\r
+                               foreach ($matchestt[1] as $match) {\r
+                                       $matchtkns = preg_split("|[ \t\r\n]*,[ \t\r\n]*|", $match);\r
+                                       if (count($matchtkns) == 3 && !in_array($matchtkns[0], $arr)) {\r
+                                               if (substr($matchtkns[1], 0, 1) == '$') {\r
+                                                       continue;\r
+                                               }\r
+\r
+                                               $arr[] = $matchtkns[0];\r
+\r
+                                               $s .= '$a->strings[' . $matchtkns[0] . "] = array(\n";\r
+                                               $s .= "\t0 => " . $matchtkns[0] . ",\n";\r
+                                               $s .= "\t1 => " . $matchtkns[1] . ",\n";\r
+                                               $s .= ");\n";\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+\r
+               $s .= '// Timezones' . PHP_EOL;\r
+\r
+               $zones = timezone_identifiers_list();\r
+               foreach ($zones as $zone) {\r
+                       $s .= '$a->strings[\'' . $zone . '\'] = \'' . $zone . '\';' . "\n";\r
+               }\r
+\r
+               $this->out($s);\r
+\r
+               return 0;\r
+       }\r
+\r
+       private function globRecursive($path) {\r
+               $dir_iterator = new \RecursiveDirectoryIterator($path);\r
+               $iterator = new \RecursiveIteratorIterator($dir_iterator, \RecursiveIteratorIterator::SELF_FIRST);\r
+\r
+               $return = [];\r
+               foreach ($iterator as $file) {\r
+                       if ($file->getBasename() != '.' && $file->getBasename() != '..') {\r
+                               $return[] = $file->getPathname();\r
+                       }\r
+               }\r
+\r
+               return $return;\r
+       }\r
+}\r
diff --git a/src/Core/Console/GlobalCommunitySilence.php b/src/Core/Console/GlobalCommunitySilence.php
new file mode 100644 (file)
index 0000000..069750a
--- /dev/null
@@ -0,0 +1,94 @@
+<?php\r
+\r
+namespace Friendica\Core\Console;\r
+\r
+use Friendica\Core\Protocol;\r
+use Friendica\Database\DBM;\r
+use Friendica\Network\Probe;\r
+\r
+require_once 'include/text.php';\r
+\r
+/**\r
+ * @brief tool to silence accounts on the global community page\r
+ *\r
+ * With this tool, you can silence an account on the global community page.\r
+ * Postings from silenced accounts will not be displayed on the community\r
+ * page. This silencing does only affect the display on the community page,\r
+ * accounts following the silenced accounts will still get their postings.\r
+ *\r
+ * License: AGPLv3 or later, same as Friendica\r
+ *\r
+ * @author Tobias Diekershoff\r
+ * @author Hypolite Petovan <mrpetovan@gmail.com>\r
+ */\r
+class GlobalCommunitySilence extends \Asika\SimpleConsole\Console\r
+{\r
+       protected $helpOptions = ['h', 'help', '?'];\r
+\r
+       protected function getHelp()\r
+       {\r
+               $help = <<<HELP\r
+console globalcommunitysilence - Silence remote profile from global community page\r
+Usage\r
+       bin/console globalcommunitysilence <profile_url> [-h|--help|-?] [-v]\r
+\r
+Description\r
+       With this tool, you can silence an account on the global community page.\r
+       Postings from silenced accounts will not be displayed on the community page.\r
+       This silencing does only affect the display on the community page, accounts\r
+       following the silenced accounts will still get their postings.\r
+\r
+Options\r
+    -h|--help|-? Show help information\r
+    -v           Show more debug information.\r
+HELP;\r
+               return $help;\r
+       }\r
+\r
+       protected function doExecute()\r
+       {\r
+               if ($this->getOption('v')) {\r
+                       $this->out('Class: ' . __CLASS__);\r
+                       $this->out('Arguments: ' . var_export($this->args, true));\r
+                       $this->out('Options: ' . var_export($this->options, true));\r
+               }\r
+\r
+               if (count($this->args) == 0) {\r
+                       $this->out($this->getHelp());\r
+                       return 0;\r
+               }\r
+\r
+               if (count($this->args) > 1) {\r
+                       throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');\r
+               }\r
+\r
+               require_once '.htconfig.php';\r
+               $result = \dba::connect($db_host, $db_user, $db_pass, $db_data);\r
+               unset($db_host, $db_user, $db_pass, $db_data);\r
+\r
+               if (!$result) {\r
+                       throw new \RuntimeException('Unable to connect to database');\r
+               }\r
+\r
+               /**\r
+                * 1. make nurl from last parameter\r
+                * 2. check DB (contact) if there is a contact with uid=0 and that nurl, get the ID\r
+                * 3. set the flag hidden=1 for the contact entry with the found ID\r
+                * */\r
+               $net = Probe::uri($this->getArgument(0));\r
+               if (in_array($net['network'], [Protocol::PHANTOM, Protocol::MAIL])) {\r
+                       throw new \RuntimeException('This account seems not to exist.');\r
+               }\r
+\r
+               $nurl = normalise_link($net['url']);\r
+               $contact = \dba::selectFirst("contact", ["id"], ["nurl" => $nurl, "uid" => 0]);\r
+               if (DBM::is_result($contact)) {\r
+                       \dba::update("contact", ["hidden" => true], ["id" => $contact["id"]]);\r
+                       $this->out('NOTICE: The account should be silenced from the global community page');\r
+               } else {\r
+                       throw new \RuntimeException('NOTICE: Could not find any entry for this URL (' . $nurl . ')');\r
+               }\r
+\r
+               return 0;\r
+       }\r
+}\r
diff --git a/src/Core/Console/Maintenance.php b/src/Core/Console/Maintenance.php
new file mode 100644 (file)
index 0000000..f9a4f9d
--- /dev/null
@@ -0,0 +1,121 @@
+<?php\r
+\r
+namespace Friendica\Core\Console;\r
+\r
+use Friendica\Core;\r
+\r
+require_once 'boot.php';\r
+require_once 'include/dba.php';\r
+\r
+/**\r
+ * @brief tool to silence accounts on the global community page\r
+ *\r
+ * With this tool, you can silence an account on the global community page.\r
+ * Postings from silenced accounts will not be displayed on the community\r
+ * page. This silencing does only affect the display on the community page,\r
+ * accounts following the silenced accounts will still get their postings.\r
+ *\r
+ * Usage: pass the URL of the profile to be silenced account as only parameter\r
+ *        at the command line when running this tool. E.g.\r
+ *\r
+ *        $> util/global_community_silence.php http://example.com/profile/bob\r
+ *\r
+ *        will silence bob@example.com so that his postings won't appear at\r
+ *        the global community page.\r
+ *\r
+ * License: AGPLv3 or later, same as Friendica\r
+ *\r
+ * @author Tobias Diekershoff\r
+ * @author Hypolite Petovan <mrpetovan@gmail.com>\r
+ */\r
+class Maintenance extends \Asika\SimpleConsole\Console\r
+{\r
+       protected $helpOptions = ['h', 'help', '?'];\r
+\r
+       protected function getHelp()\r
+       {\r
+               $help = <<<HELP\r
+console maintenance - Sets maintenance mode for this node\r
+Usage\r
+       bin/console maintenance <enable> [<reason>] [-h|--help|-?] [-v]\r
+\r
+Description\r
+       <enable> cen be either 0 or 1 to disabled or enable the maintenance mode on this node.\r
+\r
+       <reason> is a quote-enclosed string with the optional reason for the maintenance mode.\r
+\r
+Examples\r
+       bin/console maintenance 1\r
+               Enables the maintenance mode without setting a reason message\r
+\r
+       bin/console maintenance 1 "SSL certification update"\r
+               Enables the maintenance mode with setting a reason message\r
+\r
+       bin/console maintenance 0\r
+               Disables the maintenance mode\r
+\r
+Options\r
+    -h|--help|-? Show help information\r
+    -v           Show more debug information.\r
+HELP;\r
+               return $help;\r
+       }\r
+\r
+       protected function doExecute()\r
+       {\r
+               if ($this->getOption('v')) {\r
+                       $this->out('Class: ' . __CLASS__);\r
+                       $this->out('Arguments: ' . var_export($this->args, true));\r
+                       $this->out('Options: ' . var_export($this->options, true));\r
+               }\r
+\r
+               if (count($this->args) == 0) {\r
+                       $this->out($this->getHelp());\r
+                       return 0;\r
+               }\r
+\r
+               if (count($this->args) > 2) {\r
+                       throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');\r
+               }\r
+\r
+               require_once '.htconfig.php';\r
+               $result = \dba::connect($db_host, $db_user, $db_pass, $db_data);\r
+               unset($db_host, $db_user, $db_pass, $db_data);\r
+\r
+               if (!$result) {\r
+                       throw new \RuntimeException('Unable to connect to database');\r
+               }\r
+\r
+               Core\Config::load();\r
+\r
+               $lang = Core\L10n::getBrowserLanguage();\r
+               Core\L10n::loadTranslationTable($lang);\r
+\r
+               $enabled = intval($this->getArgument(0));\r
+\r
+               Core\Config::set('system', 'maintenance', $enabled);\r
+\r
+               $reason = $this->getArgument(1);\r
+\r
+               if ($enabled && $this->getArgument(1)) {\r
+                       Core\Config::set('system', 'maintenance_reason', $this->getArgument(1));\r
+               } else {\r
+                       Core\Config::set('system', 'maintenance_reason', '');\r
+               }\r
+\r
+               if ($enabled) {\r
+                       $mode_str = "maintenance mode";\r
+               } else {\r
+                       $mode_str = "normal mode";\r
+               }\r
+\r
+               $this->out('System set in ' . $mode_str);\r
+\r
+               if ($enabled && $reason != '') {\r
+                       $this->out('Maintenance reason: ' . $reason);\r
+               }\r
+\r
+               return 0;\r
+       }\r
+\r
+}\r
diff --git a/src/Core/Console/PhpToPo.php b/src/Core/Console/PhpToPo.php
new file mode 100644 (file)
index 0000000..e26ea88
--- /dev/null
@@ -0,0 +1,241 @@
+<?php\r
+\r
+namespace Friendica\Core\Console;\r
+\r
+/**\r
+ * Read a strings.php file and create messages.po in the same directory\r
+ *\r
+ * @author Hypolite Petovan <mrpetovan@gmail.com>\r
+ */\r
+class PhpToPo extends \Asika\SimpleConsole\Console\r
+{\r
+\r
+       protected $helpOptions = ['h', 'help', '?'];\r
+\r
+       private $normBaseMsgIds = [];\r
+       const NORM_REGEXP = "|[\\\]|";\r
+\r
+       protected function getHelp()\r
+       {\r
+               $help = <<<HELP\r
+console php2po - Generate a messages.po file from a string.php file\r
+Usage\r
+       bin/console php2po [-p <n>] <path/to/strings.php> [-h|--help|-?] [-v]\r
+\r
+Options:\r
+       -p <n> Number of plural forms/ Default: 2\r
+\r
+Description\r
+       Read a strings.php file and create the according messages.po in the same directory\r
+\r
+Options\r
+    -h|--help|-? Show help information\r
+    -v           Show more debug information.\r
+HELP;\r
+               return $help;\r
+       }\r
+\r
+       protected function doExecute()\r
+       {\r
+               if ($this->getOption('v')) {\r
+                       $this->out('Class: ' . __CLASS__);\r
+                       $this->out('Arguments: ' . var_export($this->args, true));\r
+                       $this->out('Options: ' . var_export($this->options, true));\r
+               }\r
+\r
+               if (count($this->args) == 0) {\r
+                       $this->out($this->getHelp());\r
+                       return 0;\r
+               }\r
+\r
+               if (count($this->args) > 1) {\r
+                       throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');\r
+               }\r
+\r
+               $a = get_app();\r
+\r
+               $phpfile = realpath($this->getArgument(0));\r
+\r
+               if (!file_exists($phpfile)) {\r
+                       throw new \RuntimeException('Supplied file path doesn\'t exist.');\r
+               }\r
+\r
+               if (!is_writable(dirname($phpfile))) {\r
+                       throw new \RuntimeException('Supplied directory isn\'t writable.');\r
+               }\r
+\r
+               $pofile = dirname($phpfile) . '/messages.po';\r
+\r
+               // start !\r
+               include_once($phpfile);\r
+\r
+               $out = '';\r
+               $out .= "# FRIENDICA Distributed Social Network\n";\r
+               $out .= "# Copyright (C) 2010, 2011, 2012, 2013 the Friendica Project\n";\r
+               $out .= "# This file is distributed under the same license as the Friendica package.\n";\r
+               $out .= "# \n";\r
+               $out .= 'msgid ""' . "\n";\r
+               $out .= 'msgstr ""' . "\n";\r
+               $out .= '"Project-Id-Version: friendica\n"' . "\n";\r
+               $out .= '"Report-Msgid-Bugs-To: \n"' . "\n";\r
+               $out .= '"POT-Creation-Date: ' . date("Y-m-d H:i:sO") . '\n"' . "\n";\r
+               $out .= '"MIME-Version: 1.0\n"' . "\n";\r
+               $out .= '"Content-Type: text/plain; charset=UTF-8\n"' . "\n";\r
+               $out .= '"Content-Transfer-Encoding: 8bit\n"' . "\n";\r
+\r
+               // search for plural info\r
+               $lang = "";\r
+               $lang_logic = "";\r
+               $lang_pnum = 2;\r
+\r
+               $_idx = array_search('-p', $argv);\r
+               if ($_idx !== false) {\r
+                       $lang_pnum = $argv[$_idx + 1];\r
+               }\r
+\r
+               $infile = file($phpfile);\r
+               foreach ($infile as $l) {\r
+                       $l = trim($l);\r
+                       if ($this->startsWith($l, 'function string_plural_select_')) {\r
+                               $lang = str_replace('function string_plural_select_', '', str_replace('($n){', '', $l));\r
+                       }\r
+                       if ($this->startsWith($l, 'return')) {\r
+                               $lang_logic = str_replace('$', '', trim(str_replace('return ', '', $l), ';'));\r
+                               break;\r
+                       }\r
+               }\r
+\r
+               $this->out('Language: ' . $lang);\r
+               $this->out('Plural forms: ' . $lang_pnum);\r
+               $this->out('Plural forms: ' . $lang_logic);\r
+\r
+               $out .= sprintf('"Language: %s\n"', $lang) . "\n";\r
+               $out .= sprintf('"Plural-Forms: nplurals=%s; plural=%s;\n"', $lang_pnum, $lang_logic) . "\n";\r
+               $out .= "\n";\r
+\r
+               $this->out('Loading base message.po...');\r
+\r
+               // load base messages.po and extract msgids\r
+               $base_msgids = [];\r
+               $base_f = file("util/messages.po");\r
+               if (!$base_f) {\r
+                       throw new \RuntimeException('The base util/messages.po file is missing.');\r
+               }\r
+\r
+               $_f = 0;\r
+               $_mid = "";\r
+               $_mids = [];\r
+               foreach ($base_f as $l) {\r
+                       $l = trim($l);\r
+\r
+                       if ($this->startsWith($l, 'msgstr')) {\r
+                               if ($_mid != '""') {\r
+                                       $base_msgids[$_mid] = $_mids;\r
+                                       $this->normBaseMsgIds[preg_replace(self::NORM_REGEXP, "", $_mid)] = $_mid;\r
+                               }\r
+\r
+                               $_f = 0;\r
+                               $_mid = "";\r
+                               $_mids = [];\r
+                       }\r
+\r
+                       if ($this->startsWith($l, '"') && $_f == 2) {\r
+                               $_mids[count($_mids) - 1] .= "\n" . $l;\r
+                       }\r
+                       if ($this->startsWith($l, 'msgid_plural ')) {\r
+                               $_f = 2;\r
+                               $_mids[] = str_replace('msgid_plural ', '', $l);\r
+                       }\r
+\r
+                       if ($this->startsWith($l, '"') && $_f == 1) {\r
+                               $_mid .= "\n" . $l;\r
+                               $_mids[count($_mids) - 1] .= "\n" . $l;\r
+                       }\r
+                       if ($this->startsWith($l, 'msgid ')) {\r
+                               $_f = 1;\r
+                               $_mid = str_replace('msgid ', '', $l);\r
+                               $_mids = [$_mid];\r
+                       }\r
+               }\r
+\r
+               $this->out('Done.');\r
+               $this->out('Creating ' . $pofile . '...');\r
+\r
+               // create msgid and msgstr\r
+               $warnings = "";\r
+               foreach ($a->strings as $key => $str) {\r
+                       $msgid = $this->massageString($key);\r
+\r
+                       if (preg_match("|%[sd0-9](\$[sn])*|", $msgid)) {\r
+                               $out .= "#, php-format\n";\r
+                       }\r
+                       $msgid = $this->findOriginalMsgId($msgid);\r
+                       $out .= 'msgid ' . $msgid . "\n";\r
+\r
+                       if (is_array($str)) {\r
+                               if (array_key_exists($msgid, $base_msgids) && isset($base_msgids[$msgid][1])) {\r
+                                       $out .= 'msgid_plural ' . $base_msgids[$msgid][1] . "\n";\r
+                               } else {\r
+                                       $out .= 'msgid_plural ' . $msgid . "\n";\r
+                                       $warnings .= "[W] No source plural form for msgid:\n" . str_replace("\n", "\n\t", $msgid) . "\n\n";\r
+                               }\r
+                               foreach ($str as $n => $msgstr) {\r
+                                       $out .= 'msgstr[' . $n . '] ' . $this->massageString($msgstr) . "\n";\r
+                               }\r
+                       } else {\r
+                               $out .= 'msgstr ' . $this->massageString($str) . "\n";\r
+                       }\r
+\r
+                       $out .= "\n";\r
+               }\r
+\r
+               file_put_contents($pofile, $out);\r
+\r
+               $this->out('Done.');\r
+\r
+               if ($warnings == "") {\r
+                       $this->out('No warnings.');\r
+               } else {\r
+                       $this->out($warnings);\r
+               }\r
+\r
+               return 0;\r
+       }\r
+\r
+       private function startsWith($haystack, $needle)\r
+       {\r
+               // search backwards starting from haystack length characters from the end\r
+               return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;\r
+       }\r
+\r
+       /**\r
+        * Get a string and retun a message.po ready text\r
+        * - replace " with \"\r
+        * - replace tab char with \t\r
+        * - manage multiline strings\r
+        */\r
+       private function massageString($str)\r
+       {\r
+               $str = str_replace('\\', '\\\\', $str);\r
+               $str = str_replace('"', '\"', $str);\r
+               $str = str_replace("\t", '\t', $str);\r
+               $str = str_replace("\n", '\n"' . "\n" . '"', $str);\r
+               if (strpos($str, "\n") !== false && $str[0] !== '"') {\r
+                       $str = '"' . "\n" . $str;\r
+               }\r
+\r
+               $str = preg_replace("|\n([^\"])|", "\n\"$1", $str);\r
+               return sprintf('"%s"', $str);\r
+       }\r
+\r
+       private function findOriginalMsgId($str)\r
+       {\r
+               $norm_str = preg_replace(self::NORM_REGEXP, "", $str);\r
+               if (array_key_exists($norm_str, $this->normBaseMsgIds)) {\r
+                       return $this->normBaseMsgIds[$norm_str];\r
+               }\r
+\r
+               return $str;\r
+       }\r
+\r
+}\r