Commit Diff


commit - 13f23f154c199d25117407bd5be120c16498a74a
commit + 3a4930680e0b3b7e67dc7caff51cd9b2064e80a3
blob - ef6133e32665cd288f65dc489a0eb2ef4b2e6305
blob + a82c515de940a30810efca70c11eba80bb99cdf2
--- gtransl.retro
+++ gtransl.retro
@@ -2,9 +2,10 @@
 
 ~~~
 '\r\n s:format 'CRLF s:const
-:user-input     '10_🔁_Text: CRLF s:append s:put #0 unix:exit ;
-:not-found      '51_Not_found CRLF s:append s:put #0 unix:exit ;
-:bad-request    '59_Bad_request CRLF s:append s:put #0 unix:exit ;
+:vgi:user-input     '10_🔁_Text:         CRLF s:append s:put #0 unix:exit ;
+:vgi:not-found      '51_Not_found        CRLF s:append s:put #0 unix:exit ;
+:vgi:bad-request    '59_Bad_request      CRLF s:append s:put #0 unix:exit ;
+:vgi:tmp-failure    '40_Unexpected_error CRLF s:append s:put #0 unix:exit ;
 ~~~
 
 Разбираем запрашиваемый URL из стандратного потока ввода:
@@ -15,17 +16,17 @@
 * последним извлекаем query строку с текстом, который требуется перевести (константа QUERY).
 ~~~
 :cut-required-path  (s-s)
-    dup '/gtransl/ s:index/string dup n:negative? [ not-found ] if
+    dup '/gtransl/ s:index/string dup n:negative? [ vgi:not-found ] if
     over s:length swap - '/gtransl/ s:length - #1 - s:right
 ;
 :drop-fist-char     (s-s)   dup s:length #1 - s:right ;
 :extract-path-part  (ss-s)
-    swap dup $/ s:index/char n:negative? [ not-found ] if
+    swap dup $/ s:index/char n:negative? [ vgi:not-found ] if
     $/ s:split/char rot s:const
     drop-fist-char
 ;
-:user-input? dup s:length n:zero? [ user-input ] if ;
-:required-query     (s-s)   dup #0 s:fetch $? -eq? [ not-found ] if ;
+:user-input? dup s:length n:zero? [ vgi:user-input ] if ;
+:required-query     (s-s)   dup #0 s:fetch $? -eq? [ vgi:not-found ] if ;
 :extract-query      (s-)    required-query drop-fist-char 'QUERY s:const ;
 s:get
     cut-required-path 'SL extract-path-part 'TL extract-path-part 
@@ -37,7 +38,7 @@ s:get
 что бы исключить возможность исполнения произвольной команды.
 (Command Injection)
 ~~~
-:check-lang  (s-) [ $a $z n:between? [ not-found ] -if ] s:for-each ;
+:check-lang  (s-) [ $a $z n:between? [ vgi:not-found ] -if ] s:for-each ;
 SL check-lang
 TL check-lang
 
@@ -52,7 +53,7 @@ TL check-lang
         dup $. eq?              [ drop ] if;
         dup $- eq?              [ drop ] if;
             $~ eq?              [ ] if;
-        bad-request
+        vgi:bad-request
     ] s:for-each
 ;
 QUERY check-query
@@ -61,25 +62,27 @@ QUERY check-query
 Результат выполнения команды curl будем хранить в буфере из 16-ти килоячеек
 (+ ячейка для ASCII:NULL)
 ~~~
-:BUFFER_SIZE #16384 ;
-'Buffer d:create BUFFER_SIZE #1 + allot
+:html:BUFFER_SIZE #16384 ;
+'html:Buffer d:create html:BUFFER_SIZE #1 + allot
 ~~~
 
-Для выполнения HTTPS-запроса используем curl.
+Для выполнения HTTPS-запроса используем curl или (для тестов) cat из существующего файла с HTML-ответом.
 Чтение результата выполнения команды curl по переданному описателю пайпа происходит до чтения 
 0 символа (не байта). Считаем, что в этом случае пайп закрыт другой стороной.
 ~~~
-:read-curl-output  (h-) BUFFER_SIZE [ dup file:read/c 0; buffer:add ] times ;
-:do-curl
-    Buffer buffer:set
-    'CURLRESP s:empty [ unix:getenv ] sip
-    dup s:length n:zero? [
-        drop
-        QUERY TL SL 'curl_-s_--url-query_sl=%s_--url-query_tl=%s_"https://translate.google.com/m?q=%s" s:format
-        file:R unix:popen read-curl-output unix:pclose
-    ] if;
-    [ buffer:add ] s:for-each
+:html:read-buffer    (h-h)
+    html:Buffer buffer:set html:BUFFER_SIZE [ dup file:read/c 0; buffer:add ] times
 ;
+:html:pipe-command-curl      (-s)
+    QUERY TL SL 'curl_-m_5_-s_--url-query_sl=%s_--url-query_tl=%s_"https://translate.google.com/m?q=%s" s:format
+;
+:html:pipe-command           (-s)
+    'GTRANSLRESPFILE s:empty [ unix:getenv ] sip dup s:length n:zero? 
+    [ drop html:pipe-command-curl ] [ 'cat_%s s:format ] choose
+;
+:html:do-request
+    html:pipe-command file:R unix:popen html:read-buffer unix:pclose
+;
 ~~~
 
 Рабоче-крестьянский парсинг HTML: 
@@ -91,9 +94,15 @@ QUERY check-query
 
 Затем в результате заменяем наиболее часто-используемые escape-последовательности HTML на символы.
 ~~~
-:extract-result-container  (s-s) 
-    '"result-container" s:split/string drop
-    $> s:split/char drop
+:html:get-result-container  (-s) 
+    html:do-request
+
+    html:Buffer '"result-container" s:index/string dup n:negative? [ vgi:tmp-failure ] if
+    html:Buffer s:length swap - html:Buffer swap s:right
+
+    dup $> s:index/char dup n:negative? [ vgi:tmp-failure ] if
+    swap dup s:length rot - s:right
+
     dup $< s:index/char #1 - #1 swap s:substr
 
     '&amp;  '& s:replace-all '&#38; '& s:replace-all '&#x26; '& s:replace-all
@@ -106,7 +115,7 @@ QUERY check-query
 
 Отдаём результат в формате "text/gemini" в стандартный поток вывода
 ~~~
-do-curl buffer:start extract-result-container
+html:get-result-container
 '20_text/gemini CRLF s:append s:put
 '#_🔁_GTransl CRLF s:append s:put
 CRLF s:put
blob - 297fd45b2a2a2a466021ce0e1228e39d3bc6f18d (mode 755)
blob + /dev/null
--- tests.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/sh
-
-echo "Not found tests..."
-echo "gemini://any-key.press/vgi" | ./gtransl.retro | head -n 1 | \
-    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl" | ./gtransl.retro | \
-    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/" | ./gtransl.retro | \
-    head -n 1 | grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransX/auto/ru/?hello" | ./gtransl.retro | head -n 1 | \
-    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/?\" ; ls/ru/?hello" | ./gtransl.retro | head -n 1 | \
-    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/auto/?\" ; ls/?hello" | ./gtransl.retro | head -n 1 | \
-    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
-
-echo "Bad request tests..."
-echo "gemini://any-key.press/vgi/gtransl/auto/ru/?\" ; ls" | ./gtransl.retro | head -n 1 | \
-    grep "^59 Bad request" > /dev/null && echo "passed" || echo "FAILED"
-
-echo "Escaping tests..."
-echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
-    CURLRESP="<div class=\"result-container\">amp=&amp; lt=&lt; gt=&gt; quot=&quot; apos=&apos;</div>" ./gtransl.retro | \
-    head -n 8 | tail -n 1 | \
-    grep "^amp=& lt=< gt=> quot=\" apos='" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
-    CURLRESP="<div class=\"result-container\">amp=&#38; lt=&#60; gt=&#62; quot=&#34; apos=&#39;</div>" ./gtransl.retro | \
-    head -n 8 | tail -n 1 | \
-    grep "^amp=& lt=< gt=> quot=\" apos='" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
-    CURLRESP="<div class=\"result-container\">amp=&#x26; lt=&#x3C; gt=&#x3E; quot=&#x22; apos=&#x27;</div>" ./gtransl.retro | \
-    head -n 8 | tail -n 1 | \
-    grep "^amp=& lt=< gt=> quot=\" apos='" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
-    CURLRESP="<div class=\"result-container\">lt=&#x3c; gt=&#x3e;</div>" ./gtransl.retro | \
-    head -n 8 | tail -n 1 | \
-    grep "^lt=< gt=>" > /dev/null && echo "passed" || echo "FAILED"
-
-echo "Multiline tests..."
-echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
-    CURLRESP="<div class=\"result-container\">hello
-world</div>" ./gtransl.retro | \
-    head -n 8 | tail -n 1 | \
-    grep "^hello$" > /dev/null && echo "passed" || echo "FAILED"
-echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
-    CURLRESP="<div class=\"result-container\">hello
-world</div>" ./gtransl.retro | \
-    head -n 9 | tail -n 1  | \
-    grep "^world" > /dev/null && echo "passed" || echo "FAILED"
blob - /dev/null
blob + 8fec8baad98ef610e4eb3dc983a414fa8dc107c8 (mode 644)
Binary files /dev/null and tests/.tests.sh.swp differ
blob - /dev/null
blob + 4f65256c3c73a6da75fefcffe460af7b7ae6a46b (mode 644)
--- /dev/null
+++ tests/resp_escaped-01.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html><html dir="ltr" lang="en-US"><head><title>Google Translate</title><meta name="google" content="notranslate"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="//ssl.gstatic.com/translate/favicon.ico" sizes="64x64"><style nonce="lXDqFVxzKw1uk3fJPjXcVg">
+    body {
+      font-family: 'Arial', sans-serif;
+      margin: 0;
+    }
+    a:link,
+    a:visited,
+    a:active {
+      color: #1a73e8; /* blue 600 */
+      text-decoration: none;
+    }
+    .header {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #5f6368; /* grey 700 */
+      font-size: 18px;
+      line-height: 21px;
+      padding: 14px 16px;
+    }
+    .logo-image {
+      background-image: url('https://ssl.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_68x28px.svg');
+      background-repeat: no-repeat;
+      height: 48px;
+      width: 68px;
+      position: absolute;
+    }
+    .logo-text {
+      margin-left: 72px;
+      margin-right: 76px;
+      margin-top: 2px;
+    }
+  
+    body {
+      background-color: #f1f3f4; /* grey 100 */
+    }
+    .languages-container {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #3c4043; /* grey 800 */
+      font-size: 14px;
+      height: 48px;
+      padding: 14px 16px;
+    }
+    .sl-and-tl {
+      display: inline-block;
+    }
+    .swap-container {
+      float: right;
+    }
+    html[dir="rtl"] .swap-container {
+      float: left;
+    }
+    .input-container {
+      background-color: #fff;
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+    }
+    .input-field {
+      border: 0;
+      color: #3c4043; /* grey 800 */
+      font-size: 18px;
+      line-height: 24px;
+      padding: 18px 16px;
+      width: 100%;
+    }
+    .translate-button-container {
+      padding: 16px;
+      text-align: right;
+    }
+    html[dir="rtl"] .translate-button-container {
+      text-align: left;
+    }
+    .translate-button {
+      background: #1a73e8; /* blue 600 */
+      border-radius: 4px;
+      color: #fff;
+      font-size: 14px;
+      line-height: 20px;
+      padding: 8px 16px;
+    }
+    .result-container {
+      background: #1a73e8; /* blue 600 */
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+      color: #fff;
+      font-size: 18px;
+      line-height: 24px;
+      margin-bottom: 50px;
+      padding: 16px 16px 40px 16px;
+    }
+    .links-container a {
+      color: #1967d2; /* blue 700 */
+    }
+    .links-container ul {
+      font-size: 12px;
+      line-height: 16px;
+      list-style-type: none;
+      margin: 16px;
+      padding: 0;
+    }
+    .links-container ul li {
+      margin-bottom: 16px;
+    }
+  </style></head><body><div class="header"><div class="logo-image"></div><div class="logo-text">Translate</div></div><div class="languages-container"><div class="sl-and-tl"><a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=sl&amp;hl=en-US">Detect language</a> → <a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=tl&amp;hl=en-US">Russian</a></div></div><div class="input-container"><form action="/m"><input type="hidden" name="sl" value="auto"><input type="hidden" name="tl" value="ru"><input type="hidden" name="hl" value="en-US"><input type="text" aria-label="Source text" name="q" class="input-field" maxlength="2048" value="It&#39;s become clear to me that, no doubt as a consequence of the fact that Project Gemini has been undergoing a very slow and gradual reawakening&quot; after a long period of stasis, not everybody in the community is necessarily on the same page with regard to how much and what kind of change they expect to happen during the specification finalisation phase of the project."><div class="translate-button-container"><input type="submit" value="Translate" class="translate-button"></div></form></div><div class="result-container">amp=&amp; lt=&lt; gt=&gt; quot=&quot; apos=&apos;</div><div class="links-container"><ul><li><a href="https://www.google.com/m?hl=en-US">Google home</a></li><li><a href="https://www.google.com/tools/feedback/survey/xhtml?productId=95112&hl=en-US">Send feedback</a></li><li><a href="https://www.google.com/intl/en-US/policies">Privacy and terms</a></li><li><a href="./full">Switch to full site</a></li></ul></div></body></html>
blob - /dev/null
blob + e32d49970a329b89d1ca194944db520f548d1908 (mode 644)
--- /dev/null
+++ tests/resp_escaped-02.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html><html dir="ltr" lang="en-US"><head><title>Google Translate</title><meta name="google" content="notranslate"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="//ssl.gstatic.com/translate/favicon.ico" sizes="64x64"><style nonce="lXDqFVxzKw1uk3fJPjXcVg">
+    body {
+      font-family: 'Arial', sans-serif;
+      margin: 0;
+    }
+    a:link,
+    a:visited,
+    a:active {
+      color: #1a73e8; /* blue 600 */
+      text-decoration: none;
+    }
+    .header {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #5f6368; /* grey 700 */
+      font-size: 18px;
+      line-height: 21px;
+      padding: 14px 16px;
+    }
+    .logo-image {
+      background-image: url('https://ssl.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_68x28px.svg');
+      background-repeat: no-repeat;
+      height: 48px;
+      width: 68px;
+      position: absolute;
+    }
+    .logo-text {
+      margin-left: 72px;
+      margin-right: 76px;
+      margin-top: 2px;
+    }
+  
+    body {
+      background-color: #f1f3f4; /* grey 100 */
+    }
+    .languages-container {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #3c4043; /* grey 800 */
+      font-size: 14px;
+      height: 48px;
+      padding: 14px 16px;
+    }
+    .sl-and-tl {
+      display: inline-block;
+    }
+    .swap-container {
+      float: right;
+    }
+    html[dir="rtl"] .swap-container {
+      float: left;
+    }
+    .input-container {
+      background-color: #fff;
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+    }
+    .input-field {
+      border: 0;
+      color: #3c4043; /* grey 800 */
+      font-size: 18px;
+      line-height: 24px;
+      padding: 18px 16px;
+      width: 100%;
+    }
+    .translate-button-container {
+      padding: 16px;
+      text-align: right;
+    }
+    html[dir="rtl"] .translate-button-container {
+      text-align: left;
+    }
+    .translate-button {
+      background: #1a73e8; /* blue 600 */
+      border-radius: 4px;
+      color: #fff;
+      font-size: 14px;
+      line-height: 20px;
+      padding: 8px 16px;
+    }
+    .result-container {
+      background: #1a73e8; /* blue 600 */
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+      color: #fff;
+      font-size: 18px;
+      line-height: 24px;
+      margin-bottom: 50px;
+      padding: 16px 16px 40px 16px;
+    }
+    .links-container a {
+      color: #1967d2; /* blue 700 */
+    }
+    .links-container ul {
+      font-size: 12px;
+      line-height: 16px;
+      list-style-type: none;
+      margin: 16px;
+      padding: 0;
+    }
+    .links-container ul li {
+      margin-bottom: 16px;
+    }
+  </style></head><body><div class="header"><div class="logo-image"></div><div class="logo-text">Translate</div></div><div class="languages-container"><div class="sl-and-tl"><a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=sl&amp;hl=en-US">Detect language</a> → <a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=tl&amp;hl=en-US">Russian</a></div></div><div class="input-container"><form action="/m"><input type="hidden" name="sl" value="auto"><input type="hidden" name="tl" value="ru"><input type="hidden" name="hl" value="en-US"><input type="text" aria-label="Source text" name="q" class="input-field" maxlength="2048" value="It&#39;s become clear to me that, no doubt as a consequence of the fact that Project Gemini has been undergoing a very slow and gradual reawakening&quot; after a long period of stasis, not everybody in the community is necessarily on the same page with regard to how much and what kind of change they expect to happen during the specification finalisation phase of the project."><div class="translate-button-container"><input type="submit" value="Translate" class="translate-button"></div></form></div><div class="result-container">amp=&#38; lt=&#60; gt=&#62; quot=&#34; apos=&#39;</div><div class="links-container"><ul><li><a href="https://www.google.com/m?hl=en-US">Google home</a></li><li><a href="https://www.google.com/tools/feedback/survey/xhtml?productId=95112&hl=en-US">Send feedback</a></li><li><a href="https://www.google.com/intl/en-US/policies">Privacy and terms</a></li><li><a href="./full">Switch to full site</a></li></ul></div></body></html>
blob - /dev/null
blob + 3f9e776a95240db4099fa1a7f2c2386ccb44f4c2 (mode 644)
--- /dev/null
+++ tests/resp_escaped-03.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html><html dir="ltr" lang="en-US"><head><title>Google Translate</title><meta name="google" content="notranslate"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="//ssl.gstatic.com/translate/favicon.ico" sizes="64x64"><style nonce="lXDqFVxzKw1uk3fJPjXcVg">
+    body {
+      font-family: 'Arial', sans-serif;
+      margin: 0;
+    }
+    a:link,
+    a:visited,
+    a:active {
+      color: #1a73e8; /* blue 600 */
+      text-decoration: none;
+    }
+    .header {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #5f6368; /* grey 700 */
+      font-size: 18px;
+      line-height: 21px;
+      padding: 14px 16px;
+    }
+    .logo-image {
+      background-image: url('https://ssl.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_68x28px.svg');
+      background-repeat: no-repeat;
+      height: 48px;
+      width: 68px;
+      position: absolute;
+    }
+    .logo-text {
+      margin-left: 72px;
+      margin-right: 76px;
+      margin-top: 2px;
+    }
+  
+    body {
+      background-color: #f1f3f4; /* grey 100 */
+    }
+    .languages-container {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #3c4043; /* grey 800 */
+      font-size: 14px;
+      height: 48px;
+      padding: 14px 16px;
+    }
+    .sl-and-tl {
+      display: inline-block;
+    }
+    .swap-container {
+      float: right;
+    }
+    html[dir="rtl"] .swap-container {
+      float: left;
+    }
+    .input-container {
+      background-color: #fff;
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+    }
+    .input-field {
+      border: 0;
+      color: #3c4043; /* grey 800 */
+      font-size: 18px;
+      line-height: 24px;
+      padding: 18px 16px;
+      width: 100%;
+    }
+    .translate-button-container {
+      padding: 16px;
+      text-align: right;
+    }
+    html[dir="rtl"] .translate-button-container {
+      text-align: left;
+    }
+    .translate-button {
+      background: #1a73e8; /* blue 600 */
+      border-radius: 4px;
+      color: #fff;
+      font-size: 14px;
+      line-height: 20px;
+      padding: 8px 16px;
+    }
+    .result-container {
+      background: #1a73e8; /* blue 600 */
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+      color: #fff;
+      font-size: 18px;
+      line-height: 24px;
+      margin-bottom: 50px;
+      padding: 16px 16px 40px 16px;
+    }
+    .links-container a {
+      color: #1967d2; /* blue 700 */
+    }
+    .links-container ul {
+      font-size: 12px;
+      line-height: 16px;
+      list-style-type: none;
+      margin: 16px;
+      padding: 0;
+    }
+    .links-container ul li {
+      margin-bottom: 16px;
+    }
+  </style></head><body><div class="header"><div class="logo-image"></div><div class="logo-text">Translate</div></div><div class="languages-container"><div class="sl-and-tl"><a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=sl&amp;hl=en-US">Detect language</a> → <a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=tl&amp;hl=en-US">Russian</a></div></div><div class="input-container"><form action="/m"><input type="hidden" name="sl" value="auto"><input type="hidden" name="tl" value="ru"><input type="hidden" name="hl" value="en-US"><input type="text" aria-label="Source text" name="q" class="input-field" maxlength="2048" value="It&#39;s become clear to me that, no doubt as a consequence of the fact that Project Gemini has been undergoing a very slow and gradual reawakening&quot; after a long period of stasis, not everybody in the community is necessarily on the same page with regard to how much and what kind of change they expect to happen during the specification finalisation phase of the project."><div class="translate-button-container"><input type="submit" value="Translate" class="translate-button"></div></form></div><div class="result-container">amp=&#x26; lt=&#x3C; gt=&#x3E; quot=&#x22; apos=&#x27;</div><div class="links-container"><ul><li><a href="https://www.google.com/m?hl=en-US">Google home</a></li><li><a href="https://www.google.com/tools/feedback/survey/xhtml?productId=95112&hl=en-US">Send feedback</a></li><li><a href="https://www.google.com/intl/en-US/policies">Privacy and terms</a></li><li><a href="./full">Switch to full site</a></li></ul></div></body></html>
blob - /dev/null
blob + e6f55a4a57507487e053336af9f1a15cd8189d6b (mode 644)
--- /dev/null
+++ tests/resp_escaped-04.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html><html dir="ltr" lang="en-US"><head><title>Google Translate</title><meta name="google" content="notranslate"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="//ssl.gstatic.com/translate/favicon.ico" sizes="64x64"><style nonce="lXDqFVxzKw1uk3fJPjXcVg">
+    body {
+      font-family: 'Arial', sans-serif;
+      margin: 0;
+    }
+    a:link,
+    a:visited,
+    a:active {
+      color: #1a73e8; /* blue 600 */
+      text-decoration: none;
+    }
+    .header {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #5f6368; /* grey 700 */
+      font-size: 18px;
+      line-height: 21px;
+      padding: 14px 16px;
+    }
+    .logo-image {
+      background-image: url('https://ssl.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_68x28px.svg');
+      background-repeat: no-repeat;
+      height: 48px;
+      width: 68px;
+      position: absolute;
+    }
+    .logo-text {
+      margin-left: 72px;
+      margin-right: 76px;
+      margin-top: 2px;
+    }
+  
+    body {
+      background-color: #f1f3f4; /* grey 100 */
+    }
+    .languages-container {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #3c4043; /* grey 800 */
+      font-size: 14px;
+      height: 48px;
+      padding: 14px 16px;
+    }
+    .sl-and-tl {
+      display: inline-block;
+    }
+    .swap-container {
+      float: right;
+    }
+    html[dir="rtl"] .swap-container {
+      float: left;
+    }
+    .input-container {
+      background-color: #fff;
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+    }
+    .input-field {
+      border: 0;
+      color: #3c4043; /* grey 800 */
+      font-size: 18px;
+      line-height: 24px;
+      padding: 18px 16px;
+      width: 100%;
+    }
+    .translate-button-container {
+      padding: 16px;
+      text-align: right;
+    }
+    html[dir="rtl"] .translate-button-container {
+      text-align: left;
+    }
+    .translate-button {
+      background: #1a73e8; /* blue 600 */
+      border-radius: 4px;
+      color: #fff;
+      font-size: 14px;
+      line-height: 20px;
+      padding: 8px 16px;
+    }
+    .result-container {
+      background: #1a73e8; /* blue 600 */
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+      color: #fff;
+      font-size: 18px;
+      line-height: 24px;
+      margin-bottom: 50px;
+      padding: 16px 16px 40px 16px;
+    }
+    .links-container a {
+      color: #1967d2; /* blue 700 */
+    }
+    .links-container ul {
+      font-size: 12px;
+      line-height: 16px;
+      list-style-type: none;
+      margin: 16px;
+      padding: 0;
+    }
+    .links-container ul li {
+      margin-bottom: 16px;
+    }
+  </style></head><body><div class="header"><div class="logo-image"></div><div class="logo-text">Translate</div></div><div class="languages-container"><div class="sl-and-tl"><a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=sl&amp;hl=en-US">Detect language</a> → <a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=tl&amp;hl=en-US">Russian</a></div></div><div class="input-container"><form action="/m"><input type="hidden" name="sl" value="auto"><input type="hidden" name="tl" value="ru"><input type="hidden" name="hl" value="en-US"><input type="text" aria-label="Source text" name="q" class="input-field" maxlength="2048" value="It&#39;s become clear to me that, no doubt as a consequence of the fact that Project Gemini has been undergoing a very slow and gradual reawakening&quot; after a long period of stasis, not everybody in the community is necessarily on the same page with regard to how much and what kind of change they expect to happen during the specification finalisation phase of the project."><div class="translate-button-container"><input type="submit" value="Translate" class="translate-button"></div></form></div><div class="result-container">lt=&#x3c; gt=&#x3e;</div><div class="links-container"><ul><li><a href="https://www.google.com/m?hl=en-US">Google home</a></li><li><a href="https://www.google.com/tools/feedback/survey/xhtml?productId=95112&hl=en-US">Send feedback</a></li><li><a href="https://www.google.com/intl/en-US/policies">Privacy and terms</a></li><li><a href="./full">Switch to full site</a></li></ul></div></body></html>
blob - /dev/null
blob + bdff46dee47af68c5023bffe70612e834f0ac3a2 (mode 644)
--- /dev/null
+++ tests/resp_multiline-01.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html><html dir="ltr" lang="en-US"><head><title>Google Translate</title><meta name="google" content="notranslate"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="//ssl.gstatic.com/translate/favicon.ico" sizes="64x64"><style nonce="lXDqFVxzKw1uk3fJPjXcVg">
+    body {
+      font-family: 'Arial', sans-serif;
+      margin: 0;
+    }
+    a:link,
+    a:visited,
+    a:active {
+      color: #1a73e8; /* blue 600 */
+      text-decoration: none;
+    }
+    .header {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #5f6368; /* grey 700 */
+      font-size: 18px;
+      line-height: 21px;
+      padding: 14px 16px;
+    }
+    .logo-image {
+      background-image: url('https://ssl.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_68x28px.svg');
+      background-repeat: no-repeat;
+      height: 48px;
+      width: 68px;
+      position: absolute;
+    }
+    .logo-text {
+      margin-left: 72px;
+      margin-right: 76px;
+      margin-top: 2px;
+    }
+  
+    body {
+      background-color: #f1f3f4; /* grey 100 */
+    }
+    .languages-container {
+      background-color: #fff;
+      border-bottom: 1px solid #dadce0; /* grey 300 */
+      box-sizing: border-box;
+      color: #3c4043; /* grey 800 */
+      font-size: 14px;
+      height: 48px;
+      padding: 14px 16px;
+    }
+    .sl-and-tl {
+      display: inline-block;
+    }
+    .swap-container {
+      float: right;
+    }
+    html[dir="rtl"] .swap-container {
+      float: left;
+    }
+    .input-container {
+      background-color: #fff;
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+    }
+    .input-field {
+      border: 0;
+      color: #3c4043; /* grey 800 */
+      font-size: 18px;
+      line-height: 24px;
+      padding: 18px 16px;
+      width: 100%;
+    }
+    .translate-button-container {
+      padding: 16px;
+      text-align: right;
+    }
+    html[dir="rtl"] .translate-button-container {
+      text-align: left;
+    }
+    .translate-button {
+      background: #1a73e8; /* blue 600 */
+      border-radius: 4px;
+      color: #fff;
+      font-size: 14px;
+      line-height: 20px;
+      padding: 8px 16px;
+    }
+    .result-container {
+      background: #1a73e8; /* blue 600 */
+      box-shadow:
+        0px -1px 5px rgba(128, 134, 139, 0.09),
+        0px 3px 5px rgba(128, 134, 139, 0.06),
+        0px 1px 2px rgba(60, 64, 67, 0.3),
+        0px 1px 3px rgba(60, 64, 67, 0.15);
+      color: #fff;
+      font-size: 18px;
+      line-height: 24px;
+      margin-bottom: 50px;
+      padding: 16px 16px 40px 16px;
+    }
+    .links-container a {
+      color: #1967d2; /* blue 700 */
+    }
+    .links-container ul {
+      font-size: 12px;
+      line-height: 16px;
+      list-style-type: none;
+      margin: 16px;
+      padding: 0;
+    }
+    .links-container ul li {
+      margin-bottom: 16px;
+    }
+  </style></head><body><div class="header"><div class="logo-image"></div><div class="logo-text">Translate</div></div><div class="languages-container"><div class="sl-and-tl"><a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=sl&amp;hl=en-US">Detect language</a> → <a href="./m?sl=auto&amp;tl=ru&amp;q=It%27s%20become%20clear%20to%20me%20that%2C%20no%20doubt%20as%20a%20consequence%20of%20the%20fact%20that%20Project%20Gemini%20has%20been%20undergoing%20a%20very%20slow%20and%20gradual%20reawakening%22%20after%20a%20long%20period%20of%20stasis%2C%20not%20everybody%20in%20the%20community%20is%20necessarily%20on%20the%20same%20page%20with%20regard%20to%20how%20much%20and%20what%20kind%20of%20change%20they%20expect%20to%20happen%20during%20the%20specification%20finalisation%20phase%20of%20the%20project.&amp;mui=tl&amp;hl=en-US">Russian</a></div></div><div class="input-container"><form action="/m"><input type="hidden" name="sl" value="auto"><input type="hidden" name="tl" value="ru"><input type="hidden" name="hl" value="en-US"><input type="text" aria-label="Source text" name="q" class="input-field" maxlength="2048" value="It&#39;s become clear to me that, no doubt as a consequence of the fact that Project Gemini has been undergoing a very slow and gradual reawakening&quot; after a long period of stasis, not everybody in the community is necessarily on the same page with regard to how much and what kind of change they expect to happen during the specification finalisation phase of the project."><div class="translate-button-container"><input type="submit" value="Translate" class="translate-button"></div></form></div><div class="result-container">hello
+world</div><div class="links-container"><ul><li><a href="https://www.google.com/m?hl=en-US">Google home</a></li><li><a href="https://www.google.com/tools/feedback/survey/xhtml?productId=95112&hl=en-US">Send feedback</a></li><li><a href="https://www.google.com/intl/en-US/policies">Privacy and terms</a></li><li><a href="./full">Switch to full site</a></li></ul></div></body></html>
blob - /dev/null
blob + da2a7cd65e33a46bf0e8919df18a0640a8d30a9b (mode 755)
--- /dev/null
+++ tests/tests.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+
+echo "Not found tests..."
+echo "gemini://any-key.press/vgi" | ./gtransl.retro | head -n 1 | \
+    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl" | ./gtransl.retro | \
+    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/" | ./gtransl.retro | \
+    head -n 1 | grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransX/auto/ru/?hello" | ./gtransl.retro | head -n 1 | \
+    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/?\" ; ls/ru/?hello" | ./gtransl.retro | head -n 1 | \
+    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/auto/?\" ; ls/?hello" | ./gtransl.retro | head -n 1 | \
+    grep "^51 Not found" > /dev/null && echo "passed" || echo "FAILED"
+
+echo "Bad request tests..."
+echo "gemini://any-key.press/vgi/gtransl/auto/ru/?\" ; ls" | ./gtransl.retro | head -n 1 | \
+    grep "^59 Bad request" > /dev/null && echo "passed" || echo "FAILED"
+
+echo "Escaping tests..."
+echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
+    GTRANSLRESPFILE=tests/resp_escaped-01.html ./gtransl.retro | \
+    head -n 8 | tail -n 1 | grep "^amp=& lt=< gt=> quot=\" apos='" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
+    GTRANSLRESPFILE=tests/resp_escaped-02.html ./gtransl.retro | head -n 8 | tail -n 1 | \
+    grep "^amp=& lt=< gt=> quot=\" apos='" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
+    GTRANSLRESPFILE=tests/resp_escaped-03.html ./gtransl.retro | head -n 8 | tail -n 1 | \
+    grep "^amp=& lt=< gt=> quot=\" apos='" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
+    GTRANSLRESPFILE=tests/resp_escaped-04.html ./gtransl.retro | head -n 8 | tail -n 1 | \
+    grep "^lt=< gt=>" > /dev/null && echo "passed" || echo "FAILED"
+
+echo "Multiline tests..."
+echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
+    GTRANSLRESPFILE=tests/resp_multiline-01.html ./gtransl.retro | head -n 8 | tail -n 1 | \
+    grep "^hello$" > /dev/null && echo "passed" || echo "FAILED"
+echo "gemini://any-key.press/vgi/gtransl/sl/tl/?query" | \
+    GTRANSLRESPFILE=tests/resp_multiline-01.html ./gtransl.retro | head -n 9 | tail -n 1 | \
+    grep "^world" > /dev/null && echo "passed" || echo "FAILED"