Получить цвет ANSI для символа по индексу

Я разработал couleurs Пакет NPM, который можно установить для добавления rgb метод для String.prototype:

> console.log("Hello World!".rgb(255, 0, 0)) // "Hello World!" in red
Hello World!
undefined
> "Hello World!".rgb(255, 0, 0)
'\u001b[38;5;196mHello World!\u001b[0m'

Это отлично работает. Как правильно получить цвет / стиль символа ANSI по индексу i?

Возможно, это можно взломать с помощью некоторых регулярных выражений, но я не уверен, что это действительно хорошо (однако, если доступна правильная реализация, я не против)... Я бы предпочел собственный способ получить цвет / стиль путем доступа к символу, интерпретируемому tty.

> function getStyle (input, i) { /* get style at index `i` */ return style; }

> getStyle("Hello World!".rgb(255, 0, 0), 0); // Get style of the first char
{
   start: "\u001b[38;5;196m",
   end: "\u001b[0m",
   char: "H"
}
> getStyle("Hello " + "World!".rgb(255, 0, 0), 0); // Get style of the first char
{
   start: "",
   end: "",
   char: "H"
}

Все становится сложнее, когда у нас есть несколько комбинированных стилей:

> console.log("Green and Italic".rgb(0, 255, 0).italic())
Green and Italic
undefined
> getStyle("Green and Italic".rgb(0, 255, 0).italic(), 0);
{
   start: "\u001b[3m\u001b[38;5;46m",
   end: "\u001b[0m\u001b[23m",
   char: "G"
}
> getStyle(("Bold & Red".bold() + " but this one is only red").rgb(255, 0, 0), 0);
{
   start: "\u001b[38;5;196m\u001b[1m",
   end: "\u001b[22m\u001b[0m",
   char: "B"
}
> getStyle(("Bold & Red".bold() + " but this one is only red").rgb(255, 0, 0), 11);
{
   start: "\u001b[38;5;196m",
   end: "\u001b[0m",
   char: "u"
}
> ("Bold & Red".bold() + " but this one is only red").rgb(255, 0, 0)
'\u001b[38;5;196m\u001b[1mBold & Red\u001b[22m but this one is only red\u001b[0m'

Как я уже сказал, я ищу нативный способ (возможно, с использованием дочернего процесса).

Итак, как получить полный стиль ANSI для символа по индексу i?

2 ответа

Решение

Есть несколько способов "добавить" форматирование в текст, и это один из них. Проблема в том, что вы смешиваете текст и стиль в один и тот же объект - текстовую строку. Это похоже на RTF

Here is some \b bold\b0 and {\i italic} text\par

но отличается от, скажем, собственного формата файлов Word .DOC, который работает с текстовыми прогонами:

(text) Here is some bold and italic text\r
(chp)  13 None
       4  sprmCFBold
       5  None
       6  sprmCFItalic
       6  None

- число слева - это количество символов с определенным форматированием.

Последний формат - это то, что вы ищете, так как вы хотите индексировать символы в виде простого текста. Вычитание длин форматирования покажет, какой из них вас интересует. В зависимости от того, сколько раз вы ожидаете запросить форматирование, вы можете выполнять только однократные прогоны или кэшировать форматированный текст где-нибудь.

Однократный прогон должен проверять каждый элемент кодированной строки, увеличивать индекс "текст", если он не находится внутри строки цвета, и обновлять строку "последний раз видел", если она есть. Я добавил совместимый getCharAt функция для отладки.

var str = '\u001b[38;5;196m\u001b[1mBo\x1B[22mld & Red\u001b[22m but this one is only red\u001b[0m';

const map = {
    bold: ["\x1B[1m", "\x1B[22m" ]
  , italic: ["\x1B[3m", "\x1B[23m" ]
  , underline: ["\x1B[4m", "\x1B[24m" ]
  , inverse: ["\x1B[7m", "\x1B[27m" ]
  , strikethrough: ["\x1B[9m", "\x1B[29m" ]
};

String.prototype.getColorAt = function(index)
{
    var strindex=0, color=[], cmatch, i,j;

    while (strindex < this.length)
    {
        cmatch = this.substr(strindex).match(/^(\u001B\[[^m]*m)/);
        if (cmatch)
        {
            // Global reset?
            if (cmatch[0] == '\x1B[0m')
            {
                color = [];
            } else
            {
                // Off code?
                for (i=0; i<map.length; i++)
                {
                    if (map[i][1] == cmatch[0])
                    {
                        // Remove On code?
                        for (j=color.length-1; j>=0; j--)
                        {
                            if (color[j] == map[i][0])
                                color.splice (j,1);
                        }
                        break;
                    }
                }
                if (j==map.length)
                    color.push (cmatch[0]);
            }
            strindex += cmatch[0].length;
        } else
        {
            /* a regular character! */
            if (!index)
                break;
            strindex++;
            index--;
        }
    }
    return color.join('');
}

String.prototype.getCharAt = function(index)
{
    var strindex=0, cmatch;

    while (strindex < this.length)
    {
        cmatch = this.substr(strindex).match(/^(\u001B\[[^m]*m)/);
        if (cmatch)
        {
            strindex += cmatch[0].length;
        } else
        {
            /* a regular character! */
            if (!index)
                return this.substr(strindex,1);
            strindex++;
            index--;
        }
    }
    return '';
}

console.log (str);

color = str.getColorAt (1);
text = str.getCharAt (1);
console.log ('color is '+color+color.length+', char is '+text);

Возвращенный color все еще в исходной кодировке. Вы можете сделать так, чтобы он возвращал постоянную некоторого вида, добавив их в свой оригинал. map массив.

Я не могу предоставить вам полное решение, но вот набросок:

  • поддерживать стек, который накапливает текущий формат
  • разбить строку на куски espace sequence | just a character
  • перебрать этот список кусков
  • если это просто символ, сохраните его индекс + текущее состояние стека
  • если это escape, либо поместите соответствующий формат в стек, либо извлеките формат из него

Вы также можете использовать этот алгоритм для преобразования экранированной строки в html и использовать методы XML для обхода дерева результатов.

Кстати, последнее было бы неплохо и наоборот, как насчет этого:

console.log("<font color='red'>hi <b>there</b></font>".toANSI())
Другие вопросы по тегам