parse-ansi
Version:
🤖 Parse ANSI text into an array of ansi-tags and text-chunks.
258 lines (203 loc) • 4.85 kB
JavaScript
const arrayUniq = require('array-uniq')
const ansiRegex = require('ansi-regex')
const superSplit = require('super-split')
const stripAnsi = require('strip-ansi')
const ansiTags = require('./ansi-seqs-to-ansi-tags')
const decorators = require('./ansi-tags-to-decorator-names')
const meassureTextArea = plainText => {
const lines = plainText.split('\n')
const rows = lines.length
let columns = 0
lines.forEach(line => {
const len = line.length
if (len > columns) {
columns = len
}
})
return {columns, rows}
}
// Atomize
// Splits text into "words" by sticky delimiters [ANSI Escape Seq, \n]
// Eg: words = ['\u001b[37m', 'Line 1', '\n', 'Line 2', '\u001b[39m']
const atomize = text => {
const ansies = arrayUniq(text.match(ansiRegex()))
const words = superSplit(text, ansies.concat(['\n']))
return {ansies, words}
}
const parse = ansi => {
const plainText = stripAnsi(ansi)
const textArea = meassureTextArea(plainText)
const result = {
raw: ansi,
plainText,
textArea,
chunks: []
}
const {
ansies,
words
} = atomize(ansi)
const styleStack = {
foregroundColor: [],
backgroundColor: [],
boldDim: []
}
const getForegroundColor = () => {
if (styleStack.foregroundColor.length > 0) {
return styleStack.foregroundColor[styleStack.foregroundColor.length - 1]
}
return false
}
const getBackgroundColor = () => {
if (styleStack.backgroundColor.length > 0) {
return styleStack.backgroundColor[styleStack.backgroundColor.length - 1]
}
return false
}
const getDim = () => {
return styleStack.boldDim.includes('dim')
}
const getBold = () => {
return styleStack.boldDim.includes('bold')
}
const styleState = {
italic: false,
underline: false,
inverse: false,
hidden: false,
strikethrough: false
}
let x = 0
let y = 0
let nAnsi = 0
let nPlain = 0
const bundle = (type, value) => {
const chunk = {
type,
value,
position: {
x, y, n: nPlain, raw: nAnsi
}
}
if (type === 'text' || type === 'ansi') {
const style = {}
const foregroundColor = getForegroundColor()
const backgroundColor = getBackgroundColor()
const dim = getDim()
const bold = getBold()
if (foregroundColor) {
style.foregroundColor = foregroundColor
}
if (backgroundColor) {
style.backgroundColor = backgroundColor
}
if (dim) {
style.dim = dim
}
if (bold) {
style.bold = bold
}
if (styleState.italic) {
style.italic = true
}
if (styleState.underline) {
style.underline = true
}
if (styleState.inverse) {
style.inverse = true
}
if (styleState.strikethrough) {
style.strikethrough = true
}
chunk.style = style
}
return chunk
}
words.forEach(word => {
// Newline character
if (word === '\n') {
const chunk = bundle('newline', '\n')
result.chunks.push(chunk)
x = 0
y += 1
nAnsi += 1
nPlain += 1
return
}
// Text characters
if (ansies.includes(word) === false) {
const chunk = bundle('text', word)
result.chunks.push(chunk)
x += word.length
nAnsi += word.length
nPlain += word.length
return
}
// ANSI Escape characters
const ansiTag = ansiTags[word]
const decorator = decorators[ansiTag]
const color = ansiTag
if (decorator === 'foregroundColorOpen') {
styleStack.foregroundColor.push(color)
}
if (decorator === 'foregroundColorClose') {
styleStack.foregroundColor.pop()
}
if (decorator === 'backgroundColorOpen') {
styleStack.backgroundColor.push(color)
}
if (decorator === 'backgroundColorClose') {
styleStack.backgroundColor.pop()
}
if (decorator === 'boldOpen') {
styleStack.boldDim.push('bold')
}
if (decorator === 'dimOpen') {
styleStack.boldDim.push('dim')
}
if (decorator === 'boldDimClose') {
styleStack.boldDim.pop()
}
if (decorator === 'italicOpen') {
styleState.italic = true
}
if (decorator === 'italicClose') {
styleState.italic = false
}
if (decorator === 'underlineOpen') {
styleState.underline = true
}
if (decorator === 'underlineClose') {
styleState.underline = false
}
if (decorator === 'inverseOpen') {
styleState.inverse = true
}
if (decorator === 'inverseClose') {
styleState.inverse = false
}
if (decorator === 'strikethroughOpen') {
styleState.strikethrough = true
}
if (decorator === 'strikethroughClose') {
styleState.strikethrough = false
}
if (decorator === 'reset') {
styleState.strikethrough = false
styleState.inverse = false
styleState.italic = false
styleStack.boldDim = []
styleStack.backgroundColor = []
styleStack.foregroundColor = []
}
const chunk = bundle('ansi', {
tag: ansiTag,
ansi: word,
decorator
})
result.chunks.push(chunk)
nAnsi += word.length
})
return result
}
module.exports = parse