Render IRC colors and formatting, closes #46

This commit is contained in:
Ken-Håvard Lieng 2020-05-16 08:25:58 +02:00
parent ed432881ef
commit 0c902f8ac8
3 changed files with 411 additions and 53 deletions

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,7 @@ import {
isChannel,
formatDate
} from 'utils';
import colorify from 'utils/colorify';
import createReducer from 'utils/createReducer';
import { getApp } from './app';
import { getSelectedTab } from './tab';
@ -224,7 +225,8 @@ function initMessage(message, tab, state) {
6 * charWidth,
windowWidth
);
message.content = linkify(message.content);
message.content = colorify(linkify(message.content));
return message;
}

356
client/js/utils/colorify.js Normal file
View File

@ -0,0 +1,356 @@
import React from 'react';
const formatChars = {
bold: 0x02,
italic: 0x1d,
underline: 0x1f,
strikethrough: 0x1e,
color: 0x03,
reverseColor: 0x16,
reset: 0x0f
};
const colors = {
0: 'white',
1: 'black',
2: 'blue',
3: 'green',
4: 'red',
5: 'brown',
6: 'magenta',
7: 'orange',
8: 'yellow',
9: 'lightgreen',
10: 'cyan',
11: 'lightcyan',
12: 'lightblue',
13: 'pink',
14: 'gray',
15: 'lightgray',
16: '#470000',
17: '#472100',
18: '#474700',
19: '#324700',
20: '#004700',
21: '#00472c',
22: '#004747',
23: '#002747',
24: '#000047',
25: '#2e0047',
26: '#470047',
27: '#47002a',
28: '#740000',
29: '#743a00',
30: '#747400',
31: '#517400',
32: '#007400',
33: '#007449',
34: '#007474',
35: '#004074',
36: '#000074',
37: '#4b0074',
38: '#740074',
39: '#740045',
40: '#b50000',
41: '#b56300',
42: '#b5b500',
43: '#7db500',
44: '#00b500',
45: '#00b571',
46: '#00b5b5',
47: '#0063b5',
48: '#0000b5',
49: '#7500b5',
50: '#b500b5',
51: '#b5006b',
52: '#ff0000',
53: '#ff8c00',
54: '#ffff00',
55: '#b2ff00',
56: '#00ff00',
57: '#00ffa0',
58: '#00ffff',
59: '#008cff',
60: '#0000ff',
61: '#a500ff',
62: '#ff00ff',
63: '#ff0098',
64: '#ff5959',
65: '#ffb459',
66: '#ffff71',
67: '#cfff60',
68: '#6fff6f',
69: '#65ffc9',
70: '#6dffff',
71: '#59b4ff',
72: '#5959ff',
73: '#c459ff',
74: '#ff66ff',
75: '#ff59bc',
76: '#ff9c9c',
77: '#ffd39c',
78: '#ffff9c',
79: '#e2ff9c',
80: '#9cff9c',
81: '#9cffdb',
82: '#9cffff',
83: '#9cd3ff',
84: '#9c9cff',
85: '#dc9cff',
86: '#ff9cff',
87: '#ff94d3',
88: '#000000',
89: '#131313',
90: '#282828',
91: '#363636',
92: '#4d4d4d',
93: '#656565',
94: '#818181',
95: '#9f9f9f',
96: '#bcbcbc',
97: '#e2e2e2',
98: '#ffffff'
};
function tokenize(str) {
const tokens = [];
let colorBuffer = '';
let color = false;
let background = false;
let colorToken;
let start = 0;
let end = 0;
const pushText = () => {
if (end > start) {
tokens.push({
type: 'text',
content: str.slice(start, end)
});
start = end;
}
};
const pushToken = token => {
pushText();
tokens.push(token);
};
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
if (color) {
if (charCode >= 48 && charCode <= 57 && colorBuffer.length < 2) {
colorBuffer += str[i];
} else if (charCode === 44 && !background) {
colorToken.color = colors[parseInt(colorBuffer, 10)];
colorBuffer = '';
background = true;
} else {
if (background) {
if (colorBuffer.length > 0) {
colorToken.background = colors[parseInt(colorBuffer, 10)];
} else {
// Trailing comma
start--;
}
} else {
colorToken.color = colors[parseInt(colorBuffer, 10)];
}
start--;
colorBuffer = '';
color = false;
tokens.push(colorToken);
}
} else {
switch (charCode) {
case formatChars.bold:
pushToken({
type: 'bold'
});
break;
case formatChars.italic:
pushToken({
type: 'italic'
});
break;
case formatChars.underline:
pushToken({
type: 'underline'
});
break;
case formatChars.strikethrough:
pushToken({
type: 'strikethrough'
});
break;
case formatChars.color:
pushText();
colorToken = {
type: 'color'
};
color = true;
background = false;
break;
case formatChars.reverseColor:
pushToken({
type: 'reverse'
});
break;
case formatChars.reset:
pushToken({
type: 'reset'
});
break;
default:
start--;
}
}
start++;
end++;
}
if (start === 0) {
return str;
}
pushText();
return tokens;
}
function colorifyString(str, state = {}) {
const tokens = tokenize(str);
if (typeof tokens === 'string') {
return [tokens, state];
}
const result = [];
let style = state.style || {};
let reverse = state.reverse || false;
const toggle = (prop, value, multiple) => {
if (style[prop]) {
if (multiple) {
const props = style[prop].split(' ');
const i = props.indexOf(value);
if (i !== -1) {
props.splice(i, 1);
} else {
props.push(value);
}
style[prop] = props.join(' ');
} else {
delete style[prop];
}
} else {
style[prop] = value;
}
};
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
switch (token.type) {
case 'bold':
toggle('fontWeight', 700);
break;
case 'italic':
toggle('fontStyle', 'italic');
break;
case 'underline':
toggle('textDecoration', 'underline', true);
break;
case 'strikethrough':
toggle('textDecoration', 'line-through', true);
break;
case 'color':
if (!token.color) {
delete style.color;
delete style.background;
} else if (reverse) {
style.color = token.background;
style.background = token.color;
} else {
style.color = token.color;
style.background = token.background;
}
break;
case 'reverse':
reverse = !reverse;
if (style.color) {
const bg = style.background;
style.background = style.color;
style.color = bg;
}
break;
case 'reset':
style = {};
break;
case 'text':
if (Object.keys(style).length > 0) {
result.push(<span style={style}>{token.content}</span>);
style = { ...style };
} else {
result.push(token.content);
}
break;
default:
}
}
return [result, { style, reverse }];
}
export default function colorify(input) {
if (typeof input === 'string') {
const [colored] = colorifyString(input);
return colored;
}
if (Array.isArray(input)) {
const result = [];
let state;
for (let i = 0; i < input.length; i++) {
if (typeof input[i] === 'string') {
const [colored, nextState] = colorifyString(input[i], state);
if (typeof colored === 'string') {
result.push(colored);
} else {
result.push(...colored);
}
state = nextState;
} else {
result.push(input[i]);
}
}
return result;
}
return input;
}