/* jslint node: true */ 'use strict'; // ENiGMA½ const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSI = require('./ansi_term.js'); const { splitTextAtTerms, renderStringLength } = require('./string_util.js'); // deps const _ = require('lodash'); module.exports = function ansiPrep(input, options, cb) { if(!input) { return cb(null, ''); } options.termWidth = options.termWidth || 80; options.termHeight = options.termHeight || 25; options.cols = options.cols || options.termWidth || 80; options.rows = options.rows || options.termHeight || 'auto'; options.startCol = options.startCol || 1; options.exportMode = options.exportMode || false; // in auto we start out at 25 rows, but can always expand for more const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); const state = { row : 0, col : 0, }; let lastRow = 0; function ensureRow(row) { if(canvas[row]) { return; } canvas[row] = Array.from( { length : options.cols}, () => new Object() ); } parser.on('position update', (row, col) => { state.row = row - 1; state.col = col - 1; if(0 === state.col) { state.initialSgr = state.lastSgr; } lastRow = Math.max(state.row, lastRow); }); parser.on('literal', literal => { // // CR/LF are handled for 'position update'; we don't need the chars themselves // literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); for(let c of literal) { if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) { ensureRow(state.row); if(0 === state.col) { canvas[state.row][state.col].initialSgr = state.initialSgr; } canvas[state.row][state.col].char = c; if(state.sgr) { canvas[state.row][state.col].sgr = _.clone(state.sgr); state.lastSgr = canvas[state.row][state.col].sgr; state.sgr = null; } } state.col += 1; } }); parser.on('sgr update', sgr => { ensureRow(state.row); if(state.col < options.cols) { canvas[state.row][state.col].sgr = _.clone(sgr); state.lastSgr = canvas[state.row][state.col].sgr; } else { state.sgr = sgr; } }); function getLastPopulatedColumn(row) { let col = row.length; while(--col > 0) { if(row[col].char || row[col].sgr) { break; } } return col; } parser.on('complete', () => { let output = ''; let line; let sgr; canvas.slice(0, lastRow + 1).forEach(row => { const lastCol = getLastPopulatedColumn(row) + 1; let i; line = ''; for(i = 0; i < lastCol; ++i) { const col = row[i]; sgr = 0 === i ? col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : ''; if(col.sgr) { sgr += ANSI.getSGRFromGraphicRendition(col.sgr); } line += `${sgr}${col.char || ' '}`; } output += line; if(i < row.length) { output += `${ANSI.blackBG()}${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; } if(options.startCol + i < options.termWidth || options.forceLineTerm) { output += '\r\n'; } }); if(options.exportMode) { // // If we're in export mode, we do some additional hackery: // // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) // if a line must wrap early, we'll place a ESC[A ESC[C where // represents chars to get back to the position we were previously at // // * Replace contig spaces with ESC[C as well to save... space. // // :TODO: this would be better to do as part of the processing above, but this will do for now const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with let exportOutput = ''; let m; let afterSeq; let wantMore; let renderStart; splitTextAtTerms(output).forEach(fullLine => { renderStart = 0; while(fullLine.length > 0) { let splitAt; const ANSI_REGEXP = ANSI.getFullMatchRegExp(); wantMore = true; while((m = ANSI_REGEXP.exec(fullLine))) { afterSeq = m.index + m[0].length; if(afterSeq < MAX_CHARS) { // after current seq splitAt = afterSeq; } else { if(m.index < MAX_CHARS) { // before last found seq splitAt = m.index; wantMore = false; // can't eat up any more } break; // seq's beyond this point are >= MAX_CHARS } } if(splitAt) { if(wantMore) { splitAt = Math.min(fullLine.length, MAX_CHARS - 1); } } else { splitAt = Math.min(fullLine.length, MAX_CHARS - 1); } const part = fullLine.slice(0, splitAt); fullLine = fullLine.slice(splitAt); renderStart += renderStringLength(part); exportOutput += `${part}\r\n`; if(fullLine.length > 0) { // more to go for this line? exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; } else { exportOutput += ANSI.up(); } } }); return cb(null, exportOutput); } return cb(null, output); }); parser.parse(input); };