209 lines
9.1 KiB
JavaScript
209 lines
9.1 KiB
JavaScript
// 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<l;i++) {
|
|
line = lines[i].trim() // remove all leading/trailing whitespace-class chars
|
|
if(line.length) // if the line is not empty, just append it and a whitespace to the buffer
|
|
buf += line + ' '
|
|
else { // output logic
|
|
out.push(buf, '')
|
|
buf = ''
|
|
}
|
|
}
|
|
if(buf.length) // process the remaining output
|
|
out.push(buf)
|
|
return out.map(function(s) { // final whitespace sanitation
|
|
return s.replace(/\s+/g, ' ').trim()
|
|
}).join('\n')
|
|
}
|
|
|
|
function terminalize(str, attrs) { // partial terminal styling support with ANSI codes
|
|
if(!attrs) attrs = [] // accept previous attributes array if present
|
|
var prevattrs = attrs.join(' ') // preserve stringified copy
|
|
var out = '', c, styleseq, sl, l = str.length, i, j, att, attindex, strattrs,
|
|
visualOpenTag='<span data-styling="%s">', visualCloseTag='</span>',
|
|
modes = ['', 'BB', 'FN', 'IT', 'UL', 'BL', '', 'IN', 'HI', 'ST']
|
|
for(i=0;i<l;i++) {
|
|
c = str[i]
|
|
if(c === '\x1b') { // escape character encountered
|
|
c = str[i+1] // get the next character, don't increase the counter yet
|
|
if(c === '[') { // ANSI sequence starting
|
|
i++ // increase the counter, now it's pointing to [
|
|
styleseq='' // init style sequence buffer
|
|
do { // read the style sequence until m character or any whitespace
|
|
c = str[++i] // increase the counter once more, [ is ignored
|
|
styleseq += c
|
|
} while(c !== 'm' && c !== ' ' && c !== '\t' && c !== '\r' && c !== '\n')
|
|
styleseq = styleseq.slice(0, -1).split(';') // attributes are delimited by ;
|
|
for(sl=styleseq.length,j=0;j<sl;j++) { // iterate over attributes in their order
|
|
att = parseInt(styleseq[j]) // in this case parseInt is more reliable than |0
|
|
if(att === 0) attrs = [] // reset all styling
|
|
else if(att >= 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 = '<br>',
|
|
type, rest, desc, sel, host, port,
|
|
iattrs = [], istyled, // attribute cache and styled infomessage placeholder
|
|
output = '', pretag = 'pre', preflag = false
|
|
for(i=0;i<l;i++) {
|
|
line = lines[i]
|
|
if(line === '.')
|
|
break // immediately stop parsing on the . line
|
|
if(line.indexOf('\t') == -1 && !line.startsWith('i')) // this also handles empty lines
|
|
line = 'i' + line
|
|
type = line[0] // type
|
|
rest = line.substr(1).split('\t')
|
|
desc = esc(rest[0]) // escaping description (obviously must not contain tabs)
|
|
sel = rest.length > 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 += '</' + pretag + '>'
|
|
}
|
|
if(desc = desc.trim()) { // ignore empty descriptions
|
|
if(type == '3') // error message
|
|
output += '<span class=error>' + desc + '</span>' + 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 += '<a href="' + encodeURI(link) + '">' + desc + '</a>' + 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)
|