colletch
Version:
A collection of etch components
1,171 lines (972 loc) • 36.3 kB
JavaScript
/** @babel */
/** @jsx etch.dom */
/* global Promise Symbol document window atom */
import etch from 'etch'
import {Emitter} from 'atom'
import EtchComponent from './etch-component'
import symbols from './symbols'
const createNewLine = Symbol()
const cursorLineElement = Symbol()
const metaForLine = Symbol()
const elementForLine = Symbol()
const insert = Symbol()
const buildAttributes = Symbol()
const setState = Symbol()
const forEachToken = Symbol()
const resetData = Symbol()
const moveCursor = Symbol()
const classes = [
"ansi-black",
"ansi-red",
"ansi-green",
"ansi-yellow",
"ansi-blue",
"ansi-magenta",
"ansi-cyan",
"ansi-white",
"ansi-bright-black",
"ansi-bright-red",
"ansi-bright-green",
"ansi-bright-yellow",
"ansi-bright-blue",
"ansi-bright-magenta",
"ansi-bright-cyan",
"ansi-bright-white"
]
const palette = [
[0, 0, 0], // class_name: "ansi-black"
[187, 0, 0], // class_name: "ansi-red"
[0, 187, 0], // class_name: "ansi-green"
[187, 187, 0], // class_name: "ansi-yellow"
[0, 0, 187], // class_name: "ansi-blue"
[187, 0, 187], // class_name: "ansi-magenta"
[0, 187, 187], // class_name: "ansi-cyan"
[255, 255, 255], // class_name: "ansi-white"
[85, 85, 85], // class_name: "ansi-bright-black"
[255, 85, 85], // class_name: "ansi-bright-red"
[0, 255, 0], // class_name: "ansi-bright-green"
[255, 255, 85], // class_name: "ansi-bright-yellow"
[85, 85, 255], // class_name: "ansi-bright-blue"
[255, 85, 255], // class_name: "ansi-bright-magenta"
[85, 255, 255], // class_name: "ansi-bright-cyan"
[255, 255, 255], // class_name: "ansi-bright-white"
]
// Index 16..231 : RGB 6x6x6
// https://gist.github.com/jasonm23/2868981#file-xterm-256color-yaml
const levels = [
0,
95,
135,
175,
215,
255
];
for (let r = 0; r < 6; ++r) {
for (let g = 0; g < 6; ++g) {
for (let b = 0; b < 6; ++b) {
palette.push([
levels[r], levels[g], levels[b]
])
}
}
}
// Index 232..255 : Grayscale
for (let i = 0, grey = 8; i < 24; ++i, grey += 10) {
palette.push([grey, grey, grey])
}
const DEFAULT_BACKGROUND = palette[0]
const DEFAULT_FOREGROUND = palette[7]
const DEFAULT_STATE = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
invert: false,
conceal: false,
bold: false,
italic: false,
underline: false,
strikethrough: false,
blinkSlow: false,
blinkFast: false
}
function innerTextNode (node) {
while (node && 3 !== node.nodeType) {
node = node.firstChild
}
if (node && 3 === node.nodeType) {
return node
}
return null
}
function splitNode (node, delta) {
const clone = node.cloneNode(true)
const leading = innerTextNode(clone)
const trailing = innerTextNode(node)
leading.textContent = leading.textContent.substr(0, delta)
trailing.textContent = trailing.textContent.substr(delta)
clone.dataset.length = delta
node.dataset.length -= delta
node.parentNode.insertBefore(clone, node)
return clone
}
class Token
{
constructor () {
this.mode = null
this.args = []
this.modifier = null
this.command = null
this.text = ''
this.currArg = ''
}
finalize () {
if (this.currArg) {
this.args.push(this.currArg)
this.currArg = null
}
this.args = this.args.map(Number)
this.finalized = true
return this
}
isValid () {
return !!(this.command || this.text)
}
toString () {
let c = this.mode || ''
c += this.args.join(';')
c += this.modifier || ''
c += this.command || ''
c += this.text
return c
}
getArg (idx, def) {
if (!Number.isNaN(this.args[idx])) {
return this.args[idx]
}
return def
}
}
class Cursor
{
constructor (row = 1, column = 1) {
this.row = row
this.column = column
this.state = Object.assign({}, DEFAULT_STATE)
}
clone () {
return new Cursor(this.getRow(), this.getColumn())
}
reset () {
this.clear()
this.resetState()
this.row = 1
this.column = 1
// NOTE when etch detects a parent node removal, it propogates all child
// nodes removing them from the parents. This means this.element.children
// will also be removed
this.element = null
}
clear () {
if (this.element) {
this.element.textContent = ''
}
}
getRow () {
return Math.max(this.row, 1)
}
getColumn () {
return Math.max(this.column, 1)
}
storePosition () {
this.storePositiondRow = this.getRow()
this.storePositiondColumn = this.getColumn()
}
restorePosition () {
return new Cursor(this.storePositiondRow || this.getRow(), this.storePositiondColumn || this.getColumn())
}
focus () {
if (this.element) {
this.element.lastChild.focus()
}
}
blur () {
if (this.element) {
this.element.lastChild.blur()
}
}
getElement () {
if (!this.element) {
const input = document.createElement('input')
const display = document.createElement('span')
const cursor = document.createElement('cursor')
cursor.textContent = ' '
cursor.style.width = '0.6em'
let index = 0
let length = 0
const handleKeyboard = (event) => {
if (event.ctrlKey && event.key === 'c' && !event.altKey) {
this.write('^C')
if (this.emitter) {
this.emitter.emit('signal', 'SIGKILL')
}
} else if (1 === event.key.length) {
if (index === length) {
display.textContent += event.key
} else if (0 === index) {
display.textContent = event.key + display.textContent
} else {
const leading = display.textContent.slice(0, index)
const trailing = display.textContent.slice(index)
display.textContent = leading + event.key + trailing
}
index++
length++
} else {
switch (event.key) {
case 'ArrowLeft':
index = Math.max(index - 1, 0)
break
case 'ArrowRight':
index = Math.min(index + 1, length)
break
case 'Backspace':
if (0 === index) {
break
}
index--
// Fall through
case 'Delete':
if (index < length - 1) {
const leading = display.textContent.slice(0, index)
const trailing = display.textContent.slice(index + 1)
display.textContent = leading + trailing
length--
}
break
case 'Enter':
if (this.emitter) {
this.emitter.emit('input', display.textContent + '\n')
}
display.textContent = ''
index = 0
length = 0
break
}
}
cursor.style.left = `calc(0.6em * ${index})`
cursor.textContent = display.textContent.charCodeAt(index) ? display.textContent[index] : ' '
input.value = ''
}
input.type = 'text'
input.onkeydown = handleKeyboard
input.onfocus = () => this.element.classList.add('blinking-cursor')
input.onblur = () => this.element.classList.remove('blinking-cursor')
this.element = document.createElement('span')
this.element.dataset.length = 0
this.element.className = 'etch-term-cursor'
this.element.onclick = () => input.focus()
this.element.appendChild(display)
this.element.appendChild(cursor)
this.element.appendChild(input)
}
return this.element
}
getState () {
return Object.assign({}, this.state)
}
setState (state) {
Object.assign(this.state, state)
if (this.element) {
const {classes, styles} = this[buildAttributes]()
this.element.className = [
'etch-term-cursor',
...classes
].join(' ')
for (const name in styles) {
this.element.style[name] = styles[name]
}
}
}
resetState () {
this.setState(DEFAULT_STATE)
}
write (text) {
const {classes, styles} = this[buildAttributes]()
const span = document.createElement('span')
span.classList.add(...classes)
span.dataset.length = text.length
span.textContent = text
for (const name in styles) {
span.style[name] = styles[name]
}
const parentNode = this.element ? this.element.parentNode : null
if (!parentNode) {
throw new Error('Cursor is not attached')
}
parentNode.insertBefore(span, this.element)
parentNode.dataset.length = parseInt(parentNode.dataset.length) + text.length
this.column += text.length
// Remove text.length after
let nextNode = this.element.nextSibling
let remainder = text.length
while (nextNode && remainder >= nextNode.dataset.length) {
const nodeLength = parseInt(nextNode.dataset.length)
parentNode.dataset.length = parseInt(parentNode.dataset.length) - nodeLength
remainder -= nodeLength
parentNode.removeChild(nextNode)
nextNode = this.element.nextSibling
}
if (nextNode && remainder) {
const leading = splitNode(nextNode, remainder)
parentNode.dataset.length = parseInt(parentNode.dataset.length) - remainder
parentNode.removeChild(leading)
}
let total = Array.from(parentNode.childNodes).reduce((t, i) => t + parseInt(i.dataset.length), 0)
console.assert(!isNaN(parentNode.dataset.length), `not a number ${parentNode.dataset.length}`) // eslint-disable-line no-undef
console.assert(parentNode.dataset.length == total, `line total missmatch ${parentNode.dataset.length} !== ${total}`) // eslint-disable-line no-undef
return span
}
on (name, callback) {
if (!this.emitter) {
this.emitter = new Emitter()
}
return this.emitter.on(name, callback)
}
[buildAttributes] ({background = DEFAULT_BACKGROUND, foreground = DEFAULT_FOREGROUND} = {}) {
const classes = []
const styles = {}
let bg = this.state.background
let fg = this.state.foreground
if (this.state.invert) {
const swap = bg
bg = fg
fg = swap
}
if (this.state.conceal) {
fg = bg
}
if (background !== bg) {
if (typeof bg === 'string') {
classes.push(bg + '-bg')
} else {
styles['background-color'] = 'rgb(' + bg.join(',') + ')'
}
}
if (foreground !== fg) {
if (typeof fg === 'string') {
classes.push(fg + '-fg')
} else {
styles['color'] = 'rgb(' + fg.join(',') + ')'
}
}
if (this.state.bold) { // font-weight: bold
styles.fontWeight = 'bold'
}
if (this.state.italic) { // font-style: italic
styles.fontStyle = 'italic'
}
if (this.state.underline) { // text-decoration: underline
styles.textDecoration = 'underline'
}
if (this.state.strikethrough) { // text-decoration: line-through
styles.textDecoration = (styles.textDecoration ? styles.textDecoration + ' ' : '') + 'line-through'
}
return {classes, styles, background: bg, foreground: fg}
}
}
export default class EtchTerminal extends EtchComponent
{
constructor () {
super(...arguments)
if (this[symbols.self].properties.onRender) {
this.on('render', this[symbols.self].properties.onRender)
}
if (this[symbols.self].properties.onInput) {
this.on('input', this[symbols.self].properties.onInput)
}
if (false === this[symbols.self].properties.showCursor) {
this.hideCursor()
}
}
update () {
return Promise.resolve()
}
clear () {
return this[symbols.scheduleUpdate](() => {
this[resetData]()
etch.updateSync(this)
if (false === this[symbols.self].properties.showCursor) {
return this.hideCursor()
}
})
}
focus () {
if (this[symbols.self].properties.enableInput) {
this[symbols.self].cursor.focus()
}
}
blur () {
if (this[symbols.self].properties.enableInput) {
this[symbols.self].cursor.blur()
}
}
write (data) {
return this[symbols.scheduleUpdate](() => {
const listElement = this.refs.list
let autoScroll = listElement.scrollHeight - listElement.scrollTop === listElement.clientHeight
this[forEachToken](data, token => {
this[setState](token)
if (token.text) {
this[insert](token.text)
}
})
etch.updateSync(this)
if (autoScroll) {
this[elementForLine](-1).scrollIntoView()
}
this.focus()
})
}
writeln (data) {
return this.write(data + '\n')
}
selectAll () {
const selection = window.getSelection()
selection.removeAllRanges()
const range = document.createRange()
range.selectNodeContents(this.element)
selection.addRange(range)
}
getSelection () {
const selection = window.getSelection()
if (this.element.contains(selection.anchorNode) && this.element.contains(selection.focusNode)) {
return selection.toString()
}
return ''
}
copySelection () {
const selection = this.getSelection()
atom.clipboard.write(selection)
return selection
}
pasteContent (data) {
if (!this[symbols.self].properties.enableInput) {
return
}
// Care needs to be taken when data contains newlines. They should invoke an
// 'Enter'
const lines = data.split('\n')
for (let i = 0; i < lines.length - 1; i++) {
const text = lines[i]
// TODO should we append this to current line?
this[symbols.emit]('input', text + '\n')
}
this[symbols.self].input.value = lines[lines.length - 1]
}
getCursor () {
return this[symbols.self].cursor.clone()
}
setCursor (row, column) {
return this[symbols.scheduleUpdate](() => {
this[moveCursor](new Cursor(row, column))
})
}
showCursor () {
return this[symbols.scheduleUpdate](() => {
const element = this[symbols.self].cursor.getElement()
element.style.display = 'initial'
})
}
hideCursor () {
return this[symbols.scheduleUpdate](() => {
const element = this[symbols.self].cursor.getElement()
element.style.display = 'none'
})
}
replaceNode (span, nodes) {
if (!Array.isArray(nodes)) {
nodes = [nodes]
}
const idx = nodes.indexOf(span)
if (-1 !== idx) {
if (typeof span.cloneNode !== 'function') {
throw new Error('Given span is not clonable')
}
nodes[idx] = span.cloneNode(true)
}
let lineElement = null
if (span.parentNode && span.dataset && /^\d+$/.test(span.dataset.length)) {
for (const li of this.refs.list.childNodes) {
if (span.parentNode === li && li.contains(span)) {
lineElement = li
break
}
}
}
let cursorElement = this[symbols.self].cursor.getElement()
if (!lineElement || span === cursorElement) {
throw new Error('Given span does not belong to list')
}
if (lineElement === cursorElement.parentNode) {
let next = span
while (next) {
if (next === cursorElement) {
break
}
next = next.nextSibling
}
if (!next) {
cursorElement = null
}
} else {
cursorElement = null
}
let lineLength = parseInt(lineElement.dataset.length)
let cursorOffset = parseInt(span.dataset.length)
for (const node of nodes) {
const nodeLength = node.textContent.length
node.dataset.length = nodeLength
lineLength += nodeLength
cursorOffset -= nodeLength
lineElement.insertBefore(node, span)
}
lineLength -= parseInt(span.dataset.length)
lineElement.dataset.length = lineLength
lineElement.removeChild(span)
if (cursorElement) {
this[symbols.self].cursor.column -= cursorOffset
}
}
render () {
return (
<ul
ref="list"
className={ this[symbols.getClassName]('etch-term', 'native-key-bindings') }
tabIndex="-1"
onClick={ () => this[symbols.self].cursor.focus() }
>
{ this[symbols.self].virtualLines }
</ul>
)
}
[symbols.initialize] () {
this[symbols.self].cursor = new Cursor()
this[resetData]()
const changeListener = () => {
const selection = window.getSelection()
const selectedNode = selection.baseNode
if (!selectedNode || selectedNode !== this.element || this.element.contains(selectedNode)) {
if (selection.isCollapsed) {
this.element.classList.remove('has-selection')
} else {
this.element.classList.add('has-selection')
}
}
}
this[symbols.addEventListener](document, 'selectionchange', changeListener)
this[symbols.addDisposable](this[symbols.self].cursor.on('signal', (signal) => {
this[symbols.emit]('signal', signal)
}))
this[symbols.addDisposable](this[symbols.self].cursor.on('input', (data) => {
this[symbols.emit]('input', data)
}))
}
[symbols.getDefaultProperties] () {
return {
enableInput: true
}
}
[insert] (text) {
const span = this[symbols.self].cursor.write(text)
this[symbols.emit]('render', {
component: this,
list: this.refs.list,
line: this[cursorLineElement](),
span: span
})
}
[cursorLineElement] () {
const lineIndex = this[symbols.self].cursor.getRow() - 1
const cursorElement = this[symbols.self].cursor.getElement()
const element = this[elementForLine](lineIndex)
if (!element.contains(cursorElement)) {
throw new Error(`Cursor does not belong to current line (${lineIndex})`)
}
return element
}
[metaForLine] (idx = null) {
if (null === idx) {
idx = this[symbols.self].cursor.getRow() - 1
} else if (idx < 0) {
idx = this[symbols.self].virtualLines.length + idx
}
if (!(idx in this[symbols.self].virtualLines)) {
throw new Error(`Line index '${idx} is out of range (0 - ${this[symbols.self].virtualLines.length})'`)
}
return this[symbols.self].virtualLines[idx]
}
[elementForLine] (idx = null) {
return this[metaForLine](idx).element
}
[resetData] () {
this[symbols.self].virtualLines = []
this[symbols.self].buffer = ''
this[symbols.self].cursor.reset()
this[createNewLine]()
this[symbols.self].virtualLines[0].element.appendChild(this[symbols.self].cursor.getElement())
}
[moveCursor] (cursor) {
// NOTE cursor is 1 based
const lineIndex = cursor.getRow() - 1
const columnIndex = cursor.getColumn() - 1
while (this[symbols.self].virtualLines.length <= lineIndex) {
this[createNewLine](true)
}
const currLine = this[symbols.self].virtualLines[lineIndex].element
const cursorElement = this[symbols.self].cursor.getElement()
const currLineLength = currLine.dataset.length
if (0 === columnIndex) {
currLine.insertBefore(cursorElement, currLine.firstChild)
} else if (currLineLength === columnIndex) {
currLine.appendChild(cursorElement)
} else if (currLineLength > columnIndex) {
let node = currLine.firstChild
let prev = null
let currStartIndex = 0
while (currStartIndex < columnIndex) {
currStartIndex += node.innerText.length
prev = node
node = node.nextSibling
}
if (currStartIndex !== columnIndex) {
node = prev
currStartIndex -= node.innerText.length
const delta = columnIndex - currStartIndex
splitNode(node, delta)
}
currLine.insertBefore(cursorElement, node)
} else {
const padding = ' '.repeat(columnIndex - currLineLength)
currLine.appendChild(cursorElement)
this[symbols.self].cursor.write(padding)
}
// console.log(`Moved cursor to row: ${cursor.getRow()}, column: ${cursor.getColumn()}`);
// console.assert(currLine.contains(cursorElement))
this[symbols.self].cursor.row = cursor.getRow()
this[symbols.self].cursor.column = cursor.getColumn()
}
[createNewLine] (append = false) {
const line = document.createElement('li')
line.classList.add('etch-term-line')
line.dataset.length = 0
this[symbols.self].virtualLines.push({
tag: function() {
// NOTE unbound functions have their own 'this'
this.element = line
},
element: line,
})
if (append) {
this.refs.list.appendChild(line)
}
return line
}
[setState] (token) {
if ('m' === token.command) {
let state = this[symbols.self].cursor.getState()
// TODO styles should be storePositiond in the cursor
// See https://stackoverflow.com/a/33206814/1479092 for token args
for (let i = 0; i < token.args.length; i++) {
let arg = parseInt(token.args[i])
switch (arg) {
case 0: // Reset
Object.assign(state, DEFAULT_STATE)
// state = this[symbols.self].cursor.resetState()
break
case 1:
state.bold = true
break
// case 2: Faint
case 3: // Italic
state.italic = true
break
case 4: // Underline
state.underline = true
break
case 5: // Slow Blink
state.blinkSlow = true
break
case 6: // Rapid Blink
state.blinkFast = true
break
case 7: // Invert Colours
state.invert = true
break
case 8: // Conceal
state.conceal = true
break
case 9: // Crossed-Out
state.strikethrough = true;
break
case 21: // Faint-Off
case 22: // Bold-Off
state.bold = false
break
case 23: // Italic-Off
state.italic = false
break
case 24: // Underline-Off
state.underline = false
break
case 25: // Blink Off
state.blinkSlow = false
break
case 26: // Blink Off
state.blinkFast = false
break
case 27: // Invert Off
state.invert = false
break
case 28: // Conceal Off
state.conceal = false
break
case 29: // Crossed-Out Off
state.strikethrough = false
break
case 38:
case 48:
if (i + 2 < token.args.length) {
const mode = token.args[i + 1]
let color
if (mode === '5') {
const idx = parseInt(token.args[i + 2])
if (0 <= idx && idx <= 255) {
color = palette[idx]
}
i += 2
} else if (mode === '2' && i + 4 < token.args.length) {
const r = parseInt(token.args[i + 2], 10)
const g = parseInt(token.args[i + 3], 10)
const b = parseInt(token.args[i + 4], 10)
if ((0 <= r && r <= 255) && (0 <= g && g <= 255) && (0 <= b && b <= 255)) {
color = [r, g, b]
}
i += 4
}
if (arg === 38) {
state.foreground = color
} else {
state.background = color
}
}
break
case 39: // Foreground Colour
state.foreground = DEFAULT_FOREGROUND
break
case 49: // Background Colour
state.background = DEFAULT_BACKGROUND
break
default:
// Standard Foreground Colour
if (30 <= arg && arg <= 37) {
// NOTE: if bold (1) is also present 8 should be added to the index
state.foreground = classes[
arg - 30// Standard Background Colour
]
} else if (40 <= arg && arg <= 47) {
// NOTE: if bold (1) is also present 8 should be added to the index
state.background = classes[
arg - 40// Bright Foreground Colour
]
} else if (90 <= arg && arg <= 98) {
state.foreground = palette[
arg - 82// Bright Background Colour
]
} else if (100 <= arg && arg <= 108) {
state.background = palette[arg - 92]
}
break
}
}
this[symbols.self].cursor.setState(state)
} else if ('A' <= token.command && token.command <= 'G') {
const cursor = this[symbols.self].cursor.clone()
const offset = token.getArg(0, 1)
switch (token.command) {
case 'A': // Move up
cursor.row = Math.max(cursor.row - offset, 1)
break
case 'B': // Move down
cursor.row += offset
break
case 'C': // Move forward
cursor.column += offset
break
case 'D': // Move backward
cursor.column = Math.max(cursor.column - offset, 1)
break
case 'E': // Move next line
cursor.column = 1
cursor.row += offset
break
case 'F': // Move prev line
cursor.column = 1
cursor.row = Math.max(cursor.row - offset, 1)
break
case 'G': // Move to column
cursor.column = Math.max(offset, 1)
break
}
this[moveCursor](cursor)
} else if ('H' === token.command) {
const row = token.getArg(0, 1)
const col = token.getArg(1, 1)
this[moveCursor](new Cursor(row, col))
} else if ('s' === token.command) {
this.token.storePosition()
} else if ('u' === token.command) {
this[moveCursor](this.token.restorePosition())
}
}
[forEachToken] (data, cb) {
data = this[symbols.self].buffer + data
this[symbols.self].buffer = ''
let nextIndex = 0
const readNextChar = () => {
let char = data[nextIndex]
let code = data.charCodeAt(nextIndex)
if (isNaN(code)) {
return null
}
nextIndex += 1
// Surrogate high
if (0xD800 <= code && code <= 0xDBFF) {
const low = data.charCodeAt(nextIndex)
if (isNaN(low)) {
this[symbols.self].buffer = char
return null
}
nextIndex += 1
if (0xDC00 <= low && low <= 0xDFFF) {
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000
char += data.charAt(nextIndex + 1)
}
}
return {char, code}
}
let token = new Token()
const finalizeToken = (final = '') => {
if ('\x1B' === final[0] && '[' === final[1]) {
cb(token.finalize())
token = new Token()
let index = 2
while (index < final.length) {
const char = final[index]
const code = final.charCodeAt(index)
if (code === 33 || (code >= 60 && code <= 63)) {
token.mode = char
}
// ';' separated digits are command args
else if (code >= 48 && code <= 57) {
token.currArg += char
}
// argument separator
else if (code === 59) {
if (token.currArg) {
token.args.push(token.currArg)
token.currArg = ''
}
}
// (space), '!', '"', '#', '$', '%', '&', ''', '(', ')', '*', '+', ',', '-', '.', '/'
// Are intermedaite modifiers
else if (code >= 32 && code <= 47) {
token.modifier = char
}
// The command itself
else if (code >= 64 && code <= 126) {
token.command = char
}
index++
}
} else {
token.text += final
}
cb(token.finalize())
token = new Token()
}
let parseEscape = false
let iter = readNextChar()
while (iter) {
const {char, code} = iter
if (parseEscape) {
// The leading chars (!, <, =, >, ?) are private mode
if (code === 33 || (code >= 60 && code <= 63)) {
token.mode = char
}
// ';' separated digits are command args
else if (code >= 48 && code <= 57) {
token.currArg += char
}
// argument separator
else if (code === 59) {
if (token.currArg) {
token.args.push(token.currArg)
token.currArg = ''
}
}
// (space), '!', '"', '#', '$', '%', '&', ''', '(', ')', '*', '+', ',', '-', '.', '/'
// Are intermedaite modifiers
else if (code >= 32 && code <= 47) {
token.modifier = char
}
// The command itself
else if (code >= 64 && code <= 126) {
token.command = char
parseEscape = false
}
// Illegal char within escape sequence
else {
// Render the whole sequence and let the browser handle the display
const newToken = new Token()
newToken.text = token.toString()
token = newToken
parseEscape = false
}
} else if (char === '\r') {
// Translates to 'move to column 1'
finalizeToken('\x1B[0G')
} else if (char === '\n') {
// Translates to 'move to column 1' then 'move down one line'
finalizeToken('\x1B[0G')
finalizeToken('\x1B[1B')
} else if (char === '\b') {
// Translates to 'move back one space'
finalizeToken('\x1B[1D')
} else if (code === 27) {
iter = readNextChar()
if (!iter) {
// Incomplete Escape sequence
this[symbols.self].buffer = "\x1B"
break
} else if (iter.char === '[') {
// Begin reading an ANSI escape sequence
finalizeToken()
parseEscape = true
} else {
// Unprintable character. The browser should print a special
token.text += "\x1B" + char
}
}
else {
// Add char to current span data
token.text += char
}
iter = readNextChar()
}
if (!token.isValid()) {
// If the buffer was written above, the token should be empty
this[symbols.self].buffer += token.toString()
} else {
finalizeToken()
}
}
}