Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs
This commit is contained in:
commit
e15e4be1d7
|
@ -1,22 +1,17 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var miscUtil = require('./misc_util.js');
|
const miscUtil = require('./misc_util.js');
|
||||||
var ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
|
|
||||||
var events = require('events');
|
const events = require('events');
|
||||||
var util = require('util');
|
const util = require('util');
|
||||||
var _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.ANSIEscapeParser = ANSIEscapeParser;
|
exports.ANSIEscapeParser = ANSIEscapeParser;
|
||||||
|
|
||||||
var CR = 0x0d;
|
const CR = 0x0d;
|
||||||
var LF = 0x0a;
|
const LF = 0x0a;
|
||||||
|
|
||||||
//
|
|
||||||
// Resources, Specs, etc.
|
|
||||||
//
|
|
||||||
// * https://github.com/M-griffin/EtherTerm/blob/master/ansiParser.cpp
|
|
||||||
|
|
||||||
function ANSIEscapeParser(options) {
|
function ANSIEscapeParser(options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -44,14 +39,6 @@ function ANSIEscapeParser(options) {
|
||||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
||||||
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
||||||
|
|
||||||
function getArgArray(array) {
|
|
||||||
var i = array.length;
|
|
||||||
while(i--) {
|
|
||||||
array[i] = parseInt(array[i], 10);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.moveCursor = function(cols, rows) {
|
self.moveCursor = function(cols, rows) {
|
||||||
self.column += cols;
|
self.column += cols;
|
||||||
self.row += rows;
|
self.row += rows;
|
||||||
|
@ -123,7 +110,8 @@ function ANSIEscapeParser(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit('chunk', text);
|
//self.emit('chunk', text);
|
||||||
|
self.emit('literal', text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProcessedMCI(mci) {
|
function getProcessedMCI(mci) {
|
||||||
|
@ -179,7 +167,10 @@ function ANSIEscapeParser(options) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if(self.mciReplaceChar.length > 0) {
|
if(self.mciReplaceChar.length > 0) {
|
||||||
self.emit('chunk', ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase));
|
//self.emit('chunk', ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase));
|
||||||
|
const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase);
|
||||||
|
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[\;m]/).slice(0, 3));
|
||||||
|
//self.emit('control', ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase));
|
||||||
literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
|
literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
|
||||||
} else {
|
} else {
|
||||||
literal(match[0]);
|
literal(match[0]);
|
||||||
|
@ -235,11 +226,12 @@ function ANSIEscapeParser(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
opCode = match[2];
|
opCode = match[2];
|
||||||
args = getArgArray(match[1].split(';'));
|
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
|
||||||
|
|
||||||
escape(opCode, args);
|
escape(opCode, args);
|
||||||
|
|
||||||
self.emit('chunk', match[0]);
|
//self.emit('chunk', match[0]);
|
||||||
|
self.emit('control', match[0], opCode, args);
|
||||||
}
|
}
|
||||||
} while(0 !== re.lastIndex);
|
} while(0 !== re.lastIndex);
|
||||||
|
|
||||||
|
@ -492,4 +484,5 @@ ANSIEscapeParser.styles = {
|
||||||
8 : 'invisibleOn', // FG set to BG
|
8 : 'invisibleOn', // FG set to BG
|
||||||
28 : 'invisibleOff', // Not supported by most BBS-like terminals
|
28 : 'invisibleOff', // Not supported by most BBS-like terminals
|
||||||
};
|
};
|
||||||
Object.freeze(ANSIEscapeParser.styles);
|
Object.freeze(ANSIEscapeParser.styles);
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,8 @@
|
||||||
// * http://www.inwap.com/pdp10/ansicode.txt
|
// * http://www.inwap.com/pdp10/ansicode.txt
|
||||||
//
|
//
|
||||||
|
|
||||||
var assert = require('assert');
|
const assert = require('assert');
|
||||||
var binary = require('binary');
|
const miscUtil = require('./misc_util.js');
|
||||||
var miscUtil = require('./misc_util.js');
|
|
||||||
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
exports.getFGColorValue = getFGColorValue;
|
exports.getFGColorValue = getFGColorValue;
|
||||||
exports.getBGColorValue = getBGColorValue;
|
exports.getBGColorValue = getBGColorValue;
|
||||||
|
|
10
core/art.js
10
core/art.js
|
@ -360,9 +360,19 @@ function display(options, cb) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
parser.on('chunk', function onChunk(chunk) {
|
parser.on('chunk', function onChunk(chunk) {
|
||||||
options.client.term.write(chunk, false);
|
options.client.term.write(chunk, false);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
parser.on('literal', literal => {
|
||||||
|
options.client.term.write(literal, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.on('control', control => {
|
||||||
|
options.client.term.write(control, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
parser.on('complete', function onComplete() {
|
parser.on('complete', function onComplete() {
|
||||||
parseComplete = true;
|
parseComplete = true;
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// enigma-bbs
|
||||||
|
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
|
const resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const SSHClient = require('ssh2').Client;
|
||||||
|
|
||||||
|
exports.getModule = DoorPartyModule;
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'DoorParty',
|
||||||
|
desc : 'DoorParty Access Module',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function DoorPartyModule(options) {
|
||||||
|
MenuModule.call(this, options);
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// establish defaults
|
||||||
|
this.config = options.menuConfig.config;
|
||||||
|
this.config.host = this.config.host || 'dp.throwbackbbs.com';
|
||||||
|
this.config.sshPort = this.config.sshPort || 22;
|
||||||
|
this.config.rloginPort = this.config.rloginPort || 513;
|
||||||
|
|
||||||
|
|
||||||
|
this.initSequence = function() {
|
||||||
|
let clientTerminated;
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function validateConfig(callback) {
|
||||||
|
if(!_.isString(self.config.username)) {
|
||||||
|
return callback(new Error('Config requires "username"!'));
|
||||||
|
}
|
||||||
|
if(!_.isString(self.config.password)) {
|
||||||
|
return callback(new Error('Config requires "password"!'));
|
||||||
|
}
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
function establishSecureConnection(callback) {
|
||||||
|
self.client.term.write(resetScreen());
|
||||||
|
self.client.term.write('Connecting to DoorParty, please wait...\n');
|
||||||
|
|
||||||
|
const sshClient = new SSHClient();
|
||||||
|
|
||||||
|
let pipeRestored = false;
|
||||||
|
let pipedStream;
|
||||||
|
const restorePipe = function() {
|
||||||
|
if(pipedStream && !pipeRestored && !clientTerminated) {
|
||||||
|
self.client.term.output.unpipe(pipedStream);
|
||||||
|
self.client.term.output.resume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sshClient.on('ready', () => {
|
||||||
|
// track client termination so we can clean up early
|
||||||
|
self.client.once('end', () => {
|
||||||
|
self.client.log.info('Connection ended. Terminating DoorParty connection');
|
||||||
|
clientTerminated = true;
|
||||||
|
sshClient.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// establish tunnel for rlogin
|
||||||
|
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(new Error('Failed to establish tunnel'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// send rlogin
|
||||||
|
const rlogin = `\x00${self.config.username}\x00${self.config.username}\x00${self.client.term.termType}\x00`;
|
||||||
|
stream.write(rlogin);
|
||||||
|
|
||||||
|
pipedStream = stream; // :TODO: this is hacky...
|
||||||
|
self.client.term.output.pipe(stream);
|
||||||
|
|
||||||
|
stream.on('data', d => {
|
||||||
|
// :TODO: we should just pipe this...
|
||||||
|
self.client.term.rawWrite(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('close', () => {
|
||||||
|
restorePipe();
|
||||||
|
sshClient.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sshClient.on('close', () => {
|
||||||
|
restorePipe();
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
sshClient.connect( {
|
||||||
|
host : self.config.host,
|
||||||
|
port : self.config.sshPort,
|
||||||
|
username : self.config.username,
|
||||||
|
password : self.config.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// note: no explicit callback() until we're finished!
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
self.client.log.warn( { error : err.toString() }, 'DoorParty error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the client is stil here, go to previous
|
||||||
|
if(!clientTerminated) {
|
||||||
|
self.prevMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
require('util').inherits(DoorPartyModule, MenuModule);
|
57
core/fse.js
57
core/fse.js
|
@ -1,20 +1,21 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
var ViewController = require('../core/view_controller.js').ViewController;
|
const ViewController = require('../core/view_controller.js').ViewController;
|
||||||
var ansi = require('../core/ansi_term.js');
|
const ansi = require('../core/ansi_term.js');
|
||||||
var theme = require('../core/theme.js');
|
const theme = require('../core/theme.js');
|
||||||
var MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
|
||||||
var Message = require('../core/message.js');
|
const Message = require('../core/message.js');
|
||||||
var getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
|
const getMessageAreaByTag = require('../core/message_area.js').getMessageAreaByTag;
|
||||||
var updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
|
const updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
|
||||||
var getUserIdAndName = require('../core/user.js').getUserIdAndName;
|
const getUserIdAndName = require('../core/user.js').getUserIdAndName;
|
||||||
|
const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
|
||||||
|
|
||||||
var async = require('async');
|
const async = require('async');
|
||||||
var assert = require('assert');
|
const assert = require('assert');
|
||||||
var _ = require('lodash');
|
const _ = require('lodash');
|
||||||
var moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
exports.FullScreenEditorModule = FullScreenEditorModule;
|
exports.FullScreenEditorModule = FullScreenEditorModule;
|
||||||
|
|
||||||
|
@ -229,25 +230,32 @@ function FullScreenEditorModule(options) {
|
||||||
|
|
||||||
self.message = new Message(msgOpts);
|
self.message = new Message(msgOpts);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
this.setBodyMessageViewText = function() {
|
||||||
|
self.bodyMessageView.setText(cleanControlCodes(self.message.message));
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
this.setMessage = function(message) {
|
this.setMessage = function(message) {
|
||||||
self.message = message;
|
self.message = message;
|
||||||
|
|
||||||
updateMessageAreaLastReadId(
|
updateMessageAreaLastReadId(
|
||||||
self.client.user.userId, self.messageAreaTag, self.message.messageId,
|
self.client.user.userId, self.messageAreaTag, self.message.messageId, () => {
|
||||||
function lastReadUpdated() {
|
|
||||||
|
|
||||||
if(self.isReady) {
|
if(self.isReady) {
|
||||||
self.initHeaderViewMode();
|
self.initHeaderViewMode();
|
||||||
self.initFooterViewMode();
|
self.initFooterViewMode();
|
||||||
|
|
||||||
var bodyMessageView = self.viewControllers.body.getView(1);
|
var bodyMessageView = self.viewControllers.body.getView(1);
|
||||||
if(bodyMessageView && _.has(self, 'message.message')) {
|
if(bodyMessageView && _.has(self, 'message.message')) {
|
||||||
bodyMessageView.setText(self.message.message);
|
//self.setBodyMessageViewText();
|
||||||
//bodyMessageView.redraw();
|
bodyMessageView.setText(cleanControlCodes(self.message.message));
|
||||||
|
//bodyMessageView.redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getMessage = function(cb) {
|
this.getMessage = function(cb) {
|
||||||
|
@ -533,7 +541,8 @@ function FullScreenEditorModule(options) {
|
||||||
|
|
||||||
var bodyMessageView = self.viewControllers.body.getView(1);
|
var bodyMessageView = self.viewControllers.body.getView(1);
|
||||||
if(bodyMessageView && _.has(self, 'message.message')) {
|
if(bodyMessageView && _.has(self, 'message.message')) {
|
||||||
bodyMessageView.setText(self.message.message);
|
//self.setBodyMessageViewText();
|
||||||
|
bodyMessageView.setText(cleanControlCodes(self.message.message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -11,6 +11,7 @@ exports.replaceAt = replaceAt;
|
||||||
exports.isPrintable = isPrintable;
|
exports.isPrintable = isPrintable;
|
||||||
exports.debugEscapedString = debugEscapedString;
|
exports.debugEscapedString = debugEscapedString;
|
||||||
exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
||||||
|
exports.cleanControlCodes = cleanControlCodes;
|
||||||
|
|
||||||
// :TODO: create Unicode verison of this
|
// :TODO: create Unicode verison of this
|
||||||
var VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
|
var VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
|
||||||
|
@ -179,14 +180,6 @@ function debugEscapedString(s) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringFromNullTermBuffer(buf, encoding) {
|
function stringFromNullTermBuffer(buf, encoding) {
|
||||||
/*var nullPos = buf.length;
|
|
||||||
for(var i = 0; i < buf.length; ++i) {
|
|
||||||
if(0x00 === buf[i]) {
|
|
||||||
nullPos = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
let nullPos = buf.indexOf(new Buffer( [ 0x00 ] ));
|
let nullPos = buf.indexOf(new Buffer( [ 0x00 ] ));
|
||||||
if(-1 === nullPos) {
|
if(-1 === nullPos) {
|
||||||
nullPos = buf.length;
|
nullPos = buf.length;
|
||||||
|
@ -231,4 +224,104 @@ var stringFormatExtensions = {
|
||||||
// :TODO: Add padding/etc. modifiers.
|
// :TODO: Add padding/etc. modifiers.
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.stringFormatExtensions = stringFormatExtensions;
|
exports.stringFormatExtensions = stringFormatExtensions;
|
||||||
|
|
||||||
|
// :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's
|
||||||
|
//const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g;
|
||||||
|
const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([\?=;0-9]*?)([A-ORZcf-npsu=><])/g;
|
||||||
|
const ANSI_OPCODES_ALLOWED_CLEAN = [
|
||||||
|
'C', 'm' ,
|
||||||
|
'A', 'B', 'D'
|
||||||
|
];
|
||||||
|
|
||||||
|
function cleanControlCodes(input) {
|
||||||
|
let m;
|
||||||
|
let pos;
|
||||||
|
let cleaned = '';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Loop through |input| adding only allowed ESC
|
||||||
|
// sequences and literals to |cleaned|
|
||||||
|
//
|
||||||
|
do {
|
||||||
|
pos = REGEXP_ANSI_CONTROL_CODES.lastIndex;
|
||||||
|
m = REGEXP_ANSI_CONTROL_CODES.exec(input);
|
||||||
|
|
||||||
|
if(null !== m) {
|
||||||
|
if(m.index > pos) {
|
||||||
|
cleaned += input.slice(pos, m.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ANSI_OPCODES_ALLOWED_CLEAN.indexOf(m[2].charAt(0)) > -1) {
|
||||||
|
cleaned += m[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex);
|
||||||
|
|
||||||
|
// remainder
|
||||||
|
if(pos < input.length) {
|
||||||
|
cleaned += input.slice(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCleanAnsi(input) {
|
||||||
|
//
|
||||||
|
// Process |input| and produce |cleaned|, an array
|
||||||
|
// of lines with "clean" ANSI.
|
||||||
|
//
|
||||||
|
// Clean ANSI:
|
||||||
|
// * Contains only color/SGR sequences
|
||||||
|
// * All movement (up/down/left/right) removed but positioning
|
||||||
|
// left intact via spaces/etc.
|
||||||
|
//
|
||||||
|
// Temporary processing will occur in a grid. Each cell
|
||||||
|
// containing a character (defaulting to space) possibly a SGR
|
||||||
|
//
|
||||||
|
|
||||||
|
let m;
|
||||||
|
let pos;
|
||||||
|
let grid = [];
|
||||||
|
let gridPos = { row : 0, col : 0 };
|
||||||
|
|
||||||
|
function updateGrid(data, dataType) {
|
||||||
|
//
|
||||||
|
// Start at to grid[row][col] and populate val[0]...val[N]
|
||||||
|
// creating cells as necessary
|
||||||
|
//
|
||||||
|
if(!grid[gridPos.row]) {
|
||||||
|
grid[gridPos.row] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if('literal' === dataType) {
|
||||||
|
data.forEach(c => {
|
||||||
|
grid[gridPos.row][gridPos.col] = (grid[gridPos.row][gridPos.col] || '') + c; // append to existing SGR
|
||||||
|
gridPos.col++;
|
||||||
|
});
|
||||||
|
} else if('sgr' === dataType) {
|
||||||
|
grid[gridPos.row][gridPos.col] = (grid[gridPos.row][gridPos.col] || '') + data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function literal(s) {
|
||||||
|
let charCode;
|
||||||
|
const len = s.length;
|
||||||
|
for(let i = 0; i < len; ++i) {
|
||||||
|
charCode = s.charCodeAt(i) & 0xff;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
pos = REGEXP_ANSI_CONTROL_CODES.lastIndex;
|
||||||
|
m = REGEXP_ANSI_CONTROL_CODES.exec(input);
|
||||||
|
|
||||||
|
if(null !== m) {
|
||||||
|
if(m.index > pos) {
|
||||||
|
updateGrid(input.slice(pos, m.index), 'literal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex);
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,133 @@
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
exports.wordWrapText = wordWrapText;
|
exports.wordWrapText = wordWrapText2;
|
||||||
|
|
||||||
|
const SPACE_CHARS = [
|
||||||
|
' ', '\f', '\n', '\r', '\v',
|
||||||
|
'\u00a0', '\u1680', '\u180e', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004',
|
||||||
|
'\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200a', '\u2028', '\u2029',
|
||||||
|
'\u202f', '\u205f', '\u3000',
|
||||||
|
];
|
||||||
|
|
||||||
|
const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g');
|
||||||
|
|
||||||
|
//
|
||||||
|
// ANSI & pipe codes we indend to strip
|
||||||
|
//
|
||||||
|
// See also https://github.com/chalk/ansi-regex/blob/master/index.js
|
||||||
|
//
|
||||||
|
// :TODO: Consolidate this, regexp's in ansi_escape_parser, and strutil. Need more complete set that includes common standads, bansi, and cterm.txt
|
||||||
|
const REGEXP_CONTROL_CODES = /(\|[\d]{2})|(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g;
|
||||||
|
|
||||||
|
function getRenderLength(s) {
|
||||||
|
let m;
|
||||||
|
let pos;
|
||||||
|
let len = 0;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Loop counting only literal (non-control) sequences
|
||||||
|
// paying special attention to ESC[<N>C which means foward <N>
|
||||||
|
//
|
||||||
|
do {
|
||||||
|
pos = REGEXP_CONTROL_CODES.lastIndex;
|
||||||
|
m = REGEXP_CONTROL_CODES.exec(s);
|
||||||
|
|
||||||
|
if(null !== m) {
|
||||||
|
if(m.index > pos) {
|
||||||
|
len += s.slice(pos, m.index).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if('C' === m[3]) { // ESC[<N>C is foward/right
|
||||||
|
len += parseInt(m[2], 10) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while(0 !== REGEXP_CONTROL_CODES.lastIndex);
|
||||||
|
|
||||||
|
if(pos < s.length) {
|
||||||
|
len += s.slice(pos).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordWrapText2(text, options) {
|
||||||
|
assert(_.isObject(options));
|
||||||
|
assert(_.isNumber(options.width));
|
||||||
|
|
||||||
|
options.tabHandling = options.tabHandling || 'expand';
|
||||||
|
options.tabWidth = options.tabWidth || 4;
|
||||||
|
options.tabChar = options.tabChar || ' ';
|
||||||
|
|
||||||
|
const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g');
|
||||||
|
|
||||||
|
let m;
|
||||||
|
let word;
|
||||||
|
let c;
|
||||||
|
let renderLen;
|
||||||
|
let i = 0;
|
||||||
|
let wordStart = 0;
|
||||||
|
let result = { wrapped : [ '' ], renderLen : [] };
|
||||||
|
|
||||||
|
function expandTab(column) {
|
||||||
|
const remainWidth = options.tabWidth - (column % options.tabWidth);
|
||||||
|
return new Array(remainWidth).join(options.tabChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendWord() {
|
||||||
|
word.match(REGEXP_GOBBLE).forEach( w => {
|
||||||
|
renderLen = getRenderLength(w);
|
||||||
|
|
||||||
|
if(result.renderLen[i] + renderLen > options.width) {
|
||||||
|
if(0 === i) {
|
||||||
|
result.firstWrapRange = { start : wordStart, end : wordStart + w.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
result.wrapped[++i] = w;
|
||||||
|
result.renderLen[i] = renderLen;
|
||||||
|
} else {
|
||||||
|
result.wrapped[i] += w;
|
||||||
|
result.renderLen[i] = (result.renderLen[i] || 0) + renderLen;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Some of the way we word wrap is modeled after Sublime Test 3:
|
||||||
|
//
|
||||||
|
// * Sublime Text 3 for example considers spaces after a word
|
||||||
|
// part of said word. For example, "word " would be wraped
|
||||||
|
// in it's entirity.
|
||||||
|
//
|
||||||
|
// * Tabs in Sublime Text 3 are also treated as a word, so, e.g.
|
||||||
|
// "\t" may resolve to " " and must fit within the space.
|
||||||
|
//
|
||||||
|
// * If a word is ultimately too long to fit, break it up until it does.
|
||||||
|
//
|
||||||
|
while(null !== (m = REGEXP_WORD_WRAP.exec(text))) {
|
||||||
|
word = text.substring(wordStart, REGEXP_WORD_WRAP.lastIndex - 1);
|
||||||
|
|
||||||
|
c = m[0].charAt(0);
|
||||||
|
if(SPACE_CHARS.indexOf(c) > -1) {
|
||||||
|
word += m[0];
|
||||||
|
} else if('\t' === c) {
|
||||||
|
if('expand' === options.tabHandling) {
|
||||||
|
// Good info here: http://c-for-dummies.com/blog/?p=424
|
||||||
|
word += expandTab(result.wrapped[i].length + word.length) + options.tabChar;
|
||||||
|
} else {
|
||||||
|
word += m[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendWord();
|
||||||
|
wordStart = REGEXP_WORD_WRAP.lastIndex + m[0].length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
word = text.substring(wordStart);
|
||||||
|
appendWord();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function wordWrapText(text, options) {
|
function wordWrapText(text, options) {
|
||||||
//
|
//
|
||||||
|
@ -65,6 +191,7 @@ function wordWrapText(text, options) {
|
||||||
results.firstWrapRange = { start : wordStart, end : wordStart + w.length };
|
results.firstWrapRange = { start : wordStart, end : wordStart + w.length };
|
||||||
//results.firstWrapRange = { start : wordStart, end : wordStart + wordLen };
|
//results.firstWrapRange = { start : wordStart, end : wordStart + wordLen };
|
||||||
}
|
}
|
||||||
|
// :TODO: Must handle len of |w| itself > options.width & split how ever many times required (e.g. handle paste)
|
||||||
results.wrapped[++i] = w;
|
results.wrapped[++i] = w;
|
||||||
} else {
|
} else {
|
||||||
results.wrapped[i] += w;
|
results.wrapped[i] += w;
|
||||||
|
@ -105,4 +232,16 @@ function wordWrapText(text, options) {
|
||||||
addWord();
|
addWord();
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//const input = 'Hello, |04World! This |08i|02s a test it is \x1b[20Conly a test of the emergency broadcast system. What you see is not a joke!';
|
||||||
|
//const input = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five enturies, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
|
||||||
|
|
||||||
|
const iconv = require('iconv-lite');
|
||||||
|
const input = iconv.decode(require('fs').readFileSync('/home/nuskooler/Downloads/msg_out.txt'), 'cp437');
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
width : 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(wordWrapText2(input, opts).wrapped, 'utf8')
|
Loading…
Reference in New Issue