hi01379.js (9313B)
1 // Platform-agnostic Gopher response handler and Gophermap parser 2 // Depends on the platform-specific gopherRequest(resource, input, success, error) function 3 // (defined in the transport.js file here) 4 5 Hi01379 = (function(psGopherRequest) { 6 7 var tagsToReplace = {'&': '&', '<': '<', '>': '>'} 8 9 function esc(str) { // escape HTML entities 10 return str.replace(/[&<>]/g, function(tag) {return tagsToReplace[tag] || tag}) 11 } 12 13 function unphlow(str) { // Unphlow algorithm implementation 14 var lines=str.split('\n'), line, l = lines.length, i, buf = '', out = [] 15 for(i=0;i<l;i++) { 16 line = lines[i].trim() // remove all leading/trailing whitespace-class chars 17 if(line.length) // if the line is not empty, just append it and a whitespace to the buffer 18 buf += line + ' ' 19 else { // output logic 20 out.push(buf, '') 21 buf = '' 22 } 23 } 24 if(buf.length) // process the remaining output 25 out.push(buf) 26 return out.map(function(s) { // final whitespace sanitation 27 return s.replace(/\s+/g, ' ').trim() 28 }).join('\n') 29 } 30 31 function terminalize(str, attrs) { // partial terminal styling support with ANSI codes 32 if(!attrs) attrs = [] // accept previous attributes array if present 33 var prevattrs = attrs.join(' ') // preserve stringified copy 34 var out = '', c, styleseq, sl, l = str.length, i, j, att, attindex, strattrs, 35 visualOpenTag='<span data-styling="%s">', visualCloseTag='</span>', 36 modes = ['', 'BB', 'FN', 'IT', 'UL', 'BL', '', 'IN', 'HI', 'ST'] 37 for(i=0;i<l;i++) { 38 c = str[i] 39 if(c === '\x1b') { // escape character encountered 40 c = str[i+1] // get the next character, don't increase the counter yet 41 if(c === '[') { // ANSI sequence starting 42 i++ // increase the counter, now it's pointing to [ 43 styleseq='' // init style sequence buffer 44 do { // read the style sequence until m character or any whitespace 45 c = str[++i] // increase the counter once more, [ is ignored 46 styleseq += c 47 } while(c !== 'm' && c !== ' ' && c !== '\t' && c !== '\r' && c !== '\n') 48 styleseq = styleseq.slice(0, -1).split(';') // attributes are delimited by ; 49 for(sl=styleseq.length,j=0;j<sl;j++) { // iterate over attributes in their order 50 att = parseInt(styleseq[j]) // in this case parseInt is more reliable than |0 51 if(att === 0) attrs = [] // reset all styling 52 else if(att >= 30 && att <= 37) attrs.push('FC'+(att-30)) // foreground color 53 else if(att >= 40 && att <= 47) attrs.push('BC'+(att-40)) // background color 54 else if(att < 10) attrs.push(modes[att]) // activate special modes 55 else if(att > 20 && att < 30) { // deactivate special modes 56 if(att === 22) { // also deactivate bold mode 1 57 attindex = attrs.indexOf('BB') 58 if(attindex > -1) 59 attrs.splice(attindex, 1) 60 } 61 attindex = attrs.indexOf(modes[att - 20]) 62 if(attindex > -1) 63 attrs.splice(attindex, 1) 64 } 65 else if(att === 39) { // reset only the foreground color 66 attrs = attrs.map(function(v) { 67 return v.startsWith('FC') ? '' : v 68 }).filter(String) 69 } 70 else if(att === 49) { // reset only the background color 71 attrs = attrs.map(function(v) { 72 return v.startsWith('BC') ? '' : v 73 }).filter(String) 74 } 75 } 76 // now we have our attrs array with all styling applied, output the tags 77 strattrs = attrs.join(' ') 78 if(strattrs != prevattrs) { // there are some changes 79 if(prevattrs != '') // previous attributes were not empty 80 out += visualCloseTag 81 if(strattrs) // current attributes are not empty 82 out += visualOpenTag.replace('%s', strattrs) 83 prevattrs = strattrs // update the previous attribute cache 84 } 85 } // don't add any processed characters "as is" to the output 86 else continue // skip the escape and proceed with the loop 87 } 88 else out += c // just append it to the output 89 } 90 return {output: out, attrs: attrs} // return both output and remaining attributes 91 } 92 93 function gophermapToHTML(gmap, chost, cport) { 94 if(!cport) cport = 70 95 if(!chost) chost = 'localhost' 96 var lines = gmap.split(/\r?\n/), line, l = lines.length, i, lbr = '<br>', 97 type, rest, desc, sel, host, port, 98 iattrs = [], istyled, // attribute cache and styled infomessage placeholder 99 output = '', pretag = 'pre', preflag = false 100 for(i=0;i<l;i++) { 101 line = lines[i] 102 if(line === '.') 103 break // immediately stop parsing on the . line 104 if(line.indexOf('\t') == -1 && !line.startsWith('i')) // this also handles empty lines 105 line = 'i' + line 106 type = line[0] // type 107 rest = line.substr(1).split('\t') 108 desc = esc(rest[0]) // escaping description (obviously must not contain tabs) 109 sel = rest.length > 1 ? rest[1] : '' // selector 110 host = rest.length > 2 ? rest[2] : '' // hostname 111 port = rest.length > 3 ? (0|rest[3]) : '' // port 112 // selector defaults to the description only if there are no other fields 113 if(!sel && rest.length < 2) sel = desc 114 if(!host) host = chost // host defaults to the current host 115 if(!port) port = cport // port defaults to the current port 116 if(type == 'i') { // information message - wrap them in pretag 117 if(!preflag) { 118 preflag = true 119 output += '<' + pretag + '>' 120 } 121 istyled = terminalize(desc, iattrs) 122 iattrs = istyled.attrs 123 output += istyled.output + '\n' 124 } 125 else { //information messages ended, stop preformatting 126 if(preflag) { 127 preflag = false 128 iattrs = [] // reset info styling attributes 129 output += '</' + pretag + '>' 130 } 131 if(desc = desc.trim()) { // ignore empty descriptions 132 if(type == '3') // error message 133 output += '<span class=error>' + desc + '</span>' + lbr 134 else if(type == '8') { // output the telnet:// link as is 135 var tlink = 'telnet://' + (sel ? (sel+'@') : '') + host + (port ? (':'+port) : '') 136 output += desc + ': ' + esc(tlink) + lbr 137 } 138 else { // shape the link (internal hi:type|sel|host|port format unless it's an external URL) 139 var deflink = 'hi:'+[type,sel,host,port].join('|'), link = deflink 140 if(type == 'h' && sel.startsWith('URL:')) { 141 link = sel.split(':').slice(1).join(':').trim() 142 if(link.startsWith('javascript:')) // XSS protection 143 link = deflink 144 } 145 output += '<a href="' + encodeURI(link) + '">' + desc + '</a>' + lbr 146 } 147 } 148 } 149 } 150 return output 151 } 152 153 // resource format: [type, selector, host, port] 154 function loadHole(resource, input, successCb, errorCb) { 155 var fname = resource[1].split('/').pop(), // as the most intelligent guess 156 ext = fname.split('.').pop().toLowerCase(), // presumed extension 157 extMappings = { // not including AVIF here because they aren't supported by Gecko 48 anyway 158 'gif': 'image/gif', // including GIF though because nothing prevents using I instead of g 159 'png': 'image/png', 160 'svg': 'image/svg+xml', 161 'webp': 'image/webp', 162 'apng': 'image/apng', 163 'jpg': 'image/jpeg', 164 'jpeg': 'image/jpeg', 165 'jfif': 'image/jpeg', 166 'pjpeg': 'image/jpeg', 167 'pjp': 'image/jpeg' 168 } 169 psGopherRequest(resource, input, function(rawdata, type) { 170 if(type == '5' || type == 's' || type == ';' || type == 'd') 171 type = '9' // remap unknown binary types to 9 172 if(type == '9' || type == 'g' || type == 'I') { // binary file 173 var ctype = 'application/octet-stream' // the default one if everything else fails 174 // update content type 175 if(type == 'g') // GIF-only resource type 176 ctype = 'image/gif' 177 else if(type == 'I') // attempt to guess the image MIME type by the extension 178 ctype = extMappings[ext] 179 var datablob = new Blob([rawdata], {type: ctype}) 180 successCb({ 181 content: datablob, 182 contentType: ctype, 183 contentName: fname, 184 serviceMsg: type + ' ' + resource[1], 185 updateAddr: false 186 }) 187 } 188 else { // assuming text content otherwise 189 var output = (new TextDecoder).decode(rawdata), ctype = 'text/plain', // defaulting to type 0 190 wo = '' // wrapper-friendly output 191 if(type == '1' || type == '7') { // gophermap 192 output = gophermapToHTML(output, resource[2], resource[3]) 193 ctype = 'text/html' 194 wo = output 195 } 196 else wo = unphlow(output) 197 successCb({ 198 content: output, 199 contentWf: wo, 200 contentType: ctype, 201 serviceMsg: type + ' ' + resource[1] 202 }) 203 } 204 }, errorCb) 205 } 206 207 return {load: loadHole, render: gophermapToHTML} 208 })(gopherRequest)