// Platform-agnostic Gopher response handler and Gophermap parser // Depends on the platform-specific gopherRequest(resource, input, success, error) function // (defined in the transport.js file here) Hi01379 = (function(psGopherRequest) { var tagsToReplace = {'&': '&', '<': '<', '>': '>'} function esc(str) { // escape HTML entities return str.replace(/[&<>]/g, function(tag) {return tagsToReplace[tag] || tag}) } function unphlow(str) { // Unphlow algorithm implementation var lines=str.split('\n'), line, l = lines.length, i, buf = '', out = [] for(i=0;i= 30 && att <= 37) attrs.push('FC'+(att-30)) // foreground color else if(att >= 40 && att <= 47) attrs.push('BC'+(att-40)) // background color else if(att < 10) attrs.push(modes[att]) // activate special modes else if(att > 20 && att < 30) { // deactivate special modes if(att === 22) { // also deactivate bold mode 1 attindex = attrs.indexOf('BB') if(attindex > -1) attrs.splice(attindex, 1) } attindex = attrs.indexOf(modes[att - 20]) if(attindex > -1) attrs.splice(attindex, 1) } else if(att === 39) { // reset only the foreground color attrs = attrs.map(function(v) { return v.startsWith('FC') ? '' : v }).filter(String) } else if(att === 49) { // reset only the background color attrs = attrs.map(function(v) { return v.startsWith('BC') ? '' : v }).filter(String) } } // now we have our attrs array with all styling applied, output the tags strattrs = attrs.join(' ') if(strattrs != prevattrs) { // there are some changes if(prevattrs != '') // previous attributes were not empty out += visualCloseTag if(strattrs) // current attributes are not empty out += visualOpenTag.replace('%s', strattrs) prevattrs = strattrs // update the previous attribute cache } } // don't add any processed characters "as is" to the output else continue // skip the escape and proceed with the loop } else out += c // just append it to the output } return {output: out, attrs: attrs} // return both output and remaining attributes } function gophermapToHTML(gmap, chost, cport) { if(!cport) cport = 70 if(!chost) chost = 'localhost' var lines = gmap.split(/\r?\n/), line, l = lines.length, i, lbr = '
', type, rest, desc, sel, host, port, iattrs = [], istyled, // attribute cache and styled infomessage placeholder output = '', pretag = 'pre', preflag = false for(i=0;i 1 ? rest[1] : '' // selector host = rest.length > 2 ? rest[2] : '' // hostname port = rest.length > 3 ? (0|rest[3]) : '' // port // selector defaults to the description only if there are no other fields if(!sel && rest.length < 2) sel = desc if(!host) host = chost // host defaults to the current host if(!port) port = cport // port defaults to the current port if(type == 'i') { // information message - wrap them in pretag if(!preflag) { preflag = true output += '<' + pretag + '>' } istyled = terminalize(desc, iattrs) iattrs = istyled.attrs output += istyled.output + '\n' } else { //information messages ended, stop preformatting if(preflag) { preflag = false iattrs = [] // reset info styling attributes output += '' } if(desc = desc.trim()) { // ignore empty descriptions if(type == '3') // error message output += '' + desc + '' + lbr else if(type == '8') { // output the telnet:// link as is var tlink = 'telnet://' + (sel ? (sel+'@') : '') + host + (port ? (':'+port) : '') output += desc + ': ' + esc(tlink) + lbr } else { // shape the link (internal hi:type|sel|host|port format unless it's an external URL) var deflink = 'hi:'+[type,sel,host,port].join('|'), link = deflink if(type == 'h' && sel.startsWith('URL:')) { link = sel.split(':').slice(1).join(':').trim() if(link.startsWith('javascript:')) // XSS protection link = deflink } output += '' + desc + '' + lbr } } } } return output } // resource format: [type, selector, host, port] function loadHole(resource, input, successCb, errorCb) { var fname = resource[1].split('/').pop(), // as the most intelligent guess ext = fname.split('.').pop().toLowerCase(), // presumed extension extMappings = { // not including AVIF here because they aren't supported by Gecko 48 anyway 'gif': 'image/gif', // including GIF though because nothing prevents using I instead of g 'png': 'image/png', 'svg': 'image/svg+xml', 'webp': 'image/webp', 'apng': 'image/apng', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'jfif': 'image/jpeg', 'pjpeg': 'image/jpeg', 'pjp': 'image/jpeg' } psGopherRequest(resource, input, function(rawdata, type) { if(type == '5' || type == 's' || type == ';' || type == 'd') type = '9' // remap unknown binary types to 9 if(type == '9' || type == 'g' || type == 'I') { // binary file var ctype = 'application/octet-stream' // the default one if everything else fails // update content type if(type == 'g') // GIF-only resource type ctype = 'image/gif' else if(type == 'I') // attempt to guess the image MIME type by the extension ctype = extMappings[ext] var datablob = new Blob([rawdata], {type: ctype}) successCb({ content: datablob, contentType: ctype, contentName: fname, serviceMsg: type + ' ' + resource[1], updateAddr: false }) } else { // assuming text content otherwise var output = (new TextDecoder).decode(rawdata), ctype = 'text/plain', // defaulting to type 0 wo = '' // wrapper-friendly output if(type == '1' || type == '7') { // gophermap output = gophermapToHTML(output, resource[2], resource[3]) ctype = 'text/html' wo = output } else wo = unphlow(output) successCb({ content: output, contentWf: wo, contentType: ctype, serviceMsg: type + ' ' + resource[1] }) } }, errorCb) } return {load: loadHole, render: gophermapToHTML} })(gopherRequest)