From 6d31589c8b8871a96f9c1026e3a232a23175a9fa Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Nov 2017 18:55:57 -0700 Subject: [PATCH] Add PCB/WildCat!, WWIV, Renegade, etc. color code support to file descriptions --- core/color_codes.js | 158 +++++++++++++++++++++++++++++++++++++++++ mods/file_area_list.js | 47 +++++++----- 2 files changed, 187 insertions(+), 18 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index 4dc8da99..7e71bd1c 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -11,11 +11,13 @@ exports.enigmaToAnsi = enigmaToAnsi; exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; +exports.controlCodesToAnsi = controlCodesToAnsi; // :TODO: Not really happy with the module name of "color_codes". Would like something better + // Also add: // * fromCelerity(): | // * fromPCBoard(): (@X) @@ -148,3 +150,159 @@ function renegadeToAnsi(s, client) { return (0 === result.length ? s : result + s.substr(lastIndex)); } + +// +// Converts various control codes popular in BBS packages +// to ANSI escape sequences. Additionaly supports ENiGMA style +// MCI codes. +// +// Supported control code formats: +// * Renegade : |## +// * PCBoard : @X## where the first number/char is FG color, and second is BG +// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix +// * WWIV : ^# +// +// TODO: Add Synchronet and Celerity format support +// +// Resources: +// * http://wiki.synchro.net/custom:colors +// +function controlCodesToAnsi(s, client) { + const RE = /(\|([A-Z0-9]{2})|\|)|(\@X([0-9A-F]{2}))|(\@([0-9A-F]{2})\@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + + let m; + let result = ''; + let lastIndex = 0; + let v; + let fg; + let bg; + + while((m = RE.exec(s))) { + switch(m[0].charAt(0)) { + case '|' : + // Renegade or ENiGMA MCI + v = parseInt(m[2], 10); + + if(isNaN(v)) { + v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal + } + + if(_.isString(v)) { + result += s.substr(lastIndex, m.index - lastIndex) + v; + } else { + v = ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], + + 8 : [ 'bold', 'black' ], + 9 : [ 'bold', 'blue' ], + 10 : [ 'bold', 'green' ], + 11 : [ 'bold', 'cyan' ], + 12 : [ 'bold', 'red' ], + 13 : [ 'bold', 'magenta' ], + 14 : [ 'bold', 'yellow' ], + 15 : [ 'bold', 'white' ], + + 16 : [ 'blackBG' ], + 17 : [ 'blueBG' ], + 18 : [ 'greenBG' ], + 19 : [ 'cyanBG' ], + 20 : [ 'redBG' ], + 21 : [ 'magentaBG' ], + 22 : [ 'yellowBG' ], + 23 : [ 'whiteBG' ], + }[v] || 'normal'); + + result += s.substr(lastIndex, m.index - lastIndex) + v; + } + break; + + case '@' : + // PCBoard @X## or Wildcat! @##@ + if('@' === m[0].substr(-1)) { + // Wildcat! + v = m[6]; + } else { + v = m[4]; + } + + fg = { + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], + + 8 : [ 'blink', 'black' ], + 9 : [ 'blink', 'blue' ], + A : [ 'blink', 'green' ], + B : [ 'blink', 'cyan' ], + C : [ 'blink', 'red' ], + D : [ 'blink', 'magenta' ], + E : [ 'blink', 'yellow' ], + F : [ 'blink', 'white' ], + }[v.charAt(0)] || ['normal']; + + bg = { + 0 : [ 'blackBG' ], + 1 : [ 'blueBG' ], + 2 : [ 'greenBG' ], + 3 : [ 'cyanBG' ], + 4 : [ 'redBG' ], + 5 : [ 'magentaBG' ], + 6 : [ 'yellowBG' ], + 7 : [ 'whiteBG' ], + + 8 : [ 'bold', 'blackBG' ], + 9 : [ 'bold', 'blueBG' ], + A : [ 'bold', 'greenBG' ], + B : [ 'bold', 'cyanBG' ], + C : [ 'bold', 'redBG' ], + D : [ 'bold', 'magentaBG' ], + E : [ 'bold', 'yellowBG' ], + F : [ 'bold', 'whiteBG' ], + }[v.charAt(1)] || [ 'normal' ]; + + v = ansi.sgr(fg.concat(bg)); + result += s.substr(lastIndex, m.index - lastIndex) + v; + break; + + case '\x03' : + v = parseInt(m[8], 10); + + if(isNaN(v)) { + v += m[0]; + } else { + v = ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'bold', 'cyan' ], + 2 : [ 'bold', 'yellow' ], + 3 : [ 'reset', 'magenta' ], + 4 : [ 'bold', 'white', 'blueBG' ], + 5 : [ 'reset', 'green' ], + 6 : [ 'bold', 'blink', 'red' ], + 7 : [ 'bold', 'blue' ], + 8 : [ 'reset', 'blue' ], + 9 : [ 'reset', 'cyan' ], + }[v] || 'normal'); + } + + result += s.substr(lastIndex, m.index - lastIndex) + v; + + break; + } + + lastIndex = RE.lastIndex; + } + + return (0 === result.length ? s : result + s.substr(lastIndex)); +} \ No newline at end of file diff --git a/mods/file_area_list.js b/mods/file_area_list.js index 4937eb49..0c24f9f8 100644 --- a/mods/file_area_list.js +++ b/mods/file_area_list.js @@ -2,22 +2,23 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const ViewController = require('../core/view_controller.js').ViewController; -const ansi = require('../core/ansi_term.js'); -const theme = require('../core/theme.js'); -const FileEntry = require('../core/file_entry.js'); -const stringFormat = require('../core/string_format.js'); -const FileArea = require('../core/file_base_area.js'); -const Errors = require('../core/enig_error.js').Errors; -const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; -const ArchiveUtil = require('../core/archive_util.js'); -const Config = require('../core/config.js').config; -const DownloadQueue = require('../core/download_queue.js'); -const FileAreaWeb = require('../core/file_area_web.js'); -const FileBaseFilters = require('../core/file_base_filter.js'); -const resolveMimeType = require('../core/mime_util.js').resolveMimeType; -const isAnsi = require('../core/string_util.js').isAnsi; +const MenuModule = require('../core/menu_module.js').MenuModule; +const ViewController = require('../core/view_controller.js').ViewController; +const ansi = require('../core/ansi_term.js'); +const theme = require('../core/theme.js'); +const FileEntry = require('../core/file_entry.js'); +const stringFormat = require('../core/string_format.js'); +const FileArea = require('../core/file_base_area.js'); +const Errors = require('../core/enig_error.js').Errors; +const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; +const ArchiveUtil = require('../core/archive_util.js'); +const Config = require('../core/config.js').config; +const DownloadQueue = require('../core/download_queue.js'); +const FileAreaWeb = require('../core/file_area_web.js'); +const FileBaseFilters = require('../core/file_base_filter.js'); +const resolveMimeType = require('../core/mime_util.js').resolveMimeType; +const isAnsi = require('../core/string_util.js').isAnsi; +const controlCodesToAnsi = require('../core/color_codes.js').controlCodesToAnsi; // deps const async = require('async'); @@ -385,9 +386,19 @@ exports.getModule = class FileAreaList extends MenuModule { if(_.isString(self.currentFileEntry.desc)) { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); if(descView) { - if(isAnsi(self.currentFileEntry.desc)) { + // + // For descriptions we want to support as many color code systems + // as we can for coverage of what is found in the while (e.g. Renegade + // pipes, PCB @X##, etc.) + // + // MLTEV doesn't support all of this, so convert. If we produced ANSI + // esc sequences, we'll proceed with specialization, else just treat + // it as text. + // + const desc = controlCodesToAnsi(self.currentFileEntry.desc); + if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { descView.setAnsi( - self.currentFileEntry.desc, + desc, { prepped : false, forceLineTerm : true